═══════════════════════════════════════════════════════════════════════
@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)
138 lines
4.7 KiB
TypeScript
138 lines
4.7 KiB
TypeScript
import { useCallback, useMemo, useState } from 'react';
|
|
import type { ToolCallStatus, ToolInvocation } from './types.js';
|
|
|
|
export interface UseToolCallsHelpers {
|
|
/** Map of id → ToolInvocation, stable across renders. */
|
|
invocations: Record<string, ToolInvocation>;
|
|
/** Ordered list of invocations (insertion order). */
|
|
ordered: ToolInvocation[];
|
|
/** Begin a new tool call. Idempotent — repeated calls update in place. */
|
|
begin: (
|
|
init: Pick<ToolInvocation, 'id' | 'name'> & Partial<Omit<ToolInvocation, 'id' | 'name'>>,
|
|
) => void;
|
|
/** Merge a partial update into an existing invocation. */
|
|
update: (id: string, patch: Partial<ToolInvocation>) => void;
|
|
/** Transition an invocation to its terminal state (success / error). */
|
|
settle: (
|
|
id: string,
|
|
result:
|
|
| { status: 'success'; output?: unknown }
|
|
| { status: 'error'; error: string },
|
|
) => void;
|
|
/** Remove a specific invocation (e.g. on reload). */
|
|
clear: (id: string) => void;
|
|
/** Remove all invocations. */
|
|
clearAll: () => void;
|
|
}
|
|
|
|
/**
|
|
* `useToolCalls` — per-turn tool-invocation state machine.
|
|
*
|
|
* Stores tool calls in two parallel shapes — a Record for O(1) lookup
|
|
* by id and an ordered array for rendering. Insertion order is preserved
|
|
* across `update` calls so vertical scroll position stays stable while
|
|
* a stream is in flight.
|
|
*
|
|
* Typical wiring (Wave 2 0.2 — usage with raw streams):
|
|
*
|
|
* ```ts
|
|
* const tools = useToolCalls();
|
|
* for await (const event of parseToolStream(response.body)) {
|
|
* if (event.type === 'tool-call') tools.begin(event);
|
|
* if (event.type === 'tool-delta') tools.update(event.id, event);
|
|
* if (event.type === 'tool-result') tools.settle(event.id, event.result);
|
|
* }
|
|
* ```
|
|
*
|
|
* 0.4 (`<ToolPalette>` with MCP discovery) will layer auto-wiring on top
|
|
* of this hook.
|
|
*/
|
|
export function useToolCalls(initial: ToolInvocation[] = []): UseToolCallsHelpers {
|
|
const [invocations, setInvocations] = useState<Record<string, ToolInvocation>>(
|
|
() => Object.fromEntries(initial.map(t => [t.id, t])),
|
|
);
|
|
const [order, setOrder] = useState<string[]>(() => initial.map(t => t.id));
|
|
|
|
const begin = useCallback<UseToolCallsHelpers['begin']>(init => {
|
|
const id = init.id;
|
|
const status: ToolCallStatus = init.status ?? 'pending';
|
|
const startedAt = performance.now();
|
|
setInvocations(prev => {
|
|
const existing = prev[id];
|
|
const next: ToolInvocation = {
|
|
...existing,
|
|
status,
|
|
...init,
|
|
// Carry the start timestamp into a private field for later
|
|
// settle() to compute durationMs. We tuck it under a symbol-
|
|
// free convention so consumers can still read everything.
|
|
};
|
|
// Stash startedAt on a hidden field. We can't use Symbol because
|
|
// we want this hook to survive JSON serialization in tests.
|
|
(next as { __startedAt?: number }).__startedAt = startedAt;
|
|
return { ...prev, [id]: next };
|
|
});
|
|
setOrder(prev => (prev.includes(id) ? prev : [...prev, id]));
|
|
}, []);
|
|
|
|
const update = useCallback<UseToolCallsHelpers['update']>((id, patch) => {
|
|
setInvocations(prev => {
|
|
const existing = prev[id];
|
|
if (!existing) return prev;
|
|
return { ...prev, [id]: { ...existing, ...patch } };
|
|
});
|
|
}, []);
|
|
|
|
const settle = useCallback<UseToolCallsHelpers['settle']>((id, result) => {
|
|
setInvocations(prev => {
|
|
const existing = prev[id];
|
|
if (!existing) return prev;
|
|
const startedAt = (existing as { __startedAt?: number }).__startedAt;
|
|
const durationMs =
|
|
typeof startedAt === 'number'
|
|
? Math.max(0, Math.round(performance.now() - startedAt))
|
|
: existing.durationMs;
|
|
const next: ToolInvocation =
|
|
result.status === 'success'
|
|
? {
|
|
...existing,
|
|
status: 'success',
|
|
output: result.output ?? existing.output,
|
|
error: undefined,
|
|
durationMs,
|
|
}
|
|
: {
|
|
...existing,
|
|
status: 'error',
|
|
error: result.error,
|
|
durationMs,
|
|
};
|
|
return { ...prev, [id]: next };
|
|
});
|
|
}, []);
|
|
|
|
const clear = useCallback<UseToolCallsHelpers['clear']>(id => {
|
|
setInvocations(prev => {
|
|
if (!(id in prev)) return prev;
|
|
const { [id]: _drop, ...rest } = prev;
|
|
return rest;
|
|
});
|
|
setOrder(prev => prev.filter(x => x !== id));
|
|
}, []);
|
|
|
|
const clearAll = useCallback(() => {
|
|
setInvocations({});
|
|
setOrder([]);
|
|
}, []);
|
|
|
|
const ordered = useMemo(
|
|
() =>
|
|
order
|
|
.map(id => invocations[id])
|
|
.filter((x): x is ToolInvocation => x !== undefined),
|
|
[order, invocations],
|
|
);
|
|
|
|
return { invocations, ordered, begin, update, settle, clear, clearAll };
|
|
}
|