═══════════════════════════════════════════════════════════════════════
@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)
69 lines
2.1 KiB
TypeScript
69 lines
2.1 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
export interface UseCommandPaletteOptions {
|
|
/** Default hotkey: Cmd-K / Ctrl-K. Pass `null` to disable. */
|
|
hotkey?: { key: string; meta?: boolean; ctrl?: boolean; shift?: boolean } | null;
|
|
/** Initial open state. */
|
|
defaultOpen?: boolean;
|
|
}
|
|
|
|
export interface UseCommandPaletteHelpers {
|
|
open: boolean;
|
|
show: () => void;
|
|
hide: () => void;
|
|
toggle: () => void;
|
|
}
|
|
|
|
/**
|
|
* Manages open/closed state for `<CommandPalette>` and (optionally)
|
|
* binds a global hotkey. Drop it next to your app shell:
|
|
*
|
|
* ```tsx
|
|
* const cmdk = useCommandPalette();
|
|
* return (
|
|
* <>
|
|
* <YourApp />
|
|
* <CommandPalette open={cmdk.open} onClose={cmdk.hide} />
|
|
* </>
|
|
* );
|
|
* ```
|
|
*
|
|
* The default hotkey (`Cmd-K` / `Ctrl-K`) is suppressed inside text
|
|
* inputs unless the user explicitly holds the modifier.
|
|
*/
|
|
export function useCommandPalette(
|
|
options: UseCommandPaletteOptions = {},
|
|
): UseCommandPaletteHelpers {
|
|
const { hotkey = { key: 'k', meta: true, ctrl: true }, defaultOpen = false } =
|
|
options;
|
|
|
|
const [open, setOpen] = useState(defaultOpen);
|
|
const show = useCallback(() => setOpen(true), []);
|
|
const hide = useCallback(() => setOpen(false), []);
|
|
const toggle = useCallback(() => setOpen(o => !o), []);
|
|
|
|
useEffect(() => {
|
|
if (!hotkey) return;
|
|
const handler = (e: KeyboardEvent) => {
|
|
if (e.key.toLowerCase() !== hotkey.key.toLowerCase()) return;
|
|
const wantMeta = hotkey.meta ?? false;
|
|
const wantCtrl = hotkey.ctrl ?? false;
|
|
const wantShift = hotkey.shift ?? false;
|
|
// Match if EITHER meta or ctrl is held when both are flagged (Mac/Win parity).
|
|
const modOk =
|
|
wantMeta || wantCtrl
|
|
? (wantMeta && e.metaKey) || (wantCtrl && e.ctrlKey)
|
|
: !e.metaKey && !e.ctrlKey;
|
|
const shiftOk = wantShift ? e.shiftKey : true;
|
|
if (modOk && shiftOk) {
|
|
e.preventDefault();
|
|
setOpen(o => !o);
|
|
}
|
|
};
|
|
window.addEventListener('keydown', handler);
|
|
return () => window.removeEventListener('keydown', handler);
|
|
}, [hotkey]);
|
|
|
|
return { open, show, hide, toggle };
|
|
}
|