@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,106 @@
|
|
|
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 { AgentPlan } from "../types";
|
|
6
|
+
|
|
7
|
+
export interface AgentPlanPreviewProps extends Omit<SurfaceComponentProps, "children"> {
|
|
8
|
+
plan: AgentPlan;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AgentPlanPreview({ plan, title, className, ...props }: AgentPlanPreviewProps) {
|
|
12
|
+
return (
|
|
13
|
+
<Surface
|
|
14
|
+
{...props}
|
|
15
|
+
className={clsx("eth-agent-plan-preview", className)}
|
|
16
|
+
data-eth-component="AgentPlanPreview"
|
|
17
|
+
title={title ?? "Proposed plan"}
|
|
18
|
+
subtitle="Plan before execution"
|
|
19
|
+
>
|
|
20
|
+
<div className="eth-agent-plan-preview__body">
|
|
21
|
+
<div className="eth-agent-plan-preview__summary" aria-label="Plan summary">
|
|
22
|
+
<div>
|
|
23
|
+
<p className="eth-agent-plan-preview__summary-label">Execution plan</p>
|
|
24
|
+
<p className="eth-agent-plan-preview__summary-title">{plan.title}</p>
|
|
25
|
+
</div>
|
|
26
|
+
<span className="eth-agent-plan-preview__summary-count">
|
|
27
|
+
{plan.steps.length} {plan.steps.length === 1 ? "step" : "steps"}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
{plan.steps.length ? (
|
|
32
|
+
<ol className="eth-agent-plan-preview__steps" aria-label="Proposed execution steps">
|
|
33
|
+
{plan.steps.map((step, index) => (
|
|
34
|
+
<li
|
|
35
|
+
key={step.id}
|
|
36
|
+
className={clsx(
|
|
37
|
+
"eth-agent-plan-preview__step",
|
|
38
|
+
step.risks?.length && "eth-agent-plan-preview__step--risk"
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
<article className="eth-agent-plan-preview__step-card">
|
|
42
|
+
<span className="eth-agent-plan-preview__step-index" aria-hidden="true">
|
|
43
|
+
{String(index + 1).padStart(2, "0")}
|
|
44
|
+
</span>
|
|
45
|
+
<div className="eth-agent-plan-preview__step-main">
|
|
46
|
+
<p className="eth-agent-plan-preview__step-description">{step.description}</p>
|
|
47
|
+
{step.durationEstimate || step.risks?.length ? (
|
|
48
|
+
<div className="eth-agent-plan-preview__step-meta" aria-label="Step metadata">
|
|
49
|
+
{step.durationEstimate ? <Badge>{step.durationEstimate}</Badge> : null}
|
|
50
|
+
{step.risks?.map((risk) => (
|
|
51
|
+
<Badge key={risk} severity="warning">
|
|
52
|
+
{risk}
|
|
53
|
+
</Badge>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
</article>
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</ol>
|
|
62
|
+
) : (
|
|
63
|
+
<p className="eth-agent-plan-preview__empty">No execution steps have been proposed.</p>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{plan.assumptions?.length || plan.risks?.length ? (
|
|
67
|
+
<div className="eth-agent-plan-preview__supporting">
|
|
68
|
+
{plan.assumptions?.length ? (
|
|
69
|
+
<TextList title="Assumptions" items={plan.assumptions} />
|
|
70
|
+
) : null}
|
|
71
|
+
{plan.risks?.length ? <TextList title="Risks" items={plan.risks} tone="risk" /> : null}
|
|
72
|
+
</div>
|
|
73
|
+
) : null}
|
|
74
|
+
</div>
|
|
75
|
+
</Surface>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function TextList({
|
|
80
|
+
title,
|
|
81
|
+
items,
|
|
82
|
+
tone = "neutral"
|
|
83
|
+
}: {
|
|
84
|
+
title: string;
|
|
85
|
+
items: string[];
|
|
86
|
+
tone?: "neutral" | "risk";
|
|
87
|
+
}) {
|
|
88
|
+
return (
|
|
89
|
+
<section
|
|
90
|
+
className={clsx(
|
|
91
|
+
"eth-agent-plan-preview__note-section",
|
|
92
|
+
`eth-agent-plan-preview__note-section--${tone}`
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
<h3 className="eth-agent-plan-preview__note-title">{title}</h3>
|
|
96
|
+
<ul className="eth-agent-plan-preview__note-list">
|
|
97
|
+
{items.map((item) => (
|
|
98
|
+
<li key={item}>
|
|
99
|
+
<span className="eth-agent-plan-preview__note-marker" aria-hidden="true" />
|
|
100
|
+
<span>{item}</span>
|
|
101
|
+
</li>
|
|
102
|
+
))}
|
|
103
|
+
</ul>
|
|
104
|
+
</section>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { IconButton, Tag, Textarea } from "@echothink-ui/core";
|
|
4
|
+
import { ChevronRightIcon, PlusIcon } from "@echothink-ui/icons";
|
|
5
|
+
import type { AgentScopeAttachment } from "../types";
|
|
6
|
+
|
|
7
|
+
export interface AgentPromptBoxProps
|
|
8
|
+
extends Omit<React.FormHTMLAttributes<HTMLFormElement>, "children" | "onChange" | "onSubmit"> {
|
|
9
|
+
value: string;
|
|
10
|
+
onChange: (value: string) => void;
|
|
11
|
+
onSubmit?: (value: string) => void;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
scopeLabel?: React.ReactNode;
|
|
15
|
+
attachments?: AgentScopeAttachment[];
|
|
16
|
+
onAttach?: () => void;
|
|
17
|
+
onAttachmentRemove?: (id: string) => void;
|
|
18
|
+
onSlash?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AgentPromptBox({
|
|
22
|
+
value,
|
|
23
|
+
onChange,
|
|
24
|
+
onSubmit,
|
|
25
|
+
placeholder = "Message the agent...",
|
|
26
|
+
disabled,
|
|
27
|
+
scopeLabel,
|
|
28
|
+
attachments = [],
|
|
29
|
+
onAttach,
|
|
30
|
+
onAttachmentRemove,
|
|
31
|
+
onSlash,
|
|
32
|
+
className,
|
|
33
|
+
style,
|
|
34
|
+
...props
|
|
35
|
+
}: AgentPromptBoxProps) {
|
|
36
|
+
const canSubmit = value.trim().length > 0 && !disabled;
|
|
37
|
+
const hasContext = Boolean(scopeLabel) || attachments.length > 0;
|
|
38
|
+
|
|
39
|
+
const submit = React.useCallback(() => {
|
|
40
|
+
if (!canSubmit) return;
|
|
41
|
+
onSubmit?.(value);
|
|
42
|
+
}, [canSubmit, onSubmit, value]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<form
|
|
46
|
+
{...props}
|
|
47
|
+
className={clsx("eth-agent-prompt-box", className)}
|
|
48
|
+
data-eth-component="AgentPromptBox"
|
|
49
|
+
style={style}
|
|
50
|
+
onSubmit={(event) => {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
submit();
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{hasContext ? (
|
|
56
|
+
<div className="eth-agent-prompt-box__context">
|
|
57
|
+
{scopeLabel ? (
|
|
58
|
+
<span className="eth-agent-prompt-box__scope">
|
|
59
|
+
<span>Scope</span>
|
|
60
|
+
<strong>{scopeLabel}</strong>
|
|
61
|
+
</span>
|
|
62
|
+
) : null}
|
|
63
|
+
{attachments.length ? (
|
|
64
|
+
<div className="eth-agent-prompt-box__attachments" aria-label="Attached context">
|
|
65
|
+
{attachments.map((attachment) => (
|
|
66
|
+
<Tag
|
|
67
|
+
key={attachment.id}
|
|
68
|
+
removable={Boolean(onAttachmentRemove)}
|
|
69
|
+
onRemove={
|
|
70
|
+
onAttachmentRemove ? () => onAttachmentRemove(attachment.id) : undefined
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
<span className="eth-agent-prompt-box__attachment-kind">
|
|
74
|
+
{kindLabel(attachment.kind)}
|
|
75
|
+
</span>
|
|
76
|
+
<span className="eth-agent-prompt-box__attachment-label">
|
|
77
|
+
{attachment.label}
|
|
78
|
+
</span>
|
|
79
|
+
{attachment.status ? (
|
|
80
|
+
<span
|
|
81
|
+
className="eth-agent-prompt-box__attachment-status"
|
|
82
|
+
data-status={attachment.status}
|
|
83
|
+
role="img"
|
|
84
|
+
aria-label={`Status: ${statusLabel(attachment.status)}`}
|
|
85
|
+
/>
|
|
86
|
+
) : null}
|
|
87
|
+
</Tag>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
) : null}
|
|
91
|
+
</div>
|
|
92
|
+
) : null}
|
|
93
|
+
|
|
94
|
+
<Textarea
|
|
95
|
+
aria-label="Agent prompt"
|
|
96
|
+
className="eth-agent-prompt-box__input"
|
|
97
|
+
disabled={disabled}
|
|
98
|
+
placeholder={placeholder}
|
|
99
|
+
rows={3}
|
|
100
|
+
value={value}
|
|
101
|
+
onChange={(event) => onChange(event.currentTarget.value)}
|
|
102
|
+
onKeyDown={(event) => {
|
|
103
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
submit();
|
|
106
|
+
}
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
|
|
110
|
+
<div className="eth-agent-prompt-box__actions">
|
|
111
|
+
<div className="eth-agent-prompt-box__tools">
|
|
112
|
+
<IconButton
|
|
113
|
+
label="Attach scope or file"
|
|
114
|
+
intent="ghost"
|
|
115
|
+
density="compact"
|
|
116
|
+
disabled={disabled || !onAttach}
|
|
117
|
+
icon={<PlusIcon size={16} />}
|
|
118
|
+
onClick={onAttach}
|
|
119
|
+
/>
|
|
120
|
+
<IconButton
|
|
121
|
+
label="Open slash commands"
|
|
122
|
+
intent="ghost"
|
|
123
|
+
density="compact"
|
|
124
|
+
disabled={disabled}
|
|
125
|
+
icon={<span className="eth-agent-prompt-box__command-icon" aria-hidden>/</span>}
|
|
126
|
+
onClick={() => {
|
|
127
|
+
if (onSlash) {
|
|
128
|
+
onSlash();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!value) onChange("/");
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
<span className="eth-agent-prompt-box__spacer" />
|
|
136
|
+
<IconButton
|
|
137
|
+
className="eth-agent-prompt-box__submit"
|
|
138
|
+
label="Submit prompt"
|
|
139
|
+
intent="primary"
|
|
140
|
+
density="compact"
|
|
141
|
+
disabled={!canSubmit}
|
|
142
|
+
icon={<ChevronRightIcon size={16} />}
|
|
143
|
+
type="submit"
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</form>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function kindLabel(kind: AgentScopeAttachment["kind"]) {
|
|
151
|
+
switch (kind) {
|
|
152
|
+
case "app-domain":
|
|
153
|
+
return "App";
|
|
154
|
+
case "document":
|
|
155
|
+
return "Doc";
|
|
156
|
+
default:
|
|
157
|
+
return kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function statusLabel(status: NonNullable<AgentScopeAttachment["status"]>) {
|
|
162
|
+
return status.replace(/-/g, " ");
|
|
163
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { Badge, Button, InlineNotification, Textarea } from "@echothink-ui/core";
|
|
4
|
+
import type { EthIntent, EthSeverity } from "@echothink-ui/core";
|
|
5
|
+
import { InterruptIcon, PauseIcon, PlayIcon, RetryIcon, StopIcon } from "@echothink-ui/icons";
|
|
6
|
+
|
|
7
|
+
export interface AgentRunControlsProps
|
|
8
|
+
extends Omit<React.HTMLAttributes<HTMLElement>, "children" | "onChange"> {
|
|
9
|
+
state: "running" | "paused" | "stopped" | "failed" | "completed";
|
|
10
|
+
onPause?: () => void;
|
|
11
|
+
onResume?: () => void;
|
|
12
|
+
onStop?: () => void;
|
|
13
|
+
onRetry?: () => void;
|
|
14
|
+
onInterrupt?: (reason: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type AgentRunState = AgentRunControlsProps["state"];
|
|
18
|
+
|
|
19
|
+
interface AgentRunStateMeta {
|
|
20
|
+
label: string;
|
|
21
|
+
severity: EthSeverity;
|
|
22
|
+
description: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface AgentRunControlAction {
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
intent: EthIntent;
|
|
29
|
+
disabled: boolean;
|
|
30
|
+
icon: React.ReactNode;
|
|
31
|
+
onSelect?: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const stateMeta: Record<AgentRunState, AgentRunStateMeta> = {
|
|
35
|
+
running: {
|
|
36
|
+
label: "Running",
|
|
37
|
+
severity: "info",
|
|
38
|
+
description:
|
|
39
|
+
"The agent is actively executing; pause for review, interrupt with steering, or stop the run."
|
|
40
|
+
},
|
|
41
|
+
paused: {
|
|
42
|
+
label: "Paused",
|
|
43
|
+
severity: "warning",
|
|
44
|
+
description:
|
|
45
|
+
"Execution is held. Resume when the run can continue or stop it to cancel remaining work."
|
|
46
|
+
},
|
|
47
|
+
stopped: {
|
|
48
|
+
label: "Stopped",
|
|
49
|
+
severity: "neutral",
|
|
50
|
+
description: "The run was stopped before completion. Retry to start a fresh attempt."
|
|
51
|
+
},
|
|
52
|
+
failed: {
|
|
53
|
+
label: "Failed",
|
|
54
|
+
severity: "danger",
|
|
55
|
+
description: "The last run failed. Retry after reviewing the failure context."
|
|
56
|
+
},
|
|
57
|
+
completed: {
|
|
58
|
+
label: "Completed",
|
|
59
|
+
severity: "success",
|
|
60
|
+
description: "The run completed. Retry only when a new pass is required."
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function AgentRunControls({
|
|
65
|
+
state,
|
|
66
|
+
onPause,
|
|
67
|
+
onResume,
|
|
68
|
+
onStop,
|
|
69
|
+
onRetry,
|
|
70
|
+
onInterrupt,
|
|
71
|
+
className,
|
|
72
|
+
style,
|
|
73
|
+
"aria-label": ariaLabel,
|
|
74
|
+
"aria-describedby": ariaDescribedBy,
|
|
75
|
+
...props
|
|
76
|
+
}: AgentRunControlsProps) {
|
|
77
|
+
const [interrupting, setInterrupting] = React.useState(false);
|
|
78
|
+
const [reason, setReason] = React.useState("");
|
|
79
|
+
const descriptionId = React.useId();
|
|
80
|
+
const meta = stateMeta[state];
|
|
81
|
+
const canInterrupt = Boolean((state === "running" || state === "paused") && onInterrupt);
|
|
82
|
+
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
if (!canInterrupt && interrupting) {
|
|
85
|
+
setInterrupting(false);
|
|
86
|
+
setReason("");
|
|
87
|
+
}
|
|
88
|
+
}, [canInterrupt, interrupting]);
|
|
89
|
+
|
|
90
|
+
const actions = React.useMemo<AgentRunControlAction[]>(() => {
|
|
91
|
+
const next: AgentRunControlAction[] = [];
|
|
92
|
+
if (state === "running") {
|
|
93
|
+
next.push({
|
|
94
|
+
id: "pause",
|
|
95
|
+
label: "Pause",
|
|
96
|
+
intent: "secondary",
|
|
97
|
+
disabled: !onPause,
|
|
98
|
+
icon: <PauseIcon size={16} />,
|
|
99
|
+
onSelect: onPause
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (state === "paused") {
|
|
103
|
+
next.push({
|
|
104
|
+
id: "resume",
|
|
105
|
+
label: "Resume",
|
|
106
|
+
intent: "primary",
|
|
107
|
+
disabled: !onResume,
|
|
108
|
+
icon: <PlayIcon size={16} />,
|
|
109
|
+
onSelect: onResume
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if ((state === "running" || state === "paused") && onInterrupt) {
|
|
113
|
+
next.push({
|
|
114
|
+
id: "interrupt",
|
|
115
|
+
label: "Interrupt",
|
|
116
|
+
intent: "tertiary",
|
|
117
|
+
disabled: false,
|
|
118
|
+
icon: <InterruptIcon size={16} />,
|
|
119
|
+
onSelect: () => setInterrupting(true)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (state === "running" || state === "paused") {
|
|
123
|
+
next.push({
|
|
124
|
+
id: "stop",
|
|
125
|
+
label: "Stop",
|
|
126
|
+
intent: "danger",
|
|
127
|
+
disabled: !onStop,
|
|
128
|
+
icon: <StopIcon size={16} />,
|
|
129
|
+
onSelect: onStop
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (state === "failed" || state === "stopped" || state === "completed") {
|
|
133
|
+
next.push({
|
|
134
|
+
id: "retry",
|
|
135
|
+
label: "Retry",
|
|
136
|
+
intent: state === "completed" ? "secondary" : "primary",
|
|
137
|
+
disabled: !onRetry,
|
|
138
|
+
icon: <RetryIcon size={16} />,
|
|
139
|
+
onSelect: onRetry
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return next;
|
|
143
|
+
}, [onInterrupt, onPause, onResume, onRetry, onStop, state]);
|
|
144
|
+
|
|
145
|
+
const describedBy = ariaDescribedBy ? `${ariaDescribedBy} ${descriptionId}` : descriptionId;
|
|
146
|
+
|
|
147
|
+
const sendInterrupt = () => {
|
|
148
|
+
const trimmedReason = reason.trim();
|
|
149
|
+
if (!trimmedReason) return;
|
|
150
|
+
onInterrupt?.(trimmedReason);
|
|
151
|
+
setReason("");
|
|
152
|
+
setInterrupting(false);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<section
|
|
157
|
+
{...props}
|
|
158
|
+
aria-label={ariaLabel ?? `${meta.label} agent run controls`}
|
|
159
|
+
aria-describedby={describedBy}
|
|
160
|
+
className={clsx("eth-agent-run-controls", className)}
|
|
161
|
+
data-eth-component="AgentRunControls"
|
|
162
|
+
data-state={state}
|
|
163
|
+
style={style}
|
|
164
|
+
>
|
|
165
|
+
<div className="eth-agent-run-controls__summary">
|
|
166
|
+
<div className="eth-agent-run-controls__heading">
|
|
167
|
+
<span className="eth-agent-run-controls__state-dot" aria-hidden="true" />
|
|
168
|
+
<span className="eth-agent-run-controls__title">Agent run</span>
|
|
169
|
+
<Badge severity={meta.severity}>{meta.label}</Badge>
|
|
170
|
+
</div>
|
|
171
|
+
<p id={descriptionId} className="eth-agent-run-controls__description">
|
|
172
|
+
{meta.description}
|
|
173
|
+
</p>
|
|
174
|
+
</div>
|
|
175
|
+
<div
|
|
176
|
+
className="eth-agent-run-controls__actions"
|
|
177
|
+
role="group"
|
|
178
|
+
aria-label={`${meta.label} run controls`}
|
|
179
|
+
>
|
|
180
|
+
{actions.map((action) => (
|
|
181
|
+
<Button
|
|
182
|
+
key={action.id}
|
|
183
|
+
density="compact"
|
|
184
|
+
intent={action.intent}
|
|
185
|
+
disabled={action.disabled}
|
|
186
|
+
icon={action.icon}
|
|
187
|
+
onClick={action.onSelect}
|
|
188
|
+
>
|
|
189
|
+
{action.label}
|
|
190
|
+
</Button>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
{interrupting ? (
|
|
194
|
+
<div className="eth-agent-run-controls__interrupt">
|
|
195
|
+
<InlineNotification severity="warning" title="Interrupt run">
|
|
196
|
+
Send steering instructions before the agent continues.
|
|
197
|
+
</InlineNotification>
|
|
198
|
+
<Textarea
|
|
199
|
+
aria-label="Interrupt reason"
|
|
200
|
+
rows={3}
|
|
201
|
+
value={reason}
|
|
202
|
+
onChange={(event) => setReason(event.currentTarget.value)}
|
|
203
|
+
/>
|
|
204
|
+
<div className="eth-agent-run-controls__interrupt-actions">
|
|
205
|
+
<Button
|
|
206
|
+
density="compact"
|
|
207
|
+
intent="primary"
|
|
208
|
+
disabled={!reason.trim()}
|
|
209
|
+
onClick={sendInterrupt}
|
|
210
|
+
>
|
|
211
|
+
Send interrupt
|
|
212
|
+
</Button>
|
|
213
|
+
<Button density="compact" intent="ghost" onClick={() => setInterrupting(false)}>
|
|
214
|
+
Cancel
|
|
215
|
+
</Button>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
) : null}
|
|
219
|
+
</section>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { Badge, EmptyState, Surface } from "@echothink-ui/core";
|
|
4
|
+
import type { SurfaceComponentProps } from "@echothink-ui/core";
|
|
5
|
+
import type { AgentSafetyCheck } from "../types";
|
|
6
|
+
import { safetySeverity } from "./utils";
|
|
7
|
+
|
|
8
|
+
export interface AgentSafetyPanelProps extends Omit<SurfaceComponentProps, "children"> {
|
|
9
|
+
checks: AgentSafetyCheck[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function AgentSafetyPanel({
|
|
13
|
+
checks,
|
|
14
|
+
title = "Safety checks",
|
|
15
|
+
subtitle,
|
|
16
|
+
className,
|
|
17
|
+
...props
|
|
18
|
+
}: AgentSafetyPanelProps) {
|
|
19
|
+
const summary = summarizeChecks(checks);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Surface
|
|
23
|
+
{...props}
|
|
24
|
+
className={clsx("eth-agent-safety-panel", className)}
|
|
25
|
+
data-eth-component="AgentSafetyPanel"
|
|
26
|
+
subtitle={subtitle ?? summaryLabel(summary)}
|
|
27
|
+
title={title}
|
|
28
|
+
>
|
|
29
|
+
{!checks.length ? (
|
|
30
|
+
<EmptyState title="No safety checks" description="No policy gates are attached to this run." />
|
|
31
|
+
) : (
|
|
32
|
+
<div className="eth-agent-safety-panel__body">
|
|
33
|
+
<dl className="eth-agent-safety-panel__summary" aria-label="Safety check summary">
|
|
34
|
+
<div className="eth-agent-safety-panel__summary-item">
|
|
35
|
+
<dt>Total</dt>
|
|
36
|
+
<dd>{summary.total}</dd>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="eth-agent-safety-panel__summary-item eth-agent-safety-panel__summary-item--pass">
|
|
39
|
+
<dt>Passed</dt>
|
|
40
|
+
<dd>{summary.pass}</dd>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="eth-agent-safety-panel__summary-item eth-agent-safety-panel__summary-item--warn">
|
|
43
|
+
<dt>Review</dt>
|
|
44
|
+
<dd>{summary.warn}</dd>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="eth-agent-safety-panel__summary-item eth-agent-safety-panel__summary-item--fail">
|
|
47
|
+
<dt>Blocked</dt>
|
|
48
|
+
<dd>{summary.fail}</dd>
|
|
49
|
+
</div>
|
|
50
|
+
</dl>
|
|
51
|
+
|
|
52
|
+
<div className="eth-agent-safety-panel__list" role="list">
|
|
53
|
+
{checks.map((check) => (
|
|
54
|
+
<article
|
|
55
|
+
key={check.id}
|
|
56
|
+
className={clsx(
|
|
57
|
+
"eth-agent-safety-panel__check",
|
|
58
|
+
`eth-agent-safety-panel__check--${check.status}`
|
|
59
|
+
)}
|
|
60
|
+
role="listitem"
|
|
61
|
+
>
|
|
62
|
+
<span className="eth-agent-safety-panel__indicator" aria-hidden="true" />
|
|
63
|
+
<div className="eth-agent-safety-panel__check-content">
|
|
64
|
+
<div className="eth-agent-safety-panel__check-main">
|
|
65
|
+
<h3>{check.label}</h3>
|
|
66
|
+
<Badge severity={safetySeverity(check)}>{statusLabel(check.status)}</Badge>
|
|
67
|
+
</div>
|
|
68
|
+
<p>{check.details ?? statusDescription(check.status)}</p>
|
|
69
|
+
</div>
|
|
70
|
+
</article>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
</Surface>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function summarizeChecks(checks: AgentSafetyCheck[]) {
|
|
80
|
+
return checks.reduce(
|
|
81
|
+
(summary, check) => {
|
|
82
|
+
summary.total += 1;
|
|
83
|
+
summary[check.status] += 1;
|
|
84
|
+
return summary;
|
|
85
|
+
},
|
|
86
|
+
{ fail: 0, pass: 0, total: 0, warn: 0 }
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function summaryLabel(summary: ReturnType<typeof summarizeChecks>) {
|
|
91
|
+
if (summary.total === 0) return "No policy gates are active.";
|
|
92
|
+
if (summary.fail > 0) return `${summary.fail} blocking ${plural("issue", summary.fail)} found.`;
|
|
93
|
+
if (summary.warn > 0) {
|
|
94
|
+
return summary.warn === 1 ? "1 check needs review." : `${summary.warn} checks need review.`;
|
|
95
|
+
}
|
|
96
|
+
return "All policy checks passed.";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function statusLabel(status: AgentSafetyCheck["status"]) {
|
|
100
|
+
if (status === "pass") return "Passed";
|
|
101
|
+
if (status === "warn") return "Review needed";
|
|
102
|
+
return "Blocked";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function statusDescription(status: AgentSafetyCheck["status"]) {
|
|
106
|
+
if (status === "pass") return "No policy issue detected.";
|
|
107
|
+
if (status === "warn") return "Review recommended before proceeding.";
|
|
108
|
+
return "Policy gate blocked this action.";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function plural(label: string, count: number) {
|
|
112
|
+
return count === 1 ? label : `${label}s`;
|
|
113
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import type { AgentRunState } from "../types";
|
|
4
|
+
import { isActiveAgentState, stateLabel } from "./utils";
|
|
5
|
+
|
|
6
|
+
export interface AgentStateBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
state?: AgentRunState;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function AgentStateBadge({ state = "idle", className, ...props }: AgentStateBadgeProps) {
|
|
11
|
+
const active = isActiveAgentState(state);
|
|
12
|
+
const label = stateLabel(state);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<span
|
|
16
|
+
{...props}
|
|
17
|
+
className={clsx(
|
|
18
|
+
"eth-agent-state-badge",
|
|
19
|
+
`eth-agent-state-badge--${state}`,
|
|
20
|
+
active && "eth-agent-state-badge--active",
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
data-eth-component="AgentStateBadge"
|
|
24
|
+
data-state={state}
|
|
25
|
+
>
|
|
26
|
+
<span className="eth-agent-state-badge__dot" aria-hidden="true" />
|
|
27
|
+
<span className="eth-agent-state-badge__label">{label}</span>
|
|
28
|
+
</span>
|
|
29
|
+
);
|
|
30
|
+
}
|