learning_ai_common_plat/packages/command-palette/src/registry.tsx
saravanakumardb1 e2eea086dc feat(packages): Wave 2 v0.4 + Wave 3 v0.1 — ai-ui expanded, command-palette new
═══════════════════════════════════════════════════════════════════════
@bytelyst/ai-ui  bump 0.1.0 → 0.4.0
═══════════════════════════════════════════════════════════════════════
Folds three more roadmap milestones into the flagship package.

  0.2: <ToolCallCard>   — disclosure card; status pill, JSON preview
       <CitationChip>   — inline citation marker + hover preview
       useToolCalls()   — per-turn tool-invocation state machine
                          (begin/update/settle/clear); preserves insertion
                          order across updates; auto-computes durationMs
  0.3: <AgentTimeline>  — vertical think→act→observe→respond trace;
                          embeds ToolCallCard for kind='tool_call' steps
       <ModelPicker>    — model dropdown with capability chips, cost,
                          latency, context window, disabled gating
  0.4: <ToolPalette>    — searchable tool list with MCP-style discovery
                          (source can be ToolDescriptor[] OR an
                          'mcp://...' URL resolved via a discover
                          adapter; default adapter is fetch+JSON)

Types extended:
  - ToolInvocation, ToolCallStatus, Citation added
  - Message gains optional toolInvocations + citations

Tests: 53/53 (27 old + 26 new) · typecheck clean · 7.65 KB / 35 KB

═══════════════════════════════════════════════════════════════════════
NEW PACKAGE: @bytelyst/command-palette@0.1.0
═══════════════════════════════════════════════════════════════════════
Wave 3 deliverable — Cmd-K dialog with three modes and pluggable command
registration. Roadmap §Wave 3 of ROADMAP_2026.md.

What's exported:
  <CommandRegistryProvider>  — wrap your app once
  <CommandPalette>           — the dialog (Cmd-K / Ctrl-K)
  useRegisterCommands()      — contribute commands for component lifetime
  useCommands()              — read snapshot
  useCommandRegistry()       — imperative access
  useCommandPalette()        — open/close state + global hotkey
  fuzzyScore / scoreCommand  — exposed for tests + custom UIs

Three modes:
  actions   — invoke a registered run()
  navigate  — jump to href via onNavigate or window.location
  ask-ai    — host-supplied askAiPanel; default renders an 'Ask AI: <q>'
              suggestion that products can wire to <ChatStream>

Keyboard:
  ↑ ↓     navigate selection
  Enter   activate
  Tab     cycle mode tabs (Shift+Tab reverses)
  Esc     close

Niceties:
  - Fuzzy matcher (substring + subsequence with light scoring)
  - localStorage-backed recents float to top of actions mode
  - requires() gate hides commands wholesale (auth / feature-flag)
  - aria-haspopup, role=dialog, role=listbox, role=option, aria-selected
  - Backdrop click closes; Esc handler at document level
  - Hotkey suppressed by Cmd-K / Ctrl-K default; configurable

Tests: 26/26 · typecheck clean · 3.91 KB / 15 KB

═══════════════════════════════════════════════════════════════════════
CI plumbing
═══════════════════════════════════════════════════════════════════════
  - .size-limit.cjs gains @bytelyst/command-palette entry
  - .gitea/workflows/size-limit.yml build filter expanded
  - All 8 measured packages comfortably under budget

Refs:
  learning_ai_uxui_web/docs/ROADMAP_2026.md §Wave 2 (0.2/0.3/0.4)
  learning_ai_uxui_web/docs/ROADMAP_2026.md §Wave 3 (Command palette)
  docs/ROADMAP_2026_DECISIONS.md §10 (Vercel AI SDK shape continues)
2026-05-27 12:43:23 -07:00

129 lines
3.6 KiB
TypeScript

import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from 'react';
import type { Command } from './types.js';
interface RegistryState {
commands: Map<string, Command>;
register: (commands: Command[]) => () => void;
unregister: (ids: string[]) => void;
clear: () => void;
}
const RegistryContext = createContext<RegistryState | null>(null);
export interface CommandRegistryProviderProps {
/** Optional seed of commands always available (app-level chrome). */
initial?: Command[];
children: ReactNode;
}
/**
* Provides a shared command registry to descendants. Wrap your app once
* (typically next to `<DashboardShell>`) and any nested component can
* call `useRegisterCommands(...)` to contribute commands for as long as
* it stays mounted.
*/
export function CommandRegistryProvider({
initial = [],
children,
}: CommandRegistryProviderProps) {
const [commands, setCommands] = useState<Map<string, Command>>(() => {
const m = new Map<string, Command>();
for (const c of initial) m.set(c.id, c);
return m;
});
const register = useCallback<RegistryState['register']>(next => {
setCommands(prev => {
const m = new Map(prev);
for (const c of next) m.set(c.id, c);
return m;
});
return () => {
setCommands(prev => {
const m = new Map(prev);
for (const c of next) m.delete(c.id);
return m;
});
};
}, []);
const unregister = useCallback<RegistryState['unregister']>(ids => {
setCommands(prev => {
const m = new Map(prev);
for (const id of ids) m.delete(id);
return m;
});
}, []);
const clear = useCallback(() => setCommands(new Map()), []);
const value = useMemo<RegistryState>(
() => ({ commands, register, unregister, clear }),
[commands, register, unregister, clear],
);
return <RegistryContext.Provider value={value}>{children}</RegistryContext.Provider>;
}
function useRegistry(): RegistryState {
const ctx = useContext(RegistryContext);
if (!ctx) {
throw new Error(
'@bytelyst/command-palette: useRegisterCommands / useCommands must be used within <CommandRegistryProvider>',
);
}
return ctx;
}
/**
* Register commands for the lifetime of the calling component. Returns a
* stable function callers can ignore — registration unwinds automatically
* on unmount via the returned effect cleanup.
*
* @example
* ```tsx
* useRegisterCommands([
* { id: 'new-task', label: 'New task', icon: '+', run: openNewTask },
* { id: 'goto-billing', label: 'Billing', mode: 'navigate', href: '/billing' },
* ]);
* ```
*/
export function useRegisterCommands(commands: Command[]): void {
const { register } = useRegistry();
// Stable ref to the most-recent commands list. We intentionally avoid
// re-registering on every keystroke / state tick — callers usually
// pass an inline array. The serialization key below keeps it cheap.
const key = useMemo(
() =>
commands
.map(c => `${c.id}|${c.label}|${c.mode ?? ''}|${c.section ?? ''}`)
.join('\n'),
[commands],
);
useEffect(() => {
const off = register(commands);
return off;
// eslint-disable-next-line react-hooks/exhaustive-deps -- key is the canonical identity
}, [key, register]);
}
/** Read the current registry snapshot. */
export function useCommands(): Command[] {
const { commands } = useRegistry();
return useMemo(() => Array.from(commands.values()), [commands]);
}
/** Imperative access (e.g. for tests or shell-level wiring). */
export function useCommandRegistry(): RegistryState {
return useRegistry();
}