@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.
Files changed (51) hide show
  1. package/dist/index.d.mts +51 -4
  2. package/dist/index.d.ts +51 -4
  3. package/dist/index.js +244 -113
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +246 -115
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/manifests/attachment.manifest.json +37 -8
  8. package/dist/manifests/branch.manifest.json +3 -1
  9. package/dist/manifests/chain-thought.manifest.json +1 -1
  10. package/dist/manifests/code-block.manifest.json +3 -1
  11. package/dist/manifests/command-palette.manifest.json +59 -4
  12. package/dist/manifests/confirmation.manifest.json +3 -1
  13. package/dist/manifests/conversation.manifest.json +1 -1
  14. package/dist/manifests/inline-citation.manifest.json +3 -1
  15. package/dist/manifests/message-actions.manifest.json +61 -10
  16. package/dist/manifests/message.manifest.json +13 -1
  17. package/dist/manifests/prompt-input.manifest.json +2 -2
  18. package/dist/manifests/reasoning.manifest.json +19 -1
  19. package/dist/manifests/response.manifest.json +3 -1
  20. package/dist/manifests/source.manifest.json +2 -2
  21. package/dist/manifests/streaming-indicator.manifest.json +1 -1
  22. package/dist/manifests/suggestions.manifest.json +2 -2
  23. package/dist/manifests/tool-call.manifest.json +7 -1
  24. package/package.json +10 -10
  25. package/src/components/code-block/CodeBlock.tsx +91 -5
  26. package/src/components/command-palette/CommandPalette.test.tsx +42 -0
  27. package/src/components/command-palette/CommandPalette.tsx +82 -8
  28. package/src/components/confirmation/Confirmation.tsx +1 -1
  29. package/src/components/conversation/Conversation.tsx +6 -1
  30. package/src/components/message/Message.test.tsx +15 -0
  31. package/src/components/message/Message.tsx +9 -1
  32. package/src/components/reasoning/Reasoning.test.tsx +15 -0
  33. package/src/components/reasoning/Reasoning.tsx +20 -7
  34. package/src/components/tool-call/ToolCall.tsx +24 -8
  35. package/src/manifests/attachment.manifest.json +37 -8
  36. package/src/manifests/branch.manifest.json +3 -1
  37. package/src/manifests/chain-thought.manifest.json +1 -1
  38. package/src/manifests/code-block.manifest.json +3 -1
  39. package/src/manifests/command-palette.manifest.json +59 -4
  40. package/src/manifests/confirmation.manifest.json +3 -1
  41. package/src/manifests/conversation.manifest.json +1 -1
  42. package/src/manifests/inline-citation.manifest.json +3 -1
  43. package/src/manifests/message-actions.manifest.json +61 -10
  44. package/src/manifests/message.manifest.json +13 -1
  45. package/src/manifests/prompt-input.manifest.json +2 -2
  46. package/src/manifests/reasoning.manifest.json +19 -1
  47. package/src/manifests/response.manifest.json +3 -1
  48. package/src/manifests/source.manifest.json +2 -2
  49. package/src/manifests/streaming-indicator.manifest.json +1 -1
  50. package/src/manifests/suggestions.manifest.json +2 -2
  51. 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
- placeholder = 'Type a command…',
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="Command palette"
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 id={listboxId} role="listbox" aria-label="Commands" className="max-h-72 overflow-y-auto py-1">
117
- {filtered.length === 0 && (
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
- No matching commands.
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
- <span>{command.label}</span>
147
- {command.shortcut && <span className="font-mono text-xs">{command.shortcut}</span>}
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 ref={scrollRef} className="flex flex-col gap-4 p-4 overflow-y-auto h-full">
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
- {new Date(message.timestamp).toLocaleTimeString()}
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 ? '· thinking…' : '',
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' : '')}>▶</span>
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
- {new Date(step.timestamp).toLocaleTimeString()}
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 statusLabels: Record<ToolCallType['status'], string> = {
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
- { "name": "attachment", "type": "AttachmentItem", "required": true, "description": "File descriptor: id, name, size, type, previewUrl, status." },
9
- { "name": "attachments", "type": "AttachmentItem[]", "required": true, "description": "Files to display (AttachmentList)." },
10
- { "name": "onRemove", "type": "(attachment) => void", "required": false, "description": "Remove callback; omit to hide remove controls." }
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
- { "title": "Do not use Attachment for drag-and-drop targets", "explanation": "The chip displays an existing item; build dropzones in the consumer and render AttachmentList once items exist." }
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
- { "feature": "Remove control", "implementation": "Icon-only remove button has an aria-label including the file name." },
27
- { "feature": "Upload status", "implementation": "Uploading state is announced via role=status." },
28
- { "feature": "Decorative thumbnails", "implementation": "Preview images are aria-hidden; the file name carries the meaning." }
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.1"
61
+ "version": "0.6.0"
33
62
  }
@@ -59,7 +59,9 @@
59
59
  "implementation": "N/M indicator uses aria-live=polite."
60
60
  }
61
61
  ],
62
+ "sourceDir": "branch",
63
+ "exportName": "Branch",
62
64
  "package": "@slipstream-ui/ai",
63
65
  "importPath": "@slipstream-ui/ai",
64
- "version": "0.0.1"
66
+ "version": "0.6.0"
65
67
  }
@@ -33,7 +33,7 @@
33
33
  ],
34
34
  "package": "@slipstream-ui/ai",
35
35
  "importPath": "@slipstream-ui/ai",
36
- "version": "0.0.1",
36
+ "version": "0.6.0",
37
37
  "sourceDir": "chainthought",
38
38
  "exportName": "ChainThought"
39
39
  }
@@ -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.1"
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": [{ "id": "go-docs", "label": "Open documentation", "section": "Navigation" }]
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": ["PromptInput", "Dialog"],
126
+ "relatedComponents": [
127
+ "PromptInput",
128
+ "Dialog"
129
+ ],
75
130
  "package": "@slipstream-ui/ai",
76
131
  "importPath": "@slipstream-ui/ai",
77
- "version": "0.0.1",
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.1"
144
+ "version": "0.6.0"
143
145
  }
@@ -284,7 +284,7 @@
284
284
  ],
285
285
  "package": "@slipstream-ui/ai",
286
286
  "importPath": "@slipstream-ui/ai",
287
- "version": "0.0.1",
287
+ "version": "0.6.0",
288
288
  "sourceDir": "conversation",
289
289
  "exportName": "Conversation"
290
290
  }
@@ -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.1"
47
+ "version": "0.6.0"
46
48
  }