diff --git a/packages/app/pr/cmdk-session-model-thinking.md b/packages/app/pr/cmdk-session-model-thinking.md new file mode 100644 index 00000000..d0afd21f --- /dev/null +++ b/packages/app/pr/cmdk-session-model-thinking.md @@ -0,0 +1,65 @@ +# Cmd+K Proof: Session Search, Model, Thinking + +## What was implemented + +- Added a `Cmd+K` quick actions entry point in session view. +- Added root quick actions: + - `Search sessions` + - `Change model` + - `Change thinking` +- Added session search mode with filtering and cross-session jump. +- Added thinking mode with options (`None`, `Low`, `Medium`, `High`, `X-High`) that apply directly. + +## End-to-end proof (screenshots reviewed) + +I captured and reviewed each screenshot below to verify the flow works. + +### 1) Root quick actions are available via Cmd+K + +![Cmd+K root actions](./screenshots/cmdk-root-actions.png) + +Verified: +- Command palette opens over session view. +- Root actions show `Search sessions`, `Change model`, `Change thinking`. + +### 2) Search sessions flow works + +Session search mode: + +![Search sessions mode](./screenshots/cmdk-search-sessions.png) + +Filtered session search: + +![Filtered session search](./screenshots/cmdk-search-sessions-filtered.png) + +Verified: +- `Search sessions` opens a dedicated session search mode. +- Typing filters sessions by title. +- Selecting a result closes the palette and switches to the chosen session. + +### 3) Change model flow works + +![Change model modal](./screenshots/cmdk-change-model.png) + +Verified: +- `Change model` opens the model picker modal from Cmd+K. + +### 4) Change thinking flow works + +Thinking options mode: + +![Change thinking options](./screenshots/cmdk-change-thinking-options.png) + +Applied state (`Thinking High`): + +![Thinking High applied](./screenshots/cmdk-thinking-high-applied.png) + +Verified: +- `Change thinking` opens a dedicated thinking mode in Cmd+K. +- Selecting `High` updates composer state to `Thinking High`. + +## Notes on environment + +- Attempted `packaging/docker/dev-up.sh` in this worktree; orchestrator container was repeatedly killed (`exit 137`) during dependency install. +- For UI validation, used this worktree's Vite app and connected it to an already-running healthy OpenWork Docker server. +- Chrome MCP was used for the interactive verification and screenshot capture above. diff --git a/packages/app/pr/screenshots/cmdk-change-model.png b/packages/app/pr/screenshots/cmdk-change-model.png new file mode 100644 index 00000000..ac5157fa Binary files /dev/null and b/packages/app/pr/screenshots/cmdk-change-model.png differ diff --git a/packages/app/pr/screenshots/cmdk-change-thinking-options.png b/packages/app/pr/screenshots/cmdk-change-thinking-options.png new file mode 100644 index 00000000..049dfe5a Binary files /dev/null and b/packages/app/pr/screenshots/cmdk-change-thinking-options.png differ diff --git a/packages/app/pr/screenshots/cmdk-root-actions.png b/packages/app/pr/screenshots/cmdk-root-actions.png new file mode 100644 index 00000000..ab9bba19 Binary files /dev/null and b/packages/app/pr/screenshots/cmdk-root-actions.png differ diff --git a/packages/app/pr/screenshots/cmdk-search-sessions-filtered.png b/packages/app/pr/screenshots/cmdk-search-sessions-filtered.png new file mode 100644 index 00000000..a19f2871 Binary files /dev/null and b/packages/app/pr/screenshots/cmdk-search-sessions-filtered.png differ diff --git a/packages/app/pr/screenshots/cmdk-search-sessions.png b/packages/app/pr/screenshots/cmdk-search-sessions.png new file mode 100644 index 00000000..6dcbd9f8 Binary files /dev/null and b/packages/app/pr/screenshots/cmdk-search-sessions.png differ diff --git a/packages/app/pr/screenshots/cmdk-thinking-high-applied.png b/packages/app/pr/screenshots/cmdk-thinking-high-applied.png new file mode 100644 index 00000000..a0a3569d Binary files /dev/null and b/packages/app/pr/screenshots/cmdk-thinking-high-applied.png differ diff --git a/packages/app/src/app/pages/session.tsx b/packages/app/src/app/pages/session.tsx index ee819919..e9c74304 100644 --- a/packages/app/src/app/pages/session.tsx +++ b/packages/app/src/app/pages/session.tsx @@ -233,6 +233,16 @@ const STREAM_RENDER_BATCH_MS = 220; const MAIN_THREAD_LAG_INTERVAL_MS = 200; const MAIN_THREAD_LAG_WARN_MS = 180; +type CommandPaletteMode = "root" | "sessions" | "thinking"; + +const COMMAND_PALETTE_THINKING_OPTIONS = [ + { value: "none", label: "None", detail: "Fastest responses" }, + { value: "low", label: "Low", detail: "Light reasoning" }, + { value: "medium", label: "Medium", detail: "Balanced depth" }, + { value: "high", label: "High", detail: "Deeper reasoning" }, + { value: "xhigh", label: "X-High", detail: "Maximum effort" }, +] as const; + export default function SessionView(props: SessionViewProps) { let messagesEndEl: HTMLDivElement | undefined; let chatContainerEl: HTMLDivElement | undefined; @@ -266,6 +276,10 @@ export default function SessionView(props: SessionViewProps) { const [searchQuery, setSearchQuery] = createSignal(""); const [searchQueryDebounced, setSearchQueryDebounced] = createSignal(""); const [activeSearchHitIndex, setActiveSearchHitIndex] = createSignal(0); + const [commandPaletteOpen, setCommandPaletteOpen] = createSignal(false); + const [commandPaletteMode, setCommandPaletteMode] = createSignal("root"); + const [commandPaletteQuery, setCommandPaletteQuery] = createSignal(""); + const [commandPaletteActiveIndex, setCommandPaletteActiveIndex] = createSignal(0); const [historyActionBusy, setHistoryActionBusy] = createSignal<"undo" | "redo" | "compact" | null>(null); const [messageWindowStart, setMessageWindowStart] = createSignal(0); const [messageWindowSessionId, setMessageWindowSessionId] = createSignal(null); @@ -277,6 +291,8 @@ export default function SessionView(props: SessionViewProps) { // When a session is selected (i.e. we are in SessionView), the right sidebar is // navigation-only. Avoid showing any tab as "selected" to reduce confusion. const showRightSidebarSelection = createMemo(() => !props.selectedSessionId); + let commandPaletteInputEl: HTMLInputElement | undefined; + const commandPaletteOptionRefs: HTMLButtonElement[] = []; const agentLabel = createMemo(() => props.selectedSessionAgent ?? "Default agent"); const workspaceLabel = (workspace: WorkspaceInfo) => @@ -299,10 +315,61 @@ export default function SessionView(props: SessionViewProps) { todoList().filter((todo) => todo.status === "completed").length ); + const commandPaletteSessionOptions = createMemo(() => { + const out: Array<{ + workspaceId: string; + sessionId: string; + title: string; + workspaceTitle: string; + updatedAt: number; + searchText: string; + }> = []; + + for (const group of props.workspaceSessionGroups) { + const workspaceId = group.workspace.id?.trim() ?? ""; + if (!workspaceId) continue; + const workspaceTitle = workspaceLabel(group.workspace); + for (const session of group.sessions) { + const sessionId = session.id?.trim() ?? ""; + if (!sessionId) continue; + const title = session.title?.trim() || "Untitled session"; + const slug = session.slug?.trim() ?? ""; + const updatedAt = session.time?.updated ?? session.time?.created ?? 0; + out.push({ + workspaceId, + sessionId, + title, + workspaceTitle, + updatedAt, + searchText: [title, workspaceTitle, slug].join(" ").toLowerCase(), + }); + } + } + + out.sort((a, b) => { + const aActive = a.workspaceId === props.activeWorkspaceId; + const bActive = b.workspaceId === props.activeWorkspaceId; + if (aActive !== bActive) return aActive ? -1 : 1; + return b.updatedAt - a.updatedAt; + }); + + return out; + }); + + const totalSessionCount = createMemo(() => commandPaletteSessionOptions().length); + type SearchHit = { messageId: string; }; + type CommandPaletteItem = { + id: string; + title: string; + detail?: string; + meta?: string; + action: () => void; + }; + const messageIdFromInfo = (message: MessageWithParts) => { const id = (message.info as { id?: string | number }).id; if (typeof id === "string") return id; @@ -1250,9 +1317,73 @@ export default function SessionView(props: SessionViewProps) { target.scrollIntoView({ behavior: "smooth", block: "center" }); }); + createEffect(() => { + if (!commandPaletteOpen()) return; + focusCommandPaletteInput(); + }); + + createEffect(() => { + if (!commandPaletteOpen()) return; + const total = commandPaletteItems().length; + if (total === 0) { + setCommandPaletteActiveIndex(0); + return; + } + setCommandPaletteActiveIndex((current) => Math.max(0, Math.min(current, total - 1))); + }); + + createEffect(() => { + if (!commandPaletteOpen()) return; + const idx = commandPaletteActiveIndex(); + requestAnimationFrame(() => { + commandPaletteOptionRefs[idx]?.scrollIntoView({ block: "nearest" }); + }); + }); + createEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { const mod = event.metaKey || event.ctrlKey; + if (mod && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "k") { + event.preventDefault(); + if (commandPaletteOpen()) { + closeCommandPalette(); + } else { + openCommandPalette(); + } + return; + } + + if (commandPaletteOpen()) { + if (event.key === "Escape") { + event.preventDefault(); + closeCommandPalette(); + return; + } + if (event.key === "ArrowDown") { + event.preventDefault(); + stepCommandPaletteIndex(1, commandPaletteItems().length); + return; + } + if (event.key === "ArrowUp") { + event.preventDefault(); + stepCommandPaletteIndex(-1, commandPaletteItems().length); + return; + } + if (event.key === "Enter") { + if (event.isComposing || event.keyCode === 229) return; + const item = commandPaletteItems()[commandPaletteActiveIndex()]; + if (!item) return; + event.preventDefault(); + item.action(); + return; + } + if (event.key === "Backspace" && !commandPaletteQuery().trim() && commandPaletteMode() !== "root") { + event.preventDefault(); + returnToCommandRoot(); + } + return; + } + if (mod && !event.altKey && event.key.toLowerCase() === "f") { event.preventDefault(); openSearch(); @@ -1448,6 +1579,47 @@ export default function SessionView(props: SessionViewProps) { }); }; + const focusCommandPaletteInput = () => { + queueMicrotask(() => { + commandPaletteInputEl?.focus(); + commandPaletteInputEl?.select(); + }); + }; + + const openCommandPalette = (mode: CommandPaletteMode = "root") => { + setCommandPaletteMode(mode); + setCommandPaletteQuery(""); + setCommandPaletteActiveIndex(0); + setCommandPaletteOpen(true); + focusCommandPaletteInput(); + }; + + const closeCommandPalette = () => { + setCommandPaletteOpen(false); + setCommandPaletteMode("root"); + setCommandPaletteQuery(""); + setCommandPaletteActiveIndex(0); + }; + + const stepCommandPaletteIndex = (delta: number, total: number) => { + if (total <= 0) { + setCommandPaletteActiveIndex(0); + return; + } + setCommandPaletteActiveIndex((current) => { + const normalized = ((current % total) + total) % total; + return (normalized + delta + total) % total; + }); + }; + + const returnToCommandRoot = () => { + if (commandPaletteMode() === "root") return; + setCommandPaletteMode("root"); + setCommandPaletteQuery(""); + setCommandPaletteActiveIndex(0); + focusCommandPaletteInput(); + }; + const openSearch = () => { setSearchOpen(true); focusSearchInput(); @@ -2055,6 +2227,123 @@ export default function SessionView(props: SessionViewProps) { })(); }; + const commandPaletteRootItems = createMemo(() => { + const items: CommandPaletteItem[] = [ + { + id: "sessions", + title: "Search sessions", + detail: `${totalSessionCount().toLocaleString()} available across workers`, + meta: "Jump", + action: () => { + setCommandPaletteMode("sessions"); + setCommandPaletteQuery(""); + setCommandPaletteActiveIndex(0); + focusCommandPaletteInput(); + }, + }, + { + id: "model", + title: "Change model", + detail: `Current: ${props.selectedSessionModelLabel || "Model"}`, + meta: "Open", + action: () => { + closeCommandPalette(); + props.openSessionModelPicker(); + }, + }, + { + id: "thinking", + title: "Change thinking", + detail: `Current: ${props.modelVariantLabel}`, + meta: "Adjust", + action: () => { + setCommandPaletteMode("thinking"); + setCommandPaletteQuery(""); + setCommandPaletteActiveIndex(0); + focusCommandPaletteInput(); + }, + }, + ]; + + const query = commandPaletteQuery().trim().toLowerCase(); + if (!query) return items; + return items.filter((item) => `${item.title} ${item.detail ?? ""}`.toLowerCase().includes(query)); + }); + + const commandPaletteSessionItems = createMemo(() => { + const query = commandPaletteQuery().trim().toLowerCase(); + const candidates = query + ? commandPaletteSessionOptions().filter((item) => item.searchText.includes(query)) + : commandPaletteSessionOptions(); + + return candidates.slice(0, 80).map((item) => ({ + id: `session:${item.workspaceId}:${item.sessionId}`, + title: item.title, + detail: item.workspaceTitle, + meta: item.workspaceId === props.activeWorkspaceId ? "Current worker" : "Switch", + action: () => { + closeCommandPalette(); + openSessionFromList(item.workspaceId, item.sessionId); + }, + })); + }); + + const commandPaletteThinkingItems = createMemo(() => { + const normalizedRaw = (props.modelVariant ?? "none").trim().toLowerCase(); + const activeVariant = + normalizedRaw === "balanced" || normalizedRaw === "balance" ? "none" : normalizedRaw; + const query = commandPaletteQuery().trim().toLowerCase(); + + return COMMAND_PALETTE_THINKING_OPTIONS + .filter((option) => { + if (!query) return true; + return `${option.label} ${option.detail}`.toLowerCase().includes(query); + }) + .map((option) => ({ + id: `thinking:${option.value}`, + title: option.label, + detail: option.detail, + meta: activeVariant === option.value ? "Current" : undefined, + action: () => { + props.setModelVariant(option.value); + closeCommandPalette(); + setToastMessage(`Thinking set to ${option.label}.`); + }, + })); + }); + + const commandPaletteItems = createMemo(() => { + const mode = commandPaletteMode(); + if (mode === "sessions") return commandPaletteSessionItems(); + if (mode === "thinking") return commandPaletteThinkingItems(); + return commandPaletteRootItems(); + }); + + const commandPaletteTitle = createMemo(() => { + const mode = commandPaletteMode(); + if (mode === "sessions") return "Search sessions"; + if (mode === "thinking") return "Change thinking"; + return "Quick actions"; + }); + + const commandPalettePlaceholder = createMemo(() => { + const mode = commandPaletteMode(); + if (mode === "sessions") return "Find by session title or worker"; + if (mode === "thinking") return "Filter thinking options"; + return "Search actions"; + }); + + createEffect( + on( + () => [commandPaletteMode(), commandPaletteQuery()], + () => { + if (!commandPaletteOpen()) return; + commandPaletteOptionRefs.length = 0; + setCommandPaletteActiveIndex(0); + }, + ), + ); + const openSettings = (tab: SettingsTab = "general") => { props.setSettingsTab(tab); props.setTab("settings"); @@ -2619,6 +2908,27 @@ export default function SessionView(props: SessionViewProps) {
+ + + + (commandPaletteInputEl = el)} + type="text" + value={commandPaletteQuery()} + onInput={(event) => setCommandPaletteQuery(event.currentTarget.value)} + placeholder={commandPalettePlaceholder()} + class="min-w-0 flex-1 bg-transparent text-sm text-dls-text placeholder:text-dls-secondary focus:outline-none" + aria-label={commandPaletteTitle()} + /> + +
+
{commandPaletteTitle()}
+ + +
+ 0} + fallback={ +
+ No matches. +
+ } + > + + {(item, index) => { + const idx = () => index(); + return ( + + ); + }} + +
+
+ +
+ Arrow keys to navigate + Enter to run ยท Esc to close +
+ + + +