remotion-claude-agent-demo 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 (128) hide show
  1. package/README.md +160 -0
  2. package/apps/web/README.md +36 -0
  3. package/apps/web/env.example +20 -0
  4. package/apps/web/eslint.config.mjs +18 -0
  5. package/apps/web/next.config.ts +7 -0
  6. package/apps/web/package-lock.json +10348 -0
  7. package/apps/web/package.json +35 -0
  8. package/apps/web/postcss.config.mjs +7 -0
  9. package/apps/web/public/file.svg +1 -0
  10. package/apps/web/public/globe.svg +1 -0
  11. package/apps/web/public/next.svg +1 -0
  12. package/apps/web/public/vercel.svg +1 -0
  13. package/apps/web/public/window.svg +1 -0
  14. package/apps/web/src/app/.well-known/agent-card.json/route.ts +50 -0
  15. package/apps/web/src/app/background-tasks/[jobId]/cancel/route.ts +29 -0
  16. package/apps/web/src/app/events/stream/route.ts +58 -0
  17. package/apps/web/src/app/favicon.ico +0 -0
  18. package/apps/web/src/app/globals.css +174 -0
  19. package/apps/web/src/app/layout.tsx +34 -0
  20. package/apps/web/src/app/messages/answer/route.ts +57 -0
  21. package/apps/web/src/app/messages/stream/route.ts +381 -0
  22. package/apps/web/src/app/page.tsx +358 -0
  23. package/apps/web/src/app/tasks/[taskId]/cancel/route.ts +24 -0
  24. package/apps/web/src/app/tasks/[taskId]/route.ts +24 -0
  25. package/apps/web/src/app/tasks/route.ts +13 -0
  26. package/apps/web/src/components/chat/agent-blocks.tsx +111 -0
  27. package/apps/web/src/components/chat/ask-user-question-panel.tsx +172 -0
  28. package/apps/web/src/components/chat/session-sidebar.tsx +222 -0
  29. package/apps/web/src/components/chat/subagent-activity-sidebar.tsx +248 -0
  30. package/apps/web/src/components/chat/tool-blocks.tsx +550 -0
  31. package/apps/web/src/lib/a2a/activity-store.ts +150 -0
  32. package/apps/web/src/lib/a2a/client.ts +357 -0
  33. package/apps/web/src/lib/a2a/sse.ts +19 -0
  34. package/apps/web/src/lib/a2a/task-store.ts +111 -0
  35. package/apps/web/src/lib/a2a/types.ts +216 -0
  36. package/apps/web/src/lib/agent/answer-store.ts +109 -0
  37. package/apps/web/src/lib/agent/background-delivery.ts +343 -0
  38. package/apps/web/src/lib/agent/background-tool.ts +78 -0
  39. package/apps/web/src/lib/agent/background.ts +452 -0
  40. package/apps/web/src/lib/agent/chat.ts +543 -0
  41. package/apps/web/src/lib/agent/session-store.ts +26 -0
  42. package/apps/web/src/lib/chat/types.ts +44 -0
  43. package/apps/web/src/lib/env.ts +31 -0
  44. package/apps/web/src/lib/hooks/useA2AChat.ts +863 -0
  45. package/apps/web/src/lib/state/chat-atoms.ts +52 -0
  46. package/apps/web/src/lib/workspace.ts +9 -0
  47. package/apps/web/tsconfig.json +35 -0
  48. package/bin/remotion-agent.js +451 -0
  49. package/package.json +34 -0
  50. package/templates/.claude/CLAUDE.md +95 -0
  51. package/templates/.claude/README.md +129 -0
  52. package/templates/.claude/agents/composer-agent.md +188 -0
  53. package/templates/.claude/agents/crafter.md +181 -0
  54. package/templates/.claude/agents/creator.md +134 -0
  55. package/templates/.claude/agents/perceiver.md +92 -0
  56. package/templates/.claude/settings.json +36 -0
  57. package/templates/.claude/settings.local.json +39 -0
  58. package/templates/.claude/skills/agent-browser/SKILL.md +349 -0
  59. package/templates/.claude/skills/agent-browser/references/authentication.md +188 -0
  60. package/templates/.claude/skills/agent-browser/references/proxy-support.md +175 -0
  61. package/templates/.claude/skills/agent-browser/references/session-management.md +181 -0
  62. package/templates/.claude/skills/agent-browser/references/snapshot-refs.md +186 -0
  63. package/templates/.claude/skills/agent-browser/references/video-recording.md +162 -0
  64. package/templates/.claude/skills/agent-browser/templates/authenticated-session.sh +91 -0
  65. package/templates/.claude/skills/agent-browser/templates/capture-workflow.sh +68 -0
  66. package/templates/.claude/skills/agent-browser/templates/form-automation.sh +64 -0
  67. package/templates/.claude/skills/algorithmic-art/LICENSE.txt +202 -0
  68. package/templates/.claude/skills/algorithmic-art/SKILL.md +405 -0
  69. package/templates/.claude/skills/algorithmic-art/templates/generator_template.js +223 -0
  70. package/templates/.claude/skills/algorithmic-art/templates/viewer.html +599 -0
  71. package/templates/.claude/skills/asset-validator/SKILL.md +376 -0
  72. package/templates/.claude/skills/audio-video-sync/SKILL.md +219 -0
  73. package/templates/.claude/skills/bgm-manager/SKILL.md +334 -0
  74. package/templates/.claude/skills/remotion-best-practices/SKILL.md +45 -0
  75. package/templates/.claude/skills/remotion-best-practices/rules/3d.md +86 -0
  76. package/templates/.claude/skills/remotion-best-practices/rules/animations.md +29 -0
  77. package/templates/.claude/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
  78. package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
  79. package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
  80. package/templates/.claude/skills/remotion-best-practices/rules/assets.md +78 -0
  81. package/templates/.claude/skills/remotion-best-practices/rules/audio.md +172 -0
  82. package/templates/.claude/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
  83. package/templates/.claude/skills/remotion-best-practices/rules/can-decode.md +75 -0
  84. package/templates/.claude/skills/remotion-best-practices/rules/charts.md +58 -0
  85. package/templates/.claude/skills/remotion-best-practices/rules/compositions.md +141 -0
  86. package/templates/.claude/skills/remotion-best-practices/rules/display-captions.md +126 -0
  87. package/templates/.claude/skills/remotion-best-practices/rules/extract-frames.md +229 -0
  88. package/templates/.claude/skills/remotion-best-practices/rules/fonts.md +152 -0
  89. package/templates/.claude/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
  90. package/templates/.claude/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
  91. package/templates/.claude/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
  92. package/templates/.claude/skills/remotion-best-practices/rules/gifs.md +138 -0
  93. package/templates/.claude/skills/remotion-best-practices/rules/images.md +130 -0
  94. package/templates/.claude/skills/remotion-best-practices/rules/import-srt-captions.md +67 -0
  95. package/templates/.claude/skills/remotion-best-practices/rules/lottie.md +68 -0
  96. package/templates/.claude/skills/remotion-best-practices/rules/maps.md +403 -0
  97. package/templates/.claude/skills/remotion-best-practices/rules/measuring-dom-nodes.md +35 -0
  98. package/templates/.claude/skills/remotion-best-practices/rules/measuring-text.md +143 -0
  99. package/templates/.claude/skills/remotion-best-practices/rules/parameters.md +98 -0
  100. package/templates/.claude/skills/remotion-best-practices/rules/sequencing.md +118 -0
  101. package/templates/.claude/skills/remotion-best-practices/rules/tailwind.md +11 -0
  102. package/templates/.claude/skills/remotion-best-practices/rules/text-animations.md +20 -0
  103. package/templates/.claude/skills/remotion-best-practices/rules/timing.md +179 -0
  104. package/templates/.claude/skills/remotion-best-practices/rules/transcribe-captions.md +19 -0
  105. package/templates/.claude/skills/remotion-best-practices/rules/transitions.md +122 -0
  106. package/templates/.claude/skills/remotion-best-practices/rules/trimming.md +53 -0
  107. package/templates/.claude/skills/remotion-best-practices/rules/videos.md +171 -0
  108. package/templates/.claude/skills/remotion-components/SKILL.md +453 -0
  109. package/templates/.claude/skills/render-config/SKILL.md +290 -0
  110. package/templates/.claude/skills/script-writer/SKILL.md +59 -0
  111. package/templates/.claude/skills/style-director/script-writer/SKILL.md +82 -0
  112. package/templates/.claude/skills/style-director/style-director/SKILL.md +287 -0
  113. package/templates/.claude/skills/style-director/style-director/references/audience-and-scenarios.md +43 -0
  114. package/templates/.claude/skills/style-director/style-director/references/interaction-innovation.md +26 -0
  115. package/templates/.claude/skills/style-director/style-director/references/motion-grammar.md +66 -0
  116. package/templates/.claude/skills/style-director/style-director/references/quality-checklist.md +29 -0
  117. package/templates/.claude/skills/style-director/style-director/references/scene-recipes.md +38 -0
  118. package/templates/.claude/skills/style-director/style-director/references/visual-style-system.md +148 -0
  119. package/templates/.claude/skills/subtitle-composer/SKILL.md +304 -0
  120. package/templates/.claude/skills/subtitle-processor/SKILL.md +308 -0
  121. package/templates/.claude/skills/timeline-generator/SKILL.md +253 -0
  122. package/templates/.claude/skills/video-preflight-check/SKILL.md +353 -0
  123. package/templates/.claude/skills/voice-synthesizer/SKILL.md +296 -0
  124. package/templates/.claude/skills/voice-synthesizer/scripts/synthesize_voice.py +315 -0
  125. package/templates/.claude/skills/voice-synthesizer/scripts/tts_cli.py +142 -0
  126. package/templates/.claude/skills/web-design-guidelines/SKILL.md +36 -0
  127. package/templates/.claude/skills/youtube-downloader/SKILL.md +99 -0
  128. package/templates/.claude/skills/youtube-downloader/scripts/download_video.py +145 -0
@@ -0,0 +1,863 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo } from "react";
4
+ import { useAtom } from "jotai";
5
+ import { cancelA2aTask, streamA2aChat, streamA2aEvents, submitA2aAnswer } from "@/lib/a2a/client";
6
+ import type {
7
+ A2aActivityEvent,
8
+ A2aAskUserQuestionEvent,
9
+ A2aQuestionDismissedEvent,
10
+ A2aSubagentEvent,
11
+ A2aToolCallEvent,
12
+ } from "@/lib/a2a/types";
13
+ import type { AgentToolBlock } from "@/lib/chat/types";
14
+ import { currentSessionIdAtom, sessionsAtom } from "@/lib/state/chat-atoms";
15
+ import type { ChatSession } from "@/lib/state/chat-atoms";
16
+
17
+ export type { AgentBlock, AgentTextBlock, AgentToolBlock, ChatItem, PendingQuestion, SubagentActivity } from "@/lib/chat/types";
18
+
19
+ type TaskToolInput = {
20
+ prompt?: string;
21
+ description?: string;
22
+ run_in_background?: boolean;
23
+ background?: boolean;
24
+ runInBackground?: boolean;
25
+ };
26
+
27
+ type SendArgs = {
28
+ text: string;
29
+ sessionId?: string;
30
+ };
31
+
32
+ const DEFAULT_TITLE = "New session";
33
+ const TITLE_MAX_LEN = 48;
34
+
35
+ function nowId(): string {
36
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
37
+ }
38
+
39
+ function createSession(): ChatSession {
40
+ const now = Date.now();
41
+ const id = globalThis.crypto?.randomUUID?.() ?? nowId();
42
+ return {
43
+ id,
44
+ title: DEFAULT_TITLE,
45
+ createdAt: now,
46
+ updatedAt: now,
47
+ contextId: undefined,
48
+ sessionId: undefined,
49
+ items: [],
50
+ isStreaming: false,
51
+ error: null,
52
+ activeTaskId: undefined,
53
+ pendingQuestion: null,
54
+ subagentActivities: [],
55
+ inputDraft: "",
56
+ };
57
+ }
58
+
59
+ function deriveTitle(text: string): string {
60
+ const firstLine = text.split("\n")[0]?.trim() ?? "";
61
+ if (!firstLine) return DEFAULT_TITLE;
62
+ if (firstLine.length <= TITLE_MAX_LEN) return firstLine;
63
+ return firstLine.slice(0, TITLE_MAX_LEN - 3).trimEnd() + "...";
64
+ }
65
+
66
+ type PendingTask = {
67
+ toolId: string;
68
+ prompt?: string;
69
+ description?: string;
70
+ isBackground?: boolean;
71
+ };
72
+
73
+ type SessionRuntime = {
74
+ abortController: AbortController | null;
75
+ activityAbortController: AbortController | null;
76
+ activityContextId: string | null;
77
+ lastActivityId: number;
78
+ deliveredMessageIds: Set<number>;
79
+ pendingTask: PendingTask | null;
80
+ activeAgentId: string | null;
81
+ };
82
+
83
+ const runtimeBySession = new Map<string, SessionRuntime>();
84
+
85
+ function getRuntime(sessionId: string): SessionRuntime {
86
+ const existing = runtimeBySession.get(sessionId);
87
+ if (existing) return existing;
88
+ const created: SessionRuntime = {
89
+ abortController: null,
90
+ activityAbortController: null,
91
+ activityContextId: null,
92
+ lastActivityId: 0,
93
+ deliveredMessageIds: new Set(),
94
+ pendingTask: null,
95
+ activeAgentId: null,
96
+ };
97
+ runtimeBySession.set(sessionId, created);
98
+ return created;
99
+ }
100
+
101
+ function clearRuntime(sessionId: string): void {
102
+ runtimeBySession.delete(sessionId);
103
+ }
104
+
105
+ export function useA2AChat() {
106
+ const [sessions, setSessions] = useAtom(sessionsAtom);
107
+ const [currentSessionId, setCurrentSessionId] = useAtom(currentSessionIdAtom);
108
+
109
+ useEffect(() => {
110
+ setSessions((prev) =>
111
+ prev.map((session) => ({
112
+ ...session,
113
+ title: session.title || DEFAULT_TITLE,
114
+ createdAt: session.createdAt ?? Date.now(),
115
+ updatedAt: session.updatedAt ?? Date.now(),
116
+ items: session.items ?? [],
117
+ isStreaming: false,
118
+ error: null,
119
+ activeTaskId: undefined,
120
+ pendingQuestion: null,
121
+ subagentActivities: session.subagentActivities ?? [],
122
+ inputDraft: session.inputDraft ?? "",
123
+ })),
124
+ );
125
+ }, [setSessions]);
126
+
127
+ useEffect(() => {
128
+ if (sessions.length === 0) {
129
+ const session = createSession();
130
+ setSessions([session]);
131
+ setCurrentSessionId(session.id);
132
+ return;
133
+ }
134
+ if (!currentSessionId || !sessions.some((s) => s.id === currentSessionId)) {
135
+ setCurrentSessionId(sessions[0]?.id ?? null);
136
+ }
137
+ }, [currentSessionId, sessions, setCurrentSessionId, setSessions]);
138
+
139
+
140
+ const currentSession = useMemo(
141
+ () => sessions.find((s) => s.id === currentSessionId) ?? null,
142
+ [sessions, currentSessionId],
143
+ );
144
+
145
+ const updateSession = useCallback(
146
+ (
147
+ sessionId: string,
148
+ updater: (session: ChatSession) => ChatSession,
149
+ options?: { touch?: boolean },
150
+ ) => {
151
+ setSessions((prev) =>
152
+ prev.map((session) => {
153
+ if (session.id !== sessionId) return session;
154
+ const next = updater(session);
155
+ const touch = options?.touch !== false;
156
+ return {
157
+ ...next,
158
+ updatedAt: touch ? Date.now() : session.updatedAt,
159
+ };
160
+ }),
161
+ );
162
+ },
163
+ [setSessions],
164
+ );
165
+
166
+ const setInputDraft = useCallback(
167
+ (text: string, sessionId?: string) => {
168
+ const sid = sessionId ?? currentSessionId;
169
+ if (!sid) return;
170
+ updateSession(
171
+ sid,
172
+ (session) => ({ ...session, inputDraft: text }),
173
+ { touch: false },
174
+ );
175
+ },
176
+ [currentSessionId, updateSession],
177
+ );
178
+
179
+ const switchSession = useCallback(
180
+ (sessionId: string) => {
181
+ setCurrentSessionId(sessionId);
182
+ },
183
+ [setCurrentSessionId],
184
+ );
185
+
186
+ const newSession = useCallback(() => {
187
+ const session = createSession();
188
+ setSessions((prev) => [session, ...prev]);
189
+ setCurrentSessionId(session.id);
190
+ }, [setCurrentSessionId, setSessions]);
191
+
192
+ const stopSessionRuntime = useCallback((sessionId: string, taskId?: string) => {
193
+ const runtime = getRuntime(sessionId);
194
+ runtime.abortController?.abort();
195
+ runtime.abortController = null;
196
+ runtime.activityAbortController?.abort();
197
+ runtime.activityAbortController = null;
198
+ runtime.activityContextId = null;
199
+ runtime.pendingTask = null;
200
+ runtime.activeAgentId = null;
201
+ runtime.lastActivityId = 0;
202
+ runtime.deliveredMessageIds = new Set();
203
+
204
+ if (taskId) {
205
+ void cancelA2aTask({ taskId });
206
+ }
207
+ }, []);
208
+
209
+ const deleteSession = useCallback(
210
+ (sessionId: string) => {
211
+ const target = sessions.find((s) => s.id === sessionId);
212
+ stopSessionRuntime(sessionId, target?.activeTaskId);
213
+ clearRuntime(sessionId);
214
+ setSessions((prev) => {
215
+ const next = prev.filter((session) => session.id !== sessionId);
216
+ if (next.length === 0) {
217
+ const fresh = createSession();
218
+ setCurrentSessionId(fresh.id);
219
+ return [fresh];
220
+ }
221
+ if (currentSessionId === sessionId) {
222
+ setCurrentSessionId(next[0].id);
223
+ }
224
+ return next;
225
+ });
226
+ },
227
+ [currentSessionId, sessions, setCurrentSessionId, setSessions, stopSessionRuntime],
228
+ );
229
+
230
+ const cancel = useCallback(() => {
231
+ if (!currentSession) return;
232
+ stopSessionRuntime(currentSession.id, currentSession.activeTaskId);
233
+ updateSession(currentSession.id, (session) => ({ ...session, isStreaming: false }), {
234
+ touch: false,
235
+ });
236
+ }, [currentSession, stopSessionRuntime, updateSession]);
237
+
238
+ const submitAnswer = useCallback(
239
+ async (answers: Record<string, string>) => {
240
+ if (!currentSession?.pendingQuestion) return;
241
+ const pending = currentSession.pendingQuestion;
242
+ try {
243
+ await submitA2aAnswer({
244
+ requestId: pending.requestId,
245
+ taskId: pending.taskId,
246
+ answers,
247
+ });
248
+ updateSession(
249
+ currentSession.id,
250
+ (session) => ({ ...session, pendingQuestion: null }),
251
+ { touch: false },
252
+ );
253
+ } catch (e) {
254
+ updateSession(
255
+ currentSession.id,
256
+ (session) => ({
257
+ ...session,
258
+ error: e instanceof Error ? e.message : "Failed to submit answer",
259
+ }),
260
+ { touch: false },
261
+ );
262
+ }
263
+ },
264
+ [currentSession, updateSession],
265
+ );
266
+
267
+ const upsertBackgroundActivity = useCallback(
268
+ (sessionId: string, event: A2aActivityEvent) => {
269
+ if (event.kind !== "backgroundTask") return;
270
+ const data = event.data;
271
+ const status =
272
+ data.status === "completed" || data.status === "failed" || data.status === "cancelled"
273
+ ? "stopped"
274
+ : "running";
275
+ const startedAt = data.startedAt ?? data.createdAt ?? Date.now();
276
+ const stoppedAt = data.finishedAt;
277
+ const result =
278
+ data.result ??
279
+ (data.status === "cancelled"
280
+ ? "Cancelled"
281
+ : data.error
282
+ ? `Error: ${data.error}`
283
+ : undefined);
284
+
285
+ updateSession(
286
+ sessionId,
287
+ (session) => {
288
+ const existing = session.subagentActivities.find((a) => a.agentId === data.jobId);
289
+ if (!existing) {
290
+ return {
291
+ ...session,
292
+ subagentActivities: [
293
+ ...session.subagentActivities,
294
+ {
295
+ agentId: data.jobId,
296
+ agentType: data.subagentType || "background",
297
+ status,
298
+ startedAt,
299
+ stoppedAt,
300
+ isBackground: true,
301
+ prompt: data.prompt,
302
+ description: data.description,
303
+ result,
304
+ },
305
+ ],
306
+ };
307
+ }
308
+
309
+ return {
310
+ ...session,
311
+ subagentActivities: session.subagentActivities.map((a) =>
312
+ a.agentId === data.jobId
313
+ ? {
314
+ ...a,
315
+ agentType: data.subagentType || a.agentType,
316
+ status,
317
+ startedAt: a.startedAt ?? startedAt,
318
+ stoppedAt: stoppedAt ?? a.stoppedAt,
319
+ isBackground: true,
320
+ prompt: data.prompt ?? a.prompt,
321
+ description: data.description ?? a.description,
322
+ result: result ?? a.result,
323
+ }
324
+ : a,
325
+ ),
326
+ };
327
+ },
328
+ { touch: false },
329
+ );
330
+ },
331
+ [updateSession],
332
+ );
333
+
334
+ const upsertTodoActivity = useCallback(
335
+ (sessionId: string, event: A2aActivityEvent) => {
336
+ if (event.kind !== "todoUpdate") return;
337
+ const data = event.data;
338
+ updateSession(
339
+ sessionId,
340
+ (session) => {
341
+ const existing = session.subagentActivities.find((a) => a.agentId === data.jobId);
342
+ if (!existing) {
343
+ return {
344
+ ...session,
345
+ subagentActivities: [
346
+ ...session.subagentActivities,
347
+ {
348
+ agentId: data.jobId,
349
+ agentType: "background",
350
+ status: "running",
351
+ startedAt: data.updatedAt,
352
+ isBackground: true,
353
+ todos: data.todos,
354
+ todoUpdatedAt: data.updatedAt,
355
+ },
356
+ ],
357
+ };
358
+ }
359
+
360
+ return {
361
+ ...session,
362
+ subagentActivities: session.subagentActivities.map((a) =>
363
+ a.agentId === data.jobId
364
+ ? { ...a, todos: data.todos, todoUpdatedAt: data.updatedAt }
365
+ : a,
366
+ ),
367
+ };
368
+ },
369
+ { touch: false },
370
+ );
371
+ },
372
+ [updateSession],
373
+ );
374
+
375
+ const appendDeliveryMessage = useCallback(
376
+ (sessionId: string, event: A2aActivityEvent) => {
377
+ if (event.kind !== "deliveryMessage") return;
378
+ const runtime = getRuntime(sessionId);
379
+ if (runtime.deliveredMessageIds.has(event.id)) return;
380
+ runtime.deliveredMessageIds.add(event.id);
381
+ const data = event.data;
382
+ if (!data.text) return;
383
+ const isDelta = data.isDelta === true;
384
+ updateSession(
385
+ sessionId,
386
+ (session) => {
387
+ const upsertIndex =
388
+ data.jobId
389
+ ? session.items.findIndex(
390
+ (it) => it.role === "agent" && it.jobId === data.jobId,
391
+ )
392
+ : -1;
393
+
394
+ if (isDelta) {
395
+ const items = [...session.items];
396
+ if (upsertIndex < 0) {
397
+ items.push({
398
+ id: nowId(),
399
+ role: "agent",
400
+ jobId: data.jobId,
401
+ blocks: [{ kind: "text", text: data.text }],
402
+ });
403
+ return { ...session, items };
404
+ }
405
+
406
+ const target = items[upsertIndex];
407
+ if (target.role !== "agent") return { ...session, items };
408
+ const blocks = [...target.blocks];
409
+ const last = blocks[blocks.length - 1];
410
+ if (last?.kind === "text") {
411
+ blocks[blocks.length - 1] = { kind: "text", text: last.text + data.text };
412
+ } else {
413
+ blocks.push({ kind: "text", text: data.text });
414
+ }
415
+ items[upsertIndex] = { ...target, blocks };
416
+ return { ...session, items };
417
+ }
418
+
419
+ if (upsertIndex >= 0) {
420
+ const items = [...session.items];
421
+ const current = items[upsertIndex];
422
+ if (current.role === "agent") {
423
+ items[upsertIndex] = {
424
+ ...current,
425
+ jobId: data.jobId,
426
+ blocks: [{ kind: "text", text: data.text }],
427
+ };
428
+ }
429
+ return { ...session, items };
430
+ }
431
+
432
+ return {
433
+ ...session,
434
+ items: [
435
+ ...session.items,
436
+ {
437
+ id: nowId(),
438
+ role: "agent",
439
+ jobId: data.jobId,
440
+ blocks: [{ kind: "text", text: data.text }],
441
+ },
442
+ ],
443
+ };
444
+ },
445
+ { touch: true },
446
+ );
447
+ },
448
+ [updateSession],
449
+ );
450
+
451
+ const startActivityStream = useCallback(
452
+ (sessionId: string, ctxId: string) => {
453
+ const runtime = getRuntime(sessionId);
454
+ if (runtime.activityContextId === ctxId && runtime.activityAbortController) {
455
+ return;
456
+ }
457
+
458
+ runtime.activityAbortController?.abort();
459
+ const abort = new AbortController();
460
+ runtime.activityAbortController = abort;
461
+ runtime.activityContextId = ctxId;
462
+
463
+ const run = async () => {
464
+ let attempt = 0;
465
+ while (!abort.signal.aborted) {
466
+ try {
467
+ await streamA2aEvents({
468
+ contextId: ctxId,
469
+ since: runtime.lastActivityId,
470
+ signal: abort.signal,
471
+ callbacks: {
472
+ onActivity: (event) => {
473
+ runtime.lastActivityId = event.id;
474
+ upsertBackgroundActivity(sessionId, event);
475
+ upsertTodoActivity(sessionId, event);
476
+ appendDeliveryMessage(sessionId, event);
477
+ },
478
+ onError: (err) => {
479
+ if (abort.signal.aborted) return;
480
+ console.warn("Activity stream error", err);
481
+ },
482
+ },
483
+ });
484
+ } catch (err) {
485
+ if (abort.signal.aborted) return;
486
+ console.warn("Activity stream disconnected", err);
487
+ }
488
+
489
+ if (abort.signal.aborted) return;
490
+ attempt += 1;
491
+ const delayMs = Math.min(1000 * 2 ** attempt, 15000);
492
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
493
+ }
494
+ };
495
+
496
+ void run();
497
+ },
498
+ [appendDeliveryMessage, upsertBackgroundActivity, upsertTodoActivity],
499
+ );
500
+
501
+ useEffect(() => {
502
+ if (!currentSession?.contextId || !currentSession) return;
503
+ startActivityStream(currentSession.id, currentSession.contextId);
504
+ }, [currentSession, startActivityStream]);
505
+
506
+ const send = useCallback(
507
+ async ({ text, sessionId }: SendArgs) => {
508
+ const sid = sessionId ?? currentSessionId;
509
+ if (!sid) return;
510
+ const session = sessions.find((s) => s.id === sid);
511
+ if (!session || session.isStreaming) return;
512
+
513
+ const userId = nowId();
514
+ const agentId = nowId();
515
+ const runtime = getRuntime(sid);
516
+ runtime.activeAgentId = agentId;
517
+ runtime.pendingTask = null;
518
+
519
+ updateSession(
520
+ sid,
521
+ (prev) => ({
522
+ ...prev,
523
+ title: prev.title === DEFAULT_TITLE ? deriveTitle(text) : prev.title,
524
+ items: [
525
+ ...prev.items,
526
+ { id: userId, role: "user", text },
527
+ { id: agentId, role: "agent", blocks: [] },
528
+ ],
529
+ isStreaming: true,
530
+ error: null,
531
+ activeTaskId: undefined,
532
+ inputDraft: "",
533
+ }),
534
+ { touch: true },
535
+ );
536
+
537
+ const abort = new AbortController();
538
+ runtime.abortController?.abort();
539
+ runtime.abortController = abort;
540
+
541
+ try {
542
+ await streamA2aChat({
543
+ text,
544
+ contextId: session.contextId,
545
+ sessionId: session.sessionId,
546
+ signal: abort.signal,
547
+ callbacks: {
548
+ onOpen: ({ taskId, contextId: ctx }) => {
549
+ updateSession(
550
+ sid,
551
+ (prev) => ({
552
+ ...prev,
553
+ activeTaskId: taskId,
554
+ contextId: ctx ?? prev.contextId,
555
+ items: prev.items.map((it) =>
556
+ it.role === "agent" && it.id === agentId ? { ...it, taskId } : it,
557
+ ),
558
+ }),
559
+ { touch: false },
560
+ );
561
+ if (ctx) startActivityStream(sid, ctx);
562
+ },
563
+ onSessionId: (sessionId) => {
564
+ updateSession(
565
+ sid,
566
+ (prev) => ({ ...prev, sessionId }),
567
+ { touch: false },
568
+ );
569
+ },
570
+ onDelta: (deltaText) => {
571
+ updateSession(
572
+ sid,
573
+ (prev) => ({
574
+ ...prev,
575
+ items: prev.items.map((it) => {
576
+ if (it.role !== "agent" || it.id !== agentId) return it;
577
+ const blocks = [...it.blocks];
578
+ const last = blocks[blocks.length - 1];
579
+ if (last?.kind === "text") {
580
+ blocks[blocks.length - 1] = {
581
+ kind: "text",
582
+ text: last.text + deltaText,
583
+ };
584
+ } else {
585
+ blocks.push({ kind: "text", text: deltaText });
586
+ }
587
+ return { ...it, blocks };
588
+ }),
589
+ }),
590
+ { touch: false },
591
+ );
592
+ },
593
+ onToolCall: (tc: A2aToolCallEvent) => {
594
+ if (tc.toolName === "Task") {
595
+ if (tc.status === "started" && tc.input) {
596
+ const input = tc.input as TaskToolInput;
597
+ const isBackground =
598
+ input.run_in_background === true ||
599
+ input.background === true ||
600
+ input.runInBackground === true;
601
+ runtime.pendingTask = {
602
+ toolId: tc.toolId,
603
+ prompt: input.prompt,
604
+ description: input.description,
605
+ isBackground,
606
+ };
607
+ } else if (tc.status === "completed") {
608
+ const output = tc.output;
609
+ if (output) {
610
+ const outputStr =
611
+ typeof output === "string" ? output : JSON.stringify(output);
612
+ updateSession(
613
+ sid,
614
+ (prev) => {
615
+ const lastRunning = [...prev.subagentActivities]
616
+ .reverse()
617
+ .find((a) => a.status === "running" || !a.result);
618
+ if (!lastRunning) return prev;
619
+ return {
620
+ ...prev,
621
+ subagentActivities: prev.subagentActivities.map((a) =>
622
+ a.agentId === lastRunning.agentId
623
+ ? { ...a, result: outputStr.slice(0, 2000) }
624
+ : a,
625
+ ),
626
+ };
627
+ },
628
+ { touch: false },
629
+ );
630
+ }
631
+ runtime.pendingTask = null;
632
+ }
633
+ }
634
+
635
+ updateSession(
636
+ sid,
637
+ (prev) => ({
638
+ ...prev,
639
+ items: prev.items.map((it) => {
640
+ if (it.role !== "agent" || it.id !== agentId) return it;
641
+ const blocks = [...it.blocks];
642
+ let idx = -1;
643
+ if (tc.toolSeq !== undefined) {
644
+ idx = blocks.findIndex(
645
+ (b) => b.kind === "tool" && (b as AgentToolBlock).toolSeq === tc.toolSeq,
646
+ );
647
+ }
648
+ if (idx < 0 && tc.toolId) {
649
+ idx = blocks.findIndex(
650
+ (b) => b.kind === "tool" && b.toolId === tc.toolId,
651
+ );
652
+ }
653
+ if (idx < 0 && tc.status === "completed") {
654
+ idx = blocks.findIndex(
655
+ (b) =>
656
+ b.kind === "tool" &&
657
+ b.toolName === tc.toolName &&
658
+ (b as AgentToolBlock).status === "started",
659
+ );
660
+ }
661
+
662
+ const existing =
663
+ idx >= 0 && blocks[idx].kind === "tool"
664
+ ? (blocks[idx] as AgentToolBlock)
665
+ : null;
666
+ const newBlock: AgentToolBlock = {
667
+ kind: "tool",
668
+ toolId: tc.toolId || existing?.toolId || "",
669
+ toolName: tc.toolName,
670
+ input:
671
+ tc.input !== undefined && tc.input !== null
672
+ ? tc.input
673
+ : existing?.input,
674
+ status: tc.status,
675
+ output: tc.output ?? existing?.output,
676
+ toolSeq: tc.toolSeq ?? existing?.toolSeq,
677
+ };
678
+ if (idx >= 0) {
679
+ blocks[idx] = newBlock;
680
+ } else {
681
+ blocks.push(newBlock);
682
+ }
683
+ return { ...it, blocks };
684
+ }),
685
+ }),
686
+ { touch: false },
687
+ );
688
+ },
689
+ onAskUserQuestion: (event: A2aAskUserQuestionEvent) => {
690
+ updateSession(
691
+ sid,
692
+ (prev) => ({
693
+ ...prev,
694
+ pendingQuestion: {
695
+ requestId: event.requestId,
696
+ taskId: event.taskId,
697
+ questions: event.questions,
698
+ },
699
+ }),
700
+ { touch: false },
701
+ );
702
+ },
703
+ onQuestionDismissed: (event: A2aQuestionDismissedEvent) => {
704
+ updateSession(
705
+ sid,
706
+ (prev) => ({
707
+ ...prev,
708
+ pendingQuestion:
709
+ prev.pendingQuestion?.requestId === event.requestId
710
+ ? null
711
+ : prev.pendingQuestion,
712
+ }),
713
+ { touch: false },
714
+ );
715
+ },
716
+ onSubagent: (event: A2aSubagentEvent) => {
717
+ updateSession(
718
+ sid,
719
+ (prev) => {
720
+ if (event.status === "started") {
721
+ const exists = prev.subagentActivities.find(
722
+ (a) => a.agentId === event.agentId,
723
+ );
724
+ if (exists) {
725
+ return {
726
+ ...prev,
727
+ subagentActivities: prev.subagentActivities.map((a) =>
728
+ a.agentId === event.agentId
729
+ ? { ...a, status: "running" as const }
730
+ : a,
731
+ ),
732
+ };
733
+ }
734
+ const pending = runtime.pendingTask;
735
+ return {
736
+ ...prev,
737
+ subagentActivities: [
738
+ ...prev.subagentActivities,
739
+ {
740
+ agentId: event.agentId,
741
+ agentType: event.agentType,
742
+ status: "running" as const,
743
+ startedAt: Date.now(),
744
+ isBackground: pending?.isBackground,
745
+ prompt: pending?.prompt,
746
+ description: pending?.description,
747
+ },
748
+ ],
749
+ };
750
+ }
751
+
752
+ return {
753
+ ...prev,
754
+ subagentActivities: prev.subagentActivities.map((a) =>
755
+ a.agentId === event.agentId
756
+ ? { ...a, status: "stopped" as const, stoppedAt: Date.now() }
757
+ : a,
758
+ ),
759
+ };
760
+ },
761
+ { touch: false },
762
+ );
763
+ },
764
+ onFinal: () => {
765
+ runtime.abortController = null;
766
+ updateSession(
767
+ sid,
768
+ (prev) => ({
769
+ ...prev,
770
+ isStreaming: false,
771
+ items: prev.items.map((it) => {
772
+ if (it.role !== "agent" || it.id !== agentId) return it;
773
+ const blocks = it.blocks.map((b) => {
774
+ if (b.kind === "tool" && (b as AgentToolBlock).status === "started") {
775
+ return {
776
+ ...b,
777
+ status: "completed" as const,
778
+ output: (b as AgentToolBlock).output,
779
+ };
780
+ }
781
+ return b;
782
+ });
783
+ return { ...it, blocks };
784
+ }),
785
+ }),
786
+ { touch: false },
787
+ );
788
+ },
789
+ onError: (err) => {
790
+ runtime.abortController = null;
791
+ updateSession(
792
+ sid,
793
+ (prev) => ({ ...prev, error: err.message, isStreaming: false }),
794
+ { touch: false },
795
+ );
796
+ },
797
+ },
798
+ });
799
+ } catch (e) {
800
+ if (abort.signal.aborted) return;
801
+ runtime.abortController = null;
802
+ updateSession(
803
+ sid,
804
+ (prev) => ({
805
+ ...prev,
806
+ error: e instanceof Error ? e.message : "Unknown error",
807
+ isStreaming: false,
808
+ }),
809
+ { touch: false },
810
+ );
811
+ }
812
+ },
813
+ [currentSessionId, sessions, startActivityStream, updateSession],
814
+ );
815
+
816
+ const value = useMemo(
817
+ () => {
818
+ const items = currentSession?.items ?? [];
819
+ const isStreaming = currentSession?.isStreaming ?? false;
820
+ const error = currentSession?.error ?? null;
821
+ const contextId = currentSession?.contextId;
822
+ const activeTaskId = currentSession?.activeTaskId;
823
+ const pendingQuestion = currentSession?.pendingQuestion ?? null;
824
+ const subagentActivities = currentSession?.subagentActivities ?? [];
825
+ const inputDraft = currentSession?.inputDraft ?? "";
826
+
827
+ return {
828
+ sessions,
829
+ currentSessionId,
830
+ currentSession,
831
+ items,
832
+ isStreaming,
833
+ error,
834
+ contextId,
835
+ activeTaskId,
836
+ pendingQuestion,
837
+ subagentActivities,
838
+ inputDraft,
839
+ send,
840
+ cancel,
841
+ newSession,
842
+ switchSession,
843
+ deleteSession,
844
+ setInputDraft,
845
+ submitAnswer,
846
+ };
847
+ },
848
+ [
849
+ cancel,
850
+ currentSession,
851
+ currentSessionId,
852
+ deleteSession,
853
+ newSession,
854
+ send,
855
+ sessions,
856
+ setInputDraft,
857
+ submitAnswer,
858
+ switchSession,
859
+ ],
860
+ );
861
+
862
+ return value;
863
+ }