Skip to content
Merged
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
94 changes: 76 additions & 18 deletions apps/desktop/src/app/command-palette/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
Package,
Palette,
PawPrint,
Pin,
Plus,
RefreshCw,
Settings,
Expand All @@ -70,11 +71,13 @@ import {
setCommandPaletteOpen
} from '@/store/command-palette'
import { $bindings, bindingsFor } from '@/store/keybinds'
import { $dismissedAutoProjectIds, filterVisibleProjects } from '@/store/layout'
import { $dismissedAutoProjectIds, $pinnedSessionIds, filterVisibleProjects } from '@/store/layout'
import { openPetGenerate } from '@/store/pet-generate'
import { openBrowserTab } from '@/store/preview'
import { $projectTree, goToProject, openFolderAsProject, requestStartWorkSession } from '@/store/projects'
import { $connection } from '@/store/session'
import { $connection, $cronSessions, $messagingSessions, $sessions } from '@/store/session'
import { $unconfirmedPinWrites } from '@/store/session-pin-sync'
import { $removedSessionIds } from '@/store/session-removal'
import { runGatewayRestart } from '@/store/system-actions'
import {
$backendUpdateApply,
Expand All @@ -89,6 +92,7 @@ import { luminance } from '@/themes/color'
import { type ThemeMode, useTheme } from '@/themes/context'
import { isUserTheme, resolveTheme } from '@/themes/user-themes'

import { buildSessionByAnyId, resolvePinnedSessions } from '../chat/sidebar/session-index'
import { openSessionFromPicker, openSessionIntentFromModifiers } from '../open-session'
import {
AGENTS_ROUTE,
Expand Down Expand Up @@ -393,6 +397,11 @@ const toSessionEntry = (session: SessionRow): SessionEntry => ({
title: sessionTitle(session)
})

// Search terms beyond the label: the preview and branch, so a session is
// findable by what it's about, not only what it's called.
const sessionKeywords = (session: SessionEntry, ...tags: string[]): string[] =>
[...tags, 'chat', 'session', session.preview, session.git_branch].filter((word): word is string => !!word)

type NonConfigSettingsLabel =
'about' | 'archivedChats' | 'gateway' | 'keysSettings' | 'keysTools' | 'mcp' | 'providerAccounts' | 'providerApiKeys'

Expand Down Expand Up @@ -637,21 +646,65 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) {
queryFn: () => getHermesConfigRecord()
})

// staleTime 0 (not the 60s client default): renames, pins, and archives
// happen in the sidebar while this component is unmounted, so nothing can
// invalidate these keys — every open must revalidate. The cached page
// still paints instantly; the live-store overlay below covers the gap.
const sessionsQuery = useQuery({
queryKey: ['command-palette', 'sessions'],
queryFn: () => listAllProfileSessions(200, 1, 'exclude')
queryFn: () => listAllProfileSessions(200, 1, 'exclude'),
staleTime: 0
})

const archivedQuery = useQuery({
queryKey: ['command-palette', 'archived'],
queryFn: () => listAllProfileSessions(200, 0, 'only')
queryFn: () => listAllProfileSessions(200, 0, 'only'),
staleTime: 0
})

const liveSessions = useStore($sessions)
const liveCronSessions = useStore($cronSessions)
const liveMessagingSessions = useStore($messagingSessions)
const pinnedSessionIds = useStore($pinnedSessionIds)
const unconfirmedPinWrites = useStore($unconfirmedPinWrites)
const removedSessionIds = useStore($removedSessionIds)

// getServers is the shared choke point that also drops malformed (null/
// scalar) entries, so the palette never lists a server the MCP tab dropped.
const mcpServers = useMemo(() => Object.keys(getServers(configQuery.data ?? null)).sort(), [configQuery.data])

const sessions = useMemo(() => (sessionsQuery.data?.sessions ?? []).map(toSessionEntry), [sessionsQuery.data])
// The sidebar's stores are where a rename / pin / archive lands first (the
// server page confirms later). Overlay them on the fetched 200-row page so
// the palette says what the sidebar says: same title, same pin, and no row
// the user just archived or deleted.
const liveRows = useMemo(() => {
const byId = new Map(
[...liveCronSessions, ...liveMessagingSessions, ...liveSessions].map(row => [row.id, row] as const)
)

return (sessionsQuery.data?.sessions ?? [])
.filter(session => !removedSessionIds.has(session.id))
.map(session => {
const live = byId.get(session.id)

return live ? { ...session, pinned: live.pinned, title: live.title } : session
})
}, [liveCronSessions, liveMessagingSessions, liveSessions, removedSessionIds, sessionsQuery.data])

// Same resolution as the sidebar's Pinned section: local pin order first,
// then server-flagged pins, minus our own in-flight unpins.
const pinnedSessions = useMemo(() => {
const byAnyId = buildSessionByAnyId(liveRows, [], [])

return resolvePinnedSessions(pinnedSessionIds, byAnyId, liveRows, unconfirmedPinWrites).map(toSessionEntry)
}, [liveRows, pinnedSessionIds, unconfirmedPinWrites])

const sessions = useMemo(() => {
const pinned = new Set(pinnedSessions.map(session => session.id))

return liveRows.filter(session => !pinned.has(session.id)).map(toSessionEntry)
}, [liveRows, pinnedSessions])

const archivedSessions = useMemo(() => (archivedQuery.data?.sessions ?? []).map(toSessionEntry), [archivedQuery.data])

// Search/sub-page are local to a mount, and this component remounts per open
Expand Down Expand Up @@ -1162,18 +1215,28 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) {
}))
})

// Pinned before Sessions: rankGroups' stable sort keeps source order on
// equal scores, so a pin wins a tie with an unpinned row of the same name.
if (pinnedSessions.length > 0) {
result.push({
heading: t.sidebar.pinned,
items: pinnedSessions.map(session => ({
icon: Pin,
id: `pinned-${session.id}`,
keywords: sessionKeywords(session, 'pinned'),
label: session.title,
runWithEvent: goSession(session.id)
}))
})
}

if (sessions.length > 0) {
result.push({
heading: t.commandCenter.sections.sessions,
items: sessions.map(session => ({
icon: MessageCircle,
id: `session-${session.id}`,
keywords: [
'chat',
'session',
...(session.preview ? [session.preview] : []),
...(session.git_branch ? [session.git_branch] : [])
],
keywords: sessionKeywords(session),
label: session.title,
runWithEvent: goSession(session.id)
}))
Expand Down Expand Up @@ -1230,13 +1293,7 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) {
items: archivedSessions.map(session => ({
icon: Archive,
id: `archived-${session.id}`,
keywords: [
'archived',
'chat',
'session',
...(session.preview ? [session.preview] : []),
...(session.git_branch ? [session.git_branch] : [])
],
keywords: sessionKeywords(session, 'archived'),
label: session.title,
run: go(`${SETTINGS_ROUTE}?tab=sessions&session=${encodeURIComponent(session.id)}`)
}))
Expand All @@ -1251,6 +1308,7 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) {
goSession,
mcpServers,
mode,
pinnedSessions,
previewTheme,
resolvedMode,
resolveThemeMode,
Expand Down
Loading