dsh-ssh-tui 0.7.2 → 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.
- package/README.en.md +16 -1
- package/README.md +14 -1
- package/cordis.patch.yml +15 -0
- package/docs/terminals.md +113 -0
- package/lib/diag.js +35 -0
- package/lib/diag.js.map +1 -1
- package/lib/display-sock.js +259 -42
- package/lib/display-sock.js.map +1 -1
- package/lib/doctor.js +71 -37
- package/lib/doctor.js.map +1 -1
- package/lib/dsh-compat.js +206 -88
- package/lib/dsh-compat.js.map +1 -1
- package/lib/footer.js +4 -2
- package/lib/footer.js.map +1 -1
- package/lib/i18n/en.js +18 -0
- package/lib/i18n/en.js.map +1 -1
- package/lib/i18n/index.js +13 -7
- package/lib/i18n/index.js.map +1 -1
- package/lib/i18n/zh.js +18 -0
- package/lib/i18n/zh.js.map +1 -1
- package/lib/index.js +36 -11
- package/lib/index.js.map +1 -1
- package/lib/paint.js +5 -2
- package/lib/paint.js.map +1 -1
- package/lib/picker.js +5 -3
- package/lib/picker.js.map +1 -1
- package/lib/platform.js +296 -11
- package/lib/platform.js.map +1 -1
- package/lib/preset-authoring.js +10 -14
- package/lib/preset-authoring.js.map +1 -1
- package/lib/preset-compat.js +100 -0
- package/lib/preset-compat.js.map +1 -0
- package/lib/preset-picker.js +5 -1
- package/lib/preset-picker.js.map +1 -1
- package/lib/preset-rows.js +83 -13
- package/lib/preset-rows.js.map +1 -1
- package/lib/provider-catalog.js +4 -4
- package/lib/route-memory.js +3 -3
- package/lib/route-memory.js.map +1 -1
- package/lib/session-index.js +5 -0
- package/lib/session-index.js.map +1 -1
- package/lib/session-lock.js +4 -1
- package/lib/session-lock.js.map +1 -1
- package/lib/session-route.js +3 -0
- package/lib/session-route.js.map +1 -1
- package/lib/settings-routes.js +10 -0
- package/lib/settings-routes.js.map +1 -0
- package/lib/settings-subagent.js +10 -0
- package/lib/settings-subagent.js.map +1 -0
- package/lib/subagent-model.js +5 -5
- package/lib/subagent-model.js.map +1 -1
- package/lib/supergrok-token.js +4 -0
- package/lib/supergrok-token.js.map +1 -1
- package/lib/terminal-caps.js +358 -0
- package/lib/terminal-caps.js.map +1 -0
- package/lib/tui.js +184 -120
- package/lib/tui.js.map +1 -1
- package/lib/types/diag.d.ts +18 -1
- package/lib/types/display-sock.d.ts +63 -22
- package/lib/types/doctor.d.ts +8 -1
- package/lib/types/dsh-compat.d.ts +141 -44
- package/lib/types/footer.d.ts +3 -1
- package/lib/types/i18n/index.d.ts +18 -12
- package/lib/types/index.d.ts +45 -0
- package/lib/types/picker.d.ts +3 -0
- package/lib/types/platform.d.ts +175 -10
- package/lib/types/preset-authoring.d.ts +8 -14
- package/lib/types/preset-compat.d.ts +58 -0
- package/lib/types/preset-picker.d.ts +1 -1
- package/lib/types/preset-rows.d.ts +51 -9
- package/lib/types/settings-routes.d.ts +16 -0
- package/lib/types/settings-subagent.d.ts +24 -0
- package/lib/types/subagent-model.d.ts +7 -7
- package/lib/types/terminal-caps.d.ts +105 -0
- package/lib/types/tui.d.ts +41 -7
- package/package.json +82 -55
package/lib/types/platform.d.ts
CHANGED
|
@@ -22,21 +22,125 @@ export declare function usesProcessIdentity(platform?: NodeJS.Platform): boolean
|
|
|
22
22
|
* POSIX wants `detached: true` (setsid) so the Host survives the launcher and a
|
|
23
23
|
* hung-up terminal.
|
|
24
24
|
*
|
|
25
|
-
* Windows is the opposite: `detached: true` maps to
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* not
|
|
25
|
+
* Windows is the opposite, and the trade-off is forced: `detached: true` maps to
|
|
26
|
+
* DETACHED_PROCESS, and Windows ignores CREATE_NO_WINDOW (what `windowsHide`
|
|
27
|
+
* sets) when DETACHED_PROCESS is present. Every console child the Host then
|
|
28
|
+
* starts — each tool call, every shell, node, git — allocates its own console,
|
|
29
|
+
* which is a visible window flashing over the TUI. So the Host is spawned
|
|
30
|
+
* non-detached and inherits the launcher's console; descendants inherit it too
|
|
31
|
+
* instead of creating one.
|
|
32
|
+
*
|
|
33
|
+
* The price *was* on the lifecycle side, and it was measured (on the Windows CI
|
|
34
|
+
* leg by `scripts/tui-mock-probe.mjs --busy`, not reasoned about): libuv assigns
|
|
35
|
+
* a non-detached child to its global job object, which is created with
|
|
36
|
+
* KILL_ON_JOB_CLOSE. The launcher died, the job closed, and the Host was
|
|
37
|
+
* terminated with it — a closed terminal window ended the session's compute.
|
|
38
|
+
*
|
|
39
|
+
* This function still returns `detached: false` there, because it describes a
|
|
40
|
+
* *direct* spawn, and a direct spawn cannot have both properties: Node exposes
|
|
41
|
+
* `detached` (DETACHED_PROCESS, which makes Windows ignore CREATE_NO_WINDOW) and
|
|
42
|
+
* `windowsHide`, but not `CREATE_NEW_CONSOLE`. The way out is to not spawn the
|
|
43
|
+
* Host directly on Windows: {@link hostBootstrapCommand} starts it through the
|
|
44
|
+
* OS PowerShell, which *can* ask for a new console and hide it. This direct path
|
|
45
|
+
* stays as the fallback for a Windows box without PowerShell.
|
|
35
46
|
*/
|
|
36
47
|
export declare function hostSpawnOptions(platform?: NodeJS.Platform): {
|
|
37
48
|
detached: boolean;
|
|
38
49
|
windowsHide: boolean;
|
|
39
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* The Windows PowerShell that ships with the operating system.
|
|
53
|
+
*
|
|
54
|
+
* `powershell.exe` and not `pwsh.exe`: the latter is PowerShell 7, an optional
|
|
55
|
+
* install, while 5.1 is part of Windows 10/11. `%SystemRoot%` is where it lives
|
|
56
|
+
* (`%windir%` is the legacy spelling of the same thing); when neither is set
|
|
57
|
+
* there is nothing to find, and the caller falls back to a direct spawn.
|
|
58
|
+
*/
|
|
59
|
+
export declare function windowsPowerShellPath(env?: NodeJS.ProcessEnv): string | undefined;
|
|
60
|
+
/** A PowerShell single-quoted literal; `''` is the only escape inside one. */
|
|
61
|
+
export declare function psQuote(value: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Quote one argv array into a Windows command line, the way `CreateProcess`
|
|
64
|
+
* parses it back (the rule from "Everyone quotes command line arguments the
|
|
65
|
+
* wrong way"): wrap in double quotes when the argument is empty or contains
|
|
66
|
+
* whitespace or a quote, double the backslashes that precede a quote, and double
|
|
67
|
+
* trailing backslashes before the closing quote. `Start-Process -ArgumentList`
|
|
68
|
+
* joins its array with spaces and adds no quoting of its own, so the line has to
|
|
69
|
+
* be right before it is handed over — a `DSH_HOME` with a space in it is the
|
|
70
|
+
* normal case, not the exotic one.
|
|
71
|
+
*/
|
|
72
|
+
export declare function windowsCommandLine(argv: string[]): string;
|
|
73
|
+
/** `-EncodedCommand` wants base64 of UTF-16LE, which is also what dodges quoting. */
|
|
74
|
+
export declare function encodePowerShellCommand(script: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* The bootstrap script: start the Host with **its own console, hidden**, and
|
|
77
|
+
* print its pid so the launcher can watch it.
|
|
78
|
+
*
|
|
79
|
+
* `Start-Process -WindowStyle Hidden` is the only way to ask for this from a
|
|
80
|
+
* Node process. It is ShellExecuteEx/CreateProcess with `CREATE_NEW_CONSOLE` and
|
|
81
|
+
* `SW_HIDE`: the Host gets a console of its own, so closing the user's terminal
|
|
82
|
+
* no longer takes it down, and that console is invisible, so the tool calls that
|
|
83
|
+
* inherit it do not flash. `-PassThru` gives the object whose `Id` is printed;
|
|
84
|
+
* `-RedirectStandardError` keeps the Host's stderr log, which the direct spawn
|
|
85
|
+
* used to feed through an inherited fd.
|
|
86
|
+
*
|
|
87
|
+
* The whole script travels as an encoded command, so nothing in it is ever
|
|
88
|
+
* re-parsed by a shell.
|
|
89
|
+
*/
|
|
90
|
+
export declare function hiddenConsoleHostScript(options: {
|
|
91
|
+
execPath: string;
|
|
92
|
+
argv: string[];
|
|
93
|
+
/** Where the Host's stderr goes; omitted only in tests. */
|
|
94
|
+
stderrFile?: string;
|
|
95
|
+
/** Where the Host's pid is written for the launcher to read. */
|
|
96
|
+
pidFile: string;
|
|
97
|
+
}): string;
|
|
98
|
+
/**
|
|
99
|
+
* Set on the Host's environment by the bootstrap below, through
|
|
100
|
+
* {@link bootstrapEnv}.
|
|
101
|
+
*
|
|
102
|
+
* The Host cannot ask whether it has a console of its own — Node exposes no such
|
|
103
|
+
* question — so the launcher tells it. That is what lets the one build where the
|
|
104
|
+
* lifecycle promise does not hold (Windows, no PowerShell, direct child) say so
|
|
105
|
+
* at boot instead of letting the user discover it by closing the window.
|
|
106
|
+
*/
|
|
107
|
+
export declare const TUI_HOST_START_ENV = "DSH_TUI_HOST_START";
|
|
108
|
+
/** Marker value for a Host started with a hidden console of its own. */
|
|
109
|
+
export declare const TUI_HOST_START_BOOTSTRAP = "hidden-console";
|
|
110
|
+
/** The environment the bootstrap hands to the Host: the marker is added here. */
|
|
111
|
+
export declare function bootstrapEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
112
|
+
/**
|
|
113
|
+
* Whether this Host survives its terminal being closed.
|
|
114
|
+
*
|
|
115
|
+
* POSIX always does (`setsid`); Windows does exactly when the bootstrap started
|
|
116
|
+
* it. A Windows Host without the marker is the fallback direct child, which
|
|
117
|
+
* libuv's `KILL_ON_JOB_CLOSE` job takes down with the launcher — the one case
|
|
118
|
+
* worth a boot notice.
|
|
119
|
+
*/
|
|
120
|
+
export declare function hostHasOwnConsole(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|
|
121
|
+
/** How the Host is started when it must not be a direct child. */
|
|
122
|
+
export interface HostBootstrapCommand {
|
|
123
|
+
command: string;
|
|
124
|
+
args: string[];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The hidden-console bootstrap for this platform, or `undefined` when the Host
|
|
128
|
+
* should be spawned directly.
|
|
129
|
+
*
|
|
130
|
+
* Windows only, and only when the OS PowerShell is really there: the caller
|
|
131
|
+
* falls back to {@link hostSpawnOptions}, which keeps the boot working (without
|
|
132
|
+
* the survival property) on a machine where PowerShell is missing or blocked.
|
|
133
|
+
* `exists` is injectable so the decision is assertable on Linux.
|
|
134
|
+
*/
|
|
135
|
+
export declare function hostBootstrapCommand(options: {
|
|
136
|
+
platform?: NodeJS.Platform;
|
|
137
|
+
env?: NodeJS.ProcessEnv;
|
|
138
|
+
exists?: (path: string) => boolean;
|
|
139
|
+
execPath: string;
|
|
140
|
+
argv: string[];
|
|
141
|
+
stderrFile?: string;
|
|
142
|
+
pidFile: string;
|
|
143
|
+
}): HostBootstrapCommand | undefined;
|
|
40
144
|
/**
|
|
41
145
|
* A path a human can read, with the platform's own shorthand.
|
|
42
146
|
*
|
|
@@ -48,3 +152,64 @@ export declare function displayHomePath(home: string, file: string, options?: {
|
|
|
48
152
|
env?: NodeJS.ProcessEnv;
|
|
49
153
|
userHome?: string;
|
|
50
154
|
}): string;
|
|
155
|
+
/**
|
|
156
|
+
* Who owns the files this plugin writes, and how to keep it that way.
|
|
157
|
+
*
|
|
158
|
+
* On POSIX the `mode` passed to `writeFile`/`mkdir` (`0o600`, `0o700`) is the
|
|
159
|
+
* whole story. **On Windows it is silently ignored**, and the files that matter
|
|
160
|
+
* here are not cosmetic: `env.cmd` carries API keys, the SuperGrok token file
|
|
161
|
+
* carries an OAuth grant, and the lock/socket directories carry session
|
|
162
|
+
* metadata. What they get instead is the ACL inherited from their parent — fine
|
|
163
|
+
* under `%USERPROFILE%\.dsh`, and *not* fine when `DSH_HOME` points somewhere
|
|
164
|
+
* shared (`C:\dsh`, a network share, a machine where `Users` can read the
|
|
165
|
+
* directory), which is exactly when nobody notices.
|
|
166
|
+
*
|
|
167
|
+
* So the intent is applied explicitly: `icacls` with inheritance removed and a
|
|
168
|
+
* single grant to the current user. The argv is built by a pure function so it
|
|
169
|
+
* can be asserted on Linux; applying it is best-effort by design — a machine
|
|
170
|
+
* without `icacls`, or a path held open by another process, must not fail the
|
|
171
|
+
* write that just succeeded. Failing closed here would mean a TUI that cannot
|
|
172
|
+
* save its own settings.
|
|
173
|
+
*/
|
|
174
|
+
/**
|
|
175
|
+
* The account `icacls` should grant, or undefined when the environment has none.
|
|
176
|
+
*
|
|
177
|
+
* `%USERDOMAIN%\%USERNAME%` where both are present: on a domain-joined machine a
|
|
178
|
+
* bare name can resolve to the machine-local account of the same name, and a
|
|
179
|
+
* grant to the wrong account — with inheritance already removed — would leave
|
|
180
|
+
* the user's own API keys inaccessible to them.
|
|
181
|
+
*/
|
|
182
|
+
export declare function aclUserName(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): string | undefined;
|
|
183
|
+
/**
|
|
184
|
+
* The exact `icacls` arguments for one path.
|
|
185
|
+
*
|
|
186
|
+
* `/inheritance:r` drops what the parent offered (this is the part that matters:
|
|
187
|
+
* adding a grant without removing inheritance leaves `Users` in place), and
|
|
188
|
+
* `(OI)(CI)` makes a directory's grant apply to what is created inside it —
|
|
189
|
+
* without it every new lock file would need its own call.
|
|
190
|
+
*/
|
|
191
|
+
export declare function restrictPathArgs(path: string, user: string, options?: {
|
|
192
|
+
directory?: boolean;
|
|
193
|
+
}): string[];
|
|
194
|
+
/** Directories and files whose contents must not be readable by other users. */
|
|
195
|
+
export interface RestrictDeps {
|
|
196
|
+
/** Runner for `icacls`; injected by tests. Returns true on success. */
|
|
197
|
+
run?: (command: string, args: string[]) => boolean;
|
|
198
|
+
platform?: NodeJS.Platform;
|
|
199
|
+
env?: NodeJS.ProcessEnv;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Apply `mode` on POSIX, a single-user ACL on Windows.
|
|
203
|
+
*
|
|
204
|
+
* Returns whether the restriction was applied. Never throws: the caller has
|
|
205
|
+
* already written the file, and a permission call is not worth losing it over.
|
|
206
|
+
*/
|
|
207
|
+
export declare function restrictPathToUser(path: string, options?: {
|
|
208
|
+
mode: number;
|
|
209
|
+
directory?: boolean;
|
|
210
|
+
} & RestrictDeps): Promise<boolean>;
|
|
211
|
+
/** Synchronous twin, for the sites that create their file with `openSync`. */
|
|
212
|
+
export declare function restrictPathToUserSync(path: string, options?: {
|
|
213
|
+
mode: number;
|
|
214
|
+
directory?: boolean;
|
|
215
|
+
} & RestrictDeps): boolean;
|
|
@@ -20,21 +20,13 @@
|
|
|
20
20
|
* every later launch falls back to the default.
|
|
21
21
|
* @module dsh-ssh-tui/preset-authoring
|
|
22
22
|
*/
|
|
23
|
-
import { type AgentPreset, type PresetMetadata } from '
|
|
23
|
+
import { presetDirectory, type AgentPreset, type PresetMetadata } from './preset-compat.js';
|
|
24
|
+
export { presetDirectory };
|
|
24
25
|
/** The id shape discovery accepts; a directory that fails it is skipped. */
|
|
25
26
|
export declare const PRESET_ID_PATTERN: RegExp;
|
|
26
27
|
/** Whether an id would be discovered at all. */
|
|
27
28
|
export declare function validatePresetId(id: string): boolean;
|
|
28
|
-
|
|
29
|
-
* The directory a preset owns.
|
|
30
|
-
*
|
|
31
|
-
* `AgentPreset.path` is the composition file the preset publishes, not the
|
|
32
|
-
* directory, so anything that writes beside it (display metadata) starts here.
|
|
33
|
-
* @param preset - a discovered preset.
|
|
34
|
-
* @returns the absolute preset directory.
|
|
35
|
-
*/
|
|
36
|
-
export declare function presetDirectory(preset: AgentPreset): string;
|
|
37
|
-
export type PresetRefusalCode = 'invalid-id' | 'exists' | 'not-found' | 'read-only' | 'system' | 'running' | 'empty-metadata';
|
|
29
|
+
export type PresetRefusalCode = 'invalid-id' | 'exists' | 'not-found' | 'read-only' | 'system' | 'managed' | 'running' | 'empty-metadata';
|
|
38
30
|
export interface PresetRefusal {
|
|
39
31
|
error: PresetRefusalCode;
|
|
40
32
|
/** The id the refusal is about, when it names one. */
|
|
@@ -78,9 +70,11 @@ export interface PresetCompositionView {
|
|
|
78
70
|
}
|
|
79
71
|
/**
|
|
80
72
|
* The authoring subset of the service, feature-detected rather than assumed:
|
|
81
|
-
* the
|
|
82
|
-
* `
|
|
83
|
-
*
|
|
73
|
+
* the two supported lines expose different preset services under the same
|
|
74
|
+
* `agentPresets` name. 0.1.5's `dsh-agent-presets` carries `copy`, `remove`,
|
|
75
|
+
* `read` and `compositionInventory`; the 0.1.7 registry
|
|
76
|
+
* (`dsh-agent-preset-registry`) does not. A missing member turns into a clear
|
|
77
|
+
* message instead of a crash.
|
|
84
78
|
*/
|
|
85
79
|
export interface PresetAuthoringApi {
|
|
86
80
|
readonly authorable?: boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** The optional display-metadata file beside a preset's composition. */
|
|
2
|
+
export declare const METADATA_FILE = "preset.yml";
|
|
3
|
+
/** Display text a preset may publish about itself. */
|
|
4
|
+
export interface PresetMetadata {
|
|
5
|
+
/** Human-facing name; falls back to the preset id when absent. */
|
|
6
|
+
readonly name?: string;
|
|
7
|
+
/** One sentence on what this preset is for. */
|
|
8
|
+
readonly description?: string;
|
|
9
|
+
/** Position within its group; lower comes first. */
|
|
10
|
+
readonly order?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Where a preset's composition came from, on the line that still records it. */
|
|
13
|
+
export type PresetTrust = 'system' | 'user';
|
|
14
|
+
/**
|
|
15
|
+
* One preset, as much of it as this plugin reads.
|
|
16
|
+
*
|
|
17
|
+
* `id`/`name`/`description`/`order`/`broken` are the fields both lines agree on.
|
|
18
|
+
* `path` (the composition file) and `trust` (the root it was discovered under)
|
|
19
|
+
* only exist on the 0.1.5 line; every consumer here treats them as optional,
|
|
20
|
+
* and the authoring paths refuse rather than guess when they are gone.
|
|
21
|
+
*/
|
|
22
|
+
export interface AgentPreset {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly name?: string;
|
|
25
|
+
readonly description?: string;
|
|
26
|
+
readonly order?: number;
|
|
27
|
+
readonly broken?: string;
|
|
28
|
+
/** 0.1.5 line only: absolute path of the preset's agent composition file. */
|
|
29
|
+
readonly path?: string;
|
|
30
|
+
/** 0.1.5 line only: trust recorded from the root it was discovered under. */
|
|
31
|
+
readonly trust?: PresetTrust;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The directory a preset owns, when the host still exposes one.
|
|
35
|
+
*
|
|
36
|
+
* `AgentPreset.path` is the composition file the preset publishes, not the
|
|
37
|
+
* directory, so anything that writes beside it (display metadata) starts here.
|
|
38
|
+
* `undefined` on a host that lists declarative presets instead of directories.
|
|
39
|
+
* @param preset - a discovered preset.
|
|
40
|
+
* @returns the absolute preset directory, or undefined when there is none.
|
|
41
|
+
*/
|
|
42
|
+
export declare function presetDirectory(preset: AgentPreset): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Read a preset's display metadata. Every failure degrades to no metadata: a
|
|
45
|
+
* preset whose display text is missing, malformed, or unreadable still mounts.
|
|
46
|
+
* @param directory - the preset directory to read `preset.yml` from.
|
|
47
|
+
* @returns the published name/description/order, or `{}`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readPresetMetadata(directory: string): Promise<PresetMetadata>;
|
|
50
|
+
/**
|
|
51
|
+
* Render display metadata as the file's contents.
|
|
52
|
+
*
|
|
53
|
+
* Absent fields are omitted rather than written empty, so a preset with no
|
|
54
|
+
* description does not ship a key that reads as an intentional blank.
|
|
55
|
+
* @param metadata - the display text to store.
|
|
56
|
+
* @returns the YAML document, or undefined when there is nothing to store.
|
|
57
|
+
*/
|
|
58
|
+
export declare function renderPresetMetadata(metadata: PresetMetadata): string | undefined;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* service — the caller passes what `list()` returned.
|
|
10
10
|
* @module dsh-ssh-tui/preset-picker
|
|
11
11
|
*/
|
|
12
|
-
import type { AgentPreset } from '
|
|
12
|
+
import type { AgentPreset } from './preset-compat.js';
|
|
13
13
|
/** One option as the question dialog wants it, plus the id it stands for. */
|
|
14
14
|
export interface PresetPickerOption {
|
|
15
15
|
id: string;
|
|
@@ -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,13 +30,35 @@ export interface RosterRow {
|
|
|
27
30
|
config?: readonly string[];
|
|
28
31
|
}
|
|
29
32
|
/**
|
|
30
|
-
* The rows
|
|
31
|
-
* the TypeScript runtime the PTC preset needs, and the host-owned
|
|
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. What the base does *not* mount are the three rows the
|
|
46
|
+
* shipped standard preset owns beyond it — the persona prompt, and the
|
|
47
|
+
* `ask_user_question` / `present` tools — so those are what a TUI profile adds.
|
|
48
|
+
* Written verbatim as upstream's own `dsh-web-app/presets/standard.patch.yml`
|
|
49
|
+
* declares them, minus the rows the base already carries.
|
|
50
|
+
*/
|
|
51
|
+
export declare const FORMS_ROWS: readonly RosterRow[];
|
|
52
|
+
/** The rows one host line's profile has to mount. */
|
|
53
|
+
export declare function rosterRows(generation: HostGeneration): readonly RosterRow[];
|
|
54
|
+
/** Every row either line knows about, for name-to-row lookups. */
|
|
55
|
+
export declare const ALL_ROSTER_ROWS: readonly RosterRow[];
|
|
35
56
|
/** The comment header the block introduces itself with, in both writers. */
|
|
36
57
|
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";
|
|
58
|
+
/** The same header on a host that composes its agent process-wide. */
|
|
59
|
+
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 every row the standard preset\n# needs except these three: the persona prompt and the ask_user_question /\n# present tools. The profile's user layer owns them.\n";
|
|
60
|
+
/** The header for one host line. */
|
|
61
|
+
export declare function rosterPatchHeader(generation: HostGeneration): string;
|
|
37
62
|
/** One top-level `- insert:` entry mounting exactly these rows. */
|
|
38
63
|
export declare function rosterInsertEntry(rows: readonly RosterRow[]): string;
|
|
39
64
|
/**
|
|
@@ -42,8 +67,23 @@ export declare function rosterInsertEntry(rows: readonly RosterRow[]): string;
|
|
|
42
67
|
* the same text; a test compares the two so they cannot drift.
|
|
43
68
|
*/
|
|
44
69
|
export declare const ROSTER_PATCH_BLOCK: string;
|
|
70
|
+
/** The 0.1.7 block, the same way. */
|
|
71
|
+
export declare const FORMS_PATCH_BLOCK: string;
|
|
72
|
+
/** The block for one host line. */
|
|
73
|
+
export declare function rosterPatchBlock(generation: HostGeneration): string;
|
|
45
74
|
/** The profile patch file the roster block belongs in. */
|
|
46
75
|
export declare function rosterPatchPath(home: string, profile: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Whether a profile patch already declares every 0.1.7 agent-plane row.
|
|
78
|
+
*
|
|
79
|
+
* Synchronous on purpose: the footer chip is rendered from the main loop and
|
|
80
|
+
* cannot await a read. The file is small and this runs at startup and after a
|
|
81
|
+
* repair, not per frame.
|
|
82
|
+
* @param home - the harness home carrying `profiles/`.
|
|
83
|
+
* @param profile - the profile to inspect.
|
|
84
|
+
* @returns whether all three rows are declared (or the row is unreachable).
|
|
85
|
+
*/
|
|
86
|
+
export declare function formsRowsDeclared(home: string, profile: string): boolean;
|
|
47
87
|
/** One row a patch file declares, with enough position to point at it. */
|
|
48
88
|
export interface PatchRowRef {
|
|
49
89
|
/** Insert id, or the target of an override/disable entry. */
|
|
@@ -88,7 +128,7 @@ export declare function duplicatePatchRows(rows: readonly PatchRowRef[]): PatchR
|
|
|
88
128
|
/** Whether a patch already declares one roster row, by id or by module name. */
|
|
89
129
|
export declare function patchNamesRow(rows: readonly PatchRowRef[], row: RosterRow): boolean;
|
|
90
130
|
/** The roster rows a patch does not declare yet. */
|
|
91
|
-
export declare function missingRosterRows(rows: readonly PatchRowRef[]): RosterRow[];
|
|
131
|
+
export declare function missingRosterRows(rows: readonly PatchRowRef[], candidates?: readonly RosterRow[]): RosterRow[];
|
|
92
132
|
export interface PatchRepair {
|
|
93
133
|
text: string;
|
|
94
134
|
/** Roster rows this repair adds, by id. */
|
|
@@ -108,10 +148,11 @@ export interface PatchRepair {
|
|
|
108
148
|
* (the profile template) is replaced; anything else keeps its content and gains
|
|
109
149
|
* a new `- insert:` entry after a blank line.
|
|
110
150
|
* @param existing - the patch file's current text.
|
|
111
|
-
* @param missing - the roster rows to mount; defaults to
|
|
151
|
+
* @param missing - the roster rows to mount; defaults to the 0.1.5 set.
|
|
152
|
+
* @param generation - which host line the profile boots on.
|
|
112
153
|
* @returns the new text, or `undefined` when nothing has to change.
|
|
113
154
|
*/
|
|
114
|
-
export declare function planRosterRepair(existing: string, missing?: readonly RosterRow[]): PatchRepair | undefined;
|
|
155
|
+
export declare function planRosterRepair(existing: string, missing?: readonly RosterRow[], generation?: HostGeneration): PatchRepair | undefined;
|
|
115
156
|
/**
|
|
116
157
|
* The patch text with every repeated insert removed, or `undefined` when the
|
|
117
158
|
* file has no duplicate.
|
|
@@ -132,10 +173,11 @@ export declare function planDuplicateRepair(existing: string): PatchRepair | und
|
|
|
132
173
|
* half-written patch would only be seen by the next launch.
|
|
133
174
|
* @param home - the harness home carrying `profiles/`.
|
|
134
175
|
* @param profile - the profile to patch.
|
|
135
|
-
* @param missing - the rows to mount; defaults to the
|
|
176
|
+
* @param missing - the rows to mount; defaults to the 0.1.5 set.
|
|
177
|
+
* @param generation - which host line the profile boots on.
|
|
136
178
|
* @returns `present` when the row already exists, else `written`.
|
|
137
179
|
*/
|
|
138
|
-
export declare function ensureRosterRows(home: string, profile: string, missing?: readonly RosterRow[]): Promise<'present' | 'written'>;
|
|
180
|
+
export declare function ensureRosterRows(home: string, profile: string, missing?: readonly RosterRow[], generation?: HostGeneration): Promise<'present' | 'written'>;
|
|
139
181
|
/**
|
|
140
182
|
* Write a repaired patch next to a copy of the previous content.
|
|
141
183
|
*
|
|
@@ -148,4 +190,4 @@ export declare function ensureRosterRows(home: string, profile: string, missing?
|
|
|
148
190
|
*/
|
|
149
191
|
export declare function writePatchWithBackup(path: string, text: string, stamp?: string): Promise<string | undefined>;
|
|
150
192
|
/** 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;
|
|
193
|
+
export declare function rosterPatchText(existing: string, generation?: HostGeneration): string | undefined;
|
|
@@ -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<{
|
|
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<{
|
|
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
|
+
}>>;
|
|
23
|
+
/** Nothing to mount: the TUI owns every read and write of this section. */
|
|
24
|
+
export declare function apply(_ctx: Context): void;
|
|
@@ -129,15 +129,15 @@ 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
|
|
132
|
+
/** Settings schema for the subagent selection; every field is form-writable. */
|
|
133
133
|
export declare const SUBAGENT_SETTINGS_SCHEMA: z<Schemastery.ObjectS<{
|
|
134
|
-
provider: z<string
|
|
135
|
-
model: z<string
|
|
136
|
-
reasoningEffort: z<string
|
|
134
|
+
provider: z<string>;
|
|
135
|
+
model: z<string>;
|
|
136
|
+
reasoningEffort: z<string>;
|
|
137
137
|
}>, Schemastery.ObjectT<{
|
|
138
|
-
provider: z<string
|
|
139
|
-
model: z<string
|
|
140
|
-
reasoningEffort: z<string
|
|
138
|
+
provider: z<string>;
|
|
139
|
+
model: z<string>;
|
|
140
|
+
reasoningEffort: z<string>;
|
|
141
141
|
}>>;
|
|
142
142
|
/** Normalize a raw settings section into a live typed selection. */
|
|
143
143
|
export declare function normalizeSubagentSelection(value: unknown): SubagentSelection;
|
|
@@ -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 {};
|