@elabs-ai/components-ai 4.0.0 → 4.1.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/README.md +61 -11
- package/dist/{_audio-player-media-chrome-KA5DY54G.js → _audio-player-media-chrome-T3XVXWRZ.js} +8 -4
- package/dist/_audio-player-media-chrome-T3XVXWRZ.js.map +1 -0
- package/dist/{_flow-boundary-D63PJ65S.js → _flow-boundary-SHNWLQG5.js} +32 -43
- package/dist/_flow-boundary-SHNWLQG5.js.map +1 -0
- package/dist/{_persona-rive-RFR2EUWP.js → _persona-rive-JEG44YHX.js} +9 -5
- package/dist/_persona-rive-JEG44YHX.js.map +1 -0
- package/dist/index.d.ts +1109 -202
- package/dist/index.js +4867 -2878
- package/dist/index.js.map +1 -1
- package/package.json +31 -16
- package/src/_audio-player-media-chrome.tsx +102 -15
- package/src/_flow-boundary.tsx +68 -49
- package/src/_lazy-boundary-conformance.ts +38 -0
- package/src/_lazy-engine-boundary.tsx +61 -0
- package/src/_lazy-mermaid-absent.test.ts +53 -0
- package/src/_lazy-mermaid.test.ts +15 -0
- package/src/_lazy-mermaid.ts +24 -1
- package/src/_mermaid-error-panel.test.tsx +50 -0
- package/src/_mermaid-error-panel.tsx +66 -0
- package/src/_persona-rive.tsx +62 -10
- package/src/_streamdown-i18n.ts +21 -1
- package/src/_streamdown-safety.ts +170 -0
- package/src/agent-event.stories.tsx +97 -0
- package/src/agent-event.test.tsx +145 -0
- package/src/agent-event.tsx +187 -0
- package/src/agent-timeline.stories.tsx +9 -1
- package/src/agent.test.tsx +47 -0
- package/src/agent.tsx +28 -14
- package/src/agentic-workspace.stories.tsx +27 -20
- package/src/artifact.tsx +15 -12
- package/src/audio-player.test.tsx +78 -0
- package/src/audio-player.tsx +243 -56
- package/src/audio-visualizer.stories.tsx +126 -0
- package/src/audio-visualizer.test.tsx +438 -0
- package/src/audio-visualizer.tsx +367 -0
- package/src/canvas.stories.tsx +150 -1
- package/src/chat-shell.stories.tsx +18 -3
- package/src/chat.stories.tsx +16 -2
- package/src/code-block.stories.tsx +9 -1
- package/src/composer.stories.tsx +372 -9
- package/src/composer.test.tsx +357 -6
- package/src/composer.tsx +217 -35
- package/src/confirmation.stories.tsx +72 -1
- package/src/confirmation.test.tsx +216 -2
- package/src/confirmation.tsx +263 -3
- package/src/context-panel.stories.tsx +9 -1
- package/src/context-panel.tsx +2 -1
- package/src/conversation.stories.tsx +63 -2
- package/src/conversation.test.tsx +13 -0
- package/src/conversation.tsx +20 -2
- package/src/diff-view.stories.tsx +196 -0
- package/src/diff-view.test.tsx +188 -0
- package/src/diff-view.tsx +642 -0
- package/src/gallery.tsx +4 -2
- package/src/index.ts +14 -4
- package/src/jsx-preview.stories.tsx +2 -2
- package/src/markdown-view.stories.tsx +92 -1
- package/src/markdown-view.test.tsx +232 -1
- package/src/markdown-view.tsx +150 -6
- package/src/message-compare.stories.tsx +175 -0
- package/src/message-compare.test.tsx +207 -0
- package/src/message-compare.tsx +453 -0
- package/src/message-form.stories.tsx +29 -1
- package/src/message.stories.tsx +9 -1
- package/src/message.test.tsx +176 -0
- package/src/message.tsx +90 -4
- package/src/microcopy.test.tsx +40 -0
- package/src/{model-selector.stories.tsx → model-provider-logo.stories.tsx} +17 -8
- package/src/{model-selector.test.tsx → model-provider-logo.test.tsx} +10 -10
- package/src/model-provider-logo.tsx +149 -0
- package/src/permission-mode-select.stories.tsx +82 -0
- package/src/permission-mode-select.test.tsx +100 -0
- package/src/permission-mode-select.tsx +137 -0
- package/src/persona-missing-peer.test.tsx +54 -0
- package/src/persona.tsx +68 -22
- package/src/plan.stories.tsx +166 -0
- package/src/plan.test.tsx +267 -0
- package/src/plan.tsx +182 -20
- package/src/prompt-input-effort.stories.tsx +123 -0
- package/src/prompt-input-effort.test.tsx +83 -0
- package/src/prompt-input-effort.tsx +136 -0
- package/src/prompt-input-mode.stories.tsx +108 -0
- package/src/prompt-input-mode.test.tsx +99 -0
- package/src/prompt-input-mode.tsx +169 -0
- package/src/prompt-input-slash.stories.tsx +211 -0
- package/src/prompt-input-slash.test.tsx +262 -0
- package/src/prompt-input-slash.tsx +541 -0
- package/src/prompt-input.stories.tsx +2 -2
- package/src/reasoning.tsx +27 -13
- package/src/sandbox.stories.tsx +9 -1
- package/src/schema-display.tsx +5 -2
- package/src/selection-toolbar.stories.tsx +9 -1
- package/src/session-header.stories.tsx +128 -0
- package/src/session-header.test.tsx +138 -0
- package/src/session-header.tsx +243 -0
- package/src/session-status-bar.stories.tsx +73 -0
- package/src/session-status-bar.test.tsx +94 -0
- package/src/session-status-bar.tsx +165 -0
- package/src/snippet.stories.tsx +9 -1
- package/src/stack-trace.tsx +6 -2
- package/src/streamdown-i18n.test.tsx +1 -1
- package/src/task.stories.tsx +13 -3
- package/src/templates-ai-assistant.stories.tsx +21 -1
- package/src/token-usage.stories.tsx +24 -0
- package/src/token-usage.test.tsx +92 -0
- package/src/{context.tsx → token-usage.tsx} +67 -53
- package/src/tool-result-card.stories.tsx +9 -1
- package/src/tool.stories.tsx +13 -3
- package/src/tool.tsx +15 -8
- package/src/turn-status.stories.tsx +124 -0
- package/src/turn-status.test.tsx +74 -0
- package/src/turn-status.tsx +174 -0
- package/src/use-audio-level.ts +104 -0
- package/src/web-preview.tsx +8 -3
- package/dist/_audio-player-media-chrome-KA5DY54G.js.map +0 -1
- package/dist/_flow-boundary-D63PJ65S.js.map +0 -1
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +0 -11
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +0 -1
- package/dist/_persona-rive-RFR2EUWP.js.map +0 -1
- package/src/_interactive-terminal-xterm.ts +0 -32
- package/src/blocks-ai-composer.stories.tsx +0 -83
- package/src/context.stories.tsx +0 -16
- package/src/interactive-terminal.stories.tsx +0 -165
- package/src/interactive-terminal.test.tsx +0 -448
- package/src/interactive-terminal.tsx +0 -444
- package/src/model-selector.tsx +0 -225
- package/src/terminal.tsx +0 -244
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { SessionStatusBar } from "./session-status-bar";
|
|
4
|
+
|
|
5
|
+
describe("SessionStatusBar", () => {
|
|
6
|
+
it("renders nothing at all when every segment is empty", () => {
|
|
7
|
+
const { container } = render(<SessionStatusBar />);
|
|
8
|
+
expect(container).toBeEmptyDOMElement();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("renders only the segments whose prop is supplied", () => {
|
|
12
|
+
render(<SessionStatusBar workspace="brand-ui" />);
|
|
13
|
+
|
|
14
|
+
expect(screen.getByText("brand-ui")).toBeInTheDocument();
|
|
15
|
+
expect(screen.queryByText(/main|feature\//)).not.toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("renders workspace, branch and model together when all are supplied", () => {
|
|
19
|
+
render(<SessionStatusBar branch="main" model="Claude Opus 4" workspace="brand-ui" />);
|
|
20
|
+
|
|
21
|
+
expect(screen.getByText("brand-ui")).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByText("main")).toBeInTheDocument();
|
|
23
|
+
expect(screen.getByText("Claude Opus 4")).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("docks children (e.g. Context) rather than reimplementing usage maths", () => {
|
|
27
|
+
render(
|
|
28
|
+
<SessionStatusBar workspace="brand-ui">
|
|
29
|
+
<span>Context slot</span>
|
|
30
|
+
</SessionStatusBar>,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(screen.getByText("Context slot")).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("renders a connection count and a distinct connecting state", () => {
|
|
37
|
+
const { rerender } = render(
|
|
38
|
+
<SessionStatusBar connections={{ connected: 2, total: 4 }} workspace="brand-ui" />,
|
|
39
|
+
);
|
|
40
|
+
expect(screen.getByText("2/4")).toBeInTheDocument();
|
|
41
|
+
|
|
42
|
+
rerender(
|
|
43
|
+
<SessionStatusBar
|
|
44
|
+
connections={{ connected: 0, total: 4, connecting: true }}
|
|
45
|
+
workspace="brand-ui"
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
expect(screen.getByText("0/4")).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// #155: the connections live region's only text content used to be
|
|
52
|
+
// aria-hidden, so a screen reader heard an empty string on every update.
|
|
53
|
+
// `screen.getByText` alone cannot catch this (it matches text regardless of
|
|
54
|
+
// aria-hidden) — this asserts what the region ACTUALLY announces: its text
|
|
55
|
+
// content with every aria-hidden subtree stripped out first.
|
|
56
|
+
const announceableText = (el: Element): string => {
|
|
57
|
+
const clone = el.cloneNode(true) as HTMLElement;
|
|
58
|
+
for (const hidden of Array.from(clone.querySelectorAll('[aria-hidden="true"]'))) {
|
|
59
|
+
hidden.remove();
|
|
60
|
+
}
|
|
61
|
+
return clone.textContent?.trim() ?? "";
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
it("announces the connection state via real text content, not just an aria-label (#155)", () => {
|
|
65
|
+
const { container, rerender } = render(
|
|
66
|
+
<SessionStatusBar connections={{ connected: 0, total: 4, connecting: true }} />,
|
|
67
|
+
);
|
|
68
|
+
const connecting = container.querySelector(
|
|
69
|
+
'[data-slot="session-status-bar-connections"]',
|
|
70
|
+
) as HTMLElement;
|
|
71
|
+
expect(connecting).toHaveAttribute("role", "status");
|
|
72
|
+
expect(connecting).toHaveAttribute("aria-live", "polite");
|
|
73
|
+
// No separate `aria-label`: an explicit one would duplicate the
|
|
74
|
+
// announced content as the accessible name too, and some ATs speak a
|
|
75
|
+
// live region's name alongside its changed content ("Connecting…
|
|
76
|
+
// Connecting…"). Leaving it off does NOT relocate the name to the
|
|
77
|
+
// (sr-only) text — `role="status"` is `nameFrom: author`, so with no
|
|
78
|
+
// `aria-label`/`aria-labelledby` the region simply has no accessible
|
|
79
|
+
// name. That is fine: no acceptance criterion needs one here, only the
|
|
80
|
+
// announced content (asserted below) does.
|
|
81
|
+
expect(connecting).not.toHaveAttribute("aria-label");
|
|
82
|
+
// Before the fix this was "" — the only text node was aria-hidden.
|
|
83
|
+
expect(announceableText(connecting)).toBe("Connecting…");
|
|
84
|
+
|
|
85
|
+
rerender(<SessionStatusBar connections={{ connected: 2, total: 4 }} />);
|
|
86
|
+
const connected = container.querySelector(
|
|
87
|
+
'[data-slot="session-status-bar-connections"]',
|
|
88
|
+
) as HTMLElement;
|
|
89
|
+
expect(announceableText(connected)).toBe("2 of 4 connections");
|
|
90
|
+
// The visible, aria-hidden counter is unchanged — the fix adds an
|
|
91
|
+
// announcement, it does not alter the rendered appearance (#155 AC).
|
|
92
|
+
expect(screen.getByText("2/4")).toHaveAttribute("aria-hidden", "true");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn, useLocale } from "@elabs-ai/components-ui";
|
|
4
|
+
import { Cpu, Folder, GitBranch, Loader2Icon, PlugZapIcon } from "lucide-react";
|
|
5
|
+
import type { ComponentProps, ComponentType, ReactNode, SVGProps } from "react";
|
|
6
|
+
import { forwardRef } from "react";
|
|
7
|
+
|
|
8
|
+
export interface SessionStatusBarConnections {
|
|
9
|
+
/** Number of integrations/MCP servers currently connected. */
|
|
10
|
+
connected: number;
|
|
11
|
+
/** Total number of configured integrations/MCP servers. */
|
|
12
|
+
total: number;
|
|
13
|
+
/** Renders a spinner in place of the connection icon while establishing. */
|
|
14
|
+
connecting?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SessionStatusBarProps extends ComponentProps<"div"> {
|
|
18
|
+
/** Current workspace/project name. */
|
|
19
|
+
workspace?: string;
|
|
20
|
+
/** Current git branch. */
|
|
21
|
+
branch?: string;
|
|
22
|
+
/** Active model id/label. */
|
|
23
|
+
model?: string;
|
|
24
|
+
/** Integration/MCP connection progress; renders a spinner while connecting. */
|
|
25
|
+
connections?: SessionStatusBarConnections;
|
|
26
|
+
/**
|
|
27
|
+
* Docks another component alongside the ambient segments — most often
|
|
28
|
+
* `<TokenUsage />` (`@elabs-ai/components-ai`), so token usage/cost keeps
|
|
29
|
+
* its one owner (`token-usage.tsx`, tokenlens) instead of being re-implemented
|
|
30
|
+
* here. Rendered flush to the end of the bar.
|
|
31
|
+
*/
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type SegmentIcon = ComponentType<SVGProps<SVGSVGElement>>;
|
|
36
|
+
|
|
37
|
+
const SessionStatusBarSegment = ({
|
|
38
|
+
icon: Icon,
|
|
39
|
+
slot,
|
|
40
|
+
value,
|
|
41
|
+
}: {
|
|
42
|
+
icon: SegmentIcon;
|
|
43
|
+
slot: string;
|
|
44
|
+
value: string;
|
|
45
|
+
}) => (
|
|
46
|
+
<span className="inline-flex min-w-0 items-center gap-1.5" data-slot={slot}>
|
|
47
|
+
<Icon aria-hidden="true" className="size-3.5 shrink-0" />
|
|
48
|
+
<span className="truncate">{value}</span>
|
|
49
|
+
</span>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* SessionStatusBar — the ambient session row: workspace, branch, model, and
|
|
54
|
+
* integration-connection progress (#105). Every segment is independently
|
|
55
|
+
* optional and renders only when its prop is supplied; with nothing supplied
|
|
56
|
+
* the bar renders nothing at all, not an empty shell.
|
|
57
|
+
*
|
|
58
|
+
* Docks `Context` (or any other node) via `children` rather than
|
|
59
|
+
* re-implementing token-usage/cost maths — `context.tsx` stays the one owner
|
|
60
|
+
* of that calculation (tokenlens).
|
|
61
|
+
*/
|
|
62
|
+
export const SessionStatusBar = forwardRef<HTMLDivElement, SessionStatusBarProps>(
|
|
63
|
+
function SessionStatusBar(
|
|
64
|
+
{ workspace, branch, model, connections, children, className, ...props },
|
|
65
|
+
ref,
|
|
66
|
+
) {
|
|
67
|
+
const { formatNumber, t } = useLocale();
|
|
68
|
+
const hasContent = Boolean(workspace ?? branch ?? model ?? connections ?? children);
|
|
69
|
+
|
|
70
|
+
if (!hasContent) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Computed once and reused for BOTH the connections region's name (for a
|
|
75
|
+
// user who navigates to it deliberately) and its real, non-hidden text
|
|
76
|
+
// content — what a polite announcement actually reads on change. An
|
|
77
|
+
// aria-label alone is NOT that (#155): a live region announces its
|
|
78
|
+
// content, not its own accessible name.
|
|
79
|
+
const connectionsAnnouncement = connections
|
|
80
|
+
? connections.connecting
|
|
81
|
+
? t("ai.sessionStatusBar.connecting")
|
|
82
|
+
: t("ai.sessionStatusBar.connections", {
|
|
83
|
+
connected: connections.connected,
|
|
84
|
+
total: connections.total,
|
|
85
|
+
})
|
|
86
|
+
: undefined;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
ref={ref}
|
|
91
|
+
className={cn(
|
|
92
|
+
"flex w-full items-center gap-4 border-t bg-surface-muted px-3 py-1.5 text-meta text-muted-foreground",
|
|
93
|
+
className,
|
|
94
|
+
)}
|
|
95
|
+
data-slot="session-status-bar"
|
|
96
|
+
{...props}
|
|
97
|
+
>
|
|
98
|
+
{workspace ? (
|
|
99
|
+
<SessionStatusBarSegment
|
|
100
|
+
icon={Folder}
|
|
101
|
+
slot="session-status-bar-workspace"
|
|
102
|
+
value={workspace}
|
|
103
|
+
/>
|
|
104
|
+
) : null}
|
|
105
|
+
{branch ? (
|
|
106
|
+
<SessionStatusBarSegment
|
|
107
|
+
icon={GitBranch}
|
|
108
|
+
slot="session-status-bar-branch"
|
|
109
|
+
value={branch}
|
|
110
|
+
/>
|
|
111
|
+
) : null}
|
|
112
|
+
{model ? (
|
|
113
|
+
<SessionStatusBarSegment icon={Cpu} slot="session-status-bar-model" value={model} />
|
|
114
|
+
) : null}
|
|
115
|
+
{connections ? (
|
|
116
|
+
<span
|
|
117
|
+
aria-live="polite"
|
|
118
|
+
className="inline-flex shrink-0 items-center gap-1.5"
|
|
119
|
+
data-slot="session-status-bar-connections"
|
|
120
|
+
role="status"
|
|
121
|
+
>
|
|
122
|
+
{connections.connecting ? (
|
|
123
|
+
<Loader2Icon
|
|
124
|
+
aria-hidden="true"
|
|
125
|
+
className="size-3.5 shrink-0 animate-spin motion-reduce:animate-none"
|
|
126
|
+
/>
|
|
127
|
+
) : (
|
|
128
|
+
<PlugZapIcon aria-hidden="true" className="size-3.5 shrink-0" />
|
|
129
|
+
)}
|
|
130
|
+
<span aria-hidden="true" className="tabular-nums">
|
|
131
|
+
{formatNumber(connections.connected)}/{formatNumber(connections.total)}
|
|
132
|
+
</span>
|
|
133
|
+
{/* The live region's only NON-hidden text — mirrors
|
|
134
|
+
terminal-status-bar.tsx's aria-hidden-visual +
|
|
135
|
+
sr-only-sentence pair (#155). Without this, every polite
|
|
136
|
+
update announced an empty string: the counter above is
|
|
137
|
+
aria-hidden, so it was the region's ONLY text content.
|
|
138
|
+
Deliberately no `aria-label` here (unlike
|
|
139
|
+
terminal-status-bar.tsx, where the label lives on the OUTER
|
|
140
|
+
bar as a generic region name distinct from any segment's
|
|
141
|
+
content): an explicit `aria-label` here would give this
|
|
142
|
+
span an ACCESSIBLE NAME byte-identical to its announced
|
|
143
|
+
CONTENT, which risks a double announcement ("Connecting…
|
|
144
|
+
Connecting…") on ATs that speak a live region's name
|
|
145
|
+
alongside its changed content. Leaving `aria-label` off
|
|
146
|
+
does NOT relocate the name to this text — `role="status"`
|
|
147
|
+
is `nameFrom: author`, so it never falls back to content
|
|
148
|
+
the way e.g. a `button` does. The span simply has no
|
|
149
|
+
accessible name, which is fine: no acceptance criterion
|
|
150
|
+
needs one here, only the announced content does. */}
|
|
151
|
+
<span className="sr-only">{connectionsAnnouncement}</span>
|
|
152
|
+
</span>
|
|
153
|
+
) : null}
|
|
154
|
+
{children ? (
|
|
155
|
+
<div
|
|
156
|
+
className="ms-auto flex shrink-0 items-center"
|
|
157
|
+
data-slot="session-status-bar-context"
|
|
158
|
+
>
|
|
159
|
+
{children}
|
|
160
|
+
</div>
|
|
161
|
+
) : null}
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
);
|
package/src/snippet.stories.tsx
CHANGED
|
@@ -3,7 +3,15 @@ import { Snippet, SnippetAddon, SnippetCopyButton, SnippetInput } from "./snippe
|
|
|
3
3
|
const meta = {
|
|
4
4
|
title: "AI/Snippet",
|
|
5
5
|
component: Snippet,
|
|
6
|
-
parameters: {
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: "padded",
|
|
8
|
+
docs: {
|
|
9
|
+
description: {
|
|
10
|
+
component:
|
|
11
|
+
"A single-line copyable COMMAND — an `InputGroup` in a mono face wrapping a read-only value and a copy button (`npm i …`, a curl line, an id). Multi-line highlighted source with a language picker is `AI/CodeBlock`; see [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). Nothing here highlights: `Snippet` never loads Shiki.",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
7
15
|
} satisfies Meta<typeof Snippet>;
|
|
8
16
|
export default meta;
|
|
9
17
|
type Story = StoryObj<typeof meta>;
|
package/src/stack-trace.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
4
4
|
import { Button } from "@elabs-ai/components-ui";
|
|
5
5
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@elabs-ai/components-ui";
|
|
6
|
+
import { useLocale } from "@elabs-ai/components-ui";
|
|
6
7
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
7
8
|
import { AlertTriangleIcon, CheckIcon, ChevronDownIcon, CopyIcon } from "lucide-react";
|
|
8
9
|
import type { ComponentProps } from "react";
|
|
@@ -236,7 +237,9 @@ export const StackTraceErrorType = memo(
|
|
|
236
237
|
const { trace } = useStackTrace();
|
|
237
238
|
|
|
238
239
|
return (
|
|
239
|
-
|
|
240
|
+
// #124: the error type NAME is running text (e.g. "TypeError"), not a
|
|
241
|
+
// mark — ink rung, unlike the AlertTriangleIcon mark above.
|
|
242
|
+
<span className={cn("shrink-0 font-semibold text-destructive-text", className)} {...props}>
|
|
240
243
|
{children ?? trace.errorType}
|
|
241
244
|
</span>
|
|
242
245
|
);
|
|
@@ -426,6 +429,7 @@ FilePathButton.displayName = "FilePathButton";
|
|
|
426
429
|
export const StackTraceFrames = memo(
|
|
427
430
|
({ className, showInternalFrames = true, ...props }: StackTraceFramesProps) => {
|
|
428
431
|
const { trace, onFilePathClick } = useStackTrace();
|
|
432
|
+
const { t } = useLocale();
|
|
429
433
|
|
|
430
434
|
const framesToShow = showInternalFrames
|
|
431
435
|
? trace.frames
|
|
@@ -460,7 +464,7 @@ export const StackTraceFrames = memo(
|
|
|
460
464
|
</div>
|
|
461
465
|
))}
|
|
462
466
|
{framesToShow.length === 0 && (
|
|
463
|
-
<div className="text-muted-foreground text-xs">
|
|
467
|
+
<div className="text-muted-foreground text-xs">{t("ai.stackTrace.empty")}</div>
|
|
464
468
|
)}
|
|
465
469
|
</div>
|
|
466
470
|
);
|
|
@@ -129,7 +129,7 @@ describe("Streamdown code-block theme (#315 follow-up)", () => {
|
|
|
129
129
|
// (github-light's keyword is #d73a49, github-dark's is #ff7b72).
|
|
130
130
|
expect(span.getAttribute("style")).toContain("--sdm-c: #133796");
|
|
131
131
|
// Both dual-theme slots are pinned to the SAME active-theme color (#315
|
|
132
|
-
// follow-up design: brand-ui has
|
|
132
|
+
// follow-up design: brand-ui has more themes than the two Shiki's
|
|
133
133
|
// light/dark mechanism expects), so `.dark` never resolves a different palette.
|
|
134
134
|
expect(span.getAttribute("style")).toContain("--shiki-dark: #133796");
|
|
135
135
|
});
|
package/src/task.stories.tsx
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { Task, TaskContent, TaskItem, TaskItemFile, TaskTrigger } from "./task";
|
|
3
|
-
const meta = {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const meta = {
|
|
4
|
+
title: "AI/Task",
|
|
5
|
+
component: Task,
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: "padded",
|
|
8
|
+
docs: {
|
|
9
|
+
description: {
|
|
10
|
+
component:
|
|
11
|
+
"The CHAT run summary — what got done, collapsed by default so a settled transcript keeps only its focal surfaces open. The console counterpart of an agent checklist is `Terminal/TerminalTodoList`; see [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). The body rides the canonical `AI/AgentTimeline` rail rather than a hand-rolled one, and what the agent intends to do BEFORE it runs is `AI/Plan`.",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<typeof Task>;
|
|
6
16
|
export default meta;
|
|
7
17
|
type Story = StoryObj<typeof meta>;
|
|
8
18
|
// Collapsed by default since #192; `defaultOpen` here only so the docs render
|
|
@@ -9,6 +9,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
|
9
9
|
import { useState } from "react";
|
|
10
10
|
import { AppIcon } from "@elabs-ai/components-icons";
|
|
11
11
|
import {
|
|
12
|
+
Button,
|
|
12
13
|
Sidebar,
|
|
13
14
|
SidebarContent,
|
|
14
15
|
SidebarGroup,
|
|
@@ -31,7 +32,7 @@ import {
|
|
|
31
32
|
MessageContent,
|
|
32
33
|
MessageResponse,
|
|
33
34
|
} from "./index";
|
|
34
|
-
import { Bot, History, Home, Settings } from "lucide-react";
|
|
35
|
+
import { Bot, History, Home, MessageSquare, Settings } from "lucide-react";
|
|
35
36
|
|
|
36
37
|
interface ChatMessage {
|
|
37
38
|
id: string;
|
|
@@ -101,8 +102,27 @@ function AiAssistantTemplate() {
|
|
|
101
102
|
<ConversationContent>
|
|
102
103
|
{messages.length === 0 ? (
|
|
103
104
|
<ConversationEmptyState
|
|
105
|
+
icon={<MessageSquare className="size-8" aria-hidden="true" />}
|
|
104
106
|
title="Start the conversation"
|
|
105
107
|
description="Ask anything to begin."
|
|
108
|
+
actions={
|
|
109
|
+
<>
|
|
110
|
+
<Button
|
|
111
|
+
onClick={() => send("What can you help me with?")}
|
|
112
|
+
size="sm"
|
|
113
|
+
variant="outline"
|
|
114
|
+
>
|
|
115
|
+
What can you help me with?
|
|
116
|
+
</Button>
|
|
117
|
+
<Button
|
|
118
|
+
onClick={() => send("Summarize my recent activity")}
|
|
119
|
+
size="sm"
|
|
120
|
+
variant="outline"
|
|
121
|
+
>
|
|
122
|
+
Summarize my recent activity
|
|
123
|
+
</Button>
|
|
124
|
+
</>
|
|
125
|
+
}
|
|
106
126
|
/>
|
|
107
127
|
) : (
|
|
108
128
|
messages.map((m) => (
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { TokenUsage, TokenUsageTrigger } from "./token-usage";
|
|
3
|
+
const meta = {
|
|
4
|
+
title: "AI/TokenUsage",
|
|
5
|
+
component: TokenUsage,
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: "padded",
|
|
8
|
+
docs: {
|
|
9
|
+
description: {
|
|
10
|
+
component:
|
|
11
|
+
"The context-WINDOW usage ring: a compact `usedTokens` / `maxTokens` dial that opens a hover card with the input/output token split and, when `modelId` is known, an estimated cost. It was called `Context` until it was renamed for exactly this reason — the chat workspace’s right rail is `AI/ContextPanel`, a different component with a confusingly similar old name. See [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs).",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<typeof TokenUsage>;
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof meta>;
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
render: () => (
|
|
20
|
+
<TokenUsage usedTokens={2400} maxTokens={8000}>
|
|
21
|
+
<TokenUsageTrigger />
|
|
22
|
+
</TokenUsage>
|
|
23
|
+
),
|
|
24
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { LanguageModelUsage } from "ai";
|
|
2
|
+
import { DEFAULT_MESSAGES, LocaleProvider } from "@elabs-ai/components-ui";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { TokenUsage, TokenUsageCache, TokenUsageReasoning } from "./token-usage";
|
|
6
|
+
|
|
7
|
+
// `LanguageModelUsage`'s reasoning/cache figures live under nested detail
|
|
8
|
+
// objects (`outputTokenDetails.reasoningTokens`, `inputTokenDetails.cacheReadTokens`).
|
|
9
|
+
// ai@6 also carried deprecated flat top-level aliases (`reasoningTokens`,
|
|
10
|
+
// `cachedInputTokens`); ai@7 removed them. This mock deliberately omits the
|
|
11
|
+
// flat aliases so the test only passes if the components read the nested path.
|
|
12
|
+
const usage: LanguageModelUsage = {
|
|
13
|
+
inputTokens: 1000,
|
|
14
|
+
inputTokenDetails: {
|
|
15
|
+
noCacheTokens: 600,
|
|
16
|
+
cacheReadTokens: 400,
|
|
17
|
+
cacheWriteTokens: 0,
|
|
18
|
+
},
|
|
19
|
+
outputTokens: 200,
|
|
20
|
+
outputTokenDetails: {
|
|
21
|
+
textTokens: 150,
|
|
22
|
+
reasoningTokens: 50,
|
|
23
|
+
},
|
|
24
|
+
totalTokens: 1200,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("TokenUsage usage readouts (#30, ai@7 LanguageModelUsage shape)", () => {
|
|
28
|
+
it("TokenUsageReasoning reads outputTokenDetails.reasoningTokens, not a flat field", () => {
|
|
29
|
+
render(
|
|
30
|
+
<TokenUsage maxTokens={8000} usage={usage} usedTokens={1200}>
|
|
31
|
+
<TokenUsageReasoning />
|
|
32
|
+
</TokenUsage>,
|
|
33
|
+
);
|
|
34
|
+
expect(screen.getByText("Reasoning")).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText("50")).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("TokenUsageCache reads inputTokenDetails.cacheReadTokens, not a flat field", () => {
|
|
39
|
+
render(
|
|
40
|
+
<TokenUsage maxTokens={8000} usage={usage} usedTokens={1200}>
|
|
41
|
+
<TokenUsageCache />
|
|
42
|
+
</TokenUsage>,
|
|
43
|
+
);
|
|
44
|
+
expect(screen.getByText("Cache")).toBeInTheDocument();
|
|
45
|
+
expect(screen.getByText("400")).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders nothing when the nested detail objects report zero", () => {
|
|
49
|
+
const zeroUsage: LanguageModelUsage = {
|
|
50
|
+
...usage,
|
|
51
|
+
inputTokenDetails: { ...usage.inputTokenDetails, cacheReadTokens: 0 },
|
|
52
|
+
outputTokenDetails: { ...usage.outputTokenDetails, reasoningTokens: 0 },
|
|
53
|
+
};
|
|
54
|
+
const { container } = render(
|
|
55
|
+
<TokenUsage maxTokens={8000} usage={zeroUsage} usedTokens={1200}>
|
|
56
|
+
<TokenUsageReasoning />
|
|
57
|
+
<TokenUsageCache />
|
|
58
|
+
</TokenUsage>,
|
|
59
|
+
);
|
|
60
|
+
expect(container).toBeEmptyDOMElement();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("TokenUsage locale keys (#141, ai.context.* renamed to ai.tokenUsage.*)", () => {
|
|
65
|
+
it("the shipped dictionary defines no ai.context.* key", () => {
|
|
66
|
+
const staleKeys = Object.keys(DEFAULT_MESSAGES).filter((key) => key.startsWith("ai.context."));
|
|
67
|
+
expect(staleKeys).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("renders its labels from the ai.tokenUsage.* namespace, not a stale fallback", () => {
|
|
71
|
+
render(
|
|
72
|
+
<LocaleProvider
|
|
73
|
+
messages={{
|
|
74
|
+
"ai.tokenUsage.reasoning": "Custom reasoning label",
|
|
75
|
+
"ai.tokenUsage.cache": "Custom cache label",
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<TokenUsage maxTokens={8000} usage={usage} usedTokens={1200}>
|
|
79
|
+
<TokenUsageReasoning />
|
|
80
|
+
<TokenUsageCache />
|
|
81
|
+
</TokenUsage>
|
|
82
|
+
</LocaleProvider>,
|
|
83
|
+
);
|
|
84
|
+
// A component still reading the old `ai.context.*` namespace would ignore
|
|
85
|
+
// this override and keep rendering the untranslated English default
|
|
86
|
+
// ("Reasoning" / "Cache") instead.
|
|
87
|
+
expect(screen.getByText("Custom reasoning label")).toBeInTheDocument();
|
|
88
|
+
expect(screen.getByText("Custom cache label")).toBeInTheDocument();
|
|
89
|
+
expect(screen.queryByText("Reasoning")).not.toBeInTheDocument();
|
|
90
|
+
expect(screen.queryByText("Cache")).not.toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
});
|