Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
loadSession,
loadWorkspaceState,
replaySession,
startNewChat,
} from './app/store'
import { bridgeWS } from './app/wsBridge'
import { useChatRuntime } from './app/runtime'
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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')
}
}
Expand Down
18 changes: 18 additions & 0 deletions web/src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {
HandRaisedIcon,
ShieldExclamationIcon,
ClipboardDocumentListIcon,
BoltIcon,
ViewfinderCircleIcon,
PlusIcon,
PaperClipIcon,
XMarkIcon,
Expand Down Expand Up @@ -801,7 +801,7 @@ export function ChatInput({ onSent, pickerPlacement = 'top', elevated = false }:
goalArmed ? 'bg-[var(--neutral-wash)] text-[var(--color-foreground)]' : ''
}`}
>
<BoltIcon className={`h-3.5 w-3.5 shrink-0 ${goalArmed ? 'text-[var(--color-primary)]' : 'text-[var(--color-muted-foreground)]'}`} />
<ViewfinderCircleIcon className={`h-3.5 w-3.5 shrink-0 ${goalArmed ? 'text-[var(--color-primary)]' : 'text-[var(--color-muted-foreground)]'}`} />
<span>Goal</span>
</button>
</div>
Expand Down Expand Up @@ -906,7 +906,7 @@ export function ChatInput({ onSent, pickerPlacement = 'top', elevated = false }:
>
<XMarkIcon className="h-2.5 w-2.5" />
</button>
<BoltIcon className="h-3 w-3" />
<ViewfinderCircleIcon className="h-3 w-3" />
<span>Goal</span>
</div>
</>
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/GoalBanner.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)' }}
>
<BoltIcon className="mt-0.5 h-3.5 w-3.5 shrink-0" style={{ color: statusColor }} />
<ViewfinderCircleIcon className="mt-0.5 h-3.5 w-3.5 shrink-0" style={{ color: statusColor }} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1736,12 +1736,12 @@ function ApprovalReviewSection() {

const SHORTCUTS: { keys: string; labelKey: string }[] = [
{ keys: '⌘K', labelKey: 'commandPalette' },
{ keys: '⌘N', labelKey: 'newChat' },
{ keys: '⇧⌘O', labelKey: 'newChat' },
{ keys: '⌘,', labelKey: 'openSettings' },
{ keys: '⇧⌘P', labelKey: 'planMode' },
{ keys: '⇧⌘E', labelKey: 'filesPanel' },
{ keys: '⇧⌘G', labelKey: 'changesPanel' },
{ keys: '⌘`', labelKey: 'toggleTerminal' },
{ keys: '⌘J', labelKey: 'toggleTerminal' },
{ keys: '⌘L', labelKey: 'focusInput' },
]

Expand Down
17 changes: 4 additions & 13 deletions web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
} from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
import { useAppDispatch, useAppSelector } from '../app/hooks'
import { uiActions, sessionActions, chatActions, loadSession, loadWorkspaceState } from '../app/store'
import { uiActions, sessionActions, chatActions, loadSession, loadWorkspaceState, startNewChat } from '../app/store'
import { api } from '../lib/api'
import type { TaskItem } from '../lib/types'
import { ThemeToggle } from './ThemeToggle'
Expand Down Expand Up @@ -325,18 +325,9 @@ export function Sidebar() {

// ── Actions ──

// Shared with the ⌘N / ⇧⌘O shortcuts in App.tsx (see startNewChat in the store).
async function newChat() {
dispatch(chatActions.clearChat())
dispatch(sessionActions.setCurrentSession(''))
dispatch(uiActions.setView('chat'))
try {
const resp = await api.newSession()
// Keep the welcome screen empty-session out of the sidebar until the
// first user message (backend only indexes then; a UUID-only row looks broken).
dispatch(sessionActions.setCurrentSession(resp.session_id))
} catch {
// surfaced via health/gate
}
await dispatch(startNewChat())
}

async function openItem(row: SessionRow) {
Expand Down Expand Up @@ -517,7 +508,7 @@ export function Sidebar() {
>
<PlusIcon className="sb-nav-ic" />
<span className="sb-nav-name">{t('nav.newTask')}</span>
<span className="sb-nav-kbd">⌘ N</span>
<span className="sb-nav-kbd">⇧⌘ O</span>
</button>
<button
type="button"
Expand Down
Loading