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,95 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { useAuth } from "@/components/auth/auth-context";
6
+ import { PreferencesModal } from "@/components/preferences-modal";
7
+ import { GearIcon, LogOutIcon, ClockIcon } from "@/components/icons";
8
+
9
+ export function ProfileMenu() {
10
+ const [open, setOpen] = useState(false);
11
+ const [prefsOpen, setPrefsOpen] = useState(false);
12
+ const menuRef = useRef<HTMLDivElement>(null);
13
+ const router = useRouter();
14
+ const { user, loading, signOut } = useAuth();
15
+
16
+ useEffect(() => {
17
+ if (!open) return;
18
+ const handle = (e: MouseEvent) => {
19
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
20
+ setOpen(false);
21
+ }
22
+ };
23
+ document.addEventListener("mousedown", handle);
24
+ return () => document.removeEventListener("mousedown", handle);
25
+ }, [open]);
26
+
27
+ const displayName =
28
+ user?.user_metadata?.full_name ||
29
+ user?.email?.split("@")[0] ||
30
+ "User";
31
+ const initials = displayName.charAt(0).toUpperCase();
32
+ const avatarUrl = user?.user_metadata?.avatar_url;
33
+
34
+ if (loading) {
35
+ return (
36
+ <div className="flex items-center gap-2 px-2.5 py-1.5">
37
+ <div className="h-5 w-5 rounded-full bg-[var(--bg-elevated)] animate-pulse" />
38
+ <div className="h-3 w-20 rounded bg-[var(--bg-elevated)] animate-pulse" />
39
+ </div>
40
+ );
41
+ }
42
+
43
+ return (
44
+ <div ref={menuRef} className="relative">
45
+ <button
46
+ onClick={() => setOpen(!open)}
47
+ className="flex items-center gap-2 w-full rounded-md px-2.5 py-1.5 text-[13px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] transition-colors"
48
+ >
49
+ {avatarUrl ? (
50
+ <img
51
+ src={avatarUrl}
52
+ alt=""
53
+ className="h-5 w-5 rounded-full shrink-0"
54
+ referrerPolicy="no-referrer"
55
+ />
56
+ ) : (
57
+ <div className="flex items-center justify-center h-5 w-5 rounded-full bg-[var(--bg-elevated)] text-[10px] font-semibold text-[var(--text-secondary)] shrink-0">
58
+ {initials}
59
+ </div>
60
+ )}
61
+ <span className="truncate">{displayName}</span>
62
+ </button>
63
+
64
+ {open && (
65
+ <div className="absolute bottom-full left-0 mb-1 w-full rounded-md border border-[var(--border-default)] bg-[#111113] shadow-xl shadow-black/40 overflow-hidden z-50">
66
+ <button
67
+ onClick={() => { setOpen(false); setPrefsOpen(true); }}
68
+ className="flex items-center gap-2.5 w-full px-3 py-2 text-[13px] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)] transition-colors"
69
+ >
70
+ <ClockIcon size={14} className="opacity-60" />
71
+ Preferences
72
+ </button>
73
+ <div className="h-px bg-[var(--border-subtle)]" />
74
+ <button
75
+ onClick={() => { setOpen(false); router.push("/settings"); }}
76
+ className="flex items-center gap-2.5 w-full px-3 py-2 text-[13px] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)] transition-colors"
77
+ >
78
+ <GearIcon size={14} className="opacity-60" />
79
+ Settings
80
+ </button>
81
+ <div className="h-px bg-[var(--border-subtle)]" />
82
+ <button
83
+ onClick={() => { setOpen(false); signOut(); }}
84
+ className="flex items-center gap-2.5 w-full px-3 py-2 text-[13px] text-[var(--text-secondary)] hover:text-red-400 hover:bg-[var(--bg-hover)] transition-colors"
85
+ >
86
+ <LogOutIcon size={14} className="opacity-60" />
87
+ Log out
88
+ </button>
89
+ </div>
90
+ )}
91
+
92
+ <PreferencesModal open={prefsOpen} onClose={() => setPrefsOpen(false)} />
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+ import { useOverview } from "@/components/overview/overview-context";
5
+ import { Nav } from "./nav";
6
+ import { AgentSidebar } from "./agent-sidebar";
7
+
8
+ export function Sidebar() {
9
+ const pathname = usePathname();
10
+ const { isOverview } = useOverview();
11
+
12
+ // If the overview context says we're on an overview tab, show Nav
13
+ if (isOverview) {
14
+ return <Nav />;
15
+ }
16
+
17
+ // Root path is rewritten to /agents/main/chat — treat it as main agent
18
+ if (pathname === "/") {
19
+ return <AgentSidebar slug="main" />;
20
+ }
21
+
22
+ // On any agent chat page, show agent session sidebar
23
+ const chatMatch = pathname.match(/^\/agents\/([^/]+)\/chat/);
24
+ if (chatMatch) {
25
+ return <AgentSidebar slug={chatMatch[1]} />;
26
+ }
27
+
28
+ // Fallback
29
+ return <Nav />;
30
+ }
@@ -0,0 +1,57 @@
1
+ import ReactMarkdown from "react-markdown";
2
+ import remarkGfm from "remark-gfm";
3
+
4
+ export function Markdown({ children }: { children: string }) {
5
+ return (
6
+ <ReactMarkdown
7
+ remarkPlugins={[remarkGfm]}
8
+ components={{
9
+ h1: ({ children }) => <h1 className="text-[15px] font-semibold text-[var(--text-primary)] mt-4 mb-2">{children}</h1>,
10
+ h2: ({ children }) => <h2 className="text-[14px] font-semibold text-[var(--text-primary)] mt-4 mb-2">{children}</h2>,
11
+ h3: ({ children }) => <h3 className="text-[13px] font-semibold text-[var(--text-primary)] mt-3 mb-1.5">{children}</h3>,
12
+ h4: ({ children }) => <h4 className="text-[13px] font-medium text-[var(--text-secondary)] mt-2 mb-1">{children}</h4>,
13
+ p: ({ children }) => <p className="mb-2 last:mb-0 leading-relaxed">{children}</p>,
14
+ strong: ({ children }) => <strong className="font-semibold text-[var(--text-primary)]">{children}</strong>,
15
+ em: ({ children }) => <em className="italic text-[var(--text-secondary)]">{children}</em>,
16
+ a: ({ href, children }) => (
17
+ <a href={href} target="_blank" rel="noopener noreferrer" className="text-[var(--accent-text)] hover:underline underline-offset-2">
18
+ {children}
19
+ </a>
20
+ ),
21
+ ul: ({ children }) => <ul className="list-disc list-outside pl-5 mb-2 space-y-0.5">{children}</ul>,
22
+ ol: ({ children }) => <ol className="list-decimal list-outside pl-5 mb-2 space-y-0.5">{children}</ol>,
23
+ li: ({ children }) => <li className="leading-relaxed">{children}</li>,
24
+ code: ({ className, children }) => {
25
+ const isBlock = className?.includes("language-");
26
+ if (isBlock) {
27
+ return (
28
+ <code className="block text-[12px] font-mono text-[var(--text-secondary)]">{children}</code>
29
+ );
30
+ }
31
+ return (
32
+ <code className="rounded-md bg-[var(--bg-elevated)] px-1.5 py-0.5 text-[12px] font-mono text-[var(--text-secondary)]">{children}</code>
33
+ );
34
+ },
35
+ pre: ({ children }) => (
36
+ <pre className="rounded-md bg-[var(--bg-base)] border border-[var(--border-subtle)] p-3 mb-2 overflow-x-auto text-[12px]">
37
+ {children}
38
+ </pre>
39
+ ),
40
+ blockquote: ({ children }) => (
41
+ <blockquote className="border-l-2 border-[var(--border-strong)] pl-3 my-2 text-[var(--text-tertiary)] italic">{children}</blockquote>
42
+ ),
43
+ hr: () => <hr className="border-[var(--border-subtle)] my-3" />,
44
+ table: ({ children }) => (
45
+ <div className="overflow-x-auto mb-2">
46
+ <table className="w-full text-[12px] border-collapse">{children}</table>
47
+ </div>
48
+ ),
49
+ thead: ({ children }) => <thead className="border-b border-[var(--border-default)]">{children}</thead>,
50
+ th: ({ children }) => <th className="text-left px-2 py-1.5 font-medium text-[var(--text-primary)]">{children}</th>,
51
+ td: ({ children }) => <td className="px-2 py-1.5 border-t border-[var(--border-subtle)] text-[var(--text-tertiary)]">{children}</td>,
52
+ }}
53
+ >
54
+ {children}
55
+ </ReactMarkdown>
56
+ );
57
+ }
@@ -0,0 +1,161 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { PaperclipIcon, SendIcon, StopIcon, XIcon, ImageIcon, FileIcon } from "@/components/icons";
5
+
6
+ const ACCEPTED_TYPES = "image/png,image/jpeg,image/gif,image/webp,text/markdown,.md";
7
+
8
+ interface MessageBarProps {
9
+ onSend: (message: string, files?: File[]) => void;
10
+ onStop?: () => void;
11
+ disabled?: boolean;
12
+ isRunning?: boolean;
13
+ placeholder?: string;
14
+ }
15
+
16
+ export function MessageBar({ onSend, onStop, disabled, isRunning, placeholder }: MessageBarProps) {
17
+ const [value, setValue] = useState("");
18
+ const [files, setFiles] = useState<File[]>([]);
19
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
20
+ const fileInputRef = useRef<HTMLInputElement>(null);
21
+
22
+ const handleSubmit = () => {
23
+ const trimmed = value.trim();
24
+ if ((!trimmed && files.length === 0) || disabled) return;
25
+ onSend(trimmed, files.length > 0 ? files : undefined);
26
+ setValue("");
27
+ setFiles([]);
28
+ if (textareaRef.current) {
29
+ textareaRef.current.style.height = "auto";
30
+ }
31
+ };
32
+
33
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
34
+ const selected = Array.from(e.target.files || []);
35
+ setFiles((prev) => [...prev, ...selected]);
36
+ e.target.value = "";
37
+ };
38
+
39
+ const removeFile = (index: number) => {
40
+ setFiles((prev) => prev.filter((_, i) => i !== index));
41
+ };
42
+
43
+ const handleKeyDown = (e: React.KeyboardEvent) => {
44
+ if (e.key === "Enter" && !e.shiftKey) {
45
+ e.preventDefault();
46
+ handleSubmit();
47
+ }
48
+ };
49
+
50
+ // Auto-focus on mount and when a run completes
51
+ useEffect(() => {
52
+ if (!isRunning) {
53
+ textareaRef.current?.focus();
54
+ }
55
+ }, [isRunning]);
56
+
57
+ const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
58
+ setValue(e.target.value);
59
+ const el = e.target;
60
+ el.style.height = "auto";
61
+ el.style.height = Math.min(el.scrollHeight, 200) + "px";
62
+ };
63
+
64
+ const hasContent = value.trim().length > 0 || files.length > 0;
65
+
66
+ return (
67
+ <div className="bg-[var(--bg-base)] px-4 pb-2 pt-1">
68
+ <div className="max-w-3xl mx-auto">
69
+ <div className="rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-raised)] overflow-hidden">
70
+ {/* File chips */}
71
+ {files.length > 0 && (
72
+ <div className="flex flex-wrap gap-1.5 px-4 pt-3 pb-0">
73
+ {files.map((f, i) => {
74
+ const isImage = f.type.startsWith("image/");
75
+ return (
76
+ <span
77
+ key={`${f.name}-${i}`}
78
+ className="flex items-center gap-1.5 rounded-md bg-[var(--bg-elevated)] border border-[var(--border-subtle)] px-2 py-1 text-[12px] text-[var(--text-secondary)]"
79
+ >
80
+ {isImage ? <ImageIcon size={12} className="text-[var(--text-muted)]" /> : <FileIcon size={12} className="text-[var(--text-muted)]" />}
81
+ <span className="truncate max-w-[140px]">{f.name}</span>
82
+ <button
83
+ onClick={() => removeFile(i)}
84
+ className="ml-0.5 text-[var(--text-muted)] hover:text-[var(--text-secondary)] transition-colors"
85
+ type="button"
86
+ >
87
+ <XIcon size={10} />
88
+ </button>
89
+ </span>
90
+ );
91
+ })}
92
+ </div>
93
+ )}
94
+
95
+ {/* Textarea */}
96
+ <div className="flex items-end gap-2 px-4 pt-2.5 pb-1.5">
97
+ <textarea
98
+ ref={textareaRef}
99
+ value={value}
100
+ onChange={handleInput}
101
+ onKeyDown={handleKeyDown}
102
+ placeholder={placeholder || "Message..."}
103
+ disabled={isRunning}
104
+ rows={1}
105
+ className="flex-1 resize-none bg-transparent text-[13px] text-[var(--text-primary)] placeholder-[var(--text-muted)] outline-none focus:outline-none focus-visible:outline-none leading-relaxed max-h-[200px] disabled:opacity-50"
106
+ />
107
+ </div>
108
+
109
+ {/* Bottom row: attach + send */}
110
+ <div className="flex items-center justify-between px-3 pb-2 pt-0">
111
+ <div className="relative">
112
+ <input
113
+ ref={fileInputRef}
114
+ type="file"
115
+ accept={ACCEPTED_TYPES}
116
+ multiple
117
+ onChange={handleFileChange}
118
+ className="hidden"
119
+ />
120
+ <button
121
+ onClick={() => fileInputRef.current?.click()}
122
+ className={`flex items-center justify-center h-7 w-7 rounded-md transition-colors ${
123
+ files.length > 0
124
+ ? "text-[var(--accent-text)] hover:bg-[var(--bg-hover)]"
125
+ : "text-[var(--text-muted)] hover:text-[var(--text-tertiary)] hover:bg-[var(--bg-hover)]"
126
+ }`}
127
+ aria-label="Attach file"
128
+ type="button"
129
+ >
130
+ <PaperclipIcon size={16} />
131
+ </button>
132
+ </div>
133
+
134
+ {isRunning && onStop ? (
135
+ <button
136
+ onClick={onStop}
137
+ className="flex items-center justify-center h-7 w-7 rounded-md transition-all bg-red-500/80 text-white hover:bg-red-500"
138
+ aria-label="Stop"
139
+ >
140
+ <StopIcon size={14} />
141
+ </button>
142
+ ) : (
143
+ <button
144
+ onClick={handleSubmit}
145
+ disabled={!hasContent || disabled}
146
+ className={`flex items-center justify-center h-7 w-7 rounded-md transition-all ${
147
+ hasContent && !disabled
148
+ ? "bg-[var(--accent)] text-white hover:bg-[var(--accent-hover)]"
149
+ : "text-[var(--text-muted)]"
150
+ }`}
151
+ aria-label="Send message"
152
+ >
153
+ <SendIcon size={14} />
154
+ </button>
155
+ )}
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ );
161
+ }
@@ -0,0 +1,104 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useRef, useCallback } from "react"; // useRef kept for prevCountRef
4
+ import type { Notification } from "@/lib/types";
5
+ import { NotificationPanel } from "./notification-panel";
6
+
7
+ export function NotificationBell() {
8
+ const [notifications, setNotifications] = useState<Notification[]>([]);
9
+ const [open, setOpen] = useState(false);
10
+ const [permissionGranted, setPermissionGranted] = useState(false);
11
+ const prevCountRef = useRef(0);
12
+
13
+ const unreadCount = notifications.filter((n) => !n.read).length;
14
+
15
+ const fetchNotifications = useCallback(async () => {
16
+ try {
17
+ const res = await fetch("/api/notifications?unread=true");
18
+ if (!res.ok) return;
19
+ const data: Notification[] = await res.json();
20
+ setNotifications(data);
21
+
22
+ // Fire desktop notification for new unreads
23
+ if (permissionGranted && data.length > prevCountRef.current && prevCountRef.current >= 0) {
24
+ const newest = data[0];
25
+ if (newest) {
26
+ new window.Notification(newest.title, {
27
+ body: newest.summary || undefined,
28
+ icon: "/favicon.ico",
29
+ });
30
+ }
31
+ }
32
+ prevCountRef.current = data.length;
33
+ } catch {
34
+ // silently fail
35
+ }
36
+ }, [permissionGranted]);
37
+
38
+ useEffect(() => {
39
+ // Request browser notification permission
40
+ if (typeof window !== "undefined" && "Notification" in window) {
41
+ if (Notification.permission === "granted") {
42
+ setPermissionGranted(true);
43
+ } else if (Notification.permission !== "denied") {
44
+ Notification.requestPermission().then((p) => setPermissionGranted(p === "granted"));
45
+ }
46
+ }
47
+ }, []);
48
+
49
+ useEffect(() => {
50
+ fetchNotifications();
51
+ const interval = setInterval(fetchNotifications, 30_000);
52
+ return () => clearInterval(interval);
53
+ }, [fetchNotifications]);
54
+
55
+
56
+ const handleMarkAllRead = async () => {
57
+ await fetch("/api/notifications", {
58
+ method: "PATCH",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify({ all: true }),
61
+ });
62
+ setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
63
+ };
64
+
65
+ const handleMarkRead = async (id: string) => {
66
+ await fetch("/api/notifications", {
67
+ method: "PATCH",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({ ids: [id] }),
70
+ });
71
+ setNotifications((prev) =>
72
+ prev.map((n) => (n.id === id ? { ...n, read: true } : n))
73
+ );
74
+ };
75
+
76
+ return (
77
+ <div className="relative">
78
+ <button
79
+ onClick={() => setOpen(!open)}
80
+ className="relative flex items-center justify-center h-6 w-6 rounded-md text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] transition-colors"
81
+ aria-label="Notifications"
82
+ >
83
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
84
+ <path d="M4.5 6.5a3.5 3.5 0 017 0c0 2.5 1 4 1.5 4.5H3c.5-.5 1.5-2 1.5-4.5z" />
85
+ <path d="M6.5 11v.5a1.5 1.5 0 003 0V11" />
86
+ </svg>
87
+ {unreadCount > 0 && (
88
+ <span className="absolute -top-0.5 -right-0.5 flex h-3.5 min-w-[0.875rem] items-center justify-center rounded-full bg-[var(--accent)] px-0.5 text-[9px] font-semibold text-white">
89
+ {unreadCount > 9 ? "9+" : unreadCount}
90
+ </span>
91
+ )}
92
+ </button>
93
+
94
+ {open && (
95
+ <NotificationPanel
96
+ notifications={notifications}
97
+ onMarkAllRead={handleMarkAllRead}
98
+ onMarkRead={handleMarkRead}
99
+ onClose={() => setOpen(false)}
100
+ />
101
+ )}
102
+ </div>
103
+ );
104
+ }
@@ -0,0 +1,116 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import type { Notification } from "@/lib/types";
5
+ import { XIcon } from "@/components/icons";
6
+
7
+ function timeAgo(dateStr: string): string {
8
+ const diff = Date.now() - new Date(dateStr).getTime();
9
+ const mins = Math.floor(diff / 60_000);
10
+ if (mins < 1) return "just now";
11
+ if (mins < 60) return `${mins}m ago`;
12
+ const hours = Math.floor(mins / 60);
13
+ if (hours < 24) return `${hours}h ago`;
14
+ const days = Math.floor(hours / 24);
15
+ return `${days}d ago`;
16
+ }
17
+
18
+ function typeIcon(type: Notification["type"]): { icon: string; color: string } {
19
+ switch (type) {
20
+ case "completed":
21
+ return { icon: "✓", color: "text-green-400" };
22
+ case "failed":
23
+ return { icon: "✕", color: "text-red-400" };
24
+ case "needs_review":
25
+ return { icon: "!", color: "text-yellow-400" };
26
+ case "approval_needed":
27
+ return { icon: "⏳", color: "text-amber-400" };
28
+ }
29
+ }
30
+
31
+ interface NotificationPanelProps {
32
+ notifications: Notification[];
33
+ onMarkAllRead: () => void;
34
+ onMarkRead: (id: string) => void;
35
+ onClose: () => void;
36
+ }
37
+
38
+ export function NotificationPanel({
39
+ notifications,
40
+ onMarkAllRead,
41
+ onMarkRead,
42
+ onClose,
43
+ }: NotificationPanelProps) {
44
+ const router = useRouter();
45
+
46
+ const handleClick = (n: Notification) => {
47
+ onMarkRead(n.id);
48
+ onClose();
49
+ if (n.session_id) {
50
+ router.push(`/agents/${n.agent_slug}/chat?session=${n.session_id}`);
51
+ } else {
52
+ router.push(`/agents/${n.agent_slug}/chat`);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <div className="fixed inset-0 z-50 flex items-center justify-center" onClick={onClose}>
58
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
59
+ <div
60
+ className="relative z-10 w-[420px] max-h-[80vh] rounded-lg border border-[var(--border-default)] bg-[#111113] shadow-2xl shadow-black/60 overflow-hidden"
61
+ onClick={(e) => e.stopPropagation()}
62
+ >
63
+ <div className="flex items-center justify-between px-5 py-4 border-b border-[var(--border-subtle)]">
64
+ <span className="text-[14px] font-semibold text-[var(--text-primary)]">Notifications</span>
65
+ <div className="flex items-center gap-3">
66
+ {notifications.some((n) => !n.read) && (
67
+ <button onClick={onMarkAllRead} className="text-[11px] text-[var(--accent-text)] hover:underline">
68
+ Mark all read
69
+ </button>
70
+ )}
71
+ <button onClick={onClose} className="flex items-center justify-center h-6 w-6 rounded-md text-[var(--text-muted)] transition-colors hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]">
72
+ <XIcon size={14} />
73
+ </button>
74
+ </div>
75
+ </div>
76
+
77
+ <div className="overflow-y-auto max-h-[calc(80vh-60px)]">
78
+ {notifications.length === 0 ? (
79
+ <p className="py-12 text-center text-[13px] text-[var(--text-tertiary)]">No notifications</p>
80
+ ) : (
81
+ notifications.map((n) => {
82
+ const { icon, color } = typeIcon(n.type);
83
+ return (
84
+ <button
85
+ key={n.id}
86
+ onClick={() => handleClick(n)}
87
+ className={`flex w-full items-start gap-3 px-5 py-3 text-left transition-colors hover:bg-[var(--bg-hover)] ${
88
+ !n.read ? "bg-[var(--bg-raised)]" : ""
89
+ }`}
90
+ >
91
+ <span className={`mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-[11px] font-bold ${color} bg-[var(--bg-elevated)]`}>
92
+ {icon}
93
+ </span>
94
+ <div className="min-w-0 flex-1">
95
+ <div className="flex items-center justify-between gap-2">
96
+ <span className={`text-[13px] truncate ${!n.read ? "text-[var(--text-primary)] font-medium" : "text-[var(--text-tertiary)]"}`}>
97
+ {n.title}
98
+ </span>
99
+ <span className="shrink-0 text-[10px] text-[var(--text-muted)]">{timeAgo(n.created_at)}</span>
100
+ </div>
101
+ {n.summary && (
102
+ <p className="mt-0.5 text-[11px] text-[var(--text-muted)] line-clamp-2">{n.summary}</p>
103
+ )}
104
+ </div>
105
+ {!n.read && (
106
+ <span className="mt-2.5 h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--accent)]" />
107
+ )}
108
+ </button>
109
+ );
110
+ })
111
+ )}
112
+ </div>
113
+ </div>
114
+ </div>
115
+ );
116
+ }