@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.
- package/README.md +5 -0
- package/dist/components/AgentApprovalGate.d.ts +10 -0
- package/dist/components/AgentContextViewer.d.ts +7 -0
- package/dist/components/AgentGeneratedArtifactPanel.d.ts +8 -0
- package/dist/components/AgentHandoffPanel.d.ts +22 -0
- package/dist/components/AgentInterruptionPanel.d.ts +13 -0
- package/dist/components/AgentMemoryPanel.d.ts +9 -0
- package/dist/components/AgentMessageList.d.ts +8 -0
- package/dist/components/AgentPlanDiff.d.ts +7 -0
- package/dist/components/AgentPlanPreview.d.ts +6 -0
- package/dist/components/AgentPromptBox.d.ts +15 -0
- package/dist/components/AgentRunControls.d.ts +10 -0
- package/dist/components/AgentSafetyPanel.d.ts +6 -0
- package/dist/components/AgentStateBadge.d.ts +6 -0
- package/dist/components/AgentThinkingChain.d.ts +8 -0
- package/dist/components/AgentThinkingPanel.d.ts +8 -0
- package/dist/components/AgentToolCallLog.d.ts +7 -0
- package/dist/components/AgentTraceViewer.d.ts +6 -0
- package/dist/components/AppDomainAgentPanel.d.ts +17 -0
- package/dist/components/ChatAgentRail.d.ts +24 -0
- package/dist/components/ScopeAttachmentPanel.d.ts +7 -0
- package/dist/components/utils.d.ts +20 -0
- package/dist/index.cjs +2709 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +2433 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +2666 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +128 -0
- package/package.json +45 -0
- package/src/components/AgentApprovalGate.tsx +165 -0
- package/src/components/AgentContextViewer.tsx +161 -0
- package/src/components/AgentGeneratedArtifactPanel.tsx +224 -0
- package/src/components/AgentHandoffPanel.tsx +154 -0
- package/src/components/AgentInterruptionPanel.tsx +85 -0
- package/src/components/AgentMemoryPanel.tsx +167 -0
- package/src/components/AgentMessageList.tsx +209 -0
- package/src/components/AgentPlanDiff.tsx +149 -0
- package/src/components/AgentPlanPreview.tsx +106 -0
- package/src/components/AgentPromptBox.tsx +163 -0
- package/src/components/AgentRunControls.tsx +221 -0
- package/src/components/AgentSafetyPanel.tsx +113 -0
- package/src/components/AgentStateBadge.tsx +30 -0
- package/src/components/AgentThinkingChain.tsx +151 -0
- package/src/components/AgentThinkingPanel.tsx +56 -0
- package/src/components/AgentToolCallLog.tsx +262 -0
- package/src/components/AgentTraceViewer.tsx +218 -0
- package/src/components/AppDomainAgentPanel.tsx +66 -0
- package/src/components/ChatAgentRail.tsx +192 -0
- package/src/components/ScopeAttachmentPanel.tsx +130 -0
- package/src/components/utils.ts +186 -0
- package/src/index.test.tsx +212 -0
- package/src/index.tsx +88 -0
- package/src/styles.css +2902 -0
- 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
|
+
}
|