@slipstream-ui/ai 0.3.0 → 0.5.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 +33 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.js +236 -110
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +238 -112
- package/dist/index.mjs.map +1 -1
- package/dist/manifests/attachment.manifest.json +2 -0
- package/dist/manifests/branch.manifest.json +2 -0
- package/dist/manifests/code-block.manifest.json +2 -0
- package/dist/manifests/command-palette.manifest.json +37 -3
- package/dist/manifests/confirmation.manifest.json +2 -0
- package/dist/manifests/inline-citation.manifest.json +2 -0
- package/dist/manifests/message-actions.manifest.json +3 -0
- package/dist/manifests/reasoning.manifest.json +6 -0
- package/dist/manifests/response.manifest.json +2 -0
- package/dist/manifests/tool-call.manifest.json +6 -0
- package/package.json +3 -3
- package/src/components/code-block/CodeBlock.tsx +91 -5
- package/src/components/command-palette/CommandPalette.test.tsx +21 -0
- package/src/components/command-palette/CommandPalette.tsx +71 -6
- package/src/components/confirmation/Confirmation.tsx +1 -1
- package/src/components/conversation/Conversation.tsx +6 -1
- package/src/components/reasoning/Reasoning.tsx +13 -6
- package/src/components/tool-call/ToolCall.tsx +24 -8
- package/src/manifests/attachment.manifest.json +2 -0
- package/src/manifests/branch.manifest.json +2 -0
- package/src/manifests/code-block.manifest.json +2 -0
- package/src/manifests/command-palette.manifest.json +37 -3
- package/src/manifests/confirmation.manifest.json +2 -0
- package/src/manifests/inline-citation.manifest.json +2 -0
- package/src/manifests/message-actions.manifest.json +3 -0
- package/src/manifests/reasoning.manifest.json +6 -0
- package/src/manifests/response.manifest.json +2 -0
- package/src/manifests/tool-call.manifest.json +6 -0
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
{ "feature": "Upload status", "implementation": "Uploading state is announced via role=status." },
|
|
28
28
|
{ "feature": "Decorative thumbnails", "implementation": "Preview images are aria-hidden; the file name carries the meaning." }
|
|
29
29
|
],
|
|
30
|
+
"sourceDir": "attachment",
|
|
31
|
+
"exportName": "Attachment",
|
|
30
32
|
"package": "@slipstream-ui/ai",
|
|
31
33
|
"importPath": "@slipstream-ui/ai",
|
|
32
34
|
"version": "0.0.1"
|
|
@@ -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,34 @@
|
|
|
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."
|
|
39
70
|
}
|
|
40
71
|
],
|
|
41
72
|
"examples": [
|
|
@@ -71,7 +102,10 @@
|
|
|
71
102
|
"relationship": "CommandPalette switches modes/actions; PromptInput captures free-form chat text."
|
|
72
103
|
}
|
|
73
104
|
],
|
|
74
|
-
"relatedComponents": [
|
|
105
|
+
"relatedComponents": [
|
|
106
|
+
"PromptInput",
|
|
107
|
+
"Dialog"
|
|
108
|
+
],
|
|
75
109
|
"package": "@slipstream-ui/ai",
|
|
76
110
|
"importPath": "@slipstream-ui/ai",
|
|
77
111
|
"version": "0.0.1",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
{ "name": "onCopy", "type": "(content: string) => void", "required": false, "description": "Called after copy with the copied text." },
|
|
10
10
|
{ "name": "onFeedback", "type": "(rating: 'up' | 'down' | undefined) => void", "required": false, "description": "Rating callback; selecting the active rating again passes undefined." },
|
|
11
11
|
{ "name": "onRegenerate", "type": "() => void", "required": false, "description": "Regeneration request; omit to hide the action." },
|
|
12
|
+
{ "name": "autohide", "type": "'always' | 'on-hover' | 'not-last'", "required": false, "defaultValue": "always", "enum": ["always", "on-hover", "not-last"], "description": "Controls visibility: always visible, visible on hover/focus, or hidden until hover for non-latest messages." },
|
|
12
13
|
{ "name": "labels", "type": "{ copy?, copied?, thumbUp?, thumbDown?, regenerate? }", "required": false, "description": "Overrides for all visible labels; enables localization." }
|
|
13
14
|
],
|
|
14
15
|
"examples": [
|
|
@@ -29,6 +30,8 @@
|
|
|
29
30
|
{ "feature": "Toggle state", "implementation": "Rating buttons expose aria-pressed so screen readers announce selection." },
|
|
30
31
|
{ "feature": "Icon buttons", "implementation": "All icon-only controls have accessible labels; labels are localizable." }
|
|
31
32
|
],
|
|
33
|
+
"sourceDir": "message-actions",
|
|
34
|
+
"exportName": "MessageActions",
|
|
32
35
|
"package": "@slipstream-ui/ai",
|
|
33
36
|
"importPath": "@slipstream-ui/ai",
|
|
34
37
|
"version": "0.0.1"
|
|
@@ -53,6 +53,12 @@
|
|
|
53
53
|
"type": "boolean",
|
|
54
54
|
"required": false,
|
|
55
55
|
"description": "Shows a streaming indicator while reasoning is in progress"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "labels",
|
|
59
|
+
"type": "{ title?: string; count?: (count: number) => string; thinking?: string; step?: (index: number) => string }",
|
|
60
|
+
"required": false,
|
|
61
|
+
"description": "Localized header, count and step labels."
|
|
56
62
|
}
|
|
57
63
|
],
|
|
58
64
|
"examples": [
|
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
"type": "string",
|
|
31
31
|
"required": false,
|
|
32
32
|
"description": "Additional CSS classes to apply"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "labels",
|
|
36
|
+
"type": "{ pending?: string; running?: string; completed?: string; failed?: string; arguments?: string; result?: string; error?: string }",
|
|
37
|
+
"required": false,
|
|
38
|
+
"description": "Localized status and section labels."
|
|
33
39
|
}
|
|
34
40
|
],
|
|
35
41
|
"examples": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slipstream-ui/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/michaelmcshinsky/slipstream-ui.git",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"react-markdown": "^10.1.0",
|
|
33
33
|
"remark-gfm": "^4.0.1",
|
|
34
34
|
"tailwind-merge": "^2.2.0",
|
|
35
|
-
"@slipstream-ui/core": "0.
|
|
35
|
+
"@slipstream-ui/core": "0.5.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@testing-library/jest-dom": "^7.0.1",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@types/react": "^18.2.0",
|
|
42
42
|
"eslint": "^8.56.0",
|
|
43
43
|
"jest-axe": "^9.0.0",
|
|
44
|
-
"jsdom": "^
|
|
44
|
+
"jsdom": "^30.1.3",
|
|
45
45
|
"tsup": "^8.0.0",
|
|
46
46
|
"typescript": "^5.3.3",
|
|
47
47
|
"vitest": "^5.0.5",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
|
-
import { Highlight,
|
|
4
|
+
import { Highlight, type PrismTheme } from 'prism-react-renderer';
|
|
5
5
|
import { cn } from '@slipstream-ui/core';
|
|
6
6
|
|
|
7
7
|
export type CodeBlockProps = {
|
|
@@ -29,12 +29,95 @@ function useIsDark(): boolean {
|
|
|
29
29
|
return dark;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* AA-safe syntax palettes.
|
|
34
|
+
*
|
|
35
|
+
* prism-react-renderer's stock themes put comments (#999988) and constants
|
|
36
|
+
* (#36acaa) at ~2.7-2.9:1 on the code surface. Every colour below clears
|
|
37
|
+
* 4.5:1 against its background (white light / near-black dark).
|
|
38
|
+
*/
|
|
39
|
+
const LIGHT_CODE_THEME: PrismTheme = {
|
|
40
|
+
plain: { color: '#1f2328', backgroundColor: 'transparent' },
|
|
41
|
+
styles: [
|
|
42
|
+
{ types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: '#57606a' } },
|
|
43
|
+
{ types: ['punctuation', 'operator'], style: { color: '#24292f' } },
|
|
44
|
+
{ types: ['namespace'], style: { color: '#6f42c1' } },
|
|
45
|
+
{ types: ['tag', 'deleted', 'keyword', 'atrule', 'important'], style: { color: '#b31d28' } },
|
|
46
|
+
{ types: ['attr-name', 'variable'], style: { color: '#953800' } },
|
|
47
|
+
{ types: ['string', 'char', 'attr-value', 'inserted', 'regex'], style: { color: '#0a5c2f' } },
|
|
48
|
+
{
|
|
49
|
+
types: [
|
|
50
|
+
'function',
|
|
51
|
+
'class-name',
|
|
52
|
+
'number',
|
|
53
|
+
'boolean',
|
|
54
|
+
'constant',
|
|
55
|
+
'symbol',
|
|
56
|
+
'property',
|
|
57
|
+
'selector',
|
|
58
|
+
'builtin',
|
|
59
|
+
'entity',
|
|
60
|
+
'url',
|
|
61
|
+
],
|
|
62
|
+
style: { color: '#0550ae' },
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const DARK_CODE_THEME: PrismTheme = {
|
|
68
|
+
plain: { color: '#e6edf3', backgroundColor: 'transparent' },
|
|
69
|
+
styles: [
|
|
70
|
+
{ types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: '#8b949e' } },
|
|
71
|
+
{ types: ['punctuation', 'operator'], style: { color: '#c9d1d9' } },
|
|
72
|
+
{ types: ['namespace'], style: { color: '#d2a8ff' } },
|
|
73
|
+
{ types: ['tag', 'deleted', 'keyword', 'atrule', 'important'], style: { color: '#ff7b72' } },
|
|
74
|
+
{ types: ['attr-name', 'variable'], style: { color: '#ffa657' } },
|
|
75
|
+
{ types: ['string', 'char', 'attr-value', 'inserted', 'regex'], style: { color: '#7ee787' } },
|
|
76
|
+
{
|
|
77
|
+
types: [
|
|
78
|
+
'function',
|
|
79
|
+
'class-name',
|
|
80
|
+
'number',
|
|
81
|
+
'boolean',
|
|
82
|
+
'constant',
|
|
83
|
+
'symbol',
|
|
84
|
+
'property',
|
|
85
|
+
'selector',
|
|
86
|
+
'builtin',
|
|
87
|
+
'entity',
|
|
88
|
+
'url',
|
|
89
|
+
],
|
|
90
|
+
style: { color: '#79c0ff' },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
|
|
32
95
|
const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
|
33
96
|
({ code, language = 'tsx', showCopyButton = true, className, ...props }, ref) => {
|
|
34
97
|
const [copied, setCopied] = React.useState(false);
|
|
35
98
|
const isDark = useIsDark();
|
|
36
99
|
const trimmedCode = code.replace(/\n$/, '');
|
|
37
100
|
|
|
101
|
+
// Horizontally scrollable code must be reachable by keyboard (WCAG 2.1.1).
|
|
102
|
+
// Only make it a tab stop when it actually overflows, so short snippets
|
|
103
|
+
// do not add noise to the tab order.
|
|
104
|
+
const preRef = React.useRef<HTMLPreElement>(null);
|
|
105
|
+
const [scrollable, setScrollable] = React.useState(false);
|
|
106
|
+
React.useEffect(() => {
|
|
107
|
+
const element = preRef.current;
|
|
108
|
+
if (!element) return;
|
|
109
|
+
const update = () => setScrollable(element.scrollWidth > element.clientWidth + 1);
|
|
110
|
+
update();
|
|
111
|
+
// jsdom (tests) has no ResizeObserver; window resize is the fallback.
|
|
112
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
113
|
+
window.addEventListener('resize', update);
|
|
114
|
+
return () => window.removeEventListener('resize', update);
|
|
115
|
+
}
|
|
116
|
+
const observer = new ResizeObserver(update);
|
|
117
|
+
observer.observe(element);
|
|
118
|
+
return () => observer.disconnect();
|
|
119
|
+
}, [trimmedCode, language]);
|
|
120
|
+
|
|
38
121
|
const handleCopy = async () => {
|
|
39
122
|
try {
|
|
40
123
|
await navigator.clipboard.writeText(trimmedCode);
|
|
@@ -50,8 +133,8 @@ const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
|
|
50
133
|
<div
|
|
51
134
|
ref={ref}
|
|
52
135
|
className={cn(
|
|
53
|
-
// bg-background (not muted):
|
|
54
|
-
// surface
|
|
136
|
+
// bg-background (not muted): the light syntax palette is tuned for
|
|
137
|
+
// a white surface; on gray the token contrast drops below AA.
|
|
55
138
|
'relative group rounded-md border border-border bg-background overflow-hidden',
|
|
56
139
|
className
|
|
57
140
|
)}
|
|
@@ -70,10 +153,13 @@ const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
|
|
70
153
|
</button>
|
|
71
154
|
)}
|
|
72
155
|
</div>
|
|
73
|
-
<Highlight theme={isDark ?
|
|
156
|
+
<Highlight theme={isDark ? DARK_CODE_THEME : LIGHT_CODE_THEME} code={trimmedCode} language={language}>
|
|
74
157
|
{({ style, tokens, getLineProps, getTokenProps }) => (
|
|
75
158
|
<pre
|
|
76
|
-
|
|
159
|
+
ref={preRef}
|
|
160
|
+
tabIndex={scrollable ? 0 : undefined}
|
|
161
|
+
aria-label={scrollable ? `${language} code, scrollable` : undefined}
|
|
162
|
+
className="overflow-x-auto p-4 text-xs leading-relaxed m-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
77
163
|
style={{ ...style, background: 'transparent' }}
|
|
78
164
|
>
|
|
79
165
|
{tokens.map((line, i) => (
|
|
@@ -72,3 +72,24 @@ 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
|
+
});
|
|
@@ -11,13 +11,26 @@ 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;
|
|
22
35
|
};
|
|
23
36
|
|
|
@@ -31,22 +44,49 @@ export function CommandPalette({
|
|
|
31
44
|
onOpenChange,
|
|
32
45
|
commands,
|
|
33
46
|
onSelect,
|
|
34
|
-
|
|
47
|
+
loadCommands,
|
|
48
|
+
debounceMs = 200,
|
|
49
|
+
renderCommand,
|
|
50
|
+
loadingText = 'Searching…',
|
|
51
|
+
placeholder = 'Type a command.',
|
|
35
52
|
}: CommandPaletteProps) {
|
|
36
53
|
const [query, setQuery] = React.useState('');
|
|
37
54
|
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
55
|
+
const [asyncCommands, setAsyncCommands] = React.useState<Command[]>([]);
|
|
56
|
+
const [loading, setLoading] = React.useState(false);
|
|
57
|
+
const requestId = React.useRef(0);
|
|
58
|
+
const asyncSignature = loadCommands ? String(loadCommands.length) : '';
|
|
38
59
|
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
|
39
60
|
const listboxId = React.useId();
|
|
40
61
|
const optionId = (index: number) => `${listboxId}-option-${index}`;
|
|
41
62
|
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (!open || !loadCommands) return;
|
|
65
|
+
const id = ++requestId.current;
|
|
66
|
+
setLoading(true);
|
|
67
|
+
const timer = setTimeout(async () => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await loadCommands(query);
|
|
70
|
+
if (requestId.current === id) setAsyncCommands(result);
|
|
71
|
+
} catch {
|
|
72
|
+
if (requestId.current === id) setAsyncCommands([]);
|
|
73
|
+
} finally {
|
|
74
|
+
if (requestId.current === id) setLoading(false);
|
|
75
|
+
}
|
|
76
|
+
}, debounceMs);
|
|
77
|
+
return () => clearTimeout(timer);
|
|
78
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
|
+
}, [open, query, debounceMs, asyncSignature]);
|
|
80
|
+
|
|
42
81
|
const filtered = React.useMemo(() => {
|
|
82
|
+
if (loadCommands) return asyncCommands;
|
|
43
83
|
const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
44
84
|
if (terms.length === 0) return commands;
|
|
45
85
|
return commands.filter((command) => {
|
|
46
86
|
const haystack = `${command.label} ${command.section ?? ''} ${(command.keywords ?? []).join(' ')}`.toLowerCase();
|
|
47
87
|
return terms.every((term) => haystack.includes(term));
|
|
48
88
|
});
|
|
49
|
-
}, [commands, query]);
|
|
89
|
+
}, [commands, query, asyncCommands, loadCommands]);
|
|
50
90
|
|
|
51
91
|
React.useEffect(() => {
|
|
52
92
|
if (open) {
|
|
@@ -113,8 +153,19 @@ export function CommandPalette({
|
|
|
113
153
|
onKeyDown={onKeyDown}
|
|
114
154
|
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
155
|
/>
|
|
116
|
-
<ul
|
|
117
|
-
{
|
|
156
|
+
<ul
|
|
157
|
+
id={listboxId}
|
|
158
|
+
role="listbox"
|
|
159
|
+
aria-label="Commands"
|
|
160
|
+
aria-busy={loading || undefined}
|
|
161
|
+
className="max-h-72 overflow-y-auto py-1"
|
|
162
|
+
>
|
|
163
|
+
{loading && filtered.length === 0 && (
|
|
164
|
+
<li role="presentation" className="px-4 py-3 text-sm text-muted-foreground">
|
|
165
|
+
{loadingText}
|
|
166
|
+
</li>
|
|
167
|
+
)}
|
|
168
|
+
{!loading && filtered.length === 0 && (
|
|
118
169
|
<li role="presentation" className="px-4 py-3 text-sm text-muted-foreground">
|
|
119
170
|
No matching commands.
|
|
120
171
|
</li>
|
|
@@ -143,8 +194,22 @@ export function CommandPalette({
|
|
|
143
194
|
index === activeIndex ? 'bg-primary/10 text-foreground' : 'text-muted-foreground'
|
|
144
195
|
)}
|
|
145
196
|
>
|
|
146
|
-
|
|
147
|
-
|
|
197
|
+
{renderCommand ? (
|
|
198
|
+
renderCommand(command, { active: index === activeIndex })
|
|
199
|
+
) : (
|
|
200
|
+
<>
|
|
201
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
202
|
+
{command.icon && <span className="shrink-0 [&_svg]:size-4">{command.icon}</span>}
|
|
203
|
+
<span className="min-w-0">
|
|
204
|
+
<span className="block truncate">{command.label}</span>
|
|
205
|
+
{command.description && (
|
|
206
|
+
<span className="block text-xs text-muted-foreground">{command.description}</span>
|
|
207
|
+
)}
|
|
208
|
+
</span>
|
|
209
|
+
</span>
|
|
210
|
+
{command.shortcut && <span className="font-mono text-xs">{command.shortcut}</span>}
|
|
211
|
+
</>
|
|
212
|
+
)}
|
|
148
213
|
</li>
|
|
149
214
|
</React.Fragment>
|
|
150
215
|
);
|
|
@@ -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"
|
|
@@ -13,10 +13,17 @@ export type ReasoningProps = {
|
|
|
13
13
|
duration?: number | string;
|
|
14
14
|
/** Marks reasoning as still arriving: animated header state. */
|
|
15
15
|
isStreaming?: boolean;
|
|
16
|
+
/** Localized header, count and step labels. */
|
|
17
|
+
labels?: {
|
|
18
|
+
title?: string;
|
|
19
|
+
count?: (count: number) => string;
|
|
20
|
+
thinking?: string;
|
|
21
|
+
step?: (index: number) => string;
|
|
22
|
+
};
|
|
16
23
|
};
|
|
17
24
|
|
|
18
25
|
const Reasoning = React.forwardRef<HTMLDivElement, ReasoningProps>(
|
|
19
|
-
({ steps, className, collapsible = true, defaultExpanded = false, duration, isStreaming = false, ...props }, ref) => {
|
|
26
|
+
({ steps, className, collapsible = true, defaultExpanded = false, duration, isStreaming = false, labels, ...props }, ref) => {
|
|
20
27
|
const [isExpanded, setIsExpanded] = React.useState(defaultExpanded);
|
|
21
28
|
const contentId = React.useId();
|
|
22
29
|
|
|
@@ -25,10 +32,10 @@ const Reasoning = React.forwardRef<HTMLDivElement, ReasoningProps>(
|
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
const headerText = [
|
|
28
|
-
'Reasoning',
|
|
29
|
-
`(${steps.length} ${steps.length === 1 ? 'step' : 'steps'})`,
|
|
35
|
+
labels?.title ?? 'Reasoning',
|
|
36
|
+
labels?.count ? labels.count(steps.length) : `(${steps.length} ${steps.length === 1 ? 'step' : 'steps'})`,
|
|
30
37
|
typeof duration === 'number' ? `· ${duration}s` : duration ? `· ${duration}` : '',
|
|
31
|
-
isStreaming ?
|
|
38
|
+
isStreaming ? `· ${labels?.thinking ?? 'thinking…'}` : '',
|
|
32
39
|
]
|
|
33
40
|
.filter(Boolean)
|
|
34
41
|
.join(' ');
|
|
@@ -42,7 +49,7 @@ const Reasoning = React.forwardRef<HTMLDivElement, ReasoningProps>(
|
|
|
42
49
|
aria-expanded={isExpanded}
|
|
43
50
|
aria-controls={contentId}
|
|
44
51
|
>
|
|
45
|
-
<span className={cn('transition-transform', isExpanded ? 'rotate-90' : '')}
|
|
52
|
+
<span className={cn('transition-transform', isExpanded ? 'rotate-90' : '')}>?</span>
|
|
46
53
|
<span>{headerText}</span>
|
|
47
54
|
{isStreaming && (
|
|
48
55
|
<span className="flex gap-0.5" aria-hidden="true">
|
|
@@ -64,7 +71,7 @@ const Reasoning = React.forwardRef<HTMLDivElement, ReasoningProps>(
|
|
|
64
71
|
<div key={step.id} className="flex gap-3">
|
|
65
72
|
<div className="w-3 h-3 rounded-full bg-primary/20 border-2 border-primary flex-shrink-0 mt-1" />
|
|
66
73
|
<div className="text-sm flex-1">
|
|
67
|
-
<div className="font-medium text-foreground mb-1">Step {index + 1}</div>
|
|
74
|
+
<div className="font-medium text-foreground mb-1">{labels?.step?.(index + 1) ?? `Step ${index + 1}`}</div>
|
|
68
75
|
<div className="text-muted-foreground">{step.content}</div>
|
|
69
76
|
{step.timestamp && (
|
|
70
77
|
<div className="text-xs text-muted-foreground mt-1">
|
|
@@ -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>
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
{ "feature": "Upload status", "implementation": "Uploading state is announced via role=status." },
|
|
28
28
|
{ "feature": "Decorative thumbnails", "implementation": "Preview images are aria-hidden; the file name carries the meaning." }
|
|
29
29
|
],
|
|
30
|
+
"sourceDir": "attachment",
|
|
31
|
+
"exportName": "Attachment",
|
|
30
32
|
"package": "@slipstream-ui/ai",
|
|
31
33
|
"importPath": "@slipstream-ui/ai",
|
|
32
34
|
"version": "0.0.1"
|