Skip to content

Commit 5228f91

Browse files
committed
Update stdio protocol documentation for clarity and consistency
1 parent 49e463b commit 5228f91

1 file changed

Lines changed: 262 additions & 44 deletions

File tree

references/stdio-protocol.md

Lines changed: 262 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ zread internally uses a Driver / Client split:
1313
`Transport`.
1414
- **Client** is either a Bubbletea TUI or `StdioClient`. With `--stdio`, the
1515
cobra command instantiates `transport.NewLocalTransport()` (a channel pair),
16-
spawns the driver in a goroutine, then runs `transport.NewStdioClient(clientT).Run(ctx)`.
16+
spawns the driver in a goroutine, then runs `StdioClient.Run(ctx)`.
1717
- `StdioClient` reads JSON lines from `os.Stdin` and forwards them as commands
1818
to the driver, while encoding `Event`s from the driver to `os.Stdout` (one
1919
JSON object per line). It returns when an event with `"done": true` is
@@ -26,18 +26,22 @@ JSON. stderr is reserved for logs/errors and should not be parsed.
2626

2727
```json
2828
{
29-
"vm": { ... }, // current ViewModel snapshot (shape per-command)
30-
"waiting_for": ["..."], // command types accepted in the current state
31-
"done": false, // true on the final event; client should exit
32-
"error": "" // non-empty when the previous command was rejected
29+
"vm": { ... },
30+
"waiting_for": ["..."],
31+
"done": false,
32+
"error": ""
3333
}
3434
```
3535

36-
- An event is emitted after every state transition. The client should treat
37-
`vm` as the full state (no diffs).
38-
- `waiting_for` enumerates the legal `type` values the client may send right
39-
now. `quit` is always implicitly accepted.
40-
- The very last event always has `"done": true` and an empty `waiting_for`.
36+
- `vm`: full ViewModel snapshot for the current state (shape varies per command — see below).
37+
- `waiting_for`: command `type` values the client may legally send right now. `quit` is always accepted regardless.
38+
- `done`: `true` on the final event; the process will exit on its own.
39+
- `error`: non-empty when the previous command was rejected; `vm`/`waiting_for` remain unchanged.
40+
41+
Rules:
42+
- An event is emitted after every state transition. Treat `vm` as the full current state (no diffs).
43+
- `waiting_for: []` with `done: false` means the driver is awaiting an internal async result — do not send any command; just keep reading.
44+
- The very last event always has `done: true` and an empty `waiting_for`.
4145

4246
## Command format (client → driver, stdin)
4347

@@ -48,47 +52,261 @@ One JSON object per line:
4852
```
4953

5054
- `type` is required. `params` may be omitted (treated as `{}`).
51-
- Universal command: `{"type":"quit","params":{}}` — every driver handles it
52-
and shuts down cleanly.
53-
- Command types prefixed with `_` are driver-internal (async results injected
54-
by the driver itself) and must never appear in `waiting_for`; clients should
55-
not send them.
56-
- Sending a command not in `waiting_for` produces an event with `error` set
57-
while `vm`/`waiting_for` remain unchanged.
55+
- Universal command: `{"type":"quit","params":{}}` — every driver handles it and shuts down cleanly.
56+
- Command types prefixed with `_` are driver-internal. They never appear in `waiting_for`; clients must not send them.
57+
- Sending a command not in `waiting_for` returns an event with `error` set while `vm`/`waiting_for` remain unchanged.
58+
- Error message format: `"command \"xxx\" not allowed in state yyy"` / `"invalid params: ..."` / `"invalid JSON: ..."`.
5859

5960
## Driving a session
6061

6162
1. Spawn `zread <command> --stdio` with piped stdin/stdout.
6263
2. Read the first event — it carries the initial ViewModel and `waiting_for`.
63-
3. Render or inspect `vm`; pick a legal command from `waiting_for` and write it
64-
as a single JSON line followed by `\n`.
64+
3. Render or inspect `vm`; pick a legal command from `waiting_for` and write it as a single JSON line followed by `\n`.
6565
4. Loop: read next event, react, write next command.
66-
5. Stop when an event arrives with `"done": true`. The process will exit on
67-
its own; no further input is required. Sending `quit` at any time triggers
68-
the same shutdown path.
69-
70-
## Per-command notes
71-
72-
The `vm` schema, state machine, and command vocabulary are defined per command
73-
in `pkg/ui/<command>/viewmodel.go` and `driver.go` in the zread_cli repo. The
74-
common ones:
75-
76-
- `zread browse --stdio` — emits loading → serving (with server URL in `vm`),
77-
or `select_version` when multiple wikis exist. Commands include
78-
`select_version` (`{"wiki_id":"..."}`) and `quit`.
79-
- `zread generate --stdio` — emits catalog progress, an optional
80-
`catalog_confirm` gate, then per-page progress. Commands include
81-
`confirm_catalog`, `retry`, `skip`, `quit`. `--yes` skips the confirm gate.
82-
- `zread config --stdio`, `zread login --stdio`, `zread update --stdio`,
83-
`zread version --stdio` follow the same envelope; consult the matching
84-
driver for their `waiting_for` values.
66+
5. Stop when `done: true` arrives. The process will exit on its own. Sending `quit` at any time triggers the same shutdown.
67+
68+
---
69+
70+
## `zread browse --stdio`
71+
72+
### ViewModel fields
73+
74+
| Field | Type | Notes |
75+
|---|---|---|
76+
| `state` | string | `"loading"` `"serving"` `"select_version"` `"no_wiki"` `"error"` |
77+
| `url` | string | Local server URL; present when `state="serving"` |
78+
| `browser_opened` | bool | Whether the browser was auto-launched |
79+
| `has_current` | bool | Whether a "current" wiki version exists |
80+
| `versions` | array | Present when `state="select_version"` |
81+
| `versions[].id` | string | Wiki version ID |
82+
| `versions[].timestamp` | string | Version timestamp |
83+
| `versions[].is_current` | bool | Whether this is the current version |
84+
| `error` | string | Present when `state="error"` |
85+
86+
### Commands
87+
88+
| State | `waiting_for` | Command | Params |
89+
|---|---|---|---|
90+
| `select_version` | `["select_version","quit"]` | `select_version` | `{"wiki_id":"<id>"}` or `{"current":true}` |
91+
| `serving` | `["quit"]` |||
92+
| `no_wiki` | `["quit"]` |||
93+
| `loading` | `[]` | *(wait for internal event)* ||
94+
| `error` | `[]` |||
95+
96+
### State machine
97+
98+
```
99+
loading ──(server ok)──────────────────► serving
100+
loading ──(error)──────────────────────► error (terminal)
101+
select_version ──(select_version cmd)──► loading ──► serving
102+
no_wiki ───────────────────────────────► (terminal, done=true)
103+
```
104+
105+
---
106+
107+
## `zread generate --stdio`
108+
109+
### ViewModel fields
110+
111+
| Field | Type | Notes |
112+
|---|---|---|
113+
| `state` | string | `"select_action"` `"running"` `"done"` `"error"` |
114+
| `max_retries` | int | Configured max retries per page |
115+
| `select_action.scenario` | string | `"has_wiki"` `"has_draft"` `"empty"` |
116+
| `select_action.wiki_date` | string | Date of existing wiki (when `scenario="has_wiki"`) |
117+
| `select_action.draft_done` | int | Pages already done in draft |
118+
| `select_action.draft_total` | int | Total pages in draft |
119+
| `select_action.next_action` | string | Pre-suggested action: `"generate"` `"browse"` `""` |
120+
| `catalog.status` | string | `"idle"` `"running"` `"done"` `"resumed"` `"error"` |
121+
| `catalog.tool_name` | string | Currently-running LLM tool (if any) |
122+
| `catalog.error` | string | Catalog error message |
123+
| `catalog.auto_retry` | int | Auto-retry count so far |
124+
| `pages.tasks` | array | Per-page task list |
125+
| `pages.tasks[].id` | int | Task ID (used for `retry` command) |
126+
| `pages.tasks[].title` | string | Page title |
127+
| `pages.tasks[].slug` | string | Page slug |
128+
| `pages.tasks[].state` | string | `"pending"` `"running"` `"retry_pending"` `"done"` `"failed"` `"resumed"` |
129+
| `pages.tasks[].retry_count` | int | Retries attempted so far |
130+
| `pages.tasks[].max_retries` | int | Retry limit for this task |
131+
| `pages.tasks[].error` | string | Error message if failed |
132+
| `pages.done` | int | Completed page count |
133+
| `pages.total` | int | Total page count |
134+
| `pages.waiting_retry` | bool | `true` when driver is paused waiting for retry/skip decision |
135+
| `done_total` | int | Total pages done (in `state="done"`) |
136+
| `error` | string | Fatal error message |
137+
138+
### Commands
139+
140+
| State / condition | `waiting_for` | Command | Params |
141+
|---|---|---|---|
142+
| `select_action` | `["select_action","cancel","quit"]` | `select_action` | `{"action":"generate"}` or `"browse"` or `"resume"` or `"clear"` or `"cancel"` |
143+
| `running` (normal) | `["quit"]` |||
144+
| `running` (catalog error) | `["quit","retry_catalog"]` | `retry_catalog` | `{}` |
145+
| `running` (pages waiting retry) | `["quit","retry","skip_all"]` | `retry` | `{"task_id":<int>}` |
146+
| `running` (pages waiting retry) | `["quit","retry","skip_all"]` | `skip_all` | `{}` |
147+
| `running` | `["quit","cancel"]` | `cancel` | `{}` |
148+
149+
### CLI flags that affect stdio flow
150+
151+
- `--yes` / `-y`: skips the `select_action` gate; generation starts immediately.
152+
- `--draft-action <resume|clear|cancel>`: pre-answers the draft prompt.
153+
- `--skip-failed`: equivalent to auto-sending `skip_all` when pages fail.
154+
155+
### State machine
156+
157+
```
158+
select_action ──(generate/resume)──► running ──(all done, no failures)──► done
159+
select_action ──(cancel/browse)────► done (immediate)
160+
running ──(pages failed)───────────► waiting_retry=true, accepts retry/skip_all
161+
running ──(cancel)─────────────────► done
162+
```
163+
164+
---
165+
166+
## `zread config --stdio`
167+
168+
### ViewModel fields
169+
170+
| Field | Type | Notes |
171+
|---|---|---|
172+
| `fields` | array (9 items) | Ordered config fields |
173+
| `fields[].title_key` | string | Display label key |
174+
| `fields[].json_key` | string | Key to use in `update_fields`; empty = read-only |
175+
| `fields[].initial` | string | Value at load time |
176+
| `fields[].value` | string | Current (possibly edited) value |
177+
| `dirty` | bool | `true` if any field differs from `initial` |
178+
| `saved` | bool | `true` after successful save |
179+
| `error` | string | Validation error |
180+
181+
Field index → `json_key` mapping:
182+
183+
| Index | `json_key` | Notes |
184+
|---|---|---|
185+
| 0 | `language` | generation language |
186+
| 1 | `doc_language` | doc language |
187+
| 2 | *(empty)* | read-only LLM provider display |
188+
| 3 | `max_concurrent` | int |
189+
| 4 | `max_retries` | int |
190+
| 5 | `llm_provider` | editable in stdio mode |
191+
| 6 | `llm_base_url` | editable in stdio mode |
192+
| 7 | `llm_model` | editable in stdio mode |
193+
| 8 | `llm_api_key` | editable in stdio mode |
194+
195+
### Commands
196+
197+
No state machine. Fixed `waiting_for`: `["update_fields","save","reload_llm","quit"]`.
198+
199+
| Command | Params | Effect |
200+
|---|---|---|
201+
| `update_fields` | `{"fields":{"<json_key>": "<value>", ...}}` | Partial update; only listed keys change |
202+
| `save` | `{}` | Persists config and terminates (`done=true`) |
203+
| `reload_llm` | `{}` | Re-validates LLM provider settings |
204+
| `quit` | `{}` | Exits without saving |
205+
206+
---
207+
208+
## `zread login --stdio`
209+
210+
### ViewModel fields
211+
212+
| Field | Type | Notes |
213+
|---|---|---|
214+
| `state` | string | See states below |
215+
| `authorize_url` | string | OAuth URL to open; present in `open_browser` state |
216+
| `auth_timeout` | int | Nanoseconds until auth expires; present in `waiting` state |
217+
| `browser_warn` | string | Warning if browser launch failed |
218+
| `username` | string | Logged-in username; present in `done` state |
219+
| `avail_models` | array of string | Models to choose from; present in `select_model` state |
220+
| `selected_model` | string | Currently selected model |
221+
| `error` | string | Error message in `error` state |
222+
| `wants_llm_provider_editor` | bool | `true` (with `done=true`) when `--custom` flag was used |
223+
224+
States: `"select_region"``"init_flow"``"open_browser"``"waiting"``"select_model"``"saving"``"done"` / `"error"`
225+
226+
### Commands
227+
228+
| State | `waiting_for` | Command | Params |
229+
|---|---|---|---|
230+
| `select_region` | `["select_region","quit"]` | `select_region` | `{"region":"<provider_key>"}` or `{"region":"__use_own_key__"}` |
231+
| `select_model` | `["select_model","quit"]` | `select_model` | `{"model":"<model_id>"}` |
232+
| all others | `[]` | *(wait for internal auth events)* ||
233+
234+
### Special cases
235+
236+
- `--custom` flag: driver immediately emits `done: true` with `wants_llm_provider_editor: true`. Client should then launch `zread config --stdio` to configure a custom LLM provider.
237+
238+
### State machine
239+
240+
```
241+
select_region ──(cmd)──► init_flow ──► open_browser ──► waiting ──► select_model
242+
select_model ──(cmd)───► saving ──► done
243+
any ────────────────────────────────────────────────────────────────► error
244+
```
245+
246+
---
247+
248+
## `zread update --stdio`
249+
250+
### ViewModel fields
251+
252+
| Field | Type | Notes |
253+
|---|---|---|
254+
| `state` | string | See states below |
255+
| `current_version` | string | Installed version |
256+
| `latest_version` | string | Available version; present in `has_update`+ |
257+
| `download_url` | string | Asset URL for the update binary |
258+
| `package_manager` | string | e.g. `"brew"`; present in `package_manager` state |
259+
| `downloaded_bytes` | int | Bytes downloaded so far |
260+
| `total_bytes` | int | Total download size |
261+
| `download_percent` | float | 0–100 |
262+
| `error` | string | Error message in `error` state |
263+
264+
States: `"checking"``"up_to_date"` / `"package_manager"` / `"has_update"``"downloading"``"confirm_restart"` / `"error"`
265+
266+
Terminal states (emit `done=true`): `up_to_date`, `package_manager`, `error`, and after restart/skip.
267+
268+
### Commands
269+
270+
| State | `waiting_for` | Command | Params |
271+
|---|---|---|---|
272+
| `has_update` | `["download","quit"]` | `download` | `{}` |
273+
| `confirm_restart` | `["restart","skip_restart","quit"]` | `restart` | `{}` |
274+
| `confirm_restart` | `["restart","skip_restart","quit"]` | `skip_restart` | `{}` |
275+
| all others | `[]` | *(wait for internal events)* ||
276+
277+
### State machine
278+
279+
```
280+
checking ──► up_to_date (terminal)
281+
──► package_manager (terminal)
282+
──► has_update ──(download)──► downloading ──► confirm_restart
283+
──(restart/skip)──► done
284+
checking / downloading ──(error)──► error (terminal)
285+
```
286+
287+
---
288+
289+
## `zread version --stdio`
290+
291+
Emits a single event with `done: true` immediately. No commands are accepted.
292+
293+
### ViewModel fields
294+
295+
| Field | Type | Notes |
296+
|---|---|---|
297+
| `version` | string | e.g. `"1.2.3"` |
298+
| `channel` | string | Release channel (if set) |
299+
| `go_version` | string | Go runtime version |
300+
| `os` | string | OS name |
301+
| `arch` | string | CPU architecture |
302+
303+
---
85304

86305
## Practical tips
87306

88307
- Always flush stdin after writing a command (the driver reads line-by-line).
89-
- Treat any line on stdout that fails to parse as JSON as a bug — log it but
90-
do not crash; zread should not print non-JSON to stdout in `--stdio` mode.
91-
- For long-running commands (`generate`), keep reading events continuously;
92-
there is no heartbeat, but progress events arrive whenever state changes.
93-
- To cancel cleanly, send `{"type":"quit","params":{}}` and then drain events
94-
until `done` rather than killing the process.
308+
- Treat any line on stdout that fails to parse as JSON as a bug — log it but do not crash; zread should not print non-JSON to stdout in `--stdio` mode.
309+
- When `waiting_for` is `[]` and `done` is `false`, keep reading — the driver is awaiting an internal async result and will emit the next event on its own.
310+
- For long-running commands (`generate`), keep reading events continuously; there is no heartbeat, but progress events arrive whenever state changes.
311+
- To cancel cleanly, send `{"type":"quit","params":{}}` and then drain events until `done` rather than killing the process.
312+
- `done: true` events are always terminal. Do not send further commands.

0 commit comments

Comments
 (0)