learning_ai_common_plat/packages/ai-ui/src/AgentTimeline.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

183 lines
4.9 KiB
TypeScript

import type { CSSProperties, ReactNode } from 'react';
import { ToolCallCard } from './ToolCallCard.js';
import type { ToolInvocation } from './types.js';
export type AgentStepKind =
| 'thought'
| 'tool_call'
| 'observation'
| 'response'
| 'custom';
export interface AgentStep {
id: string;
kind: AgentStepKind;
/** Step body. For 'tool_call' supply a ToolInvocation, else freeform. */
content?: ReactNode;
/** Required when kind === 'tool_call'. */
invocation?: ToolInvocation;
/** Override the label rendered in the marker column. */
label?: string;
/** Visually mark this step as still in-progress (e.g. streaming). */
isActive?: boolean;
}
export interface AgentTimelineProps {
steps: AgentStep[];
className?: string;
style?: CSSProperties;
}
/**
* `<AgentTimeline>` — vertical trace of agent steps in the
* `think → act → observe → respond` loop. Used by JarvisJr and any
* agentic flow that wants to expose the agent's reasoning chain.
*
* Each step renders with a colored marker dot, a kind label, and a
* body slot. `kind === 'tool_call'` automatically uses `<ToolCallCard>`
* so the rendering stays consistent across the catalog.
*/
export function AgentTimeline({ steps, className, style }: AgentTimelineProps) {
return (
<ol
data-testid="bl-agent-timeline"
className={className}
style={{
listStyle: 'none',
margin: 0,
padding: 0,
display: 'flex',
flexDirection: 'column',
gap: 0,
position: 'relative',
...style,
}}
>
{steps.map((step, idx) => (
<Step key={step.id} step={step} isLast={idx === steps.length - 1} />
))}
</ol>
);
}
function Step({ step, isLast }: { step: AgentStep; isLast: boolean }) {
const palette = STEP_PALETTE[step.kind];
return (
<li
data-testid={`bl-agent-step-${step.id}`}
data-kind={step.kind}
style={{
position: 'relative',
display: 'grid',
gridTemplateColumns: '32px 1fr',
gap: 'var(--bl-space-3, 12px)',
paddingBottom: isLast ? 0 : 'var(--bl-space-4, 16px)',
}}
>
{/* Marker rail */}
<div
style={{
position: 'relative',
display: 'flex',
justifyContent: 'center',
paddingTop: 4,
}}
>
<span
aria-hidden
style={{
display: 'inline-block',
width: 12,
height: 12,
borderRadius: '50%',
background: palette.dot,
boxShadow: step.isActive
? `0 0 0 4px ${palette.glow}`
: `0 0 0 2px var(--bl-surface-card, #fff)`,
transition: 'box-shadow 200ms ease',
}}
/>
{!isLast && (
<span
aria-hidden
style={{
position: 'absolute',
top: 18,
bottom: -12,
width: 2,
background: 'var(--bl-border, rgba(0,0,0,0.1))',
}}
/>
)}
</div>
{/* Body */}
<div style={{ minWidth: 0 }}>
<div
style={{
fontSize: '0.7rem',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.06em',
color: palette.label,
marginBottom: 'var(--bl-space-1, 4px)',
}}
>
{step.label ?? STEP_LABELS[step.kind]}
</div>
{step.kind === 'tool_call' && step.invocation ? (
<ToolCallCard invocation={step.invocation} defaultExpanded={step.isActive} />
) : (
<div
style={{
fontSize: '0.9rem',
lineHeight: 1.5,
color: 'var(--bl-text-primary, inherit)',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{step.content}
</div>
)}
</div>
</li>
);
}
const STEP_LABELS: Record<AgentStepKind, string> = {
thought: 'Thought',
tool_call: 'Action',
observation: 'Observation',
response: 'Response',
custom: 'Step',
};
const STEP_PALETTE: Record<AgentStepKind, { dot: string; glow: string; label: string }> = {
thought: {
dot: 'var(--bl-text-tertiary, #888)',
glow: 'var(--bl-surface-muted, rgba(0,0,0,0.04))',
label: 'var(--bl-text-tertiary, #888)',
},
tool_call: {
dot: 'var(--bl-accent, #6366f1)',
glow: 'var(--bl-accent-muted, rgba(99,102,241,0.18))',
label: 'var(--bl-accent, #6366f1)',
},
observation: {
dot: 'var(--bl-info, #38bdf8)',
glow: 'var(--bl-info-muted, rgba(56,189,248,0.18))',
label: 'var(--bl-info, #38bdf8)',
},
response: {
dot: 'var(--bl-success, #22c55e)',
glow: 'var(--bl-success-muted, rgba(34,197,94,0.18))',
label: 'var(--bl-success, #22c55e)',
},
custom: {
dot: 'var(--bl-text-secondary, #555)',
glow: 'var(--bl-surface-muted, rgba(0,0,0,0.06))',
label: 'var(--bl-text-secondary, #555)',
},
};