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,338 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback, useRef } from "react";
4
+ import type { SkillInfo } from "@/lib/capabilities";
5
+ import {
6
+ SearchIcon,
7
+ XIcon,
8
+ ChevronLeftIcon,
9
+ ChevronDownIcon,
10
+ ChevronRightIcon,
11
+ EditIcon,
12
+ CheckIcon,
13
+ PenIcon,
14
+ BuildingIcon,
15
+ RefreshIcon,
16
+ FlaskIcon,
17
+ UserIcon,
18
+ ChartIcon,
19
+ DatabaseIcon,
20
+ WrenchIcon,
21
+ PackageIcon,
22
+ } from "@/components/icons";
23
+
24
+ const CATEGORY_ICONS: Record<string, React.ComponentType<{ size?: number; className?: string }>> = {
25
+ "Content": PenIcon,
26
+ "Google Workspace": BuildingIcon,
27
+ "Workflows": RefreshIcon,
28
+ "Recipes": FlaskIcon,
29
+ "Personas": UserIcon,
30
+ "Analytics": ChartIcon,
31
+ "Data": DatabaseIcon,
32
+ "Development": WrenchIcon,
33
+ "Other": PackageIcon,
34
+ };
35
+
36
+ const CATEGORY_ORDER = [
37
+ "Content", "Google Workspace", "Workflows", "Recipes",
38
+ "Personas", "Analytics", "Data", "Development", "Other",
39
+ ];
40
+
41
+ export function SkillsBrowser() {
42
+ const [skills, setSkills] = useState<SkillInfo[] | null>(null);
43
+ const [selectedSkill, setSelectedSkill] = useState<string | null>(null);
44
+ const [skillContent, setSkillContent] = useState<string | null>(null);
45
+ const [loadingContent, setLoadingContent] = useState(false);
46
+ const [editing, setEditing] = useState(false);
47
+ const [editContent, setEditContent] = useState("");
48
+ const [saving, setSaving] = useState(false);
49
+ const [saveStatus, setSaveStatus] = useState<"idle" | "saved" | "error">("idle");
50
+ const [searchQuery, setSearchQuery] = useState("");
51
+ const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
52
+ const detailRef = useRef<HTMLDivElement>(null);
53
+
54
+ useEffect(() => {
55
+ fetch("/api/skills")
56
+ .then((r) => r.json())
57
+ .then((data: SkillInfo[]) => setSkills(data));
58
+ }, []);
59
+
60
+ const loadSkillContent = useCallback((slug: string) => {
61
+ setSelectedSkill(slug);
62
+ setLoadingContent(true);
63
+ setEditing(false);
64
+ setSaveStatus("idle");
65
+ fetch(`/api/skills/${slug}`)
66
+ .then((r) => r.json())
67
+ .then((data) => {
68
+ setSkillContent(data.content || "");
69
+ setEditContent(data.content || "");
70
+ setLoadingContent(false);
71
+ })
72
+ .catch(() => {
73
+ setSkillContent(null);
74
+ setLoadingContent(false);
75
+ });
76
+ setTimeout(() => detailRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }), 50);
77
+ }, []);
78
+
79
+ const handleSave = async () => {
80
+ if (!selectedSkill) return;
81
+ setSaving(true);
82
+ setSaveStatus("idle");
83
+ try {
84
+ const res = await fetch(`/api/skills/${selectedSkill}`, {
85
+ method: "PATCH",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify({ content: editContent }),
88
+ });
89
+ if (res.ok) {
90
+ setSkillContent(editContent);
91
+ setEditing(false);
92
+ setSaveStatus("saved");
93
+ setTimeout(() => setSaveStatus("idle"), 2500);
94
+ } else {
95
+ setSaveStatus("error");
96
+ }
97
+ } catch {
98
+ setSaveStatus("error");
99
+ }
100
+ setSaving(false);
101
+ };
102
+
103
+ const handleBack = () => {
104
+ setSelectedSkill(null);
105
+ setSkillContent(null);
106
+ setEditing(false);
107
+ setSaveStatus("idle");
108
+ };
109
+
110
+ const toggleCategory = (cat: string) => {
111
+ setCollapsed((prev) => ({ ...prev, [cat]: !prev[cat] }));
112
+ };
113
+
114
+ if (!skills) {
115
+ return (
116
+ <div>
117
+ <h1 className="text-[15px] font-semibold text-[var(--text-primary)] mb-4">Skills</h1>
118
+ <div className="space-y-2 animate-pulse">
119
+ {[...Array(8)].map((_, i) => (
120
+ <div key={i} className="h-9 rounded-md bg-[var(--bg-raised)]" />
121
+ ))}
122
+ </div>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ // Group by category
128
+ const categories = skills.reduce<Record<string, SkillInfo[]>>((acc, skill) => {
129
+ (acc[skill.category] ||= []).push(skill);
130
+ return acc;
131
+ }, {});
132
+ const sortedCategories = CATEGORY_ORDER.filter((c) => categories[c]);
133
+
134
+ // Filter by search
135
+ const q = searchQuery.toLowerCase();
136
+ const getFilteredSkills = (cat: string) => {
137
+ const items = categories[cat] || [];
138
+ if (!q) return items;
139
+ return items.filter(
140
+ (s) => s.name.toLowerCase().includes(q) || s.slug.toLowerCase().includes(q) || s.description.toLowerCase().includes(q)
141
+ );
142
+ };
143
+
144
+ const selectedSkillInfo = skills.find((s) => s.slug === selectedSkill);
145
+
146
+ // ---- Detail View ----
147
+ if (selectedSkill) {
148
+ return (
149
+ <div ref={detailRef} className="flex flex-col" style={{ height: "calc(100vh - 6rem)" }}>
150
+ {/* Top bar */}
151
+ <div className="flex items-center justify-between mb-4 shrink-0">
152
+ <div className="flex items-center gap-3 min-w-0">
153
+ <button
154
+ onClick={handleBack}
155
+ className="shrink-0 flex items-center gap-1 rounded-md px-2 py-1 text-[12px] text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] transition-colors"
156
+ >
157
+ <ChevronLeftIcon size={14} />
158
+ Back
159
+ </button>
160
+ <div className="min-w-0">
161
+ <h1 className="text-[15px] font-semibold text-[var(--text-primary)] truncate">
162
+ {selectedSkillInfo?.name || selectedSkill}
163
+ </h1>
164
+ {selectedSkillInfo?.description && (
165
+ <p className="text-[12px] text-[var(--text-tertiary)] mt-0.5 truncate">{selectedSkillInfo.description}</p>
166
+ )}
167
+ </div>
168
+ </div>
169
+ <div className="flex items-center gap-2 shrink-0">
170
+ {saveStatus === "saved" && (
171
+ <span className="flex items-center gap-1 text-[11px] text-green-400/80">
172
+ <CheckIcon size={12} />
173
+ Saved
174
+ </span>
175
+ )}
176
+ {saveStatus === "error" && (
177
+ <span className="text-[11px] text-red-400/80">Save failed</span>
178
+ )}
179
+ {editing ? (
180
+ <>
181
+ <button
182
+ onClick={() => { setEditing(false); setEditContent(skillContent || ""); }}
183
+ className="rounded-md px-3 py-1.5 text-[12px] text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] transition-colors"
184
+ >
185
+ Cancel
186
+ </button>
187
+ <button
188
+ onClick={handleSave}
189
+ disabled={saving}
190
+ className="rounded-md bg-[var(--accent)] px-3 py-1.5 text-[12px] font-medium text-white hover:bg-[var(--accent-hover)] disabled:opacity-50 transition-colors"
191
+ >
192
+ {saving ? "Saving..." : "Save"}
193
+ </button>
194
+ </>
195
+ ) : (
196
+ <button
197
+ onClick={() => setEditing(true)}
198
+ className="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-[12px] text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] transition-colors"
199
+ >
200
+ <EditIcon size={12} />
201
+ Edit
202
+ </button>
203
+ )}
204
+ </div>
205
+ </div>
206
+
207
+ {/* Slug path */}
208
+ <div className="mb-3 shrink-0">
209
+ <code className="text-[11px] text-[var(--text-muted)] bg-[var(--bg-raised)] border border-[var(--border-subtle)] px-2 py-0.5 rounded-md">
210
+ .claude/skills/{selectedSkill}/SKILL.md
211
+ </code>
212
+ </div>
213
+
214
+ {/* Content area */}
215
+ {loadingContent ? (
216
+ <div className="flex-1 animate-pulse space-y-2">
217
+ {[...Array(6)].map((_, i) => (
218
+ <div key={i} className="h-4 rounded bg-[var(--bg-raised)]" style={{ width: `${70 + Math.random() * 30}%` }} />
219
+ ))}
220
+ </div>
221
+ ) : editing ? (
222
+ <textarea
223
+ value={editContent}
224
+ onChange={(e) => setEditContent(e.target.value)}
225
+ className="flex-1 min-h-0 w-full rounded-lg border border-[var(--border-default)] bg-[var(--bg-base)] p-4 text-[13px] text-[var(--text-secondary)] font-mono leading-relaxed resize-none focus:border-[var(--border-focus)] focus:outline-none"
226
+ spellCheck={false}
227
+ />
228
+ ) : (
229
+ <div className="flex-1 overflow-y-auto">
230
+ <pre className="whitespace-pre-wrap break-words text-[13px] text-[var(--text-secondary)] font-mono leading-relaxed bg-[var(--bg-raised)] rounded-lg border border-[var(--border-subtle)] p-4">
231
+ {skillContent}
232
+ </pre>
233
+ </div>
234
+ )}
235
+ </div>
236
+ );
237
+ }
238
+
239
+ // ---- Browse View ----
240
+ const totalFiltered = sortedCategories.reduce((sum, cat) => sum + getFilteredSkills(cat).length, 0);
241
+
242
+ return (
243
+ <div>
244
+ {/* Header */}
245
+ <div className="flex items-center justify-between mb-4">
246
+ <div>
247
+ <h1 className="text-[15px] font-semibold text-[var(--text-primary)]">Skills</h1>
248
+ <p className="text-[12px] text-[var(--text-muted)] mt-0.5">
249
+ {q ? `${totalFiltered} of ${skills.length}` : `${skills.length} skills`}
250
+ </p>
251
+ </div>
252
+ <div className="w-56">
253
+ <div className="relative">
254
+ <SearchIcon size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[var(--text-muted)]" />
255
+ <input
256
+ type="text"
257
+ placeholder="Search..."
258
+ value={searchQuery}
259
+ onChange={(e) => setSearchQuery(e.target.value)}
260
+ className="w-full rounded-md border border-[var(--border-subtle)] bg-[var(--bg-raised)] pl-8 pr-3 py-1.5 text-[13px] text-[var(--text-secondary)] placeholder:text-[var(--text-muted)] focus:border-[var(--border-focus)] focus:outline-none transition-colors"
261
+ />
262
+ {searchQuery && (
263
+ <button
264
+ onClick={() => setSearchQuery("")}
265
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-[var(--text-muted)] hover:text-[var(--text-secondary)]"
266
+ >
267
+ <XIcon size={12} />
268
+ </button>
269
+ )}
270
+ </div>
271
+ </div>
272
+ </div>
273
+
274
+ {/* Categories */}
275
+ <div className="space-y-1">
276
+ {sortedCategories.map((cat) => {
277
+ const items = getFilteredSkills(cat);
278
+ if (!items.length) return null;
279
+ const CatIcon = CATEGORY_ICONS[cat] || PackageIcon;
280
+ const isCollapsed = collapsed[cat] ?? false;
281
+
282
+ return (
283
+ <div key={cat}>
284
+ {/* Category header */}
285
+ <button
286
+ onClick={() => toggleCategory(cat)}
287
+ className="flex items-center gap-2.5 w-full text-left px-2 py-1.5 rounded-md hover:bg-[var(--bg-hover)] transition-colors group"
288
+ >
289
+ <ChevronDownIcon
290
+ size={12}
291
+ className={`text-[var(--text-muted)] transition-transform duration-150 ${isCollapsed ? "-rotate-90" : ""}`}
292
+ />
293
+ <CatIcon size={14} className="text-[var(--text-tertiary)] opacity-70" />
294
+ <span className="text-[12px] font-medium text-[var(--text-secondary)]">
295
+ {cat}
296
+ </span>
297
+ <span className="text-[11px] text-[var(--text-muted)] tabular-nums">{items.length}</span>
298
+ </button>
299
+
300
+ {/* Skill rows */}
301
+ {!isCollapsed && (
302
+ <div className="ml-[26px] border-l border-[var(--border-subtle)]">
303
+ {items.map((skill) => (
304
+ <button
305
+ key={skill.slug}
306
+ onClick={() => loadSkillContent(skill.slug)}
307
+ className="group/item flex items-center gap-2 w-full text-left pl-4 pr-2 py-1.5 hover:bg-[var(--bg-hover)] transition-colors"
308
+ >
309
+ <span className="text-[13px] text-[var(--text-secondary)] group-hover/item:text-[var(--text-primary)] transition-colors truncate flex-1">
310
+ {skill.name}
311
+ </span>
312
+ <ChevronRightIcon
313
+ size={12}
314
+ className="text-[var(--text-muted)] opacity-0 group-hover/item:opacity-100 transition-opacity shrink-0"
315
+ />
316
+ </button>
317
+ ))}
318
+ </div>
319
+ )}
320
+ </div>
321
+ );
322
+ })}
323
+
324
+ {q && totalFiltered === 0 && (
325
+ <div className="text-center py-12">
326
+ <p className="text-[13px] text-[var(--text-tertiary)]">No skills match &ldquo;{searchQuery}&rdquo;</p>
327
+ <button
328
+ onClick={() => setSearchQuery("")}
329
+ className="mt-2 text-[12px] text-[var(--accent-text)] hover:text-[var(--accent-hover)]"
330
+ >
331
+ Clear search
332
+ </button>
333
+ </div>
334
+ )}
335
+ </div>
336
+ </div>
337
+ );
338
+ }
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import { useRef, useState, useCallback, useEffect } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { getToolDescription } from "@/lib/tool-descriptions";
6
+
7
+ /**
8
+ * Portal-based tooltip for tool names.
9
+ * Renders the tooltip at document.body level so it's never clipped by
10
+ * overflow:hidden/auto on parent containers (modals, sidebars, etc.).
11
+ */
12
+ export function ToolBadge({
13
+ name,
14
+ className,
15
+ children,
16
+ }: {
17
+ name: string;
18
+ className?: string;
19
+ children?: React.ReactNode;
20
+ }) {
21
+ const desc = getToolDescription(name);
22
+ const ref = useRef<HTMLSpanElement>(null);
23
+ const [show, setShow] = useState(false);
24
+ const [pos, setPos] = useState<{ x: number; y: number } | null>(null);
25
+
26
+ const updatePos = useCallback(() => {
27
+ if (!ref.current) return;
28
+ const rect = ref.current.getBoundingClientRect();
29
+ setPos({
30
+ x: rect.left + rect.width / 2,
31
+ y: rect.top,
32
+ });
33
+ }, []);
34
+
35
+ const handleEnter = useCallback(() => {
36
+ if (!desc) return;
37
+ updatePos();
38
+ setShow(true);
39
+ }, [desc, updatePos]);
40
+
41
+ const handleLeave = useCallback(() => {
42
+ setShow(false);
43
+ }, []);
44
+
45
+ // Update position on scroll while visible
46
+ useEffect(() => {
47
+ if (!show) return;
48
+ const onScroll = () => updatePos();
49
+ window.addEventListener("scroll", onScroll, true);
50
+ return () => window.removeEventListener("scroll", onScroll, true);
51
+ }, [show, updatePos]);
52
+
53
+ return (
54
+ <>
55
+ <span
56
+ ref={ref}
57
+ onMouseEnter={handleEnter}
58
+ onMouseLeave={handleLeave}
59
+ className={className}
60
+ >
61
+ {children ?? name}
62
+ </span>
63
+ {show && desc && pos &&
64
+ createPortal(
65
+ <div
66
+ style={{
67
+ position: "fixed",
68
+ left: pos.x,
69
+ top: pos.y - 8,
70
+ transform: "translate(-50%, -100%)",
71
+ zIndex: 9999,
72
+ pointerEvents: "none",
73
+ }}
74
+ className="whitespace-nowrap rounded-md bg-[#111113] border border-[var(--border-default)] px-2.5 py-1.5 text-[11px] text-[var(--text-secondary)] shadow-lg"
75
+ >
76
+ {desc}
77
+ </div>,
78
+ document.body
79
+ )}
80
+ </>
81
+ );
82
+ }
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import { useCallback, useRef, useState } from "react";
4
+
5
+ export interface SSEMessage {
6
+ type: string;
7
+ seq?: number;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ export function useSSE() {
12
+ const [messages, setMessages] = useState<SSEMessage[]>([]);
13
+ const [isConnected, setIsConnected] = useState(false);
14
+ const controllerRef = useRef<AbortController | null>(null);
15
+ const lastSeqRef = useRef<number>(0);
16
+
17
+ const connect = useCallback(async (url: string, afterSeq?: number) => {
18
+ // Close any existing connection
19
+ controllerRef.current?.abort();
20
+
21
+ const controller = new AbortController();
22
+ controllerRef.current = controller;
23
+ // Don't reset messages — caller may have pre-loaded historical events
24
+ if (!afterSeq) {
25
+ setMessages([]);
26
+ lastSeqRef.current = 0;
27
+ } else {
28
+ lastSeqRef.current = afterSeq;
29
+ }
30
+ setIsConnected(true);
31
+
32
+ // Append after_seq to URL for mid-run reconnection
33
+ const streamUrl = afterSeq
34
+ ? `${url}${url.includes("?") ? "&" : "?"}after_seq=${afterSeq}`
35
+ : url;
36
+
37
+ try {
38
+ const response = await fetch(streamUrl, { signal: controller.signal });
39
+ if (!response.ok || !response.body) {
40
+ setIsConnected(false);
41
+ return;
42
+ }
43
+
44
+ const reader = response.body.getReader();
45
+ const decoder = new TextDecoder();
46
+ let buffer = "";
47
+
48
+ while (true) {
49
+ const { done, value } = await reader.read();
50
+ if (done) break;
51
+
52
+ buffer += decoder.decode(value, { stream: true });
53
+ const lines = buffer.split("\n");
54
+ buffer = lines.pop() || "";
55
+
56
+ for (const line of lines) {
57
+ if (line.startsWith("data: ")) {
58
+ try {
59
+ const data = JSON.parse(line.slice(6)) as SSEMessage;
60
+
61
+ // Deduplicate by seq (skip events we've already seen)
62
+ if (data.seq && data.seq <= lastSeqRef.current) continue;
63
+ if (data.seq) lastSeqRef.current = data.seq;
64
+
65
+ setMessages((prev) => [...prev, data]);
66
+ } catch {
67
+ // skip malformed JSON
68
+ }
69
+ }
70
+ }
71
+ }
72
+ } catch (err) {
73
+ if ((err as Error).name !== "AbortError") {
74
+ console.error("SSE error:", err);
75
+ }
76
+ } finally {
77
+ setIsConnected(false);
78
+ }
79
+ }, []);
80
+
81
+ const disconnect = useCallback(() => {
82
+ controllerRef.current?.abort();
83
+ setIsConnected(false);
84
+ }, []);
85
+
86
+ const reset = useCallback(() => {
87
+ setMessages([]);
88
+ lastSeqRef.current = 0;
89
+ }, []);
90
+
91
+ // Load historical events from /api/runs/[id]/events and populate messages.
92
+ // Returns the events AND maxSeq so the caller can decide whether to connect SSE.
93
+ const loadHistory = useCallback(async (runId: string): Promise<{ maxSeq: number; events: SSEMessage[] }> => {
94
+ const res = await fetch(`/api/runs/${runId}/events`);
95
+ if (!res.ok) return { maxSeq: 0, events: [] };
96
+
97
+ const rawEvents = (await res.json()) as Record<string, unknown>[];
98
+ if (rawEvents.length === 0) return { maxSeq: 0, events: [] };
99
+
100
+ // Convert run_events DB format to SSE message format
101
+ const sseMessages: SSEMessage[] = rawEvents.map((evt) => ({
102
+ type: evt.event_type as string,
103
+ seq: evt.seq as number,
104
+ ...(evt.payload as Record<string, unknown>),
105
+ }));
106
+
107
+ setMessages(sseMessages);
108
+
109
+ const maxSeq = Math.max(...sseMessages.map((m) => m.seq || 0));
110
+ lastSeqRef.current = maxSeq;
111
+ return { maxSeq, events: sseMessages };
112
+ }, []);
113
+
114
+ return { messages, isConnected, connect, disconnect, reset, loadHistory };
115
+ }
@@ -0,0 +1,9 @@
1
+ export async function register() {
2
+ // Only run on the server (not edge)
3
+ if (process.env.NEXT_RUNTIME === "nodejs") {
4
+ const { cleanupStaleRuns } = await import("@/lib/agent-runner");
5
+ cleanupStaleRuns().catch((err) => {
6
+ console.error("[instrumentation] Failed to clean up stale runs:", err);
7
+ });
8
+ }
9
+ }
@@ -0,0 +1,19 @@
1
+ import type { AgentMeta } from "./types";
2
+
3
+ // Simple client-side cache for agent metadata.
4
+ // Populated by the homepage, consumed by agent detail pages — avoids
5
+ // a redundant /api/agents fetch on every navigation.
6
+
7
+ let cache: AgentMeta[] | null = null;
8
+
9
+ export function setAgentCache(agents: AgentMeta[]) {
10
+ cache = agents;
11
+ }
12
+
13
+ export function getCachedAgents(): AgentMeta[] | null {
14
+ return cache;
15
+ }
16
+
17
+ export function getCachedAgent(slug: string): AgentMeta | undefined {
18
+ return cache?.find((a) => a.slug === slug);
19
+ }