From 66dd81976f7639a8c1513ce95832009ab4fb2990 Mon Sep 17 00:00:00 2001
From: simosmik
Date: Thu, 30 Apr 2026 07:16:58 +0000
Subject: [PATCH] =?UTF-8?q?feat(command-palette):=20add=20settings,=20navi?=
=?UTF-8?q?gation,=20message=20search,=20and=20=E2=8C=98K=20hints?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ProviderSelectionEmptyState.tsx | 9 ++
.../command-palette/CommandPalette.tsx | 109 ++++++++++++++++--
.../sources/useSessionMessageSearch.ts | 98 ++++++++++++++++
.../view/subcomponents/SidebarHeader.tsx | 13 ++-
4 files changed, 219 insertions(+), 10 deletions(-)
create mode 100644 src/components/command-palette/sources/useSessionMessageSearch.ts
diff --git a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
index 5a5feeb7..62a578e8 100644
--- a/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
+++ b/src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx
@@ -288,6 +288,15 @@ export default function ProviderSelectionEmptyState({
}
+
+ Press
+
+ {typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘" : "Ctrl"}
+ K
+
+ to search sessions, files, and commits
+
+
{provider && tasksEnabled && isTaskMasterInstalled && (
void;
- onOpenSettings: () => void;
+ onOpenSettings: (tab?: string) => void;
onShowTab?: (tab: AppTab) => void;
};
@@ -36,6 +52,25 @@ function parseMode(input: string): { mode: Mode; query: string } {
return { mode: 'mixed', query: input };
}
+const SETTINGS_TABS: Array<{ id: string; label: string; keywords: string; icon: React.ComponentType<{ className?: string }> }> = [
+ { id: 'agents', label: 'Agents', keywords: 'agents subagents claude code', icon: Bot },
+ { id: 'appearance', label: 'Appearance', keywords: 'appearance theme dark light language', icon: Palette },
+ { id: 'git', label: 'Git', keywords: 'git github commits', icon: GitBranch },
+ { id: 'api', label: 'API Tokens', keywords: 'api tokens auth keys', icon: KeyRound },
+ { id: 'tasks', label: 'Tasks', keywords: 'tasks taskmaster', icon: ListChecks },
+ { id: 'notifications', label: 'Notifications', keywords: 'notifications alerts push', icon: Bell },
+ { id: 'plugins', label: 'Plugins', keywords: 'plugins extensions integrations', icon: Plug },
+ { id: 'about', label: 'About', keywords: 'about version info', icon: Info },
+];
+
+const NAV_TABS: Array<{ id: AppTab; label: string; keywords: string }> = [
+ { id: 'chat', label: 'Go to Chat', keywords: 'chat messages conversation' },
+ { id: 'files', label: 'Go to Files', keywords: 'files file tree explorer' },
+ { id: 'shell', label: 'Go to Shell', keywords: 'shell terminal console' },
+ { id: 'git', label: 'Go to Git', keywords: 'git diff branches' },
+ { id: 'tasks', label: 'Go to Tasks', keywords: 'tasks taskmaster' },
+];
+
export default function CommandPalette({
selectedProject,
onStartNewChat,
@@ -62,10 +97,11 @@ export default function CommandPalette({
if (!open) setSearch('');
}, [open]);
- const { mode } = parseMode(search);
+ const { mode, query } = parseMode(search);
const projectId = selectedProject?.projectId;
const { items: sessions } = useSessionsSource(projectId, open && (mode === 'mixed'));
+ const { items: messageMatches } = useSessionMessageSearch(projectId, query, open && mode === 'mixed');
const { items: files } = useFilesSource(projectId, open && (mode === 'mixed' || mode === 'files'));
const { items: commits } = useCommitsSource(projectId, open && (mode === 'mixed' || mode === 'commits'));
@@ -74,6 +110,29 @@ export default function CommandPalette({
const showFiles = mode === 'mixed' || mode === 'files';
const showCommits = mode === 'mixed' || mode === 'commits';
+ const sessionRows = React.useMemo(() => {
+ if (!showSessions) return [];
+ type Row = { id: string; label: string; provider?: string; snippet?: string };
+ const byId = new Map();
+ for (const s of sessions) {
+ byId.set(s.id, { id: s.id, label: s.label, provider: s.provider });
+ }
+ for (const m of messageMatches) {
+ const existing = byId.get(m.sessionId);
+ if (existing) {
+ existing.snippet = m.snippet;
+ } else {
+ byId.set(m.sessionId, {
+ id: m.sessionId,
+ label: m.label,
+ provider: m.provider,
+ snippet: m.snippet,
+ });
+ }
+ }
+ return Array.from(byId.values());
+ }, [sessions, messageMatches, showSessions]);
+
const filter = React.useCallback(
(value: string, rawSearch: string) => {
const stripped = parseMode(rawSearch).query.trim().toLowerCase();
@@ -119,7 +178,7 @@ export default function CommandPalette({
Select a project first
)}
- run(onOpenSettings)}>
+ run(() => onOpenSettings())}>
Open settings
@@ -133,16 +192,50 @@ export default function CommandPalette({
)}
- {showSessions && sessions.length > 0 && (
+ {showActions && (
+
+ {NAV_TABS.map((tab) => (
+ run(() => onShowTab?.(tab.id))}
+ >
+ {tab.label}
+
+ ))}
+
+ )}
+
+ {showActions && (
+
+ {SETTINGS_TABS.map(({ id, label, keywords, icon: Icon }) => (
+ run(() => onOpenSettings(id))}
+ >
+
+ Settings: {label}
+
+ ))}
+
+ )}
+
+ {showSessions && sessionRows.length > 0 && (
- {sessions.map((s) => (
+ {sessionRows.map((s) => (
run(() => navigate(`/session/${s.id}`))}
>
- {s.label}
+
+ {s.label}
+ {s.snippet && (
+ {s.snippet}
+ )}
+
{s.provider && (
{s.provider}
)}
diff --git a/src/components/command-palette/sources/useSessionMessageSearch.ts b/src/components/command-palette/sources/useSessionMessageSearch.ts
new file mode 100644
index 00000000..87598583
--- /dev/null
+++ b/src/components/command-palette/sources/useSessionMessageSearch.ts
@@ -0,0 +1,98 @@
+import { useEffect, useRef, useState } from 'react';
+
+import { api } from '../../../utils/api';
+import type { LLMProvider } from '../../../types/app';
+
+export type SessionMessageMatch = {
+ sessionId: string;
+ label: string;
+ snippet: string;
+ provider: LLMProvider;
+};
+
+type ProjectResult = {
+ projectId: string | null;
+ projectName: string;
+ sessions: Array<{
+ sessionId: string;
+ provider: LLMProvider;
+ sessionSummary: string;
+ matches: Array<{ snippet: string }>;
+ }>;
+};
+
+const MIN_QUERY = 2;
+const DEBOUNCE_MS = 250;
+
+export function useSessionMessageSearch(
+ projectId: string | undefined,
+ query: string,
+ enabled: boolean,
+) {
+ const [items, setItems] = useState([]);
+ const seqRef = useRef(0);
+ const esRef = useRef(null);
+
+ useEffect(() => {
+ const trimmed = query.trim();
+ if (!enabled || !projectId || trimmed.length < MIN_QUERY) {
+ setItems([]);
+ esRef.current?.close();
+ esRef.current = null;
+ return;
+ }
+
+ const handle = setTimeout(() => {
+ esRef.current?.close();
+ const seq = ++seqRef.current;
+ const url = api.searchConversationsUrl(trimmed);
+ const es = new EventSource(url);
+ esRef.current = es;
+ const accumulated: SessionMessageMatch[] = [];
+
+ es.addEventListener('result', (evt) => {
+ if (seq !== seqRef.current) {
+ es.close();
+ return;
+ }
+ try {
+ const data = JSON.parse((evt as MessageEvent).data) as { projectResult: ProjectResult };
+ const pr = data.projectResult;
+ if (pr.projectId !== projectId) return;
+ for (const s of pr.sessions) {
+ accumulated.push({
+ sessionId: s.sessionId,
+ label: s.sessionSummary || s.sessionId,
+ snippet: s.matches[0]?.snippet ?? '',
+ provider: s.provider,
+ });
+ }
+ setItems([...accumulated]);
+ } catch {
+ // ignore malformed
+ }
+ });
+
+ const finish = () => {
+ if (seq !== seqRef.current) return;
+ es.close();
+ esRef.current = null;
+ };
+ es.addEventListener('done', finish);
+ es.addEventListener('error', finish);
+ }, DEBOUNCE_MS);
+
+ return () => {
+ clearTimeout(handle);
+ };
+ }, [projectId, query, enabled]);
+
+ useEffect(() => {
+ return () => {
+ esRef.current?.close();
+ esRef.current = null;
+ };
+ }, []);
+
+ return { items };
+}
diff --git a/src/components/sidebar/view/subcomponents/SidebarHeader.tsx b/src/components/sidebar/view/subcomponents/SidebarHeader.tsx
index 551c0095..833be2ff 100644
--- a/src/components/sidebar/view/subcomponents/SidebarHeader.tsx
+++ b/src/components/sidebar/view/subcomponents/SidebarHeader.tsx
@@ -148,9 +148,9 @@ export default function SidebarHeader({
placeholder={searchMode === 'conversations' ? t('search.conversationsPlaceholder') : t('projects.searchPlaceholder')}
value={searchFilter}
onChange={(event) => onSearchFilterChange(event.target.value)}
- className="nav-search-input h-9 rounded-xl border-0 pl-9 pr-8 text-sm transition-all duration-200 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0"
+ className="nav-search-input h-9 rounded-xl border-0 pl-9 pr-14 text-sm transition-all duration-200 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0"
/>
- {searchFilter && (
+ {searchFilter ? (
+ ) : (
+
+ {typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform) ? '⌘' : 'Ctrl'}
+ K
+
)}