diff --git a/internal/server/web/dist/index.html b/internal/server/web/dist/index.html index e69236073ff22fda736c72267e83466d77661280..96db9527ec87a7d37c99188bca2508b08ed0409d 100644 --- a/internal/server/web/dist/index.html +++ b/internal/server/web/dist/index.html @@ -13,8 +13,8 @@ - - + +
diff --git a/web/src/shell/Palette.tsx b/web/src/shell/Palette.tsx index e63fefba998a20701f0f7dee6e9d909bfcd8cefd..11f3f46de89b3f05b938a9dd9bed4b823969867e 100644 --- a/web/src/shell/Palette.tsx +++ b/web/src/shell/Palette.tsx @@ -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 @@ hint: string 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' }, @@ -43,16 +71,48 @@ // Reset cursor when query changes useEffect(() => { 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 @@ void navigate({ to: '/search', search: { q: query } }) 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 @@ fire(cursor) } } + // 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 (
e.stopPropagation()}> @@ -113,20 +205,28 @@ "{query}"
)} - {filtered.map((item, i) => { - const idx = showSearch ? i + 1 : i + {groups.map(group => { + if (group.items.length === 0) return null return ( -
fire(idx)} - > - {item.kind} - {item.label} - {item.hint !== '' && ( - {item.hint} - )} -
+ +
{group.head}
+ {group.items.map((item, i) => { + const idx = group.startIdx + i + return ( +
fire(idx)} + > + {item.kind} + {item.label} + {item.hint !== undefined && item.hint !== '' && ( + {item.hint} + )} +
+ ) + })} +
) })} {filtered.length === 0 && !showSearch && ( diff --git a/web/src/styles/palette.css b/web/src/styles/palette.css index 60977b91160ab4ab63c513bf1590a4343954b103..40063731866dbeb7333bb975b4396347d53c81c5 100644 --- a/web/src/styles/palette.css +++ b/web/src/styles/palette.css @@ -118,6 +118,15 @@ font-family: var(--mono); } .palette-hint .spacer { flex: 1; } +/* ─── Group headers ─── */ +.palette-group-head { + padding: 8px 12px 4px; + font-size: 11px; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--ink-3); +} + /* ─── No matches ─── */ .palette-empty { padding: 14px;