@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,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
+ }