@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/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
- **Session events** is a secondary source view retaining the existing actual-parent Trace Explorer and timeline. Navigator and Event Inspector are collapsed by default and open only on request; choosing a Navigator event switches to Session events without changing the Pi leaf; captured live calls remain in a compact closed drawer and its empty primary region is omitted. The Session Navigator handles search, filtering and concise hierarchy navigation; the Trace Explorer handles progressive inline log inspection; the Event Inspector shows complete bounded historical/debugging details. Desktop side panes are resizable with pointer dragging or focused-separator ArrowLeft/ArrowRight/Home/End, and can be collapsed. Pointer cancellation, Escape, blur and unmount release drag listeners. When the viewport or resized sides leave insufficient trace space, side panes become Radix Dialog drawers with focus trapping, Escape dismissal and focus restoration. Widths and view choices are ephemeral browser state, not saved settings.
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
- Log rows use actual `parentId` relationships; live execution rows use only `parentToolCallId`. They are never merged into a fictional execution graph. Filters retain actual ancestors, visibly marked as context; trace child counts are labeled visible counts, not persisted totals. Explicit related-entry navigation can reveal a selected entry outside the current filters without clearing them; changing filters ends that exception. Metadata-only navigation items do not print JSON snippets. Entry and session names are sanitized before the 512-character bound; truncation is labeled. Raw exposes the full bounded captured entry value without changing its source. Long labels remain one line with ellipsis and hover text.
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
- **List is the default Session events mode.** Clicking a trace row selects and toggles it; its chevron toggles without changing selection. Enter selects, Space toggles, ArrowUp/ArrowDown move focus, ArrowRight expands or enters a child, and ArrowLeft collapses or moves to the recorded parent. Multiple rows can be expanded. Collapsing a parent retains descendant expansion state. Selected, expanded and keyboard-focused states are distinct. Inline Overview/Content/Raw/Metadata panels lazily fetch full bounded entries, display recorded text/output and tool arguments, and expose collapsible JSON objects/arrays. Inspector and navigator selection stay synchronized without navigating Pi.
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
- Trace pages contain at most 50 visible rows; navigator pages contain at most 100. Paging, filtering and collapse retain one visible keyboard tab target; ArrowUp/ArrowDown can cross trace pages. Arbitrary hierarchy levels are supported with iterative traversal, not a depth cap. Long chains use page-local/scaled indentation while retaining absolute ARIA levels and original parent IDs. Initial selection reveals its ancestors without fetching every ancestor\'s details. Detail request failures are shown with an explicit retry for both the Inspector and inline sections; historical-branch request failures have a separate selected-branch alert/retry that survives live snapshot refreshes; unavailable historical data remains a separate display state. Inline loads use four concurrent requests and a 64-entry immutable historical-detail cache; collapsing/unmounting rows cancels their work. JSON object disclosure state is isolated by scope and retained within a bounded 128-scope cache; Inspector historical sections use entry-specific scopes. Existing ingestion depth/node/character bounds still apply; UI expansion cannot recover omitted data.
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
- Live calls occupy a collapsed bottom drawer, including when calls arrive. Opening it is explicit; its own scroll area shows parent-correlated records and allows selecting an execution in the Inspector. The navigation highlight then denotes that execution\'s recorded anchor entry, not a new transcript result.
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
- The Inspector preserves Raw/Prompt/Tools/Context/Skills/Codemode, uses a non-wrapping horizontally scrollable tab strip, and disables inapplicable tabs consistently: Context is unavailable for non-transcript live events, Skills requires catalog/evidence, and Codemode requires recorded codemode source/output. It falls back to Raw if the selected tab becomes inapplicable. Structured metadata and object disclosure are the default; Raw JSON text is explicit. Data sections can be collapsed, and technical strings/JSON scroll horizontally rather than forcing page-wide overflow. The panel owns vertical scrolling, rather than stacking nested vertical code scrollers.
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
- | View | Available | Limitations |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-inspect",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Read-only browser inspector for Pi session context and live tool calls.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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 type { BranchView, Snapshot } from "../model.js";
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 [liveOpen, setLiveOpen] = useState(false);
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("raw");
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
- detail,
72
- error: detailError,
73
- retry: retryDetails,
74
- } = useDetail(selection?.kind === "entry" ? entryId : undefined);
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
- setLiveOpen(true);
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 sidebar = (
237
- <Sidebar
238
- nodes={entries}
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={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 type="button" aria-pressed={surface === "context"} onClick={() => setSurface("context")}>
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 events
336
+ Session
299
337
  </button>
300
338
  </fieldset>
301
339
  <IconButton
340
+ hidden={surface !== "events"}
302
341
  ref={navTrigger}
303
- aria-label="Toggle navigator"
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 inspector"
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: `${navSize}px ${navOpen ? 6 : 0}px minmax(0, 1fr) ${inspectorOpen ? 6 : 0}px ${inspectorSize}px`,
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 navigator" value={navWidth} change={setNavWidth} min={180} max={420} />
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
- <ContextComposition context={snapshot?.context} payload={snapshot?.providerObservation} />
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
- <Trace
371
- entries={entries}
372
- matches={matching}
373
- selected={entryId}
374
- serial={selection?.serial ?? 0}
375
- revealSelected={selection?.filterVersion === filterVersion}
376
- select={selectEntry}
377
- view={view}
378
- changeView={setView}
379
- calls={calls}
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 inspector"
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 navigator" side="left" open={navDrawer} change={setNavDrawer}>
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="Event inspector"
553
+ name="Details"
423
554
  side="right"
424
555
  open={inspectorDrawer}
425
556
  change={setInspectorDrawer}