dsh-ssh-tui 0.7.2 → 0.7.4

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 (112) hide show
  1. package/README.en.md +113 -27
  2. package/README.md +90 -27
  3. package/cordis.patch.yml +15 -0
  4. package/docs/remote-ops.md +27 -1
  5. package/docs/terminals.md +126 -0
  6. package/docs/windows.md +128 -0
  7. package/lib/attach.js +14 -1
  8. package/lib/attach.js.map +1 -1
  9. package/lib/commands.js +1 -0
  10. package/lib/commands.js.map +1 -1
  11. package/lib/copy-text.js +2 -0
  12. package/lib/copy-text.js.map +1 -1
  13. package/lib/diag.js +38 -0
  14. package/lib/diag.js.map +1 -1
  15. package/lib/dialogs.js.map +1 -1
  16. package/lib/display-sock.js +572 -44
  17. package/lib/display-sock.js.map +1 -1
  18. package/lib/doctor.js +71 -37
  19. package/lib/doctor.js.map +1 -1
  20. package/lib/dsh-compat.js +252 -88
  21. package/lib/dsh-compat.js.map +1 -1
  22. package/lib/footer.js +4 -2
  23. package/lib/footer.js.map +1 -1
  24. package/lib/i18n/en.js +54 -10
  25. package/lib/i18n/en.js.map +1 -1
  26. package/lib/i18n/index.js +18 -7
  27. package/lib/i18n/index.js.map +1 -1
  28. package/lib/i18n/zh.js +54 -10
  29. package/lib/i18n/zh.js.map +1 -1
  30. package/lib/index.js +111 -26
  31. package/lib/index.js.map +1 -1
  32. package/lib/line-mode.js +4 -0
  33. package/lib/line-mode.js.map +1 -1
  34. package/lib/paint.js +37 -3
  35. package/lib/paint.js.map +1 -1
  36. package/lib/picker.js +109 -18
  37. package/lib/picker.js.map +1 -1
  38. package/lib/plan.js +8 -0
  39. package/lib/plan.js.map +1 -1
  40. package/lib/platform.js +377 -11
  41. package/lib/platform.js.map +1 -1
  42. package/lib/preset-authoring.js +10 -14
  43. package/lib/preset-authoring.js.map +1 -1
  44. package/lib/preset-compat.js +100 -0
  45. package/lib/preset-compat.js.map +1 -0
  46. package/lib/preset-picker.js +5 -1
  47. package/lib/preset-picker.js.map +1 -1
  48. package/lib/preset-rows.js +119 -19
  49. package/lib/preset-rows.js.map +1 -1
  50. package/lib/provider-catalog.js +4 -4
  51. package/lib/question-wait.js +418 -0
  52. package/lib/question-wait.js.map +1 -0
  53. package/lib/route-memory.js +3 -3
  54. package/lib/route-memory.js.map +1 -1
  55. package/lib/selection.js +26 -8
  56. package/lib/selection.js.map +1 -1
  57. package/lib/session-index.js +5 -0
  58. package/lib/session-index.js.map +1 -1
  59. package/lib/session-lock.js +4 -1
  60. package/lib/session-lock.js.map +1 -1
  61. package/lib/session-route.js +3 -0
  62. package/lib/session-route.js.map +1 -1
  63. package/lib/settings-routes.js +10 -0
  64. package/lib/settings-routes.js.map +1 -0
  65. package/lib/settings-subagent.js +10 -0
  66. package/lib/settings-subagent.js.map +1 -0
  67. package/lib/subagent-model.js +5 -5
  68. package/lib/subagent-model.js.map +1 -1
  69. package/lib/supergrok-token.js +4 -0
  70. package/lib/supergrok-token.js.map +1 -1
  71. package/lib/term-text.js +79 -8
  72. package/lib/term-text.js.map +1 -1
  73. package/lib/terminal-caps.js +358 -0
  74. package/lib/terminal-caps.js.map +1 -0
  75. package/lib/terminal-input.js +13 -0
  76. package/lib/terminal-input.js.map +1 -1
  77. package/lib/tui.js +775 -165
  78. package/lib/tui.js.map +1 -1
  79. package/lib/types/attach.d.ts +8 -0
  80. package/lib/types/commands.d.ts +3 -0
  81. package/lib/types/diag.d.ts +20 -1
  82. package/lib/types/dialogs.d.ts +16 -0
  83. package/lib/types/display-sock.d.ts +141 -24
  84. package/lib/types/doctor.d.ts +8 -1
  85. package/lib/types/dsh-compat.d.ts +173 -44
  86. package/lib/types/footer.d.ts +3 -1
  87. package/lib/types/i18n/index.d.ts +31 -15
  88. package/lib/types/index.d.ts +45 -0
  89. package/lib/types/paint.d.ts +21 -0
  90. package/lib/types/picker.d.ts +25 -0
  91. package/lib/types/platform.d.ts +197 -10
  92. package/lib/types/preset-authoring.d.ts +8 -14
  93. package/lib/types/preset-compat.d.ts +58 -0
  94. package/lib/types/preset-picker.d.ts +1 -1
  95. package/lib/types/preset-rows.d.ts +63 -13
  96. package/lib/types/question-wait.d.ts +221 -0
  97. package/lib/types/selection.d.ts +12 -4
  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 +10 -10
  101. package/lib/types/term-text.d.ts +14 -0
  102. package/lib/types/terminal-caps.d.ts +105 -0
  103. package/lib/types/terminal-input.d.ts +10 -0
  104. package/lib/types/transcript-types.d.ts +22 -0
  105. package/lib/types/tui.d.ts +167 -10
  106. package/lib/types/update-check.d.ts +19 -4
  107. package/lib/types/workspace-changes.d.ts +135 -0
  108. package/lib/update-check.js +25 -8
  109. package/lib/update-check.js.map +1 -1
  110. package/lib/workspace-changes.js +120 -0
  111. package/lib/workspace-changes.js.map +1 -0
  112. package/package.json +124 -59
@@ -7,8 +7,8 @@
7
7
  * STORE accepts additive rows with plugin-owned ids and no `@deepseek-ai/*`
8
8
  * module names. The profile's user layer is the supported home for the row, so
9
9
  * both the install script and the running TUI write the same block here — the
10
- * TUI needs it because `dsh plugin add dsh-ssh-tui@latest` (the in-app update
11
- * path) never runs `scripts/`, which npm installs do not ship.
10
+ * TUI needs it because the in-app update (`dsh plugin add`, at whatever version)
11
+ * never runs `scripts/`, which npm installs do not ship.
12
12
  *
13
13
  * The roster is not cosmetic: without it `/mode` cannot switch, and the tools
14
14
  * the shipped presets own (`ask_user_question`, `present`, PTC's presentation)
@@ -19,6 +19,9 @@
19
19
  * must never lose the previous content or touch a row the user wrote.
20
20
  * @module dsh-ssh-tui/preset-rows
21
21
  */
22
+ import type { SettingsGeneration } from './dsh-compat.js';
23
+ /** Which host line the profile being repaired boots on. */
24
+ export type HostGeneration = SettingsGeneration;
22
25
  /** One host row the roster block mounts. */
23
26
  export interface RosterRow {
24
27
  id: string;
@@ -27,23 +30,68 @@ export interface RosterRow {
27
30
  config?: readonly string[];
28
31
  }
29
32
  /**
30
- * The rows the terminal profile has to mount itself: the roster `/mode` lists,
31
- * the TypeScript runtime the PTC preset needs, and the host-owned subagent
32
- * delegation setting.
33
+ * The rows a 0.1.5 terminal profile has to mount itself: the roster `/mode`
34
+ * lists, the TypeScript runtime the PTC preset needs, and the host-owned
35
+ * subagent delegation setting.
33
36
  */
34
37
  export declare const ROSTER_ROWS: readonly RosterRow[];
38
+ /**
39
+ * The rows a 0.1.7 terminal profile mounts for itself.
40
+ *
41
+ * 0.1.7 deleted the roster this plugin used to list and switch: presets became
42
+ * per-session declarations a surface mounts (`@deepseek-ai/dsh-agent-preset`
43
+ * rows over the `agent-preset-registry` service), and `dsh-base` keeps the
44
+ * agent-plane rows enabled for the TUI, which is single-session and composes
45
+ * its agent process-wide. Beyond the base, upstream's standard preset declares a
46
+ * persona and these two tools. Only the tools are mounted here: a *preset* is an
47
+ * agent scope, but a profile is not one, and `@deepseek-ai/dsh-persona`
48
+ * registers the two prompt sections `dsh-system-prompt` already owns at this
49
+ * layer — the loader rejects the row ("prompt section
50
+ * \"deployment:persona-prefix\" is already registered") and one entry then
51
+ * silently never activates. The deployment persona this line ships with is
52
+ * `dsh-system-prompt`'s own empty default, which is what a profile is meant to
53
+ * use unless it deliberately sets one.
54
+ */
55
+ export declare const FORMS_ROWS: readonly RosterRow[];
56
+ /** The rows one host line's profile has to mount. */
57
+ export declare function rosterRows(generation: HostGeneration): readonly RosterRow[];
58
+ /** Every row either line knows about, for name-to-row lookups. */
59
+ export declare const ALL_ROSTER_ROWS: readonly RosterRow[];
35
60
  /** The comment header the block introduces itself with, in both writers. */
36
61
  export declare const ROSTER_PATCH_HEADER = "# dsh-ssh-tui /mode: the agent-preset roster (standard / minimal / PTC /\n# cordis, plus every preset under $DSH_HOME/.agent-presets) and the two host\n# services the shipped presets need. dsh-base composes no roster in a terminal\n# profile, and a third-party bundle patch may not mount an @deepseek-ai row, so\n# the profile's user layer owns them.\n";
37
- /** One top-level `- insert:` entry mounting exactly these rows. */
38
- export declare function rosterInsertEntry(rows: readonly RosterRow[]): string;
62
+ /** The same header on a host that composes its agent process-wide. */
63
+ export declare const FORMS_PATCH_HEADER = "# dsh-ssh-tui: the agent-plane rows a 0.1.7 terminal profile mounts for itself.\n# That line composes the agent process-wide (presets are a per-session Web\n# feature now), and dsh-base already carries the rest of what the standard\n# preset declares: dsh-system-prompt owns the persona sections at this layer, so\n# only the two tools are left to mount. @deepseek-ai/dsh-persona is deliberately\n# absent \u2014 mounting it here collides with those sections and never activates.\n";
64
+ /** The header for one host line. */
65
+ export declare function rosterPatchHeader(generation: HostGeneration): string;
66
+ /**
67
+ * The entries one host line's rows need: a single `- insert:` list.
68
+ * @param rows - the rows to write.
69
+ * @returns the YAML text, ending in a newline.
70
+ */
71
+ export declare function rosterEntriesText(rows: readonly RosterRow[]): string;
39
72
  /**
40
73
  * The exact profile patch block that mounts the roster and the two host
41
74
  * services the shipped presets need. `scripts/ensure-profile-rows.sh` carries
42
75
  * the same text; a test compares the two so they cannot drift.
43
76
  */
44
77
  export declare const ROSTER_PATCH_BLOCK: string;
78
+ /** The 0.1.7 block, the same way. */
79
+ export declare const FORMS_PATCH_BLOCK: string;
80
+ /** The block for one host line. */
81
+ export declare function rosterPatchBlock(generation: HostGeneration): string;
45
82
  /** The profile patch file the roster block belongs in. */
46
83
  export declare function rosterPatchPath(home: string, profile: string): string;
84
+ /**
85
+ * Whether a profile patch already declares every 0.1.7 agent-plane row.
86
+ *
87
+ * Synchronous on purpose: the footer chip is rendered from the main loop and
88
+ * cannot await a read. The file is small and this runs at startup and after a
89
+ * repair, not per frame.
90
+ * @param home - the harness home carrying `profiles/`.
91
+ * @param profile - the profile to inspect.
92
+ * @returns whether all three rows are declared (or the row is unreachable).
93
+ */
94
+ export declare function formsRowsDeclared(home: string, profile: string): boolean;
47
95
  /** One row a patch file declares, with enough position to point at it. */
48
96
  export interface PatchRowRef {
49
97
  /** Insert id, or the target of an override/disable entry. */
@@ -88,7 +136,7 @@ export declare function duplicatePatchRows(rows: readonly PatchRowRef[]): PatchR
88
136
  /** Whether a patch already declares one roster row, by id or by module name. */
89
137
  export declare function patchNamesRow(rows: readonly PatchRowRef[], row: RosterRow): boolean;
90
138
  /** The roster rows a patch does not declare yet. */
91
- export declare function missingRosterRows(rows: readonly PatchRowRef[]): RosterRow[];
139
+ export declare function missingRosterRows(rows: readonly PatchRowRef[], candidates?: readonly RosterRow[]): RosterRow[];
92
140
  export interface PatchRepair {
93
141
  text: string;
94
142
  /** Roster rows this repair adds, by id. */
@@ -108,10 +156,11 @@ export interface PatchRepair {
108
156
  * (the profile template) is replaced; anything else keeps its content and gains
109
157
  * a new `- insert:` entry after a blank line.
110
158
  * @param existing - the patch file's current text.
111
- * @param missing - the roster rows to mount; defaults to all of them.
159
+ * @param missing - the roster rows to mount; defaults to the 0.1.5 set.
160
+ * @param generation - which host line the profile boots on.
112
161
  * @returns the new text, or `undefined` when nothing has to change.
113
162
  */
114
- export declare function planRosterRepair(existing: string, missing?: readonly RosterRow[]): PatchRepair | undefined;
163
+ export declare function planRosterRepair(existing: string, missing?: readonly RosterRow[], generation?: HostGeneration): PatchRepair | undefined;
115
164
  /**
116
165
  * The patch text with every repeated insert removed, or `undefined` when the
117
166
  * file has no duplicate.
@@ -132,10 +181,11 @@ export declare function planDuplicateRepair(existing: string): PatchRepair | und
132
181
  * half-written patch would only be seen by the next launch.
133
182
  * @param home - the harness home carrying `profiles/`.
134
183
  * @param profile - the profile to patch.
135
- * @param missing - the rows to mount; defaults to the whole roster.
184
+ * @param missing - the rows to mount; defaults to the 0.1.5 set.
185
+ * @param generation - which host line the profile boots on.
136
186
  * @returns `present` when the row already exists, else `written`.
137
187
  */
138
- export declare function ensureRosterRows(home: string, profile: string, missing?: readonly RosterRow[]): Promise<'present' | 'written'>;
188
+ export declare function ensureRosterRows(home: string, profile: string, missing?: readonly RosterRow[], generation?: HostGeneration): Promise<'present' | 'written'>;
139
189
  /**
140
190
  * Write a repaired patch next to a copy of the previous content.
141
191
  *
@@ -148,4 +198,4 @@ export declare function ensureRosterRows(home: string, profile: string, missing?
148
198
  */
149
199
  export declare function writePatchWithBackup(path: string, text: string, stamp?: string): Promise<string | undefined>;
150
200
  /** The patch text the roster block would produce on its own, for callers that report it. */
151
- export declare function rosterPatchText(existing: string): string | undefined;
201
+ export declare function rosterPatchText(existing: string, generation?: HostGeneration): string | undefined;
@@ -0,0 +1,221 @@
1
+ /**
2
+ * What a question does while nobody is attached to answer it.
3
+ *
4
+ * A dropped SSH link leaves the question queued in memory, and the only trace of
5
+ * it used to be a transcript line written *after* the user reconnected — which is
6
+ * how a turn spent waiting for an answer went unnoticed until then. Two signals
7
+ * exist before that:
8
+ *
9
+ * - a marker file under `$DSH_HOME/tui-socks/`, so logging back into the jump
10
+ * host shows a session waiting on an answer before the TUI is even open;
11
+ * - one command the user configured, run once, carrying the question in its
12
+ * environment. Mail and WeCom are presets of that command rather than clients
13
+ * of their own: a jump host has whichever of them it already has, and neither
14
+ * belongs in this plugin's dependency tree.
15
+ * @module dsh-ssh-tui/question-wait
16
+ */
17
+ import { spawn } from 'node:child_process';
18
+ /** One question waiting on a person who is not attached. */
19
+ export interface WaitingQuestion {
20
+ /** The session the question belongs to. */
21
+ sessionId: string;
22
+ /** How many questions are waiting, this one included. */
23
+ count: number;
24
+ /** The question text, already clipped. */
25
+ question: string;
26
+ /** When the wait started, so a later reader can say how long it has been. */
27
+ since: number;
28
+ }
29
+ /**
30
+ * The marker for one session.
31
+ *
32
+ * Beside the session's stderr log, under the same digested label, so a long id
33
+ * stays inside a Windows path and two ids that sanitize to the same stem do not
34
+ * share a file. `.waiting` rather than a dotfile: the point is that `ls` shows it.
35
+ * @param sessionId - the session that is waiting.
36
+ * @param dshHome - the harness home; defaults to the process one.
37
+ * @returns the marker path.
38
+ */
39
+ export declare function waitingMarkerPath(sessionId: string, dshHome?: string): string;
40
+ /**
41
+ * Write the marker, replacing one already there.
42
+ *
43
+ * The question text is the only sensitive part, so the file is owner-only. A
44
+ * failure to write is reported as `false` and never thrown: the question is
45
+ * still queued, and a marker is not worth failing it over.
46
+ *
47
+ * Deliberately a plain `writeFile`, not a staged sibling renamed into place.
48
+ * The rename is atomic on POSIX, but Windows refuses to replace a file another
49
+ * process has open — and the whole point of this marker is that something else
50
+ * polls it (a jump-host `cat` in a loop), so the "atomic" version stops updating
51
+ * exactly while it is being read. The cost of the plain write is a window
52
+ * between the create and the first byte where a reader sees an empty file; the
53
+ * marker is rewritten on every change, so that window is what a consumer has to
54
+ * tolerate rather than something the writer can hide.
55
+ * @param question - the question that just started waiting.
56
+ * @param dshHome - the harness home; defaults to the process one.
57
+ * @returns whether the marker is on disk.
58
+ */
59
+ export declare function writeWaitingMarker(question: WaitingQuestion, dshHome?: string): Promise<boolean>;
60
+ /**
61
+ * Remove the marker. Missing is fine: the question may have been answered, or
62
+ * the marker may never have been written.
63
+ * @param sessionId - the session whose wait ended.
64
+ * @param dshHome - the harness home; defaults to the process one.
65
+ */
66
+ export declare function clearWaitingMarker(sessionId: string, dshHome?: string): Promise<void>;
67
+ /**
68
+ * The command to run when a question starts waiting, if the user configured one.
69
+ *
70
+ * The environment wins over settings, the same order `/disconnect` uses, so a
71
+ * one-off invocation can point somewhere else without editing the profile.
72
+ * Empty and whitespace are "not configured".
73
+ * @param env - the process environment.
74
+ * @param saved - the `ssh-tui.notify` value, when settings has one.
75
+ * @returns the command, or undefined when notifications are off.
76
+ */
77
+ export declare function notifyCommand(env?: NodeJS.ProcessEnv, saved?: string): string | undefined;
78
+ /** What one notification knows about the question it is announcing. */
79
+ export interface NotifyContext {
80
+ sessionId: string;
81
+ /** How many questions are waiting. */
82
+ count: number;
83
+ /** The question text. */
84
+ question: string;
85
+ /** Milliseconds the question has already been waiting. */
86
+ waitedMs: number;
87
+ /** A command that reattaches to the session. */
88
+ resumeCommand: string;
89
+ /** SMTP username, present only for an authenticated server. */
90
+ smtpUser?: string;
91
+ /** SMTP password, present only for an authenticated server. */
92
+ smtpPassword?: string;
93
+ }
94
+ /**
95
+ * One notify target, as `/notify` parsed it.
96
+ *
97
+ * `off` clears the command. `mail` is the local mailer. `smtp` is a submission
98
+ * to a server, authenticated when a password is given. `local` is the machine's
99
+ * own SMTP listener, which needs no account.
100
+ */
101
+ export type NotifyTarget = {
102
+ kind: 'off';
103
+ } | {
104
+ kind: 'mail';
105
+ address: string;
106
+ } | {
107
+ kind: 'smtp';
108
+ host: string;
109
+ port: number;
110
+ from: string;
111
+ to: string;
112
+ user?: string;
113
+ password?: string;
114
+ } | {
115
+ kind: 'local';
116
+ port: number;
117
+ from: string;
118
+ to: string;
119
+ };
120
+ /**
121
+ * Parse one `/notify` argument into a target.
122
+ *
123
+ * Four shapes, deliberately small:
124
+ *
125
+ * - empty, `off`, `none` — turn it off;
126
+ * - `mail you@example.com` — the local mailer;
127
+ * - `smtp host[:port] from to [user [password]]` — an SMTP server, port 587
128
+ * when omitted, authenticated only when a user is given;
129
+ * - `local [port] from to` — the machine's own mailer on 25, or another port.
130
+ *
131
+ * @param raw - everything after `/notify`.
132
+ * @returns the target, or undefined when the shape is not one of those.
133
+ */
134
+ export declare function parseNotifyTarget(raw: string): NotifyTarget | undefined;
135
+ /**
136
+ * The shell command one target runs.
137
+ *
138
+ * SMTP and the local mailer both go through Python's stdlib `smtplib`, which
139
+ * is present on every jump host this plugin runs on (Node itself is). The
140
+ * password, when there is one, is read from `DSH_TUI_NOTIFY_SMTP_PASSWORD` at
141
+ * send time rather than written into the command, so it never lands in the
142
+ * settings file or the process list. The message text arrives on stdin.
143
+ * @param target - a parsed target other than `off`.
144
+ * @returns the command `/notify` stores.
145
+ */
146
+ /**
147
+ * What `/notify` confirms back: the target in the user's own words, without the
148
+ * password.
149
+ * @param target - a parsed target other than `off`.
150
+ * @returns a short label.
151
+ */
152
+ export declare function notifyTargetLabel(target: Exclude<NotifyTarget, {
153
+ kind: 'off';
154
+ }>): string;
155
+ export declare function notifyTargetCommand(target: Exclude<NotifyTarget, {
156
+ kind: 'off';
157
+ }>): string;
158
+ /**
159
+ * A mail command for an address, using whichever sender the host has.
160
+ *
161
+ * `mail` (or `mailx`) is on most jump hosts and speaks SMTP for them; no
162
+ * provider, token, or library joins this plugin. The body arrives on stdin, so
163
+ * the question text is never a shell argument.
164
+ * @param address - where the mail goes.
165
+ * @returns the command.
166
+ */
167
+ export declare function mailNotifyCommand(address: string): string;
168
+ /**
169
+ * A WeCom group-robot command for one webhook key.
170
+ *
171
+ * The payload is assembled by the caller and sent with `curl`, which a jump
172
+ * host already has. The key is the only secret, and it stays in the command the
173
+ * user configured rather than in this repo.
174
+ * @param key - the robot's webhook key.
175
+ * @returns the command.
176
+ */
177
+ export declare function wecomNotifyCommand(key: string): string;
178
+ /**
179
+ * The text a notification shows. Short on purpose: a mail subject and a WeCom
180
+ * message both get truncated by their carrier, so the resume command has to fit.
181
+ * @param context - the question being announced.
182
+ * @returns the message, one fact per line.
183
+ */
184
+ export declare function notifyMessage(context: NotifyContext): string;
185
+ /**
186
+ * The body a WeCom robot expects: a text message whose content is the notice.
187
+ * @param context - the question being announced.
188
+ * @returns JSON.
189
+ */
190
+ export declare function wecomPayload(context: NotifyContext): string;
191
+ /**
192
+ * Run the user's notify command once.
193
+ *
194
+ * The message goes to stdin and the facts go to the environment, so a command
195
+ * can be either `mail` (reads stdin) or anything that reads the variables. The
196
+ * command runs through a shell because it is a shell command by nature — the
197
+ * user wrote it as one — and the platform's own shell, so a Windows host uses
198
+ * `cmd`. Nothing it prints or fails with comes back: a notification that errors
199
+ * must not surface in the transcript or delay the question.
200
+ * @param command - the configured command.
201
+ * @param context - the question being announced.
202
+ * @param deps - injectable process spawn and clock, for tests.
203
+ * @returns once the command has exited, timed out, or failed to start.
204
+ */
205
+ export declare function runNotify(command: string, context: NotifyContext, deps?: {
206
+ spawnFn?: typeof spawn;
207
+ platform?: NodeJS.Platform;
208
+ timeoutMs?: number;
209
+ }): Promise<void>;
210
+ /**
211
+ * The shell that runs a notify command, and the flag that precedes it.
212
+ *
213
+ * A jump host means `sh -c`. Windows has no `sh` on PATH as a rule, so there
214
+ * the command runs under `cmd /c` and the user writes it for `cmd`.
215
+ * @param platform - injectable so the Windows shape is testable from POSIX.
216
+ * @returns the executable and its command flag.
217
+ */
218
+ export declare function notifyShell(platform?: NodeJS.Platform): {
219
+ command: string;
220
+ flag: string;
221
+ };
@@ -9,10 +9,10 @@
9
9
  * OSC 52 (the channel `/copy` already uses).
10
10
  *
11
11
  * Only **model replies** are freely selectable. Everything here is pure: the
12
- * caller passes the painted lines (plain text plus whether each one came from a
13
- * reply) and gets back the ordered selection, the cell spans to paint in
14
- * reverse video, and the text to copy. The IO — mouse reports, OSC 52, notices —
15
- * stays in `tui.ts`.
12
+ * caller passes the painted lines (plain text, whether each one came from a
13
+ * reply, and how many leading cells are chrome) and gets back the ordered
14
+ * selection, the cell spans to paint in reverse video, and the text to copy.
15
+ * The IO — mouse reports, OSC 52, notices — stays in `tui.ts`.
16
16
  *
17
17
  * Columns are **cells**, not characters: a CJK glyph is two cells wide, and a
18
18
  * selection that touches either half includes the whole glyph. Surrogate pairs
@@ -24,6 +24,14 @@ export interface SelectableLine {
24
24
  text: string;
25
25
  /** True when this line came from a model reply, the only freely copyable kind. */
26
26
  copyable: boolean;
27
+ /**
28
+ * Cells at the start of the line that are chrome, not content.
29
+ *
30
+ * The focused card or reply carries a `▶ ` marker, and a reply's lines are
31
+ * copyable, so without this the marker was part of the text a drag copied off
32
+ * its first line. Only the first line of the focused row carries one.
33
+ */
34
+ gutter?: number;
27
35
  }
28
36
  /** A point in the transcript: `line` indexes the painted lines, `column` is a cell. */
29
37
  export interface SelectionPoint {
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Loader row that carries the `/model` route memory as a settings form.
3
+ *
4
+ * On the 0.1.5 line `route-memory.ts` registers this namespace itself through
5
+ * `installSettingsSection`; from 0.1.7 a settings form is projected out of a
6
+ * loader entry's own `Config` schema, so the same schema is mounted as a row.
7
+ * The row id (`ssh-tui-routes`, see `cordis.patch.yml`) is the namespace every
8
+ * read goes through, and it is also the id the host's legacy `settings.yaml`
9
+ * import lands on, so an existing `ssh-tui-routes` section keeps its routes.
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ export declare const name = "ssh-tui-settings-routes";
13
+ /** The route-memory section, as the 0.1.7 settings service wants to see it. */
14
+ export declare const Config: import("@deepseek-ai/schemastery").default<import("./route-memory.js").RouteMemorySettings>;
15
+ /** Nothing to mount: the TUI owns every read and write of this section. */
16
+ export declare function apply(_ctx: Context): void;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Loader row that carries the subagent model selection as a settings form.
3
+ *
4
+ * On the 0.1.5 line `subagent-model.ts` registers this namespace itself through
5
+ * `installSettingsSection`; from 0.1.7 a settings form is projected out of a
6
+ * loader entry's own `Config` schema, so the same schema is mounted as a row.
7
+ * The row id (`ssh-tui-subagent`, see `cordis.patch.yml`) is the namespace
8
+ * every read and write goes through, and the id the host's legacy
9
+ * `settings.yaml` import lands on, so an existing `/submodel` pin survives.
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ export declare const name = "ssh-tui-settings-subagent";
13
+ /** The subagent selection, as the 0.1.7 settings service wants to see it. */
14
+ export declare const Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
15
+ provider: import("@deepseek-ai/schemastery").default<string>;
16
+ model: import("@deepseek-ai/schemastery").default<string>;
17
+ reasoningEffort: import("@deepseek-ai/schemastery").default<string>;
18
+ }>>, Schemastery.ObjectT<NoInfer<{
19
+ provider: import("@deepseek-ai/schemastery").default<string>;
20
+ model: import("@deepseek-ai/schemastery").default<string>;
21
+ reasoningEffort: import("@deepseek-ai/schemastery").default<string>;
22
+ }>>, "plain">;
23
+ /** Nothing to mount: the TUI owns every read and write of this section. */
24
+ export declare function apply(_ctx: Context): void;
@@ -129,16 +129,16 @@ export declare function adoptSessionSubagentSelection(ref: SubagentSelectionRef,
129
129
  * the built-in default instead.
130
130
  */
131
131
  export declare function releaseSessionSubagentSelection(ref: SubagentSelectionRef, settingsValue: unknown): void;
132
- /** Settings schema for `$DSH_HOME/settings.yaml`. */
133
- export declare const SUBAGENT_SETTINGS_SCHEMA: z<Schemastery.ObjectS<{
134
- provider: z<string, string>;
135
- model: z<string, string>;
136
- reasoningEffort: z<string, string>;
137
- }>, Schemastery.ObjectT<{
138
- provider: z<string, string>;
139
- model: z<string, string>;
140
- reasoningEffort: z<string, string>;
141
- }>>;
132
+ /** Settings schema for the subagent selection; every field is form-writable. */
133
+ export declare const SUBAGENT_SETTINGS_SCHEMA: z<Schemastery.ObjectS<NoInfer<{
134
+ provider: z<string>;
135
+ model: z<string>;
136
+ reasoningEffort: z<string>;
137
+ }>>, Schemastery.ObjectT<NoInfer<{
138
+ provider: z<string>;
139
+ model: z<string>;
140
+ reasoningEffort: z<string>;
141
+ }>>, "plain">;
142
142
  /** Normalize a raw settings section into a live typed selection. */
143
143
  export declare function normalizeSubagentSelection(value: unknown): SubagentSelection;
144
144
  /**
@@ -38,6 +38,20 @@ export declare function waitCardCopy(input: {
38
38
  * end the last one with an ellipsis when the text does not fit.
39
39
  */
40
40
  export declare function wrapWaitDetails(detail: string, width: number, maxLines?: number): string[];
41
+ /** Forget the cached decision, so the next read re-consults the environment. */
42
+ export declare function resetAsciiChrome(): void;
43
+ /**
44
+ * Rewrite one string's chrome into ASCII when the terminal cannot decode UTF-8.
45
+ *
46
+ * Only the glyphs in {@link ASCII_CHROME} move; CJK text is left byte for byte,
47
+ * because there is no ASCII for it and a Chinese locale on a non-UTF-8 console
48
+ * needs `/language en`, not a mangled translation. Widths are preserved for the
49
+ * two-cell marks, so a row measured either side of this call occupies the same
50
+ * number of cells.
51
+ * @param text - one row, possibly with ANSI sequences in it.
52
+ * @returns the row unchanged when UTF-8 chrome is safe.
53
+ */
54
+ export declare function mapAsciiChrome(text: string): string;
41
55
  /**
42
56
  * Terminal cell width for one string.
43
57
  *
@@ -0,0 +1,105 @@
1
+ /**
2
+ * What the terminal in front of us can actually do.
3
+ *
4
+ * `color-depth.ts` answers the palette question; this answers the rest of the
5
+ * ones that change what we may safely emit: mouse reporting, bracketed paste,
6
+ * the alternate screen, clipboard writes (OSC 52) and hyperlinks (OSC 8), plus
7
+ * which family of terminal we are talking to.
8
+ *
9
+ * Why it is a module and not a few `if`s: these decisions are made from
10
+ * environment variables that nobody on the team can test by hand for every
11
+ * desktop — GNOME Terminal, XFCE Terminal, Konsole, xterm, tmux, the Linux
12
+ * virtual console, Windows Terminal, a legacy Windows console. So the input is
13
+ * a parameter, the table is data, and `tests/terminal-caps.test.mjs` pins one
14
+ * fixture per family on any platform.
15
+ *
16
+ * The bias is **not** "assume the best". A capability we claim but the terminal
17
+ * lacks costs the user something real: `?1000h` on a console that cannot report
18
+ * SGR coordinates swallows its mouse; OSC 52 where nothing reads it makes
19
+ * `/copy` look like it worked; the alternate screen on a serial console loses
20
+ * the scrollback. Where a terminal is known to be marginal, the answer is no,
21
+ * and `DSH_TUI_TERM_CAPS` is the escape hatch for the user who knows better.
22
+ *
23
+ * @module dsh-ssh-tui/terminal-caps
24
+ */
25
+ import { type ColorDepth } from './color-depth.js';
26
+ /** Which terminal implementation is on the other end. */
27
+ export type TerminalFamily = 'windows-terminal' | 'windows-console' | 'vte' | 'konsole' | 'xterm' | 'tmux' | 'screen' | 'linux-console' | 'dumb' | 'unknown';
28
+ /** One terminal's answer to every question this TUI asks of it. */
29
+ export interface TerminalCapabilities {
30
+ family: TerminalFamily;
31
+ /** A human name for diagnostics (`/diag`), e.g. `GNOME Terminal (VTE 7000)`. */
32
+ label: string;
33
+ colors: ColorDepth;
34
+ /** Report mouse events at all (`?1000h`). */
35
+ mouse: boolean;
36
+ /** Report them as SGR (`?1006h`) instead of the 32-column X10 encoding. */
37
+ mouseSgr: boolean;
38
+ /** Report motion while a button is held (`?1002h`), which a drag needs. */
39
+ mouseDrag: boolean;
40
+ /** `?2004h`: paste arrives wrapped, so a multi-line paste is one event. */
41
+ bracketedPaste: boolean;
42
+ /** `?1049h`: the TUI gets its own screen and restores the scrollback on exit. */
43
+ alternateScreen: boolean;
44
+ /** OSC 52 clipboard writes reach the user's own machine. */
45
+ osc52: boolean;
46
+ /** OSC 8 hyperlinks are clickable rather than noise. */
47
+ osc8: boolean;
48
+ /** OSC 0/2 window-title updates are honoured. */
49
+ title: boolean;
50
+ /** `DSH_TUI_TERM_CAPS` tokens that were rejected, for `/diag` to report. */
51
+ ignoredOverrides: readonly string[];
52
+ }
53
+ /** Everything the classifier reads. `platform` and `env` are injectable. */
54
+ export interface TerminalProbe {
55
+ env?: NodeJS.ProcessEnv;
56
+ platform?: NodeJS.Platform;
57
+ }
58
+ /** Capability names a user may force on or off through `DSH_TUI_TERM_CAPS`. */
59
+ declare const TOGGLEABLE: readonly ["mouse", "mouseSgr", "mouseDrag", "bracketedPaste", "alternateScreen", "osc52", "osc8", "title"];
60
+ /**
61
+ * Parse `DSH_TUI_TERM_CAPS`.
62
+ *
63
+ * `no-mouse`, `mouse`, `mouse=false` and `mouse = false` all mean what they
64
+ * look like: the `=` binds first, so the spaced spelling cannot silently invert
65
+ * into the opposite answer (it did, and `osc52 = false` then *re-enabled* the
66
+ * clipboard promise). A value outside the two vocabularies, an unknown name or a
67
+ * bare `no-` is rejected rather than guessed, and the rejects are returned so
68
+ * `/diag` can show them — this variable is the only feedback channel a user has
69
+ * when an override does not appear to work.
70
+ */
71
+ export declare function parseCapsOverrideReport(raw: string): {
72
+ overrides: Partial<Record<(typeof TOGGLEABLE)[number], boolean>>;
73
+ ignored: string[];
74
+ };
75
+ /** The overrides alone, for callers that do not report. */
76
+ export declare function parseCapsOverride(raw: string): Partial<Record<(typeof TOGGLEABLE)[number], boolean>>;
77
+ /**
78
+ * Classify the terminal from the environment.
79
+ *
80
+ * Detection is by the markers terminals actually set: Windows Terminal exports
81
+ * `WT_SESSION`; the VTE family (GNOME Terminal, XFCE Terminal, MATE, Tilix,
82
+ * Terminator, Guake) exports `VTE_VERSION`; Konsole exports `KONSOLE_VERSION`;
83
+ * tmux and screen export `TMUX` / `STY` and set a `tmux*` / `screen*` TERM.
84
+ * Everything else falls back to the TERM name.
85
+ */
86
+ export declare function detectTerminalFamily(probe?: TerminalProbe): {
87
+ family: TerminalFamily;
88
+ label: string;
89
+ };
90
+ /**
91
+ * Every capability for the terminal in front of us.
92
+ *
93
+ * `DSH_TUI_TERM_CAPS` overrides individual answers (`no-mouse`, `osc52=false`);
94
+ * `DSH_TUI_NO_ALT_SCREEN` and `DSH_TUI_OSC8` keep working because they came
95
+ * first and are documented.
96
+ */
97
+ export declare function terminalCapabilities(probe?: TerminalProbe): TerminalCapabilities;
98
+ /** Mouse-tracking sequences to enable, in one write. Empty when unsupported. */
99
+ export declare function mouseEnableSequence(caps: TerminalCapabilities): string;
100
+ /** Mouse-tracking sequences to disable. Always safe to emit: `l` on a mode the
101
+ * terminal never enabled is ignored, and leaving a mode on is not. */
102
+ export declare function mouseDisableSequence(): string;
103
+ /** Bracketed-paste enable/disable, or empty when the terminal lacks it. */
104
+ export declare function bracketedPasteSequence(caps: TerminalCapabilities, on: boolean): string;
105
+ export {};
@@ -133,6 +133,16 @@ export declare class TerminalInputPump {
133
133
  * and 0 ms, two "agreeing" samples, early stop, chip frozen on a wrong value).
134
134
  */
135
135
  private quietMs;
136
+ /**
137
+ * One request, one window: what a display-liveness check needs.
138
+ *
139
+ * `measure()` deliberately is not this. It waits out its whole budget between
140
+ * attempts so a late answer cannot be read as a fast one, and a check that
141
+ * costs seconds per question is a picker that hangs. Reading a late answer as
142
+ * this window's is the harmless direction here: it reports "the terminal is
143
+ * there", which is the conservative answer for every caller.
144
+ */
145
+ measureOnce(timeoutMs?: number): Promise<boolean>;
136
146
  /**
137
147
  * Wait until the line has been quiet for a full window, measured from the
138
148
  * later of the last answer and the last request. An answer to a request we
@@ -142,6 +142,26 @@ export type Row = {
142
142
  text: string;
143
143
  plugin?: string;
144
144
  expanded: boolean;
145
+ } | {
146
+ /**
147
+ * Files one turn changed, from the Host's `workspaceChanges` service.
148
+ * Display only: the summary is not in the session log, and a restarted
149
+ * Host cannot reopen it, so the card is never rebuilt from history.
150
+ */
151
+ kind: 'changes';
152
+ /** The turn the summary describes. A later event for it replaces this card. */
153
+ turn: number;
154
+ /** Sequence of the `workspace/changes` event the summary was read from. */
155
+ seq: number;
156
+ /** Session the summary belongs to; `diff` is served per session. */
157
+ sessionId: string;
158
+ /** Header text, rendered once so the card does not recompute it per frame. */
159
+ header: string;
160
+ /** One line per listed file, in the service's own order. */
161
+ files: string[];
162
+ /** Files the service's cap left off the list, appended after `files`. */
163
+ more?: string;
164
+ expanded: boolean;
145
165
  } | {
146
166
  kind: 'system';
147
167
  text: string;
@@ -171,6 +191,8 @@ export type CollapsibleBlock = Extract<Row, {
171
191
  kind: 'compaction';
172
192
  } | {
173
193
  kind: 'prompt';
194
+ } | {
195
+ kind: 'changes';
174
196
  }> | {
175
197
  kind: 'streaming-reasoning';
176
198
  expanded: boolean;