@aiwayds/dsh-tui-pi 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +13 -10
  2. package/README.zh.md +7 -4
  3. package/lib/append-system.d.ts +12 -3
  4. package/lib/append-system.js +33 -7
  5. package/lib/append-system.js.map +1 -1
  6. package/lib/ask-user.d.ts +79 -61
  7. package/lib/ask-user.js +117 -112
  8. package/lib/ask-user.js.map +1 -1
  9. package/lib/custom-provider.d.ts +119 -0
  10. package/lib/custom-provider.js +243 -0
  11. package/lib/custom-provider.js.map +1 -0
  12. package/lib/dsh-events.d.ts +15 -0
  13. package/lib/dsh-events.js +12 -0
  14. package/lib/dsh-events.js.map +1 -1
  15. package/lib/index.js +45 -17
  16. package/lib/index.js.map +1 -1
  17. package/lib/live-widgets.js +4 -0
  18. package/lib/live-widgets.js.map +1 -1
  19. package/lib/login.js +32 -2
  20. package/lib/login.js.map +1 -1
  21. package/lib/messages.d.ts +5 -3
  22. package/lib/messages.js +7 -2
  23. package/lib/messages.js.map +1 -1
  24. package/lib/provider-catalog.d.ts +7 -0
  25. package/lib/provider-catalog.js +1 -0
  26. package/lib/provider-catalog.js.map +1 -1
  27. package/lib/queue-panel.d.ts +19 -0
  28. package/lib/queue-panel.js +27 -1
  29. package/lib/queue-panel.js.map +1 -1
  30. package/lib/session.js +12 -3
  31. package/lib/session.js.map +1 -1
  32. package/lib/sessions.d.ts +16 -0
  33. package/lib/sessions.js +94 -9
  34. package/lib/sessions.js.map +1 -1
  35. package/lib/settings.d.ts +9 -0
  36. package/lib/settings.js +12 -1
  37. package/lib/settings.js.map +1 -1
  38. package/lib/subagent-policy.d.ts +14 -4
  39. package/lib/subagent-policy.js +40 -14
  40. package/lib/subagent-policy.js.map +1 -1
  41. package/lib/subagent-viewer.js +14 -2
  42. package/lib/subagent-viewer.js.map +1 -1
  43. package/lib/tui.d.ts +14 -0
  44. package/lib/tui.js +11 -2
  45. package/lib/tui.js.map +1 -1
  46. package/package.json +1 -1
  47. package/templates/APPEND_SYSTEM.md +2 -0
package/README.md CHANGED
@@ -116,7 +116,7 @@ Running subagent activity is shown in the **last-request area below the editor**
116
116
  ↳ ⠼ Subagent B 10s 任务 · 562 token · 6.0s
117
117
  ```
118
118
 
119
- Each line shows: spinner + agent **name**, retries (`↻N≤M`), compact **current-context usage** (`X/Y` — the child's latest request's billed input+output plus a CJK estimate of messages after it, over its context window; NOT the cumulative token spend, which only grows), rounds (`round N/M` — the assistant-message count against the cap, `M` only when `maxRounds > 0`), elapsed. No provider shown, no box, no header — just one line per running child.
119
+ Each line shows: spinner + agent **name**, retries (`↻N≤M`), compact **current-context usage** (`X/Y` — the child's latest request's billed input+output plus a CJK estimate of messages after it, over its context window; NOT the cumulative token spend, which only grows), rounds (`round N/M` — the assistant-message count against the cap, `M` only when `maxRounds > 0`), elapsed, and a `⚡` marker when a policy injection (maxRounds wrap-up, steer) reached the child. No provider shown, no box, no header — just one line per running child.
120
120
 
121
121
  Both **spawn-driven** and **fork-driven** children are tracked — dsh creates both through `childSessionMeta`, which writes `origin: 'subagent'` + a `delegationDepth` budget together, so header discovery recognises either marker (a budget-without-origin header is admitted as a defensive fallback and labelled `fork <id8>`; current dsh does not produce that shape). Non-children stay off the board by **value**, not by field presence: the jsonl persistence backend materialises `delegationDepth: 0` on every restored header, so the gate requires a budget `> 0`. User-facing session forks (a forked *conversation*: `Session.fork` sets `parentSession` + `seedLength`, no budget) are deliberately kept off the subagent board and stay resumable via `/resume` — whose filter (`isResumableSessionHeader`) excludes exactly the delegated children (`origin: 'subagent'` or budget > 0).
122
122
 
@@ -143,7 +143,7 @@ Icons: `☑` completed, `◐` in-progress, `☐` pending. A settled child drops
143
143
  Two caps (`/agents` → `l` to configure):
144
144
 
145
145
  - **`maxAgents`** (default 4, `0` = unlimited) — spawns are denied when the cap is hit.
146
- - **`maxRounds`** (default 75, `0` = unlimited) — after a child's assistant messages (one per LLM round-trip — the "rounds") reach the cap, the TUI queues one wrap-up request and never force-stops.
146
+ - **`maxRounds`** (default 75, `0` = unlimited) — after a child's assistant messages (one per LLM round-trip — the "rounds") reach the cap, the TUI injects one wrap-up directive and never force-stops: a running child receives it at its next step boundary (`steer` — the very next LLM round-trip), an idle child as its own next turn. The injection is visible: the compact line, the Ctrl+G picker row and the viewer header show a `⚡` marker, and the transcript renders the injected message as `⚡ <text>` — so a wrap-up the child LLM ignored can be told apart from one that never fired.
147
147
 
148
148
  ---
149
149
 
@@ -168,7 +168,7 @@ Inside a subagent, a committed compaction is visible too: DCP appends one `user/
168
168
  A user-editable markdown file whose content is appended to the **system prompt of the main agent this TUI creates** — borrows pi's `~/.pi/agent/APPEND_SYSTEM.md` convention, dsh side: `$DSH_HOME/APPEND_SYSTEM.md` (default `~/.dsh/APPEND_SYSTEM.md`, honors the same `$DSH_HOME` override as the rest of dsh).
169
169
 
170
170
  - **Hot-applied** — the section provider reads the file at every prompt assembly, so editing the file picks up on the **next request**: no restart, no watcher, no `/reload`.
171
- - **Auto-seeded on first run** — when the file is missing, the TUI seeds it once at startup from the shipped template `templates/APPEND_SYSTEM.md` (the English orchestrator-identity template: identity, core rules, execution workflow). An existing file is yours — the TUI never overwrites user content.
171
+ - **Auto-seeded on first run** — when the file is missing, the TUI seeds it once at startup from the shipped template `templates/APPEND_SYSTEM.md` (the English orchestrator-identity template: identity, core rules, execution workflow — including the registered-subagents vocabulary rule: "subagent" means the registered subagents only). An existing file is yours — the TUI never overwrites user content; it only appends the marked todo-lifecycle section and (idempotently, by phrase match) the subagents rule when a file does not phrase them yet.
172
172
  - **TUI-owned section** — a marked block (`<!-- dsh-tui-pi:todo-lifecycle -->`) is appended once and then maintained idempotently so the model clears its `todo/write` list when every item is done. A marked file is left byte-identical on later startups.
173
173
  - **Legacy migration** — the same todo block used to be delivered through `~/.dsh/AGENTS.md`. On startup the TUI strips that block once (no-op when absent), so the guidance is never duplicated.
174
174
  - **Empty / unreadable = no section** — if the file is missing or can't be read, the section is silently dropped. No error, no TUI startup failure.
@@ -199,12 +199,12 @@ There's no slash command to toggle the feature — it's always on, controlled by
199
199
 
200
200
  ### Ask User Question
201
201
 
202
- While the model is mid-turn it can pause and ask you structured questions via the `ask_user_question` tool (`@deepseek-ai/dsh-tool-ask-user`, mounted by this profile's bundle patch). The TUI hosts the answering side: a framed overlay opens in place, the tool call stays pending until you answer, and your answers flow back to the model as a normal tool result.
202
+ While the model is mid-turn it can pause and ask you structured questions via the `ask_user_question` tool (`@deepseek-ai/dsh-tool-ask-user`, mounted by this profile's bundle patch). The TUI hosts the answering side: a bordered panel pins itself directly above the chat input (the Todos-panel slot — no floating popup), takes the keyboard while open, the tool call stays pending until you answer, and your answers flow back to the model as a normal tool result.
203
203
 
204
- - **One overlay, all questions flattened** — every question renders as a header row followed by its supporting `detail` text (when the model supplies any), option rows, plus a `Type something.` sentinel row for free text. Single-select replaces on Enter; multi-select toggles (`[+]` marks).
204
+ - **One panel, all questions flattened** — every question renders as a header row followed by its supporting `detail` text (when the model supplies any), option rows, plus a `Type something.` sentinel row for free text. Single-select replaces on Enter; multi-select toggles (`[+]` marks).
205
205
  - **Single-question fast path** — a lone single-select question submits immediately on Enter: picking an option or committing typed free text both submit right away (a question without options is answered by typing alone). A lone multiSelect question instead gets a `⏎ Confirm answers` row so you can pick several options before submitting.
206
206
  - **Multi-question review page** — with ≥ 2 questions a `⏎ Confirm answers` row hops to a review listing every answer, each row editable in place; `Submit answers` commits (Enter on it while an answer is missing flashes a hint instead of failing silently). After committing free text on one question the cursor hops to the next unanswered one.
207
- - **Double-Esc declines** — two Esc presses within 200 ms return a declined envelope (the model reads it as a normal reply that no answer was given); holding Esc does not accidentally fire (key auto-repeat below a minimum gap is ignored), and closing the overlay through any other path theme swap, `/reload`, or the tool call being aborted settles as declined too.
207
+ - **Double-Esc declines** — two Esc presses within 200 ms return a declined envelope (the model reads it as a normal reply that no answer was given); holding Esc does not accidentally fire (key auto-repeat below a minimum gap is ignored), and the tool call being aborted settles as declined too. While the panel is open it owns the keyboard exactly like an open overlay: Esc never arms the running-task stop, and app keys (Ctrl+L/G/O, Tab) yield to the panel.
208
208
  - **Conservative-use guidance** — a system-prompt section nudges the model to ask only when it genuinely needs you (1–3 questions, 2–4 options each), so the TUI doesn't turn into a questionnaire.
209
209
  - **Keyboard** — `↑↓` navigate · `Enter` select/toggle/confirm · type into the sentinel for free text · `Esc` twice to decline.
210
210
 
@@ -219,7 +219,7 @@ Inspired by [juicesharp/rpiv-ask-user-question](https://github.com/juicesharp/rp
219
219
  | `/model` | Two-stage provider/model picker (then thinking level). Live switch, persisted; in-panel keys: `f` favorite · `h` hide · `/` filter (favorites/hidden persisted via settings). |
220
220
  | `/think` | Reasoning-effort picker for the current model (`Off`/`High`/`Max`). |
221
221
  | `/session` | Read-only info panel: id, cwd, model, token usage, event count. |
222
- | `/resume` | Pick a persisted session, validate its log, then restore it. |
222
+ | `/resume` | Pick a persisted session, validate its log, then restore it. Ordered by last update (log-file mtime), newest first; the `Updated` column shows the effective time. |
223
223
  | `/new` | Detach the current session; the next prompt opens a fresh one. |
224
224
  | `/settings` | Text-based settings browser (namespaces, schema walk, inline editors, secrets masked). |
225
225
  | `/export` | Write the current session log as JSONL (`~/Downloads/dsh-session-<id>.jsonl`). |
@@ -229,6 +229,8 @@ Inspired by [juicesharp/rpiv-ask-user-question](https://github.com/juicesharp/rp
229
229
  | `/agents` | Manage agent markdown files + subagent limits (`maxAgents`, `maxRounds`). |
230
230
  | `/subagents` | Pick a running/recent subagent and watch its live transcript; `Enter` inside the viewer steers the child (see Subagents). |
231
231
  | `/reload` | Hot-reload the plugin from source (after `pnpm build`) without restarting dsh. |
232
+ | `/login` | Log in to a provider: pick from the directory (or `/login openai` to jump), enter one API key. The **Custom provider…** entry (`/login custom`) opens a six-field form for any OpenAI/Anthropic-compatible gateway pi-ai does not ship — route id, display name, protocol, base URL, model list, API key — and writes the same hand-declared route the web Models page composes. |
233
+ | `/logout` | Pick a logged-in provider and remove both the stored key and its provider profile. |
232
234
  | `/hotkeys` | Keybinding browser and live editor. |
233
235
 
234
236
  Anything that is not a resolvable command falls through to the model as an ordinary prompt.
@@ -390,7 +392,7 @@ dsh --profile tui # or: dsh-tui-pi (bin shim)
390
392
  ```sh
391
393
  pnpm check # tsc --noEmit
392
394
  pnpm build # emit lib/
393
- pnpm test # unit tests, node --test against lib/ (745 tests, pretest builds)
395
+ pnpm test # unit tests, node --test against lib/ (757 tests, pretest builds)
394
396
  ```
395
397
 
396
398
  Local type-checking symlinks `node_modules/@deepseek-ai/*` to the installed
@@ -421,8 +423,9 @@ src/
421
423
  footer.ts PowerlineFooter (7 segments + clock)
422
424
  editor.ts CwdBorderEditor (top border: cwd + git branch)
423
425
  subagent-policy.ts maxAgents guard + maxRounds wrap-up injection
426
+ (steer-when-running; ⚡-marked, visible in the viewer)
424
427
  subagent-viewer.ts Ctrl+G picker + live transcript panel + Enter steer injection
425
- ask-user.ts Ask User Question overlay: pure state reducers +
428
+ ask-user.ts Ask User Question docked panel: pure state reducers +
426
429
  framed overlay UI + ctx.userQuestions provider
427
430
  steer-flow.ts Steer / follow-up decision layer: routed delivery with
428
431
  race fallback, queue actions (remove / promote), notices
@@ -431,7 +434,7 @@ src/
431
434
  queue-panel.ts Ctrl+O pending-message queue: d remove · s steer now,
432
435
  live-refreshed overlay
433
436
  theme/ GitHub light/dark palettes + terminal detection
434
- test/*.test.mjs unit tests (728 across 44 files)
437
+ test/*.test.mjs unit tests (757 across 44 files)
435
438
  ```
436
439
 
437
440
  ---
package/README.zh.md CHANGED
@@ -113,7 +113,7 @@ dsh ▸ volc-ark-plan ▸ deepseek-v4-flash ▸ high ▸ 48.7k/1.0M(4.6%) ▸
113
113
  ↳ ⠼ Subagent B 10s 任务 · 562 token · 6.0s
114
114
  ```
115
115
 
116
- 每行显示:spinner + 代理**名称**,重试次数(`↻N≤M`),当前上下文占用(`X/Y` —— 子代理最近一次请求的 billed input+output 加上其后消息的 CJK 估算,除以它的上下文窗口;**不是**只增不减的累计 token 消耗),rounds(`round N/M` —— assistant 消息数对上限,`maxRounds > 0` 时才显示 `/M`),耗时。不显示 provider,无边框,无标题。
116
+ 每行显示:spinner + 代理**名称**,重试次数(`↻N≤M`),当前上下文占用(`X/Y` —— 子代理最近一次请求的 billed input+output 加上其后消息的 CJK 估算,除以它的上下文窗口;**不是**只增不减的累计 token 消耗),rounds(`round N/M` —— assistant 消息数对上限,`maxRounds > 0` 时才显示 `/M`),耗时,以及策略注入(maxRounds 收尾、steer)到达后出现的 `⚡` 标记。不显示 provider,无边框,无标题。
117
117
 
118
118
  **spawn 派生**与 **fork 派生**两类子代理都会被追踪——dsh 通过 `childSessionMeta` 同时写入 `origin: 'subagent'` 和 `delegationDepth` 预算,头部识别对两种标记都认得(只有预算没有 origin 的头部作为防御性兜底也会被接纳,标记为 `fork <id8>`;当前 dsh 不会产生这种形态)。非子代理会话按**值**而非字段有无被挡在板外:jsonl 持久化后端在每条恢复的头部上都会物化 `delegationDepth: 0`,所以闸门要求预算严格 `> 0`。面向用户的会话 fork(fork 出的*对话*:`Session.fork` 只设 `parentSession` + `seedLength`,不带预算)刻意不进子代理面板,仍可通过 `/resume` 恢复——`/resume` 的过滤器(`isResumableSessionHeader`)恰好排除被委派的子代理(`origin: 'subagent'` 或预算 > 0)。
119
119
 
@@ -138,7 +138,7 @@ dsh ▸ volc-ark-plan ▸ deepseek-v4-flash ▸ high ▸ 48.7k/1.0M(4.6%) ▸
138
138
  两个限制项(通过 `/agents` → `l` 配置):
139
139
 
140
140
  - **`maxAgents`**(默认 4,`0` = 无限制)—— 超过上限时拒绝新的子代理创建。
141
- - **`maxRounds`**(默认 75,`0` = 无限制)—— 子代理的 assistant 消息数(每次 LLM 往返计 1 round)达到上限后,TUI 排队发送一个收尾请求,从不强制终止。
141
+ - **`maxRounds`**(默认 75,`0` = 无限制)—— 子代理的 assistant 消息数(每次 LLM 往返计 1 round)达到上限后,TUI 注入一条收尾指令,从不强制终止:运行中的子代理在**下一步边界**收到(`steer`,即下一次 LLM 往返),空闲的作为自己的下一个 turn。注入可见:紧凑行、Ctrl+G 选择器行和查看器头部显示 `⚡`,对话记录里注入消息渲染为 `⚡ <文本>`——子代理 LLM 无视收尾指令与注入从未发生,现在可以区分。
142
142
 
143
143
  ---
144
144
 
@@ -163,7 +163,7 @@ dsh plugin --profile tui add @aiwayds/dsh-dcp
163
163
  一份用户可编辑的 markdown 文件,内容会**追加到 TUI 创建的主代理的系统提示末尾** —— 借鉴 pi 的 `~/.pi/agent/APPEND_SYSTEM.md` 约定,dsh 侧对应 `$DSH_HOME/APPEND_SYSTEM.md`(默认 `~/.dsh/APPEND_SYSTEM.md`,沿用 dsh 其余部分共用的 `$DSH_HOME` 覆盖)。
164
164
 
165
165
  - **热应用** —— section 提供者在每次组装提示词时读盘,改完文件**下一次请求**即生效:无需重启、无需 watcher、无需 `/reload`。
166
- - **首次启动自动播种** —— 文件不存在时,TUI 在启动时一次性从随包模板 `templates/APPEND_SYSTEM.md`(英文版协调者身份模板:身份、核心规则、执行工作流)创建。已有文件归用户所有 —— TUI 永远不会覆盖用户内容。
166
+ - **首次启动自动播种** —— 文件不存在时,TUI 在启动时一次性从随包模板 `templates/APPEND_SYSTEM.md`(英文版协调者身份模板:身份、核心规则、执行工作流,含「subagent 仅指已注册子代理」的用语铁律)创建。已有文件归用户所有 —— TUI 永远不会覆盖用户内容;只在缺失标记的 todo-lifecycle 段、或尚未出现 subagents 铁律措辞(按短语匹配,幂等)时追加对应段落。
167
167
  - **TUI 自有段落** —— 一段带标记的 block(`<!-- dsh-tui-pi:todo-lifecycle -->`)只在缺失时追加一次,并保持幂等,确保模型在所有 todo 都完成时清空 `todo/write` 列表。已带标记的文件后续启动原样保留。
168
168
  - **旧版迁移** —— 同一段 todo block 早期通过 `~/.dsh/AGENTS.md` 下发。启动时 TUI 一次性把它剥掉(无标记时 no-op),避免重复下发。
169
169
  - **空 / 读不到 = 不挂载该 section** —— 文件缺失或读不了时该 section 被静默丢弃,无报错、不影响 TUI 启动。
@@ -199,7 +199,7 @@ EOF
199
199
  | `/model` | 两阶段选择 provider/model(然后选推理等级),实时切换并持久化。面板内按键:`f` 收藏 · `h` 隐藏 · `/` 过滤(收藏/隐藏经 settings 持久化)。 |
200
200
  | `/think` | 当前模型的推理强度选择(`Off`/`High`/`Max`)。 |
201
201
  | `/session` | 只读信息面板:id、cwd、模型、token 用量、事件计数。 |
202
- | `/resume` | 选择已保存的会话,验证日志后恢复。 |
202
+ | `/resume` | 选择已保存的会话,验证日志后恢复。按最后更新时间排序(日志文件 mtime),新的在上;`Updated` 列显示生效时间。 |
203
203
  | `/new` | 分离当前会话;下一次输入开启新会话。 |
204
204
  | `/settings` | 文本式设置浏览器(命名空间、schema 遍历、内联编辑器、密钥脱敏)。 |
205
205
  | `/export` | 将当前会话日志导出为 JSONL(默认 `~/Downloads/dsh-session-<id>.jsonl`)。 |
@@ -209,6 +209,8 @@ EOF
209
209
  | `/agents` | 管理 agent markdown 文件 + 子代理限制(`maxAgents`、`maxRounds`)。 |
210
210
  | `/subagents` | 选择运行中/最近的子代理,查看其实时对话。 |
211
211
  | `/reload` | 从源码热重载插件(`pnpm build` 后执行,无需重启 dsh)。 |
212
+ | `/login` | 登录 provider:从目录选择(或 `/login openai` 直达),输入一个 API key。**Custom provider…** 条目(`/login custom`)打开六字段表单,接入 pi-ai 未收录的任意 OpenAI/Anthropic 兼容网关 —— 路由 id、显示名、协议、base URL、模型列表、API key —— 写出与 Web Models 页相同的 hand-declared 路由。 |
213
+ | `/logout` | 选择已登录的 provider,同时删除存储的 key 和 provider 配置。 |
212
214
  | `/hotkeys` | 快捷键浏览器和实时编辑。 |
213
215
 
214
216
  不是已注册命令的内容会作为普通提示词发送给模型。
@@ -383,6 +385,7 @@ src/
383
385
  footer.ts PowerlineFooter(7 分段 + 时钟)
384
386
  editor.ts CwdBorderEditor(顶部边框:cwd + git 分支)
385
387
  subagent-policy.ts maxAgents 守卫 + maxRounds 收尾请求注入
388
+ (运行中走 steer;⚡ 标记,查看器可见)
386
389
  subagent-viewer.ts Ctrl+G 选择器 + 实时对话面板
387
390
  theme/ GitHub light/dark 配色 + 终端检测
388
391
  test/*.test.mjs 单元测试(569 个,覆盖 38 个文件)
@@ -26,6 +26,14 @@ export declare const TODO_LIFECYCLE_MARKER = "<!-- dsh-tui-pi:todo-lifecycle -->
26
26
  * an all-completed list would stay pinned above the chat input.
27
27
  */
28
28
  export declare const TODO_LIFECYCLE_SECTION = "<!-- dsh-tui-pi:todo-lifecycle -->\n## Todo list lifecycle (dsh-tui-pi)\n\nThe UI renders your todo list as a fixed panel above the chat input. Keep\nitems `pending` or `in_progress` while they are not done. When EVERY todo is\ncompleted \u2014 no pending or in-progress items remain \u2014 write an EMPTY todo list\n(`todos: []`) so the panel clears. Never leave a fully-completed list behind.\n";
29
+ /**
30
+ * The subagent vocabulary rule (user iron rule, 2026-08-24): "subagent" means
31
+ * the registered subagents only. Shipped inside the template's Core rules for
32
+ * fresh installs; appended idempotently to pre-template files that lack it.
33
+ */
34
+ export declare const SUBAGENTS_RULE_SECTION = "## Subagents\n\nWhen the user says \"subagent\", they mean the registered subagents only; never use unregistered subagents.\n";
35
+ /** Idempotency phrase for the subagents rule (matches template and section). */
36
+ export declare const SUBAGENTS_RULE_PHRASE = "registered subagents only";
29
37
  /** Default harness home: `$DSH_HOME` or `~/.dsh`. */
30
38
  export declare function dshHome(): string;
31
39
  /** The user-editable append file path (`$DSH_HOME/APPEND_SYSTEM.md`). */
@@ -49,12 +57,13 @@ export declare function readAppendSystem(path?: string): string;
49
57
  * marked todo-lifecycle section: a missing file is seeded from
50
58
  * `templates/APPEND_SYSTEM.md` (the English orchestrator template) followed
51
59
  * by the marked section; an existing file is user-owned — only the marked
52
- * section is appended when its marker is missing, and a marked file is left
53
- * untouched.
60
+ * section is appended when its marker is missing, a marked file is left
61
+ * untouched, and the subagents vocabulary rule is appended to any file that
62
+ * does not phrase it yet (template ships it as a Core rule).
54
63
  * @param path - target file (injectable for tests).
55
64
  * @param templatePath - the shipped template (injectable for tests).
56
65
  * @returns an error message on failure, undefined on success (including the
57
- * no-op case where the marker is already present).
66
+ * no-op case where the marker and the rule are already present).
58
67
  */
59
68
  export declare function ensureAppendSystemFile(path?: string, templatePath?: string): Promise<string | undefined>;
60
69
  /**
@@ -38,6 +38,27 @@ items \`pending\` or \`in_progress\` while they are not done. When EVERY todo is
38
38
  completed — no pending or in-progress items remain — write an EMPTY todo list
39
39
  (\`todos: []\`) so the panel clears. Never leave a fully-completed list behind.
40
40
  `;
41
+ /**
42
+ * The subagent vocabulary rule (user iron rule, 2026-08-24): "subagent" means
43
+ * the registered subagents only. Shipped inside the template's Core rules for
44
+ * fresh installs; appended idempotently to pre-template files that lack it.
45
+ */
46
+ export const SUBAGENTS_RULE_SECTION = `## Subagents
47
+
48
+ When the user says "subagent", they mean the registered subagents only; never use unregistered subagents.
49
+ `;
50
+ /** Idempotency phrase for the subagents rule (matches template and section). */
51
+ export const SUBAGENTS_RULE_PHRASE = 'registered subagents only';
52
+ /**
53
+ * Append the subagents rule unless the content already carries it (the shipped
54
+ * template embeds it as a Core rule; a hand-edited file may phrase it anywhere
55
+ * — the case-insensitive phrase check accepts both).
56
+ */
57
+ function withSubagentsRule(content) {
58
+ if (content.toLowerCase().includes(SUBAGENTS_RULE_PHRASE))
59
+ return content;
60
+ return `${content.replace(/\s+$/u, '')}\n\n${SUBAGENTS_RULE_SECTION}`;
61
+ }
41
62
  /** Default harness home: `$DSH_HOME` or `~/.dsh`. */
42
63
  export function dshHome() {
43
64
  return process.env.DSH_HOME ?? join(homedir(), '.dsh');
@@ -78,12 +99,13 @@ export function readAppendSystem(path = appendSystemPath()) {
78
99
  * marked todo-lifecycle section: a missing file is seeded from
79
100
  * `templates/APPEND_SYSTEM.md` (the English orchestrator template) followed
80
101
  * by the marked section; an existing file is user-owned — only the marked
81
- * section is appended when its marker is missing, and a marked file is left
82
- * untouched.
102
+ * section is appended when its marker is missing, a marked file is left
103
+ * untouched, and the subagents vocabulary rule is appended to any file that
104
+ * does not phrase it yet (template ships it as a Core rule).
83
105
  * @param path - target file (injectable for tests).
84
106
  * @param templatePath - the shipped template (injectable for tests).
85
107
  * @returns an error message on failure, undefined on success (including the
86
- * no-op case where the marker is already present).
108
+ * no-op case where the marker and the rule are already present).
87
109
  */
88
110
  export async function ensureAppendSystemFile(path = appendSystemPath(), templatePath = appendSystemTemplatePath()) {
89
111
  let existing;
@@ -93,10 +115,14 @@ export async function ensureAppendSystemFile(path = appendSystemPath(), template
93
115
  catch {
94
116
  existing = '';
95
117
  }
96
- if (existing.includes(TODO_LIFECYCLE_MARKER))
97
- return undefined;
118
+ if (existing.includes(TODO_LIFECYCLE_MARKER)) {
119
+ // Marker section already maintained — only the subagents rule may be new.
120
+ if (existing.toLowerCase().includes(SUBAGENTS_RULE_PHRASE))
121
+ return undefined;
122
+ return writeAtomically(path, withSubagentsRule(existing));
123
+ }
98
124
  if (existing !== '') {
99
- return writeAtomically(path, `${existing.replace(/\s+$/u, '')}\n\n${TODO_LIFECYCLE_SECTION}`);
125
+ return writeAtomically(path, withSubagentsRule(`${existing.replace(/\s+$/u, '')}\n\n${TODO_LIFECYCLE_SECTION}`));
100
126
  }
101
127
  // Fresh file: seed from the shipped template, then the marked section. A
102
128
  // missing template (broken tarball) degrades to the marked section alone.
@@ -108,7 +134,7 @@ export async function ensureAppendSystemFile(path = appendSystemPath(), template
108
134
  template = '';
109
135
  }
110
136
  const seed = template === '' ? TODO_LIFECYCLE_SECTION : `${template.replace(/\s+$/u, '')}\n\n${TODO_LIFECYCLE_SECTION}`;
111
- return writeAtomically(path, seed);
137
+ return writeAtomically(path, withSubagentsRule(seed));
112
138
  }
113
139
  /**
114
140
  * Migration: strip the TUI-owned todo-lifecycle block from `~/.dsh/AGENTS.md`
@@ -1 +1 @@
1
- {"version":3,"file":"append-system.js","sourceRoot":"","sources":["../src/append-system.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"append-system.js","sourceRoot":"","sources":["../src/append-system.ts"],"names":[],"mappings":"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"}
package/lib/ask-user.d.ts CHANGED
@@ -2,52 +2,62 @@
2
2
  * Ask the human a question while the model is mid-turn.
3
3
  *
4
4
  * Wires the upstream `ctx.userQuestions` capability seam (`dsh-user-questions`)
5
- * with a terminal-side UI: an in-place multi-question overlay that pauses the
6
- * tool call until the human answers, then feeds the canonical
5
+ * with a terminal-side UI: a DOCKED panel pinned above the chat input (the
6
+ * Todos-panel slot of the widgets dock not a floating overlay) that pauses
7
+ * the tool call until the human answers, then feeds the canonical
7
8
  * `AskUserQuestionAnswer` envelope back to `dsh-tool-ask-user` as a normal
8
9
  * tool result.
9
10
  *
10
- * Layout — one framed overlay, all questions flattened (numbered rows, a
11
+ * Layout — one bordered dock panel, all questions flattened (numbered rows, a
11
12
  * divider between each question block and its options, and a scroll window
12
- * so the cursor row is always visible even when the overlay is height-capped):
13
+ * so the cursor row is always visible even when the dock is height-capped):
13
14
  *
14
- * ● Questions (2)
15
- * SELECTION
16
- * ───────────────────────────── (table chrome)
17
- * Fruit Where should we deploy? (header — not selectable)
18
- * ───────────────────────────── (divider)
19
- * ● 1. staging (cursor + inline selection mark)
20
- * 2. production
21
- * red and round (option description, own muted line)
22
- * 3. Type something. (sentinel row inline input)
23
- * (blank separator before confirm)
24
- * ⏎ Confirm answers (when 2 questions, or any multiSelect)
15
+ * ┌────────────────────────────────────┐
16
+ * │ ● Questions (2) │
17
+ * SELECTION │
18
+ * ───────────────────────────── (table chrome)
19
+ * │ Fruit Where should we deploy? (header — not selectable)
20
+ * ───────────────────────────── │ (divider)
21
+ * ▸ ● 1. staging │ (cursor + inline selection mark)
22
+ * │ ○ 2. production
23
+ * │ red and round │ (option description, own muted line)
24
+ * │ 3. Type something. │ (sentinel row inline input)
25
+ * │ │ (blank separator before confirm)
26
+ * │ ⏎ Confirm answers │ (when ≥ 2 questions, or any multiSelect)
27
+ * └────────────────────────────────────┘
28
+ *
29
+ * While the panel is open it owns the keyboard: it is mounted into the dock
30
+ * slot between the live widgets and the editor, takes focus through
31
+ * `tui.setFocus`, and `setModalActive(true)` routes the app keymap exactly
32
+ * like an open overlay (Esc/Ctrl+C/app keys yield to the panel — Esc never
33
+ * arms the running-task stop from inside a modal). On close the panel
34
+ * unmounts, clears the modal flag, and focus returns to the current editor
35
+ * through `restoreFocus`.
25
36
  *
26
37
  * The body renders through a scroll window sized from the live terminal
27
- * height (`askUserMaxVisibleForRows`; falls back to `ASK_USER_MAX_VISIBLE`
28
- * when the terminal row count is unknown):
29
- * pi-tui hard-clips overlay tails at `maxHeight`, so without an inner window
30
- * the last options fall off-screen while the cursor can still reach them
31
- * ("unselectable" bug). The window slides with the cursor and the footer
32
- * gains a `(n/m)` position readout while content overflows.
38
+ * height (`askUserMaxVisibleForRows`; the dock budget subtracts the editor,
39
+ * footer and a transcript floor, and falls back to `ASK_USER_MAX_VISIBLE`
40
+ * when the terminal row count is unknown): pi-tui would lay a taller dock
41
+ * out at full height and squeeze the transcript to nothing, so the window
42
+ * caps the body. The window slides with the cursor and the footer gains a
43
+ * `(n/m)` position readout while content overflows.
33
44
  *
34
- * Single-question single-select overlay: Enter on an option (or committing a
45
+ * Single-question single-select panel: Enter on an option (or committing a
35
46
  * filled sentinel) submits immediately. A multiSelect question — even a lone
36
47
  * one — gets a Confirm row instead of auto-submitting, so the user can pick
37
- * several options first. Multi-question overlay: Enter on the Confirm row
38
- * hops to the review page (all answers listed, each editable in place).
39
- * Esc double-press within 200 ms declines — but terminal key auto-repeat
40
- * (holding Esc) is ignored below `ESC_REPEAT_GUARD_MS`, so a long press
41
- * cannot accidentally fire the decline. The provider returns the "declined"
42
- * envelope and the model reads it as a normal user reply. An aborted
43
- * request signal (`request.signal`) settles declined too — we resolve the
44
- * declined envelope instead of rejecting ASK_ABORTED because the upstream
45
- * service already screens entry-time aborts and an aborted step discards
46
- * the result anyway.
48
+ * several options first. Multi-question panel: Enter on the Confirm row hops
49
+ * to the review page (all answers listed, each editable in place). Esc
50
+ * double-press within 200 ms declines — but terminal key auto-repeat (holding
51
+ * Esc) is ignored below `ESC_REPEAT_GUARD_MS`, so a long press cannot
52
+ * accidentally fire the decline. The provider returns the "declined" envelope
53
+ * and the model reads it as a normal user reply. An aborted request signal
54
+ * (`request.signal`) settles declined too — we resolve the declined envelope
55
+ * instead of rejecting ASK_ABORTED because the upstream service already
56
+ * screens entry-time aborts and an aborted step discards the result anyway.
47
57
  *
48
58
  * Pure logic lives in the top of this file (initial state, answer envelope,
49
- * declined envelope, double-Esc state machine, row-layout math) so it can
50
- * be unit-tested without a TTY. The component below owns the TUI render +
59
+ * declined envelope, double-Esc state machine, row-layout math) so it can be
60
+ * unit-tested without a TTY. The component below owns the TUI render +
51
61
  * keyboard handling; the install function at the bottom registers the
52
62
  * provider under `ctx.userQuestions` (a Cordis effect, single active
53
63
  * provider in the tree).
@@ -55,7 +65,7 @@
55
65
  * Inspired by juicesharp/rpiv-ask-user-question
56
66
  * (https://github.com/juicesharp/rpiv-ask-user-question).
57
67
  */
58
- import { type TUI } from '@earendil-works/pi-tui';
68
+ import { type Component, type TUI } from '@earendil-works/pi-tui';
59
69
  import type { AskUserQuestionAnswer, AskUserQuestionItem, AskUserQuestionOption } from '@deepseek-ai/dsh-user-questions';
60
70
  import type { Context } from '@deepseek-ai/cordis';
61
71
  import { type TuiTheme } from './theme/index.ts';
@@ -77,23 +87,21 @@ export declare const INCOMPLETE_HINT = "Answer every question first";
77
87
  /**
78
88
  * Fallback visible body-line cap for the questions/review panes, used only
79
89
  * when the terminal row count is unknown (fake TUIs in tests, exotic
80
- * terminals). Derived for a 24-row terminal (the e2e matrix floor): 80%
81
- * maxHeight = 19 framed lines 2 frame borders = 17 content lines, and
82
- * title(1) + table chrome(3) + 9 body lines + bottom rule + blank + footer
83
- * = 16, leaving one line of headroom for a hint. The live path derives the
84
- * window from the real terminal height via `askUserMaxVisibleForRows`.
90
+ * terminals). Matches the 24-row dock budget: 24 8 reserved (editor 3 +
91
+ * footer 1 + status 1 + transcript floor 3) 10 panel chrome lines = 6.
92
+ * The live path derives the window from the real terminal height via
93
+ * `askUserMaxVisibleForRows`.
85
94
  */
86
- export declare const ASK_USER_MAX_VISIBLE = 9;
95
+ export declare const ASK_USER_MAX_VISIBLE = 6;
87
96
  /**
88
- * Derive the scroll-window size from the terminal height so the window grows
89
- * with the terminal instead of pinning to the conservative 24-row budget:
90
- * pi-tui hard-clips the framed overlay at its resolved maxHeight, so body
91
- * lines beyond `budget ASK_USER_VIEW_OVERHEAD` would be invisible anyway.
92
- * At the 24-row e2e floor this yields exactly ASK_USER_MAX_VISIBLE (9);
93
- * larger terminals scale up proportionally. Without a usable row count it
94
- * degrades to ASK_USER_MAX_VISIBLE.
97
+ * Derive the scroll-window size from the terminal height: the dock stacks
98
+ * (it does not float over the transcript like the old overlay did), so the
99
+ * panel budget is the terminal height minus the reserved dock rows, minus
100
+ * the panel's own chrome. At the 24-row e2e floor this yields exactly
101
+ * ASK_USER_MAX_VISIBLE (6); larger terminals scale up. Without a usable row
102
+ * count it degrades to ASK_USER_MAX_VISIBLE.
95
103
  */
96
- export declare function askUserMaxVisibleForRows(termRows: number | undefined, maxHeight?: AskUserPanelDeps['maxHeight']): number;
104
+ export declare function askUserMaxVisibleForRows(termRows: number | undefined): number;
97
105
  /** A row in the multi-question view. */
98
106
  export interface FlatRow {
99
107
  kind: 'question-header' | 'option' | 'sentinel' | 'confirm';
@@ -237,28 +245,38 @@ export declare function renderReviewView(theme: TuiTheme, state: AskUserState, w
237
245
  /** Options for assembling the panel + provider function. */
238
246
  export interface AskUserPanelDeps {
239
247
  tui: TUI;
240
- /** Live theme getter — re-read on every render so a mid-overlay hot-swap applies (frame included). */
248
+ /** Live theme getter — re-read on every render so a mid-panel hot-swap applies (borders included). */
241
249
  theme: () => TuiTheme;
242
- /** Re-focus the current editor on overlay close. */
250
+ /** Re-focus the current editor on panel close. */
243
251
  restoreFocus: () => void;
252
+ /**
253
+ * Mount the panel component into the dock slot above the chat input (the
254
+ * Todos-panel slot); returns the unmount function. The panel is NOT an
255
+ * overlay — it renders pinned above the editor like the live widgets.
256
+ */
257
+ mount: (component: Component) => () => void;
258
+ /**
259
+ * Declare the docked modal's keyboard ownership. While active, the app
260
+ * keymap treats the panel exactly like an open overlay (app keys and the
261
+ * Esc/Ctrl+C chains yield to the focused panel) and `refocusEditor` must
262
+ * not steal focus from it.
263
+ */
264
+ setModalActive: (active: boolean) => void;
244
265
  /** Injectable clock for tests; defaults to Date.now. */
245
266
  now?: () => number;
246
- /** Width and height of the framed overlay. */
247
- width?: `${number}%` | number;
248
- maxHeight?: `${number}%` | number;
249
267
  }
250
268
  /** Result promise from `openAskUserPanel`. Declined carries the canonical decline envelope. */
251
269
  export type AskUserResult = AskUserQuestionAnswer;
252
270
  /**
253
- * Open the AskUser overlay for one set of questions.
271
+ * Open the AskUser docked panel for one set of questions.
254
272
  *
255
273
  * `signal` is the caller's abort signal (the tool execution's). Already-aborted
256
- * settles declined WITHOUT staging an overlay; a live signal closes the
257
- * overlay and settles declined when it fires. We resolve the declined envelope
258
- * rather than rejecting with the upstream ASK_ABORTED code on purpose: the
259
- * upstream service already screens entry-time aborts, and a step aborted after
260
- * this point discards the tool result anyway — resolving keeps the pending
261
- * promise from ever hanging either way.
274
+ * settles declined WITHOUT mounting a panel; a live signal closes the panel
275
+ * and settles declined when it fires. We resolve the declined envelope rather
276
+ * than rejecting with the upstream ASK_ABORTED code on purpose: the upstream
277
+ * service already screens entry-time aborts, and a step aborted after this
278
+ * point discards the tool result anyway — resolving keeps the pending promise
279
+ * from ever hanging either way.
262
280
  */
263
281
  export declare function openAskUserPanel(deps: AskUserPanelDeps, questions: readonly AskUserQuestionItem[], signal?: AbortSignal): Promise<AskUserResult>;
264
282
  /**