@narumitw/pi-inspect 0.1.0 → 0.2.1
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/README.md +4 -1
- package/dist/app.css +1 -1
- package/dist/app.js +31 -29
- package/docs/viewer.md +26 -15
- package/package.json +1 -1
- package/src/entry-summary.ts +34 -0
- package/src/model.ts +4 -0
- package/src/projection.ts +14 -1
- package/src/web/app.tsx +196 -65
- package/src/web/{live-drawer.tsx → captured-executions.tsx} +29 -27
- package/src/web/components.tsx +47 -95
- package/src/web/context-composition.tsx +8 -3
- package/src/web/context-inventory.tsx +65 -0
- package/src/web/format.ts +3 -1
- package/src/web/history.ts +56 -0
- package/src/web/inspector.tsx +104 -58
- package/src/web/recorded-content.tsx +49 -0
- package/src/web/session-filters.tsx +100 -0
- package/src/web/session.css +114 -0
- package/src/web/style.css +3 -2
- package/src/web/tool-inspector.css +175 -0
- package/src/web/tool-inspector.tsx +210 -0
- package/src/web/tool-schema.ts +217 -0
- package/src/web/trace.css +1 -0
- package/src/web/trace.tsx +58 -18
- package/src/web/tree.tsx +0 -281
package/docs/viewer.md
CHANGED
|
@@ -10,14 +10,19 @@ A narrow right-hand minimap summarizes the same filtered segment sequence in up
|
|
|
10
10
|
|
|
11
11
|
**Source distinction is explicit.** After consent, context_with_system observes Pi messages after conversation-only context handlers and prompt/tool restoration. It returns nothing and does not modify the event. The primary view labels this as the last observed Pi-stage context, including its leaf/time when available; later context_with_system hooks, provider conversion and payload hooks can still differ. The advanced provider-payload copy is an independently timestamped hook-stage observation, not a guaranteed final transport body. Public callbacks have no request identity proving turn ownership: cache-warming and retry bodies may appear, so they are never attributed to the context leaf or observation time. Until a request is observed, the native active-leaf projection is labeled session-derived, not exact model input. Compaction/context edits use native Pi semantics, not the full session log. Session events and execution events are never silently appended to context.
|
|
12
12
|
|
|
13
|
-
**
|
|
13
|
+
**Context and Session have independent selection and reading state.** Context is full-width: it never shows Session filters, event Details, or an execution list. Current runtime prompt, tool schemas/exposure/active/callable state, and currently advertised skills are available under **Current runtime information**, not mixed into historical event evidence. This information may not yet have been sent to a model. Inventory cards mount only while their enclosing disclosures are open; closed schema/Data bodies do not mount recursive JSON or text previews, while bounded Copy remains available. JSON object disclosure state is retained within the existing bounded scope cache.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**Session** answers what happened, with one primary navigation area and a content-first **Details** panel. **History** defaults to List and follows the active branch; entries are ordered by recorded ancestry, not timestamps, and grouped by user turn. Entries before the first user turn have their own group. Groups are reading aids, not execution parentage. User/assistant/tool summaries come from bounded redacted content; truncation is labeled. Missing leaves/parents and cycles produce explicit partial-history notices, never repaired links.
|
|
16
16
|
|
|
17
|
-
**
|
|
17
|
+
**Show internal events** includes generic Pi bookkeeping: custom state entries, labels, session metadata, and usage entries. Classification uses the original entry type, not a private extension name or message role; model-visible custom messages, including legacy messages with role `custom`, remain visible. Errors, compaction, branch summaries, system/context changes, model/thinking changes, and unknown types are not silently hidden. Matching hidden internal records are counted, excluding selected records retained visibly by the navigation exception. History searches show direct matches without every ancestor. Search covers the bounded summary/label inventory, not uncaptured full entry content.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
**Branch view** replaces the duplicate sidebar tree. It retains actual session-entry `parentId` links and access to off-branch entries. Its filters retain actual ancestors, visibly marked as context; child counts are visible counts, not persisted totals. **History at selected entry** explicitly changes the browser's history scope; **Follow active branch** restores automatic active-leaf tracking. Neither changes Pi's leaf. A selection outside the displayed branch is explicitly labeled. Related-entry navigation can reveal an entry outside filters without clearing them; changing filters ends that exception.
|
|
20
20
|
|
|
21
|
+
Clicking a log row selects it without changing disclosure; its chevron toggles disclosure without selecting. Enter selects, Space toggles, ArrowUp/ArrowDown move focus, ArrowRight expands or enters a child in Branch view, and ArrowLeft collapses or moves to the recorded parent. Multiple rows can be expanded; collapsing a parent retains descendant expansion state. Inline Overview/Content/Raw/Metadata sections lazily load bounded recorded text/output, tool arguments, usage, and structured JSON. Complete inspection is available in Details.
|
|
22
|
+
|
|
23
|
+
History and Branch pages contain at most 50 visible rows, with one visible keyboard tab target; ArrowUp/ArrowDown can cross pages. Branch ancestry traversal is iterative and uncapped by visual depth: page-local/scaled indentation preserves absolute ARIA levels and original parents. Initial reveal does not fetch every ancestor's details. Session filters and Details are collapsible, resizable desktop panes; pointer dragging and focused-separator ArrowLeft/ArrowRight/Home/End resize them. Cancellation, Escape, blur and unmount release drag listeners. Narrow layouts use focus-trapped Dialog drawers with Escape dismissal and focus restoration. Entry selection opens Details; headers expose these controls only in Session. View choices, filters, widths, and disclosure are ephemeral, not saved settings.
|
|
24
|
+
|
|
25
|
+
Entry and branch failures have separate visible retries; unavailable historical checkpoints remain distinct from request failures. Live snapshots do not clear a failed selected-branch alert. Inline loads use four concurrent requests and a 64-entry immutable historical-detail cache; collapse, inactive views, disposal, and terminal session failures cancel owned requests. Historical section JSON disclosure is isolated by entry/section scope within a bounded 128-scope cache. Existing capture bounds still apply; UI expansion cannot recover omitted data.
|
|
21
26
|
Malformed entry/parent IDs, non-string type/role/timestamp fields and all duplicated entry identities are excluded from navigation without manufacturing replacement IDs. Duplicate ancestor/detail identities are explicitly unavailable rather than selecting the last indexed occurrence. The overview labels the count and exposes up to 20 indexed raw diagnostic samples, each bounded to 2 KiB-equivalent characters; source entries are unchanged. Invalid leaf identities cannot become a browser selection or captured transcript anchor.
|
|
22
27
|
|
|
23
28
|
A session-owned structural index is rebuilt on structural changes and released on stop/replacement. Inline requests reuse it and project only their validated ancestor path, not every off-branch entry; valid context-edit/compaction replay remains the native Pi result.
|
|
@@ -28,22 +33,28 @@ The compact overview distinguishes persisted entries, indexed assistant messages
|
|
|
28
33
|
|
|
29
34
|
Each captured execution has a unique `occurrenceId`; `id` remains the provider/native raw tool-call ID. Repeated raw IDs retain separate history and occurrence-parent links. Transcript correlation requires the recorded owning-assistant anchor, not raw ID alone; overlapping invalid IDs are explicitly marked unavailable rather than assigning an ambiguous result. Evicting a selected occurrence returns selection to its remembered recorded anchor (or current leaf).
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
**Captured executions** is an explicit Session view, not a drawer attached to Context. Running/error counts remain visible in Session navigation without switching the user's view on arrival. Rows use occurrence-parent relationships; chevrons expand child navigation only. Arguments, results, relationships, correlation availability, reported duration and observed times appear once in the selection-owned Details panel. Root calls have no recorded parent; unresolved or invalid parent evidence is labeled unavailable. Correlation requires a recorded anchor and unambiguous IDs, not merely the absence of an error flag. Related execution buttons navigate real parent/child occurrences; **Inspect recorded anchor** returns to Branch view only when that anchor is represented. Eviction returns selection to its represented remembered anchor or current leaf; it switches Captured executions to Branch view but preserves History when already reading it.
|
|
32
37
|
|
|
33
38
|
Timeline is an optional shared-axis view. Log timestamps are points, not duration spans. `observedStartedAt`/`observedEndedAt` are wall-clock samples taken on entry to this collector\'s actual start/end callbacks, after activation; their intervals are labeled observations, not provider transport timing. Public `durationMs` remains the separate monotonic time spent in `execute()`, absent if it did not run. Missing starts are never synthesized from durations; update/end-only, running or reversed-clock records do not become closed spans. Earlier callbacks or framework work can separate observation intervals from reported execution durations.
|
|
34
39
|
|
|
35
|
-
|
|
40
|
+
**Details** defaults to Content: literal recorded message blocks, thinking, tool arguments/results, or internal-event payload. Metadata and Raw JSON are secondary; historical Prompt/Tools/Context/Skills/Codemode remain accessible in a horizontally scrollable tab strip. Live executions disable historical transcript tabs; their recorded anchor can be inspected separately. Inapplicable selections fall back to Content. Historical prompt sections, updates, previous-node diffs, declared tools, selected-entry contribution, and skill-read evidence never borrow the current runtime inventory as historical evidence. Full IDs remain in metadata/copy while identity headings use short IDs. Technical JSON can scroll within the pane without page-wide overflow.
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
| --- | --- | --- |
|
|
39
|
-
| Tree / transcript | Raw entries, roots, branches, labels, compaction, summaries, and unknown entry previews | Up to 10,000 nodes, reserving the active leaf and bounded real ancestry; 50 trace rows and 100 navigator rows per page |
|
|
40
|
-
| Prompt | Historical replay through Pi AI helpers, sections, updates, previous-node diff; separate current runtime prompt | Older history may have no system checkpoint; current runtime changes may not have been sent yet |
|
|
41
|
-
| Tools / MCP | Configured tool schemas, namespace, exposure, active/callable status; historical declared tools on selected branch | Active does not equal provider-visible; full MCP connection/auth/config state is unavailable |
|
|
42
|
-
| Skills | Currently advertised catalog, explicit skill invocation, successful exact-path reads and persisted nested-read metadata | Relative/aliased reads may lack evidence; old catalogs and model compliance are unknown |
|
|
43
|
-
| Context | Pi's compaction/context-edit projection and selected-entry contribution | Request-local hooks and provider adapters may transform it further; not a final HTTP request |
|
|
44
|
-
| Codemode | Script source, final printed output, public persisted nestedCalls metadata, and live correlated child results | Historical child results are not saved by Pi; metadata can be incomplete |
|
|
45
|
-
| Live calls | Parent IDs, bounded args/results, status, reported duration, observed callback timestamps, partial updates | Session-wide, not a selected-branch Promise dependency graph; running calls at settle become unfinished |
|
|
42
|
+
**View branch context**, in Details' Context tab, opens the same composition reader with a **Session-derived preview** identity and selected leaf. Native Pi compaction/context edits apply, but request-local hooks and provider serialization remain unknown: this is not a captured historical request. Missing checkpoints or invalid ancestry remain explicitly unavailable. The preview has no unrelated provider observation or current runtime inventory. **Back to last observed context** restores the observed reader and its independent search/disclosure/scroll state.
|
|
46
43
|
|
|
44
|
+
| Capability | Location | Provenance / limitation |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| Ordered model input, search, minimap, inline message/metadata/raw/copy | Context | Last observed Pi context; explicit session-derived active-leaf fallback until observed |
|
|
47
|
+
| Current prompt, tools/MCP schemas and exposure, advertised skills | Context → Current runtime information | Current inventory, not historical request evidence; active ≠ provider-visible; MCP connection/auth state unavailable |
|
|
48
|
+
| Provider payload | Context → Advanced independent provider observation | Independent time; request/leaf association unavailable, including warming/retries |
|
|
49
|
+
| Conversation turns, summaries, internal entries, List/Timeline | Session → History | Selected branch ancestry; original entries unchanged; up to 50 visible rows per page |
|
|
50
|
+
| Roots, branches, labels, unknown/malformed ancestry | Session → Branch view and overview diagnostics | Up to 10,000 nodes reserving active ancestry; original parent relationships, not call parentage |
|
|
51
|
+
| Historical prompt, sections, updates, previous-node diff | Details → Prompt | Pi replay from stored checkpoints; missing historical checkpoints unavailable |
|
|
52
|
+
| Historical declared tools and skill invocation/read evidence | Details → Tools / Skills | Branch-local stored evidence; old catalogs and model compliance unknown |
|
|
53
|
+
| Branch projection and selected-entry contribution | Details → Context → View branch context | Explicit native session-derived preview, not final HTTP input |
|
|
54
|
+
| Script source, printed output, persisted nested metadata | Details → Codemode and Raw | Historical child results are not saved by Pi; discovery/model helpers unavailable unless printed |
|
|
55
|
+
| Live nested arguments/results, statuses, occurrence IDs, observed intervals | Session → Captured executions → Details | Session-wide public execution events; no invented Promise graph or request association |
|
|
56
|
+
|
|
57
|
+
Timeline uses log timestamps as points and observed execution callbacks as intervals, never invented model/transport spans. History/List and History/Timeline show the same branch scope. The timing axis includes the indexed session and captured observations for comparison; it does not infer execution causality from adjacent log rows.
|
|
47
58
|
Live results come from public `tool_execution_end` events, after the runtime's tool-result transformation pipeline (verified with a real nested call and a transforming hook). Partial updates are provisional. Unknown tools, schema failures, and permission blocks still have nested execution events. Direct transcript results remain authoritative persisted history. The inspector observes only: its context_with_system and before_provider_request handlers return nothing and never mutate messages or payloads; it registers no result-transforming handler.
|
|
48
59
|
|
|
49
60
|
Codemode discovery helpers (`searchTools`, `describeTool`, `describeNamespace`, catalog helpers), sandbox local variables, and internal Promise scheduling are not normal child-tool events. `models.classify` and `models.generateImages` are also not normal nested tools: their complete input/output is unavailable unless printed. The generic Raw tab can show documented persisted metadata but the inspector never imports another extension or depends on private codemode implementation types.
|
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Json } from "./model.js";
|
|
3
|
+
import { capture } from "./privacy.js";
|
|
4
|
+
|
|
5
|
+
/** Summarize only an already redacted, bounded display copy. */
|
|
6
|
+
export function readableSummary(entry: SessionEntry): { summary: string; summaryTruncated?: true } {
|
|
7
|
+
const source =
|
|
8
|
+
entry.type === "message"
|
|
9
|
+
? "content" in entry.message
|
|
10
|
+
? entry.message.content
|
|
11
|
+
: undefined
|
|
12
|
+
: entry.type === "custom_message"
|
|
13
|
+
? entry.content
|
|
14
|
+
: entry.type === "compaction" || entry.type === "branch_summary"
|
|
15
|
+
? entry.summary
|
|
16
|
+
: undefined;
|
|
17
|
+
if (source === undefined || source === null) return { summary: "" };
|
|
18
|
+
const captured = capture(source, 1024);
|
|
19
|
+
const text = (value: Json): string => {
|
|
20
|
+
if (typeof value === "string") return value;
|
|
21
|
+
if (!value || typeof value !== "object") return "";
|
|
22
|
+
if (Array.isArray(value)) return value.map(text).filter(Boolean).join(" · ");
|
|
23
|
+
if (typeof value.text === "string") return value.text;
|
|
24
|
+
if (value.type === "thinking") return "Thinking";
|
|
25
|
+
if (value.type === "image") return "Image";
|
|
26
|
+
if (value.type === "toolCall") return `${value.name ?? "Tool call"} ${JSON.stringify(value.arguments ?? {})}`;
|
|
27
|
+
return "";
|
|
28
|
+
};
|
|
29
|
+
const summary = text(captured.value).replace(/\s+/g, " ").trim();
|
|
30
|
+
return {
|
|
31
|
+
summary: summary.slice(0, 180),
|
|
32
|
+
summaryTruncated: captured.truncated || summary.length > 180 ? true : undefined,
|
|
33
|
+
};
|
|
34
|
+
}
|
package/src/model.ts
CHANGED
|
@@ -8,6 +8,9 @@ export interface EntrySummary {
|
|
|
8
8
|
parentId: string | null;
|
|
9
9
|
kind: string;
|
|
10
10
|
label: string;
|
|
11
|
+
summary?: string;
|
|
12
|
+
summaryTruncated?: boolean;
|
|
13
|
+
internal?: true;
|
|
11
14
|
timestamp: string;
|
|
12
15
|
name?: string;
|
|
13
16
|
nameTruncated?: boolean;
|
|
@@ -102,6 +105,7 @@ export interface BranchView {
|
|
|
102
105
|
declaredTools: Capture;
|
|
103
106
|
promptUpdates: Capture;
|
|
104
107
|
projection: Capture;
|
|
108
|
+
context?: ContextComposition;
|
|
105
109
|
skillEvidence: Capture;
|
|
106
110
|
}
|
|
107
111
|
export interface DetailView {
|
package/src/projection.ts
CHANGED
|
@@ -16,9 +16,10 @@ type ReadonlySessionManager = ExtensionContext["sessionManager"];
|
|
|
16
16
|
|
|
17
17
|
import { ancestry } from "./ancestry.js";
|
|
18
18
|
import type { Collector } from "./collector.js";
|
|
19
|
-
import { sessionContext } from "./context.js";
|
|
19
|
+
import { captureContext, sessionContext } from "./context.js";
|
|
20
20
|
import { correlatedCalls } from "./correlation.js";
|
|
21
21
|
import { EntryIndex } from "./entry-index.js";
|
|
22
|
+
import { readableSummary } from "./entry-summary.js";
|
|
22
23
|
import { identityIssue, recordedLeaf } from "./identity.js";
|
|
23
24
|
import type { BranchView, ContextComposition, DetailView, EntrySummary, SkillView, Snapshot } from "./model.js";
|
|
24
25
|
import { nativeProjection } from "./native-projection.js";
|
|
@@ -48,6 +49,8 @@ export function summarize(entry: SessionEntry, label?: string): EntrySummary {
|
|
|
48
49
|
return {
|
|
49
50
|
id: entry.id,
|
|
50
51
|
parentId: entry.parentId,
|
|
52
|
+
...readableSummary(entry),
|
|
53
|
+
internal: ["custom", "label", "session_info", "usage"].includes(entry.type) ? true : undefined,
|
|
51
54
|
kind,
|
|
52
55
|
timestamp: entry.timestamp,
|
|
53
56
|
name: displayName?.slice(0, 512),
|
|
@@ -199,6 +202,15 @@ export function branch(
|
|
|
199
202
|
declaredTools: unavailable,
|
|
200
203
|
promptUpdates: unavailable,
|
|
201
204
|
projection: unavailable,
|
|
205
|
+
context: {
|
|
206
|
+
source: "session-derived",
|
|
207
|
+
leafId,
|
|
208
|
+
totalMessages: 0,
|
|
209
|
+
messages: [],
|
|
210
|
+
segments: [],
|
|
211
|
+
incomplete: true,
|
|
212
|
+
unavailable: issue,
|
|
213
|
+
},
|
|
202
214
|
skillEvidence: unavailable,
|
|
203
215
|
};
|
|
204
216
|
}
|
|
@@ -305,6 +317,7 @@ export function branch(
|
|
|
305
317
|
projection.entries.map((e) => ({ id: e.sourceEntry.id, messages: e.messages })),
|
|
306
318
|
65536,
|
|
307
319
|
),
|
|
320
|
+
context: captureContext(projection.messages, "session-derived", leafId),
|
|
308
321
|
skillEvidence: capturedEvidence,
|
|
309
322
|
};
|
|
310
323
|
}
|
package/src/web/app.tsx
CHANGED
|
@@ -5,17 +5,20 @@ import { createRoot } from "react-dom/client";
|
|
|
5
5
|
import "@radix-ui/themes/styles.css";
|
|
6
6
|
import "./style.css";
|
|
7
7
|
import "./composition.css";
|
|
8
|
-
import
|
|
8
|
+
import "./session.css";
|
|
9
|
+
import type { BranchView, ContextComposition as Composition, Snapshot } from "../model.js";
|
|
9
10
|
import { generation, headers, RequestFailure, request, useDetail } from "./api.js";
|
|
11
|
+
import { CapturedExecutions } from "./captured-executions.js";
|
|
10
12
|
import { ContextComposition } from "./context-composition.js";
|
|
13
|
+
import { ContextInventory } from "./context-inventory.js";
|
|
11
14
|
import { type Filters, matches } from "./format.js";
|
|
15
|
+
import { history, internalEntry } from "./history.js";
|
|
12
16
|
import { Inspector } from "./inspector.js";
|
|
13
|
-
import { LiveDrawer } from "./live-drawer.js";
|
|
14
17
|
import { Overview } from "./overview.js";
|
|
15
18
|
import { PaneDrawer, ResizeHandle, useNarrow } from "./panes.js";
|
|
16
19
|
import { boundedSearch, searchNeedle } from "./search.js";
|
|
20
|
+
import { SessionFilters } from "./session-filters.js";
|
|
17
21
|
import { Trace } from "./trace.js";
|
|
18
|
-
import { Sidebar } from "./tree.js";
|
|
19
22
|
|
|
20
23
|
type Selection = { kind: "entry" | "call"; id: string; serial: number; filterVersion?: number; anchor?: string | null };
|
|
21
24
|
function App() {
|
|
@@ -29,7 +32,10 @@ function App() {
|
|
|
29
32
|
const [selection, setSelection] = useState<Selection>();
|
|
30
33
|
const [view, setView] = useState("list");
|
|
31
34
|
const [surface, setSurface] = useState("context");
|
|
32
|
-
const [
|
|
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>();
|
|
33
39
|
const [filters, setFilters] = useState<Filters>({
|
|
34
40
|
query: "",
|
|
35
41
|
kind: "all",
|
|
@@ -46,7 +52,7 @@ function App() {
|
|
|
46
52
|
const [error, setError] = useState("");
|
|
47
53
|
const [connected, setConnected] = useState(false);
|
|
48
54
|
const lifecycle = useRef<{ terminal: boolean; stream?: AbortController }>({ terminal: false });
|
|
49
|
-
const [tab, setTab] = useState("
|
|
55
|
+
const [tab, setTab] = useState("content");
|
|
50
56
|
const navTrigger = useRef<HTMLButtonElement>(null);
|
|
51
57
|
const inspectorTrigger = useRef<HTMLButtonElement>(null);
|
|
52
58
|
const [navOpen, setNavOpen] = useState(false);
|
|
@@ -67,11 +73,15 @@ function App() {
|
|
|
67
73
|
const callAnchorAvailable = Boolean(selectedCall?.branchAnchor && represented.has(selectedCall.branchAnchor));
|
|
68
74
|
const entryId =
|
|
69
75
|
selection?.kind === "entry" ? selection.id : callAnchorAvailable ? (selectedCall?.branchAnchor ?? "") : "";
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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]);
|
|
75
85
|
const selectedNode = entries.find((node) => node.id === entryId);
|
|
76
86
|
const preparedFilters = useMemo(() => ({ ...filters, query: searchNeedle(filters.query) }), [filters]);
|
|
77
87
|
const matching = useMemo(
|
|
@@ -80,6 +90,9 @@ function App() {
|
|
|
80
90
|
);
|
|
81
91
|
function selectEntry(id: string) {
|
|
82
92
|
setSurface("events");
|
|
93
|
+
if (sessionView === "executions") setSessionView("branches");
|
|
94
|
+
if (narrow) setInspectorDrawer(true);
|
|
95
|
+
else setInspectorOpen(true);
|
|
83
96
|
setSelection((old) => ({ kind: "entry", id, serial: (old?.serial ?? 0) + 1, filterVersion }));
|
|
84
97
|
}
|
|
85
98
|
function selectCall(id: string) {
|
|
@@ -92,7 +105,9 @@ function App() {
|
|
|
92
105
|
filterVersion,
|
|
93
106
|
anchor: calls.find((call) => call.occurrenceId === id)?.branchAnchor,
|
|
94
107
|
}));
|
|
95
|
-
|
|
108
|
+
setSurface("events");
|
|
109
|
+
setSessionView("executions");
|
|
110
|
+
setTab("content");
|
|
96
111
|
}
|
|
97
112
|
useEffect(() => {
|
|
98
113
|
const controller = new AbortController();
|
|
@@ -214,7 +229,8 @@ function App() {
|
|
|
214
229
|
}, [revision]);
|
|
215
230
|
useEffect(() => {
|
|
216
231
|
setBranchError("");
|
|
217
|
-
if (!entryId) {
|
|
232
|
+
if (!entryId || !paneActive) {
|
|
233
|
+
branchOwner.current = undefined;
|
|
218
234
|
setBranch(undefined);
|
|
219
235
|
return;
|
|
220
236
|
}
|
|
@@ -231,21 +247,26 @@ function App() {
|
|
|
231
247
|
setBranchError("Could not load selected branch context.");
|
|
232
248
|
});
|
|
233
249
|
return () => controller.abort();
|
|
234
|
-
}, [entryId, branchAttempt]);
|
|
250
|
+
}, [entryId, branchAttempt, paneActive]);
|
|
235
251
|
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
matches={matching}
|
|
240
|
-
total={snapshot?.totalEntries ?? 0}
|
|
241
|
-
selected={entryId}
|
|
242
|
-
serial={selection?.serial ?? 0}
|
|
243
|
-
filters={filters}
|
|
244
|
-
change={changeFilters}
|
|
245
|
-
revealSelected={selection?.filterVersion === filterVersion}
|
|
246
|
-
choose={selectEntry}
|
|
247
|
-
/>
|
|
252
|
+
const scoped = useMemo(
|
|
253
|
+
() => history(entries, historyLeaf ?? snapshot?.leafId ?? null),
|
|
254
|
+
[entries, historyLeaf, snapshot?.leafId],
|
|
248
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;
|
|
249
270
|
const inspector = (
|
|
250
271
|
<Inspector
|
|
251
272
|
selected={selection?.id ?? ""}
|
|
@@ -254,14 +275,22 @@ function App() {
|
|
|
254
275
|
detailError={detailError}
|
|
255
276
|
retryDetails={retryDetails}
|
|
256
277
|
call={selectedCall}
|
|
257
|
-
branch={
|
|
258
|
-
branchError={branchError}
|
|
278
|
+
branch={selectedBranch}
|
|
279
|
+
branchError={branchOwner.current?.entryId === entryId ? branchError : ""}
|
|
259
280
|
retryBranch={() => setBranchAttempt((attempt) => attempt + 1)}
|
|
260
281
|
snapshot={snapshot}
|
|
261
282
|
tab={tab}
|
|
262
283
|
changeTab={setTab}
|
|
263
284
|
select={selectEntry}
|
|
264
285
|
selectCall={selectCall}
|
|
286
|
+
preview={() => {
|
|
287
|
+
if (selectedBranch?.context) {
|
|
288
|
+
setPreview(selectedBranch.context);
|
|
289
|
+
setSurface("context");
|
|
290
|
+
setInspectorDrawer(false);
|
|
291
|
+
setNavDrawer(false);
|
|
292
|
+
}
|
|
293
|
+
}}
|
|
265
294
|
/>
|
|
266
295
|
);
|
|
267
296
|
return (
|
|
@@ -291,16 +320,26 @@ function App() {
|
|
|
291
320
|
</div>
|
|
292
321
|
<div className="header-actions">
|
|
293
322
|
<fieldset className="surface-switch" aria-label="Inspection source">
|
|
294
|
-
<button
|
|
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
|
+
>
|
|
295
333
|
Context
|
|
296
334
|
</button>
|
|
297
335
|
<button type="button" aria-pressed={surface === "events"} onClick={() => setSurface("events")}>
|
|
298
|
-
Session
|
|
336
|
+
Session
|
|
299
337
|
</button>
|
|
300
338
|
</fieldset>
|
|
301
339
|
<IconButton
|
|
340
|
+
hidden={surface !== "events"}
|
|
302
341
|
ref={navTrigger}
|
|
303
|
-
aria-label="Toggle
|
|
342
|
+
aria-label="Toggle filters"
|
|
304
343
|
aria-expanded={narrow ? navDrawer : navOpen}
|
|
305
344
|
variant="ghost"
|
|
306
345
|
onClick={() => (narrow ? setNavDrawer(!navDrawer) : setNavOpen(!navOpen))}
|
|
@@ -308,8 +347,9 @@ function App() {
|
|
|
308
347
|
<HamburgerMenuIcon />
|
|
309
348
|
</IconButton>
|
|
310
349
|
<IconButton
|
|
350
|
+
hidden={surface !== "events"}
|
|
311
351
|
ref={inspectorTrigger}
|
|
312
|
-
aria-label="Toggle
|
|
352
|
+
aria-label="Toggle details"
|
|
313
353
|
aria-expanded={narrow ? inspectorDrawer : inspectorOpen}
|
|
314
354
|
variant="ghost"
|
|
315
355
|
onClick={() => (narrow ? setInspectorDrawer(!inspectorDrawer) : setInspectorOpen(!inspectorOpen))}
|
|
@@ -347,58 +387,149 @@ function App() {
|
|
|
347
387
|
narrow
|
|
348
388
|
? undefined
|
|
349
389
|
: {
|
|
350
|
-
gridTemplateColumns:
|
|
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`,
|
|
351
394
|
}
|
|
352
395
|
}
|
|
353
396
|
>
|
|
354
|
-
{!narrow && (
|
|
397
|
+
{!narrow && surface === "events" && (
|
|
355
398
|
<>
|
|
356
399
|
<div className="side-slot nav-slot" hidden={!navOpen}>
|
|
357
400
|
{sidebar}
|
|
358
401
|
</div>
|
|
359
402
|
{navOpen && (
|
|
360
|
-
<ResizeHandle name="Resize
|
|
403
|
+
<ResizeHandle name="Resize filters" value={navWidth} change={setNavWidth} min={180} max={420} />
|
|
361
404
|
)}
|
|
362
405
|
</>
|
|
363
406
|
)}
|
|
364
407
|
<div className="center-column">
|
|
365
408
|
<div className="context-surface" hidden={surface !== "context"}>
|
|
366
|
-
<
|
|
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
|
+
)}
|
|
367
424
|
</div>
|
|
368
425
|
<div className="events-surface" hidden={surface !== "events"}>
|
|
369
426
|
<Overview snapshot={snapshot} />
|
|
370
|
-
<
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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>
|
|
381
526
|
</div>
|
|
382
|
-
{(calls.length > 0 || surface === "events") && (
|
|
383
|
-
<LiveDrawer
|
|
384
|
-
entries={entries}
|
|
385
|
-
calls={calls}
|
|
386
|
-
selected={selectedCall?.occurrenceId}
|
|
387
|
-
select={selectCall}
|
|
388
|
-
open={liveOpen}
|
|
389
|
-
changeOpen={setLiveOpen}
|
|
390
|
-
view={view}
|
|
391
|
-
dropped={snapshot?.droppedCalls ?? 0}
|
|
392
|
-
invalidEvents={snapshot?.invalidCallEvents ?? 0}
|
|
393
|
-
filters={filters}
|
|
394
|
-
/>
|
|
395
|
-
)}
|
|
396
527
|
</div>
|
|
397
|
-
{!narrow && (
|
|
528
|
+
{!narrow && surface === "events" && (
|
|
398
529
|
<>
|
|
399
530
|
{inspectorOpen ? (
|
|
400
531
|
<ResizeHandle
|
|
401
|
-
name="Resize
|
|
532
|
+
name="Resize details"
|
|
402
533
|
value={inspectorWidth}
|
|
403
534
|
change={setInspectorWidth}
|
|
404
535
|
min={260}
|
|
@@ -412,14 +543,14 @@ function App() {
|
|
|
412
543
|
</>
|
|
413
544
|
)}
|
|
414
545
|
</main>
|
|
415
|
-
{narrow && (
|
|
546
|
+
{narrow && surface === "events" && (
|
|
416
547
|
<>
|
|
417
|
-
<PaneDrawer trigger={navTrigger} name="Session
|
|
548
|
+
<PaneDrawer trigger={navTrigger} name="Session filters" side="left" open={navDrawer} change={setNavDrawer}>
|
|
418
549
|
{sidebar}
|
|
419
550
|
</PaneDrawer>
|
|
420
551
|
<PaneDrawer
|
|
421
552
|
trigger={inspectorTrigger}
|
|
422
|
-
name="
|
|
553
|
+
name="Details"
|
|
423
554
|
side="right"
|
|
424
555
|
open={inspectorDrawer}
|
|
425
556
|
change={setInspectorDrawer}
|