From ac7e06ffe46e035bfbe2988dfc631c50fc99340f Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sun, 26 Apr 2026 16:51:58 +0300 Subject: [PATCH] =?UTF-8?q?web:=20palette=20items=20=E2=80=94=20projects,?= =?UTF-8?q?=20sessions,=20saved=20searches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the command palette with three new item kinds (project, session, saved) backed by unconditional TanStack Query hooks. Items are grouped in fixed order (jump → projects → sessions → saved) with section headers; fire() dispatches kind-aware navigation; .palette-group-head CSS added. --- internal/server/web/dist/index.html | 4 +- web/src/shell/Palette.tsx | 132 ++++++++++++++++++++++++---- web/src/styles/palette.css | 9 ++ 3 files changed, 127 insertions(+), 18 deletions(-) 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 @@ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" /> - - + +
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 @@ 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 (
e.stopPropagation()}> @@ -113,20 +205,28 @@ export function Palette({ open, onClose }: PaletteProps): React.JSX.Element {
)} - {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 @@ } .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;