@narumitw/pi-inspect 0.0.0-stage → 0.2.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 +110 -2
- package/dist/app.css +1 -0
- package/dist/app.js +128 -0
- package/dist/index.html +1 -0
- package/docs/REVIEW.md +189 -0
- package/docs/VERIFICATION.md +186 -0
- package/docs/viewer.md +74 -0
- package/package.json +67 -4
- package/src/ancestry.ts +32 -0
- package/src/collector.ts +143 -0
- package/src/context.ts +115 -0
- package/src/correlation.ts +27 -0
- package/src/entry-index.ts +17 -0
- package/src/entry-summary.ts +34 -0
- package/src/extension.ts +259 -0
- package/src/feed.ts +145 -0
- package/src/identity.ts +112 -0
- package/src/index.ts +1 -0
- package/src/model.ts +117 -0
- package/src/native-projection.ts +28 -0
- package/src/privacy.ts +136 -0
- package/src/projection.ts +374 -0
- package/src/server.ts +202 -0
- package/src/system-message.ts +117 -0
- package/src/web/api.ts +137 -0
- package/src/web/app.tsx +570 -0
- package/src/web/captured-executions.tsx +177 -0
- package/src/web/components.css +261 -0
- package/src/web/components.tsx +241 -0
- package/src/web/composition.css +554 -0
- package/src/web/context-composition.tsx +329 -0
- package/src/web/context-inventory.tsx +65 -0
- package/src/web/entry-calls.ts +14 -0
- package/src/web/format.ts +94 -0
- package/src/web/hierarchy.ts +117 -0
- package/src/web/history.ts +56 -0
- package/src/web/inline-entry.tsx +124 -0
- package/src/web/inspector.tsx +386 -0
- package/src/web/json-tree.tsx +74 -0
- package/src/web/overview.tsx +67 -0
- package/src/web/panels.css +267 -0
- package/src/web/panes.tsx +123 -0
- package/src/web/recorded-content.tsx +49 -0
- package/src/web/search.ts +16 -0
- package/src/web/session-filters.tsx +100 -0
- package/src/web/session.css +114 -0
- package/src/web/style.css +332 -0
- package/src/web/styles.d.ts +1 -0
- package/src/web/timeline.tsx +33 -0
- package/src/web/timing.ts +26 -0
- package/src/web/trace.css +411 -0
- package/src/web/trace.tsx +313 -0
- package/src/web/virtual-context.ts +138 -0
package/src/web/app.tsx
ADDED
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import { HamburgerMenuIcon, MoonIcon, ReaderIcon, SunIcon } from "@radix-ui/react-icons";
|
|
2
|
+
import { Badge, Callout, Heading, IconButton, Theme } from "@radix-ui/themes";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import "@radix-ui/themes/styles.css";
|
|
6
|
+
import "./style.css";
|
|
7
|
+
import "./composition.css";
|
|
8
|
+
import "./session.css";
|
|
9
|
+
import type { BranchView, ContextComposition as Composition, Snapshot } from "../model.js";
|
|
10
|
+
import { generation, headers, RequestFailure, request, useDetail } from "./api.js";
|
|
11
|
+
import { CapturedExecutions } from "./captured-executions.js";
|
|
12
|
+
import { ContextComposition } from "./context-composition.js";
|
|
13
|
+
import { ContextInventory } from "./context-inventory.js";
|
|
14
|
+
import { type Filters, matches } from "./format.js";
|
|
15
|
+
import { history, internalEntry } from "./history.js";
|
|
16
|
+
import { Inspector } from "./inspector.js";
|
|
17
|
+
import { Overview } from "./overview.js";
|
|
18
|
+
import { PaneDrawer, ResizeHandle, useNarrow } from "./panes.js";
|
|
19
|
+
import { boundedSearch, searchNeedle } from "./search.js";
|
|
20
|
+
import { SessionFilters } from "./session-filters.js";
|
|
21
|
+
import { Trace } from "./trace.js";
|
|
22
|
+
|
|
23
|
+
type Selection = { kind: "entry" | "call"; id: string; serial: number; filterVersion?: number; anchor?: string | null };
|
|
24
|
+
function App() {
|
|
25
|
+
const [appearance, setAppearance] = useState<"dark" | "light">("light");
|
|
26
|
+
const [revision, setRevision] = useState(-1);
|
|
27
|
+
const [snapshot, setSnapshot] = useState<Snapshot>();
|
|
28
|
+
const [branch, setBranch] = useState<BranchView>();
|
|
29
|
+
const [branchError, setBranchError] = useState("");
|
|
30
|
+
const [branchAttempt, setBranchAttempt] = useState(0);
|
|
31
|
+
const branchOwner = useRef<{ entryId: string; attempt: number } | undefined>(undefined);
|
|
32
|
+
const [selection, setSelection] = useState<Selection>();
|
|
33
|
+
const [view, setView] = useState("list");
|
|
34
|
+
const [surface, setSurface] = useState("context");
|
|
35
|
+
const [sessionView, setSessionView] = useState("history");
|
|
36
|
+
const [historyLeaf, setHistoryLeaf] = useState<string | null>(null);
|
|
37
|
+
const [showInternal, setShowInternal] = useState(false);
|
|
38
|
+
const [preview, setPreview] = useState<Composition>();
|
|
39
|
+
const [filters, setFilters] = useState<Filters>({
|
|
40
|
+
query: "",
|
|
41
|
+
kind: "all",
|
|
42
|
+
groups: [],
|
|
43
|
+
errorsOnly: false,
|
|
44
|
+
slowOnly: false,
|
|
45
|
+
});
|
|
46
|
+
const [filterVersion, setFilterVersion] = useState(0);
|
|
47
|
+
const filterEpoch = useRef(0);
|
|
48
|
+
function changeFilters(value: Filters) {
|
|
49
|
+
setFilters({ ...value, query: boundedSearch(value.query) });
|
|
50
|
+
setFilterVersion(++filterEpoch.current);
|
|
51
|
+
}
|
|
52
|
+
const [error, setError] = useState("");
|
|
53
|
+
const [connected, setConnected] = useState(false);
|
|
54
|
+
const lifecycle = useRef<{ terminal: boolean; stream?: AbortController }>({ terminal: false });
|
|
55
|
+
const [tab, setTab] = useState("content");
|
|
56
|
+
const navTrigger = useRef<HTMLButtonElement>(null);
|
|
57
|
+
const inspectorTrigger = useRef<HTMLButtonElement>(null);
|
|
58
|
+
const [navOpen, setNavOpen] = useState(false);
|
|
59
|
+
const [inspectorOpen, setInspectorOpen] = useState(false);
|
|
60
|
+
const [navDrawer, setNavDrawer] = useState(false);
|
|
61
|
+
const [inspectorDrawer, setInspectorDrawer] = useState(false);
|
|
62
|
+
const [navWidth, setNavWidth] = useState(() => Math.max(200, Math.min(360, window.innerWidth * 0.185)));
|
|
63
|
+
const [inspectorWidth, setInspectorWidth] = useState(() => Math.max(280, Math.min(520, window.innerWidth * 0.235)));
|
|
64
|
+
const narrow = useNarrow();
|
|
65
|
+
const sideBudget = Math.max(0, window.innerWidth - 600);
|
|
66
|
+
const navSize = navOpen ? Math.min(navWidth, inspectorOpen ? sideBudget * 0.4 : sideBudget) : 0;
|
|
67
|
+
const inspectorSize = inspectorOpen ? Math.min(inspectorWidth, navOpen ? sideBudget * 0.6 : sideBudget) : 0;
|
|
68
|
+
const calls = snapshot?.calls ?? [];
|
|
69
|
+
const entries = snapshot?.nodes ?? [];
|
|
70
|
+
const selectedCall =
|
|
71
|
+
selection?.kind === "call" ? calls.find((call) => call.occurrenceId === selection.id) : undefined;
|
|
72
|
+
const represented = useMemo(() => new Set(entries.map((entry) => entry.id)), [entries]);
|
|
73
|
+
const callAnchorAvailable = Boolean(selectedCall?.branchAnchor && represented.has(selectedCall.branchAnchor));
|
|
74
|
+
const entryId =
|
|
75
|
+
selection?.kind === "entry" ? selection.id : callAnchorAvailable ? (selectedCall?.branchAnchor ?? "") : "";
|
|
76
|
+
const paneActive =
|
|
77
|
+
surface === "events" && revision >= 0 && (inspectorOpen || inspectorDrawer) && selection?.kind === "entry";
|
|
78
|
+
const { detail, error: detailError, retry: retryDetails } = useDetail(paneActive ? entryId : undefined);
|
|
79
|
+
const previousSelection = useRef<Selection | undefined>(undefined);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (sessionView === "executions" && previousSelection.current?.kind === "call" && selection?.kind === "entry")
|
|
82
|
+
setSessionView("branches");
|
|
83
|
+
previousSelection.current = selection;
|
|
84
|
+
}, [selection, sessionView]);
|
|
85
|
+
const selectedNode = entries.find((node) => node.id === entryId);
|
|
86
|
+
const preparedFilters = useMemo(() => ({ ...filters, query: searchNeedle(filters.query) }), [filters]);
|
|
87
|
+
const matching = useMemo(
|
|
88
|
+
() => new Set(entries.filter((node) => matches(node, preparedFilters, calls)).map((node) => node.id)),
|
|
89
|
+
[entries, calls, preparedFilters],
|
|
90
|
+
);
|
|
91
|
+
function selectEntry(id: string) {
|
|
92
|
+
setSurface("events");
|
|
93
|
+
if (sessionView === "executions") setSessionView("branches");
|
|
94
|
+
if (narrow) setInspectorDrawer(true);
|
|
95
|
+
else setInspectorOpen(true);
|
|
96
|
+
setSelection((old) => ({ kind: "entry", id, serial: (old?.serial ?? 0) + 1, filterVersion }));
|
|
97
|
+
}
|
|
98
|
+
function selectCall(id: string) {
|
|
99
|
+
if (narrow) setInspectorDrawer(true);
|
|
100
|
+
else setInspectorOpen(true);
|
|
101
|
+
setSelection((old) => ({
|
|
102
|
+
kind: "call",
|
|
103
|
+
id,
|
|
104
|
+
serial: (old?.serial ?? 0) + 1,
|
|
105
|
+
filterVersion,
|
|
106
|
+
anchor: calls.find((call) => call.occurrenceId === id)?.branchAnchor,
|
|
107
|
+
}));
|
|
108
|
+
setSurface("events");
|
|
109
|
+
setSessionView("executions");
|
|
110
|
+
setTab("content");
|
|
111
|
+
}
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
let retry: ReturnType<typeof setTimeout> | undefined;
|
|
115
|
+
let refresh: ReturnType<typeof setTimeout> | undefined;
|
|
116
|
+
lifecycle.current.stream = controller;
|
|
117
|
+
const stop = (message: string) => {
|
|
118
|
+
lifecycle.current.terminal = true;
|
|
119
|
+
setRevision(-1);
|
|
120
|
+
controller.abort();
|
|
121
|
+
clearTimeout(refresh);
|
|
122
|
+
setConnected(false);
|
|
123
|
+
setError(message);
|
|
124
|
+
};
|
|
125
|
+
const connect = async () => {
|
|
126
|
+
try {
|
|
127
|
+
const response = await fetch(`/api/events?generation=${encodeURIComponent(generation)}`, {
|
|
128
|
+
headers: headers(),
|
|
129
|
+
signal: controller.signal,
|
|
130
|
+
});
|
|
131
|
+
if (controller.signal.aborted) return;
|
|
132
|
+
if ([401, 403, 409, 410].includes(response.status)) {
|
|
133
|
+
stop("Session expired or unauthorized; open the viewer again from Pi.");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!response.ok || !response.body) throw new Error("Transient viewer refusal");
|
|
137
|
+
setConnected(true);
|
|
138
|
+
const reader = response.body.getReader();
|
|
139
|
+
const decoder = new TextDecoder();
|
|
140
|
+
let buffer = "";
|
|
141
|
+
try {
|
|
142
|
+
while (!controller.signal.aborted) {
|
|
143
|
+
const { value, done } = await reader.read();
|
|
144
|
+
if (done || controller.signal.aborted) break;
|
|
145
|
+
buffer += decoder.decode(value, { stream: true });
|
|
146
|
+
let end = buffer.indexOf("\n\n");
|
|
147
|
+
while (end !== -1) {
|
|
148
|
+
const frame = buffer.slice(0, end);
|
|
149
|
+
buffer = buffer.slice(end + 2);
|
|
150
|
+
const line = frame.split("\n").find((s) => s.startsWith("data: "));
|
|
151
|
+
if (line) {
|
|
152
|
+
const data = JSON.parse(line.slice(6)) as { protocol: number; generation: string; revision: number };
|
|
153
|
+
if (data.protocol !== 1 || data.generation !== generation) {
|
|
154
|
+
stop("Session changed; open the viewer again from Pi.");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!refresh)
|
|
158
|
+
refresh = setTimeout(() => {
|
|
159
|
+
refresh = undefined;
|
|
160
|
+
setRevision((previous) => previous + 1);
|
|
161
|
+
}, 50);
|
|
162
|
+
}
|
|
163
|
+
end = buffer.indexOf("\n\n");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} finally {
|
|
167
|
+
await reader.cancel().catch(() => {});
|
|
168
|
+
reader.releaseLock();
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
/* Offline state is visible; reconnect uses a fresh snapshot, not stale replay. */
|
|
172
|
+
}
|
|
173
|
+
if (!controller.signal.aborted) {
|
|
174
|
+
setConnected(false);
|
|
175
|
+
retry = setTimeout(() => void connect(), 1000);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
void connect();
|
|
179
|
+
return () => {
|
|
180
|
+
controller.abort();
|
|
181
|
+
clearTimeout(retry);
|
|
182
|
+
clearTimeout(refresh);
|
|
183
|
+
};
|
|
184
|
+
}, []);
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (revision < 0) return;
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
let retry: ReturnType<typeof setTimeout> | undefined;
|
|
190
|
+
const load = () => {
|
|
191
|
+
if (lifecycle.current.terminal || controller.signal.aborted) return;
|
|
192
|
+
void request<Snapshot>("snapshot", controller.signal)
|
|
193
|
+
.then((value) => {
|
|
194
|
+
if (controller.signal.aborted || lifecycle.current.terminal) return;
|
|
195
|
+
setSnapshot(value);
|
|
196
|
+
setError("");
|
|
197
|
+
const represented = new Set(value.nodes.map((node) => node.id));
|
|
198
|
+
const id = value.leafId && represented.has(value.leafId) ? value.leafId : value.nodes[0]?.id;
|
|
199
|
+
setSelection((old) => {
|
|
200
|
+
if (old?.kind === "call" && !value.calls.some((call) => call.occurrenceId === old.id)) {
|
|
201
|
+
const fallback = old.anchor && represented.has(old.anchor) ? old.anchor : id;
|
|
202
|
+
return fallback
|
|
203
|
+
? { kind: "entry", id: fallback, serial: old.serial + 1, filterVersion: filterEpoch.current }
|
|
204
|
+
: undefined;
|
|
205
|
+
}
|
|
206
|
+
if (old?.kind === "entry" && !represented.has(old.id)) {
|
|
207
|
+
return id ? { kind: "entry", id, serial: old.serial + 1, filterVersion: filterEpoch.current } : undefined;
|
|
208
|
+
}
|
|
209
|
+
return old ?? (id ? { kind: "entry", id, serial: 0 } : undefined);
|
|
210
|
+
});
|
|
211
|
+
})
|
|
212
|
+
.catch((error) => {
|
|
213
|
+
if (!controller.signal.aborted && !lifecycle.current.terminal) {
|
|
214
|
+
setError(String(error.message));
|
|
215
|
+
if (error instanceof RequestFailure && error.terminal) {
|
|
216
|
+
lifecycle.current.terminal = true;
|
|
217
|
+
lifecycle.current.stream?.abort();
|
|
218
|
+
setConnected(false);
|
|
219
|
+
setRevision(-1);
|
|
220
|
+
} else retry = setTimeout(load, 1000);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
load();
|
|
225
|
+
return () => {
|
|
226
|
+
controller.abort();
|
|
227
|
+
clearTimeout(retry);
|
|
228
|
+
};
|
|
229
|
+
}, [revision]);
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
setBranchError("");
|
|
232
|
+
if (!entryId || !paneActive) {
|
|
233
|
+
branchOwner.current = undefined;
|
|
234
|
+
setBranch(undefined);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
setBranch(undefined);
|
|
239
|
+
const owner = { entryId, attempt: branchAttempt };
|
|
240
|
+
branchOwner.current = owner;
|
|
241
|
+
void request<BranchView>(`branch?leaf=${encodeURIComponent(entryId)}&offset=0`, controller.signal)
|
|
242
|
+
.then((value) => {
|
|
243
|
+
if (!controller.signal.aborted && branchOwner.current === owner) setBranch(value);
|
|
244
|
+
})
|
|
245
|
+
.catch(() => {
|
|
246
|
+
if (!controller.signal.aborted && branchOwner.current === owner)
|
|
247
|
+
setBranchError("Could not load selected branch context.");
|
|
248
|
+
});
|
|
249
|
+
return () => controller.abort();
|
|
250
|
+
}, [entryId, branchAttempt, paneActive]);
|
|
251
|
+
|
|
252
|
+
const scoped = useMemo(
|
|
253
|
+
() => history(entries, historyLeaf ?? snapshot?.leafId ?? null),
|
|
254
|
+
[entries, historyLeaf, snapshot?.leafId],
|
|
255
|
+
);
|
|
256
|
+
const historyRows = useMemo(
|
|
257
|
+
() =>
|
|
258
|
+
scoped.rows.filter(
|
|
259
|
+
({ node }) =>
|
|
260
|
+
showInternal || !internalEntry(node) || (selection?.filterVersion === filterVersion && node.id === entryId),
|
|
261
|
+
),
|
|
262
|
+
[scoped.rows, showInternal, selection?.filterVersion, filterVersion, entryId],
|
|
263
|
+
);
|
|
264
|
+
const visibleHistory = useMemo(() => new Set(historyRows.map(({ node }) => node.id)), [historyRows]);
|
|
265
|
+
const hiddenMatches = scoped.rows.filter(
|
|
266
|
+
({ node }) => internalEntry(node) && matching.has(node.id) && !visibleHistory.has(node.id),
|
|
267
|
+
).length;
|
|
268
|
+
const sidebar = <SessionFilters filters={filters} change={changeFilters} />;
|
|
269
|
+
const selectedBranch = branch?.leafId === entryId ? branch : undefined;
|
|
270
|
+
const inspector = (
|
|
271
|
+
<Inspector
|
|
272
|
+
selected={selection?.id ?? ""}
|
|
273
|
+
node={selectedNode}
|
|
274
|
+
detail={detail}
|
|
275
|
+
detailError={detailError}
|
|
276
|
+
retryDetails={retryDetails}
|
|
277
|
+
call={selectedCall}
|
|
278
|
+
branch={selectedBranch}
|
|
279
|
+
branchError={branchOwner.current?.entryId === entryId ? branchError : ""}
|
|
280
|
+
retryBranch={() => setBranchAttempt((attempt) => attempt + 1)}
|
|
281
|
+
snapshot={snapshot}
|
|
282
|
+
tab={tab}
|
|
283
|
+
changeTab={setTab}
|
|
284
|
+
select={selectEntry}
|
|
285
|
+
selectCall={selectCall}
|
|
286
|
+
preview={() => {
|
|
287
|
+
if (selectedBranch?.context) {
|
|
288
|
+
setPreview(selectedBranch.context);
|
|
289
|
+
setSurface("context");
|
|
290
|
+
setInspectorDrawer(false);
|
|
291
|
+
setNavDrawer(false);
|
|
292
|
+
}
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
);
|
|
296
|
+
return (
|
|
297
|
+
<Theme
|
|
298
|
+
className="inspector-app"
|
|
299
|
+
appearance={appearance}
|
|
300
|
+
accentColor="blue"
|
|
301
|
+
grayColor="slate"
|
|
302
|
+
radius="medium"
|
|
303
|
+
scaling="100%"
|
|
304
|
+
>
|
|
305
|
+
<header className="app-header">
|
|
306
|
+
<div className="brand">
|
|
307
|
+
<div className="brand-logo" aria-hidden="true">
|
|
308
|
+
π
|
|
309
|
+
</div>
|
|
310
|
+
<Heading size="4">Pi Session Inspector</Heading>
|
|
311
|
+
</div>
|
|
312
|
+
<div className="header-context">
|
|
313
|
+
<span title={snapshot?.nameTruncated ? "Session name truncated to 512 characters" : undefined}>
|
|
314
|
+
{snapshot?.name ?? "Connecting"}
|
|
315
|
+
{snapshot?.nameTruncated && " · truncated"}
|
|
316
|
+
</span>
|
|
317
|
+
<span title={snapshot?.sessionId}>
|
|
318
|
+
Session: <strong>{typeof snapshot?.sessionId === "string" ? snapshot.sessionId.slice(0, 8) : "—"}</strong>
|
|
319
|
+
</span>
|
|
320
|
+
</div>
|
|
321
|
+
<div className="header-actions">
|
|
322
|
+
<fieldset className="surface-switch" aria-label="Inspection source">
|
|
323
|
+
<button
|
|
324
|
+
type="button"
|
|
325
|
+
aria-pressed={surface === "context"}
|
|
326
|
+
onClick={() => {
|
|
327
|
+
setPreview(undefined);
|
|
328
|
+
setSurface("context");
|
|
329
|
+
setInspectorDrawer(false);
|
|
330
|
+
setNavDrawer(false);
|
|
331
|
+
}}
|
|
332
|
+
>
|
|
333
|
+
Context
|
|
334
|
+
</button>
|
|
335
|
+
<button type="button" aria-pressed={surface === "events"} onClick={() => setSurface("events")}>
|
|
336
|
+
Session
|
|
337
|
+
</button>
|
|
338
|
+
</fieldset>
|
|
339
|
+
<IconButton
|
|
340
|
+
hidden={surface !== "events"}
|
|
341
|
+
ref={navTrigger}
|
|
342
|
+
aria-label="Toggle filters"
|
|
343
|
+
aria-expanded={narrow ? navDrawer : navOpen}
|
|
344
|
+
variant="ghost"
|
|
345
|
+
onClick={() => (narrow ? setNavDrawer(!navDrawer) : setNavOpen(!navOpen))}
|
|
346
|
+
>
|
|
347
|
+
<HamburgerMenuIcon />
|
|
348
|
+
</IconButton>
|
|
349
|
+
<IconButton
|
|
350
|
+
hidden={surface !== "events"}
|
|
351
|
+
ref={inspectorTrigger}
|
|
352
|
+
aria-label="Toggle details"
|
|
353
|
+
aria-expanded={narrow ? inspectorDrawer : inspectorOpen}
|
|
354
|
+
variant="ghost"
|
|
355
|
+
onClick={() => (narrow ? setInspectorDrawer(!inspectorDrawer) : setInspectorOpen(!inspectorOpen))}
|
|
356
|
+
>
|
|
357
|
+
<ReaderIcon />
|
|
358
|
+
</IconButton>
|
|
359
|
+
<Badge color={connected ? "green" : "amber"}>{connected ? "Live" : "Disconnected"}</Badge>
|
|
360
|
+
<Badge>Read-only</Badge>
|
|
361
|
+
<IconButton
|
|
362
|
+
aria-label="Toggle appearance"
|
|
363
|
+
variant="ghost"
|
|
364
|
+
onClick={() => setAppearance(appearance === "dark" ? "light" : "dark")}
|
|
365
|
+
>
|
|
366
|
+
{appearance === "dark" ? <SunIcon /> : <MoonIcon />}
|
|
367
|
+
</IconButton>
|
|
368
|
+
</div>
|
|
369
|
+
</header>
|
|
370
|
+
{(error || snapshot?.incomplete) && (
|
|
371
|
+
<div className="app-notices">
|
|
372
|
+
{error && (
|
|
373
|
+
<Callout.Root color="red">
|
|
374
|
+
<Callout.Text>{error}</Callout.Text>
|
|
375
|
+
</Callout.Root>
|
|
376
|
+
)}
|
|
377
|
+
{snapshot?.incomplete && (
|
|
378
|
+
<Callout.Root color="amber">
|
|
379
|
+
<Callout.Text>Indexed data is incomplete; existing entry/capture limits apply.</Callout.Text>
|
|
380
|
+
</Callout.Root>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
)}
|
|
384
|
+
<main
|
|
385
|
+
className={`app-grid ${narrow ? "narrow-layout" : ""}`}
|
|
386
|
+
style={
|
|
387
|
+
narrow
|
|
388
|
+
? undefined
|
|
389
|
+
: {
|
|
390
|
+
gridTemplateColumns:
|
|
391
|
+
surface === "context"
|
|
392
|
+
? "0px 0px minmax(0, 1fr) 0px 0px"
|
|
393
|
+
: `${navSize}px ${navOpen ? 6 : 0}px minmax(0, 1fr) ${inspectorOpen ? 6 : 0}px ${inspectorSize}px`,
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
>
|
|
397
|
+
{!narrow && surface === "events" && (
|
|
398
|
+
<>
|
|
399
|
+
<div className="side-slot nav-slot" hidden={!navOpen}>
|
|
400
|
+
{sidebar}
|
|
401
|
+
</div>
|
|
402
|
+
{navOpen && (
|
|
403
|
+
<ResizeHandle name="Resize filters" value={navWidth} change={setNavWidth} min={180} max={420} />
|
|
404
|
+
)}
|
|
405
|
+
</>
|
|
406
|
+
)}
|
|
407
|
+
<div className="center-column">
|
|
408
|
+
<div className="context-surface" hidden={surface !== "context"}>
|
|
409
|
+
<div className="observed-context" hidden={Boolean(preview)}>
|
|
410
|
+
<ContextComposition context={snapshot?.context} payload={snapshot?.providerObservation} />
|
|
411
|
+
<ContextInventory snapshot={snapshot} />
|
|
412
|
+
</div>
|
|
413
|
+
{preview && (
|
|
414
|
+
<div className="preview-context">
|
|
415
|
+
<div className="preview-heading">
|
|
416
|
+
<strong>Session-derived preview · leaf {preview.leafId}</strong>
|
|
417
|
+
<button type="button" onClick={() => setPreview(undefined)}>
|
|
418
|
+
Back to last observed context
|
|
419
|
+
</button>
|
|
420
|
+
</div>
|
|
421
|
+
<ContextComposition context={preview} preview />
|
|
422
|
+
</div>
|
|
423
|
+
)}
|
|
424
|
+
</div>
|
|
425
|
+
<div className="events-surface" hidden={surface !== "events"}>
|
|
426
|
+
<Overview snapshot={snapshot} />
|
|
427
|
+
<div className="session-toolbar">
|
|
428
|
+
<fieldset className="view-switch" aria-label="Session navigation">
|
|
429
|
+
{[
|
|
430
|
+
["history", "History"],
|
|
431
|
+
["branches", "Branch view"],
|
|
432
|
+
["executions", "Captured executions"],
|
|
433
|
+
].map(([value, label]) => (
|
|
434
|
+
<button
|
|
435
|
+
type="button"
|
|
436
|
+
key={value}
|
|
437
|
+
aria-pressed={sessionView === value}
|
|
438
|
+
onClick={() => setSessionView(value ?? "history")}
|
|
439
|
+
>
|
|
440
|
+
{label}
|
|
441
|
+
{value === "executions"
|
|
442
|
+
? ` · ${calls.filter((call) => call.status === "running").length} running · ${calls.filter((call) => call.status === "error").length} errors`
|
|
443
|
+
: ""}
|
|
444
|
+
</button>
|
|
445
|
+
))}
|
|
446
|
+
</fieldset>
|
|
447
|
+
{sessionView === "history" && (
|
|
448
|
+
<>
|
|
449
|
+
<span>Branch leaf: {historyLeaf ?? snapshot?.leafId ?? "unavailable"}</span>
|
|
450
|
+
<label>
|
|
451
|
+
<input
|
|
452
|
+
type="checkbox"
|
|
453
|
+
checked={showInternal}
|
|
454
|
+
onChange={(event) => setShowInternal(event.target.checked)}
|
|
455
|
+
/>{" "}
|
|
456
|
+
Show internal events
|
|
457
|
+
</label>
|
|
458
|
+
{!showInternal && <span>{hiddenMatches} matching internal events hidden</span>}
|
|
459
|
+
{historyLeaf && (
|
|
460
|
+
<button type="button" onClick={() => setHistoryLeaf(null)}>
|
|
461
|
+
Follow active branch
|
|
462
|
+
</button>
|
|
463
|
+
)}
|
|
464
|
+
{scoped.issue && <span role="status">{scoped.issue}</span>}
|
|
465
|
+
{entryId && !scoped.rows.some(({ node }) => node.id === entryId) && (
|
|
466
|
+
<span role="status">Selected entry is outside this branch; use Branch view to locate it.</span>
|
|
467
|
+
)}
|
|
468
|
+
</>
|
|
469
|
+
)}
|
|
470
|
+
{sessionView === "branches" && entryId && (
|
|
471
|
+
<button
|
|
472
|
+
type="button"
|
|
473
|
+
onClick={() => {
|
|
474
|
+
setHistoryLeaf(entryId);
|
|
475
|
+
setSessionView("history");
|
|
476
|
+
}}
|
|
477
|
+
>
|
|
478
|
+
History at selected entry
|
|
479
|
+
</button>
|
|
480
|
+
)}
|
|
481
|
+
</div>
|
|
482
|
+
<div className="session-primary" hidden={sessionView !== "history"}>
|
|
483
|
+
<Trace
|
|
484
|
+
entries={entries}
|
|
485
|
+
matches={matching}
|
|
486
|
+
selected={entryId}
|
|
487
|
+
serial={selection?.serial ?? 0}
|
|
488
|
+
revealSelected={selection?.filterVersion === filterVersion}
|
|
489
|
+
select={selectEntry}
|
|
490
|
+
view={view}
|
|
491
|
+
changeView={setView}
|
|
492
|
+
calls={calls}
|
|
493
|
+
historyRows={historyRows}
|
|
494
|
+
active={surface === "events" && sessionView === "history" && revision >= 0}
|
|
495
|
+
/>
|
|
496
|
+
</div>
|
|
497
|
+
<div className="session-primary" hidden={sessionView !== "branches"}>
|
|
498
|
+
<Trace
|
|
499
|
+
entries={entries}
|
|
500
|
+
matches={matching}
|
|
501
|
+
selected={entryId}
|
|
502
|
+
serial={selection?.serial ?? 0}
|
|
503
|
+
revealSelected={selection?.filterVersion === filterVersion}
|
|
504
|
+
select={selectEntry}
|
|
505
|
+
view={view}
|
|
506
|
+
changeView={setView}
|
|
507
|
+
calls={calls}
|
|
508
|
+
active={surface === "events" && sessionView === "branches" && revision >= 0}
|
|
509
|
+
/>
|
|
510
|
+
</div>
|
|
511
|
+
<div className="session-primary execution-primary" hidden={sessionView !== "executions"}>
|
|
512
|
+
<CapturedExecutions
|
|
513
|
+
entries={entries}
|
|
514
|
+
calls={calls}
|
|
515
|
+
selected={selectedCall?.occurrenceId}
|
|
516
|
+
select={selectCall}
|
|
517
|
+
open={surface === "events" && sessionView === "executions"}
|
|
518
|
+
view={view}
|
|
519
|
+
changeView={setView}
|
|
520
|
+
dropped={snapshot?.droppedCalls ?? 0}
|
|
521
|
+
invalidEvents={snapshot?.invalidCallEvents ?? 0}
|
|
522
|
+
filters={filters}
|
|
523
|
+
revealSelected={selection?.filterVersion === filterVersion}
|
|
524
|
+
/>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
{!narrow && surface === "events" && (
|
|
529
|
+
<>
|
|
530
|
+
{inspectorOpen ? (
|
|
531
|
+
<ResizeHandle
|
|
532
|
+
name="Resize details"
|
|
533
|
+
value={inspectorWidth}
|
|
534
|
+
change={setInspectorWidth}
|
|
535
|
+
min={260}
|
|
536
|
+
max={600}
|
|
537
|
+
inverse
|
|
538
|
+
/>
|
|
539
|
+
) : null}
|
|
540
|
+
<div className="side-slot inspector-slot" hidden={!inspectorOpen}>
|
|
541
|
+
{inspector}
|
|
542
|
+
</div>
|
|
543
|
+
</>
|
|
544
|
+
)}
|
|
545
|
+
</main>
|
|
546
|
+
{narrow && surface === "events" && (
|
|
547
|
+
<>
|
|
548
|
+
<PaneDrawer trigger={navTrigger} name="Session filters" side="left" open={navDrawer} change={setNavDrawer}>
|
|
549
|
+
{sidebar}
|
|
550
|
+
</PaneDrawer>
|
|
551
|
+
<PaneDrawer
|
|
552
|
+
trigger={inspectorTrigger}
|
|
553
|
+
name="Details"
|
|
554
|
+
side="right"
|
|
555
|
+
open={inspectorDrawer}
|
|
556
|
+
change={setInspectorDrawer}
|
|
557
|
+
>
|
|
558
|
+
{inspector}
|
|
559
|
+
</PaneDrawer>
|
|
560
|
+
</>
|
|
561
|
+
)}
|
|
562
|
+
<footer className="app-footer">
|
|
563
|
+
<span>{connected ? "Connected · observing live events" : "Disconnected · reopen from Pi"}</span>
|
|
564
|
+
<span>Read-only · bounded display copies · no provider transport spans</span>
|
|
565
|
+
</footer>
|
|
566
|
+
</Theme>
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const root = document.getElementById("root");
|
|
570
|
+
if (root) createRoot(root).render(<App />);
|