@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.
- package/README.md +62 -12
- 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 +1152 -218
- package/dist/index.js +5601 -3241
- package/dist/index.js.map +1 -1
- package/package.json +30 -15
- package/src/__contract__/audio-visualizer.contract.test.tsx +49 -0
- package/src/__contract__/chat-shell.contract.test.tsx +49 -0
- package/src/__contract__/grouped-parts.contract.test.tsx +49 -0
- package/src/__contract__/image.contract.test.tsx +49 -0
- package/src/__contract__/markdown-view.contract.test.tsx +49 -0
- package/src/__contract__/message-feedback.contract.test.tsx +49 -0
- package/src/__contract__/message-form.contract.test.tsx +49 -0
- package/src/__contract__/message-table.contract.test.tsx +49 -0
- package/src/__contract__/model-provider-logo.contract.test.tsx +49 -0
- package/src/__contract__/persona.contract.test.tsx +49 -0
- package/src/__contract__/prompt-input-effort.contract.test.tsx +49 -0
- package/src/__contract__/prompt-input-mode.contract.test.tsx +49 -0
- package/src/_audio-player-media-chrome.tsx +102 -15
- package/src/_chat-shell-rail.tsx +2 -2
- package/src/_flow-boundary.tsx +68 -49
- package/src/_lazy-boundary-conformance.ts +38 -0
- package/src/_lazy-cjk.test.ts +43 -0
- package/src/_lazy-cjk.ts +73 -0
- package/src/_lazy-engine-boundary.tsx +61 -0
- package/src/_lazy-math.test.ts +63 -0
- package/src/_lazy-math.ts +90 -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 +94 -22
- package/src/_streamdown-safety.ts +170 -0
- package/src/_theme-scope-store.test.ts +83 -0
- package/src/_theme-scope-store.ts +103 -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 +43 -24
- package/src/agentic-workspace.stories.tsx +27 -20
- package/src/artifact.tsx +17 -14
- package/src/asset-preview.test.tsx +40 -0
- package/src/asset-preview.tsx +83 -12
- package/src/attachments.tsx +7 -4
- 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/code-block.test.tsx +100 -1
- package/src/code-block.tsx +166 -103
- package/src/commit.tsx +30 -41
- 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 +264 -4
- package/src/context-panel.stories.tsx +9 -1
- package/src/context-panel.tsx +3 -2
- package/src/conversation.stories.tsx +86 -2
- package/src/conversation.test.tsx +66 -0
- package/src/conversation.tsx +52 -9
- package/src/diff-view.stories.tsx +196 -0
- package/src/diff-view.test.tsx +238 -0
- package/src/diff-view.tsx +697 -0
- package/src/environment-variables.tsx +20 -37
- package/src/file-tree.test.tsx +21 -0
- package/src/file-tree.tsx +12 -2
- package/src/gallery.tsx +4 -2
- package/src/index.ts +14 -4
- package/src/inline-citation.tsx +5 -5
- package/src/jsx-preview.stories.tsx +2 -2
- package/src/jsx-preview.tsx +151 -43
- package/src/markdown-view.stories.tsx +92 -1
- package/src/markdown-view.test.tsx +237 -2
- package/src/markdown-view.tsx +157 -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 +65 -4
- package/src/message-form.test.tsx +8 -2
- package/src/message-form.tsx +15 -7
- package/src/message-table.stories.tsx +2 -2
- package/src/message-table.test.tsx +7 -0
- package/src/message-table.tsx +8 -4
- package/src/message.stories.tsx +9 -1
- package/src/message.test.tsx +247 -1
- package/src/message.tsx +125 -15
- package/src/microcopy.test.tsx +40 -0
- package/src/{model-selector.stories.tsx → model-provider-logo.stories.tsx} +17 -8
- package/src/model-provider-logo.test.tsx +109 -0
- package/src/model-provider-logo.tsx +183 -0
- package/src/open-in-chat.tsx +50 -29
- package/src/package-info.tsx +12 -12
- 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/prompt-input.test.tsx +67 -1
- package/src/prompt-input.tsx +42 -4
- package/src/queue.tsx +4 -4
- package/src/reasoning.tsx +42 -17
- package/src/sandbox.stories.tsx +9 -1
- package/src/sandbox.tsx +3 -3
- package/src/schema-display.test.tsx +56 -0
- package/src/schema-display.tsx +71 -37
- 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/snippet.test.tsx +6 -2
- package/src/snippet.tsx +14 -34
- package/src/speech-input.test.tsx +109 -0
- package/src/speech-input.tsx +31 -3
- package/src/stack-trace.tsx +24 -36
- package/src/streamdown-i18n.test.tsx +24 -1
- package/src/task.stories.tsx +13 -3
- package/src/templates-ai-assistant.stories.tsx +21 -1
- package/src/test-results.tsx +47 -27
- package/src/token-usage.stories.tsx +24 -0
- package/src/token-usage.test.tsx +92 -0
- package/src/{context.tsx → token-usage.tsx} +73 -59
- package/src/tool-result-card.stories.tsx +9 -1
- package/src/tool.stories.tsx +13 -3
- package/src/tool.test.tsx +65 -0
- package/src/tool.tsx +73 -30
- package/src/transcription.tsx +1 -1
- 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/voice-selector.tsx +5 -5
- package/src/web-preview.test.tsx +51 -1
- package/src/web-preview.tsx +46 -10
- 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.test.tsx +0 -55
- package/src/model-selector.tsx +0 -225
- package/src/terminal.tsx +0 -244
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentEvent — the lifecycle/hook event line (#109).
|
|
5
|
+
*
|
|
6
|
+
* Nothing in the trace vocabulary (`Message` → `Tool` → `Task`/`AgentStep`)
|
|
7
|
+
* has a place for something the *runtime* did to the agent: a lifecycle hook
|
|
8
|
+
* (`user_prompt_submit`, `stop`) or a policy/guard check
|
|
9
|
+
* (`pre_tool_use`/`post_tool_use`) that fired around a tool call. `AgentEvent`
|
|
10
|
+
* fills that gap as an `AgentStep` VARIANT on the existing `AgentTimeline`
|
|
11
|
+
* rail — not a second spine. It sits in the same `<ol>` as ordinary steps,
|
|
12
|
+
* distinguished by glyph and label rather than by a different layout
|
|
13
|
+
* (docs/decisions/2026-09-01-brainless-adoption-architecture.md § 6, and the
|
|
14
|
+
* `Task`/`TaskItem` precedent this file follows in `./agent-timeline`).
|
|
15
|
+
*
|
|
16
|
+
* `outcome` maps onto the existing closed 7-state `Status` (`@elabs-ai/components-ui`) —
|
|
17
|
+
* no eighth status is introduced. `checks` renders a gate's verdicts: an
|
|
18
|
+
* array renders each check as icon + accessible pass/fail TEXT (never colour
|
|
19
|
+
* alone — the greyscale test in `.claude/rules/accessibility.md`); a count
|
|
20
|
+
* summary renders `passed/ran` as plain text.
|
|
21
|
+
*/
|
|
22
|
+
import { Webhook, type LucideIcon } from "lucide-react";
|
|
23
|
+
import { forwardRef, type ReactNode } from "react";
|
|
24
|
+
import {
|
|
25
|
+
StatusIcon,
|
|
26
|
+
agentEventOutcomeStatus,
|
|
27
|
+
formatElapsed,
|
|
28
|
+
useLocale,
|
|
29
|
+
type AgentEventOutcome,
|
|
30
|
+
type AgentEventPhase,
|
|
31
|
+
type CheckResult,
|
|
32
|
+
type CheckSummary,
|
|
33
|
+
} from "@elabs-ai/components-ui";
|
|
34
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
35
|
+
import { AgentStep, type AgentStepProps } from "./agent-timeline";
|
|
36
|
+
|
|
37
|
+
// `AgentEventPhase`, `AgentEventOutcome` and the outcome→`Status` mapping
|
|
38
|
+
// (`agentEventOutcomeStatus`, formerly this file's private `OUTCOME_STATUS`)
|
|
39
|
+
// moved to `@elabs-ai/components-ui` (`lib/agent-event-model.ts`) — the
|
|
40
|
+
// terminal CLI look-alike family's own event line (issue #117) reuses the
|
|
41
|
+
// same model, and `@elabs-ai/components-ai`/`@elabs-ai/components-terminal`
|
|
42
|
+
// are layer-2 DAG siblings that may not import each other (T0; see
|
|
43
|
+
// docs/decisions/2026-09-01-brainless-adoption-architecture.md § 4). Imported
|
|
44
|
+
// above; NOT re-exported from here — a consumer imports `AgentEventPhase`/
|
|
45
|
+
// `AgentEventOutcome` from `@elabs-ai/components-ui`.
|
|
46
|
+
|
|
47
|
+
export interface AgentEventProps extends Omit<
|
|
48
|
+
AgentStepProps,
|
|
49
|
+
"name" | "status" | "children" | "icon" | "timestamp"
|
|
50
|
+
> {
|
|
51
|
+
/** The runtime event name, verbatim as the runtime emits it — e.g. `"user_prompt_submit"`, `"pre_tool_use"`, `"stop"`. */
|
|
52
|
+
label: string;
|
|
53
|
+
/** When this event fired relative to the action it gates. See `AgentEventPhase`. */
|
|
54
|
+
phase?: AgentEventPhase;
|
|
55
|
+
/** The event's verdict. @default "ok" */
|
|
56
|
+
outcome?: AgentEventOutcome;
|
|
57
|
+
/** Wall-clock duration of the event, in milliseconds. Rendered with the shared `formatElapsed`. */
|
|
58
|
+
durationMs?: number;
|
|
59
|
+
/** Gate checks the event ran: individual verdicts, or a count summary. */
|
|
60
|
+
checks?: CheckSummary | CheckResult[];
|
|
61
|
+
/** Optional leading glyph on the title row (Lucide). @default Webhook */
|
|
62
|
+
icon?: LucideIcon;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isCheckResultList(checks: CheckSummary | CheckResult[]): checks is CheckResult[] {
|
|
66
|
+
return Array.isArray(checks);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A single gate-check verdict row: icon + label + accessible status word +
|
|
71
|
+
* optional duration + optional detail. Mirrors the anatomy of `ChangeReview`'s
|
|
72
|
+
* check row (`packages/ui/src/components/change-review/change-review.tsx`) so
|
|
73
|
+
* the two `CheckResult` surfaces read as one convention — reimplemented here
|
|
74
|
+
* (rather than imported) because that row is a private, unexported function.
|
|
75
|
+
*
|
|
76
|
+
* The pass/fail signal is carried by TWO non-colour cues — `StatusIcon`'s
|
|
77
|
+
* distinct glyph (decorative, `aria-hidden`) and the visible status word
|
|
78
|
+
* (what reaches assistive tech) — so it survives in greyscale (WCAG 1.4.1).
|
|
79
|
+
*/
|
|
80
|
+
function AgentEventCheckRow({ check }: { check: CheckResult }) {
|
|
81
|
+
const { t } = useLocale();
|
|
82
|
+
const { label, ok, detail, durationMs } = check;
|
|
83
|
+
const statusWord = t(ok ? "ai.agentEvent.checkPassed" : "ai.agentEvent.checkFailed");
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div data-slot="agent-event-check" data-ok={ok} className="flex flex-col gap-1">
|
|
87
|
+
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 min-w-0">
|
|
88
|
+
<StatusIcon status={ok ? "complete" : "failed"} className="size-3.5 shrink-0" />
|
|
89
|
+
<span className="text-caption font-medium text-foreground truncate">{label}</span>
|
|
90
|
+
<span
|
|
91
|
+
data-slot="agent-event-check-status"
|
|
92
|
+
className={cn("text-caption", ok ? "text-success-text" : "text-destructive-text")}
|
|
93
|
+
>
|
|
94
|
+
{statusWord}
|
|
95
|
+
</span>
|
|
96
|
+
{durationMs !== undefined && (
|
|
97
|
+
<span className="text-caption text-muted-foreground tabular-nums shrink-0">
|
|
98
|
+
{formatElapsed(durationMs)}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
{detail !== undefined && detail !== "" && (
|
|
103
|
+
<p
|
|
104
|
+
data-slot="agent-event-check-detail"
|
|
105
|
+
className="ps-5 text-caption text-muted-foreground break-words"
|
|
106
|
+
>
|
|
107
|
+
{detail}
|
|
108
|
+
</p>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The checks section: a flat list of verdict rows, or a `passed/ran` count line. */
|
|
115
|
+
function AgentEventChecks({ checks }: { checks: CheckSummary | CheckResult[] }) {
|
|
116
|
+
const { t } = useLocale();
|
|
117
|
+
|
|
118
|
+
if (isCheckResultList(checks)) {
|
|
119
|
+
if (checks.length === 0) return null;
|
|
120
|
+
return (
|
|
121
|
+
<div data-slot="agent-event-checks" className="flex flex-col gap-1.5">
|
|
122
|
+
{checks.map((check, index) => (
|
|
123
|
+
<AgentEventCheckRow key={`${check.label}-${index}`} check={check} />
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { ran, passed } = checks;
|
|
130
|
+
return (
|
|
131
|
+
<p
|
|
132
|
+
data-slot="agent-event-checks-summary"
|
|
133
|
+
className="text-meta text-muted-foreground tabular-nums"
|
|
134
|
+
>
|
|
135
|
+
{t("ai.agentEvent.checksSummary", { passed, ran })}
|
|
136
|
+
</p>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const PHASE_KEY: Record<AgentEventPhase, string> = {
|
|
141
|
+
before: "ai.agentEvent.phaseBefore",
|
|
142
|
+
after: "ai.agentEvent.phaseAfter",
|
|
143
|
+
lifecycle: "ai.agentEvent.phaseLifecycle",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* One lifecycle/hook event line on the `AgentTimeline` rail — an `AgentStep`
|
|
148
|
+
* variant, never a second spine. Composes `AgentStep` exactly as `TaskItem`
|
|
149
|
+
* does in `./agent-timeline`, so it rides the same `TimelineRoot`/`TimelineItem`
|
|
150
|
+
* primitives every other trace entry uses.
|
|
151
|
+
*/
|
|
152
|
+
export const AgentEvent = forwardRef<HTMLLIElement, AgentEventProps>(function AgentEvent(
|
|
153
|
+
{ label, phase, outcome = "ok", durationMs, checks, icon: Icon = Webhook, summary, ...props },
|
|
154
|
+
ref,
|
|
155
|
+
) {
|
|
156
|
+
const { t } = useLocale();
|
|
157
|
+
const status = agentEventOutcomeStatus(outcome);
|
|
158
|
+
const phaseLabel = phase === undefined ? undefined : t(PHASE_KEY[phase]);
|
|
159
|
+
|
|
160
|
+
const name: ReactNode = phaseLabel ? (
|
|
161
|
+
<>
|
|
162
|
+
{label}{" "}
|
|
163
|
+
<span data-slot="agent-event-phase" className="text-meta font-normal text-muted-foreground">
|
|
164
|
+
· {phaseLabel}
|
|
165
|
+
</span>
|
|
166
|
+
</>
|
|
167
|
+
) : (
|
|
168
|
+
label
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<AgentStep
|
|
173
|
+
ref={ref}
|
|
174
|
+
icon={Icon}
|
|
175
|
+
name={name}
|
|
176
|
+
status={status}
|
|
177
|
+
summary={summary}
|
|
178
|
+
timestamp={durationMs === undefined ? undefined : formatElapsed(durationMs)}
|
|
179
|
+
data-slot="agent-event"
|
|
180
|
+
{...props}
|
|
181
|
+
>
|
|
182
|
+
{checks ? <AgentEventChecks checks={checks} /> : null}
|
|
183
|
+
</AgentStep>
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
AgentEvent.displayName = "AgentEvent";
|
|
@@ -6,7 +6,15 @@ import { ToolDetails, ToolInput, ToolOutput } from "./tool";
|
|
|
6
6
|
const meta = {
|
|
7
7
|
title: "AI/AgentTimeline",
|
|
8
8
|
component: AgentTimeline,
|
|
9
|
-
parameters: {
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: "padded",
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
"The agent EXECUTION trace on the canonical rail. A generic ordered-steps rail is `Core/Timeline`, and git history is `Data/RevisionTimeline` — see [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs). `AgentTimeline` is the convergence compound: sequenced, status-bearing steps on ONE spine (the `@elabs-ai/components-ui` Timeline rail) speaking ONE vocabulary (the closed seven-state `Status` enum). `AI/Task` and `AI/AgentEvent` compose it rather than hand-rolling rails.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
10
18
|
tags: ["autodocs"],
|
|
11
19
|
} satisfies Meta<typeof AgentTimeline>;
|
|
12
20
|
export default meta;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Tool } from "ai";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { AgentTool, AgentTools } from "./agent";
|
|
5
|
+
|
|
6
|
+
const staticTool: Tool = {
|
|
7
|
+
description: "Search the web for a query.",
|
|
8
|
+
inputSchema: { type: "object" } as never,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// ai@7 widened `Tool.description` to `string | ((options) => string)` (a
|
|
12
|
+
// per-call dynamic description). `AgentTool` renders a static list with no
|
|
13
|
+
// call context to invoke that function with.
|
|
14
|
+
const dynamicTool: Tool = {
|
|
15
|
+
description: () => "computed at call time",
|
|
16
|
+
inputSchema: { type: "object" } as never,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("AgentTool description (#30, ai@7 Tool.description can be a function)", () => {
|
|
20
|
+
it("renders a string description", () => {
|
|
21
|
+
render(
|
|
22
|
+
<AgentTools type="single" collapsible>
|
|
23
|
+
<AgentTool tool={staticTool} value="search" />
|
|
24
|
+
</AgentTools>,
|
|
25
|
+
);
|
|
26
|
+
expect(screen.getByText("Search the web for a query.")).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("falls back to the empty-description copy instead of rendering a function as a node", () => {
|
|
30
|
+
render(
|
|
31
|
+
<AgentTools type="single" collapsible>
|
|
32
|
+
<AgentTool tool={dynamicTool} value="dynamic" />
|
|
33
|
+
</AgentTools>,
|
|
34
|
+
);
|
|
35
|
+
expect(screen.getByText("No description")).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("falls back to the empty-description copy when there is no description at all", () => {
|
|
39
|
+
const bareTool: Tool = { inputSchema: { type: "object" } as never };
|
|
40
|
+
render(
|
|
41
|
+
<AgentTools type="single" collapsible>
|
|
42
|
+
<AgentTool tool={bareTool} value="bare" />
|
|
43
|
+
</AgentTools>,
|
|
44
|
+
);
|
|
45
|
+
expect(screen.getByText("No description")).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
});
|
package/src/agent.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
AccordionTrigger,
|
|
8
8
|
} from "@elabs-ai/components-ui";
|
|
9
9
|
import { Badge } from "@elabs-ai/components-ui";
|
|
10
|
+
import { useLocale } from "@elabs-ai/components-ui";
|
|
10
11
|
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
11
12
|
import type { Tool } from "ai";
|
|
12
13
|
import { BotIcon } from "lucide-react";
|
|
@@ -30,9 +31,9 @@ export const AgentHeader = memo(({ className, name, model, ...props }: AgentHead
|
|
|
30
31
|
<div className={cn("flex w-full items-center justify-between gap-4 p-3", className)} {...props}>
|
|
31
32
|
<div className="flex items-center gap-2">
|
|
32
33
|
<BotIcon className="size-4 text-muted-foreground" />
|
|
33
|
-
<span className="font-medium text-
|
|
34
|
+
<span className="font-medium text-body">{name}</span>
|
|
34
35
|
{model && (
|
|
35
|
-
<Badge className="font-mono text-
|
|
36
|
+
<Badge className="font-mono text-meta" variant="secondary">
|
|
36
37
|
{model}
|
|
37
38
|
</Badge>
|
|
38
39
|
)}
|
|
@@ -51,24 +52,32 @@ export type AgentInstructionsProps = ComponentProps<"div"> & {
|
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
export const AgentInstructions = memo(
|
|
54
|
-
({ className, children, ...props }: AgentInstructionsProps) =>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<div className="
|
|
58
|
-
<
|
|
55
|
+
({ className, children, ...props }: AgentInstructionsProps) => {
|
|
56
|
+
const { t } = useLocale();
|
|
57
|
+
return (
|
|
58
|
+
<div className={cn("space-y-2", className)} {...props}>
|
|
59
|
+
<span className="font-medium text-muted-foreground text-body">
|
|
60
|
+
{t("ai.agent.instructions")}
|
|
61
|
+
</span>
|
|
62
|
+
<div className="rounded-md bg-muted/50 p-3 text-muted-foreground text-body">
|
|
63
|
+
<p>{children}</p>
|
|
64
|
+
</div>
|
|
59
65
|
</div>
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
);
|
|
67
|
+
},
|
|
62
68
|
);
|
|
63
69
|
|
|
64
70
|
export type AgentToolsProps = ComponentProps<typeof Accordion>;
|
|
65
71
|
|
|
66
|
-
export const AgentTools = memo(({ className, ...props }: AgentToolsProps) =>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
export const AgentTools = memo(({ className, ...props }: AgentToolsProps) => {
|
|
73
|
+
const { t } = useLocale();
|
|
74
|
+
return (
|
|
75
|
+
<div className={cn("space-y-2", className)}>
|
|
76
|
+
<span className="font-medium text-muted-foreground text-body">{t("ai.agent.tools")}</span>
|
|
77
|
+
<Accordion className="rounded-md border" {...props} />
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
});
|
|
72
81
|
|
|
73
82
|
export type AgentToolProps = ComponentProps<typeof AccordionItem> & {
|
|
74
83
|
tool: Tool;
|
|
@@ -76,11 +85,16 @@ export type AgentToolProps = ComponentProps<typeof AccordionItem> & {
|
|
|
76
85
|
|
|
77
86
|
export const AgentTool = memo(({ className, tool, value, ...props }: AgentToolProps) => {
|
|
78
87
|
const schema = "jsonSchema" in tool && tool.jsonSchema ? tool.jsonSchema : tool.inputSchema;
|
|
88
|
+
// `tool.description` may be a fixed string OR (ai@7+) a function of the live
|
|
89
|
+
// call context (`(options) => string`) for a per-call dynamic description.
|
|
90
|
+
// AgentTool renders a static list, so it has no call context to invoke that
|
|
91
|
+
// function with — show it only when it is already a plain string.
|
|
92
|
+
const description = typeof tool.description === "string" ? tool.description : undefined;
|
|
79
93
|
|
|
80
94
|
return (
|
|
81
95
|
<AccordionItem className={cn("border-b last:border-b-0", className)} value={value} {...props}>
|
|
82
|
-
<AccordionTrigger className="px-3 py-2 text-
|
|
83
|
-
{
|
|
96
|
+
<AccordionTrigger className="px-3 py-2 text-body hover:no-underline">
|
|
97
|
+
{description ?? "No description"}
|
|
84
98
|
</AccordionTrigger>
|
|
85
99
|
<AccordionContent className="px-3 pb-3">
|
|
86
100
|
<div className="rounded-md bg-muted/50">
|
|
@@ -95,14 +109,19 @@ export type AgentOutputProps = ComponentProps<"div"> & {
|
|
|
95
109
|
schema: string;
|
|
96
110
|
};
|
|
97
111
|
|
|
98
|
-
export const AgentOutput = memo(({ className, schema, ...props }: AgentOutputProps) =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<div className="
|
|
102
|
-
<
|
|
112
|
+
export const AgentOutput = memo(({ className, schema, ...props }: AgentOutputProps) => {
|
|
113
|
+
const { t } = useLocale();
|
|
114
|
+
return (
|
|
115
|
+
<div className={cn("space-y-2", className)} {...props}>
|
|
116
|
+
<span className="font-medium text-muted-foreground text-body">
|
|
117
|
+
{t("ai.agent.outputSchema")}
|
|
118
|
+
</span>
|
|
119
|
+
<div className="rounded-md bg-muted/50">
|
|
120
|
+
<CodeBlock code={schema} language="typescript" />
|
|
121
|
+
</div>
|
|
103
122
|
</div>
|
|
104
|
-
|
|
105
|
-
)
|
|
123
|
+
);
|
|
124
|
+
});
|
|
106
125
|
|
|
107
126
|
Agent.displayName = "Agent";
|
|
108
127
|
AgentHeader.displayName = "AgentHeader";
|
|
@@ -86,12 +86,12 @@ import {
|
|
|
86
86
|
ApprovalCardTitle,
|
|
87
87
|
} from "./confirmation";
|
|
88
88
|
import {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
} from "./
|
|
89
|
+
TokenUsage,
|
|
90
|
+
TokenUsageContent,
|
|
91
|
+
TokenUsageContentFooter,
|
|
92
|
+
TokenUsageContentHeader,
|
|
93
|
+
TokenUsageTrigger,
|
|
94
|
+
} from "./token-usage";
|
|
95
95
|
import {
|
|
96
96
|
ContextPanel,
|
|
97
97
|
ContextPanelBody,
|
|
@@ -321,13 +321,15 @@ function WorkspaceSidebar({
|
|
|
321
321
|
<SidebarHeader>
|
|
322
322
|
<SidebarMenu>
|
|
323
323
|
<SidebarMenuItem>
|
|
324
|
-
<SidebarMenuButton size="lg" tooltip="Atlas workspace">
|
|
324
|
+
<SidebarMenuButton size="lg" tooltip="Atlas workspace" className="group">
|
|
325
325
|
<span className="flex aspect-square size-8 shrink-0 items-center justify-center group-data-[collapsible=icon]:size-4">
|
|
326
326
|
<BrandLogo variant="mark" className="size-full" aria-hidden />
|
|
327
327
|
</span>
|
|
328
328
|
<span className="grid flex-1 text-start leading-tight">
|
|
329
329
|
<span className="truncate text-body font-semibold">Atlas</span>
|
|
330
|
-
<span className="truncate text-meta text-muted-foreground">
|
|
330
|
+
<span className="truncate text-meta text-sidebar-muted-foreground group-hover:text-sidebar-accent-foreground">
|
|
331
|
+
Agentic workspace
|
|
332
|
+
</span>
|
|
331
333
|
</span>
|
|
332
334
|
</SidebarMenuButton>
|
|
333
335
|
</SidebarMenuItem>
|
|
@@ -378,13 +380,16 @@ function WorkspaceSidebar({
|
|
|
378
380
|
<SidebarMenuSubButton asChild isActive={activeProject === p.id}>
|
|
379
381
|
<a
|
|
380
382
|
href={`#project-${p.id}`}
|
|
383
|
+
className="group"
|
|
381
384
|
onClick={(e) => {
|
|
382
385
|
e.preventDefault();
|
|
383
386
|
onSelectProject(p.id);
|
|
384
387
|
}}
|
|
385
388
|
>
|
|
386
389
|
<span className="flex-1 truncate">{p.name}</span>
|
|
387
|
-
<span className="text-meta text-muted-foreground">
|
|
390
|
+
<span className="text-meta text-sidebar-muted-foreground group-hover:text-sidebar-accent-foreground group-data-[active=true]:text-sidebar-accent-foreground">
|
|
391
|
+
{p.tag}
|
|
392
|
+
</span>
|
|
388
393
|
</a>
|
|
389
394
|
</SidebarMenuSubButton>
|
|
390
395
|
</SidebarMenuSubItem>
|
|
@@ -455,9 +460,9 @@ function WorkspaceSidebar({
|
|
|
455
460
|
{ASSET_CATEGORIES.map((c) => (
|
|
456
461
|
<SidebarMenuSubItem key={c.id}>
|
|
457
462
|
<SidebarMenuSubButton asChild>
|
|
458
|
-
<a href={`#assets-${c.id}`}>
|
|
463
|
+
<a href={`#assets-${c.id}`} className="group">
|
|
459
464
|
<span className="flex-1 truncate">{c.name}</span>
|
|
460
|
-
<span className="text-meta tabular-nums text-muted-foreground">
|
|
465
|
+
<span className="text-meta tabular-nums text-sidebar-muted-foreground group-hover:text-sidebar-accent-foreground">
|
|
461
466
|
{c.count}
|
|
462
467
|
</span>
|
|
463
468
|
</a>
|
|
@@ -478,13 +483,15 @@ function WorkspaceSidebar({
|
|
|
478
483
|
<SidebarMenuItem>
|
|
479
484
|
<DropdownMenu>
|
|
480
485
|
<DropdownMenuTrigger asChild>
|
|
481
|
-
<SidebarMenuButton size="lg" tooltip="Account">
|
|
486
|
+
<SidebarMenuButton size="lg" tooltip="Account" className="group">
|
|
482
487
|
<Avatar className="size-7 rounded-md">
|
|
483
488
|
<AvatarFallback className="rounded-md text-meta">AR</AvatarFallback>
|
|
484
489
|
</Avatar>
|
|
485
490
|
<span className="grid flex-1 text-start leading-tight">
|
|
486
491
|
<span className="truncate text-body font-medium">Avery Rao</span>
|
|
487
|
-
<span className="truncate text-meta text-muted-foreground">
|
|
492
|
+
<span className="truncate text-meta text-sidebar-muted-foreground group-hover:text-sidebar-accent-foreground">
|
|
493
|
+
avery@acme.co
|
|
494
|
+
</span>
|
|
488
495
|
</span>
|
|
489
496
|
<ChevronsUpDown className="ms-auto size-4" />
|
|
490
497
|
</SidebarMenuButton>
|
|
@@ -557,13 +564,13 @@ function WorkspaceContextPanel() {
|
|
|
557
564
|
return (
|
|
558
565
|
<ContextPanel>
|
|
559
566
|
<ContextPanelHeader title="Context">
|
|
560
|
-
<
|
|
561
|
-
<
|
|
562
|
-
<
|
|
563
|
-
<
|
|
564
|
-
<
|
|
565
|
-
</
|
|
566
|
-
</
|
|
567
|
+
<TokenUsage usedTokens={42000} maxTokens={200000} modelId="anthropic/claude-opus-4">
|
|
568
|
+
<TokenUsageTrigger />
|
|
569
|
+
<TokenUsageContent>
|
|
570
|
+
<TokenUsageContentHeader />
|
|
571
|
+
<TokenUsageContentFooter />
|
|
572
|
+
</TokenUsageContent>
|
|
573
|
+
</TokenUsage>
|
|
567
574
|
</ContextPanelHeader>
|
|
568
575
|
<ContextPanelBody root={<ContextRailRoot />} detail={<ContextRailDetail />} />
|
|
569
576
|
</ContextPanel>
|
package/src/artifact.tsx
CHANGED
|
@@ -67,29 +67,32 @@ export const ArtifactClose = ({
|
|
|
67
67
|
size = "sm",
|
|
68
68
|
variant = "ghost",
|
|
69
69
|
...props
|
|
70
|
-
}: ArtifactCloseProps) =>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
)
|
|
70
|
+
}: ArtifactCloseProps) => {
|
|
71
|
+
const { t } = useLocale();
|
|
72
|
+
return (
|
|
73
|
+
<Button
|
|
74
|
+
className={cn("size-8 p-0 text-muted-foreground hover:text-foreground", className)}
|
|
75
|
+
size={size}
|
|
76
|
+
type="button"
|
|
77
|
+
variant={variant}
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
{children ?? <XIcon className="size-4" />}
|
|
81
|
+
<span className="sr-only">{t("close")}</span>
|
|
82
|
+
</Button>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
82
85
|
|
|
83
86
|
export type ArtifactTitleProps = HTMLAttributes<HTMLParagraphElement>;
|
|
84
87
|
|
|
85
88
|
export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
|
|
86
|
-
<p className={cn("font-medium text-foreground text-
|
|
89
|
+
<p className={cn("font-medium text-foreground text-body", className)} {...props} />
|
|
87
90
|
);
|
|
88
91
|
|
|
89
92
|
export type ArtifactDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
|
|
90
93
|
|
|
91
94
|
export const ArtifactDescription = ({ className, ...props }: ArtifactDescriptionProps) => (
|
|
92
|
-
<p className={cn("text-muted-foreground text-
|
|
95
|
+
<p className={cn("text-muted-foreground text-body", className)} {...props} />
|
|
93
96
|
);
|
|
94
97
|
|
|
95
98
|
export type ArtifactActionsProps = HTMLAttributes<HTMLDivElement>;
|
|
@@ -54,6 +54,46 @@ describe("AssetPreview (#193, research 04 §5 ASSET-4)", () => {
|
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
// The old parser split every line on a bare `,` — a comma, an escaped quote,
|
|
58
|
+
// or a newline INSIDE a quoted field corrupted the row/column count.
|
|
59
|
+
describe("AssetPreview CSV parsing — quoted fields (review #1)", () => {
|
|
60
|
+
it("keeps a comma inside a quoted field as part of that one cell", () => {
|
|
61
|
+
const asset: ContextAsset = {
|
|
62
|
+
id: "quoted-comma",
|
|
63
|
+
name: "quoted-comma.csv",
|
|
64
|
+
type: "csv",
|
|
65
|
+
content: 'name,note\n"Acme, Inc.",big account',
|
|
66
|
+
};
|
|
67
|
+
render(<AssetPreview asset={asset} />);
|
|
68
|
+
expect(screen.getByText("Acme, Inc.")).toBeInTheDocument();
|
|
69
|
+
expect(screen.getByText("1 row")).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("unescapes a doubled quote inside a quoted field", () => {
|
|
73
|
+
const asset: ContextAsset = {
|
|
74
|
+
id: "escaped-quote",
|
|
75
|
+
name: "escaped-quote.csv",
|
|
76
|
+
type: "csv",
|
|
77
|
+
content: 'name,note\n"Say ""hi""",greeting',
|
|
78
|
+
};
|
|
79
|
+
render(<AssetPreview asset={asset} />);
|
|
80
|
+
expect(screen.getByText('Say "hi"')).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("keeps a newline inside a quoted field from starting a new row", () => {
|
|
84
|
+
const asset: ContextAsset = {
|
|
85
|
+
id: "embedded-newline",
|
|
86
|
+
name: "embedded-newline.csv",
|
|
87
|
+
type: "csv",
|
|
88
|
+
content: 'name,note\n"multi\nline",x\nsecond,y',
|
|
89
|
+
};
|
|
90
|
+
render(<AssetPreview asset={asset} />);
|
|
91
|
+
// Two data rows total — the embedded newline must NOT read as a third.
|
|
92
|
+
expect(screen.getByText("2 rows")).toBeInTheDocument();
|
|
93
|
+
expect(screen.getByText("second")).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
57
97
|
/**
|
|
58
98
|
* The seam that lets a format this package cannot parse (a PDF, a spreadsheet,
|
|
59
99
|
* a video) show up in the rail without `@elabs-ai/components-ai`
|