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,38 @@
1
+ {
2
+ "name": "personal-assistant-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev --port 3000",
7
+ "dev:worker": "tsx watch worker/index.ts",
8
+ "dev:all": "concurrently \"npm run dev\" \"npm run dev:worker\"",
9
+ "build": "next build",
10
+ "start": "next start",
11
+ "start:worker": "tsx worker/index.ts"
12
+ },
13
+ "dependencies": {
14
+ "@anthropic-ai/claude-agent-sdk": "^0.2",
15
+ "@supabase/ssr": "^0.9.0",
16
+ "@supabase/supabase-js": "^2",
17
+ "bullmq": "^5",
18
+ "cronstrue": "^2",
19
+ "dotenv": "^16",
20
+ "gray-matter": "^4",
21
+ "next": "^15",
22
+ "react": "^19",
23
+ "react-dom": "^19",
24
+ "react-markdown": "^10.1.0",
25
+ "remark-gfm": "^4.0.1"
26
+ },
27
+ "devDependencies": {
28
+ "@tailwindcss/postcss": "^4",
29
+ "@types/node": "^22",
30
+ "@types/react": "^19",
31
+ "@types/react-dom": "^19.2.3",
32
+ "concurrently": "^9",
33
+ "postcss": "^8",
34
+ "tailwindcss": "^4",
35
+ "tsx": "^4",
36
+ "typescript": "^5"
37
+ }
38
+ }
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
@@ -0,0 +1,26 @@
1
+ export default function AgentLoading() {
2
+ return (
3
+ <div className="flex flex-col h-[calc(100vh-3rem)] -m-6 animate-pulse">
4
+ {/* Header skeleton */}
5
+ <div className="flex items-center gap-2.5 px-5 py-3 border-b border-[var(--border-subtle)]">
6
+ <div className="h-5 w-5 rounded bg-[var(--bg-elevated)]" />
7
+ <div className="space-y-1.5">
8
+ <div className="h-3.5 w-40 rounded bg-[var(--bg-elevated)]" />
9
+ <div className="h-3 w-64 rounded bg-[var(--bg-raised)]" />
10
+ </div>
11
+ </div>
12
+
13
+ {/* Chat area skeleton */}
14
+ <div className="flex-1 flex flex-col items-center justify-center gap-3">
15
+ <div className="h-3 w-32 rounded bg-[var(--bg-raised)]" />
16
+ </div>
17
+
18
+ {/* Message bar skeleton */}
19
+ <div className="border-t border-[var(--border-subtle)] px-4 py-3">
20
+ <div className="max-w-2xl mx-auto">
21
+ <div className="h-10 rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-raised)]" />
22
+ </div>
23
+ </div>
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,579 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback, useRef, use } from "react";
4
+ import { useSearchParams, useRouter } from "next/navigation";
5
+ import { ChatView } from "@/components/chat-view";
6
+ import { MessageBar } from "@/components/message-bar";
7
+ import { useSSE, type SSEMessage } from "@/hooks/use-sse";
8
+ import { getCachedAgent } from "@/lib/agent-cache";
9
+ import type { AgentMeta, AgentRun } from "@/lib/types";
10
+
11
+ export default function AgentDetailPage({ params }: { params: Promise<{ slug: string }> }) {
12
+ const { slug } = use(params);
13
+ const searchParams = useSearchParams();
14
+ const router = useRouter();
15
+
16
+ const cachedAgent = getCachedAgent(slug);
17
+ const [agent, setAgent] = useState<AgentMeta | null>(cachedAgent || null);
18
+ const [allRuns, setAllRuns] = useState<AgentRun[]>([]);
19
+ const [isRunning, setIsRunning] = useState(false);
20
+ const { messages, isConnected, connect, disconnect, reset, loadHistory } = useSSE();
21
+
22
+ // Conversation state
23
+ const [sessionId, setSessionId] = useState<string | null>(null);
24
+ const [conversationRuns, setConversationRuns] = useState<AgentRun[]>([]);
25
+ const [currentPrompt, setCurrentPrompt] = useState<string | null>(null);
26
+ const [completedRunEvents, setCompletedRunEvents] = useState<Record<string, SSEMessage[]>>({});
27
+
28
+ // Prompt suggestions for empty chat
29
+ const [suggestions, setSuggestions] = useState<string[]>([]);
30
+
31
+ // Track current run ID for approval POSTs
32
+ const currentRunIdRef = useRef<string | null>(null);
33
+
34
+ // Track whether title has been generated for this session
35
+ const titleGeneratedRef = useRef<Set<string>>(new Set());
36
+
37
+ // Guard: skip session-param effect when URL was just updated by handleSend
38
+ const justSentRef = useRef<string | null>(null);
39
+
40
+ // Track which session we're currently viewing to avoid redundant loads
41
+ const activeSessionRef = useRef<string | null>(null);
42
+
43
+ // True while loadSession is fetching data
44
+ const [sessionLoading, setSessionLoading] = useState(false);
45
+
46
+ // Track if initial session load has been triggered (prevents double-fire)
47
+ const initialLoadRef = useRef(false);
48
+
49
+ // Mark notifications as read for the currently viewed session
50
+ const markSessionNotificationsRead = useCallback((sid: string) => {
51
+ fetch("/api/notifications", {
52
+ method: "PATCH",
53
+ headers: { "Content-Type": "application/json" },
54
+ body: JSON.stringify({ session_id: sid }),
55
+ }).catch(() => {});
56
+ }, []);
57
+
58
+ // --- Data loading ---
59
+
60
+ useEffect(() => {
61
+ if (!agent) {
62
+ fetch("/api/agents")
63
+ .then((r) => r.json())
64
+ .then((agents: AgentMeta[]) => {
65
+ setAgent(agents.find((a) => a.slug === slug) || null);
66
+ });
67
+ }
68
+
69
+ // Background fetch for sidebar context — does NOT block session loading
70
+ fetch(`/api/runs?agent_slug=${slug}&limit=50`)
71
+ .then((r) => r.json())
72
+ .then((runs) => {
73
+ setAllRuns(runs);
74
+ });
75
+ // eslint-disable-next-line react-hooks/exhaustive-deps
76
+ }, [slug]);
77
+
78
+ // --- Fetch prompt suggestions when agent is known ---
79
+
80
+ useEffect(() => {
81
+ if (!agent) return;
82
+
83
+ // Static fallbacks so empty state always has suggestions, even if the API is down
84
+ const fallbacks = [
85
+ `What can you help me with?`,
86
+ `Summarize your capabilities`,
87
+ `What should I know before we start?`,
88
+ ];
89
+
90
+ const fetchSuggestions = () => {
91
+ const p = new URLSearchParams({
92
+ slug: agent.slug,
93
+ name: agent.name,
94
+ description: agent.description,
95
+ });
96
+ fetch(`/api/ai/suggestions?${p}`)
97
+ .then((r) => r.json())
98
+ .then((d) => {
99
+ if (d.suggestions) setSuggestions(d.suggestions);
100
+ })
101
+ .catch(() => {});
102
+ };
103
+
104
+ // Set fallbacks immediately, then try to fetch better ones
105
+ setSuggestions(fallbacks);
106
+ fetchSuggestions();
107
+
108
+ // Retry once after 10s in case the API was temporarily unavailable (e.g. credit propagation delay)
109
+ const retryTimer = setTimeout(() => {
110
+ // Only retry if we still have fallbacks (AI suggestions didn't arrive)
111
+ setSuggestions((prev) => {
112
+ if (prev === fallbacks || prev.length === 0) {
113
+ fetchSuggestions();
114
+ }
115
+ return prev;
116
+ });
117
+ }, 10_000);
118
+
119
+ return () => clearTimeout(retryTimer);
120
+ }, [agent?.slug, agent?.name, agent?.description]); // eslint-disable-line react-hooks/exhaustive-deps
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // loadSession — the ONE function that handles all session navigation.
124
+ // Renders progressively: shows conversation structure first, then loads events.
125
+ // ---------------------------------------------------------------------------
126
+
127
+ const loadSession = useCallback(
128
+ async (newSessionId: string | null) => {
129
+ // 1. Immediately clear previous state and show loading
130
+ disconnect();
131
+ reset();
132
+ setIsRunning(false);
133
+ setCurrentPrompt(null);
134
+ setConversationRuns([]);
135
+ setCompletedRunEvents({});
136
+ currentRunIdRef.current = null;
137
+ activeSessionRef.current = newSessionId;
138
+
139
+ if (!newSessionId) {
140
+ setSessionId(null);
141
+ setSessionLoading(false);
142
+ return;
143
+ }
144
+
145
+ setSessionId(newSessionId);
146
+ setSessionLoading(true);
147
+
148
+ // Dismiss any notifications for this session since user is viewing it
149
+ markSessionNotificationsRead(newSessionId);
150
+
151
+ try {
152
+ // 2. Fetch runs — single fetch, no duplication
153
+ let freshRuns: AgentRun[];
154
+ try {
155
+ const res = await fetch(`/api/runs?agent_slug=${slug}&limit=50`);
156
+ freshRuns = await res.json();
157
+ setAllRuns(freshRuns);
158
+ } catch {
159
+ return;
160
+ }
161
+
162
+ if (activeSessionRef.current !== newSessionId) return;
163
+
164
+ const sessionRuns = freshRuns
165
+ .filter((r) => r.session_id === newSessionId)
166
+ .sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
167
+
168
+ if (sessionRuns.length === 0) {
169
+ setConversationRuns([]);
170
+ return;
171
+ }
172
+
173
+ const latestRun = sessionRuns[sessionRuns.length - 1];
174
+
175
+ // 3. IMMEDIATELY show conversation structure (prompts + outputs visible)
176
+ // Events load in background to fill in tool/thinking detail.
177
+ if (
178
+ latestRun.status === "completed" ||
179
+ latestRun.status === "failed" ||
180
+ latestRun.status === "stopped"
181
+ ) {
182
+ setConversationRuns(sessionRuns);
183
+ setSessionLoading(false); // ← Render NOW, don't wait for events
184
+
185
+ // 4. Load events in background (enriches the display with tool details)
186
+ const completedRuns = sessionRuns.filter(
187
+ (r) => r.status === "completed" || r.status === "failed" || r.status === "stopped"
188
+ );
189
+ if (completedRuns.length > 0) {
190
+ const eventsMap: Record<string, SSEMessage[]> = {};
191
+ await Promise.all(
192
+ completedRuns.map(async (run) => {
193
+ try {
194
+ const res = await fetch(`/api/runs/${run.id}/events`);
195
+ if (!res.ok) return;
196
+ const rawEvents = (await res.json()) as Record<string, unknown>[];
197
+ if (rawEvents.length > 0) {
198
+ eventsMap[run.id] = rawEvents.map((evt) => ({
199
+ type: evt.event_type as string,
200
+ seq: evt.seq as number,
201
+ ...(evt.payload as Record<string, unknown>),
202
+ }));
203
+ }
204
+ } catch { /* skip */ }
205
+ })
206
+ );
207
+ if (activeSessionRef.current !== newSessionId) return;
208
+ setCompletedRunEvents(eventsMap);
209
+ }
210
+ return;
211
+ }
212
+
213
+ // 5. Latest run is running/queued → show prior turns, reconnect to live
214
+ const priorRuns = sessionRuns.slice(0, -1);
215
+ setConversationRuns(priorRuns);
216
+ setCurrentPrompt(latestRun.prompt);
217
+ setIsRunning(true);
218
+ currentRunIdRef.current = latestRun.id;
219
+ setSessionLoading(false); // ← Render prior turns NOW
220
+
221
+ // Load historical events from DB
222
+ const { maxSeq, events } = await loadHistory(latestRun.id);
223
+
224
+ if (activeSessionRef.current !== newSessionId) return;
225
+
226
+ const alreadyDone = events.some(
227
+ (e) => e.type === "done" || e.type === "stopped" || (e.type === "error" && !events.some((d) => d.type === "done" || d.type === "stopped"))
228
+ );
229
+
230
+ if (alreadyDone) {
231
+ setIsRunning(false);
232
+ return;
233
+ }
234
+
235
+ connect(`/api/runs/${latestRun.id}/stream`, maxSeq > 0 ? maxSeq : undefined);
236
+
237
+ // Load events for prior completed runs in background
238
+ if (priorRuns.length > 0) {
239
+ const eventsMap: Record<string, SSEMessage[]> = {};
240
+ await Promise.all(
241
+ priorRuns.map(async (run) => {
242
+ try {
243
+ const res = await fetch(`/api/runs/${run.id}/events`);
244
+ if (!res.ok) return;
245
+ const rawEvents = (await res.json()) as Record<string, unknown>[];
246
+ if (rawEvents.length > 0) {
247
+ eventsMap[run.id] = rawEvents.map((evt) => ({
248
+ type: evt.event_type as string,
249
+ seq: evt.seq as number,
250
+ ...(evt.payload as Record<string, unknown>),
251
+ }));
252
+ }
253
+ } catch { /* skip */ }
254
+ })
255
+ );
256
+ if (activeSessionRef.current !== newSessionId) return;
257
+ setCompletedRunEvents(eventsMap);
258
+ }
259
+ } finally {
260
+ setSessionLoading(false);
261
+ }
262
+ },
263
+ [slug, disconnect, reset, loadHistory, connect, markSessionNotificationsRead]
264
+ );
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // Session navigation: when ?session=X changes, load immediately.
268
+ // No longer waits for runsLoaded — loadSession fetches its own data.
269
+ // ---------------------------------------------------------------------------
270
+
271
+ useEffect(() => {
272
+ const sessionParam = searchParams.get("session");
273
+
274
+ // Guard: skip if handleSend just set this URL
275
+ if (sessionParam && justSentRef.current === sessionParam) {
276
+ justSentRef.current = null;
277
+ return;
278
+ }
279
+
280
+ // Don't reload if already viewing this session
281
+ if (sessionParam === sessionId) return;
282
+
283
+ // Prevent double-fire on initial mount
284
+ if (!initialLoadRef.current) {
285
+ initialLoadRef.current = true;
286
+ }
287
+
288
+ loadSession(sessionParam || null);
289
+ // eslint-disable-next-line react-hooks/exhaustive-deps
290
+ }, [searchParams]);
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // Stream completion: when the live stream finishes, update sidebar + metadata
294
+ // ---------------------------------------------------------------------------
295
+
296
+ useEffect(() => {
297
+ const doneMsg = messages.find(
298
+ (m) => m.type === "done" || m.type === "stopped"
299
+ );
300
+ const statusMsg = messages.find(
301
+ (m) =>
302
+ m.type === "status" &&
303
+ (m.status === "completed" || m.status === "failed" || m.status === "stopped")
304
+ );
305
+ const errorMsg = messages.find(
306
+ (m) =>
307
+ m.type === "error" &&
308
+ !messages.some((d) => d.type === "done" || d.type === "stopped")
309
+ );
310
+ const finished = doneMsg || statusMsg || errorMsg;
311
+
312
+ if (finished && !isConnected) {
313
+ setIsRunning(false);
314
+
315
+ if (sessionId) {
316
+ // User is watching this chat live — dismiss any notifications for it
317
+ markSessionNotificationsRead(sessionId);
318
+
319
+ fetch(`/api/runs?agent_slug=${slug}&limit=50`)
320
+ .then((r) => r.json())
321
+ .then((fetchedRuns: AgentRun[]) => {
322
+ setAllRuns(fetchedRuns);
323
+ window.dispatchEvent(new Event("agent-session-updated"));
324
+ });
325
+
326
+ if (!titleGeneratedRef.current.has(sessionId) && currentPrompt) {
327
+ titleGeneratedRef.current.add(sessionId);
328
+ fetch("/api/ai/title", {
329
+ method: "POST",
330
+ headers: { "Content-Type": "application/json" },
331
+ body: JSON.stringify({ session_id: sessionId, message: currentPrompt }),
332
+ })
333
+ .then((r) => r.json())
334
+ .then((d) => {
335
+ if (d.title) {
336
+ window.dispatchEvent(
337
+ new CustomEvent("session-title-ready", {
338
+ detail: { session_id: sessionId, title: d.title },
339
+ })
340
+ );
341
+ }
342
+ })
343
+ .catch((err) => console.error("[title] generation failed:", err));
344
+ }
345
+ }
346
+
347
+ // Save tool details to run metadata
348
+ const runId = currentRunIdRef.current;
349
+ const toolCalls = messages.filter(
350
+ (m) => m.type === "tool_call" && m.status === "start"
351
+ );
352
+ if (runId && toolCalls.length > 0) {
353
+ const toolNames = toolCalls.map((m) => m.name as string);
354
+ fetch(`/api/runs/${runId}/metadata`, {
355
+ method: "PATCH",
356
+ headers: { "Content-Type": "application/json" },
357
+ body: JSON.stringify({
358
+ tool_count: toolCalls.length,
359
+ tools_used: toolNames,
360
+ }),
361
+ }).catch(() => {});
362
+ }
363
+ }
364
+ // eslint-disable-next-line react-hooks/exhaustive-deps
365
+ }, [messages, isConnected]);
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // Send message
369
+ // ---------------------------------------------------------------------------
370
+
371
+ const handleSend = useCallback(
372
+ async (prompt: string, files?: File[]) => {
373
+ if (!agent) return;
374
+
375
+ // Before clearing live state, snapshot previous turns into conversationRuns
376
+ if (sessionId && allRuns.length > 0) {
377
+ const convRuns = allRuns
378
+ .filter((r) => r.session_id === sessionId)
379
+ .sort(
380
+ (a, b) =>
381
+ new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
382
+ );
383
+ setConversationRuns(convRuns);
384
+ }
385
+
386
+ // Generate session ID for new conversations so sidebar shows immediately
387
+ const isNewSession = !sessionId;
388
+ const sid = sessionId || crypto.randomUUID();
389
+ if (isNewSession) {
390
+ justSentRef.current = sid;
391
+ setSessionId(sid);
392
+ activeSessionRef.current = sid;
393
+ router.replace(`/agents/${slug}/chat?session=${sid}`, { scroll: false });
394
+ window.dispatchEvent(
395
+ new CustomEvent("agent-session-started", {
396
+ detail: { session_id: sid },
397
+ })
398
+ );
399
+
400
+ // Fire-and-forget title generation immediately
401
+ titleGeneratedRef.current.add(sid);
402
+ fetch("/api/ai/title", {
403
+ method: "POST",
404
+ headers: { "Content-Type": "application/json" },
405
+ body: JSON.stringify({ session_id: sid, message: prompt }),
406
+ })
407
+ .then((r) => r.json())
408
+ .then((d) => {
409
+ if (d.title) {
410
+ window.dispatchEvent(
411
+ new CustomEvent("session-title-ready", {
412
+ detail: { session_id: sid, title: d.title },
413
+ })
414
+ );
415
+ }
416
+ })
417
+ .catch((err) => console.error("[title] generation failed:", err));
418
+ }
419
+
420
+ disconnect();
421
+ reset();
422
+ setIsRunning(true);
423
+ setCurrentPrompt(prompt);
424
+
425
+ // 1. Create run record
426
+ const res = await fetch("/api/runs", {
427
+ method: "POST",
428
+ headers: { "Content-Type": "application/json" },
429
+ body: JSON.stringify({
430
+ agent_slug: agent.slug,
431
+ agent_name: agent.name,
432
+ prompt,
433
+ session_id: sid,
434
+ }),
435
+ });
436
+
437
+ if (!res.ok) {
438
+ setIsRunning(false);
439
+ setCurrentPrompt(null);
440
+ return;
441
+ }
442
+
443
+ const run = await res.json();
444
+ currentRunIdRef.current = run.id;
445
+
446
+ // 2. Upload attached files if any
447
+ let filePaths: string[] = [];
448
+ if (files && files.length > 0) {
449
+ const form = new FormData();
450
+ form.append("run_id", run.id);
451
+ for (const f of files) form.append("files", f);
452
+ try {
453
+ const uploadRes = await fetch("/api/uploads", {
454
+ method: "POST",
455
+ body: form,
456
+ });
457
+ if (uploadRes.ok) {
458
+ const { files: saved } = await uploadRes.json();
459
+ filePaths = saved.map((f: { path: string }) => f.path);
460
+ }
461
+ } catch {
462
+ // Continue without files if upload fails
463
+ }
464
+ }
465
+
466
+ // 3. Start detached execution
467
+ await fetch(`/api/runs/${run.id}/start`, {
468
+ method: "POST",
469
+ headers: { "Content-Type": "application/json" },
470
+ body: JSON.stringify(filePaths.length > 0 ? { files: filePaths } : {}),
471
+ });
472
+
473
+ // 4. Observe via SSE
474
+ connect(`/api/runs/${run.id}/stream`);
475
+ },
476
+ [agent, sessionId, allRuns, slug, connect, disconnect, reset, router]
477
+ );
478
+
479
+ const handleStop = useCallback(async () => {
480
+ const runId = currentRunIdRef.current;
481
+ if (runId) {
482
+ fetch(`/api/runs/${runId}/stop`, { method: "POST" }).catch(() => {});
483
+ }
484
+ disconnect();
485
+ setIsRunning(false);
486
+ }, [disconnect]);
487
+
488
+ const handleApprove = useCallback(
489
+ async (toolUseId: string, approved: boolean) => {
490
+ const runId = currentRunIdRef.current;
491
+ if (!runId) return;
492
+
493
+ await fetch(`/api/runs/${runId}/approve`, {
494
+ method: "POST",
495
+ headers: { "Content-Type": "application/json" },
496
+ body: JSON.stringify({ tool_use_id: toolUseId, approved }),
497
+ });
498
+ },
499
+ []
500
+ );
501
+
502
+ const agentReady = !!agent;
503
+ const inConversation =
504
+ sessionId !== null ||
505
+ conversationRuns.length > 0 ||
506
+ isRunning ||
507
+ (currentPrompt !== null && messages.length > 0);
508
+
509
+ return (
510
+ <div className="flex flex-col h-[calc(100vh-3rem)] -m-6">
511
+ {/* Header */}
512
+ <div className="flex items-center justify-between px-5 py-3 border-b border-[var(--border-subtle)]">
513
+ <div className="flex items-center gap-2.5 min-w-0">
514
+ {!agentReady ? (
515
+ <div className="animate-pulse flex items-center gap-2.5">
516
+ <div className="h-5 w-5 rounded bg-[var(--bg-elevated)]" />
517
+ <div className="space-y-1.5">
518
+ <div className="h-3.5 w-40 rounded bg-[var(--bg-elevated)]" />
519
+ <div className="h-3 w-64 rounded bg-[var(--bg-raised)]" />
520
+ </div>
521
+ </div>
522
+ ) : (
523
+ <>
524
+ {agent.emoji && <span className="text-lg">{agent.emoji}</span>}
525
+ <div className="min-w-0">
526
+ <h1 className="text-[13px] font-semibold text-[var(--text-primary)] truncate">
527
+ {agent.name}
528
+ </h1>
529
+ <p className="text-[11px] text-[var(--text-muted)] truncate">
530
+ {agent.description}
531
+ </p>
532
+ </div>
533
+ </>
534
+ )}
535
+ </div>
536
+ <div className="flex items-center gap-2 shrink-0">
537
+ {isConnected && (
538
+ <span className="flex items-center gap-1.5 text-[11px] text-green-500/70">
539
+ <span className="h-1.5 w-1.5 rounded-full bg-green-500/70 animate-pulse" />
540
+ Live
541
+ </span>
542
+ )}
543
+ </div>
544
+ </div>
545
+
546
+ {/* Chat area */}
547
+ <ChatView
548
+ conversationRuns={conversationRuns}
549
+ completedRunEvents={completedRunEvents}
550
+ messages={messages}
551
+ isConnected={isConnected}
552
+ isRunning={isRunning}
553
+ currentPrompt={currentPrompt}
554
+ onApprove={handleApprove}
555
+ loading={sessionLoading}
556
+ suggestions={suggestions}
557
+ onSuggestionClick={(text) => handleSend(text)}
558
+ agentName={agent?.name}
559
+ agentEmoji={agent?.emoji}
560
+ />
561
+
562
+ {/* Message bar */}
563
+ <MessageBar
564
+ key={sessionId || "new"}
565
+ onSend={handleSend}
566
+ onStop={handleStop}
567
+ disabled={isRunning || !agentReady}
568
+ isRunning={isRunning && isConnected}
569
+ placeholder={
570
+ !agentReady
571
+ ? "Loading..."
572
+ : inConversation
573
+ ? "Follow up..."
574
+ : `Message ${agent.name}...`
575
+ }
576
+ />
577
+ </div>
578
+ );
579
+ }
@@ -0,0 +1,19 @@
1
+ export default function AgentLoading() {
2
+ return (
3
+ <div className="max-w-4xl mx-auto py-8 px-2 animate-pulse">
4
+ <div className="h-3 w-16 rounded bg-[var(--bg-raised)] mb-6" />
5
+ <div className="flex items-start gap-4 mb-8">
6
+ <div className="h-8 w-8 rounded-md bg-[var(--bg-elevated)]" />
7
+ <div className="flex-1">
8
+ <div className="h-5 w-48 rounded bg-[var(--bg-elevated)] mb-2" />
9
+ <div className="h-3.5 w-96 rounded bg-[var(--bg-raised)]" />
10
+ </div>
11
+ </div>
12
+ <div className="space-y-2">
13
+ {[...Array(4)].map((_, i) => (
14
+ <div key={i} className="h-10 rounded-md bg-[var(--bg-raised)]" />
15
+ ))}
16
+ </div>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,8 @@
1
+ import { getAgents } from "@/lib/agents";
2
+ import { AgentGrid } from "@/components/agent-grid";
3
+
4
+ export default function AgentsPage() {
5
+ const agents = getAgents();
6
+ const specialists = agents.filter((a) => a.slug !== "main");
7
+ return <AgentGrid agents={specialists} />;
8
+ }
@@ -0,0 +1,11 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getAgentCapabilities } from "@/lib/capabilities";
3
+
4
+ export async function GET(
5
+ _req: Request,
6
+ { params }: { params: Promise<{ slug: string }> }
7
+ ) {
8
+ const { slug } = await params;
9
+ const capabilities = getAgentCapabilities(slug);
10
+ return NextResponse.json(capabilities);
11
+ }