@osolmaz/pi-workflows 0.1.0 → 0.3.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/README.md +107 -26
- package/dist/builtins/monitor.workflow.d.ts +71 -0
- package/dist/builtins/monitor.workflow.js +234 -0
- package/dist/builtins/monitor.workflow.js.map +1 -0
- package/dist/controllers/conditions.d.ts +6 -0
- package/dist/controllers/conditions.js +68 -0
- package/dist/controllers/conditions.js.map +1 -0
- package/dist/controllers/definition.d.ts +6 -0
- package/dist/controllers/definition.js +45 -0
- package/dist/controllers/definition.js.map +1 -0
- package/dist/controllers/effects.d.ts +14 -0
- package/dist/controllers/effects.js +104 -0
- package/dist/controllers/effects.js.map +1 -0
- package/dist/controllers/errors.d.ts +12 -0
- package/dist/controllers/errors.js +25 -0
- package/dist/controllers/errors.js.map +1 -0
- package/dist/controllers/index.d.ts +13 -0
- package/dist/controllers/index.js +13 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/controllers/json.d.ts +5 -0
- package/dist/controllers/json.js +57 -0
- package/dist/controllers/json.js.map +1 -0
- package/dist/controllers/loader.d.ts +23 -0
- package/dist/controllers/loader.js +74 -0
- package/dist/controllers/loader.js.map +1 -0
- package/dist/controllers/manager.d.ts +58 -0
- package/dist/controllers/manager.js +399 -0
- package/dist/controllers/manager.js.map +1 -0
- package/dist/controllers/results.d.ts +5 -0
- package/dist/controllers/results.js +32 -0
- package/dist/controllers/results.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +212 -0
- package/dist/controllers/sqlite.js +1009 -0
- package/dist/controllers/sqlite.js.map +1 -0
- package/dist/controllers/store.d.ts +112 -0
- package/dist/controllers/store.js +32 -0
- package/dist/controllers/store.js.map +1 -0
- package/dist/controllers/types.d.ts +159 -0
- package/dist/controllers/types.js +2 -0
- package/dist/controllers/types.js.map +1 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +25 -0
- package/dist/controllers/workflow-engine-scheduler.js +93 -0
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -0
- package/dist/controllers/workflows.d.ts +27 -0
- package/dist/controllers/workflows.js +109 -0
- package/dist/controllers/workflows.js.map +1 -0
- package/dist/extension/controller-host.d.ts +47 -0
- package/dist/extension/controller-host.js +110 -0
- package/dist/extension/controller-host.js.map +1 -0
- package/dist/extension/executor.d.ts +14 -1
- package/dist/extension/executor.js +12 -2
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/index.d.ts +7 -0
- package/dist/extension/index.js +1118 -89
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +91 -0
- package/dist/extension/recorder.js +537 -0
- package/dist/extension/recorder.js.map +1 -0
- package/dist/extension/session-events.d.ts +134 -0
- package/dist/extension/session-events.js +60 -0
- package/dist/extension/session-events.js.map +1 -0
- package/dist/extension/widget.js +25 -24
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-tool.d.ts +28 -0
- package/dist/extension/workflow-tool.js +33 -0
- package/dist/extension/workflow-tool.js.map +1 -0
- package/dist/host/processes.d.ts +24 -0
- package/dist/host/processes.js +114 -0
- package/dist/host/processes.js.map +1 -0
- package/dist/host/rpc-bridge.d.ts +9 -0
- package/dist/host/rpc-bridge.js +39 -0
- package/dist/host/rpc-bridge.js.map +1 -0
- package/dist/host/rpc-executor.d.ts +38 -0
- package/dist/host/rpc-executor.js +254 -0
- package/dist/host/rpc-executor.js.map +1 -0
- package/dist/host/runner.d.ts +49 -0
- package/dist/host/runner.js +350 -0
- package/dist/host/runner.js.map +1 -0
- package/dist/render/canvas.d.ts +1 -1
- package/dist/render/canvas.js +5 -0
- package/dist/render/canvas.js.map +1 -1
- package/dist/render/graph-render.d.ts +5 -0
- package/dist/render/graph-render.js +211 -48
- package/dist/render/graph-render.js.map +1 -1
- package/dist/viewer/cli.d.ts +7 -3
- package/dist/viewer/cli.js +150 -19
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +19 -3
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +45 -0
- package/dist/viewer/session-reducer.js +266 -0
- package/dist/viewer/session-reducer.js.map +1 -0
- package/dist/workflows/artifacts.d.ts +40 -0
- package/dist/workflows/artifacts.js +155 -0
- package/dist/workflows/artifacts.js.map +1 -0
- package/dist/workflows/engine.d.ts +38 -0
- package/dist/workflows/engine.js +281 -20
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +23 -0
- package/dist/workflows/errors.js +38 -0
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/graph.js +0 -5
- package/dist/workflows/graph.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -2
- package/dist/workflows/index.js +2 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/loader.d.ts +5 -3
- package/dist/workflows/loader.js +10 -1
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/schema.js +1 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +98 -10
- package/dist/workflows/store.js +921 -46
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +136 -3
- package/docs/CONTROLLERS.md +215 -0
- package/docs/development.md +51 -24
- package/docs/live-replay-protocol.md +155 -0
- package/docs/plans/2026-08-04-controller-runtime-plan.md +169 -0
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +125 -0
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +184 -0
- package/docs/plans/piw-viewer-experience-implementation-plan.md +674 -0
- package/docs/plans/replayable-run-bundles-implementation-plan.md +65 -0
- package/docs/plans/session-event-replay-implementation-plan.md +494 -0
- package/docs/plans/tui-viewer-implementation-plan.md +64 -0
- package/docs/run-bundles.md +359 -55
- package/docs/session-event-journal.md +470 -0
- package/docs/tui-viewer.md +218 -0
- package/docs/workflows.md +131 -11
- package/examples/controllers/pull-request.controller.ts +215 -0
- package/package.json +11 -2
- package/src/builtins/monitor.workflow.ts +278 -0
- package/src/controllers/conditions.ts +110 -0
- package/src/controllers/definition.ts +65 -0
- package/src/controllers/effects.ts +123 -0
- package/src/controllers/errors.ts +27 -0
- package/src/controllers/index.ts +90 -0
- package/src/controllers/json.ts +62 -0
- package/src/controllers/loader.ts +104 -0
- package/src/controllers/manager.ts +533 -0
- package/src/controllers/results.ts +46 -0
- package/src/controllers/sqlite.ts +1427 -0
- package/src/controllers/store.ts +160 -0
- package/src/controllers/types.ts +183 -0
- package/src/controllers/workflow-engine-scheduler.ts +145 -0
- package/src/controllers/workflows.ts +152 -0
- package/src/extension/controller-host.ts +163 -0
- package/src/extension/executor.ts +29 -2
- package/src/extension/index.ts +1315 -109
- package/src/extension/recorder.ts +662 -0
- package/src/extension/session-events.ts +119 -0
- package/src/extension/widget.ts +26 -24
- package/src/extension/workflow-tool.ts +59 -0
- package/src/host/processes.ts +119 -0
- package/src/host/rpc-bridge.ts +44 -0
- package/src/host/rpc-executor.ts +299 -0
- package/src/host/runner.ts +406 -0
- package/src/render/canvas.ts +19 -1
- package/src/render/graph-render.ts +277 -44
- package/src/viewer/cli.ts +167 -21
- package/src/viewer/render.ts +21 -3
- package/src/viewer/session-reducer.ts +347 -0
- package/src/workflows/artifacts.ts +188 -0
- package/src/workflows/engine.ts +365 -19
- package/src/workflows/errors.ts +45 -0
- package/src/workflows/graph.ts +0 -5
- package/src/workflows/index.ts +15 -0
- package/src/workflows/loader.ts +13 -3
- package/src/workflows/schema.ts +1 -1
- package/src/workflows/store.ts +1157 -48
- package/src/workflows/types.ts +151 -3
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# Session event journal specification
|
|
2
|
+
|
|
3
|
+
This document defines the target format for temporal Pi session history in a
|
|
4
|
+
workflow run bundle. The journal records the order and timing of turns,
|
|
5
|
+
messages, assistant output, and tool execution while a workflow runs.
|
|
6
|
+
|
|
7
|
+
The format extends the authoritative bundle contract in
|
|
8
|
+
[run-bundles.md](run-bundles.md).
|
|
9
|
+
|
|
10
|
+
## Bundle structure
|
|
11
|
+
|
|
12
|
+
A session-bound run contains these files:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
<run-id>/
|
|
16
|
+
├── manifest.json
|
|
17
|
+
├── workflow.json
|
|
18
|
+
├── state.json
|
|
19
|
+
├── trace.ndjson
|
|
20
|
+
├── session/
|
|
21
|
+
│ ├── binding.json
|
|
22
|
+
│ ├── entries.ndjson
|
|
23
|
+
│ ├── events.ndjson
|
|
24
|
+
│ └── capture.json
|
|
25
|
+
└── artifacts/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`events.ndjson` is an append-only journal. `capture.json` is an atomic status
|
|
29
|
+
file that says whether the journal is complete.
|
|
30
|
+
|
|
31
|
+
## History ownership
|
|
32
|
+
|
|
33
|
+
Each persisted file answers a different question.
|
|
34
|
+
|
|
35
|
+
| File | Question | Authority |
|
|
36
|
+
| ------------------------ | ---------------------------------------------------------- | ------------------------------------------------------- |
|
|
37
|
+
| `trace.ndjson` | What did the workflow engine do? | Workflow transitions and outputs |
|
|
38
|
+
| `session/entries.ndjson` | What settled in the Pi conversation? | Final Pi session entries |
|
|
39
|
+
| `session/events.ndjson` | What happened over time while the conversation was active? | Timing for turns, messages, assistant output, and tools |
|
|
40
|
+
| `session/capture.json` | Is the temporal history complete? | Capture status and final counts |
|
|
41
|
+
|
|
42
|
+
A reader must not rebuild final conversation content from session events after
|
|
43
|
+
a matching entry has settled. The Pi entry is the final content. Session events
|
|
44
|
+
preserve how that content appeared and preserve partial work that never settled.
|
|
45
|
+
|
|
46
|
+
The three NDJSON journals keep independent sequence spaces. A sequence number
|
|
47
|
+
from one file cannot be compared with a sequence number from another file.
|
|
48
|
+
Explicit ids provide cross-file linkage.
|
|
49
|
+
|
|
50
|
+
## Minimal journal
|
|
51
|
+
|
|
52
|
+
A journal with one assistant text block can contain these lines:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{"seq":1,"at":"2026-07-30T10:00:00.010Z","nodeId":"review","attemptId":"a1","turnId":"t1","type":"turn_started","payload":{"turnIndex":0}}
|
|
56
|
+
{"seq":2,"at":"2026-07-30T10:00:00.020Z","nodeId":"review","attemptId":"a1","turnId":"t1","messageId":"m1","type":"message_started","payload":{"role":"assistant"}}
|
|
57
|
+
{"seq":3,"at":"2026-07-30T10:00:00.100Z","nodeId":"review","attemptId":"a1","turnId":"t1","messageId":"m1","type":"assistant_event","payload":{"type":"text_start","contentIndex":0}}
|
|
58
|
+
{"seq":4,"at":"2026-07-30T10:00:00.140Z","nodeId":"review","attemptId":"a1","turnId":"t1","messageId":"m1","type":"assistant_event","payload":{"type":"text_delta","contentIndex":0,"delta":"Looks good."}}
|
|
59
|
+
{"seq":5,"at":"2026-07-30T10:00:00.150Z","nodeId":"review","attemptId":"a1","turnId":"t1","messageId":"m1","type":"assistant_event","payload":{"type":"text_end","contentIndex":0,"content":"Looks good."}}
|
|
60
|
+
{"seq":6,"at":"2026-07-30T10:00:00.170Z","nodeId":"review","attemptId":"a1","turnId":"t1","messageId":"m1","type":"message_finished","payload":{"role":"assistant","settled":true,"entryId":"e1"}}
|
|
61
|
+
{"seq":7,"at":"2026-07-30T10:00:00.180Z","nodeId":"review","attemptId":"a1","turnId":"t1","type":"turn_finished","payload":{"turnIndex":0,"messageId":"m1","toolCallIds":[]}}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The matching final Pi entry remains in `session/entries.ndjson`:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"seq": 2,
|
|
69
|
+
"at": "2026-07-30T10:00:00.165Z",
|
|
70
|
+
"entry": {
|
|
71
|
+
"type": "message",
|
|
72
|
+
"id": "e1",
|
|
73
|
+
"parentId": "e0",
|
|
74
|
+
"timestamp": "2026-07-30T10:00:00.160Z",
|
|
75
|
+
"message": { "role": "assistant", "content": [{ "type": "text", "text": "Looks good." }] }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Event record
|
|
81
|
+
|
|
82
|
+
Each line in `events.ndjson` has schema
|
|
83
|
+
`pi-workflows.session-event.v1`:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
type WorkflowSessionEventRecord = {
|
|
87
|
+
seq: number;
|
|
88
|
+
at: string;
|
|
89
|
+
nodeId: string;
|
|
90
|
+
attemptId: string;
|
|
91
|
+
turnId?: string;
|
|
92
|
+
messageId?: string;
|
|
93
|
+
toolCallId?: string;
|
|
94
|
+
type:
|
|
95
|
+
| "turn_started"
|
|
96
|
+
| "turn_finished"
|
|
97
|
+
| "message_started"
|
|
98
|
+
| "assistant_event"
|
|
99
|
+
| "message_finished"
|
|
100
|
+
| "tool_execution_started"
|
|
101
|
+
| "tool_execution_updated"
|
|
102
|
+
| "tool_execution_finished";
|
|
103
|
+
payload: Record<string, unknown>;
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Fields
|
|
108
|
+
|
|
109
|
+
| Field | Required | Type | Meaning |
|
|
110
|
+
| ------------ | -------- | ---------------- | --------------------------------------------- |
|
|
111
|
+
| `seq` | Yes | positive integer | Position in `events.ndjson`, starting at 1 |
|
|
112
|
+
| `at` | Yes | ISO 8601 string | Time when the extension received the Pi event |
|
|
113
|
+
| `nodeId` | Yes | string | Workflow node that owned the event |
|
|
114
|
+
| `attemptId` | Yes | string | Workflow attempt that owned the event |
|
|
115
|
+
| `turnId` | By event | string | Run-local turn id |
|
|
116
|
+
| `messageId` | By event | string | Run-local message id |
|
|
117
|
+
| `toolCallId` | By event | string | Pi tool-call id |
|
|
118
|
+
| `type` | Yes | string | Normalized event type |
|
|
119
|
+
| `payload` | Yes | object | Fields specific to the event type |
|
|
120
|
+
|
|
121
|
+
`seq` must increase by exactly 1. Physical line order must match `seq` order.
|
|
122
|
+
The writer must capture `at` before any asynchronous file work. Readers order
|
|
123
|
+
events by `seq`. They use `at` only for playback timing.
|
|
124
|
+
|
|
125
|
+
Every record has `nodeId` and `attemptId`. The recorder takes ownership from
|
|
126
|
+
the active agent-step contract when the turn, message, or tool execution
|
|
127
|
+
starts. That ownership remains fixed until the matching end event, even if the
|
|
128
|
+
workflow moves to another node.
|
|
129
|
+
|
|
130
|
+
The ids under `turnId` and `messageId` are generated by the recorder. They are
|
|
131
|
+
unique within one run and have no meaning outside that run. Readers must use
|
|
132
|
+
them and `toolCallId` directly. Timing is never used to infer relationships.
|
|
133
|
+
|
|
134
|
+
## Event catalog
|
|
135
|
+
|
|
136
|
+
The journal records normalized events from Pi's documented extension hooks.
|
|
137
|
+
The payloads below are the complete version 1 contract. Unknown payload fields
|
|
138
|
+
must be ignored.
|
|
139
|
+
|
|
140
|
+
### `turn_started`
|
|
141
|
+
|
|
142
|
+
Source hook: `turn_start`.
|
|
143
|
+
|
|
144
|
+
Required ids: `turnId`.
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{ "turnIndex": 0 }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`turnIndex` is copied from Pi. The local `turnId` remains the journal identity.
|
|
151
|
+
|
|
152
|
+
### `turn_finished`
|
|
153
|
+
|
|
154
|
+
Source hook: `turn_end`.
|
|
155
|
+
|
|
156
|
+
Required ids: `turnId`.
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{ "turnIndex": 0, "messageId": "m1", "toolCallIds": ["call_1"] }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The payload contains references instead of copying the final message and tool
|
|
163
|
+
results. Final message content belongs in `entries.ndjson`, and final tool
|
|
164
|
+
results belong to `tool_execution_finished` and the settled Pi entries.
|
|
165
|
+
|
|
166
|
+
### `message_started`
|
|
167
|
+
|
|
168
|
+
Source hook: `message_start`.
|
|
169
|
+
|
|
170
|
+
Required ids: `turnId`, `messageId`.
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{ "role": "assistant" }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`role` is the Pi message role observed at start.
|
|
177
|
+
|
|
178
|
+
### `assistant_event`
|
|
179
|
+
|
|
180
|
+
Source hook: `message_update`.
|
|
181
|
+
|
|
182
|
+
Required ids: `turnId`, `messageId`.
|
|
183
|
+
|
|
184
|
+
The payload contains one normalized `AssistantMessageEvent`. The allowed shapes
|
|
185
|
+
are:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
type NormalizedAssistantEvent =
|
|
189
|
+
| { type: "start" }
|
|
190
|
+
| { type: "text_start"; contentIndex: number }
|
|
191
|
+
| { type: "text_delta"; contentIndex: number; delta: string }
|
|
192
|
+
| { type: "text_end"; contentIndex: number; content: string }
|
|
193
|
+
| { type: "thinking_start"; contentIndex: number }
|
|
194
|
+
| { type: "thinking_delta"; contentIndex: number; delta: string }
|
|
195
|
+
| { type: "thinking_end"; contentIndex: number; content: string }
|
|
196
|
+
| { type: "toolcall_start"; contentIndex: number }
|
|
197
|
+
| { type: "toolcall_delta"; contentIndex: number; delta: string }
|
|
198
|
+
| { type: "toolcall_end"; contentIndex: number; toolCall: unknown }
|
|
199
|
+
| { type: "done"; reason: "stop" | "length" | "toolUse" }
|
|
200
|
+
| { type: "error"; reason: "aborted" | "error" };
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The recorder must discard Pi's cumulative `partial` field. It must also discard
|
|
204
|
+
the cumulative `message` or `error` object on stream-terminal events. Persisting
|
|
205
|
+
those snapshots once per chunk would make storage grow with every repeated
|
|
206
|
+
prefix. Delta fields preserve the generated content without that duplication.
|
|
207
|
+
|
|
208
|
+
The `content` on text and thinking end events and the final `toolCall` object
|
|
209
|
+
are retained once. Readers may compare them with the folded deltas and report a
|
|
210
|
+
mismatch. A mismatch does not replace the matching final Pi entry.
|
|
211
|
+
|
|
212
|
+
The event writer must preserve every normalized assistant event. It may batch
|
|
213
|
+
several records into one filesystem append, but it must not merge adjacent
|
|
214
|
+
deltas or change their timestamps.
|
|
215
|
+
|
|
216
|
+
### `message_finished`
|
|
217
|
+
|
|
218
|
+
Source hook: `message_end`.
|
|
219
|
+
|
|
220
|
+
Required ids: `turnId`, `messageId`.
|
|
221
|
+
|
|
222
|
+
Settled message:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{ "role": "assistant", "settled": true, "entryId": "e1" }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Message without a recorded Pi entry:
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{ "role": "assistant", "settled": false }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`entryId` is required when `settled` is true and forbidden when it is false.
|
|
235
|
+
The recorder must obtain the id while it records the corresponding branch
|
|
236
|
+
entry. Readers must not guess this link from message content or timestamps.
|
|
237
|
+
|
|
238
|
+
### `tool_execution_started`
|
|
239
|
+
|
|
240
|
+
Source hook: `tool_execution_start`.
|
|
241
|
+
|
|
242
|
+
Required ids: `turnId`, `messageId`, `toolCallId`.
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{ "toolName": "read", "args": { "path": "README.md" } }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The tool-call owner is fixed at this event. Later update and finish events with
|
|
249
|
+
the same `toolCallId` keep that owner.
|
|
250
|
+
|
|
251
|
+
### `tool_execution_updated`
|
|
252
|
+
|
|
253
|
+
Source hook: `tool_execution_update`.
|
|
254
|
+
|
|
255
|
+
Required ids: `turnId`, `messageId`, `toolCallId`.
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
{}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Version 1 records each update's occurrence and timestamp. Pi exposes
|
|
262
|
+
`partialResult` as an arbitrary cumulative value. The recorder omits it to
|
|
263
|
+
avoid repeatedly storing growing prefixes. The final result remains available
|
|
264
|
+
on `tool_execution_finished` and in the Pi session entries.
|
|
265
|
+
|
|
266
|
+
### `tool_execution_finished`
|
|
267
|
+
|
|
268
|
+
Source hook: `tool_execution_end`.
|
|
269
|
+
|
|
270
|
+
Required ids: `turnId`, `messageId`, `toolCallId`.
|
|
271
|
+
|
|
272
|
+
```json
|
|
273
|
+
{ "toolName": "read", "isError": false, "result": { "content": [] } }
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`result` is the final public-hook result. Large string leaves use the bundle's
|
|
277
|
+
existing artifact encoding. Readers must resolve those references through the
|
|
278
|
+
same containment and size checks used for other bundle values.
|
|
279
|
+
|
|
280
|
+
## Capture status
|
|
281
|
+
|
|
282
|
+
`session/capture.json` uses schema `pi-workflows.session-capture.v1`. The
|
|
283
|
+
recorder creates it before the first session event and replaces it atomically.
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"schema": "pi-workflows.session-capture.v1",
|
|
288
|
+
"eventSchema": "pi-workflows.session-event.v1",
|
|
289
|
+
"status": "complete",
|
|
290
|
+
"eventCount": 47,
|
|
291
|
+
"entryCount": 6,
|
|
292
|
+
"lastEventSeq": 47
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
| Field | Required | Type | Meaning |
|
|
297
|
+
| -------------- | ----------- | -------------------- | -------------------------------------------- |
|
|
298
|
+
| `schema` | Yes | string | Must equal `pi-workflows.session-capture.v1` |
|
|
299
|
+
| `eventSchema` | Yes | string | Must equal `pi-workflows.session-event.v1` |
|
|
300
|
+
| `status` | Yes | string | `recording`, `complete`, or `failed` |
|
|
301
|
+
| `eventCount` | Yes | non-negative integer | Number of complete event records on disk |
|
|
302
|
+
| `entryCount` | Yes | non-negative integer | Number of complete entry records on disk |
|
|
303
|
+
| `lastEventSeq` | Yes | non-negative integer | Last durable event sequence, or 0 |
|
|
304
|
+
| `failure` | Failed only | object | First capture failure |
|
|
305
|
+
|
|
306
|
+
A failed status has this shape:
|
|
307
|
+
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"schema": "pi-workflows.session-capture.v1",
|
|
311
|
+
"eventSchema": "pi-workflows.session-event.v1",
|
|
312
|
+
"status": "failed",
|
|
313
|
+
"eventCount": 31,
|
|
314
|
+
"entryCount": 4,
|
|
315
|
+
"lastEventSeq": 31,
|
|
316
|
+
"failure": {
|
|
317
|
+
"failedAt": "2026-07-30T10:00:01.900Z",
|
|
318
|
+
"code": "event_write_failed",
|
|
319
|
+
"message": "could not append session event batch"
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Status meanings:
|
|
325
|
+
|
|
326
|
+
- `recording` means the run may append more events or entries. Its counts are
|
|
327
|
+
lower bounds from the last atomic projection and may lag the files.
|
|
328
|
+
- `complete` means all observed events were written, both session writers were
|
|
329
|
+
drained, and the counts match the durable files.
|
|
330
|
+
- `failed` means the temporal history may be incomplete. Workflow execution
|
|
331
|
+
continues, and settled entries may still be usable.
|
|
332
|
+
|
|
333
|
+
The first capture failure is retained. After an event append fails, the event
|
|
334
|
+
writer must stop appending so a torn line remains at the file tail. The final
|
|
335
|
+
status counts only complete lines. If `capture.json` is missing, invalid, still
|
|
336
|
+
`recording` after a terminal workflow state, or inconsistent with the journal,
|
|
337
|
+
the viewer must report unverified capture instead of treating the journal as
|
|
338
|
+
empty or complete.
|
|
339
|
+
|
|
340
|
+
## Write lifecycle
|
|
341
|
+
|
|
342
|
+
The recorder follows this order:
|
|
343
|
+
|
|
344
|
+
1. Bind the run to the Pi conversation.
|
|
345
|
+
2. Create `capture.json` with status `recording`.
|
|
346
|
+
3. Accept documented Pi events while an agent attempt owns the conversation.
|
|
347
|
+
4. Stamp correlation ids and sequence in each hook, then record the receipt time.
|
|
348
|
+
5. Queue normalized records in memory and return immediately from high-rate
|
|
349
|
+
update hooks.
|
|
350
|
+
6. Append queued records in ordered batches on a dedicated writer.
|
|
351
|
+
7. At `message_end`, buffer that receipt and later events until a subsequent
|
|
352
|
+
synchronized hook exposes the durable Pi entry, then attach its exact id
|
|
353
|
+
without changing event order or receipt timestamps.
|
|
354
|
+
8. When the workflow requests terminal persistence during an active Pi turn,
|
|
355
|
+
keep capture routed through that turn's final tool, message, and `turn_end`
|
|
356
|
+
hooks.
|
|
357
|
+
9. Stop accepting events and drain the event and entry writers.
|
|
358
|
+
10. Write terminal `capture.json` atomically.
|
|
359
|
+
11. Allow the engine to append its terminal workflow event.
|
|
360
|
+
|
|
361
|
+
This order keeps the bundle immutable once the terminal workflow event exists.
|
|
362
|
+
Waiting for the active turn is bounded at 30 seconds. If the turn does not
|
|
363
|
+
finish, capture stops as failed so it cannot hold workflow persistence
|
|
364
|
+
indefinitely. A capture failure must never fail the workflow.
|
|
365
|
+
|
|
366
|
+
The queue must have tested byte and record limits. Reaching either limit marks
|
|
367
|
+
capture as failed and stops temporal capture. Silent event dropping is
|
|
368
|
+
forbidden.
|
|
369
|
+
|
|
370
|
+
## Torn writes and validation
|
|
371
|
+
|
|
372
|
+
Each event is one UTF-8 JSON object followed by `\n`. A reader may ignore an
|
|
373
|
+
incomplete final line while capture is `recording`. Any malformed line before
|
|
374
|
+
the tail is corruption.
|
|
375
|
+
|
|
376
|
+
A reader validates these rules:
|
|
377
|
+
|
|
378
|
+
- Required fields have the documented types.
|
|
379
|
+
- `seq` starts at 1 and has no duplicates or gaps.
|
|
380
|
+
- Correlation ids required by the event type are present.
|
|
381
|
+
- Every finish event refers to a matching start event.
|
|
382
|
+
- `message_finished.entryId` exists in `entries.ndjson` when `settled` is true.
|
|
383
|
+
- `capture.json` counts match complete journal lines at terminal status.
|
|
384
|
+
- No session file changes after terminal workflow state.
|
|
385
|
+
|
|
386
|
+
Unknown event types and unknown payload fields are ignored. The reader still
|
|
387
|
+
advances past their sequence numbers. An unsupported `eventSchema` makes the
|
|
388
|
+
temporal journal unavailable. The reader must not guess another shape.
|
|
389
|
+
|
|
390
|
+
## Deterministic reduction
|
|
391
|
+
|
|
392
|
+
Live display and replay must use the same reducer. The reducer processes events
|
|
393
|
+
in `seq` order and keeps run-local state for turns, messages, content blocks,
|
|
394
|
+
and tools.
|
|
395
|
+
|
|
396
|
+
Text and thinking deltas append to their content blocks. Tool-call deltas use
|
|
397
|
+
the same `messageId` and `contentIndex` addressing. Tool lifecycle events update the tool identified
|
|
398
|
+
by `toolCallId`. A settled message switches to the matching verbatim Pi entry at
|
|
399
|
+
`message_finished`. An unsealed message remains a partial reconstruction.
|
|
400
|
+
|
|
401
|
+
Replay uses `at` to schedule events at 1x or another selected speed. Ordering
|
|
402
|
+
always comes from `seq`. Seeking may use in-memory checkpoints and a timestamp
|
|
403
|
+
index, but those are derived viewer data and do not belong in the run bundle.
|
|
404
|
+
|
|
405
|
+
The TypeScript and Rust reducers must pass the same fixtures. Fixtures cover
|
|
406
|
+
normal completion, thinking, tool calls, interleaved tool updates, aborts,
|
|
407
|
+
missing final entries, unknown events, timestamp ties, and capture failure.
|
|
408
|
+
|
|
409
|
+
## Live transport
|
|
410
|
+
|
|
411
|
+
The live replay protocol adds the journal and status to the existing run view:
|
|
412
|
+
|
|
413
|
+
```json
|
|
414
|
+
{
|
|
415
|
+
"session": {
|
|
416
|
+
"binding": { "schema": "pi-workflows.session-binding.v1" },
|
|
417
|
+
"entries": [],
|
|
418
|
+
"events": [],
|
|
419
|
+
"capture": { "schema": "pi-workflows.session-capture.v1" }
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
The server tails `events.ndjson` like the existing trace and entry files.
|
|
425
|
+
Journal growth uses an `append` patch at `/session/events`. Capture changes use
|
|
426
|
+
a `replace` patch at `/session/capture`.
|
|
427
|
+
|
|
428
|
+
The server batches pending append records into one patch for a short interval.
|
|
429
|
+
It must preserve every event record and its sequence. This batching limits
|
|
430
|
+
revision growth without changing the durable history. Reconnection always
|
|
431
|
+
starts with a fresh snapshot, followed by later patches.
|
|
432
|
+
|
|
433
|
+
## Privacy and trust
|
|
434
|
+
|
|
435
|
+
Session events can contain prompts, generated text, thinking, tool arguments,
|
|
436
|
+
tool results, file paths, and command output. Bundle permissions remain `0700`
|
|
437
|
+
for directories and `0600` for files. Remote viewing remains loopback-only and
|
|
438
|
+
uses the existing SSH tunnel workflow.
|
|
439
|
+
|
|
440
|
+
Readers must treat all strings and nested values as untrusted. Terminal output
|
|
441
|
+
is sanitized before drawing. Artifact paths must stay inside the manifest's
|
|
442
|
+
artifact directory.
|
|
443
|
+
|
|
444
|
+
## Versioning
|
|
445
|
+
|
|
446
|
+
This change replaces the session-bound `pi-workflows.run-bundle.v1` contract in
|
|
447
|
+
place. A session-bound bundle must contain both `events.ndjson` and
|
|
448
|
+
`capture.json` after the implementation lands. Missing files are reported as
|
|
449
|
+
an invalid or incomplete capture. There is no legacy read path.
|
|
450
|
+
|
|
451
|
+
The new files use `pi-workflows.session-event.v1` and
|
|
452
|
+
`pi-workflows.session-capture.v1`. Breaking either shape requires changing its
|
|
453
|
+
schema identifier. New event types and optional payload fields may be added
|
|
454
|
+
within version 1 because readers ignore unknown values.
|
|
455
|
+
|
|
456
|
+
The network view grows within `pi-workflows.replay.v1`. Its existing unknown
|
|
457
|
+
field rule makes `session.events` and `session.capture` additive protocol
|
|
458
|
+
fields. No dual writer, migration, or fallback reader is part of this change.
|
|
459
|
+
|
|
460
|
+
## Contract impact
|
|
461
|
+
|
|
462
|
+
The extension uses only Pi's documented `turn_start`, `turn_end`,
|
|
463
|
+
`message_start`, `message_update`, `message_end`, `tool_execution_start`,
|
|
464
|
+
`tool_execution_update`, and `tool_execution_end` hooks.
|
|
465
|
+
|
|
466
|
+
Pi session state is unchanged. Normal Pi behavior still creates its own session
|
|
467
|
+
entries, and the extension only reads those entries for bundling and linkage.
|
|
468
|
+
Pi internals, provider protocols, and global Pi session files are unchanged.
|
|
469
|
+
The only new persistent data is `session/events.ndjson` and
|
|
470
|
+
`session/capture.json` inside the private workflow run bundle.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Rust TUI viewer (piw)
|
|
2
|
+
|
|
3
|
+
`tui/` contains `piw`, a Rust terminal viewer for workflow runs. It renders
|
|
4
|
+
the same graph as the bundled TypeScript viewer, pinned by the golden fixtures
|
|
5
|
+
under `fixtures/layout/`, and adds live following, replay, detailed inspection,
|
|
6
|
+
a recorded Pi conversation, themes, and remote viewing.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
The crates.io package uses the project name and installs the shorter `piw`
|
|
11
|
+
command:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cargo install pi-workflows
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Modes
|
|
18
|
+
|
|
19
|
+
- `piw` browses the local runs directory (`PI_WORKFLOWS_RUNS_DIR` or
|
|
20
|
+
`~/.pi/agent/workflows/runs`).
|
|
21
|
+
- `piw <runId|runDir>` opens one run. A bare run id resolves inside the default
|
|
22
|
+
runs directory.
|
|
23
|
+
- `piw serve [--runs-dir <dir>] [--bind 127.0.0.1:9377]` exposes the runs
|
|
24
|
+
directory over the [live replay protocol](live-replay-protocol.md). Only
|
|
25
|
+
loopback addresses are accepted; use an SSH tunnel for remote viewing.
|
|
26
|
+
- `piw --connect ws://…` reads from another `piw serve` process.
|
|
27
|
+
- `piw --theme <name>` selects a theme for this invocation.
|
|
28
|
+
- `piw --list-themes` prints the built-in theme names.
|
|
29
|
+
|
|
30
|
+
Direct filesystem mode and connected mode use the same semantic run view.
|
|
31
|
+
The protocol is the network form of that view.
|
|
32
|
+
|
|
33
|
+
## Layout
|
|
34
|
+
|
|
35
|
+
The normal layout contains a run browser, graph, inspector, and two-line replay
|
|
36
|
+
timeline. Short terminals use a compact one-line transport. Terminals below 100
|
|
37
|
+
columns start with the run browser collapsed to a status rail; `b` toggles it.
|
|
38
|
+
Drag the run browser's right border to resize it. Drag the inspector's top
|
|
39
|
+
border to resize the bottom panel. PIW saves both sizes in its viewer config and
|
|
40
|
+
clamps them when the terminal is smaller. A directly opened single run hides
|
|
41
|
+
the browser.
|
|
42
|
+
|
|
43
|
+
- **Run browser:** every bundle, newest first, with status, title, elapsed time,
|
|
44
|
+
and a `?` marker for a possibly interrupted run.
|
|
45
|
+
- **Graph:** the complete workflow definition using the same layered layout as
|
|
46
|
+
the TypeScript renderer. Full bordered cards are the default. Every card in
|
|
47
|
+
one graph has the same width and height, fixed before layout. The exact step
|
|
48
|
+
id is centered in a header above a horizontal divider. Structured body rows
|
|
49
|
+
pair a type badge with status (`●` agent, `ƒ` compute, `⚙` action, `◆`
|
|
50
|
+
checkpoint), then attempts (`↻`) with elapsed time (`◷`). Branch labels use
|
|
51
|
+
`◇`; the final reserved row carries a short detail while unbounded content
|
|
52
|
+
stays in the inspector. The outer border and header divider share the node's
|
|
53
|
+
state color on the graph background. The body background starts inside that
|
|
54
|
+
border, while the header interior uses a separate panel surface. Type badges
|
|
55
|
+
keep their semantic colors. Updates fill reserved rows without moving nodes
|
|
56
|
+
or edges.
|
|
57
|
+
Narrow terminals pan over the fixed graph. Compact line nodes remain
|
|
58
|
+
available with `z`. Start `▶` and terminal `■` markers sit outside the card.
|
|
59
|
+
Branch labels remain on edges and loops use a right-hand gutter.
|
|
60
|
+
- **Inspector:** Steps, Trace, Conversation, and Info tabs. Each tab is a
|
|
61
|
+
bracketed symbol button with a full-label mouse target; the selected tab uses
|
|
62
|
+
the accent surface. Steps can expand to full prompt, output, timestamps,
|
|
63
|
+
error, and action receipt fields. Trace can
|
|
64
|
+
show the selected attempt, the replay-visible prefix, or the full run, with
|
|
65
|
+
expandable JSON payloads. Conversation shows live text, thinking, tool-call
|
|
66
|
+
construction, and tool execution, then switches settled messages to the
|
|
67
|
+
verbatim Pi entry. Raw records remain expandable. Info shows run metadata,
|
|
68
|
+
final output, capture status, counts, and integrity diagnostics.
|
|
69
|
+
- **Timeline:** run status, elapsed time, replay track, playhead, position,
|
|
70
|
+
playback controls, and speed. Every playback action uses the same bracketed
|
|
71
|
+
symbol-button style as the inspector and theme actions. The controls accept
|
|
72
|
+
full-label mouse clicks; the track supports click and drag seeking.
|
|
73
|
+
|
|
74
|
+
## State presentation
|
|
75
|
+
|
|
76
|
+
The graph distinguishes these states:
|
|
77
|
+
|
|
78
|
+
- queued;
|
|
79
|
+
- running;
|
|
80
|
+
- replay focus;
|
|
81
|
+
- completed;
|
|
82
|
+
- failed;
|
|
83
|
+
- timed out;
|
|
84
|
+
- waiting; and
|
|
85
|
+
- cancelled.
|
|
86
|
+
|
|
87
|
+
A live running node uses a blue heavy border and `◐`. A selected historical
|
|
88
|
+
step uses a mauve heavy border and `◆`, so replay never looks live. Timed-out
|
|
89
|
+
nodes use `×` and a separate color. Type color is independent of state: agent
|
|
90
|
+
is green, compute is blue, action is yellow, and checkpoint is mauve. The graph
|
|
91
|
+
title distinguishes `(live)`,
|
|
92
|
+
`(latest)`, and `(replay)` and also reports paused, reconnecting, disconnected,
|
|
93
|
+
and failed or invalid capture states.
|
|
94
|
+
|
|
95
|
+
## Themes
|
|
96
|
+
|
|
97
|
+
Catppuccin Mocha is the default. The whole frame uses the theme: application,
|
|
98
|
+
panels, graph canvas, node surfaces, selected rows, borders, states,
|
|
99
|
+
conversation roles, and timeline. Boxed nodes use a surface color different
|
|
100
|
+
from the graph canvas.
|
|
101
|
+
|
|
102
|
+
Press `,` to open the theme picker. Moving through the list previews a theme
|
|
103
|
+
immediately. Enter applies and saves it; Escape restores the exact original
|
|
104
|
+
palette. Mouse selection is supported.
|
|
105
|
+
|
|
106
|
+
Built-in themes are:
|
|
107
|
+
|
|
108
|
+
- Catppuccin Mocha and Latte;
|
|
109
|
+
- terminal palette;
|
|
110
|
+
- Tokyo Night and Day;
|
|
111
|
+
- Dracula;
|
|
112
|
+
- Nord;
|
|
113
|
+
- Gruvbox Dark and Light;
|
|
114
|
+
- One Dark and Light;
|
|
115
|
+
- Solarized Dark and Light;
|
|
116
|
+
- Kanagawa and Lotus;
|
|
117
|
+
- Rosé Pine and Dawn; and
|
|
118
|
+
- Vesper.
|
|
119
|
+
|
|
120
|
+
Theme configuration is loaded from `PIW_CONFIG_PATH`, then
|
|
121
|
+
`$XDG_CONFIG_HOME/piw/config.toml`, then `~/.config/piw/config.toml`:
|
|
122
|
+
|
|
123
|
+
```toml
|
|
124
|
+
[theme]
|
|
125
|
+
name = "catppuccin"
|
|
126
|
+
auto_switch = false
|
|
127
|
+
dark_name = "catppuccin"
|
|
128
|
+
light_name = "catppuccin-latte"
|
|
129
|
+
|
|
130
|
+
[theme.custom]
|
|
131
|
+
# canvas_bg = "#1e1e2e"
|
|
132
|
+
# node_bg = "#313244"
|
|
133
|
+
# accent = "#89b4fa"
|
|
134
|
+
|
|
135
|
+
[ui]
|
|
136
|
+
# sidebar_width = 34
|
|
137
|
+
# inspector_height = 16
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`PIW_THEME` overrides the config file and `--theme` overrides both. Custom
|
|
141
|
+
colors accept `#rrggbb`, `#rgb`, `rgb(r,g,b)`, named terminal colors, and
|
|
142
|
+
`reset`. Invalid fields are reported without discarding valid fields.
|
|
143
|
+
|
|
144
|
+
When `auto_switch` is enabled, `PIW_THEME_APPEARANCE=dark|light` or the host's
|
|
145
|
+
`COLORFGBG` value selects `dark_name` or `light_name` at startup. A manual
|
|
146
|
+
selection in the picker disables automatic switching.
|
|
147
|
+
|
|
148
|
+
## Replay semantics
|
|
149
|
+
|
|
150
|
+
A session-bound run uses the temporal session event journal as its replay
|
|
151
|
+
track. `-1` means before capture and `None` means latest/live. Each event cursor
|
|
152
|
+
folds text, thinking, tool calls, and tool execution through that event's
|
|
153
|
+
sequence. PIW keeps a workflow-step selection beside the temporal cursor so
|
|
154
|
+
the graph, trace, and attempt inspectors stay aligned. Runs without session
|
|
155
|
+
events retain step-based replay.
|
|
156
|
+
|
|
157
|
+
Playback uses event timestamps at 1x, 2x, 5x, or 10x and breaks timestamp ties
|
|
158
|
+
by event sequence. Step-only runs use the 700 ms base interval. Seeking
|
|
159
|
+
backward detaches from live. Active runs show a Live button that rejoins the
|
|
160
|
+
latest event and enables graph follow. Finished runs omit that button; End
|
|
161
|
+
jumps to their latest event. A viewer already at latest follows newly appended
|
|
162
|
+
events. A detached viewer stays at its chosen position. Rewinding
|
|
163
|
+
never changes graph geometry because card size and layout depend only on the
|
|
164
|
+
persisted definition snapshot.
|
|
165
|
+
|
|
166
|
+
Graph follow is on by default and is shown as `FOLLOW` in the graph title. It
|
|
167
|
+
centers the running node, the waiting checkpoint, or the selected replay step,
|
|
168
|
+
including nodes at the graph edges and graphs smaller than the viewport. `f`
|
|
169
|
+
toggles follow. Keyboard or mouse panning turns it off; `f`, `0`, or returning
|
|
170
|
+
to the latest position turns it back on.
|
|
171
|
+
|
|
172
|
+
The conversation folds `session/events.ndjson` through the temporal cursor.
|
|
173
|
+
Unsealed messages stay visible as partial output. A settled `message_finished`
|
|
174
|
+
with `entryId` switches that message to the matching verbatim record from
|
|
175
|
+
`session/entries.ndjson`. Capture failures, sequence gaps, count mismatches,
|
|
176
|
+
and reconciliation diagnostics remain visible. Conversation auto-follow stays
|
|
177
|
+
at the bottom until the user moves to an older message and returns with End.
|
|
178
|
+
|
|
179
|
+
## Remote behavior
|
|
180
|
+
|
|
181
|
+
The client reconnects automatically with bounded backoff. It restores the run
|
|
182
|
+
listing and selected-run subscription after the server returns. Cached content
|
|
183
|
+
stays visible but is labeled reconnecting or disconnected, never current.
|
|
184
|
+
Revision gaps still force a fresh snapshot.
|
|
185
|
+
|
|
186
|
+
Expanded remote prompt and output fields fetch artifact content on demand.
|
|
187
|
+
The client caches bounded responses while the server enforces bundle path,
|
|
188
|
+
symlink, and 4 MiB limits.
|
|
189
|
+
|
|
190
|
+
## Interaction
|
|
191
|
+
|
|
192
|
+
- Replay: `[`/`]` previous/next, Space play/pause, Home or `g` to start, End
|
|
193
|
+
to jump to latest, and `{`/`}` to change speed. Active runs also show Live;
|
|
194
|
+
`G` and `L` are its keyboard shortcuts.
|
|
195
|
+
- Focus: Tab cycles Runs → Graph → Inspector. Click the full `[symbol Tab]`
|
|
196
|
+
buttons, or use `t` or `1`–`4`, to change inspector tabs.
|
|
197
|
+
- Graph: arrows or `hjkl` pan, `0` resets, `f` toggles centered follow, and
|
|
198
|
+
`z`, `+`, or `-` switches boxed/compact density. Clicking a node selects its
|
|
199
|
+
latest visible attempt. Dragging inside the graph pans.
|
|
200
|
+
- Browser: `b` expands or collapses it. Up/Down or `j`/`k` selects a run. Drag
|
|
201
|
+
its right border to resize it.
|
|
202
|
+
- Inspector: Enter expands the selected step, trace payload, or conversation
|
|
203
|
+
entry. In Trace, `v` changes scope. Page Up/Down scrolls long content. Drag
|
|
204
|
+
its top border to resize the bottom panel.
|
|
205
|
+
- Theme: `,` opens the picker; arrows or `j`/`k` preview. Click `[✓ Apply]` or
|
|
206
|
+
`[× Cancel]`; Enter and Escape remain the keyboard equivalents.
|
|
207
|
+
- `q` or Ctrl-C quits.
|
|
208
|
+
|
|
209
|
+
## TypeScript renderer parity
|
|
210
|
+
|
|
211
|
+
The Rust and TypeScript renderers must reproduce the same plain graph and
|
|
212
|
+
layout fixtures under `fixtures/layout/`. Theme backgrounds and ratatui styles
|
|
213
|
+
are Rust-only and do not change plain fixture output. Any node text or geometry
|
|
214
|
+
change must be made in both renderers and committed with regenerated fixtures:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
npm run fixtures
|
|
218
|
+
```
|