@xmoon76/dsh-pi-tui 0.1.8 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,53 +1,413 @@
1
- # @xmoon76/dsh-pi-tui
1
+ # dsh-pi-tui
2
+
3
+ English | [简体中文](README.zh-CN.md)
4
+
5
+ Release history: [CHANGELOG.md](CHANGELOG.md) · [简体中文更新日志](CHANGELOG.zh-CN.md)
2
6
 
3
7
  A third-party TUI mode for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`), built on a vendored fork of [pi-tui](https://github.com/MoonshotAI/kimi-code/tree/main/packages/pi-tui).
4
8
 
5
- This package is the `dsh.bundle` layer that makes `dsh --profile pi-tui` run a terminal UI instead of the browser GUI (`dsh --profile web`) or one-shot mode (`dsh --profile headless`). The vendored pi-tui fork (`@xmoon76/pi-tui`, with its local rendering fixes — the divergence ledger lives in the repo at `packages/pi-tui/AGENTS.md`) is bundled inside this package's build output — you only ever install this one package.
9
+ Run `dsh --profile pi-tui` for a terminal UI instead of the browser GUI (`dsh --profile web`) or one-shot mode (`dsh --profile headless`).
10
+
11
+ > **Status: working.** The TUI covers the main session loop — input → session events,
12
+ > approvals, commands, session switching and full-text search — plus presets, skills,
13
+ > model/settings menus, and slash commands. Rendering and input routing are verified
14
+ > by headless tests (`@xterm/headless`) with no TTY or model connection needed.
15
+
16
+ ## Screenshot
17
+
18
+ ![dsh-pi-tui running in a terminal](https://raw.githubusercontent.com/XMoon/dsh-pi-tui/main/docs/dsh-pi-tui.png)
19
+
20
+ ## Layout
21
+
22
+ The full repository layout lives in [AGENTS.md](AGENTS.md) (the contributor
23
+ operating manual). In one line: the **repository root is the published
24
+ `@xmoon76/dsh-pi-tui` bundle** (the only published package — its manifest
25
+ declares `dsh.bundle.patch` and the `exports` point at the built `dist/`),
26
+ and `packages/pi-tui/` is the vendored `@moonshot-ai/pi-tui` fork (rescoped
27
+ to `@xmoon76/pi-tui`, private, never published — its divergence ledger lives
28
+ in `packages/pi-tui/AGENTS.md`), bundled into the root package's build
29
+ output.
30
+
31
+ ## Prerequisites
32
+
33
+ - A DeepSeek Harness installation with profiles support (`dsh` on your `PATH`).
34
+ - Node >= 22.19 (`^22.19.0 || >=24`, same range as dsh). Running from source
35
+ needs Node with native TypeScript support (>= 23.6) or the tsx ESM hook
36
+ (`node --import tsx/esm`, how dsh's own source launch works).
37
+ - [pnpm](https://pnpm.io) only when installing from source.
6
38
 
7
39
  ## Install
8
40
 
9
- Prerequisite: a [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) installation with profiles support.
41
+ `dsh plugin` runs pnpm inside the target profile's directory, so the usual
42
+ pnpm verbs (`add`, `remove`, `update`, `list`) all work.
43
+
44
+ ### Option A — from the npm registry (recommended)
45
+
46
+ The published package is self-contained: the vendored pi-tui fork is bundled
47
+ into its build output, so `@xmoon76/dsh-pi-tui` is the only package you install
48
+ (`@xmoon76/pi-tui` stays private in this repo, like kimi-code keeps
49
+ `@moonshot-ai/pi-tui` private):
10
50
 
11
51
  ```sh
52
+ # install the bundle into the pi-tui profile (creates the profile if needed)
12
53
  dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui
54
+
55
+ # run it
13
56
  dsh --profile pi-tui
14
57
  ```
15
58
 
16
- `dsh plugin` runs pnpm inside the profile; any dependency whose manifest declares `dsh.bundle` joins the profile's layer stack automatically.
59
+ Any dependency whose manifest declares `dsh.bundle` joins the profile's layer
60
+ stack automatically — no manual `cordis.patch.yml` wiring.
17
61
 
18
- ## Verify / update / uninstall
62
+ ### Option B — from source
63
+
64
+ Build artifacts are not committed (`dist/` for both packages is gitignored and
65
+ the package `exports` point at the built files), so build before installing
66
+ from a clone:
19
67
 
20
68
  ```sh
21
- dsh plugin --profile pi-tui -- list # is it installed?
22
- dsh plugin --profile pi-tui -- update @xmoon76/dsh-pi-tui # registry installs
69
+ git clone https://github.com/XMoon/dsh-pi-tui
70
+ cd dsh-pi-tui
71
+ pnpm install
72
+ pnpm build # pi-tui tsdown (packages/pi-tui/dist/) + root tsdown (dist/, bundles pi-tui)
73
+
74
+ # file: — the bundle is copied into the profile at add time; rebuild + re-add
75
+ # to refresh (see "Update / uninstall" below)
76
+ dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@file:$PWD
77
+
78
+ # link: — a live symlink instead; `pnpm build` output is picked up directly
79
+ dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@link:$PWD
80
+ ```
81
+
82
+ ### Verify the install
83
+
84
+ ```sh
85
+ dsh plugin --profile pi-tui -- list # @xmoon76/dsh-pi-tui present
86
+ dsh --profile pi-tui # TUI starts instead of the web GUI
87
+ ```
88
+
89
+ ### Update / uninstall
90
+
91
+ ```sh
92
+ # registry installs:
93
+ dsh plugin --profile pi-tui -- update @xmoon76/dsh-pi-tui
94
+ # file: source installs copy at add time — rebuild + re-add to refresh
95
+ # (link: installs track the repo live and need only `pnpm build`):
96
+ pnpm build && dsh plugin --profile pi-tui -- add @xmoon76/dsh-pi-tui@file:$PWD
97
+
23
98
  dsh plugin --profile pi-tui -- remove @xmoon76/dsh-pi-tui
24
99
  ```
25
100
 
26
- ## Slash commands (selection)
101
+ ## Development
102
+
103
+ ```sh
104
+ pnpm install
105
+ pnpm build # pi-tui tsdown (packages/pi-tui/dist/) + root tsdown (dist/, bundles pi-tui)
106
+ pnpm test # pi-tui's own suite (node --test) + dsh-pi-tui headless tests
107
+ pnpm typecheck
108
+ node --expose-gc scripts/bench.mts # performance baseline (optional)
109
+ ```
27
110
 
28
- - `/sessions [query]` — session picker with search-as-you-type, grouping by workspace and live titles
29
- - `/search <query>` — full-text search over persisted session logs
30
- - `/title [title]` — show or set the current session's title
31
- - `/preset`, `/model`, `/settings`, `/export`, `/fork`, `/subagents` — see the command autocomplete (`/` + Tab)
111
+ Tests drive the UI through `@xterm/headless` (see `test/virtual-terminal.ts`),
112
+ so rendering and input routing are verified without a TTY or a model connection.
32
113
 
33
- ## ⚠️ One surface per session
114
+ ### Development history (dogfooding)
34
115
 
35
- **Upstream limitation (a known dsh issue this TUI cannot fix):** dsh's session persistence has no cross-process coordination — each process numbers events from its own in-memory log length (`seq = log.length`) and appends to the same session file. When two processes hold the **same active session** at the same time (TUI + web GUI, two TUIs, TUI + any third-party plugin), one side's resume writes a `session/end-seed` marker and the other side's next append collides on the same seq — **the log corrupts and the session becomes unreadable** (`corrupt session log: seq gap in committed region`).
116
+ This project started development on the browser surface (`dsh --profile web`) and
117
+ switched to building itself with itself: since August 15 2026, all fixes and
118
+ features are developed inside this TUI, the same way this README and the
119
+ codebase are maintained. The dev loop runs on a dedicated `pi-tui-dev` profile
120
+ installed with Option B's `link:` specifier
121
+ (`dsh plugin --profile pi-tui-dev -- add @xmoon76/dsh-pi-tui@link:$PWD`)
122
+ — a live symlink, so `pnpm build` is picked up without re-adding — while the
123
+ `pi-tui` profile stays on the published registry package for real use.
36
124
 
37
- Therefore:
125
+ ## Extensions (early, stabilizing)
38
126
 
39
- - **Do not open the same active session in two surfaces at once** (TUI, web GUI, or any other plugin);
40
- - To switch surfaces, close the session in the current one first, then open it in the other;
41
- - This TUI ships a **divergence guard**: before each message send it checks whether another process has written the session file, and **blocks the send with a warning** when it has (pressing Enter again force-sends at your own risk — it may corrupt the log);
42
- - If a session is already corrupted, fix it with the [session repair script](#session-repair) below (close every process holding the session first).
127
+ Since `0.2.0` the bundle ships a small, versioned extension surface so a
128
+ third-party Cordis plugin can contribute chrome without touching the TUI
129
+ internals. It is **early and stabilizing**: the capabilities below are the
130
+ current set; the API version (`1`) is bumped only on breaking changes, and
131
+ plugins must **feature-detect** capabilities instead of parsing the package
132
+ version.
133
+
134
+ All extension plugins remain standard DeepSeek Harness / Cordis plugins using
135
+ `name`, `inject`, and `apply(ctx)` against the single `piTuiExtensions`
136
+ service; the tiers are capability facades over that one service, not separate
137
+ plugin systems or runtimes.
138
+
139
+ The extension surface ships three tiers: a plugin imports ONLY the
140
+ public entry — never the stable entry's internals (`TuiApp`,
141
+ `TuiMainScreen`, `TuiAltScreen`) nor repository-relative paths.
142
+
143
+ | Tier | Entry | Contract |
144
+ |---|---|---|
145
+ | Stable | `@xmoon76/dsh-pi-tui/extensions` | compatibility-oriented; additive-first; existing semantics never silently change; removal requires a planned breaking change |
146
+ | Advanced | `@xmoon76/dsh-pi-tui/extensions/advanced` | experimental; minor releases may break; a migration note is required; no long-term shims |
147
+ | Unstable | `@xmoon76/dsh-pi-tui/extensions/unstable` | NO compatibility guarantee; implementation may change anytime |
148
+
149
+ All tiers reuse the SAME shared extension runtime: caller-fiber ownership,
150
+ surface lifecycle, invalidation, capability discovery. Do not fork a second
151
+ ownership/lifecycle model per tier. The tiers have grown since Phase 1:
152
+
153
+ - **Advanced** (`ADVANCED_API_LEVEL = 1`, Phase 2 + Phase 4): normalized
154
+ input capture, focused interactive surfaces (interactive managed
155
+ overlays), advanced editor control, the imperative UI broker
156
+ (select/confirm/input/notify), custom interactive UI and the host-state
157
+ facade (theme/title/working/tools-expanded) — still Host-mediated,
158
+ never raw terminal bytes. Author guide: `docs/extension-advanced.md`;
159
+ Pi capability reference: `docs/extension-capability-matrix.md`.
160
+ - **Unstable** (`UNSTABLE_API_LEVEL = 1`, Phase 3): raw input
161
+ interception (observe/consume/rewrite, exclusive raw ownership), the
162
+ Host emergency fail-safe (triple-Esc), and a selected low-level surface
163
+ seam — NO compatibility guarantee; a broken plugin can disrupt Host
164
+ behavior. Author guide: `docs/extension-unstable.md`.
165
+ - **Real-plugin validation (Phase 5):** the tier selection is proven by
166
+ real consumers in `examples/plugins/` — a
167
+ production-class vim modal editor (Advanced editor SDK), a
168
+ questionnaire form (Advanced imperative UI broker) and an interactive
169
+ shell (Unstable raw seam). The "which tier should I use?" decision
170
+ tree: `docs/plugin-authoring.md`.
171
+
172
+ A plugin imports only the public entry:
173
+
174
+ ```ts
175
+ import { PI_TUI_EXTENSIONS_SERVICE, type PiTuiExtensionService } from '@xmoon76/dsh-pi-tui/extensions'
176
+
177
+ export const name = 'my-plugin'
178
+ export const inject = ['tuiStartup', PI_TUI_EXTENSIONS_SERVICE]
179
+
180
+ export function apply(ctx: Context): void {
181
+ const service = ctx.get(PI_TUI_EXTENSIONS_SERVICE) as PiTuiExtensionService
182
+ if (!service.api().capabilities.has('slot.chrome.header.badge')) return
183
+ service.register<{ text: string; tone?: 'info' | 'warning' | 'error' | 'success' }>(
184
+ 'chrome.header.badge',
185
+ { id: 'my-badge', order: 100, description: 'A header badge from my plugin.' },
186
+ { text: 'my-badge', tone: 'info' },
187
+ )
188
+ }
189
+ ```
190
+
191
+ Current extension points (v1):
192
+
193
+ | Slot | Semantics | Contribution |
194
+ |---|---|---|
195
+ | `chrome.header.badge` | list | a short `[badge]` after the host title |
196
+ | `input.dock.item` | list | a dock line above the todo panel |
197
+ | `chrome.footer.status` | list | a footer segment (host owns width/truncation) |
198
+ | `input.widget.above` / `input.widget.below` | list | a bounded widget around the editor (M4 component kit) |
199
+
200
+ Contributions are **plain data**, not render functions: a plugin supplies
201
+ `HeaderBadge` / `DockItem` / `FooterSegment` / `InputWidget` values (text +
202
+ semantic tone spans, or a structured `ExtensionView` tree for widgets) and
203
+ the host owns rendering, ANSI compilation, width budgets and truncation.
204
+ There is deliberately no `render(context)` callback in v1 — plugins never
205
+ hold a rendering context, so a contribution can never capture or mutate
206
+ host internals.
207
+
208
+ Since `0.2.0` the widget slots (`input.widget.above` / `input.widget.below`)
209
+ accept a bounded component kit: `ExtensionView` is a structured view tree
210
+ (`text` / `markdown` / `spacer` / `stack` / `frame` / `rows` views with
211
+ semantic style tokens) that the host compiles into private components. A
212
+ plugin can add helper rows above or below the editor — for example a status
213
+ widget or a quick-reference line — without touching the root layout, the
214
+ editor, or focus. The host owns the row budgets: under height pressure the
215
+ lowest-importance widgets collapse first, and the editor always survives.
216
+
217
+ ```ts
218
+ service.register<InputWidget>('input.widget.below', {
219
+ id: 'my-widget',
220
+ order: 100,
221
+ }, {
222
+ view: {
223
+ kind: 'text',
224
+ spans: [{ text: 'my-plugin ready', tone: 'success' }],
225
+ },
226
+ })
227
+ ```
228
+
229
+ Since M5 the extension surface also covers registries (plan §10):
230
+
231
+ - `registerCommand(contribution)` — slash-command OWNERSHIP metadata
232
+ (`execution: 'local' | 'submission'`): a plugin-declared local command
233
+ always executes directly (never steered by the busy-Enter preference);
234
+ a submission command flows through the session policy. Actual execution
235
+ stays in the host's commands service; `/name args...` keeps
236
+ `rawInput` verbatim. Name conflicts are reported, never guessed.
237
+ - `registerTheme(contribution)` — a named semantic palette selectable
238
+ from the /settings theme picker; the owner's unload falls back to the
239
+ built-in palette (a selected plugin theme never dangles).
240
+ - `registerSetting(contribution)` — a settings row appended to the
241
+ /settings panel (label + current value + choices + optional rejection);
242
+ the host owns the panel.
243
+ - `registerAutocomplete(contribution)` — an autocomplete provider
244
+ consulted after the host's own provider returns null (deterministic
245
+ order, per-provider isolation, latest-only commit).
246
+ - `registerKeybinding(contribution)` — normalized-key → semantic-action
247
+ binding, routed by the host's InputRouter (M6). A plugin declares a key
248
+ with the public `NormalizedKey` shape (key + ctrl/alt/shift/super) and
249
+ a semantic action from the host's list (`submit-draft`, `queue-draft`,
250
+ `steer-draft`, `cancel-activity`, `open-search`, `toggle-fullscreen`,
251
+ `cycle-permission`). The host normalizes ALL terminal input (Kitty
252
+ CSI-u, modifyOtherKeys, legacy sequences) — a plugin never sees raw
253
+ escape data. Reserved host lifecycle keys (Ctrl+C/D/S/F/O/T/G/J,
254
+ Ctrl+Enter, Enter, Esc) cannot be claimed; plain printable keys never
255
+ fire a binding (typing always wins); bindings are non-capturing and
256
+ fire LAST in the precedence ladder (after questions, approvals,
257
+ overlays and the editor). The action executes through the host's own
258
+ paths — submission/session safety is never bypassed.
259
+ - `registerMessageRenderer(contribution)` — a TRANSCRIPT message renderer
260
+ (M7, chain slot): receives a semantic `MessagePresentationSnapshot`
261
+ (immutable; never the mutable message or the container) and returns an
262
+ `ExtensionView` or `undefined` (abdicate → the next renderer → the host
263
+ fallback). Kind-scoped renderers apply to one message kind.
264
+ - `registerToolRenderer(contribution)` — a TOOL card renderer (M7, keyed
265
+ slot): presents the card for ONE tool name from a
266
+ `ToolPresentationSnapshot` (callId, toolName, status, arguments,
267
+ result, expanded); the winner (lowest priority) abdicates to the next
268
+ renderer, then the host fallback. A priority tie on the same tool name
269
+ is an explicit error.
270
+ Renderers never stall the transcript: a throwing renderer is isolated
271
+ and the chain continues, and the message cache embeds the renderer
272
+ identity + registry revision, so an HMR/unload rebuilds exactly the
273
+ affected components.
274
+ - `showOverlay(view, options)` — a MANAGED overlay lease (M8): the plugin
275
+ supplies an `ExtensionView` + sizing hints; the host mounts it through
276
+ its overlay broker (modal stacking, focus, fullscreen migration). The
277
+ returned lease is generation-scoped (the surface's final dispose closes
278
+ every still-owned lease), close() is idempotent, and hide()/show()
279
+ toggle visibility without closing. A plugin can never mount a raw
280
+ component or steal focus — the host owns the terminal and the overlay
281
+ stack.
282
+ The [extension API v1 author guide](docs/extension-api.md) records the
283
+ import rules, the full surface table, the lifecycle/render contracts, the
284
+ M11 deprecation policy and the stability contract.
285
+
286
+ The M10 acceptance fixture (plan §15): the repo ships a vim-mode fixture
287
+ (`test/fixtures/vim-plugin/`) that validates the editor-extension seam — the
288
+ packed public SDK is consumable by a third-party Cordis plugin, its
289
+ replacement editor receives SEMANTIC `EditorInputEvent`s (never raw terminal
290
+ bytes), and editor `create()`/`dispose()` work — importing ONLY
291
+ `@xmoon76/dsh-pi-tui/extensions`. It is NOT a production Vim and NOT a
292
+ Stable-API completeness proof: modal-mode behavior (insert/normal) is not
293
+ part of the Stable contract, and the other public surfaces (commands,
294
+ themes, settings, autocomplete, keybindings, renderers, overlays, widgets)
295
+ have their own dedicated tests. Its CI gate forbids `@xmoon76/pi-tui`,
296
+ `src/tui-app` and repository-relative internal paths: if a STABLE plugin
297
+ ever needs a private import, the SDK is missing a capability (there is no
298
+ `unsafeGetTuiApp()` escape hatch).
299
+
300
+ - `registerEditor(contribution)` — the EDITOR SDK (M9, plan §14):
301
+ single-winner by priority (a tie is an explicit error); the winner
302
+ occupies the editor seat through the host's ATOMIC handoff (create →
303
+ transfer draft/cursor → mount → focus → dispose old). A creation throw
304
+ keeps the current editor working; winner unload restores the next
305
+ winner / the host default editor WITH the draft preserved. The plugin
306
+ editor receives an `EditorHost` (surfaceId, generation, getSnapshot,
307
+ replaceText, dispatch of semantic actions submit/queue-submit/steer/
308
+ open-external-editor, subscribe, invalidate) — but the host still owns
309
+ busy-Enter, Ctrl+Enter, local-command classification, paste protection,
310
+ approval/question capture, session guard/lock, external editor and
311
+ exit: a plugin editor can never bypass those.
312
+
313
+ Lifecycle is host-owned: registrations are disposed when the plugin's Cordis
314
+ fiber unloads (HMR, disable), regular and fullscreen both refresh, and
315
+ `handle.invalidate()/replace()` re-render through the active screen. The
316
+ `@xmoon76/dsh-pi-tui/builtins` entry is the Loader-only first-party
317
+ contributor (version badge, turn/step counters, todo-summary dock item) —
318
+ not a stable third-party SDK. Raw terminal access, pre-host input
319
+ interception and full input ownership are NOT part of the Stable tier (see
320
+ the Advanced/Unstable roadmap).
321
+
322
+ ## Slash commands (selection)
323
+
324
+ - `/sessions [query]` — open the session picker: search-as-you-type over
325
+ session ids, titles, and workspaces, rows grouped by workspace with live
326
+ `filtered/total` counts, and titles loaded in the background as they are
327
+ read. Enter switches to the selected session.
328
+ - `/search <query>` — full-text search over persisted session logs, then
329
+ switch to a hit.
330
+ - `/title [title]` / `/rename [title]` — with an argument, set the current
331
+ session's title (pins it against automatic generation; titles appear in
332
+ the `/sessions` picker); **without an argument, regenerate the title from
333
+ the conversation — this overwrites the current title, including one you
334
+ pinned earlier**.
335
+ - `/yolo` — switch to `danger-full-access` (alias of `/permission danger-full-access`).
336
+ - `/queue` — per-item queue management: edit, delete, steer one, or insert a
337
+ message into the agent's inbox (the queue pane above the editor shows
338
+ pending messages; `Ctrl+S` steers them all at once, `Alt+↑` pulls them all
339
+ back into the editor).
340
+ - `/status` — show the current session's stats and identity (turn counts,
341
+ token usage, workspace, installed dsh version).
342
+ - `/preset`, `/model`, `/settings`, `/export`, `/fork`, `/subagents` — see
343
+ `dsh --profile pi-tui`'s command autocomplete (`/` + Tab).
344
+
345
+ ## Keybindings (selection)
346
+
347
+ - `Ctrl+F` — toggle transcript search (the `/search <query>` overlay; a
348
+ second press closes it).
349
+ - `Shift+Tab` — cycle the permission preset (read-only → workspace-write →
350
+ danger-full-access); the footer's mode slot badges every preset
351
+ (`[workspace-write]` / `[read-only]` / `[custom]`, with `[yolo]` flagging
352
+ the no-approval mode).
353
+ - `Ctrl+S` — steer: with queued messages, sends the whole queue (plus the
354
+ draft, if any) into the running turn at once; otherwise sends the draft
355
+ alone. An idle agent starts a fresh turn with everything.
356
+ - `Alt+↑` — dequeue: pull every queued message back into the editor draft.
357
+ - `Ctrl+T` — toggle the full todo list; the dock above the editor always shows
358
+ the todo summary and background tasks, and queued input renders between
359
+ them.
360
+ - `@` — file/folder mentions in the editor: `@` + Tab completes files from the
361
+ whole workspace (fd-backed when `fd` is on PATH, with a built-in recursive
362
+ fallback otherwise). The literal `@path` is submitted and the model reads
363
+ the file itself. With background work running, an empty editor's `↓` or
364
+ `Ctrl+J` opens the task browser over both surfaces:
365
+ - **subagent rows** (live continuable children) — `Enter` opens the child's
366
+ transcript read-only (`Esc` returns); they never register jobs records, so
367
+ this browser is their only glanceable home.
368
+ - **job rows** (bash and one-shot subagent jobs) — `Enter` shows the status
369
+ viewer only: a bash job's output read cursor belongs to the model's
370
+ `job_output`, and a one-shot subagent job record carries no child session
371
+ id, so the transcript is reached via `/subagents` (`s` stops a job).
372
+ The footer badge shows `[N tasks running · M agents · ↓ view]` while any
373
+ background work is live.
374
+
375
+ ## Launch options
376
+
377
+ The TUI's startup row adds `--preset <id>` — the agent preset a fresh
378
+ session starts on (falls back to `$DSH_PI_TUI_PRESET`, then the saved
379
+ settings default). It exists because `/preset` only applies to a blank (not
380
+ yet created) session, so launch-time selection is the other half of choosing
381
+ a preset. All other flags are the dsh runner's own (`--session <id>`, …).
382
+
383
+ ## Session lifecycle
384
+
385
+ Opening the TUI with no `--session` creates **no session at all**: the first
386
+ user message (text, slash command, `Ctrl+S` steer, or `!` shell) starts it
387
+ lazily. `--session <id>` still resumes immediately, and a local `!!` command
388
+ runs without needing a session.
389
+
390
+ ## Verified in the P0 spike
391
+
392
+ - Vendored pi-tui: the fork's own suite passes under `node --test` (run it as
393
+ the sync gate after every re-vendor; the count is deliberately not copied here —
394
+ `packages/pi-tui/package.json` is the single source of version facts).
395
+ - `TuiApp` renders, accepts editor input, and handles Ctrl+C on a headless xterm.
396
+ - The whole import chain (pi-tui, tui-app, `@deepseek-ai/dsh-cmdline`, commander)
397
+ loads under the tsx ESM hook — the dsh source-launch contract.
398
+ - Native modifier-key addons are optional: on Linux the loader returns `undefined`
399
+ without attempting a load, and the non-TTY stdin path is guarded.
43
400
 
44
401
  ## Diagnostic log
45
402
 
46
- The TUI writes its own diagnostics to stderr and a log file (`ctx.logger` is invisible in this process — no exporter):
403
+ The TUI writes its own diagnostics to stderr and a log file (`ctx.logger` is
404
+ invisible in this process — no exporter):
47
405
 
48
406
  - Default file: `$DSH_HOME/logs/pi-tui-<pid>.log` (default `~/.dsh/logs/`);
49
407
  - Line format: `[tui] <ISO time> <level> <message> k=v ...`;
50
- - Default level `info`: key lifecycle events only (boot/resume/switch/exit, divergence-guard warnings, errors); `debug` additionally logs every guard check before a send.
408
+ - Default level `info`: key lifecycle events only (boot/resume/switch/exit,
409
+ divergence-guard warnings, errors); `debug` additionally logs every guard
410
+ check before a send.
51
411
 
52
412
  Configuration (environment variables):
53
413
 
@@ -63,37 +423,39 @@ DSH_PI_TUI_LOG_LEVEL=debug dsh --profile pi-tui
63
423
  tail -f ~/.dsh/logs/pi-tui-*.log
64
424
  ```
65
425
 
66
- ## Session repair
67
-
68
- **Background:** when a session log is corrupted (duplicate or missing seqs) dsh can no longer read that session. This repository ships a standalone repair script, `scripts/repair-session.mjs`: it locates the session file, diagnoses the damage, and repairs it — renumbering seqs (duplicates), truncating at the first missing/unparsable event (which cannot be recreated), or re-framing the log when the frame layout is wrong (dsh requires the first zstd frame to be exactly the header line; a whole-log single frame is structurally valid zstd but rejected by every dsh reader). It **always backs up the original before writing** and verifies the result with the same scans the reader performs (frame layout + event contiguity).
69
-
70
- **Dependency:** resolves `@deepseek-ai/dsh-session` from the dsh installation (auto-detected through the `dsh` launcher on PATH, or `--dsh-dir <path>`).
71
-
72
- **Repair one session (dry run by default; `--yes` writes):**
73
-
74
- ```sh
75
- # Close every process holding the session first (TUI / web / plugins)!
76
- node scripts/repair-session.mjs session-ab79200b-0e36-4a62-a7a6-3ac620c05f1d # diagnose + repair plan (no write)
77
- node scripts/repair-session.mjs session-ab79200b-0e36-4a62-a7a6-3ac620c05f1d --yes # apply the repair
78
- ```
79
-
80
- The original file is backed up to `session.jsonl.zstd.bak-<timestamp>` before writing (a failed backup aborts the write); the report prints the backup path.
81
-
82
- **Read-only scan of all sessions, listing damaged ids:**
83
-
84
- ```sh
85
- node scripts/repair-session.mjs --scan
86
- ```
87
-
88
- Prints one `CORRUPT <id>: <reason>` per damaged session; `no damaged sessions found` when everything is healthy.
89
-
90
- **Other options:** `--dsh-home <path>` sets the sessions root (default `$DSH_HOME` or `~/.dsh`); `--help` shows usage.
91
-
92
- ## Development
93
-
94
- Source and tests live in the [dsh-pi-tui repository](https://github.com/XMoon/dsh-pi-tui) (`packages/dsh-pi-tui/`); headless tests drive the UI through `@xterm/headless` with no TTY or model connection.
426
+ ## Safety & operational notes
427
+
428
+ - **One surface per session.** dsh has no cross-process session coordination:
429
+ a session open in TWO dsh processes (TUI + web, or two TUIs) can corrupt its
430
+ log. The TUI refuses to open a session already held by another live dsh
431
+ process (an `owner.lock` file next to the log, with a pid/starttime probe
432
+ for stale locks left by crashes — so the second surface is stopped at
433
+ OPEN time, not after the damage). For writes, the TUI detects the other
434
+ writer and blocks the send; the SAME action pressed again (Enter for a
435
+ submit, Ctrl+S for a steer, unchanged draft) forces through — an edited
436
+ draft, a swapped key, a new file revision, or a session switch invalidates
437
+ the force. Never run two surfaces on one session (full contract:
438
+ `docs/concurrency.md`).
439
+ - **Session repair.** `node_modules/@xmoon76/dsh-pi-tui/scripts/repair-session.mjs`
440
+ repairs corrupted logs (`--scan` lists damage read-only; `--yes` applies with
441
+ a mandatory backup). A torn (truncated) tail is truncated at the last
442
+ complete frame and reported with exact byte accounting; references to a
443
+ duplicated seq are never auto-resolved — the repair refuses and asks for
444
+ `--duplicate-reference=first|last|segment`. Repaired logs are re-verified
445
+ with the dsh reader's own layout checks before the backup is considered
446
+ redundant. (Full repair contract, incl. the frame-layout constraint:
447
+ `docs/repair-session.md`.)
448
+ - **Exit.** `/exit` (alias `/quit`) flushes the session with a 10s hard
449
+ timeout: a hung provider cannot trap the TUI. If the flush fails or times
450
+ out, the terminal prints a warning (the tail may not be persisted) and the
451
+ process still exits.
452
+ - **Performance.** `scripts/bench.mts` (non-default) measures ingest,
453
+ projection, cold/warm rebuilds, streaming frames, theme switches, and heap;
454
+ the saved baseline lives in `docs/perf-baseline.md`. Unchanged transcript
455
+ messages reuse their rendered components, so the warm per-frame rebuild
456
+ does not grow with history.
95
457
 
96
458
  ## License
97
459
 
98
- MIT. The bundled pi-tui fork retains its upstream MIT license and authorship
460
+ MIT. `packages/pi-tui` retains its upstream MIT license and authorship
99
461
  (Copyright (c) 2025 Mario Zechner; Moonshot AI fork).