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,397 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback } from "react";
4
+ import { ChevronDownIcon, ChevronRightIcon, DollarIcon } from "@/components/icons";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Types
8
+ // ---------------------------------------------------------------------------
9
+
10
+ type RiskLevel = "critical" | "warning" | "info";
11
+
12
+ interface ToolCall {
13
+ toolName: string;
14
+ inputSummary: string;
15
+ risk: { level: RiskLevel; label: string };
16
+ timestamp: string;
17
+ }
18
+
19
+ interface SessionSummary {
20
+ sessionId: string;
21
+ project: string;
22
+ startedAt: string;
23
+ lastActivity: string;
24
+ version: string;
25
+ gitBranch: string;
26
+ messageCount: number;
27
+ toolCalls: ToolCall[];
28
+ riskCounts: { critical: number; warning: number; info: number };
29
+ highestRisk: RiskLevel;
30
+ }
31
+
32
+ interface GovernanceData {
33
+ sessions: SessionSummary[];
34
+ projects: string[];
35
+ stats: {
36
+ totalSessions: number;
37
+ totalToolCalls: number;
38
+ criticalCount: number;
39
+ warningCount: number;
40
+ infoCount: number;
41
+ };
42
+ }
43
+
44
+ interface CostPeriod {
45
+ total: number;
46
+ count: number;
47
+ }
48
+
49
+ interface AgentCost {
50
+ slug: string;
51
+ name: string;
52
+ total: number;
53
+ count: number;
54
+ avgPerRun: number;
55
+ }
56
+
57
+ interface TopRun {
58
+ agent_name: string;
59
+ cost_usd: number;
60
+ prompt: string;
61
+ created_at: string;
62
+ }
63
+
64
+ interface CostData {
65
+ periods: {
66
+ today: CostPeriod;
67
+ week: CostPeriod;
68
+ month: CostPeriod;
69
+ };
70
+ agentBreakdown: AgentCost[];
71
+ topRuns: TopRun[];
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Helpers
76
+ // ---------------------------------------------------------------------------
77
+
78
+ const RISK_STYLES: Record<RiskLevel, { dot: string; text: string; rowBg: string }> = {
79
+ critical: { dot: "bg-red-400", text: "text-red-400", rowBg: "bg-red-500/5" },
80
+ warning: { dot: "bg-amber-400", text: "text-amber-400", rowBg: "bg-transparent" },
81
+ info: { dot: "bg-[var(--text-muted)]", text: "text-[var(--text-tertiary)]", rowBg: "bg-transparent" },
82
+ };
83
+
84
+ function timeAgo(ts: string): string {
85
+ if (!ts) return "—";
86
+ const diff = Date.now() - new Date(ts).getTime();
87
+ const mins = Math.floor(diff / 60000);
88
+ if (mins < 1) return "just now";
89
+ if (mins < 60) return `${mins}m ago`;
90
+ const hours = Math.floor(mins / 60);
91
+ if (hours < 24) return `${hours}h ago`;
92
+ const days = Math.floor(hours / 24);
93
+ return `${days}d ago`;
94
+ }
95
+
96
+ function shortProject(p: string): string {
97
+ const parts = p.split("/");
98
+ return parts[parts.length - 1] || p;
99
+ }
100
+
101
+ function toolIcon(toolName: string): string {
102
+ if (toolName === "Bash") return "$";
103
+ if (toolName === "Read") return "R";
104
+ if (toolName === "Write") return "W";
105
+ if (toolName === "Edit") return "E";
106
+ if (toolName === "Glob") return "G";
107
+ if (toolName === "Grep") return "?";
108
+ if (toolName === "Agent") return "A";
109
+ if (toolName.startsWith("mcp__")) return "M";
110
+ if (toolName === "WebFetch" || toolName === "WebSearch") return "N";
111
+ return "T";
112
+ }
113
+
114
+ function formatCost(usd: number): string {
115
+ if (usd === 0) return "$0.00";
116
+ if (usd < 0.01) return `$${usd.toFixed(4)}`;
117
+ if (usd < 1) return `$${usd.toFixed(2)}`;
118
+ return `$${usd.toFixed(2)}`;
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Sub-components
123
+ // ---------------------------------------------------------------------------
124
+
125
+ function StatItem({ label, value, color }: { label: string; value: number | string; color?: string }) {
126
+ return (
127
+ <div>
128
+ <div className={`text-[18px] font-semibold tabular-nums ${color || "text-[var(--text-primary)]"}`}>{value}</div>
129
+ <div className="text-[11px] text-[var(--text-muted)] mt-0.5">{label}</div>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ function CostPeriodDisplay({ label, period }: { label: string; period: CostPeriod }) {
135
+ return (
136
+ <div>
137
+ <div className="text-[18px] font-semibold tabular-nums text-[var(--text-primary)]">
138
+ {formatCost(period.total)}
139
+ </div>
140
+ <div className="text-[11px] text-[var(--text-muted)] mt-0.5">
141
+ {label} <span className="text-[var(--text-muted)]">· {period.count} run{period.count !== 1 ? "s" : ""}</span>
142
+ </div>
143
+ </div>
144
+ );
145
+ }
146
+
147
+ function RiskDot({ level }: { level: RiskLevel }) {
148
+ return <span className={`h-1.5 w-1.5 rounded-full shrink-0 ${RISK_STYLES[level].dot}`} />;
149
+ }
150
+
151
+ function ToolCallRow({ tc }: { tc: ToolCall }) {
152
+ const s = RISK_STYLES[tc.risk.level];
153
+ return (
154
+ <div className={`flex items-center gap-3 px-3 py-1.5 text-[12px] ${s.rowBg}`}>
155
+ <span className="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-[var(--bg-elevated)] text-[10px] font-mono text-[var(--text-muted)]">
156
+ {toolIcon(tc.toolName)}
157
+ </span>
158
+ <span className="font-medium text-[var(--text-secondary)] w-24 shrink-0 truncate">{tc.toolName}</span>
159
+ {tc.risk.level !== "info" && (
160
+ <span className={`flex items-center gap-1 text-[11px] ${s.text} shrink-0`}>
161
+ <RiskDot level={tc.risk.level} />
162
+ {tc.risk.label}
163
+ </span>
164
+ )}
165
+ <span className="text-[var(--text-muted)] truncate flex-1 font-mono">{tc.inputSummary || "—"}</span>
166
+ <span className="text-[var(--text-muted)] shrink-0 tabular-nums">{timeAgo(tc.timestamp)}</span>
167
+ </div>
168
+ );
169
+ }
170
+
171
+ function SessionRow({
172
+ session,
173
+ expanded,
174
+ onToggle,
175
+ }: {
176
+ session: SessionSummary;
177
+ expanded: boolean;
178
+ onToggle: () => void;
179
+ }) {
180
+ const riskyTools = session.toolCalls.filter((t) => t.risk.level !== "info");
181
+ const displayTools = expanded ? session.toolCalls : riskyTools.slice(0, 5);
182
+
183
+ return (
184
+ <div>
185
+ <button onClick={onToggle} className="w-full text-left flex items-center gap-3 px-3 py-2.5 hover:bg-[var(--bg-hover)] transition-colors">
186
+ {expanded ? <ChevronDownIcon size={12} className="text-[var(--text-muted)] shrink-0" /> : <ChevronRightIcon size={12} className="text-[var(--text-muted)] shrink-0" />}
187
+ <RiskDot level={session.highestRisk} />
188
+ <span className="text-[13px] font-medium text-[var(--text-primary)] w-40 shrink-0 truncate">{shortProject(session.project)}</span>
189
+ {session.gitBranch && (
190
+ <code className="rounded-md bg-[var(--bg-elevated)] px-1.5 py-0.5 text-[10px] text-[var(--text-muted)] shrink-0">{session.gitBranch}</code>
191
+ )}
192
+ <span className="flex-1" />
193
+ <span className="text-[11px] text-[var(--text-muted)] tabular-nums shrink-0 flex items-center gap-3">
194
+ {session.riskCounts.critical > 0 && <span className="text-red-400">{session.riskCounts.critical} critical</span>}
195
+ {session.riskCounts.warning > 0 && <span className="text-amber-400">{session.riskCounts.warning} warn</span>}
196
+ <span>{session.toolCalls.length} tools</span>
197
+ <span>{timeAgo(session.lastActivity)}</span>
198
+ </span>
199
+ </button>
200
+
201
+ {(expanded || riskyTools.length > 0) && displayTools.length > 0 && (
202
+ <div className="border-t border-[var(--border-subtle)] ml-7 divide-y divide-[var(--border-subtle)]">
203
+ {displayTools.map((tc, i) => (
204
+ <ToolCallRow key={`${tc.toolName}-${i}`} tc={tc} />
205
+ ))}
206
+ {!expanded && session.toolCalls.length > riskyTools.slice(0, 5).length && (
207
+ <button onClick={onToggle} className="w-full text-left px-3 py-1.5 text-[11px] text-[var(--text-muted)] hover:text-[var(--text-tertiary)]">
208
+ Show all {session.toolCalls.length} tool calls
209
+ </button>
210
+ )}
211
+ </div>
212
+ )}
213
+ </div>
214
+ );
215
+ }
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // Main Dashboard
219
+ // ---------------------------------------------------------------------------
220
+
221
+ export function GovernanceDashboard() {
222
+ const [data, setData] = useState<GovernanceData | null>(null);
223
+ const [costData, setCostData] = useState<CostData | null>(null);
224
+ const [loading, setLoading] = useState(true);
225
+ const [error, setError] = useState<string | null>(null);
226
+ const [riskFilter, setRiskFilter] = useState<RiskLevel | "all">("all");
227
+ const [projectFilter, setProjectFilter] = useState<string>("all");
228
+ const [expandedSession, setExpandedSession] = useState<string | null>(null);
229
+
230
+ const fetchData = useCallback(() => {
231
+ setLoading(true);
232
+ const params = new URLSearchParams();
233
+ if (riskFilter !== "all") params.set("risk", riskFilter);
234
+ if (projectFilter !== "all") params.set("project", projectFilter);
235
+ params.set("limit", "50");
236
+
237
+ Promise.all([
238
+ fetch(`/api/governance/sessions?${params}`)
239
+ .then((r) => { if (!r.ok) throw new Error(`Sessions: HTTP ${r.status}`); return r.json(); }),
240
+ fetch("/api/governance/costs")
241
+ .then((r) => { if (!r.ok) throw new Error(`Costs: HTTP ${r.status}`); return r.json(); })
242
+ .catch(() => null), // costs are optional — don't block if it fails
243
+ ])
244
+ .then(([govData, costs]: [GovernanceData, CostData | null]) => {
245
+ setData(govData);
246
+ setCostData(costs);
247
+ setError(null);
248
+ setLoading(false);
249
+ })
250
+ .catch((e) => { setError(e.message); setLoading(false); });
251
+ }, [riskFilter, projectFilter]);
252
+
253
+ useEffect(() => { fetchData(); }, [fetchData]);
254
+
255
+ if (loading && !data) {
256
+ return (
257
+ <div className="space-y-4">
258
+ <h1 className="text-[15px] font-semibold text-[var(--text-primary)]">Governance</h1>
259
+ <div className="space-y-1 animate-pulse">
260
+ {[...Array(6)].map((_, i) => (
261
+ <div key={i} className="h-10 rounded-md bg-[var(--bg-raised)]" />
262
+ ))}
263
+ </div>
264
+ </div>
265
+ );
266
+ }
267
+
268
+ if (error) {
269
+ return (
270
+ <div className="space-y-4">
271
+ <h1 className="text-[15px] font-semibold text-[var(--text-primary)]">Governance</h1>
272
+ <div className="rounded-md border border-red-500/15 bg-red-500/5 p-3 text-red-400 text-[13px]">
273
+ Failed to load governance data: {error}
274
+ </div>
275
+ </div>
276
+ );
277
+ }
278
+
279
+ const stats = data!.stats;
280
+ const sessions = data!.sessions;
281
+ const projects = data!.projects;
282
+
283
+ return (
284
+ <div className="space-y-5">
285
+ {/* Header */}
286
+ <div className="flex items-center justify-between">
287
+ <h1 className="text-[15px] font-semibold text-[var(--text-primary)]">Governance</h1>
288
+ <button onClick={fetchData} disabled={loading}
289
+ className="rounded-md px-2.5 py-1 text-[12px] text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] transition-colors disabled:opacity-50">
290
+ {loading ? "Loading..." : "Refresh"}
291
+ </button>
292
+ </div>
293
+
294
+ {/* Cost overview */}
295
+ {costData && (
296
+ <div className="space-y-4">
297
+ {/* Period totals */}
298
+ <div className="flex items-end gap-8 pb-4 border-b border-[var(--border-subtle)]">
299
+ <div className="flex items-center gap-2 mr-2">
300
+ <DollarIcon size={14} className="text-[var(--text-muted)]" />
301
+ <span className="text-[11px] font-medium uppercase tracking-wider text-[var(--text-muted)]">Spend</span>
302
+ </div>
303
+ <CostPeriodDisplay label="Today" period={costData.periods.today} />
304
+ <CostPeriodDisplay label="This week" period={costData.periods.week} />
305
+ <CostPeriodDisplay label="This month" period={costData.periods.month} />
306
+ </div>
307
+
308
+ {/* Per-agent breakdown — only if there's data */}
309
+ {costData.agentBreakdown.length > 0 && (
310
+ <div>
311
+ <div className="text-[11px] font-medium uppercase tracking-wider text-[var(--text-muted)] mb-2">Cost by agent (this month)</div>
312
+ <div className="border border-[var(--border-subtle)] rounded-lg divide-y divide-[var(--border-subtle)]">
313
+ {costData.agentBreakdown.map((agent) => (
314
+ <div key={agent.slug} className="flex items-center gap-3 px-3 py-2">
315
+ <span className="text-[13px] font-medium text-[var(--text-primary)] w-40 shrink-0 truncate">{agent.name}</span>
316
+ <span className="flex-1" />
317
+ <span className="text-[12px] text-[var(--text-muted)] tabular-nums">{agent.count} runs</span>
318
+ <span className="text-[12px] text-[var(--text-tertiary)] tabular-nums w-16 text-right">~{formatCost(agent.avgPerRun)}/run</span>
319
+ <span className="text-[13px] font-medium text-[var(--text-primary)] tabular-nums w-20 text-right">{formatCost(agent.total)}</span>
320
+ </div>
321
+ ))}
322
+ </div>
323
+ </div>
324
+ )}
325
+
326
+ {/* Top runs — compact */}
327
+ {costData.topRuns.length > 0 && (
328
+ <div>
329
+ <div className="text-[11px] font-medium uppercase tracking-wider text-[var(--text-muted)] mb-2">Most expensive runs (this month)</div>
330
+ <div className="border border-[var(--border-subtle)] rounded-lg divide-y divide-[var(--border-subtle)]">
331
+ {costData.topRuns.map((run, i) => (
332
+ <div key={i} className="flex items-center gap-3 px-3 py-2 text-[12px]">
333
+ <span className="text-[var(--text-secondary)] w-28 shrink-0 truncate">{run.agent_name}</span>
334
+ <span className="text-[var(--text-muted)] truncate flex-1">{run.prompt}</span>
335
+ <span className="text-[var(--text-muted)] tabular-nums shrink-0">{timeAgo(run.created_at)}</span>
336
+ <span className="font-medium text-[var(--text-primary)] tabular-nums w-20 text-right">{formatCost(run.cost_usd)}</span>
337
+ </div>
338
+ ))}
339
+ </div>
340
+ </div>
341
+ )}
342
+ </div>
343
+ )}
344
+
345
+ {/* Risk stats — inline */}
346
+ <div className="flex items-end gap-8 pb-4 border-b border-[var(--border-subtle)]">
347
+ <StatItem label="Sessions" value={stats.totalSessions} />
348
+ <StatItem label="Tool Calls" value={stats.totalToolCalls} />
349
+ <StatItem label="Critical" value={stats.criticalCount} color={stats.criticalCount > 0 ? "text-red-400" : "text-[var(--text-muted)]"} />
350
+ <StatItem label="Warnings" value={stats.warningCount} color={stats.warningCount > 0 ? "text-amber-400" : "text-[var(--text-muted)]"} />
351
+ <StatItem label="Info" value={stats.infoCount} color="text-[var(--text-tertiary)]" />
352
+ </div>
353
+
354
+ {/* Filters */}
355
+ <div className="flex items-center gap-3">
356
+ <div className="flex items-center gap-1">
357
+ <span className="text-[11px] text-[var(--text-muted)] mr-1">Risk:</span>
358
+ {(["all", "critical", "warning", "info"] as const).map((level) => (
359
+ <button key={level} onClick={() => setRiskFilter(level)}
360
+ className={`rounded-md px-2 py-1 text-[11px] transition-colors ${
361
+ riskFilter === level
362
+ ? "bg-[var(--bg-active)] text-[var(--text-primary)]"
363
+ : "text-[var(--text-muted)] hover:text-[var(--text-tertiary)] hover:bg-[var(--bg-hover)]"
364
+ }`}>
365
+ {level === "all" ? "All" : level.charAt(0).toUpperCase() + level.slice(1)}
366
+ </button>
367
+ ))}
368
+ </div>
369
+
370
+ <div className="flex items-center gap-1.5">
371
+ <span className="text-[11px] text-[var(--text-muted)]">Project:</span>
372
+ <select value={projectFilter} onChange={(e) => setProjectFilter(e.target.value)}
373
+ className="rounded-md bg-[var(--bg-raised)] border border-[var(--border-subtle)] px-2 py-1 text-[11px] text-[var(--text-secondary)] focus:outline-none focus:border-[var(--border-focus)]">
374
+ <option value="all">All projects</option>
375
+ {projects.map((p) => <option key={p} value={p}>{shortProject(p)}</option>)}
376
+ </select>
377
+ </div>
378
+ </div>
379
+
380
+ {/* Session list */}
381
+ <div className="border border-[var(--border-subtle)] rounded-lg divide-y divide-[var(--border-subtle)]">
382
+ {sessions.length === 0 ? (
383
+ <p className="text-[13px] text-[var(--text-tertiary)] py-8 text-center">No sessions found matching filters.</p>
384
+ ) : (
385
+ sessions.map((session) => (
386
+ <SessionRow
387
+ key={session.sessionId}
388
+ session={session}
389
+ expanded={expandedSession === session.sessionId}
390
+ onToggle={() => setExpandedSession(expandedSession === session.sessionId ? null : session.sessionId)}
391
+ />
392
+ ))
393
+ )}
394
+ </div>
395
+ </div>
396
+ );
397
+ }