create-claude-code-visualizer 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 (220) hide show
  1. package/index.js +393 -0
  2. package/package.json +31 -0
  3. package/templates/CLAUDE.md +108 -0
  4. package/templates/app/.env.local.example +14 -0
  5. package/templates/app/ecosystem.config.js +29 -0
  6. package/templates/app/next-env.d.ts +6 -0
  7. package/templates/app/next.config.ts +16 -0
  8. package/templates/app/package-lock.json +4581 -0
  9. package/templates/app/package.json +38 -0
  10. package/templates/app/postcss.config.js +5 -0
  11. package/templates/app/src/app/agents/[slug]/chat/loading.tsx +26 -0
  12. package/templates/app/src/app/agents/[slug]/chat/page.tsx +579 -0
  13. package/templates/app/src/app/agents/[slug]/loading.tsx +19 -0
  14. package/templates/app/src/app/agents/page.tsx +8 -0
  15. package/templates/app/src/app/api/agents/[slug]/capabilities/route.ts +11 -0
  16. package/templates/app/src/app/api/agents/[slug]/route.ts +57 -0
  17. package/templates/app/src/app/api/agents/route.ts +28 -0
  18. package/templates/app/src/app/api/ai/generate-agent/route.ts +87 -0
  19. package/templates/app/src/app/api/ai/improve-claude-md/route.ts +78 -0
  20. package/templates/app/src/app/api/ai/suggestions/route.ts +64 -0
  21. package/templates/app/src/app/api/ai/title/route.ts +88 -0
  22. package/templates/app/src/app/api/auth/role/route.ts +17 -0
  23. package/templates/app/src/app/api/commands/[slug]/route.ts +61 -0
  24. package/templates/app/src/app/api/commands/route.ts +6 -0
  25. package/templates/app/src/app/api/governance/costs/route.ts +117 -0
  26. package/templates/app/src/app/api/governance/sessions/route.ts +335 -0
  27. package/templates/app/src/app/api/notifications/route.ts +62 -0
  28. package/templates/app/src/app/api/preferences/route.ts +44 -0
  29. package/templates/app/src/app/api/runs/[id]/approve/route.ts +38 -0
  30. package/templates/app/src/app/api/runs/[id]/events/route.ts +28 -0
  31. package/templates/app/src/app/api/runs/[id]/metadata/route.ts +30 -0
  32. package/templates/app/src/app/api/runs/[id]/route.ts +21 -0
  33. package/templates/app/src/app/api/runs/[id]/start/route.ts +61 -0
  34. package/templates/app/src/app/api/runs/[id]/stop/route.ts +16 -0
  35. package/templates/app/src/app/api/runs/[id]/stream/route.ts +201 -0
  36. package/templates/app/src/app/api/runs/route.ts +95 -0
  37. package/templates/app/src/app/api/schedules/[id]/route.ts +81 -0
  38. package/templates/app/src/app/api/schedules/route.ts +75 -0
  39. package/templates/app/src/app/api/settings/access-logs/route.ts +33 -0
  40. package/templates/app/src/app/api/settings/claude-md/route.ts +44 -0
  41. package/templates/app/src/app/api/settings/env-keys/route.ts +271 -0
  42. package/templates/app/src/app/api/settings/users/route.ts +108 -0
  43. package/templates/app/src/app/api/skills/[slug]/route.ts +43 -0
  44. package/templates/app/src/app/api/skills/route.ts +6 -0
  45. package/templates/app/src/app/api/tools/route.ts +65 -0
  46. package/templates/app/src/app/api/uploads/cleanup/route.ts +29 -0
  47. package/templates/app/src/app/api/uploads/route.ts +77 -0
  48. package/templates/app/src/app/auth/callback/route.ts +19 -0
  49. package/templates/app/src/app/globals.css +115 -0
  50. package/templates/app/src/app/layout.tsx +24 -0
  51. package/templates/app/src/app/loading.tsx +16 -0
  52. package/templates/app/src/app/login/page.tsx +64 -0
  53. package/templates/app/src/app/not-authorized/page.tsx +33 -0
  54. package/templates/app/src/app/runs/page.tsx +55 -0
  55. package/templates/app/src/app/schedules/page.tsx +110 -0
  56. package/templates/app/src/app/settings/page.tsx +1294 -0
  57. package/templates/app/src/app/skills/page.tsx +7 -0
  58. package/templates/app/src/components/agent-card.tsx +58 -0
  59. package/templates/app/src/components/agent-grid.tsx +90 -0
  60. package/templates/app/src/components/auth/auth-context.tsx +79 -0
  61. package/templates/app/src/components/chat-thread.tsx +50 -0
  62. package/templates/app/src/components/chat-view.tsx +670 -0
  63. package/templates/app/src/components/commands-browser.tsx +349 -0
  64. package/templates/app/src/components/create-agent-modal.tsx +388 -0
  65. package/templates/app/src/components/governance-dashboard.tsx +397 -0
  66. package/templates/app/src/components/icons.tsx +401 -0
  67. package/templates/app/src/components/layout/agent-sidebar.tsx +504 -0
  68. package/templates/app/src/components/layout/app-shell.tsx +29 -0
  69. package/templates/app/src/components/layout/nav.tsx +87 -0
  70. package/templates/app/src/components/layout/overview-inner.tsx +14 -0
  71. package/templates/app/src/components/layout/profile-menu.tsx +95 -0
  72. package/templates/app/src/components/layout/sidebar.tsx +30 -0
  73. package/templates/app/src/components/markdown.tsx +57 -0
  74. package/templates/app/src/components/message-bar.tsx +161 -0
  75. package/templates/app/src/components/notifications/notification-bell.tsx +104 -0
  76. package/templates/app/src/components/notifications/notification-panel.tsx +116 -0
  77. package/templates/app/src/components/overview/overview-content.tsx +287 -0
  78. package/templates/app/src/components/overview/overview-context.tsx +88 -0
  79. package/templates/app/src/components/preferences-modal.tsx +112 -0
  80. package/templates/app/src/components/run-form.tsx +73 -0
  81. package/templates/app/src/components/run-history-table.tsx +226 -0
  82. package/templates/app/src/components/run-output.tsx +187 -0
  83. package/templates/app/src/components/schedule-form.tsx +148 -0
  84. package/templates/app/src/components/skills-browser.tsx +338 -0
  85. package/templates/app/src/components/tool-tooltip.tsx +82 -0
  86. package/templates/app/src/hooks/use-sse.ts +115 -0
  87. package/templates/app/src/instrumentation.ts +9 -0
  88. package/templates/app/src/lib/agent-cache.ts +19 -0
  89. package/templates/app/src/lib/agent-runner.ts +411 -0
  90. package/templates/app/src/lib/agents.ts +168 -0
  91. package/templates/app/src/lib/ai.ts +40 -0
  92. package/templates/app/src/lib/approval-store.ts +70 -0
  93. package/templates/app/src/lib/auth-guard.ts +116 -0
  94. package/templates/app/src/lib/capabilities.ts +191 -0
  95. package/templates/app/src/lib/line-diff.ts +96 -0
  96. package/templates/app/src/lib/queue.ts +22 -0
  97. package/templates/app/src/lib/redis.ts +12 -0
  98. package/templates/app/src/lib/role-permissions.ts +166 -0
  99. package/templates/app/src/lib/run-agent.ts +442 -0
  100. package/templates/app/src/lib/supabase-browser.ts +8 -0
  101. package/templates/app/src/lib/supabase-middleware.ts +63 -0
  102. package/templates/app/src/lib/supabase-server.ts +28 -0
  103. package/templates/app/src/lib/supabase.ts +6 -0
  104. package/templates/app/src/lib/tool-descriptions.ts +29 -0
  105. package/templates/app/src/lib/types.ts +73 -0
  106. package/templates/app/src/lib/typewriter-animation.ts +159 -0
  107. package/templates/app/src/middleware.ts +13 -0
  108. package/templates/app/tsconfig.json +21 -0
  109. package/templates/app/uploads/.gitkeep +0 -0
  110. package/templates/app/worker/index.ts +342 -0
  111. package/templates/claude/agents/ai-trends-scout.md +66 -0
  112. package/templates/claude/commands/add-to-todos.md +56 -0
  113. package/templates/claude/commands/check-todos.md +56 -0
  114. package/templates/claude/hooks/auto-approve-safe.sh +34 -0
  115. package/templates/claude/hooks/auto-format.sh +25 -0
  116. package/templates/claude/hooks/block-destructive.sh +32 -0
  117. package/templates/claude/hooks/compaction-preserver.sh +16 -0
  118. package/templates/claude/hooks/notify.sh +26 -0
  119. package/templates/claude/settings.local.json +66 -0
  120. package/templates/claude/skills/frontend-design/SKILL.md +127 -0
  121. package/templates/claude/skills/frontend-design/reference/color-and-contrast.md +132 -0
  122. package/templates/claude/skills/frontend-design/reference/interaction-design.md +123 -0
  123. package/templates/claude/skills/frontend-design/reference/motion-design.md +99 -0
  124. package/templates/claude/skills/frontend-design/reference/responsive-design.md +114 -0
  125. package/templates/claude/skills/frontend-design/reference/spatial-design.md +100 -0
  126. package/templates/claude/skills/frontend-design/reference/typography.md +131 -0
  127. package/templates/claude/skills/frontend-design/reference/ux-writing.md +107 -0
  128. package/templates/claude/skills/gws-admin-reports/SKILL.md +57 -0
  129. package/templates/claude/skills/gws-calendar/SKILL.md +108 -0
  130. package/templates/claude/skills/gws-calendar-agenda/SKILL.md +52 -0
  131. package/templates/claude/skills/gws-calendar-insert/SKILL.md +55 -0
  132. package/templates/claude/skills/gws-chat/SKILL.md +73 -0
  133. package/templates/claude/skills/gws-chat-send/SKILL.md +49 -0
  134. package/templates/claude/skills/gws-classroom/SKILL.md +75 -0
  135. package/templates/claude/skills/gws-docs/SKILL.md +48 -0
  136. package/templates/claude/skills/gws-docs-write/SKILL.md +49 -0
  137. package/templates/claude/skills/gws-drive/SKILL.md +137 -0
  138. package/templates/claude/skills/gws-drive-upload/SKILL.md +52 -0
  139. package/templates/claude/skills/gws-events/SKILL.md +67 -0
  140. package/templates/claude/skills/gws-events-renew/SKILL.md +48 -0
  141. package/templates/claude/skills/gws-events-subscribe/SKILL.md +59 -0
  142. package/templates/claude/skills/gws-forms/SKILL.md +45 -0
  143. package/templates/claude/skills/gws-gmail/SKILL.md +59 -0
  144. package/templates/claude/skills/gws-gmail-forward/SKILL.md +53 -0
  145. package/templates/claude/skills/gws-gmail-reply/SKILL.md +56 -0
  146. package/templates/claude/skills/gws-gmail-reply-all/SKILL.md +60 -0
  147. package/templates/claude/skills/gws-gmail-send/SKILL.md +55 -0
  148. package/templates/claude/skills/gws-gmail-triage/SKILL.md +50 -0
  149. package/templates/claude/skills/gws-gmail-watch/SKILL.md +58 -0
  150. package/templates/claude/skills/gws-keep/SKILL.md +48 -0
  151. package/templates/claude/skills/gws-meet/SKILL.md +51 -0
  152. package/templates/claude/skills/gws-modelarmor/SKILL.md +42 -0
  153. package/templates/claude/skills/gws-modelarmor-create-template/SKILL.md +53 -0
  154. package/templates/claude/skills/gws-modelarmor-sanitize-prompt/SKILL.md +48 -0
  155. package/templates/claude/skills/gws-modelarmor-sanitize-response/SKILL.md +48 -0
  156. package/templates/claude/skills/gws-people/SKILL.md +67 -0
  157. package/templates/claude/skills/gws-shared/SKILL.md +66 -0
  158. package/templates/claude/skills/gws-sheets/SKILL.md +53 -0
  159. package/templates/claude/skills/gws-sheets-append/SKILL.md +51 -0
  160. package/templates/claude/skills/gws-sheets-read/SKILL.md +47 -0
  161. package/templates/claude/skills/gws-slides/SKILL.md +43 -0
  162. package/templates/claude/skills/gws-tasks/SKILL.md +56 -0
  163. package/templates/claude/skills/gws-workflow/SKILL.md +44 -0
  164. package/templates/claude/skills/gws-workflow-email-to-task/SKILL.md +47 -0
  165. package/templates/claude/skills/gws-workflow-file-announce/SKILL.md +50 -0
  166. package/templates/claude/skills/gws-workflow-meeting-prep/SKILL.md +47 -0
  167. package/templates/claude/skills/gws-workflow-standup-report/SKILL.md +46 -0
  168. package/templates/claude/skills/gws-workflow-weekly-digest/SKILL.md +46 -0
  169. package/templates/claude/skills/persona-content-creator/SKILL.md +33 -0
  170. package/templates/claude/skills/persona-customer-support/SKILL.md +34 -0
  171. package/templates/claude/skills/persona-event-coordinator/SKILL.md +35 -0
  172. package/templates/claude/skills/persona-exec-assistant/SKILL.md +35 -0
  173. package/templates/claude/skills/persona-hr-coordinator/SKILL.md +33 -0
  174. package/templates/claude/skills/persona-it-admin/SKILL.md +30 -0
  175. package/templates/claude/skills/persona-project-manager/SKILL.md +35 -0
  176. package/templates/claude/skills/persona-researcher/SKILL.md +33 -0
  177. package/templates/claude/skills/persona-sales-ops/SKILL.md +35 -0
  178. package/templates/claude/skills/persona-team-lead/SKILL.md +36 -0
  179. package/templates/claude/skills/recipe-backup-sheet-as-csv/SKILL.md +25 -0
  180. package/templates/claude/skills/recipe-batch-invite-to-event/SKILL.md +25 -0
  181. package/templates/claude/skills/recipe-block-focus-time/SKILL.md +24 -0
  182. package/templates/claude/skills/recipe-bulk-download-folder/SKILL.md +25 -0
  183. package/templates/claude/skills/recipe-collect-form-responses/SKILL.md +25 -0
  184. package/templates/claude/skills/recipe-compare-sheet-tabs/SKILL.md +25 -0
  185. package/templates/claude/skills/recipe-copy-sheet-for-new-month/SKILL.md +25 -0
  186. package/templates/claude/skills/recipe-create-classroom-course/SKILL.md +25 -0
  187. package/templates/claude/skills/recipe-create-doc-from-template/SKILL.md +29 -0
  188. package/templates/claude/skills/recipe-create-events-from-sheet/SKILL.md +24 -0
  189. package/templates/claude/skills/recipe-create-expense-tracker/SKILL.md +26 -0
  190. package/templates/claude/skills/recipe-create-feedback-form/SKILL.md +25 -0
  191. package/templates/claude/skills/recipe-create-gmail-filter/SKILL.md +26 -0
  192. package/templates/claude/skills/recipe-create-meet-space/SKILL.md +25 -0
  193. package/templates/claude/skills/recipe-create-presentation/SKILL.md +25 -0
  194. package/templates/claude/skills/recipe-create-shared-drive/SKILL.md +25 -0
  195. package/templates/claude/skills/recipe-create-task-list/SKILL.md +26 -0
  196. package/templates/claude/skills/recipe-create-vacation-responder/SKILL.md +25 -0
  197. package/templates/claude/skills/recipe-draft-email-from-doc/SKILL.md +25 -0
  198. package/templates/claude/skills/recipe-email-drive-link/SKILL.md +25 -0
  199. package/templates/claude/skills/recipe-find-free-time/SKILL.md +25 -0
  200. package/templates/claude/skills/recipe-find-large-files/SKILL.md +24 -0
  201. package/templates/claude/skills/recipe-forward-labeled-emails/SKILL.md +27 -0
  202. package/templates/claude/skills/recipe-generate-report-from-sheet/SKILL.md +34 -0
  203. package/templates/claude/skills/recipe-label-and-archive-emails/SKILL.md +25 -0
  204. package/templates/claude/skills/recipe-log-deal-update/SKILL.md +25 -0
  205. package/templates/claude/skills/recipe-organize-drive-folder/SKILL.md +26 -0
  206. package/templates/claude/skills/recipe-plan-weekly-schedule/SKILL.md +26 -0
  207. package/templates/claude/skills/recipe-post-mortem-setup/SKILL.md +25 -0
  208. package/templates/claude/skills/recipe-reschedule-meeting/SKILL.md +25 -0
  209. package/templates/claude/skills/recipe-review-meet-participants/SKILL.md +25 -0
  210. package/templates/claude/skills/recipe-review-overdue-tasks/SKILL.md +25 -0
  211. package/templates/claude/skills/recipe-save-email-attachments/SKILL.md +26 -0
  212. package/templates/claude/skills/recipe-save-email-to-doc/SKILL.md +29 -0
  213. package/templates/claude/skills/recipe-schedule-recurring-event/SKILL.md +24 -0
  214. package/templates/claude/skills/recipe-send-team-announcement/SKILL.md +24 -0
  215. package/templates/claude/skills/recipe-share-doc-and-notify/SKILL.md +25 -0
  216. package/templates/claude/skills/recipe-share-event-materials/SKILL.md +25 -0
  217. package/templates/claude/skills/recipe-share-folder-with-team/SKILL.md +26 -0
  218. package/templates/claude/skills/recipe-sync-contacts-to-sheet/SKILL.md +25 -0
  219. package/templates/claude/skills/recipe-watch-drive-changes/SKILL.md +25 -0
  220. package/templates/mcp.json +12 -0
@@ -0,0 +1,670 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import type { SSEMessage } from "@/hooks/use-sse";
6
+ import type { AgentRun } from "@/lib/types";
7
+ import { Markdown } from "@/components/markdown";
8
+ import { ToolBadge } from "@/components/tool-tooltip";
9
+ import { XIcon, CheckIcon, ToolIcon, ChevronRightIcon } from "@/components/icons";
10
+
11
+ // --- Types ---
12
+
13
+ interface ApprovalData {
14
+ tool_name: string;
15
+ tool_input: Record<string, unknown>;
16
+ tool_use_id: string;
17
+ }
18
+
19
+ interface ChatViewProps {
20
+ conversationRuns: AgentRun[];
21
+ completedRunEvents?: Record<string, SSEMessage[]>;
22
+ messages: SSEMessage[];
23
+ isConnected: boolean;
24
+ isRunning: boolean;
25
+ currentPrompt: string | null;
26
+ onApprove?: (toolUseId: string, approved: boolean) => void;
27
+ loading?: boolean;
28
+ suggestions?: string[];
29
+ onSuggestionClick?: (text: string) => void;
30
+ agentName?: string;
31
+ agentEmoji?: string;
32
+ }
33
+
34
+ interface ToolEntry {
35
+ name: string;
36
+ done: boolean;
37
+ input?: unknown;
38
+ result?: string;
39
+ }
40
+
41
+ type Segment =
42
+ | { kind: "thinking" }
43
+ | { kind: "tools"; tools: ToolEntry[]; startIndex: number }
44
+ | { kind: "text"; text: string }
45
+ | { kind: "approval"; data: ApprovalData }
46
+ | { kind: "error"; message: string };
47
+
48
+ // --- Helpers ---
49
+
50
+ function formatToolName(name: string): string {
51
+ if (!name || name === "unknown") return name;
52
+ if (!name.startsWith("mcp__")) return name;
53
+ const parts = name.replace(/^mcp__/, "").split("__");
54
+ const service = (parts[0] || "").replace(/^claude_ai_/, "");
55
+ const action = (parts[1] || "").replace(/^[^_]+_/, "").replace(/_/g, " ");
56
+ return `${service} / ${action}`;
57
+ }
58
+
59
+ function formatDuration(ms: number): string {
60
+ return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
61
+ }
62
+
63
+ function isCreditError(msg: string): boolean {
64
+ const lower = msg.toLowerCase();
65
+ return lower.includes("credit balance") || lower.includes("insufficient") || lower.includes("billing");
66
+ }
67
+
68
+ function buildSegments(messages: SSEMessage[], handledApprovals: Set<string>): Segment[] {
69
+ const segments: Segment[] = [];
70
+ let currentText = "";
71
+ let currentToolSegment: { kind: "tools"; tools: ToolEntry[]; startIndex: number } | null = null;
72
+ let isThinking = false;
73
+ let creditErrorEmitted = false;
74
+
75
+ const flushText = () => {
76
+ if (currentText) {
77
+ segments.push({ kind: "text", text: currentText });
78
+ currentText = "";
79
+ }
80
+ };
81
+
82
+ const flushTools = () => {
83
+ if (currentToolSegment) {
84
+ segments.push(currentToolSegment);
85
+ currentToolSegment = null;
86
+ }
87
+ };
88
+
89
+ for (let i = 0; i < messages.length; i++) {
90
+ const m = messages[i];
91
+
92
+ if (m.type === "thinking") {
93
+ if (m.active) {
94
+ flushText();
95
+ flushTools();
96
+ if (!isThinking) {
97
+ segments.push({ kind: "thinking" });
98
+ isThinking = true;
99
+ }
100
+ } else {
101
+ isThinking = false;
102
+ }
103
+ continue;
104
+ }
105
+
106
+ if (m.type === "tool_call" && m.status === "start") {
107
+ isThinking = false;
108
+ flushText();
109
+ if (!currentToolSegment) {
110
+ currentToolSegment = { kind: "tools", tools: [], startIndex: segments.length };
111
+ }
112
+ const toolName = (m.name as string) || "unknown";
113
+ currentToolSegment.tools.push({ name: toolName, done: false, input: m.input as unknown });
114
+ continue;
115
+ }
116
+
117
+ if (m.type === "tool_call" && m.status === "end") {
118
+ if (currentToolSegment && currentToolSegment.tools.length > 0) {
119
+ const undone = currentToolSegment.tools.find((t) => !t.done);
120
+ if (undone) {
121
+ undone.done = true;
122
+ if (m.result) undone.result = m.result as string;
123
+ if (!undone.input && m.input) undone.input = m.input as unknown;
124
+ }
125
+ }
126
+ continue;
127
+ }
128
+
129
+ if (m.type === "token" || m.type === "text_chunk") {
130
+ isThinking = false;
131
+ flushTools();
132
+ currentText += m.text as string;
133
+ continue;
134
+ }
135
+
136
+ if (m.type === "approval_required" && !handledApprovals.has(m.tool_use_id as string)) {
137
+ flushText();
138
+ flushTools();
139
+ segments.push({
140
+ kind: "approval",
141
+ data: {
142
+ tool_name: m.tool_name as string,
143
+ tool_input: m.tool_input as Record<string, unknown>,
144
+ tool_use_id: m.tool_use_id as string,
145
+ },
146
+ });
147
+ continue;
148
+ }
149
+
150
+ if (m.type === "error" && !messages.some((d) => d.type === "done" || d.type === "stopped")) {
151
+ const errMsg = m.error as string;
152
+ // Deduplicate credit errors — only show one CreditErrorCard per run
153
+ if (isCreditError(errMsg)) {
154
+ if (creditErrorEmitted) continue;
155
+ creditErrorEmitted = true;
156
+ }
157
+ flushText();
158
+ flushTools();
159
+ segments.push({ kind: "error", message: errMsg });
160
+ continue;
161
+ }
162
+ }
163
+
164
+ flushTools();
165
+ flushText();
166
+ return segments;
167
+ }
168
+
169
+ function countTools(messages: SSEMessage[]): number {
170
+ return messages.filter((m) => m.type === "tool_call" && m.status === "start").length;
171
+ }
172
+
173
+ function getLiveState(messages: SSEMessage[]): "thinking" | "tool" | "streaming" | "idle" {
174
+ for (let i = messages.length - 1; i >= 0; i--) {
175
+ const m = messages[i];
176
+ if (m.type === "done" || m.type === "stopped" || m.type === "error") return "idle";
177
+ if (m.type === "thinking" && m.active) return "thinking";
178
+ if (m.type === "tool_call" && m.status === "start") return "tool";
179
+ if (m.type === "token" || m.type === "text_chunk") return "streaming";
180
+ if (m.type === "tool_call" && m.status === "end") return "idle";
181
+ if (m.type === "thinking" && !m.active) return "idle";
182
+ }
183
+ return "idle";
184
+ }
185
+
186
+ // --- Sub-components ---
187
+
188
+ /** User message — right-aligned, subtle background, no heavy bubble */
189
+ function UserMessage({ children, meta }: { children: React.ReactNode; meta?: React.ReactNode }) {
190
+ return (
191
+ <div className="flex justify-end mb-5">
192
+ <div className="max-w-[75%] min-w-[60px]">
193
+ <div className="rounded-lg bg-[var(--bg-elevated)] px-4 py-2.5">
194
+ <div className="text-[13px] leading-relaxed text-[var(--text-primary)] whitespace-pre-wrap">{children}</div>
195
+ </div>
196
+ {meta && <div className="mt-1 text-right">{meta}</div>}
197
+ </div>
198
+ </div>
199
+ );
200
+ }
201
+
202
+ /** Agent response — left-aligned, no container/bubble, just content */
203
+ function AgentResponse({
204
+ children,
205
+ meta,
206
+ }: {
207
+ children: React.ReactNode;
208
+ live?: boolean;
209
+ meta?: React.ReactNode;
210
+ }) {
211
+ return (
212
+ <div className="mb-5">
213
+ <div className="max-w-[90%]">
214
+ {children}
215
+ </div>
216
+ {meta && <div className="mt-1">{meta}</div>}
217
+ </div>
218
+ );
219
+ }
220
+
221
+ /** Tool result detail panel */
222
+ function ToolResultPopup({ tool, onClose }: { tool: ToolEntry; onClose: () => void }) {
223
+ useEffect(() => {
224
+ const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
225
+ window.addEventListener("keydown", handler);
226
+ return () => window.removeEventListener("keydown", handler);
227
+ }, [onClose]);
228
+
229
+ const hasInput = tool.input != null && Object.keys(tool.input as Record<string, unknown>).length > 0;
230
+ const hasResult = !!tool.result;
231
+
232
+ const formatInput = (input: unknown): string => {
233
+ if (!input) return "";
234
+ if (typeof input === "string") return input;
235
+ try { return JSON.stringify(input, null, 2); } catch { return String(input); }
236
+ };
237
+
238
+ return createPortal(
239
+ <div
240
+ className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/60 backdrop-blur-sm"
241
+ onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
242
+ >
243
+ <div className="relative w-full max-w-2xl max-h-[80vh] mx-4 rounded-lg bg-[#111113] border border-[var(--border-default)] shadow-2xl flex flex-col overflow-hidden">
244
+ {/* Header */}
245
+ <div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border-subtle)]">
246
+ <div className="flex items-center gap-2 min-w-0">
247
+ <ToolIcon size={14} className="text-[var(--text-muted)] shrink-0" />
248
+ <span className="text-[13px] font-mono text-[var(--text-primary)] truncate">{formatToolName(tool.name)}</span>
249
+ </div>
250
+ <button onClick={onClose} className="text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors p-1 -mr-1">
251
+ <XIcon size={14} />
252
+ </button>
253
+ </div>
254
+
255
+ {/* Body */}
256
+ <div className="flex-1 overflow-y-auto p-4 space-y-4">
257
+ {hasInput && (
258
+ <div>
259
+ <div className="text-[10px] font-medium text-[var(--text-muted)] uppercase tracking-wider mb-1.5">Input</div>
260
+ <pre className="rounded-md bg-[var(--bg-base)] border border-[var(--border-subtle)] p-3 text-[12px] text-[var(--text-tertiary)] font-mono whitespace-pre-wrap break-words overflow-x-auto max-h-48 overflow-y-auto">
261
+ {formatInput(tool.input)}
262
+ </pre>
263
+ </div>
264
+ )}
265
+ {hasResult ? (
266
+ <div>
267
+ <div className="text-[10px] font-medium text-[var(--text-muted)] uppercase tracking-wider mb-1.5">Result</div>
268
+ <pre className="rounded-md bg-[var(--bg-base)] border border-[var(--border-subtle)] p-3 text-[12px] text-[var(--text-tertiary)] font-mono whitespace-pre-wrap break-words overflow-x-auto max-h-96 overflow-y-auto">
269
+ {tool.result}
270
+ </pre>
271
+ </div>
272
+ ) : !hasInput ? (
273
+ <p className="text-[12px] text-[var(--text-muted)] italic">No data available for this tool call.</p>
274
+ ) : null}
275
+ </div>
276
+ </div>
277
+ </div>,
278
+ document.body
279
+ );
280
+ }
281
+
282
+ /** Tool call list — compact, inline */
283
+ function ToolSegmentBlock({ tools, isLive }: { tools: ToolEntry[]; isLive: boolean }) {
284
+ const activeTool = tools.find((t) => !t.done);
285
+ const [selectedTool, setSelectedTool] = useState<ToolEntry | null>(null);
286
+
287
+ return (
288
+ <>
289
+ <div className="mb-3 pl-3 border-l-2 border-[var(--border-subtle)] space-y-0.5">
290
+ {tools.map((tool, i) => {
291
+ const hasDetail = !!(tool.done && (tool.result || tool.input));
292
+ return (
293
+ <div
294
+ key={i}
295
+ className={`flex items-center gap-2 py-0.5 ${hasDetail ? "cursor-pointer group/tool" : ""}`}
296
+ onClick={hasDetail ? () => setSelectedTool(tool) : undefined}
297
+ >
298
+ {tool.done ? (
299
+ <CheckIcon size={12} className="text-green-400/60 shrink-0" />
300
+ ) : (
301
+ <span className="relative flex h-3 w-3 shrink-0 items-center justify-center">
302
+ <span className="absolute h-2 w-2 animate-ping rounded-full bg-[var(--accent)] opacity-30" />
303
+ <span className="relative h-1.5 w-1.5 rounded-full bg-[var(--accent)]" />
304
+ </span>
305
+ )}
306
+ <ToolBadge
307
+ name={tool.name}
308
+ className={`text-[12px] font-mono ${hasDetail ? "cursor-pointer" : "cursor-help"} ${tool.done ? "text-[var(--text-muted)] group-hover/tool:text-[var(--text-tertiary)]" : "text-[var(--text-secondary)]"} transition-colors`}
309
+ >
310
+ {formatToolName(tool.name)}
311
+ </ToolBadge>
312
+ {hasDetail && (
313
+ <ChevronRightIcon size={10} className="text-[var(--text-muted)] opacity-0 group-hover/tool:opacity-100 transition-opacity shrink-0" />
314
+ )}
315
+ </div>
316
+ );
317
+ })}
318
+ {isLive && activeTool && (
319
+ <span className="text-[10px] text-[var(--text-muted)] pl-5">Running...</span>
320
+ )}
321
+ </div>
322
+ {selectedTool && <ToolResultPopup tool={selectedTool} onClose={() => setSelectedTool(null)} />}
323
+ </>
324
+ );
325
+ }
326
+
327
+ function ThinkingIndicator() {
328
+ return (
329
+ <div className="mb-3 flex items-center gap-2 pl-3">
330
+ <div className="flex gap-0.5">
331
+ <span className="h-1 w-1 rounded-full bg-[var(--text-muted)] animate-bounce" style={{ animationDelay: "0ms" }} />
332
+ <span className="h-1 w-1 rounded-full bg-[var(--text-muted)] animate-bounce" style={{ animationDelay: "150ms" }} />
333
+ <span className="h-1 w-1 rounded-full bg-[var(--text-muted)] animate-bounce" style={{ animationDelay: "300ms" }} />
334
+ </div>
335
+ <span className="text-[11px] text-[var(--text-muted)]">Thinking</span>
336
+ </div>
337
+ );
338
+ }
339
+
340
+ function RunMeta({ toolCount, costUsd, durationMs }: { toolCount: number; costUsd?: number | null; durationMs?: number | null }) {
341
+ if (toolCount === 0 && costUsd == null && durationMs == null) return null;
342
+ return (
343
+ <div className="flex items-center gap-2 mb-5 text-[10px] text-[var(--text-muted)] tabular-nums">
344
+ {toolCount > 0 && (
345
+ <span className="flex items-center gap-1">
346
+ <ToolIcon size={10} className="opacity-50" />
347
+ {toolCount} tool{toolCount !== 1 ? "s" : ""}
348
+ </span>
349
+ )}
350
+ {costUsd != null && <span>${costUsd.toFixed(4)}</span>}
351
+ {durationMs != null && <span>{formatDuration(durationMs)}</span>}
352
+ </div>
353
+ );
354
+ }
355
+
356
+ function ApprovalCard({ data, onApprove }: { data: ApprovalData; onApprove: (toolUseId: string, approved: boolean) => void }) {
357
+ const [expanded, setExpanded] = useState(false);
358
+
359
+ return (
360
+ <div className="mb-4 rounded-md border border-amber-500/15 bg-amber-500/5 px-4 py-3 space-y-2.5 max-w-[90%]">
361
+ <div className="flex items-center gap-2">
362
+ <span className="relative flex h-2 w-2">
363
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-amber-400 opacity-40" />
364
+ <span className="relative inline-flex h-2 w-2 rounded-full bg-amber-400" />
365
+ </span>
366
+ <span className="text-[12px] text-amber-300/90 font-medium">Approval needed</span>
367
+ </div>
368
+
369
+ <p className="text-[13px] text-[var(--text-secondary)]">
370
+ Run <ToolBadge name={data.tool_name} className="font-mono text-amber-200/90 text-[12px] cursor-help">{formatToolName(data.tool_name)}</ToolBadge>?
371
+ </p>
372
+
373
+ {Object.keys(data.tool_input).length > 0 && (
374
+ <div>
375
+ <button
376
+ onClick={() => setExpanded(!expanded)}
377
+ className="text-[11px] text-[var(--text-muted)] hover:text-[var(--text-tertiary)] transition-colors"
378
+ >
379
+ {expanded ? "Hide" : "Show"} parameters
380
+ </button>
381
+ {expanded && (
382
+ <pre className="mt-1 max-h-32 overflow-auto rounded-md border border-[var(--border-subtle)] bg-[var(--bg-base)] p-2 text-[11px] text-[var(--text-muted)] font-mono">
383
+ {JSON.stringify(data.tool_input, null, 2)}
384
+ </pre>
385
+ )}
386
+ </div>
387
+ )}
388
+
389
+ <div className="flex gap-2">
390
+ <button
391
+ onClick={() => onApprove(data.tool_use_id, true)}
392
+ className="rounded-md bg-green-600/80 px-3 py-1 text-[12px] font-medium text-white transition-colors hover:bg-green-600"
393
+ >
394
+ Allow
395
+ </button>
396
+ <button
397
+ onClick={() => onApprove(data.tool_use_id, false)}
398
+ className="rounded-md border border-[var(--border-default)] px-3 py-1 text-[12px] font-medium text-[var(--text-tertiary)] transition-colors hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
399
+ >
400
+ Deny
401
+ </button>
402
+ </div>
403
+ </div>
404
+ );
405
+ }
406
+
407
+ function CreditErrorCard() {
408
+ return (
409
+ <div className="space-y-2.5">
410
+ <div className="flex items-center gap-2">
411
+ <span className="h-2 w-2 rounded-full bg-red-400" />
412
+ <span className="text-[13px] font-medium text-red-400">API key stuck — credit balance not recognized</span>
413
+ </div>
414
+ <p className="text-[13px] text-[var(--text-secondary)] leading-relaxed">
415
+ Anthropic caches credit balance per API key. Even after credits reload, the old key can stay stuck.
416
+ Generate a new key and paste it in Settings to fix this instantly.
417
+ </p>
418
+ <div className="flex items-center gap-2">
419
+ <a
420
+ href="https://console.anthropic.com/settings/keys"
421
+ target="_blank"
422
+ rel="noopener noreferrer"
423
+ className="inline-flex items-center gap-1.5 rounded-md bg-[var(--accent)] px-3 py-1.5 text-[12px] font-medium text-white transition-colors hover:bg-[var(--accent-hover)]"
424
+ >
425
+ Generate new key
426
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" className="opacity-70">
427
+ <path d="M4.5 2.5H9.5V7.5M9.5 2.5L2.5 9.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
428
+ </svg>
429
+ </a>
430
+ <a
431
+ href="/settings"
432
+ className="inline-flex items-center gap-1.5 rounded-md border border-[var(--border-default)] px-3 py-1.5 text-[12px] font-medium text-[var(--text-tertiary)] transition-colors hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
433
+ >
434
+ Paste in Settings
435
+ </a>
436
+ </div>
437
+ </div>
438
+ );
439
+ }
440
+
441
+ // --- Main Component ---
442
+
443
+ export function ChatView({
444
+ conversationRuns,
445
+ completedRunEvents,
446
+ messages,
447
+ isConnected,
448
+ isRunning,
449
+ currentPrompt,
450
+ onApprove,
451
+ loading,
452
+ suggestions,
453
+ onSuggestionClick,
454
+ agentName,
455
+ agentEmoji,
456
+ }: ChatViewProps) {
457
+ const scrollRef = useRef<HTMLDivElement>(null);
458
+ const [handledApprovals, setHandledApprovals] = useState<Set<string>>(new Set());
459
+
460
+ useEffect(() => {
461
+ if (scrollRef.current) {
462
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
463
+ }
464
+ }, [messages, conversationRuns]);
465
+
466
+ const segments = useMemo(() => buildSegments(messages, handledApprovals), [messages, handledApprovals]);
467
+ const toolCount = useMemo(() => countTools(messages), [messages]);
468
+ const liveState = useMemo(() => getLiveState(messages), [messages]);
469
+ const doneMsg = messages.find((m) => m.type === "done" || m.type === "stopped");
470
+
471
+ const handleApproval = (toolUseId: string, approved: boolean) => {
472
+ setHandledApprovals((prev) => new Set(prev).add(toolUseId));
473
+ onApprove?.(toolUseId, approved);
474
+ };
475
+
476
+ const hasContent = conversationRuns.length > 0 || isRunning || messages.length > 0;
477
+
478
+ return (
479
+ <div ref={scrollRef} className="flex-1 overflow-y-auto">
480
+ <div className="max-w-3xl mx-auto px-4 py-6">
481
+ {/* Loading skeleton */}
482
+ {loading && !hasContent && (
483
+ <div className="space-y-4 animate-pulse pt-2">
484
+ <div className="flex justify-end"><div className="h-8 w-48 rounded-lg bg-[var(--bg-elevated)]" /></div>
485
+ <div className="h-16 w-72 rounded-lg bg-[var(--bg-raised)]" />
486
+ <div className="flex justify-end"><div className="h-8 w-36 rounded-lg bg-[var(--bg-elevated)]" /></div>
487
+ <div className="h-12 w-64 rounded-lg bg-[var(--bg-raised)]" />
488
+ </div>
489
+ )}
490
+
491
+ {/* Empty state */}
492
+ {!loading && !hasContent && (
493
+ <div className="flex flex-col items-center justify-center h-[50vh] gap-4">
494
+ {agentEmoji && <span className="text-3xl">{agentEmoji}</span>}
495
+ <div className="text-center">
496
+ <h2 className="text-[15px] font-semibold text-[var(--text-primary)]">
497
+ {agentName ? `What can I help with?` : "Start a conversation"}
498
+ </h2>
499
+ {agentName && (
500
+ <p className="text-[12px] text-[var(--text-muted)] mt-1">
501
+ Ask {agentName} anything to get started
502
+ </p>
503
+ )}
504
+ </div>
505
+ {suggestions && suggestions.length > 0 && (
506
+ <div className="flex flex-col gap-1.5 w-full max-w-sm mt-2">
507
+ {suggestions.slice(0, 3).map((s, i) => (
508
+ <button
509
+ key={i}
510
+ onClick={() => onSuggestionClick?.(s)}
511
+ className="text-left px-3 py-2.5 rounded-md border border-[var(--border-subtle)] text-[13px] text-[var(--text-tertiary)] leading-snug transition-colors hover:border-[var(--border-default)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
512
+ >
513
+ {s}
514
+ </button>
515
+ ))}
516
+ </div>
517
+ )}
518
+ </div>
519
+ )}
520
+
521
+ {/* Completed conversation turns */}
522
+ {conversationRuns.map((run) => {
523
+ const runEvents = completedRunEvents?.[run.id];
524
+ const meta = run.metadata as Record<string, unknown> | undefined;
525
+ const storedToolCount = meta?.tool_count as number | undefined;
526
+
527
+ if (runEvents && runEvents.length > 0) {
528
+ const completedSegments = buildSegments(runEvents, handledApprovals);
529
+ const completedToolCount = runEvents.filter((m) => m.type === "tool_call" && m.status === "start").length;
530
+
531
+ return (
532
+ <div key={run.id}>
533
+ <UserMessage>{run.prompt}</UserMessage>
534
+ {completedSegments.map((seg, i) => {
535
+ switch (seg.kind) {
536
+ case "thinking":
537
+ return <div key={i} className="mb-2"><span className="text-[11px] text-[var(--text-muted)] italic">Thought about the request</span></div>;
538
+ case "tools":
539
+ return <ToolSegmentBlock key={`tools-${i}`} tools={seg.tools} isLive={false} />;
540
+ case "text":
541
+ return (
542
+ <AgentResponse key={`text-${i}`}>
543
+ <div className="text-[13px] leading-relaxed text-[var(--text-secondary)] break-words">
544
+ <Markdown>{seg.text}</Markdown>
545
+ </div>
546
+ </AgentResponse>
547
+ );
548
+ case "error":
549
+ return (
550
+ <AgentResponse key={`error-${i}`}>
551
+ {isCreditError(seg.message) ? <CreditErrorCard /> : <p className="text-[13px] text-red-400">{seg.message}</p>}
552
+ </AgentResponse>
553
+ );
554
+ default:
555
+ return null;
556
+ }
557
+ })}
558
+ {run.status === "completed" && (
559
+ <RunMeta toolCount={completedToolCount} costUsd={run.cost_usd} durationMs={run.duration_ms} />
560
+ )}
561
+ </div>
562
+ );
563
+ }
564
+
565
+ // Fallback: no events stored
566
+ const storedTools = meta?.tools_used as string[] | undefined;
567
+ return (
568
+ <div key={run.id}>
569
+ <UserMessage>{run.prompt}</UserMessage>
570
+ {storedTools && storedTools.length > 0 ? (
571
+ <ToolSegmentBlock tools={storedTools.map((t) => ({ name: t, done: true }))} isLive={false} />
572
+ ) : storedToolCount != null && storedToolCount > 0 ? (
573
+ <RunMeta toolCount={storedToolCount} />
574
+ ) : null}
575
+ <AgentResponse>
576
+ {run.status === "completed" && run.output ? (
577
+ <div className="text-[13px] leading-relaxed text-[var(--text-secondary)] break-words">
578
+ <Markdown>{run.output}</Markdown>
579
+ </div>
580
+ ) : run.status === "failed" ? (
581
+ <p className="text-[13px] text-red-400">Failed{run.error ? `: ${run.error}` : ""}</p>
582
+ ) : run.status === "stopped" ? (
583
+ <p className="text-[13px] text-amber-400/70 italic">Stopped</p>
584
+ ) : (
585
+ <p className="text-[13px] text-[var(--text-muted)] italic">{run.status}</p>
586
+ )}
587
+ </AgentResponse>
588
+ {run.status === "completed" && (
589
+ <RunMeta toolCount={storedToolCount || 0} costUsd={run.cost_usd} durationMs={run.duration_ms} />
590
+ )}
591
+ </div>
592
+ );
593
+ })}
594
+
595
+ {/* Live: user prompt */}
596
+ {currentPrompt && (isRunning || messages.length > 0) && (
597
+ <UserMessage>{currentPrompt}</UserMessage>
598
+ )}
599
+
600
+ {/* Live: starting state */}
601
+ {(isRunning || messages.length > 0) && segments.length === 0 && isRunning && (
602
+ <div className="mb-3 flex items-center gap-2">
603
+ <span className="relative flex h-2 w-2">
604
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--text-muted)] opacity-40" />
605
+ <span className="relative inline-flex h-2 w-2 rounded-full bg-[var(--text-muted)]" />
606
+ </span>
607
+ <span className="text-[11px] text-[var(--text-muted)]">Starting...</span>
608
+ </div>
609
+ )}
610
+
611
+ {/* Live: segments */}
612
+ {segments.map((seg, i) => {
613
+ const isLast = i === segments.length - 1;
614
+
615
+ switch (seg.kind) {
616
+ case "thinking":
617
+ return isLast && liveState === "thinking" ? (
618
+ <ThinkingIndicator key={i} />
619
+ ) : (
620
+ <div key={i} className="mb-2"><span className="text-[11px] text-[var(--text-muted)] italic">Thought about the request</span></div>
621
+ );
622
+
623
+ case "tools":
624
+ return (
625
+ <ToolSegmentBlock
626
+ key={`tools-${i}`}
627
+ tools={seg.tools}
628
+ isLive={isLast && (liveState === "tool" || liveState === "idle") && isRunning && !doneMsg}
629
+ />
630
+ );
631
+
632
+ case "text":
633
+ return (
634
+ <AgentResponse key={`text-${i}`} live={isLast && isRunning && !doneMsg}>
635
+ <div className="text-[13px] leading-relaxed text-[var(--text-secondary)] break-words">
636
+ <Markdown>{seg.text}</Markdown>
637
+ {isLast && isRunning && !doneMsg && liveState === "streaming" && (
638
+ <span className="inline-block w-[2px] h-[13px] bg-[var(--accent)] ml-0.5 animate-pulse align-text-bottom" />
639
+ )}
640
+ </div>
641
+ </AgentResponse>
642
+ );
643
+
644
+ case "approval":
645
+ return <ApprovalCard key={`approval-${i}`} data={seg.data} onApprove={handleApproval} />;
646
+
647
+ case "error":
648
+ return (
649
+ <AgentResponse key={`error-${i}`}>
650
+ {isCreditError(seg.message) ? <CreditErrorCard /> : <p className="text-[13px] text-red-400">{seg.message}</p>}
651
+ </AgentResponse>
652
+ );
653
+
654
+ default:
655
+ return null;
656
+ }
657
+ })}
658
+
659
+ {/* Completion meta */}
660
+ {doneMsg && (
661
+ <RunMeta
662
+ toolCount={toolCount}
663
+ costUsd={doneMsg.cost_usd as number | undefined}
664
+ durationMs={doneMsg.duration_ms as number | undefined}
665
+ />
666
+ )}
667
+ </div>
668
+ </div>
669
+ );
670
+ }