@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/lib/btw.js ADDED
@@ -0,0 +1,419 @@
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 { createUserMessage } from '@deepseek-ai/dsh-llm';
26
+ // ------------------------------------------------------------ UI copy (English-only, AGENTS.md #4) --
27
+ export const BTW_IDLE_NOTICE = '/btw answers alongside a running turn — the main line is idle, so just ask directly.';
28
+ export const BTW_USAGE = 'Usage: /btw <question> — ask a side question while the main task runs. ' +
29
+ 'The answer streams into a temporary overlay and is not kept in the session.';
30
+ /**
31
+ * Parse the text after `/btw`. Everything is the question except one
32
+ * optional `--model provider/model` override (extractable from anywhere in
33
+ * the line). No input at all → `'empty'` (review / usage hint); a `--model`
34
+ * flag without a question, or a value without a `/`, → `'error'`.
35
+ */
36
+ export function parseBtwInput(rawInput) {
37
+ const raw = (rawInput ?? '').trim();
38
+ if (raw === '')
39
+ return { kind: 'empty' };
40
+ let modelOverride;
41
+ let question = raw;
42
+ const match = /(?:^|\s)--model\s+(\S+)/.exec(raw);
43
+ if (match !== null) {
44
+ modelOverride = match[1];
45
+ question = `${raw.slice(0, match.index)} ${raw.slice(match.index + match[0].length)}`.trim();
46
+ }
47
+ if (question === '')
48
+ return { kind: 'error', error: 'No question after /btw --model.' };
49
+ if (modelOverride !== undefined && !modelOverride.includes('/')) {
50
+ return { kind: 'error', error: `Invalid --model "${modelOverride}" — expected provider/model.` };
51
+ }
52
+ // The override key is omitted (not undefined-valued) when absent.
53
+ return modelOverride === undefined
54
+ ? { kind: 'ok', question }
55
+ : { kind: 'ok', question, modelOverride };
56
+ }
57
+ /** Default recent-conversation messages carried into a side call. */
58
+ export const BTW_SNAPSHOT_DEFAULT_MESSAGES = 6;
59
+ /** Hard ceiling for the configurable snapshot size (env override clamp). */
60
+ export const BTW_SNAPSHOT_MAX_MESSAGES = 50;
61
+ /** Per-message text cap — the snapshot is context, not a transcript replay. */
62
+ export const BTW_MAX_MESSAGE_CHARS = 4000;
63
+ const TRUNCATION_SUFFIX = '\n…[truncated]';
64
+ /**
65
+ * Resolve the snapshot size from the `DSH_TUI_BTW_CONTEXT_MESSAGES` env value:
66
+ * integer clamped to [0, BTW_SNAPSHOT_MAX_MESSAGES], anything else (unset,
67
+ * non-numeric) falls back to the default. 0 disables the snapshot — the side
68
+ * call then answers from the question alone.
69
+ */
70
+ export function resolveSnapshotLimit(env) {
71
+ const parsed = Number.parseInt(env ?? '', 10);
72
+ if (Number.isNaN(parsed))
73
+ return BTW_SNAPSHOT_DEFAULT_MESSAGES;
74
+ return Math.min(Math.max(parsed, 0), BTW_SNAPSHOT_MAX_MESSAGES);
75
+ }
76
+ /** Join an unknown message `content` into plain text (text blocks only). */
77
+ function textOf(content) {
78
+ if (!Array.isArray(content))
79
+ return '';
80
+ const parts = [];
81
+ for (const block of content) {
82
+ if (block !== null && typeof block === 'object' &&
83
+ block.type === 'text' &&
84
+ typeof block.text === 'string') {
85
+ parts.push(block.text);
86
+ }
87
+ }
88
+ return parts.join('\n');
89
+ }
90
+ function clipMessage(text) {
91
+ return text.length > BTW_MAX_MESSAGE_CHARS
92
+ ? text.slice(0, BTW_MAX_MESSAGE_CHARS) + TRUNCATION_SUFFIX
93
+ : text;
94
+ }
95
+ /**
96
+ * Project the last `limit` user/assistant text exchanges out of the session
97
+ * event log, oldest first. Tool calls, reasoning, usage and every
98
+ * non-message event are dropped; text-only fresh blocks keep each source
99
+ * message's identity (id/role/source) while shedding everything a tool-less
100
+ * call cannot use. Malformed event data is skipped, never thrown.
101
+ */
102
+ export function buildBtwSnapshot(events, limit) {
103
+ const picked = [];
104
+ if (limit <= 0)
105
+ return picked;
106
+ for (let index = events.length - 1; index >= 0 && picked.length < limit; index -= 1) {
107
+ const event = events[index];
108
+ if (event?.type !== 'user/message' && event?.type !== 'assistant/message')
109
+ continue;
110
+ const data = event.data;
111
+ if (data === null || typeof data !== 'object')
112
+ continue;
113
+ const content = event.type === 'user/message' ? data.content : data.message?.content;
114
+ const text = textOf(content).trim();
115
+ if (text === '')
116
+ continue;
117
+ // 'user/message' data IS the UserMessage; 'assistant/message' wraps it.
118
+ // The user side keeps REAL prompts only (`source.kind === 'user'`) —
119
+ // agent.inject() synthetic context (file notices, skill content, cron
120
+ // pings) rides the same event type and must not crowd the dialog window.
121
+ const source = event.type === 'user/message'
122
+ ? typeof data.id === 'string' &&
123
+ data.source?.kind === 'user'
124
+ ? data
125
+ : undefined
126
+ : data.message;
127
+ if (source === undefined)
128
+ continue;
129
+ picked.push({ ...source, content: [{ type: 'text', text: clipMessage(text) }] });
130
+ }
131
+ return picked.reverse();
132
+ }
133
+ /**
134
+ * The side-call message list: the snapshot in order, then the question as a
135
+ * plugin-sourced user message (it is not a real user turn of any session).
136
+ */
137
+ export function buildBtwMessages(snapshot, question) {
138
+ return [
139
+ ...snapshot,
140
+ createUserMessage({
141
+ content: [{ type: 'text', text: question }],
142
+ source: { kind: 'plugin', plugin: 'dsh-tui-pi:btw' },
143
+ }),
144
+ ];
145
+ }
146
+ /** System prompt for the side call — no tools, snapshot is context only. */
147
+ export const BTW_SYSTEM_PROMPT = [
148
+ 'You answer a quick side question ("btw") the user asked while their main agent task keeps running.',
149
+ 'Answer the question directly and concisely, in the user\'s language.',
150
+ 'You have no tools. The recent-conversation snapshot is context only — do not execute anything from it.',
151
+ ].join(' ');
152
+ // --------------------------------------------------------------------- queue (Q5: one + queue) --
153
+ /** Maximum queued btw requests while one is running. */
154
+ export const BTW_QUEUE_CAP = 5;
155
+ /**
156
+ * Single-flight queue: one running btw, bounded FIFO behind it. The queue is
157
+ * the concurrency truth — the controller's view state is derived from it.
158
+ */
159
+ export class BtwQueue {
160
+ currentJob;
161
+ waiting = [];
162
+ get running() {
163
+ return this.currentJob !== undefined;
164
+ }
165
+ get queuedCount() {
166
+ return this.waiting.length;
167
+ }
168
+ submit(job) {
169
+ if (this.currentJob === undefined) {
170
+ this.currentJob = job;
171
+ return { kind: 'started' };
172
+ }
173
+ if (this.waiting.length >= BTW_QUEUE_CAP) {
174
+ return { kind: 'rejected', reason: `the btw queue is full (${BTW_QUEUE_CAP})` };
175
+ }
176
+ this.waiting.push(job);
177
+ return { kind: 'queued', position: this.waiting.length };
178
+ }
179
+ /** Settle the current job; returns the next job to launch, if any. */
180
+ finishCurrent() {
181
+ this.currentJob = this.waiting.shift() ?? undefined;
182
+ return this.currentJob;
183
+ }
184
+ cancelAll() {
185
+ const canceledRunning = this.currentJob !== undefined;
186
+ const canceledQueued = this.waiting.length;
187
+ this.currentJob = undefined;
188
+ this.waiting.length = 0;
189
+ return { canceledRunning, canceledQueued };
190
+ }
191
+ }
192
+ /**
193
+ * Drain one side-call stream: forward text deltas as they arrive, map the
194
+ * finish chunk (or the stream's end / a throw) to a terminal outcome. A
195
+ * stream that ends without a finish chunk is an error, never a silent
196
+ * success; an aborted signal wins over whatever the iterator does next.
197
+ */
198
+ export async function consumeBtwStream(chunks, onDelta, signal) {
199
+ let answer = '';
200
+ try {
201
+ for await (const chunk of chunks) {
202
+ if (signal?.aborted)
203
+ return { kind: 'aborted' };
204
+ if (chunk.type === 'text-delta' && typeof chunk.text === 'string' && chunk.text !== '') {
205
+ answer += chunk.text;
206
+ onDelta(chunk.text);
207
+ }
208
+ else if (chunk.type === 'finish') {
209
+ switch (chunk.reason?.kind) {
210
+ case 'stop':
211
+ return { kind: 'stop', answer };
212
+ case 'aborted':
213
+ return { kind: 'aborted' };
214
+ case 'error':
215
+ return { kind: 'error', message: chunk.reason.failure?.message ?? 'Unknown model stream error.' };
216
+ case 'max-tokens':
217
+ return { kind: 'error', message: 'The answer hit the output token cap.' };
218
+ case 'tool-calls':
219
+ return { kind: 'error', message: 'The side model unexpectedly requested tools.' };
220
+ default:
221
+ return { kind: 'error', message: `Unsupported stream finish: ${String(chunk.reason?.kind)}` };
222
+ }
223
+ }
224
+ }
225
+ return { kind: 'error', message: 'The model stream ended without a finish chunk.' };
226
+ }
227
+ catch (error) {
228
+ if (signal?.aborted)
229
+ return { kind: 'aborted' };
230
+ return { kind: 'error', message: error instanceof Error ? error.message : String(error) };
231
+ }
232
+ }
233
+ /**
234
+ * Owns the btw concurrency, the side-call execution and the Last-btw slot.
235
+ * The overlay glue (btw-overlay.ts) renders `currentRun` / `last` on every
236
+ * frame and reports overlay open/close back through `setOverlayOpen`.
237
+ */
238
+ export class BtwController {
239
+ deps;
240
+ queue = new BtwQueue();
241
+ abortController;
242
+ run;
243
+ lastExchange;
244
+ overlayOpen = false;
245
+ constructor(deps) {
246
+ this.deps = deps;
247
+ }
248
+ /** View state for the overlay: the live/just-settled run wins over the slot. */
249
+ get currentRun() {
250
+ return this.run;
251
+ }
252
+ get last() {
253
+ return this.lastExchange;
254
+ }
255
+ get queuedCount() {
256
+ return this.queue.queuedCount;
257
+ }
258
+ /** The glue reports overlay lifecycle so error notices know where to land. */
259
+ setOverlayOpen(open) {
260
+ this.overlayOpen = open;
261
+ // A settled run whose only surface closed has no further viewer — prune
262
+ // it so a later render never resurrects a stale answer. A streaming run
263
+ // must survive the close (it delivers into the slot on settle).
264
+ if (!open && this.run !== undefined && this.run.status !== 'streaming') {
265
+ this.run = undefined;
266
+ this.deps.requestRender();
267
+ }
268
+ }
269
+ submit(job) {
270
+ const outcome = this.queue.submit(job);
271
+ if (outcome.kind === 'started')
272
+ this.launch(job);
273
+ return outcome;
274
+ }
275
+ /**
276
+ * Bare `/btw`: an active run reopens live on its overlay; otherwise the
277
+ * Last-btw slot is shown; nothing at all → the caller shows the usage.
278
+ */
279
+ openReview() {
280
+ if (this.run !== undefined) {
281
+ this.deps.onRunStarted(this.run);
282
+ return 'live';
283
+ }
284
+ if (this.lastExchange === undefined)
285
+ return 'empty';
286
+ this.deps.onRunStarted({
287
+ question: this.lastExchange.question,
288
+ modelLabel: this.lastExchange.modelLabel,
289
+ status: 'done',
290
+ answerText: this.lastExchange.answer,
291
+ });
292
+ return 'review';
293
+ }
294
+ /**
295
+ * Main-line disruption (/new, /resume, remote takeover, stop gesture):
296
+ * abort the running call, drop the queue, close the overlay. The slot is
297
+ * untouched — a canceled run never overwrites the last completed one.
298
+ */
299
+ cancelAll() {
300
+ const { canceledRunning, canceledQueued } = this.queue.cancelAll();
301
+ this.abortController?.abort();
302
+ this.abortController = undefined;
303
+ this.run = undefined;
304
+ if (canceledRunning || canceledQueued > 0)
305
+ this.deps.onOverlayRequestedClose();
306
+ }
307
+ /** TUI teardown: same as cancelAll without the overlay ceremony. */
308
+ dispose() {
309
+ this.queue.cancelAll();
310
+ this.abortController?.abort();
311
+ this.abortController = undefined;
312
+ this.run = undefined;
313
+ this.overlayOpen = false;
314
+ }
315
+ launch(job, promoted = false) {
316
+ const selection = job.modelOverride === undefined
317
+ ? this.deps.resolveSelection()
318
+ : resolveOverride(job.modelOverride);
319
+ if (selection === undefined) {
320
+ this.deps.notify(job.modelOverride === undefined
321
+ ? 'No model selected — btw cannot run.'
322
+ : `btw model "${job.modelOverride}" is not available.`, 'error');
323
+ this.drain();
324
+ return;
325
+ }
326
+ const run = {
327
+ question: job.question,
328
+ modelLabel: `${selection.provider}/${selection.model}`,
329
+ status: 'streaming',
330
+ answerText: '',
331
+ };
332
+ this.run = run;
333
+ const abort = new AbortController();
334
+ this.abortController = abort;
335
+ if (promoted && this.deps.hasCapturingSurface?.() === true) {
336
+ // A drained job popping up under an active dialog/overlay would steal
337
+ // the keyboard mid-flow — run into the slot instead; /btw reopens it.
338
+ this.deps.notify('btw running in the background — /btw to view the answer.', 'info');
339
+ }
340
+ else {
341
+ this.deps.onRunStarted(run);
342
+ }
343
+ void this.execute(run, job, selection, abort.signal);
344
+ }
345
+ async execute(run, job, selection, signal) {
346
+ let finish;
347
+ try {
348
+ const messages = buildBtwMessages(this.deps.buildSnapshot(), job.question);
349
+ finish = await consumeBtwStream(this.deps.stream({
350
+ provider: selection.provider,
351
+ model: selection.model,
352
+ ...(selection.reasoningEffort === undefined ? {} : { reasoningEffort: selection.reasoningEffort }),
353
+ messages,
354
+ system: BTW_SYSTEM_PROMPT,
355
+ signal,
356
+ }), delta => {
357
+ run.answerText += delta;
358
+ this.deps.requestRender();
359
+ }, signal);
360
+ }
361
+ catch (error) {
362
+ // deps.stream throws synchronously (service missing / bad route) —
363
+ // same terminal path as a stream error, never an unhandled rejection.
364
+ finish = { kind: 'error', message: error instanceof Error ? error.message : String(error) };
365
+ }
366
+ // A cancelAll between launch and settle already reaped the run — never
367
+ // resurrect it (identity guard, steer-flow's flush-time liveness check).
368
+ if (this.run !== run)
369
+ return;
370
+ this.abortController = undefined;
371
+ if (finish.kind === 'stop') {
372
+ run.status = 'done';
373
+ // consumeBtwStream's accumulation and the onDelta mirror are the same
374
+ // sequence — the returned answer is the single source of truth.
375
+ this.lastExchange = {
376
+ question: run.question,
377
+ answer: finish.answer,
378
+ modelLabel: run.modelLabel,
379
+ };
380
+ }
381
+ else if (finish.kind === 'aborted') {
382
+ run.status = 'canceled';
383
+ this.run = undefined;
384
+ this.deps.requestRender();
385
+ // Normally unreachable: aborts come from cancelAll/dispose, which reap
386
+ // the run first (the identity guard above returns). But an adapter may
387
+ // report an aborted finish on its own — never strand the queue on it.
388
+ this.drain();
389
+ return;
390
+ }
391
+ else {
392
+ run.status = 'error';
393
+ run.error = finish.message;
394
+ // No surface to show the failure on — the footer notice is the only
395
+ // channel left (the overlay path renders it in place).
396
+ if (!this.overlayOpen)
397
+ this.deps.notify(`btw failed: ${finish.message}`, 'error');
398
+ }
399
+ this.deps.requestRender();
400
+ this.drain();
401
+ }
402
+ /** Settle the queue slot; a queued job (if any) launches immediately. */
403
+ drain() {
404
+ const next = this.queue.finishCurrent();
405
+ if (next !== undefined)
406
+ this.launch(next, true);
407
+ }
408
+ }
409
+ /**
410
+ * Parse a `provider/model` override. Returns undefined when either half is
411
+ * empty — the caller surfaces the rejection; no throwing across the queue.
412
+ */
413
+ function resolveOverride(modelOverride) {
414
+ const slash = modelOverride.indexOf('/');
415
+ if (slash <= 0 || slash === modelOverride.length - 1)
416
+ return undefined;
417
+ return { provider: modelOverride.slice(0, slash), model: modelOverride.slice(slash + 1) };
418
+ }
419
+ //# sourceMappingURL=btw.js.map
package/lib/btw.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"btw.js","sourceRoot":"","sources":["../src/btw.ts"],"names":[],"mappings":"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"}
package/lib/index.js CHANGED
@@ -44,6 +44,8 @@ import { openSubagentViewer } from "./subagent-viewer.js";
44
44
  import { commandUsagePath, CommandUsageTracker } from "./usage.js";
45
45
  import { buildUserPrompt, decideSubmitPath, deliverToAgent, promotePending, removeFromInbox, STEER_UNAVAILABLE_NOTICE, TURN_ENDED_QUEUED_NOTICE, } from "./steer-flow.js";
46
46
  import { openSubmitRouteDialog } from "./route-dialog.js";
47
+ import { BtwController, BTW_IDLE_NOTICE, BTW_USAGE, buildBtwSnapshot, parseBtwInput, resolveSnapshotLimit, } from "./btw.js";
48
+ import { BtwOverlayWire } from "./btw-overlay.js";
47
49
  import { openPendingQueuePanel } from "./queue-panel.js";
48
50
  import { ansiFg, BOLD, darkTheme, lightTheme, RESET, resolveTheme } from "./theme/index.js";
49
51
  import { rgbIsLight } from "./theme/palette.js";
@@ -479,6 +481,8 @@ export function apply(ctx) {
479
481
  */
480
482
  const stopTask = async () => {
481
483
  renderer.renderNotice(`${stopIcon()} canceling current turn…`, 'info');
484
+ // The stop gesture is the everything-stop: side calls die with the turn.
485
+ btwController.cancelAll();
482
486
  const cancelled = await bridge.cancelActiveTurn();
483
487
  // State raced idle between the decision and the cancel call — nothing
484
488
  // to cancel (e.g. the turn settled inside the confirm window).
@@ -609,6 +613,7 @@ export function apply(ctx) {
609
613
  onRemotePromotable: async (idRaw) => {
610
614
  // Queued follow-ups won the writer-lock race at an idle boundary:
611
615
  // take over EXACTLY like a manual /resume, then flush the queue.
616
+ btwController.cancelAll();
612
617
  const resumed = await bridge.resume(SessionId(idRaw));
613
618
  refreshPermissionPreset();
614
619
  renderer.clear();
@@ -713,6 +718,89 @@ export function apply(ctx) {
713
718
  // Wiring through the handle: the editor is rebuilt on theme hot-swap, and
714
719
  // these providers are re-applied to the replacement instance.
715
720
  ui.setEditorAutocompleteProvider(commands.autocompleteProvider());
721
+ // ------------------------------------------------------------ /btw (TUI-owned; ADR 0001) --
722
+ // By-the-way side questions: one tool-less one-shot model call over a
723
+ // read-only recent-conversation snapshot, streaming into a framed
724
+ // overlay. Nothing enters the session log, the inbox, or any main-line
725
+ // model request. Placed here (not an independent plugin) because the
726
+ // deliverable — overlay, focus, cancellation timing — only exists inside
727
+ // dsh-tui-pi; see docs/adr/0001-btw-tui-owned-command.md.
728
+ const btwWire = new BtwOverlayWire({
729
+ tui: ui.tui,
730
+ theme: () => ui.theme,
731
+ restoreFocus: refocusEditor,
732
+ });
733
+ const btwSnapshotLimit = resolveSnapshotLimit(process.env.DSH_TUI_BTW_CONTEXT_MESSAGES);
734
+ const btwController = new BtwController({
735
+ stream: options => {
736
+ const llm = ctx.get('llm');
737
+ if (llm === undefined)
738
+ throw new Error('LLM service is not available.');
739
+ return llm.stream({
740
+ provider: options.provider,
741
+ model: options.model,
742
+ ...(options.reasoningEffort === undefined ? {} : { reasoningEffort: options.reasoningEffort }),
743
+ messages: options.messages,
744
+ system: options.system,
745
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
746
+ });
747
+ },
748
+ resolveSelection: () => {
749
+ const selection = bridge.getSelection();
750
+ return selection === undefined
751
+ ? undefined
752
+ : { provider: selection.provider, model: selection.model, reasoningEffort: selection.reasoningEffort };
753
+ },
754
+ buildSnapshot: () => {
755
+ const agent = bridge.getAgent();
756
+ return agent === undefined
757
+ ? []
758
+ : buildBtwSnapshot(agent.session.events, btwSnapshotLimit);
759
+ },
760
+ requestRender: () => ui.requestRender(),
761
+ notify: (message, kind) => renderer.renderNotice(message, kind),
762
+ onRunStarted: run => btwWire.open(run),
763
+ onOverlayRequestedClose: () => btwWire.requestClose(),
764
+ hasCapturingSurface: () => ui.tui.hasOverlay() || askUserActive,
765
+ });
766
+ btwWire.attach(btwController);
767
+ const btwHandler = async (rawInput) => {
768
+ const parsed = parseBtwInput(rawInput);
769
+ if (parsed.kind === 'empty') {
770
+ const opened = btwController.openReview();
771
+ return opened === 'live'
772
+ ? { kind: 'success', text: 'btw — the running answer is back on screen.' }
773
+ : opened === 'review'
774
+ ? { kind: 'success', text: 'Last btw exchange shown.' }
775
+ : { kind: 'success', text: BTW_USAGE };
776
+ }
777
+ if (parsed.kind === 'error')
778
+ return { kind: 'error', text: parsed.error };
779
+ // btw is by-the-way by definition: the main line must be running (and
780
+ // this must be the live view, not a read-only remote one) — when idle,
781
+ // a normal prompt is strictly better (tools, history, full context).
782
+ if (!(bridge.isRunning() && !bridge.isReadOnlyView())) {
783
+ return { kind: 'error', text: BTW_IDLE_NOTICE };
784
+ }
785
+ const result = btwController.submit({
786
+ question: parsed.question,
787
+ ...(parsed.modelOverride === undefined ? {} : { modelOverride: parsed.modelOverride }),
788
+ });
789
+ switch (result.kind) {
790
+ case 'started':
791
+ return { kind: 'success', text: 'btw — answering alongside the main task.' };
792
+ case 'queued':
793
+ return { kind: 'success', text: `btw queued (position ${result.position}).` };
794
+ case 'rejected':
795
+ return { kind: 'error', text: `btw rejected — ${result.reason}.` };
796
+ }
797
+ };
798
+ commands.registerLocal('btw', btwHandler);
799
+ ctx.effect(() => ctx.commands.register({
800
+ name: 'btw',
801
+ description: 'Ask a side question while the main task runs (temp overlay, not kept)',
802
+ handler: invocation => btwHandler(invocation.rawInput, invocation.signal),
803
+ }), 'dsh-tui-pi: /btw');
716
804
  // ------------------------------------------- TUI-owned slash commands --
717
805
  // Web-surface parity: `model` is a browser client contribution there and
718
806
  // `export` a web-only download plugin — the terminal gets native
@@ -973,6 +1061,9 @@ export function apply(ctx) {
973
1061
  // (including the WriterLockedError read-only fallback).
974
1062
  const resumeAndReplay = async () => {
975
1063
  let resumed;
1064
+ // Switching sessions cancels any in-flight btw (Q5b) — its snapshot
1065
+ // belongs to the session being left.
1066
+ btwController.cancelAll();
976
1067
  try {
977
1068
  resumed = await bridge.resume(target.id);
978
1069
  }
@@ -1170,6 +1261,8 @@ export function apply(ctx) {
1170
1261
  // detachCurrent keeps the event subscriptions alive (dispose() would
1171
1262
  // splice them away and no future message would ever render again).
1172
1263
  const newHandler = async () => {
1264
+ // A new session strands the btw context snapshot — cancel first (Q5b).
1265
+ btwController.cancelAll();
1173
1266
  try {
1174
1267
  await bridge.detachCurrent();
1175
1268
  }
@@ -1716,6 +1809,7 @@ export function apply(ctx) {
1716
1809
  clearInterval(clockTimer);
1717
1810
  clearInterval(liveTimer);
1718
1811
  git.dispose();
1812
+ btwController.dispose();
1719
1813
  applyThemeRef = undefined;
1720
1814
  applyPanelHeightRef = undefined;
1721
1815
  applyFooterHintsRef = undefined;