dsh-ssh-tui 0.7.1 → 0.7.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 (63) hide show
  1. package/README.en.md +76 -7
  2. package/README.md +50 -7
  3. package/lib/approval-cache.js +12 -9
  4. package/lib/approval-cache.js.map +1 -1
  5. package/lib/color-depth.js +10 -4
  6. package/lib/color-depth.js.map +1 -1
  7. package/lib/diag.js +41 -17
  8. package/lib/diag.js.map +1 -1
  9. package/lib/dialogs.js.map +1 -1
  10. package/lib/display-sock.js +68 -10
  11. package/lib/display-sock.js.map +1 -1
  12. package/lib/footer.js +35 -7
  13. package/lib/footer.js.map +1 -1
  14. package/lib/gateway-protocol.js +202 -0
  15. package/lib/gateway-protocol.js.map +1 -0
  16. package/lib/i18n/en.js +61 -41
  17. package/lib/i18n/en.js.map +1 -1
  18. package/lib/i18n/zh.js +61 -41
  19. package/lib/i18n/zh.js.map +1 -1
  20. package/lib/index.js +99 -23
  21. package/lib/index.js.map +1 -1
  22. package/lib/job-label.js +99 -17
  23. package/lib/job-label.js.map +1 -1
  24. package/lib/plan.js +269 -17
  25. package/lib/plan.js.map +1 -1
  26. package/lib/platform.js +88 -0
  27. package/lib/platform.js.map +1 -0
  28. package/lib/provider-catalog.js +3 -0
  29. package/lib/provider-catalog.js.map +1 -1
  30. package/lib/route-memory.js +8 -1
  31. package/lib/route-memory.js.map +1 -1
  32. package/lib/session-list.js +18 -1
  33. package/lib/session-list.js.map +1 -1
  34. package/lib/session-lock.js +75 -23
  35. package/lib/session-lock.js.map +1 -1
  36. package/lib/session-route.js +331 -0
  37. package/lib/session-route.js.map +1 -0
  38. package/lib/subagent-model.js +79 -1
  39. package/lib/subagent-model.js.map +1 -1
  40. package/lib/term-text.js +7 -4
  41. package/lib/term-text.js.map +1 -1
  42. package/lib/tool-present.js +46 -14
  43. package/lib/tool-present.js.map +1 -1
  44. package/lib/tui.js +1789 -275
  45. package/lib/tui.js.map +1 -1
  46. package/lib/types/color-depth.d.ts +3 -3
  47. package/lib/types/dialogs.d.ts +4 -0
  48. package/lib/types/display-sock.d.ts +52 -1
  49. package/lib/types/footer.d.ts +18 -4
  50. package/lib/types/gateway-protocol.d.ts +88 -0
  51. package/lib/types/job-label.d.ts +38 -7
  52. package/lib/types/plan.d.ts +94 -4
  53. package/lib/types/platform.d.ts +50 -0
  54. package/lib/types/route-memory.d.ts +17 -22
  55. package/lib/types/session-list.d.ts +5 -0
  56. package/lib/types/session-lock.d.ts +9 -0
  57. package/lib/types/session-route.d.ts +191 -0
  58. package/lib/types/subagent-model.d.ts +53 -0
  59. package/lib/types/term-text.d.ts +3 -2
  60. package/lib/types/tool-present.d.ts +2 -17
  61. package/lib/types/transcript-types.d.ts +44 -1
  62. package/lib/types/tui.d.ts +292 -2
  63. package/package.json +1 -1
@@ -20,6 +20,7 @@ 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 SessionRoute } from './session-route.js';
23
24
  import { type SubagentSelectionRef } from './subagent-model.js';
24
25
  import { type AskUserQuestionAnswer, type AskUserQuestionRequest } from '@deepseek-ai/dsh-user-questions';
25
26
  import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval';
@@ -28,10 +29,10 @@ import { paintedLinkHits } from './term-text.js';
28
29
  export type { CollapsibleBlock, DisplayKind, DisconnectPolicyName, PlanTodoItem, Row, SubagentLogEntry, ToolDiffHunk, } from './transcript-types.js';
29
30
  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
31
  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';
32
+ 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
33
  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
34
  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 { 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
36
  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
37
  /** Presentation configuration for the terminal channel. */
37
38
  export interface TuiConfig {
@@ -51,6 +52,24 @@ export interface TuiConfig {
51
52
  resume?: boolean;
52
53
  /** One-line notice after entering a resumed session's working directory. */
53
54
  cwdNotice?: string;
55
+ /**
56
+ * The route this resumed session ran on, when it has a record. Applied before
57
+ * the TUI mounts; this is only for saying so on screen.
58
+ */
59
+ restoredRoute?: SessionRoute;
60
+ /**
61
+ * What the launcher decided about the resumed route: a supplier that is gone,
62
+ * or one that cannot be routed to at all. Pushed on screen at boot.
63
+ */
64
+ launchNotices?: readonly {
65
+ kind: 'system' | 'error';
66
+ text: string;
67
+ }[];
68
+ /**
69
+ * Report a settled route to the launcher, which owns the per-session record.
70
+ * Absent in tests and in line mode: nothing here touches the disk.
71
+ */
72
+ onRouteSettled?: (route: Omit<SessionRoute, 'updatedAt'>) => void;
54
73
  /** Provider route selected at launch (defaults to deepseek-official). */
55
74
  provider?: string;
56
75
  /** Model selected at launch (defaults to the saved/fallback model). */
@@ -153,6 +172,19 @@ export declare class SshTui {
153
172
  private catalogLoad;
154
173
  /** Memoized id→window index over {@link catalogPresets}. */
155
174
  private catalogWindows;
175
+ /** Wizard drafts whose models step is already asking the gateway, so a
176
+ * second Enter cannot start a duplicate listing. */
177
+ private readonly finishingModels;
178
+ /** `<base>\0<model>` → the family row last seen serving that model. */
179
+ /**
180
+ * `<base>\0<model>` → the family row last seen serving that model.
181
+ *
182
+ * Bounded: a gateway can publish hundreds of models and their rows are
183
+ * re-learned on every listing, so the oldest entries are dropped once the map
184
+ * passes FAMILY_OWNER_CACHE_MAX rather than growing for the life of the
185
+ * session. Losing an entry only costs one future "which row?" lookup.
186
+ */
187
+ private readonly familyModelOwner;
156
188
  private input;
157
189
  private cursor;
158
190
  private inputFolded;
@@ -194,10 +226,13 @@ export declare class SshTui {
194
226
  private readonly selectionRef;
195
227
  private readonly subagentSelection;
196
228
  private readonly onSelectionChanged;
229
+ private readonly onRouteSettled;
197
230
  private readonly disposers;
198
231
  private userQuestionDisposer;
199
232
  private presetId;
200
233
  private presetName;
234
+ /** Last route reported to the launcher; a repaint must not report it again. */
235
+ private lastSessionRoute;
201
236
  private readonly useAlternateScreen;
202
237
  private agentGone;
203
238
  private onboarding;
@@ -213,6 +248,8 @@ export declare class SshTui {
213
248
  private readonly seenCommandDoneIds;
214
249
  private activeSubagents;
215
250
  private subagentSessions;
251
+ /** Parent `subagent` tool descriptions waiting for the matching child card. */
252
+ private pendingSubagentTasks;
216
253
  private openToolCalls;
217
254
  /** Survives result settlement so a card-less result can still be labelled. */
218
255
  private toolCallNames;
@@ -389,6 +426,20 @@ export declare class SshTui {
389
426
  * footer sat on `SSH ○○○○` for the whole load.
390
427
  */
391
428
  replayHistory(): Promise<void>;
429
+ /**
430
+ * Close the chips replay could not settle.
431
+ *
432
+ * A spawn `tool/call` with no `tool/result` in the log means the wrapper was
433
+ * still in flight when the log was last written. Replay only runs in a Host
434
+ * that has just started, and the child lived in the Host running back then,
435
+ * so that child is gone and no `subagent/end` will ever arrive for it.
436
+ * Leaving the chip "running" made every resume of an interrupted turn show a
437
+ * child counting up forever while `/status` reported none.
438
+ *
439
+ * `stopReason: 'unknown'` is also what lets a late live end adopt this row
440
+ * instead of adding a twin beside it.
441
+ */
442
+ private settleUnfinishedReplayChips;
392
443
  /** Show the first-launch provider/API-key onboarding when nothing is configured. */
393
444
  private maybeRunOnboarding;
394
445
  /** Run the provider/API-key onboarding wizard. Resolves true when saved. */
@@ -513,6 +564,9 @@ export declare class SshTui {
513
564
  private endWait;
514
565
  private waitCardSource;
515
566
  private findSubagentRow;
567
+ private subagentInspectId;
568
+ /** Courtesy name for a child session; never the raw session hash. */
569
+ private subagentNameFor;
516
570
  private findLivePlanRow;
517
571
  /** Older / finished plans stay in the scrolling transcript. */
518
572
  private archiveStalePlans;
@@ -531,16 +585,51 @@ export declare class SshTui {
531
585
  * including a monochrome terminal.
532
586
  */
533
587
  private styleEmphasisedLine;
588
+ /**
589
+ * A body row whose parts carry different roles: the two columns of a
590
+ * side-by-side diff.
591
+ *
592
+ * No single kind fits such a row — the left column is a removal, the right an
593
+ * addition — so each part is painted with its own style, the gutter keeps the
594
+ * row's own (neutral) one, and a word emphasis rides inside whichever column
595
+ * it belongs to. The first column takes the leading indent and the last one
596
+ * runs to the end of the row, so both bars are flush.
597
+ */
598
+ private styleColumnedLine;
534
599
  private paintToolBodyLine;
535
600
  private workspaceRowsFor;
536
601
  private paintInspectOverlay;
602
+ /**
603
+ * Reverse-video (or a `»` in monochrome) on the overlay row that holds the
604
+ * current `/find` needle, matching what the transcript does for the same hit.
605
+ */
606
+ private markSearchRow;
537
607
  private openToolInspect;
608
+ private openSubagentInspect;
609
+ /**
610
+ * Line mode has no framed scroller, so an inspect body goes into the log.
611
+ *
612
+ * Opening the modal there printed nothing and then swallowed the next Enter:
613
+ * an invisible dialog. The log is the only detail surface line mode has, and
614
+ * writing it keeps the input free.
615
+ */
616
+ private echoInspectToLog;
538
617
  closeInspect(): void;
539
618
  private paintCollapsibleHeader;
540
619
  /** Move the expand/collapse focus among reasoning and tool rows. */
541
620
  private moveCollapsibleFocus;
542
621
  /** Toggle the focused block; without focus, toggle the most recent one. */
543
622
  toggleCollapsible(): void;
623
+ /**
624
+ * Whether an inline tool body would be cut short.
625
+ *
626
+ * Two things stop a body short: it cannot fit the workspace, or the measured
627
+ * link is slow enough that the paint budget trims it. Both leave the reader
628
+ * looking at part of a diff under a header that counts all of it, and both
629
+ * are what the overlay is for — so both answers are the overlay, and the
630
+ * card's own `Enter 全览` hint stays true.
631
+ */
632
+ private toolBodyTruncated;
544
633
  toggleCard(target: CollapsibleBlock): void;
545
634
  /** Expand all collapsible blocks, or collapse them again when all are open. */
546
635
  toggleAllCollapsible(): void;
@@ -636,6 +725,28 @@ export declare class SshTui {
636
725
  private mutedSeparator;
637
726
  private mutedSgr;
638
727
  private styleLine;
728
+ /**
729
+ * One emphasised piece of a body line — the words a diff changed.
730
+ *
731
+ * Inside a filled diff row the mark is a lighter shade of that same fill.
732
+ * Inverse video was there first, and it inverted the *fill*: a line replaced
733
+ * wholesale is emphasised from end to end, so it came out a bright block
734
+ * beside rows that kept the muted tone — visibly from another palette. On a
735
+ * terminal with no colour left to shift, the attribute is what survives, so
736
+ * the inverse video stays for that case.
737
+ */
738
+ private styleEmphasisedPiece;
739
+ /**
740
+ * Paint a row the reader has selected.
741
+ *
742
+ * The obvious `7`…`27` wrap only reaches the part before the line's first
743
+ * reset, and every painted segment ends with one: a focused card kept its
744
+ * marker and status dot highlighted while the title, the state word and the
745
+ * summary stayed plain, so ↑/↓ gave no visible clue which card was picked —
746
+ * on exactly the rows that carry a segmented header. Re-opening the attribute
747
+ * after every reset covers the whole row, whatever colours sit on top of it.
748
+ */
749
+ private selectLine;
639
750
  /**
640
751
  * Fold one live or durable stream chunk into the in-progress assistant
641
752
  * row. 0.1.2 hosts append `assistant/chunk`; 0.1.5 emits the same chunk
@@ -653,6 +764,12 @@ export declare class SshTui {
653
764
  * The parent request is left untouched (its own `/model` waterfall already
654
765
  * owns the route); direct children created by tool-subagent inherit the
655
766
  * parent provider unless `/submodel` stored an explicit subagent provider.
767
+ *
768
+ * Effort is not inherited from the parent. `next()` copies the live parent
769
+ * config, so a grok-4.6 `xhigh` parent would otherwise send `xhigh` with
770
+ * the light grok-4.5 child and fail with UNSUPPORTED_REASONING_EFFORT
771
+ * before the child writes a closing message. An explicit `/subeffort`
772
+ * still wins; otherwise the child's own adapter default is used.
656
773
  */
657
774
  readonly handleAgentRequest: ({ agent }: {
658
775
  agent: Agent;
@@ -669,6 +786,28 @@ export declare class SshTui {
669
786
  /** Plan-mode / command / team events that plugins merge into SessionEventMap. */
670
787
  private handleExtensionEvent;
671
788
  private findCompactionRow;
789
+ /**
790
+ * Compaction cards that can still be in flight: not finished, and within the
791
+ * window a real compaction could take. A stale card is treated as not running
792
+ * so it cannot hold the footer, the spinner, or `/compact` hostage.
793
+ */
794
+ private runningCompactions;
795
+ /** Whether a compaction is genuinely in flight right now. */
796
+ private compactionRunning;
797
+ /**
798
+ * Close compaction cards whose Host is gone.
799
+ *
800
+ * `compaction/start` is written before the work and `compaction/end` after it,
801
+ * so a Host that exits in between leaves an open start in the durable log.
802
+ * Replaying that log faithfully used to recreate a card stuck on "running"
803
+ * forever: the footer said 压缩中, the spinner kept animating, and every later
804
+ * `/compact` was refused as "already compacting". After a full replay nothing
805
+ * more can arrive for those starts, so they are settled here.
806
+ *
807
+ * `staleOnly` keeps the same sweep usable while the session is live, where a
808
+ * compaction that is merely slow must not be closed under it.
809
+ */
810
+ private settleUnfinishedCompactions;
672
811
  private handleCompactionEvent;
673
812
  private readContextPressure;
674
813
  private refreshContextPressure;
@@ -685,6 +824,8 @@ export declare class SshTui {
685
824
  readonly handleSubagentSessionEvent: (sessionId: SessionId, event: SessionEvent) => void;
686
825
  readonly handleSubagentStart: (info: SubagentRunInfo) => void;
687
826
  readonly handleSubagentEnd: (info: SubagentRunEndInfo) => void;
827
+ /** Keep an open inspect overlay in sync with the live child log. */
828
+ private refreshOpenSubagentInspect;
688
829
  private hasLiveDisplay;
689
830
  private waitForLiveDisplay;
690
831
  /**
@@ -726,6 +867,12 @@ export declare class SshTui {
726
867
  /** Queue one dialog behind an already-open one instead of overwriting it. */
727
868
  private openDialog;
728
869
  private showNextDialog;
870
+ /**
871
+ * Line mode has no framed dialog. Write the prompt (and its keys) into the
872
+ * log once when it becomes active, so a `tee` or a screen reader can answer
873
+ * it. Options stay on the framed path; the log only needs the question.
874
+ */
875
+ private echoLineModeDialog;
729
876
  private removeQueuedDialog;
730
877
  private settleQuestion;
731
878
  private openConfirm;
@@ -737,6 +884,58 @@ export declare class SshTui {
737
884
  private askQuestion;
738
885
  /** The stored llm-pi-ai profile for one provider route, when settings provide one. */
739
886
  private piAiProviderProfile;
887
+ /**
888
+ * Every settings-backed row one gateway family occupies, base first.
889
+ *
890
+ * `llm-pi-ai` puts the wire protocol on the provider entry, so a gateway
891
+ * whose catalogue spans protocols (Command Code, OpenCode Go) is stored as
892
+ * sibling rows. To the user they are one supplier: the TUI lists, picks and
893
+ * remembers them under the family's base id, and files each picked model on
894
+ * the row that actually serves it.
895
+ *
896
+ * The `-responses` / `-completions` / `-messages` suffix is only a naming
897
+ * convention, so a row is adopted into the family only when it corroborates:
898
+ * it must speak the protocol its suffix names, point at the same endpoint,
899
+ * and (when both name one) carry the same display name. A provider someone
900
+ * legitimately called `foo-messages` on another host therefore stays its own
901
+ * supplier instead of vanishing from `/provider` behind `foo`. A family of
902
+ * one — the ordinary case, and a sibling whose base row is missing — stays
903
+ * exactly as configured.
904
+ */
905
+ private providerFamilyRows;
906
+ /**
907
+ * Whether `sibling` is genuinely the same gateway as `base`, on another
908
+ * protocol: its `api` matches its suffix, its endpoint matches the base row's,
909
+ * and the two labels agree when both are set.
910
+ */
911
+ private isFamilyRow;
912
+ /** The id the TUI shows for a route: its family's base when it is spread. */
913
+ private displayProviderId;
914
+ /** The models one row configures, as ids with the labels settings carry. */
915
+ private configuredModelsOf;
916
+ /**
917
+ * The protocols the gateway publishes for one model, best-effort.
918
+ *
919
+ * Used when a model is being filed but no picker listed it yet (`/model <id>`
920
+ * on a new id, a `--model` override, a `/submodel` pin): the gateway's own
921
+ * answer is the only thing that can put it on a route that can send it.
922
+ * Any failure is "no opinion", never a blocked selection.
923
+ */
924
+ private advertisedFor;
925
+ /**
926
+ * The row of a gateway family that serves `model`: the one learned from the
927
+ * last listing, else the one that already configures it, else the base —
928
+ * where a brand-new model lands and is re-filed from.
929
+ */
930
+ /**
931
+ * Record which row a family's model belongs to, keeping the map bounded.
932
+ *
933
+ * Re-inserting keeps a key fresh, and the oldest half is dropped once the cap
934
+ * is crossed, so a gateway whose catalogue is re-listed on every `/model`
935
+ * does not accumulate a second copy of itself each time.
936
+ */
937
+ private rememberFamilyOwner;
938
+ private familyOwnerRow;
740
939
  /** Default listing endpoint for a built-in OpenCode route with no stored base URL. */
741
940
  private openCodeListingBaseURL;
742
941
  /**
@@ -744,8 +943,28 @@ export declare class SshTui {
744
943
  * OpenAI-compatible listing endpoint. The provider route is deliberately not
745
944
  * passed to discovery: pi-ai would answer a catalog route from its installed
746
945
  * registry, while the TUI wants the endpoint's current list.
946
+ *
947
+ * A row speaks exactly one protocol (`api` on the provider entry), so with a
948
+ * gateway that publishes `supported_endpoints` the list is narrowed to the
949
+ * models that row can actually serve — otherwise `/model` on
950
+ * `command-code-messages` would offer, and then persist, a model that only
951
+ * answers `/responses`.
747
952
  */
748
953
  private discoverEndpointModels;
954
+ /**
955
+ * One gateway's own model listing: every model the endpoint returns plus the
956
+ * route table it publishes. The caller narrows it to a protocol; a gateway
957
+ * spread over rows asks once and files the answer per row, so opening a
958
+ * picker costs one listing instead of one per sibling.
959
+ */
960
+ private discoverGatewayListing;
961
+ /**
962
+ * The row of the same gateway that the gateway itself says serves `model`,
963
+ * when that is not the row the caller is on. `undefined` when the gateway has
964
+ * no opinion, when the current row already serves it, or when the model would
965
+ * need a row that does not exist yet.
966
+ */
967
+ private rowServingModel;
749
968
  /** Add one endpoint-listed model to the stored provider profile when needed. */
750
969
  private ensureProviderModelConfigured;
751
970
  /**
@@ -758,12 +977,34 @@ export declare class SshTui {
758
977
  /** Built-in SuperGrok catalog used when the live adapter list is still warming up. */
759
978
  private static readonly XAI_FALLBACK_MODELS;
760
979
  private loadModelOptions;
980
+ /**
981
+ * The model list of a gateway spread over protocol rows.
982
+ *
983
+ * Every row is asked what it serves and the answers are merged under the
984
+ * family's base id, so the user picks from one list. Each model remembers the
985
+ * row that offered it, which is what routes the pick back to the protocol
986
+ * that can actually send it — the split stays invisible end to end.
987
+ */
988
+ private loadFamilyModelOptions;
761
989
  /** /model: models and effort for the current provider only. */
762
990
  private runModelCommand;
763
991
  /** /provider: pick a provider, then its model (remembered route pre-filled). */
764
992
  private runProviderCommand;
765
993
  /** Persist a provider/model/effort choice and keep the subagent on the same family. */
766
994
  private rememberedRoute;
995
+ /** The route this session is running right now, record-shaped. */
996
+ private sessionRouteRecord;
997
+ /**
998
+ * Tell the launcher which route this session has settled on, so resuming it
999
+ * comes back on the same supplier — the subagent route included, which is what
1000
+ * makes a resumed cross-provider conversation reproducible.
1001
+ *
1002
+ * Reported when a route settles (`/model`, `/submodel`, `/setup`) and at the
1003
+ * start of every turn, which catches a preset or a settings edit moving the
1004
+ * route behind the TUI's back. The launcher owns the file; the TUI only says
1005
+ * what changed, and says nothing when nothing did.
1006
+ */
1007
+ private noteSessionRoute;
767
1008
  private rememberRoute;
768
1009
  private applyModelSelection;
769
1010
  /**
@@ -780,8 +1021,21 @@ export declare class SshTui {
780
1021
  * When the parent provider changes (OAuth or API key), keep the subagent
781
1022
  * on a same-family model. An explicit leftover DeepSeek flash id after
782
1023
  * switching to xAI is treated as stale.
1024
+ *
1025
+ * A `/submodel` pin is the user's explicit route, so a parent switch asks
1026
+ * before moving the children: dropping the pin silently spends on a route
1027
+ * they pinned away from, and keeping it silently leaves the children on the
1028
+ * old provider.
783
1029
  */
784
1030
  private syncSubagentToProvider;
1031
+ /**
1032
+ * Ask whether a `/submodel` pin survives a parent provider change.
1033
+ *
1034
+ * Returns true when the pin should be kept. Cancelling the dialog keeps it
1035
+ * too: the pin is the user's explicit choice, so only an explicit answer
1036
+ * moves the children onto the new parent route.
1037
+ */
1038
+ private confirmKeepSubagentPin;
785
1039
  private clearQuotaForProvider;
786
1040
  /** Persist one subagent selection and publish it to the live request waterfall. */
787
1041
  private saveSubagentSelection;
@@ -789,6 +1043,8 @@ export declare class SshTui {
789
1043
  private subagentModelOptions;
790
1044
  /** /submodel: pick (or set) the model subagent children use. */
791
1045
  private runSubmodelCommand;
1046
+ /** Persist one subagent provider/model pair and say whether it follows or is pinned. */
1047
+ private commitSubagentRoute;
792
1048
  /** /effort: pick or set the reasoning effort for the current model. */
793
1049
  private runEffortCommand;
794
1050
  /**
@@ -894,8 +1150,42 @@ export declare class SshTui {
894
1150
  * @returns the windows discovered for this pick, by model id.
895
1151
  */
896
1152
  private sizeOnboardingModels;
1153
+ /**
1154
+ * Leave the models step, after one best-effort trip to the gateway for what
1155
+ * the picks are still missing: the context window / output cap of every model
1156
+ * the listing has not described yet, and — for a gateway that spreads over
1157
+ * several protocols — the endpoints each picked model answers.
1158
+ *
1159
+ * `Ctrl+F` stays as it was, but it is no longer the only way to learn these:
1160
+ * a model typed by hand would otherwise be saved with no capacity at all, and
1161
+ * with no endpoint data the split had nothing to file it by.
1162
+ */
1163
+ private finishModelsStep;
1164
+ /** Whether leaving the models step has anything left to ask the gateway for. */
1165
+ private onboardingWantsEndpoint;
1166
+ /** Fill the picks' capacities and endpoints from the gateway; never fatal. */
1167
+ private enrichOnboardingModels;
897
1168
  private handleOnboardingChar;
898
1169
  private advanceOnboarding;
1170
+ /**
1171
+ * Open the models picker: every model the listing offered — or the template's
1172
+ * pinned ones when nothing was listed — with the template's models checked.
1173
+ *
1174
+ * Enter used to accept the whole listing, which quietly wrote every model of
1175
+ * the gateway into settings and made the session's model whichever id came
1176
+ * first. The picker makes both the set and the session's model explicit; a
1177
+ * typed list still skips it.
1178
+ */
1179
+ private openOnboardingModelPicker;
1180
+ /** The checked models of the wizard's picker, in listing order. */
1181
+ private checkedOnboardingModels;
1182
+ /** Move the highlight in either picker step. */
1183
+ private moveOnboardingModelCursor;
1184
+ /**
1185
+ * Esc in a picker step goes back one step instead of throwing the wizard
1186
+ * away — the listing is the expensive part and the user is still choosing.
1187
+ */
1188
+ private stepBackOnboarding;
899
1189
  /** Fetch the endpoint's model list into the onboarding wizard's models step. */
900
1190
  private fetchOnboardingModels;
901
1191
  private saveOnboarding;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ssh-tui",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "SSH-friendly interactive terminal TUI plugin for DeepSeek Harness",
5
5
  "keywords": [
6
6
  "deepseek-harness",