@aiwayds/dsh-tui-pi 0.20.1 → 0.22.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 (58) hide show
  1. package/README.md +62 -8
  2. package/README.zh.md +7 -4
  3. package/assets/deepseek-icon.svg +4 -0
  4. package/lib/append-system.d.ts +12 -3
  5. package/lib/append-system.js +33 -7
  6. package/lib/append-system.js.map +1 -1
  7. package/lib/ask-user.d.ts +79 -61
  8. package/lib/ask-user.js +117 -112
  9. package/lib/ask-user.js.map +1 -1
  10. package/lib/custom-provider.d.ts +119 -0
  11. package/lib/custom-provider.js +243 -0
  12. package/lib/custom-provider.js.map +1 -0
  13. package/lib/dsh-events.d.ts +15 -0
  14. package/lib/dsh-events.js +12 -0
  15. package/lib/dsh-events.js.map +1 -1
  16. package/lib/index.js +139 -16
  17. package/lib/index.js.map +1 -1
  18. package/lib/live-widgets.js +4 -0
  19. package/lib/live-widgets.js.map +1 -1
  20. package/lib/login.js +32 -2
  21. package/lib/login.js.map +1 -1
  22. package/lib/messages.d.ts +26 -17
  23. package/lib/messages.js +36 -17
  24. package/lib/messages.js.map +1 -1
  25. package/lib/provider-catalog.d.ts +7 -0
  26. package/lib/provider-catalog.js +1 -0
  27. package/lib/provider-catalog.js.map +1 -1
  28. package/lib/retention.d.ts +236 -0
  29. package/lib/retention.js +345 -0
  30. package/lib/retention.js.map +1 -0
  31. package/lib/session.d.ts +11 -0
  32. package/lib/session.js +33 -3
  33. package/lib/session.js.map +1 -1
  34. package/lib/sessions.d.ts +138 -5
  35. package/lib/sessions.js +264 -17
  36. package/lib/sessions.js.map +1 -1
  37. package/lib/settings.d.ts +9 -0
  38. package/lib/settings.js +12 -1
  39. package/lib/settings.js.map +1 -1
  40. package/lib/skills-manager.d.ts +69 -6
  41. package/lib/skills-manager.js +217 -99
  42. package/lib/skills-manager.js.map +1 -1
  43. package/lib/startup-info.d.ts +139 -0
  44. package/lib/startup-info.js +238 -0
  45. package/lib/startup-info.js.map +1 -0
  46. package/lib/subagent-policy.d.ts +14 -4
  47. package/lib/subagent-policy.js +40 -14
  48. package/lib/subagent-policy.js.map +1 -1
  49. package/lib/subagent-viewer.js +14 -2
  50. package/lib/subagent-viewer.js.map +1 -1
  51. package/lib/theme-settings.d.ts +48 -0
  52. package/lib/theme-settings.js +109 -7
  53. package/lib/theme-settings.js.map +1 -1
  54. package/lib/tui.d.ts +14 -0
  55. package/lib/tui.js +11 -2
  56. package/lib/tui.js.map +1 -1
  57. package/package.json +1 -1
  58. 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.
@@ -275,6 +277,57 @@ theme color via BCE, no patched dependencies.
275
277
 
276
278
  ---
277
279
 
280
+ ## Session management
281
+
282
+ Two knobs clusters govern the session store, both living under the
283
+ `dsh-tui` settings namespace (`~/.dsh/settings.yaml`) with an environment
284
+ escape hatch each:
285
+
286
+ ```yaml
287
+ dsh-tui:
288
+ # Startup janitor for ~/.dsh/sessions — DELETES whole session log
289
+ # directories outside the window. Runs once per process at startup.
290
+ retention:
291
+ maxCount: 100 # keep at most this many sessions; <= 0 DISABLES the janitor
292
+ maxAgeDays: 7 # delete logs untouched for more than this many days (> 0)
293
+ minIdleHours: 24 # count-rule-only idle guard in hours (>= 0)
294
+
295
+ # /resume display filter — only HIDES picker rows, never deletes.
296
+ # Resolved fresh at every picker open (a settings change applies to
297
+ # the next /resume, no restart).
298
+ resume:
299
+ maxAgeDays: 7 # only sessions with log activity inside this window (> 0)
300
+ minBytes: 20480 # minimum compressed on-disk log size for a row (>= 0)
301
+ ```
302
+
303
+ Precedence per field: an explicit value in settings.yaml > the
304
+ `DSH_TUI_RETENTION_MAX_COUNT` / `DSH_TUI_RETENTION_MAX_AGE_DAYS` /
305
+ `DSH_TUI_RETENTION_MIN_IDLE_HOURS` and `DSH_TUI_RESUME_MAX_AGE_DAYS` /
306
+ `DSH_TUI_RESUME_MIN_BYTES` environment variables > the defaults above.
307
+ An invalid settings value warns once on stderr and falls to the next
308
+ level; an invalid env value falls back silently to the default — a typo
309
+ never widens or guts the policy. `maxCount` and `minBytes` must be
310
+ integers at every layer (a fractional cap or byte floor is garbage, not a
311
+ window).
312
+
313
+ **Disabling retention entirely** — for a long-lived process (a remote
314
+ bridge, a headless cron run) that read-attaches old sessions the default
315
+ window would prune:
316
+
317
+ ```yaml
318
+ dsh-tui:
319
+ retention:
320
+ maxCount: 0 # or: DSH_TUI_RETENTION_MAX_COUNT=0
321
+ ```
322
+
323
+ Timing: **retention runs once at startup** (never mid-session; a
324
+ `/reload` does not re-run it — the next cold start does), while the
325
+ **resume filter applies at every `/resume` open**. The two `7`s default
326
+ to the same "one week is the working set" decision but serve different
327
+ masters — retention deletes logs, the resume filter only hides rows.
328
+
329
+ ---
330
+
278
331
  ## Fonts
279
332
 
280
333
  The TUI's only Private-Use-Area glyph is the powerline segment separator
@@ -421,8 +474,9 @@ src/
421
474
  footer.ts PowerlineFooter (7 segments + clock)
422
475
  editor.ts CwdBorderEditor (top border: cwd + git branch)
423
476
  subagent-policy.ts maxAgents guard + maxRounds wrap-up injection
477
+ (steer-when-running; ⚡-marked, visible in the viewer)
424
478
  subagent-viewer.ts Ctrl+G picker + live transcript panel + Enter steer injection
425
- ask-user.ts Ask User Question overlay: pure state reducers +
479
+ ask-user.ts Ask User Question docked panel: pure state reducers +
426
480
  framed overlay UI + ctx.userQuestions provider
427
481
  steer-flow.ts Steer / follow-up decision layer: routed delivery with
428
482
  race fallback, queue actions (remove / promote), notices
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 个文件)
@@ -0,0 +1,4 @@
1
+ <!-- DeepSeek official whale logo (brand blue #4D6BFE). Source: DeepSeek (deepseek.com) brand asset. Kept in-repo for future branding/README use; not referenced by code yet. © DeepSeek. -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50" fill="none">
3
+ <path d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#4D6BFE" fill-rule="nonzero"/>
4
+ </svg>
@@ -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
  /**