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,172 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type { PendingQuestion } from "@/lib/chat/types";
5
+ import { Check, HelpCircle } from "lucide-react";
6
+
7
+ const OTHER_OPTION_KEY = "__other__";
8
+
9
+ export function AskUserQuestionPanel({
10
+ pendingQuestion,
11
+ onSubmit,
12
+ }: {
13
+ pendingQuestion: PendingQuestion;
14
+ onSubmit: (answers: Record<string, string>) => void;
15
+ }) {
16
+ const [selectedAnswers, setSelectedAnswers] = useState<Record<string, string[]>>({});
17
+ const [customTexts, setCustomTexts] = useState<Record<string, string>>({});
18
+
19
+ const handleSelect = (questionText: string, label: string, multiSelect?: boolean) => {
20
+ setSelectedAnswers((prev) => {
21
+ const current = prev[questionText] || [];
22
+ if (multiSelect) {
23
+ if (current.includes(label)) {
24
+ return { ...prev, [questionText]: current.filter((l) => l !== label) };
25
+ }
26
+ return { ...prev, [questionText]: [...current, label] };
27
+ }
28
+ if (label !== OTHER_OPTION_KEY) {
29
+ setCustomTexts((prev) => ({ ...prev, [questionText]: "" }));
30
+ }
31
+ return { ...prev, [questionText]: [label] };
32
+ });
33
+ };
34
+
35
+ const handleCustomTextChange = (questionText: string, text: string) => {
36
+ setCustomTexts((prev) => ({ ...prev, [questionText]: text }));
37
+ setSelectedAnswers((prev) => {
38
+ const current = prev[questionText] || [];
39
+ if (!current.includes(OTHER_OPTION_KEY)) {
40
+ return { ...prev, [questionText]: [OTHER_OPTION_KEY] };
41
+ }
42
+ return prev;
43
+ });
44
+ };
45
+
46
+ const handleSubmit = () => {
47
+ const answers: Record<string, string> = {};
48
+ for (const q of pendingQuestion.questions) {
49
+ const selected = selectedAnswers[q.question] || [];
50
+ const finalAnswers = selected
51
+ .map((label) => (label === OTHER_OPTION_KEY ? customTexts[q.question] || "" : label))
52
+ .filter(Boolean);
53
+ answers[q.question] = finalAnswers.join(", ");
54
+ }
55
+ onSubmit(answers);
56
+ };
57
+
58
+ const allAnswered = pendingQuestion.questions.every((q) => {
59
+ const selected = selectedAnswers[q.question] || [];
60
+ if (selected.length === 0) return false;
61
+ if (selected.length === 1 && selected[0] === OTHER_OPTION_KEY) {
62
+ return (customTexts[q.question] || "").trim().length > 0;
63
+ }
64
+ return true;
65
+ });
66
+
67
+ return (
68
+ <div className="rounded-xl border-2 border-amber-200 dark:border-amber-800 bg-amber-50/50 dark:bg-amber-950/20 p-4 space-y-4 animate-in slide-in-from-bottom-2">
69
+ <div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
70
+ <HelpCircle className="w-5 h-5" />
71
+ <span className="font-medium">Agent needs your input</span>
72
+ </div>
73
+
74
+ {pendingQuestion.questions.map((q, qIdx) => {
75
+ const isOtherSelected = (selectedAnswers[q.question] || []).includes(OTHER_OPTION_KEY);
76
+
77
+ return (
78
+ <div key={qIdx} className="space-y-2">
79
+ <div className="flex items-center gap-2">
80
+ <span className="text-xs font-medium text-muted-foreground bg-muted px-2 py-0.5 rounded">
81
+ {q.header}
82
+ </span>
83
+ {q.multiSelect && <span className="text-xs text-muted-foreground">(multiple)</span>}
84
+ </div>
85
+ <p className="text-sm text-foreground font-medium">{q.question}</p>
86
+ <div className="grid gap-2">
87
+ {q.options.map((opt, optIdx) => {
88
+ const isSelected = (selectedAnswers[q.question] || []).includes(opt.label);
89
+ return (
90
+ <button
91
+ key={optIdx}
92
+ type="button"
93
+ onClick={() => handleSelect(q.question, opt.label, q.multiSelect)}
94
+ className={`w-full text-left px-3 py-2 rounded-lg border transition-all ${
95
+ isSelected
96
+ ? "border-amber-400 dark:border-amber-600 bg-amber-100 dark:bg-amber-900/40"
97
+ : "border-border hover:border-amber-300 dark:hover:border-amber-700 hover:bg-muted/50"
98
+ }`}
99
+ >
100
+ <div className="flex items-start gap-2">
101
+ <div
102
+ className={`mt-0.5 w-4 h-4 rounded ${
103
+ q.multiSelect ? "rounded" : "rounded-full"
104
+ } border-2 flex items-center justify-center shrink-0 ${
105
+ isSelected ? "border-amber-500 bg-amber-500" : "border-muted-foreground/40"
106
+ }`}
107
+ >
108
+ {isSelected && <Check className="w-3 h-3 text-white" />}
109
+ </div>
110
+ <div className="flex-1 min-w-0">
111
+ <div className="text-sm font-medium text-foreground">{opt.label}</div>
112
+ <div className="text-xs text-muted-foreground">{opt.description}</div>
113
+ </div>
114
+ </div>
115
+ </button>
116
+ );
117
+ })}
118
+
119
+ <button
120
+ type="button"
121
+ onClick={() => handleSelect(q.question, OTHER_OPTION_KEY, q.multiSelect)}
122
+ className={`w-full text-left px-3 py-2 rounded-lg border transition-all ${
123
+ isOtherSelected
124
+ ? "border-amber-400 dark:border-amber-600 bg-amber-100 dark:bg-amber-900/40"
125
+ : "border-border hover:border-amber-300 dark:hover:border-amber-700 hover:bg-muted/50"
126
+ }`}
127
+ >
128
+ <div className="flex items-start gap-2">
129
+ <div
130
+ className={`mt-0.5 w-4 h-4 rounded ${
131
+ q.multiSelect ? "rounded" : "rounded-full"
132
+ } border-2 flex items-center justify-center shrink-0 ${
133
+ isOtherSelected
134
+ ? "border-amber-500 bg-amber-500"
135
+ : "border-muted-foreground/40"
136
+ }`}
137
+ >
138
+ {isOtherSelected && <Check className="w-3 h-3 text-white" />}
139
+ </div>
140
+ <div className="flex-1 min-w-0">
141
+ <div className="text-sm font-medium text-foreground">Other</div>
142
+ <div className="text-xs text-muted-foreground">Enter custom answer</div>
143
+ </div>
144
+ </div>
145
+ </button>
146
+
147
+ {isOtherSelected && (
148
+ <input
149
+ type="text"
150
+ value={customTexts[q.question] || ""}
151
+ onChange={(e) => handleCustomTextChange(q.question, e.target.value)}
152
+ placeholder="Enter your answer..."
153
+ autoFocus
154
+ className="w-full px-3 py-2 rounded-lg border border-amber-300 dark:border-amber-700 bg-background text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-amber-400/50"
155
+ />
156
+ )}
157
+ </div>
158
+ </div>
159
+ );
160
+ })}
161
+
162
+ <button
163
+ type="button"
164
+ onClick={handleSubmit}
165
+ disabled={!allAnswered}
166
+ className="w-full py-2 px-4 rounded-lg bg-amber-500 hover:bg-amber-600 text-white font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed enabled:cursor-pointer"
167
+ >
168
+ Submit
169
+ </button>
170
+ </div>
171
+ );
172
+ }
@@ -0,0 +1,222 @@
1
+ "use client";
2
+
3
+ import type { ChatSession } from "@/lib/state/chat-atoms";
4
+ import {
5
+ Loader2,
6
+ MessageSquare,
7
+ PanelLeftClose,
8
+ PanelLeftOpen,
9
+ Plus,
10
+ Trash2,
11
+ } from "lucide-react";
12
+ import { useEffect, useState } from "react";
13
+
14
+ const DEFAULT_TITLE = "New session";
15
+
16
+ export function SessionSidebar({
17
+ sessions,
18
+ currentSessionId,
19
+ isOpen,
20
+ onToggle,
21
+ onSelect,
22
+ onNew,
23
+ onDelete,
24
+ }: {
25
+ sessions: ChatSession[];
26
+ currentSessionId: string | null;
27
+ isOpen: boolean;
28
+ onToggle: () => void;
29
+ onSelect: (sessionId: string) => void;
30
+ onNew: () => void;
31
+ onDelete: (sessionId: string) => void;
32
+ }) {
33
+ const sortedSessions = [...sessions].sort(
34
+ (a, b) => b.updatedAt - a.updatedAt,
35
+ );
36
+ const [mounted, setMounted] = useState(false);
37
+
38
+ useEffect(() => {
39
+ queueMicrotask(() => setMounted(true));
40
+ }, []);
41
+
42
+ return (
43
+ <aside
44
+ className={`
45
+ relative flex flex-col h-screen border-r border-border/50
46
+ bg-background/95 backdrop-blur-xl supports-backdrop-filter:bg-background/60
47
+ transition-all duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]
48
+ will-change-[width]
49
+ ${isOpen ? "w-[280px]" : "w-[68px]"}
50
+ `}
51
+ >
52
+ {/* Header Section */}
53
+ <div className="flex flex-col shrink-0 pt-2 relative">
54
+ {/* Top Controls */}
55
+ <div className="flex items-center h-12 px-3 mb-2 relative shrink-0">
56
+ {/* Title (Only visible when open) */}
57
+ <div
58
+ className={`
59
+ font-semibold text-sm tracking-tight text-foreground/90 overflow-hidden whitespace-nowrap transition-all duration-300
60
+ ${isOpen ? "w-auto opacity-100 translate-x-0" : "w-0 opacity-0 -translate-x-4 pointer-events-none"}
61
+ `}
62
+ >
63
+ Sessions
64
+ </div>
65
+
66
+ {/* Toggle Button - Absolute positioning for smooth movement from Right to Center */}
67
+ <button
68
+ onClick={onToggle}
69
+ className={`
70
+ absolute flex h-8 w-8 items-center justify-center rounded-lg
71
+ hover:bg-muted/80 text-muted-foreground hover:text-foreground
72
+ transition-all duration-500 ease-[cubic-bezier(0.32,0.72,0,1)]
73
+ ${
74
+ isOpen
75
+ ? "right-3 bg-transparent"
76
+ : "left-1/2 -translate-x-1/2 bg-transparent"
77
+ }
78
+ `}
79
+ aria-label={isOpen ? "Collapse sidebar" : "Expand sidebar"}
80
+ >
81
+ {isOpen ? (
82
+ <PanelLeftClose className="h-4.5 w-4.5" />
83
+ ) : (
84
+ <PanelLeftOpen className="h-4.5 w-4.5" />
85
+ )}
86
+ </button>
87
+ </div>
88
+
89
+ {/* New Chat Button */}
90
+ <div className="px-3 pb-3">
91
+ <button
92
+ onClick={onNew}
93
+ className={`
94
+ group flex items-center
95
+ rounded-xl border transition-all duration-300 active:scale-[0.98] overflow-hidden
96
+ h-10 w-full
97
+ ${
98
+ isOpen
99
+ ? "border-border/50 bg-background shadow-sm hover:shadow-md hover:border-border hover:bg-muted/30"
100
+ : "border-transparent bg-transparent hover:bg-muted/50"
101
+ }
102
+ `}
103
+ title="New Chat"
104
+ >
105
+ {/* Stable Icon Wrapper */}
106
+ <div className="flex w-10 h-full shrink-0 items-center justify-center">
107
+ <Plus
108
+ className={`h-5 w-5 text-primary transition-transform duration-300 group-hover:rotate-90`}
109
+ />
110
+ </div>
111
+
112
+ {/* Text */}
113
+ <span
114
+ className={`
115
+ text-sm font-medium text-foreground/80 whitespace-nowrap overflow-hidden transition-all duration-300
116
+ ${isOpen ? "w-auto opacity-100 pr-4" : "w-0 opacity-0 pr-0"}
117
+ `}
118
+ >
119
+ New Chat
120
+ </span>
121
+ </button>
122
+ </div>
123
+ </div>
124
+
125
+ {/* Divider */}
126
+ <div
127
+ className={`mx-4 h-px bg-border/40 mb-2 transition-opacity duration-300 ${isOpen ? "opacity-100" : "opacity-0"}`}
128
+ />
129
+
130
+ {/* Session List */}
131
+ <div className="flex-1 overflow-y-auto overflow-x-hidden px-3 pb-4 space-y-2 scrollbar-none">
132
+ {mounted &&
133
+ sortedSessions.map((session) => {
134
+ const isActive = session.id === currentSessionId;
135
+ const isRunning =
136
+ session.isStreaming ||
137
+ session.subagentActivities?.some((a) => a.status === "running");
138
+ const title = session.title || DEFAULT_TITLE;
139
+
140
+ return (
141
+ <div
142
+ key={session.id}
143
+ onClick={() => onSelect(session.id)}
144
+ className={`
145
+ group relative flex items-center rounded-lg cursor-pointer
146
+ transition-all duration-200 ease-out border w-full
147
+ h-10
148
+ ${
149
+ isActive
150
+ ? "bg-muted border-border/60 shadow-sm"
151
+ : "border-transparent hover:bg-muted/50 hover:border-border/30"
152
+ }
153
+ `}
154
+ title={!isOpen ? title : undefined}
155
+ >
156
+ {/* Active Indicator */}
157
+ {isActive && (
158
+ <div
159
+ className={`
160
+ absolute left-0 top-1/2 -translate-y-1/2 w-1 h-5 bg-primary rounded-r-full transition-all duration-300
161
+ ${isOpen ? "-ml-3 opacity-0" : "-ml-3 opacity-100"}
162
+ `}
163
+ />
164
+ )}
165
+ {/* Indicator at -ml-3 (12px) aligns with px-3 padding edge perfectly */}
166
+
167
+ {/* Stable Icon Wrapper */}
168
+ <div
169
+ className={`
170
+ flex w-10 h-full shrink-0 items-center justify-center transition-colors duration-300
171
+ `}
172
+ >
173
+ {isRunning ? (
174
+ <Loader2 className="h-4.5 w-4.5 text-blue-500 animate-spin" />
175
+ ) : (
176
+ <MessageSquare
177
+ className={`
178
+ h-4.5 w-4.5 transition-colors
179
+ ${isActive ? "text-foreground" : "text-muted-foreground group-hover:text-foreground/80"}
180
+ `}
181
+ />
182
+ )}
183
+ </div>
184
+
185
+ {/* Text Content */}
186
+ <div
187
+ className={`
188
+ flex flex-col flex-1 min-w-0 transition-opacity duration-200
189
+ ${isOpen ? "opacity-100 pl-1" : "opacity-0 w-0 px-0 absolute pointer-events-none"}
190
+ `}
191
+ >
192
+ <span
193
+ className={`text-sm font-medium truncate leading-tight ${isActive ? "text-foreground" : "text-muted-foreground group-hover:text-foreground"}`}
194
+ >
195
+ {title}
196
+ </span>
197
+ <span className="text-[10px] text-muted-foreground/70 truncate pt-0.5">
198
+ {new Date(session.updatedAt).toLocaleDateString()}
199
+ </span>
200
+ </div>
201
+
202
+ {/* Actions (Expanded Only) */}
203
+ {isOpen && (
204
+ <div className="flex items-center pr-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
205
+ <button
206
+ onClick={(e) => {
207
+ e.stopPropagation();
208
+ onDelete(session.id);
209
+ }}
210
+ className="p-1.5 rounded-md text-muted-foreground hover:text-red-500 hover:bg-red-500/10 transition-colors"
211
+ >
212
+ <Trash2 className="h-3.5 w-3.5" />
213
+ </button>
214
+ </div>
215
+ )}
216
+ </div>
217
+ );
218
+ })}
219
+ </div>
220
+ </aside>
221
+ );
222
+ }
@@ -0,0 +1,248 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import type { SubagentActivity } from "@/lib/chat/types";
5
+ import { Activity, CheckCircle2, ChevronDown, ChevronRight, Circle, Clock, Loader2 } from "lucide-react";
6
+
7
+ function getAgentTypeLabel(type: string) {
8
+ switch (type) {
9
+ case "perceiver":
10
+ return "Perceiver";
11
+ case "creator":
12
+ return "Creator";
13
+ case "crafter":
14
+ return "Crafter";
15
+ case "composer-agent":
16
+ return "Composer";
17
+ case "generalPurpose":
18
+ return "General";
19
+ case "explore":
20
+ return "Explorer";
21
+ case "background":
22
+ return "Background";
23
+ default:
24
+ return type;
25
+ }
26
+ }
27
+
28
+ function formatRunningTime(startedAt: number, now: number) {
29
+ const seconds = Math.floor((now - startedAt) / 1000);
30
+ if (seconds < 60) return `${seconds}s`;
31
+ const minutes = Math.floor(seconds / 60);
32
+ const remainingSeconds = seconds % 60;
33
+ return `${minutes}m ${remainingSeconds}s`;
34
+ }
35
+
36
+ function SubagentActivityItem({
37
+ agent,
38
+ now,
39
+ }: {
40
+ agent: SubagentActivity;
41
+ now: number;
42
+ }) {
43
+ const [expanded, setExpanded] = useState(false);
44
+ const isRunning = agent.status === "running";
45
+ const hasTodos = (agent.todos ?? []).length > 0;
46
+ const hasDetails = agent.prompt || agent.result || agent.description || hasTodos;
47
+ const todoStats = hasTodos
48
+ ? {
49
+ total: agent.todos?.length ?? 0,
50
+ completed: agent.todos?.filter((t) => t.status === "completed").length ?? 0,
51
+ inProgress: agent.todos?.filter((t) => t.status === "in_progress").length ?? 0,
52
+ pending: agent.todos?.filter((t) => t.status === "pending").length ?? 0,
53
+ }
54
+ : null;
55
+
56
+ return (
57
+ <div
58
+ className={`rounded-lg border overflow-hidden ${
59
+ isRunning
60
+ ? "border-violet-200 dark:border-violet-800 bg-violet-50/50 dark:bg-violet-950/20"
61
+ : "border-border bg-muted/30"
62
+ }`}
63
+ >
64
+ <button
65
+ type="button"
66
+ onClick={() => hasDetails && setExpanded((e) => !e)}
67
+ disabled={!hasDetails}
68
+ className={`w-full p-3 text-left ${hasDetails ? "enabled:cursor-pointer hover:bg-muted/50" : ""}`}
69
+ >
70
+ <div className="flex items-center gap-2">
71
+ {isRunning ? (
72
+ <Loader2 className="w-4 h-4 text-violet-500 animate-spin shrink-0" />
73
+ ) : (
74
+ <CheckCircle2 className="w-4 h-4 text-emerald-500 shrink-0" />
75
+ )}
76
+ <span
77
+ className={`text-sm font-medium ${isRunning ? "text-foreground" : "text-muted-foreground"}`}
78
+ >
79
+ {agent.description || getAgentTypeLabel(agent.agentType)}
80
+ </span>
81
+ <div className="flex-1" />
82
+ <span
83
+ className={`text-xs px-1.5 py-0.5 rounded ${
84
+ isRunning
85
+ ? "bg-violet-100 dark:bg-violet-900/40 text-violet-600 dark:text-violet-400"
86
+ : "bg-muted text-muted-foreground"
87
+ }`}
88
+ >
89
+ {getAgentTypeLabel(agent.agentType)}
90
+ </span>
91
+ {agent.isBackground && (
92
+ <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">
93
+ BG
94
+ </span>
95
+ )}
96
+ {hasDetails &&
97
+ (expanded ? (
98
+ <ChevronDown className="w-4 h-4 text-muted-foreground shrink-0" />
99
+ ) : (
100
+ <ChevronRight className="w-4 h-4 text-muted-foreground shrink-0" />
101
+ ))}
102
+ </div>
103
+ <div className="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
104
+ <Clock className="w-3 h-3" />
105
+ <span>
106
+ {isRunning
107
+ ? formatRunningTime(agent.startedAt, now)
108
+ : agent.stoppedAt
109
+ ? formatRunningTime(agent.startedAt, agent.stoppedAt)
110
+ : "—"}
111
+ </span>
112
+ {todoStats && (
113
+ <>
114
+ <span className="opacity-50">·</span>
115
+ <span>{todoStats.completed}/{todoStats.total}</span>
116
+ </>
117
+ )}
118
+ <span className="opacity-50">·</span>
119
+ <span className="font-mono opacity-60">{agent.agentId.slice(0, 8)}</span>
120
+ </div>
121
+ </button>
122
+
123
+ {expanded && hasDetails && (
124
+ <div className="px-3 pb-3 pt-0 border-t border-border/50 space-y-2">
125
+ {hasTodos && (
126
+ <div className="text-xs">
127
+ <div className="font-medium text-muted-foreground mb-1">Todo</div>
128
+ <div className="space-y-1">
129
+ {agent.todos?.map((todo, idx) => (
130
+ <div key={idx} className="flex items-start gap-2">
131
+ {todo.status === "completed" ? (
132
+ <CheckCircle2 className="w-3 h-3 text-emerald-500" />
133
+ ) : todo.status === "in_progress" ? (
134
+ <Clock className="w-3 h-3 text-violet-500" />
135
+ ) : (
136
+ <Circle className="w-3 h-3 text-muted-foreground" />
137
+ )}
138
+ <span
139
+ className={`text-muted-foreground ${
140
+ todo.status === "completed" ? "line-through opacity-70" : ""
141
+ }`}
142
+ >
143
+ {todo.content}
144
+ </span>
145
+ </div>
146
+ ))}
147
+ </div>
148
+ </div>
149
+ )}
150
+ {agent.prompt && (
151
+ <div className="text-xs">
152
+ <div className="font-medium text-muted-foreground mb-1">Prompt</div>
153
+ <div className="text-foreground bg-background/50 rounded p-2 max-h-24 overflow-y-auto whitespace-pre-wrap">
154
+ {agent.prompt.length > 300 ? agent.prompt.slice(0, 300) + "..." : agent.prompt}
155
+ </div>
156
+ </div>
157
+ )}
158
+ {agent.result && (
159
+ <div className="text-xs">
160
+ <div className="font-medium text-muted-foreground mb-1">Result</div>
161
+ <div className="text-foreground bg-background/50 rounded p-2 max-h-32 overflow-y-auto whitespace-pre-wrap">
162
+ {agent.result.length > 500 ? agent.result.slice(0, 500) + "..." : agent.result}
163
+ </div>
164
+ </div>
165
+ )}
166
+ </div>
167
+ )}
168
+ </div>
169
+ );
170
+ }
171
+
172
+ export function SubagentActivitySidebar({
173
+ activities,
174
+ isOpen,
175
+ }: {
176
+ activities: SubagentActivity[];
177
+ isOpen: boolean;
178
+ }) {
179
+ const [now, setNow] = useState(() => Date.now());
180
+ const runningAgents = activities.filter((a) => a.status === "running");
181
+ const stoppedAgents = activities.filter((a) => a.status === "stopped");
182
+ const hasRunning = runningAgents.length > 0;
183
+
184
+ useEffect(() => {
185
+ if (!hasRunning) return;
186
+ const timer = setInterval(() => setNow(Date.now()), 1000);
187
+ return () => clearInterval(timer);
188
+ }, [hasRunning]);
189
+
190
+ if (activities.length === 0) {
191
+ return null;
192
+ }
193
+
194
+ return (
195
+ <div
196
+ className={`fixed right-0 top-0 h-full w-96 bg-background border-l border-border shadow-lg z-10 transform transition-transform duration-300 ${
197
+ isOpen ? "translate-x-0" : "translate-x-full"
198
+ }`}
199
+ >
200
+ <div className="flex flex-col h-full">
201
+ <div className="flex items-center gap-2 h-16 px-6 py-4 border-b border-border bg-background/80 backdrop-blur-md">
202
+ <Activity className="w-5 h-5 text-violet-500" />
203
+ <h2 className="text-sm font-semibold text-foreground">Subagent Activity</h2>
204
+ <div className="flex-1" />
205
+ {hasRunning && (
206
+ <span className="text-xs px-2 py-0.5 rounded-full bg-violet-100 dark:bg-violet-900/40 text-violet-600 dark:text-violet-400">
207
+ {runningAgents.length} running
208
+ </span>
209
+ )}
210
+ </div>
211
+
212
+ <div className="flex-1 overflow-y-auto p-4 space-y-3">
213
+ {runningAgents.length > 0 && (
214
+ <div className="space-y-2">
215
+ <div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
216
+ Running
217
+ </div>
218
+ {runningAgents.map((agent) => (
219
+ <SubagentActivityItem key={agent.agentId} agent={agent} now={now} />
220
+ ))}
221
+ </div>
222
+ )}
223
+
224
+ {stoppedAgents.length > 0 && (
225
+ <div className="space-y-2">
226
+ <div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
227
+ Completed
228
+ </div>
229
+ {stoppedAgents
230
+ .slice(-10)
231
+ .reverse()
232
+ .map((agent) => (
233
+ <SubagentActivityItem key={agent.agentId} agent={agent} now={now} />
234
+ ))}
235
+ </div>
236
+ )}
237
+
238
+ {activities.length === 0 && (
239
+ <div className="text-center py-8 text-muted-foreground">
240
+ <Activity className="w-8 h-8 mx-auto mb-2 opacity-30" />
241
+ <p className="text-sm">No subagent activity</p>
242
+ </div>
243
+ )}
244
+ </div>
245
+ </div>
246
+ </div>
247
+ );
248
+ }