@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.
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/next.config.mjs +25 -0
- package/package.json +104 -0
- package/postcss.config.mjs +8 -0
- package/src/app/actions/__tests__/approve-breakpoint.test.ts +246 -0
- package/src/app/actions/approve-breakpoint.ts +145 -0
- package/src/app/api/config/route.ts +137 -0
- package/src/app/api/digest/route.ts +45 -0
- package/src/app/api/runs/[runId]/events/route.ts +56 -0
- package/src/app/api/runs/[runId]/route.ts +84 -0
- package/src/app/api/runs/[runId]/tasks/[effectId]/route.ts +44 -0
- package/src/app/api/runs/route.ts +48 -0
- package/src/app/api/stream/route.ts +136 -0
- package/src/app/api/test/route.ts +1 -0
- package/src/app/api/version/route.ts +57 -0
- package/src/app/globals.css +555 -0
- package/src/app/icon.svg +20 -0
- package/src/app/layout.tsx +39 -0
- package/src/app/not-found.tsx +16 -0
- package/src/app/page.tsx +120 -0
- package/src/app/runs/[runId]/page.tsx +279 -0
- package/src/cli.ts +271 -0
- package/src/components/breakpoint/__tests__/breakpoint-approval.test.tsx +212 -0
- package/src/components/breakpoint/__tests__/breakpoint-panel.test.tsx +130 -0
- package/src/components/breakpoint/__tests__/file-preview.test.tsx +313 -0
- package/src/components/breakpoint/breakpoint-approval.tsx +138 -0
- package/src/components/breakpoint/breakpoint-panel.tsx +95 -0
- package/src/components/breakpoint/file-preview.tsx +215 -0
- package/src/components/dashboard/.gitkeep +0 -0
- package/src/components/dashboard/__tests__/breakpoint-banner.test.tsx +177 -0
- package/src/components/dashboard/__tests__/catch-up-banner.test.tsx +141 -0
- package/src/components/dashboard/__tests__/executive-summary-banner.test.tsx +164 -0
- package/src/components/dashboard/__tests__/kpi-grid.test.tsx +101 -0
- package/src/components/dashboard/__tests__/pagination-controls.test.tsx +125 -0
- package/src/components/dashboard/__tests__/project-accordion.test.tsx +97 -0
- package/src/components/dashboard/__tests__/project-list-view.test.tsx +174 -0
- package/src/components/dashboard/__tests__/project-search-input.test.tsx +110 -0
- package/src/components/dashboard/__tests__/project-section-header.test.tsx +91 -0
- package/src/components/dashboard/__tests__/project-section.test.tsx +151 -0
- package/src/components/dashboard/__tests__/run-card.test.tsx +164 -0
- package/src/components/dashboard/__tests__/run-filter-bar.test.tsx +109 -0
- package/src/components/dashboard/__tests__/run-list.test.tsx +123 -0
- package/src/components/dashboard/__tests__/search-filter.test.tsx +150 -0
- package/src/components/dashboard/__tests__/virtualized-run-list.test.tsx +179 -0
- package/src/components/dashboard/breakpoint-banner.tsx +301 -0
- package/src/components/dashboard/catch-up-banner.tsx +88 -0
- package/src/components/dashboard/executive-summary-banner.tsx +174 -0
- package/src/components/dashboard/global-search.tsx +323 -0
- package/src/components/dashboard/kpi-grid.tsx +140 -0
- package/src/components/dashboard/pagination-controls.tsx +100 -0
- package/src/components/dashboard/project-accordion.tsx +72 -0
- package/src/components/dashboard/project-health-card.tsx +536 -0
- package/src/components/dashboard/project-list-view.tsx +246 -0
- package/src/components/dashboard/project-search-input.tsx +41 -0
- package/src/components/dashboard/project-section-header.tsx +73 -0
- package/src/components/dashboard/project-section.tsx +89 -0
- package/src/components/dashboard/run-card.tsx +218 -0
- package/src/components/dashboard/run-filter-bar.tsx +100 -0
- package/src/components/dashboard/run-list.tsx +77 -0
- package/src/components/dashboard/search-filter.tsx +69 -0
- package/src/components/dashboard/virtualized-run-list.tsx +130 -0
- package/src/components/details/.gitkeep +0 -0
- package/src/components/details/__tests__/agent-panel.test.tsx +236 -0
- package/src/components/details/__tests__/json-tree.test.tsx +347 -0
- package/src/components/details/__tests__/log-viewer.test.tsx +168 -0
- package/src/components/details/__tests__/task-detail.test.tsx +212 -0
- package/src/components/details/__tests__/timing-panel.test.tsx +271 -0
- package/src/components/details/agent-panel.tsx +234 -0
- package/src/components/details/json-tree/categorize.ts +131 -0
- package/src/components/details/json-tree/index.tsx +120 -0
- package/src/components/details/json-tree/json-node.tsx +223 -0
- package/src/components/details/json-tree/smart-summary.tsx +596 -0
- package/src/components/details/json-tree/tree-controls.tsx +47 -0
- package/src/components/details/json-tree.tsx +9 -0
- package/src/components/details/log-viewer.tsx +140 -0
- package/src/components/details/task-detail.tsx +114 -0
- package/src/components/details/timing-panel.tsx +247 -0
- package/src/components/events/.gitkeep +0 -0
- package/src/components/events/__tests__/event-item.test.tsx +211 -0
- package/src/components/events/__tests__/event-stream.test.tsx +225 -0
- package/src/components/events/event-item.tsx +121 -0
- package/src/components/events/event-stream.tsx +260 -0
- package/src/components/notifications/.gitkeep +0 -0
- package/src/components/notifications/__tests__/notification-panel.test.tsx +287 -0
- package/src/components/notifications/__tests__/notification-provider.test.tsx +585 -0
- package/src/components/notifications/__tests__/toast-stack.test.tsx +217 -0
- package/src/components/notifications/notification-panel.tsx +124 -0
- package/src/components/notifications/notification-provider.tsx +175 -0
- package/src/components/notifications/toast-stack.tsx +75 -0
- package/src/components/pipeline/.gitkeep +0 -0
- package/src/components/pipeline/__tests__/parallel-group.test.tsx +88 -0
- package/src/components/pipeline/__tests__/pipeline-view.test.tsx +345 -0
- package/src/components/pipeline/__tests__/step-card.test.tsx +330 -0
- package/src/components/pipeline/parallel-group.tsx +39 -0
- package/src/components/pipeline/pipeline-view.tsx +197 -0
- package/src/components/pipeline/step-card.tsx +166 -0
- package/src/components/providers/event-stream-provider.tsx +29 -0
- package/src/components/providers.tsx +24 -0
- package/src/components/shared/.gitkeep +0 -0
- package/src/components/shared/__tests__/empty-state.test.tsx +49 -0
- package/src/components/shared/__tests__/friendly-id.test.tsx +47 -0
- package/src/components/shared/__tests__/kbd.test.tsx +45 -0
- package/src/components/shared/__tests__/kind-badge.test.tsx +71 -0
- package/src/components/shared/__tests__/metrics-row.test.tsx +74 -0
- package/src/components/shared/__tests__/outcome-banner.test.tsx +71 -0
- package/src/components/shared/__tests__/progress-bar.test.tsx +89 -0
- package/src/components/shared/__tests__/session-pill.test.tsx +62 -0
- package/src/components/shared/__tests__/settings-modal.test.tsx +201 -0
- package/src/components/shared/__tests__/shortcuts-help.test.tsx +103 -0
- package/src/components/shared/__tests__/status-badge.test.tsx +98 -0
- package/src/components/shared/__tests__/theme-provider.test.tsx +100 -0
- package/src/components/shared/__tests__/truncated-id.test.tsx +53 -0
- package/src/components/shared/app-footer.tsx +80 -0
- package/src/components/shared/app-header.tsx +160 -0
- package/src/components/shared/empty-state.tsx +18 -0
- package/src/components/shared/error-boundary.tsx +81 -0
- package/src/components/shared/friendly-id.tsx +48 -0
- package/src/components/shared/kbd.tsx +15 -0
- package/src/components/shared/kind-badge.tsx +51 -0
- package/src/components/shared/metrics-row.tsx +106 -0
- package/src/components/shared/outcome-banner.tsx +56 -0
- package/src/components/shared/progress-bar.tsx +42 -0
- package/src/components/shared/session-pill.tsx +69 -0
- package/src/components/shared/settings-modal.tsx +509 -0
- package/src/components/shared/shortcuts-help.tsx +113 -0
- package/src/components/shared/status-badge.tsx +110 -0
- package/src/components/shared/theme-provider.tsx +46 -0
- package/src/components/shared/truncated-id.tsx +51 -0
- package/src/components/ui/.gitkeep +0 -0
- package/src/components/ui/__tests__/accordion.test.tsx +96 -0
- package/src/components/ui/__tests__/badge.test.tsx +69 -0
- package/src/components/ui/__tests__/button.test.tsx +113 -0
- package/src/components/ui/__tests__/tabs.test.tsx +75 -0
- package/src/components/ui/__tests__/tooltip.test.tsx +90 -0
- package/src/components/ui/accordion.tsx +61 -0
- package/src/components/ui/badge.tsx +25 -0
- package/src/components/ui/button.tsx +40 -0
- package/src/components/ui/card.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +35 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/tabs.tsx +64 -0
- package/src/components/ui/tooltip.tsx +37 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/__tests__/use-animated-number.test.ts +184 -0
- package/src/hooks/__tests__/use-batched-updates.test.ts +315 -0
- package/src/hooks/__tests__/use-event-stream.test.ts +243 -0
- package/src/hooks/__tests__/use-keyboard.test.ts +217 -0
- package/src/hooks/__tests__/use-notifications.test.ts +230 -0
- package/src/hooks/__tests__/use-polling.test.ts +274 -0
- package/src/hooks/__tests__/use-project-runs.test.ts +163 -0
- package/src/hooks/__tests__/use-projects.test.ts +248 -0
- package/src/hooks/__tests__/use-run-dashboard.test.ts +168 -0
- package/src/hooks/__tests__/use-run-detail.test.ts +273 -0
- package/src/hooks/__tests__/use-smart-polling.test.ts +305 -0
- package/src/hooks/use-animated-number.ts +87 -0
- package/src/hooks/use-batched-updates.ts +150 -0
- package/src/hooks/use-event-stream.ts +150 -0
- package/src/hooks/use-keyboard.ts +45 -0
- package/src/hooks/use-notifications.ts +82 -0
- package/src/hooks/use-persisted-state.ts +60 -0
- package/src/hooks/use-polling.ts +60 -0
- package/src/hooks/use-project-runs.ts +51 -0
- package/src/hooks/use-projects.ts +26 -0
- package/src/hooks/use-run-dashboard.ts +207 -0
- package/src/hooks/use-run-detail.ts +77 -0
- package/src/hooks/use-smart-polling.ts +144 -0
- package/src/lib/.gitkeep +0 -0
- package/src/lib/__tests__/cn.test.ts +69 -0
- package/src/lib/__tests__/config-loader.test.ts +210 -0
- package/src/lib/__tests__/config.test.ts +561 -0
- package/src/lib/__tests__/error-handler.test.ts +143 -0
- package/src/lib/__tests__/fetcher.test.ts +517 -0
- package/src/lib/__tests__/global-registry.test.ts +214 -0
- package/src/lib/__tests__/parser.test.ts +1532 -0
- package/src/lib/__tests__/path-resolver.test.ts +112 -0
- package/src/lib/__tests__/run-cache.test.ts +591 -0
- package/src/lib/__tests__/server-init.test.ts +512 -0
- package/src/lib/__tests__/source-discovery.test.ts +246 -0
- package/src/lib/__tests__/utils.test.ts +160 -0
- package/src/lib/__tests__/watcher.test.ts +227 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/config-loader.ts +195 -0
- package/src/lib/config.ts +20 -0
- package/src/lib/error-handler.ts +76 -0
- package/src/lib/fetcher.ts +394 -0
- package/src/lib/global-registry.ts +117 -0
- package/src/lib/parser.ts +794 -0
- package/src/lib/path-resolver.ts +16 -0
- package/src/lib/run-cache.ts +404 -0
- package/src/lib/server-init.ts +226 -0
- package/src/lib/services/__tests__/run-query-service.test.ts +819 -0
- package/src/lib/services/run-query-service.ts +286 -0
- package/src/lib/source-discovery.ts +216 -0
- package/src/lib/utils.ts +103 -0
- package/src/lib/watcher.ts +265 -0
- package/src/test/fixtures.ts +269 -0
- package/src/test/mocks/handlers.ts +110 -0
- package/src/test/mocks/server.ts +17 -0
- package/src/test/setup.ts +200 -0
- package/src/test/test-utils.tsx +36 -0
- package/src/types/.gitkeep +0 -0
- package/src/types/breakpoint.ts +17 -0
- package/src/types/index.ts +214 -0
- package/tsconfig.json +50 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
import { Copy, Check, ChevronDown, ChevronUp } from "lucide-react";
|
|
5
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
6
|
+
import { StatusBadge } from "@/components/shared/status-badge";
|
|
7
|
+
import { KindBadge } from "@/components/shared/kind-badge";
|
|
8
|
+
import { TruncatedId } from "@/components/shared/truncated-id";
|
|
9
|
+
import type { TaskDetail } from "@/types";
|
|
10
|
+
|
|
11
|
+
/** Tiny copy-to-clipboard button (icon only) — magenta hover */
|
|
12
|
+
function CopyButton({ text }: { text: string }) {
|
|
13
|
+
const [copied, setCopied] = useState(false);
|
|
14
|
+
const handleCopy = useCallback(() => {
|
|
15
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
16
|
+
setCopied(true);
|
|
17
|
+
setTimeout(() => setCopied(false), 1500);
|
|
18
|
+
});
|
|
19
|
+
}, [text]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
onClick={handleCopy}
|
|
25
|
+
className="inline-flex items-center justify-center h-6 w-6 rounded text-foreground-muted hover:text-primary hover:bg-primary-muted transition-colors"
|
|
26
|
+
title="Copy to clipboard"
|
|
27
|
+
>
|
|
28
|
+
{copied ? <Check className="h-3 w-3 text-success" /> : <Copy className="h-3 w-3" />}
|
|
29
|
+
</button>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Consistent section header with magenta left border accent */
|
|
34
|
+
function SectionHeader({ children }: { children: React.ReactNode }) {
|
|
35
|
+
return (
|
|
36
|
+
<h4 className="text-xs font-medium text-foreground-muted tracking-wider mb-1.5 pl-2 border-l-2 border-primary">
|
|
37
|
+
{children}
|
|
38
|
+
</h4>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderDescription(prompt: Record<string, unknown> | undefined, description: string | null) {
|
|
43
|
+
if (prompt || !description) return null;
|
|
44
|
+
const text = description.length > 500 ? description.slice(0, 500) + "..." : description;
|
|
45
|
+
return (
|
|
46
|
+
<div>
|
|
47
|
+
<SectionHeader>Description</SectionHeader>
|
|
48
|
+
<p className="text-sm text-foreground-secondary whitespace-pre-wrap break-words leading-relaxed">{text}</p>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ErrorSection({
|
|
54
|
+
resultError,
|
|
55
|
+
taskError,
|
|
56
|
+
}: {
|
|
57
|
+
resultError: Record<string, unknown> | undefined | null;
|
|
58
|
+
taskError: TaskDetail["error"] | undefined;
|
|
59
|
+
}) {
|
|
60
|
+
const [stackExpanded, setStackExpanded] = useState(false);
|
|
61
|
+
|
|
62
|
+
const errorName =
|
|
63
|
+
taskError?.name ?? (resultError?.name != null ? String(resultError.name) : "Error");
|
|
64
|
+
const errorMessage =
|
|
65
|
+
taskError?.message ?? (resultError?.message != null ? String(resultError.message) : "Unknown error");
|
|
66
|
+
const errorStack =
|
|
67
|
+
taskError?.stack ?? (resultError?.stack != null ? String(resultError.stack) : undefined);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div>
|
|
71
|
+
<SectionHeader>Error</SectionHeader>
|
|
72
|
+
<div className="rounded-md bg-error-muted border border-error/30 border-l-[3px] border-l-error p-3 shadow-glow-error">
|
|
73
|
+
<div className="flex items-start justify-between gap-2">
|
|
74
|
+
<p className="text-sm font-medium text-error">
|
|
75
|
+
{errorName}: {errorMessage}
|
|
76
|
+
</p>
|
|
77
|
+
<CopyButton text={`${errorName}: ${errorMessage}`} />
|
|
78
|
+
</div>
|
|
79
|
+
{errorStack && (
|
|
80
|
+
<div className="mt-2">
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => setStackExpanded((prev) => !prev)}
|
|
84
|
+
className="text-xs text-foreground-muted hover:text-foreground-secondary transition-colors flex items-center gap-1"
|
|
85
|
+
>
|
|
86
|
+
<span className="inline-block transition-transform" style={{ transform: stackExpanded ? "rotate(90deg)" : "rotate(0deg)" }}>
|
|
87
|
+
▶
|
|
88
|
+
</span>
|
|
89
|
+
Stack Trace
|
|
90
|
+
</button>
|
|
91
|
+
{stackExpanded && (
|
|
92
|
+
<pre className="mt-2 rounded bg-background-secondary p-2 text-[11px] font-mono text-foreground-secondary overflow-x-auto max-h-48 overflow-y-auto whitespace-pre-wrap break-words">
|
|
93
|
+
{errorStack}
|
|
94
|
+
</pre>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const OUTPUT_CHAR_LIMIT = 500;
|
|
104
|
+
|
|
105
|
+
function ResultOutput({ value }: { value: unknown }) {
|
|
106
|
+
const [expanded, setExpanded] = useState(false);
|
|
107
|
+
const fullText = JSON.stringify(value, null, 2);
|
|
108
|
+
const isLong = fullText.length > OUTPUT_CHAR_LIMIT;
|
|
109
|
+
const displayText = isLong && !expanded ? fullText.slice(0, OUTPUT_CHAR_LIMIT) + "\n..." : fullText;
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div>
|
|
113
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
114
|
+
<SectionHeader>Output</SectionHeader>
|
|
115
|
+
<CopyButton text={fullText} />
|
|
116
|
+
</div>
|
|
117
|
+
<pre className="rounded-md bg-background-secondary border border-border/50 p-3 text-xs font-mono text-secondary overflow-x-auto max-h-[60vh] overflow-y-auto">
|
|
118
|
+
{displayText}
|
|
119
|
+
</pre>
|
|
120
|
+
{isLong && (
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
onClick={() => setExpanded((v) => !v)}
|
|
124
|
+
className="mt-1.5 flex items-center gap-1 text-xs leading-tight text-foreground-muted hover:text-primary transition-colors"
|
|
125
|
+
>
|
|
126
|
+
{expanded ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
|
127
|
+
{expanded ? "Collapse" : `Expand (${fullText.length.toLocaleString()} chars)`}
|
|
128
|
+
</button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function AgentPanel({ task }: { task: TaskDetail | null }) {
|
|
135
|
+
if (!task) return <div className="p-4 text-sm text-foreground-muted">Select a task to view details</div>;
|
|
136
|
+
|
|
137
|
+
// Try to find agent prompt data from taskDef or inputs
|
|
138
|
+
const taskDef = task.taskDef || {};
|
|
139
|
+
const inputs = task.input || (taskDef.inputs as Record<string, unknown> | undefined) || {};
|
|
140
|
+
const metadata = taskDef.metadata as Record<string, unknown> | undefined;
|
|
141
|
+
|
|
142
|
+
// Extract displayable info
|
|
143
|
+
const title = task.title || task.label || task.effectId;
|
|
144
|
+
const description: string | null =
|
|
145
|
+
(inputs.prd as string) ||
|
|
146
|
+
(inputs.description as string) ||
|
|
147
|
+
(inputs.task as string) ||
|
|
148
|
+
(inputs.prompt as string) ||
|
|
149
|
+
(metadata?.label as string) ||
|
|
150
|
+
null;
|
|
151
|
+
|
|
152
|
+
// Check for agent prompt structure (if present)
|
|
153
|
+
const agentDef = taskDef.agent as Record<string, unknown> | undefined;
|
|
154
|
+
const prompt = agentDef?.prompt as Record<string, unknown> | undefined;
|
|
155
|
+
|
|
156
|
+
// Result summary
|
|
157
|
+
const resultData = task.result;
|
|
158
|
+
const resultStatus = resultData?.status as string | undefined;
|
|
159
|
+
const resultValue = resultData?.result || resultData?.value;
|
|
160
|
+
const resultError = resultData?.error as Record<string, unknown> | undefined;
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<ScrollArea className="h-full">
|
|
164
|
+
<div className="space-y-4 p-4">
|
|
165
|
+
{/* Header with title, kind, status */}
|
|
166
|
+
<div className="space-y-2">
|
|
167
|
+
<h3 className="text-sm font-semibold text-foreground">{String(title)}</h3>
|
|
168
|
+
<div className="flex items-center gap-2">
|
|
169
|
+
<KindBadge kind={task.kind} />
|
|
170
|
+
<StatusBadge status={task.status} />
|
|
171
|
+
</div>
|
|
172
|
+
{task.invocationKey && (
|
|
173
|
+
<TruncatedId id={task.invocationKey} chars={4} className="text-foreground-muted" />
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{/* Agent prompt data (if present) */}
|
|
178
|
+
{typeof prompt?.role === "string" && (
|
|
179
|
+
<div>
|
|
180
|
+
<SectionHeader>Role</SectionHeader>
|
|
181
|
+
<p className="text-sm text-foreground">{prompt.role}</p>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
{typeof prompt?.task === "string" && (
|
|
185
|
+
<div>
|
|
186
|
+
<SectionHeader>Task</SectionHeader>
|
|
187
|
+
<p className="text-sm text-foreground">{prompt.task}</p>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
{Array.isArray(prompt?.instructions) && (
|
|
191
|
+
<div>
|
|
192
|
+
<SectionHeader>Instructions</SectionHeader>
|
|
193
|
+
<ol className="space-y-2">
|
|
194
|
+
{(prompt.instructions as string[]).map((inst: string, i: number) => (
|
|
195
|
+
<li key={i} className="flex items-start gap-2.5 text-sm text-foreground-secondary">
|
|
196
|
+
<span className="flex-shrink-0 flex items-center justify-center h-5 w-5 rounded-full bg-secondary-muted text-secondary text-xs leading-tight font-medium mt-px">
|
|
197
|
+
{i + 1}
|
|
198
|
+
</span>
|
|
199
|
+
<span className="leading-relaxed">{inst}</span>
|
|
200
|
+
</li>
|
|
201
|
+
))}
|
|
202
|
+
</ol>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{/* Description / inputs summary (when no agent prompt) */}
|
|
207
|
+
{renderDescription(prompt, description)}
|
|
208
|
+
|
|
209
|
+
{/* Result status */}
|
|
210
|
+
{resultStatus != null && (
|
|
211
|
+
<div>
|
|
212
|
+
<SectionHeader>Result</SectionHeader>
|
|
213
|
+
<div className="flex items-center gap-2 mb-2">
|
|
214
|
+
<StatusBadge status={resultStatus === "ok" ? "resolved" : "error"} />
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{/* Error display with expandable stack trace */}
|
|
220
|
+
{(resultError != null || task.error != null) && (
|
|
221
|
+
<ErrorSection
|
|
222
|
+
resultError={resultError}
|
|
223
|
+
taskError={task.error}
|
|
224
|
+
/>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{/* Result value — monospace with brand accent */}
|
|
228
|
+
{resultValue != null && (
|
|
229
|
+
<ResultOutput value={resultValue} />
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</ScrollArea>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/** Type guard for plain objects */
|
|
2
|
+
export function isRecord(v: unknown): v is Record<string, unknown> {
|
|
3
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Format a camelCase or snake_case key into a readable label */
|
|
7
|
+
export function formatLabel(key: string): string {
|
|
8
|
+
return key
|
|
9
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
10
|
+
.replace(/_/g, " ")
|
|
11
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Known array keys that represent "findings"-style lists of strings */
|
|
15
|
+
export const FINDINGS_KEYS = new Set([
|
|
16
|
+
"findings",
|
|
17
|
+
"issues",
|
|
18
|
+
"recommendations",
|
|
19
|
+
"errors",
|
|
20
|
+
"warnings",
|
|
21
|
+
"notes",
|
|
22
|
+
"suggestions",
|
|
23
|
+
"problems",
|
|
24
|
+
"improvements",
|
|
25
|
+
"todos",
|
|
26
|
+
"comments",
|
|
27
|
+
"messages",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export interface CategorizedData {
|
|
31
|
+
status: string | null;
|
|
32
|
+
score: number | null;
|
|
33
|
+
passesQuality: boolean | null;
|
|
34
|
+
booleans: Array<{ key: string; value: boolean }>;
|
|
35
|
+
findings: Array<{ key: string; items: string[] }>;
|
|
36
|
+
summary: string | null;
|
|
37
|
+
taskId: string | null;
|
|
38
|
+
metadata: Array<{ key: string; value: unknown }>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function categorizeData(data: unknown): CategorizedData {
|
|
42
|
+
const result: CategorizedData = {
|
|
43
|
+
status: null,
|
|
44
|
+
score: null,
|
|
45
|
+
passesQuality: null,
|
|
46
|
+
booleans: [],
|
|
47
|
+
findings: [],
|
|
48
|
+
summary: null,
|
|
49
|
+
taskId: null,
|
|
50
|
+
metadata: [],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (!isRecord(data)) {
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const obj = data;
|
|
58
|
+
const consumed = new Set<string>();
|
|
59
|
+
|
|
60
|
+
// Status
|
|
61
|
+
if ("status" in obj && typeof obj.status === "string") {
|
|
62
|
+
result.status = obj.status;
|
|
63
|
+
consumed.add("status");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Score
|
|
67
|
+
if ("score" in obj && typeof obj.score === "number") {
|
|
68
|
+
result.score = obj.score;
|
|
69
|
+
consumed.add("score");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// passesQuality
|
|
73
|
+
if ("passesQuality" in obj && typeof obj.passesQuality === "boolean") {
|
|
74
|
+
result.passesQuality = obj.passesQuality;
|
|
75
|
+
consumed.add("passesQuality");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Summary
|
|
79
|
+
if ("summary" in obj && typeof obj.summary === "string") {
|
|
80
|
+
result.summary = obj.summary;
|
|
81
|
+
consumed.add("summary");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// TaskId
|
|
85
|
+
if ("taskId" in obj && typeof obj.taskId === "string") {
|
|
86
|
+
result.taskId = obj.taskId;
|
|
87
|
+
consumed.add("taskId");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Collect booleans and findings
|
|
91
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
92
|
+
if (consumed.has(key)) continue;
|
|
93
|
+
|
|
94
|
+
// Boolean fields (excluding passesQuality already consumed)
|
|
95
|
+
if (typeof val === "boolean") {
|
|
96
|
+
result.booleans.push({ key, value: val });
|
|
97
|
+
consumed.add(key);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Findings: arrays of strings with recognized key names
|
|
102
|
+
if (
|
|
103
|
+
Array.isArray(val) &&
|
|
104
|
+
val.length > 0 &&
|
|
105
|
+
val.every((item) => typeof item === "string") &&
|
|
106
|
+
FINDINGS_KEYS.has(key.toLowerCase())
|
|
107
|
+
) {
|
|
108
|
+
result.findings.push({ key, items: val as string[] });
|
|
109
|
+
consumed.add(key);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// If fewer than 2 booleans, move them back to metadata
|
|
115
|
+
if (result.booleans.length < 2) {
|
|
116
|
+
// Don't render boolean grid -- push to metadata
|
|
117
|
+
for (const b of result.booleans) {
|
|
118
|
+
result.metadata.push({ key: b.key, value: b.value });
|
|
119
|
+
}
|
|
120
|
+
result.booleans = [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Everything else goes to metadata
|
|
124
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
125
|
+
if (!consumed.has(key)) {
|
|
126
|
+
result.metadata.push({ key, value: val });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from "react";
|
|
4
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
5
|
+
import type { TaskDetail } from "@/types";
|
|
6
|
+
|
|
7
|
+
import { JsonTreeView } from "./json-node";
|
|
8
|
+
import { categorizeData } from "./categorize";
|
|
9
|
+
import { DataToggle } from "./tree-controls";
|
|
10
|
+
import {
|
|
11
|
+
AtAGlanceHeader,
|
|
12
|
+
BooleanFlagsGrid,
|
|
13
|
+
FindingsSection,
|
|
14
|
+
SummaryBlock,
|
|
15
|
+
MetadataGrid,
|
|
16
|
+
CollapsibleRawJson,
|
|
17
|
+
} from "./smart-summary";
|
|
18
|
+
|
|
19
|
+
/* ------------------------------------------------------------------ */
|
|
20
|
+
/* JsonTree -- Smart Data View (main export) */
|
|
21
|
+
/* ------------------------------------------------------------------ */
|
|
22
|
+
|
|
23
|
+
export function JsonTree({ task }: { task: TaskDetail | null }) {
|
|
24
|
+
const [showInput, setShowInput] = useState(true);
|
|
25
|
+
|
|
26
|
+
const activeData = showInput ? task?.input : task?.result;
|
|
27
|
+
|
|
28
|
+
const categorized = useMemo(
|
|
29
|
+
() => categorizeData(activeData),
|
|
30
|
+
[activeData]
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (!task) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="p-4 text-sm text-foreground-muted">
|
|
36
|
+
Select a task to view data
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const hasData = task.input || task.result;
|
|
42
|
+
if (!hasData) {
|
|
43
|
+
return (
|
|
44
|
+
<div className="p-4 text-sm text-foreground-muted">
|
|
45
|
+
No I/O data for this task
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const isPrimitive =
|
|
51
|
+
activeData === null ||
|
|
52
|
+
activeData === undefined ||
|
|
53
|
+
typeof activeData !== "object" ||
|
|
54
|
+
Array.isArray(activeData);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="p-4">
|
|
58
|
+
{/* 1. Input/Output Toggle */}
|
|
59
|
+
<DataToggle showInput={showInput} onToggle={setShowInput} />
|
|
60
|
+
|
|
61
|
+
<ScrollArea>
|
|
62
|
+
{isPrimitive ? (
|
|
63
|
+
/* For non-object data, just show the raw tree */
|
|
64
|
+
<div className="rounded-md bg-background-secondary p-3">
|
|
65
|
+
<JsonTreeView data={activeData} />
|
|
66
|
+
</div>
|
|
67
|
+
) : (
|
|
68
|
+
<div className="space-y-3">
|
|
69
|
+
{/* 2. At-a-Glance Header Bar */}
|
|
70
|
+
<AtAGlanceHeader
|
|
71
|
+
status={categorized.status}
|
|
72
|
+
score={categorized.score}
|
|
73
|
+
passesQuality={categorized.passesQuality}
|
|
74
|
+
taskId={categorized.taskId}
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
{/* 3. Boolean Flags Grid */}
|
|
78
|
+
<BooleanFlagsGrid booleans={categorized.booleans} />
|
|
79
|
+
|
|
80
|
+
{/* 4. Findings / Issues Section */}
|
|
81
|
+
<FindingsSection findings={categorized.findings} />
|
|
82
|
+
|
|
83
|
+
{/* 5. Summary Block */}
|
|
84
|
+
{categorized.summary && (
|
|
85
|
+
<SummaryBlock summary={categorized.summary} />
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{/* 6. Metadata Section */}
|
|
89
|
+
<MetadataGrid metadata={categorized.metadata} />
|
|
90
|
+
|
|
91
|
+
{/* 7. Collapsible Raw JSON */}
|
|
92
|
+
<CollapsibleRawJson data={activeData} />
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</ScrollArea>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ------------------------------------------------------------------ */
|
|
101
|
+
/* Re-exports for backward compatibility */
|
|
102
|
+
/* ------------------------------------------------------------------ */
|
|
103
|
+
|
|
104
|
+
export { JsonTreeView } from "./json-node";
|
|
105
|
+
export { CopyButton, JsonNode } from "./json-node";
|
|
106
|
+
export { categorizeData, FINDINGS_KEYS, formatLabel, isRecord } from "./categorize";
|
|
107
|
+
export type { CategorizedData } from "./categorize";
|
|
108
|
+
export {
|
|
109
|
+
StatusPill,
|
|
110
|
+
ScoreBar,
|
|
111
|
+
QualityBadge,
|
|
112
|
+
AtAGlanceHeader,
|
|
113
|
+
BooleanFlagsGrid,
|
|
114
|
+
FindingsSection,
|
|
115
|
+
SummaryBlock,
|
|
116
|
+
MetadataGrid,
|
|
117
|
+
CollapsibleRawJson,
|
|
118
|
+
SmartSectionHeader,
|
|
119
|
+
} from "./smart-summary";
|
|
120
|
+
export { DataToggle } from "./tree-controls";
|