@aiwayds/dsh-tui-pi 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,347 +1,277 @@
1
1
  # dsh-tui-pi
2
2
 
3
- pi-style terminal UI for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh).
3
+ pi-style terminal UI for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) — a plugin suite that turns dsh into a pi-like coding agent experience.
4
4
 
5
- ## Preview (ASCII mock-up)
5
+ > 中文说明: [README.zh.md](README.zh.md)
6
+
7
+ ## Screenshot
8
+
9
+ ![dsh-tui-pi demo](./dsh-tui-pi-demo.gif)
10
+
11
+ A live terminal recording of a session — todos, running subagents, think/tool
12
+ panels and the powerline footer in action. ([Interactive playback on
13
+ asciinema](https://asciinema.org/a/BE212ZO8x1zEZyZn))
14
+
15
+ ### Layout overview
16
+
17
+ ```
18
+ ┌─────────────────────────────────────────────────────────────────────┐
19
+ │ Transcript (scrollable) │
20
+ │ ┌─────────────────────────────────────────────────────────────┐ │
21
+ │ │ 💭 thinking — reasoning in progress │ │
22
+ │ └─────────────────────────────────────────────────────────────┘ │
23
+ │ ⚙ bash python scripts/demo.py … ✔ bash │
24
+ │ ↳ 生成 2 个 todo, 每个 todo 起一个 10s 的 subagent │
25
+ │ ↳ ⠼ Workhorse 10s 任务 · 1.2k token · 19.0s │
26
+ └─────────────────────────────────────────────────────────────────────┘
27
+ ┌─ ● Todos (0/8) ────────────────────────────────────────────────────┐
28
+ │ ├─ ☑ 调研 dsh-tui-pi 斜杠命令/补全机制 │
29
+ │ ├─ ◐ 调研 harness ctx.skills API │
30
+ │ └─ ☐ 实现 /skill:<name> 补全并触发 skill │
31
+ └─────────────────────────────────────────────────────────────────────┘
32
+ ∴ working… │
33
+ ~/github (Full access) │ ⎇ main │
34
+ [ 请输入指令… ] │
35
+ ↳ 第一 打slash 命令的时候 显示 /skill:<skill name> 选择后使用 │
36
+ ↳ ⠼ 牛马狗 · 1.5m/1m · 635.7s │
37
+ dsh ▸ volc-ark-plan ▸ deepseek-v4-flash ▸ high ▸ 48.7k/1.0M(4.6%) │
38
+ ▸ ⚡ CH85.4% ▸ 15 msgs ▸ 11 tools 00:02:13 │
39
+ Esc ×2: stop · Ctrl+C ×2: quit · Ctrl+G: subagents · ↑↓: history │
40
+ └─────────────────────────────────────────────────────────────────────┘
41
+ │ │ │
42
+ │ │ └─ Footer (powerline)
43
+ │ └─ Running subagents (last-request area)
44
+ └─ Todos panel (bordered, above editor)
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Features
50
+
51
+ ### Footer
52
+
53
+ A powerline-style status bar pinned at the bottom of the screen, showing live session state at a glance:
6
54
 
7
55
  ```
8
- ┌──────────────────────────────────────────────────────────────┐
9
- │ 🐳: 两个 subagent 已在后台并行启动 (A: 5b19b15c, B: d2072ffd) │ ← transcript (scrolls)
10
- │ ┌─ 💭 thinking ──────────────────────────────────────────┐ │
11
- │ │ Actually, I can check list_agents or wait… │ │
12
- │ └────────────────────────────────────────────────────────┘ │
13
- │ ⚙ bash python scripts/demo.py … ✔ bash │ │
14
- └──────────────────────────────────────────────────────────────┘
15
- ┌─ Todos (1/2) ────────────────────────────────────────────┐
16
- ├─ Todo 1: 启动 subagent A 执行 10s 任务并收集结果 │ ← fixed widgets
17
- └─ Todo 2: 启动 subagent B 执行 10s 任务并收集结果 │ above the input
18
- └─────────────────────────────────────────────────────────────┘
19
- working… ← status
20
- 📁 ~/github (Full access) main ← editor border
21
- [ 请输入指令… ] input
22
- 创建 2 todo, 每个 todo 起一个 10s 的 subagent ← last request
23
- ↳ ⠼ Subagent A 10s 任务 · 1.2k token · 19.0s ← running agents
24
- Subagent B 10s 任务 · 562 token · 6.0s ← (compact lines)
25
- dsh ▸ ☁ opencode-go ▸ 🤖 deepseek-v4-flash ▸ ● high ▸ 🧠 11.6k/1.0M (1.2%) ▸ ⚡ CH98.9% ▸ 💬 8 ▸ 🔧 4 00:00:14 ← footer
26
- Enter: send · Ctrl+C: cancel / double: quit ← hints
56
+ dsh ▸ volc-ark-plan ▸ deepseek-v4-flash ▸ high ▸ 48.7k/1.0M(4.6%) ▸ ⚡ CH85.4% ▸ 15 msgs ▸ 11 tools 00:02:13
57
+ ```
58
+
59
+ Seven segments read O(1) maintained counters (never re-scan the session log):
60
+
61
+ | Segment | Content |
62
+ |---|---|
63
+ | **Provider** | current `provider/model` route |
64
+ | **Model** | model short-name |
65
+ | **Thinking** | reasoning effort level (`off` / `high` / `max`) |
66
+ | **Context** | `used / max (percent%)` |
67
+ | **Cache-hit** | `CHxx%` — prompt-cache hit rate |
68
+ | **Messages** | total user + assistant messages |
69
+ | **Tools** | total tool invocations |
70
+ | **Clock** | live right-aligned HH:MM:SS (tick every second) |
71
+
72
+ Segments are rendered with [U+E0B0](https://www.nerdfonts.com/cheat-sheet) powerline arrows; the palette is hot-swappable with the current theme.
73
+
74
+ The editor's top border shows the working directory and git branch:
75
+
76
+ ```
77
+ ~/github (Full access) │ ⎇ main
78
+ ```
79
+
80
+ ---
81
+
82
+ ### Think & Tool Blocks
83
+
84
+ In-flight thinking and tool calls render as fixed **panels pinned above the chat input** (they never appear in the scrollable transcript):
85
+
86
+ ```
87
+ ┌─ 💭 thinking ──────────────────────────────────────────────┐
88
+ │ Actually, I can check list_agents or wait… │
89
+ └────────────────────────────────────────────────────────────┘
90
+ ⚙ bash python scripts/demo.py … ✔ bash
91
+ ```
92
+
93
+ Key behavior:
94
+
95
+ - **One panel per type** — a single `ThinkPanel` and a single `ToolPanel` exist for the whole run; each event refreshes the panel in place, so there's no transcript churn.
96
+ - **Empty = hidden** — when nothing is active, the panel renders zero rows and disappears.
97
+ - **`dsh-tui.panelHeight`** (default `1`): one borderless row (block id + elapsed + last content line, right-truncated); `5`/`7`/`10` renders a boxed panel; `all` prints the full body.
98
+ - **Delegation tools** (`use_agent`, `subagent`, `workflow`, `ralph`) never open a tool block — their children appear as running-agent lines (see Subagents).
99
+
100
+ ---
101
+
102
+ ### Subagents
103
+
104
+ Running subagent activity is shown in the **last-request area below the editor** as compact, one-line-per-child status rows:
105
+
106
+ ```
107
+ ↳ 创建 2 个 todo, 每个 todo 起一个 10s 的 subagent
108
+ ↳ ⠼ Subagent A 10s 任务 · 1.2k token · 19.0s
109
+ ↳ ⠼ Subagent B 10s 任务 · 562 token · 6.0s
110
+ ```
111
+
112
+ Each line shows: spinner + agent **name**, retries (`↻N≤M`), token count (+ context %), elapsed. No provider shown, no box, no header — just one line per running child.
113
+
114
+ #### Todos
115
+
116
+ The `● Todos (done/total)` tree is a bordered panel pinned **above the chat input** (never scrolls with the transcript):
117
+
118
+ ```
119
+ ┌─ ● Todos (0/8) ──────────────────────────────────────────┐
120
+ │ ├─ ☑ Todo 1: research subagent spawn API │
121
+ │ ├─ ◐ Todo 2: implement /skill:<name> autocomplete │
122
+ │ └─ ☐ Todo 3: add settings panel skills branch │
123
+ └───────────────────────────────────────────────────────────┘
124
+ ```
125
+
126
+ Icons: `☑` completed, `◐` in-progress, `☐` pending. A settled child drops off; when empty, both the todo panel and agent lines collapse to zero rows.
127
+
128
+ #### Viewer & limits
129
+
130
+ `Ctrl+G` (or `/subagents`) opens an 80% picker over tracked children — running ones first, then the five most recently settled. Enter opens a live transcript viewer (refreshes ~3×/s with tail-follow).
131
+
132
+ Two caps (`/agents` → `l` to configure):
133
+
134
+ - **`maxAgents`** (default 4, `0` = unlimited) — spawns are denied when the cap is hit.
135
+ - **`maxRounds`** (default 50, `0` = unlimited) — after a child's completed turns reach the cap, the TUI queues one wrap-up request and never force-stops.
136
+
137
+ ---
138
+
139
+ ### DCP (Dynamic Context Pruning)
140
+
141
+ [DCP](https://github.com/fan56/dsh-dcp) is a standalone zero-LLM compaction plugin for dsh — it automatically trims context to stay within limits without calling an LLM to summarize.
142
+
143
+ `dsh-tui-pi` lists `@aiwayds/dsh-dcp` as a dependency, but **does not mount it** — dsh-dcp ships its own `cordis.patch.yml` (since `@aiwayds/dsh-dcp@0.2.0`). To activate:
144
+
145
+ ```sh
146
+ dsh plugin --profile tui add @aiwayds/dsh-dcp
27
147
  ```
28
148
 
29
- - **Look & feel**: pi coding agent interactive TUI, built on
30
- `@earendil-works/pi-tui` 0.84.2 (pinned) — alt-screen scrollable transcript,
31
- docked editor/status/footer, markdown messages, slash-command autocomplete.
32
- - **Slash commands**: dsh's own, untouched. Autocomplete from
33
- `ctx.commands.list(agent)`, executed via `ctx.commands.execute(agent, line, signal)`.
34
- Verified: `/compact`, `/plan`, `/goal`, `/permission`, `/feedback`. The TUI
35
- adds its own surface commands — `/model /think /session /resume /new
36
- /settings /export /theme /reload` (see below).
37
- - **Themes**: GitHub light / GitHub dark palettes (aligned with the
38
- `cmux-theme.sh` GitHub terminal themes). Hot-switchable at runtime: pick one
39
- with `/theme` (applies immediately), edit the `dsh-tui.theme` setting
40
- (external edits hot-apply too), or pin with `DSH_TUI_THEME=light|dark` — the
41
- env var wins over every preference. The app paints its own canvas, so a
42
- switch recolors the whole screen (background included) even inside
43
- multiplexers; without a preference, `auto` detects the terminal background
44
- (COLORFGBG + a live OSC 11 / CSI 996n query) and follows the terminal's
45
- light/dark switches in real time.
46
- - **Footer**: powerline segments ported from
47
- [pi-powerline-footer](https://github.com/fan56/pi-powerline-footer) —
48
- provider / model+thinking / context / cache-hit / msgs / tools with U+E0B0
49
- arrows, right-aligned live clock, cwd+git-branch editor top border, and the
50
- `↳ last-request` widget.
51
- - **Live todos & subagents**: the `● Todos (done/total)` tree (`☐`/`◐`/`☑`
52
- status icons) is a bordered panel pinned **above the chat input**; the
53
- running subagent activity merges into the **last-request area below the
54
- editor** as compact lines (` ↳ ` prefix, spinner + agent **name** first,
55
- retries, token count + context percent, elapsed — no provider) — no box, no
56
- header, just one line per running
57
- child. Both refresh ~10×/s. Show while there is content, clear when done —
58
- a settled child drops off and an empty panel/area collapses to zero rows.
59
- Subagents are tracked from the child sessions themselves (header `origin:
60
- subagent` + `parentSession`), so any spawn mechanism works. The TUI also
61
- supports pi's `APPEND_SYSTEM.md` convention
62
- (dsh side: `~/.dsh/APPEND_SYSTEM.md`): a user-editable file whose content is
63
- appended to the system prompt of every agent the TUI creates — read at each
64
- assembly, so edits apply to the next request without a restart. The TUI's
65
- own `dsh-tui-pi:todo-lifecycle` guidance (marker
66
- `<!-- dsh-tui-pi:todo-lifecycle -->`) lives in that file, telling the model
67
- to write an empty todo list once everything is completed (idempotent,
68
- atomic, best-effort; the panel-side all-completed hide remains as fallback).
69
-
70
- ## Commands
149
+ Once mounted, DCP runs transparently in the background. The footer's **context** and **cache-hit** segments reflect the pruning effect in real time.
150
+
151
+ ---
152
+
153
+ ## Slash commands
71
154
 
72
155
  | Command | What it does |
73
156
  |---|---|
74
- | `/model` | pick provider/model every route the mounted llm services list (pi-ai's built-in catalog + llm-deepseek's static pair). Two-stage picker: choose the model, then a think level when the route exposes one (Esc on stage 2 abandons the whole pick). Live switch with footer sync, persisted as the default. |
75
- | `/think` | reasoning-effort picker for the current model — `(provider default)` clears the override, then the route's efforts (Off / High / Max). Live + persisted. |
76
- | `/session` | read-only info panel: id, cwd, created, model, think level, status, message/tool counts, token usage, event count, parent session. |
77
- | `/resume` | pick a persisted session (subagent children and the current session are filtered out), validate its log *before* touching the live agent, then restore it — transcript and footer stats rebuild from the stored events. |
78
- | `/new` | detach the current session and clear the transcript; the next prompt opens a fresh one (the escape hatch when the current history must not follow, e.g. images in it). |
79
- | `/settings` | text-based settings browser: namespaces grouped into categories (General / Models / Plugins / Agent Presets / Other), schema walk with drill-ins, cycle rows, inline editors (secrets masked), dict add-key, reset-to-defaults. Writes go through the settings mutate chain. |
80
- | `/export` | write the current session log as JSONL — default `~/Downloads/dsh-session-<id>.jsonl`, or a path argument. |
81
- | `/permission` | permission-preset picker (whatever the deployment table advertises — read-only / workspace-write / danger-full-access). Select a preset to apply it through dsh's canonical `/permission <name>` command, or Esc to keep the current one. The editor's top border shows the live preset badge (danger-full-access → "Full access"). |
82
- | `/theme` | color-scheme picker (auto / light / dark). The choice applies immediately and is persisted to `dsh-tui.theme`. |
83
- | `/agents` | manage agent definition markdown files (name/model/thinking/deep per agent) **and the subagent limits** `l` from the table opens the limits panel: `maxAgents` (concurrent live children, default 4) and `maxRounds` (completed turns before the TUI queues a wrap-up request, default 50; both `0 = unlimited`). Limits are read live at every spawn/turn decision; writes go to the `dsh-tui` settings namespace and hot-apply. Also the initial view when no agent files exist yet. |
84
- | `/subagents` | the command twin of `Ctrl+G`: pick a running (or recently settled) subagent and watch its live transcript in the 80% viewer — status, rounds against the cap, tokens, tool calls. |
85
- | `/reload` | hot-reload the plugin from the current source (after `pnpm build`) without restarting dsh — the TUI and the live agent are torn down; the session log persists and can be rejoined with `/resume`. |
86
- | `/hotkeys` | keybinding browser: the effective app-key table (custom overrides starred) plus the keybindings file path — see [Custom keybindings](#custom-keybindings). |
87
-
88
- Anything that is not a resolvable command falls through to the model as an
89
- ordinary prompt, so dsh packages' commands (and future registrations) appear
90
- automatically.
91
-
92
- ### Models: provider-first
93
-
94
- `/settings Models` does not expose the raw `llm-pi-ai` namespace. It lists
95
- one row per configured provider — label (`displayName` ?? catalog name ??
96
- route key), value column (first model / `N models` / `catalog` when pi-ai
97
- serves the route), and one-line API-key state (`API key set` / `missing` /
98
- `not configured`) probed from the process environment plus the credentials
99
- document with dedicated `DeepSeek (official)` and `Default model` rows and a
100
- `+ Add provider…` action. The add flow mirrors pi's `/login`:
101
-
102
- 1. pick from the directory — every llm-pi-ai catalog route that takes an API
103
- key (36 in the installed pi-ai 0.82.1), read live from the llm service with
104
- a static fallback, the same directory as the web Models page;
105
- 2. enter exactly one API key — masked dot-row editor, the value never echoes
106
- and never reaches the rendered output;
107
- 3. the commit double-writes like the web Models page: `llm-pi-ai.providers.<id>`
108
- gets `{ apiKeyEnv: <ref> }` through the settings mutate chain (ref derived
109
- by convention: route key uppercased, non-alphanumerics → `_`, `_API_KEY`
110
- suffix, e.g. `opencode-go → OPENCODE_GO_API_KEY`) and the key is stored via
111
- `ctx.credentials.set` — never in `settings.yaml`. Without a credentials
112
- service the profile still commits and the UI says
113
- `export <REF>=<key> to use it` instead.
114
-
115
- ### Theme hot-switch
116
-
117
- Themes change live, no restart:
118
-
119
- - `/theme` (or an edit through `/settings → General → dsh-tui`) commits the
120
- preference to the `dsh-tui` settings namespace, which is registered
121
- `applies: 'live'`. The namespace's watch hook pushes the commit to the
122
- running TUI, which repaints everything on the next frame: transcript
123
- (replayed from its operation buffer), editor border, footer hint, spinner.
124
- - **The whole screen changes, background included.** The TUI paints its own
125
- canvas (a patched pi-tui paints every rendered row with the palette's
126
- canvas color), so a light→dark switch recolors the entire surface — the
127
- terminal's own background never shows through, which is what makes the
128
- switch look broken inside multiplexers like cmux/gostty where the pane
129
- background belongs to the terminal, not the app. Set
130
- `DSH_TUI_TRANSPARENT=1` to go back to the see-through canvas and keep your
131
- terminal theme visible.
132
- - An **external edit** of `~/.dsh/settings.yaml` (`dsh-tui.theme: dark`)
133
- hot-applies through the same watch path.
134
- - `DSH_TUI_THEME=light|dark` **pins** the display regardless of preference —
135
- it wins at startup and keeps winning; `/theme` still persists the
136
- preference and honestly reports `Theme preference saved — display is pinned
137
- by DSH_TUI_THEME=…` instead of claiming it applied.
138
- - The choice survives restarts. `auto` detects the terminal: the synchronous
139
- startup guess reads `COLORFGBG`; a background refinement then asks the
140
- terminal itself (CSI `?996n` color-scheme query, falling back to an OSC 11
141
- background-color query — both answered by Ghostty/cmux, kitty and iTerm),
142
- and while `auto` stays selected the TUI follows live light/dark switches of
143
- the terminal (CSI 997 push notifications) and repaints on the next frame.
144
-
145
- ## APPEND_SYSTEM.md
146
-
147
- dsh-tui-pi supports pi's `APPEND_SYSTEM.md` convention on the dsh side:
148
- **`~/.dsh/APPEND_SYSTEM.md`** (`$DSH_HOME` or `~/.dsh`) is appended to the
149
- system prompt of every agent this TUI creates. The file is read at each
150
- prompt assembly, so **edits apply to the very next request** — no restart,
151
- no reload, no watcher.
152
-
153
- - **The file is not shipped with the source**: the repo contains no
154
- `APPEND_SYSTEM.md` at its root — it lives in your `~/.dsh` and is yours to
155
- edit freely (identity, persona rules, UI conventions — anything you want
156
- the model to know). The English **template** the installer seeds from is
157
- `templates/APPEND_SYSTEM.md` (the pi orchestrator-identity definition,
158
- translated — content lives in that file, not in code).
159
- - On first run the TUI **creates** the file from the template if missing and
160
- **maintains one marked section** in it (`<!-- dsh-tui-pi:todo-lifecycle
161
- -->`, telling the model to clear the todo list once everything is
162
- completed). Your own content is never touched; the maintenance is
163
- idempotent, atomic (tmp + rename) and best-effort.
164
- - An empty or missing file contributes nothing to the prompt (the section is
165
- dropped by the prompt renderer).
157
+ | `/model` | Two-stage provider/model picker (then thinking level). Live switch, persisted. |
158
+ | `/think` | Reasoning-effort picker for the current model (`Off`/`High`/`Max`). |
159
+ | `/session` | Read-only info panel: id, cwd, model, token usage, event count. |
160
+ | `/resume` | Pick a persisted session, validate its log, then restore it. |
161
+ | `/new` | Detach the current session; the next prompt opens a fresh one. |
162
+ | `/settings` | Text-based settings browser (namespaces, schema walk, inline editors, secrets masked). |
163
+ | `/export` | Write the current session log as JSONL (`~/Downloads/dsh-session-<id>.jsonl`). |
164
+ | `/permission` | Permission-preset picker (read-only / workspace-write / danger-full-access). |
165
+ | `/theme` | Color-scheme picker (`auto` / `light` / `dark`). Applies immediately. |
166
+ | `/agents` | Manage agent markdown files + subagent limits (`maxAgents`, `maxRounds`). |
167
+ | `/subagents` | Pick a running/recent subagent and watch its live transcript. |
168
+ | `/reload` | Hot-reload the plugin from source (after `pnpm build`) without restarting dsh. |
169
+ | `/hotkeys` | Keybinding browser and live editor. |
170
+
171
+ Anything that is not a resolvable command falls through to the model as an ordinary prompt.
172
+
173
+ ---
174
+
175
+ ## Keyboard shortcuts
176
+
177
+ | Key | Action |
178
+ |---|---|
179
+ | `Enter` | Send the prompt |
180
+ | `Esc` | **Double-press to stop** — single press arms (500ms window); popup open → closes popup instead; empty editor → second press opens `/session` |
181
+ | `Ctrl+C` | Mid-turn: first press cancels turn, second quits. Idle: clears editor / quits. **Held-key auto-repeat never quits.** |
182
+ | `Ctrl+D` | Quit (only when editor is empty) |
183
+ | `Ctrl+L` | Open model/think picker |
184
+ | `Ctrl+G` | Open subagent picker (while children are running) |
185
+ | `Tab` | Autocomplete |
186
+ | `↑` / `↓` | Browse submitted-message history (shell-style, 500 entries) |
187
+
188
+ ### Custom keybindings
189
+
190
+ Remap any app key through `~/.dsh/keybindings.json` a partial JSON map of app keys to key ids (`ctrl+letter`, `alt+letter`, named keys). Edit by hand or use `/hotkeys` to change interactively (live-applied, no restart).
191
+
192
+ ---
193
+
194
+ ## Themes
195
+
196
+ GitHub light / GitHub dark palettes, hot-switchable at runtime:
197
+
198
+ - `/theme` — live picker; the whole screen repaints including background.
199
+ - `DSH_TUI_THEME=light|dark` — env pin that wins over preference.
200
+ - `DSH_TUI_TRANSPARENT=1` see-through canvas (terminal background shows through).
201
+ - `auto` mode detects the terminal and follows live light/dark switches.
202
+
203
+ ---
166
204
 
167
205
  ## Install (local)
168
206
 
169
207
  ```sh
170
- # build + pack + install into the profile in one step (the recommended dev loop)
171
- node scripts/dev-install.mjs # = pnpm build → pnpm pack → refresh the profile's copies
208
+ # build + pack + install into the profile in one step
209
+ node scripts/dev-install.mjs # pnpm build → pnpm pack → refresh profile copies
172
210
 
173
- # or, manually, like a real user would:
174
- pnpm pack # → aiwayds-dsh-tui-pi-0.2.0.tgz
175
- dsh plugin --profile tui add /path/to/aiwayds-dsh-tui-pi-0.2.0.tgz
211
+ # or manually:
212
+ pnpm pack # → aiwayds-dsh-tui-pi-<version>.tgz
213
+ dsh plugin --profile tui add /path/to/aiwayds-dsh-tui-pi-<version>.tgz
176
214
  ```
177
215
 
178
- The profile's `package.json` carries **both** keys pointing at the tarball
216
+ The profile's `package.json` carries two keys pointing at the tarball:
179
217
  `dsh-tui-pi` (dsh resolves the bundle by this name) and
180
- `@aiwayds/dsh-tui-pi` (the loader entry in `cordis.patch.yml` imports this
181
- name) — and its `pnpm-workspace.yaml` declares the pi-tui
182
- `patchedDependencies` (see `~/.dsh/profiles/tui/`). At runtime
183
- `@deepseek-ai/*` resolve through dsh's shared
184
- `~/.dsh/profiles/node_modules` fallback to the installed closure, so the
185
- profile never needs the repo's `node_modules`. Known quirk: pnpm does not
186
- re-read a changed `file:` tarball while its `node_modules` entry exists —
187
- `scripts/dev-install.mjs` removes the two installed copies before installing,
188
- which forces the refresh.
218
+ `@aiwayds/dsh-tui-pi` (the loader entry in `cordis.patch.yml`).
189
219
 
190
- ## Use
220
+ ## Install (npm)
221
+
222
+ Install the full dsh plugin suite into a fresh profile:
191
223
 
192
224
  ```sh
193
- dsh --profile tui # or: dsh-tui-pi (bin shim)
225
+ dsh plugin --profile tui add @aiwayds/dsh-tui-pi
226
+ dsh plugin --profile tui add @aiwayds/dsh-subagent-registry
227
+ dsh plugin --profile tui add @aiwayds/dsh-dcp
194
228
  ```
195
229
 
196
- - Type a prompt → Enter. Streaming reply renders live; tool calls render as
197
- `⚙/✔/✘` cards.
198
- - Todos the model spawns show in a bordered panel pinned **above the chat
199
- input** (never scrolls with the transcript): a `● Todos (done/total)` tree.
200
- Subagent children render as **compact lines in the last-request area below
201
- the editor** (` ↳ <last request>` then one line per running child) — ` ↳ `
202
- prefix, spinner + agent **name** first, retries (`↻N≤M`), tokens
203
- (+ context percent), elapsed (no box, no `● Agents` header, no provider). A
204
- finished child drops off; when nothing is left the panel and the activity
205
- lines collapse away.
206
- - `/` opens slash-command autocomplete (Tab/arrows/Enter).
230
+ Then launch:
207
231
 
208
- ## Keyboard shortcuts
232
+ ```sh
233
+ dsh --profile tui
234
+ ```
209
235
 
210
- App-level keys (key mappings mirror [pi](https://github.com/badlogic/pi-mono)):
236
+ **What happens automatically:**
211
237
 
212
- | Key | Action |
213
- | --- | --- |
214
- | `Enter` | send the prompt |
215
- | `Esc` | **stop the current task — as a deliberate double-press** priority chain: a popup that is open closes itself first (Esc inside a popup *never* stops the running task); the editor's autocomplete closes; a mid-turn agent waits for a second `Esc` within 500ms to cancel the whole task (parent + subagents, `⏹ canceling current turn…`; the first press only arms the window and shows a hint); a non-empty editor does **nothing** (anti-misfire); on an **empty** editor a second `Esc` within 500ms opens `/session` |
216
- | `Ctrl+C` | mid-turn: first press cancels the running turn, second press (within 500ms) quits; idle: first press clears the editor, second press quits. With a popup open it cancels the popup instead. **Held-key auto-repeat never quits** — repeats under 80ms apart are swallowed, and the double-press quit is confirmed for 200ms (a follow-up repeat aborts it, a human-speed re-press fires it immediately) |
217
- | `Ctrl+D` | quit — only when the editor is **empty**, like pi's `app.exit`; with text it is the regular delete-character-forward |
218
- | `Ctrl+L` | open the model/think picker (pi's `app.model.select`) |
219
- | `Ctrl+G` | open the subagent picker while subagents run (see `/subagents`); idle the key falls through untouched. dsh's own mapping — pi spends this key on an external editor we don't have, remap in `keybindings.json` if you miss it |
220
- | `Tab` | autocomplete |
221
-
222
- Editor keys (movement/deletion/undo) come from the pi-tui `Editor` default
223
- bindings — no dsh code involved: `←→` / `Ctrl+B`/`Ctrl+F` move, `Alt+←→` /
224
- `Ctrl+←→` / `Alt+B`/`Alt+F` word-move, `Home`/`End` / `Ctrl+A`/`Ctrl+E` line
225
- edges, `PageUp`/`PageDown` scroll, `Backspace` / `Delete`/`Ctrl+D` delete,
226
- `Ctrl+W`/`Alt+Backspace` delete word back, `Alt+D` delete word forward,
227
- `Ctrl+U`/`Ctrl+K` delete to line start/end, `Ctrl+-` undo, `Ctrl+Y`/`Alt+Y`
228
- yank, `Shift+Enter`/`Ctrl+J` newline. `↑` / `↓` follow this interplay: on the
229
- first line `↑` moves the cursor to the line start, whereas on an **empty**
230
- editor (or with the cursor at the line start) it browses history instead (next
231
- paragraph).
232
-
233
- `↑` / `↓` browse the submitted-message history shell-style. `↑` recalls the
234
- most recent prompt and walks further back; `↓` moves forward again, and past
235
- the newest entry it restores your in-progress draft. Browsing starts when the
236
- cursor is at the start of the first line — including on an empty editor — and
237
- the text you were leaving is preserved as the draft, so `↓` all the way back
238
- hands you the draft again. The history holds up to **500 entries**, dropping
239
- the oldest ones beyond that; it survives a theme hot-swap (the editor is
240
- rebuilt on switch). The history is in-memory for the current TUI run — it does
241
- **not** survive `/reload` or a restart. Submitted slash commands (`/theme`,
242
- `/hotkeys`, …) are recorded too. Recalling a multi-line entry places the
243
- cursor at its start; `↓` then walks the entry's lines before advancing the
244
- history — but a single-line entry round-trips identically (do not fight the
245
- pi-tui behavior).
246
-
247
- Not supported yet (documented status): `Ctrl+O` collapse tool output, `Ctrl+X`
248
- copy the last assistant message, `Alt+Enter` follow-up queue, `Ctrl+V` paste
249
- image, `Ctrl+Z` suspend, `Ctrl+P`/`Ctrl+Shift+P` model cycle, `Shift+Tab` think
250
- cycle, `Ctrl+T` collapse thinking.
251
-
252
- ### Subagent viewer & fine-grained control
253
-
254
- `Ctrl+G` (or `/subagents`) opens an 80% picker over the tracked children —
255
- running ones first (spinner, mode, rounds against `maxRounds`, tokens,
256
- elapsed), then the five most recently settled. Enter opens the transcript
257
- viewer: one readable line per buffered child event (user/assistant messages,
258
- tool calls paired with truncated results, turns, todos), refreshing ~3x/s
259
- with tail-follow (scroll up to detach, reach the bottom to re-attach), and a
260
- truncation note when the per-child 2000-event ring buffer dropped its head.
261
- `Esc` closes; a deliberate double-`x` within 500ms closes too.
262
-
263
- Two caps steer delegation (configure in `/agents` → `l`, both live-read at
264
- every decision):
265
-
266
- - **`maxAgents`** (default 4, `0` = unlimited) — a `tools.guard` denies
267
- model-facing spawn tools (`subagent`, `subagent_fork`, `workflow`, `ralph`,
268
- `use_agent`) once that many children run, with the running labels in the
269
- deny reason so the model can wait or `list_agents`. The cap is approximate
270
- under a burst of parallel spawns; workflow fan-out (which bypasses the tool
271
- pipeline) is pruned after the fact on `subagent/start`.
272
- - **`maxRounds`** (default 50, `0` = unlimited) — when a child's completed
273
- turns reach the cap, the TUI queues one wrap-up request
274
- ("总结和结束这个任务,汇报情况。") as its next turn — it never interrupts
275
- work underway, never repeats per child, and never re-awakens a child that
276
- already settled. There is deliberately **no force stop**.
238
+ - dsh registers all three plugins in `dsh.profile.bundles` (via `reconcilePlugins`).
239
+ - dsh sets `autoInstallPeers: false` in the profile's `pnpm-workspace.yaml`.
240
+ - On first boot, dsh calls `healProfilesModuleFallback` to create symlinks
241
+ under `~/.dsh/profiles/node_modules/@deepseek-ai/*` the global dsh
242
+ closure (`$(which dsh)/../../node_modules/@deepseek-ai`). This gives all
243
+ plugins a single `@deepseek-ai/cordis` instance no manual closure setup
244
+ is needed.
245
+ - `compaction-basic` is disabled by `@aiwayds/dsh-dcp`'s patch; dsh-dcp
246
+ takes over as the compaction backend.
277
247
 
278
- ### Custom keybindings
248
+ **What does NOT happen automatically:**
249
+
250
+ - The pi-tui patchedDependencies (editor autocomplete framing, SelectList
251
+ full-row backdrop) are **not** applied for npm consumers — they require
252
+ `pnpm-workspace.yaml` entries that dsh-tui-pi cannot inject into a
253
+ consumer's profile. This is **cosmetic only**: the TUI boots and works
254
+ without the patch; the unpatched select-list just renders unselected rows
255
+ as plain `prefix + value` instead of a full-row backdrop.
256
+
257
+ ### Troubleshooting
279
258
 
280
- The five app-level keys are remappable through `$DSH_HOME/keybindings.json`
281
- (`~/.dsh/keybindings.json` by default) — pi's
282
- `~/.pi/agent/keybindings.json` convention. The file is a **partial** map of
283
- the app keys to pi-tui key ids; anything missing keeps its default. Key id
284
- format: `modifier+key`, modifiers `ctrl`/`shift`/`alt`/`super` (combined with
285
- `+`), key a letter/digit/symbol or a named key (`escape`, `enter`, `tab`,
286
- `space`, `backspace`, `delete`, `home`, `end`, `pageUp`, `pageDown`, arrows,
287
- `f1`–`f24` …).
288
-
289
- ```json
290
- {
291
- "escape": "ctrl+x",
292
- "ctrlC": "alt+c",
293
- "ctrlD": "ctrl+w",
294
- "modelPicker": "ctrl+m"
295
- }
259
+ | Symptom | Cause | Fix |
260
+ |---|---|---|
261
+ | `Cannot find package '<name>' imported from ~/.dsh/profiles/...` | A bundle's `cordis.patch.yml` `name` field doesn't match the scoped package name. | Update the plugin; all `@aiwayds/*` plugins now use `name: '@aiwayds/<pkg>'` in their patch. |
262
+ | `Cannot read properties of undefined (reading 'prepare')` | Duplicate `@deepseek-ai/cordis` module instances (two physical copies in the profile tree). | See iron rule 8 in AGENTS.md. Delete physical `~/.dsh/profiles/tui/node_modules/@deepseek-ai` copies and let dsh heal the fallback: `rm -rf ~/.dsh/profiles/tui/node_modules/@deepseek-ai && dsh --profile tui` (the heal recreates them as symlinks). |
263
+ | pnpm `Peer dependencies that should be installed: @deepseek-ai/...` warning | A plugin declares `@deepseek-ai/*` as regular `dependencies` instead of `peerDependencies`. | Update the plugin (all `@aiwayds/*` dsh plugins use optional peerDeps). The warning is harmless — pnpm doesn't auto-install optional peers. |
264
+ | pnpm `Ignored build scripts: @aiwayds/dsh-tui-pi@...` warning | pnpm 10 blocks build scripts by default; the tui-pi postinstall (`link-dsh-closure.mjs`) was skipped. | This is expected and **harmless** — the postinstall only matters for the repo dev flow, not npm consumers. dsh handles closure linking via `healProfilesModuleFallback`. |
265
+
266
+ ---
267
+
268
+ ## Use
269
+
270
+ ```sh
271
+ dsh --profile tui # or: dsh-tui-pi (bin shim)
296
272
  ```
297
273
 
298
- The file is read when the TUI starts. You can edit it by hand (then
299
- `/reload`) — or use `/hotkeys`, which shows the effective table in the same
300
- select-panel style as `/agents`: each app key is a row (custom overrides
301
- starred), `Enter` prompts for a new key id (empty input resets the key to its
302
- default), and a commit **writes the file and applies the change live** — no
303
- `/reload` needed. Invalid entries never block: they warn and keep the default.
304
-
305
- ## Performance rules (from the pi-turbo findings)
306
-
307
- pi's TUI lags in long sessions because its footer re-scans the whole session
308
- log on every render (O(n)) and a 1s clock tick recomputes everything.
309
- dsh-tui-pi avoids both by construction:
310
-
311
- - **Event-driven incremental state**: `session/event` listeners maintain an
312
- append-only transcript model + running counters (tokens, messages, tools,
313
- cache-hit rate). Render never re-scans the dsh session log.
314
- - **Footer reads O(1) maintained values** — never derived in `render()`.
315
- - **Clock tick only re-renders the footer line**; transcript components cache.
316
- - **Live widgets tick at 100 ms** (`AGENT_TICK_MS`, unref'd timer): a tick
317
- only re-setTexts the widget's single Text (O(agents)) and is a no-op while
318
- no child runs — never a transcript re-scan.
319
- - **Streaming strategy**: deltas accumulate in a plain Text via `setText` on
320
- the same component (never remove+re-add per token); markdown renders once on
321
- the assembled `assistant/message` (no per-token markdown parsing).
322
- - **Fixed think/tool status panels** (pinned above the chat input, like the
323
- Todos panel): think/tool activity never creates transcript blocks — one
324
- ThinkPanel and one ToolPanel exist for the whole run, every event refreshes
325
- the same panel in place, and a panel with no content renders zero rows
326
- (hidden). Delegation spawn tools (`use_agent`, `subagent`, `workflow`,
327
- `ralph`) never open a tool block — their children show in the running-agent
328
- lines below the editor. The bottom running-agent line shows the child's
329
- latest CONTENT line (live-refreshed assistant text/reasoning, never a tool
330
- name), truncated at the right edge without wrapping.
331
- - **Configurable panel height** (`dsh-tui.panelHeight`, default `'1'`):
332
- `'1'` renders one borderless row — block identifier + elapsed time + the
333
- last content line, right-truncated, never wrapped; `'5'/'7'/'10'` box the
334
- panel (top border + header row + body rows + bottom border); `'all'` prints
335
- the full body, with bounded on-screen content — a streaming reasoning panel
336
- boxes a 200-line live tail while chunks are in flight and a settled tool
337
- result keeps at most 2000 lines (a `… (+N lines)` marker reports the drop).
338
- No inner scroll — pi-tui 0.84.2 never lays out nested components, so a
339
- nested ScrollView cannot obtain a viewport. Body lines are clipped to one
340
- physical row *before* styling, so long output can never wrap the panel past
341
- its configured rows.
342
- - **Width safety**: every truncation goes through `clipToWidth` (src/text.ts)
343
- — CJK full-width characters count 2 columns and graphemes are never split.
344
- Bare `String.length` clipping is banned.
274
+ ---
345
275
 
346
276
  ## Dev
347
277
 
@@ -353,119 +283,39 @@ pnpm test # unit tests, node --test against lib/ (296 tests, pretest builds)
353
283
 
354
284
  Local type-checking symlinks `node_modules/@deepseek-ai/*` to the installed
355
285
  dsh closure (`/opt/homebrew/lib/node_modules/@deepseek-ai/dsh/node_modules`);
356
- at runtime those imports resolve to the same module instances the running dsh
357
- uses. Those symlinks stay out of any tarball (`files` ships lib/bin/patch only).
358
-
359
- `scripts/link-dsh-closure.mjs` (the package's `postinstall`) re-creates every
360
- `node_modules/@deepseek-ai/*` link to the global dsh closure after **each**
361
- `pnpm install`/`patch-commit`, and is a no-op without a global dsh install.
362
- That keeps a single `@deepseek-ai/cordis` module identity in the type graph
363
- (declaring any of them in `package.json` would make pnpm install a second
364
- local copy and break the cordis `settings` augmentation). A wiped
365
- `node_modules` self-heals on the next `pnpm install`.
366
-
367
- **pi-tui patch**: this plugin applies a small patch to the pinned
368
- `@earendil-works/pi-tui` 0.84.2 (`pnpm.patchedDependencies` in
369
- `pnpm-workspace.yaml`, patch in `patches/`): it adds an `unselectedText`
370
- SelectListTheme hook (full-row background on unselected rows), wires the
371
- previously dead `selectedPrefix` hook (accent arrow on the selected row), and
372
- frames the editor's slash-autocomplete list in a `│` box. The patch travels in
373
- the tarball (`files` includes `patches` + `pnpm-workspace.yaml`); the
374
- link:-mounted dev workflow uses the already-patched copy in this repo's
375
- `node_modules`.
286
+ those symlinks stay out of any tarball. `scripts/link-dsh-closure.mjs` (the
287
+ package's `postinstall`) re-creates every link after each `pnpm install`.
288
+
289
+ **pi-tui patch**: a small patch to `@earendil-works/pi-tui` 0.84.2
290
+ (`pnpm-workspace.yaml`, patch in `patches/`) adds `unselectedText` and
291
+ `selectedPrefix` SelectListTheme hooks plus the editor autocomplete box frame.
292
+
293
+ ---
376
294
 
377
295
  ## Layout
378
296
 
379
297
  ```
380
- bin/dsh-tui-pi launcher shim (exec dsh --profile tui)
381
- cordis.patch.yml bundle patch: mounts the plugin as `tui-pi`
298
+ bin/dsh-tui-pi launcher shim (exec dsh --profile tui)
299
+ cordis.patch.yml bundle patch: mounts the plugin as `tui-pi`
382
300
  src/
383
- index.ts cordis plugin entry + wiring: command registration, footer,
384
- git watcher, clock, bridge, theme hot-swap sink, shutdown
385
- tui.ts TUI bootstrap: alt-screen tree, transcript ScrollView,
386
- dock (status/editor/last-request/footer), editor rebuild,
387
- app-owned canvas background (patched pi-tui)
388
- session.ts DshSessionBridge: lazy agent create, followup, resume,
389
- replay, cancel, O(1) incremental stats, persistDefaultModel,
390
- subagent tracker (tool-workflow + child events → live rows)
391
- dsh-events.ts local types + guards for tool-workflow/subagent/llm-retry
392
- events (declaring packages not installed) + AgentView
393
- live-widgets.ts LiveWidgets: Todos boxed above the input + running-agent
394
- activity merged under the last-request line
395
- (renderTodos/renderAgents/setLastRequest/tickLive/setTheme)
396
- commands.ts CommandService: slash autocomplete + dual-channel dispatch
397
- (registerLocal agentless direct / ctx.commands host path)
398
- messages.ts TranscriptRenderer: session events → pi-tui components;
399
- streaming setText, height-configurable panels, ReplayOp
400
- buffer for theme-switch rebuilds
401
- footer.ts PowerlineFooter (ported segment palette, 7 segments + clock)
402
- editor.ts CwdBorderEditor (top border: 📁 cwd │ ⎇ branch)
403
- git.ts GitBranchWatcher (polled, cached)
404
- frame.ts FramedOverlay: shared top/bottom ─ border for every popup
405
- panels.ts select-panel framework: TablePanel/FieldPanel/ViewerPanel/
406
- PanelHost + padCell/columnWidths/ListController
407
- keymap.ts pure key-action decision (resolveKeyAction) — Esc/Ctrl+C/
408
- Ctrl+D/Ctrl+L/Ctrl+G chains with double-press guards
409
- hotkeys.ts keybindings.json contract + validation + /hotkeys manager
410
- agent-manager.ts agent markdown files: parse/validate/write-back engine +
411
- ~/.zcode/agents seeding, `deep` policy
412
- agents.ts /agents table + fields window + subagent limits panel
413
- subagent-policy.ts maxAgents guard + maxRounds wrap-up injection (read live)
414
- subagent-viewer.ts Ctrl+G picker + live transcript panel (300 ms tick)
415
- provider-catalog.ts built-in provider directory (36 llm-pi-ai catalog
416
- routes, mirrors the web Models page) + deriveKeyRef + row
417
- views (pure data/functions for the Models add-provider flow)
418
- reload.ts /reload hot-reload (cordis-plugin-hmr style partial reload)
419
- text.ts clipToWidth / visibleWidth (grapheme-safe column clipping)
420
- theme-settings.ts dsh-tui settings namespace (applies: 'live') + watch sink
421
- + preference read/write with conflict retry
422
- selectors.ts /model (two-stage), /think, /theme and /permission picker
423
- overlays
424
- permission.ts permission display names (web-client conventions) + picker
425
- option assembly (pure, unit-tested)
426
- sessions.ts /session info panel + /resume persisted-session picker
427
- settings.ts /settings browser: categories, schema walk, inline editors,
428
- serialized mutate write chain, add-provider flow
429
- welcome.ts startup whale banner (WHALE_ART + PIXEL_FONT glyphs)
430
- quotes.ts startup quote pool
431
- append-system.ts APPEND_SYSTEM.md support + todo-lifecycle section
432
- maintenance (idempotent, atomic)
433
- theme/
434
- palette.ts GitHub light/dark palettes + terminal-background detection
435
- (rgbIsLight luminance)
436
- index.ts buildTheme: Editor/Markdown/SelectList/chat roles, POWERLINE
437
- segment palette, resolveTheme (env > preference > detect)
438
- test/*.test.mjs unit tests, node --test against lib/ (296 across 24 files)
301
+ index.ts cordis plugin entry: command registration, footer,
302
+ git watcher, clock, bridge, theme hot-swap, shutdown
303
+ tui.ts alt-screen tree, transcript ScrollView, dock, canvas bg
304
+ session.ts DshSessionBridge: agent create, followup, resume,
305
+ O(1) incremental stats, subagent tracker
306
+ live-widgets.ts Todos panel + running-agent activity lines
307
+ messages.ts TranscriptRenderer: session events pi-tui components,
308
+ streaming setText, height-configurable panels
309
+ footer.ts PowerlineFooter (7 segments + clock)
310
+ editor.ts CwdBorderEditor (top border: cwd + git branch)
311
+ subagent-policy.ts maxAgents guard + maxRounds wrap-up injection
312
+ subagent-viewer.ts Ctrl+G picker + live transcript panel
313
+ theme/ GitHub light/dark palettes + terminal detection
314
+ test/*.test.mjs unit tests (296 across 24 files)
439
315
  ```
440
316
 
317
+ ---
318
+
441
319
  ## Changelog
442
320
 
443
321
  See [CHANGELOG.md](CHANGELOG.md) for the release history.
444
-
445
- ## Status (0.2.0)
446
-
447
- All surface commands shipped and tmux-e2e verified: `/model /think /session
448
- /resume /new /settings /export /theme /reload /agents /subagents /hotkeys`;
449
- provider-first Models with the add-provider flow; overlay chrome
450
- (backgrounds + borders); theme hot-switch (immediate apply, external-change
451
- watch, env pinning) with an app-owned canvas background that recolors the
452
- whole screen; terminal-following `auto` theme; subagent viewer with live
453
- rounds/tokens/elapsed; subagent `maxAgents`/`maxRounds` limits; pi-aligned
454
- keybindings with double-press guards; live todos + subagent progress blocks;
455
- clean Ctrl+C exit. `pnpm check` clean, 296 unit tests green, e2e run
456
- confirmed the settings/credentials files are restored byte-for-byte.
457
-
458
- Known limitations (accepted, pi-tui 0.84.2 constraints):
459
-
460
- - **SelectList unselected rows and the SettingsList search row have no
461
- background**: unselected rows render as raw `prefix + value` and the search
462
- input row is pushed without a theme hook — the popup backdrop is striped
463
- rather than one solid surface.
464
- - **On a 24-row terminal a popup can reach the dock rows**: overlays float
465
- over the status/editor area (the frame adds 4 rows on top of the list cap),
466
- so the popup bottom border and the status line share screen rows while a
467
- popup is open. Cosmetic — popups are modal.
468
- - **An overlay open at switch time does not follow a theme hot-switch**: the
469
- transcript, dock and editor repaint immediately, but an open popup keeps the
470
- palette it was built with until it closes (its submenus inherit the same
471
- stale bundle).