@@ 1,5 1,8 @@
import React, { useState, useEffect, useRef } from 'react'
import { useNavigate } from '@tanstack/react-router'
+import { useProjects } from '../features/projects/useProjects'
+import { useSessions } from '../features/home/useSessions'
+import { useSavedSearches } from '../features/settings/useSavedSearches'
interface PaletteProps {
open: boolean
@@ 13,6 16,31 @@ interface JumpItem {
path: string
}
+interface ProjectItem {
+ kind: 'project'
+ label: string
+ hint?: string
+ cwd: string
+}
+
+interface SessionItem {
+ kind: 'session'
+ label: string
+ hint?: string
+ tool: string
+ host: string
+ sessionId: string
+}
+
+interface SavedItem {
+ kind: 'saved'
+ label: string
+ hint?: string
+ query: string
+}
+
+type PaletteItem = JumpItem | ProjectItem | SessionItem | SavedItem
+
const JUMP_ITEMS: JumpItem[] = [
{ kind: 'jump', label: 'Recent', hint: 'g h', path: '/' },
{ kind: 'jump', label: 'Projects', hint: 'g p', path: '/projects' },
@@ 44,15 72,47 @@ export function Palette({ open, onClose }: PaletteProps): React.JSX.Element {
setCursor(0)
}, [query])
+ // Always call hooks unconditionally (React rules); data is used only when open
+ const { data: projects } = useProjects({ since: 'all' })
+ const { data: sessions } = useSessions({})
+ const { data: savedSearches } = useSavedSearches()
+
if (!open) return <></>
const q = query.trim()
- const filtered: JumpItem[] = q === ''
+
+ const filteredJumps: JumpItem[] = q === ''
? JUMP_ITEMS
: JUMP_ITEMS.filter(item =>
item.label.toLowerCase().includes(q.toLowerCase()),
)
+ const filteredProjects: ProjectItem[] = (projects ?? [])
+ .map(p => ({ kind: 'project' as const, label: p.cwd, cwd: p.cwd }))
+ .filter(item => q === '' || item.label.toLowerCase().includes(q.toLowerCase()))
+
+ const filteredSessions: SessionItem[] = (sessions ?? [])
+ .slice(0, 50)
+ .map(s => ({
+ kind: 'session' as const,
+ label: `${s.tool} · ${s.host} · ${s.summary || s.sessionId}`,
+ tool: s.tool,
+ host: s.host,
+ sessionId: s.sessionId,
+ }))
+ .filter(item => q === '' || item.label.toLowerCase().includes(q.toLowerCase()))
+
+ const filteredSaved: SavedItem[] = (savedSearches ?? [])
+ .map(ss => ({ kind: 'saved' as const, label: ss.name, hint: ss.query, query: ss.query }))
+ .filter(item => q === '' || item.label.toLowerCase().includes(q.toLowerCase()))
+
+ const filtered: PaletteItem[] = [
+ ...filteredJumps,
+ ...filteredProjects,
+ ...filteredSessions,
+ ...filteredSaved,
+ ]
+
const showSearch = q !== '' && filtered.length === 0
const total = filtered.length + (showSearch ? 1 : 0)
@@ 62,9 122,25 @@ export function Palette({ open, onClose }: PaletteProps): React.JSX.Element {
onClose()
return
}
- const item = filtered[idx]
+ const item = filtered[showSearch ? idx - 1 : idx]
if (item) {
- void navigate({ to: item.path })
+ switch (item.kind) {
+ case 'jump':
+ void navigate({ to: item.path })
+ break
+ case 'project':
+ void navigate({ to: '/project/$', params: { _splat: item.cwd } })
+ break
+ case 'session':
+ void navigate({
+ to: '/session/$tool/$host/$id',
+ params: { tool: item.tool, host: item.host, id: item.sessionId },
+ })
+ break
+ case 'saved':
+ void navigate({ to: '/search', search: { q: item.query } })
+ break
+ }
onClose()
}
}
@@ 85,6 161,22 @@ export function Palette({ open, onClose }: PaletteProps): React.JSX.Element {
}
}
+ // Build grouped render list with section headers
+ interface Group {
+ key: string
+ head: string
+ items: PaletteItem[]
+ startIdx: number
+ }
+
+ const offset = showSearch ? 1 : 0
+ const groups: Group[] = [
+ { key: 'jump', head: 'jump', items: filteredJumps, startIdx: offset },
+ { key: 'projects', head: 'projects', items: filteredProjects, startIdx: offset + filteredJumps.length },
+ { key: 'sessions', head: 'sessions', items: filteredSessions, startIdx: offset + filteredJumps.length + filteredProjects.length },
+ { key: 'saved', head: 'saved', items: filteredSaved, startIdx: offset + filteredJumps.length + filteredProjects.length + filteredSessions.length },
+ ]
+
return (
<div className="scrim" onClick={onClose}>
<div className="palette" onClick={e => e.stopPropagation()}>
@@ 113,20 205,28 @@ export function Palette({ open, onClose }: PaletteProps): React.JSX.Element {
<span className="row-hint mono muted">↵</span>
</div>
)}
- {filtered.map((item, i) => {
- const idx = showSearch ? i + 1 : i
+ {groups.map(group => {
+ if (group.items.length === 0) return null
return (
- <div
- key={item.label}
- className={'palette-row' + (idx === cursor ? ' cursor' : '')}
- onClick={() => fire(idx)}
- >
- <span className="kind">{item.kind}</span>
- <span className="label">{item.label}</span>
- {item.hint !== '' && (
- <span className="row-hint mono muted">{item.hint}</span>
- )}
- </div>
+ <React.Fragment key={group.key}>
+ <div className="palette-group-head muted mono">{group.head}</div>
+ {group.items.map((item, i) => {
+ const idx = group.startIdx + i
+ return (
+ <div
+ key={`${item.kind}:${item.label}:${i}`}
+ className={'palette-row' + (idx === cursor ? ' cursor' : '')}
+ onClick={() => fire(idx)}
+ >
+ <span className="kind">{item.kind}</span>
+ <span className="label">{item.label}</span>
+ {item.hint !== undefined && item.hint !== '' && (
+ <span className="row-hint mono muted">{item.hint}</span>
+ )}
+ </div>
+ )
+ })}
+ </React.Fragment>
)
})}
{filtered.length === 0 && !showSearch && (