@hybridlabor-api/bdb-synapse 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 +173 -0
- package/bin/synapse +0 -0
- package/cmd/rubriceval/main.go +308 -0
- package/cmd/synapse/main.go +280 -0
- package/cmd/synapse/main_test.go +16 -0
- package/go.mod +7 -0
- package/go.sum +6 -0
- package/internal/adapter/adapter.go +1117 -0
- package/internal/adapter/adapter_test.go +518 -0
- package/internal/adapter/agy/adapter.go +193 -0
- package/internal/adapter/claudecode/adapter.go +415 -0
- package/internal/adapter/claudecode/adapter_test.go +260 -0
- package/internal/adapter/claudecode/agents.go +387 -0
- package/internal/adapter/claudecode/agents_test.go +480 -0
- package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
- package/internal/adapter/codex/adapter.go +919 -0
- package/internal/adapter/codex/adapter_test.go +920 -0
- package/internal/adapter/codex/agents.go +401 -0
- package/internal/adapter/codex/agents_test.go +610 -0
- package/internal/adapter/codex/summary_inputs_test.go +20 -0
- package/internal/adapter/pi/adapter.go +483 -0
- package/internal/adapter/pi/adapter_test.go +517 -0
- package/internal/citymap/builder.go +1124 -0
- package/internal/citymap/builder_test.go +818 -0
- package/internal/judge/cache.go +180 -0
- package/internal/judge/cli.go +240 -0
- package/internal/judge/cli_test.go +68 -0
- package/internal/judge/fresh_summary_test.go +37 -0
- package/internal/judge/input.go +233 -0
- package/internal/judge/judge.go +416 -0
- package/internal/judge/judge_test.go +288 -0
- package/internal/judge/prompt.go +129 -0
- package/internal/judge/rubric.go +275 -0
- package/internal/judge/rubric_test.go +642 -0
- package/internal/model/agent.go +63 -0
- package/internal/model/agent_schema_test.go +86 -0
- package/internal/model/agent_test.go +64 -0
- package/internal/model/model.go +175 -0
- package/internal/model/report.go +166 -0
- package/internal/model/stats.go +151 -0
- package/internal/model/stats_test.go +89 -0
- package/internal/model/trace_schema_test.go +67 -0
- package/internal/server/analyze.go +286 -0
- package/internal/server/analyze_test.go +297 -0
- package/internal/server/codex_index_test.go +40 -0
- package/internal/server/hardening_test.go +147 -0
- package/internal/server/reportindex.go +91 -0
- package/internal/server/reportindex_test.go +116 -0
- package/internal/server/server.go +1099 -0
- package/internal/server/server_test.go +1389 -0
- package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
- package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
- package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
- package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
- package/internal/server/static/assets/index-C_adLrJr.js +3 -0
- package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
- package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
- package/internal/server/static/index.html +26 -0
- package/internal/server/tracestore.go +173 -0
- package/internal/server/tracestore_test.go +51 -0
- package/internal/textutil/truncate.go +30 -0
- package/internal/textutil/truncate_test.go +39 -0
- package/package.json +35 -0
- package/web/e2e/agent-lens.spec.ts +688 -0
- package/web/index.html +23 -0
- package/web/package-lock.json +1933 -0
- package/web/package.json +33 -0
- package/web/playwright.config.ts +24 -0
- package/web/src/App.tsx +876 -0
- package/web/src/api/client.ts +74 -0
- package/web/src/main.tsx +12 -0
- package/web/src/playback/recorder.ts +160 -0
- package/web/src/playback/reducer.ts +91 -0
- package/web/src/scene/CityScene.tsx +638 -0
- package/web/src/scene/TreeScene.tsx +656 -0
- package/web/src/scene/dirLabels.ts +145 -0
- package/web/src/scene/sceneUtils.ts +144 -0
- package/web/src/scene/textures.ts +60 -0
- package/web/src/scene/trail.ts +79 -0
- package/web/src/scene/treeLayout.ts +169 -0
- package/web/src/state/filters.ts +40 -0
- package/web/src/state/store.ts +83 -0
- package/web/src/styles.css +2565 -0
- package/web/src/types.ts +315 -0
- package/web/src/ui/AgentsPanel.tsx +376 -0
- package/web/src/ui/Dock.tsx +104 -0
- package/web/src/ui/Hud.tsx +335 -0
- package/web/src/ui/Inspector.tsx +107 -0
- package/web/src/ui/LogoMark.tsx +38 -0
- package/web/src/ui/ReportPanel.tsx +491 -0
- package/web/src/ui/SessionRail.tsx +316 -0
- package/web/src/ui/Timeline.tsx +458 -0
- package/web/src/ui/ViewPanel.tsx +45 -0
- package/web/src/ui/shortcuts.ts +4 -0
- package/web/tsconfig.json +21 -0
- package/web/vite.config.ts +28 -0
package/web/src/types.ts
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
export type Action = "search" | "read" | "edit" | "exec" | "verify" | "other";
|
|
2
|
+
export type Touch = "hit" | "read" | "edit";
|
|
3
|
+
|
|
4
|
+
/** the words the HUD legend uses for each touch state — every surface that
|
|
5
|
+
* names a touch must speak this vocabulary, not the wire values */
|
|
6
|
+
export function touchWord(touch?: Touch): string {
|
|
7
|
+
switch (touch) {
|
|
8
|
+
case "hit":
|
|
9
|
+
return "seen";
|
|
10
|
+
case "read":
|
|
11
|
+
return "read";
|
|
12
|
+
case "edit":
|
|
13
|
+
return "edited";
|
|
14
|
+
default:
|
|
15
|
+
return "unvisited";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SessionMeta {
|
|
20
|
+
key: string;
|
|
21
|
+
id: string;
|
|
22
|
+
harness: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
path: string;
|
|
25
|
+
cwd?: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
gitBranch?: string;
|
|
28
|
+
startedAt?: string;
|
|
29
|
+
endedAt?: string;
|
|
30
|
+
eventCount: number;
|
|
31
|
+
/** user-turn count; with eventCount, the badge's cheap staleness signal */
|
|
32
|
+
userTurns?: number;
|
|
33
|
+
/** evaluation state for the rail badge; absent when never evaluated */
|
|
34
|
+
reportState?: "running" | "done" | "stale" | "failed";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type AgentKind = "main" | "subagent";
|
|
38
|
+
export type AgentStatus = "main" | "launched" | "failed" | "unknown";
|
|
39
|
+
export type TraceAvailability = "available" | "missing" | "unavailable";
|
|
40
|
+
export type AgentLinkQuality = "exact" | "derived" | "unavailable";
|
|
41
|
+
export type AgentLinkMethod =
|
|
42
|
+
| "root"
|
|
43
|
+
| "codex-agent-id"
|
|
44
|
+
| "codex-parent-thread-id"
|
|
45
|
+
| "claude-tool-use-id"
|
|
46
|
+
| "claude-subagents-directory"
|
|
47
|
+
| "unavailable";
|
|
48
|
+
|
|
49
|
+
export interface AgentGraph {
|
|
50
|
+
version: number;
|
|
51
|
+
rootSessionKey: string;
|
|
52
|
+
agents: AgentNode[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AgentNode {
|
|
56
|
+
id: string;
|
|
57
|
+
parentId?: string;
|
|
58
|
+
depth: number;
|
|
59
|
+
kind: AgentKind;
|
|
60
|
+
label: string;
|
|
61
|
+
role?: string;
|
|
62
|
+
instructionPreview?: string;
|
|
63
|
+
launchSeq?: number;
|
|
64
|
+
launchCallId?: string;
|
|
65
|
+
status: AgentStatus;
|
|
66
|
+
traceAvailability: TraceAvailability;
|
|
67
|
+
traceSessionKey?: string;
|
|
68
|
+
traceEventCount: number;
|
|
69
|
+
linkQuality: AgentLinkQuality;
|
|
70
|
+
linkMethod: AgentLinkMethod;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface Rect {
|
|
74
|
+
x: number;
|
|
75
|
+
z: number;
|
|
76
|
+
w: number;
|
|
77
|
+
d: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CityMap {
|
|
81
|
+
version: number;
|
|
82
|
+
repo: {
|
|
83
|
+
root: string;
|
|
84
|
+
commit?: string;
|
|
85
|
+
dirty: boolean;
|
|
86
|
+
generatedAt: string;
|
|
87
|
+
/** the tree holds more files than the map shows — a scan budget cut in */
|
|
88
|
+
truncated?: boolean;
|
|
89
|
+
};
|
|
90
|
+
files: CityFile[];
|
|
91
|
+
dirs: CityDir[];
|
|
92
|
+
layout: {
|
|
93
|
+
algorithm: string;
|
|
94
|
+
weight: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface CityFile {
|
|
99
|
+
id: number;
|
|
100
|
+
path: string;
|
|
101
|
+
dir: string;
|
|
102
|
+
lines: number;
|
|
103
|
+
bytes: number;
|
|
104
|
+
lang?: string;
|
|
105
|
+
rect: Rect;
|
|
106
|
+
ghost: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface CityDir {
|
|
110
|
+
path: string;
|
|
111
|
+
depth: number;
|
|
112
|
+
rect: Rect;
|
|
113
|
+
fileCount: number;
|
|
114
|
+
lines: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface Trace {
|
|
118
|
+
version: number;
|
|
119
|
+
session: {
|
|
120
|
+
id: string;
|
|
121
|
+
harness: string;
|
|
122
|
+
model?: string;
|
|
123
|
+
title?: string;
|
|
124
|
+
cwd?: string;
|
|
125
|
+
commit?: string;
|
|
126
|
+
startedAt?: string;
|
|
127
|
+
endedAt?: string;
|
|
128
|
+
eventCount: number;
|
|
129
|
+
path?: string;
|
|
130
|
+
};
|
|
131
|
+
events: TraceEvent[];
|
|
132
|
+
marks: Mark[];
|
|
133
|
+
stats: Stats;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface TraceEvent {
|
|
137
|
+
seq: number;
|
|
138
|
+
ts?: string;
|
|
139
|
+
tool: string;
|
|
140
|
+
action: Action;
|
|
141
|
+
targets: Target[];
|
|
142
|
+
outside?: OutsideTouch[];
|
|
143
|
+
resultBytes: number;
|
|
144
|
+
isError: boolean;
|
|
145
|
+
outcomeKnown?: boolean;
|
|
146
|
+
summary: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface Target {
|
|
150
|
+
path: string;
|
|
151
|
+
fileId?: number;
|
|
152
|
+
touch: Touch;
|
|
153
|
+
lines?: [number, number][];
|
|
154
|
+
weak?: boolean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface OutsideTouch {
|
|
158
|
+
scope: "home" | "tmp" | "other";
|
|
159
|
+
path: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface Mark {
|
|
163
|
+
seq: number;
|
|
164
|
+
type: "compaction" | "user-message" | "subagent";
|
|
165
|
+
note?: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface Stats {
|
|
169
|
+
filesInRepo: number;
|
|
170
|
+
fovea: number;
|
|
171
|
+
parafovea: number;
|
|
172
|
+
edited: number;
|
|
173
|
+
eventsBeforeFirstEdit: number;
|
|
174
|
+
regressionRate: number;
|
|
175
|
+
errorRate: number;
|
|
176
|
+
actions: ActionCounts;
|
|
177
|
+
errors: ActionCounts;
|
|
178
|
+
maxEditsPerFile: number;
|
|
179
|
+
/** files edited in three or more events */
|
|
180
|
+
churnFiles: number;
|
|
181
|
+
userTurns: number;
|
|
182
|
+
compactions: number;
|
|
183
|
+
subagents: number;
|
|
184
|
+
resultBytes: number;
|
|
185
|
+
/** edit events after the last verify event; every edit event when the session never verified */
|
|
186
|
+
editsAfterLastVerify: number;
|
|
187
|
+
observability: Observability;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Grades each derived metric's source signal: "exact" when the harness
|
|
192
|
+
* records it structurally, "estimated" when inferred from command or output
|
|
193
|
+
* text, "unavailable" when the log carries no usable signal.
|
|
194
|
+
*/
|
|
195
|
+
export interface Observability {
|
|
196
|
+
reads: MetricObservability;
|
|
197
|
+
errors: MetricObservability;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export type MetricObservability = "exact" | "estimated" | "unavailable";
|
|
201
|
+
|
|
202
|
+
/** LLM-assisted evaluation of one session; verdicts are server-derived
|
|
203
|
+
* from finding severities, never decided by the judge itself */
|
|
204
|
+
export interface Report {
|
|
205
|
+
version: number;
|
|
206
|
+
session: {
|
|
207
|
+
id: string;
|
|
208
|
+
harness: string;
|
|
209
|
+
model?: string;
|
|
210
|
+
/** event count the report was generated from — the staleness signal */
|
|
211
|
+
eventCount: number;
|
|
212
|
+
};
|
|
213
|
+
judge: {
|
|
214
|
+
cli: string;
|
|
215
|
+
/** the LLM that actually judged, as reported by the CLI itself */
|
|
216
|
+
model?: string;
|
|
217
|
+
promptVersion: number;
|
|
218
|
+
/** set only when the report carries a scored rubric */
|
|
219
|
+
rubricPromptVersion?: number;
|
|
220
|
+
generatedAt: string;
|
|
221
|
+
};
|
|
222
|
+
taskSummary: string;
|
|
223
|
+
dimensions: ReportDimension[];
|
|
224
|
+
/** task-accounting layer; absent on --no-rubric and pre-rubric reports */
|
|
225
|
+
rubric?: Rubric;
|
|
226
|
+
notableMoments?: ReportMoment[];
|
|
227
|
+
narrative: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export type RubricStatus = "scored" | "unavailable";
|
|
231
|
+
export type RubricReason = "generation-failed" | "no-task-text" | "weak-task-text" | "no-events";
|
|
232
|
+
/** what the log let the scorer judge; none forces insufficient-data */
|
|
233
|
+
export type Coverage = "sufficient" | "partial" | "none";
|
|
234
|
+
|
|
235
|
+
/** session-specific criteria generated before scoring, grouped by the
|
|
236
|
+
* independent tasks the judge enumerated from the user messages */
|
|
237
|
+
export interface Rubric {
|
|
238
|
+
status: RubricStatus;
|
|
239
|
+
reason?: RubricReason;
|
|
240
|
+
source?: "full" | "task";
|
|
241
|
+
taskDigest?: string;
|
|
242
|
+
tasks?: RubricTask[];
|
|
243
|
+
/** what the scorer felt the rubric did not let it express */
|
|
244
|
+
note?: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface RubricTask {
|
|
248
|
+
title: string;
|
|
249
|
+
type?: string;
|
|
250
|
+
anchorUserMessages: number[];
|
|
251
|
+
/** mark seqs the anchors resolve to — the task's start on the timeline */
|
|
252
|
+
anchorSeqs?: number[];
|
|
253
|
+
criteria: RubricCriterion[];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface RubricCriterion {
|
|
257
|
+
id: string;
|
|
258
|
+
title: string;
|
|
259
|
+
why?: string;
|
|
260
|
+
good?: string;
|
|
261
|
+
bad?: string;
|
|
262
|
+
coverage?: Coverage;
|
|
263
|
+
verdict: Verdict;
|
|
264
|
+
findings: ReportFinding[];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export type Verdict = "good" | "warning" | "problem" | "insufficient-data";
|
|
268
|
+
export type Severity = "info" | "warning" | "problem";
|
|
269
|
+
export type DimensionName = "exploration" | "scope" | "wandering" | "verification";
|
|
270
|
+
|
|
271
|
+
export interface ReportDimension {
|
|
272
|
+
name: DimensionName;
|
|
273
|
+
verdict: Verdict;
|
|
274
|
+
findings: ReportFinding[];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export interface ReportFinding {
|
|
278
|
+
claim: string;
|
|
279
|
+
severity: Severity;
|
|
280
|
+
evidenceSeqs?: number[];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface ReportMoment {
|
|
284
|
+
seq: number;
|
|
285
|
+
note: string;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface ReportStatus {
|
|
289
|
+
state: "none" | "running" | "done" | "failed";
|
|
290
|
+
/** done, but generated from fewer events than the trace now has */
|
|
291
|
+
stale: boolean;
|
|
292
|
+
report?: Report;
|
|
293
|
+
error?: string;
|
|
294
|
+
judgeAvailable: boolean;
|
|
295
|
+
/** default judge (first available) */
|
|
296
|
+
judgeCli?: string;
|
|
297
|
+
/** every installed judge CLI, preference order first */
|
|
298
|
+
judgeClis?: string[];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** the panel's judge selection, sent with the analyze request */
|
|
302
|
+
export interface JudgeChoice {
|
|
303
|
+
cli: string;
|
|
304
|
+
/** empty string keeps the CLI's default model */
|
|
305
|
+
model: string;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export interface ActionCounts {
|
|
309
|
+
search: number;
|
|
310
|
+
read: number;
|
|
311
|
+
edit: number;
|
|
312
|
+
exec: number;
|
|
313
|
+
verify: number;
|
|
314
|
+
other: number;
|
|
315
|
+
}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { AlertTriangle, Bot, Circle, Info, Loader, RefreshCw, X } from "lucide-react";
|
|
2
|
+
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import type { AgentGraph, AgentNode } from "../types";
|
|
5
|
+
|
|
6
|
+
interface AgentsPanelProps {
|
|
7
|
+
graph?: AgentGraph;
|
|
8
|
+
current: string | null;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
loadingAgentID?: string;
|
|
11
|
+
locked?: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
retryAgentID?: string | null;
|
|
14
|
+
onSelect: (agentID: string | null) => void;
|
|
15
|
+
onRetry: () => void;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type AgentDetailState = {
|
|
20
|
+
agentID: string;
|
|
21
|
+
mode: "preview" | "pinned";
|
|
22
|
+
anchor: HTMLElement;
|
|
23
|
+
} | null;
|
|
24
|
+
|
|
25
|
+
interface PopoverPosition {
|
|
26
|
+
left: number;
|
|
27
|
+
top: number;
|
|
28
|
+
width: number;
|
|
29
|
+
maxHeight: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function AgentsPanel({
|
|
33
|
+
graph,
|
|
34
|
+
current,
|
|
35
|
+
loading,
|
|
36
|
+
loadingAgentID,
|
|
37
|
+
locked = false,
|
|
38
|
+
error,
|
|
39
|
+
retryAgentID,
|
|
40
|
+
onSelect,
|
|
41
|
+
onRetry,
|
|
42
|
+
onClose
|
|
43
|
+
}: AgentsPanelProps) {
|
|
44
|
+
const children = graph?.agents.filter((agent) => agent.kind !== "main") ?? [];
|
|
45
|
+
const main = graph?.agents.find((agent) => agent.kind === "main");
|
|
46
|
+
const graphError = error && retryAgentID === null ? error : undefined;
|
|
47
|
+
const [detail, setDetail] = useState<AgentDetailState>(null);
|
|
48
|
+
const detailAgent = graph?.agents.find((agent) => agent.id === detail?.agentID);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (detail && graph && !detailAgent) setDetail(null);
|
|
52
|
+
}, [detail, detailAgent, graph]);
|
|
53
|
+
|
|
54
|
+
const closePanel = () => {
|
|
55
|
+
setDetail(null);
|
|
56
|
+
onClose();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="dock-body agents-panel" aria-label="Agent lenses">
|
|
61
|
+
<div className="inspector-head">
|
|
62
|
+
<div>
|
|
63
|
+
<div className="inspector-path">Agents</div>
|
|
64
|
+
<div className="agents-head-note">Choose one trace at a time</div>
|
|
65
|
+
</div>
|
|
66
|
+
<button className="icon-btn" onClick={closePanel} title="Close" aria-label="Close agents">
|
|
67
|
+
<X size={15} />
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{graphError ? (
|
|
72
|
+
<AgentError error={graphError} locked={locked} onRetry={onRetry} />
|
|
73
|
+
) : null}
|
|
74
|
+
|
|
75
|
+
<div className="agent-list" aria-busy={loading || loadingAgentID !== undefined}>
|
|
76
|
+
<button
|
|
77
|
+
className={
|
|
78
|
+
current === null ? "agent-row agent-row-select active" : "agent-row agent-row-select"
|
|
79
|
+
}
|
|
80
|
+
aria-pressed={current === null}
|
|
81
|
+
disabled={locked}
|
|
82
|
+
onClick={() => onSelect(null)}
|
|
83
|
+
>
|
|
84
|
+
<span className="agent-row-icon" aria-hidden>
|
|
85
|
+
<Circle size={12} />
|
|
86
|
+
</span>
|
|
87
|
+
<span className="agent-row-copy">
|
|
88
|
+
<span className="agent-row-primary">
|
|
89
|
+
<span className="agent-row-title">
|
|
90
|
+
Main
|
|
91
|
+
{current === null ? <span className="agent-current">current</span> : null}
|
|
92
|
+
</span>
|
|
93
|
+
<span className="agent-row-count">{eventCount(main?.traceEventCount ?? 0)}</span>
|
|
94
|
+
</span>
|
|
95
|
+
<span className="agent-row-secondary">Root trace</span>
|
|
96
|
+
</span>
|
|
97
|
+
</button>
|
|
98
|
+
|
|
99
|
+
{children.map((agent) => {
|
|
100
|
+
const rowError = error && retryAgentID === agent.id ? error : undefined;
|
|
101
|
+
return (
|
|
102
|
+
<Fragment key={agent.id}>
|
|
103
|
+
<AgentRow
|
|
104
|
+
agent={agent}
|
|
105
|
+
current={current === agent.id}
|
|
106
|
+
loading={loadingAgentID === agent.id}
|
|
107
|
+
locked={locked}
|
|
108
|
+
onSelect={onSelect}
|
|
109
|
+
detailMode={detail?.agentID === agent.id ? detail.mode : undefined}
|
|
110
|
+
onPreview={(anchor) =>
|
|
111
|
+
setDetail((currentDetail) =>
|
|
112
|
+
currentDetail?.mode === "pinned"
|
|
113
|
+
? currentDetail
|
|
114
|
+
: { agentID: agent.id, mode: "preview", anchor }
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
onPreviewEnd={(agentID) =>
|
|
118
|
+
setDetail((currentDetail) =>
|
|
119
|
+
currentDetail?.agentID === agentID && currentDetail.mode === "preview"
|
|
120
|
+
? null
|
|
121
|
+
: currentDetail
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
onToggleDetails={(anchor) =>
|
|
125
|
+
setDetail((currentDetail) =>
|
|
126
|
+
currentDetail?.agentID === agent.id && currentDetail.mode === "pinned"
|
|
127
|
+
? null
|
|
128
|
+
: { agentID: agent.id, mode: "pinned", anchor }
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
/>
|
|
132
|
+
{rowError ? (
|
|
133
|
+
<AgentError error={rowError} locked={locked} onRetry={onRetry} rowLocal />
|
|
134
|
+
) : null}
|
|
135
|
+
</Fragment>
|
|
136
|
+
);
|
|
137
|
+
})}
|
|
138
|
+
|
|
139
|
+
{graph && children.length === 0 ? <p className="agents-empty">No child agents found.</p> : null}
|
|
140
|
+
{!graph && loading ? (
|
|
141
|
+
<p className="agents-state" aria-live="polite">
|
|
142
|
+
<Loader size={13} className="spin" aria-hidden />
|
|
143
|
+
Loading agents…
|
|
144
|
+
</p>
|
|
145
|
+
) : null}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{detail && detailAgent ? (
|
|
149
|
+
<AgentDetailPopover agent={detailAgent} state={detail} onClose={() => setDetail(null)} />
|
|
150
|
+
) : null}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function AgentError({
|
|
156
|
+
error,
|
|
157
|
+
locked,
|
|
158
|
+
onRetry,
|
|
159
|
+
rowLocal = false
|
|
160
|
+
}: {
|
|
161
|
+
error: string;
|
|
162
|
+
locked: boolean;
|
|
163
|
+
onRetry: () => void;
|
|
164
|
+
rowLocal?: boolean;
|
|
165
|
+
}) {
|
|
166
|
+
return (
|
|
167
|
+
<div className={rowLocal ? "agents-error row-local" : "agents-error"} role="alert">
|
|
168
|
+
<span>
|
|
169
|
+
<AlertTriangle size={14} aria-hidden />
|
|
170
|
+
{error}
|
|
171
|
+
</span>
|
|
172
|
+
<button className="agents-retry" onClick={onRetry} disabled={locked}>
|
|
173
|
+
<RefreshCw size={13} aria-hidden />
|
|
174
|
+
Retry
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function AgentRow({
|
|
181
|
+
agent,
|
|
182
|
+
current,
|
|
183
|
+
loading,
|
|
184
|
+
locked,
|
|
185
|
+
onSelect,
|
|
186
|
+
detailMode,
|
|
187
|
+
onPreview,
|
|
188
|
+
onPreviewEnd,
|
|
189
|
+
onToggleDetails
|
|
190
|
+
}: {
|
|
191
|
+
agent: AgentNode;
|
|
192
|
+
current: boolean;
|
|
193
|
+
loading: boolean;
|
|
194
|
+
locked: boolean;
|
|
195
|
+
onSelect: (agentID: string | null) => void;
|
|
196
|
+
detailMode?: "preview" | "pinned";
|
|
197
|
+
onPreview: (anchor: HTMLElement) => void;
|
|
198
|
+
onPreviewEnd: (agentID: string) => void;
|
|
199
|
+
onToggleDetails: (anchor: HTMLElement) => void;
|
|
200
|
+
}) {
|
|
201
|
+
const available = agent.traceAvailability === "available";
|
|
202
|
+
const status = agentStatus(agent, loading);
|
|
203
|
+
const secondary = [agent.role, agent.instructionPreview].filter(Boolean).join(" · ");
|
|
204
|
+
const disabled = !available || locked;
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
className={["agent-row", current ? "active" : "", disabled ? "disabled" : ""]
|
|
209
|
+
.filter(Boolean)
|
|
210
|
+
.join(" ")}
|
|
211
|
+
onMouseEnter={(event) => onPreview(event.currentTarget)}
|
|
212
|
+
onMouseLeave={() => onPreviewEnd(agent.id)}
|
|
213
|
+
onFocus={(event) => onPreview(event.currentTarget)}
|
|
214
|
+
onBlur={(event) => {
|
|
215
|
+
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) {
|
|
216
|
+
onPreviewEnd(agent.id);
|
|
217
|
+
}
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<button
|
|
221
|
+
className="agent-row-select"
|
|
222
|
+
style={{ paddingLeft: `${12 + Math.min(agent.depth, 4) * 14}px` }}
|
|
223
|
+
aria-pressed={current}
|
|
224
|
+
disabled={disabled}
|
|
225
|
+
onClick={() => onSelect(agent.id)}
|
|
226
|
+
aria-label={`${agent.label}, ${status}`}
|
|
227
|
+
>
|
|
228
|
+
<span className="agent-row-icon" aria-hidden>
|
|
229
|
+
{loading ? <Loader size={13} className="spin" /> : <Bot size={13} />}
|
|
230
|
+
</span>
|
|
231
|
+
<span className="agent-row-copy">
|
|
232
|
+
<span className="agent-row-primary">
|
|
233
|
+
<span className="agent-row-title">
|
|
234
|
+
{agent.label}
|
|
235
|
+
{current ? <span className="agent-current">current</span> : null}
|
|
236
|
+
</span>
|
|
237
|
+
<span className={`agent-row-count agent-status-${agent.traceAvailability}`}>
|
|
238
|
+
{status}
|
|
239
|
+
</span>
|
|
240
|
+
</span>
|
|
241
|
+
<span className="agent-row-secondary">{secondary || "No launch details"}</span>
|
|
242
|
+
</span>
|
|
243
|
+
</button>
|
|
244
|
+
<button
|
|
245
|
+
className="agent-row-detail-trigger"
|
|
246
|
+
aria-label={`${detailMode === "pinned" ? "Unpin" : "Pin"} details for ${agent.label}`}
|
|
247
|
+
aria-pressed={detailMode === "pinned"}
|
|
248
|
+
onClick={(event) => {
|
|
249
|
+
event.stopPropagation();
|
|
250
|
+
onToggleDetails(event.currentTarget.closest(".agent-row") as HTMLElement);
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<Info size={12} aria-hidden />
|
|
254
|
+
</button>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function AgentDetailPopover({
|
|
260
|
+
agent,
|
|
261
|
+
state,
|
|
262
|
+
onClose
|
|
263
|
+
}: {
|
|
264
|
+
agent: AgentNode;
|
|
265
|
+
state: NonNullable<AgentDetailState>;
|
|
266
|
+
onClose: () => void;
|
|
267
|
+
}) {
|
|
268
|
+
const margin = 12;
|
|
269
|
+
const popoverRef = useRef<HTMLDivElement>(null);
|
|
270
|
+
const [position, setPosition] = useState<PopoverPosition>({
|
|
271
|
+
left: margin,
|
|
272
|
+
top: margin,
|
|
273
|
+
width: Math.min(520, window.innerWidth - margin * 2),
|
|
274
|
+
maxHeight: window.innerHeight - margin * 2
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
useLayoutEffect(() => {
|
|
278
|
+
const updatePosition = () => {
|
|
279
|
+
const gap = 12;
|
|
280
|
+
const width = Math.min(520, window.innerWidth - margin * 2);
|
|
281
|
+
const anchorRect = state.anchor.getBoundingClientRect();
|
|
282
|
+
const leftCandidate = anchorRect.left - width - gap;
|
|
283
|
+
const rightCandidate = anchorRect.right + gap;
|
|
284
|
+
const left =
|
|
285
|
+
leftCandidate >= margin
|
|
286
|
+
? leftCandidate
|
|
287
|
+
: rightCandidate + width <= window.innerWidth - margin
|
|
288
|
+
? rightCandidate
|
|
289
|
+
: Math.min(
|
|
290
|
+
Math.max(anchorRect.left, margin),
|
|
291
|
+
window.innerWidth - width - margin
|
|
292
|
+
);
|
|
293
|
+
const maxHeight = window.innerHeight - margin * 2;
|
|
294
|
+
const measuredHeight = Math.min(popoverRef.current?.offsetHeight ?? maxHeight, maxHeight);
|
|
295
|
+
const top = Math.min(
|
|
296
|
+
Math.max(anchorRect.top, margin),
|
|
297
|
+
window.innerHeight - measuredHeight - margin
|
|
298
|
+
);
|
|
299
|
+
setPosition({ left, top, width, maxHeight });
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const animatedPanel = state.anchor.closest(".dock-panel");
|
|
303
|
+
updatePosition();
|
|
304
|
+
window.addEventListener("resize", updatePosition);
|
|
305
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
306
|
+
animatedPanel?.addEventListener("animationend", updatePosition);
|
|
307
|
+
return () => {
|
|
308
|
+
window.removeEventListener("resize", updatePosition);
|
|
309
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
310
|
+
animatedPanel?.removeEventListener("animationend", updatePosition);
|
|
311
|
+
};
|
|
312
|
+
}, [state.anchor]);
|
|
313
|
+
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
if (state.mode !== "pinned") return;
|
|
316
|
+
|
|
317
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
318
|
+
if (event.key === "Escape") onClose();
|
|
319
|
+
};
|
|
320
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
321
|
+
const target = event.target as Node;
|
|
322
|
+
if (popoverRef.current?.contains(target) || state.anchor.contains(target)) return;
|
|
323
|
+
onClose();
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
327
|
+
document.addEventListener("pointerdown", handlePointerDown);
|
|
328
|
+
return () => {
|
|
329
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
330
|
+
document.removeEventListener("pointerdown", handlePointerDown);
|
|
331
|
+
};
|
|
332
|
+
}, [onClose, state.anchor, state.mode]);
|
|
333
|
+
|
|
334
|
+
return createPortal(
|
|
335
|
+
<div
|
|
336
|
+
ref={popoverRef}
|
|
337
|
+
className={`agent-detail-popover ${state.mode}`}
|
|
338
|
+
role={state.mode === "preview" ? "tooltip" : "dialog"}
|
|
339
|
+
aria-label={`${agent.label} details`}
|
|
340
|
+
style={position}
|
|
341
|
+
>
|
|
342
|
+
<div className="agent-detail-head">
|
|
343
|
+
<span>
|
|
344
|
+
<strong>{agent.label}</strong>
|
|
345
|
+
{agent.role ? <span>{agent.role}</span> : null}
|
|
346
|
+
</span>
|
|
347
|
+
{state.mode === "pinned" ? (
|
|
348
|
+
<button className="agent-detail-close" onClick={onClose} aria-label="Close details">
|
|
349
|
+
<X size={14} aria-hidden />
|
|
350
|
+
</button>
|
|
351
|
+
) : null}
|
|
352
|
+
</div>
|
|
353
|
+
{agent.instructionPreview ? (
|
|
354
|
+
<div className="agent-detail-instruction">{agent.instructionPreview}</div>
|
|
355
|
+
) : null}
|
|
356
|
+
<div className="agent-detail-meta">{agentDetail(agent)}</div>
|
|
357
|
+
</div>,
|
|
358
|
+
document.body
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function agentStatus(agent: AgentNode, loading: boolean): string {
|
|
363
|
+
if (loading) return "Loading trace…";
|
|
364
|
+
if (agent.traceAvailability === "missing") return "Trace missing";
|
|
365
|
+
if (agent.status === "failed") return "Launch failed · no trace";
|
|
366
|
+
if (agent.traceAvailability === "unavailable") return "Trace unavailable";
|
|
367
|
+
return eventCount(agent.traceEventCount);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function eventCount(count: number): string {
|
|
371
|
+
return `${count} event${count === 1 ? "" : "s"}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function agentDetail(agent: AgentNode): string {
|
|
375
|
+
return `Launch: ${agent.status} · Trace: ${agent.traceAvailability} · Correlation: ${agent.linkQuality} via ${agent.linkMethod}`;
|
|
376
|
+
}
|