@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,85 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { Button, InlineNotification, Surface, Textarea } from "@echothink-ui/core";
|
|
4
|
+
import type { SurfaceComponentProps } from "@echothink-ui/core";
|
|
5
|
+
import { InterruptIcon } from "@echothink-ui/icons";
|
|
6
|
+
|
|
7
|
+
export interface AgentInterruptionPanelProps
|
|
8
|
+
extends Omit<SurfaceComponentProps, "children" | "onChange" | "onSubmit"> {
|
|
9
|
+
prompt: string;
|
|
10
|
+
onChange: (prompt: string) => void;
|
|
11
|
+
onSubmit?: (prompt: string) => void;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
promptLabel?: React.ReactNode;
|
|
14
|
+
helperText?: React.ReactNode;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
submitLabel?: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const defaultMetadata: NonNullable<SurfaceComponentProps["metadata"]> = [
|
|
20
|
+
{ label: "Mode", value: "Steering interrupt" },
|
|
21
|
+
{ label: "Delivery", value: "Before next agent step" }
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function AgentInterruptionPanel({
|
|
25
|
+
prompt,
|
|
26
|
+
onChange,
|
|
27
|
+
onSubmit,
|
|
28
|
+
disabled,
|
|
29
|
+
title = "Interrupt agent",
|
|
30
|
+
subtitle = "Send operator guidance before the agent continues.",
|
|
31
|
+
metadata = defaultMetadata,
|
|
32
|
+
promptLabel = "Steering instruction",
|
|
33
|
+
helperText = "Delivered before the next agent step.",
|
|
34
|
+
placeholder = "Tell the agent what to change, stop, or prioritize.",
|
|
35
|
+
submitLabel = "Send interruption",
|
|
36
|
+
className,
|
|
37
|
+
...props
|
|
38
|
+
}: AgentInterruptionPanelProps) {
|
|
39
|
+
const hasPrompt = Boolean(prompt.trim());
|
|
40
|
+
const submit = () => {
|
|
41
|
+
if (!disabled && hasPrompt) onSubmit?.(prompt);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Surface
|
|
46
|
+
{...props}
|
|
47
|
+
className={clsx("eth-agent-interruption-panel", className)}
|
|
48
|
+
data-eth-component="AgentInterruptionPanel"
|
|
49
|
+
metadata={metadata}
|
|
50
|
+
subtitle={subtitle}
|
|
51
|
+
title={title}
|
|
52
|
+
>
|
|
53
|
+
<div className="eth-agent-interruption-panel__body">
|
|
54
|
+
<InlineNotification severity="warning" title="Steering interrupt">
|
|
55
|
+
The agent will receive this instruction before continuing the current run.
|
|
56
|
+
</InlineNotification>
|
|
57
|
+
<Textarea
|
|
58
|
+
className="eth-agent-interruption-panel__prompt"
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
helperText={helperText}
|
|
61
|
+
labelText={promptLabel}
|
|
62
|
+
placeholder={placeholder}
|
|
63
|
+
rows={4}
|
|
64
|
+
value={prompt}
|
|
65
|
+
onChange={(event) => onChange(event.currentTarget.value)}
|
|
66
|
+
onKeyDown={(event) => {
|
|
67
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
submit();
|
|
70
|
+
}
|
|
71
|
+
}}
|
|
72
|
+
/>
|
|
73
|
+
<div className="eth-agent-interruption-panel__actions">
|
|
74
|
+
<Button
|
|
75
|
+
disabled={disabled || !hasPrompt}
|
|
76
|
+
icon={<InterruptIcon size={16} />}
|
|
77
|
+
onClick={submit}
|
|
78
|
+
>
|
|
79
|
+
{submitLabel}
|
|
80
|
+
</Button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</Surface>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { Badge, Button, EmptyState, Surface } from "@echothink-ui/core";
|
|
4
|
+
import type { SurfaceComponentProps } from "@echothink-ui/core";
|
|
5
|
+
import type { AgentMemoryEntry } from "../types";
|
|
6
|
+
import { formatDateTime } from "./utils";
|
|
7
|
+
|
|
8
|
+
export interface AgentMemoryPanelProps extends Omit<SurfaceComponentProps, "children"> {
|
|
9
|
+
entries: AgentMemoryEntry[];
|
|
10
|
+
onPin?: (id: string) => void;
|
|
11
|
+
onEdit?: (id: string) => void;
|
|
12
|
+
onDelete?: (id: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const kinds: AgentMemoryEntry["kind"][] = ["user", "project", "feedback", "reference"];
|
|
16
|
+
|
|
17
|
+
export function AgentMemoryPanel({
|
|
18
|
+
entries,
|
|
19
|
+
onPin,
|
|
20
|
+
onEdit,
|
|
21
|
+
onDelete,
|
|
22
|
+
title = "Agent memory",
|
|
23
|
+
subtitle = "Stored instructions and retrieved context available to the agent",
|
|
24
|
+
className,
|
|
25
|
+
...props
|
|
26
|
+
}: AgentMemoryPanelProps) {
|
|
27
|
+
const headingPrefix = React.useId();
|
|
28
|
+
const groups = React.useMemo(
|
|
29
|
+
() =>
|
|
30
|
+
kinds
|
|
31
|
+
.map((kind) => ({ kind, entries: entries.filter((entry) => entry.kind === kind) }))
|
|
32
|
+
.filter((group) => group.entries.length),
|
|
33
|
+
[entries]
|
|
34
|
+
);
|
|
35
|
+
const pinnedCount = React.useMemo(
|
|
36
|
+
() => entries.filter((entry) => entry.pinned).length,
|
|
37
|
+
[entries]
|
|
38
|
+
);
|
|
39
|
+
const latestUpdatedAt = React.useMemo(() => {
|
|
40
|
+
const sortedEntries = entries
|
|
41
|
+
.filter((entry): entry is AgentMemoryEntry & { lastUpdatedAt: string } =>
|
|
42
|
+
Boolean(entry.lastUpdatedAt && !Number.isNaN(new Date(entry.lastUpdatedAt).getTime()))
|
|
43
|
+
)
|
|
44
|
+
.sort((a, b) => new Date(b.lastUpdatedAt).getTime() - new Date(a.lastUpdatedAt).getTime());
|
|
45
|
+
|
|
46
|
+
return sortedEntries[0]?.lastUpdatedAt;
|
|
47
|
+
}, [entries]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Surface
|
|
51
|
+
{...props}
|
|
52
|
+
className={clsx("eth-agent-memory-panel", className)}
|
|
53
|
+
data-eth-component="AgentMemoryPanel"
|
|
54
|
+
subtitle={subtitle}
|
|
55
|
+
title={title}
|
|
56
|
+
>
|
|
57
|
+
{!groups.length ? (
|
|
58
|
+
<EmptyState title="No memory entries" />
|
|
59
|
+
) : (
|
|
60
|
+
<div className="eth-agent-memory-panel__body">
|
|
61
|
+
<dl className="eth-agent-memory-panel__summary" aria-label="Memory summary">
|
|
62
|
+
<div className="eth-agent-memory-panel__summary-item">
|
|
63
|
+
<dt>Total entries</dt>
|
|
64
|
+
<dd>{entries.length}</dd>
|
|
65
|
+
</div>
|
|
66
|
+
<div className="eth-agent-memory-panel__summary-item">
|
|
67
|
+
<dt>Pinned</dt>
|
|
68
|
+
<dd>{pinnedCount}</dd>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="eth-agent-memory-panel__summary-item">
|
|
71
|
+
<dt>Latest update</dt>
|
|
72
|
+
<dd>
|
|
73
|
+
{latestUpdatedAt ? (
|
|
74
|
+
<time dateTime={latestUpdatedAt}>{formatDateTime(latestUpdatedAt)}</time>
|
|
75
|
+
) : (
|
|
76
|
+
"Not recorded"
|
|
77
|
+
)}
|
|
78
|
+
</dd>
|
|
79
|
+
</div>
|
|
80
|
+
</dl>
|
|
81
|
+
{groups.map((group) => (
|
|
82
|
+
<section
|
|
83
|
+
key={group.kind}
|
|
84
|
+
className="eth-agent-memory-panel__group"
|
|
85
|
+
aria-labelledby={`${headingPrefix}-${group.kind}`}
|
|
86
|
+
>
|
|
87
|
+
<header className="eth-agent-memory-panel__group-header">
|
|
88
|
+
<h3 id={`${headingPrefix}-${group.kind}`}>{kindTitle(group.kind)}</h3>
|
|
89
|
+
<Badge severity="neutral">
|
|
90
|
+
{group.entries.length} {group.entries.length === 1 ? "entry" : "entries"}
|
|
91
|
+
</Badge>
|
|
92
|
+
</header>
|
|
93
|
+
<div className="eth-agent-memory-panel__entries">
|
|
94
|
+
{group.entries.map((entry) => (
|
|
95
|
+
<article
|
|
96
|
+
key={entry.id}
|
|
97
|
+
className={clsx(
|
|
98
|
+
"eth-agent-memory-panel__entry",
|
|
99
|
+
entry.pinned && "eth-agent-memory-panel__entry--pinned"
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
<header className="eth-agent-memory-panel__entry-header">
|
|
103
|
+
<span className="eth-agent-memory-panel__entry-title">
|
|
104
|
+
<strong>{entry.key}</strong>
|
|
105
|
+
{entry.pinned ? <Badge severity="info">Pinned</Badge> : null}
|
|
106
|
+
</span>
|
|
107
|
+
{entry.lastUpdatedAt ? (
|
|
108
|
+
<time
|
|
109
|
+
className="eth-agent-memory-panel__updated-at"
|
|
110
|
+
dateTime={entry.lastUpdatedAt}
|
|
111
|
+
>
|
|
112
|
+
{formatDateTime(entry.lastUpdatedAt)}
|
|
113
|
+
</time>
|
|
114
|
+
) : null}
|
|
115
|
+
</header>
|
|
116
|
+
<div className="eth-agent-memory-panel__value">{entry.value}</div>
|
|
117
|
+
{onPin || onEdit || onDelete ? (
|
|
118
|
+
<div
|
|
119
|
+
className="eth-agent-memory-panel__actions"
|
|
120
|
+
aria-label={`${entry.key} memory actions`}
|
|
121
|
+
>
|
|
122
|
+
{onPin ? (
|
|
123
|
+
<Button
|
|
124
|
+
aria-label={`${entry.pinned ? "Unpin" : "Pin"} ${entry.key}`}
|
|
125
|
+
density="compact"
|
|
126
|
+
intent="secondary"
|
|
127
|
+
onClick={() => onPin(entry.id)}
|
|
128
|
+
>
|
|
129
|
+
{entry.pinned ? "Unpin" : "Pin"}
|
|
130
|
+
</Button>
|
|
131
|
+
) : null}
|
|
132
|
+
{onEdit ? (
|
|
133
|
+
<Button
|
|
134
|
+
aria-label={`Edit ${entry.key}`}
|
|
135
|
+
density="compact"
|
|
136
|
+
intent="secondary"
|
|
137
|
+
onClick={() => onEdit(entry.id)}
|
|
138
|
+
>
|
|
139
|
+
Edit
|
|
140
|
+
</Button>
|
|
141
|
+
) : null}
|
|
142
|
+
{onDelete ? (
|
|
143
|
+
<Button
|
|
144
|
+
aria-label={`Delete ${entry.key}`}
|
|
145
|
+
density="compact"
|
|
146
|
+
intent="danger"
|
|
147
|
+
onClick={() => onDelete(entry.id)}
|
|
148
|
+
>
|
|
149
|
+
Delete
|
|
150
|
+
</Button>
|
|
151
|
+
) : null}
|
|
152
|
+
</div>
|
|
153
|
+
) : null}
|
|
154
|
+
</article>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
</section>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</Surface>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function kindTitle(kind: AgentMemoryEntry["kind"]) {
|
|
166
|
+
return kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
167
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { StatusDot, Tag, statusLabel } from "@echothink-ui/core";
|
|
4
|
+
import type { AgentMessage } from "../types";
|
|
5
|
+
import { formatDateTime, mergeStyle } from "./utils";
|
|
6
|
+
|
|
7
|
+
export interface AgentMessageListProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
8
|
+
messages: AgentMessage[];
|
|
9
|
+
streaming?: boolean;
|
|
10
|
+
autoScroll?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const roleLabels: Record<AgentMessage["role"], string> = {
|
|
14
|
+
user: "You",
|
|
15
|
+
agent: "Agent",
|
|
16
|
+
system: "System",
|
|
17
|
+
tool: "Tool"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function AgentMessageList({
|
|
21
|
+
messages,
|
|
22
|
+
streaming,
|
|
23
|
+
autoScroll = true,
|
|
24
|
+
className,
|
|
25
|
+
style,
|
|
26
|
+
...props
|
|
27
|
+
}: AgentMessageListProps) {
|
|
28
|
+
const listRef = React.useRef<HTMLDivElement>(null);
|
|
29
|
+
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (!autoScroll) return;
|
|
32
|
+
const node = listRef.current;
|
|
33
|
+
if (!node) return;
|
|
34
|
+
node.scrollTo({ top: node.scrollHeight, behavior: "smooth" });
|
|
35
|
+
}, [autoScroll, messages.length, streaming]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
{...props}
|
|
40
|
+
ref={listRef}
|
|
41
|
+
role="log"
|
|
42
|
+
aria-live={streaming ? "polite" : "off"}
|
|
43
|
+
aria-relevant="additions text"
|
|
44
|
+
className={clsx("eth-message-list", "eth-agent-message-list", className)}
|
|
45
|
+
data-eth-component="AgentMessageList"
|
|
46
|
+
style={mergeStyle(
|
|
47
|
+
{
|
|
48
|
+
background: "var(--eth-color-layer-01)",
|
|
49
|
+
display: "flex",
|
|
50
|
+
flexDirection: "column",
|
|
51
|
+
gap: "1rem",
|
|
52
|
+
maxHeight: "100%",
|
|
53
|
+
overflowY: "auto",
|
|
54
|
+
padding: "1rem"
|
|
55
|
+
},
|
|
56
|
+
style
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
{messages.map((message, index) => {
|
|
60
|
+
const isStreaming = Boolean(message.streaming || (streaming && index === messages.length - 1));
|
|
61
|
+
const isUser = message.role === "user";
|
|
62
|
+
const isSystem = message.role === "system";
|
|
63
|
+
const isTool = message.role === "tool";
|
|
64
|
+
const accentBorder = isTool
|
|
65
|
+
? "3px solid var(--eth-color-border-strong)"
|
|
66
|
+
: "3px solid var(--eth-color-interactive-primary)";
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<article
|
|
70
|
+
key={message.id}
|
|
71
|
+
className={clsx(
|
|
72
|
+
"eth-message-list__item",
|
|
73
|
+
"eth-agent-message",
|
|
74
|
+
`eth-agent-message--${message.role}`,
|
|
75
|
+
isStreaming && "eth-agent-message--streaming"
|
|
76
|
+
)}
|
|
77
|
+
aria-busy={isStreaming || undefined}
|
|
78
|
+
style={{
|
|
79
|
+
alignSelf: isSystem ? "stretch" : isUser ? "flex-end" : "flex-start",
|
|
80
|
+
contentVisibility: "auto",
|
|
81
|
+
containIntrinsicSize: "1px 96px",
|
|
82
|
+
display: "grid",
|
|
83
|
+
gap: "0.375rem",
|
|
84
|
+
maxWidth: isSystem ? "100%" : "min(100%, 42rem)",
|
|
85
|
+
minWidth: 0,
|
|
86
|
+
textAlign: isSystem ? "center" : "start",
|
|
87
|
+
width: isSystem ? "100%" : "fit-content"
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<header
|
|
91
|
+
className="eth-agent-message__header"
|
|
92
|
+
style={{
|
|
93
|
+
alignItems: "center",
|
|
94
|
+
color: "var(--eth-color-text-secondary)",
|
|
95
|
+
display: "flex",
|
|
96
|
+
flexWrap: "wrap",
|
|
97
|
+
fontSize: "0.75rem",
|
|
98
|
+
gap: "0.5rem",
|
|
99
|
+
justifyContent: isUser ? "flex-end" : isSystem ? "center" : "flex-start",
|
|
100
|
+
lineHeight: 1.333
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<span
|
|
104
|
+
className="eth-agent-message__avatar"
|
|
105
|
+
aria-hidden
|
|
106
|
+
style={{
|
|
107
|
+
alignItems: "center",
|
|
108
|
+
background: isSystem
|
|
109
|
+
? "transparent"
|
|
110
|
+
: isUser
|
|
111
|
+
? "var(--eth-color-interactive-primary)"
|
|
112
|
+
: "var(--eth-color-layer-02)",
|
|
113
|
+
border: isSystem
|
|
114
|
+
? "1px dashed var(--eth-color-border-subtle)"
|
|
115
|
+
: "1px solid var(--eth-color-border-subtle)",
|
|
116
|
+
borderRadius: "var(--eth-radius-none)",
|
|
117
|
+
color: isUser ? "#fff" : "var(--eth-color-text-primary)",
|
|
118
|
+
display: "inline-flex",
|
|
119
|
+
fontSize: "0.72rem",
|
|
120
|
+
fontWeight: 700,
|
|
121
|
+
height: 24,
|
|
122
|
+
justifyContent: "center",
|
|
123
|
+
width: 24
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
{roleLabels[message.role].slice(0, 1)}
|
|
127
|
+
</span>
|
|
128
|
+
<strong>{roleLabels[message.role]}</strong>
|
|
129
|
+
<time dateTime={message.createdAt}>{formatDateTime(message.createdAt)}</time>
|
|
130
|
+
{message.status ? (
|
|
131
|
+
<StatusDot status={message.status} label={statusLabel(message.status)} />
|
|
132
|
+
) : null}
|
|
133
|
+
</header>
|
|
134
|
+
|
|
135
|
+
<div
|
|
136
|
+
className="eth-agent-message__bubble"
|
|
137
|
+
style={{
|
|
138
|
+
background: isSystem
|
|
139
|
+
? "var(--eth-color-layer-02)"
|
|
140
|
+
: isUser
|
|
141
|
+
? "var(--eth-color-navy)"
|
|
142
|
+
: "var(--eth-color-layer-02)",
|
|
143
|
+
border: isSystem
|
|
144
|
+
? "1px dashed var(--eth-color-border-subtle)"
|
|
145
|
+
: "1px solid var(--eth-color-border-subtle)",
|
|
146
|
+
borderInlineEnd: isStreaming ? accentBorder : undefined,
|
|
147
|
+
borderInlineStart: isUser || isSystem ? undefined : accentBorder,
|
|
148
|
+
borderRadius: "var(--eth-radius-panel)",
|
|
149
|
+
color: isUser ? "#fff" : "var(--eth-color-text-primary)",
|
|
150
|
+
fontFamily: isTool ? "ui-monospace, SFMono-Regular, Menlo, monospace" : undefined,
|
|
151
|
+
fontSize: isTool ? "0.875rem" : undefined,
|
|
152
|
+
lineHeight: 1.5,
|
|
153
|
+
overflowWrap: "anywhere",
|
|
154
|
+
padding: isSystem ? "0.5rem 0.75rem" : "0.875rem 1rem",
|
|
155
|
+
whiteSpace: isTool ? "pre-wrap" : "normal",
|
|
156
|
+
wordBreak: "normal"
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
{message.content}
|
|
160
|
+
{isStreaming ? (
|
|
161
|
+
<span
|
|
162
|
+
className="eth-agent-message__cursor"
|
|
163
|
+
aria-hidden
|
|
164
|
+
style={{
|
|
165
|
+
animation: "eth-pulse 0.8s infinite",
|
|
166
|
+
color: "var(--eth-color-interactive-primary)",
|
|
167
|
+
display: "inline-block",
|
|
168
|
+
fontWeight: 600,
|
|
169
|
+
marginInlineStart: 3,
|
|
170
|
+
transform: "translateY(1px)"
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
▌
|
|
174
|
+
</span>
|
|
175
|
+
) : null}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{message.citations?.length ? (
|
|
179
|
+
<div
|
|
180
|
+
className="eth-agent-message__citations"
|
|
181
|
+
aria-label="Citations"
|
|
182
|
+
style={{
|
|
183
|
+
display: "flex",
|
|
184
|
+
flexWrap: "wrap",
|
|
185
|
+
gap: "0.375rem",
|
|
186
|
+
justifyContent: isUser ? "flex-end" : isSystem ? "center" : "flex-start"
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{message.citations.map((citation) =>
|
|
190
|
+
citation.href ? (
|
|
191
|
+
<a
|
|
192
|
+
key={citation.id}
|
|
193
|
+
className="eth-agent-message__citation-link eth-tag"
|
|
194
|
+
href={citation.href}
|
|
195
|
+
>
|
|
196
|
+
{citation.label}
|
|
197
|
+
</a>
|
|
198
|
+
) : (
|
|
199
|
+
<Tag key={citation.id}>{citation.label}</Tag>
|
|
200
|
+
)
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
) : null}
|
|
204
|
+
</article>
|
|
205
|
+
);
|
|
206
|
+
})}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { Badge, Surface } from "@echothink-ui/core";
|
|
4
|
+
import type { SurfaceComponentProps } from "@echothink-ui/core";
|
|
5
|
+
import type { AgentPlanStep } from "../types";
|
|
6
|
+
import { diffPlanSteps } from "./utils";
|
|
7
|
+
|
|
8
|
+
export interface AgentPlanDiffProps extends Omit<SurfaceComponentProps, "children"> {
|
|
9
|
+
before: AgentPlanStep[];
|
|
10
|
+
after: AgentPlanStep[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AgentPlanDiff({
|
|
14
|
+
before,
|
|
15
|
+
after,
|
|
16
|
+
title = "Plan diff",
|
|
17
|
+
subtitle = "Compare proposed plan changes before execution",
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: AgentPlanDiffProps) {
|
|
21
|
+
const diff = React.useMemo(() => diffPlanSteps(before, after), [before, after]);
|
|
22
|
+
const summary = React.useMemo(
|
|
23
|
+
() => ({
|
|
24
|
+
added: diff.filter((item) => item.status === "added").length,
|
|
25
|
+
modified: diff.filter((item) => item.status === "modified").length,
|
|
26
|
+
removed: diff.filter((item) => item.status === "removed").length,
|
|
27
|
+
unchanged: diff.filter((item) => item.status === "unchanged").length
|
|
28
|
+
}),
|
|
29
|
+
[diff]
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Surface
|
|
34
|
+
{...props}
|
|
35
|
+
className={clsx("eth-agent-plan-diff", className)}
|
|
36
|
+
data-eth-component="AgentPlanDiff"
|
|
37
|
+
subtitle={subtitle}
|
|
38
|
+
title={title}
|
|
39
|
+
>
|
|
40
|
+
<div className="eth-agent-plan-diff__body">
|
|
41
|
+
<dl className="eth-agent-plan-diff__summary" aria-label="Plan diff summary">
|
|
42
|
+
<SummaryItem label="Added" value={summary.added} tone="added" />
|
|
43
|
+
<SummaryItem label="Modified" value={summary.modified} tone="modified" />
|
|
44
|
+
<SummaryItem label="Removed" value={summary.removed} tone="removed" />
|
|
45
|
+
<SummaryItem label="Unchanged" value={summary.unchanged} tone="unchanged" />
|
|
46
|
+
</dl>
|
|
47
|
+
|
|
48
|
+
{diff.length ? (
|
|
49
|
+
<ol className="eth-agent-plan-diff__list" aria-label="Plan step changes">
|
|
50
|
+
{diff.map((item) => {
|
|
51
|
+
const stepId = item.after?.id ?? item.before?.id ?? item.id;
|
|
52
|
+
return (
|
|
53
|
+
<li key={item.id} className="eth-agent-plan-diff__entry">
|
|
54
|
+
<article
|
|
55
|
+
className={clsx(
|
|
56
|
+
"eth-agent-plan-diff__item",
|
|
57
|
+
`eth-agent-plan-diff__item--${item.status}`
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
<header className="eth-agent-plan-diff__item-header">
|
|
61
|
+
<div className="eth-agent-plan-diff__item-heading">
|
|
62
|
+
<span className="eth-agent-plan-diff__step-id">Step {stepId}</span>
|
|
63
|
+
<Badge severity={item.severity}>{statusLabelFor(item.status)}</Badge>
|
|
64
|
+
</div>
|
|
65
|
+
</header>
|
|
66
|
+
{item.status === "modified" ? (
|
|
67
|
+
<div className="eth-agent-plan-diff__comparison">
|
|
68
|
+
<StepSnapshot label="Before" step={item.before} />
|
|
69
|
+
<StepSnapshot label="After" step={item.after} />
|
|
70
|
+
</div>
|
|
71
|
+
) : (
|
|
72
|
+
<StepSnapshot
|
|
73
|
+
label={snapshotLabelFor(item.status)}
|
|
74
|
+
step={item.after ?? item.before}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</article>
|
|
78
|
+
</li>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
</ol>
|
|
82
|
+
) : (
|
|
83
|
+
<p className="eth-agent-plan-diff__empty">No plan steps to compare.</p>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</Surface>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function StepSnapshot({ label, step }: { label: string; step?: AgentPlanStep }) {
|
|
91
|
+
if (!step) return null;
|
|
92
|
+
return (
|
|
93
|
+
<section className="eth-agent-plan-diff__snapshot">
|
|
94
|
+
<h3 className="eth-agent-plan-diff__snapshot-label">{label}</h3>
|
|
95
|
+
<p className="eth-agent-plan-diff__snapshot-description">{step.description}</p>
|
|
96
|
+
{step.durationEstimate || step.risks?.length ? (
|
|
97
|
+
<div className="eth-agent-plan-diff__snapshot-meta" aria-label={`${label} metadata`}>
|
|
98
|
+
{step.durationEstimate ? <Badge>{step.durationEstimate}</Badge> : null}
|
|
99
|
+
{step.risks?.map((risk) => (
|
|
100
|
+
<Badge key={risk} severity="warning">
|
|
101
|
+
{risk}
|
|
102
|
+
</Badge>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
) : null}
|
|
106
|
+
</section>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function SummaryItem({
|
|
111
|
+
label,
|
|
112
|
+
value,
|
|
113
|
+
tone
|
|
114
|
+
}: {
|
|
115
|
+
label: string;
|
|
116
|
+
value: number;
|
|
117
|
+
tone: "added" | "removed" | "modified" | "unchanged";
|
|
118
|
+
}) {
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
className={clsx(
|
|
122
|
+
"eth-agent-plan-diff__summary-item",
|
|
123
|
+
`eth-agent-plan-diff__summary-item--${tone}`
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
<dt>{label}</dt>
|
|
127
|
+
<dd>{value}</dd>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function statusLabelFor(status: "added" | "removed" | "modified" | "unchanged") {
|
|
133
|
+
switch (status) {
|
|
134
|
+
case "added":
|
|
135
|
+
return "Added";
|
|
136
|
+
case "removed":
|
|
137
|
+
return "Removed";
|
|
138
|
+
case "modified":
|
|
139
|
+
return "Modified";
|
|
140
|
+
case "unchanged":
|
|
141
|
+
return "Unchanged";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function snapshotLabelFor(status: "added" | "removed" | "modified" | "unchanged") {
|
|
146
|
+
if (status === "added") return "Added step";
|
|
147
|
+
if (status === "removed") return "Removed step";
|
|
148
|
+
return "Step";
|
|
149
|
+
}
|