@steerable/agent-ui 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +91 -0
  2. package/README.md +118 -0
  3. package/dist/components/ChatPanel.d.ts +42 -0
  4. package/dist/components/ChatPanel.d.ts.map +1 -0
  5. package/dist/components/ChatPanel.js +46 -0
  6. package/dist/components/ChatPanel.js.map +1 -0
  7. package/dist/components/ChatPanel.stories.d.ts +17 -0
  8. package/dist/components/ChatPanel.stories.d.ts.map +1 -0
  9. package/dist/components/ChatPanel.stories.js +143 -0
  10. package/dist/components/ChatPanel.stories.js.map +1 -0
  11. package/dist/components/MessageList.d.ts +44 -0
  12. package/dist/components/MessageList.d.ts.map +1 -0
  13. package/dist/components/MessageList.js +43 -0
  14. package/dist/components/MessageList.js.map +1 -0
  15. package/dist/components/MessageList.stories.d.ts +12 -0
  16. package/dist/components/MessageList.stories.d.ts.map +1 -0
  17. package/dist/components/MessageList.stories.js +88 -0
  18. package/dist/components/MessageList.stories.js.map +1 -0
  19. package/dist/components/OrchestrationPlanCard.d.ts +32 -0
  20. package/dist/components/OrchestrationPlanCard.d.ts.map +1 -0
  21. package/dist/components/OrchestrationPlanCard.js +48 -0
  22. package/dist/components/OrchestrationPlanCard.js.map +1 -0
  23. package/dist/components/OrchestrationPlanCard.stories.d.ts +12 -0
  24. package/dist/components/OrchestrationPlanCard.stories.d.ts.map +1 -0
  25. package/dist/components/OrchestrationPlanCard.stories.js +84 -0
  26. package/dist/components/OrchestrationPlanCard.stories.js.map +1 -0
  27. package/dist/components/SSEStreamView.d.ts +29 -0
  28. package/dist/components/SSEStreamView.d.ts.map +1 -0
  29. package/dist/components/SSEStreamView.js +78 -0
  30. package/dist/components/SSEStreamView.js.map +1 -0
  31. package/dist/components/SSEStreamView.stories.d.ts +12 -0
  32. package/dist/components/SSEStreamView.stories.d.ts.map +1 -0
  33. package/dist/components/SSEStreamView.stories.js +86 -0
  34. package/dist/components/SSEStreamView.stories.js.map +1 -0
  35. package/dist/components/ToolCallRenderer.d.ts +42 -0
  36. package/dist/components/ToolCallRenderer.d.ts.map +1 -0
  37. package/dist/components/ToolCallRenderer.js +70 -0
  38. package/dist/components/ToolCallRenderer.js.map +1 -0
  39. package/dist/components/ToolCallRenderer.stories.d.ts +13 -0
  40. package/dist/components/ToolCallRenderer.stories.d.ts.map +1 -0
  41. package/dist/components/ToolCallRenderer.stories.js +112 -0
  42. package/dist/components/ToolCallRenderer.stories.js.map +1 -0
  43. package/dist/components/cn.d.ts +7 -0
  44. package/dist/components/cn.d.ts.map +1 -0
  45. package/dist/components/cn.js +9 -0
  46. package/dist/components/cn.js.map +1 -0
  47. package/dist/components/index.d.ts +11 -0
  48. package/dist/components/index.d.ts.map +1 -0
  49. package/dist/components/index.js +6 -0
  50. package/dist/components/index.js.map +1 -0
  51. package/dist/hooks/index.d.ts +7 -0
  52. package/dist/hooks/index.d.ts.map +1 -0
  53. package/dist/hooks/index.js +4 -0
  54. package/dist/hooks/index.js.map +1 -0
  55. package/dist/hooks/useAgentSession.d.ts +56 -0
  56. package/dist/hooks/useAgentSession.d.ts.map +1 -0
  57. package/dist/hooks/useAgentSession.js +128 -0
  58. package/dist/hooks/useAgentSession.js.map +1 -0
  59. package/dist/hooks/useChatStream.d.ts +59 -0
  60. package/dist/hooks/useChatStream.d.ts.map +1 -0
  61. package/dist/hooks/useChatStream.js +251 -0
  62. package/dist/hooks/useChatStream.js.map +1 -0
  63. package/dist/hooks/useToolCallStatus.d.ts +47 -0
  64. package/dist/hooks/useToolCallStatus.d.ts.map +1 -0
  65. package/dist/hooks/useToolCallStatus.js +57 -0
  66. package/dist/hooks/useToolCallStatus.js.map +1 -0
  67. package/dist/index.d.ts +12 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +12 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/tailwind-preset.d.ts +76 -0
  72. package/dist/tailwind-preset.d.ts.map +1 -0
  73. package/dist/tailwind-preset.js +104 -0
  74. package/dist/tailwind-preset.js.map +1 -0
  75. package/package.json +97 -0
  76. package/src/Introduction.mdx +71 -0
  77. package/src/components/ChatPanel.stories.tsx +173 -0
  78. package/src/components/ChatPanel.tsx +188 -0
  79. package/src/components/MessageList.stories.tsx +112 -0
  80. package/src/components/MessageList.test.tsx +57 -0
  81. package/src/components/MessageList.tsx +144 -0
  82. package/src/components/OrchestrationPlanCard.stories.tsx +98 -0
  83. package/src/components/OrchestrationPlanCard.test.tsx +34 -0
  84. package/src/components/OrchestrationPlanCard.tsx +159 -0
  85. package/src/components/SSEStreamView.stories.tsx +99 -0
  86. package/src/components/SSEStreamView.test.tsx +42 -0
  87. package/src/components/SSEStreamView.tsx +155 -0
  88. package/src/components/ToolCallRenderer.stories.tsx +152 -0
  89. package/src/components/ToolCallRenderer.test.tsx +93 -0
  90. package/src/components/ToolCallRenderer.tsx +213 -0
  91. package/src/components/cn.ts +10 -0
  92. package/src/components/index.ts +18 -0
  93. package/src/hooks/index.ts +21 -0
  94. package/src/hooks/useAgentSession.mdx +115 -0
  95. package/src/hooks/useAgentSession.test.ts +127 -0
  96. package/src/hooks/useAgentSession.ts +181 -0
  97. package/src/hooks/useChatStream.mdx +129 -0
  98. package/src/hooks/useChatStream.test.ts +233 -0
  99. package/src/hooks/useChatStream.ts +341 -0
  100. package/src/hooks/useToolCallStatus.mdx +102 -0
  101. package/src/hooks/useToolCallStatus.test.ts +99 -0
  102. package/src/hooks/useToolCallStatus.ts +99 -0
  103. package/src/index.ts +12 -0
  104. package/src/tailwind-preset.ts +109 -0
@@ -0,0 +1,155 @@
1
+ /**
2
+ * `SSEStreamView` — debug / power-user view that prints the raw SSE event log.
3
+ *
4
+ * Used in two production contexts today:
5
+ * 1. The "Trace" tab in `deeppath/apps/web` admin tooling.
6
+ * 2. The mockCflog log window in `deeppath-agent`, where it doubles as a
7
+ * dev console for the Electron app.
8
+ *
9
+ * The component is intentionally append-only; consumers feed it events via the
10
+ * `events` prop and it auto-scrolls to keep the latest in view.
11
+ */
12
+
13
+ import { useEffect, useMemo, useRef } from 'react';
14
+ import type { SSEEvent } from '@steerable/agent-protocol';
15
+ import { cn } from './cn.js';
16
+
17
+ export interface SSEStreamViewProps {
18
+ events: SSEEvent[];
19
+ className?: string;
20
+ /** Show full JSON payloads instead of a one-line summary. */
21
+ verbose?: boolean;
22
+ autoScroll?: boolean;
23
+ /**
24
+ * Optional filter; only events whose `type` is in the set are rendered.
25
+ * Useful for production debugging where you only care about, e.g.,
26
+ * `tool_call` + `tool_result` chains.
27
+ */
28
+ filterTypes?: SSEEvent['type'][];
29
+ emptyState?: React.ReactNode;
30
+ maxRows?: number;
31
+ }
32
+
33
+ const typeColor: Record<SSEEvent['type'], string> = {
34
+ content: 'text-agent-foreground',
35
+ error: 'text-agent-destructive',
36
+ agent: 'text-agent-accent',
37
+ orchestration: 'text-agent-tool-write',
38
+ 'loader-hint': 'text-agent-muted-foreground',
39
+ keepalive: 'text-agent-muted-foreground/60',
40
+ done: 'text-agent-tool-read',
41
+ budget_exhausted: 'text-agent-destructive',
42
+ tool_call: 'text-agent-tool-write',
43
+ tool_result: 'text-agent-tool-read',
44
+ };
45
+
46
+ function summarize(event: SSEEvent): string {
47
+ if (event.type === 'content') {
48
+ const c = typeof event.content === 'string' ? event.content : '';
49
+ return c.length > 80 ? c.slice(0, 80) + '…' : c;
50
+ }
51
+ if (event.type === 'error' || event.type === 'budget_exhausted') {
52
+ return event.message ?? '';
53
+ }
54
+ if (event.type === 'tool_call' || event.type === 'tool_result') {
55
+ const payload = event.payload as Record<string, unknown> | undefined;
56
+ return payload ? JSON.stringify(payload).slice(0, 120) : '';
57
+ }
58
+ if (event.type === 'loader-hint') {
59
+ return event.hint ?? '';
60
+ }
61
+ if (event.type === 'orchestration') {
62
+ return event.orchestrationGroupId ?? '';
63
+ }
64
+ if (event.type === 'agent') {
65
+ return JSON.stringify(event).slice(0, 120);
66
+ }
67
+ return '';
68
+ }
69
+
70
+ export function SSEStreamView(props: SSEStreamViewProps) {
71
+ const {
72
+ events,
73
+ className,
74
+ verbose = false,
75
+ autoScroll = true,
76
+ filterTypes,
77
+ emptyState,
78
+ maxRows = 1000,
79
+ } = props;
80
+ const containerRef = useRef<HTMLDivElement>(null);
81
+
82
+ const visible = useMemo(() => {
83
+ let list = filterTypes ? events.filter((e) => filterTypes.includes(e.type)) : events;
84
+ if (list.length > maxRows) {
85
+ list = list.slice(list.length - maxRows);
86
+ }
87
+ return list;
88
+ }, [events, filterTypes, maxRows]);
89
+
90
+ useEffect(() => {
91
+ if (!autoScroll) return;
92
+ const el = containerRef.current;
93
+ if (!el) return;
94
+ el.scrollTop = el.scrollHeight;
95
+ }, [autoScroll, visible.length]);
96
+
97
+ return (
98
+ <div
99
+ ref={containerRef}
100
+ className={cn(
101
+ 'h-full overflow-y-auto rounded-agent-md border border-agent-border bg-agent-canvas font-mono text-[11px] leading-relaxed',
102
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-agent-accent',
103
+ className,
104
+ )}
105
+ role="log"
106
+ aria-live="polite"
107
+ aria-label="SSE event log"
108
+ tabIndex={0}
109
+ >
110
+ {visible.length === 0 ? (
111
+ <div className="flex h-full items-center justify-center text-agent-muted-foreground">
112
+ {emptyState ?? 'No events yet.'}
113
+ </div>
114
+ ) : (
115
+ <ul className="divide-y divide-agent-border">
116
+ {visible.map((event, idx) => (
117
+ <li
118
+ key={`${idx}_${event.type}`}
119
+ className="flex flex-col gap-0.5 px-3 py-1.5"
120
+ >
121
+ <div className="flex items-center gap-2">
122
+ <span
123
+ className={cn(
124
+ 'shrink-0 font-semibold uppercase tracking-wide',
125
+ typeColor[event.type],
126
+ )}
127
+ >
128
+ {event.type}
129
+ </span>
130
+ {!verbose ? (
131
+ <span className="truncate text-agent-muted-foreground">
132
+ {summarize(event)}
133
+ </span>
134
+ ) : null}
135
+ </div>
136
+ {verbose ? (
137
+ <pre className="overflow-x-auto whitespace-pre-wrap break-words text-agent-muted-foreground">
138
+ {safeStringify(event)}
139
+ </pre>
140
+ ) : null}
141
+ </li>
142
+ ))}
143
+ </ul>
144
+ )}
145
+ </div>
146
+ );
147
+ }
148
+
149
+ function safeStringify(value: unknown): string {
150
+ try {
151
+ return JSON.stringify(value, null, 2);
152
+ } catch {
153
+ return String(value);
154
+ }
155
+ }
@@ -0,0 +1,152 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import type { ToolCall, ToolResult } from '@steerable/agent-protocol';
3
+ import { ToolCallRenderer } from './ToolCallRenderer.js';
4
+
5
+ const meta: Meta<typeof ToolCallRenderer> = {
6
+ title: 'Components/ToolCallRenderer',
7
+ component: ToolCallRenderer,
8
+ parameters: {
9
+ layout: 'centered',
10
+ actions: { argTypesRegex: '^on[A-Z].*' },
11
+ docs: {
12
+ description: {
13
+ component:
14
+ 'Single tool-call card. Computes its display state from `useToolCallStatus` (call + optional result + mode hint). Pass `mode="local"` and `onApprove` / `onReject` to render an approval prompt for local-execution tools.',
15
+ },
16
+ },
17
+ },
18
+ decorators: [
19
+ (Story) => (
20
+ <div className="w-[520px]">
21
+ <Story />
22
+ </div>
23
+ ),
24
+ ],
25
+ };
26
+
27
+ export default meta;
28
+ type Story = StoryObj<typeof ToolCallRenderer>;
29
+
30
+ const readCall: ToolCall = {
31
+ id: 'call_read_1',
32
+ name: 'web_search',
33
+ arguments: { query: 'tokyo vegetarian restaurants 2026' },
34
+ };
35
+
36
+ const writeCall: ToolCall = {
37
+ id: 'call_write_1',
38
+ name: 'create_calendar_event',
39
+ arguments: {
40
+ title: 'Team retro',
41
+ startTime: '2026-05-20T14:00:00Z',
42
+ durationMinutes: 60,
43
+ },
44
+ };
45
+
46
+ const destructiveCall: ToolCall = {
47
+ id: 'call_destructive_1',
48
+ name: 'shell_exec',
49
+ arguments: { command: 'rm -rf /tmp/build-cache' },
50
+ };
51
+
52
+ const localCall: ToolCall = {
53
+ id: 'call_local_1',
54
+ name: 'cflog_open_workspace',
55
+ arguments: { path: '/Users/me/projects/cflog/case-2024-001' },
56
+ };
57
+
58
+ const successResult: ToolResult = {
59
+ success: true,
60
+ message: 'Found 12 results.',
61
+ data: {
62
+ hits: [
63
+ { title: 'T\u2019s Tantan', url: 'https://example.com/1' },
64
+ { title: 'AIN SOPH. Soar', url: 'https://example.com/2' },
65
+ ],
66
+ },
67
+ };
68
+
69
+ const errorResult: ToolResult = {
70
+ success: false,
71
+ error: 'TimeRangeConflict: another event already occupies 14:00–15:00 on 2026-05-20',
72
+ };
73
+
74
+ export const ReadModePending: Story = {
75
+ args: {
76
+ call: readCall,
77
+ mode: 'read',
78
+ },
79
+ };
80
+
81
+ export const ReadModeSuccess: Story = {
82
+ args: {
83
+ call: readCall,
84
+ result: successResult,
85
+ mode: 'read',
86
+ defaultExpanded: true,
87
+ },
88
+ };
89
+
90
+ export const SafeWriteRunning: Story = {
91
+ args: {
92
+ call: writeCall,
93
+ mode: 'safe_write',
94
+ runningHint: true,
95
+ },
96
+ };
97
+
98
+ export const SafeWriteError: Story = {
99
+ args: {
100
+ call: writeCall,
101
+ result: errorResult,
102
+ mode: 'safe_write',
103
+ defaultExpanded: true,
104
+ },
105
+ };
106
+
107
+ export const DestructivePending: Story = {
108
+ args: {
109
+ call: destructiveCall,
110
+ mode: 'destructive',
111
+ defaultExpanded: true,
112
+ },
113
+ };
114
+
115
+ export const LocalAwaitingApproval: Story = {
116
+ args: {
117
+ call: localCall,
118
+ mode: 'local',
119
+ onApprove: () => {},
120
+ onReject: () => {},
121
+ },
122
+ };
123
+
124
+ export const CustomRenderers: Story = {
125
+ args: {
126
+ call: readCall,
127
+ result: successResult,
128
+ mode: 'read',
129
+ defaultExpanded: true,
130
+ renderArgs: (call) => (
131
+ <div className="text-xs">
132
+ <span className="font-medium text-agent-foreground">Query:</span>{' '}
133
+ <span className="text-agent-muted-foreground">
134
+ {String((call.arguments as { query?: string }).query ?? '')}
135
+ </span>
136
+ </div>
137
+ ),
138
+ renderResult: (result) => (
139
+ <ul className="space-y-1 text-xs text-agent-muted-foreground">
140
+ {((result.data as { hits?: Array<{ title: string; url: string }> } | undefined)?.hits ?? []).map(
141
+ (hit) => (
142
+ <li key={hit.url}>
143
+ <a className="text-agent-accent underline" href={hit.url}>
144
+ {hit.title}
145
+ </a>
146
+ </li>
147
+ ),
148
+ )}
149
+ </ul>
150
+ ),
151
+ },
152
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Render tests for `<ToolCallRenderer />`. We don't snapshot the whole DOM —
3
+ * Tailwind class strings shift any time we tweak tokens — but we assert the
4
+ * data-attributes (used by consumer styling overrides) and the visibility
5
+ * rules for the approval row + result subtree.
6
+ */
7
+
8
+ import { fireEvent, render, screen } from '@testing-library/react';
9
+ import { describe, expect, it, vi } from 'vitest';
10
+ import { ToolCallRenderer } from './ToolCallRenderer';
11
+
12
+ describe('ToolCallRenderer', () => {
13
+ it('renders pending state with collapsed args by default', () => {
14
+ render(
15
+ <ToolCallRenderer
16
+ call={{ id: 'c1', name: 'get_weather', arguments: { city: 'SF' } }}
17
+ />,
18
+ );
19
+ const card = screen.getByText('get_weather').closest('[data-status]');
20
+ expect(card?.getAttribute('data-status')).toBe('pending');
21
+ expect(card?.getAttribute('data-mode')).toBe('read');
22
+ // Args body is hidden until expanded.
23
+ expect(screen.queryByText(/"city"/)).toBeNull();
24
+ });
25
+
26
+ it('expands args + result on click', () => {
27
+ render(
28
+ <ToolCallRenderer
29
+ call={{ id: 'c1', name: 'create_event', arguments: { title: 'meeting' } }}
30
+ result={{ success: true, data: { id: 'e1' } }}
31
+ />,
32
+ );
33
+ fireEvent.click(screen.getByRole('button'));
34
+ expect(screen.getByText(/"title": "meeting"/)).toBeTruthy();
35
+ expect(screen.getByText(/"id": "e1"/)).toBeTruthy();
36
+ });
37
+
38
+ it('renders done status when result.success=true', () => {
39
+ render(
40
+ <ToolCallRenderer
41
+ call={{ id: 'c1', name: 'create_event', arguments: {} }}
42
+ result={{ success: true }}
43
+ />,
44
+ );
45
+ const card = screen.getByText('create_event').closest('[data-status]');
46
+ expect(card?.getAttribute('data-status')).toBe('done');
47
+ });
48
+
49
+ it('renders error status and the error text in the result subtree when expanded', () => {
50
+ render(
51
+ <ToolCallRenderer
52
+ call={{ id: 'c1', name: 'create_event', arguments: {} }}
53
+ result={{ success: false, error: 'missing field' }}
54
+ />,
55
+ );
56
+ const card = screen.getByText('create_event').closest('[data-status]');
57
+ expect(card?.getAttribute('data-status')).toBe('error');
58
+ fireEvent.click(screen.getByRole('button'));
59
+ expect(screen.getByText('missing field')).toBeTruthy();
60
+ });
61
+
62
+ it('shows approve / reject buttons for local tools when handlers are provided', () => {
63
+ const onApprove = vi.fn();
64
+ const onReject = vi.fn();
65
+ render(
66
+ <ToolCallRenderer
67
+ call={{ id: 'c1', name: 'local_run_script', arguments: { cmd: 'ls' } }}
68
+ onApprove={onApprove}
69
+ onReject={onReject}
70
+ />,
71
+ );
72
+ fireEvent.click(screen.getByRole('button', { name: 'Approve' }));
73
+ fireEvent.click(screen.getByRole('button', { name: 'Reject' }));
74
+ expect(onApprove).toHaveBeenCalledWith({
75
+ id: 'c1',
76
+ name: 'local_run_script',
77
+ arguments: { cmd: 'ls' },
78
+ });
79
+ expect(onReject).toHaveBeenCalledTimes(1);
80
+ });
81
+
82
+ it('does not show approval row once a result has arrived', () => {
83
+ const onApprove = vi.fn();
84
+ render(
85
+ <ToolCallRenderer
86
+ call={{ id: 'c1', name: 'local_run_script', arguments: { cmd: 'ls' } }}
87
+ result={{ success: true }}
88
+ onApprove={onApprove}
89
+ />,
90
+ );
91
+ expect(screen.queryByRole('button', { name: 'Approve' })).toBeNull();
92
+ });
93
+ });
@@ -0,0 +1,213 @@
1
+ /**
2
+ * `ToolCallRenderer` — display a single tool call + (optional) tool result.
3
+ *
4
+ * Renders three visual rows depending on lifecycle / mode:
5
+ *
6
+ * ┌──────────────────────────────────────────────────┐
7
+ * │ [icon] tool_name [mode-pill] [time] │
8
+ * │ args (collapsed by default) │
9
+ * │ ── result ── │
10
+ * │ message + data (collapsed by default) │
11
+ * └──────────────────────────────────────────────────┘
12
+ *
13
+ * Headless body: callers can override the args / result subtree with
14
+ * `renderArgs` / `renderResult` if they need richer formatting (diffs, code
15
+ * blocks, structured cards). Default body uses pretty-printed JSON.
16
+ */
17
+
18
+ import { useState } from 'react';
19
+ import type { ToolCall, ToolResult } from '@steerable/agent-protocol';
20
+ import { useToolCallStatus, type ToolCallMode } from '../hooks/useToolCallStatus.js';
21
+ import { cn } from './cn.js';
22
+
23
+ export interface ToolCallRendererProps {
24
+ call: ToolCall;
25
+ result?: ToolResult;
26
+ /**
27
+ * Optional explicit mode override. Use when the framework's `decide_tool_mode`
28
+ * has already classified the tool (the local `ToolRouter` ships this value
29
+ * alongside the call in production setups).
30
+ */
31
+ mode?: ToolCallMode;
32
+ runningHint?: boolean;
33
+ /**
34
+ * Approval handler. Must be supplied when rendering a `local`-mode call you
35
+ * want the user to confirm before execution. If omitted, the renderer falls
36
+ * back to the read-only "awaiting result" treatment.
37
+ */
38
+ onApprove?: (call: ToolCall) => void;
39
+ onReject?: (call: ToolCall) => void;
40
+ className?: string;
41
+ renderArgs?: (call: ToolCall) => React.ReactNode;
42
+ renderResult?: (result: ToolResult) => React.ReactNode;
43
+ defaultExpanded?: boolean;
44
+ }
45
+
46
+ const modeBadgeClass: Record<ToolCallMode, string> = {
47
+ read: 'bg-agent-tool-read/15 text-agent-tool-read border-agent-tool-read/40',
48
+ safe_write:
49
+ 'bg-agent-tool-write/15 text-agent-tool-write border-agent-tool-write/40',
50
+ destructive:
51
+ 'bg-agent-tool-destructive/15 text-agent-tool-destructive border-agent-tool-destructive/40',
52
+ local:
53
+ 'bg-agent-accent/15 text-agent-accent border-agent-accent/40',
54
+ external:
55
+ 'bg-agent-muted text-agent-muted-foreground border-agent-border',
56
+ ui: 'bg-agent-muted text-agent-muted-foreground border-agent-border',
57
+ synthetic:
58
+ 'bg-agent-muted text-agent-muted-foreground border-agent-border',
59
+ unknown:
60
+ 'bg-agent-muted text-agent-muted-foreground border-agent-border',
61
+ };
62
+
63
+ const statusDotClass: Record<string, string> = {
64
+ pending: 'bg-agent-muted-foreground animate-pulse',
65
+ running: 'bg-agent-accent animate-pulse',
66
+ done: 'bg-agent-tool-read',
67
+ error: 'bg-agent-destructive',
68
+ };
69
+
70
+ function defaultArgsRenderer(call: ToolCall) {
71
+ return (
72
+ <pre className="whitespace-pre-wrap break-words text-xs leading-relaxed text-agent-muted-foreground">
73
+ {safeStringify(call.arguments)}
74
+ </pre>
75
+ );
76
+ }
77
+
78
+ function defaultResultRenderer(result: ToolResult) {
79
+ const lines: React.ReactNode[] = [];
80
+ if (result.message) {
81
+ lines.push(
82
+ <div key="msg" className="text-xs text-agent-foreground">
83
+ {result.message}
84
+ </div>,
85
+ );
86
+ }
87
+ if (result.error) {
88
+ lines.push(
89
+ <div key="err" className="text-xs text-agent-destructive">
90
+ {result.error}
91
+ </div>,
92
+ );
93
+ }
94
+ if (result.data) {
95
+ lines.push(
96
+ <pre
97
+ key="data"
98
+ className="mt-1 whitespace-pre-wrap break-words text-xs leading-relaxed text-agent-muted-foreground"
99
+ >
100
+ {safeStringify(result.data)}
101
+ </pre>,
102
+ );
103
+ }
104
+ if (lines.length === 0) {
105
+ lines.push(
106
+ <div key="ok" className="text-xs text-agent-muted-foreground">
107
+ ok
108
+ </div>,
109
+ );
110
+ }
111
+ return <div className="space-y-1">{lines}</div>;
112
+ }
113
+
114
+ function safeStringify(value: unknown): string {
115
+ try {
116
+ return JSON.stringify(value, null, 2);
117
+ } catch {
118
+ return String(value);
119
+ }
120
+ }
121
+
122
+ export function ToolCallRenderer(props: ToolCallRendererProps) {
123
+ const {
124
+ call,
125
+ result,
126
+ mode,
127
+ runningHint,
128
+ onApprove,
129
+ onReject,
130
+ className,
131
+ renderArgs = defaultArgsRenderer,
132
+ renderResult = defaultResultRenderer,
133
+ defaultExpanded = false,
134
+ } = props;
135
+ const [expanded, setExpanded] = useState(defaultExpanded);
136
+
137
+ const status = useToolCallStatus({ call, result, mode, runningHint });
138
+
139
+ return (
140
+ <div
141
+ data-status={status.status}
142
+ data-mode={status.mode}
143
+ className={cn(
144
+ 'rounded-agent-md border bg-agent-canvas px-3 py-2 text-agent-foreground',
145
+ status.isDestructive ? 'border-agent-destructive/40' : 'border-agent-border',
146
+ className,
147
+ )}
148
+ >
149
+ <button
150
+ type="button"
151
+ className="flex w-full items-center justify-between gap-2 text-left"
152
+ onClick={() => setExpanded((v) => !v)}
153
+ aria-expanded={expanded}
154
+ >
155
+ <span className="flex min-w-0 items-center gap-2">
156
+ <span
157
+ className={cn('h-2 w-2 rounded-full', statusDotClass[status.status])}
158
+ aria-hidden
159
+ />
160
+ <span className="truncate font-mono text-xs font-medium">
161
+ {call.name}
162
+ </span>
163
+ <span
164
+ className={cn(
165
+ 'shrink-0 rounded-full border px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
166
+ modeBadgeClass[status.mode],
167
+ )}
168
+ >
169
+ {status.mode}
170
+ </span>
171
+ </span>
172
+ <span className="shrink-0 text-[10px] uppercase tracking-wide text-agent-muted-foreground">
173
+ {status.status}
174
+ </span>
175
+ </button>
176
+ {status.requiresApproval && !result ? (
177
+ <div className="mt-2 flex items-center gap-2">
178
+ <span className="text-xs text-agent-muted-foreground">
179
+ Local execution requires approval.
180
+ </span>
181
+ {onApprove ? (
182
+ <button
183
+ type="button"
184
+ className="rounded-agent-sm bg-agent-accent px-2 py-1 text-xs font-medium text-agent-accent-foreground"
185
+ onClick={() => onApprove(call)}
186
+ >
187
+ Approve
188
+ </button>
189
+ ) : null}
190
+ {onReject ? (
191
+ <button
192
+ type="button"
193
+ className="rounded-agent-sm border border-agent-border px-2 py-1 text-xs font-medium text-agent-foreground"
194
+ onClick={() => onReject(call)}
195
+ >
196
+ Reject
197
+ </button>
198
+ ) : null}
199
+ </div>
200
+ ) : null}
201
+ {expanded ? (
202
+ <div className="mt-2 space-y-2 border-t border-agent-border pt-2">
203
+ {renderArgs(call)}
204
+ {result ? (
205
+ <div className="border-t border-dashed border-agent-border pt-2">
206
+ {renderResult(result)}
207
+ </div>
208
+ ) : null}
209
+ </div>
210
+ ) : null}
211
+ </div>
212
+ );
213
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Tiny class-name combiner. We intentionally don't depend on `clsx` /
3
+ * `tailwind-merge` to keep the package's runtime footprint near zero — those
4
+ * niceties belong in the consumer app.
5
+ */
6
+ export function cn(
7
+ ...values: Array<string | false | null | undefined>
8
+ ): string {
9
+ return values.filter(Boolean).join(' ');
10
+ }
@@ -0,0 +1,18 @@
1
+ export { MessageList } from './MessageList.js';
2
+ export type { MessageListProps, MessageRendererProps } from './MessageList.js';
3
+
4
+ export { ChatPanel } from './ChatPanel.js';
5
+ export type { ChatPanelProps } from './ChatPanel.js';
6
+
7
+ export { OrchestrationPlanCard } from './OrchestrationPlanCard.js';
8
+ export type {
9
+ OrchestrationPlanCardProps,
10
+ OrchestrationStep,
11
+ OrchestrationStepStatus,
12
+ } from './OrchestrationPlanCard.js';
13
+
14
+ export { ToolCallRenderer } from './ToolCallRenderer.js';
15
+ export type { ToolCallRendererProps } from './ToolCallRenderer.js';
16
+
17
+ export { SSEStreamView } from './SSEStreamView.js';
18
+ export type { SSEStreamViewProps } from './SSEStreamView.js';
@@ -0,0 +1,21 @@
1
+ export { useChatStream } from './useChatStream.js';
2
+ export type {
3
+ UseChatStreamOptions,
4
+ UseChatStreamReturn,
5
+ ChatStreamTransport,
6
+ ChatStreamSendInput,
7
+ } from './useChatStream.js';
8
+
9
+ export { useToolCallStatus } from './useToolCallStatus.js';
10
+ export type {
11
+ ToolCallStatus,
12
+ UseToolCallStatusOptions,
13
+ UseToolCallStatusReturn,
14
+ } from './useToolCallStatus.js';
15
+
16
+ export { useAgentSession } from './useAgentSession.js';
17
+ export type {
18
+ UseAgentSessionOptions,
19
+ UseAgentSessionReturn,
20
+ AgentSessionTransport,
21
+ } from './useAgentSession.js';