dsh-ssh-tui 0.7.1 → 0.7.3

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 (106) hide show
  1. package/README.en.md +92 -8
  2. package/README.md +64 -8
  3. package/cordis.patch.yml +15 -0
  4. package/docs/terminals.md +113 -0
  5. package/lib/approval-cache.js +12 -9
  6. package/lib/approval-cache.js.map +1 -1
  7. package/lib/color-depth.js +10 -4
  8. package/lib/color-depth.js.map +1 -1
  9. package/lib/diag.js +76 -17
  10. package/lib/diag.js.map +1 -1
  11. package/lib/dialogs.js.map +1 -1
  12. package/lib/display-sock.js +304 -29
  13. package/lib/display-sock.js.map +1 -1
  14. package/lib/doctor.js +71 -37
  15. package/lib/doctor.js.map +1 -1
  16. package/lib/dsh-compat.js +206 -88
  17. package/lib/dsh-compat.js.map +1 -1
  18. package/lib/footer.js +39 -9
  19. package/lib/footer.js.map +1 -1
  20. package/lib/gateway-protocol.js +202 -0
  21. package/lib/gateway-protocol.js.map +1 -0
  22. package/lib/i18n/en.js +79 -41
  23. package/lib/i18n/en.js.map +1 -1
  24. package/lib/i18n/index.js +13 -7
  25. package/lib/i18n/index.js.map +1 -1
  26. package/lib/i18n/zh.js +79 -41
  27. package/lib/i18n/zh.js.map +1 -1
  28. package/lib/index.js +132 -31
  29. package/lib/index.js.map +1 -1
  30. package/lib/job-label.js +99 -17
  31. package/lib/job-label.js.map +1 -1
  32. package/lib/paint.js +5 -2
  33. package/lib/paint.js.map +1 -1
  34. package/lib/picker.js +5 -3
  35. package/lib/picker.js.map +1 -1
  36. package/lib/plan.js +269 -17
  37. package/lib/plan.js.map +1 -1
  38. package/lib/platform.js +373 -0
  39. package/lib/platform.js.map +1 -0
  40. package/lib/preset-authoring.js +10 -14
  41. package/lib/preset-authoring.js.map +1 -1
  42. package/lib/preset-compat.js +100 -0
  43. package/lib/preset-compat.js.map +1 -0
  44. package/lib/preset-picker.js +5 -1
  45. package/lib/preset-picker.js.map +1 -1
  46. package/lib/preset-rows.js +83 -13
  47. package/lib/preset-rows.js.map +1 -1
  48. package/lib/provider-catalog.js +7 -4
  49. package/lib/provider-catalog.js.map +1 -1
  50. package/lib/route-memory.js +11 -4
  51. package/lib/route-memory.js.map +1 -1
  52. package/lib/session-index.js +5 -0
  53. package/lib/session-index.js.map +1 -1
  54. package/lib/session-list.js +18 -1
  55. package/lib/session-list.js.map +1 -1
  56. package/lib/session-lock.js +79 -24
  57. package/lib/session-lock.js.map +1 -1
  58. package/lib/session-route.js +334 -0
  59. package/lib/session-route.js.map +1 -0
  60. package/lib/settings-routes.js +10 -0
  61. package/lib/settings-routes.js.map +1 -0
  62. package/lib/settings-subagent.js +10 -0
  63. package/lib/settings-subagent.js.map +1 -0
  64. package/lib/subagent-model.js +84 -6
  65. package/lib/subagent-model.js.map +1 -1
  66. package/lib/supergrok-token.js +4 -0
  67. package/lib/supergrok-token.js.map +1 -1
  68. package/lib/term-text.js +7 -4
  69. package/lib/term-text.js.map +1 -1
  70. package/lib/terminal-caps.js +358 -0
  71. package/lib/terminal-caps.js.map +1 -0
  72. package/lib/tool-present.js +46 -14
  73. package/lib/tool-present.js.map +1 -1
  74. package/lib/tui.js +1949 -371
  75. package/lib/tui.js.map +1 -1
  76. package/lib/types/color-depth.d.ts +3 -3
  77. package/lib/types/diag.d.ts +18 -1
  78. package/lib/types/dialogs.d.ts +4 -0
  79. package/lib/types/display-sock.d.ts +94 -2
  80. package/lib/types/doctor.d.ts +8 -1
  81. package/lib/types/dsh-compat.d.ts +141 -44
  82. package/lib/types/footer.d.ts +21 -5
  83. package/lib/types/gateway-protocol.d.ts +88 -0
  84. package/lib/types/i18n/index.d.ts +18 -12
  85. package/lib/types/index.d.ts +45 -0
  86. package/lib/types/job-label.d.ts +38 -7
  87. package/lib/types/picker.d.ts +3 -0
  88. package/lib/types/plan.d.ts +94 -4
  89. package/lib/types/platform.d.ts +215 -0
  90. package/lib/types/preset-authoring.d.ts +8 -14
  91. package/lib/types/preset-compat.d.ts +58 -0
  92. package/lib/types/preset-picker.d.ts +1 -1
  93. package/lib/types/preset-rows.d.ts +51 -9
  94. package/lib/types/route-memory.d.ts +17 -22
  95. package/lib/types/session-list.d.ts +5 -0
  96. package/lib/types/session-lock.d.ts +9 -0
  97. package/lib/types/session-route.d.ts +191 -0
  98. package/lib/types/settings-routes.d.ts +16 -0
  99. package/lib/types/settings-subagent.d.ts +24 -0
  100. package/lib/types/subagent-model.d.ts +60 -7
  101. package/lib/types/term-text.d.ts +3 -2
  102. package/lib/types/terminal-caps.d.ts +105 -0
  103. package/lib/types/tool-present.d.ts +2 -17
  104. package/lib/types/transcript-types.d.ts +44 -1
  105. package/lib/types/tui.d.ts +333 -9
  106. package/package.json +82 -55
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { AskUserQuestionItem } from '@deepseek-ai/dsh-user-questions';
5
5
  import { type TextSegment } from './term-text.js';
6
- import type { DisplayKind, Row, ToolDiffHunk } from './transcript-types.js';
6
+ import type { DiffDisplayLine, Row, ToolDiffHunk } from './transcript-types.js';
7
7
  export declare const SHELL_TOOL_NAMES: Set<string>;
8
8
  export declare const DIFF_TOOL_NAMES: Set<string>;
9
9
  export declare const JOB_TOOL_NAMES: Set<string>;
@@ -12,7 +12,6 @@ export declare function formatModelList(models: readonly string[], max?: number)
12
12
  /** Prefer the fields a human scans for; fall back to the first scalar pairs. */
13
13
  export declare function friendlyArgsSummary(name: string, args: string): string;
14
14
  export declare function countDiffLines(hunks: readonly ToolDiffHunk[] | undefined): number;
15
- /** Added / removed line counts for a diff (`oldText: null` means a new file). */
16
15
  export declare function countDiffAddDel(hunks: readonly ToolDiffHunk[] | undefined): {
17
16
  add: number;
18
17
  del: number;
@@ -121,21 +120,7 @@ export declare function presentToolCall(name: string, args: string): {
121
120
  export declare function diffMetaDiffs(meta: unknown): ToolDiffHunk[] | null;
122
121
  /** Split one diff side into content lines (trailing newline is a terminator). */
123
122
  export declare function diffContentLines(text: string): string[];
124
- /** One rendered diff body line with its display role. */
125
- export interface DiffDisplayLine {
126
- kind: DisplayKind;
127
- text: string;
128
- /**
129
- * Ranges of `text` to emphasise, in UTF-16 offsets. They travel with the line
130
- * rather than being baked in as escapes because the painter sanitises the text
131
- * it styles — an escape inserted down here would be stripped, leaving a
132
- * literal `[7m` on screen.
133
- */
134
- spans?: readonly {
135
- start: number;
136
- end: number;
137
- }[];
138
- }
123
+ export type { DiffDisplayLine } from './transcript-types.js';
139
124
  /** Cap one flat diff/body row list to `maxLines` while preserving the final line. */
140
125
  export declare function capDisplayLines(lines: readonly DiffDisplayLine[], maxLines: number): DiffDisplayLine[];
141
126
  /** Running / ok / error → ANSI color for the status dot and status word only. */
@@ -8,6 +8,10 @@ export type SubagentLogKind = 'user' | 'assistant' | 'tool' | 'result' | 'turn'
8
8
  export interface SubagentLogEntry {
9
9
  kind: SubagentLogKind;
10
10
  text: string;
11
+ /** Parent tool-call id, so a later result can settle on the same log line. */
12
+ callId?: string;
13
+ /** Result body kept beside the call line, when the caller has one. */
14
+ detail?: string;
11
15
  }
12
16
  /** One todo-list item as the plan card renders it. */
13
17
  export interface PlanTodoItem {
@@ -62,14 +66,24 @@ export type Row = {
62
66
  } | {
63
67
  kind: 'subagent';
64
68
  sessionId: string;
69
+ /** Live child id when this chip was first built from the parent spawn tool. */
70
+ childSessionId?: string;
71
+ /** Parent spawn tool-call id, so the wrapper's result can find this child. */
72
+ spawnCallId?: string;
73
+ /** Model route the child runs on. Never the subagent backend name (`spawn`). */
74
+ modelProvider?: string;
65
75
  runId: string;
66
76
  provider: string;
67
77
  local: boolean;
68
78
  label: string;
79
+ /** Parent `subagent` tool description, when the spawn call named the job. */
80
+ task?: string;
69
81
  status: 'running' | 'ok' | 'error' | 'aborted';
70
82
  startedAt: number;
71
83
  endedAt?: number;
72
84
  stopReason?: string;
85
+ /** One-line failure explanation shown on the collapsed chip. */
86
+ failHint?: string;
73
87
  lastActivity: string;
74
88
  logs: SubagentLogEntry[];
75
89
  expanded: boolean;
@@ -161,7 +175,36 @@ export type CollapsibleBlock = Extract<Row, {
161
175
  kind: 'streaming-reasoning';
162
176
  expanded: boolean;
163
177
  };
164
- export type DisplayKind = Row['kind'] | 'tool-result' | 'diff-add' | 'diff-del' | 'diff-path' | 'todo-done' | 'todo-active' | 'todo-pending' | 'todo-failed' | 'todo-skipped' | 'plan-dock';
178
+ export type DisplayKind = Row['kind'] | 'tool-result' | 'diff-add' | 'diff-del' | 'diff-path' | 'todo-done' | 'todo-active' | 'todo-pending' | 'todo-failed' | 'todo-skipped' | 'plan-dock' | 'subagent-header';
179
+ /** One rendered diff/inspect body line with its display role. */
180
+ export interface DiffDisplayLine {
181
+ kind: DisplayKind;
182
+ text: string;
183
+ /**
184
+ * Ranges of `text` to emphasise, in UTF-16 offsets. They travel with the line
185
+ * rather than being baked in as escapes because the painter sanitises the text
186
+ * it styles — an escape inserted down here would be stripped, leaving a
187
+ * literal `[7m` on screen.
188
+ */
189
+ spans?: readonly {
190
+ start: number;
191
+ end: number;
192
+ }[];
193
+ /**
194
+ * Parts of `text` that carry their own role, for a line that holds two at
195
+ * once: the columns of a side-by-side diff are a removal on the left and an
196
+ * addition on the right, and painting the row as either one would mislabel
197
+ * half of it. Offsets are UTF-16 into `text`; anything not covered keeps the
198
+ * line's own `kind`.
199
+ */
200
+ columns?: readonly DiffDisplayColumn[];
201
+ }
202
+ /** One part of a line, painted with its own display role. */
203
+ export interface DiffDisplayColumn {
204
+ start: number;
205
+ end: number;
206
+ kind: DisplayKind;
207
+ }
165
208
  /** One file's change, matching the web diff-card contract (`card: 'diff'`). */
166
209
  export interface ToolDiffHunk {
167
210
  path: string;
@@ -20,6 +20,8 @@ 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
22
  import type { SubagentRunEndInfo, SubagentRunInfo } from '@deepseek-ai/dsh-subagent';
23
+ import { type TerminalCapabilities } from './terminal-caps.js';
24
+ import { type SessionRoute } from './session-route.js';
23
25
  import { type SubagentSelectionRef } from './subagent-model.js';
24
26
  import { type AskUserQuestionAnswer, type AskUserQuestionRequest } from '@deepseek-ai/dsh-user-questions';
25
27
  import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval';
@@ -28,10 +30,10 @@ import { paintedLinkHits } from './term-text.js';
28
30
  export type { CollapsibleBlock, DisplayKind, DisconnectPolicyName, PlanTodoItem, Row, SubagentLogEntry, ToolDiffHunk, } from './transcript-types.js';
29
31
  export { clipAnsiToWidth, cursorVisualPosition, displayWidth, foldInputView, hrefAtColumn, osc52Clipboard, osc8Enabled, paintedLinkHits, fmtElapsedCompact, padAnsiToWidth, padToWidth, renderMarkdownLines, repeatToWidth, shimmerText, truncateToWidth, visibleWidth, waitCardCopy, waitSummaryFromReasoning, wrapWaitDetails, } from './term-text.js';
30
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';
31
- 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, 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 { 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';
32
34
  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';
33
- export { commandAcceptsAttachments, forEachSessionEvent, isAssistantStreamEvent, isTokenDeltaChunk, listPersistenceHeaders, inspectPersistenceSession, sessionEventType, sessionEvents, settingsNamespace, streamChunkOf, streamFirstTokenTime, streamFrameAttemptId, streamFrameOwner, } from './dsh-compat.js';
34
- export { applyTurnEndToPlan, askSummary, cardCategoryOf, compactionHeaderText, formatCompactCommandError, isPromptInjectionMessage, matchTranscriptRows, parseFindQuery, parsePlanTodos, planCloseNudgeText, planDockNote, planIsLive, planTitleFromMarkdown, planTurnLeftOpen, promptInjectionSources, promptInjectionTitle, subagentHeaderText, todoProgressLabel, todoSummary, type CardCategory, } from './plan.js';
35
+ export { commandAcceptsAttachments, forEachSessionEvent, isTokenDeltaChunk, listPersistenceHeaders, inspectPersistenceSession, sessionEvents, settingsNamespace, streamChunkOf, streamFirstTokenTime, streamFrameAttemptId, streamFrameOwner, } from './dsh-compat.js';
36
+ 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';
35
37
  export { buildToolHeader, canMergeToolCall, compactEditPath, compactToolBursts, compactToolGroups, countDiffAddDel, countDiffLines, countOutputLines, diffMetaDiffs, diffStatToken, friendlyJsonLines, parseExitStatus, presentToolCall, READ_TOOL_NAMES, renderToolDiff, toolBodyFitsWorkspace, toolBodyLines, toolStateColor, toolStateLabel, wrappedToolBodyLineCount, } from './tool-present.js';
36
38
  /** Presentation configuration for the terminal channel. */
37
39
  export interface TuiConfig {
@@ -51,6 +53,24 @@ export interface TuiConfig {
51
53
  resume?: boolean;
52
54
  /** One-line notice after entering a resumed session's working directory. */
53
55
  cwdNotice?: string;
56
+ /**
57
+ * The route this resumed session ran on, when it has a record. Applied before
58
+ * the TUI mounts; this is only for saying so on screen.
59
+ */
60
+ restoredRoute?: SessionRoute;
61
+ /**
62
+ * What the launcher decided about the resumed route: a supplier that is gone,
63
+ * or one that cannot be routed to at all. Pushed on screen at boot.
64
+ */
65
+ launchNotices?: readonly {
66
+ kind: 'system' | 'error';
67
+ text: string;
68
+ }[];
69
+ /**
70
+ * Report a settled route to the launcher, which owns the per-session record.
71
+ * Absent in tests and in line mode: nothing here touches the disk.
72
+ */
73
+ onRouteSettled?: (route: Omit<SessionRoute, 'updatedAt'>) => void;
54
74
  /** Provider route selected at launch (defaults to deepseek-official). */
55
75
  provider?: string;
56
76
  /** Model selected at launch (defaults to the saved/fallback model). */
@@ -59,6 +79,12 @@ export interface TuiConfig {
59
79
  selectionRef?: ModelSelectionRef;
60
80
  /** Settings-backed model/effort selection applied to subagent requests. */
61
81
  subagentSelection?: SubagentSelectionRef;
82
+ /**
83
+ * The terminal capabilities to act on. Defaults to reading the environment;
84
+ * tests pass their own so a suite does not change meaning with the terminal
85
+ * it happens to run in (`TERM=linux node --test` used to fail three cases).
86
+ */
87
+ terminalCaps?: TerminalCapabilities;
62
88
  /** Active agent-preset id (standard/code/minimal/cordis/...). */
63
89
  presetId?: string;
64
90
  /** Display name of the active preset. */
@@ -153,6 +179,19 @@ export declare class SshTui {
153
179
  private catalogLoad;
154
180
  /** Memoized id→window index over {@link catalogPresets}. */
155
181
  private catalogWindows;
182
+ /** Wizard drafts whose models step is already asking the gateway, so a
183
+ * second Enter cannot start a duplicate listing. */
184
+ private readonly finishingModels;
185
+ /** `<base>\0<model>` → the family row last seen serving that model. */
186
+ /**
187
+ * `<base>\0<model>` → the family row last seen serving that model.
188
+ *
189
+ * Bounded: a gateway can publish hundreds of models and their rows are
190
+ * re-learned on every listing, so the oldest entries are dropped once the map
191
+ * passes FAMILY_OWNER_CACHE_MAX rather than growing for the life of the
192
+ * session. Losing an entry only costs one future "which row?" lookup.
193
+ */
194
+ private readonly familyModelOwner;
156
195
  private input;
157
196
  private cursor;
158
197
  private inputFolded;
@@ -194,11 +233,22 @@ export declare class SshTui {
194
233
  private readonly selectionRef;
195
234
  private readonly subagentSelection;
196
235
  private readonly onSelectionChanged;
236
+ private readonly onRouteSettled;
197
237
  private readonly disposers;
198
238
  private userQuestionDisposer;
199
239
  private presetId;
200
240
  private presetName;
241
+ /** Last route reported to the launcher; a repaint must not report it again. */
242
+ private lastSessionRoute;
201
243
  private readonly useAlternateScreen;
244
+ /**
245
+ * What the terminal in front of us can do. Read from the environment at
246
+ * construction (see `terminal-caps.ts`): the mouse, bracketed paste and the
247
+ * alternate screen are only claimed where the terminal has them.
248
+ */
249
+ private readonly terminalCaps;
250
+ /** The OSC 52 caveat is worth saying once, not after every copy. */
251
+ private osc52HintShown;
202
252
  private agentGone;
203
253
  private onboarding;
204
254
  private commandSuggestions;
@@ -213,6 +263,8 @@ export declare class SshTui {
213
263
  private readonly seenCommandDoneIds;
214
264
  private activeSubagents;
215
265
  private subagentSessions;
266
+ /** Parent `subagent` tool descriptions waiting for the matching child card. */
267
+ private pendingSubagentTasks;
216
268
  private openToolCalls;
217
269
  /** Survives result settlement so a card-less result can still be labelled. */
218
270
  private toolCallNames;
@@ -350,6 +402,15 @@ export declare class SshTui {
350
402
  constructor(ctx: Context, agent: Agent, config: TuiConfig);
351
403
  /** Enter raw mode, switch to the alternate screen, and start listening. */
352
404
  start(): void;
405
+ /**
406
+ * Take over the screen: alternate screen where it is safe, then the input
407
+ * modes the terminal actually supports, then hide the cursor.
408
+ *
409
+ * Joining the parts here (rather than at each call site) is what keeps
410
+ * `start`, a reattach and a teardown from drifting apart — the reattach path
411
+ * used to duplicate this string verbatim.
412
+ */
413
+ private enterScreenSequence;
353
414
  private bindAgentEvents;
354
415
  private bootBackgroundTasks;
355
416
  private notifyPluginUpdate;
@@ -389,6 +450,20 @@ export declare class SshTui {
389
450
  * footer sat on `SSH ○○○○` for the whole load.
390
451
  */
391
452
  replayHistory(): Promise<void>;
453
+ /**
454
+ * Close the chips replay could not settle.
455
+ *
456
+ * A spawn `tool/call` with no `tool/result` in the log means the wrapper was
457
+ * still in flight when the log was last written. Replay only runs in a Host
458
+ * that has just started, and the child lived in the Host running back then,
459
+ * so that child is gone and no `subagent/end` will ever arrive for it.
460
+ * Leaving the chip "running" made every resume of an interrupted turn show a
461
+ * child counting up forever while `/status` reported none.
462
+ *
463
+ * `stopReason: 'unknown'` is also what lets a late live end adopt this row
464
+ * instead of adding a twin beside it.
465
+ */
466
+ private settleUnfinishedReplayChips;
392
467
  /** Show the first-launch provider/API-key onboarding when nothing is configured. */
393
468
  private maybeRunOnboarding;
394
469
  /** Run the provider/API-key onboarding wizard. Resolves true when saved. */
@@ -513,6 +588,9 @@ export declare class SshTui {
513
588
  private endWait;
514
589
  private waitCardSource;
515
590
  private findSubagentRow;
591
+ private subagentInspectId;
592
+ /** Courtesy name for a child session; never the raw session hash. */
593
+ private subagentNameFor;
516
594
  private findLivePlanRow;
517
595
  /** Older / finished plans stay in the scrolling transcript. */
518
596
  private archiveStalePlans;
@@ -531,16 +609,51 @@ export declare class SshTui {
531
609
  * including a monochrome terminal.
532
610
  */
533
611
  private styleEmphasisedLine;
612
+ /**
613
+ * A body row whose parts carry different roles: the two columns of a
614
+ * side-by-side diff.
615
+ *
616
+ * No single kind fits such a row — the left column is a removal, the right an
617
+ * addition — so each part is painted with its own style, the gutter keeps the
618
+ * row's own (neutral) one, and a word emphasis rides inside whichever column
619
+ * it belongs to. The first column takes the leading indent and the last one
620
+ * runs to the end of the row, so both bars are flush.
621
+ */
622
+ private styleColumnedLine;
534
623
  private paintToolBodyLine;
535
624
  private workspaceRowsFor;
536
625
  private paintInspectOverlay;
626
+ /**
627
+ * Reverse-video (or a `»` in monochrome) on the overlay row that holds the
628
+ * current `/find` needle, matching what the transcript does for the same hit.
629
+ */
630
+ private markSearchRow;
537
631
  private openToolInspect;
632
+ private openSubagentInspect;
633
+ /**
634
+ * Line mode has no framed scroller, so an inspect body goes into the log.
635
+ *
636
+ * Opening the modal there printed nothing and then swallowed the next Enter:
637
+ * an invisible dialog. The log is the only detail surface line mode has, and
638
+ * writing it keeps the input free.
639
+ */
640
+ private echoInspectToLog;
538
641
  closeInspect(): void;
539
642
  private paintCollapsibleHeader;
540
643
  /** Move the expand/collapse focus among reasoning and tool rows. */
541
644
  private moveCollapsibleFocus;
542
645
  /** Toggle the focused block; without focus, toggle the most recent one. */
543
646
  toggleCollapsible(): void;
647
+ /**
648
+ * Whether an inline tool body would be cut short.
649
+ *
650
+ * Two things stop a body short: it cannot fit the workspace, or the measured
651
+ * link is slow enough that the paint budget trims it. Both leave the reader
652
+ * looking at part of a diff under a header that counts all of it, and both
653
+ * are what the overlay is for — so both answers are the overlay, and the
654
+ * card's own `Enter 全览` hint stays true.
655
+ */
656
+ private toolBodyTruncated;
544
657
  toggleCard(target: CollapsibleBlock): void;
545
658
  /** Expand all collapsible blocks, or collapse them again when all are open. */
546
659
  toggleAllCollapsible(): void;
@@ -637,15 +750,37 @@ export declare class SshTui {
637
750
  private mutedSgr;
638
751
  private styleLine;
639
752
  /**
640
- * Fold one live or durable stream chunk into the in-progress assistant
641
- * row. 0.1.2 hosts append `assistant/chunk`; 0.1.5 emits the same chunk
642
- * on `agent/assistant-stream` and never writes it to the log.
753
+ * One emphasised piece of a body line — the words a diff changed.
754
+ *
755
+ * Inside a filled diff row the mark is a lighter shade of that same fill.
756
+ * Inverse video was there first, and it inverted the *fill*: a line replaced
757
+ * wholesale is emphasised from end to end, so it came out a bright block
758
+ * beside rows that kept the muted tone — visibly from another palette. On a
759
+ * terminal with no colour left to shift, the attribute is what survives, so
760
+ * the inverse video stays for that case.
761
+ */
762
+ private styleEmphasisedPiece;
763
+ /**
764
+ * Paint a row the reader has selected.
765
+ *
766
+ * The obvious `7`…`27` wrap only reaches the part before the line's first
767
+ * reset, and every painted segment ends with one: a focused card kept its
768
+ * marker and status dot highlighted while the title, the state word and the
769
+ * summary stayed plain, so ↑/↓ gave no visible clue which card was picked —
770
+ * on exactly the rows that carry a segmented header. Re-opening the attribute
771
+ * after every reset covers the whole row, whatever colours sit on top of it.
772
+ */
773
+ private selectLine;
774
+ /**
775
+ * Fold one live stream chunk into the in-progress assistant row. Hosts from
776
+ * 0.1.5 on emit the chunk on `agent/assistant-stream` and never write it to
777
+ * the log.
643
778
  */
644
779
  private applyStreamChunk;
645
780
  /**
646
- * 0.1.5 live tokens arrive as process-local `agent/assistant-stream`
647
- * frames (start / chunk / end). Chunk frames carry the same
648
- * `StreamChunk` the 0.1.2 log used to store as `assistant/chunk`.
781
+ * 0.1.5 and 0.1.7 live tokens arrive as process-local
782
+ * `agent/assistant-stream` frames (start / chunk / end). Chunk frames carry
783
+ * the same `StreamChunk` the settled `assistant/message` embeds in `stream`.
649
784
  */
650
785
  readonly handleAssistantStream: (...args: unknown[]) => void;
651
786
  /**
@@ -653,6 +788,12 @@ export declare class SshTui {
653
788
  * The parent request is left untouched (its own `/model` waterfall already
654
789
  * owns the route); direct children created by tool-subagent inherit the
655
790
  * parent provider unless `/submodel` stored an explicit subagent provider.
791
+ *
792
+ * Effort is not inherited from the parent. `next()` copies the live parent
793
+ * config, so a grok-4.6 `xhigh` parent would otherwise send `xhigh` with
794
+ * the light grok-4.5 child and fail with UNSUPPORTED_REASONING_EFFORT
795
+ * before the child writes a closing message. An explicit `/subeffort`
796
+ * still wins; otherwise the child's own adapter default is used.
656
797
  */
657
798
  readonly handleAgentRequest: ({ agent }: {
658
799
  agent: Agent;
@@ -669,6 +810,28 @@ export declare class SshTui {
669
810
  /** Plan-mode / command / team events that plugins merge into SessionEventMap. */
670
811
  private handleExtensionEvent;
671
812
  private findCompactionRow;
813
+ /**
814
+ * Compaction cards that can still be in flight: not finished, and within the
815
+ * window a real compaction could take. A stale card is treated as not running
816
+ * so it cannot hold the footer, the spinner, or `/compact` hostage.
817
+ */
818
+ private runningCompactions;
819
+ /** Whether a compaction is genuinely in flight right now. */
820
+ private compactionRunning;
821
+ /**
822
+ * Close compaction cards whose Host is gone.
823
+ *
824
+ * `compaction/start` is written before the work and `compaction/end` after it,
825
+ * so a Host that exits in between leaves an open start in the durable log.
826
+ * Replaying that log faithfully used to recreate a card stuck on "running"
827
+ * forever: the footer said 压缩中, the spinner kept animating, and every later
828
+ * `/compact` was refused as "already compacting". After a full replay nothing
829
+ * more can arrive for those starts, so they are settled here.
830
+ *
831
+ * `staleOnly` keeps the same sweep usable while the session is live, where a
832
+ * compaction that is merely slow must not be closed under it.
833
+ */
834
+ private settleUnfinishedCompactions;
672
835
  private handleCompactionEvent;
673
836
  private readContextPressure;
674
837
  private refreshContextPressure;
@@ -685,6 +848,8 @@ export declare class SshTui {
685
848
  readonly handleSubagentSessionEvent: (sessionId: SessionId, event: SessionEvent) => void;
686
849
  readonly handleSubagentStart: (info: SubagentRunInfo) => void;
687
850
  readonly handleSubagentEnd: (info: SubagentRunEndInfo) => void;
851
+ /** Keep an open inspect overlay in sync with the live child log. */
852
+ private refreshOpenSubagentInspect;
688
853
  private hasLiveDisplay;
689
854
  private waitForLiveDisplay;
690
855
  /**
@@ -726,6 +891,12 @@ export declare class SshTui {
726
891
  /** Queue one dialog behind an already-open one instead of overwriting it. */
727
892
  private openDialog;
728
893
  private showNextDialog;
894
+ /**
895
+ * Line mode has no framed dialog. Write the prompt (and its keys) into the
896
+ * log once when it becomes active, so a `tee` or a screen reader can answer
897
+ * it. Options stay on the framed path; the log only needs the question.
898
+ */
899
+ private echoLineModeDialog;
729
900
  private removeQueuedDialog;
730
901
  private settleQuestion;
731
902
  private openConfirm;
@@ -737,6 +908,58 @@ export declare class SshTui {
737
908
  private askQuestion;
738
909
  /** The stored llm-pi-ai profile for one provider route, when settings provide one. */
739
910
  private piAiProviderProfile;
911
+ /**
912
+ * Every settings-backed row one gateway family occupies, base first.
913
+ *
914
+ * `llm-pi-ai` puts the wire protocol on the provider entry, so a gateway
915
+ * whose catalogue spans protocols (Command Code, OpenCode Go) is stored as
916
+ * sibling rows. To the user they are one supplier: the TUI lists, picks and
917
+ * remembers them under the family's base id, and files each picked model on
918
+ * the row that actually serves it.
919
+ *
920
+ * The `-responses` / `-completions` / `-messages` suffix is only a naming
921
+ * convention, so a row is adopted into the family only when it corroborates:
922
+ * it must speak the protocol its suffix names, point at the same endpoint,
923
+ * and (when both name one) carry the same display name. A provider someone
924
+ * legitimately called `foo-messages` on another host therefore stays its own
925
+ * supplier instead of vanishing from `/provider` behind `foo`. A family of
926
+ * one — the ordinary case, and a sibling whose base row is missing — stays
927
+ * exactly as configured.
928
+ */
929
+ private providerFamilyRows;
930
+ /**
931
+ * Whether `sibling` is genuinely the same gateway as `base`, on another
932
+ * protocol: its `api` matches its suffix, its endpoint matches the base row's,
933
+ * and the two labels agree when both are set.
934
+ */
935
+ private isFamilyRow;
936
+ /** The id the TUI shows for a route: its family's base when it is spread. */
937
+ private displayProviderId;
938
+ /** The models one row configures, as ids with the labels settings carry. */
939
+ private configuredModelsOf;
940
+ /**
941
+ * The protocols the gateway publishes for one model, best-effort.
942
+ *
943
+ * Used when a model is being filed but no picker listed it yet (`/model <id>`
944
+ * on a new id, a `--model` override, a `/submodel` pin): the gateway's own
945
+ * answer is the only thing that can put it on a route that can send it.
946
+ * Any failure is "no opinion", never a blocked selection.
947
+ */
948
+ private advertisedFor;
949
+ /**
950
+ * The row of a gateway family that serves `model`: the one learned from the
951
+ * last listing, else the one that already configures it, else the base —
952
+ * where a brand-new model lands and is re-filed from.
953
+ */
954
+ /**
955
+ * Record which row a family's model belongs to, keeping the map bounded.
956
+ *
957
+ * Re-inserting keeps a key fresh, and the oldest half is dropped once the cap
958
+ * is crossed, so a gateway whose catalogue is re-listed on every `/model`
959
+ * does not accumulate a second copy of itself each time.
960
+ */
961
+ private rememberFamilyOwner;
962
+ private familyOwnerRow;
740
963
  /** Default listing endpoint for a built-in OpenCode route with no stored base URL. */
741
964
  private openCodeListingBaseURL;
742
965
  /**
@@ -744,8 +967,28 @@ export declare class SshTui {
744
967
  * OpenAI-compatible listing endpoint. The provider route is deliberately not
745
968
  * passed to discovery: pi-ai would answer a catalog route from its installed
746
969
  * registry, while the TUI wants the endpoint's current list.
970
+ *
971
+ * A row speaks exactly one protocol (`api` on the provider entry), so with a
972
+ * gateway that publishes `supported_endpoints` the list is narrowed to the
973
+ * models that row can actually serve — otherwise `/model` on
974
+ * `command-code-messages` would offer, and then persist, a model that only
975
+ * answers `/responses`.
747
976
  */
748
977
  private discoverEndpointModels;
978
+ /**
979
+ * One gateway's own model listing: every model the endpoint returns plus the
980
+ * route table it publishes. The caller narrows it to a protocol; a gateway
981
+ * spread over rows asks once and files the answer per row, so opening a
982
+ * picker costs one listing instead of one per sibling.
983
+ */
984
+ private discoverGatewayListing;
985
+ /**
986
+ * The row of the same gateway that the gateway itself says serves `model`,
987
+ * when that is not the row the caller is on. `undefined` when the gateway has
988
+ * no opinion, when the current row already serves it, or when the model would
989
+ * need a row that does not exist yet.
990
+ */
991
+ private rowServingModel;
749
992
  /** Add one endpoint-listed model to the stored provider profile when needed. */
750
993
  private ensureProviderModelConfigured;
751
994
  /**
@@ -758,12 +1001,34 @@ export declare class SshTui {
758
1001
  /** Built-in SuperGrok catalog used when the live adapter list is still warming up. */
759
1002
  private static readonly XAI_FALLBACK_MODELS;
760
1003
  private loadModelOptions;
1004
+ /**
1005
+ * The model list of a gateway spread over protocol rows.
1006
+ *
1007
+ * Every row is asked what it serves and the answers are merged under the
1008
+ * family's base id, so the user picks from one list. Each model remembers the
1009
+ * row that offered it, which is what routes the pick back to the protocol
1010
+ * that can actually send it — the split stays invisible end to end.
1011
+ */
1012
+ private loadFamilyModelOptions;
761
1013
  /** /model: models and effort for the current provider only. */
762
1014
  private runModelCommand;
763
1015
  /** /provider: pick a provider, then its model (remembered route pre-filled). */
764
1016
  private runProviderCommand;
765
1017
  /** Persist a provider/model/effort choice and keep the subagent on the same family. */
766
1018
  private rememberedRoute;
1019
+ /** The route this session is running right now, record-shaped. */
1020
+ private sessionRouteRecord;
1021
+ /**
1022
+ * Tell the launcher which route this session has settled on, so resuming it
1023
+ * comes back on the same supplier — the subagent route included, which is what
1024
+ * makes a resumed cross-provider conversation reproducible.
1025
+ *
1026
+ * Reported when a route settles (`/model`, `/submodel`, `/setup`) and at the
1027
+ * start of every turn, which catches a preset or a settings edit moving the
1028
+ * route behind the TUI's back. The launcher owns the file; the TUI only says
1029
+ * what changed, and says nothing when nothing did.
1030
+ */
1031
+ private noteSessionRoute;
767
1032
  private rememberRoute;
768
1033
  private applyModelSelection;
769
1034
  /**
@@ -780,8 +1045,21 @@ export declare class SshTui {
780
1045
  * When the parent provider changes (OAuth or API key), keep the subagent
781
1046
  * on a same-family model. An explicit leftover DeepSeek flash id after
782
1047
  * switching to xAI is treated as stale.
1048
+ *
1049
+ * A `/submodel` pin is the user's explicit route, so a parent switch asks
1050
+ * before moving the children: dropping the pin silently spends on a route
1051
+ * they pinned away from, and keeping it silently leaves the children on the
1052
+ * old provider.
783
1053
  */
784
1054
  private syncSubagentToProvider;
1055
+ /**
1056
+ * Ask whether a `/submodel` pin survives a parent provider change.
1057
+ *
1058
+ * Returns true when the pin should be kept. Cancelling the dialog keeps it
1059
+ * too: the pin is the user's explicit choice, so only an explicit answer
1060
+ * moves the children onto the new parent route.
1061
+ */
1062
+ private confirmKeepSubagentPin;
785
1063
  private clearQuotaForProvider;
786
1064
  /** Persist one subagent selection and publish it to the live request waterfall. */
787
1065
  private saveSubagentSelection;
@@ -789,6 +1067,8 @@ export declare class SshTui {
789
1067
  private subagentModelOptions;
790
1068
  /** /submodel: pick (or set) the model subagent children use. */
791
1069
  private runSubmodelCommand;
1070
+ /** Persist one subagent provider/model pair and say whether it follows or is pinned. */
1071
+ private commitSubagentRoute;
792
1072
  /** /effort: pick or set the reasoning effort for the current model. */
793
1073
  private runEffortCommand;
794
1074
  /**
@@ -825,6 +1105,16 @@ export declare class SshTui {
825
1105
  */
826
1106
  /** Re-read the roster fact this process was composed with (cheap, no IO). */
827
1107
  private refreshRosterHealth;
1108
+ /** Which settings protocol this host speaks; decides what "missing" means. */
1109
+ private get settingsGeneration();
1110
+ /**
1111
+ * The two lines `/mode` and `/preset` print when the preset service is absent.
1112
+ *
1113
+ * The reason differs by host line: 0.1.5 lost a roster the profile can mount,
1114
+ * while 0.1.7 composes the agent process-wide and has no terminal presets at
1115
+ * all — the repair there is the agent-plane rows, not a roster.
1116
+ */
1117
+ private missingPresetService;
828
1118
  private repairRoster;
829
1119
  /** Current provider route selected for the running agent. */
830
1120
  private currentProvider;
@@ -894,8 +1184,42 @@ export declare class SshTui {
894
1184
  * @returns the windows discovered for this pick, by model id.
895
1185
  */
896
1186
  private sizeOnboardingModels;
1187
+ /**
1188
+ * Leave the models step, after one best-effort trip to the gateway for what
1189
+ * the picks are still missing: the context window / output cap of every model
1190
+ * the listing has not described yet, and — for a gateway that spreads over
1191
+ * several protocols — the endpoints each picked model answers.
1192
+ *
1193
+ * `Ctrl+F` stays as it was, but it is no longer the only way to learn these:
1194
+ * a model typed by hand would otherwise be saved with no capacity at all, and
1195
+ * with no endpoint data the split had nothing to file it by.
1196
+ */
1197
+ private finishModelsStep;
1198
+ /** Whether leaving the models step has anything left to ask the gateway for. */
1199
+ private onboardingWantsEndpoint;
1200
+ /** Fill the picks' capacities and endpoints from the gateway; never fatal. */
1201
+ private enrichOnboardingModels;
897
1202
  private handleOnboardingChar;
898
1203
  private advanceOnboarding;
1204
+ /**
1205
+ * Open the models picker: every model the listing offered — or the template's
1206
+ * pinned ones when nothing was listed — with the template's models checked.
1207
+ *
1208
+ * Enter used to accept the whole listing, which quietly wrote every model of
1209
+ * the gateway into settings and made the session's model whichever id came
1210
+ * first. The picker makes both the set and the session's model explicit; a
1211
+ * typed list still skips it.
1212
+ */
1213
+ private openOnboardingModelPicker;
1214
+ /** The checked models of the wizard's picker, in listing order. */
1215
+ private checkedOnboardingModels;
1216
+ /** Move the highlight in either picker step. */
1217
+ private moveOnboardingModelCursor;
1218
+ /**
1219
+ * Esc in a picker step goes back one step instead of throwing the wizard
1220
+ * away — the listing is the expensive part and the user is still choosing.
1221
+ */
1222
+ private stepBackOnboarding;
899
1223
  /** Fetch the endpoint's model list into the onboarding wizard's models step. */
900
1224
  private fetchOnboardingModels;
901
1225
  private saveOnboarding;