@aiwayds/dsh-tui-pi 0.18.1 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,21 +2,22 @@
2
2
 
3
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
- **Compatibility:** dsh `0.1.0-rc.7` and `0.1.0-rc.8`. Slash-command execution
6
- adapts to both `dsh-commands` `execute()` signatures at runtime (rc.8 added an
7
- `images` parameter before `signal`; the TUI detects the arity and calls either
8
- form). Verified under rc.8 by unit tests plus a live tmux e2e smoke; the rc.7
9
- call path is identical to the pre-rc.8 direct invocation.
5
+ **Compatibility:** tested against dsh `0.1.1-rc.2`. Slash-command execution goes
6
+ through an `executeCommand()` compat shim (`src/commands.ts`) that probes the
7
+ arity of `dsh-commands`' `execute()` at runtime supporting both the pre-rc.8
8
+ 3-arg form `(agent, line, signal)` and the current 4-arg form
9
+ `(agent, line, images, signal)`, unchanged since `0.1.0-rc.8`. Verified by unit
10
+ tests plus a live tmux e2e smoke.
10
11
 
11
12
  > 中文说明: [README.zh.md](README.zh.md)
12
13
 
13
14
  ## Screenshot
14
15
 
15
- ![dsh-tui-pi demo](./dsh-tui-pi-demo.gif)
16
+ https://github.com/user-attachments/assets/6a7e00bb-1fd0-4bc5-9070-457f1e9fa54d
16
17
 
17
- A live terminal recording of a session — todos, running subagents, think/tool
18
- panels and the powerline footer in action. ([Interactive playback on
19
- asciinema](https://asciinema.org/a/BE212ZO8x1zEZyZn))
18
+ A live terminal recording of a session (MP4, 1.5× speed) — todos, running
19
+ subagents, think/tool panels and the powerline footer in action.
20
+ ([Interactive playback on asciinema](https://asciinema.org/a/BE212ZO8x1zEZyZn))
20
21
 
21
22
  ### Layout overview
22
23
 
@@ -156,7 +157,7 @@ Two caps (`/agents` → `l` to configure):
156
157
  dsh plugin --profile tui add @aiwayds/dsh-dcp
157
158
  ```
158
159
 
159
- Once mounted, DCP runs transparently in the background. The footer's **context** segment prices the current occupancy — the latest request's billed context plus a CJK estimate of messages after it — so after a compaction the next request lands smaller and the display follows it down (the percent is capped at 100, the window being a hard ceiling). The **cache-hit** segment reflects the session's cumulative cache reuse.
160
+ Once mounted, DCP runs transparently in the background. The footer's **context** segment prices the current occupancy — the latest request's billed context plus a CJK estimate of messages after it — so after a compaction the next request lands smaller and the display follows it down (the percent is capped at 100, the window being a hard ceiling). The **cache-hit** segment reflects the current provider/model route's cache reuse — the rate is computed per route segment and restarts (hiding until the next billed message) when the provider or model changes.
160
161
 
161
162
  Inside a subagent, a committed compaction is visible too: DCP appends one `user/message` **notice** row per compaction on the child's own log, and the Ctrl+G transcript renders it with a `🧹` marker (distinct from the generic `ⓘ`), with the picker rows carrying the per-child compaction count (`🧹 N×` in the description). Both DCP's `roundInterval` and the TUI's `maxRounds` count the **same** thing — `assistant/message` events, one per LLM round-trip — but act differently: the TUI queues one wrap-up request once a child's count reaches `maxRounds`, while DCP compacts (prunes context) at the next idle boundary once a session's count reaches `roundInterval`. One triggers work, the other frees context.
162
163
 
@@ -389,7 +390,7 @@ dsh --profile tui # or: dsh-tui-pi (bin shim)
389
390
  ```sh
390
391
  pnpm check # tsc --noEmit
391
392
  pnpm build # emit lib/
392
- pnpm test # unit tests, node --test against lib/ (654 tests, pretest builds)
393
+ pnpm test # unit tests, node --test against lib/ (677 tests, pretest builds)
393
394
  ```
394
395
 
395
396
  Local type-checking symlinks `node_modules/@deepseek-ai/*` to the installed
@@ -424,7 +425,7 @@ src/
424
425
  ask-user.ts Ask User Question overlay: pure state reducers +
425
426
  framed overlay UI + ctx.userQuestions provider
426
427
  theme/ GitHub light/dark palettes + terminal detection
427
- test/*.test.mjs unit tests (654 across 40 files)
428
+ test/*.test.mjs unit tests (677 across 40 files)
428
429
  ```
429
430
 
430
431
  ---
package/README.zh.md CHANGED
@@ -2,15 +2,19 @@
2
2
 
3
3
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) 的 pi 风格终端 UI 插件 —— 把 dsh 变成 pi 风格的编码代理体验。
4
4
 
5
- **兼容性**:支持 dsh `0.1.0-rc.7` `0.1.0-rc.8`。斜杠命令执行在运行时自适应两个版本的 `dsh-commands` `execute()` 签名(rc.8 在 `signal` 前插入了 `images` 参数;TUI 探测参数个数后按对应形式调用)。rc.8 下经单元测试 + tmux 真机冒烟验证;rc.7 调用路径与升级前的直接调用完全一致。
5
+ **兼容性**:针对 dsh `0.1.1-rc.2` 测试。斜杠命令执行经过 `executeCommand()`
6
+ 兼容 shim(`src/commands.ts`),在运行时探测 `dsh-commands` `execute()` 的参数
7
+ 个数——同时支持 rc.8 之前的 3 参形式 `(agent, line, signal)` 与当前的 4 参形式
8
+ `(agent, line, images, signal)`(自 `0.1.0-rc.8` 起未再变化)。经单元测试 +
9
+ tmux 真机冒烟验证。
6
10
 
7
11
  > English version: [README.md](README.md)
8
12
 
9
13
  ## 截图
10
14
 
11
- ![dsh-tui-pi 演示](./dsh-tui-pi-demo.gif)
15
+ https://github.com/user-attachments/assets/6a7e00bb-1fd0-4bc5-9070-457f1e9fa54d
12
16
 
13
- 真实会话的终端录制——Todos、运行中的 subagent、思考/工具面板和 powerline footer 一览。([asciinema 交互播放](https://asciinema.org/a/BE212ZO8x1zEZyZn))
17
+ 真实会话的终端录制(MP4,1.5× 速度)——Todos、运行中的 subagent、思考/工具面板和 powerline footer 一览。([asciinema 交互播放](https://asciinema.org/a/BE212ZO8x1zEZyZn))
14
18
 
15
19
  ### 布局总览
16
20
 
package/lib/ask-user.d.ts CHANGED
@@ -7,16 +7,27 @@
7
7
  * `AskUserQuestionAnswer` envelope back to `dsh-tool-ask-user` as a normal
8
8
  * tool result.
9
9
  *
10
- * Layout — one framed overlay, all questions flattened:
10
+ * Layout — one framed overlay, all questions flattened (numbered rows, a
11
+ * 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):
11
13
  *
12
- * ● Question 1
13
- * Where should we deploy? (left = header / right = question text)
14
- * ▸ staging
15
- * production
16
- * Type something. (sentinel row — inline input)
17
- * Question 2 ··· (continues vertically)
18
- *
19
- * Confirm answers (when ≥ 2 questions, or any multiSelect question)
14
+ * ● Questions (2)
15
+ * Fruit Where should we deploy? (header not selectable)
16
+ * ───────────────────────────── (divider)
17
+ * ▸ 1. staging (cursor marker + per-question number)
18
+ * 2. production
19
+ * red and round (option description, own muted line)
20
+ * 3. Type something. (sentinel row — inline input)
21
+ * Vehicle Pick a vehicle ··· (continues vertically)
22
+ * ⏎ Confirm answers (when ≥ 2 questions, or any multiSelect)
23
+ *
24
+ * The body renders through a scroll window sized from the live terminal
25
+ * height (`askUserMaxVisibleForRows`; falls back to `ASK_USER_MAX_VISIBLE`
26
+ * when the terminal row count is unknown):
27
+ * pi-tui hard-clips overlay tails at `maxHeight`, so without an inner window
28
+ * the last options fall off-screen while the cursor can still reach them
29
+ * ("unselectable" bug). The window slides with the cursor and the footer
30
+ * gains a `(n/m)` position readout while content overflows.
20
31
  *
21
32
  * Single-question single-select overlay: Enter on an option (or committing a
22
33
  * filled sentinel) submits immediately. A multiSelect question — even a lone
@@ -61,6 +72,26 @@ export declare const SENTINEL_LABEL = "Type something.";
61
72
  export declare const DECLINE_MESSAGE = "User declined to answer questions.";
62
73
  /** Transient hint when Enter lands on an incomplete confirm/submit row. */
63
74
  export declare const INCOMPLETE_HINT = "Answer every question first";
75
+ /**
76
+ * Fallback visible body-line cap for the questions/review panes, used only
77
+ * when the terminal row count is unknown (fake TUIs in tests, exotic
78
+ * terminals). Derived for a 24-row terminal (the e2e matrix floor): 80%
79
+ * maxHeight = 19 framed lines − 2 frame borders = 17 content lines, and
80
+ * title(1) + table chrome(3) + 9 body lines + bottom rule + blank + footer
81
+ * = 16, leaving one line of headroom for a hint. The live path derives the
82
+ * window from the real terminal height via `askUserMaxVisibleForRows`.
83
+ */
84
+ export declare const ASK_USER_MAX_VISIBLE = 9;
85
+ /**
86
+ * Derive the scroll-window size from the terminal height so the window grows
87
+ * with the terminal instead of pinning to the conservative 24-row budget:
88
+ * pi-tui hard-clips the framed overlay at its resolved maxHeight, so body
89
+ * lines beyond `budget − ASK_USER_VIEW_OVERHEAD` would be invisible anyway.
90
+ * At the 24-row e2e floor this yields exactly ASK_USER_MAX_VISIBLE (9);
91
+ * larger terminals scale up proportionally. Without a usable row count it
92
+ * degrades to ASK_USER_MAX_VISIBLE.
93
+ */
94
+ export declare function askUserMaxVisibleForRows(termRows: number | undefined, maxHeight?: AskUserPanelDeps['maxHeight']): number;
64
95
  /** A row in the multi-question view. */
65
96
  export interface FlatRow {
66
97
  kind: 'question-header' | 'option' | 'sentinel' | 'confirm';
@@ -80,7 +111,14 @@ export interface PendingAnswer {
80
111
  /** Free-text answer from the sentinel path. Optional — set only when the user typed. */
81
112
  custom?: string;
82
113
  }
83
- /** Whole-state of one AskUser overlay. All setters are pure reducers. */
114
+ /**
115
+ * Whole-state of one AskUser overlay. All setters are pure reducers — with
116
+ * ONE deliberate exception: the render pass writes the clamped scroll offset
117
+ * back into `questionsScroll`/`reviewScroll` (see `clampScrollWindow`). The
118
+ * clamp depends on the rendered body height, which only exists at render
119
+ * time; persisting it here makes navigation slide smoothly instead of
120
+ * snapping, and direct render calls simply start from offset 0.
121
+ */
84
122
  export interface AskUserState {
85
123
  questions: readonly AskUserQuestionItem[];
86
124
  /** Per-question pending answer. */
@@ -101,6 +139,10 @@ export interface AskUserState {
101
139
  cancelHint: boolean;
102
140
  /** Transient "you can't do that yet" hint (e.g. Enter on an incomplete confirm row). Cleared by any navigation. */
103
141
  attentionHint: string | null;
142
+ /** Questions-pane scroll offset in rendered body lines (clamped by the render pass, see `clampScrollWindow`). */
143
+ questionsScroll: number;
144
+ /** Review-pane scroll offset in rendered rows (same mechanism). */
145
+ reviewScroll: number;
104
146
  }
105
147
  /** Initial state for a `questions` payload — defaults: cursor on Q0/option-0, no edit, no Esc history. */
106
148
  export declare function initialState(questions: readonly AskUserQuestionItem[]): AskUserState;
@@ -112,7 +154,14 @@ export declare function setCustomAnswer(state: AskUserState, questionIndex: numb
112
154
  export declare function patchCustomInput(state: AskUserState, questionIndex: number, mutator: (current: string) => string): AskUserState;
113
155
  /** Enter inline-edit mode for a question (sentinel row got pressed). */
114
156
  export declare function enterCustomEdit(state: AskUserState, questionIndex: number): AskUserState;
115
- /** Leave inline-edit mode; commit when `commit === true` and the buffer is non-empty. */
157
+ /**
158
+ * Leave inline-edit mode; commit when `commit === true` and the buffer is
159
+ * non-empty. Intentionally NEVER triggers auto-submit on its own: submission
160
+ * decisions live exclusively with the callers (`commitCustomAnswer` after an
161
+ * Enter commit, plain navigation after the ↑↓ arrow-exit path), so exiting
162
+ * the editor — especially via arrow keys mid-multi-question flow — can never
163
+ * settle the overlay as a side effect.
164
+ */
116
165
  export declare function exitCustomEdit(state: AskUserState, commit: boolean): AskUserState;
117
166
  /**
118
167
  * Double-Esc state machine: 1st press arms; 2nd within the window fires
@@ -134,6 +183,32 @@ export declare function buildRowList(questions: readonly AskUserQuestionItem[],
134
183
  export declare function nextSelectableIndex(rows: readonly FlatRow[], from: number, direction: 1 | -1): number;
135
184
  /** True when every question has at least one selected option or a non-empty custom answer. */
136
185
  export declare function allQuestionsAnswered(state: AskUserState): boolean;
186
+ /**
187
+ * Fold newlines (and the whitespace around them) into single spaces so a
188
+ * label can never break a table row into two rendered lines. Width clipping
189
+ * still happens later via `clipToWidth`.
190
+ */
191
+ export declare function foldText(text: string): string;
192
+ /**
193
+ * The scroll offset that keeps `cursor` inside `[offset, offset + visibleRows)`
194
+ * for a body of `length` rendered lines — same contract as skills.ts's
195
+ * `clampScrollOffset`, kept local so ask-user scrolling is self-contained.
196
+ * Pure; an empty body or non-positive window pins to 0.
197
+ */
198
+ export declare function clampScrollWindow(cursor: number, visibleRows: number, length: number, currentOffset: number): number;
199
+ /**
200
+ * 1-based per-question number of the selectable row at `index`: options count
201
+ * `1..N` inside their question and the sentinel continues after them (`N+1`).
202
+ * Header rows and the confirm pseudo-row are unnumbered (null). Pure so both
203
+ * the renderer and tests share one numbering vocabulary.
204
+ */
205
+ export declare function rowNumber(rows: readonly FlatRow[], index: number): number | null;
206
+ /**
207
+ * Inverse of `rowNumber` for the digit quick-pick: the index of the `n`-th
208
+ * numbered row belonging to question `qi`, or -1 when out of range. Rows are
209
+ * scanned in display order, so options come before the question's sentinel.
210
+ */
211
+ export declare function rowIndexForNumber(rows: readonly FlatRow[], qi: number, n: number): number;
137
212
  /**
138
213
  * Whether the overlay needs an explicit `⏎ Confirm answers` row: any
139
214
  * multi-question layout, plus a lone multiSelect question — multiSelect can
@@ -153,10 +228,10 @@ export declare function canAutoSubmit(state: AskUserState): boolean;
153
228
  * later question is already answered (caller keeps the current cursor).
154
229
  */
155
230
  export declare function nextUnansweredRow(rows: readonly FlatRow[], perQuestion: readonly PendingAnswer[], answeredQi: number): number;
156
- /** Render the questions pane as a flat table line list. */
157
- export declare function renderQuestionsView(theme: TuiTheme, state: AskUserState, width: number): string[];
158
- /** Render the review page for multi-question overlays. */
159
- export declare function renderReviewView(theme: TuiTheme, state: AskUserState, width: number): string[];
231
+ /** Render the questions pane as a flat table line list behind a scroll window. */
232
+ export declare function renderQuestionsView(theme: TuiTheme, state: AskUserState, width: number, maxVisible?: number): string[];
233
+ /** Render the review page for multi-question overlays (scroll-windowed). */
234
+ export declare function renderReviewView(theme: TuiTheme, state: AskUserState, width: number, maxVisible?: number): string[];
160
235
  /** Options for assembling the panel + provider function. */
161
236
  export interface AskUserPanelDeps {
162
237
  tui: TUI;