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,504 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
+ import { getCachedAgent } from "@/lib/agent-cache";
6
+ import { NotificationBell } from "@/components/notifications/notification-bell";
7
+ import { useOverview } from "@/components/overview/overview-context";
8
+ import type { AgentMeta, AgentRun } from "@/lib/types";
9
+ import { ProfileMenu } from "./profile-menu";
10
+ import { ToolBadge } from "@/components/tool-tooltip";
11
+ import {
12
+ GridIcon,
13
+ ChevronLeftIcon,
14
+ InfoIcon,
15
+ PlusIcon,
16
+ XIcon,
17
+ TrashIcon,
18
+ } from "@/components/icons";
19
+
20
+ interface SessionSummary {
21
+ session_id: string;
22
+ label: string;
23
+ turnCount: number;
24
+ lastAt: string;
25
+ status: "completed" | "failed" | "running" | "mixed";
26
+ }
27
+
28
+ interface Capabilities {
29
+ skills: { slug: string; name: string; description: string; category: string }[];
30
+ commands: { slug: string; name: string; description: string; group?: string }[];
31
+ mcpServers: { name: string; type: string }[];
32
+ subagents: { slug: string; name: string; description: string; emoji?: string }[];
33
+ tools: string[];
34
+ }
35
+
36
+ function timeAgo(date: string): string {
37
+ const seconds = Math.floor((Date.now() - new Date(date).getTime()) / 1000);
38
+ if (seconds < 60) return "just now";
39
+ const minutes = Math.floor(seconds / 60);
40
+ if (minutes < 60) return `${minutes}m ago`;
41
+ const hours = Math.floor(minutes / 60);
42
+ if (hours < 24) return `${hours}h ago`;
43
+ const days = Math.floor(hours / 24);
44
+ return `${days}d ago`;
45
+ }
46
+
47
+ function formatSessionDate(dateStr: string): string {
48
+ const d = new Date(dateStr);
49
+ const now = new Date();
50
+ const isToday = d.toDateString() === now.toDateString();
51
+ const yesterday = new Date(now);
52
+ yesterday.setDate(yesterday.getDate() - 1);
53
+ const isYesterday = d.toDateString() === yesterday.toDateString();
54
+
55
+ const time = d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
56
+ if (isToday) return `Today ${time}`;
57
+ if (isYesterday) return `Yesterday ${time}`;
58
+ return d.toLocaleDateString([], { month: "short", day: "numeric" }) + ` ${time}`;
59
+ }
60
+
61
+ function groupSessions(runs: AgentRun[]): SessionSummary[] {
62
+ const sessions = new Map<string, AgentRun[]>();
63
+ for (const run of runs) {
64
+ if (!run.session_id) continue;
65
+ const arr = sessions.get(run.session_id) || [];
66
+ arr.push(run);
67
+ sessions.set(run.session_id, arr);
68
+ }
69
+
70
+ return Array.from(sessions.entries())
71
+ .map(([sid, sRuns]) => {
72
+ const sorted = sRuns.sort(
73
+ (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
74
+ );
75
+ const statuses = new Set(sorted.map((r) => r.status));
76
+ let status: SessionSummary["status"] = "completed";
77
+ if (statuses.has("running")) status = "running";
78
+ else if (statuses.size > 1) status = "mixed";
79
+ else if (statuses.has("failed")) status = "failed";
80
+
81
+ return {
82
+ session_id: sid,
83
+ label: formatSessionDate(sorted[0].created_at),
84
+ turnCount: sorted.length,
85
+ lastAt: sorted[sorted.length - 1].created_at,
86
+ status,
87
+ };
88
+ })
89
+ .sort((a, b) => new Date(b.lastAt).getTime() - new Date(a.lastAt).getTime());
90
+ }
91
+
92
+ function OverviewLink({ label, icon }: { label: string; icon: "grid" | "back" }) {
93
+ const { setTab } = useOverview();
94
+ return (
95
+ <button
96
+ onClick={() => setTab("agents")}
97
+ className="flex items-center gap-2 px-2.5 py-1.5 rounded-md text-[12px] text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]"
98
+ >
99
+ {icon === "grid" ? <GridIcon size={14} /> : <ChevronLeftIcon size={14} />}
100
+ {label}
101
+ </button>
102
+ );
103
+ }
104
+
105
+ function DetailSection({
106
+ title,
107
+ count,
108
+ defaultOpen = false,
109
+ children,
110
+ }: {
111
+ title: string;
112
+ count: number;
113
+ defaultOpen?: boolean;
114
+ children: React.ReactNode;
115
+ }) {
116
+ const [open, setOpen] = useState(defaultOpen);
117
+ if (count === 0) return null;
118
+
119
+ return (
120
+ <div className="border-t border-[var(--border-subtle)] pt-3">
121
+ <button
122
+ onClick={() => setOpen(!open)}
123
+ className="flex items-center justify-between w-full text-[11px] font-medium uppercase tracking-wider text-[var(--text-tertiary)] hover:text-[var(--text-secondary)] transition-colors mb-2"
124
+ >
125
+ <span>
126
+ {title}
127
+ <span className="ml-1.5 text-[var(--text-muted)] normal-case font-normal">{count}</span>
128
+ </span>
129
+ <svg
130
+ width="10"
131
+ height="10"
132
+ viewBox="0 0 10 10"
133
+ fill="none"
134
+ className={`transition-transform duration-150 ${open ? "rotate-180" : ""}`}
135
+ >
136
+ <path d="M2.5 3.75L5 6.25L7.5 3.75" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
137
+ </svg>
138
+ </button>
139
+ {open && children}
140
+ </div>
141
+ );
142
+ }
143
+
144
+ export function AgentSidebar({ slug }: { slug: string }) {
145
+ const router = useRouter();
146
+ const searchParams = useSearchParams();
147
+ const activeSession = searchParams.get("session");
148
+ const [deletingSession, setDeletingSession] = useState<string | null>(null);
149
+
150
+ const cachedAgent = getCachedAgent(slug);
151
+ const [agent, setAgent] = useState<AgentMeta | null>(cachedAgent || null);
152
+ const [sessions, setSessions] = useState<SessionSummary[]>([]);
153
+ const [sessionsLoaded, setSessionsLoaded] = useState(false);
154
+ const [caps, setCaps] = useState<Capabilities | null>(null);
155
+ const [showTooltip, setShowTooltip] = useState(false);
156
+ const [titles, setTitles] = useState<Record<string, string>>({});
157
+
158
+
159
+ const fetchTitles = useCallback((sessionIds: string[]) => {
160
+ if (sessionIds.length === 0) return;
161
+ fetch(`/api/ai/title?session_ids=${sessionIds.join(",")}`)
162
+ .then((r) => r.json())
163
+ .then((d) => {
164
+ if (d.titles) setTitles((prev) => ({ ...prev, ...d.titles }));
165
+ })
166
+ .catch(() => {});
167
+ }, []);
168
+
169
+ const fetchSessions = useCallback(() => {
170
+ fetch(`/api/runs?agent_slug=${slug}&limit=100`)
171
+ .then((r) => r.json())
172
+ .then((runs: AgentRun[]) => {
173
+ const grouped = groupSessions(runs);
174
+ setSessions(grouped);
175
+ setSessionsLoaded(true);
176
+ fetchTitles(grouped.map((s) => s.session_id));
177
+ })
178
+ .catch(() => setSessionsLoaded(true));
179
+ }, [slug, fetchTitles]);
180
+
181
+ useEffect(() => {
182
+ if (!agent) {
183
+ fetch("/api/agents")
184
+ .then((r) => r.json())
185
+ .then((agents: AgentMeta[]) => {
186
+ setAgent(agents.find((a: AgentMeta) => a.slug === slug) || null);
187
+ });
188
+ }
189
+
190
+ fetchSessions();
191
+
192
+ fetch(`/api/agents/${slug}/capabilities`)
193
+ .then((r) => r.json())
194
+ .then((data) => setCaps(data))
195
+ .catch(() => {});
196
+ // eslint-disable-next-line react-hooks/exhaustive-deps
197
+ }, [slug, fetchSessions]);
198
+
199
+ useEffect(() => {
200
+ const handler = () => fetchSessions();
201
+ window.addEventListener("agent-session-updated", handler);
202
+
203
+ const titleHandler = (e: Event) => {
204
+ const detail = (e as CustomEvent).detail as { session_id?: string; title?: string } | undefined;
205
+ if (detail?.session_id && detail?.title) {
206
+ setTitles((prev) => ({ ...prev, [detail.session_id!]: detail.title! }));
207
+ }
208
+ };
209
+ window.addEventListener("session-title-ready", titleHandler);
210
+
211
+ const startHandler = (e: Event) => {
212
+ const detail = (e as CustomEvent).detail as { session_id?: string } | undefined;
213
+ if (!detail?.session_id) return;
214
+ setSessions((prev) => {
215
+ if (prev.some((s) => s.session_id === detail.session_id)) return prev;
216
+ return [
217
+ {
218
+ session_id: detail.session_id!,
219
+ label: formatSessionDate(new Date().toISOString()),
220
+ turnCount: 1,
221
+ lastAt: new Date().toISOString(),
222
+ status: "running" as const,
223
+ },
224
+ ...prev,
225
+ ];
226
+ });
227
+ };
228
+ window.addEventListener("agent-session-started", startHandler);
229
+
230
+ return () => {
231
+ window.removeEventListener("agent-session-updated", handler);
232
+ window.removeEventListener("session-title-ready", titleHandler);
233
+ window.removeEventListener("agent-session-started", startHandler);
234
+ };
235
+ }, [fetchSessions, fetchTitles]);
236
+
237
+ const handleNewChat = () => {
238
+ router.push(`/agents/${slug}/chat`, { scroll: false });
239
+ };
240
+
241
+ const handleResumeSession = (sessionId: string) => {
242
+ router.push(`/agents/${slug}/chat?session=${sessionId}`, { scroll: false });
243
+ };
244
+
245
+ const handleDeleteSession = async (sessionId: string) => {
246
+ setDeletingSession(sessionId);
247
+ try {
248
+ const res = await fetch(`/api/runs?session_id=${sessionId}`, { method: "DELETE" });
249
+ if (!res.ok) return;
250
+ setSessions((prev) => prev.filter((s) => s.session_id !== sessionId));
251
+ if (activeSession === sessionId) {
252
+ router.push(`/agents/${slug}/chat`, { scroll: false });
253
+ }
254
+ window.dispatchEvent(new Event("agent-session-updated"));
255
+ } finally {
256
+ setDeletingSession(null);
257
+ }
258
+ };
259
+
260
+ return (
261
+ <nav className="w-[220px] shrink-0 border-r border-[var(--border-subtle)] bg-[var(--bg-base)] flex flex-col overflow-hidden">
262
+ {/* Top bar */}
263
+ <div className="flex items-center justify-between px-3 h-12">
264
+ {slug === "main" ? (
265
+ <OverviewLink label="Overview" icon="grid" />
266
+ ) : (
267
+ <OverviewLink label="All Agents" icon="back" />
268
+ )}
269
+ <NotificationBell />
270
+ </div>
271
+
272
+ {/* Agent header */}
273
+ <div className="px-4 py-3 border-b border-[var(--border-subtle)]">
274
+ <div className="flex items-center justify-between">
275
+ <div className="flex items-center gap-2 min-w-0">
276
+ {agent?.emoji && <span className="text-base">{agent.emoji}</span>}
277
+ <span className="text-[13px] font-semibold text-[var(--text-primary)] truncate">
278
+ {agent?.name || slug}
279
+ </span>
280
+ </div>
281
+
282
+ <div className="relative flex items-center gap-1">
283
+ <button
284
+ onClick={() => setShowTooltip(!showTooltip)}
285
+ className={`flex items-center justify-center h-6 w-6 rounded-md transition-colors ${
286
+ showTooltip
287
+ ? "text-[var(--text-primary)] bg-[var(--bg-active)]"
288
+ : "text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]"
289
+ }`}
290
+ >
291
+ <InfoIcon size={14} />
292
+ </button>
293
+
294
+ {showTooltip && (
295
+ <div className="fixed inset-0 z-50 flex items-center justify-center" onClick={() => setShowTooltip(false)}>
296
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
297
+ <div
298
+ className="relative z-10 w-[420px] max-h-[80vh] rounded-lg border border-[var(--border-default)] bg-[#111113] shadow-2xl shadow-black/60 flex flex-col"
299
+ onClick={(e) => e.stopPropagation()}
300
+ >
301
+ <button
302
+ onClick={() => setShowTooltip(false)}
303
+ className="absolute top-4 right-4 z-10 flex items-center justify-center h-6 w-6 rounded-md text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]"
304
+ >
305
+ <XIcon size={14} />
306
+ </button>
307
+
308
+ <div className="overflow-y-auto p-6 pr-6">
309
+ <div className="flex items-start gap-3 mb-4 pr-8">
310
+ {agent?.emoji && <span className="text-2xl">{agent.emoji}</span>}
311
+ <div className="min-w-0">
312
+ <h2 className="text-[15px] font-semibold text-[var(--text-primary)]">{agent?.name}</h2>
313
+ {agent?.vibe && (
314
+ <p className="text-[11px] text-[var(--text-muted)] italic mt-0.5">{agent.vibe}</p>
315
+ )}
316
+ </div>
317
+ </div>
318
+ <p className="text-[13px] text-[var(--text-secondary)] leading-relaxed mb-5">
319
+ {agent?.description}
320
+ </p>
321
+
322
+ {/* Meta chips */}
323
+ {(agent?.model || agent?.color) && (
324
+ <div className="flex flex-wrap items-center gap-2 mb-5">
325
+ {agent.model && (
326
+ <span className="inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] font-medium border border-[var(--accent-muted)] bg-[var(--accent-muted)] text-[var(--accent-text)]">
327
+ {agent.model}
328
+ </span>
329
+ )}
330
+ {agent.color && (
331
+ <span
332
+ className="h-3.5 w-3.5 rounded-full border border-[var(--border-default)] shrink-0"
333
+ style={{ backgroundColor: agent.color }}
334
+ />
335
+ )}
336
+ </div>
337
+ )}
338
+
339
+ {/* Capability sections */}
340
+ {caps && (
341
+ <div className="space-y-4">
342
+ <DetailSection title="Tools" count={caps.tools.length} defaultOpen>
343
+ <div className="flex flex-wrap gap-1.5">
344
+ {caps.tools.map((t) => (
345
+ <ToolBadge
346
+ key={t}
347
+ name={t}
348
+ className="rounded-md px-2 py-0.5 text-[11px] font-medium border border-[var(--accent-muted)] bg-[var(--accent-muted)] text-[var(--accent-text)] cursor-help"
349
+ />
350
+ ))}
351
+ </div>
352
+ </DetailSection>
353
+
354
+ <DetailSection title="Integrations" count={caps.mcpServers.length} defaultOpen>
355
+ <div className="flex flex-wrap gap-1.5">
356
+ {caps.mcpServers.map((s) => (
357
+ <span
358
+ key={s.name}
359
+ className={`rounded-md px-2 py-0.5 text-[11px] font-medium border ${
360
+ s.type === "remote"
361
+ ? "border-green-500/15 bg-green-500/8 text-green-400/80"
362
+ : "border-amber-500/15 bg-amber-500/8 text-amber-400/80"
363
+ }`}
364
+ >
365
+ <span className="opacity-50 text-[9px] mr-1">{s.type === "remote" ? "CLOUD" : "LOCAL"}</span>
366
+ {s.name}
367
+ </span>
368
+ ))}
369
+ </div>
370
+ </DetailSection>
371
+
372
+ <DetailSection title="Subagents" count={caps.subagents.length}>
373
+ <div className="space-y-2">
374
+ {caps.subagents.map((a) => (
375
+ <div key={a.slug} className="flex items-start gap-2">
376
+ {a.emoji && <span className="text-sm mt-0.5">{a.emoji}</span>}
377
+ <div className="min-w-0">
378
+ <p className="text-[12px] font-medium text-[var(--text-secondary)]">{a.name}</p>
379
+ <p className="text-[11px] text-[var(--text-muted)] leading-snug">{a.description}</p>
380
+ </div>
381
+ </div>
382
+ ))}
383
+ </div>
384
+ </DetailSection>
385
+
386
+ <DetailSection title="Skills" count={caps.skills.length}>
387
+ {(() => {
388
+ const grouped: Record<string, typeof caps.skills> = {};
389
+ for (const s of caps.skills) {
390
+ const cat = s.category || "Other";
391
+ if (!grouped[cat]) grouped[cat] = [];
392
+ grouped[cat].push(s);
393
+ }
394
+ return (
395
+ <div className="space-y-3">
396
+ {Object.entries(grouped).map(([category, skills]) => (
397
+ <div key={category}>
398
+ <p className="text-[10px] font-medium uppercase tracking-wider text-[var(--text-muted)] mb-1.5">{category}</p>
399
+ <div className="flex flex-wrap gap-1.5">
400
+ {skills.map((s) => (
401
+ <span key={s.slug} className="rounded-md px-2 py-0.5 text-[11px] font-medium border border-[var(--border-subtle)] bg-[var(--bg-raised)] text-[var(--text-tertiary)]">
402
+ {s.name}
403
+ </span>
404
+ ))}
405
+ </div>
406
+ </div>
407
+ ))}
408
+ </div>
409
+ );
410
+ })()}
411
+ </DetailSection>
412
+
413
+ <DetailSection title="Commands" count={caps.commands.length}>
414
+ <div className="flex flex-wrap gap-1.5">
415
+ {caps.commands.map((c) => (
416
+ <span key={c.slug} className="rounded-md px-2 py-0.5 text-[11px] font-mono border border-[var(--border-subtle)] bg-[var(--bg-raised)] text-[var(--text-tertiary)]">
417
+ /{c.slug}
418
+ </span>
419
+ ))}
420
+ </div>
421
+ </DetailSection>
422
+ </div>
423
+ )}
424
+ </div>
425
+ </div>
426
+ </div>
427
+ )}
428
+ </div>
429
+ </div>
430
+ </div>
431
+
432
+ {/* New chat button */}
433
+ <div className="px-2 pt-2">
434
+ <button
435
+ onClick={handleNewChat}
436
+ className={`w-full flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-[12px] transition-colors ${
437
+ !activeSession
438
+ ? "border-[var(--border-default)] bg-[var(--bg-elevated)] text-[var(--text-primary)]"
439
+ : "border-[var(--border-subtle)] bg-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:border-[var(--border-default)] hover:bg-[var(--bg-hover)]"
440
+ }`}
441
+ >
442
+ <PlusIcon size={14} />
443
+ New chat
444
+ </button>
445
+ </div>
446
+
447
+ {/* Sessions list */}
448
+ <div className="flex-1 overflow-y-auto px-2 py-2 space-y-px">
449
+ {!sessionsLoaded ? (
450
+ <div className="space-y-1 px-1 pt-1">
451
+ {[...Array(4)].map((_, i) => (
452
+ <div key={i} className="h-9 rounded-md bg-[var(--bg-raised)] animate-pulse" />
453
+ ))}
454
+ </div>
455
+ ) : sessions.length === 0 ? (
456
+ <p className="px-3 py-4 text-[11px] text-[var(--text-muted)] text-center">
457
+ No conversations yet
458
+ </p>
459
+ ) : (
460
+ sessions.map((s) => {
461
+ const isActive = activeSession === s.session_id;
462
+ const isDeleting = deletingSession === s.session_id;
463
+ return (
464
+ <div
465
+ key={s.session_id}
466
+ className={`group relative rounded-md transition-colors ${
467
+ isActive
468
+ ? "bg-[var(--bg-active)]"
469
+ : "hover:bg-[var(--bg-hover)]"
470
+ } ${isDeleting ? "opacity-50 pointer-events-none" : ""}`}
471
+ >
472
+ <button
473
+ onClick={() => handleResumeSession(s.session_id)}
474
+ className="w-full text-left px-2.5 py-1.5"
475
+ >
476
+ <p className={`text-[12px] truncate pr-5 ${isActive ? "text-[var(--text-primary)]" : "text-[var(--text-secondary)]"}`}>
477
+ {titles[s.session_id] || s.label}
478
+ </p>
479
+ <p className="text-[10px] text-[var(--text-muted)] mt-0.5">
480
+ {s.turnCount} turn{s.turnCount > 1 ? "s" : ""} · {timeAgo(s.lastAt)}
481
+ </p>
482
+ </button>
483
+ <button
484
+ onClick={(e) => {
485
+ e.stopPropagation();
486
+ handleDeleteSession(s.session_id);
487
+ }}
488
+ className="absolute right-1.5 top-1/2 -translate-y-1/2 flex items-center justify-center h-6 w-6 rounded-md opacity-0 group-hover:opacity-100 text-[var(--text-muted)] hover:text-red-400 hover:bg-[var(--bg-hover)] transition-all"
489
+ >
490
+ <TrashIcon size={12} />
491
+ </button>
492
+ </div>
493
+ );
494
+ })
495
+ )}
496
+ </div>
497
+
498
+ {/* Profile — pinned to bottom */}
499
+ <div className="border-t border-[var(--border-subtle)] px-2 py-2">
500
+ <ProfileMenu />
501
+ </div>
502
+ </nav>
503
+ );
504
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import { usePathname } from "next/navigation";
4
+ import { Sidebar } from "./sidebar";
5
+ import { OverviewProvider } from "@/components/overview/overview-context";
6
+ import { OverviewInner } from "./overview-inner";
7
+ import { AuthProvider } from "@/components/auth/auth-context";
8
+
9
+ export function AppShell({ children }: { children: React.ReactNode }) {
10
+ const pathname = usePathname();
11
+
12
+ // Login and not-authorized pages render standalone — no sidebar or overview shell
13
+ if (pathname === "/login" || pathname === "/not-authorized") {
14
+ return <AuthProvider>{children}</AuthProvider>;
15
+ }
16
+
17
+ return (
18
+ <AuthProvider>
19
+ <OverviewProvider pathname={pathname}>
20
+ <div className="flex h-screen">
21
+ <Sidebar />
22
+ <main className="flex-1 overflow-auto px-8 py-6">
23
+ <OverviewInner>{children}</OverviewInner>
24
+ </main>
25
+ </div>
26
+ </OverviewProvider>
27
+ </AuthProvider>
28
+ );
29
+ }
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useOverview, type OverviewTab } from "@/components/overview/overview-context";
5
+ import { useAuth } from "@/components/auth/auth-context";
6
+ import { ProfileMenu } from "./profile-menu";
7
+ import { NotificationBell } from "@/components/notifications/notification-bell";
8
+ import {
9
+ BrainIcon,
10
+ AgentsIcon,
11
+ BoltIcon,
12
+ TerminalIcon,
13
+ HistoryIcon,
14
+ ScheduleIcon,
15
+ ShieldIcon,
16
+ GearIcon,
17
+ } from "@/components/icons";
18
+
19
+ const tabs: {
20
+ tab: OverviewTab;
21
+ label: string;
22
+ Icon: React.ComponentType<{ size?: number; className?: string }>;
23
+ adminOnly?: boolean;
24
+ }[] = [
25
+ { tab: "agents", label: "Agents", Icon: AgentsIcon },
26
+ { tab: "skills", label: "Skills", Icon: BoltIcon },
27
+ { tab: "commands", label: "Commands", Icon: TerminalIcon },
28
+ { tab: "runs", label: "Runs", Icon: HistoryIcon },
29
+ { tab: "schedules", label: "Schedules", Icon: ScheduleIcon },
30
+ { tab: "governance", label: "Governance", Icon: ShieldIcon, adminOnly: true },
31
+ { tab: "settings", label: "Settings", Icon: GearIcon, adminOnly: true },
32
+ ];
33
+
34
+ export function Nav() {
35
+ const { activeTab, setTab } = useOverview();
36
+ const { role } = useAuth();
37
+
38
+ return (
39
+ <nav className="w-[220px] shrink-0 border-r border-[var(--border-subtle)] bg-[var(--bg-base)] flex flex-col">
40
+ {/* Header */}
41
+ <div className="flex items-center justify-between px-4 h-12">
42
+ <span className="text-[13px] font-semibold text-[var(--text-primary)]">Overview</span>
43
+ <NotificationBell />
44
+ </div>
45
+
46
+ {/* Nav items */}
47
+ <div className="flex flex-col gap-px px-2 flex-1">
48
+ {/* PAI Agent — real navigation */}
49
+ <Link
50
+ href="/"
51
+ className="flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-[13px] transition-colors text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]"
52
+ >
53
+ <BrainIcon size={16} className="shrink-0 opacity-70" />
54
+ <span>PAI Agent</span>
55
+ </Link>
56
+
57
+ <div className="h-px bg-[var(--border-subtle)] my-1.5 mx-1" />
58
+
59
+ {/* Overview tabs */}
60
+ {tabs
61
+ .filter(({ adminOnly }) => !adminOnly || role === "admin")
62
+ .map(({ tab, label, Icon }) => {
63
+ const isActive = activeTab === tab;
64
+ return (
65
+ <button
66
+ key={tab}
67
+ onClick={() => setTab(tab)}
68
+ className={`flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-[13px] text-left transition-colors ${
69
+ isActive
70
+ ? "bg-[var(--bg-active)] text-[var(--text-primary)]"
71
+ : "text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)]"
72
+ }`}
73
+ >
74
+ <Icon size={16} className={`shrink-0 ${isActive ? "opacity-90" : "opacity-60"}`} />
75
+ <span>{label}</span>
76
+ </button>
77
+ );
78
+ })}
79
+ </div>
80
+
81
+ {/* Profile — pinned to bottom */}
82
+ <div className="border-t border-[var(--border-subtle)] px-2 py-2">
83
+ <ProfileMenu />
84
+ </div>
85
+ </nav>
86
+ );
87
+ }
@@ -0,0 +1,14 @@
1
+ "use client";
2
+
3
+ import { useOverview } from "@/components/overview/overview-context";
4
+ import { OverviewContent } from "@/components/overview/overview-content";
5
+
6
+ export function OverviewInner({ children }: { children: React.ReactNode }) {
7
+ const { isOverview } = useOverview();
8
+
9
+ if (isOverview) {
10
+ return <OverviewContent />;
11
+ }
12
+
13
+ return <>{children}</>;
14
+ }