@@ -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
61621 . Spawn ` zread <command> --stdio ` with piped stdin/stdout.
62632 . 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 ` .
65654 . 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