@elabs-ai/components-ai 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +62 -12
  2. package/dist/{_audio-player-media-chrome-KA5DY54G.js → _audio-player-media-chrome-T3XVXWRZ.js} +8 -4
  3. package/dist/_audio-player-media-chrome-T3XVXWRZ.js.map +1 -0
  4. package/dist/{_flow-boundary-D63PJ65S.js → _flow-boundary-SHNWLQG5.js} +32 -43
  5. package/dist/_flow-boundary-SHNWLQG5.js.map +1 -0
  6. package/dist/{_persona-rive-RFR2EUWP.js → _persona-rive-JEG44YHX.js} +9 -5
  7. package/dist/_persona-rive-JEG44YHX.js.map +1 -0
  8. package/dist/index.d.ts +1152 -218
  9. package/dist/index.js +5601 -3241
  10. package/dist/index.js.map +1 -1
  11. package/package.json +30 -15
  12. package/src/__contract__/audio-visualizer.contract.test.tsx +49 -0
  13. package/src/__contract__/chat-shell.contract.test.tsx +49 -0
  14. package/src/__contract__/grouped-parts.contract.test.tsx +49 -0
  15. package/src/__contract__/image.contract.test.tsx +49 -0
  16. package/src/__contract__/markdown-view.contract.test.tsx +49 -0
  17. package/src/__contract__/message-feedback.contract.test.tsx +49 -0
  18. package/src/__contract__/message-form.contract.test.tsx +49 -0
  19. package/src/__contract__/message-table.contract.test.tsx +49 -0
  20. package/src/__contract__/model-provider-logo.contract.test.tsx +49 -0
  21. package/src/__contract__/persona.contract.test.tsx +49 -0
  22. package/src/__contract__/prompt-input-effort.contract.test.tsx +49 -0
  23. package/src/__contract__/prompt-input-mode.contract.test.tsx +49 -0
  24. package/src/_audio-player-media-chrome.tsx +102 -15
  25. package/src/_chat-shell-rail.tsx +2 -2
  26. package/src/_flow-boundary.tsx +68 -49
  27. package/src/_lazy-boundary-conformance.ts +38 -0
  28. package/src/_lazy-cjk.test.ts +43 -0
  29. package/src/_lazy-cjk.ts +73 -0
  30. package/src/_lazy-engine-boundary.tsx +61 -0
  31. package/src/_lazy-math.test.ts +63 -0
  32. package/src/_lazy-math.ts +90 -0
  33. package/src/_lazy-mermaid-absent.test.ts +53 -0
  34. package/src/_lazy-mermaid.test.ts +15 -0
  35. package/src/_lazy-mermaid.ts +24 -1
  36. package/src/_mermaid-error-panel.test.tsx +50 -0
  37. package/src/_mermaid-error-panel.tsx +66 -0
  38. package/src/_persona-rive.tsx +62 -10
  39. package/src/_streamdown-i18n.ts +94 -22
  40. package/src/_streamdown-safety.ts +170 -0
  41. package/src/_theme-scope-store.test.ts +83 -0
  42. package/src/_theme-scope-store.ts +103 -0
  43. package/src/agent-event.stories.tsx +97 -0
  44. package/src/agent-event.test.tsx +145 -0
  45. package/src/agent-event.tsx +187 -0
  46. package/src/agent-timeline.stories.tsx +9 -1
  47. package/src/agent.test.tsx +47 -0
  48. package/src/agent.tsx +43 -24
  49. package/src/agentic-workspace.stories.tsx +27 -20
  50. package/src/artifact.tsx +17 -14
  51. package/src/asset-preview.test.tsx +40 -0
  52. package/src/asset-preview.tsx +83 -12
  53. package/src/attachments.tsx +7 -4
  54. package/src/audio-player.test.tsx +78 -0
  55. package/src/audio-player.tsx +243 -56
  56. package/src/audio-visualizer.stories.tsx +126 -0
  57. package/src/audio-visualizer.test.tsx +438 -0
  58. package/src/audio-visualizer.tsx +367 -0
  59. package/src/canvas.stories.tsx +150 -1
  60. package/src/chat-shell.stories.tsx +18 -3
  61. package/src/chat.stories.tsx +16 -2
  62. package/src/code-block.stories.tsx +9 -1
  63. package/src/code-block.test.tsx +100 -1
  64. package/src/code-block.tsx +166 -103
  65. package/src/commit.tsx +30 -41
  66. package/src/composer.stories.tsx +372 -9
  67. package/src/composer.test.tsx +357 -6
  68. package/src/composer.tsx +217 -35
  69. package/src/confirmation.stories.tsx +72 -1
  70. package/src/confirmation.test.tsx +216 -2
  71. package/src/confirmation.tsx +264 -4
  72. package/src/context-panel.stories.tsx +9 -1
  73. package/src/context-panel.tsx +3 -2
  74. package/src/conversation.stories.tsx +86 -2
  75. package/src/conversation.test.tsx +66 -0
  76. package/src/conversation.tsx +52 -9
  77. package/src/diff-view.stories.tsx +196 -0
  78. package/src/diff-view.test.tsx +238 -0
  79. package/src/diff-view.tsx +697 -0
  80. package/src/environment-variables.tsx +20 -37
  81. package/src/file-tree.test.tsx +21 -0
  82. package/src/file-tree.tsx +12 -2
  83. package/src/gallery.tsx +4 -2
  84. package/src/index.ts +14 -4
  85. package/src/inline-citation.tsx +5 -5
  86. package/src/jsx-preview.stories.tsx +2 -2
  87. package/src/jsx-preview.tsx +151 -43
  88. package/src/markdown-view.stories.tsx +92 -1
  89. package/src/markdown-view.test.tsx +237 -2
  90. package/src/markdown-view.tsx +157 -6
  91. package/src/message-compare.stories.tsx +175 -0
  92. package/src/message-compare.test.tsx +207 -0
  93. package/src/message-compare.tsx +453 -0
  94. package/src/message-form.stories.tsx +65 -4
  95. package/src/message-form.test.tsx +8 -2
  96. package/src/message-form.tsx +15 -7
  97. package/src/message-table.stories.tsx +2 -2
  98. package/src/message-table.test.tsx +7 -0
  99. package/src/message-table.tsx +8 -4
  100. package/src/message.stories.tsx +9 -1
  101. package/src/message.test.tsx +247 -1
  102. package/src/message.tsx +125 -15
  103. package/src/microcopy.test.tsx +40 -0
  104. package/src/{model-selector.stories.tsx → model-provider-logo.stories.tsx} +17 -8
  105. package/src/model-provider-logo.test.tsx +109 -0
  106. package/src/model-provider-logo.tsx +183 -0
  107. package/src/open-in-chat.tsx +50 -29
  108. package/src/package-info.tsx +12 -12
  109. package/src/permission-mode-select.stories.tsx +82 -0
  110. package/src/permission-mode-select.test.tsx +100 -0
  111. package/src/permission-mode-select.tsx +137 -0
  112. package/src/persona-missing-peer.test.tsx +54 -0
  113. package/src/persona.tsx +68 -22
  114. package/src/plan.stories.tsx +166 -0
  115. package/src/plan.test.tsx +267 -0
  116. package/src/plan.tsx +182 -20
  117. package/src/prompt-input-effort.stories.tsx +123 -0
  118. package/src/prompt-input-effort.test.tsx +83 -0
  119. package/src/prompt-input-effort.tsx +136 -0
  120. package/src/prompt-input-mode.stories.tsx +108 -0
  121. package/src/prompt-input-mode.test.tsx +99 -0
  122. package/src/prompt-input-mode.tsx +169 -0
  123. package/src/prompt-input-slash.stories.tsx +211 -0
  124. package/src/prompt-input-slash.test.tsx +262 -0
  125. package/src/prompt-input-slash.tsx +541 -0
  126. package/src/prompt-input.stories.tsx +2 -2
  127. package/src/prompt-input.test.tsx +67 -1
  128. package/src/prompt-input.tsx +42 -4
  129. package/src/queue.tsx +4 -4
  130. package/src/reasoning.tsx +42 -17
  131. package/src/sandbox.stories.tsx +9 -1
  132. package/src/sandbox.tsx +3 -3
  133. package/src/schema-display.test.tsx +56 -0
  134. package/src/schema-display.tsx +71 -37
  135. package/src/selection-toolbar.stories.tsx +9 -1
  136. package/src/session-header.stories.tsx +128 -0
  137. package/src/session-header.test.tsx +138 -0
  138. package/src/session-header.tsx +243 -0
  139. package/src/session-status-bar.stories.tsx +73 -0
  140. package/src/session-status-bar.test.tsx +94 -0
  141. package/src/session-status-bar.tsx +165 -0
  142. package/src/snippet.stories.tsx +9 -1
  143. package/src/snippet.test.tsx +6 -2
  144. package/src/snippet.tsx +14 -34
  145. package/src/speech-input.test.tsx +109 -0
  146. package/src/speech-input.tsx +31 -3
  147. package/src/stack-trace.tsx +24 -36
  148. package/src/streamdown-i18n.test.tsx +24 -1
  149. package/src/task.stories.tsx +13 -3
  150. package/src/templates-ai-assistant.stories.tsx +21 -1
  151. package/src/test-results.tsx +47 -27
  152. package/src/token-usage.stories.tsx +24 -0
  153. package/src/token-usage.test.tsx +92 -0
  154. package/src/{context.tsx → token-usage.tsx} +73 -59
  155. package/src/tool-result-card.stories.tsx +9 -1
  156. package/src/tool.stories.tsx +13 -3
  157. package/src/tool.test.tsx +65 -0
  158. package/src/tool.tsx +73 -30
  159. package/src/transcription.tsx +1 -1
  160. package/src/turn-status.stories.tsx +124 -0
  161. package/src/turn-status.test.tsx +74 -0
  162. package/src/turn-status.tsx +174 -0
  163. package/src/use-audio-level.ts +104 -0
  164. package/src/voice-selector.tsx +5 -5
  165. package/src/web-preview.test.tsx +51 -1
  166. package/src/web-preview.tsx +46 -10
  167. package/dist/_audio-player-media-chrome-KA5DY54G.js.map +0 -1
  168. package/dist/_flow-boundary-D63PJ65S.js.map +0 -1
  169. package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +0 -11
  170. package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +0 -1
  171. package/dist/_persona-rive-RFR2EUWP.js.map +0 -1
  172. package/src/_interactive-terminal-xterm.ts +0 -32
  173. package/src/blocks-ai-composer.stories.tsx +0 -83
  174. package/src/context.stories.tsx +0 -16
  175. package/src/interactive-terminal.stories.tsx +0 -165
  176. package/src/interactive-terminal.test.tsx +0 -448
  177. package/src/interactive-terminal.tsx +0 -444
  178. package/src/model-selector.test.tsx +0 -55
  179. package/src/model-selector.tsx +0 -225
  180. package/src/terminal.tsx +0 -244
@@ -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) => (
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { StatusBadge, StatusIcon, type Status } from "@elabs-ai/components-ui";
3
+ import { StatusBadge, StatusIcon, useLocale, type Status } from "@elabs-ai/components-ui";
4
4
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@elabs-ai/components-ui";
5
5
  import { cn } from "@elabs-ai/components-ui/lib/cn";
6
6
  import { ChevronRightIcon } from "lucide-react";
@@ -64,7 +64,7 @@ export const TestResultsDuration = ({
64
64
 
65
65
  return (
66
66
  <span
67
- className={cn("shrink-0 text-muted-foreground text-sm tabular-nums", className)}
67
+ className={cn("shrink-0 text-muted-foreground text-body tabular-nums", className)}
68
68
  {...props}
69
69
  >
70
70
  {children ?? formatDuration(summary.duration)}
@@ -76,6 +76,7 @@ export type TestResultsSummaryProps = HTMLAttributes<HTMLDivElement>;
76
76
 
77
77
  export const TestResultsSummary = ({ className, children, ...props }: TestResultsSummaryProps) => {
78
78
  const { summary } = useContext(TestResultsContext);
79
+ const { t } = useLocale();
79
80
 
80
81
  if (!summary) {
81
82
  return null;
@@ -86,16 +87,16 @@ export const TestResultsSummary = ({ className, children, ...props }: TestResult
86
87
  {children ?? (
87
88
  <>
88
89
  <StatusBadge size="sm" status="complete">
89
- {summary.passed} passed
90
+ {t("ai.testResults.passedCount", { count: summary.passed })}
90
91
  </StatusBadge>
91
92
  {summary.failed > 0 && (
92
93
  <StatusBadge size="sm" status="failed">
93
- {summary.failed} failed
94
+ {t("ai.testResults.failedCount", { count: summary.failed })}
94
95
  </StatusBadge>
95
96
  )}
96
97
  {summary.skipped > 0 && (
97
98
  <StatusBadge size="sm" status="skipped">
98
- {summary.skipped} skipped
99
+ {t("ai.testResults.skippedCount", { count: summary.skipped })}
99
100
  </StatusBadge>
100
101
  )}
101
102
  </>
@@ -134,6 +135,7 @@ export const TestResultsProgress = ({
134
135
  ...props
135
136
  }: TestResultsProgressProps) => {
136
137
  const { summary } = useContext(TestResultsContext);
138
+ const { t, formatNumber } = useLocale();
137
139
 
138
140
  if (!summary) {
139
141
  return null;
@@ -156,11 +158,14 @@ export const TestResultsProgress = ({
156
158
  style={{ width: `${failedPercent}%` }}
157
159
  />
158
160
  </div>
159
- <div className="flex justify-between text-muted-foreground text-xs tabular-nums">
161
+ <div className="flex justify-between text-muted-foreground text-meta tabular-nums">
160
162
  <span>
161
- {summary.passed}/{summary.total} tests passed
163
+ {t("ai.testResults.testsPassed", { passed: summary.passed, total: summary.total })}
164
+ </span>
165
+ <span>
166
+ {formatNumber(passedPercent, { maximumFractionDigits: 0 })}
167
+ {"%"}
162
168
  </span>
163
- <span>{passedPercent.toFixed(0)}%</span>
164
169
  </div>
165
170
  </>
166
171
  )}
@@ -247,7 +252,7 @@ export const TestSuiteName = ({ className, children, ...props }: TestSuiteNamePr
247
252
  >
248
253
  <ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform duration-fast ease-standard motion-reduce:transition-none group-data-[state=open]:rotate-90" />
249
254
  <TestStatusIcon status={status} />
250
- <span className="min-w-0 flex-1 truncate font-medium text-sm">{children ?? name}</span>
255
+ <span className="min-w-0 flex-1 truncate font-medium text-body">{children ?? name}</span>
251
256
  </CollapsibleTrigger>
252
257
  );
253
258
  };
@@ -265,20 +270,35 @@ export const TestSuiteStats = ({
265
270
  className,
266
271
  children,
267
272
  ...props
268
- }: TestSuiteStatsProps) => (
269
- <div
270
- className={cn("ms-auto flex shrink-0 items-center gap-3 text-xs tabular-nums", className)}
271
- {...props}
272
- >
273
- {children ?? (
274
- <>
275
- {passed > 0 && <span className="text-success-text">{passed} passed</span>}
276
- {failed > 0 && <span className="text-destructive-text">{failed} failed</span>}
277
- {skipped > 0 && <span className="text-muted-foreground">{skipped} skipped</span>}
278
- </>
279
- )}
280
- </div>
281
- );
273
+ }: TestSuiteStatsProps) => {
274
+ const { t } = useLocale();
275
+ return (
276
+ <div
277
+ className={cn("ms-auto flex shrink-0 items-center gap-3 text-meta tabular-nums", className)}
278
+ {...props}
279
+ >
280
+ {children ?? (
281
+ <>
282
+ {passed > 0 && (
283
+ <span className="text-success-text">
284
+ {t("ai.testResults.passedCount", { count: passed })}
285
+ </span>
286
+ )}
287
+ {failed > 0 && (
288
+ <span className="text-destructive-text">
289
+ {t("ai.testResults.failedCount", { count: failed })}
290
+ </span>
291
+ )}
292
+ {skipped > 0 && (
293
+ <span className="text-muted-foreground">
294
+ {t("ai.testResults.skippedCount", { count: skipped })}
295
+ </span>
296
+ )}
297
+ </>
298
+ )}
299
+ </div>
300
+ );
301
+ };
282
302
 
283
303
  export type TestSuiteContentProps = ComponentProps<typeof CollapsibleContent>;
284
304
 
@@ -328,7 +348,7 @@ export const TestDuration = ({ className, children, ...props }: TestDurationProp
328
348
 
329
349
  return (
330
350
  <span
331
- className={cn("shrink-0 text-muted-foreground text-xs tabular-nums", className)}
351
+ className={cn("shrink-0 text-muted-foreground text-meta tabular-nums", className)}
332
352
  {...props}
333
353
  >
334
354
  {children ?? `${duration}ms`}
@@ -365,7 +385,7 @@ export const Test = ({ name, status, duration, className, children, ...props }:
365
385
  return (
366
386
  <TestContext.Provider value={contextValue}>
367
387
  <div
368
- className={cn("flex min-w-0 items-center gap-2 ps-10 pe-4 py-2 text-sm", className)}
388
+ className={cn("flex min-w-0 items-center gap-2 ps-10 pe-4 py-2 text-body", className)}
369
389
  {...props}
370
390
  >
371
391
  {children ?? (
@@ -391,7 +411,7 @@ export const TestError = ({ className, children, ...props }: TestErrorProps) =>
391
411
  export type TestErrorMessageProps = HTMLAttributes<HTMLParagraphElement>;
392
412
 
393
413
  export const TestErrorMessage = ({ className, children, ...props }: TestErrorMessageProps) => (
394
- <p className={cn("font-medium text-destructive-text text-sm", className)} {...props}>
414
+ <p className={cn("font-medium text-destructive-text text-body", className)} {...props}>
395
415
  {children}
396
416
  </p>
397
417
  );
@@ -400,7 +420,7 @@ export type TestErrorStackProps = HTMLAttributes<HTMLPreElement>;
400
420
 
401
421
  export const TestErrorStack = ({ className, children, ...props }: TestErrorStackProps) => (
402
422
  <pre
403
- className={cn("mt-2 overflow-auto font-mono text-destructive-text text-xs", className)}
423
+ className={cn("mt-2 overflow-auto font-mono text-destructive-text text-meta", className)}
404
424
  {...props}
405
425
  >
406
426
  {children}
@@ -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
+ });
@@ -18,50 +18,56 @@ const ICON_STROKE_WIDTH = 2;
18
18
 
19
19
  type ModelId = string;
20
20
 
21
- interface ContextSchema {
21
+ interface TokenUsageSchema {
22
22
  usedTokens: number;
23
23
  maxTokens: number;
24
24
  usage?: LanguageModelUsage;
25
25
  modelId?: ModelId;
26
26
  }
27
27
 
28
- const ContextContext = createContext<ContextSchema | null>(null);
28
+ const TokenUsageContext = createContext<TokenUsageSchema | null>(null);
29
29
 
30
- const useContextValue = () => {
31
- const context = useContext(ContextContext);
30
+ const useTokenUsageValue = () => {
31
+ const context = useContext(TokenUsageContext);
32
32
 
33
33
  if (!context) {
34
- throw new Error("Context components must be used within Context");
34
+ throw new Error("TokenUsage components must be used within TokenUsage");
35
35
  }
36
36
 
37
37
  return context;
38
38
  };
39
39
 
40
- export type ContextProps = ComponentProps<typeof HoverCard> & ContextSchema;
40
+ export type TokenUsageProps = ComponentProps<typeof HoverCard> & TokenUsageSchema;
41
41
 
42
- export const Context = ({ usedTokens, maxTokens, usage, modelId, ...props }: ContextProps) => {
42
+ export const TokenUsage = ({
43
+ usedTokens,
44
+ maxTokens,
45
+ usage,
46
+ modelId,
47
+ ...props
48
+ }: TokenUsageProps) => {
43
49
  const contextValue = useMemo(
44
50
  () => ({ maxTokens, modelId, usage, usedTokens }),
45
51
  [maxTokens, modelId, usage, usedTokens],
46
52
  );
47
53
 
48
54
  return (
49
- <ContextContext.Provider value={contextValue}>
55
+ <TokenUsageContext.Provider value={contextValue}>
50
56
  <HoverCard closeDelay={0} openDelay={0} {...props} />
51
- </ContextContext.Provider>
57
+ </TokenUsageContext.Provider>
52
58
  );
53
59
  };
54
60
 
55
- const ContextIcon = () => {
61
+ const TokenUsageIcon = () => {
56
62
  const { t } = useLocale();
57
- const { usedTokens, maxTokens } = useContextValue();
63
+ const { usedTokens, maxTokens } = useTokenUsageValue();
58
64
  const circumference = 2 * Math.PI * ICON_RADIUS;
59
65
  const usedPercent = usedTokens / maxTokens;
60
66
  const dashOffset = circumference * (1 - usedPercent);
61
67
 
62
68
  return (
63
69
  <svg
64
- aria-label={t("ai.context.usage")}
70
+ aria-label={t("ai.tokenUsage.usage")}
65
71
  height="20"
66
72
  role="img"
67
73
  style={{ color: "currentcolor" }}
@@ -94,11 +100,11 @@ const ContextIcon = () => {
94
100
  );
95
101
  };
96
102
 
97
- export type ContextTriggerProps = ComponentProps<typeof Button>;
103
+ export type TokenUsageTriggerProps = ComponentProps<typeof Button>;
98
104
 
99
- export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
105
+ export const TokenUsageTrigger = ({ children, ...props }: TokenUsageTriggerProps) => {
100
106
  const { formatNumber } = useLocale();
101
- const { usedTokens, maxTokens } = useContextValue();
107
+ const { usedTokens, maxTokens } = useTokenUsageValue();
102
108
  const usedPercent = usedTokens / maxTokens;
103
109
  const renderedPercent = formatNumber(usedPercent, {
104
110
  maximumFractionDigits: 1,
@@ -110,28 +116,28 @@ export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
110
116
  {children ?? (
111
117
  <Button type="button" variant="ghost" {...props}>
112
118
  <span className="font-medium text-muted-foreground">{renderedPercent}</span>
113
- <ContextIcon />
119
+ <TokenUsageIcon />
114
120
  </Button>
115
121
  )}
116
122
  </HoverCardTrigger>
117
123
  );
118
124
  };
119
125
 
120
- export type ContextContentProps = ComponentProps<typeof HoverCardContent>;
126
+ export type TokenUsageContentProps = ComponentProps<typeof HoverCardContent>;
121
127
 
122
- export const ContextContent = ({ className, ...props }: ContextContentProps) => (
128
+ export const TokenUsageContent = ({ className, ...props }: TokenUsageContentProps) => (
123
129
  <HoverCardContent className={cn("min-w-60 divide-y overflow-hidden p-0", className)} {...props} />
124
130
  );
125
131
 
126
- export type ContextContentHeaderProps = ComponentProps<"div">;
132
+ export type TokenUsageContentHeaderProps = ComponentProps<"div">;
127
133
 
128
- export const ContextContentHeader = ({
134
+ export const TokenUsageContentHeader = ({
129
135
  children,
130
136
  className,
131
137
  ...props
132
- }: ContextContentHeaderProps) => {
138
+ }: TokenUsageContentHeaderProps) => {
133
139
  const { formatNumber } = useLocale();
134
- const { usedTokens, maxTokens } = useContextValue();
140
+ const { usedTokens, maxTokens } = useTokenUsageValue();
135
141
  const usedPercent = usedTokens / maxTokens;
136
142
  const displayPct = formatNumber(usedPercent, {
137
143
  maximumFractionDigits: 1,
@@ -148,7 +154,7 @@ export const ContextContentHeader = ({
148
154
  <div className={cn("w-full space-y-2 p-3", className)} {...props}>
149
155
  {children ?? (
150
156
  <>
151
- <div className="flex items-center justify-between gap-3 text-xs">
157
+ <div className="flex items-center justify-between gap-3 text-meta">
152
158
  <p>{displayPct}</p>
153
159
  <p className="font-mono text-muted-foreground">
154
160
  {used} / {total}
@@ -163,23 +169,27 @@ export const ContextContentHeader = ({
163
169
  );
164
170
  };
165
171
 
166
- export type ContextContentBodyProps = ComponentProps<"div">;
172
+ export type TokenUsageContentBodyProps = ComponentProps<"div">;
167
173
 
168
- export const ContextContentBody = ({ children, className, ...props }: ContextContentBodyProps) => (
174
+ export const TokenUsageContentBody = ({
175
+ children,
176
+ className,
177
+ ...props
178
+ }: TokenUsageContentBodyProps) => (
169
179
  <div className={cn("w-full p-3", className)} {...props}>
170
180
  {children}
171
181
  </div>
172
182
  );
173
183
 
174
- export type ContextContentFooterProps = ComponentProps<"div">;
184
+ export type TokenUsageContentFooterProps = ComponentProps<"div">;
175
185
 
176
- export const ContextContentFooter = ({
186
+ export const TokenUsageContentFooter = ({
177
187
  children,
178
188
  className,
179
189
  ...props
180
- }: ContextContentFooterProps) => {
181
- const { formatNumber } = useLocale();
182
- const { modelId, usage } = useContextValue();
190
+ }: TokenUsageContentFooterProps) => {
191
+ const { formatNumber, t } = useLocale();
192
+ const { modelId, usage } = useTokenUsageValue();
183
193
  const costUSD = modelId
184
194
  ? getUsage({
185
195
  modelId,
@@ -197,14 +207,14 @@ export const ContextContentFooter = ({
197
207
  return (
198
208
  <div
199
209
  className={cn(
200
- "flex w-full items-center justify-between gap-3 bg-secondary p-3 text-xs",
210
+ "flex w-full items-center justify-between gap-3 bg-secondary p-3 text-meta",
201
211
  className,
202
212
  )}
203
213
  {...props}
204
214
  >
205
215
  {children ?? (
206
216
  <>
207
- <span className="text-muted-foreground">Total cost</span>
217
+ <span className="text-muted-foreground">{t("ai.tokenUsage.totalCost")}</span>
208
218
  <span>{totalCost}</span>
209
219
  </>
210
220
  )}
@@ -222,11 +232,11 @@ const TokensWithCost = ({ tokens, costText }: { tokens?: number; costText?: stri
222
232
  );
223
233
  };
224
234
 
225
- export type ContextInputUsageProps = ComponentProps<"div">;
235
+ export type TokenUsageInputProps = ComponentProps<"div">;
226
236
 
227
- export const ContextInputUsage = ({ className, children, ...props }: ContextInputUsageProps) => {
228
- const { formatNumber } = useLocale();
229
- const { usage, modelId } = useContextValue();
237
+ export const TokenUsageInput = ({ className, children, ...props }: TokenUsageInputProps) => {
238
+ const { formatNumber, t } = useLocale();
239
+ const { usage, modelId } = useTokenUsageValue();
230
240
  const inputTokens = usage?.inputTokens ?? 0;
231
241
 
232
242
  if (children) {
@@ -249,18 +259,18 @@ export const ContextInputUsage = ({ className, children, ...props }: ContextInpu
249
259
  });
250
260
 
251
261
  return (
252
- <div className={cn("flex items-center justify-between text-xs", className)} {...props}>
253
- <span className="text-muted-foreground">Input</span>
262
+ <div className={cn("flex items-center justify-between text-meta", className)} {...props}>
263
+ <span className="text-muted-foreground">{t("ai.tokenUsage.input")}</span>
254
264
  <TokensWithCost costText={inputCostText} tokens={inputTokens} />
255
265
  </div>
256
266
  );
257
267
  };
258
268
 
259
- export type ContextOutputUsageProps = ComponentProps<"div">;
269
+ export type TokenUsageOutputProps = ComponentProps<"div">;
260
270
 
261
- export const ContextOutputUsage = ({ className, children, ...props }: ContextOutputUsageProps) => {
262
- const { formatNumber } = useLocale();
263
- const { usage, modelId } = useContextValue();
271
+ export const TokenUsageOutput = ({ className, children, ...props }: TokenUsageOutputProps) => {
272
+ const { formatNumber, t } = useLocale();
273
+ const { usage, modelId } = useTokenUsageValue();
264
274
  const outputTokens = usage?.outputTokens ?? 0;
265
275
 
266
276
  if (children) {
@@ -283,23 +293,25 @@ export const ContextOutputUsage = ({ className, children, ...props }: ContextOut
283
293
  });
284
294
 
285
295
  return (
286
- <div className={cn("flex items-center justify-between text-xs", className)} {...props}>
287
- <span className="text-muted-foreground">Output</span>
296
+ <div className={cn("flex items-center justify-between text-meta", className)} {...props}>
297
+ <span className="text-muted-foreground">{t("ai.tokenUsage.output")}</span>
288
298
  <TokensWithCost costText={outputCostText} tokens={outputTokens} />
289
299
  </div>
290
300
  );
291
301
  };
292
302
 
293
- export type ContextReasoningUsageProps = ComponentProps<"div">;
303
+ export type TokenUsageReasoningProps = ComponentProps<"div">;
294
304
 
295
- export const ContextReasoningUsage = ({
305
+ export const TokenUsageReasoning = ({
296
306
  className,
297
307
  children,
298
308
  ...props
299
- }: ContextReasoningUsageProps) => {
300
- const { formatNumber } = useLocale();
301
- const { usage, modelId } = useContextValue();
302
- const reasoningTokens = usage?.reasoningTokens ?? 0;
309
+ }: TokenUsageReasoningProps) => {
310
+ const { formatNumber, t } = useLocale();
311
+ const { usage, modelId } = useTokenUsageValue();
312
+ // `reasoningTokens` moved under `outputTokenDetails` in ai@7 (the flat,
313
+ // deprecated top-level alias from ai@6 was removed).
314
+ const reasoningTokens = usage?.outputTokenDetails?.reasoningTokens ?? 0;
303
315
 
304
316
  if (children) {
305
317
  return children;
@@ -321,19 +333,21 @@ export const ContextReasoningUsage = ({
321
333
  });
322
334
 
323
335
  return (
324
- <div className={cn("flex items-center justify-between text-xs", className)} {...props}>
325
- <span className="text-muted-foreground">Reasoning</span>
336
+ <div className={cn("flex items-center justify-between text-meta", className)} {...props}>
337
+ <span className="text-muted-foreground">{t("ai.tokenUsage.reasoning")}</span>
326
338
  <TokensWithCost costText={reasoningCostText} tokens={reasoningTokens} />
327
339
  </div>
328
340
  );
329
341
  };
330
342
 
331
- export type ContextCacheUsageProps = ComponentProps<"div">;
343
+ export type TokenUsageCacheProps = ComponentProps<"div">;
332
344
 
333
- export const ContextCacheUsage = ({ className, children, ...props }: ContextCacheUsageProps) => {
334
- const { formatNumber } = useLocale();
335
- const { usage, modelId } = useContextValue();
336
- const cacheTokens = usage?.cachedInputTokens ?? 0;
345
+ export const TokenUsageCache = ({ className, children, ...props }: TokenUsageCacheProps) => {
346
+ const { formatNumber, t } = useLocale();
347
+ const { usage, modelId } = useTokenUsageValue();
348
+ // `cachedInputTokens` moved under `inputTokenDetails.cacheReadTokens` in
349
+ // ai@7 (the flat, deprecated top-level alias from ai@6 was removed).
350
+ const cacheTokens = usage?.inputTokenDetails?.cacheReadTokens ?? 0;
337
351
 
338
352
  if (children) {
339
353
  return children;
@@ -355,8 +369,8 @@ export const ContextCacheUsage = ({ className, children, ...props }: ContextCach
355
369
  });
356
370
 
357
371
  return (
358
- <div className={cn("flex items-center justify-between text-xs", className)} {...props}>
359
- <span className="text-muted-foreground">Cache</span>
372
+ <div className={cn("flex items-center justify-between text-meta", className)} {...props}>
373
+ <span className="text-muted-foreground">{t("ai.tokenUsage.cache")}</span>
360
374
  <TokensWithCost costText={cacheCostText} tokens={cacheTokens} />
361
375
  </div>
362
376
  );
@@ -9,7 +9,15 @@ import { ToolResultCard } from "./tool-result-card";
9
9
  const meta = {
10
10
  title: "AI/ToolResultCard",
11
11
  component: ToolResultCard,
12
- parameters: { layout: "padded" },
12
+ parameters: {
13
+ layout: "padded",
14
+ docs: {
15
+ description: {
16
+ component:
17
+ "The CHAT produced-artifact card; a console transcript renders the same moment through `Terminal/TerminalToolCall` — see [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). A separate component from `AI/Tool` by design: `Tool` is the step that RAN (JSON behind disclosure), `ToolResultCard` hosts what it produced — a chart, a table, a file preview — as `children`, on the elevation channel (raised fill, no border).",
18
+ },
19
+ },
20
+ },
13
21
  tags: ["autodocs"],
14
22
  } satisfies Meta<typeof ToolResultCard>;
15
23
  export default meta;
@@ -8,9 +8,19 @@ import {
8
8
  ToolOutput,
9
9
  statusFromToolState,
10
10
  } from "./tool";
11
- const meta = { title: "AI/Tool", component: Tool, parameters: { layout: "padded" } } satisfies Meta<
12
- typeof Tool
13
- >;
11
+ const meta = {
12
+ title: "AI/Tool",
13
+ component: Tool,
14
+ parameters: {
15
+ layout: "padded",
16
+ docs: {
17
+ description: {
18
+ component:
19
+ "The CHAT tool-call row; the console skin is `Terminal/TerminalToolCall`, and both speak the same success / error / pending vocabulary — see [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). `Tool` owns the rail-and-inspect idiom: a header carrying the derived tool name, a `StatusBadge` and a business summary, with the raw payload behind a default-collapsed `ToolDetails`. The artifact a call PRODUCED is `AI/ToolResultCard` instead.",
20
+ },
21
+ },
22
+ },
23
+ } satisfies Meta<typeof Tool>;
14
24
  export default meta;
15
25
  type Story = StoryObj<typeof meta>;
16
26
  // JSON-behind-disclosure is the package default (#192, research 10 §B.5): the