@aphrody/m3-ai 3.3.2 → 3.3.5

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 +29 -7
  2. package/dist/A2APeerInspector.d.ts +41 -0
  3. package/dist/A2APeerInspector.d.ts.map +1 -0
  4. package/dist/A2ATaskTimeline.d.ts +28 -0
  5. package/dist/A2ATaskTimeline.d.ts.map +1 -0
  6. package/dist/AgUiEventStream.d.ts +87 -0
  7. package/dist/AgUiEventStream.d.ts.map +1 -0
  8. package/dist/GeminiAccountMenu.d.ts +55 -0
  9. package/dist/GeminiAccountMenu.d.ts.map +1 -0
  10. package/dist/GeminiPersonalContextEdit.d.ts +29 -0
  11. package/dist/GeminiPersonalContextEdit.d.ts.map +1 -0
  12. package/dist/GoogleSpotlightBar.d.ts +32 -0
  13. package/dist/GoogleSpotlightBar.d.ts.map +1 -0
  14. package/dist/Markdown.d.ts +2 -1
  15. package/dist/Markdown.d.ts.map +1 -1
  16. package/dist/McpAppCard.d.ts +24 -0
  17. package/dist/McpAppCard.d.ts.map +1 -0
  18. package/dist/ModelSelector.d.ts.map +1 -1
  19. package/dist/TaskRow.d.ts.map +1 -1
  20. package/dist/ToolCall.d.ts.map +1 -1
  21. package/dist/index.d.ts +15 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3629 -1950
  24. package/dist/index.js.map +32 -1
  25. package/package.json +17 -20
  26. package/src/A2APeerInspector.tsx +370 -0
  27. package/src/A2ATaskTimeline.tsx +218 -0
  28. package/src/AgUiEventStream.ts +492 -0
  29. package/src/ChatComposer.tsx +261 -0
  30. package/src/ConversationList.tsx +207 -0
  31. package/src/GeminiAccountMenu.tsx +212 -0
  32. package/src/GeminiPersonalContextEdit.tsx +147 -0
  33. package/src/GoogleSpotlightBar.tsx +165 -0
  34. package/src/InlineBranchCard.tsx +97 -0
  35. package/src/InlineWorkerCard.tsx +182 -0
  36. package/src/Markdown.tsx +769 -0
  37. package/src/McpAppCard.tsx +161 -0
  38. package/src/MemoryPanel.tsx +176 -0
  39. package/src/MessageBubble.tsx +143 -0
  40. package/src/ModelSelector.tsx +319 -0
  41. package/src/ModelUnavailable.tsx +116 -0
  42. package/src/TaskCreateForm.tsx +123 -0
  43. package/src/TaskDetail.tsx +224 -0
  44. package/src/TaskList.tsx +136 -0
  45. package/src/TaskPriorityIcon.tsx +39 -0
  46. package/src/TaskRow.tsx +177 -0
  47. package/src/TaskStatusIcon.tsx +39 -0
  48. package/src/ToolCall.tsx +644 -0
  49. package/src/chatIcons.tsx +105 -0
  50. package/src/chatLabels.ts +243 -0
  51. package/src/citations.tsx +286 -0
  52. package/src/index.ts +173 -0
  53. package/src/internal.tsx +107 -0
  54. package/src/openaiChatStream.ts +400 -0
  55. package/src/types.ts +212 -0
  56. package/src/useOpenAIChat.ts +126 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aphrody/m3-ai",
3
- "version": "3.3.2",
3
+ "version": "3.3.5",
4
4
  "description": "Material Design 3 agent-interaction components (tool calls, markdown, chat composer, workers, tasks): the SpaceUI (@spacedrive/ai) set rebuilt on @aphrody/m3-primitives.",
5
5
  "keywords": [
6
6
  "material-3",
@@ -13,16 +13,19 @@
13
13
  "author": "aphrody-code",
14
14
  "files": [
15
15
  "dist",
16
- "LICENSE.spaceui"
16
+ "LICENSE.spaceui",
17
+ "src"
17
18
  ],
18
19
  "type": "module",
19
20
  "sideEffects": false,
20
- "main": "./dist/index.js",
21
- "module": "./dist/index.js",
22
- "types": "./dist/index.d.ts",
21
+ "main": "./src/index.ts",
22
+ "module": "./src/index.ts",
23
+ "types": "./src/index.ts",
23
24
  "exports": {
24
25
  ".": {
25
- "types": "./dist/index.d.ts",
26
+ "bun": "./src/index.ts",
27
+ "import": "./src/index.ts",
28
+ "types": "./src/index.ts",
26
29
  "default": "./dist/index.js"
27
30
  }
28
31
  },
@@ -30,30 +33,24 @@
30
33
  "access": "public"
31
34
  },
32
35
  "scripts": {
33
- "build": "tsup && tsc -p tsconfig.build.json",
36
+ "build": "bun ../../scripts/build.ts build --only $npm_package_name",
34
37
  "typecheck": "tsc --noEmit",
35
- "typecheck:fast": "tsgo --noEmit",
38
+ "typecheck:fast": "tsc --noEmit",
36
39
  "test": "bun test"
37
40
  },
38
41
  "dependencies": {
39
- "@aphrody/m3-motion": "3.3.2",
40
- "@aphrody/m3-primitives": "3.3.2",
42
+ "@aphrody/ag-ui": "1.0.1",
43
+ "@aphrody/m3-motion": "3.3.5",
44
+ "@aphrody/m3-primitives": "3.3.5",
45
+ "@aphrody/mcp-ui": "7.1.1-aphrody.1",
41
46
  "clsx": "^2.1.1",
42
- "motion": "^12.43.0",
47
+ "motion": "^13.4.4",
43
48
  "react-markdown": "^10.1.0",
44
49
  "rehype-raw": "^7.0.0",
45
50
  "remark-gfm": "^4.0.1"
46
51
  },
47
52
  "devDependencies": {
48
- "@aphrody/m3-config": "3.3.0",
49
- "@happy-dom/global-registrator": "^20.14.5",
50
- "@types/react": "^19.3.0",
51
- "@types/react-dom": "^19.3.0",
52
- "bun-types": "^1.4.2",
53
- "react": "^19.3.0",
54
- "react-dom": "^19.3.0",
55
- "tsup": "^8.5.1",
56
- "typescript": "^7.0.2"
53
+ "@aphrody/m3-config": "3.3.5"
57
54
  },
58
55
  "peerDependencies": {
59
56
  "react": ">=18.0.0",
@@ -0,0 +1,370 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * @aphrody/m3-ai - A2APeerInspector
4
+ *
5
+ * Material Design 3 Agent-to-Agent (A2A 1.0) Fleet Inspector:
6
+ * - Visualizes connected peer agents (codex, claude, agycli, nie, ocr, rag, ...).
7
+ * - Displays AgentCard interface, capabilities, and active tasks.
8
+ * - Live inspection of .coord mailbox envelopes (ask, fact, ack, ping, error).
9
+ * - File lock & resource claims view (.coord/claims).
10
+ */
11
+
12
+ import React, { useState } from "react";
13
+ import { clsx } from "clsx";
14
+ import type { CoordEnvelope, EnvelopeKind } from "./AgUiEventStream";
15
+
16
+ export interface AgentPeer {
17
+ id: string;
18
+ name: string;
19
+ role: string;
20
+ status: "idle" | "busy" | "offline";
21
+ version?: string;
22
+ skills?: string[];
23
+ activeClaim?: string;
24
+ }
25
+
26
+ export interface SendEnvelopePayload {
27
+ from: string;
28
+ to?: string;
29
+ kind: EnvelopeKind | string;
30
+ topic: string;
31
+ body: string;
32
+ re?: string;
33
+ channel_hint?: string[];
34
+ v?: number;
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ export interface A2APeerInspectorProps {
39
+ peers?: AgentPeer[];
40
+ envelopes?: CoordEnvelope[];
41
+ selectedPeerId?: string;
42
+ onSelectPeer?: (peerId: string) => void;
43
+ onSendEnvelope?: (envelope: SendEnvelopePayload) => void;
44
+ className?: string;
45
+ }
46
+
47
+ const EMPTY_ENVELOPES: CoordEnvelope[] = [];
48
+
49
+ const DEFAULT_PEERS: AgentPeer[] = [
50
+ {
51
+ id: "agy",
52
+ name: "Antigravity Core",
53
+ role: "Integrator & UI / Rust Lead",
54
+ status: "busy",
55
+ version: "2.0.0",
56
+ skills: ["m3-ui", "rust-serial", "bun-native", "a2a-coord"],
57
+ activeClaim: ".coord/claims/native-integration.json",
58
+ },
59
+ {
60
+ id: "codex",
61
+ name: "Codex Peer",
62
+ role: "Backend & Systems Worker",
63
+ status: "busy",
64
+ version: "1.4.0",
65
+ skills: ["native-wiring", "wsl-probe", "kernel-ffi"],
66
+ },
67
+ {
68
+ id: "claude",
69
+ name: "Claude Peer",
70
+ role: "Architecture & Verification",
71
+ status: "idle",
72
+ version: "3.7.0",
73
+ skills: ["design-tokens", "docs-audit", "review"],
74
+ },
75
+ ];
76
+
77
+ const KIND_COLORS: Record<EnvelopeKind, { bg: string; text: string; border: string }> = {
78
+ ask: {
79
+ bg: "bg-[var(--md-sys-color-primary-container,#eaddff)]",
80
+ text: "text-[var(--md-sys-color-on-primary-container,#21005d)]",
81
+ border: "border-[var(--md-sys-color-primary,#6750a4)]",
82
+ },
83
+ fact: {
84
+ bg: "bg-[var(--md-sys-color-tertiary-container,#ffd8e4)]",
85
+ text: "text-[var(--md-sys-color-on-tertiary-container,#31111d)]",
86
+ border: "border-[var(--md-sys-color-tertiary,#7d5260)]",
87
+ },
88
+ ack: {
89
+ bg: "bg-[var(--md-sys-color-secondary-container,#e8def8)]",
90
+ text: "text-[var(--md-sys-color-on-secondary-container,#1d192b)]",
91
+ border: "border-[var(--md-sys-color-secondary,#625b71)]",
92
+ },
93
+ ping: {
94
+ bg: "bg-[var(--md-sys-color-surface-container-high,#ece6f0)]",
95
+ text: "text-[var(--md-sys-color-on-surface-variant,#49454f)]",
96
+ border: "border-[var(--md-sys-color-outline-variant,#cac4d0)]",
97
+ },
98
+ error: {
99
+ bg: "bg-[var(--md-sys-color-error-container,#f9dedc)]",
100
+ text: "text-[var(--md-sys-color-on-error-container,#410e0b)]",
101
+ border: "border-[var(--md-sys-color-error,#b3261e)]",
102
+ },
103
+ };
104
+
105
+ export const A2APeerInspector: React.FC<A2APeerInspectorProps> = ({
106
+ peers = DEFAULT_PEERS,
107
+ envelopes = EMPTY_ENVELOPES,
108
+ selectedPeerId: controlledPeerId,
109
+ onSelectPeer,
110
+ onSendEnvelope,
111
+ className,
112
+ }) => {
113
+ const [internalSelectedPeerId, setInternalSelectedPeerId] = useState<string>(
114
+ peers[0]?.id || "agy",
115
+ );
116
+ const selectedPeerId = controlledPeerId ?? internalSelectedPeerId;
117
+
118
+ // Form state for composing envelope
119
+ const [composeTopic, setComposeTopic] = useState("");
120
+ const [composeBody, setComposeBody] = useState("");
121
+ const [composeKind, setComposeKind] = useState<EnvelopeKind>("ask");
122
+
123
+ const currentPeer = peers.find((p) => p.id === selectedPeerId) || peers[0];
124
+
125
+ const handleSelect = (peerId: string) => {
126
+ setInternalSelectedPeerId(peerId);
127
+ onSelectPeer?.(peerId);
128
+ };
129
+
130
+ const handleSend = (e: React.FormEvent) => {
131
+ e.preventDefault();
132
+ if (!composeTopic.trim() || !composeBody.trim()) return;
133
+
134
+ onSendEnvelope?.({
135
+ from: "agy",
136
+ to: selectedPeerId,
137
+ kind: composeKind,
138
+ topic: composeTopic.trim(),
139
+ body: composeBody.trim(),
140
+ });
141
+
142
+ setComposeTopic("");
143
+ setComposeBody("");
144
+ };
145
+
146
+ const filteredEnvelopes = envelopes.filter(
147
+ (env) => env.from === selectedPeerId || env.to === selectedPeerId || env.to === "fleet",
148
+ );
149
+
150
+ return (
151
+ <div
152
+ className={clsx(
153
+ "flex flex-col gap-4 rounded-3xl p-5 border",
154
+ "bg-[var(--md-sys-color-surface-container-lowest,#ffffff)]",
155
+ "border-[var(--md-sys-color-outline-variant,#cac4d0)]",
156
+ "text-[var(--md-sys-color-on-surface,#1d1b20)]",
157
+ className,
158
+ )}
159
+ >
160
+ {/* Header & Peer Chips */}
161
+ <div className="flex flex-col gap-3">
162
+ <div className="flex items-center justify-between">
163
+ <div className="flex items-center gap-2">
164
+ <span className="material-symbols-outlined text-xl text-[var(--md-sys-color-primary,#6750a4)]">
165
+ hub
166
+ </span>
167
+ <h2 className="text-base font-semibold tracking-tight">A2A Fleet Coordinator</h2>
168
+ <span className="rounded-full bg-[var(--md-sys-color-surface-container-high,#ece6f0)] px-2 py-0.5 text-xs font-mono text-[var(--md-sys-color-on-surface-variant,#49454f)]">
169
+ v1.0 (JSONL + RPC)
170
+ </span>
171
+ </div>
172
+ <span className="text-xs text-[var(--md-sys-color-outline,#79747e)]">
173
+ {peers.length} active peers
174
+ </span>
175
+ </div>
176
+
177
+ {/* Peer selection chips */}
178
+ <div className="flex items-center gap-2 overflow-x-auto pb-1">
179
+ {peers.map((peer) => {
180
+ const isSelected = peer.id === selectedPeerId;
181
+ return (
182
+ <button
183
+ key={peer.id}
184
+ type="button"
185
+ onClick={() => handleSelect(peer.id)}
186
+ className={clsx(
187
+ "flex items-center gap-2 rounded-full px-3.5 py-1.5 text-xs font-medium transition-colors shrink-0",
188
+ isSelected
189
+ ? "bg-[var(--md-sys-color-secondary-container,#e8def8)] text-[var(--md-sys-color-on-secondary-container,#1d192b)] shadow-sm"
190
+ : "bg-[var(--md-sys-color-surface-container,#f3edf7)] text-[var(--md-sys-color-on-surface-variant,#49454f)] hover:bg-[var(--md-sys-color-surface-container-high,#ece6f0)]",
191
+ )}
192
+ >
193
+ <span
194
+ className={clsx(
195
+ "h-2 w-2 rounded-full",
196
+ peer.status === "busy"
197
+ ? "bg-amber-500 animate-pulse"
198
+ : peer.status === "idle"
199
+ ? "bg-emerald-500"
200
+ : "bg-slate-400",
201
+ )}
202
+ />
203
+ <span>{peer.name}</span>
204
+ <span className="text-[10px] opacity-60">@{peer.id}</span>
205
+ </button>
206
+ );
207
+ })}
208
+ </div>
209
+ </div>
210
+
211
+ {/* Selected Peer Details Card */}
212
+ {currentPeer && (
213
+ <div className="flex flex-col gap-2 rounded-2xl bg-[var(--md-sys-color-surface-container-low,#f7f2fa)] p-4 border border-[var(--md-sys-color-outline-variant,#cac4d0)]/50">
214
+ <div className="flex items-start justify-between">
215
+ <div>
216
+ <div className="flex items-center gap-2">
217
+ <span className="font-semibold text-sm">{currentPeer.name}</span>
218
+ <span className="text-xs text-[var(--md-sys-color-on-surface-variant,#49454f)]">
219
+ ({currentPeer.role})
220
+ </span>
221
+ </div>
222
+ <div className="text-xs text-[var(--md-sys-color-outline,#79747e)] mt-0.5">
223
+ Peer ID: <code className="font-mono text-xs">{currentPeer.id}</code> · Version:{" "}
224
+ {currentPeer.version || "1.0.0"}
225
+ </div>
226
+ </div>
227
+ {currentPeer.activeClaim && (
228
+ <div className="flex items-center gap-1.5 rounded-lg bg-amber-500/10 px-2 py-1 text-xs text-amber-700 dark:text-amber-400 border border-amber-500/20">
229
+ <span className="material-symbols-outlined text-sm">lock</span>
230
+ <span className="font-mono text-[11px]">{currentPeer.activeClaim}</span>
231
+ </div>
232
+ )}
233
+ </div>
234
+
235
+ {currentPeer.skills && currentPeer.skills.length > 0 && (
236
+ <div className="flex flex-wrap gap-1.5 mt-1">
237
+ {currentPeer.skills.map((skill) => (
238
+ <span
239
+ key={skill}
240
+ className="rounded-md bg-[var(--md-sys-color-surface-container-highest,#e6e0e9)] px-2 py-0.5 text-[11px] font-mono text-[var(--md-sys-color-on-surface-variant,#49454f)]"
241
+ >
242
+ #{skill}
243
+ </span>
244
+ ))}
245
+ </div>
246
+ )}
247
+ </div>
248
+ )}
249
+
250
+ {/* Envelope Stream Section */}
251
+ <div className="flex flex-col gap-2">
252
+ <div className="flex items-center justify-between">
253
+ <h3 className="text-xs font-semibold uppercase tracking-wider text-[var(--md-sys-color-on-surface-variant,#49454f)]">
254
+ Coordination Envelopes ({filteredEnvelopes.length})
255
+ </h3>
256
+ <span className="text-[11px] text-[var(--md-sys-color-outline,#79747e)]">
257
+ .coord/inbox & .coord/outbox
258
+ </span>
259
+ </div>
260
+
261
+ {filteredEnvelopes.length === 0 ? (
262
+ <div className="flex flex-col items-center justify-center p-6 rounded-2xl bg-[var(--md-sys-color-surface-container-lowest,#ffffff)] border border-dashed border-[var(--md-sys-color-outline-variant,#cac4d0)] text-xs text-[var(--md-sys-color-outline,#79747e)]">
263
+ <span className="material-symbols-outlined text-2xl mb-1 opacity-50">
264
+ mark_email_read
265
+ </span>
266
+ No pending envelopes for @{selectedPeerId}. Fleet mailbox is synchronized.
267
+ </div>
268
+ ) : (
269
+ <div className="flex flex-col gap-2 max-h-60 overflow-y-auto pr-1">
270
+ {filteredEnvelopes.map((env) => {
271
+ const kind = (env.kind in KIND_COLORS ? env.kind : "ping") as EnvelopeKind;
272
+ const style = KIND_COLORS[kind];
273
+
274
+ return (
275
+ <div
276
+ key={env.id}
277
+ className={clsx(
278
+ "flex flex-col gap-1.5 rounded-xl p-3 border text-xs transition-shadow",
279
+ "bg-[var(--md-sys-color-surface-container-low,#f7f2fa)]",
280
+ style.border,
281
+ )}
282
+ >
283
+ <div className="flex items-center justify-between">
284
+ <div className="flex items-center gap-2">
285
+ <span
286
+ className={clsx(
287
+ "rounded-md px-1.5 py-0.5 text-[10px] font-bold uppercase",
288
+ style.bg,
289
+ style.text,
290
+ )}
291
+ >
292
+ {env.kind}
293
+ </span>
294
+ <span className="font-semibold">{env.topic}</span>
295
+ </div>
296
+ <span className="text-[10px] font-mono text-[var(--md-sys-color-outline,#79747e)]">
297
+ {env.from} → {env.to || "all"}
298
+ </span>
299
+ </div>
300
+
301
+ <p className="line-clamp-2 text-xs text-[var(--md-sys-color-on-surface-variant,#49454f)] font-mono whitespace-pre-wrap">
302
+ {env.body}
303
+ </p>
304
+
305
+ <div className="flex items-center justify-between pt-1 border-t border-[var(--md-sys-color-outline-variant,#cac4d0)]/30 text-[10px] text-[var(--md-sys-color-outline,#79747e)] font-mono">
306
+ <span>id: {env.id}</span>
307
+ {env.re && <span>re: {env.re}</span>}
308
+ <span>{new Date(env.ts).toLocaleTimeString()}</span>
309
+ </div>
310
+ </div>
311
+ );
312
+ })}
313
+ </div>
314
+ )}
315
+ </div>
316
+
317
+ {/* Compose Form */}
318
+ <form
319
+ onSubmit={handleSend}
320
+ className="flex flex-col gap-2.5 pt-2 border-t border-[var(--md-sys-color-outline-variant,#cac4d0)]/50"
321
+ >
322
+ <div className="flex items-center gap-2">
323
+ <select
324
+ value={composeKind}
325
+ onChange={(e) => setComposeKind(e.target.value as EnvelopeKind)}
326
+ className="rounded-xl border border-[var(--md-sys-color-outline-variant,#cac4d0)] bg-[var(--md-sys-color-surface-container-low,#f7f2fa)] px-2.5 py-1.5 text-xs font-medium text-[var(--md-sys-color-on-surface,#1d1b20)] focus:outline-none focus:ring-1 focus:ring-[var(--md-sys-color-primary,#6750a4)]"
327
+ >
328
+ <option value="ask">ASK</option>
329
+ <option value="fact">FACT</option>
330
+ <option value="ack">ACK</option>
331
+ <option value="ping">PING</option>
332
+ <option value="error">ERROR</option>
333
+ </select>
334
+
335
+ <input
336
+ type="text"
337
+ placeholder="Topic (e.g. work, claim-check, verification)..."
338
+ value={composeTopic}
339
+ onChange={(e) => setComposeTopic(e.target.value)}
340
+ className="flex-1 rounded-xl border border-[var(--md-sys-color-outline-variant,#cac4d0)] bg-[var(--md-sys-color-surface-container-low,#f7f2fa)] px-3 py-1.5 text-xs text-[var(--md-sys-color-on-surface,#1d1b20)] placeholder-[var(--md-sys-color-outline,#79747e)] focus:outline-none focus:ring-1 focus:ring-[var(--md-sys-color-primary,#6750a4)]"
341
+ />
342
+ </div>
343
+
344
+ <div className="flex items-center gap-2">
345
+ <input
346
+ type="text"
347
+ placeholder={`Message body to @${selectedPeerId}...`}
348
+ value={composeBody}
349
+ onChange={(e) => setComposeBody(e.target.value)}
350
+ className="flex-1 rounded-xl border border-[var(--md-sys-color-outline-variant,#cac4d0)] bg-[var(--md-sys-color-surface-container-low,#f7f2fa)] px-3 py-1.5 text-xs text-[var(--md-sys-color-on-surface,#1d1b20)] placeholder-[var(--md-sys-color-outline,#79747e)] focus:outline-none focus:ring-1 focus:ring-[var(--md-sys-color-primary,#6750a4)]"
351
+ />
352
+
353
+ <button
354
+ type="submit"
355
+ disabled={!composeTopic.trim() || !composeBody.trim()}
356
+ className={clsx(
357
+ "flex items-center gap-1.5 rounded-xl px-4 py-1.5 text-xs font-semibold transition-all",
358
+ composeTopic.trim() && composeBody.trim()
359
+ ? "bg-[var(--md-sys-color-primary,#6750a4)] text-[var(--md-sys-color-on-primary,#ffffff)] hover:opacity-90 shadow-sm"
360
+ : "bg-[var(--md-sys-color-surface-container-high,#ece6f0)] text-[var(--md-sys-color-outline,#79747e)] cursor-not-allowed",
361
+ )}
362
+ >
363
+ <span>Dispatch</span>
364
+ <span className="material-symbols-outlined text-sm">send</span>
365
+ </button>
366
+ </div>
367
+ </form>
368
+ </div>
369
+ );
370
+ };
@@ -0,0 +1,218 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * @aphrody/m3-ai - A2ATaskTimeline
4
+ *
5
+ * Material Design 3 Agent-to-Agent Delegation Timeline:
6
+ * - Chronological trace of task handoffs between agents (e.g., user -> agycli -> codex -> claude -> rag).
7
+ * - Displays delegation phase, envelope kind, file claims, and execution status.
8
+ */
9
+
10
+ import React, { useId, useState } from "react";
11
+ import { clsx } from "clsx";
12
+ import type { CoordEnvelope } from "./AgUiEventStream";
13
+
14
+ export interface TimelineStep {
15
+ id: string;
16
+ from: string;
17
+ to: string;
18
+ title: string;
19
+ description?: string;
20
+ status: "pending" | "running" | "completed" | "error";
21
+ timestamp: string;
22
+ envelope?: Partial<CoordEnvelope>;
23
+ claim?: string;
24
+ }
25
+
26
+ export interface A2ATaskTimelineProps {
27
+ steps?: TimelineStep[];
28
+ currentStepId?: string;
29
+ onSelectStep?: (step: TimelineStep) => void;
30
+ className?: string;
31
+ }
32
+
33
+ const DEFAULT_TIMELINE_STEPS: TimelineStep[] = [
34
+ {
35
+ id: "step-agy-lead",
36
+ from: "user",
37
+ to: "agy",
38
+ title: "Lead Coordination & Monorepo Integration",
39
+ status: "completed",
40
+ timestamp: "Live",
41
+ description: "Coordination active via .coord mailbox and OS resource claims.",
42
+ },
43
+ {
44
+ id: "step-workers",
45
+ from: "agy",
46
+ to: "codex",
47
+ title: "Subagent / Peer Parallel Tasks",
48
+ status: "running",
49
+ timestamp: "Ongoing",
50
+ description: "Workers executing disjoint task families under cooperatively acquired locks.",
51
+ },
52
+ ];
53
+
54
+ export const A2ATaskTimeline: React.FC<A2ATaskTimelineProps> = ({
55
+ steps = DEFAULT_TIMELINE_STEPS,
56
+ currentStepId,
57
+ onSelectStep,
58
+ className,
59
+ }) => {
60
+ const [expandedStepId, setExpandedStepId] = useState<string | null>(null);
61
+ const timelineId = useId();
62
+
63
+ const toggleExpand = (id: string) => {
64
+ setExpandedStepId((current) => (current === id ? null : id));
65
+ };
66
+
67
+ return (
68
+ <div
69
+ className={clsx(
70
+ "flex flex-col gap-3 rounded-3xl p-5 border",
71
+ "bg-[var(--md-sys-color-surface-container-lowest,#ffffff)]",
72
+ "border-[var(--md-sys-color-outline-variant,#cac4d0)]",
73
+ "text-[var(--md-sys-color-on-surface,#1d1b20)]",
74
+ className,
75
+ )}
76
+ >
77
+ <div className="flex items-center justify-between border-b border-[var(--md-sys-color-outline-variant,#cac4d0)]/40 pb-3">
78
+ <div className="flex items-center gap-2">
79
+ <span className="material-symbols-outlined text-lg text-[var(--md-sys-color-primary,#6750a4)]">
80
+ timeline
81
+ </span>
82
+ <h3 className="text-sm font-semibold tracking-tight">A2A Task Delegation Timeline</h3>
83
+ </div>
84
+ <span className="text-xs text-[var(--md-sys-color-outline,#79747e)]">
85
+ {steps.length} delegation steps
86
+ </span>
87
+ </div>
88
+
89
+ <div className="relative flex flex-col gap-4 pl-4 pt-1">
90
+ {/* Continuous connector line */}
91
+ <div className="absolute left-[27px] top-4 bottom-4 w-0.5 bg-[var(--md-sys-color-outline-variant,#cac4d0)]/60" />
92
+
93
+ {steps.map((step) => {
94
+ const isCurrent = step.id === currentStepId;
95
+ const isExpanded = step.id === expandedStepId;
96
+ const detailsId = `${timelineId}-${encodeURIComponent(step.id)}`;
97
+ const date = new Date(step.timestamp);
98
+ const time = Number.isNaN(date.getTime()) ? step.timestamp : date.toLocaleTimeString();
99
+
100
+ const statusIcon =
101
+ step.status === "completed"
102
+ ? "check_circle"
103
+ : step.status === "running"
104
+ ? "sync"
105
+ : step.status === "error"
106
+ ? "error"
107
+ : "radio_button_unchecked";
108
+
109
+ const statusColor =
110
+ step.status === "completed"
111
+ ? "text-emerald-500"
112
+ : step.status === "running"
113
+ ? "text-[var(--md-sys-color-primary,#6750a4)] animate-spin motion-reduce:animate-none"
114
+ : step.status === "error"
115
+ ? "text-rose-500"
116
+ : "text-slate-400";
117
+
118
+ return (
119
+ <div
120
+ key={step.id}
121
+ className={clsx(
122
+ "relative flex items-start gap-3 transition-all",
123
+ isCurrent && "font-medium",
124
+ )}
125
+ >
126
+ {/* Step indicator node */}
127
+ <div
128
+ className={clsx(
129
+ "relative z-10 flex h-7 w-7 items-center justify-center rounded-full border-2 bg-[var(--md-sys-color-surface-container-lowest,#ffffff)]",
130
+ step.status === "completed"
131
+ ? "border-emerald-500"
132
+ : step.status === "running"
133
+ ? "border-[var(--md-sys-color-primary,#6750a4)] ring-4 ring-[var(--md-sys-color-primary,#6750a4)]/20"
134
+ : step.status === "error"
135
+ ? "border-rose-500"
136
+ : "border-[var(--md-sys-color-outline-variant,#cac4d0)]",
137
+ )}
138
+ >
139
+ <span className={clsx("material-symbols-outlined text-sm", statusColor)}>
140
+ {statusIcon}
141
+ </span>
142
+ </div>
143
+
144
+ {/* Step details card */}
145
+ <div
146
+ className={clsx(
147
+ "flex-1 cursor-pointer rounded-2xl border p-3 text-xs transition-all",
148
+ "bg-[var(--md-sys-color-surface-container-low,#f7f2fa)] hover:bg-[var(--md-sys-color-surface-container,#f3edf7)]",
149
+ isCurrent
150
+ ? "border-[var(--md-sys-color-primary,#6750a4)] shadow-sm"
151
+ : "border-[var(--md-sys-color-outline-variant,#cac4d0)]/60",
152
+ )}
153
+ >
154
+ <button
155
+ type="button"
156
+ aria-expanded={step.envelope ? isExpanded : undefined}
157
+ aria-controls={step.envelope ? detailsId : undefined}
158
+ onClick={() => {
159
+ toggleExpand(step.id);
160
+ onSelectStep?.(step);
161
+ }}
162
+ className="block w-full rounded-medium text-left outline-none focus-visible:ring-2 focus-visible:ring-primary"
163
+ >
164
+ <span className="flex items-center justify-between gap-2">
165
+ <span className="flex items-center gap-1.5">
166
+ <span className="font-semibold text-xs text-[var(--md-sys-color-on-surface,#1d1b20)]">
167
+ {step.title}
168
+ </span>
169
+ <span className="rounded-md bg-[var(--md-sys-color-surface-container-highest,#e6e0e9)] px-1.5 py-0.2 text-[10px] font-mono text-[var(--md-sys-color-on-surface-variant,#49454f)]">
170
+ @{step.from} → @{step.to}
171
+ </span>
172
+ </span>
173
+ <span className="text-[10px] font-mono text-[var(--md-sys-color-outline,#79747e)]">
174
+ {time}
175
+ </span>
176
+ </span>
177
+
178
+ {step.description && (
179
+ <span className="mt-1 block text-xs text-[var(--md-sys-color-on-surface-variant,#49454f)]">
180
+ {step.description}
181
+ </span>
182
+ )}
183
+
184
+ {step.claim && (
185
+ <span className="mt-2 flex items-center gap-1.5 text-[11px] font-mono text-amber-700 dark:text-amber-400">
186
+ <span className="material-symbols-outlined text-xs">lock</span>
187
+ <span>claim: {step.claim}</span>
188
+ </span>
189
+ )}
190
+ </button>
191
+
192
+ {/* Expanded envelope payload view */}
193
+ {step.envelope && (
194
+ <div
195
+ id={detailsId}
196
+ hidden={!isExpanded}
197
+ className="mt-2.5 pt-2 border-t border-[var(--md-sys-color-outline-variant,#cac4d0)]/40 font-mono text-[11px] text-[var(--md-sys-color-on-surface-variant,#49454f)]"
198
+ >
199
+ <div className="flex items-center gap-2 mb-1">
200
+ <span className="font-semibold">Kind: {step.envelope.kind}</span>
201
+ <span>·</span>
202
+ <span>Topic: {step.envelope.topic}</span>
203
+ </div>
204
+ {step.envelope.body && (
205
+ <pre className="rounded-lg bg-[var(--md-sys-color-surface-container-highest,#e6e0e9)]/50 p-2 overflow-x-auto whitespace-pre-wrap">
206
+ {step.envelope.body}
207
+ </pre>
208
+ )}
209
+ </div>
210
+ )}
211
+ </div>
212
+ </div>
213
+ );
214
+ })}
215
+ </div>
216
+ </div>
217
+ );
218
+ };