@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 +13 -12
- package/README.zh.md +7 -3
- package/lib/ask-user.d.ts +90 -15
- package/lib/ask-user.js +283 -46
- package/lib/ask-user.js.map +1 -1
- package/lib/footer.js +3 -2
- package/lib/footer.js.map +1 -1
- package/lib/session.d.ts +14 -4
- package/lib/session.js +42 -0
- package/lib/session.js.map +1 -1
- package/lib/sessions.d.ts +1 -0
- package/lib/sessions.js +5 -1
- package/lib/sessions.js.map +1 -1
- package/package.json +1 -1
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.
|
|
6
|
-
|
|
7
|
-
`
|
|
8
|
-
form
|
|
9
|
-
|
|
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
|
-
|
|
16
|
+
https://github.com/user-attachments/assets/6a7e00bb-1fd0-4bc5-9070-457f1e9fa54d
|
|
16
17
|
|
|
17
|
-
A live terminal recording of a session — todos, running
|
|
18
|
-
panels and the powerline footer in action.
|
|
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
|
|
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/ (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
https://github.com/user-attachments/assets/6a7e00bb-1fd0-4bc5-9070-457f1e9fa54d
|
|
12
16
|
|
|
13
|
-
|
|
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
|
-
* ●
|
|
13
|
-
* Where should we deploy?
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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;
|