@a5c-ai/babysitter-observer-dashboard 1.0.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 (205) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +490 -0
  3. package/next.config.mjs +25 -0
  4. package/package.json +104 -0
  5. package/postcss.config.mjs +8 -0
  6. package/src/app/actions/__tests__/approve-breakpoint.test.ts +246 -0
  7. package/src/app/actions/approve-breakpoint.ts +145 -0
  8. package/src/app/api/config/route.ts +137 -0
  9. package/src/app/api/digest/route.ts +45 -0
  10. package/src/app/api/runs/[runId]/events/route.ts +56 -0
  11. package/src/app/api/runs/[runId]/route.ts +84 -0
  12. package/src/app/api/runs/[runId]/tasks/[effectId]/route.ts +44 -0
  13. package/src/app/api/runs/route.ts +48 -0
  14. package/src/app/api/stream/route.ts +136 -0
  15. package/src/app/api/test/route.ts +1 -0
  16. package/src/app/api/version/route.ts +57 -0
  17. package/src/app/globals.css +555 -0
  18. package/src/app/icon.svg +20 -0
  19. package/src/app/layout.tsx +39 -0
  20. package/src/app/not-found.tsx +16 -0
  21. package/src/app/page.tsx +120 -0
  22. package/src/app/runs/[runId]/page.tsx +279 -0
  23. package/src/cli.ts +271 -0
  24. package/src/components/breakpoint/__tests__/breakpoint-approval.test.tsx +212 -0
  25. package/src/components/breakpoint/__tests__/breakpoint-panel.test.tsx +130 -0
  26. package/src/components/breakpoint/__tests__/file-preview.test.tsx +313 -0
  27. package/src/components/breakpoint/breakpoint-approval.tsx +138 -0
  28. package/src/components/breakpoint/breakpoint-panel.tsx +95 -0
  29. package/src/components/breakpoint/file-preview.tsx +215 -0
  30. package/src/components/dashboard/.gitkeep +0 -0
  31. package/src/components/dashboard/__tests__/breakpoint-banner.test.tsx +177 -0
  32. package/src/components/dashboard/__tests__/catch-up-banner.test.tsx +141 -0
  33. package/src/components/dashboard/__tests__/executive-summary-banner.test.tsx +164 -0
  34. package/src/components/dashboard/__tests__/kpi-grid.test.tsx +101 -0
  35. package/src/components/dashboard/__tests__/pagination-controls.test.tsx +125 -0
  36. package/src/components/dashboard/__tests__/project-accordion.test.tsx +97 -0
  37. package/src/components/dashboard/__tests__/project-list-view.test.tsx +174 -0
  38. package/src/components/dashboard/__tests__/project-search-input.test.tsx +110 -0
  39. package/src/components/dashboard/__tests__/project-section-header.test.tsx +91 -0
  40. package/src/components/dashboard/__tests__/project-section.test.tsx +151 -0
  41. package/src/components/dashboard/__tests__/run-card.test.tsx +164 -0
  42. package/src/components/dashboard/__tests__/run-filter-bar.test.tsx +109 -0
  43. package/src/components/dashboard/__tests__/run-list.test.tsx +123 -0
  44. package/src/components/dashboard/__tests__/search-filter.test.tsx +150 -0
  45. package/src/components/dashboard/__tests__/virtualized-run-list.test.tsx +179 -0
  46. package/src/components/dashboard/breakpoint-banner.tsx +301 -0
  47. package/src/components/dashboard/catch-up-banner.tsx +88 -0
  48. package/src/components/dashboard/executive-summary-banner.tsx +174 -0
  49. package/src/components/dashboard/global-search.tsx +323 -0
  50. package/src/components/dashboard/kpi-grid.tsx +140 -0
  51. package/src/components/dashboard/pagination-controls.tsx +100 -0
  52. package/src/components/dashboard/project-accordion.tsx +72 -0
  53. package/src/components/dashboard/project-health-card.tsx +536 -0
  54. package/src/components/dashboard/project-list-view.tsx +246 -0
  55. package/src/components/dashboard/project-search-input.tsx +41 -0
  56. package/src/components/dashboard/project-section-header.tsx +73 -0
  57. package/src/components/dashboard/project-section.tsx +89 -0
  58. package/src/components/dashboard/run-card.tsx +218 -0
  59. package/src/components/dashboard/run-filter-bar.tsx +100 -0
  60. package/src/components/dashboard/run-list.tsx +77 -0
  61. package/src/components/dashboard/search-filter.tsx +69 -0
  62. package/src/components/dashboard/virtualized-run-list.tsx +130 -0
  63. package/src/components/details/.gitkeep +0 -0
  64. package/src/components/details/__tests__/agent-panel.test.tsx +236 -0
  65. package/src/components/details/__tests__/json-tree.test.tsx +347 -0
  66. package/src/components/details/__tests__/log-viewer.test.tsx +168 -0
  67. package/src/components/details/__tests__/task-detail.test.tsx +212 -0
  68. package/src/components/details/__tests__/timing-panel.test.tsx +271 -0
  69. package/src/components/details/agent-panel.tsx +234 -0
  70. package/src/components/details/json-tree/categorize.ts +131 -0
  71. package/src/components/details/json-tree/index.tsx +120 -0
  72. package/src/components/details/json-tree/json-node.tsx +223 -0
  73. package/src/components/details/json-tree/smart-summary.tsx +596 -0
  74. package/src/components/details/json-tree/tree-controls.tsx +47 -0
  75. package/src/components/details/json-tree.tsx +9 -0
  76. package/src/components/details/log-viewer.tsx +140 -0
  77. package/src/components/details/task-detail.tsx +114 -0
  78. package/src/components/details/timing-panel.tsx +247 -0
  79. package/src/components/events/.gitkeep +0 -0
  80. package/src/components/events/__tests__/event-item.test.tsx +211 -0
  81. package/src/components/events/__tests__/event-stream.test.tsx +225 -0
  82. package/src/components/events/event-item.tsx +121 -0
  83. package/src/components/events/event-stream.tsx +260 -0
  84. package/src/components/notifications/.gitkeep +0 -0
  85. package/src/components/notifications/__tests__/notification-panel.test.tsx +287 -0
  86. package/src/components/notifications/__tests__/notification-provider.test.tsx +585 -0
  87. package/src/components/notifications/__tests__/toast-stack.test.tsx +217 -0
  88. package/src/components/notifications/notification-panel.tsx +124 -0
  89. package/src/components/notifications/notification-provider.tsx +175 -0
  90. package/src/components/notifications/toast-stack.tsx +75 -0
  91. package/src/components/pipeline/.gitkeep +0 -0
  92. package/src/components/pipeline/__tests__/parallel-group.test.tsx +88 -0
  93. package/src/components/pipeline/__tests__/pipeline-view.test.tsx +345 -0
  94. package/src/components/pipeline/__tests__/step-card.test.tsx +330 -0
  95. package/src/components/pipeline/parallel-group.tsx +39 -0
  96. package/src/components/pipeline/pipeline-view.tsx +197 -0
  97. package/src/components/pipeline/step-card.tsx +166 -0
  98. package/src/components/providers/event-stream-provider.tsx +29 -0
  99. package/src/components/providers.tsx +24 -0
  100. package/src/components/shared/.gitkeep +0 -0
  101. package/src/components/shared/__tests__/empty-state.test.tsx +49 -0
  102. package/src/components/shared/__tests__/friendly-id.test.tsx +47 -0
  103. package/src/components/shared/__tests__/kbd.test.tsx +45 -0
  104. package/src/components/shared/__tests__/kind-badge.test.tsx +71 -0
  105. package/src/components/shared/__tests__/metrics-row.test.tsx +74 -0
  106. package/src/components/shared/__tests__/outcome-banner.test.tsx +71 -0
  107. package/src/components/shared/__tests__/progress-bar.test.tsx +89 -0
  108. package/src/components/shared/__tests__/session-pill.test.tsx +62 -0
  109. package/src/components/shared/__tests__/settings-modal.test.tsx +201 -0
  110. package/src/components/shared/__tests__/shortcuts-help.test.tsx +103 -0
  111. package/src/components/shared/__tests__/status-badge.test.tsx +98 -0
  112. package/src/components/shared/__tests__/theme-provider.test.tsx +100 -0
  113. package/src/components/shared/__tests__/truncated-id.test.tsx +53 -0
  114. package/src/components/shared/app-footer.tsx +80 -0
  115. package/src/components/shared/app-header.tsx +160 -0
  116. package/src/components/shared/empty-state.tsx +18 -0
  117. package/src/components/shared/error-boundary.tsx +81 -0
  118. package/src/components/shared/friendly-id.tsx +48 -0
  119. package/src/components/shared/kbd.tsx +15 -0
  120. package/src/components/shared/kind-badge.tsx +51 -0
  121. package/src/components/shared/metrics-row.tsx +106 -0
  122. package/src/components/shared/outcome-banner.tsx +56 -0
  123. package/src/components/shared/progress-bar.tsx +42 -0
  124. package/src/components/shared/session-pill.tsx +69 -0
  125. package/src/components/shared/settings-modal.tsx +509 -0
  126. package/src/components/shared/shortcuts-help.tsx +113 -0
  127. package/src/components/shared/status-badge.tsx +110 -0
  128. package/src/components/shared/theme-provider.tsx +46 -0
  129. package/src/components/shared/truncated-id.tsx +51 -0
  130. package/src/components/ui/.gitkeep +0 -0
  131. package/src/components/ui/__tests__/accordion.test.tsx +96 -0
  132. package/src/components/ui/__tests__/badge.test.tsx +69 -0
  133. package/src/components/ui/__tests__/button.test.tsx +113 -0
  134. package/src/components/ui/__tests__/tabs.test.tsx +75 -0
  135. package/src/components/ui/__tests__/tooltip.test.tsx +90 -0
  136. package/src/components/ui/accordion.tsx +61 -0
  137. package/src/components/ui/badge.tsx +25 -0
  138. package/src/components/ui/button.tsx +40 -0
  139. package/src/components/ui/card.tsx +21 -0
  140. package/src/components/ui/scroll-area.tsx +35 -0
  141. package/src/components/ui/separator.tsx +24 -0
  142. package/src/components/ui/tabs.tsx +64 -0
  143. package/src/components/ui/tooltip.tsx +37 -0
  144. package/src/hooks/.gitkeep +0 -0
  145. package/src/hooks/__tests__/use-animated-number.test.ts +184 -0
  146. package/src/hooks/__tests__/use-batched-updates.test.ts +315 -0
  147. package/src/hooks/__tests__/use-event-stream.test.ts +243 -0
  148. package/src/hooks/__tests__/use-keyboard.test.ts +217 -0
  149. package/src/hooks/__tests__/use-notifications.test.ts +230 -0
  150. package/src/hooks/__tests__/use-polling.test.ts +274 -0
  151. package/src/hooks/__tests__/use-project-runs.test.ts +163 -0
  152. package/src/hooks/__tests__/use-projects.test.ts +248 -0
  153. package/src/hooks/__tests__/use-run-dashboard.test.ts +168 -0
  154. package/src/hooks/__tests__/use-run-detail.test.ts +273 -0
  155. package/src/hooks/__tests__/use-smart-polling.test.ts +305 -0
  156. package/src/hooks/use-animated-number.ts +87 -0
  157. package/src/hooks/use-batched-updates.ts +150 -0
  158. package/src/hooks/use-event-stream.ts +150 -0
  159. package/src/hooks/use-keyboard.ts +45 -0
  160. package/src/hooks/use-notifications.ts +82 -0
  161. package/src/hooks/use-persisted-state.ts +60 -0
  162. package/src/hooks/use-polling.ts +60 -0
  163. package/src/hooks/use-project-runs.ts +51 -0
  164. package/src/hooks/use-projects.ts +26 -0
  165. package/src/hooks/use-run-dashboard.ts +207 -0
  166. package/src/hooks/use-run-detail.ts +77 -0
  167. package/src/hooks/use-smart-polling.ts +144 -0
  168. package/src/lib/.gitkeep +0 -0
  169. package/src/lib/__tests__/cn.test.ts +69 -0
  170. package/src/lib/__tests__/config-loader.test.ts +210 -0
  171. package/src/lib/__tests__/config.test.ts +561 -0
  172. package/src/lib/__tests__/error-handler.test.ts +143 -0
  173. package/src/lib/__tests__/fetcher.test.ts +517 -0
  174. package/src/lib/__tests__/global-registry.test.ts +214 -0
  175. package/src/lib/__tests__/parser.test.ts +1532 -0
  176. package/src/lib/__tests__/path-resolver.test.ts +112 -0
  177. package/src/lib/__tests__/run-cache.test.ts +591 -0
  178. package/src/lib/__tests__/server-init.test.ts +512 -0
  179. package/src/lib/__tests__/source-discovery.test.ts +246 -0
  180. package/src/lib/__tests__/utils.test.ts +160 -0
  181. package/src/lib/__tests__/watcher.test.ts +227 -0
  182. package/src/lib/cn.ts +6 -0
  183. package/src/lib/config-loader.ts +195 -0
  184. package/src/lib/config.ts +20 -0
  185. package/src/lib/error-handler.ts +76 -0
  186. package/src/lib/fetcher.ts +394 -0
  187. package/src/lib/global-registry.ts +117 -0
  188. package/src/lib/parser.ts +794 -0
  189. package/src/lib/path-resolver.ts +16 -0
  190. package/src/lib/run-cache.ts +404 -0
  191. package/src/lib/server-init.ts +226 -0
  192. package/src/lib/services/__tests__/run-query-service.test.ts +819 -0
  193. package/src/lib/services/run-query-service.ts +286 -0
  194. package/src/lib/source-discovery.ts +216 -0
  195. package/src/lib/utils.ts +103 -0
  196. package/src/lib/watcher.ts +265 -0
  197. package/src/test/fixtures.ts +269 -0
  198. package/src/test/mocks/handlers.ts +110 -0
  199. package/src/test/mocks/server.ts +17 -0
  200. package/src/test/setup.ts +200 -0
  201. package/src/test/test-utils.tsx +36 -0
  202. package/src/types/.gitkeep +0 -0
  203. package/src/types/breakpoint.ts +17 -0
  204. package/src/types/index.ts +214 -0
  205. package/tsconfig.json +50 -0
@@ -0,0 +1,51 @@
1
+ import { cn } from "@/lib/cn";
2
+ import type { TaskKind } from "@/types";
3
+ import { Bot, Terminal, Puzzle, Hand, Clock, Cog } from "lucide-react";
4
+
5
+ const kindConfig: Record<TaskKind, { icon: React.ReactNode; color: string; bgTint: string }> = {
6
+ agent: {
7
+ icon: <Bot className="h-3 w-3" />,
8
+ color: "text-primary",
9
+ bgTint: "bg-primary-muted",
10
+ },
11
+ node: {
12
+ icon: <Cog className="h-3 w-3" />,
13
+ color: "text-warning",
14
+ bgTint: "bg-warning-muted",
15
+ },
16
+ shell: {
17
+ icon: <Terminal className="h-3 w-3" />,
18
+ color: "text-foreground-secondary",
19
+ bgTint: "bg-background-secondary",
20
+ },
21
+ skill: {
22
+ icon: <Puzzle className="h-3 w-3" />,
23
+ color: "text-info",
24
+ bgTint: "bg-info-muted",
25
+ },
26
+ breakpoint: {
27
+ icon: <Hand className="h-3 w-3" />,
28
+ color: "text-warning",
29
+ bgTint: "bg-warning-muted",
30
+ },
31
+ sleep: {
32
+ icon: <Clock className="h-3 w-3" />,
33
+ color: "text-foreground-muted",
34
+ bgTint: "bg-background-secondary",
35
+ },
36
+ };
37
+
38
+ export function KindBadge({ kind, className }: { kind: TaskKind; className?: string }) {
39
+ const config = kindConfig[kind] || kindConfig.agent;
40
+ return (
41
+ <span className={cn(
42
+ "inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs leading-tight font-medium uppercase tracking-wider",
43
+ config.bgTint,
44
+ config.color,
45
+ className
46
+ )}>
47
+ {config.icon}
48
+ {kind === "breakpoint" ? "approval" : kind}
49
+ </span>
50
+ );
51
+ }
@@ -0,0 +1,106 @@
1
+ import { Clock, CheckCircle2, Percent, RefreshCw, Loader2, Pause } from "lucide-react";
2
+ import { cn } from "@/lib/cn";
3
+ import { formatDuration } from "@/lib/utils";
4
+ import type { Run } from "@/types";
5
+
6
+ interface MetricsRowProps {
7
+ run: Run;
8
+ }
9
+
10
+ interface MetricCardProps {
11
+ icon: React.ReactNode;
12
+ label: string;
13
+ value: string;
14
+ valueColor?: string;
15
+ testId?: string;
16
+ }
17
+
18
+ function MetricCard({ icon, label, value, valueColor, testId }: MetricCardProps) {
19
+ return (
20
+ <div data-testid={testId} className="bg-background-secondary/60 rounded-lg px-4 py-2.5 flex items-center gap-3">
21
+ <div className="text-foreground-muted">{icon}</div>
22
+ <div className="flex flex-col">
23
+ <span className="text-xs leading-tight text-foreground-muted uppercase tracking-wider font-medium">{label}</span>
24
+ <span className={cn("text-lg font-semibold tabular-nums", valueColor || "text-foreground")}>{value}</span>
25
+ </div>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ export function MetricsRow({ run }: MetricsRowProps) {
31
+ const isTerminal = run.status === "completed" || run.status === "failed";
32
+ const isRunning = run.status === "pending";
33
+ const isWaiting = run.status === "waiting";
34
+
35
+ const successRate =
36
+ run.totalTasks > 0
37
+ ? Math.round((run.completedTasks / run.totalTasks) * 100)
38
+ : 0;
39
+
40
+ // Count unique iterations from invocationKeys
41
+ const iterationKeys = new Set(run.tasks.map((t) => t.invocationKey));
42
+ const iterationCount = iterationKeys.size;
43
+
44
+ // Semantic color for success rate (neon palette)
45
+ const successRateColor = successRate === 100
46
+ ? "text-success"
47
+ : successRate >= 80
48
+ ? "text-foreground"
49
+ : successRate >= 50
50
+ ? "text-warning"
51
+ : "text-error";
52
+
53
+ // Duration display and color
54
+ const durationValue = isTerminal ? formatDuration(run.duration) : formatDuration(run.duration) || "...";
55
+ const durationColor = isRunning ? "text-info" : isWaiting ? "text-warning" : undefined;
56
+
57
+ // Status-specific icon for duration
58
+ const durationIcon = isRunning
59
+ ? <Loader2 className="h-4 w-4 animate-spin-smooth text-info drop-shadow-[var(--drop-glow-cyan)]" />
60
+ : isWaiting
61
+ ? <Pause className="h-4 w-4 text-warning" />
62
+ : <Clock className="h-4 w-4" />;
63
+
64
+ return (
65
+ <div data-testid="metrics-row" className={cn(
66
+ "flex items-center divide-x divide-primary/10 px-4 py-3 border-b border-border bg-background",
67
+ (isRunning || isWaiting) && "opacity-90"
68
+ )}>
69
+ <div className="flex items-center pr-3">
70
+ <MetricCard
71
+ icon={durationIcon}
72
+ label="Total Duration"
73
+ value={durationValue}
74
+ valueColor={durationColor}
75
+ testId="metric-total-duration"
76
+ />
77
+ </div>
78
+ <div className="flex items-center px-3">
79
+ <MetricCard
80
+ icon={<CheckCircle2 className="h-4 w-4" />}
81
+ label="Tasks"
82
+ value={`${run.completedTasks}/${run.totalTasks}`}
83
+ valueColor="text-success"
84
+ testId="metric-tasks"
85
+ />
86
+ </div>
87
+ <div className="flex items-center px-3">
88
+ <MetricCard
89
+ icon={<Percent className="h-4 w-4" />}
90
+ label="Success Rate"
91
+ value={`${successRate}%`}
92
+ valueColor={successRateColor}
93
+ testId="metric-success-rate"
94
+ />
95
+ </div>
96
+ <div className="flex items-center pl-3">
97
+ <MetricCard
98
+ icon={<RefreshCw className="h-4 w-4" />}
99
+ label="Iterations"
100
+ value={String(iterationCount)}
101
+ testId="metric-iterations"
102
+ />
103
+ </div>
104
+ </div>
105
+ );
106
+ }
@@ -0,0 +1,56 @@
1
+ import { CheckCircle2, XCircle } from "lucide-react";
2
+ import { formatDuration } from "@/lib/utils";
3
+ import type { Run } from "@/types";
4
+
5
+ interface OutcomeBannerProps {
6
+ run: Run;
7
+ }
8
+
9
+ export function OutcomeBanner({ run }: OutcomeBannerProps) {
10
+ if (run.status === "completed") {
11
+ return (
12
+ <div data-testid="outcome-banner" data-status="completed" className="bg-success-muted border-b-2 border-success/30 px-5 py-4 shadow-glow-success">
13
+ <div className="flex items-center gap-3 text-success">
14
+ <CheckCircle2 className="h-5 w-5 shrink-0 drop-shadow-[var(--drop-glow-success)]" />
15
+ <span className="text-base font-medium">
16
+ Completed in {formatDuration(run.duration)}
17
+ </span>
18
+ </div>
19
+ </div>
20
+ );
21
+ }
22
+
23
+ if (run.status === "failed") {
24
+ const failedTask = run.tasks.find((t) => t.status === "error");
25
+
26
+ // Determine step name: prefer task-level label, then run-level failedStep, then "process error" for run-level failures
27
+ const stepName = failedTask?.label || run.failedStep || (run.failureMessage ? "process error" : "unknown step");
28
+
29
+ // Determine error message: prefer task-level error, then run-level failure message from RUN_FAILED event
30
+ const rawErrorMessage = failedTask?.error?.message || run.failureMessage || "An error occurred";
31
+
32
+ // Format the error message — if it looks like JSON, try to extract the most relevant part
33
+ let errorMessage = rawErrorMessage;
34
+ if (rawErrorMessage.startsWith("{") || rawErrorMessage.startsWith("[")) {
35
+ try {
36
+ const parsed = JSON.parse(rawErrorMessage);
37
+ errorMessage = parsed.message || parsed.error || parsed.reason || rawErrorMessage;
38
+ } catch {
39
+ // Not valid JSON, use as-is
40
+ }
41
+ }
42
+
43
+ return (
44
+ <div data-testid="outcome-banner" data-status="failed" className="bg-error-muted border-b-2 border-error/30 px-5 py-4 shadow-glow-error">
45
+ <div className="flex items-center gap-3 text-error">
46
+ <XCircle className="h-5 w-5 shrink-0 drop-shadow-[var(--drop-glow-error)]" />
47
+ <span className="text-base font-medium">
48
+ Failed at step: {stepName} &mdash; {errorMessage}
49
+ </span>
50
+ </div>
51
+ </div>
52
+ );
53
+ }
54
+
55
+ return null;
56
+ }
@@ -0,0 +1,42 @@
1
+ import { cn } from "@/lib/cn";
2
+
3
+ type ProgressVariant = "default" | "success" | "error" | "warning";
4
+
5
+ const variantStyles: Record<ProgressVariant, string> = {
6
+ default: "bg-primary",
7
+ success: "bg-success",
8
+ error: "bg-error",
9
+ warning: "bg-warning",
10
+ };
11
+
12
+ const variantGlow: Record<ProgressVariant, string> = {
13
+ default: "shadow-progress-glow-primary",
14
+ success: "shadow-progress-glow-success",
15
+ error: "shadow-progress-glow-error",
16
+ warning: "shadow-progress-glow-warning",
17
+ };
18
+
19
+ interface ProgressBarProps {
20
+ value: number; // 0-100
21
+ variant?: ProgressVariant;
22
+ glow?: boolean;
23
+ className?: string;
24
+ }
25
+
26
+ export function ProgressBar({ value, variant = "default", glow = false, className }: ProgressBarProps) {
27
+ const clamped = Math.min(100, Math.max(0, value));
28
+ const isComplete = clamped === 100;
29
+ return (
30
+ <div className={cn("h-2 w-full overflow-hidden rounded-full bg-background-muted", className)}>
31
+ <div
32
+ className={cn(
33
+ "h-full transition-all duration-500 ease-out",
34
+ variantStyles[variant],
35
+ glow && variantGlow[variant],
36
+ isComplete ? "rounded-full" : "rounded-l-full"
37
+ )}
38
+ style={{ width: `${clamped}%` }}
39
+ />
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { cn } from "@/lib/cn";
5
+ import { formatShortId } from "@/lib/utils";
6
+ import {
7
+ Tooltip,
8
+ TooltipContent,
9
+ TooltipProvider,
10
+ TooltipTrigger,
11
+ } from "@/components/ui/tooltip";
12
+
13
+ interface SessionPillProps {
14
+ sessionId?: string;
15
+ active?: boolean;
16
+ className?: string;
17
+ }
18
+
19
+ export function SessionPill({ sessionId, active = false, className }: SessionPillProps) {
20
+ const [copied, setCopied] = useState(false);
21
+
22
+ if (!sessionId) return null;
23
+
24
+ const handleCopy = (e: React.MouseEvent) => {
25
+ e.stopPropagation();
26
+ e.preventDefault();
27
+ navigator.clipboard.writeText(sessionId);
28
+ setCopied(true);
29
+ setTimeout(() => setCopied(false), 1500);
30
+ };
31
+
32
+ return (
33
+ <TooltipProvider>
34
+ <Tooltip>
35
+ <TooltipTrigger asChild>
36
+ <span
37
+ className={cn(
38
+ "relative inline-flex items-center gap-1.5 rounded-full bg-background-secondary px-2.5 py-0.5 text-xs font-mono text-foreground-muted cursor-pointer transition-all hover:bg-background-tertiary select-none",
39
+ active && "shadow-neon-glow-cyan-sm",
40
+ className
41
+ )}
42
+ onClick={handleCopy}
43
+ >
44
+ <span
45
+ className={cn(
46
+ "h-1.5 w-1.5 rounded-full",
47
+ active ? "bg-secondary shadow-[0_0_6px_var(--secondary)] animate-pulse-dot" : "bg-foreground-muted/40"
48
+ )}
49
+ />
50
+ {formatShortId(sessionId, 4)}
51
+ {copied && (
52
+ <span className="absolute -top-7 left-1/2 -translate-x-1/2 rounded-md bg-primary px-2 py-0.5 text-xs leading-tight font-sans font-medium text-primary-foreground whitespace-nowrap animate-slide-in-right">
53
+ Copied!
54
+ </span>
55
+ )}
56
+ </span>
57
+ </TooltipTrigger>
58
+ <TooltipContent>
59
+ <p className="font-mono text-xs">
60
+ {copied ? "Copied!" : `Session: ${sessionId}`}
61
+ </p>
62
+ {!copied && (
63
+ <p className="text-foreground-muted text-xs leading-tight mt-0.5">Click to copy</p>
64
+ )}
65
+ </TooltipContent>
66
+ </Tooltip>
67
+ </TooltipProvider>
68
+ );
69
+ }