@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.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/synapse +0 -0
  4. package/cmd/rubriceval/main.go +308 -0
  5. package/cmd/synapse/main.go +280 -0
  6. package/cmd/synapse/main_test.go +16 -0
  7. package/go.mod +7 -0
  8. package/go.sum +6 -0
  9. package/internal/adapter/adapter.go +1117 -0
  10. package/internal/adapter/adapter_test.go +518 -0
  11. package/internal/adapter/agy/adapter.go +193 -0
  12. package/internal/adapter/claudecode/adapter.go +415 -0
  13. package/internal/adapter/claudecode/adapter_test.go +260 -0
  14. package/internal/adapter/claudecode/agents.go +387 -0
  15. package/internal/adapter/claudecode/agents_test.go +480 -0
  16. package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
  17. package/internal/adapter/codex/adapter.go +919 -0
  18. package/internal/adapter/codex/adapter_test.go +920 -0
  19. package/internal/adapter/codex/agents.go +401 -0
  20. package/internal/adapter/codex/agents_test.go +610 -0
  21. package/internal/adapter/codex/summary_inputs_test.go +20 -0
  22. package/internal/adapter/pi/adapter.go +483 -0
  23. package/internal/adapter/pi/adapter_test.go +517 -0
  24. package/internal/citymap/builder.go +1124 -0
  25. package/internal/citymap/builder_test.go +818 -0
  26. package/internal/judge/cache.go +180 -0
  27. package/internal/judge/cli.go +240 -0
  28. package/internal/judge/cli_test.go +68 -0
  29. package/internal/judge/fresh_summary_test.go +37 -0
  30. package/internal/judge/input.go +233 -0
  31. package/internal/judge/judge.go +416 -0
  32. package/internal/judge/judge_test.go +288 -0
  33. package/internal/judge/prompt.go +129 -0
  34. package/internal/judge/rubric.go +275 -0
  35. package/internal/judge/rubric_test.go +642 -0
  36. package/internal/model/agent.go +63 -0
  37. package/internal/model/agent_schema_test.go +86 -0
  38. package/internal/model/agent_test.go +64 -0
  39. package/internal/model/model.go +175 -0
  40. package/internal/model/report.go +166 -0
  41. package/internal/model/stats.go +151 -0
  42. package/internal/model/stats_test.go +89 -0
  43. package/internal/model/trace_schema_test.go +67 -0
  44. package/internal/server/analyze.go +286 -0
  45. package/internal/server/analyze_test.go +297 -0
  46. package/internal/server/codex_index_test.go +40 -0
  47. package/internal/server/hardening_test.go +147 -0
  48. package/internal/server/reportindex.go +91 -0
  49. package/internal/server/reportindex_test.go +116 -0
  50. package/internal/server/server.go +1099 -0
  51. package/internal/server/server_test.go +1389 -0
  52. package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
  53. package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
  54. package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
  55. package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
  56. package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
  57. package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
  58. package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
  59. package/internal/server/static/assets/index-C_adLrJr.js +3 -0
  60. package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
  61. package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  62. package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  63. package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
  64. package/internal/server/static/index.html +26 -0
  65. package/internal/server/tracestore.go +173 -0
  66. package/internal/server/tracestore_test.go +51 -0
  67. package/internal/textutil/truncate.go +30 -0
  68. package/internal/textutil/truncate_test.go +39 -0
  69. package/package.json +35 -0
  70. package/web/e2e/agent-lens.spec.ts +688 -0
  71. package/web/index.html +23 -0
  72. package/web/package-lock.json +1933 -0
  73. package/web/package.json +33 -0
  74. package/web/playwright.config.ts +24 -0
  75. package/web/src/App.tsx +876 -0
  76. package/web/src/api/client.ts +74 -0
  77. package/web/src/main.tsx +12 -0
  78. package/web/src/playback/recorder.ts +160 -0
  79. package/web/src/playback/reducer.ts +91 -0
  80. package/web/src/scene/CityScene.tsx +638 -0
  81. package/web/src/scene/TreeScene.tsx +656 -0
  82. package/web/src/scene/dirLabels.ts +145 -0
  83. package/web/src/scene/sceneUtils.ts +144 -0
  84. package/web/src/scene/textures.ts +60 -0
  85. package/web/src/scene/trail.ts +79 -0
  86. package/web/src/scene/treeLayout.ts +169 -0
  87. package/web/src/state/filters.ts +40 -0
  88. package/web/src/state/store.ts +83 -0
  89. package/web/src/styles.css +2565 -0
  90. package/web/src/types.ts +315 -0
  91. package/web/src/ui/AgentsPanel.tsx +376 -0
  92. package/web/src/ui/Dock.tsx +104 -0
  93. package/web/src/ui/Hud.tsx +335 -0
  94. package/web/src/ui/Inspector.tsx +107 -0
  95. package/web/src/ui/LogoMark.tsx +38 -0
  96. package/web/src/ui/ReportPanel.tsx +491 -0
  97. package/web/src/ui/SessionRail.tsx +316 -0
  98. package/web/src/ui/Timeline.tsx +458 -0
  99. package/web/src/ui/ViewPanel.tsx +45 -0
  100. package/web/src/ui/shortcuts.ts +4 -0
  101. package/web/tsconfig.json +21 -0
  102. package/web/vite.config.ts +28 -0
@@ -0,0 +1,316 @@
1
+ import { Eye, EyeOff, FolderOpen, PanelLeftClose, RefreshCw, Search } from "lucide-react";
2
+ import { memo, useEffect, useMemo, useRef, useState } from "react";
3
+ import { sessionVisible } from "../state/filters";
4
+ import { LogoMark } from "./LogoMark";
5
+ import { toggleRailShortcut } from "./shortcuts";
6
+ import type { SessionMeta } from "../types";
7
+
8
+ interface SessionRailProps {
9
+ sessions: SessionMeta[];
10
+ activeKey?: string;
11
+ loading: boolean;
12
+ hideEmpty: boolean;
13
+ harnessFilter?: string;
14
+ collapsed: boolean;
15
+ onSelect: (key: string) => void;
16
+ onRefresh: () => void;
17
+ onHideEmptyChange: (hide: boolean) => void;
18
+ onHarnessFilterChange: (harness?: string) => void;
19
+ onCollapse: () => void;
20
+ // opens the static full-repo map for a repo path in a new tab
21
+ onOpenMap: (repo: string) => void;
22
+ // the active session's repo, offered as the popover's one-click choice
23
+ activeRepo?: string;
24
+ // while a video export records, session switching is locked so it can't swap
25
+ // the canvas or playhead out from under the recorder
26
+ locked?: boolean;
27
+ // the panel's authoritative (digest-based) status for the active session;
28
+ // undefined = unknown (keep the list's approximate badge), null = no report
29
+ activeReportState?: "running" | "done" | "stale" | "failed" | null;
30
+ }
31
+
32
+ // memo: the app re-renders every playback tick; the rail's props only change
33
+ // on scans, session switches, and filter changes
34
+ export const SessionRail = memo(function SessionRail({
35
+ sessions,
36
+ activeKey,
37
+ loading,
38
+ hideEmpty,
39
+ harnessFilter,
40
+ collapsed,
41
+ onSelect,
42
+ onRefresh,
43
+ onHideEmptyChange,
44
+ onHarnessFilterChange,
45
+ onCollapse,
46
+ onOpenMap,
47
+ activeRepo,
48
+ locked = false,
49
+ activeReportState
50
+ }: SessionRailProps) {
51
+ const [query, setQuery] = useState("");
52
+ const [repoPath, setRepoPath] = useState("");
53
+ const [mapOpen, setMapOpen] = useState(false);
54
+ const mapPopRef = useRef<HTMLDivElement | null>(null);
55
+
56
+ useEffect(() => {
57
+ if (!mapOpen) return;
58
+ const onPointerDown = (event: PointerEvent) => {
59
+ if (mapPopRef.current?.contains(event.target as Node)) return;
60
+ setMapOpen(false);
61
+ };
62
+ const onKeyDown = (event: KeyboardEvent) => {
63
+ if (event.key === "Escape") setMapOpen(false);
64
+ };
65
+ document.addEventListener("pointerdown", onPointerDown);
66
+ document.addEventListener("keydown", onKeyDown);
67
+ return () => {
68
+ document.removeEventListener("pointerdown", onPointerDown);
69
+ document.removeEventListener("keydown", onKeyDown);
70
+ };
71
+ }, [mapOpen]);
72
+ const harnesses = useMemo(() => [...new Set(sessions.map((s) => s.harness))].sort(), [sessions]);
73
+ const emptyCount = useMemo(() => sessions.filter((s) => s.eventCount === 0).length, [sessions]);
74
+ // a persisted filter can name a harness with no sessions this scan; treating
75
+ // it as "all" avoids an empty list with no visible chip to clear it
76
+ const effectiveHarness = harnessFilter && harnesses.includes(harnessFilter) ? harnessFilter : undefined;
77
+ const shown = useMemo(() => {
78
+ const q = query.trim().toLowerCase();
79
+ return sessions.filter((session) => {
80
+ if (!sessionVisible(session, { hideEmpty, harness: effectiveHarness }, activeKey)) return false;
81
+ if (!q) return true;
82
+ return `${session.title ?? ""} ${session.id} ${session.gitBranch ?? ""} ${session.harness}`
83
+ .toLowerCase()
84
+ .includes(q);
85
+ });
86
+ }, [sessions, query, hideEmpty, effectiveHarness, activeKey]);
87
+
88
+ return (
89
+ <aside className={collapsed ? "session-rail collapsed" : "session-rail"}>
90
+ <div className="rail-head">
91
+ <h1 className="wordmark">
92
+ <LogoMark />
93
+ <span>
94
+ Synapse<span className="spark">.</span>
95
+ </span>
96
+ </h1>
97
+ <div className="rail-head-actions">
98
+ <div className="rail-map" ref={mapPopRef}>
99
+ <button
100
+ className="icon-btn"
101
+ onClick={() => setMapOpen((open) => !open)}
102
+ aria-expanded={mapOpen}
103
+ title="Open a repository map"
104
+ aria-label="Open a repository map"
105
+ >
106
+ <FolderOpen size={15} />
107
+ </button>
108
+ {mapOpen ? (
109
+ <div className="rail-map-pop">
110
+ {activeRepo ? (
111
+ <button
112
+ className="rail-map-primary"
113
+ onClick={() => {
114
+ onOpenMap(activeRepo);
115
+ setMapOpen(false);
116
+ }}
117
+ title={`Open the map of ${activeRepo}`}
118
+ >
119
+ <FolderOpen size={14} aria-hidden />
120
+ <span className="rail-map-primary-text">
121
+ <span className="rail-map-primary-name">{repoBasename(activeRepo)}</span>
122
+ {/* the leading LRM pins the path's neutral "/" runs to
123
+ LTR order inside the RTL ellipsis-at-start trick */}
124
+ <span className="rail-map-primary-path">{"\u200E" + activeRepo}</span>
125
+ </span>
126
+ </button>
127
+ ) : null}
128
+ {activeRepo ? (
129
+ <div className="rail-map-divider" aria-hidden>
130
+ <span>or open any repository</span>
131
+ </div>
132
+ ) : (
133
+ <p className="rail-map-label">Open a repository map</p>
134
+ )}
135
+ <form
136
+ className="rail-map-form"
137
+ onSubmit={(e) => {
138
+ e.preventDefault();
139
+ const path = repoPath.trim();
140
+ if (path) {
141
+ onOpenMap(path);
142
+ setMapOpen(false);
143
+ }
144
+ }}
145
+ >
146
+ <input
147
+ type="text"
148
+ className="rail-map-input"
149
+ placeholder="/path/to/repo"
150
+ value={repoPath}
151
+ onChange={(e) => setRepoPath(e.currentTarget.value)}
152
+ spellCheck={false}
153
+ />
154
+ <button type="submit" className="rail-map-go" disabled={repoPath.trim() === ""}>
155
+ Open
156
+ </button>
157
+ </form>
158
+ </div>
159
+ ) : null}
160
+ </div>
161
+ <button
162
+ className="icon-btn"
163
+ onClick={onRefresh}
164
+ disabled={locked}
165
+ title="Rescan sessions"
166
+ aria-label="Rescan sessions"
167
+ >
168
+ <RefreshCw size={15} />
169
+ </button>
170
+ <button
171
+ className="icon-btn"
172
+ onClick={onCollapse}
173
+ title={`Hide sidebar (${toggleRailShortcut})`}
174
+ aria-label="Hide session sidebar"
175
+ >
176
+ <PanelLeftClose size={15} />
177
+ </button>
178
+ </div>
179
+ </div>
180
+ <div className="rail-controls">
181
+ <label className="rail-filter">
182
+ <Search size={14} aria-hidden />
183
+ <input
184
+ type="search"
185
+ placeholder="Filter sessions"
186
+ value={query}
187
+ onChange={(e) => setQuery(e.currentTarget.value)}
188
+ aria-label="Filter sessions"
189
+ />
190
+ </label>
191
+ {harnesses.length > 1 || emptyCount > 0 ? (
192
+ <div className="rail-chips" role="group" aria-label="Session filters">
193
+ {harnesses.length > 1 ? (
194
+ <>
195
+ <button
196
+ className={effectiveHarness === undefined ? "chip active" : "chip"}
197
+ onClick={() => onHarnessFilterChange(undefined)}
198
+ >
199
+ all
200
+ </button>
201
+ {harnesses.map((harness) => (
202
+ <button
203
+ key={harness}
204
+ className={effectiveHarness === harness ? "chip active" : "chip"}
205
+ onClick={() => onHarnessFilterChange(harness)}
206
+ >
207
+ {harnessLabel(harness)}
208
+ </button>
209
+ ))}
210
+ </>
211
+ ) : null}
212
+ {emptyCount > 0 ? (
213
+ <button
214
+ className={hideEmpty ? "eye-toggle" : "eye-toggle showing"}
215
+ onClick={() => onHideEmptyChange(!hideEmpty)}
216
+ aria-pressed={!hideEmpty}
217
+ title={
218
+ hideEmpty ? `Show ${emptyCount} empty sessions` : `Hide ${emptyCount} empty sessions`
219
+ }
220
+ aria-label={
221
+ hideEmpty ? `Show ${emptyCount} empty sessions` : `Hide ${emptyCount} empty sessions`
222
+ }
223
+ >
224
+ {hideEmpty ? <EyeOff size={13} aria-hidden /> : <Eye size={13} aria-hidden />}
225
+ </button>
226
+ ) : null}
227
+ </div>
228
+ ) : null}
229
+ </div>
230
+ <div className="session-list" aria-busy={loading}>
231
+ {shown.map((session) => (
232
+ <button
233
+ key={session.key}
234
+ className={session.key === activeKey ? "session-row active" : "session-row"}
235
+ onClick={() => onSelect(session.key)}
236
+ disabled={locked}
237
+ >
238
+ <span className="session-title">{session.title || session.id}</span>
239
+ <span className="session-meta">
240
+ <span className="session-meta-text">
241
+ {harnessLabel(session.harness)} · {session.eventCount}{" "}
242
+ {session.eventCount === 1 ? "call" : "calls"}
243
+ {session.gitBranch ? ` · ${session.gitBranch}` : ""}
244
+ {session.endedAt ? ` · ${shortDate(session.endedAt)}` : ""}
245
+ </span>
246
+ {(() => {
247
+ // the panel's digest-based status outranks the list's cheap
248
+ // event-count grading for the active session
249
+ const evalState =
250
+ session.key === activeKey && activeReportState !== undefined
251
+ ? activeReportState
252
+ : session.reportState;
253
+ return evalState ? (
254
+ <span
255
+ className={`rail-eval rail-eval-${evalState}`}
256
+ title={evalHint(evalState)}
257
+ aria-label={evalHint(evalState)}
258
+ >
259
+ {evalState === "running" ? "evaluating" : ""}
260
+ </span>
261
+ ) : null;
262
+ })()}
263
+ </span>
264
+ </button>
265
+ ))}
266
+ {shown.length === 0 ? (
267
+ <p className="muted" style={{ padding: "10px 8px" }}>
268
+ {loading && sessions.length === 0 ? "Scanning sessions…" : "No matching sessions."}
269
+ </p>
270
+ ) : null}
271
+ </div>
272
+ <div className="rail-foot">
273
+ {shown.length === sessions.length
274
+ ? `${sessions.length} session${sessions.length === 1 ? "" : "s"}`
275
+ : `${shown.length} of ${sessions.length} sessions`}
276
+ </div>
277
+ </aside>
278
+ );
279
+ });
280
+
281
+ function repoBasename(path: string): string {
282
+ const clean = path.replace(/\/+$/, "");
283
+ return clean.slice(clean.lastIndexOf("/") + 1) || clean;
284
+ }
285
+
286
+ function evalHint(state: "running" | "done" | "stale" | "failed"): string {
287
+ switch (state) {
288
+ case "running":
289
+ return "Evaluation in progress";
290
+ case "done":
291
+ return "Evaluation ready";
292
+ case "stale":
293
+ return "Evaluation ready, but the session has grown since";
294
+ case "failed":
295
+ return "Last evaluation failed";
296
+ }
297
+ }
298
+
299
+ function harnessLabel(harness: string): string {
300
+ switch (harness) {
301
+ case "claude-code":
302
+ return "claude";
303
+ default:
304
+ return harness;
305
+ }
306
+ }
307
+
308
+ function shortDate(iso: string): string {
309
+ const d = new Date(iso);
310
+ if (Number.isNaN(d.getTime())) return "";
311
+ const now = new Date();
312
+ const sameYear = d.getFullYear() === now.getFullYear();
313
+ const md = `${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
314
+ const hm = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
315
+ return sameYear ? `${md} ${hm}` : `${d.getFullYear()}-${md}`;
316
+ }