diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml index ec60096f..787e28e6 100644 --- a/.github/workflows/electron-build.yml +++ b/.github/workflows/electron-build.yml @@ -16,9 +16,9 @@ jobs: uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: - version: 9 + version: 10 - name: Setup Node.js uses: actions/setup-node@v6 @@ -39,17 +39,17 @@ jobs: node -e " const fs = require('fs'); const version = '${{ steps.version.outputs.version }}'; - + // Update apps/x/package.json const rootPackage = JSON.parse(fs.readFileSync('apps/x/package.json', 'utf8')); rootPackage.version = version; fs.writeFileSync('apps/x/package.json', JSON.stringify(rootPackage, null, 2) + '\n'); - + // Update apps/x/apps/main/package.json const mainPackage = JSON.parse(fs.readFileSync('apps/x/apps/main/package.json', 'utf8')); mainPackage.version = version; fs.writeFileSync('apps/x/apps/main/package.json', JSON.stringify(mainPackage, null, 2) + '\n'); - + console.log('Updated version to:', version); " @@ -61,25 +61,25 @@ jobs: # Create a temporary keychain KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db KEYCHAIN_PASSWORD=$(openssl rand -base64 32) - + # Create keychain security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - + # Decode and import certificate echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 security import $RUNNER_TEMP/certificate.p12 -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" - + # Allow codesign to access the keychain security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" - + # Add keychain to search list security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain - + # Verify certificate was imported security find-identity -v "$KEYCHAIN_PATH" - + # Clean up certificate file rm -f $RUNNER_TEMP/certificate.p12 @@ -122,9 +122,9 @@ jobs: uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: - version: 9 + version: 10 - name: Setup Node.js uses: actions/setup-node@v6 @@ -145,17 +145,17 @@ jobs: node -e " const fs = require('fs'); const version = '${{ steps.version.outputs.version }}'; - + // Update apps/x/package.json const rootPackage = JSON.parse(fs.readFileSync('apps/x/package.json', 'utf8')); rootPackage.version = version; fs.writeFileSync('apps/x/package.json', JSON.stringify(rootPackage, null, 2) + '\n'); - + // Update apps/x/apps/main/package.json const mainPackage = JSON.parse(fs.readFileSync('apps/x/apps/main/package.json', 'utf8')); mainPackage.version = version; fs.writeFileSync('apps/x/apps/main/package.json', JSON.stringify(mainPackage, null, 2) + '\n'); - + console.log('Updated version to:', version); " @@ -187,9 +187,9 @@ jobs: uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: - version: 9 + version: 10 - name: Setup Node.js uses: actions/setup-node@v6 @@ -212,17 +212,17 @@ jobs: node -e " const fs = require('fs'); const version = '${{ steps.version.outputs.version }}'; - + // Update apps/x/package.json const rootPackage = JSON.parse(fs.readFileSync('apps/x/package.json', 'utf8')); rootPackage.version = version; fs.writeFileSync('apps/x/package.json', JSON.stringify(rootPackage, null, 2) + '\n'); - + // Update apps/x/apps/main/package.json const mainPackage = JSON.parse(fs.readFileSync('apps/x/apps/main/package.json', 'utf8')); mainPackage.version = version; fs.writeFileSync('apps/x/apps/main/package.json', JSON.stringify(mainPackage, null, 2) + '\n'); - + console.log('Updated version to:', version); " 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 a7d548ea..0254cdfd 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, @@ -19,6 +19,7 @@ import { ImagePlus, LoaderIcon, Mic, + MoreHorizontal, Plus, ShieldCheck, Square, @@ -29,6 +30,7 @@ import { import { Button } from '@/components/ui/button' import { DropdownMenu, + DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, @@ -283,6 +285,51 @@ function ChatInputInner({ const [permissionMode, setPermissionMode] = useState('auto') const [recentWorkDirs, setRecentWorkDirs] = useState([]) + // Responsive toolbar: measure real overflow and progressively collapse items + // right→left until everything fits. Stages: + // 1 code→icon · 2 perm→icon · 3 search label hidden · 4 workDir→icon + // 5 code→menu · 6 perm→menu · 7 search→menu · 8 workDir→menu + // Once items move into the "⋯" overflow menu (≥5) no icon is ever hidden. + // overflow-hidden on the left group is the hard guarantee against any overlap. + 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* of items changes (an item appears/disappears, or the model + // name width changes). Deliberately excludes the in-place toggles (searchEnabled, + // permissionMode, codeModeEnabled, codingAgent): those fire from the overflow menu + // for items already inside it, so resetting here would unmount the open menu. The + // no-dep effect below still re-collapses if any toggle happens to widen the row. + useLayoutEffect(() => { + setCollapseLevel(0) + }, [workDir, searchAvailable, 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 < 8) { + setCollapseLevel((l) => Math.min(8, l + 1)) + } + }) + // When a run exists, freeze the dropdown to the run's resolved model+provider. useEffect(() => { if (!runId) { @@ -757,7 +804,8 @@ function ChatInputInner({ className="min-h-6 rounded-none border-0 py-0 shadow-none focus-visible:ring-0" /> -
+
+
@@ -862,26 +910,32 @@ function ChatInputInner({
- {workDir && ( + {workDir && collapseLevel < 8 && ( -
+ {/* Level 4: collapse to a square icon */} +
= 4 ? "w-7 justify-center" : "max-w-[180px] pl-2.5 pr-2" + )}> - + {collapseLevel < 4 && ( + + )}
@@ -889,7 +943,7 @@ function ChatInputInner({ )} - {searchAvailable && ( + {searchAvailable && collapseLevel < 7 && ( )} + {collapseLevel < 6 && ( @@ -943,37 +996,54 @@ function ChatInputInner({ : 'Manual approval prompts — click for auto-permission'} - {codeModeFeatureEnabled && (codeModeEnabled ? ( -
+ )} + {codeModeFeatureEnabled && collapseLevel < 5 && (codeModeEnabled ? ( + collapseLevel >= 1 ? ( + /* Level 1: collapse the pill to a single icon */ - Code mode on — click to disable + Code mode on ({codingAgent === 'claude' ? 'Claude Code' : 'Codex'}) — click to disable - · - - - - - - Coding agent: {codingAgent === 'claude' ? 'Claude Code' : 'Codex'} — click to swap - - -
+ ) : ( +
+ + + + + Code mode on — click to disable + + · + + + + + + Coding agent: {codingAgent === 'claude' ? 'Claude Code' : 'Codex'} — click to swap + + +
+ ) ) : ( @@ -989,25 +1059,89 @@ function ChatInputInner({ Use a coding agent (Claude Code or Codex) ))} +
+ {collapseLevel >= 5 && ( + + + + + + + + More options + + + {workDir && collapseLevel >= 8 && ( + { void handleSetWorkDir() }}> + + {basename(workDir) || workDir} + + )} + {searchAvailable && collapseLevel >= 7 && ( + e.preventDefault()} + onCheckedChange={(c) => setSearchEnabled(Boolean(c))} + > + Web search + + )} + {collapseLevel >= 6 && ( + e.preventDefault()} + onCheckedChange={(c) => setPermissionMode(c ? 'auto' : 'manual')} + > + Auto-approve actions + + )} + {codeModeFeatureEnabled && collapseLevel >= 5 && ( + <> + e.preventDefault()} + onCheckedChange={(c) => setCodeModeEnabled(Boolean(c))} + > + Code mode + + {codeModeEnabled && ( + { e.preventDefault(); handleToggleCodingAgent() }}> + + Coding agent + {codingAgent === 'claude' ? 'Claude' : 'Codex'} + + )} + + )} + + + )}
{lockedModel ? ( - {getSelectedModelDisplayName(lockedModel.model)} + {getSelectedModelDisplayName(lockedModel.model)} ) : configuredModels.length > 0 ? (