@slipstream-ui/ai 0.4.0 → 0.6.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.
- package/dist/index.d.mts +51 -4
- package/dist/index.d.ts +51 -4
- package/dist/index.js +244 -113
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +246 -115
- package/dist/index.mjs.map +1 -1
- package/dist/manifests/attachment.manifest.json +37 -8
- package/dist/manifests/branch.manifest.json +3 -1
- package/dist/manifests/chain-thought.manifest.json +1 -1
- package/dist/manifests/code-block.manifest.json +3 -1
- package/dist/manifests/command-palette.manifest.json +59 -4
- package/dist/manifests/confirmation.manifest.json +3 -1
- package/dist/manifests/conversation.manifest.json +1 -1
- package/dist/manifests/inline-citation.manifest.json +3 -1
- package/dist/manifests/message-actions.manifest.json +61 -10
- package/dist/manifests/message.manifest.json +13 -1
- package/dist/manifests/prompt-input.manifest.json +2 -2
- package/dist/manifests/reasoning.manifest.json +19 -1
- package/dist/manifests/response.manifest.json +3 -1
- package/dist/manifests/source.manifest.json +2 -2
- package/dist/manifests/streaming-indicator.manifest.json +1 -1
- package/dist/manifests/suggestions.manifest.json +2 -2
- package/dist/manifests/tool-call.manifest.json +7 -1
- package/package.json +10 -10
- package/src/components/code-block/CodeBlock.tsx +91 -5
- package/src/components/command-palette/CommandPalette.test.tsx +42 -0
- package/src/components/command-palette/CommandPalette.tsx +82 -8
- package/src/components/confirmation/Confirmation.tsx +1 -1
- package/src/components/conversation/Conversation.tsx +6 -1
- package/src/components/message/Message.test.tsx +15 -0
- package/src/components/message/Message.tsx +9 -1
- package/src/components/reasoning/Reasoning.test.tsx +15 -0
- package/src/components/reasoning/Reasoning.tsx +20 -7
- package/src/components/tool-call/ToolCall.tsx +24 -8
- package/src/manifests/attachment.manifest.json +37 -8
- package/src/manifests/branch.manifest.json +3 -1
- package/src/manifests/chain-thought.manifest.json +1 -1
- package/src/manifests/code-block.manifest.json +3 -1
- package/src/manifests/command-palette.manifest.json +59 -4
- package/src/manifests/confirmation.manifest.json +3 -1
- package/src/manifests/conversation.manifest.json +1 -1
- package/src/manifests/inline-citation.manifest.json +3 -1
- package/src/manifests/message-actions.manifest.json +61 -10
- package/src/manifests/message.manifest.json +13 -1
- package/src/manifests/prompt-input.manifest.json +2 -2
- package/src/manifests/reasoning.manifest.json +19 -1
- package/src/manifests/response.manifest.json +3 -1
- package/src/manifests/source.manifest.json +2 -2
- package/src/manifests/streaming-indicator.manifest.json +1 -1
- package/src/manifests/suggestions.manifest.json +2 -2
- package/src/manifests/tool-call.manifest.json +7 -1
|
@@ -72,3 +72,45 @@ describe('CommandPalette', () => {
|
|
|
72
72
|
expect(await axe(container)).toHaveNoViolations();
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
describe('CommandPalette async commands', () => {
|
|
78
|
+
it('loads commands from loadCommands', async () => {
|
|
79
|
+
const user = userEvent.setup();
|
|
80
|
+
const loadCommands = vi.fn(async () => [{ id: 'remote', label: 'Remote result' }]);
|
|
81
|
+
render(
|
|
82
|
+
<CommandPalette
|
|
83
|
+
open
|
|
84
|
+
onOpenChange={() => undefined}
|
|
85
|
+
commands={[]}
|
|
86
|
+
onSelect={() => undefined}
|
|
87
|
+
loadCommands={loadCommands}
|
|
88
|
+
debounceMs={1}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
expect(await screen.findByText('Remote result')).toBeInTheDocument();
|
|
92
|
+
expect(loadCommands).toHaveBeenCalled();
|
|
93
|
+
expect(user).toBeTruthy();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('localized labels', () => {
|
|
98
|
+
it('uses the dialog, list and empty label overrides', async () => {
|
|
99
|
+
const user = userEvent.setup();
|
|
100
|
+
render(
|
|
101
|
+
<CommandPalette
|
|
102
|
+
open
|
|
103
|
+
onOpenChange={() => undefined}
|
|
104
|
+
commands={commands}
|
|
105
|
+
onSelect={() => undefined}
|
|
106
|
+
dialogLabel="Palette de commandes"
|
|
107
|
+
listLabel="Commandes FR"
|
|
108
|
+
emptyText="Aucune commande"
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
expect(screen.getByRole('combobox', { name: 'Palette de commandes' })).toBeInTheDocument();
|
|
112
|
+
expect(screen.getByRole('listbox', { name: 'Commandes FR' })).toBeInTheDocument();
|
|
113
|
+
await user.type(screen.getByRole('combobox'), 'zzz');
|
|
114
|
+
expect(screen.getByText('Aucune commande')).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -11,14 +11,33 @@ export type Command = {
|
|
|
11
11
|
/** Extra lexical search terms. */
|
|
12
12
|
keywords?: string[];
|
|
13
13
|
shortcut?: string;
|
|
14
|
+
/** Optional leading icon (rendered by the default row or renderCommand). */
|
|
15
|
+
icon?: React.ReactNode;
|
|
16
|
+
/** Optional secondary line in the default row. */
|
|
17
|
+
description?: string;
|
|
14
18
|
};
|
|
15
19
|
|
|
16
20
|
export type CommandPaletteProps = {
|
|
17
21
|
open: boolean;
|
|
18
22
|
onOpenChange: (open: boolean) => void;
|
|
23
|
+
/** Static command set (ignored for filtering when loadCommands is set). */
|
|
19
24
|
commands: Command[];
|
|
20
25
|
onSelect: (command: Command) => void;
|
|
26
|
+
/** Async source; called with the current query (debounced). */
|
|
27
|
+
loadCommands?: (query: string) => Promise<Command[]>;
|
|
28
|
+
/** Debounce for loadCommands, in ms. Default 200. */
|
|
29
|
+
debounceMs?: number;
|
|
30
|
+
/** Custom row renderer (icons, type badges, metadata). */
|
|
31
|
+
renderCommand?: (command: Command, state: { active: boolean }) => React.ReactNode;
|
|
32
|
+
/** Shown while async commands load. */
|
|
33
|
+
loadingText?: string;
|
|
21
34
|
placeholder?: string;
|
|
35
|
+
/** Accessible label for the combobox. Default 'Command palette'. */
|
|
36
|
+
dialogLabel?: string;
|
|
37
|
+
/** Accessible label for the listbox. Default 'Commands'. */
|
|
38
|
+
listLabel?: string;
|
|
39
|
+
/** Shown when no command matches. Default 'No matching commands.'. */
|
|
40
|
+
emptyText?: string;
|
|
22
41
|
};
|
|
23
42
|
|
|
24
43
|
/**
|
|
@@ -31,22 +50,52 @@ export function CommandPalette({
|
|
|
31
50
|
onOpenChange,
|
|
32
51
|
commands,
|
|
33
52
|
onSelect,
|
|
34
|
-
|
|
53
|
+
loadCommands,
|
|
54
|
+
debounceMs = 200,
|
|
55
|
+
renderCommand,
|
|
56
|
+
loadingText = 'Searching…',
|
|
57
|
+
placeholder = 'Type a command.',
|
|
58
|
+
dialogLabel = 'Command palette',
|
|
59
|
+
listLabel = 'Commands',
|
|
60
|
+
emptyText = 'No matching commands.',
|
|
35
61
|
}: CommandPaletteProps) {
|
|
36
62
|
const [query, setQuery] = React.useState('');
|
|
37
63
|
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
64
|
+
const [asyncCommands, setAsyncCommands] = React.useState<Command[]>([]);
|
|
65
|
+
const [loading, setLoading] = React.useState(false);
|
|
66
|
+
const requestId = React.useRef(0);
|
|
67
|
+
const asyncSignature = loadCommands ? String(loadCommands.length) : '';
|
|
38
68
|
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
|
39
69
|
const listboxId = React.useId();
|
|
40
70
|
const optionId = (index: number) => `${listboxId}-option-${index}`;
|
|
41
71
|
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
if (!open || !loadCommands) return;
|
|
74
|
+
const id = ++requestId.current;
|
|
75
|
+
setLoading(true);
|
|
76
|
+
const timer = setTimeout(async () => {
|
|
77
|
+
try {
|
|
78
|
+
const result = await loadCommands(query);
|
|
79
|
+
if (requestId.current === id) setAsyncCommands(result);
|
|
80
|
+
} catch {
|
|
81
|
+
if (requestId.current === id) setAsyncCommands([]);
|
|
82
|
+
} finally {
|
|
83
|
+
if (requestId.current === id) setLoading(false);
|
|
84
|
+
}
|
|
85
|
+
}, debounceMs);
|
|
86
|
+
return () => clearTimeout(timer);
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
+
}, [open, query, debounceMs, asyncSignature]);
|
|
89
|
+
|
|
42
90
|
const filtered = React.useMemo(() => {
|
|
91
|
+
if (loadCommands) return asyncCommands;
|
|
43
92
|
const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
44
93
|
if (terms.length === 0) return commands;
|
|
45
94
|
return commands.filter((command) => {
|
|
46
95
|
const haystack = `${command.label} ${command.section ?? ''} ${(command.keywords ?? []).join(' ')}`.toLowerCase();
|
|
47
96
|
return terms.every((term) => haystack.includes(term));
|
|
48
97
|
});
|
|
49
|
-
}, [commands, query]);
|
|
98
|
+
}, [commands, query, asyncCommands, loadCommands]);
|
|
50
99
|
|
|
51
100
|
React.useEffect(() => {
|
|
52
101
|
if (open) {
|
|
@@ -103,7 +152,7 @@ export function CommandPalette({
|
|
|
103
152
|
aria-controls={listboxId}
|
|
104
153
|
aria-activedescendant={filtered.length > 0 ? optionId(activeIndex) : undefined}
|
|
105
154
|
aria-autocomplete="list"
|
|
106
|
-
aria-label=
|
|
155
|
+
aria-label={dialogLabel}
|
|
107
156
|
placeholder={placeholder}
|
|
108
157
|
value={query}
|
|
109
158
|
onChange={(event) => {
|
|
@@ -113,10 +162,21 @@ export function CommandPalette({
|
|
|
113
162
|
onKeyDown={onKeyDown}
|
|
114
163
|
className="w-full border-b border-border bg-transparent px-4 py-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring placeholder:text-muted-foreground"
|
|
115
164
|
/>
|
|
116
|
-
<ul
|
|
117
|
-
{
|
|
165
|
+
<ul
|
|
166
|
+
id={listboxId}
|
|
167
|
+
role="listbox"
|
|
168
|
+
aria-label={listLabel}
|
|
169
|
+
aria-busy={loading || undefined}
|
|
170
|
+
className="max-h-72 overflow-y-auto py-1"
|
|
171
|
+
>
|
|
172
|
+
{loading && filtered.length === 0 && (
|
|
118
173
|
<li role="presentation" className="px-4 py-3 text-sm text-muted-foreground">
|
|
119
|
-
|
|
174
|
+
{loadingText}
|
|
175
|
+
</li>
|
|
176
|
+
)}
|
|
177
|
+
{!loading && filtered.length === 0 && (
|
|
178
|
+
<li role="presentation" className="px-4 py-3 text-sm text-muted-foreground">
|
|
179
|
+
{emptyText}
|
|
120
180
|
</li>
|
|
121
181
|
)}
|
|
122
182
|
{filtered.map((command, index) => {
|
|
@@ -143,8 +203,22 @@ export function CommandPalette({
|
|
|
143
203
|
index === activeIndex ? 'bg-primary/10 text-foreground' : 'text-muted-foreground'
|
|
144
204
|
)}
|
|
145
205
|
>
|
|
146
|
-
|
|
147
|
-
|
|
206
|
+
{renderCommand ? (
|
|
207
|
+
renderCommand(command, { active: index === activeIndex })
|
|
208
|
+
) : (
|
|
209
|
+
<>
|
|
210
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
211
|
+
{command.icon && <span className="shrink-0 [&_svg]:size-4">{command.icon}</span>}
|
|
212
|
+
<span className="min-w-0">
|
|
213
|
+
<span className="block truncate">{command.label}</span>
|
|
214
|
+
{command.description && (
|
|
215
|
+
<span className="block text-xs text-muted-foreground">{command.description}</span>
|
|
216
|
+
)}
|
|
217
|
+
</span>
|
|
218
|
+
</span>
|
|
219
|
+
{command.shortcut && <span className="font-mono text-xs">{command.shortcut}</span>}
|
|
220
|
+
</>
|
|
221
|
+
)}
|
|
148
222
|
</li>
|
|
149
223
|
</React.Fragment>
|
|
150
224
|
);
|
|
@@ -45,7 +45,7 @@ export type ConfirmationProps = {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
const statusChipClasses: Record<ConfirmationStatus, string> = {
|
|
48
|
-
pending: 'bg-warning/10 text-warning border-warning/20',
|
|
48
|
+
pending: 'bg-warning/10 text-warning-ink border-warning/20',
|
|
49
49
|
approved: 'bg-success/10 text-success border-success/20',
|
|
50
50
|
rejected: 'bg-destructive/10 text-destructive border-destructive/20',
|
|
51
51
|
};
|
|
@@ -192,7 +192,12 @@ const Conversation = React.forwardRef<HTMLDivElement, ConversationProps>(
|
|
|
192
192
|
|
|
193
193
|
return (
|
|
194
194
|
<div ref={ref} className={cn('relative h-full w-full', className)} {...props}>
|
|
195
|
-
<div
|
|
195
|
+
<div
|
|
196
|
+
ref={scrollRef}
|
|
197
|
+
tabIndex={0}
|
|
198
|
+
aria-label="Conversation transcript"
|
|
199
|
+
className="flex flex-col gap-4 p-4 overflow-y-auto h-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
200
|
+
>
|
|
196
201
|
{(hasMoreHistory || isLoadingMore) && (
|
|
197
202
|
<button
|
|
198
203
|
type="button"
|
|
@@ -78,3 +78,18 @@ describe('Message', () => {
|
|
|
78
78
|
expect(await axe(container)).toHaveNoViolations();
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
describe('timestamp formatting', () => {
|
|
83
|
+
const timestamp = new Date('2026-11-07T15:04:05.000Z');
|
|
84
|
+
const timedMessage: MessageType = { ...assistantMessage, timestamp };
|
|
85
|
+
|
|
86
|
+
it('uses a custom formatTime', () => {
|
|
87
|
+
render(<Message message={timedMessage} formatTime={(date) => `T:${date.toISOString()}`} />);
|
|
88
|
+
expect(screen.getByText('T:2026-11-07T15:04:05.000Z')).toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('defaults to the runtime locale formatter', () => {
|
|
92
|
+
render(<Message message={timedMessage} />);
|
|
93
|
+
expect(screen.getByText(timestamp.toLocaleTimeString())).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -15,6 +15,10 @@ export type MessageProps = {
|
|
|
15
15
|
userLabel?: string;
|
|
16
16
|
/** Display name for the assistant role; override for localization. */
|
|
17
17
|
assistantLabel?: string;
|
|
18
|
+
/** Formats the message timestamp; defaults to the runtime's toLocaleTimeString(). */
|
|
19
|
+
formatTime?: (date: Date) => string;
|
|
20
|
+
/** BCP-47 locale used by the default timestamp formatter. */
|
|
21
|
+
locale?: string;
|
|
18
22
|
/**
|
|
19
23
|
* Custom content renderer, e.g. for markdown/code blocks.
|
|
20
24
|
* When provided, it fully replaces the default plain-text rendering.
|
|
@@ -52,6 +56,8 @@ const Message = React.forwardRef<HTMLDivElement, MessageProps>(
|
|
|
52
56
|
avatarFallback,
|
|
53
57
|
userLabel = 'You',
|
|
54
58
|
assistantLabel = 'Assistant',
|
|
59
|
+
formatTime,
|
|
60
|
+
locale,
|
|
55
61
|
renderContent,
|
|
56
62
|
isStreaming = false,
|
|
57
63
|
error,
|
|
@@ -121,7 +127,9 @@ const Message = React.forwardRef<HTMLDivElement, MessageProps>(
|
|
|
121
127
|
<span className="text-sm font-medium">{displayLabel}</span>
|
|
122
128
|
{message.timestamp && (
|
|
123
129
|
<span className="text-xs text-muted-foreground">
|
|
124
|
-
{
|
|
130
|
+
{formatTime
|
|
131
|
+
? formatTime(new Date(message.timestamp))
|
|
132
|
+
: new Date(message.timestamp).toLocaleTimeString(locale)}
|
|
125
133
|
</span>
|
|
126
134
|
)}
|
|
127
135
|
</div>
|
|
@@ -56,3 +56,18 @@ describe('Reasoning', () => {
|
|
|
56
56
|
expect(await axe(container)).toHaveNoViolations();
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
+
|
|
60
|
+
describe('timestamp formatting', () => {
|
|
61
|
+
const timestamp = new Date('2026-11-07T15:04:05.000Z');
|
|
62
|
+
const timedSteps = [{ id: 's1', content: 'Parse the question', timestamp }];
|
|
63
|
+
|
|
64
|
+
it('uses a custom formatTime', () => {
|
|
65
|
+
render(<Reasoning steps={timedSteps} defaultExpanded formatTime={(date) => `T:${date.toISOString()}`} />);
|
|
66
|
+
expect(screen.getByText('T:2026-11-07T15:04:05.000Z')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('defaults to the runtime locale formatter', () => {
|
|
70
|
+
render(<Reasoning steps={timedSteps} defaultExpanded />);
|
|
71
|
+
expect(screen.getByText(timestamp.toLocaleTimeString())).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -13,10 +13,21 @@ export type ReasoningProps = {
|
|
|
13
13
|
duration?: number | string;
|
|
14
14
|
/** Marks reasoning as still arriving: animated header state. */
|
|
15
15
|
isStreaming?: boolean;
|
|
16
|
+
/** Formats step timestamps; defaults to the runtime's toLocaleTimeString(). */
|
|
17
|
+
formatTime?: (date: Date) => string;
|
|
18
|
+
/** BCP-47 locale used by the default timestamp formatter. */
|
|
19
|
+
locale?: string;
|
|
20
|
+
/** Localized header, count and step labels. */
|
|
21
|
+
labels?: {
|
|
22
|
+
title?: string;
|
|
23
|
+
count?: (count: number) => string;
|
|
24
|
+
thinking?: string;
|
|
25
|
+
step?: (index: number) => string;
|
|
26
|
+
};
|
|
16
27
|
};
|
|
17
28
|
|
|
18
29
|
const Reasoning = React.forwardRef<HTMLDivElement, ReasoningProps>(
|
|
19
|
-
({ steps, className, collapsible = true, defaultExpanded = false, duration, isStreaming = false, ...props }, ref) => {
|
|
30
|
+
({ steps, className, collapsible = true, defaultExpanded = false, duration, isStreaming = false, labels, formatTime, locale, ...props }, ref) => {
|
|
20
31
|
const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);
|
|
21
32
|
const contentId = React.useId();
|
|
22
33
|
|
|
@@ -25,10 +36,10 @@ const Reasoning = React.forwardRef<HTMLDivElement, ReasoningProps>(
|
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
const headerText = [
|
|
28
|
-
'Reasoning',
|
|
29
|
-
`(${steps.length} ${steps.length === 1 ? 'step' : 'steps'})`,
|
|
39
|
+
labels?.title ?? 'Reasoning',
|
|
40
|
+
labels?.count ? labels.count(steps.length) : `(${steps.length} ${steps.length === 1 ? 'step' : 'steps'})`,
|
|
30
41
|
typeof duration === 'number' ? `· ${duration}s` : duration ? `· ${duration}` : '',
|
|
31
|
-
isStreaming ?
|
|
42
|
+
isStreaming ? `· ${labels?.thinking ?? 'thinking…'}` : '',
|
|
32
43
|
]
|
|
33
44
|
.filter(Boolean)
|
|
34
45
|
.join(' ');
|
|
@@ -42,7 +53,7 @@ const Reasoning = React.forwardRef<HTMLDivElement, ReasoningProps>(
|
|
|
42
53
|
aria-expanded={isExpanded}
|
|
43
54
|
aria-controls={contentId}
|
|
44
55
|
>
|
|
45
|
-
<span className={cn('transition-transform', isExpanded ? 'rotate-90' : '')}
|
|
56
|
+
<span className={cn('transition-transform', isExpanded ? 'rotate-90' : '')}>?</span>
|
|
46
57
|
<span>{headerText}</span>
|
|
47
58
|
{isStreaming && (
|
|
48
59
|
<span className="flex gap-0.5" aria-hidden="true">
|
|
@@ -64,11 +75,13 @@ const Reasoning = React.forwardRef<HTMLDivElement, ReasoningProps>(
|
|
|
64
75
|
<div key={step.id} className="flex gap-3">
|
|
65
76
|
<div className="w-3 h-3 rounded-full bg-primary/20 border-2 border-primary flex-shrink-0 mt-1" />
|
|
66
77
|
<div className="text-sm flex-1">
|
|
67
|
-
<div className="font-medium text-foreground mb-1">Step {index + 1}</div>
|
|
78
|
+
<div className="font-medium text-foreground mb-1">{labels?.step?.(index + 1) ?? `Step ${index + 1}`}</div>
|
|
68
79
|
<div className="text-muted-foreground">{step.content}</div>
|
|
69
80
|
{step.timestamp && (
|
|
70
81
|
<div className="text-xs text-muted-foreground mt-1">
|
|
71
|
-
{
|
|
82
|
+
{formatTime
|
|
83
|
+
? formatTime(new Date(step.timestamp))
|
|
84
|
+
: new Date(step.timestamp).toLocaleTimeString(locale)}
|
|
72
85
|
</div>
|
|
73
86
|
)}
|
|
74
87
|
</div>
|
|
@@ -8,16 +8,26 @@ export type ToolCallProps = {
|
|
|
8
8
|
toolCall: ToolCallType;
|
|
9
9
|
className?: string;
|
|
10
10
|
showResult?: boolean;
|
|
11
|
+
/** Localized status and section labels. */
|
|
12
|
+
labels?: {
|
|
13
|
+
pending?: string;
|
|
14
|
+
running?: string;
|
|
15
|
+
completed?: string;
|
|
16
|
+
failed?: string;
|
|
17
|
+
arguments?: string;
|
|
18
|
+
result?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
};
|
|
11
21
|
};
|
|
12
22
|
|
|
13
23
|
const statusChipClasses: Record<ToolCallType['status'], string> = {
|
|
14
|
-
pending: 'bg-warning/10 text-warning border-warning/20',
|
|
15
|
-
running: 'bg-info/10 text-info border-info/20',
|
|
24
|
+
pending: 'bg-warning/10 text-warning-ink border-warning/20',
|
|
25
|
+
running: 'bg-info/10 text-info-ink border-info/20',
|
|
16
26
|
completed: 'bg-success/10 text-success border-success/20',
|
|
17
27
|
failed: 'bg-destructive/10 text-destructive border-destructive/20',
|
|
18
28
|
};
|
|
19
29
|
|
|
20
|
-
const
|
|
30
|
+
const defaultStatusLabels: Record<ToolCallType['status'], string> = {
|
|
21
31
|
pending: 'Pending',
|
|
22
32
|
running: 'Running',
|
|
23
33
|
completed: 'Completed',
|
|
@@ -29,8 +39,7 @@ const statusLabels: Record<ToolCallType['status'], string> = {
|
|
|
29
39
|
* arguments, optional result, and an error block on failure.
|
|
30
40
|
*/
|
|
31
41
|
const ToolCall = React.forwardRef<HTMLDivElement, ToolCallProps>(
|
|
32
|
-
({ toolCall, className, showResult = true, ...props }, ref) => {
|
|
33
|
-
// Serialize once per tool-call change instead of on every render.
|
|
42
|
+
({ toolCall, className, showResult = true, labels, ...props }, ref) => {
|
|
34
43
|
const serializedArguments = React.useMemo(
|
|
35
44
|
() => JSON.stringify(toolCall.arguments, null, 2),
|
|
36
45
|
[toolCall.arguments]
|
|
@@ -42,6 +51,13 @@ const ToolCall = React.forwardRef<HTMLDivElement, ToolCallProps>(
|
|
|
42
51
|
: JSON.stringify(toolCall.result, null, 2);
|
|
43
52
|
}, [toolCall.result]);
|
|
44
53
|
|
|
54
|
+
const statusLabels: Record<ToolCallType['status'], string> = {
|
|
55
|
+
pending: labels?.pending ?? defaultStatusLabels.pending,
|
|
56
|
+
running: labels?.running ?? defaultStatusLabels.running,
|
|
57
|
+
completed: labels?.completed ?? defaultStatusLabels.completed,
|
|
58
|
+
failed: labels?.failed ?? defaultStatusLabels.failed,
|
|
59
|
+
};
|
|
60
|
+
|
|
45
61
|
return (
|
|
46
62
|
<div
|
|
47
63
|
ref={ref}
|
|
@@ -68,7 +84,7 @@ const ToolCall = React.forwardRef<HTMLDivElement, ToolCallProps>(
|
|
|
68
84
|
|
|
69
85
|
<div className="space-y-2">
|
|
70
86
|
<div>
|
|
71
|
-
<div className="text-xs font-medium text-muted-foreground mb-1">Arguments</div>
|
|
87
|
+
<div className="text-xs font-medium text-muted-foreground mb-1">{labels?.arguments ?? 'Arguments'}</div>
|
|
72
88
|
<pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
|
|
73
89
|
<code>{serializedArguments}</code>
|
|
74
90
|
</pre>
|
|
@@ -76,7 +92,7 @@ const ToolCall = React.forwardRef<HTMLDivElement, ToolCallProps>(
|
|
|
76
92
|
|
|
77
93
|
{showResult && serializedResult !== undefined && (
|
|
78
94
|
<div>
|
|
79
|
-
<div className="text-xs font-medium text-muted-foreground mb-1">Result</div>
|
|
95
|
+
<div className="text-xs font-medium text-muted-foreground mb-1">{labels?.result ?? 'Result'}</div>
|
|
80
96
|
<pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
|
|
81
97
|
<code>{serializedResult}</code>
|
|
82
98
|
</pre>
|
|
@@ -85,7 +101,7 @@ const ToolCall = React.forwardRef<HTMLDivElement, ToolCallProps>(
|
|
|
85
101
|
|
|
86
102
|
{toolCall.status === 'failed' && toolCall.error && (
|
|
87
103
|
<div role="alert">
|
|
88
|
-
<div className="text-xs font-medium text-destructive mb-1">Error</div>
|
|
104
|
+
<div className="text-xs font-medium text-destructive mb-1">{labels?.error ?? 'Error'}</div>
|
|
89
105
|
<pre className="text-xs bg-destructive/10 text-destructive p-2 rounded overflow-x-auto whitespace-pre-wrap">
|
|
90
106
|
<code>{toolCall.error}</code>
|
|
91
107
|
</pre>
|
|
@@ -5,9 +5,24 @@
|
|
|
5
5
|
"description": "File chips for prompts and messages. `Attachment` renders one file; `AttachmentList` renders a wrapping row. Display-only: upload and file-system logic stay in the consumer.",
|
|
6
6
|
"designIntent": "Show what files are part of a conversation turn (name, size, type, thumbnail, upload status) without coupling the library to any upload implementation.",
|
|
7
7
|
"props": [
|
|
8
|
-
{
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
{
|
|
9
|
+
"name": "attachment",
|
|
10
|
+
"type": "AttachmentItem",
|
|
11
|
+
"required": true,
|
|
12
|
+
"description": "File descriptor: id, name, size, type, previewUrl, status."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "attachments",
|
|
16
|
+
"type": "AttachmentItem[]",
|
|
17
|
+
"required": true,
|
|
18
|
+
"description": "Files to display (AttachmentList)."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "onRemove",
|
|
22
|
+
"type": "(attachment) => void",
|
|
23
|
+
"required": false,
|
|
24
|
+
"description": "Remove callback; omit to hide remove controls."
|
|
25
|
+
}
|
|
11
26
|
],
|
|
12
27
|
"examples": [
|
|
13
28
|
{
|
|
@@ -20,14 +35,28 @@
|
|
|
20
35
|
}
|
|
21
36
|
],
|
|
22
37
|
"antiPatterns": [
|
|
23
|
-
{
|
|
38
|
+
{
|
|
39
|
+
"title": "Do not use Attachment for drag-and-drop targets",
|
|
40
|
+
"explanation": "The chip displays an existing item; build dropzones in the consumer and render AttachmentList once items exist."
|
|
41
|
+
}
|
|
24
42
|
],
|
|
25
43
|
"accessibility": [
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
{
|
|
45
|
+
"feature": "Remove control",
|
|
46
|
+
"implementation": "Icon-only remove button has an aria-label including the file name."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"feature": "Upload status",
|
|
50
|
+
"implementation": "Uploading state is announced via role=status."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"feature": "Decorative thumbnails",
|
|
54
|
+
"implementation": "Preview images are aria-hidden; the file name carries the meaning."
|
|
55
|
+
}
|
|
29
56
|
],
|
|
57
|
+
"sourceDir": "attachment",
|
|
58
|
+
"exportName": "Attachment",
|
|
30
59
|
"package": "@slipstream-ui/ai",
|
|
31
60
|
"importPath": "@slipstream-ui/ai",
|
|
32
|
-
"version": "0.0
|
|
61
|
+
"version": "0.6.0"
|
|
33
62
|
}
|
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
"implementation": "Icon-labeled button with aria-label that flips to Copied."
|
|
45
45
|
}
|
|
46
46
|
],
|
|
47
|
+
"sourceDir": "code-block",
|
|
48
|
+
"exportName": "CodeBlock",
|
|
47
49
|
"package": "@slipstream-ui/ai",
|
|
48
50
|
"importPath": "@slipstream-ui/ai",
|
|
49
|
-
"version": "0.0
|
|
51
|
+
"version": "0.6.0"
|
|
50
52
|
}
|
|
@@ -22,7 +22,13 @@
|
|
|
22
22
|
"type": "Command[]",
|
|
23
23
|
"required": true,
|
|
24
24
|
"description": "Available commands: { id, label, section?, keywords?, shortcut? }",
|
|
25
|
-
"examples": [
|
|
25
|
+
"examples": [
|
|
26
|
+
{
|
|
27
|
+
"id": "go-docs",
|
|
28
|
+
"label": "Open documentation",
|
|
29
|
+
"section": "Navigation"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
26
32
|
},
|
|
27
33
|
{
|
|
28
34
|
"name": "onSelect",
|
|
@@ -33,9 +39,55 @@
|
|
|
33
39
|
{
|
|
34
40
|
"name": "placeholder",
|
|
35
41
|
"type": "string",
|
|
36
|
-
"defaultValue": "Type a command
|
|
42
|
+
"defaultValue": "Type a command.",
|
|
37
43
|
"required": false,
|
|
38
44
|
"description": "Input placeholder text"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "loadCommands",
|
|
48
|
+
"type": "(query: string) => Promise<Command[]>",
|
|
49
|
+
"required": false,
|
|
50
|
+
"description": "Async command source; debounced and stale-response guarded."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "debounceMs",
|
|
54
|
+
"type": "number",
|
|
55
|
+
"required": false,
|
|
56
|
+
"defaultValue": "200",
|
|
57
|
+
"description": "Debounce for loadCommands in ms."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "renderCommand",
|
|
61
|
+
"type": "(command, state: { active: boolean }) => ReactNode",
|
|
62
|
+
"required": false,
|
|
63
|
+
"description": "Custom row renderer (icons, type badges)."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "loadingText",
|
|
67
|
+
"type": "string",
|
|
68
|
+
"required": false,
|
|
69
|
+
"description": "Shown while async commands load."
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "dialogLabel",
|
|
73
|
+
"type": "string",
|
|
74
|
+
"required": false,
|
|
75
|
+
"defaultValue": "Command palette",
|
|
76
|
+
"description": "Accessible label for the combobox."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "listLabel",
|
|
80
|
+
"type": "string",
|
|
81
|
+
"required": false,
|
|
82
|
+
"defaultValue": "Commands",
|
|
83
|
+
"description": "Accessible label for the command listbox."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "emptyText",
|
|
87
|
+
"type": "string",
|
|
88
|
+
"required": false,
|
|
89
|
+
"defaultValue": "No matching commands.",
|
|
90
|
+
"description": "Shown when no command matches the query."
|
|
39
91
|
}
|
|
40
92
|
],
|
|
41
93
|
"examples": [
|
|
@@ -71,10 +123,13 @@
|
|
|
71
123
|
"relationship": "CommandPalette switches modes/actions; PromptInput captures free-form chat text."
|
|
72
124
|
}
|
|
73
125
|
],
|
|
74
|
-
"relatedComponents": [
|
|
126
|
+
"relatedComponents": [
|
|
127
|
+
"PromptInput",
|
|
128
|
+
"Dialog"
|
|
129
|
+
],
|
|
75
130
|
"package": "@slipstream-ui/ai",
|
|
76
131
|
"importPath": "@slipstream-ui/ai",
|
|
77
|
-
"version": "0.0
|
|
132
|
+
"version": "0.6.0",
|
|
78
133
|
"sourceDir": "command-palette",
|
|
79
134
|
"exportName": "CommandPalette"
|
|
80
135
|
}
|
|
@@ -137,7 +137,9 @@
|
|
|
137
137
|
"implementation": "Status chip text announces Approved/Rejected."
|
|
138
138
|
}
|
|
139
139
|
],
|
|
140
|
+
"sourceDir": "confirmation",
|
|
141
|
+
"exportName": "Confirmation",
|
|
140
142
|
"package": "@slipstream-ui/ai",
|
|
141
143
|
"importPath": "@slipstream-ui/ai",
|
|
142
|
-
"version": "0.0
|
|
144
|
+
"version": "0.6.0"
|
|
143
145
|
}
|
|
@@ -40,7 +40,9 @@
|
|
|
40
40
|
"implementation": "Card has role=tooltip linked via aria-describedby."
|
|
41
41
|
}
|
|
42
42
|
],
|
|
43
|
+
"sourceDir": "inline-citation",
|
|
44
|
+
"exportName": "InlineCitation",
|
|
43
45
|
"package": "@slipstream-ui/ai",
|
|
44
46
|
"importPath": "@slipstream-ui/ai",
|
|
45
|
-
"version": "0.0
|
|
47
|
+
"version": "0.6.0"
|
|
46
48
|
}
|