dsh-ssh-tui 0.8.1 → 0.8.2

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 (93) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.en.md +118 -15
  3. package/README.md +78 -13
  4. package/docs/release-notes-0.7.4.md +91 -0
  5. package/docs/release-notes-0.8.0.md +56 -0
  6. package/docs/release-notes-0.8.1.md +79 -0
  7. package/docs/release-notes-0.8.2-rc.1.md +159 -0
  8. package/docs/release-notes-0.8.2.md +62 -0
  9. package/docs/screenshots/reconnect.gif +0 -0
  10. package/docs/terminals.md +23 -0
  11. package/docs/windows.md +13 -0
  12. package/lib/approval-state.js +148 -0
  13. package/lib/approval-state.js.map +1 -0
  14. package/lib/auth-failure.js +38 -0
  15. package/lib/auth-failure.js.map +1 -1
  16. package/lib/color-depth.js +26 -0
  17. package/lib/color-depth.js.map +1 -1
  18. package/lib/copy-text.js +30 -8
  19. package/lib/copy-text.js.map +1 -1
  20. package/lib/dialogs.js +81 -0
  21. package/lib/dialogs.js.map +1 -1
  22. package/lib/display-sock.js +138 -23
  23. package/lib/display-sock.js.map +1 -1
  24. package/lib/footer-accents.js +28 -0
  25. package/lib/footer-accents.js.map +1 -0
  26. package/lib/footer-budget.js +328 -0
  27. package/lib/footer-budget.js.map +1 -0
  28. package/lib/footer-format.js +433 -0
  29. package/lib/footer-format.js.map +1 -0
  30. package/lib/footer-layout.js +407 -0
  31. package/lib/footer-layout.js.map +1 -0
  32. package/lib/footer-meters.js +185 -0
  33. package/lib/footer-meters.js.map +1 -0
  34. package/lib/footer.js +19 -558
  35. package/lib/footer.js.map +1 -1
  36. package/lib/i18n/en.js +65 -5
  37. package/lib/i18n/en.js.map +1 -1
  38. package/lib/i18n/zh.js +67 -5
  39. package/lib/i18n/zh.js.map +1 -1
  40. package/lib/paint.js +53 -8
  41. package/lib/paint.js.map +1 -1
  42. package/lib/plan-projection.js +291 -0
  43. package/lib/plan-projection.js.map +1 -0
  44. package/lib/plan.js +50 -0
  45. package/lib/plan.js.map +1 -1
  46. package/lib/question-state.js +83 -0
  47. package/lib/question-state.js.map +1 -0
  48. package/lib/representation.js +283 -0
  49. package/lib/representation.js.map +1 -0
  50. package/lib/screen.js +105 -0
  51. package/lib/screen.js.map +1 -0
  52. package/lib/selection.js +5 -3
  53. package/lib/selection.js.map +1 -1
  54. package/lib/startup.js +49 -0
  55. package/lib/startup.js.map +1 -1
  56. package/lib/stats.js +101 -3
  57. package/lib/stats.js.map +1 -1
  58. package/lib/term-text.js +154 -55
  59. package/lib/term-text.js.map +1 -1
  60. package/lib/theme.js +20 -3
  61. package/lib/theme.js.map +1 -1
  62. package/lib/throughput.js +305 -0
  63. package/lib/throughput.js.map +1 -0
  64. package/lib/tool-present.js +57 -7
  65. package/lib/tool-present.js.map +1 -1
  66. package/lib/tui.js +3776 -1105
  67. package/lib/tui.js.map +1 -1
  68. package/lib/types/approval-state.d.ts +75 -0
  69. package/lib/types/auth-failure.d.ts +25 -0
  70. package/lib/types/color-depth.d.ts +21 -0
  71. package/lib/types/copy-text.d.ts +18 -1
  72. package/lib/types/dialogs.d.ts +111 -1
  73. package/lib/types/display-sock.d.ts +15 -0
  74. package/lib/types/footer-accents.d.ts +22 -0
  75. package/lib/types/footer-budget.d.ts +204 -0
  76. package/lib/types/footer-format.d.ts +221 -0
  77. package/lib/types/footer-layout.d.ts +292 -0
  78. package/lib/types/footer-meters.d.ts +151 -0
  79. package/lib/types/footer.d.ts +19 -269
  80. package/lib/types/paint.d.ts +48 -0
  81. package/lib/types/plan-projection.d.ts +162 -0
  82. package/lib/types/plan.d.ts +13 -0
  83. package/lib/types/question-state.d.ts +96 -0
  84. package/lib/types/representation.d.ts +498 -0
  85. package/lib/types/screen.d.ts +126 -0
  86. package/lib/types/startup.d.ts +24 -0
  87. package/lib/types/stats.d.ts +64 -1
  88. package/lib/types/term-text.d.ts +45 -1
  89. package/lib/types/throughput.d.ts +163 -0
  90. package/lib/types/tool-present.d.ts +41 -0
  91. package/lib/types/transcript-types.d.ts +63 -2
  92. package/lib/types/tui.d.ts +862 -11
  93. package/package.json +16 -6
@@ -19,6 +19,7 @@ import type { Agent, ModelSelection, ModelSelectionRef } from '@deepseek-ai/dsh-
19
19
  import type { Context } from '@deepseek-ai/cordis';
20
20
  import { type LlmCallConfig } from '@deepseek-ai/dsh-llm';
21
21
  import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session';
22
+ import { auditQuestionPrimaries, type RepresentationAudit } from './representation.js';
22
23
  import type { SubagentRunEndInfo, SubagentRunInfo } from '@deepseek-ai/dsh-subagent';
23
24
  import { type TerminalCapabilities } from './terminal-caps.js';
24
25
  import { type SessionRoute } from './session-route.js';
@@ -29,8 +30,8 @@ import type { CollapsibleBlock, DisconnectPolicyName, Row } from './transcript-t
29
30
  import { paintedLinkHits } from './term-text.js';
30
31
  export type { CollapsibleBlock, DisplayKind, DisconnectPolicyName, PlanTodoItem, Row, SubagentLogEntry, ToolDiffHunk, } from './transcript-types.js';
31
32
  export { clipAnsiToWidth, cursorVisualPosition, displayWidth, foldInputView, hrefAtColumn, osc52Clipboard, osc8Enabled, paintedLinkHits, fmtElapsedCompact, padAnsiToWidth, padToWidth, renderMarkdownLines, repeatToWidth, shimmerText, truncateToWidth, visibleWidth, waitCardCopy, waitSummaryFromReasoning, wrapWaitDetails, } from './term-text.js';
32
- export { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, ignoreFurtherHangupSignals, isEscapePrefix, findCursorPositionReply, isHangupErrno, linkQualityOf, linkSignalPips, paintIntervalForRtt, paintLinkLabel, parseCursorPositionReply, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, writeBootSplash, type LinkQuality, type PaintLinkKind, } from './paint.js';
33
- export { CONTEXT_IDLE_COMPACT_RATIO, CONTEXT_PRESSURE_DANGER_RATIO, CONTEXT_PRESSURE_WARN_RATIO, CONTEXT_RING_EMPTY, CONTEXT_RING_SEGMENTS, contextPressureAlertText, contextPressureRingColor, contextPressureUsedTokens, contextPressureView, describeProviderRoute, dropFooterQuotaPlanName, fitFooterStatsLine, fitFooterStatusLine, footerActivity, footerIdentityParts, footerStatsGroups, footerSubagentForeign, paintFooterSubagentChip, formatContextPressureChip, formatContextPressureRing, formatContextPressureStatusLine, formatDuration, formatFooterQuota, formatQuotaBar, formatQuotaUnknown, formatStatusReport, providerHasQuotaSurface, quotaWindowTag, shortModelName, shortQuotaPlanName, formatTokens, formatTokensPerSecond, parseContextPressure, promptPressureTokens, providerShortCode, providerUsesLocalOAuth, shouldIdleAutoCompact, subagentRouteLabel, type ContextPressureSample, type ContextPressureView, type FooterActivityKind, type FooterStatsInput, type FooterStatusInput, type StatusReportInput, } from './footer.js';
33
+ export { captureHangupSignals, composePaintOutput, detectSshSession, formatLinkQualityChip, ignoreFurtherHangupSignals, isEscapePrefix, findCursorPositionReply, isHangupErrno, linkQualityOf, linkRedrawBudgetMs, linkSignalPips, paintIntervalForRtt, paintLinkLabel, parseCursorPositionReply, pickerWindowStart, probeTerminalRttMs, releaseHangupSignals, resolvePaintIntervalMs, waitUntilIdleOrTimeout, writeBootSplash, type LinkQuality, type PaintLinkKind, } from './paint.js';
34
+ export { CONTEXT_IDLE_COMPACT_RATIO, CONTEXT_PRESSURE_DANGER_RATIO, CONTEXT_PRESSURE_WARN_RATIO, CONTEXT_RING_EMPTY, CONTEXT_RING_SEGMENTS, contextPressureAlertText, contextPressureRingColor, contextPressureUsedTokens, contextPressureView, describeProviderRoute, dropFooterQuotaPlanName, fitFooterStatsLine, fitFooterStatusLine, footerActivity, footerIdentityParts, footerStatsGroups, footerSubagentForeign, paintFooterSubagentChip, formatContextPressureChip, formatContextPressureRing, formatContextPressureStatusLine, formatDuration, formatFooterQuota, formatQuotaBar, formatQuotaUnknown, formatStatusReport, formatStatusThroughput, formatStatusStats, providerHasQuotaSurface, quotaWindowTag, runtimeStrip, shortModelName, shortQuotaPlanName, formatTokens, formatTokensPerSecond, parseContextPressure, promptPressureTokens, providerShortCode, providerUsesLocalOAuth, shouldIdleAutoCompact, subagentRouteLabel, type ContextPressureSample, type ContextPressureView, type FooterActivityKind, type FooterStatsInput, type FooterStatusInput, type StatusReportInput, } from './footer.js';
34
35
  export { commandCodePeriodStart, commandCodeSourceFor, crossedQuotaThresholds, formatAccountBalance, formatFooterBalance, formatOpenCodeGoUsage, formatQuotaSnapshot, formatQuotaStatusLine, joinUrl, openCodeSourceFor, parseCommandCodeQuota, parseDeepSeekBalance, parseOpenAiCompatibleBalance, parseOpenCodeGoQuota, parseSuperGrokBilling, preferredQuotaWindow, quotaAlertText, quotaRefreshEverySteps, quotaRefreshEveryTurns, remainingPercentFromUsed, tightestQuotaWindow, type AccountBalanceLine, type AccountBalanceSnapshot, type CommandCodeSource, type OpenCodeFlavor, type OpenCodeSource, type QuotaPeriod, type QuotaSnapshot, type QuotaWindow, } from './quota.js';
35
36
  export { commandAcceptsAttachments, forEachSessionEvent, isTokenDeltaChunk, listPersistenceHeaders, inspectPersistenceSession, sessionEvents, settingsNamespace, streamChunkOf, streamFirstTokenTime, streamFrameAttemptId, streamFrameOwner, } from './dsh-compat.js';
36
37
  export { applyTurnEndToPlan, askSummary, cardCategoryOf, compactionHeaderText, formatCompactCommandError, isPromptInjectionMessage, matchTranscriptRows, parseFindQuery, parsePlanTodos, planCloseNudgeText, planDockNote, planIsLive, planTitleFromMarkdown, planTurnLeftOpen, promptInjectionSources, promptInjectionTitle, buildSubagentHeader, clipSubagentActivity, describeSubagentFailure, subagentChipSummary, subagentDisplayName, subagentInspectLines, subagentRowFromSpawnTool, foldSubagentUserLog, todoProgressLabel, todoSummary, type CardCategory, } from './plan.js';
@@ -137,6 +138,14 @@ export interface TuiController {
137
138
  */
138
139
  sessionHadUserInput(): boolean;
139
140
  }
141
+ /**
142
+ * `DSH_TUI_FEEDBACK_MS` — how long that is, in milliseconds.
143
+ *
144
+ * `0` is the pre-0.8.2 behaviour and a legitimate preference: keep an acknowledgement
145
+ * or a warning until the reader submits something. Anything unreadable or negative
146
+ * falls back to the default rather than to a surprise.
147
+ */
148
+ export declare function resolveFeedbackTtlMs(env?: NodeJS.ProcessEnv): number;
140
149
  export type WorkspaceView = 'detailed' | 'compact';
141
150
  /**
142
151
  * One row the card cursor can land on.
@@ -227,12 +236,130 @@ export declare class SshTui {
227
236
  private historyDraft;
228
237
  private status;
229
238
  private dialog;
239
+ /**
240
+ * What the open dialog *means*, which its shape cannot say: `/model` and
241
+ * `ask_user_question` are both `questions` dialogs, and only one of them means
242
+ * the agent is waiting for a person. Declared by whoever opens the dialog.
243
+ */
244
+ private dialogRole;
230
245
  private readonly dialogQueue;
231
246
  private onboardingCompletion;
232
247
  private dirty;
233
248
  private disposed;
234
249
  private exiting;
235
250
  private hangingUp;
251
+ /** How long the last frame took to compose, for the resize pacing below. */
252
+ private lastPaintCostMs;
253
+ private resizeLastEventAt;
254
+ private resizeLastPaintAt;
255
+ private resizePaintTimer;
256
+ /**
257
+ * Whether a resize frame is currently rendering only the transcript's tail
258
+ * (see {@link narrowToResizeTail}), so the drag's last geometry can be painted
259
+ * whole.
260
+ */
261
+ private resizeTailNarrowed;
262
+ /**
263
+ * A resize arrived: paint the size the window is *at*, not every size it
264
+ * passed through.
265
+ *
266
+ * Dragging an edge emits an event every few milliseconds, and each one used to
267
+ * get a synchronous full repaint — on a long transcript that is ~100 ms of
268
+ * rendering per event, so a drag queued seconds of work and the screen trailed
269
+ * the pointer. Nothing about an intermediate geometry is worth a frame: the
270
+ * reader cannot use it and the next event invalidates it. Painting the *first*
271
+ * event of a burst is no better, because that geometry is stale by the time the
272
+ * frame is composed and it costs the frame that is actually wanted.
273
+ *
274
+ * So the frame has a deadline, recomputed on every event:
275
+ *
276
+ * - {@link RESIZE_SETTLE_MS} after the newest event (the size has stopped
277
+ * moving, so this is the geometry the user is looking at), and
278
+ * - at the latest one {@link resizeBudgetMs} after the last frame, so a drag
279
+ * that never pauses still shows the chrome following the pointer.
280
+ *
281
+ * Whichever comes first wins, and the frame is composed from the terminal's
282
+ * size at the moment it runs — so a newer geometry can never be painted after
283
+ * an older one; the older one is simply never painted.
284
+ */
285
+ private requestResizePaint;
286
+ /**
287
+ * Whether the previous *drag* frame is still on its way to the terminal.
288
+ *
289
+ * A drag frame is not a token batch: the reader is moving the edge and watching
290
+ * for the screen to agree with it, so a frame composed now would be superseded
291
+ * before it landed. What matters is the *queue*, not the pipe's absolute
292
+ * capacity — measured on a byte-rate-limited consumer, a drag toward a wider
293
+ * window is the case that hurts, because a frame's size is proportional to the
294
+ * width: every frame is bigger than the last while the terminal drains at a
295
+ * fixed rate, the backlog compounds, and the screen keeps moving for tens of
296
+ * milliseconds after the pointer has stopped (30–44 ms measured, against 0–10 ms
297
+ * for the shrinking direction).
298
+ *
299
+ * So the threshold is one frame, not a fixed 32 KB. That constant is right for
300
+ * the cadence path, where the question is "is this link slow"; here the question
301
+ * is "would this frame be a stale geometry queued behind a live one", and the
302
+ * answer is yes as soon as the last one has not been accepted — however small
303
+ * both frames are. The named constant stays as the floor, so a long session's
304
+ * frames can queue a little rather than starving the drag of feedback.
305
+ */
306
+ private resizeWireBehind;
307
+ /** Bytes the last frame carried, whoever owns the wire (see above). */
308
+ private lastFrameBytes;
309
+ private pendingBytes;
310
+ /**
311
+ * Cap what a resize frame re-renders, and remember that the frame is a
312
+ * compromise so the drag's last geometry can be painted whole.
313
+ *
314
+ * Both matters, and they are different matters: the cap is what keeps the
315
+ * event loop free while the pointer moves (see
316
+ * {@link RESIZE_TAIL_MIN_ROWS}), and the remembered flag is what stops the
317
+ * compromise from *becoming* the screen — the moment the drag pauses, the
318
+ * transcript is painted in full again at the settled width.
319
+ *
320
+ * A scroll position is left alone: a reader who has scrolled back is looking
321
+ * at rows the tail does not contain, and moving the view under them would be a
322
+ * bigger lie than a slower frame. `paintTailBudget` already refuses to fold
323
+ * anything while `scrollOffset > 0` for the same reason.
324
+ */
325
+ private narrowToResizeTail;
326
+ /**
327
+ * Undo {@link narrowToResizeTail}: the drag has settled, so the frame that is
328
+ * about to be painted must be the whole transcript at the final width.
329
+ */
330
+ private widenPastResizeTail;
331
+ /**
332
+ * The longest a resize frame may wait when the wire cannot take another one yet.
333
+ *
334
+ * This is a *ceiling*, not a rate: the normal case paints at once (see
335
+ * {@link requestResizePaint}), and this only decides how often a link that is
336
+ * genuinely behind is asked again — the previous frame has not drained, so the
337
+ * next one would be a stale geometry queued behind a live one. Two costs feed it:
338
+ *
339
+ * - **composing** a frame — measured (`lastPaintCostMs`): a short transcript is a
340
+ * few milliseconds and a long one is a hundred, and repainting faster than that
341
+ * starves the event loop that has to receive the next resize;
342
+ * - **delivering** one on a link whose speed is unknown (see
343
+ * {@link linkRedrawBudgetMs}: only an unprobed link falls back to a slow
344
+ * budget, because there the interval in force is a number nobody measured).
345
+ *
346
+ * A probed link does not need the second term: the backlog check above already
347
+ * says whether it is keeping up, and pacing it from the round trip as well would
348
+ * put the artificial delay back that this function exists to avoid.
349
+ */
350
+ private resizeBudgetMs;
351
+ /**
352
+ * Whether a resize is holding the frame.
353
+ *
354
+ * While a deadline is armed the burst is collapsing, and the cadence timer must
355
+ * not paint on its own: it fires on the paint interval, which is *shorter* than
356
+ * the budget a heavy session needs, so leaving both flushers live put the frame
357
+ * count back near one per event — the cost this exists to remove. Content that
358
+ * changes mid-drag waits at most one budget.
359
+ */
360
+ private resizeBurstOpen;
361
+ /** (Re)arm the frame's deadline. Every event pushes it out. */
362
+ private armResizePaint;
236
363
  private readonly onDirectResize;
237
364
  private readonly headlessDisplay;
238
365
  private disconnectPolicy;
@@ -377,9 +504,33 @@ export declare class SshTui {
377
504
  private liveStreamOwner;
378
505
  /** Live events parked while the (yielding) history replay holds the floor. */
379
506
  private replayQueue;
507
+ /**
508
+ * True while the durable log is still being read into the transcript.
509
+ *
510
+ * Frames are not composed during it (see `paint`): a window anchored to a session
511
+ * that is still growing is the rolling resume this flag exists to prevent.
512
+ */
513
+ private loadingHistory;
514
+ /**
515
+ * A relay attached while the log was still being read, so the screen entry it
516
+ * asks for has not been written yet (see `attachRelayDisplay` and `paint`).
517
+ */
518
+ private screenEntryPending;
380
519
  /** A relay claimed the display while a hangup was still cancelling/flushing. */
381
520
  private reattachedDuringHangup;
382
521
  private scrollOffset;
522
+ /**
523
+ * How many rows `/clear` hid, as a count from the front of `rows`.
524
+ *
525
+ * A *view* boundary, not a deletion (AD-4): the rows stay, the session log is
526
+ * untouched, and a resume shows everything again because this number is Host-local
527
+ * and never written anywhere. A count is the stable expression here — every new row
528
+ * is appended at the end, and an update to an old row mutates it in place, so a row
529
+ * cannot cross the boundary by being updated. When the transcript trims its oldest
530
+ * rows the count is decremented with them, which keeps the boundary on the same
531
+ * semantic row.
532
+ */
533
+ private clearedRows;
383
534
  private readonly clickableRows;
384
535
  /**
385
536
  * The transcript as painted, for free-form selection. Only the lines that came
@@ -389,6 +540,86 @@ export declare class SshTui {
389
540
  private selectableLines;
390
541
  /** Screen row (1-based) of the first transcript line in the last frame. */
391
542
  private transcriptTopScreenY;
543
+ /**
544
+ * The footer echo: the last thing this Host confirmed to the reader.
545
+ *
546
+ * Host-local and deliberately not history (AD-13): "theme switched" is an
547
+ * acknowledgement, not a fact about the session. It lives in the footer's own row
548
+ * as the lowest-priority chip, is replaced by the next echo, and leaves the row
549
+ * again when the reader submits — or after `feedbackTtlMs`, whichever comes first.
550
+ * A detach keeps it (the Host never died); a resume does not restore it (it was
551
+ * never in the log).
552
+ */
553
+ private footerEcho;
554
+ /**
555
+ * The ephemeral notice: feedback that needs reading now, and is not history.
556
+ *
557
+ * A failed command, an operational warning — messages with too much in them for a
558
+ * footer chip. It takes the telemetry row for its lifetime: no geometry change, no
559
+ * content hidden, always visible. It does not steal input, and the next notice
560
+ * replaces it while the next submit (or its own expiry) clears it. No NotificationManager: one field,
561
+ * one row.
562
+ */
563
+ private notice;
564
+ /**
565
+ * The Screen that is up, if any. At most one, and never a dialog.
566
+ *
567
+ * A Screen replaces the workspace rather than borrowing rows from it, so it is
568
+ * deliberately *not* part of `dialog`/`dialogQueue`: a queued question cannot
569
+ * open behind a report, and a report cannot swallow a question. Nothing else in
570
+ * this class needs to know which Screen it is — `paintScreen` renders whatever
571
+ * state is here, and every key gate reads this one field (B2.1).
572
+ */
573
+ private screen;
574
+ /**
575
+ * A Screen's own action Surface (the doctor confirmation).
576
+ *
577
+ * It exists so the channel stays separated in both directions: while a Screen is
578
+ * up, a confirmation it needs must not enter the workspace queue, and a
579
+ * workspace Surface that arrives meanwhile must not be consumed by the Screen
580
+ * (AD-3). At most one, resolved by the Screen that opened it.
581
+ */
582
+ private screenSurface;
583
+ /** Resolver for `screenSurface`, so closing it always settles its promise. */
584
+ private screenSurfaceResolve;
585
+ /**
586
+ * Whether the next Screen frame must establish the whole picture.
587
+ *
588
+ * True when a Screen opens and after a reattach: the terminal is empty or holds
589
+ * an unrelated frame, and one full paint is both cheaper and less surprising than
590
+ * a partial one against unknown contents. Every later frame is incremental.
591
+ */
592
+ private screenNeedsFullPaint;
593
+ /** The strip's last painted text, so an unchanged strip is not re-sent. */
594
+ private lastScreenStrip;
595
+ /**
596
+ * The rows the transient layer covers, 1-based and inclusive of `top`..`top+rows-1`.
597
+ *
598
+ * The layer is drawn over the transcript, so anything the frame registers as
599
+ * clickable or selectable below this line is a target the reader cannot see.
600
+ * The mouse handlers already refuse to start while a surface owns the keyboard;
601
+ * this makes the maps themselves honest, so a later change cannot introduce
602
+ * click-through by forgetting that gate. B1.2 widened what the layer carries —
603
+ * a question and a control-plane picker both land here — but not what it means:
604
+ * covered rows are hidden, never re-defined, and the composer's boundary is
605
+ * always below the last of them.
606
+ */
607
+ private interactionRegion;
608
+ /**
609
+ * The rows the live tail covers, 1-based, same convention as the layer above.
610
+ *
611
+ * The tail is a *projection* of runtime state (streaming text, the wait card),
612
+ * not history: it is drawn over the bottom of the window and owns no source row.
613
+ * Rows under it are hidden, so — exactly like the interaction layer — nothing
614
+ * behind them may stay a click, drop or link target.
615
+ */
616
+ private liveTailRegion;
617
+ /**
618
+ * The interaction row that must stay visible when the layer has to window
619
+ * itself (the highlighted option): on a short terminal the list is scrolled to
620
+ * the selection instead of being clipped at the bottom.
621
+ */
622
+ private dialogFocusLine;
392
623
  /** Where a drag started; the run it belongs to decides what can be selected. */
393
624
  private mouseAnchor;
394
625
  /**
@@ -408,8 +639,32 @@ export declare class SshTui {
408
639
  /** Screen-row → OSC 8 hits from the last paint (tests). */
409
640
  get linkHitsByRow(): Map<number, ReturnType<typeof paintedLinkHits>>;
410
641
  private streamingReasoning;
642
+ /**
643
+ * The reader's expansion choice for the live thinking card, for this turn.
644
+ *
645
+ * A turn thinks in phases: think, call a tool, think again. Each phase used to
646
+ * start a fresh collapsed card, so a reader who had just expanded the card being
647
+ * written watched it fold itself shut at the next phase — "the thinking card
648
+ * cannot be expanded" as the reader experiences it, even though each individual
649
+ * card could be. The choice is the reader's, it belongs to the turn, and
650
+ * `turn/start` is the only thing that resets it.
651
+ */
652
+ private reasoningExpandedChoice;
411
653
  private escapeBuffer;
412
654
  private escapeTimer;
655
+ /**
656
+ * How long an acknowledgement or a warning stays on the chrome.
657
+ *
658
+ * B2.3b kept them until the next submit, with no timer, so a reader who came back
659
+ * after a while still saw what happened. Measured against the actual use — a
660
+ * "copied 412 characters" chip and a "applies at the next step" notice — that is
661
+ * the wrong trade: the reader has read them within seconds, and until they type
662
+ * something the row is occupied by a sentence about the past instead of the
663
+ * telemetry it replaced. They now expire on their own (and a submit still clears
664
+ * them at once).
665
+ */
666
+ private feedbackTtlMs;
667
+ private feedbackTimer;
413
668
  /**
414
669
  * Cursor-position replies removed from the relay's stdin stream. A launcher
415
670
  * from an older release (or a reply that raced its own probe) would otherwise
@@ -457,7 +712,13 @@ export declare class SshTui {
457
712
  * mean touching the filesystem on every repaint.
458
713
  */
459
714
  private rosterMissing;
460
- /** Screen row of the health chip in the status strip, for click-to-doctor. */
715
+ /**
716
+ * Screen row of the install warning, for click-to-doctor.
717
+ *
718
+ * It is a chip on the status row like any other — that row is the last line of
719
+ * the frame, so its index is the frame's last row — and clicking anywhere on it
720
+ * opens the report that explains it.
721
+ */
461
722
  private healthChipRow;
462
723
  /** How much colour this terminal can take (see `color-depth`). */
463
724
  private readonly colorDepth;
@@ -479,6 +740,44 @@ export declare class SshTui {
479
740
  /** Approvals refused because nobody could confirm them (a subset of denials). */
480
741
  private detachedDeniedCount;
481
742
  /** Questions waiting for a display right now — their cards do not exist yet. */
743
+ /**
744
+ * The questions each `ask_user_question` call recorded, and the cards drawn for
745
+ * them.
746
+ *
747
+ * Keyed by call *and* question id: one call carries a batch, and two calls may
748
+ * reuse a question id. The card is the presentation only — the Session's
749
+ * projection is what says whether a question is still answerable and what it was
750
+ * answered with, so a replayed session draws the same card a lived one did.
751
+ */
752
+ private readonly askedByCall;
753
+ private readonly questionCards;
754
+ /**
755
+ * Every plan-relevant durable event of this session, in order (B2.5).
756
+ *
757
+ * The plan artifact is a *projection* of these, so nothing here is plan state: the
758
+ * fold decides, and the transcript row, the dock and the review Surface all read
759
+ * the result. `live` marks the events that arrived from the running Host, because
760
+ * only they can carry an unanswered review (a resumed process never finishes one).
761
+ */
762
+ private planEvents;
763
+ /** The folded artifacts. Recomputed whenever `planEvents` grows. */
764
+ private planArtifacts;
765
+ /** The state last *said* in the transcript per artifact, so a transition is one row. */
766
+ private readonly planAnnounced;
767
+ /**
768
+ * The approvals the Session log has recorded, by the Harness's own `id`.
769
+ *
770
+ * `approval/asked` states the request (and the tool call it guards),
771
+ * `approval/decided` states the outcome, and the two are joined here so a
772
+ * decision that arrives before its row exists — or a row that arrives after both
773
+ * — still lands on the same card. Nothing is written back: the log is the
774
+ * Harness's, and this plugin keeps no approval history of its own (B2.4).
775
+ */
776
+ private readonly approvalAsked;
777
+ /** Approvals resolved before their tool card existed, waiting for it. */
778
+ private readonly pendingApprovalByCall;
779
+ /** Cards that exist only because a live request opened them (no durable call id). */
780
+ private liveQuestionSeq;
482
781
  private queuedQuestions;
483
782
  /** When the current detached wait began, so the reconnect can say how long. */
484
783
  private questionWaitSince;
@@ -493,6 +792,13 @@ export declare class SshTui {
493
792
  */
494
793
  private sshSession;
495
794
  private paintProbed;
795
+ /**
796
+ * Where {@link paintIntervalMs} came from: a measured round trip, an explicit
797
+ * `DSH_TUI_PAINT_MS`, or a local TTY's constant. Only the *unprobed SSH* case is
798
+ * an unknown link, and only that case falls back to
799
+ * {@link RESIZE_UNKNOWN_LINK_BUDGET_MS}.
800
+ */
801
+ private paintCadence;
496
802
  private paintRttMs;
497
803
  /** The last few measured round-trips; the chip and the budget use their median. */
498
804
  private paintRttHistory;
@@ -631,6 +937,16 @@ export declare class SshTui {
631
937
  * user wrote is never touched, and the previous file is kept as a backup.
632
938
  */
633
939
  private repairProfilePatch;
940
+ /**
941
+ * Where an action's outcome is reported: the Screen that asked, or a row.
942
+ *
943
+ * A Screen's own action is answered *on the Screen* — the reader is looking at it,
944
+ * and the row it would otherwise push lands behind their own view. With no Screen
945
+ * up (the same code path serves `/fix`), the answer is an ordinary row.
946
+ */
947
+ private resultChannel;
948
+ /** Report a Screen action's outcome, or fall back to a row. */
949
+ private reportActionResult;
634
950
  /** Human-facing exit with goodbye and flush; called from key handling. */
635
951
  requestExit(code: number): Promise<void>;
636
952
  /**
@@ -691,9 +1007,57 @@ export declare class SshTui {
691
1007
  private toolCardSummary;
692
1008
  private mergeIntoToolCard;
693
1009
  private findToolRowByCallId;
1010
+ /**
1011
+ * The approval state of one tool call (B2.4).
1012
+ *
1013
+ * Approval has no row of its own: it is a field of the card for the call it
1014
+ * guarded. A decision can arrive before the card does (a `tool/result`-only
1015
+ * fragment, a resumed log read out of order), so an unresolved call id is kept
1016
+ * until its row appears rather than dropped.
1017
+ * @param callId - the tool call the approval belongs to.
1018
+ * @param next - the fact being applied, or undefined to read it back.
1019
+ * @returns the field after the merge.
1020
+ */
1021
+ private setToolApproval;
1022
+ /**
1023
+ * The weakest approval reading there is, and the only one allowed to be weak.
1024
+ *
1025
+ * A log from before the Harness recorded its audit pair still says *something*
1026
+ * about a refused tool: the tool never ran, and the reason is the Harness's own
1027
+ * sentence for an approval that came back rejected or cancelled. That is a
1028
+ * reading, not a decision, so it is filed as `inferred` — and it can only ever
1029
+ * produce `rejected`/`unknown`, never `approved`: a tool that ran proves nothing,
1030
+ * because a tool nobody had to approve runs too.
1031
+ *
1032
+ * Anything already known from a stronger source wins, which is also what keeps a
1033
+ * live decision from being rewritten by the sentence it produced.
1034
+ * @param row - the card that just settled.
1035
+ * @param message - the tool result's message.
1036
+ * @param error - the result's error, when it has one.
1037
+ */
1038
+ private inferApprovalFromResult;
1039
+ /** Hand a card the approval that was resolved before the card existed. */
1040
+ private adoptPendingApproval;
694
1041
  private findMergeableToolRow;
695
1042
  /** Append one transcript row, bounding memory on long sessions. */
1043
+ /**
1044
+ * The only way a line enters the transcript.
1045
+ *
1046
+ * It takes a `Representation` — a row *plus* the policy that produced it — and not
1047
+ * a `Row`, so a creation site cannot skip the classification: a bare row is a type
1048
+ * error, and a source id with no policy is one too (`RepresentationSource` is the
1049
+ * policy table's keys). The policy is copied onto the row here, which is what makes
1050
+ * a real transcript auditable afterwards: `auditRepresentations(this.rows)` can
1051
+ * then answer how many lines are history and how many are the Host's own voice.
1052
+ *
1053
+ * A JavaScript caller (a test fixture, an embedder) can still hand over something
1054
+ * that is not a representation. That case is *recorded*, not defaulted: the row is
1055
+ * pushed with no metadata and `unclassifiedRepresentations` counts it, so the audit
1056
+ * fails loudly instead of quietly calling an unknown line durable.
1057
+ */
696
1058
  private pushRow;
1059
+ /** Append one classified row to the transcript (the `transcript` destination). */
1060
+ private appendRow;
697
1061
  /** Append one event, holding it until a display can carry it. */
698
1062
  private writeLineMode;
699
1063
  /** Hand the held lines to the display that just attached. */
@@ -713,6 +1077,13 @@ export declare class SshTui {
713
1077
  * painted, so anything not in the rows is appended.
714
1078
  */
715
1079
  private focusRing;
1080
+ /**
1081
+ * The live thinking block, created on first use.
1082
+ *
1083
+ * Expanded or not is the reader's choice for the turn, not this frame's: see
1084
+ * `reasoningExpandedChoice`.
1085
+ */
1086
+ private liveThinkingBlock;
716
1087
  private spinnerFrame;
717
1088
  /**
718
1089
  * Codex wait card: shown while the turn is running. The live thinking
@@ -729,14 +1100,66 @@ export declare class SshTui {
729
1100
  /** Courtesy name for a child session; never the raw session hash. */
730
1101
  private subagentNameFor;
731
1102
  private findLivePlanRow;
1103
+ /**
1104
+ * Keep the plan log, and re-fold when it grew.
1105
+ *
1106
+ * Only the events the fold reads are kept (B2.5). They are rare — a plan mode
1107
+ * switch, a todo snapshot, a review — so a whole re-fold per plan event stays
1108
+ * trivial, and it keeps the projection a *pure function of the log* rather than a
1109
+ * running tally that could drift from one.
1110
+ */
1111
+ private recordPlanEvent;
1112
+ /**
1113
+ * The projection, onto the rows three readers share.
1114
+ *
1115
+ * One artifact, one row: the row is the artifact's *reference* in the transcript
1116
+ * (its lifecycle summary and, when expanded, its steps), the dock paints the live
1117
+ * one, and the review Surface is the dialog. None of the three keeps plan state of
1118
+ * its own (AD-15): they all read what this fold decided.
1119
+ */
1120
+ private syncPlanArtifacts;
1121
+ /** The row that stands for one artifact, if it has been drawn yet. */
1122
+ private planRowFor;
1123
+ /** The artifact a docked row belongs to, when the workspace should show one. */
1124
+ private dockedPlanRow;
1125
+ private createPlanRow;
1126
+ /**
1127
+ * Say one lifecycle transition, once.
1128
+ *
1129
+ * The transcript keeps the artifact's *lifecycle*: entering plan mode, a review's
1130
+ * answer, leaving the mode. It does not keep a second copy of the plan body — that
1131
+ * is the artifact's (the dock while it is live, the review Surface while it is under
1132
+ * review, this row's overlay afterwards) — and a transition is never announced
1133
+ * twice, which is what makes the fold idempotent under a re-`sync`.
1134
+ */
1135
+ private announcePlanTransition;
732
1136
  /** Older / finished plans stay in the scrolling transcript. */
733
1137
  private archiveStalePlans;
734
- private upsertPlanRow;
1138
+ /**
1139
+ * There used to be a second plan-row writer here (`upsertPlanRow`).
1140
+ *
1141
+ * B2.5 moved every plan row onto the artifact fold — one artifact, one row, and
1142
+ * `syncPlanArtifacts` is the only place a plan row is created — which left this
1143
+ * one with no caller. The B2 final audit removed it rather than leave a second
1144
+ * truth in the file for someone to find and call.
1145
+ */
735
1146
  /** Whether the live plan strip should occupy the workspace footer. */
736
1147
  private shouldDockPlan;
737
1148
  /** Send the leftover-todo nudge only from true idle, so /compact is not blocked. */
738
1149
  private flushPlanCloseNudge;
739
1150
  /** Compact web-style plan strip pinned above the input, not in the transcript. */
1151
+ /**
1152
+ * The live region's runtime lines: the thinking card, the streamed text, the
1153
+ * wait card — in that order.
1154
+ *
1155
+ * Built before the window is computed, because the region *reserves* rows now:
1156
+ * `paint` needs its height before it can size the transcript. `waitStart` is
1157
+ * where the wait card begins, which is where the compact view splices the
1158
+ * running burst (that is where the transcript paints it for a settled reply).
1159
+ * `thinkingRows` is how many lines the live thinking card owns, so a click on
1160
+ * them reaches the card.
1161
+ */
1162
+ private runtimeTailLines;
740
1163
  private paintPlanDock;
741
1164
  /**
742
1165
  * One diff line with its changed characters in reverse video.
@@ -758,8 +1181,133 @@ export declare class SshTui {
758
1181
  */
759
1182
  private styleColumnedLine;
760
1183
  private paintToolBodyLine;
1184
+ /**
1185
+ * The representations this transcript is made of (B2.3a's audit artifact).
1186
+ *
1187
+ * Reads the policy off the rows themselves, so it reports what is really in the
1188
+ * transcript rather than what the table says should be — including anything that
1189
+ * arrived unclassified.
1190
+ */
1191
+ representationAudit(): RepresentationAudit;
1192
+ /**
1193
+ * The ask-user half of the audit: one semantic question, one primary row.
1194
+ *
1195
+ * Read off the real transcript rather than declared, because the invariant is
1196
+ * about what a reader can see: a call with both a question card and a generic
1197
+ * tool card is two primaries for one event, and the count is what catches a
1198
+ * regression (B2.4).
1199
+ */
1200
+ questionPrimaryAudit(): ReturnType<typeof auditQuestionPrimaries>;
1201
+ /**
1202
+ * Drop an acknowledgement or a warning whose time is up.
1203
+ *
1204
+ * Called before a frame is built and by the expiry timer, so every reader — the
1205
+ * footer, the Screen strip, `/diag` and the tests — sees the same thing at the same
1206
+ * moment: the row is the session's again once the sentence has been readable.
1207
+ */
1208
+ private expireFeedback;
1209
+ /**
1210
+ * Repaint when the newest feedback expires, so the row goes back by itself.
1211
+ *
1212
+ * Unref'd: this timer exists to refresh a UI that is already running, and a Host
1213
+ * with nothing else pending (no window attached, no turn) may exit instead — the
1214
+ * expiry is evaluated against the timestamp on the next frame anyway, so nothing
1215
+ * depends on the callback being reached.
1216
+ */
1217
+ private armFeedbackExpiry;
1218
+ /** The footer echo the reader can see right now, if any (a test/`/diag` seam). */
1219
+ currentFooterEcho(): string | undefined;
1220
+ /** The ephemeral notice the reader can see right now, if any. */
1221
+ currentNotice(): string | undefined;
1222
+ /** The audit as text, for the dev script and for `/diag`-style reporting. */
1223
+ formatRepresentationAudit(): string;
1224
+ /** The rows the reader can see: everything after the `/clear` cutoff. */
1225
+ private visibleRows;
761
1226
  private workspaceRowsFor;
762
- private paintInspectOverlay;
1227
+ /**
1228
+ * Open one report as a Screen.
1229
+ *
1230
+ * A report is not narrative: it cannot be rebuilt from the session log (it is not
1231
+ * a session event), so leaving it in the transcript meant the reader scrolled
1232
+ * through something that would vanish on the next resume, pushed the real history
1233
+ * out of the window (`/help` moved the anchor 53 rows, measured) and cost a full
1234
+ * clear per invocation. It is ephemeral by design: a Screen, gone when dismissed,
1235
+ * never written to the log (AD-7).
1236
+ *
1237
+ * Only the *successful* path comes here. A command that fails still writes an
1238
+ * `error` row: a failure is part of the session's story and has to outlive the
1239
+ * visit.
1240
+ */
1241
+ private openReport;
1242
+ /**
1243
+ * Open a Screen: it replaces the workspace for as long as it is up.
1244
+ *
1245
+ * The workspace's own state — the transcript window, `scrollOffset`, the focused
1246
+ * card, `/find` hits, the composer draft, and whatever Surface is queued behind
1247
+ * it — is neither touched here nor rebuilt on the way out. A Screen is a visit,
1248
+ * not a second workspace.
1249
+ */
1250
+ private openScreen;
1251
+ /** Leave the Screen and hand the pixels back to the workspace, as they were. */
1252
+ closeScreen(): void;
1253
+ /**
1254
+ * Ask a confirmation in whichever channel owns the screen.
1255
+ *
1256
+ * A Screen's own action (the doctor repair) needs a human decision, and while a
1257
+ * Screen is up that question belongs to it: it is painted on the Screen's hint
1258
+ * row and settled by the Screen's key handler, so it never enters the workspace
1259
+ * `dialogQueue` and a workspace question waiting there is left untouched (AD-3).
1260
+ * Returns `undefined` when no Screen is up, so the caller uses the ordinary path.
1261
+ */
1262
+ private askScreenConfirm;
1263
+ /** Settle the Screen's own confirmation, if one is up. */
1264
+ private settleScreenConfirm;
1265
+ /**
1266
+ * Paint the Screen that is up: it replaces the workspace entirely.
1267
+ *
1268
+ * A Screen is state (`ScreenState`) plus this renderer, and nothing else: the
1269
+ * same state redraws the same picture, which is what makes a reattach after an
1270
+ * SSH drop ordinary rather than special. Only the *first* frame of a Screen —
1271
+ * opening, a resize, a reattach — clears the screen; scrolling a report repaints
1272
+ * the body rows that actually changed and leaves the title, hint and strip alone.
1273
+ * The previous implementation passed `sizeChanged: true` on every frame, so every
1274
+ * arrow key cost a full clear and a full frame (measured: 2.7 KB per keypress at
1275
+ * 100×20).
1276
+ */
1277
+ private paintScreen;
1278
+ /**
1279
+ * The Screen's bottom hint row: how to leave, where the reader is, how to move.
1280
+ *
1281
+ * Composed in that order so truncation takes the least load-bearing part first:
1282
+ * a reader on a 48-column terminal still sees `全文 12–16/80 · Esc 返回`, while a
1283
+ * wide one also gets the navigation keys. A confirmation owned by the Screen, and
1284
+ * a notice from an action it performed, take the row outright — both are answers
1285
+ * to something the reader just did, and both belong to *this* Screen rather than
1286
+ * to the workspace's dialog queue (AD-3).
1287
+ */
1288
+ private screenHintLine;
1289
+ /**
1290
+ * The setup Screen's rows, built from the wizard's state every frame.
1291
+ *
1292
+ * The order is the contract (B2.6 §8): what this step *is*, then its prose, then the
1293
+ * control the reader acts on, then validation, then the keys. The layout keeps the
1294
+ * last two whatever the height — prose is what gets cut, never the field — and the
1295
+ * secret step renders the key masked, because a Screen is a place other people can
1296
+ * see (B2.6 §4/§8).
1297
+ */
1298
+ private renderSetupBody;
1299
+ /**
1300
+ * Fit prose and controls into whatever rows are left.
1301
+ *
1302
+ * Under height pressure the *prose* is what goes: the field, the notice and the key
1303
+ * hints are how the reader continues, and a setup screen whose only control was cut
1304
+ * away is a dead end (B2.6 §8).
1305
+ */
1306
+ private windowSetupBody;
1307
+ /** Rows the setup body may use: the Screen's own layout, minus the step indicator. */
1308
+ private setupBodyBudget;
1309
+ /** A Screen's body, wrapped for the width it will be shown at. */
1310
+ private renderScreenBody;
763
1311
  /**
764
1312
  * Reverse-video (or a `»` in monochrome) on the overlay row that holds the
765
1313
  * current `/find` needle, matching what the transcript does for the same hit.
@@ -832,20 +1380,83 @@ export declare class SshTui {
832
1380
  private runFindCommand;
833
1381
  private stepSearch;
834
1382
  private paint;
1383
+ /**
1384
+ * The dock line for a terminal with no room for the card.
1385
+ *
1386
+ * `compact` keeps the current step (the one thing a reader acts on) and `minimal`
1387
+ * keeps only the mode and the progress. Both stay one row: the dock is under height
1388
+ * pressure precisely because the workspace is small, and a second row would come out
1389
+ * of the transcript.
1390
+ */
1391
+ private planDockCompactLine;
1392
+ /**
1393
+ * The runtime facts the footer and a Screen's compact strip both report.
1394
+ *
1395
+ * One derivation, two readers. The Screen strip could have read `this.*` for
1396
+ * itself, but then a new activity kind would have to be taught to two assemblers
1397
+ * and the two surfaces could disagree about whether the agent is running — the
1398
+ * second truth source the decision record forbids (AD-8). The two parameters are
1399
+ * the composer's *shape*, the one input that is a property of the frame rather
1400
+ * than of the session; the strip does not read them.
1401
+ */
1402
+ private footerFacts;
1403
+ private paintFrame;
835
1404
  private workspaceCwd;
836
1405
  private announceWorkspaceCwd;
837
1406
  private buildSuggestions;
838
1407
  private suggestionsVisible;
839
1408
  private currentProviderId;
840
1409
  private currentSelectionLabel;
841
- /** Compact session stats groups for the first footer row. */
842
- private statsText;
1410
+ /**
1411
+ * The session's own telemetry, in the shapes the old status line used.
1412
+ *
1413
+ * Kept because the footer is not the only reader: the numbers a user files a
1414
+ * bug report with (turns, steps, model time, tool time, cache hit, and the
1415
+ * exact per-step decode rate) are still part of the session's public surface,
1416
+ * and this is the accessor `/status` and the tests reach for. The default row
1417
+ * itself no longer shows any of it.
1418
+ */
1419
+ statsText(): string;
1420
+ /**
1421
+ * When the activity the status row is reporting began.
1422
+ *
1423
+ * Each state has its own natural clock, and the row shows whichever one
1424
+ * belongs to the state it is painting: a tool shows how long that tool has
1425
+ * run, a subagent the age of the oldest child, a compaction its card, and a
1426
+ * streaming step the time since its first delta. A state with no honest clock
1427
+ * — plan mode, a goal, a question nobody has answered yet — gets none, so the
1428
+ * chip shows the verb alone rather than borrowing a number that measures
1429
+ * something else.
1430
+ */
1431
+ private footerActivityStartedAt;
1432
+ /**
1433
+ * What the open tool calls are, as one word for the activity chip.
1434
+ *
1435
+ * One call names itself through the transcript's own tool vocabulary
1436
+ * (`toolTitle`, the same table the cards use, so `bash` reads `terminal` in
1437
+ * both places). Several at once get the count instead, because "which one" has
1438
+ * no single answer and a comma-separated list is not a status chip.
1439
+ */
1440
+ private activityToolLabel;
1441
+ private agentStatusRunning;
843
1442
  /** Refresh the terminal window title (throttled while running). */
844
1443
  private updateTerminalTitle;
845
1444
  /** Terminal bell on completion (opt out with DSH_TUI_NO_BELL=1). */
846
1445
  private playCompletionSignal;
847
1446
  private render;
848
1447
  /** Whether the previous frames have not drained yet (slow link, big burst). */
1448
+ /**
1449
+ * Whether the last frame is still on its way to the terminal.
1450
+ *
1451
+ * Two wires, one question. A direct TTY is `stdout`; a relayed session writes
1452
+ * into the display socket, and that socket is the one that knows how much has
1453
+ * not left yet. The relay path used to answer "no, never backlogged", which left
1454
+ * the Host with no way to tell a link that is keeping up from one that is not —
1455
+ * so it paced itself from the measured round trip alone, and a drag on a fast
1456
+ * link waited for a cadence nobody needed.
1457
+ */
1458
+ private outputBacklogged;
1459
+ /** The name the paint path has always used for this question. */
849
1460
  private stdoutBacklogged;
850
1461
  /**
851
1462
  * `/preset`: list, inspect, copy, rename, describe, and delete agent presets.
@@ -963,6 +1574,22 @@ export declare class SshTui {
963
1574
  private readonly handleError;
964
1575
  private readonly handleInboxClaimed;
965
1576
  private readonly handleInboxDiscarded;
1577
+ /**
1578
+ * Say that a message is queued, and hold the notice until it is really submitted.
1579
+ *
1580
+ * The one notice that must outlive its clock: it is the only sign the reader has that
1581
+ * something is waiting, and it stays true until the step claims it — so it lives as
1582
+ * long as the queue does (`noteQueueDrained` clears it), not as long as a timer says.
1583
+ */
1584
+ private showQueuedNotice;
1585
+ /**
1586
+ * The last queued message was claimed (or dropped): the acknowledgement goes with it.
1587
+ *
1588
+ * This is what "until it is really submitted" means — the notice is not about the
1589
+ * keystroke that queued the message, it is about the message still waiting, so it
1590
+ * ends where the waiting does.
1591
+ */
1592
+ private noteQueueDrained;
966
1593
  private readonly handleDisposed;
967
1594
  /**
968
1595
  * One `workspace/changes` event: the files a turn changed.
@@ -1081,9 +1708,107 @@ export declare class SshTui {
1081
1708
  * {@link TUI_SOURCE_KIND}.
1082
1709
  */
1083
1710
  private tellModelApprovalDenied;
1711
+ /**
1712
+ * The card an approval request guards, marked as *being asked*.
1713
+ *
1714
+ * Waiting is a live-only state: it is true while a Host is holding the request
1715
+ * open, and a replay can never claim it (see `approval/asked`). Set here rather
1716
+ * than in the auto path so a manual prompt and an automatic decision both show
1717
+ * the same thing on the card while the decision is being made.
1718
+ * @param request - the pending approval.
1719
+ * @param provenance - who is deciding: a person (`live`) or this plugin (`policy`).
1720
+ */
1721
+ private markApprovalWaiting;
1722
+ /**
1723
+ * Record the decision on the card it guarded.
1724
+ *
1725
+ * `provenance` is the strongest source that produced it: `policy` for a rule, a
1726
+ * cached verdict or a reviewer, `live` for the person at the keyboard. The
1727
+ * Harness's own `approval/decided` event follows this on the same turn and says
1728
+ * strictly less, so `mergeApproval` lets this win (B2.4).
1729
+ */
1730
+ private settleToolApproval;
1084
1731
  readonly handleApproval: (request: ApprovalRequest, _next: () => Promise<ApprovalOutcome>) => Promise<ApprovalOutcome>;
1732
+ /**
1733
+ * Answer a question whose window closed but whose call the Session still holds.
1734
+ *
1735
+ * `ctx.userQuestions.answer(agent, callId, batch)` is the Harness's own channel
1736
+ * for this: it steers a `user-question-reply` message into the agent, and that
1737
+ * message is what closes the question in the projection. Two preconditions come
1738
+ * from the service, not from us — the agent must be the session's live runtime
1739
+ * root, and the question must still be `continued` — so a refusal here is
1740
+ * reported rather than retried.
1741
+ * @param row - the focused question card.
1742
+ * @returns true when the card consumed the key.
1743
+ */
1744
+ private answerContinuedQuestion;
1745
+ /**
1746
+ * The text field a surface borrowed, when the surface needs typed input.
1747
+ *
1748
+ * The composer's draft used to *be* every dialog's text field: a free-text
1749
+ * answer, a setup field and a picker's search all wrote `this.input`. That made
1750
+ * "what does this string mean" depend on which mode happened to be active, and
1751
+ * a paste or a cancelled answer could overwrite the message the reader was
1752
+ * writing. A surface that needs text now gets its own field; the composer keeps
1753
+ * its draft until the composer is the thing being typed into.
1754
+ */
1755
+ private borrowedText;
1756
+ /** Whether this dialog types into a field of its own rather than the composer. */
1757
+ private dialogNeedsText;
1758
+ /** Give a text-needing dialog its own field. The composer draft stays put. */
1759
+ private borrowTextFor;
1760
+ private releaseBorrowedText;
1761
+ /** The text the keyboard is editing: a borrowed field, or the composer draft. */
1762
+ private fieldText;
1763
+ private fieldCursor;
1764
+ /** Write the field the keyboard is editing, wherever it lives. */
1765
+ private setField;
1766
+ /** Insert at the caret of whichever field owns the keyboard. */
1767
+ private insertIntoField;
1768
+ /** One card's key: a call's batch can repeat a question id across calls. */
1769
+ private questionCardKey;
1770
+ /**
1771
+ * The Session's durable question view, when the projection is registered.
1772
+ *
1773
+ * `ctx.sessionProjections` is driven by `@deepseek-ai/dsh-user-questions`, which
1774
+ * folds `tool/call` + `tool/result` (and late replies) into
1775
+ * `{ active, settled }`. Absent in a bare Context — a test, an embedder without
1776
+ * the projection package — which is why every caller here has a live fallback.
1777
+ */
1778
+ private questionProjection;
1779
+ /**
1780
+ * Draw the cards the Session log can account for.
1781
+ *
1782
+ * Called for the events that can change question state, live and replayed alike,
1783
+ * so the two paths produce the same transcript. It never removes a card: a card
1784
+ * is history once it is drawn, and a call the projection cannot see (the legacy
1785
+ * blocking schema, or no projection at all) is the live path's to own.
1786
+ */
1787
+ private syncQuestionRows;
1788
+ /** One durable record onto its card, creating the card the first time. */
1789
+ private applyQuestionRecord;
1790
+ /**
1791
+ * The card for one call's question, created from its structured form if new.
1792
+ *
1793
+ * A **plan review** gets none (B2.5 §7): it is the plan artifact's interaction, not
1794
+ * an ordinary ask, and the artifact already has a row — the same review drawn as
1795
+ * both a question summary and a plan state is the duplicate this round removes. The
1796
+ * Surface (the dialog) is unchanged, and `dsh-plan-mode` identifies the review by the
1797
+ * `exit_plan_mode` call it belongs to, which the projection reads.
1798
+ * @returns the card, or undefined when the question belongs to another artifact.
1799
+ */
1800
+ private ensureQuestionCard;
1085
1801
  readonly handleUserQuestions: (request: AskUserQuestionRequest) => Promise<AskUserQuestionAnswer>;
1086
- /** Queue one dialog behind an already-open one instead of overwriting it. */
1802
+ /**
1803
+ * Queue one dialog behind an already-open one instead of overwriting it.
1804
+ *
1805
+ * The active surface is never replaced: whoever owns the keyboard keeps it until
1806
+ * it is answered or cancelled, so a picker can never cover a question (and a
1807
+ * question can never silently discard a half-made choice). What priority decides
1808
+ * is the *line*: a task interaction takes the keyboard ahead of a picker that was
1809
+ * already waiting, because someone is blocked on it. Ranks are compared, not
1810
+ * stacked — equal ranks keep arrival order.
1811
+ */
1087
1812
  private openDialog;
1088
1813
  private showNextDialog;
1089
1814
  /**
@@ -1099,8 +1824,16 @@ export declare class SshTui {
1099
1824
  /** Resolve one queued or active confirm from its abort signal. */
1100
1825
  private abortConfirm;
1101
1826
  private openQuestion;
1102
- /** Open one question dialog and await its answer (cancellation rejects). */
1827
+ /**
1828
+ * Open one control-plane question and await its answer (cancellation rejects).
1829
+ *
1830
+ * Every `/model`-style menu goes through here, which is why the role is
1831
+ * `picker`: the agent is neither waiting nor interrupted by a menu that only
1832
+ * changes the environment.
1833
+ */
1103
1834
  private askQuestion;
1835
+ /** One question dialog with an explicit role, awaited by the caller. */
1836
+ private openQuestionAs;
1104
1837
  /** The stored llm-pi-ai profile for one provider route, when settings provide one. */
1105
1838
  private piAiProviderProfile;
1106
1839
  /**
@@ -1396,11 +2129,57 @@ export declare class SshTui {
1396
2129
  private resolveSuperGrokToken;
1397
2130
  private fetchJson;
1398
2131
  private readonly handleData;
2132
+ /**
2133
+ * One key, handled by the Screen that is up.
2134
+ *
2135
+ * The Screen owns the keyboard completely, so this is also the place that keeps
2136
+ * the workspace's state out of reach: no arrow moves a card behind the Screen, no
2137
+ * page key scrolls the transcript, no print reaches the composer draft. Closing
2138
+ * keys come from `inspectClosesOn`, which is what the overlay always accepted —
2139
+ * plus `q`, which a full-screen report is expected to answer to.
2140
+ */
2141
+ private handleScreenInput;
2142
+ /**
2143
+ * Whether anything human is waiting behind the Screen that is up.
2144
+ *
2145
+ * A Surface cannot open while a Screen owns the screen (it stays in the workspace
2146
+ * queue), so without this the reader would have no way to know that an approval
2147
+ * or a question is waiting for them behind the report — the one thing a Screen
2148
+ * must never hide (AD-8). A question the Session still holds counts too: the agent
2149
+ * is not blocked, but the reader has something to answer.
2150
+ */
2151
+ private screenWaitingWork;
2152
+ /** Rows one PageUp moves a Screen: the body, less one row of context. */
2153
+ private screenPage;
2154
+ /** Scroll a Screen by `up` rows toward the start of its body. */
2155
+ private scrollScreen;
2156
+ private scrollScreenTo;
2157
+ /** The copy key inside a Screen: what the Screen shows is what the reader means. */
2158
+ private copyFromScreen;
1399
2159
  private handleInputText;
1400
2160
  /** Handle one data chunk that may contain bracketed-paste markers. */
1401
2161
  private processPasteChunk;
1402
- /** Insert pasted text into the input buffer; CR/LF are literal newlines. */
2162
+ /**
2163
+ * Insert pasted text into the field that owns the keyboard; CR/LF are literal
2164
+ * newlines.
2165
+ *
2166
+ * A paste is text input, so it goes through the same ownership question ordinary
2167
+ * typing does: into the composer's draft when the composer owns the keyboard, and
2168
+ * into a borrowed field when a surface that has one does. A surface without a
2169
+ * text field (a confirmation, the inspect overlay) takes no paste at all — the
2170
+ * alternative was writing the composer's hidden draft behind it, which is how a
2171
+ * paste into an approval dialog could silently rewrite the message being written.
2172
+ */
1403
2173
  private handlePasteText;
2174
+ /**
2175
+ * Whether the keys that edit text belong to a field right now.
2176
+ *
2177
+ * The composer always has one; an open surface only has one when it borrowed it
2178
+ * (a free-text answer, a setup field). Everything else — a confirmation, a menu,
2179
+ * the inspect overlay — owns the keyboard without owning text, and those keys
2180
+ * must do nothing rather than reach the draft behind the surface.
2181
+ */
2182
+ private textIsEditable;
1404
2183
  private handlePlainText;
1405
2184
  private handleChar;
1406
2185
  /**
@@ -1442,6 +2221,33 @@ export declare class SshTui {
1442
2221
  private onboardingWantsEndpoint;
1443
2222
  /** Fill the picks' capacities and endpoints from the gateway; never fatal. */
1444
2223
  private enrichOnboardingModels;
2224
+ /** The wizard's field text, from the wizard's own state. */
2225
+ private setupField;
2226
+ /** Replace the wizard's field and caret; the composer is not touched. */
2227
+ private setupSetField;
2228
+ /** Insert into the wizard's field at its caret. */
2229
+ private setupInsert;
2230
+ /** Delete the code point before the wizard's caret. */
2231
+ private setupBackspace;
2232
+ /**
2233
+ * Say something inside the setup Screen.
2234
+ *
2235
+ * Framed mode keeps it on the Screen's own message row — the wizard is a screen,
2236
+ * not history, and a `Step 1 done` transcript would be a record of a flow that
2237
+ * only ever ran once (B2.6 §12/§13). Line mode has no screen to say it on and has
2238
+ * always printed the wizard's messages as text, so it still appends one.
2239
+ */
2240
+ private setupMessage;
2241
+ /**
2242
+ * The setup Screen's key handling: its own field, its own navigation.
2243
+ *
2244
+ * Everything the wizard used to get from the workspace's composer and its
2245
+ * dialog key path is handled here — the caret keys edit the wizard's field, the
2246
+ * arrows move the wizard's pickers, Esc steps back through the picker steps or
2247
+ * abandons the flow, and the confirm step answers itself. A key this does not
2248
+ * consume is *not* forwarded anywhere: a setup screen has no other owner (B2.6 §4).
2249
+ */
2250
+ private handleSetupKey;
1445
2251
  private handleOnboardingChar;
1446
2252
  private advanceOnboarding;
1447
2253
  /**
@@ -1491,6 +2297,8 @@ export declare class SshTui {
1491
2297
  private plainSelectableLines;
1492
2298
  /** Screen row from a mouse report to an index into `selectableLines`. */
1493
2299
  private selectableLineAt;
2300
+ /** The last transcript row the interaction does not cover, or the whole list. */
2301
+ private lastSelectableLine;
1494
2302
  /**
1495
2303
  * Left button pressed. The click itself waits for the release: this may turn
1496
2304
  * into a drag, and a press that never moves must still open a link or toggle
@@ -1505,8 +2313,48 @@ export declare class SshTui {
1505
2313
  private highlightSelection;
1506
2314
  handleMouseClick(y: number, x?: number): void;
1507
2315
  private copyPlainText;
2316
+ /**
2317
+ * `/copy [reply|highlight|error]` — copy plain text to the local clipboard.
2318
+ *
2319
+ * The default is the model's **last reply**, which is what a reader wants on
2320
+ * the clipboard most of the time: the answer they just read, whole, ready to
2321
+ * paste into a report. It used to be "the highlighted card, else the newest
2322
+ * reply", so any card left highlighted (one click on a tool card does it)
2323
+ * silently redirected the copy and the reply became unreachable without
2324
+ * deselecting first. `highlight` is that old rule, now said out loud.
2325
+ */
1508
2326
  copyFocusedCard(arg?: string): boolean;
1509
- private scrollInspectOrTranscript;
2327
+ /**
2328
+ * The copy key: what the reader is *pointing at*.
2329
+ *
2330
+ * The key and the command are two grammars over the same act, and they differ
2331
+ * in exactly one place: with nothing highlighted the key falls back to the
2332
+ * newest reply (a key that copies nothing reads as a broken key), while
2333
+ * `/copy highlight` says there is nothing highlighted. Inside a Surface the key
2334
+ * is the only way to copy at all: a Screen carries what it shows, and every
2335
+ * other dialog keeps the key inert, exactly as it always was.
2336
+ */
2337
+ private copyKey;
2338
+ /** The newest error or diagnostic row, whole. */
2339
+ private copyErrorRow;
2340
+ /** The highlighted card — or, for the key, the newest reply when none is. */
2341
+ private copyHighlighted;
2342
+ /** The model's last reply: the `/copy` default, and the key's last resort. */
2343
+ private copyLatestReply;
2344
+ /**
2345
+ * Scroll whichever surface the reader is looking at, by `up` lines toward the
2346
+ * earlier content.
2347
+ *
2348
+ * One sign for every caller, because the two surfaces count in opposite
2349
+ * directions: the transcript's `scrollOffset` is the distance *back* from the
2350
+ * newest line, and the overlay's `offset` is the index of its first visible line.
2351
+ * Adding one delta to both — which is what this did — made PgUp and the wheel
2352
+ * scroll the overlay the wrong way, while the arrow keys, which passed their own
2353
+ * inverted sign, were the only ones that felt right. A surface that is not the
2354
+ * inspect overlay owns the keyboard and takes no scroll at all: the transcript
2355
+ * behind a dialog must not move.
2356
+ */
2357
+ private scrollActiveSurface;
1510
2358
  private handleCtrlC;
1511
2359
  private submit;
1512
2360
  private runCommand;
@@ -1514,8 +2362,11 @@ export declare class SshTui {
1514
2362
  private graphemeBefore;
1515
2363
  /** Grapheme cluster containing or following `cursor`. */
1516
2364
  private graphemeAfter;
2365
+ /** Delete one grapheme back, in whichever field owns the keyboard. */
1517
2366
  private backspace;
2367
+ /** Delete one grapheme forward, in whichever field owns the keyboard. */
1518
2368
  private deleteAtCursor;
2369
+ /** Move the caret one grapheme, in whichever field owns the keyboard. */
1519
2370
  private moveCursor;
1520
2371
  private historyBack;
1521
2372
  private historyForward;