diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx index d297bf9c..42b56e44 100644 --- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx +++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { ArrowUp, @@ -282,6 +282,46 @@ function ChatInputInner({ const [codeModeFeatureEnabled, setCodeModeFeatureEnabled] = useState(false) const [permissionMode, setPermissionMode] = useState('auto') const [recentWorkDirs, setRecentWorkDirs] = useState([]) + + // Responsive toolbar: measure real overflow and collapse items to icons + // right→left (1=code, 2=perm, 3=search-label, 4=workDir) until everything fits. + // overflow-hidden on the left group is the hard guarantee that nothing can ever + // overlap; the measurement just decides how much to collapse before that clip. + const toolbarRef = useRef(null) + const leftGroupRef = useRef(null) + const lastWidthRef = useRef(0) + const [collapseLevel, setCollapseLevel] = useState(0) + + // Re-evaluate from scratch (level 0) whenever the available width changes… + useEffect(() => { + const outer = toolbarRef.current + if (!outer) return + const ro = new ResizeObserver(() => { + const w = outer.clientWidth + if (w !== lastWidthRef.current) { + lastWidthRef.current = w + setCollapseLevel(0) + } + }) + ro.observe(outer) + return () => ro.disconnect() + }, []) + + // …or when the set/labels of items changes (workdir name, search/code toggles). + useLayoutEffect(() => { + setCollapseLevel(0) + }, [workDir, searchEnabled, searchAvailable, codeModeEnabled, codeModeFeatureEnabled, lockedModel, activeModelKey]) + + // After each render, if the left group still overflows, collapse one more step. + // Runs before paint, so the intermediate (overflowing) state is never visible. + useLayoutEffect(() => { + const el = leftGroupRef.current + if (!el) return + if (el.scrollWidth > el.clientWidth + 1 && collapseLevel < 4) { + setCollapseLevel((l) => Math.min(4, l + 1)) + } + }) + // When a run exists, freeze the dropdown to the run's resolved model+provider. useEffect(() => { if (!runId) { @@ -653,7 +693,7 @@ function ChatInputInner({ const currentWorkDirPath = workDir ? compactWorkDirPath(workDir) : '' return ( -
+
{attachments.length > 0 && (
{attachments.map((attachment) => { @@ -756,8 +796,8 @@ function ChatInputInner({ className="min-h-6 rounded-none border-0 py-0 shadow-none focus-visible:ring-0" />
-
-
+
+
@@ -865,24 +905,29 @@ function ChatInputInner({ {workDir && ( - {/* Collapses to a square icon below ~370px container width */} -
+ {/* Level 4: collapse to a square icon */} +
= 4 ? "w-7 justify-center" : "max-w-[180px] pl-2.5 pr-2" + )}> - + {collapseLevel < 4 && ( + + )}
@@ -904,8 +949,8 @@ function ChatInputInner({ )} > - {searchEnabled && ( - + {searchEnabled && collapseLevel < 3 && ( + Search )} @@ -921,7 +966,8 @@ function ChatInputInner({ }} disabled={Boolean(runId)} className={cn( - "flex h-7 shrink-0 items-center gap-1.5 rounded-full px-2.5 text-xs font-medium transition-colors @max-[460px]:w-7 @max-[460px]:justify-center @max-[460px]:px-0", + "flex h-7 shrink-0 items-center gap-1.5 rounded-full text-xs font-medium transition-colors", + collapseLevel >= 2 ? "w-7 justify-center" : "px-2.5", permissionMode === 'auto' ? "bg-secondary text-foreground hover:bg-secondary/70" : "text-muted-foreground hover:bg-muted hover:text-foreground", @@ -930,7 +976,7 @@ function ChatInputInner({ aria-label="Permission mode" > - {permissionMode === 'auto' ? 'Auto' : 'Manual'} + {collapseLevel < 2 && {permissionMode === 'auto' ? 'Auto' : 'Manual'}} @@ -942,22 +988,22 @@ function ChatInputInner({ {codeModeFeatureEnabled && (codeModeEnabled ? ( - <> - {/* Compact icon — shown below ~560px when there's no room for the full pill */} + collapseLevel >= 1 ? ( + /* Level 1: collapse the pill to a single icon */ Code mode on ({codingAgent === 'claude' ? 'Claude Code' : 'Codex'}) — click to disable - {/* Full pill — hidden below ~560px */} -
+ ) : ( +
- + ) ) : (