From 812b0e6726087e1993a2b21a307f5caea323f7a0 Mon Sep 17 00:00:00 2001 From: jack Date: Fri, 17 Jul 2026 08:54:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20target=20icon=20for=20Goal,=20worki?= =?UTF-8?q?ng=20new-task=20shortcut,=20=E2=8C=98J=20terminal=20alias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Goal icon - Replace BoltIcon with ViewfinderCircleIcon (crosshair / target) in the GoalBanner and the ChatInput Goal menu item + armed chip. The bolt icon is kept for non-Goal settings rows (small model, BLE, MCP). New-task shortcut was broken - ⌘N dispatched loadSession(''), whose thunk short-circuited on the empty uuid (the "handled in Sidebar" comment was aspirational — nothing there handled it), so the shortcut did nothing. Extract a shared startNewChat thunk (clear timeline → switch to chat view → allocate a fresh session id) and wire both ⌘N (desktop, where Tauri has no native menu to eat it) and ⇧⌘O (browser, where ⌘N is reserved by the OS) to it. Sidebar's "new task" button calls the same thunk; the displayed hint becomes ⇧⌘O. Terminal shortcut was unreachable on macOS - ⌘` is eaten by macOS window cycling and never reaches the page. Add ⌘J as an alias (with !shiftKey so ⇧⌘J DevTools stays intact) and surface it in the settings shortcuts list. ⌘` stays as an additional trigger where it does fire. --- web/src/App.tsx | 23 +++++++++++++++-------- web/src/app/store.ts | 18 ++++++++++++++++++ web/src/components/ChatInput.tsx | 6 +++--- web/src/components/GoalBanner.tsx | 6 +++--- web/src/components/SettingsDialog.tsx | 4 ++-- web/src/components/Sidebar.tsx | 17 ++++------------- 6 files changed, 45 insertions(+), 29 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index a06a64e8..7909c8ad 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -37,6 +37,7 @@ import { loadSession, loadWorkspaceState, replaySession, + startNewChat, } from './app/store' import { bridgeWS } from './app/wsBridge' import { useChatRuntime } from './app/runtime' @@ -116,18 +117,22 @@ export default function App() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch]) - // Global keyboard shortcuts: ⌘K (command palette), ⌘N (new chat), Esc (close - // overlays). Mirrors the Vue App.vue shortcut wiring. + // Global keyboard shortcuts: ⌘K (command palette), ⌘N / ⇧⌘O (new chat), Esc + // (close overlays). ⌘N is reserved by browsers (new window) so it only fires + // in the desktop app; ⇧⌘O is interceptable everywhere and is the shortcut + // shown in the UI. useEffect(() => { function onKey(e: KeyboardEvent) { const meta = e.metaKey || e.ctrlKey - if (meta && e.key === 'k') { + if (meta && e.key === 'k' && !e.shiftKey) { e.preventDefault() dispatch(uiActions.setPaletteOpen(true)) - } else if (meta && e.key === 'n') { + } else if (meta && !e.shiftKey && e.key === 'n') { e.preventDefault() - // New chat: clear + reset session + switch to chat view. - dispatch(loadSession('')) // empty → new session flow handled in Sidebar + void dispatch(startNewChat()) + } else if (meta && e.shiftKey && e.key.toLowerCase() === 'o') { + e.preventDefault() + void dispatch(startNewChat()) } else if (meta && e.key === ',') { e.preventDefault() dispatch(uiActions.setSettingsOpen(true)) @@ -207,7 +212,7 @@ function Shell({ activeView }: { activeView: 'chat' | 'automations' | 'channels' }) }, [rightPanelOpen]) - // Panel keyboard shortcuts: ⇧⌘P (plan), ⇧⌘E (files), ⇧⌘G (changes), ⌘` (terminal). + // Panel keyboard shortcuts: ⇧⌘P (plan), ⇧⌘E (files), ⇧⌘G (changes), ⌘` / ⌘J (terminal). useEffect(() => { function onKey(e: KeyboardEvent) { const meta = e.metaKey || e.ctrlKey @@ -217,7 +222,9 @@ function Shell({ activeView }: { activeView: 'chat' | 'automations' | 'channels' e.preventDefault(); togglePanel('files') } else if (meta && e.shiftKey && e.key.toLowerCase() === 'g') { e.preventDefault(); togglePanel('changes') - } else if (meta && e.key === '`') { + } else if (meta && !e.shiftKey && (e.key === '`' || e.key.toLowerCase() === 'j')) { + // ⌘` never reaches the page on macOS (OS window cycling), so ⌘J is the + // alias shown in the UI. `!e.shiftKey` keeps ⇧⌘J (DevTools) intact. e.preventDefault(); togglePanel('terminal') } } diff --git a/web/src/app/store.ts b/web/src/app/store.ts index f465f260..951648ef 100644 --- a/web/src/app/store.ts +++ b/web/src/app/store.ts @@ -1059,6 +1059,24 @@ export const loadSession = createAsyncThunk( }, ) +/** + * Start a fresh chat: clear the timeline, switch to the chat view, and ask the + * backend for a new session id. Shared by the Sidebar "new task" button and the + * ⌘N / ⇧⌘O keyboard shortcuts. The empty session stays out of the sidebar until + * the first user message (backend only indexes then). + */ +export const startNewChat = createAsyncThunk('session/startNew', async (_, { dispatch }) => { + dispatch(chatActions.clearChat()) + dispatch(sessionActions.setCurrentSession('')) + dispatch(uiActions.setView('chat')) + try { + const resp = await api.newSession() + dispatch(sessionActions.setCurrentSession(resp.session_id)) + } catch { + // surfaced via health/gate + } +}) + export const replaySession = createAsyncThunk( 'session/replay', async (uuid: string, { dispatch }) => { diff --git a/web/src/components/ChatInput.tsx b/web/src/components/ChatInput.tsx index 88d0390a..a92809e6 100644 --- a/web/src/components/ChatInput.tsx +++ b/web/src/components/ChatInput.tsx @@ -34,7 +34,7 @@ import { HandRaisedIcon, ShieldExclamationIcon, ClipboardDocumentListIcon, - BoltIcon, + ViewfinderCircleIcon, PlusIcon, PaperClipIcon, XMarkIcon, @@ -801,7 +801,7 @@ export function ChatInput({ onSent, pickerPlacement = 'top', elevated = false }: goalArmed ? 'bg-[var(--neutral-wash)] text-[var(--color-foreground)]' : '' }`} > - + Goal @@ -906,7 +906,7 @@ export function ChatInput({ onSent, pickerPlacement = 'top', elevated = false }: > - + Goal diff --git a/web/src/components/GoalBanner.tsx b/web/src/components/GoalBanner.tsx index 6caf0c73..3b0b953e 100644 --- a/web/src/components/GoalBanner.tsx +++ b/web/src/components/GoalBanner.tsx @@ -1,11 +1,11 @@ /** * GoalBanner — active goal display (set via /goal or the Goal toggle). * Ported from web/src/components/GoalBanner.vue: a rounded inset card with - * Bolt/status tint, status label, objective, and clear button — not a full-width + * target/status tint, status label, objective, and clear button — not a full-width * border-b strip. */ -import { BoltIcon } from '@heroicons/react/24/outline' +import { ViewfinderCircleIcon } from '@heroicons/react/24/outline' import { useTranslation } from 'react-i18next' import { useAppDispatch, useAppSelector } from '../app/hooks' import { chatActions } from '../app/store' @@ -55,7 +55,7 @@ export function GoalBanner() { className="mt-2 flex items-start gap-2 rounded-md border px-3 py-2" style={{ borderColor: 'var(--color-border)', backgroundColor: 'var(--color-secondary)' }} > - +
{t('nav.newTask')} - ⌘ N + ⇧⌘ O