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.
- package/README.md +160 -0
- package/apps/web/README.md +36 -0
- package/apps/web/env.example +20 -0
- package/apps/web/eslint.config.mjs +18 -0
- package/apps/web/next.config.ts +7 -0
- package/apps/web/package-lock.json +10348 -0
- package/apps/web/package.json +35 -0
- package/apps/web/postcss.config.mjs +7 -0
- package/apps/web/public/file.svg +1 -0
- package/apps/web/public/globe.svg +1 -0
- package/apps/web/public/next.svg +1 -0
- package/apps/web/public/vercel.svg +1 -0
- package/apps/web/public/window.svg +1 -0
- package/apps/web/src/app/.well-known/agent-card.json/route.ts +50 -0
- package/apps/web/src/app/background-tasks/[jobId]/cancel/route.ts +29 -0
- package/apps/web/src/app/events/stream/route.ts +58 -0
- package/apps/web/src/app/favicon.ico +0 -0
- package/apps/web/src/app/globals.css +174 -0
- package/apps/web/src/app/layout.tsx +34 -0
- package/apps/web/src/app/messages/answer/route.ts +57 -0
- package/apps/web/src/app/messages/stream/route.ts +381 -0
- package/apps/web/src/app/page.tsx +358 -0
- package/apps/web/src/app/tasks/[taskId]/cancel/route.ts +24 -0
- package/apps/web/src/app/tasks/[taskId]/route.ts +24 -0
- package/apps/web/src/app/tasks/route.ts +13 -0
- package/apps/web/src/components/chat/agent-blocks.tsx +111 -0
- package/apps/web/src/components/chat/ask-user-question-panel.tsx +172 -0
- package/apps/web/src/components/chat/session-sidebar.tsx +222 -0
- package/apps/web/src/components/chat/subagent-activity-sidebar.tsx +248 -0
- package/apps/web/src/components/chat/tool-blocks.tsx +550 -0
- package/apps/web/src/lib/a2a/activity-store.ts +150 -0
- package/apps/web/src/lib/a2a/client.ts +357 -0
- package/apps/web/src/lib/a2a/sse.ts +19 -0
- package/apps/web/src/lib/a2a/task-store.ts +111 -0
- package/apps/web/src/lib/a2a/types.ts +216 -0
- package/apps/web/src/lib/agent/answer-store.ts +109 -0
- package/apps/web/src/lib/agent/background-delivery.ts +343 -0
- package/apps/web/src/lib/agent/background-tool.ts +78 -0
- package/apps/web/src/lib/agent/background.ts +452 -0
- package/apps/web/src/lib/agent/chat.ts +543 -0
- package/apps/web/src/lib/agent/session-store.ts +26 -0
- package/apps/web/src/lib/chat/types.ts +44 -0
- package/apps/web/src/lib/env.ts +31 -0
- package/apps/web/src/lib/hooks/useA2AChat.ts +863 -0
- package/apps/web/src/lib/state/chat-atoms.ts +52 -0
- package/apps/web/src/lib/workspace.ts +9 -0
- package/apps/web/tsconfig.json +35 -0
- package/bin/remotion-agent.js +451 -0
- package/package.json +34 -0
- package/templates/.claude/CLAUDE.md +95 -0
- package/templates/.claude/README.md +129 -0
- package/templates/.claude/agents/composer-agent.md +188 -0
- package/templates/.claude/agents/crafter.md +181 -0
- package/templates/.claude/agents/creator.md +134 -0
- package/templates/.claude/agents/perceiver.md +92 -0
- package/templates/.claude/settings.json +36 -0
- package/templates/.claude/settings.local.json +39 -0
- package/templates/.claude/skills/agent-browser/SKILL.md +349 -0
- package/templates/.claude/skills/agent-browser/references/authentication.md +188 -0
- package/templates/.claude/skills/agent-browser/references/proxy-support.md +175 -0
- package/templates/.claude/skills/agent-browser/references/session-management.md +181 -0
- package/templates/.claude/skills/agent-browser/references/snapshot-refs.md +186 -0
- package/templates/.claude/skills/agent-browser/references/video-recording.md +162 -0
- package/templates/.claude/skills/agent-browser/templates/authenticated-session.sh +91 -0
- package/templates/.claude/skills/agent-browser/templates/capture-workflow.sh +68 -0
- package/templates/.claude/skills/agent-browser/templates/form-automation.sh +64 -0
- package/templates/.claude/skills/algorithmic-art/LICENSE.txt +202 -0
- package/templates/.claude/skills/algorithmic-art/SKILL.md +405 -0
- package/templates/.claude/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/templates/.claude/skills/algorithmic-art/templates/viewer.html +599 -0
- package/templates/.claude/skills/asset-validator/SKILL.md +376 -0
- package/templates/.claude/skills/audio-video-sync/SKILL.md +219 -0
- package/templates/.claude/skills/bgm-manager/SKILL.md +334 -0
- package/templates/.claude/skills/remotion-best-practices/SKILL.md +45 -0
- package/templates/.claude/skills/remotion-best-practices/rules/3d.md +86 -0
- package/templates/.claude/skills/remotion-best-practices/rules/animations.md +29 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets.md +78 -0
- package/templates/.claude/skills/remotion-best-practices/rules/audio.md +172 -0
- package/templates/.claude/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
- package/templates/.claude/skills/remotion-best-practices/rules/can-decode.md +75 -0
- package/templates/.claude/skills/remotion-best-practices/rules/charts.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/compositions.md +141 -0
- package/templates/.claude/skills/remotion-best-practices/rules/display-captions.md +126 -0
- package/templates/.claude/skills/remotion-best-practices/rules/extract-frames.md +229 -0
- package/templates/.claude/skills/remotion-best-practices/rules/fonts.md +152 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/gifs.md +138 -0
- package/templates/.claude/skills/remotion-best-practices/rules/images.md +130 -0
- package/templates/.claude/skills/remotion-best-practices/rules/import-srt-captions.md +67 -0
- package/templates/.claude/skills/remotion-best-practices/rules/lottie.md +68 -0
- package/templates/.claude/skills/remotion-best-practices/rules/maps.md +403 -0
- package/templates/.claude/skills/remotion-best-practices/rules/measuring-dom-nodes.md +35 -0
- package/templates/.claude/skills/remotion-best-practices/rules/measuring-text.md +143 -0
- package/templates/.claude/skills/remotion-best-practices/rules/parameters.md +98 -0
- package/templates/.claude/skills/remotion-best-practices/rules/sequencing.md +118 -0
- package/templates/.claude/skills/remotion-best-practices/rules/tailwind.md +11 -0
- package/templates/.claude/skills/remotion-best-practices/rules/text-animations.md +20 -0
- package/templates/.claude/skills/remotion-best-practices/rules/timing.md +179 -0
- package/templates/.claude/skills/remotion-best-practices/rules/transcribe-captions.md +19 -0
- package/templates/.claude/skills/remotion-best-practices/rules/transitions.md +122 -0
- package/templates/.claude/skills/remotion-best-practices/rules/trimming.md +53 -0
- package/templates/.claude/skills/remotion-best-practices/rules/videos.md +171 -0
- package/templates/.claude/skills/remotion-components/SKILL.md +453 -0
- package/templates/.claude/skills/render-config/SKILL.md +290 -0
- package/templates/.claude/skills/script-writer/SKILL.md +59 -0
- package/templates/.claude/skills/style-director/script-writer/SKILL.md +82 -0
- package/templates/.claude/skills/style-director/style-director/SKILL.md +287 -0
- package/templates/.claude/skills/style-director/style-director/references/audience-and-scenarios.md +43 -0
- package/templates/.claude/skills/style-director/style-director/references/interaction-innovation.md +26 -0
- package/templates/.claude/skills/style-director/style-director/references/motion-grammar.md +66 -0
- package/templates/.claude/skills/style-director/style-director/references/quality-checklist.md +29 -0
- package/templates/.claude/skills/style-director/style-director/references/scene-recipes.md +38 -0
- package/templates/.claude/skills/style-director/style-director/references/visual-style-system.md +148 -0
- package/templates/.claude/skills/subtitle-composer/SKILL.md +304 -0
- package/templates/.claude/skills/subtitle-processor/SKILL.md +308 -0
- package/templates/.claude/skills/timeline-generator/SKILL.md +253 -0
- package/templates/.claude/skills/video-preflight-check/SKILL.md +353 -0
- package/templates/.claude/skills/voice-synthesizer/SKILL.md +296 -0
- package/templates/.claude/skills/voice-synthesizer/scripts/synthesize_voice.py +315 -0
- package/templates/.claude/skills/voice-synthesizer/scripts/tts_cli.py +142 -0
- package/templates/.claude/skills/web-design-guidelines/SKILL.md +36 -0
- package/templates/.claude/skills/youtube-downloader/SKILL.md +99 -0
- package/templates/.claude/skills/youtube-downloader/scripts/download_video.py +145 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { AgentToolBlock } from "@/lib/chat/types";
|
|
5
|
+
import {
|
|
6
|
+
Activity,
|
|
7
|
+
CheckCircle2,
|
|
8
|
+
ChevronDown,
|
|
9
|
+
ChevronRight,
|
|
10
|
+
Circle,
|
|
11
|
+
Clock,
|
|
12
|
+
Cpu,
|
|
13
|
+
Info,
|
|
14
|
+
ListTodo,
|
|
15
|
+
Loader2,
|
|
16
|
+
} from "lucide-react";
|
|
17
|
+
|
|
18
|
+
const RESULT_ONE_LINE_LEN = 120;
|
|
19
|
+
|
|
20
|
+
function getFullParamString(toolName: string, input: unknown): string {
|
|
21
|
+
if (input === null || input === undefined) return "";
|
|
22
|
+
const o = typeof input === "object" && input !== null ? (input as Record<string, unknown>) : {};
|
|
23
|
+
switch (toolName) {
|
|
24
|
+
case "Bash":
|
|
25
|
+
return String(o.command ?? "");
|
|
26
|
+
case "Read":
|
|
27
|
+
case "Write":
|
|
28
|
+
case "Edit":
|
|
29
|
+
return String(o.file_path ?? "");
|
|
30
|
+
case "Grep":
|
|
31
|
+
case "Glob":
|
|
32
|
+
return String(o.pattern ?? "");
|
|
33
|
+
case "WebFetch":
|
|
34
|
+
return String(o.url ?? "");
|
|
35
|
+
case "WebSearch":
|
|
36
|
+
return String(o.query ?? "");
|
|
37
|
+
case "TodoWrite":
|
|
38
|
+
return "";
|
|
39
|
+
case "Task":
|
|
40
|
+
return String(o.description ?? "");
|
|
41
|
+
case "mcp__background_tasks__start":
|
|
42
|
+
return String(o.description ?? "");
|
|
43
|
+
case "mcp__background_tasks__list":
|
|
44
|
+
return "";
|
|
45
|
+
default:
|
|
46
|
+
return typeof input === "string" ? input : JSON.stringify(input);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getTruncatedParam(param: string, maxLen = 60): string {
|
|
51
|
+
if (param.length <= maxLen) return param;
|
|
52
|
+
return param.slice(0, maxLen) + "...";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getResultDisplay(block: AgentToolBlock): string {
|
|
56
|
+
if (block.status === "started") return "Running...";
|
|
57
|
+
const out = block.output;
|
|
58
|
+
if (out === null || out === undefined) return "Done";
|
|
59
|
+
const s = typeof out === "string" ? out : JSON.stringify(out);
|
|
60
|
+
const line = s.split("\n").filter((l) => l.trim())[0] ?? "";
|
|
61
|
+
if (!line) return "Done";
|
|
62
|
+
return line.length > RESULT_ONE_LINE_LEN ? line.slice(0, RESULT_ONE_LINE_LEN) + "..." : line;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getResultFull(block: AgentToolBlock): string {
|
|
66
|
+
if (block.status === "started" || block.output == null) return "";
|
|
67
|
+
return typeof block.output === "string" ? block.output : JSON.stringify(block.output, null, 2);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractToolText(output: unknown): string | null {
|
|
71
|
+
if (!output) return null;
|
|
72
|
+
if (typeof output === "string") return output;
|
|
73
|
+
if (typeof output !== "object") return null;
|
|
74
|
+
const rec = output as Record<string, unknown>;
|
|
75
|
+
const content = rec.content;
|
|
76
|
+
if (!Array.isArray(content)) return null;
|
|
77
|
+
for (const item of content) {
|
|
78
|
+
if (!item || typeof item !== "object") continue;
|
|
79
|
+
const entry = item as Record<string, unknown>;
|
|
80
|
+
if (entry.type === "text" && typeof entry.text === "string") {
|
|
81
|
+
return entry.text;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseToolJson(output: unknown): Record<string, unknown> | null {
|
|
88
|
+
if (!output) return null;
|
|
89
|
+
if (typeof output === "object") {
|
|
90
|
+
const rec = output as Record<string, unknown>;
|
|
91
|
+
if ("tasks" in rec || "jobId" in rec) return rec;
|
|
92
|
+
}
|
|
93
|
+
const text = extractToolText(output) ?? (typeof output === "string" ? output : null);
|
|
94
|
+
if (!text) return null;
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(text);
|
|
97
|
+
if (parsed && typeof parsed === "object") return parsed as Record<string, unknown>;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getDotClass(block: AgentToolBlock): string {
|
|
105
|
+
if (block.status === "started") return "bg-blue-500 animate-pulse";
|
|
106
|
+
const out = block.output;
|
|
107
|
+
const s = typeof out === "string" ? out : out != null ? String(out) : "";
|
|
108
|
+
if (s.toLowerCase().includes("error")) return "bg-red-500";
|
|
109
|
+
return "bg-emerald-500";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type TodoItem = {
|
|
113
|
+
content: string;
|
|
114
|
+
status: "pending" | "in_progress" | "completed";
|
|
115
|
+
activeForm?: string;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
type TodoWriteInput = {
|
|
119
|
+
todos?: TodoItem[];
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
function getTodoStatusIcon(status: TodoItem["status"]) {
|
|
123
|
+
switch (status) {
|
|
124
|
+
case "completed":
|
|
125
|
+
return <CheckCircle2 className="w-4 h-4 text-emerald-500" />;
|
|
126
|
+
case "in_progress":
|
|
127
|
+
return <Clock className="w-4 h-4 text-blue-500 animate-pulse" />;
|
|
128
|
+
default:
|
|
129
|
+
return <Circle className="w-4 h-4 text-muted-foreground" />;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getTodoStatusClass(status: TodoItem["status"]) {
|
|
134
|
+
switch (status) {
|
|
135
|
+
case "completed":
|
|
136
|
+
return "text-muted-foreground line-through";
|
|
137
|
+
case "in_progress":
|
|
138
|
+
return "text-blue-600 dark:text-blue-400 font-medium";
|
|
139
|
+
default:
|
|
140
|
+
return "text-foreground";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getTodoStats(todos: TodoItem[]) {
|
|
145
|
+
return {
|
|
146
|
+
total: todos.length,
|
|
147
|
+
completed: todos.filter((t) => t.status === "completed").length,
|
|
148
|
+
inProgress: todos.filter((t) => t.status === "in_progress").length,
|
|
149
|
+
pending: todos.filter((t) => t.status === "pending").length,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function TodoListCard({
|
|
154
|
+
title,
|
|
155
|
+
todos,
|
|
156
|
+
}: {
|
|
157
|
+
title: string;
|
|
158
|
+
todos: TodoItem[];
|
|
159
|
+
}) {
|
|
160
|
+
const stats = getTodoStats(todos);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className="rounded-lg border border-cyan-200 dark:border-cyan-800 bg-cyan-50/50 dark:bg-cyan-950/20 overflow-hidden">
|
|
164
|
+
<div className="flex items-center gap-2 px-3 py-2 border-b border-cyan-200/50 dark:border-cyan-800/50">
|
|
165
|
+
<ListTodo className="w-4 h-4 text-cyan-500" />
|
|
166
|
+
<span className="text-sm font-medium text-foreground">{title}</span>
|
|
167
|
+
<div className="flex-1" />
|
|
168
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
169
|
+
<span className="flex items-center gap-1">
|
|
170
|
+
<CheckCircle2 className="w-3 h-3 text-emerald-500" />
|
|
171
|
+
{stats.completed}
|
|
172
|
+
</span>
|
|
173
|
+
<span className="flex items-center gap-1">
|
|
174
|
+
<Clock className="w-3 h-3 text-blue-500" />
|
|
175
|
+
{stats.inProgress}
|
|
176
|
+
</span>
|
|
177
|
+
<span className="flex items-center gap-1">
|
|
178
|
+
<Circle className="w-3 h-3" />
|
|
179
|
+
{stats.pending}
|
|
180
|
+
</span>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<div className="px-3 py-2 space-y-1.5">
|
|
184
|
+
{todos.map((todo, idx) => (
|
|
185
|
+
<div key={idx} className="flex items-start gap-2">
|
|
186
|
+
{getTodoStatusIcon(todo.status)}
|
|
187
|
+
<span className={`text-sm ${getTodoStatusClass(todo.status)}`}>
|
|
188
|
+
{todo.content}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseTodosFromResult(result?: string): TodoItem[] | null {
|
|
198
|
+
if (!result) return null;
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(result);
|
|
201
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
202
|
+
const rec = parsed as Record<string, unknown>;
|
|
203
|
+
const rawTodos = Array.isArray(rec.newTodos)
|
|
204
|
+
? rec.newTodos
|
|
205
|
+
: Array.isArray(rec.todos)
|
|
206
|
+
? rec.todos
|
|
207
|
+
: null;
|
|
208
|
+
if (!rawTodos) return null;
|
|
209
|
+
const todos = rawTodos
|
|
210
|
+
.map((todo): TodoItem | null => {
|
|
211
|
+
if (!todo || typeof todo !== "object") return null;
|
|
212
|
+
const t = todo as Record<string, unknown>;
|
|
213
|
+
const content = typeof t.content === "string" ? t.content : null;
|
|
214
|
+
const status =
|
|
215
|
+
t.status === "pending" || t.status === "in_progress" || t.status === "completed"
|
|
216
|
+
? (t.status as TodoItem["status"])
|
|
217
|
+
: null;
|
|
218
|
+
if (!content || !status) return null;
|
|
219
|
+
const item: TodoItem = { content, status };
|
|
220
|
+
if (typeof t.activeForm === "string") item.activeForm = t.activeForm;
|
|
221
|
+
return item;
|
|
222
|
+
})
|
|
223
|
+
.filter((t): t is TodoItem => t !== null);
|
|
224
|
+
return todos.length > 0 ? todos : null;
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function TodoBlock({ block }: { block: AgentToolBlock }) {
|
|
231
|
+
const input = (block.input || {}) as TodoWriteInput;
|
|
232
|
+
const todos = input.todos || [];
|
|
233
|
+
const isRunning = block.status === "started";
|
|
234
|
+
|
|
235
|
+
if (todos.length === 0 && !isRunning) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
if (todos.length === 0 && isRunning) {
|
|
239
|
+
return (
|
|
240
|
+
<div className="rounded-lg border border-cyan-200 dark:border-cyan-800 bg-cyan-50/50 dark:bg-cyan-950/20 overflow-hidden">
|
|
241
|
+
<div className="flex items-center gap-2 px-3 py-2 border-b border-cyan-200/50 dark:border-cyan-800/50">
|
|
242
|
+
<ListTodo className="w-4 h-4 text-cyan-500" />
|
|
243
|
+
<span className="text-sm font-medium text-foreground">Task List</span>
|
|
244
|
+
</div>
|
|
245
|
+
<div className="px-3 py-2">
|
|
246
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
247
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
248
|
+
Updating task list...
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return <TodoListCard title="Task List" todos={todos} />;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
type TaskInput = {
|
|
259
|
+
prompt?: string;
|
|
260
|
+
description?: string;
|
|
261
|
+
subagent_type?: string;
|
|
262
|
+
model?: string;
|
|
263
|
+
readonly?: boolean;
|
|
264
|
+
run_in_background?: boolean;
|
|
265
|
+
background?: boolean;
|
|
266
|
+
runInBackground?: boolean;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
function parseTaskOutput(output: unknown): { result?: string; agentId?: string } {
|
|
270
|
+
if (!output) return {};
|
|
271
|
+
const str = typeof output === "string" ? output : JSON.stringify(output);
|
|
272
|
+
const agentIdMatch = str.match(/agentId:\s*([a-f0-9-]+)/i);
|
|
273
|
+
return {
|
|
274
|
+
result: str,
|
|
275
|
+
agentId: agentIdMatch?.[1],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
type BackgroundTaskInput = {
|
|
280
|
+
description?: string;
|
|
281
|
+
prompt?: string;
|
|
282
|
+
subagent_type?: string;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
function parseBackgroundOutput(output: unknown): { jobId?: string } {
|
|
286
|
+
if (!output) return {};
|
|
287
|
+
try {
|
|
288
|
+
const parsed = parseToolJson(output);
|
|
289
|
+
if (parsed && typeof parsed.jobId === "string") {
|
|
290
|
+
return { jobId: parsed.jobId };
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
// ignore parse errors
|
|
294
|
+
}
|
|
295
|
+
return {};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function TaskBlock({ block }: { block: AgentToolBlock }) {
|
|
299
|
+
const [expanded, setExpanded] = useState(false);
|
|
300
|
+
const input = (block.input || {}) as TaskInput;
|
|
301
|
+
const description = input.description || "Subagent task";
|
|
302
|
+
const subagentType = input.subagent_type || "generalPurpose";
|
|
303
|
+
const model = input.model;
|
|
304
|
+
const isBackground =
|
|
305
|
+
input.run_in_background === true ||
|
|
306
|
+
input.background === true ||
|
|
307
|
+
input.runInBackground === true;
|
|
308
|
+
const isRunning = block.status === "started";
|
|
309
|
+
const { result, agentId } = parseTaskOutput(block.output);
|
|
310
|
+
const todosFromResult = parseTodosFromResult(result);
|
|
311
|
+
const showRawResult = Boolean(result && !isRunning && !todosFromResult);
|
|
312
|
+
|
|
313
|
+
const getSubagentLabel = (type: string) => {
|
|
314
|
+
switch (type) {
|
|
315
|
+
case "generalPurpose":
|
|
316
|
+
return "General";
|
|
317
|
+
case "explore":
|
|
318
|
+
return "Explorer";
|
|
319
|
+
default:
|
|
320
|
+
return type;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const getModelLabel = (m?: string) => {
|
|
325
|
+
if (!m) return null;
|
|
326
|
+
switch (m) {
|
|
327
|
+
case "fast":
|
|
328
|
+
return "Fast";
|
|
329
|
+
case "sonnet":
|
|
330
|
+
return "Sonnet";
|
|
331
|
+
case "opus":
|
|
332
|
+
return "Opus";
|
|
333
|
+
case "haiku":
|
|
334
|
+
return "Haiku";
|
|
335
|
+
default:
|
|
336
|
+
return m;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const getStatusDisplay = () => {
|
|
341
|
+
if (isBackground) {
|
|
342
|
+
return (
|
|
343
|
+
<span className="flex items-center gap-1 text-blue-500">
|
|
344
|
+
<span className="w-1.5 h-1.5 rounded-full bg-blue-500" />
|
|
345
|
+
Submitted to background
|
|
346
|
+
</span>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (isRunning) {
|
|
351
|
+
return <span className="text-violet-500">Running...</span>;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<span className="flex items-center gap-1">
|
|
356
|
+
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
|
357
|
+
Completed
|
|
358
|
+
{agentId && <span className="opacity-60">· {agentId.slice(0, 8)}</span>}
|
|
359
|
+
</span>
|
|
360
|
+
);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div
|
|
365
|
+
className={`rounded-lg border overflow-hidden ${
|
|
366
|
+
isBackground
|
|
367
|
+
? "border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-950/20"
|
|
368
|
+
: "border-violet-200 dark:border-violet-800 bg-violet-50/50 dark:bg-violet-950/20"
|
|
369
|
+
}`}
|
|
370
|
+
>
|
|
371
|
+
<button
|
|
372
|
+
type="button"
|
|
373
|
+
onClick={() => setExpanded((e) => !e)}
|
|
374
|
+
className={`w-full flex items-start gap-3 px-3 py-2.5 text-left transition-colors enabled:cursor-pointer ${
|
|
375
|
+
isBackground
|
|
376
|
+
? "hover:bg-blue-100/50 dark:hover:bg-blue-900/20"
|
|
377
|
+
: "hover:bg-violet-100/50 dark:hover:bg-violet-900/20"
|
|
378
|
+
}`}
|
|
379
|
+
>
|
|
380
|
+
<div className="flex items-center gap-2 shrink-0 mt-0.5">
|
|
381
|
+
{isBackground ? (
|
|
382
|
+
<Activity className="w-4 h-4 text-blue-500" />
|
|
383
|
+
) : isRunning ? (
|
|
384
|
+
<Loader2 className="w-4 h-4 text-violet-500 animate-spin" />
|
|
385
|
+
) : (
|
|
386
|
+
<Cpu className="w-4 h-4 text-violet-500" />
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
<div className="flex-1 min-w-0">
|
|
390
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
391
|
+
<span className="text-sm font-medium text-foreground">{description}</span>
|
|
392
|
+
<span
|
|
393
|
+
className={`text-xs px-1.5 py-0.5 rounded ${
|
|
394
|
+
isBackground
|
|
395
|
+
? "bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-400"
|
|
396
|
+
: "bg-violet-100 dark:bg-violet-900/40 text-violet-600 dark:text-violet-400"
|
|
397
|
+
}`}
|
|
398
|
+
>
|
|
399
|
+
{getSubagentLabel(subagentType)}
|
|
400
|
+
</span>
|
|
401
|
+
{isBackground && (
|
|
402
|
+
<span className="text-xs px-1.5 py-0.5 rounded bg-blue-100 dark:bg-blue-900/40 text-blue-600 dark:text-blue-400">
|
|
403
|
+
Background
|
|
404
|
+
</span>
|
|
405
|
+
)}
|
|
406
|
+
{model && (
|
|
407
|
+
<span className="text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
|
|
408
|
+
{getModelLabel(model)}
|
|
409
|
+
</span>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
<div className="mt-1 text-xs text-muted-foreground">{getStatusDisplay()}</div>
|
|
413
|
+
</div>
|
|
414
|
+
<div className="shrink-0 mt-0.5">
|
|
415
|
+
{expanded ? (
|
|
416
|
+
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
|
417
|
+
) : (
|
|
418
|
+
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
|
419
|
+
)}
|
|
420
|
+
</div>
|
|
421
|
+
</button>
|
|
422
|
+
|
|
423
|
+
{expanded && (
|
|
424
|
+
<div className="px-3 py-2 border-t border-violet-200/50 dark:border-violet-800/50 space-y-2">
|
|
425
|
+
{input.prompt && (
|
|
426
|
+
<div className="text-xs">
|
|
427
|
+
<div className="font-medium text-muted-foreground mb-1">Prompt</div>
|
|
428
|
+
<div className="text-foreground bg-background/50 rounded p-2 max-h-32 overflow-y-auto whitespace-pre-wrap">
|
|
429
|
+
{input.prompt.length > 500 ? input.prompt.slice(0, 500) + "..." : input.prompt}
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
)}
|
|
433
|
+
|
|
434
|
+
{todosFromResult && (
|
|
435
|
+
<TodoListCard title="Todo (from result)" todos={todosFromResult} />
|
|
436
|
+
)}
|
|
437
|
+
|
|
438
|
+
{showRawResult && result != null && (
|
|
439
|
+
<div className="text-xs">
|
|
440
|
+
<div className="font-medium text-muted-foreground mb-1">Result</div>
|
|
441
|
+
<div className="text-foreground bg-background/50 rounded p-2 max-h-48 overflow-y-auto whitespace-pre-wrap">
|
|
442
|
+
{result.length > 1000 ? result.slice(0, 1000) + "..." : result}
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
</div>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function BackgroundTaskBlock({ block }: { block: AgentToolBlock }) {
|
|
453
|
+
const input = (block.input || {}) as BackgroundTaskInput;
|
|
454
|
+
const description = input.description || "Background task";
|
|
455
|
+
const { jobId } = parseBackgroundOutput(block.output);
|
|
456
|
+
const isRunning = block.status === "started";
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
<div className="rounded-lg border border-blue-200 dark:border-blue-800 bg-gradient-to-r from-blue-50/70 to-transparent dark:from-blue-950/30 dark:to-transparent overflow-hidden">
|
|
460
|
+
<div className="flex items-start gap-3 px-3 py-2.5">
|
|
461
|
+
<div className="flex items-center gap-2 shrink-0 mt-0.5">
|
|
462
|
+
{isRunning ? (
|
|
463
|
+
<Loader2 className="w-4 h-4 text-blue-500 animate-spin" />
|
|
464
|
+
) : (
|
|
465
|
+
<Activity className="w-4 h-4 text-blue-500" />
|
|
466
|
+
)}
|
|
467
|
+
<span className="text-sm font-medium text-foreground">{description}</span>
|
|
468
|
+
</div>
|
|
469
|
+
<div className="flex-1" />
|
|
470
|
+
<span className="text-xs text-blue-600 dark:text-blue-400">
|
|
471
|
+
{isRunning ? "Submitting..." : "Submitted"}
|
|
472
|
+
{jobId && <span className="ml-1 opacity-60">· {jobId.slice(0, 8)}</span>}
|
|
473
|
+
</span>
|
|
474
|
+
</div>
|
|
475
|
+
<div className="px-3 pb-2 text-xs text-muted-foreground flex items-center gap-1.5">
|
|
476
|
+
<Info className="w-3 h-3" />
|
|
477
|
+
<span>进度与结果会在右侧 Activity 面板与主会话中同步更新。</span>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function ToolBlock({ block }: { block: AgentToolBlock }) {
|
|
484
|
+
if (block.toolName === "Task") {
|
|
485
|
+
return <TaskBlock block={block} />;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (block.toolName === "mcp__background_tasks__start") {
|
|
489
|
+
return <BackgroundTaskBlock block={block} />;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (block.toolName === "TodoWrite") {
|
|
493
|
+
return <TodoBlock block={block} />;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const result = getResultDisplay(block);
|
|
497
|
+
const query = getFullParamString(block.toolName, block.input);
|
|
498
|
+
const truncatedQuery = getTruncatedParam(query, 60);
|
|
499
|
+
|
|
500
|
+
return (
|
|
501
|
+
<div className="py-1.5 text-left">
|
|
502
|
+
<div className="flex items-center gap-2 flex-wrap text-sm">
|
|
503
|
+
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${getDotClass(block)}`} />
|
|
504
|
+
<span className="font-medium text-foreground">{block.toolName}</span>
|
|
505
|
+
{query && (
|
|
506
|
+
<span className="text-muted-foreground truncate max-w-[min(320px,70vw)]">
|
|
507
|
+
({truncatedQuery})
|
|
508
|
+
</span>
|
|
509
|
+
)}
|
|
510
|
+
</div>
|
|
511
|
+
<div className="mt-0.5 pl-3.5 text-sm text-muted-foreground min-w-0">
|
|
512
|
+
<span className="block truncate" title={getResultFull(block) || undefined}>
|
|
513
|
+
└ {result}
|
|
514
|
+
</span>
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function ToolGroupBlock({ tools, startIdx }: { tools: AgentToolBlock[]; startIdx: number }) {
|
|
521
|
+
const [expanded, setExpanded] = useState(true);
|
|
522
|
+
const label = "Tool calls";
|
|
523
|
+
|
|
524
|
+
return (
|
|
525
|
+
<div
|
|
526
|
+
key={`toolGroup-${startIdx}`}
|
|
527
|
+
className="rounded-lg border border-border bg-muted/60 overflow-hidden"
|
|
528
|
+
>
|
|
529
|
+
<button
|
|
530
|
+
type="button"
|
|
531
|
+
onClick={() => setExpanded((e) => !e)}
|
|
532
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-left text-sm font-medium text-foreground hover:bg-muted/70 transition-colors enabled:cursor-pointer"
|
|
533
|
+
>
|
|
534
|
+
{expanded ? (
|
|
535
|
+
<ChevronDown className="w-4 h-4 shrink-0 text-muted-foreground" />
|
|
536
|
+
) : (
|
|
537
|
+
<ChevronRight className="w-4 h-4 shrink-0 text-muted-foreground" />
|
|
538
|
+
)}
|
|
539
|
+
<span>{label}</span>
|
|
540
|
+
</button>
|
|
541
|
+
{expanded && (
|
|
542
|
+
<div className="px-3 py-2 pt-0 border-t border-border/70">
|
|
543
|
+
{tools.map((block, j) => (
|
|
544
|
+
<ToolBlock key={`${block.toolId}-${j}`} block={block} />
|
|
545
|
+
))}
|
|
546
|
+
</div>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
);
|
|
550
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
A2aActivityEvent,
|
|
3
|
+
A2aBackgroundTaskActivity,
|
|
4
|
+
A2aDeliveryMessageActivity,
|
|
5
|
+
A2aTodoUpdateActivity,
|
|
6
|
+
} from "@/lib/a2a/types";
|
|
7
|
+
|
|
8
|
+
function buildActivityEvent(
|
|
9
|
+
id: number,
|
|
10
|
+
input: Omit<A2aActivityEvent, "id">,
|
|
11
|
+
): A2aActivityEvent {
|
|
12
|
+
if (input.kind === "backgroundTask") {
|
|
13
|
+
return {
|
|
14
|
+
id,
|
|
15
|
+
contextId: input.contextId,
|
|
16
|
+
kind: "backgroundTask",
|
|
17
|
+
data: input.data as A2aBackgroundTaskActivity,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (input.kind === "todoUpdate") {
|
|
21
|
+
return {
|
|
22
|
+
id,
|
|
23
|
+
contextId: input.contextId,
|
|
24
|
+
kind: "todoUpdate",
|
|
25
|
+
data: input.data as A2aTodoUpdateActivity,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
id,
|
|
30
|
+
contextId: input.contextId,
|
|
31
|
+
kind: "deliveryMessage",
|
|
32
|
+
data: input.data as A2aDeliveryMessageActivity,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MAX_EVENTS_PER_CONTEXT = 500;
|
|
37
|
+
|
|
38
|
+
export type ActivityListener = (event: A2aActivityEvent) => void;
|
|
39
|
+
|
|
40
|
+
type ActivityStore = {
|
|
41
|
+
nextId: number;
|
|
42
|
+
eventsByContext: Map<string, A2aActivityEvent[]>;
|
|
43
|
+
listenersByContext: Map<string, Set<ActivityListener>>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
declare global {
|
|
47
|
+
var __A2A_ACTIVITY_STORE__: ActivityStore | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getStore(): ActivityStore {
|
|
51
|
+
if (!globalThis.__A2A_ACTIVITY_STORE__) {
|
|
52
|
+
globalThis.__A2A_ACTIVITY_STORE__ = {
|
|
53
|
+
nextId: 1,
|
|
54
|
+
eventsByContext: new Map(),
|
|
55
|
+
listenersByContext: new Map(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return globalThis.__A2A_ACTIVITY_STORE__;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function appendActivityEvent(
|
|
62
|
+
input: Omit<A2aActivityEvent, "id">,
|
|
63
|
+
): A2aActivityEvent {
|
|
64
|
+
const store = getStore();
|
|
65
|
+
const id = store.nextId++;
|
|
66
|
+
const event = buildActivityEvent(id, input);
|
|
67
|
+
|
|
68
|
+
const list = store.eventsByContext.get(input.contextId) ?? [];
|
|
69
|
+
list.push(event);
|
|
70
|
+
if (list.length > MAX_EVENTS_PER_CONTEXT) {
|
|
71
|
+
list.splice(0, list.length - MAX_EVENTS_PER_CONTEXT);
|
|
72
|
+
}
|
|
73
|
+
store.eventsByContext.set(input.contextId, list);
|
|
74
|
+
|
|
75
|
+
const listeners = store.listenersByContext.get(input.contextId);
|
|
76
|
+
if (listeners) {
|
|
77
|
+
for (const listener of listeners) {
|
|
78
|
+
try {
|
|
79
|
+
listener(event);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.warn("Activity listener failed", err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return event;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function appendBackgroundTaskEvent(input: {
|
|
90
|
+
contextId: string;
|
|
91
|
+
data: A2aBackgroundTaskActivity;
|
|
92
|
+
}): A2aActivityEvent {
|
|
93
|
+
return appendActivityEvent({
|
|
94
|
+
contextId: input.contextId,
|
|
95
|
+
kind: "backgroundTask",
|
|
96
|
+
data: input.data,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function appendTodoUpdateEvent(input: {
|
|
101
|
+
contextId: string;
|
|
102
|
+
data: A2aTodoUpdateActivity;
|
|
103
|
+
}): A2aActivityEvent {
|
|
104
|
+
return appendActivityEvent({
|
|
105
|
+
contextId: input.contextId,
|
|
106
|
+
kind: "todoUpdate",
|
|
107
|
+
data: input.data,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function appendDeliveryMessageEvent(input: {
|
|
112
|
+
contextId: string;
|
|
113
|
+
data: A2aDeliveryMessageActivity;
|
|
114
|
+
}): A2aActivityEvent {
|
|
115
|
+
return appendActivityEvent({
|
|
116
|
+
contextId: input.contextId,
|
|
117
|
+
kind: "deliveryMessage",
|
|
118
|
+
data: input.data,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function listActivityEvents(input: {
|
|
123
|
+
contextId: string;
|
|
124
|
+
sinceId?: number;
|
|
125
|
+
}): A2aActivityEvent[] {
|
|
126
|
+
const store = getStore();
|
|
127
|
+
const list = store.eventsByContext.get(input.contextId) ?? [];
|
|
128
|
+
const since = input.sinceId ?? 0;
|
|
129
|
+
if (since <= 0) return list;
|
|
130
|
+
return list.filter((event) => event.id > since);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function subscribeActivityEvents(
|
|
134
|
+
contextId: string,
|
|
135
|
+
listener: ActivityListener,
|
|
136
|
+
): () => void {
|
|
137
|
+
const store = getStore();
|
|
138
|
+
const listeners = store.listenersByContext.get(contextId) ?? new Set<ActivityListener>();
|
|
139
|
+
listeners.add(listener);
|
|
140
|
+
store.listenersByContext.set(contextId, listeners);
|
|
141
|
+
|
|
142
|
+
return () => {
|
|
143
|
+
const set = store.listenersByContext.get(contextId);
|
|
144
|
+
if (!set) return;
|
|
145
|
+
set.delete(listener);
|
|
146
|
+
if (set.size === 0) {
|
|
147
|
+
store.listenersByContext.delete(contextId);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|