@osolmaz/pi-workflows 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +36 -21
  2. package/dist/extension/executor.d.ts +14 -1
  3. package/dist/extension/executor.js +11 -1
  4. package/dist/extension/executor.js.map +1 -1
  5. package/dist/extension/index.js +79 -7
  6. package/dist/extension/index.js.map +1 -1
  7. package/dist/extension/recorder.d.ts +85 -0
  8. package/dist/extension/recorder.js +525 -0
  9. package/dist/extension/recorder.js.map +1 -0
  10. package/dist/extension/session-events.d.ts +134 -0
  11. package/dist/extension/session-events.js +60 -0
  12. package/dist/extension/session-events.js.map +1 -0
  13. package/dist/extension/widget.js +25 -24
  14. package/dist/extension/widget.js.map +1 -1
  15. package/dist/render/canvas.d.ts +1 -1
  16. package/dist/render/canvas.js +5 -0
  17. package/dist/render/canvas.js.map +1 -1
  18. package/dist/render/graph-render.d.ts +5 -0
  19. package/dist/render/graph-render.js +211 -48
  20. package/dist/render/graph-render.js.map +1 -1
  21. package/dist/viewer/render.js +19 -3
  22. package/dist/viewer/render.js.map +1 -1
  23. package/dist/viewer/session-reducer.d.ts +45 -0
  24. package/dist/viewer/session-reducer.js +266 -0
  25. package/dist/viewer/session-reducer.js.map +1 -0
  26. package/dist/workflows/artifacts.d.ts +40 -0
  27. package/dist/workflows/artifacts.js +155 -0
  28. package/dist/workflows/artifacts.js.map +1 -0
  29. package/dist/workflows/engine.d.ts +2 -0
  30. package/dist/workflows/engine.js +38 -7
  31. package/dist/workflows/engine.js.map +1 -1
  32. package/dist/workflows/index.d.ts +3 -2
  33. package/dist/workflows/index.js +2 -1
  34. package/dist/workflows/index.js.map +1 -1
  35. package/dist/workflows/store.d.ts +53 -9
  36. package/dist/workflows/store.js +523 -43
  37. package/dist/workflows/store.js.map +1 -1
  38. package/dist/workflows/types.d.ts +126 -3
  39. package/docs/development.md +43 -19
  40. package/docs/live-replay-protocol.md +155 -0
  41. package/docs/plans/piw-viewer-experience-implementation-plan.md +674 -0
  42. package/docs/plans/replayable-run-bundles-implementation-plan.md +65 -0
  43. package/docs/plans/session-event-replay-implementation-plan.md +494 -0
  44. package/docs/plans/tui-viewer-implementation-plan.md +64 -0
  45. package/docs/run-bundles.md +320 -55
  46. package/docs/session-event-journal.md +470 -0
  47. package/docs/tui-viewer.md +218 -0
  48. package/package.json +2 -1
  49. package/src/extension/executor.ts +28 -1
  50. package/src/extension/index.ts +87 -7
  51. package/src/extension/recorder.ts +633 -0
  52. package/src/extension/session-events.ts +119 -0
  53. package/src/extension/widget.ts +26 -24
  54. package/src/render/canvas.ts +19 -1
  55. package/src/render/graph-render.ts +277 -44
  56. package/src/viewer/render.ts +21 -3
  57. package/src/viewer/session-reducer.ts +347 -0
  58. package/src/workflows/artifacts.ts +188 -0
  59. package/src/workflows/engine.ts +39 -7
  60. package/src/workflows/index.ts +15 -0
  61. package/src/workflows/store.ts +649 -49
  62. package/src/workflows/types.ts +141 -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
+ ```