@echothink-ui/agent 0.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.
Files changed (56) hide show
  1. package/README.md +5 -0
  2. package/dist/components/AgentApprovalGate.d.ts +10 -0
  3. package/dist/components/AgentContextViewer.d.ts +7 -0
  4. package/dist/components/AgentGeneratedArtifactPanel.d.ts +8 -0
  5. package/dist/components/AgentHandoffPanel.d.ts +22 -0
  6. package/dist/components/AgentInterruptionPanel.d.ts +13 -0
  7. package/dist/components/AgentMemoryPanel.d.ts +9 -0
  8. package/dist/components/AgentMessageList.d.ts +8 -0
  9. package/dist/components/AgentPlanDiff.d.ts +7 -0
  10. package/dist/components/AgentPlanPreview.d.ts +6 -0
  11. package/dist/components/AgentPromptBox.d.ts +15 -0
  12. package/dist/components/AgentRunControls.d.ts +10 -0
  13. package/dist/components/AgentSafetyPanel.d.ts +6 -0
  14. package/dist/components/AgentStateBadge.d.ts +6 -0
  15. package/dist/components/AgentThinkingChain.d.ts +8 -0
  16. package/dist/components/AgentThinkingPanel.d.ts +8 -0
  17. package/dist/components/AgentToolCallLog.d.ts +7 -0
  18. package/dist/components/AgentTraceViewer.d.ts +6 -0
  19. package/dist/components/AppDomainAgentPanel.d.ts +17 -0
  20. package/dist/components/ChatAgentRail.d.ts +24 -0
  21. package/dist/components/ScopeAttachmentPanel.d.ts +7 -0
  22. package/dist/components/utils.d.ts +20 -0
  23. package/dist/index.cjs +2709 -0
  24. package/dist/index.cjs.map +1 -0
  25. package/dist/index.css +2433 -0
  26. package/dist/index.css.map +1 -0
  27. package/dist/index.d.ts +44 -0
  28. package/dist/index.js +2666 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/types.d.ts +128 -0
  31. package/package.json +45 -0
  32. package/src/components/AgentApprovalGate.tsx +165 -0
  33. package/src/components/AgentContextViewer.tsx +161 -0
  34. package/src/components/AgentGeneratedArtifactPanel.tsx +224 -0
  35. package/src/components/AgentHandoffPanel.tsx +154 -0
  36. package/src/components/AgentInterruptionPanel.tsx +85 -0
  37. package/src/components/AgentMemoryPanel.tsx +167 -0
  38. package/src/components/AgentMessageList.tsx +209 -0
  39. package/src/components/AgentPlanDiff.tsx +149 -0
  40. package/src/components/AgentPlanPreview.tsx +106 -0
  41. package/src/components/AgentPromptBox.tsx +163 -0
  42. package/src/components/AgentRunControls.tsx +221 -0
  43. package/src/components/AgentSafetyPanel.tsx +113 -0
  44. package/src/components/AgentStateBadge.tsx +30 -0
  45. package/src/components/AgentThinkingChain.tsx +151 -0
  46. package/src/components/AgentThinkingPanel.tsx +56 -0
  47. package/src/components/AgentToolCallLog.tsx +262 -0
  48. package/src/components/AgentTraceViewer.tsx +218 -0
  49. package/src/components/AppDomainAgentPanel.tsx +66 -0
  50. package/src/components/ChatAgentRail.tsx +192 -0
  51. package/src/components/ScopeAttachmentPanel.tsx +130 -0
  52. package/src/components/utils.ts +186 -0
  53. package/src/index.test.tsx +212 -0
  54. package/src/index.tsx +88 -0
  55. package/src/styles.css +2902 -0
  56. package/src/types.ts +158 -0
@@ -0,0 +1,151 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Badge, EmptyState, StatusDot, Surface, statusLabel } from "@echothink-ui/core";
4
+ import type { SurfaceComponentProps } from "@echothink-ui/core";
5
+ import type { AgentStep } from "../types";
6
+ import { formatDuration } from "./utils";
7
+
8
+ export interface AgentThinkingChainProps extends Omit<SurfaceComponentProps, "children"> {
9
+ steps: AgentStep[];
10
+ streaming?: boolean;
11
+ redactionPolicy?: string;
12
+ }
13
+
14
+ export function AgentThinkingChain({
15
+ steps,
16
+ streaming,
17
+ redactionPolicy,
18
+ title = "Thinking chain",
19
+ className,
20
+ ...props
21
+ }: AgentThinkingChainProps) {
22
+ return (
23
+ <Surface
24
+ {...props}
25
+ className={clsx("eth-agent-thinking-chain", className)}
26
+ data-eth-component="AgentThinkingChain"
27
+ title={title}
28
+ subtitle={redactionPolicy ? `Redaction policy: ${redactionPolicy}` : undefined}
29
+ >
30
+ {steps.length ? (
31
+ <ol className="eth-agent-thinking-chain__steps" aria-live={streaming ? "polite" : "off"}>
32
+ {steps.map((step) => (
33
+ <ThinkingStep key={step.id} step={step} depth={0} />
34
+ ))}
35
+ </ol>
36
+ ) : (
37
+ <EmptyState
38
+ title="No thinking steps"
39
+ description="Steps will appear as the agent records observations, actions, or blockers."
40
+ />
41
+ )}
42
+ </Surface>
43
+ );
44
+ }
45
+
46
+ function ThinkingStep({ step, depth }: { step: AgentStep; depth: number }) {
47
+ const hasChildren = Boolean(step.children?.length);
48
+ const stepClassName = clsx(
49
+ "eth-agent-thinking-chain__step",
50
+ `eth-agent-thinking-chain__step--${step.status}`,
51
+ hasChildren && "eth-agent-thinking-chain__step--branch"
52
+ );
53
+
54
+ return (
55
+ <li
56
+ className={clsx(
57
+ "eth-agent-thinking-chain__item",
58
+ `eth-agent-thinking-chain__item--${step.status}`
59
+ )}
60
+ style={
61
+ {
62
+ "--eth-agent-thinking-chain-depth-offset": `${depth * 1.5}rem`
63
+ } as React.CSSProperties
64
+ }
65
+ >
66
+ <span className="eth-agent-thinking-chain__marker" aria-hidden="true" />
67
+ <div className="eth-agent-thinking-chain__item-body">
68
+ <article className={stepClassName}>
69
+ <StepHeader step={step} hasChildren={hasChildren} />
70
+ <StepDetails step={step} />
71
+ </article>
72
+ {hasChildren ? (
73
+ <ol className="eth-agent-thinking-chain__children">
74
+ {step.children?.map((child) => (
75
+ <ThinkingStep key={child.id} step={child} depth={depth + 1} />
76
+ ))}
77
+ </ol>
78
+ ) : null}
79
+ </div>
80
+ </li>
81
+ );
82
+ }
83
+
84
+ function StepHeader({ step, hasChildren }: { step: AgentStep; hasChildren?: boolean }) {
85
+ return (
86
+ <header className="eth-agent-thinking-chain__step-header">
87
+ <span className="eth-agent-thinking-chain__step-heading">
88
+ <strong className="eth-agent-thinking-chain__step-title">{step.title}</strong>
89
+ {hasChildren ? (
90
+ <span className="eth-agent-thinking-chain__branch-count">
91
+ {step.children?.length} {step.children?.length === 1 ? "substep" : "substeps"}
92
+ </span>
93
+ ) : null}
94
+ </span>
95
+ <span className="eth-agent-thinking-chain__step-meta">
96
+ {step.redacted ? <Badge severity="warning">Redacted</Badge> : null}
97
+ <StatusDot status={step.status} label={statusLabel(step.status)} />
98
+ {step.durationMs != null ? (
99
+ <span className="eth-agent-thinking-chain__duration">
100
+ {formatDuration(step.durationMs)}
101
+ </span>
102
+ ) : null}
103
+ </span>
104
+ </header>
105
+ );
106
+ }
107
+
108
+ function StepDetails({ step }: { step: AgentStep }) {
109
+ const hasDetails = Boolean(step.observation || step.action || step.blocker);
110
+
111
+ if (step.redacted) {
112
+ return <p className="eth-agent-thinking-chain__redacted">Details are hidden by policy.</p>;
113
+ }
114
+
115
+ if (!hasDetails) {
116
+ return <p className="eth-agent-thinking-chain__empty-detail">No public details recorded.</p>;
117
+ }
118
+
119
+ return (
120
+ <dl className="eth-agent-thinking-chain__fields">
121
+ {step.observation ? <StepField label="Observation" value={step.observation} /> : null}
122
+ {step.action ? <StepField label="Action" value={step.action} monospaced /> : null}
123
+ {step.blocker ? (
124
+ <StepField
125
+ label="Blocker"
126
+ value={step.blocker}
127
+ className="eth-agent-thinking-chain__field--blocker"
128
+ />
129
+ ) : null}
130
+ </dl>
131
+ );
132
+ }
133
+
134
+ function StepField({
135
+ label,
136
+ value,
137
+ monospaced,
138
+ className
139
+ }: {
140
+ label: string;
141
+ value: string;
142
+ monospaced?: boolean;
143
+ className?: string;
144
+ }) {
145
+ return (
146
+ <div className={clsx("eth-agent-thinking-chain__field", className)}>
147
+ <dt>{label}</dt>
148
+ <dd className={clsx(monospaced && "eth-agent-thinking-chain__field-value--mono")}>{value}</dd>
149
+ </div>
150
+ );
151
+ }
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Surface } from "@echothink-ui/core";
4
+ import type { SurfaceComponentProps } from "@echothink-ui/core";
5
+ import { formatDateTime } from "./utils";
6
+
7
+ export interface AgentThinkingPanelProps extends Omit<SurfaceComponentProps, "children"> {
8
+ currentStep?: string;
9
+ summary?: string;
10
+ progress?: number;
11
+ lastUpdatedAt?: string;
12
+ }
13
+
14
+ export function AgentThinkingPanel({
15
+ currentStep,
16
+ summary,
17
+ progress,
18
+ lastUpdatedAt,
19
+ title = "Agent thinking",
20
+ subtitle,
21
+ metadata = [],
22
+ className,
23
+ ...props
24
+ }: AgentThinkingPanelProps) {
25
+ const normalizedProgress =
26
+ typeof progress === "number" && Number.isFinite(progress)
27
+ ? Math.max(0, Math.min(100, Math.round(progress)))
28
+ : undefined;
29
+ const panelMetadata = lastUpdatedAt
30
+ ? [{ label: "Last updated", value: formatDateTime(lastUpdatedAt) }, ...metadata]
31
+ : metadata;
32
+
33
+ return (
34
+ <Surface
35
+ {...props}
36
+ className={clsx("eth-agent-thinking-panel", className)}
37
+ data-eth-component="AgentThinkingPanel"
38
+ title={title}
39
+ subtitle={currentStep ?? subtitle}
40
+ metadata={panelMetadata.length ? panelMetadata : undefined}
41
+ >
42
+ {summary ? <p className="eth-agent-thinking-panel__summary">{summary}</p> : null}
43
+ {normalizedProgress != null ? (
44
+ <div className="eth-agent-thinking-panel__progress">
45
+ <div className="eth-agent-thinking-panel__progress-label">
46
+ <span>Progress</span>
47
+ <strong>{normalizedProgress}%</strong>
48
+ </div>
49
+ <progress max={100} value={normalizedProgress} aria-label="Agent progress">
50
+ {normalizedProgress}%
51
+ </progress>
52
+ </div>
53
+ ) : null}
54
+ </Surface>
55
+ );
56
+ }
@@ -0,0 +1,262 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Button, Drawer, EmptyState, StatusDot, Surface, statusLabel } from "@echothink-ui/core";
4
+ import type { EthOperationalStatus, SurfaceComponentProps } from "@echothink-ui/core";
5
+ import { DataTable, type DataColumn } from "@echothink-ui/data";
6
+ import { SearchIcon } from "@echothink-ui/icons";
7
+ import type { AgentToolCall } from "../types";
8
+ import { EMPTY_MARK, formatDuration, formatTime, jsonPreview, prettyJson } from "./utils";
9
+
10
+ export interface AgentToolCallLogProps extends Omit<SurfaceComponentProps, "children" | "onSelect"> {
11
+ calls: AgentToolCall[];
12
+ onSelect?: (call: AgentToolCall) => void;
13
+ }
14
+
15
+ interface ToolCallRow extends Record<string, unknown> {
16
+ id: string;
17
+ call: AgentToolCall;
18
+ time: string;
19
+ toolName: string;
20
+ status: EthOperationalStatus;
21
+ duration: string;
22
+ argsPreview: string;
23
+ argsTone: PreviewTone;
24
+ resultPreview: string;
25
+ resultTone: PreviewTone;
26
+ }
27
+
28
+ type PreviewTone = "default" | "empty" | "error" | "redacted";
29
+
30
+ export function AgentToolCallLog({
31
+ calls,
32
+ onSelect,
33
+ title = "Tool calls",
34
+ subtitle,
35
+ className,
36
+ ...props
37
+ }: AgentToolCallLogProps) {
38
+ const [selectedCall, setSelectedCall] = React.useState<AgentToolCall | undefined>();
39
+ const rows = React.useMemo<ToolCallRow[]>(
40
+ () =>
41
+ calls.map((call) => {
42
+ const argsPreview = jsonPreview(call.args, call.redacted);
43
+ const resultPreview = call.error ?? jsonPreview(call.result, call.redacted);
44
+
45
+ return {
46
+ id: call.id,
47
+ call,
48
+ time: formatTime(call.startedAt),
49
+ toolName: call.toolName,
50
+ status: call.status,
51
+ duration: formatDuration(call.durationMs),
52
+ argsPreview,
53
+ argsTone: previewTone(argsPreview, call.redacted),
54
+ resultPreview,
55
+ resultTone: call.error ? "error" : previewTone(resultPreview, call.redacted)
56
+ };
57
+ }),
58
+ [calls]
59
+ );
60
+
61
+ const openCall = React.useCallback(
62
+ (call: AgentToolCall) => {
63
+ setSelectedCall(call);
64
+ onSelect?.(call);
65
+ },
66
+ [onSelect]
67
+ );
68
+
69
+ const columns = React.useMemo<DataColumn<ToolCallRow>[]>(
70
+ () => [
71
+ {
72
+ key: "time",
73
+ header: "Time",
74
+ width: "7rem",
75
+ render: (row) => (
76
+ <time className="eth-agent-tool-call-log__time" dateTime={row.call.startedAt}>
77
+ {row.time}
78
+ </time>
79
+ )
80
+ },
81
+ {
82
+ key: "toolName",
83
+ header: "Tool",
84
+ width: "13rem",
85
+ render: (row) => <code className="eth-agent-tool-call-log__tool">{row.toolName}</code>
86
+ },
87
+ {
88
+ key: "status",
89
+ header: "Status",
90
+ width: "11rem",
91
+ render: (row) => <StatusDot status={row.status} label={statusLabel(row.status)} />
92
+ },
93
+ {
94
+ key: "duration",
95
+ header: "Duration",
96
+ width: "7rem",
97
+ align: "end",
98
+ render: (row) => <PreviewText value={row.duration} tone={previewTone(row.duration)} />
99
+ },
100
+ {
101
+ key: "argsPreview",
102
+ header: "Args",
103
+ render: (row) => <PreviewText value={row.argsPreview} tone={row.argsTone} />
104
+ },
105
+ {
106
+ key: "resultPreview",
107
+ header: "Result",
108
+ render: (row) => <PreviewText value={row.resultPreview} tone={row.resultTone} />
109
+ },
110
+ {
111
+ key: "actions",
112
+ header: "Actions",
113
+ width: "7.5rem",
114
+ align: "end",
115
+ render: (row) => (
116
+ <Button
117
+ aria-label={`Inspect ${row.toolName} tool call`}
118
+ className="eth-agent-tool-call-log__inspect"
119
+ density="compact"
120
+ icon={<SearchIcon size={16} />}
121
+ intent="ghost"
122
+ onClick={() => openCall(row.call)}
123
+ >
124
+ Inspect
125
+ </Button>
126
+ )
127
+ }
128
+ ],
129
+ [openCall]
130
+ );
131
+
132
+ const defaultSubtitle = React.useMemo(() => summarizeCalls(calls), [calls]);
133
+
134
+ return (
135
+ <>
136
+ <Surface
137
+ {...props}
138
+ className={clsx("eth-agent-tool-call-log", className)}
139
+ data-eth-component="AgentToolCallLog"
140
+ subtitle={subtitle ?? defaultSubtitle}
141
+ title={title}
142
+ >
143
+ <DataTable
144
+ rows={rows}
145
+ columns={columns}
146
+ rowKey="id"
147
+ density="compact"
148
+ className="eth-agent-tool-call-log__table"
149
+ emptyState={<EmptyState title="No tool calls" />}
150
+ />
151
+ </Surface>
152
+ <Drawer
153
+ open={Boolean(selectedCall)}
154
+ title={selectedCall?.toolName ?? "Tool call"}
155
+ onClose={() => setSelectedCall(undefined)}
156
+ >
157
+ {selectedCall ? (
158
+ <div className="eth-agent-tool-call-log__drawer">
159
+ <div className="eth-agent-tool-call-log__drawer-status">
160
+ <StatusDot status={selectedCall.status} label={statusLabel(selectedCall.status)} />
161
+ {selectedCall.redacted ? (
162
+ <span className="eth-agent-tool-call-log__redaction-note">
163
+ Redaction applied
164
+ </span>
165
+ ) : null}
166
+ </div>
167
+ <dl className="eth-agent-tool-call-log__meta">
168
+ <div>
169
+ <dt>Started</dt>
170
+ <dd>{formatTime(selectedCall.startedAt)}</dd>
171
+ </div>
172
+ <div>
173
+ <dt>Duration</dt>
174
+ <dd>{formatDuration(selectedCall.durationMs)}</dd>
175
+ </div>
176
+ <div>
177
+ <dt>Tool</dt>
178
+ <dd>
179
+ <code>{selectedCall.toolName}</code>
180
+ </dd>
181
+ </div>
182
+ <div>
183
+ <dt>Redaction</dt>
184
+ <dd>{selectedCall.redacted ? "Applied" : "None"}</dd>
185
+ </div>
186
+ </dl>
187
+ {selectedCall.error ? (
188
+ <section className="eth-agent-tool-call-log__error">
189
+ <h3>Error</h3>
190
+ <pre tabIndex={0}>{selectedCall.error}</pre>
191
+ </section>
192
+ ) : null}
193
+ <JsonBlock
194
+ title="Arguments"
195
+ value={prettyJson(selectedCall.args, selectedCall.redacted)}
196
+ />
197
+ <JsonBlock
198
+ title="Result"
199
+ value={prettyJson(selectedCall.result, selectedCall.redacted)}
200
+ />
201
+ </div>
202
+ ) : null}
203
+ </Drawer>
204
+ </>
205
+ );
206
+ }
207
+
208
+ function summarizeCalls(calls: AgentToolCall[]) {
209
+ if (!calls.length) return undefined;
210
+ const active = calls.filter((call) =>
211
+ ["queued", "running", "in-progress", "pending-approval", "approval-required"].includes(
212
+ call.status
213
+ )
214
+ ).length;
215
+ const redacted = calls.filter((call) => call.redacted).length;
216
+ return [
217
+ `${calls.length} call${calls.length === 1 ? "" : "s"}`,
218
+ active ? `${active} active` : null,
219
+ redacted ? `${redacted} redacted` : null
220
+ ]
221
+ .filter(Boolean)
222
+ .join(" | ");
223
+ }
224
+
225
+ function previewTone(value: string, redacted = false): PreviewTone {
226
+ if (redacted && value === "[redacted]") return "redacted";
227
+ if (value === EMPTY_MARK) return "empty";
228
+ return "default";
229
+ }
230
+
231
+ function PreviewText({ value, tone = "default" }: { value: string; tone?: PreviewTone }) {
232
+ if (tone === "redacted") {
233
+ return (
234
+ <span className="eth-agent-tool-call-log__preview eth-agent-tool-call-log__preview--redacted">
235
+ Redacted
236
+ </span>
237
+ );
238
+ }
239
+
240
+ return (
241
+ <span
242
+ className={clsx(
243
+ "eth-agent-tool-call-log__preview",
244
+ tone !== "default" && `eth-agent-tool-call-log__preview--${tone}`
245
+ )}
246
+ title={value === EMPTY_MARK ? undefined : value}
247
+ >
248
+ {value}
249
+ </span>
250
+ );
251
+ }
252
+
253
+ function JsonBlock({ title, value }: { title: string; value: string }) {
254
+ const headingId = React.useId();
255
+
256
+ return (
257
+ <section className="eth-agent-tool-call-log__json" aria-labelledby={headingId}>
258
+ <h3 id={headingId}>{title}</h3>
259
+ <pre tabIndex={0}>{value}</pre>
260
+ </section>
261
+ );
262
+ }
@@ -0,0 +1,218 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Badge, EmptyState, StatusDot, Surface, statusLabel } from "@echothink-ui/core";
4
+ import type { SurfaceComponentProps } from "@echothink-ui/core";
5
+ import type { AgentTraceSpan } from "../types";
6
+ import { formatDuration, formatTime, prettyJson, statusSeverity } from "./utils";
7
+
8
+ export interface AgentTraceViewerProps extends Omit<SurfaceComponentProps, "children"> {
9
+ spans: AgentTraceSpan[];
10
+ }
11
+
12
+ interface TimelineSpan {
13
+ span: AgentTraceSpan;
14
+ start: number;
15
+ end: number;
16
+ depth: number;
17
+ }
18
+
19
+ export function AgentTraceViewer({
20
+ spans,
21
+ title = "Trace viewer",
22
+ className,
23
+ ...props
24
+ }: AgentTraceViewerProps) {
25
+ const [selectedSpanId, setSelectedSpanId] = React.useState<string | undefined>(spans[0]?.id);
26
+ const timeline = React.useMemo(() => buildTimeline(spans), [spans]);
27
+ const selectedSpan = spans.find((span) => span.id === selectedSpanId) ?? spans[0];
28
+ const selectedTimelineItem = timeline.items.find((item) => item.span.id === selectedSpan?.id);
29
+ const activeCount = spans.filter((span) =>
30
+ ["running", "in-progress", "pending-approval", "approval-required"].includes(span.status)
31
+ ).length;
32
+ const selectedAttributes = Object.entries(selectedSpan?.attributes ?? {});
33
+
34
+ return (
35
+ <Surface
36
+ {...props}
37
+ className={clsx("eth-trace", "eth-agent-trace-viewer", className)}
38
+ data-eth-component="AgentTraceViewer"
39
+ title={title}
40
+ >
41
+ {!spans.length ? (
42
+ <EmptyState title="No trace spans" />
43
+ ) : (
44
+ <>
45
+ <dl className="eth-trace__summary" aria-label="Trace summary">
46
+ <div>
47
+ <dt>Spans</dt>
48
+ <dd>{spans.length}</dd>
49
+ </div>
50
+ <div>
51
+ <dt>Window</dt>
52
+ <dd>{formatDuration(timeline.total)}</dd>
53
+ </div>
54
+ <div>
55
+ <dt>Active</dt>
56
+ <dd>{activeCount}</dd>
57
+ </div>
58
+ </dl>
59
+
60
+ <div className="eth-trace__layout">
61
+ <section className="eth-trace__timeline-panel" aria-label="Trace timeline">
62
+ <div className="eth-trace__axis" aria-hidden="true">
63
+ <span>0 ms</span>
64
+ <span>{formatDuration(timeline.total / 2)}</span>
65
+ <span>{formatDuration(timeline.total)}</span>
66
+ </div>
67
+
68
+ <div className="eth-trace__timeline" role="list">
69
+ {timeline.items.map(({ span, start, end, depth }) => {
70
+ const left = ((start - timeline.min) / timeline.total) * 100;
71
+ const width = Math.max(((end - start) / timeline.total) * 100, 1);
72
+ const selected = selectedSpan?.id === span.id;
73
+
74
+ return (
75
+ <div
76
+ key={span.id}
77
+ className={clsx("eth-trace__row", selected && "eth-trace__row--selected")}
78
+ role="listitem"
79
+ style={
80
+ {
81
+ "--eth-trace-indent": `${depth}rem`,
82
+ "--eth-trace-left": `${left}%`,
83
+ "--eth-trace-width": `${width}%`
84
+ } as React.CSSProperties
85
+ }
86
+ >
87
+ <button
88
+ type="button"
89
+ className={clsx(
90
+ "eth-trace__span-label",
91
+ selected && "eth-trace__span-label--selected"
92
+ )}
93
+ aria-pressed={selected}
94
+ onClick={() => setSelectedSpanId(span.id)}
95
+ >
96
+ <span className="eth-trace__span-title">
97
+ <StatusDot status={span.status} label={statusLabel(span.status)} />
98
+ <span className="eth-trace__span-name">{span.name}</span>
99
+ </span>
100
+ <span className="eth-trace__span-meta">
101
+ {formatTime(span.startedAt)} · {formatDuration(span.durationMs)}
102
+ </span>
103
+ </button>
104
+
105
+ <div
106
+ className="eth-trace__track"
107
+ role="group"
108
+ aria-label={`${span.name}, ${statusLabel(span.status)}, ${formatDuration(span.durationMs)}`}
109
+ >
110
+ <button
111
+ type="button"
112
+ className={clsx(
113
+ "eth-trace__bar",
114
+ `eth-trace__bar--${span.status}`,
115
+ selected && "eth-trace__bar--selected"
116
+ )}
117
+ aria-label={`Select ${span.name}`}
118
+ onClick={() => setSelectedSpanId(span.id)}
119
+ />
120
+ </div>
121
+ </div>
122
+ );
123
+ })}
124
+ </div>
125
+ </section>
126
+
127
+ <aside className="eth-trace__details" aria-label="Selected span details">
128
+ {selectedSpan ? (
129
+ <>
130
+ <header className="eth-trace__details-header">
131
+ <div>
132
+ <p className="eth-trace__details-eyebrow">Selected span</p>
133
+ <h3>{selectedSpan.name}</h3>
134
+ </div>
135
+ <Badge severity={statusSeverity(selectedSpan.status)}>
136
+ {statusLabel(selectedSpan.status)}
137
+ </Badge>
138
+ </header>
139
+
140
+ <dl className="eth-trace__detail-grid">
141
+ <div>
142
+ <dt>Started</dt>
143
+ <dd>{formatTime(selectedSpan.startedAt)}</dd>
144
+ </div>
145
+ <div>
146
+ <dt>Duration</dt>
147
+ <dd>{formatDuration(selectedSpan.durationMs)}</dd>
148
+ </div>
149
+ <div>
150
+ <dt>Depth</dt>
151
+ <dd>{selectedTimelineItem?.depth ?? 0}</dd>
152
+ </div>
153
+ <div>
154
+ <dt>Span ID</dt>
155
+ <dd>{selectedSpan.id}</dd>
156
+ </div>
157
+ {selectedSpan.parentId ? (
158
+ <div>
159
+ <dt>Parent</dt>
160
+ <dd>{selectedSpan.parentId}</dd>
161
+ </div>
162
+ ) : null}
163
+ </dl>
164
+
165
+ <section className="eth-trace__attributes">
166
+ <div className="eth-trace__attributes-header">
167
+ <h4>Attributes</h4>
168
+ <Badge severity="neutral">{selectedAttributes.length}</Badge>
169
+ </div>
170
+ {selectedAttributes.length ? (
171
+ <pre>{prettyJson(selectedSpan.attributes)}</pre>
172
+ ) : (
173
+ <p>No attributes recorded.</p>
174
+ )}
175
+ </section>
176
+ </>
177
+ ) : (
178
+ <EmptyState title="Select a span" />
179
+ )}
180
+ </aside>
181
+ </div>
182
+ </>
183
+ )}
184
+ </Surface>
185
+ );
186
+ }
187
+
188
+ function buildTimeline(spans: AgentTraceSpan[]) {
189
+ if (!spans.length) return { items: [] as TimelineSpan[], min: 0, total: 1 };
190
+
191
+ const spansById = new Map(spans.map((span) => [span.id, span]));
192
+ const depthById = new Map<string, number>();
193
+
194
+ const depthFor = (span: AgentTraceSpan, seen = new Set<string>()): number => {
195
+ if (depthById.has(span.id)) return depthById.get(span.id) ?? 0;
196
+ if (!span.parentId || seen.has(span.id)) {
197
+ depthById.set(span.id, 0);
198
+ return 0;
199
+ }
200
+ const parent = spansById.get(span.parentId);
201
+ const depth = parent ? depthFor(parent, new Set(seen).add(span.id)) + 1 : 0;
202
+ depthById.set(span.id, depth);
203
+ return depth;
204
+ };
205
+
206
+ const items: TimelineSpan[] = spans
207
+ .map((span, index) => {
208
+ const parsed = new Date(span.startedAt).getTime();
209
+ const start = Number.isNaN(parsed) ? index * 10 : parsed;
210
+ const end = start + Math.max(span.durationMs, 1);
211
+ return { span, start, end, depth: depthFor(span) };
212
+ })
213
+ .sort((a, b) => a.start - b.start);
214
+
215
+ const min = Math.min(...items.map((item) => item.start));
216
+ const max = Math.max(...items.map((item) => item.end));
217
+ return { items, min, total: Math.max(max - min, 1) };
218
+ }