@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
package/src/schema-display.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Badge } from "@elabs-ai/components-ui";
|
|
4
4
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@elabs-ai/components-ui";
|
|
5
|
+
import { useLocale } from "@elabs-ai/components-ui";
|
|
5
6
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
6
7
|
import { ChevronRightIcon } from "lucide-react";
|
|
7
8
|
import type { ComponentProps, HTMLAttributes } from "react";
|
|
@@ -172,12 +173,13 @@ export const SchemaDisplayParameters = ({
|
|
|
172
173
|
...props
|
|
173
174
|
}: SchemaDisplayParametersProps) => {
|
|
174
175
|
const { parameters } = useContext(SchemaDisplayContext);
|
|
176
|
+
const { t } = useLocale();
|
|
175
177
|
|
|
176
178
|
return (
|
|
177
179
|
<Collapsible className={cn(className)} defaultOpen {...props}>
|
|
178
180
|
<CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-start transition-colors hover:bg-muted/50">
|
|
179
181
|
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
|
180
|
-
<span className="font-medium text-sm">
|
|
182
|
+
<span className="font-medium text-sm">{t("ai.schemaDisplay.parameters")}</span>
|
|
181
183
|
<Badge className="ms-auto text-xs" variant="secondary">
|
|
182
184
|
{parameters?.length}
|
|
183
185
|
</Badge>
|
|
@@ -313,12 +315,13 @@ export const SchemaDisplayResponse = ({
|
|
|
313
315
|
...props
|
|
314
316
|
}: SchemaDisplayResponseProps) => {
|
|
315
317
|
const { responseBody } = useContext(SchemaDisplayContext);
|
|
318
|
+
const { t } = useLocale();
|
|
316
319
|
|
|
317
320
|
return (
|
|
318
321
|
<Collapsible className={cn(className)} defaultOpen {...props}>
|
|
319
322
|
<CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-start transition-colors hover:bg-muted/50">
|
|
320
323
|
<ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
|
|
321
|
-
<span className="font-medium text-sm">
|
|
324
|
+
<span className="font-medium text-sm">{t("ai.schemaDisplay.response")}</span>
|
|
322
325
|
</CollapsibleTrigger>
|
|
323
326
|
<CollapsibleContent>
|
|
324
327
|
<div className="border-t">
|
|
@@ -8,7 +8,15 @@ const meta = {
|
|
|
8
8
|
title: "AI/SelectionToolbar",
|
|
9
9
|
component: SelectionToolbar,
|
|
10
10
|
tags: ["autodocs"],
|
|
11
|
-
parameters: {
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: "padded",
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
"The small toolbar that floats over a TEXT SELECTION in a transcript, offering Quote. A dense secondary control row is `Layout/Toolbar`, the row above a list or table is `Layout/ViewToolbar`, and formatting chrome for a markdown source pane is `Editor/MarkdownToolbar` — see [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). Positioned with a Popover virtual anchor at the selection rect; presentational only.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
12
20
|
} satisfies Meta<typeof SelectionToolbar>;
|
|
13
21
|
export default meta;
|
|
14
22
|
type Story = StoryObj<typeof meta>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { fn } from "storybook/test";
|
|
3
|
+
|
|
4
|
+
import { ChatGreeting } from "./chat-greeting";
|
|
5
|
+
import { SessionHeader } from "./session-header";
|
|
6
|
+
|
|
7
|
+
const CAPABILITIES = [
|
|
8
|
+
{ label: "Web search", description: "Look things up while we talk." },
|
|
9
|
+
{ label: "Code execution", description: "Run and inspect snippets in a sandbox." },
|
|
10
|
+
{ label: "File access", description: "Read files in the current workspace." },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const WHATS_NEW = [
|
|
14
|
+
{ label: "Faster file search", href: "#" },
|
|
15
|
+
{ label: "Improved reasoning traces" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const QUICK_ACTIONS = [
|
|
19
|
+
{ label: "New chat", keyHint: "⌘N", onSelect: fn() },
|
|
20
|
+
{ label: "Open workspace", keyHint: "⌘O", onSelect: fn() },
|
|
21
|
+
{ label: "Settings", onSelect: fn() },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const meta = {
|
|
25
|
+
title: "AI/SessionHeader",
|
|
26
|
+
component: SessionHeader,
|
|
27
|
+
parameters: {
|
|
28
|
+
layout: "padded",
|
|
29
|
+
docs: {
|
|
30
|
+
description: {
|
|
31
|
+
component:
|
|
32
|
+
"The CHAT launch card; the console skin is `Terminal/TerminalBanner` — see " +
|
|
33
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
|
|
34
|
+
"The launch card for an empty agent session — model, workspace, version, capabilities, what's new, and quick actions with key hints. Every section is independently optional: with only `title` supplied it renders the identity row alone, with no empty section scaffolding and no stray separators. Pair it above `ChatGreeting` for the standard first-run agent session state; distinct from `ChatGreeting`, which is the centered display-scale greeting for a general assistant chat.",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
args: {
|
|
39
|
+
title: "Codex",
|
|
40
|
+
model: "gpt-5.1-codex",
|
|
41
|
+
workspace: "~/dev/acme/api-gateway",
|
|
42
|
+
version: "v2.4.0",
|
|
43
|
+
capabilities: CAPABILITIES,
|
|
44
|
+
whatsNew: WHATS_NEW,
|
|
45
|
+
quickActions: QUICK_ACTIONS,
|
|
46
|
+
},
|
|
47
|
+
tags: ["autodocs"],
|
|
48
|
+
} satisfies Meta<typeof SessionHeader>;
|
|
49
|
+
|
|
50
|
+
export default meta;
|
|
51
|
+
type Story = StoryObj<typeof meta>;
|
|
52
|
+
|
|
53
|
+
/** The fully-populated launch card. */
|
|
54
|
+
export const Default: Story = {
|
|
55
|
+
render: (args) => (
|
|
56
|
+
<div className="mx-auto max-w-md bg-background">
|
|
57
|
+
<SessionHeader {...args} />
|
|
58
|
+
</div>
|
|
59
|
+
),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `title` is the only prop supplied — every other section (meta, workspace,
|
|
64
|
+
* capabilities, what's new, quick actions) is absent. Renders the identity
|
|
65
|
+
* row alone with no empty scaffolding and no stray separators.
|
|
66
|
+
*/
|
|
67
|
+
export const TitleOnly: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
title: "Codex",
|
|
70
|
+
model: undefined,
|
|
71
|
+
workspace: undefined,
|
|
72
|
+
version: undefined,
|
|
73
|
+
capabilities: undefined,
|
|
74
|
+
whatsNew: undefined,
|
|
75
|
+
quickActions: undefined,
|
|
76
|
+
},
|
|
77
|
+
render: (args) => (
|
|
78
|
+
<div className="mx-auto max-w-md bg-background">
|
|
79
|
+
<SessionHeader {...args} />
|
|
80
|
+
</div>
|
|
81
|
+
),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Every section populated — the full launch-card anatomy. */
|
|
85
|
+
export const FullyPopulated: Story = {
|
|
86
|
+
render: (args) => (
|
|
87
|
+
<div className="mx-auto max-w-md bg-background">
|
|
88
|
+
<SessionHeader {...args} />
|
|
89
|
+
</div>
|
|
90
|
+
),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** A long workspace path truncates instead of overflowing its container. */
|
|
94
|
+
export const LongWorkspacePath: Story = {
|
|
95
|
+
args: {
|
|
96
|
+
workspace: "~/dev/acme/monorepo/services/platform/billing/api-gateway/src/handlers/webhooks",
|
|
97
|
+
},
|
|
98
|
+
render: (args) => (
|
|
99
|
+
<div className="mx-auto max-w-md bg-background">
|
|
100
|
+
<SessionHeader {...args} />
|
|
101
|
+
</div>
|
|
102
|
+
),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** A route that already owns the page's `<h1>` — pass `level={3}` to keep a single, hierarchical outline. */
|
|
106
|
+
export const HeadingLevel3: Story = {
|
|
107
|
+
args: { level: 3 },
|
|
108
|
+
render: (args) => (
|
|
109
|
+
<div className="mx-auto max-w-md bg-background">
|
|
110
|
+
<h1 className="sr-only">Assistant</h1>
|
|
111
|
+
<h2 className="sr-only">Session</h2>
|
|
112
|
+
<SessionHeader {...args} />
|
|
113
|
+
</div>
|
|
114
|
+
),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** SessionHeader above ChatGreeting — the standard empty first-run agent session. */
|
|
118
|
+
export const WithChatGreeting: Story = {
|
|
119
|
+
parameters: { layout: "fullscreen" },
|
|
120
|
+
render: (args) => (
|
|
121
|
+
<div className="grid min-h-[32rem] place-items-center bg-background px-4 py-10 text-foreground">
|
|
122
|
+
<div className="w-full max-w-md space-y-8">
|
|
123
|
+
<SessionHeader {...args} />
|
|
124
|
+
<ChatGreeting title="Ready when you are" orb={false} level={3} />
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
),
|
|
128
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { SessionHeader } from "./session-header";
|
|
5
|
+
|
|
6
|
+
const CAPABILITIES = [
|
|
7
|
+
{ label: "Web search", description: "Look things up while we talk." },
|
|
8
|
+
{ label: "Code execution" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const WHATS_NEW = [{ label: "Faster file search", href: "https://example.com/changelog" }];
|
|
12
|
+
|
|
13
|
+
const QUICK_ACTIONS = [
|
|
14
|
+
{ label: "New chat", keyHint: "⌘N" },
|
|
15
|
+
{ label: "Open workspace", keyHint: "⌘O" },
|
|
16
|
+
{ label: "Settings" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
describe("SessionHeader", () => {
|
|
20
|
+
it("renders only the title with no empty sections or stray separators (acceptance criterion)", () => {
|
|
21
|
+
const { container } = render(<SessionHeader title="Codex" />);
|
|
22
|
+
|
|
23
|
+
expect(screen.getByRole("heading")).toHaveTextContent("Codex");
|
|
24
|
+
|
|
25
|
+
// No section for a prop that was never supplied.
|
|
26
|
+
expect(container.querySelector('[data-slot="session-header-meta"]')).toBeNull();
|
|
27
|
+
expect(container.querySelector('[data-slot="session-header-workspace"]')).toBeNull();
|
|
28
|
+
expect(container.querySelector('[data-slot="session-header-capabilities"]')).toBeNull();
|
|
29
|
+
expect(container.querySelector('[data-slot="session-header-whats-new"]')).toBeNull();
|
|
30
|
+
expect(container.querySelector('[data-slot="session-header-quick-actions"]')).toBeNull();
|
|
31
|
+
|
|
32
|
+
// No separator is ever rendered by this component — nothing left "stray"
|
|
33
|
+
// when the sections around it are absent.
|
|
34
|
+
expect(container.querySelector('[role="separator"]')).toBeNull();
|
|
35
|
+
expect(screen.queryByRole("separator")).toBeNull();
|
|
36
|
+
|
|
37
|
+
// No list/group scaffolding renders without content behind it.
|
|
38
|
+
expect(container.querySelectorAll("ul")).toHaveLength(0);
|
|
39
|
+
expect(container.querySelector('[role="group"]')).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders nothing at all — not even the identity row — with no props supplied", () => {
|
|
43
|
+
const { container } = render(<SessionHeader />);
|
|
44
|
+
|
|
45
|
+
const root = container.firstElementChild as HTMLElement;
|
|
46
|
+
expect(root).toHaveAttribute("data-slot", "session-header");
|
|
47
|
+
expect(root.querySelector('[data-slot="session-header-identity"]')).toBeNull();
|
|
48
|
+
expect(screen.queryByRole("heading")).not.toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("renders every quick action as a focusable button reachable by role and accessible name", async () => {
|
|
52
|
+
const user = userEvent.setup();
|
|
53
|
+
const onSelect = vi.fn();
|
|
54
|
+
const quickActions = [
|
|
55
|
+
{ label: "New chat", keyHint: "⌘N", onSelect },
|
|
56
|
+
...QUICK_ACTIONS.slice(1),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
render(
|
|
60
|
+
<SessionHeader
|
|
61
|
+
title="Codex"
|
|
62
|
+
model="gpt-5.1-codex"
|
|
63
|
+
workspace="~/dev/acme/api-gateway"
|
|
64
|
+
version="v2.4.0"
|
|
65
|
+
capabilities={CAPABILITIES}
|
|
66
|
+
whatsNew={WHATS_NEW}
|
|
67
|
+
quickActions={quickActions}
|
|
68
|
+
/>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
for (const action of quickActions) {
|
|
72
|
+
const button = screen.getByRole("button", { name: new RegExp(action.label) });
|
|
73
|
+
expect(button.tagName).toBe("BUTTON");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await user.click(screen.getByRole("button", { name: /New chat/ }));
|
|
77
|
+
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("renders shortcut hints via the Kbd primitive", () => {
|
|
81
|
+
render(<SessionHeader title="Codex" quickActions={QUICK_ACTIONS} />);
|
|
82
|
+
|
|
83
|
+
const button = screen.getByRole("button", { name: /New chat/ });
|
|
84
|
+
expect(button.querySelector("kbd")).toHaveTextContent("⌘N");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("renders model and version together, separated, when both are supplied", () => {
|
|
88
|
+
render(<SessionHeader title="Codex" model="gpt-5.1-codex" version="v2.4.0" />);
|
|
89
|
+
|
|
90
|
+
expect(screen.getByText("gpt-5.1-codex · v2.4.0")).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("truncates a long workspace path instead of overflowing", () => {
|
|
94
|
+
const workspace =
|
|
95
|
+
"~/dev/acme/monorepo/services/platform/billing/api-gateway/src/handlers/webhooks";
|
|
96
|
+
const { container } = render(<SessionHeader title="Codex" workspace={workspace} />);
|
|
97
|
+
|
|
98
|
+
const workspaceRow = container.querySelector('[data-slot="session-header-workspace"]');
|
|
99
|
+
const label = workspaceRow?.querySelector("span");
|
|
100
|
+
expect(label).toHaveTextContent(workspace);
|
|
101
|
+
expect(label?.className.split(" ")).toEqual(
|
|
102
|
+
expect.arrayContaining(["truncate", "min-w-0", "flex-1"]),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("defaults to a level-2 heading and honors an explicit level override", () => {
|
|
107
|
+
const { rerender } = render(<SessionHeader title="Codex" />);
|
|
108
|
+
expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument();
|
|
109
|
+
|
|
110
|
+
rerender(<SessionHeader title="Codex" level={3} />);
|
|
111
|
+
expect(screen.getByRole("heading", { level: 3 })).toBeInTheDocument();
|
|
112
|
+
expect(screen.queryByRole("heading", { level: 2 })).not.toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("renders capability and what's-new list items only when their arrays are non-empty", () => {
|
|
116
|
+
render(<SessionHeader title="Codex" capabilities={CAPABILITIES} whatsNew={WHATS_NEW} />);
|
|
117
|
+
|
|
118
|
+
expect(screen.getByText("Web search")).toBeInTheDocument();
|
|
119
|
+
expect(screen.getByText("Look things up while we talk.")).toBeInTheDocument();
|
|
120
|
+
expect(screen.getByRole("link", { name: "Faster file search" })).toHaveAttribute(
|
|
121
|
+
"href",
|
|
122
|
+
"https://example.com/changelog",
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("merges a custom className onto the root", () => {
|
|
127
|
+
const { container } = render(<SessionHeader title="Codex" className="custom-class" />);
|
|
128
|
+
|
|
129
|
+
expect(container.firstElementChild).toHaveClass("custom-class");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("forwards a ref to the root element", () => {
|
|
133
|
+
const ref = vi.fn();
|
|
134
|
+
render(<SessionHeader ref={ref} title="Codex" />);
|
|
135
|
+
|
|
136
|
+
expect(ref).toHaveBeenCalledWith(expect.any(HTMLDivElement));
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from "react";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import { Folder, Sparkles } from "lucide-react";
|
|
6
|
+
import { BrandLogo } from "@elabs-ai/components-icons";
|
|
7
|
+
import {
|
|
8
|
+
Button,
|
|
9
|
+
cn,
|
|
10
|
+
Heading,
|
|
11
|
+
Kbd,
|
|
12
|
+
useLocale,
|
|
13
|
+
type HeadingLevel,
|
|
14
|
+
type SessionCapability,
|
|
15
|
+
type SessionQuickAction,
|
|
16
|
+
type SessionWhatsNewItem,
|
|
17
|
+
} from "@elabs-ai/components-ui";
|
|
18
|
+
|
|
19
|
+
// `SessionCapability`, `SessionWhatsNewItem` and `SessionQuickAction` moved
|
|
20
|
+
// to `@elabs-ai/components-ui` (`lib/session-launch.ts`) — the terminal CLI
|
|
21
|
+
// look-alike family's own session header (issue #117) reuses the same
|
|
22
|
+
// vocabulary shape, and `@elabs-ai/components-ai`/
|
|
23
|
+
// `@elabs-ai/components-terminal` are layer-2 DAG siblings that may not
|
|
24
|
+
// import each other (T0; see
|
|
25
|
+
// docs/decisions/2026-09-01-brainless-adoption-architecture.md § 4). Imported
|
|
26
|
+
// above; NOT re-exported from here — a consumer imports these from
|
|
27
|
+
// `@elabs-ai/components-ui`.
|
|
28
|
+
|
|
29
|
+
export interface SessionHeaderProps extends ComponentProps<"div"> {
|
|
30
|
+
/**
|
|
31
|
+
* Session/agent name, e.g. "Codex". Anchors the card — every other section
|
|
32
|
+
* is independently optional and renders nothing when omitted.
|
|
33
|
+
*/
|
|
34
|
+
title?: string;
|
|
35
|
+
/** Active model, e.g. "gpt-5.1-codex". */
|
|
36
|
+
model?: string;
|
|
37
|
+
/** Working directory / project path. Truncates instead of overflowing. */
|
|
38
|
+
workspace?: string;
|
|
39
|
+
/** Build/version string, e.g. "v2.4.0". */
|
|
40
|
+
version?: string;
|
|
41
|
+
/** What this session can reach. */
|
|
42
|
+
capabilities?: SessionCapability[];
|
|
43
|
+
/** Recent changes, shown as a short list. */
|
|
44
|
+
whatsNew?: SessionWhatsNewItem[];
|
|
45
|
+
/** Shortcut actions, rendered as real buttons with optional shortcut hints. */
|
|
46
|
+
quickActions?: SessionQuickAction[];
|
|
47
|
+
/**
|
|
48
|
+
* Semantic heading level for `title`. Default `2` (a session header
|
|
49
|
+
* typically sits above a `ChatGreeting`'s level-1 headline, or inside a
|
|
50
|
+
* page that already owns its own `<h1>`).
|
|
51
|
+
*/
|
|
52
|
+
level?: HeadingLevel;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* SessionHeader — the launch card for an empty agent session: model,
|
|
57
|
+
* workspace, version, capabilities, what's new, and quick actions with
|
|
58
|
+
* shortcut hints. Pair it above `ChatGreeting` (or inside `ChatShell`'s
|
|
59
|
+
* `header` slot) for the standard first-run agent session state.
|
|
60
|
+
*
|
|
61
|
+
* Distinct from `ChatGreeting`, which is the centered display-scale greeting
|
|
62
|
+
* for a general assistant chat — `SessionHeader` answers different questions
|
|
63
|
+
* (what workspace am I in, what model, what can this thing reach, what
|
|
64
|
+
* changed) that a greeting has no home for.
|
|
65
|
+
*
|
|
66
|
+
* **Every section is independently optional.** With only `title` supplied,
|
|
67
|
+
* the card renders the identity row alone — no empty section scaffolding,
|
|
68
|
+
* no stray separators.
|
|
69
|
+
*/
|
|
70
|
+
export const SessionHeader = forwardRef<HTMLDivElement, SessionHeaderProps>(function SessionHeader(
|
|
71
|
+
{
|
|
72
|
+
title,
|
|
73
|
+
model,
|
|
74
|
+
workspace,
|
|
75
|
+
version,
|
|
76
|
+
capabilities,
|
|
77
|
+
whatsNew,
|
|
78
|
+
quickActions,
|
|
79
|
+
level = 2,
|
|
80
|
+
className,
|
|
81
|
+
...props
|
|
82
|
+
},
|
|
83
|
+
ref,
|
|
84
|
+
) {
|
|
85
|
+
const { t } = useLocale();
|
|
86
|
+
const hasMeta = model != null || version != null;
|
|
87
|
+
const hasIdentity = title != null || hasMeta || workspace != null;
|
|
88
|
+
const hasCapabilities = (capabilities?.length ?? 0) > 0;
|
|
89
|
+
const hasWhatsNew = (whatsNew?.length ?? 0) > 0;
|
|
90
|
+
const hasQuickActions = (quickActions?.length ?? 0) > 0;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
ref={ref}
|
|
95
|
+
data-slot="session-header"
|
|
96
|
+
className={cn(
|
|
97
|
+
"min-w-0 rounded-xl border bg-card p-5 text-card-foreground shadow-xs",
|
|
98
|
+
className,
|
|
99
|
+
)}
|
|
100
|
+
{...props}
|
|
101
|
+
>
|
|
102
|
+
<div className="flex min-w-0 flex-col gap-4">
|
|
103
|
+
{hasIdentity ? (
|
|
104
|
+
<div data-slot="session-header-identity" className="flex min-w-0 flex-col gap-1">
|
|
105
|
+
{title != null ? (
|
|
106
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
107
|
+
{/* Decorative — the visible Heading already carries the
|
|
108
|
+
accessible name, so the mark is hidden from AT rather than
|
|
109
|
+
announced a second time via BrandLogo's own `role="img"`. */}
|
|
110
|
+
<span aria-hidden="true" className="shrink-0">
|
|
111
|
+
<BrandLogo variant="mark" height={24} title={title} />
|
|
112
|
+
</span>
|
|
113
|
+
<Heading
|
|
114
|
+
data-slot="session-header-title"
|
|
115
|
+
level={level}
|
|
116
|
+
size="subtitle"
|
|
117
|
+
className="min-w-0 truncate"
|
|
118
|
+
>
|
|
119
|
+
{title}
|
|
120
|
+
</Heading>
|
|
121
|
+
</div>
|
|
122
|
+
) : null}
|
|
123
|
+
|
|
124
|
+
{hasMeta ? (
|
|
125
|
+
<p
|
|
126
|
+
data-slot="session-header-meta"
|
|
127
|
+
className="min-w-0 truncate text-meta text-muted-foreground"
|
|
128
|
+
>
|
|
129
|
+
{model}
|
|
130
|
+
{model != null && version != null ? " · " : null}
|
|
131
|
+
{version}
|
|
132
|
+
</p>
|
|
133
|
+
) : null}
|
|
134
|
+
|
|
135
|
+
{workspace != null ? (
|
|
136
|
+
<div
|
|
137
|
+
data-slot="session-header-workspace"
|
|
138
|
+
className="flex min-w-0 items-center gap-1.5 text-body text-muted-foreground"
|
|
139
|
+
>
|
|
140
|
+
<Folder aria-hidden="true" className="size-3.5 shrink-0" />
|
|
141
|
+
<span className="min-w-0 flex-1 truncate">{workspace}</span>
|
|
142
|
+
</div>
|
|
143
|
+
) : null}
|
|
144
|
+
</div>
|
|
145
|
+
) : null}
|
|
146
|
+
|
|
147
|
+
{hasCapabilities ? (
|
|
148
|
+
<div data-slot="session-header-capabilities" className="flex min-w-0 flex-col gap-2">
|
|
149
|
+
<p className="text-meta font-medium text-muted-foreground">
|
|
150
|
+
{t("ai.sessionHeader.capabilities")}
|
|
151
|
+
</p>
|
|
152
|
+
<ul className="flex min-w-0 flex-col gap-2">
|
|
153
|
+
{capabilities?.map((capability) => (
|
|
154
|
+
<li
|
|
155
|
+
key={capability.label}
|
|
156
|
+
data-slot="session-header-capability"
|
|
157
|
+
className="flex min-w-0 items-start gap-2"
|
|
158
|
+
>
|
|
159
|
+
{capability.icon != null ? (
|
|
160
|
+
<span
|
|
161
|
+
aria-hidden="true"
|
|
162
|
+
className="mt-0.5 shrink-0 text-muted-foreground [&_svg]:size-4"
|
|
163
|
+
>
|
|
164
|
+
{capability.icon}
|
|
165
|
+
</span>
|
|
166
|
+
) : null}
|
|
167
|
+
<div className="min-w-0">
|
|
168
|
+
<p className="min-w-0 truncate text-body text-foreground">{capability.label}</p>
|
|
169
|
+
{capability.description != null ? (
|
|
170
|
+
<p className="min-w-0 truncate text-meta text-muted-foreground">
|
|
171
|
+
{capability.description}
|
|
172
|
+
</p>
|
|
173
|
+
) : null}
|
|
174
|
+
</div>
|
|
175
|
+
</li>
|
|
176
|
+
))}
|
|
177
|
+
</ul>
|
|
178
|
+
</div>
|
|
179
|
+
) : null}
|
|
180
|
+
|
|
181
|
+
{hasWhatsNew ? (
|
|
182
|
+
<div data-slot="session-header-whats-new" className="flex min-w-0 flex-col gap-2">
|
|
183
|
+
<p className="flex items-center gap-1.5 text-meta font-medium text-muted-foreground">
|
|
184
|
+
<Sparkles aria-hidden="true" className="size-3.5 shrink-0" />
|
|
185
|
+
{t("ai.sessionHeader.whatsNew")}
|
|
186
|
+
</p>
|
|
187
|
+
<ul className="flex min-w-0 flex-col gap-1">
|
|
188
|
+
{whatsNew?.map((item) => (
|
|
189
|
+
<li
|
|
190
|
+
key={item.label}
|
|
191
|
+
data-slot="session-header-whats-new-item"
|
|
192
|
+
className="min-w-0 truncate text-body"
|
|
193
|
+
>
|
|
194
|
+
{item.href != null ? (
|
|
195
|
+
<a
|
|
196
|
+
href={item.href}
|
|
197
|
+
className="text-primary-text underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
198
|
+
>
|
|
199
|
+
{item.label}
|
|
200
|
+
</a>
|
|
201
|
+
) : (
|
|
202
|
+
<span className="text-foreground">{item.label}</span>
|
|
203
|
+
)}
|
|
204
|
+
</li>
|
|
205
|
+
))}
|
|
206
|
+
</ul>
|
|
207
|
+
</div>
|
|
208
|
+
) : null}
|
|
209
|
+
|
|
210
|
+
{hasQuickActions ? (
|
|
211
|
+
<div
|
|
212
|
+
data-slot="session-header-quick-actions"
|
|
213
|
+
role="group"
|
|
214
|
+
aria-label={t("ai.sessionHeader.quickActions")}
|
|
215
|
+
className="flex min-w-0 flex-wrap gap-2"
|
|
216
|
+
>
|
|
217
|
+
{quickActions?.map((action) => (
|
|
218
|
+
<Button
|
|
219
|
+
key={action.label}
|
|
220
|
+
type="button"
|
|
221
|
+
data-slot="session-header-quick-action"
|
|
222
|
+
variant="outline"
|
|
223
|
+
size="sm"
|
|
224
|
+
onClick={action.onSelect}
|
|
225
|
+
>
|
|
226
|
+
<span>{action.label}</span>
|
|
227
|
+
{/* An explicit space text node (not just flex `gap-2`) keeps
|
|
228
|
+
"New chat"/"⌘N" apart in the button's computed accessible
|
|
229
|
+
name, not only its visual layout. */}
|
|
230
|
+
{action.keyHint != null ? (
|
|
231
|
+
<>
|
|
232
|
+
{" "}
|
|
233
|
+
<Kbd>{action.keyHint}</Kbd>
|
|
234
|
+
</>
|
|
235
|
+
) : null}
|
|
236
|
+
</Button>
|
|
237
|
+
))}
|
|
238
|
+
</div>
|
|
239
|
+
) : null}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { TokenUsage, TokenUsageTrigger } from "./token-usage";
|
|
3
|
+
import { SessionStatusBar } from "./session-status-bar";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "AI/SessionStatusBar",
|
|
7
|
+
component: SessionStatusBar,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "padded",
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
"The CHAT status row; the console skin is `Terminal/TerminalStatusBar`, with the same " +
|
|
14
|
+
"prop names — see " +
|
|
15
|
+
"[Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). " +
|
|
16
|
+
"The ambient session row: workspace, branch, model and integration-connection progress (#105). Every segment renders only when its prop is supplied — an all-empty bar renders nothing at all. Docks `TokenUsage` via `children` rather than re-implementing token-usage/cost maths.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
tags: ["autodocs"],
|
|
21
|
+
} satisfies Meta<typeof SessionStatusBar>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof meta>;
|
|
25
|
+
|
|
26
|
+
/** Every ambient segment, with `TokenUsage` docked at the trailing edge. */
|
|
27
|
+
export const Default: Story = {
|
|
28
|
+
render: () => (
|
|
29
|
+
<SessionStatusBar branch="main" model="Claude Opus 4" workspace="brand-ui">
|
|
30
|
+
<TokenUsage maxTokens={8000} usedTokens={2400}>
|
|
31
|
+
<TokenUsageTrigger />
|
|
32
|
+
</TokenUsage>
|
|
33
|
+
</SessionStatusBar>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Only a workspace name — every other segment is independently optional. */
|
|
38
|
+
export const WorkspaceOnly: Story = {
|
|
39
|
+
render: () => <SessionStatusBar workspace="brand-ui" />,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Integrations mid-handshake — a spinner replaces the connection icon. */
|
|
43
|
+
export const ConnectingIntegrations: Story = {
|
|
44
|
+
render: () => (
|
|
45
|
+
<SessionStatusBar
|
|
46
|
+
branch="feature/turn-status"
|
|
47
|
+
connections={{ connected: 1, total: 4, connecting: true }}
|
|
48
|
+
workspace="brand-ui"
|
|
49
|
+
/>
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Integrations resolved — a static connected/total count. */
|
|
54
|
+
export const IntegrationsConnected: Story = {
|
|
55
|
+
render: () => (
|
|
56
|
+
<SessionStatusBar
|
|
57
|
+
branch="feature/turn-status"
|
|
58
|
+
connections={{ connected: 4, total: 4 }}
|
|
59
|
+
model="Claude Opus 4"
|
|
60
|
+
workspace="brand-ui"
|
|
61
|
+
/>
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** No segment supplied — the bar renders nothing at all, not an empty shell. */
|
|
66
|
+
export const Empty: Story = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<div className="rounded-md border border-dashed p-4 text-center text-meta text-muted-foreground">
|
|
69
|
+
<SessionStatusBar />
|
|
70
|
+
Nothing rendered above this line.
|
|
71
|
+
</div>
|
|
72
|
+
),
|
|
73
|
+
};
|