@aiwayds/dsh-tui-pi 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -116,6 +116,7 @@ GitHub light/dark palettes, hot-switched with `/theme`; `auto` detects your term
116
116
  | `/session` | Read-only info: id, cwd, model, token usage, event count. |
117
117
  | `/resume` | Pick a persisted session (newest first), validate its log, restore it. |
118
118
  | `/new` | Detach the current session; the next prompt opens a fresh one. |
119
+ | `/btw` | By-the-way side question while the main task runs — one tool-less model call over a recent-conversation snapshot, streamed into a temporary overlay. Never kept in the session; idle main line refuses it; `--model provider/model` overrides the route; bare `/btw` reopens the last answer (`DSH_TUI_BTW_CONTEXT_MESSAGES` sizes the snapshot). |
119
120
  | `/settings` | Text-based settings browser (namespaces, schema walk, secrets masked). |
120
121
  | `/export` | Write the current session log as JSONL. |
121
122
  | `/permission` | Permission-preset picker (read-only / workspace-write / danger-full-access). |
package/README.zh-CN.md CHANGED
@@ -116,6 +116,7 @@ GitHub 明/暗配色,`/theme` 热切换;`auto` 检测你的终端并跟随
116
116
  | `/session` | 只读信息:id、cwd、model、token 用量、事件数。 |
117
117
  | `/resume` | 选择持久化的 session(新的在前),校验日志后恢复。 |
118
118
  | `/new` | 分离当前 session;下一条 prompt 开启新会话。 |
119
+ | `/btw` | 主线任务运行中的「顺带一问」——一次无工具的单次模型调用(带最近对话快照),答案流式呈现在临时浮层里。不进会话记录;主线空闲时拒绝使用;`--model provider/model` 临时换路由;空参 `/btw` 回看上一条问答(`DSH_TUI_BTW_CONTEXT_MESSAGES` 控制快照条数)。 |
119
120
  | `/settings` | 文本式设置浏览器(命名空间、schema 遍历、密钥脱敏)。 |
120
121
  | `/export` | 把当前会话日志写成 JSONL。 |
121
122
  | `/permission` | 权限预设选择器(read-only / workspace-write / danger-full-access)。 |
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The btw overlay — framed floating panel presenting one side-call exchange
3
+ * (CONTEXT.md: Btw overlay). Bound to a single BtwRunState object: a live
4
+ * run is the controller's own mutable state (setText streaming, Markdown
5
+ * once on settle), a review is a plain snapshot object from the Last-btw
6
+ * slot. Esc closes through the shared overlay focus contract — closing
7
+ * never stops the run; it delivers into the slot regardless.
8
+ *
9
+ * Pure rendering + key routing only; all decisions live in btw.ts. This file
10
+ * is the PanelHost glue and is exercised on a terminal, not in unit tests
11
+ * (same split as route-dialog).
12
+ */
13
+ import { type Component, type TUI } from '@earendil-works/pi-tui';
14
+ import { type TuiTheme } from './theme/index.ts';
15
+ import type { BtwController, BtwRunState } from './btw.ts';
16
+ export declare class BtwOverlayPanel implements Component {
17
+ private readonly run;
18
+ private readonly controller;
19
+ private readonly theme;
20
+ private readonly onClose;
21
+ private readonly question;
22
+ /** Streaming view — one Text, setText per change (AGENTS.md #2). */
23
+ private readonly answerText;
24
+ private streamedText;
25
+ /** Built once when the run settles; never re-parsed per frame. */
26
+ private finalMarkdown;
27
+ constructor(run: BtwRunState, controller: BtwController, theme: () => TuiTheme, onClose: () => void);
28
+ invalidate(): void;
29
+ render(width: number): string[];
30
+ handleInput(data: string): void;
31
+ }
32
+ /**
33
+ * Overlay lifecycle for btw runs: `open(run)` shows/swaps the framed panel
34
+ * bound to that run state; Esc and cancelAll close it through restoreFocus.
35
+ * Two-step wiring — construct first, `attach(controller)` once it exists
36
+ * (the panel needs the controller for the queued count; the controller needs
37
+ * these callbacks at construction).
38
+ */
39
+ export declare class BtwOverlayWire {
40
+ private readonly tui;
41
+ private readonly theme;
42
+ private readonly restoreFocus;
43
+ private host;
44
+ private controller;
45
+ constructor(deps: {
46
+ tui: TUI;
47
+ theme: () => TuiTheme;
48
+ restoreFocus: () => void;
49
+ });
50
+ attach(controller: BtwController): void;
51
+ /** Called by the controller for every launched or reviewed run. */
52
+ open(run: BtwRunState): void;
53
+ /** Called by cancelAll — the overlay's run is gone either way. */
54
+ requestClose(): void;
55
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The btw overlay — framed floating panel presenting one side-call exchange
3
+ * (CONTEXT.md: Btw overlay). Bound to a single BtwRunState object: a live
4
+ * run is the controller's own mutable state (setText streaming, Markdown
5
+ * once on settle), a review is a plain snapshot object from the Last-btw
6
+ * slot. Esc closes through the shared overlay focus contract — closing
7
+ * never stops the run; it delivers into the slot regardless.
8
+ *
9
+ * Pure rendering + key routing only; all decisions live in btw.ts. This file
10
+ * is the PanelHost glue and is exercised on a terminal, not in unit tests
11
+ * (same split as route-dialog).
12
+ */
13
+ import { getKeybindings, Markdown, Text } from '@earendil-works/pi-tui';
14
+ import { PanelHost, panelThemeFns } from "./panels.js";
15
+ import { BOLD, RESET, ansiFg } from "./theme/index.js";
16
+ import { clipToWidth } from "./text.js";
17
+ /**
18
+ * Answer row budget, sized so the panel NEVER overflows the 24-row e2e
19
+ * terminal (AGENTS.md matrix floor): maxHeight '80%' ≈ 19 rows there, the
20
+ * FramedOverlay chrome eats 4, fixed rows are 4 (title + 2 blanks + status),
21
+ * and the question rows come out of the remainder per frame — an overlay
22
+ * taller than that just shows a roomier answer. Overflow of the answer
23
+ * itself is a named tail window (the newest rows stay visible).
24
+ */
25
+ const BTW_ANSWER_BASE_BUDGET = 11;
26
+ const BTW_ANSWER_MIN_BUDGET = 3;
27
+ export class BtwOverlayPanel {
28
+ run;
29
+ controller;
30
+ theme;
31
+ onClose;
32
+ question;
33
+ /** Streaming view — one Text, setText per change (AGENTS.md #2). */
34
+ answerText;
35
+ streamedText = '';
36
+ /** Built once when the run settles; never re-parsed per frame. */
37
+ finalMarkdown;
38
+ constructor(run, controller, theme, onClose) {
39
+ this.run = run;
40
+ this.controller = controller;
41
+ this.theme = theme;
42
+ this.onClose = onClose;
43
+ this.question = new Text(run.question, 1, 0);
44
+ this.answerText = new Text('', 1, 0);
45
+ }
46
+ invalidate() { }
47
+ render(width) {
48
+ const fns = panelThemeFns(this.theme());
49
+ const wrap = Math.max(2, width - 2);
50
+ const questionLines = this.question.render(wrap);
51
+ const budget = Math.max(BTW_ANSWER_MIN_BUDGET, BTW_ANSWER_BASE_BUDGET - questionLines.length);
52
+ const lines = [
53
+ fns.accent(BOLD + clipToWidth(`⌘ btw — ${this.run.modelLabel}`, wrap) + RESET),
54
+ ...questionLines,
55
+ '',
56
+ ];
57
+ const queued = this.controller.queuedCount;
58
+ if (this.run.status === 'streaming') {
59
+ if (this.run.answerText !== this.streamedText) {
60
+ this.answerText.setText(this.run.answerText);
61
+ this.streamedText = this.run.answerText;
62
+ }
63
+ if (this.run.answerText !== '') {
64
+ lines.push(...tailWindow(this.answerText.render(wrap), budget, wrap));
65
+ }
66
+ else {
67
+ lines.push(fns.muted(clipToWidth('Thinking…', wrap)));
68
+ }
69
+ lines.push('');
70
+ lines.push(fns.subtle(clipToWidth(`Running alongside the main task · Esc close${queued > 0 ? ` · ${queued} queued` : ''}`, wrap)));
71
+ return lines;
72
+ }
73
+ if (this.run.status === 'error') {
74
+ lines.push(fns.muted(clipToWidth(`✘ ${this.run.error ?? 'btw failed.'}`, wrap)));
75
+ }
76
+ else if (this.run.answerText === '') {
77
+ lines.push(fns.muted(clipToWidth('(no answer text)', wrap)));
78
+ }
79
+ else {
80
+ if (this.finalMarkdown === undefined) {
81
+ // Parses ONCE on the settled answer — never per frame, per token.
82
+ this.finalMarkdown = new Markdown(this.run.answerText, 1, 0, this.theme().markdown, {
83
+ color: text => ansiFg(this.theme().palette.fgDefault) + text + RESET,
84
+ });
85
+ }
86
+ lines.push(...tailWindow(this.finalMarkdown.render(wrap), budget, wrap));
87
+ }
88
+ lines.push('');
89
+ lines.push(fns.subtle(clipToWidth(`Not kept in the session · /btw reopens this answer${queued > 0 ? ` · ${queued} queued` : ''}`, wrap)));
90
+ return lines;
91
+ }
92
+ handleInput(data) {
93
+ if (getKeybindings().matches(data, 'tui.select.cancel'))
94
+ this.onClose();
95
+ }
96
+ }
97
+ /** Last rows that fit the budget; the hidden count is named above the tail. */
98
+ function tailWindow(rendered, budget, wrap) {
99
+ if (rendered.length <= budget)
100
+ return rendered;
101
+ const hidden = rendered.length - budget;
102
+ return [clipToWidth(`… ${hidden} lines above`, wrap), ...rendered.slice(-budget)];
103
+ }
104
+ /**
105
+ * Overlay lifecycle for btw runs: `open(run)` shows/swaps the framed panel
106
+ * bound to that run state; Esc and cancelAll close it through restoreFocus.
107
+ * Two-step wiring — construct first, `attach(controller)` once it exists
108
+ * (the panel needs the controller for the queued count; the controller needs
109
+ * these callbacks at construction).
110
+ */
111
+ export class BtwOverlayWire {
112
+ tui;
113
+ theme;
114
+ restoreFocus;
115
+ host;
116
+ controller;
117
+ constructor(deps) {
118
+ this.tui = deps.tui;
119
+ this.theme = deps.theme;
120
+ this.restoreFocus = deps.restoreFocus;
121
+ }
122
+ attach(controller) {
123
+ this.controller = controller;
124
+ }
125
+ /** Called by the controller for every launched or reviewed run. */
126
+ open(run) {
127
+ const controller = this.controller;
128
+ if (controller === undefined)
129
+ return;
130
+ // A failed PanelHost.open releases the keyboard exactly like a normal
131
+ // close (PanelHost calls onError first); the run itself is unaffected.
132
+ const close = () => {
133
+ this.host?.close();
134
+ this.host = undefined;
135
+ controller.setOverlayOpen(false);
136
+ this.restoreFocus();
137
+ };
138
+ this.host?.close();
139
+ this.host = new PanelHost(this.tui, this.theme(), close);
140
+ controller.setOverlayOpen(true);
141
+ this.host.open(new BtwOverlayPanel(run, controller, this.theme, close), '70%', '80%');
142
+ }
143
+ /** Called by cancelAll — the overlay's run is gone either way. */
144
+ requestClose() {
145
+ if (this.host !== undefined) {
146
+ this.host.close();
147
+ this.host = undefined;
148
+ this.controller?.setOverlayOpen(false);
149
+ this.restoreFocus();
150
+ }
151
+ }
152
+ }
153
+ //# sourceMappingURL=btw-overlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"btw-overlay.js","sourceRoot":"","sources":["../src/btw-overlay.ts"],"names":[],"mappings":"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"}
package/lib/btw.d.ts ADDED
@@ -0,0 +1,224 @@
1
+ /**
2
+ * /btw — by-the-way side questions (CONTEXT.md: Btw, Side call, Btw overlay,
3
+ * Last-btw slot, Queued btw).
4
+ *
5
+ * While the main agent is mid-turn, `/btw <question>` fires ONE tool-less
6
+ * one-shot model call over a read-only snapshot of the recent conversation
7
+ * and streams the answer into a framed overlay. The main line never sees it:
8
+ * nothing enters the session log, the inbox, or any main-line model request.
9
+ * The overlay is the only surface; closing it (Esc) does not stop the call —
10
+ * the finished exchange lands in the in-process Last-btw slot, reviewable
11
+ * via bare `/btw` until the process dies.
12
+ *
13
+ * Concurrency: at most one side call runs (BtwQueue); further submits queue
14
+ * behind it (bounded). Lifecycle: a main-line disruption — /new, /resume,
15
+ * a remote takeover, the stop gesture — cancels the running call and drops
16
+ * the queue; the caller wires `cancelAll` into those paths. Placement rationale:
17
+ * docs/adr/0001-btw-tui-owned-command.md.
18
+ *
19
+ * Split: pure decision layer here — arg parsing, snapshot assembly, the queue
20
+ * state machine, stream consumption and the controller, all over structural
21
+ * slices so the matrix runs without a terminal (test/btw.test.mjs). The
22
+ * overlay component + PanelHost glue live in btw-overlay.ts; the command
23
+ * wiring in index.ts.
24
+ */
25
+ import { type Message, type ReasoningEffortId } from '@deepseek-ai/dsh-llm';
26
+ export declare const BTW_IDLE_NOTICE = "/btw answers alongside a running turn \u2014 the main line is idle, so just ask directly.";
27
+ export declare const BTW_USAGE: string;
28
+ export type ParsedBtwInput = {
29
+ kind: 'empty';
30
+ } | {
31
+ kind: 'ok';
32
+ question: string;
33
+ modelOverride?: string;
34
+ } | {
35
+ kind: 'error';
36
+ error: string;
37
+ };
38
+ /**
39
+ * Parse the text after `/btw`. Everything is the question except one
40
+ * optional `--model provider/model` override (extractable from anywhere in
41
+ * the line). No input at all → `'empty'` (review / usage hint); a `--model`
42
+ * flag without a question, or a value without a `/`, → `'error'`.
43
+ */
44
+ export declare function parseBtwInput(rawInput: string | undefined): ParsedBtwInput;
45
+ /** Structural input event — the subset of SessionEvent the snapshot needs. */
46
+ export interface BtwSnapshotEvent {
47
+ readonly type: string;
48
+ readonly data: unknown;
49
+ }
50
+ /** Default recent-conversation messages carried into a side call. */
51
+ export declare const BTW_SNAPSHOT_DEFAULT_MESSAGES = 6;
52
+ /** Hard ceiling for the configurable snapshot size (env override clamp). */
53
+ export declare const BTW_SNAPSHOT_MAX_MESSAGES = 50;
54
+ /** Per-message text cap — the snapshot is context, not a transcript replay. */
55
+ export declare const BTW_MAX_MESSAGE_CHARS = 4000;
56
+ /**
57
+ * Resolve the snapshot size from the `DSH_TUI_BTW_CONTEXT_MESSAGES` env value:
58
+ * integer clamped to [0, BTW_SNAPSHOT_MAX_MESSAGES], anything else (unset,
59
+ * non-numeric) falls back to the default. 0 disables the snapshot — the side
60
+ * call then answers from the question alone.
61
+ */
62
+ export declare function resolveSnapshotLimit(env: string | undefined): number;
63
+ /**
64
+ * Project the last `limit` user/assistant text exchanges out of the session
65
+ * event log, oldest first. Tool calls, reasoning, usage and every
66
+ * non-message event are dropped; text-only fresh blocks keep each source
67
+ * message's identity (id/role/source) while shedding everything a tool-less
68
+ * call cannot use. Malformed event data is skipped, never thrown.
69
+ */
70
+ export declare function buildBtwSnapshot(events: readonly BtwSnapshotEvent[], limit: number): Message[];
71
+ /**
72
+ * The side-call message list: the snapshot in order, then the question as a
73
+ * plugin-sourced user message (it is not a real user turn of any session).
74
+ */
75
+ export declare function buildBtwMessages(snapshot: readonly Message[], question: string): Message[];
76
+ /** System prompt for the side call — no tools, snapshot is context only. */
77
+ export declare const BTW_SYSTEM_PROMPT: string;
78
+ /** Maximum queued btw requests while one is running. */
79
+ export declare const BTW_QUEUE_CAP = 5;
80
+ export interface BtwJob {
81
+ readonly question: string;
82
+ readonly modelOverride?: string;
83
+ }
84
+ export type BtwSubmitResult = {
85
+ kind: 'started';
86
+ } | {
87
+ kind: 'queued';
88
+ position: number;
89
+ } | {
90
+ kind: 'rejected';
91
+ reason: string;
92
+ };
93
+ /**
94
+ * Single-flight queue: one running btw, bounded FIFO behind it. The queue is
95
+ * the concurrency truth — the controller's view state is derived from it.
96
+ */
97
+ export declare class BtwQueue {
98
+ private currentJob;
99
+ private readonly waiting;
100
+ get running(): boolean;
101
+ get queuedCount(): number;
102
+ submit(job: BtwJob): BtwSubmitResult;
103
+ /** Settle the current job; returns the next job to launch, if any. */
104
+ finishCurrent(): BtwJob | undefined;
105
+ cancelAll(): {
106
+ canceledRunning: boolean;
107
+ canceledQueued: number;
108
+ };
109
+ }
110
+ /**
111
+ * Structural stream chunk — the subset of dsh-llm's StreamChunk a side call
112
+ * consumes (text deltas for the live view, the finish chunk for the outcome).
113
+ */
114
+ export interface BtwStreamChunk {
115
+ readonly type: string;
116
+ readonly text?: string;
117
+ readonly reason?: {
118
+ readonly kind: string;
119
+ readonly failure?: {
120
+ readonly message?: string;
121
+ };
122
+ };
123
+ }
124
+ export interface BtwCallOptions {
125
+ provider: string;
126
+ model: string;
127
+ reasoningEffort?: ReasoningEffortId;
128
+ messages: Message[];
129
+ system: string;
130
+ signal?: AbortSignal;
131
+ }
132
+ export type BtwStreamFn = (options: BtwCallOptions) => AsyncIterable<BtwStreamChunk>;
133
+ export type BtwFinish = {
134
+ kind: 'stop';
135
+ answer: string;
136
+ } | {
137
+ kind: 'aborted';
138
+ } | {
139
+ kind: 'error';
140
+ message: string;
141
+ };
142
+ /**
143
+ * Drain one side-call stream: forward text deltas as they arrive, map the
144
+ * finish chunk (or the stream's end / a throw) to a terminal outcome. A
145
+ * stream that ends without a finish chunk is an error, never a silent
146
+ * success; an aborted signal wins over whatever the iterator does next.
147
+ */
148
+ export declare function consumeBtwStream(chunks: AsyncIterable<BtwStreamChunk>, onDelta: (text: string) => void, signal?: AbortSignal): Promise<BtwFinish>;
149
+ export type BtwRunStatus = 'streaming' | 'done' | 'error' | 'canceled';
150
+ /** View state of the running (or just-finished) side call; overlay-facing. */
151
+ export interface BtwRunState {
152
+ readonly question: string;
153
+ readonly modelLabel: string;
154
+ status: BtwRunStatus;
155
+ answerText: string;
156
+ error?: string;
157
+ }
158
+ /** The Last-btw slot — the in-process record of the most recent exchange. */
159
+ export interface BtwLastExchange {
160
+ readonly question: string;
161
+ readonly answer: string;
162
+ readonly modelLabel: string;
163
+ }
164
+ export type BtwNoticeKind = 'info' | 'error' | 'warning';
165
+ export interface BtwSelection {
166
+ provider: string;
167
+ model: string;
168
+ reasoningEffort?: ReasoningEffortId;
169
+ }
170
+ export interface BtwControllerDeps {
171
+ stream: BtwStreamFn;
172
+ resolveSelection: () => BtwSelection | undefined;
173
+ buildSnapshot: () => readonly Message[];
174
+ requestRender: () => void;
175
+ notify: (message: string, kind: BtwNoticeKind) => void;
176
+ /** The glue opens (or swaps) the overlay for a launched run. */
177
+ onRunStarted: (run: BtwRunState) => void;
178
+ /** cancelAll: the glue closes the overlay (the run is gone either way). */
179
+ onOverlayRequestedClose: () => void;
180
+ /**
181
+ * Whether a capturing surface (overlay / docked ask-user panel) currently
182
+ * owns the keyboard. Queue-drained launches under one skip the popup and
183
+ * run into the slot instead — the answer is reviewable via bare /btw.
184
+ */
185
+ hasCapturingSurface?: () => boolean;
186
+ }
187
+ /**
188
+ * Owns the btw concurrency, the side-call execution and the Last-btw slot.
189
+ * The overlay glue (btw-overlay.ts) renders `currentRun` / `last` on every
190
+ * frame and reports overlay open/close back through `setOverlayOpen`.
191
+ */
192
+ export declare class BtwController {
193
+ private readonly deps;
194
+ private readonly queue;
195
+ private abortController;
196
+ private run;
197
+ private lastExchange;
198
+ private overlayOpen;
199
+ constructor(deps: BtwControllerDeps);
200
+ /** View state for the overlay: the live/just-settled run wins over the slot. */
201
+ get currentRun(): BtwRunState | undefined;
202
+ get last(): BtwLastExchange | undefined;
203
+ get queuedCount(): number;
204
+ /** The glue reports overlay lifecycle so error notices know where to land. */
205
+ setOverlayOpen(open: boolean): void;
206
+ submit(job: BtwJob): BtwSubmitResult;
207
+ /**
208
+ * Bare `/btw`: an active run reopens live on its overlay; otherwise the
209
+ * Last-btw slot is shown; nothing at all → the caller shows the usage.
210
+ */
211
+ openReview(): 'live' | 'review' | 'empty';
212
+ /**
213
+ * Main-line disruption (/new, /resume, remote takeover, stop gesture):
214
+ * abort the running call, drop the queue, close the overlay. The slot is
215
+ * untouched — a canceled run never overwrites the last completed one.
216
+ */
217
+ cancelAll(): void;
218
+ /** TUI teardown: same as cancelAll without the overlay ceremony. */
219
+ dispose(): void;
220
+ private launch;
221
+ private execute;
222
+ /** Settle the queue slot; a queued job (if any) launches immediately. */
223
+ private drain;
224
+ }