@tiphareth/dsh-hardssh 0.1.2-alpha → 0.2.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.
- package/LICENSE +30 -0
- package/README.md +49 -25
- package/SKILLS.md +140 -0
- package/lib/base/index.js +95 -0
- package/lib/client.js +1165 -1329
- package/lib/fs.js +307 -416
- package/lib/index.js +5428 -2991
- package/lib/ledger-B-LXlftp.js +43 -0
- package/lib/ledger-D2ezq1iW.js +376 -0
- package/lib/model-Cp7f70Mb.js +5 -0
- package/lib/registry-CViuzKYI.js +421 -0
- package/lib/subprocess.js +92 -549
- package/lib/types/backend.d.ts +73 -78
- package/lib/types/base/capability.d.ts +4 -0
- package/lib/types/base/ledger-router.d.ts +42 -26
- package/lib/types/base/ledger.d.ts +30 -19
- package/lib/types/base/model.d.ts +17 -8
- package/lib/types/base/plugin.d.ts +11 -0
- package/lib/types/base/registry.d.ts +28 -31
- package/lib/types/base/router.d.ts +0 -7
- package/lib/types/client/api.d.ts +8 -8
- package/lib/types/client/connect-host.d.ts +13 -0
- package/lib/types/client/index.d.ts +6 -4
- package/lib/types/client/locales.d.ts +15 -1
- package/lib/types/client/session-connect-gate.d.ts +71 -0
- package/lib/types/client/ssh/api.d.ts +11 -9
- package/lib/types/client/ssh/apply.d.ts +14 -8
- package/lib/types/client/ssh/locales.d.ts +8 -1
- package/lib/types/client/ssh/ops-tab.d.ts +34 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
- package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
- package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
- package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
- package/lib/types/client/ssh/session-target.d.ts +17 -0
- package/lib/types/client/state.d.ts +4 -1
- package/lib/types/client/workspace-panel-entry.d.ts +30 -0
- package/lib/types/client/workspace-panel.d.ts +24 -0
- package/lib/types/client-http.d.ts +77 -6
- package/lib/types/core.d.ts +5 -16
- package/lib/types/fs.d.ts +18 -25
- package/lib/types/index.d.ts +138 -9
- package/lib/types/ledger.d.ts +16 -111
- package/lib/types/protocol.d.ts +0 -5
- package/lib/types/providers/index.d.ts +6 -3
- package/lib/types/providers/local/provider.d.ts +83 -23
- package/lib/types/providers/ssh/provider.d.ts +24 -57
- package/lib/types/remote/environment.d.ts +2 -0
- package/lib/types/remote/remote-fs.d.ts +12 -1
- package/lib/types/remote/remote-process.d.ts +22 -1
- package/lib/types/remote/remote-subprocess.d.ts +13 -1
- package/lib/types/remote/remote-terminal.d.ts +4 -0
- package/lib/types/remote-search.d.ts +3 -3
- package/lib/types/routes.d.ts +36 -8
- package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
- package/lib/types/runtime/workspace-core.d.ts +73 -38
- package/lib/types/runtime/workspace-migration.d.ts +113 -0
- package/lib/types/ssh/connection/lease.d.ts +23 -0
- package/lib/types/ssh/connection/manager.d.ts +359 -0
- package/lib/types/ssh/connection/pool.d.ts +8 -1
- package/lib/types/ssh/engine.d.ts +97 -267
- package/lib/types/ssh/known-hosts.d.ts +22 -5
- package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
- package/lib/types/ssh/plugin.d.ts +10 -15
- package/lib/types/ssh/protocol.d.ts +18 -0
- package/lib/types/ssh/routes.d.ts +15 -7
- package/lib/types/ssh/sftp/service.d.ts +216 -0
- package/lib/types/ssh/store.d.ts +20 -8
- package/lib/types/ssh/terminal/service.d.ts +49 -0
- package/lib/types/ssh/tunnel/service.d.ts +26 -0
- package/lib/types/ssh/vault.d.ts +51 -4
- package/lib/types/subprocess.d.ts +13 -21
- package/lib/types/switch/switch-fs.d.ts +52 -2
- package/lib/types/switch/switch-subprocess.d.ts +32 -3
- package/lib/types/tools.d.ts +7 -6
- package/lib/types/workspace-tool-ops.d.ts +33 -0
- package/lib/types/workspace.d.ts +18 -0
- package/lib/vault-3gpWct2Q.js +559 -0
- package/lib/workspace.js +2 -0
- package/package.json +36 -11
- package/scripts/export-legacy-workspaces.mjs +136 -0
- package/src/backend.ts +155 -483
- package/src/base/capability.ts +4 -0
- package/src/base/ledger-router.ts +214 -65
- package/src/base/ledger.ts +175 -48
- package/src/base/model.ts +18 -8
- package/src/base/plugin.ts +15 -4
- package/src/base/registry.ts +49 -41
- package/src/base/router.ts +0 -8
- package/src/client/api.ts +8 -36
- package/src/client/connect-host.ts +199 -0
- package/src/client/directory-flow.tsx +91 -36
- package/src/client/index.ts +56 -53
- package/src/client/locales.ts +14 -0
- package/src/client/session-connect-gate.ts +163 -0
- package/src/client/ssh/api.ts +245 -108
- package/src/client/ssh/apply.ts +23 -24
- package/src/client/ssh/locales.ts +16 -2
- package/src/client/ssh/ops-tab.tsx +79 -0
- package/src/client/ssh/panel/ClusterTab.tsx +9 -20
- package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
- package/src/client/ssh/panel/SshPanel.tsx +81 -77
- package/src/client/ssh/panel/TerminalTab.tsx +18 -37
- package/src/client/ssh/panel/TransferTab.tsx +43 -33
- package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
- package/src/client/ssh/panel/panel.module.css +90 -127
- package/src/client/ssh/session-target.ts +69 -0
- package/src/client/state.ts +9 -1
- package/src/client/workspace-badges.ts +99 -99
- package/src/client/workspace-panel-entry.tsx +91 -0
- package/src/client/workspace-panel.tsx +227 -0
- package/src/client/workspace.module.css +76 -2
- package/src/client-http.ts +118 -18
- package/src/core.ts +37 -47
- package/src/fs.ts +137 -85
- package/src/index.ts +535 -244
- package/src/ledger.ts +62 -416
- package/src/protocol.ts +0 -6
- package/src/providers/index.ts +8 -4
- package/src/providers/local/provider.ts +259 -87
- package/src/providers/ssh/provider.ts +158 -155
- package/src/remote/environment.ts +22 -1
- package/src/remote/remote-fs.ts +123 -34
- package/src/remote/remote-process.ts +265 -203
- package/src/remote/remote-subprocess.ts +61 -18
- package/src/remote/remote-terminal.ts +106 -21
- package/src/remote-search.ts +122 -26
- package/src/routes.ts +416 -395
- package/src/runtime/dsh-capabilities.ts +19 -0
- package/src/runtime/workspace-core.ts +171 -88
- package/src/runtime/workspace-migration.ts +472 -0
- package/src/ssh/connection/lease.ts +35 -0
- package/src/ssh/connection/manager.ts +1083 -0
- package/src/ssh/connection/pool.ts +341 -275
- package/src/ssh/engine.ts +269 -1477
- package/src/ssh/known-hosts.ts +42 -18
- package/src/ssh/local-transfer-policy.ts +83 -0
- package/src/ssh/plugin.ts +20 -19
- package/src/ssh/protocol.ts +22 -1
- package/src/ssh/routes.ts +419 -187
- package/src/ssh/sftp/service.ts +967 -0
- package/src/ssh/store.ts +169 -72
- package/src/ssh/terminal/service.ts +177 -0
- package/src/ssh/tools.ts +42 -20
- package/src/ssh/tunnel/service.ts +217 -0
- package/src/ssh/vault.ts +245 -91
- package/src/subprocess.ts +88 -71
- package/src/switch/switch-fs.ts +141 -10
- package/src/switch/switch-subprocess.ts +80 -6
- package/src/tools.ts +217 -221
- package/src/workspace-tool-ops.ts +101 -0
- package/src/workspace.ts +52 -0
- package/lib/environment-BL1jddfB.js +0 -449
- package/lib/switch-fs-CAJpFY9C.js +0 -193
- package/lib/switch-fs-RrZtG2gv.js +0 -210
- package/lib/types/client/manager-button.d.ts +0 -32
- package/lib/types/client/ssh/mount.d.ts +0 -13
- package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
- package/lib/types/client/ssh/panel/controller.d.ts +0 -23
- package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
- package/lib/types/client/workspace-gate.d.ts +0 -15
- package/lib/types/remote-runner.d.ts +0 -83
- package/lib/types/seam-state.d.ts +0 -69
- package/src/client/manager-button.tsx +0 -269
- package/src/client/ssh/mount.tsx +0 -83
- package/src/client/ssh/panel/HostsTab.tsx +0 -236
- package/src/client/ssh/panel/controller.ts +0 -48
- package/src/client/ssh/sidebar-entry.ts +0 -123
- package/src/client/workspace-gate.ts +0 -232
- package/src/remote-runner.ts +0 -201
- package/src/seam-state.ts +0 -185
package/lib/client.js
CHANGED
|
@@ -7,25 +7,59 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
//#region \0rolldown/runtime.js
|
|
8
8
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
9
9
|
//#endregion
|
|
10
|
-
let react_dom_client = require("react-dom/client");
|
|
11
10
|
let react = require("react");
|
|
12
|
-
let react_dom = require("react-dom");
|
|
13
11
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
12
|
+
let react_dom = require("react-dom");
|
|
13
|
+
let react_dom_client = require("react-dom/client");
|
|
14
14
|
//#region src/client-http.ts
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* The single transport error: HTTP status plus the PARSED response body, so
|
|
17
|
+
* no caller re-parses a response and no field is flattened away. The SSH
|
|
18
|
+
* client re-exports it as `SshApiError`; the workspace client subclasses it as
|
|
19
|
+
* `WorkspaceApiError`. Both are this class at runtime, so the UI's
|
|
20
|
+
* `instanceof` / `.code` / `.hostKeyFingerprint` checks keep working.
|
|
18
21
|
*/
|
|
19
|
-
/** Error carrying the route's JSON error message and stable code/status. */
|
|
20
22
|
var HttpApiError = class extends Error {
|
|
21
|
-
code;
|
|
22
23
|
status;
|
|
23
|
-
|
|
24
|
+
body;
|
|
25
|
+
constructor(message, status, body) {
|
|
24
26
|
super(message);
|
|
25
|
-
this.code = code;
|
|
26
27
|
this.status = status;
|
|
28
|
+
this.body = body;
|
|
27
29
|
this.name = "HttpApiError";
|
|
28
30
|
}
|
|
31
|
+
/** Stable machine code from the route error body. */
|
|
32
|
+
get code() {
|
|
33
|
+
return typeof this.body?.code === "string" ? this.body.code : void 0;
|
|
34
|
+
}
|
|
35
|
+
/** Which secret a connection needs when {@link code} === 'NEEDS_PASSWORD'. */
|
|
36
|
+
get secret() {
|
|
37
|
+
const secret = this.body?.secret;
|
|
38
|
+
return secret === "password" || secret === "passphrase" ? secret : void 0;
|
|
39
|
+
}
|
|
40
|
+
/** The fingerprint when a host key was refused. */
|
|
41
|
+
get hostKeyFingerprint() {
|
|
42
|
+
return typeof this.body?.hostKeyFingerprint === "string" ? this.body.hostKeyFingerprint : void 0;
|
|
43
|
+
}
|
|
44
|
+
/** Mismatch detail (expected vs actual) when the host key changed. */
|
|
45
|
+
get hostKeyMismatch() {
|
|
46
|
+
const mismatch = this.body?.hostKeyMismatch;
|
|
47
|
+
if (typeof mismatch !== "object" || mismatch === null) return void 0;
|
|
48
|
+
const { expected, actual } = mismatch;
|
|
49
|
+
if (typeof expected !== "string" || typeof actual !== "string") return void 0;
|
|
50
|
+
return {
|
|
51
|
+
expected,
|
|
52
|
+
actual
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Vault lockout: attempts left. */
|
|
56
|
+
get remaining() {
|
|
57
|
+
return typeof this.body?.remaining === "number" ? this.body.remaining : void 0;
|
|
58
|
+
}
|
|
59
|
+
/** Vault lockout: milliseconds until the next attempt is allowed. */
|
|
60
|
+
get retryAfterMs() {
|
|
61
|
+
return typeof this.body?.retryAfterMs === "number" ? this.body.retryAfterMs : void 0;
|
|
62
|
+
}
|
|
29
63
|
};
|
|
30
64
|
/** Query-string helper (skips undefined and empty values). */
|
|
31
65
|
function buildQuery(params) {
|
|
@@ -34,27 +68,45 @@ window.__ModuleLoader__.load({
|
|
|
34
68
|
const text = search.toString();
|
|
35
69
|
return text === "" ? "" : "?" + text;
|
|
36
70
|
}
|
|
37
|
-
/** Parse a JSON response or throw
|
|
38
|
-
async function readJson
|
|
39
|
-
|
|
71
|
+
/** Parse a JSON response or throw {@link HttpApiError} (status + body kept). */
|
|
72
|
+
async function readJson(response) {
|
|
73
|
+
const body = await parseJson(response);
|
|
74
|
+
if (body === void 0) throw new HttpApiError(`HTTP ${response.status}: invalid JSON response`, response.status);
|
|
75
|
+
if (!response.ok) throw errorFor(response.status, body);
|
|
76
|
+
return body;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Throw {@link HttpApiError} for a response that does NOT carry a JSON
|
|
80
|
+
* success body (streamed upload/download): a JSON error body is still parsed
|
|
81
|
+
* and preserved, so the caller sees the route's code instead of raw text.
|
|
82
|
+
* @param response - the non-ok response (or a stream that could not start).
|
|
83
|
+
* @param fallback - message prefix used when the route sent no `error` field.
|
|
84
|
+
*/
|
|
85
|
+
async function throwHttpError(response, fallback) {
|
|
86
|
+
const object = bodyObject(await parseJson(response));
|
|
87
|
+
throw new HttpApiError(typeof object?.error === "string" ? object.error : `${fallback}: HTTP ${response.status}`, response.status, object);
|
|
88
|
+
}
|
|
89
|
+
/** Parse a response body as JSON; undefined when absent or not JSON. */
|
|
90
|
+
async function parseJson(response) {
|
|
40
91
|
try {
|
|
41
|
-
|
|
92
|
+
return await response.json();
|
|
42
93
|
} catch {
|
|
43
|
-
|
|
94
|
+
return;
|
|
44
95
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
96
|
+
}
|
|
97
|
+
/** The parsed body as an error-body record, or undefined for non-objects. */
|
|
98
|
+
function bodyObject(body) {
|
|
99
|
+
return typeof body === "object" && body !== null ? body : void 0;
|
|
100
|
+
}
|
|
101
|
+
/** Build the transport error for a non-ok response. */
|
|
102
|
+
function errorFor(status, body) {
|
|
103
|
+
const object = bodyObject(body);
|
|
104
|
+
return new HttpApiError(typeof object?.error === "string" ? object.error : `HTTP ${status}`, status, object);
|
|
50
105
|
}
|
|
51
106
|
//#endregion
|
|
52
107
|
//#region src/protocol.ts
|
|
53
108
|
const WORKSPACE_API = {
|
|
54
109
|
state: "/api/dsh-hardssh/state",
|
|
55
|
-
tree: "/api/dsh-hardssh/tree",
|
|
56
|
-
file: "/api/dsh-hardssh/file",
|
|
57
|
-
search: "/api/dsh-hardssh/search",
|
|
58
110
|
sshWorkspaces: "/api/dsh-hardssh/ws",
|
|
59
111
|
sshWorkspaceDir: "/api/dsh-hardssh/ws/dir"
|
|
60
112
|
};
|
|
@@ -70,11 +122,11 @@ window.__ModuleLoader__.load({
|
|
|
70
122
|
var WorkspaceApi = class {
|
|
71
123
|
/** List the SSH-bound workspaces. */
|
|
72
124
|
async listWorkspaces() {
|
|
73
|
-
return (await readJson
|
|
125
|
+
return (await readJson(await fetch(WORKSPACE_API.sshWorkspaces))).workspaces;
|
|
74
126
|
}
|
|
75
127
|
/** Create one SSH-bound workspace. */
|
|
76
128
|
async createWorkspace(input) {
|
|
77
|
-
return (await readJson
|
|
129
|
+
return (await readJson(await fetch(WORKSPACE_API.sshWorkspaces, {
|
|
78
130
|
method: "POST",
|
|
79
131
|
headers: { "content-type": "application/json" },
|
|
80
132
|
body: JSON.stringify(input)
|
|
@@ -82,7 +134,7 @@ window.__ModuleLoader__.load({
|
|
|
82
134
|
}
|
|
83
135
|
/** Rename one SSH-bound workspace. */
|
|
84
136
|
async renameWorkspace(id, title) {
|
|
85
|
-
return (await readJson
|
|
137
|
+
return (await readJson(await fetch(WORKSPACE_API.sshWorkspaces + "/item" + buildQuery({ id }), {
|
|
86
138
|
method: "PATCH",
|
|
87
139
|
headers: { "content-type": "application/json" },
|
|
88
140
|
body: JSON.stringify({ title })
|
|
@@ -90,58 +142,28 @@ window.__ModuleLoader__.load({
|
|
|
90
142
|
}
|
|
91
143
|
/** Delete one SSH-bound workspace. */
|
|
92
144
|
async deleteWorkspace(id) {
|
|
93
|
-
await readJson
|
|
145
|
+
await readJson(await fetch(WORKSPACE_API.sshWorkspaces + "/item" + buildQuery({ id }), { method: "DELETE" }));
|
|
94
146
|
}
|
|
95
147
|
/** List the configured SSH hosts (from the shared dsh-ssh store). */
|
|
96
148
|
async listHosts() {
|
|
97
|
-
return (await readJson
|
|
149
|
+
return (await readJson(await fetch(WORKSPACE_API.sshWorkspaces + "/hosts"))).hosts;
|
|
98
150
|
}
|
|
99
151
|
/** Browse a remote directory (for the workspace picker). */
|
|
100
152
|
async listRemoteDir(alias, path) {
|
|
101
|
-
return await readJson
|
|
153
|
+
return await readJson(await fetch(WORKSPACE_API.sshWorkspaceDir + buildQuery({
|
|
102
154
|
alias,
|
|
103
155
|
path
|
|
104
156
|
})));
|
|
105
157
|
}
|
|
106
|
-
async list(root, path) {
|
|
107
|
-
return (await readJson$1(await fetch(WORKSPACE_API.tree + buildQuery({
|
|
108
|
-
root,
|
|
109
|
-
path
|
|
110
|
-
})))).listing;
|
|
111
|
-
}
|
|
112
|
-
async read(root, path) {
|
|
113
|
-
return (await readJson$1(await fetch(WORKSPACE_API.file + buildQuery({
|
|
114
|
-
root,
|
|
115
|
-
path
|
|
116
|
-
})))).file;
|
|
117
|
-
}
|
|
118
|
-
async write(root, path, content, expectedMtime) {
|
|
119
|
-
return (await readJson$1(await fetch(WORKSPACE_API.file, {
|
|
120
|
-
method: "PUT",
|
|
121
|
-
headers: { "content-type": "application/json" },
|
|
122
|
-
body: JSON.stringify({
|
|
123
|
-
root,
|
|
124
|
-
path,
|
|
125
|
-
content,
|
|
126
|
-
expectedMtime
|
|
127
|
-
})
|
|
128
|
-
}))).result;
|
|
129
|
-
}
|
|
130
|
-
async search(root, queryText) {
|
|
131
|
-
return (await readJson$1(await fetch(WORKSPACE_API.search + buildQuery({
|
|
132
|
-
root,
|
|
133
|
-
query: queryText
|
|
134
|
-
})))).search;
|
|
135
|
-
}
|
|
136
158
|
};
|
|
137
159
|
const HOSTS_API = "/api/dsh-ssh/hosts";
|
|
138
160
|
const TEST_API = "/api/dsh-ssh/test";
|
|
139
161
|
var SshHostsApi = class {
|
|
140
162
|
async list() {
|
|
141
|
-
return (await readJson
|
|
163
|
+
return (await readJson(await fetch(HOSTS_API))).hosts;
|
|
142
164
|
}
|
|
143
165
|
async create(payload) {
|
|
144
|
-
return (await readJson
|
|
166
|
+
return (await readJson(await fetch(HOSTS_API, {
|
|
145
167
|
method: "POST",
|
|
146
168
|
headers: { "content-type": "application/json" },
|
|
147
169
|
body: JSON.stringify(payload)
|
|
@@ -151,7 +173,7 @@ window.__ModuleLoader__.load({
|
|
|
151
173
|
* may carry only the fields being changed; omitted auth keeps the stored
|
|
152
174
|
* secret. */
|
|
153
175
|
async update(alias, payload) {
|
|
154
|
-
return (await readJson
|
|
176
|
+
return (await readJson(await fetch(`${HOSTS_API}?alias=${encodeURIComponent(alias)}`, {
|
|
155
177
|
method: "PATCH",
|
|
156
178
|
headers: { "content-type": "application/json" },
|
|
157
179
|
body: JSON.stringify(payload)
|
|
@@ -159,10 +181,10 @@ window.__ModuleLoader__.load({
|
|
|
159
181
|
}
|
|
160
182
|
/** Delete a host (DELETE /api/dsh-ssh/hosts?alias=鈥?. */
|
|
161
183
|
async remove(alias) {
|
|
162
|
-
await readJson
|
|
184
|
+
await readJson(await fetch(`${HOSTS_API}?alias=${encodeURIComponent(alias)}`, { method: "DELETE" }));
|
|
163
185
|
}
|
|
164
186
|
async test(alias) {
|
|
165
|
-
return (await readJson
|
|
187
|
+
return (await readJson(await fetch(TEST_API, {
|
|
166
188
|
method: "POST",
|
|
167
189
|
headers: { "content-type": "application/json" },
|
|
168
190
|
body: JSON.stringify({ alias })
|
|
@@ -193,38 +215,25 @@ window.__ModuleLoader__.load({
|
|
|
193
215
|
* Browser-side API client for the /api/dsh-ssh route family. The only data
|
|
194
216
|
* access path the panel components use — plain fetch/WebSocket, same origin.
|
|
195
217
|
*/
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
if (!response.ok) {
|
|
217
|
-
const record = typeof body === "object" && body !== null ? body : {};
|
|
218
|
-
throw new SshApiError(typeof record.error === "string" ? record.error : `HTTP ${response.status}`, typeof record.code === "string" ? record.code : void 0, typeof record.hostKeyFingerprint === "string" ? record.hostKeyFingerprint : void 0);
|
|
219
|
-
}
|
|
220
|
-
return body;
|
|
221
|
-
}
|
|
222
|
-
/** Query-string helper. */
|
|
223
|
-
function query(params) {
|
|
224
|
-
const search = new URLSearchParams();
|
|
225
|
-
for (const [key, value] of Object.entries(params)) if (value !== void 0 && value !== "") search.set(key, String(value));
|
|
226
|
-
const text = search.toString();
|
|
227
|
-
return text === "" ? "" : "?" + text;
|
|
218
|
+
/** Query-string helper (shared transport helper, no local copy). */
|
|
219
|
+
const query = buildQuery;
|
|
220
|
+
/**
|
|
221
|
+
* Structured fields of one streamed (`application/x-ndjson`) failure frame.
|
|
222
|
+
* The upload route currently sends only `error`, so the interactive-gate
|
|
223
|
+
* fields are picked up when present and simply absent otherwise — the same
|
|
224
|
+
* shape the JSON routes produce, so callers handle one error type.
|
|
225
|
+
*/
|
|
226
|
+
function frameFailure(frame) {
|
|
227
|
+
const record = frame;
|
|
228
|
+
const secret = record.secret;
|
|
229
|
+
return {
|
|
230
|
+
...typeof record.error === "string" ? { error: record.error } : {},
|
|
231
|
+
...typeof record.code === "string" ? { code: record.code } : {},
|
|
232
|
+
...secret === "password" || secret === "passphrase" ? { secret } : {},
|
|
233
|
+
...typeof record.hostKeyFingerprint === "string" ? { hostKeyFingerprint: record.hostKeyFingerprint } : {},
|
|
234
|
+
...typeof record.remaining === "number" ? { remaining: record.remaining } : {},
|
|
235
|
+
...typeof record.retryAfterMs === "number" ? { retryAfterMs: record.retryAfterMs } : {}
|
|
236
|
+
};
|
|
228
237
|
}
|
|
229
238
|
/** The browser half's only data entry point. */
|
|
230
239
|
var SshApi = class {
|
|
@@ -323,45 +332,60 @@ window.__ModuleLoader__.load({
|
|
|
323
332
|
* Upload one file (raw bytes) to a remote path. Progress arrives through
|
|
324
333
|
* the NDJSON response stream; resolves when the result frame lands.
|
|
325
334
|
*/
|
|
326
|
-
async uploadFile(file, alias, remotePath, onProgress) {
|
|
335
|
+
async uploadFile(file, alias, remotePath, onProgress, signal) {
|
|
327
336
|
const response = await fetch(SSH_API.upload + query({
|
|
328
337
|
alias,
|
|
329
338
|
remotePath
|
|
330
339
|
}), {
|
|
331
340
|
method: "POST",
|
|
332
|
-
body: file
|
|
341
|
+
body: file,
|
|
342
|
+
signal
|
|
333
343
|
});
|
|
334
|
-
if (!response.ok
|
|
344
|
+
if (!response.ok) await throwHttpError(response, "upload failed");
|
|
345
|
+
if (response.body === null) throw new HttpApiError("upload failed: the response carried no body stream", response.status);
|
|
335
346
|
const reader = response.body.getReader();
|
|
336
347
|
const decoder = new TextDecoder();
|
|
337
348
|
let buffer = "";
|
|
338
|
-
let
|
|
349
|
+
let failure;
|
|
339
350
|
let sawResult = false;
|
|
351
|
+
let commitStarted = false;
|
|
352
|
+
let readerDone = false;
|
|
340
353
|
let transferredBytes = 0;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
354
|
+
try {
|
|
355
|
+
for (;;) {
|
|
356
|
+
const { done, value } = await reader.read();
|
|
357
|
+
if (done) {
|
|
358
|
+
readerDone = true;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
buffer += decoder.decode(value, { stream: true });
|
|
362
|
+
const lines = buffer.split("\n");
|
|
363
|
+
buffer = lines.pop() ?? "";
|
|
364
|
+
for (const line of lines) {
|
|
365
|
+
if (line.trim() === "") continue;
|
|
366
|
+
let parsed;
|
|
367
|
+
try {
|
|
368
|
+
parsed = JSON.parse(line);
|
|
369
|
+
} catch {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (parsed.type === "progress") onProgress?.(parsed.progress);
|
|
373
|
+
else if (parsed.type === "commit") commitStarted = true;
|
|
374
|
+
else if (parsed.type === "result") {
|
|
375
|
+
sawResult = true;
|
|
376
|
+
if (parsed.ok) transferredBytes = parsed.transferredBytes ?? 0;
|
|
377
|
+
else failure = frameFailure(parsed);
|
|
378
|
+
}
|
|
360
379
|
}
|
|
361
380
|
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (commitStarted) throw new HttpApiError("upload connection closed after commit began; the remote result is unknown", response.status, { code: "RESULT_UNKNOWN" });
|
|
383
|
+
throw error;
|
|
384
|
+
} finally {
|
|
385
|
+
if (!readerDone) await reader.cancel().catch(() => void 0);
|
|
362
386
|
}
|
|
363
|
-
if (
|
|
364
|
-
if (!sawResult) throw new
|
|
387
|
+
if (failure !== void 0) throw new HttpApiError(typeof failure.error === "string" ? failure.error : "upload failed", response.status, failure);
|
|
388
|
+
if (!sawResult) throw new HttpApiError(commitStarted ? "upload connection closed after commit began; the remote result is unknown" : "upload ended without a result frame — the remote destination was not published", response.status, commitStarted ? { code: "RESULT_UNKNOWN" } : { code: "ABORTED" });
|
|
365
389
|
return { transferredBytes };
|
|
366
390
|
}
|
|
367
391
|
/**
|
|
@@ -369,15 +393,13 @@ window.__ModuleLoader__.load({
|
|
|
369
393
|
* disk when the File System Access API is available (no full-file RAM
|
|
370
394
|
* copy); otherwise falls back to an in-memory Blob.
|
|
371
395
|
*/
|
|
372
|
-
async downloadFile(alias, remotePath, onProgress) {
|
|
396
|
+
async downloadFile(alias, remotePath, onProgress, signal) {
|
|
373
397
|
const response = await fetch(SSH_API.download + query({
|
|
374
398
|
alias,
|
|
375
399
|
remotePath
|
|
376
|
-
}));
|
|
377
|
-
if (!response.ok
|
|
378
|
-
|
|
379
|
-
throw new SshApiError(text !== "" && text.startsWith("{") ? text : `download failed: HTTP ${response.status}`);
|
|
380
|
-
}
|
|
400
|
+
}), { signal });
|
|
401
|
+
if (!response.ok) await throwHttpError(response, "download failed");
|
|
402
|
+
if (response.body === null) throw new HttpApiError("download failed: the response carried no body stream", response.status);
|
|
381
403
|
const total = Number(response.headers.get("content-length") ?? "0");
|
|
382
404
|
const disposition = response.headers.get("content-disposition") ?? "";
|
|
383
405
|
const filename = /filename="([^"]+)"/.exec(disposition)?.[1] ?? remotePath.split("/").pop() ?? "download";
|
|
@@ -387,6 +409,8 @@ window.__ModuleLoader__.load({
|
|
|
387
409
|
let writable;
|
|
388
410
|
const chunks = [];
|
|
389
411
|
let received = 0;
|
|
412
|
+
let readerDone = false;
|
|
413
|
+
let writableClosed = false;
|
|
390
414
|
const progress = () => {
|
|
391
415
|
onProgress?.({
|
|
392
416
|
phase: "transferring",
|
|
@@ -397,33 +421,52 @@ window.__ModuleLoader__.load({
|
|
|
397
421
|
});
|
|
398
422
|
};
|
|
399
423
|
try {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
424
|
+
try {
|
|
425
|
+
if (picker !== void 0) {
|
|
426
|
+
const handle = await picker.call(window, { suggestedName: filename });
|
|
427
|
+
signal?.throwIfAborted();
|
|
428
|
+
writable = await handle.createWritable();
|
|
429
|
+
streamed = true;
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (signal?.aborted === true) throw error;
|
|
403
433
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
434
|
+
for (;;) {
|
|
435
|
+
signal?.throwIfAborted();
|
|
436
|
+
const { done, value } = await reader.read();
|
|
437
|
+
if (done) {
|
|
438
|
+
readerDone = true;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
if (writable !== void 0) await writable.write(value);
|
|
442
|
+
else chunks.push(value);
|
|
443
|
+
received += value.length;
|
|
444
|
+
progress();
|
|
445
|
+
}
|
|
446
|
+
signal?.throwIfAborted();
|
|
447
|
+
if (writable !== void 0) {
|
|
448
|
+
await writable.close();
|
|
449
|
+
writableClosed = true;
|
|
450
|
+
}
|
|
451
|
+
onProgress?.({
|
|
452
|
+
phase: "done",
|
|
453
|
+
file: remotePath,
|
|
454
|
+
transferred: received,
|
|
455
|
+
total: received > 0 ? received : total,
|
|
456
|
+
percent: 100
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
blob: streamed ? void 0 : new Blob(chunks),
|
|
460
|
+
filename,
|
|
461
|
+
streamed,
|
|
462
|
+
bytes: received
|
|
463
|
+
};
|
|
464
|
+
} catch (error) {
|
|
465
|
+
chunks.length = 0;
|
|
466
|
+
if (!readerDone) await reader.cancel(error).catch(() => void 0);
|
|
467
|
+
if (writable !== void 0 && !writableClosed) await writable.abort?.(error).catch(() => void 0);
|
|
468
|
+
throw error;
|
|
412
469
|
}
|
|
413
|
-
if (writable !== void 0) await writable.close();
|
|
414
|
-
onProgress?.({
|
|
415
|
-
phase: "done",
|
|
416
|
-
file: remotePath,
|
|
417
|
-
transferred: received,
|
|
418
|
-
total: received > 0 ? received : total,
|
|
419
|
-
percent: 100
|
|
420
|
-
});
|
|
421
|
-
return {
|
|
422
|
-
blob: streamed ? void 0 : new Blob(chunks),
|
|
423
|
-
filename,
|
|
424
|
-
streamed,
|
|
425
|
-
bytes: received
|
|
426
|
-
};
|
|
427
470
|
}
|
|
428
471
|
/** Open a WebSocket terminal session. */
|
|
429
472
|
openTerminal(alias, cols, rows) {
|
|
@@ -433,22 +476,96 @@ window.__ModuleLoader__.load({
|
|
|
433
476
|
rows
|
|
434
477
|
});
|
|
435
478
|
const socket = new WebSocket(url);
|
|
479
|
+
const MAX_PENDING_INPUT = 65536;
|
|
480
|
+
let ready = false;
|
|
481
|
+
let finalized = false;
|
|
482
|
+
let queuedInput = "";
|
|
483
|
+
let queuedResize;
|
|
484
|
+
let readyListener;
|
|
485
|
+
let readyDelivered = false;
|
|
486
|
+
let outputListener;
|
|
487
|
+
let exitListener;
|
|
488
|
+
let exitRecord;
|
|
489
|
+
let exitDelivered = false;
|
|
490
|
+
const deliverReady = () => {
|
|
491
|
+
if (!ready || readyDelivered || readyListener === void 0) return;
|
|
492
|
+
readyDelivered = true;
|
|
493
|
+
readyListener();
|
|
494
|
+
};
|
|
495
|
+
const deliverExit = () => {
|
|
496
|
+
if (exitRecord === void 0 || exitDelivered || exitListener === void 0) return;
|
|
497
|
+
exitDelivered = true;
|
|
498
|
+
exitListener(exitRecord.code, exitRecord.error);
|
|
499
|
+
};
|
|
500
|
+
const finalize = (code, error) => {
|
|
501
|
+
if (finalized) return;
|
|
502
|
+
finalized = true;
|
|
503
|
+
queuedInput = "";
|
|
504
|
+
queuedResize = void 0;
|
|
505
|
+
exitRecord = error === void 0 ? { code } : {
|
|
506
|
+
code,
|
|
507
|
+
error
|
|
508
|
+
};
|
|
509
|
+
deliverExit();
|
|
510
|
+
};
|
|
511
|
+
const sendFrame = (frame) => {
|
|
512
|
+
if (finalized || !ready || socket.readyState !== WebSocket.OPEN) return;
|
|
513
|
+
socket.send(JSON.stringify(frame));
|
|
514
|
+
};
|
|
436
515
|
const connection = {
|
|
437
|
-
onReady
|
|
438
|
-
|
|
439
|
-
|
|
516
|
+
get onReady() {
|
|
517
|
+
return readyListener;
|
|
518
|
+
},
|
|
519
|
+
set onReady(listener) {
|
|
520
|
+
readyListener = listener;
|
|
521
|
+
deliverReady();
|
|
522
|
+
},
|
|
523
|
+
get onOutput() {
|
|
524
|
+
return outputListener;
|
|
525
|
+
},
|
|
526
|
+
set onOutput(listener) {
|
|
527
|
+
outputListener = listener;
|
|
528
|
+
},
|
|
529
|
+
get onExit() {
|
|
530
|
+
return exitListener;
|
|
531
|
+
},
|
|
532
|
+
set onExit(listener) {
|
|
533
|
+
exitListener = listener;
|
|
534
|
+
deliverExit();
|
|
535
|
+
},
|
|
440
536
|
send: (data) => {
|
|
441
|
-
if (
|
|
537
|
+
if (finalized) return;
|
|
538
|
+
if (!ready || socket.readyState !== WebSocket.OPEN) {
|
|
539
|
+
if (queuedInput.length + data.length > MAX_PENDING_INPUT) {
|
|
540
|
+
finalize(null, "terminal input buffer exceeded before ready");
|
|
541
|
+
try {
|
|
542
|
+
socket.close(1008, "terminal input buffer exceeded");
|
|
543
|
+
} catch {}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
queuedInput += data;
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
sendFrame({
|
|
442
550
|
type: "input",
|
|
443
551
|
data
|
|
444
|
-
})
|
|
552
|
+
});
|
|
445
553
|
},
|
|
446
|
-
resize: (
|
|
447
|
-
if (
|
|
554
|
+
resize: (nextCols, nextRows) => {
|
|
555
|
+
if (finalized) return;
|
|
556
|
+
const frame = {
|
|
448
557
|
type: "resize",
|
|
449
|
-
cols,
|
|
450
|
-
rows
|
|
451
|
-
}
|
|
558
|
+
cols: nextCols,
|
|
559
|
+
rows: nextRows
|
|
560
|
+
};
|
|
561
|
+
if (!ready || socket.readyState !== WebSocket.OPEN) {
|
|
562
|
+
queuedResize = {
|
|
563
|
+
cols: nextCols,
|
|
564
|
+
rows: nextRows
|
|
565
|
+
};
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
sendFrame(frame);
|
|
452
569
|
},
|
|
453
570
|
close: () => {
|
|
454
571
|
try {
|
|
@@ -457,21 +574,54 @@ window.__ModuleLoader__.load({
|
|
|
457
574
|
}
|
|
458
575
|
};
|
|
459
576
|
socket.onmessage = (event) => {
|
|
460
|
-
let frame;
|
|
461
577
|
try {
|
|
462
|
-
|
|
578
|
+
const value = JSON.parse(event.data);
|
|
579
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("invalid terminal frame");
|
|
580
|
+
const frame = value;
|
|
581
|
+
if (frame.type === "ready" && typeof frame.alias === "string") {
|
|
582
|
+
if (ready || finalized) return;
|
|
583
|
+
ready = true;
|
|
584
|
+
deliverReady();
|
|
585
|
+
if (queuedResize !== void 0) {
|
|
586
|
+
sendFrame({
|
|
587
|
+
type: "resize",
|
|
588
|
+
...queuedResize
|
|
589
|
+
});
|
|
590
|
+
queuedResize = void 0;
|
|
591
|
+
}
|
|
592
|
+
if (queuedInput !== "") {
|
|
593
|
+
sendFrame({
|
|
594
|
+
type: "input",
|
|
595
|
+
data: queuedInput
|
|
596
|
+
});
|
|
597
|
+
queuedInput = "";
|
|
598
|
+
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (frame.type === "output" && typeof frame.data === "string") {
|
|
602
|
+
outputListener?.(frame.data);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (frame.type === "exit" && (typeof frame.code === "number" || frame.code === null) && (frame.error === void 0 || typeof frame.error === "string")) {
|
|
606
|
+
finalize(frame.code, frame.error);
|
|
607
|
+
try {
|
|
608
|
+
socket.close(1e3);
|
|
609
|
+
} catch {}
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
throw new Error("invalid terminal frame");
|
|
463
613
|
} catch {
|
|
464
|
-
|
|
614
|
+
finalize(null, "invalid terminal server frame");
|
|
615
|
+
try {
|
|
616
|
+
socket.close(1008, "invalid terminal server frame");
|
|
617
|
+
} catch {}
|
|
465
618
|
}
|
|
466
|
-
if (frame.type === "ready") connection.onReady?.();
|
|
467
|
-
else if (frame.type === "output") connection.onOutput?.(frame.data);
|
|
468
|
-
else if (frame.type === "exit") connection.onExit?.(frame.code, frame.error);
|
|
469
619
|
};
|
|
470
620
|
socket.onclose = () => {
|
|
471
|
-
|
|
621
|
+
finalize(null, "connection closed");
|
|
472
622
|
};
|
|
473
623
|
socket.onerror = () => {
|
|
474
|
-
|
|
624
|
+
finalize(null, "connection error");
|
|
475
625
|
};
|
|
476
626
|
return connection;
|
|
477
627
|
}
|
|
@@ -523,7 +673,14 @@ window.__ModuleLoader__.load({
|
|
|
523
673
|
"manager.label": "SSH 工作区",
|
|
524
674
|
"manager.tooltip": "管理 SSH 工作区:绑定服务器目录,绑定后的会话在远程操作,其余会话在本机",
|
|
525
675
|
"manager.title": "SSH 工作区管理",
|
|
676
|
+
"panel.workspaces": "SSH 工作区",
|
|
677
|
+
"ops.tab": "SSH",
|
|
678
|
+
"ops.guide": "打开 SSH 运维面板:终端、传输、隧道、集群",
|
|
526
679
|
"manager.empty": "还没有 SSH 工作区",
|
|
680
|
+
"manager.connected": "已连接",
|
|
681
|
+
"manager.disconnected": "未连接",
|
|
682
|
+
"manager.connectionUnknown": "状态未知",
|
|
683
|
+
"manager.connectionError": "无法读取服务器连接状态:%s",
|
|
527
684
|
"manager.delete": "删除",
|
|
528
685
|
"manager.editServer": "编辑服务器",
|
|
529
686
|
"manager.deleteServer": "删除服务器",
|
|
@@ -567,7 +724,14 @@ window.__ModuleLoader__.load({
|
|
|
567
724
|
"manager.label": "SSH workspaces",
|
|
568
725
|
"manager.tooltip": "Manage SSH workspaces: bind a server directory — sessions in a bound workspace operate remotely, all others stay local",
|
|
569
726
|
"manager.title": "SSH Workspaces",
|
|
727
|
+
"panel.workspaces": "SSH workspaces",
|
|
728
|
+
"ops.tab": "SSH",
|
|
729
|
+
"ops.guide": "Open the SSH operations panel: terminal, transfer, tunnels, cluster",
|
|
570
730
|
"manager.empty": "No SSH workspaces yet",
|
|
731
|
+
"manager.connected": "Connected",
|
|
732
|
+
"manager.disconnected": "Disconnected",
|
|
733
|
+
"manager.connectionUnknown": "Status unknown",
|
|
734
|
+
"manager.connectionError": "Could not read server connection state: %s",
|
|
571
735
|
"manager.delete": "Delete",
|
|
572
736
|
"manager.editServer": "Edit server",
|
|
573
737
|
"manager.deleteServer": "Delete server",
|
|
@@ -632,6 +796,8 @@ window.__ModuleLoader__.load({
|
|
|
632
796
|
};
|
|
633
797
|
listeners = /* @__PURE__ */ new Set();
|
|
634
798
|
timer;
|
|
799
|
+
/** Monotonic list-workspaces request id; only the newest request may commit. */
|
|
800
|
+
refreshSequence = 0;
|
|
635
801
|
constructor(api) {
|
|
636
802
|
this.api = api;
|
|
637
803
|
}
|
|
@@ -648,9 +814,12 @@ window.__ModuleLoader__.load({
|
|
|
648
814
|
for (const listener of [...this.listeners]) listener();
|
|
649
815
|
}
|
|
650
816
|
async refresh() {
|
|
817
|
+
const request = ++this.refreshSequence;
|
|
651
818
|
try {
|
|
819
|
+
const workspaces = await this.api.listWorkspaces();
|
|
820
|
+
if (request !== this.refreshSequence) return;
|
|
652
821
|
const next = {
|
|
653
|
-
workspaces
|
|
822
|
+
workspaces,
|
|
654
823
|
error: null
|
|
655
824
|
};
|
|
656
825
|
if (!sameLedger(this.state.workspaces, next.workspaces) || this.state.error !== null) {
|
|
@@ -658,6 +827,7 @@ window.__ModuleLoader__.load({
|
|
|
658
827
|
this.emit();
|
|
659
828
|
}
|
|
660
829
|
} catch (error) {
|
|
830
|
+
if (request !== this.refreshSequence) return;
|
|
661
831
|
const message = error instanceof Error ? error.message : String(error);
|
|
662
832
|
if (this.state.error !== message) {
|
|
663
833
|
this.state = {
|
|
@@ -673,6 +843,7 @@ window.__ModuleLoader__.load({
|
|
|
673
843
|
this.timer = window.setInterval(() => void this.refresh(), POLL_MS);
|
|
674
844
|
}
|
|
675
845
|
stop() {
|
|
846
|
+
this.refreshSequence += 1;
|
|
676
847
|
if (this.timer !== void 0) {
|
|
677
848
|
window.clearInterval(this.timer);
|
|
678
849
|
this.timer = void 0;
|
|
@@ -703,7 +874,7 @@ window.__ModuleLoader__.load({
|
|
|
703
874
|
}
|
|
704
875
|
//#endregion
|
|
705
876
|
//#region \0dsh-css:C:\Users\Kether\.dsh\dsh-hardssh\packages\dsh-hardssh\src\client\workspace.module.css.mjs
|
|
706
|
-
const css$1 = ".-bXcdG_dialogGrid{grid-template-columns:1fr;gap:10px;display:grid}.-bXcdG_field{flex-direction:column;gap:5px;display:flex}.-bXcdG_field>span{color:var(--dsw-alias-label-primary,#0f1115);font-size:13px;font-weight:500}.-bXcdG_field input,.-bXcdG_field select{box-sizing:border-box;min-height:36px;color:var(--dsw-alias-label-primary,#0f1115);background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#00000024);border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px;transition:border-color .15s,box-shadow .15s}.-bXcdG_field select option{background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#0f1115)}.-bXcdG_field input::placeholder,.-bXcdG_field select::placeholder{color:var(--dsw-alias-label-tertiary,#81858c)}.-bXcdG_field input:focus,.-bXcdG_field select:focus{border-color:var(--dsw-alias-brand-primary-new-colorprimary-new-color,#4176e6);box-shadow:0 0 0 2px #4176e626}.-bXcdG_dialogFail{background:var(--dsw-alias-state-error-bg,#f53f3f1a);border:1px solid var(--dsw-alias-state-error-primary,#f53f3f66);color:var(--dsw-alias-state-error-primary,#d03050);border-radius:6px;margin-top:10px;padding:8px 12px;font-size:12px}.-bXcdG_dialogInfo{background:var(--dsw-alias-bg-base-2,#0000000a);border:1px solid var(--dsw-alias-border-l2,#0000001a);color
|
|
877
|
+
const css$1 = ".-bXcdG_dialogGrid{grid-template-columns:1fr;gap:10px;display:grid}.-bXcdG_field{flex-direction:column;gap:5px;display:flex}.-bXcdG_field>span{color:var(--dsw-alias-label-primary,#0f1115);font-size:13px;font-weight:500}.-bXcdG_field input,.-bXcdG_field select{box-sizing:border-box;min-height:36px;color:var(--dsw-alias-label-primary,#0f1115);background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#00000024);border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px;transition:border-color .15s,box-shadow .15s}.-bXcdG_field select option{background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#0f1115)}.-bXcdG_field input::placeholder,.-bXcdG_field select::placeholder{color:var(--dsw-alias-label-tertiary,#81858c)}.-bXcdG_field input:focus,.-bXcdG_field select:focus{border-color:var(--dsw-alias-brand-primary-new-colorprimary-new-color,#4176e6);box-shadow:0 0 0 2px #4176e626}.-bXcdG_dialogFail{background:var(--dsw-alias-state-error-bg,#f53f3f1a);border:1px solid var(--dsw-alias-state-error-primary,#f53f3f66);color:var(--dsw-alias-state-error-primary,#d03050);border-radius:6px;margin-top:10px;padding:8px 12px;font-size:12px}.-bXcdG_dialogInfo{background:var(--dsw-alias-bg-base-2,#0000000a);border:1px solid var(--dsw-alias-border-l2,#0000001a);color:var(--dsw-alias-label-primary,#0f1115);border-radius:6px;margin-top:10px;padding:8px 12px;font-size:12px}.-bXcdG_dialogActions{justify-content:flex-end;gap:8px;margin-top:14px;display:flex}.-bXcdG_button{border:1px solid var(--dsw-alias-border-l2,#00000026);color:var(--dsw-alias-label-primary,#0f1115);cursor:pointer;background:0 0;border-radius:8px;padding:7px 15px;font-family:inherit;font-size:13px;transition:background .15s}.-bXcdG_button:hover:not(:disabled){background:var(--dsw-alias-bg-overlay,#0000000d)}.-bXcdG_button:disabled{opacity:.55;cursor:default}.-bXcdG_buttonPrimary{background:var(--dsw-alias-button-primary-fill,#0f1115);color:var(--dsw-alias-label-primary-foreground,#fff);border-color:#0000;font-weight:600}.-bXcdG_buttonPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,#2b2f36);color:var(--dsw-alias-label-primary-foreground,#fff)}.-bXcdG_dialogHint{color:var(--dsw-alias-label-tertiary,#81858c);margin-top:10px;font-size:11.5px;line-height:1.45}.-bXcdG_hostRow{border:1px solid var(--dsw-alias-border-l2,#0000001a);background:var(--dsw-alias-bg-base,#00000005);color:var(--dsw-alias-label-primary,#0f1115);border-radius:8px;align-items:center;gap:8px;padding:7px 10px;font-size:13px;display:flex}.-bXcdG_hostRow:hover{background:var(--dsw-alias-bg-overlay,#0000000d)}.-bXcdG_hostAlias{color:var(--dsw-alias-label-primary,#0f1115);white-space:nowrap;font-size:13px;font-weight:500}.-bXcdG_hostMeta{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-tertiary,#81858c);flex:1;font-size:12px;overflow:hidden}.-bXcdG_danger{background:var(--dsw-alias-state-error-primary,#d03050);color:#fff;border-color:#0000}.-bXcdG_managerButton{border:1px solid var(--dsw-alias-border-l2,#0003);min-width:111px;height:32px;color:var(--dsw-alias-label-primary,#0f1115);font-family:var(--dsw-font-family,-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, system-ui, \"PingFang SC\", \"Microsoft YaHei\", sans-serif);cursor:pointer;white-space:nowrap;background:0 0;border-radius:12px;justify-content:center;align-items:center;gap:4px;padding:6px 12px;font-size:13px;font-weight:400;line-height:20px;transition:background .15s,border-color .15s;display:inline-flex}.-bXcdG_managerButton:hover{background:var(--dsw-alias-bg-overlay,#0000000d)}.-bXcdG_managerButton svg{flex:none}.-bXcdG_managerMenuHeader{color:var(--dsw-alias-label-primary,#0f1115);margin:0 0 2px;font-size:13px;font-weight:600}.-bXcdG_managerMenuClose{float:right;color:var(--dsw-alias-label-secondary,#61666b);cursor:pointer;background:0 0;border:none;padding:0 2px;font-size:14px;line-height:18px}.-bXcdG_managerMenuClose:hover{color:var(--dsw-alias-label-primary,#0f1115)}.-bXcdG_menuDropdown{z-index:99999;box-sizing:border-box;background:var(--dsw-alias-bg-base,#fff);border:1px solid var(--dsw-alias-border-l1,#0000000a);min-width:230px;max-width:calc(100vw - 16px);max-height:min(600px,100vh - 64px);color:var(--dsw-alias-label-primary,#0f1115);font-family:var(--dsw-font-family,-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, system-ui, \"PingFang SC\", \"Microsoft YaHei\", sans-serif);scrollbar-width:thin;scrollbar-color:var(--dsw-alias-label-caption,#80808080) transparent;border-radius:12px;padding:8px;font-size:13px;line-height:20px;position:fixed;overflow:hidden auto}.-bXcdG_menuItem{width:100%;color:var(--dsw-alias-label-primary,#0f1115);font:inherit;text-align:left;cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:9px;padding:8px 11px;transition:background .12s,color .12s;display:flex}.-bXcdG_menuItem:hover:not(:disabled){background:var(--dsw-alias-bg-overlay,#0000000d)}.-bXcdG_menuItem:disabled{opacity:.55;cursor:default}.-bXcdG_menuItemIcon{flex:none;font-size:14px;line-height:1}.-bXcdG_menuDivider{background:var(--dsw-alias-border-l2,#0000001a);height:1px;margin:5px 9px}.-bXcdG_managerGroupHeader{border-bottom:1px solid var(--dsw-alias-border-l2,#0000001a);margin-bottom:5px}.-bXcdG_connectionBadge{color:var(--dsw-alias-label-secondary,#61666b);white-space:nowrap;border:1px solid var(--dsw-alias-border-l2,#0000001f);border-radius:999px;flex:none;align-items:center;gap:5px;padding:1px 7px;font-size:11px;line-height:18px;display:inline-flex}.-bXcdG_connectionDot{background:var(--dsw-alias-label-tertiary,#81858c);border-radius:50%;width:7px;height:7px}.-bXcdG_connectionBadge[data-state=connected]{color:var(--dsw-alias-state-success-primary,#18a058);border-color:color-mix(in srgb, var(--dsw-alias-state-success-primary,#18a058) 48%, transparent)}.-bXcdG_connectionBadge[data-state=connected] .-bXcdG_connectionDot{background:var(--dsw-alias-state-success-primary,#18a058)}.-bXcdG_connectionBadge[data-state=disconnected]{color:var(--dsw-alias-label-tertiary,#81858c)}.-bXcdG_menuItemIconButton{border:1px solid var(--dsw-alias-border-l2,#00000026);width:22px;height:22px;color:var(--dsw-alias-label-secondary,#4a5060);cursor:pointer;background:0 0;border-radius:6px;flex:none;justify-content:center;align-items:center;font-size:13px;line-height:1;transition:background .12s,color .12s,border-color .12s;display:inline-flex}.-bXcdG_menuItemIconButton:hover:not(:disabled){background:var(--dsw-alias-bg-overlay,#0000000f);border-color:var(--dsw-alias-border-l3,#0000001f);color:var(--dsw-alias-label-primary,#0f1115)}.-bXcdG_menuItemIconButton:disabled{opacity:.4;cursor:default}.-bXcdG_managerPanel{box-sizing:border-box;width:100%;height:100%;min-height:0;color:var(--dsw-alias-label-primary,#0f1115);font-family:var(--dsw-font-family,-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, system-ui, \"PingFang SC\", \"Microsoft YaHei\", sans-serif);scrollbar-width:thin;scrollbar-color:var(--dsw-alias-label-caption,#80808080) transparent;padding:16px 20px 24px;font-size:13px;line-height:20px;overflow-y:auto}.-bXcdG_managerPanelHeader{align-items:center;gap:10px;margin-bottom:12px;display:flex}.-bXcdG_managerPanelTitle{white-space:nowrap;flex:1;margin:0;font-size:16px;font-weight:700}.-bXcdG_managerPanelGroup{margin-bottom:14px}.-bXcdG_managerPanelFooter{border-top:1px solid var(--dsw-alias-border-l2,#0000001a);margin-top:8px;padding-top:8px}";
|
|
707
878
|
const tagId$1 = "@tiphareth/dsh-hardssh/workspace.module.css";
|
|
708
879
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
709
880
|
const tag = document.createElement("style");
|
|
@@ -715,6 +886,8 @@ window.__ModuleLoader__.load({
|
|
|
715
886
|
var workspace_module_css_default = {
|
|
716
887
|
"button": "-bXcdG_button",
|
|
717
888
|
"buttonPrimary": "-bXcdG_buttonPrimary",
|
|
889
|
+
"connectionBadge": "-bXcdG_connectionBadge",
|
|
890
|
+
"connectionDot": "-bXcdG_connectionDot",
|
|
718
891
|
"danger": "-bXcdG_danger",
|
|
719
892
|
"dialogActions": "-bXcdG_dialogActions",
|
|
720
893
|
"dialogFail": "-bXcdG_dialogFail",
|
|
@@ -729,6 +902,11 @@ window.__ModuleLoader__.load({
|
|
|
729
902
|
"managerGroupHeader": "-bXcdG_managerGroupHeader",
|
|
730
903
|
"managerMenuClose": "-bXcdG_managerMenuClose",
|
|
731
904
|
"managerMenuHeader": "-bXcdG_managerMenuHeader",
|
|
905
|
+
"managerPanel": "-bXcdG_managerPanel",
|
|
906
|
+
"managerPanelFooter": "-bXcdG_managerPanelFooter",
|
|
907
|
+
"managerPanelGroup": "-bXcdG_managerPanelGroup",
|
|
908
|
+
"managerPanelHeader": "-bXcdG_managerPanelHeader",
|
|
909
|
+
"managerPanelTitle": "-bXcdG_managerPanelTitle",
|
|
732
910
|
"menuDivider": "-bXcdG_menuDivider",
|
|
733
911
|
"menuDropdown": "-bXcdG_menuDropdown",
|
|
734
912
|
"menuItem": "-bXcdG_menuItem",
|
|
@@ -779,6 +957,13 @@ window.__ModuleLoader__.load({
|
|
|
779
957
|
"tab.transfer": "传输",
|
|
780
958
|
"tab.tunnels": "隧道",
|
|
781
959
|
"tab.cluster": "集群",
|
|
960
|
+
"session.server": "当前服务器",
|
|
961
|
+
"session.target": "跟随当前会话:{alias} · {remoteRoot}",
|
|
962
|
+
"session.localTitle": "当前会话位于本地工作区",
|
|
963
|
+
"session.localUnavailable": "SSH 操作台仅适用于 SSH 工作区会话。请切换到远程会话后使用。",
|
|
964
|
+
"connectionError.title": "无法连接服务器 {alias}",
|
|
965
|
+
"connectionError.intro": "SSH 连接未建立,请检查服务器地址、网络和认证配置。",
|
|
966
|
+
"connectionError.close": "知道了",
|
|
782
967
|
"hosts.empty": "尚未配置主机。点击「新增主机」添加,或从 ~/.ssh/config 导入。",
|
|
783
968
|
"hosts.add": "新增主机",
|
|
784
969
|
"hosts.import": "导入 ~/.ssh/config",
|
|
@@ -833,7 +1018,7 @@ window.__ModuleLoader__.load({
|
|
|
833
1018
|
"terminal.selectHost": "选择主机",
|
|
834
1019
|
"terminal.connect": "连接",
|
|
835
1020
|
"terminal.disconnect": "断开",
|
|
836
|
-
"terminal.placeholder": "
|
|
1021
|
+
"terminal.placeholder": "点击「连接」打开当前会话服务器的远程终端。",
|
|
837
1022
|
"terminal.connecting": "正在连接…",
|
|
838
1023
|
"terminal.ready": "终端已连接({alias})",
|
|
839
1024
|
"terminal.exited": "终端已退出({alias})",
|
|
@@ -926,6 +1111,13 @@ window.__ModuleLoader__.load({
|
|
|
926
1111
|
"tab.transfer": "Transfer",
|
|
927
1112
|
"tab.tunnels": "Tunnels",
|
|
928
1113
|
"tab.cluster": "Cluster",
|
|
1114
|
+
"session.server": "Current server",
|
|
1115
|
+
"session.target": "Following current session: {alias} · {remoteRoot}",
|
|
1116
|
+
"session.localTitle": "This session uses a local workspace",
|
|
1117
|
+
"session.localUnavailable": "The SSH console is available only for SSH workspace sessions. Switch to a remote session to use it.",
|
|
1118
|
+
"connectionError.title": "Unable to connect to {alias}",
|
|
1119
|
+
"connectionError.intro": "The SSH connection was not established. Check the server address, network, and authentication settings.",
|
|
1120
|
+
"connectionError.close": "OK",
|
|
929
1121
|
"hosts.empty": "No hosts configured. Click \"Add host\" or import from ~/.ssh/config.",
|
|
930
1122
|
"hosts.add": "Add host",
|
|
931
1123
|
"hosts.import": "Import ~/.ssh/config",
|
|
@@ -980,7 +1172,7 @@ window.__ModuleLoader__.load({
|
|
|
980
1172
|
"terminal.selectHost": "Select host",
|
|
981
1173
|
"terminal.connect": "Connect",
|
|
982
1174
|
"terminal.disconnect": "Disconnect",
|
|
983
|
-
"terminal.placeholder": "
|
|
1175
|
+
"terminal.placeholder": "Click \"Connect\" to open a terminal on the current session server.",
|
|
984
1176
|
"terminal.connecting": "Connecting…",
|
|
985
1177
|
"terminal.ready": "Terminal connected ({alias})",
|
|
986
1178
|
"terminal.exited": "Terminal exited ({alias})",
|
|
@@ -1092,7 +1284,7 @@ window.__ModuleLoader__.load({
|
|
|
1092
1284
|
}
|
|
1093
1285
|
//#endregion
|
|
1094
1286
|
//#region \0dsh-css:C:\Users\Kether\.dsh\dsh-hardssh\packages\dsh-hardssh\src\client\ssh\panel\panel.module.css.mjs
|
|
1095
|
-
const css = "[data-pane=conversation]{position:relative}[data-dsh-ssh-view]{z-index:5;display:none;position:absolute;inset:0}html[data-dsh-ssh-active] [data-dsh-ssh-view]{display:block}html[data-dsh-ssh-active] [data-pane=conversation]>:not([data-dsh-ssh-view]){display:none}.GAuiuG_entry{width:100%;height:32px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 12px;font-size:13px;display:flex}.GAuiuG_entry:hover{background:var(--dsw-specific-sidebar-nav-item-hover);color:var(--dsw-alias-label-primary)}.GAuiuG_entry[data-active]{background:var(--dsw-specific-sidebar-nav-item-active);color:var(--dsw-alias-label-primary);font-weight:600}.GAuiuG_entryIcon{flex:none;justify-content:center;align-items:center;display:inline-flex}.GAuiuG_entryLabel{text-overflow:ellipsis;overflow:hidden}[data-dsh-frame][data-sidebar-collapsed] .GAuiuG_entry{justify-content:center;width:100%;padding:0}[data-dsh-frame][data-sidebar-collapsed] .GAuiuG_entryLabel{display:none}.GAuiuG_view{overflow:hidden}.GAuiuG_panel{background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;padding:14px 16px 16px;display:flex}.GAuiuG_panelHeader{flex:none;align-items:center;gap:10px;display:flex}.GAuiuG_panelTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;flex:1;margin:0;font-size:16px;font-weight:700}.GAuiuG_tabBar{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}.GAuiuG_tab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-bottom:2px solid #0000;border-radius:6px 6px 0 0;padding:7px 14px;font-size:13px}.GAuiuG_tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.GAuiuG_tab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}.GAuiuG_panelContent{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.GAuiuG_tabBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow:hidden auto}.GAuiuG_fillBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow:hidden}.GAuiuG_toolbar,.GAuiuG_controls{flex-wrap:wrap;flex:none;align-items:center;gap:8px;display:flex}.GAuiuG_controls .GAuiuG_input{flex:0 260px;min-width:150px}.GAuiuG_toolbarSpacer{flex:1}.GAuiuG_search{min-width:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:0 260px;padding:6px 10px;font-size:13px}.GAuiuG_search::placeholder{color:var(--dsw-alias-label-tertiary)}.GAuiuG_tableWrap{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex:1;min-height:0;overflow:auto}.GAuiuG_table{border-collapse:collapse;width:100%;font-size:12.5px}.GAuiuG_table th{z-index:1;text-align:left;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1);white-space:nowrap;padding:8px 10px;font-weight:600;position:sticky;top:0}.GAuiuG_table td{border-bottom:1px solid var(--dsw-alias-separator-primary);vertical-align:top;padding:7px 10px}.GAuiuG_table tbody tr:last-child td{border-bottom:none}.GAuiuG_table tbody tr:hover td{background:var(--dsw-alias-interactive-bg-hover)}.GAuiuG_mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.GAuiuG_cellMuted{color:var(--dsw-alias-label-tertiary)}.GAuiuG_actions{white-space:nowrap;align-items:center;gap:8px;display:flex}.GAuiuG_inlineTest{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:11.5px;display:inline-flex}.GAuiuG_inlineTest[data-status=ok]{color:var(--dsw-alias-state-success-primary)}.GAuiuG_inlineTest[data-status=fail]{color:var(--dsw-alias-state-error-primary)}.GAuiuG_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;line-height:1.6;display:inline-block}.GAuiuG_badge[data-kind=key]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.GAuiuG_badge[data-kind=password]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.GAuiuG_badge[data-status=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.GAuiuG_badge[data-status=fail]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.GAuiuG_badge[data-status=timeout]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.GAuiuG_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:600}.GAuiuG_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.GAuiuG_primaryButton:disabled{opacity:.5;cursor:default}.GAuiuG_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}.GAuiuG_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.GAuiuG_ghostButton:disabled{opacity:.45;cursor:default}.GAuiuG_iconButton{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:center;align-items:center;padding:0;font-size:13px;display:inline-flex}.GAuiuG_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.GAuiuG_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}.GAuiuG_linkButton:hover:not(:disabled){text-decoration:underline}.GAuiuG_linkButton:disabled{opacity:.45;cursor:default}.GAuiuG_linkButton[data-danger]{color:var(--dsw-alias-state-error-primary)}.GAuiuG_spinner{border:2px solid var(--dsw-alias-state-business-primary);vertical-align:-1px;border-top-color:#0000;border-radius:50%;flex:none;width:11px;height:11px;animation:.8s linear infinite GAuiuG_dshSshSpin;display:inline-block}@keyframes GAuiuG_dshSshSpin{to{transform:rotate(360deg)}}.GAuiuG_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.5}.GAuiuG_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.GAuiuG_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.GAuiuG_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.GAuiuG_empty,.GAuiuG_loading{text-align:center;color:var(--dsw-alias-label-tertiary);padding:28px 12px;font-size:12.5px}.GAuiuG_modalBackdrop{z-index:40;background:var(--dsw-alias-bg-mask-1,#00000073);justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}.GAuiuG_modal{box-sizing:border-box;background:var(--dsw-alias-bg-base,#fff);border:1px solid var(--dsw-alias-border-l1,#0000000a);color:#000;width:min(560px,100vw - 48px);min-width:0;min-height:0;max-height:calc(100vh - 48px);font-family:var(--dsw-font-family,-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, system-ui, \"PingFang SC\", \"Microsoft YaHei\", sans-serif);border-radius:12px;flex-direction:column;display:flex;position:relative;overflow:hidden}.GAuiuG_modalHeader{box-sizing:border-box;flex:none;align-items:center;gap:10px;min-width:0;padding:14px 18px 10px;display:flex}.GAuiuG_modalTitle{color:#000;flex:1;align-items:center;gap:8px;margin:0;font-size:15px;font-weight:600;display:flex}.GAuiuG_whaleMark{background:radial-gradient(circle at 32% 30%,#ffffffb3 0 12%,#0000 13%),radial-gradient(circle at 50% 60%,#4d6bfe 0 42%,#0000 43%);border-radius:50% 50% 38% 38%;flex:none;width:14px;height:14px;box-shadow:0 0 0 1px #4d6bfe59}.GAuiuG_modalBody{overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-label-caption,#80808080) transparent;flex-direction:column;flex:auto;gap:12px;min-width:0;min-height:0;padding:4px 18px 8px;display:flex;overflow:hidden auto}.GAuiuG_modalBody::-webkit-scrollbar{width:6px}.GAuiuG_modalBody::-webkit-scrollbar-thumb{background:var(--dsw-alias-label-caption,#80808080);border-radius:6px}.GAuiuG_modalBody::-webkit-scrollbar-track{background:0 0}.GAuiuG_modalFooter{box-sizing:border-box;border-top:1px solid var(--dsw-alias-border-l2,#00000014);flex:none;justify-content:flex-end;align-items:center;gap:10px;min-width:0;padding:12px 18px 14px;display:flex}.GAuiuG_modalPrimary{background:var(--dsw-alias-button-primary-fill,#0f1115);color:var(--dsw-alias-label-primary-foreground,#fff);cursor:pointer;border:none;border-radius:8px;padding:7px 18px;font-family:inherit;font-size:13px;font-weight:600;transition:background .15s}.GAuiuG_modalPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,#2b2f36)}.GAuiuG_modalPrimary:disabled{opacity:.5;cursor:default}.GAuiuG_modalGhost{border:1px solid var(--dsw-alias-border-l2,#00000026);color:var(--dsw-alias-label-primary,#0f1115);cursor:pointer;background:0 0;border-radius:8px;padding:7px 15px;font-family:inherit;font-size:13px;transition:background .15s}.GAuiuG_modalGhost:hover:not(:disabled){background:var(--dsw-alias-bg-overlay,#0000000d)}.GAuiuG_modalGhost:disabled{opacity:.5;cursor:default}.GAuiuG_formError{background:var(--dsw-alias-state-error-bg,#f53f3f1a);border:1px solid var(--dsw-alias-state-error-primary,#f53f3f66);color:var(--dsw-alias-state-error-primary,#d03050);border-radius:6px;margin:0 18px 2px;padding:8px 12px;font-size:12px}.GAuiuG_hint{color:var(--dsw-alias-label-tertiary,#81858c);font-size:11.5px;line-height:1.45}.GAuiuG_field{flex-direction:column;gap:5px;min-width:0;display:flex}.GAuiuG_fieldLabel{color:var(--dsw-alias-label-primary,#0f1115);font-size:13px;font-weight:500}.GAuiuG_input{box-sizing:border-box;width:100%;min-width:0;min-height:36px;color:var(--dsw-alias-label-primary,#0f1115);background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#00000024);border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px;transition:border-color .15s,box-shadow .15s}.GAuiuG_input:focus{border-color:var(--dsw-alias-brand-primary-new-colorprimary-new-color,#4176e6);box-shadow:0 0 0 2px #4176e626}.GAuiuG_input::placeholder{color:var(--dsw-alias-label-tertiary,#81858c)}.GAuiuG_input:disabled{opacity:.5}.GAuiuG_formRow{grid-template-columns:minmax(0,1.25fr) minmax(0,1fr);gap:12px;min-width:0;display:grid}.GAuiuG_radioRow{align-items:center;gap:18px;display:flex}.GAuiuG_radioLabel{color:#000;cursor:pointer;align-items:center;gap:7px;font-size:13px;display:inline-flex}.GAuiuG_radioLabel input[type=radio]{accent-color:var(--dsw-alias-brand-primary-new-colorprimary-new-color,#4176e6)}.GAuiuG_termBody{flex-direction:column;flex:1;gap:8px;min-height:0;display:flex;overflow:hidden}.GAuiuG_termWrap{border:1px solid var(--dsw-alias-border-l1);background:#0b0e14;border-radius:10px;flex:1;min-height:0;position:relative;overflow:hidden}.GAuiuG_termContainer{padding:8px 10px;position:absolute;inset:0}.GAuiuG_termPlaceholder{z-index:2;text-align:center;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-base);justify-content:center;align-items:center;padding:0 24px;font-size:12.5px;display:flex;position:absolute;inset:0}.GAuiuG_hiddenFile{display:none}.GAuiuG_browsePanel{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;display:flex;overflow:hidden}.GAuiuG_browseHeader{background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;gap:8px;padding:8px 10px;display:flex}.GAuiuG_browsePath{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;overflow:hidden}.GAuiuG_browseList{max-height:240px;overflow:hidden auto}.GAuiuG_dirRow{text-align:left;width:100%;min-width:0;font:inherit;color:var(--dsw-alias-label-primary);border:none;border-bottom:1px solid var(--dsw-alias-separator-primary);cursor:pointer;background:0 0;grid-template-columns:minmax(0,1fr) auto auto;gap:12px;padding:5px 10px;font-size:12.5px;display:grid}.GAuiuG_dirRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.GAuiuG_dirRow[data-type=dir] .GAuiuG_dirName{color:var(--dsw-alias-state-business-primary)}.GAuiuG_dirRow[data-up]{color:var(--dsw-alias-label-secondary)}.GAuiuG_dirName{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}.GAuiuG_dirType{color:var(--dsw-alias-label-tertiary);white-space:nowrap}.GAuiuG_dirSize{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-align:right}.GAuiuG_transferBlock{flex-direction:column;flex:none;gap:6px;display:flex}.GAuiuG_progressMeta{min-width:0;color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.GAuiuG_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}.GAuiuG_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}.GAuiuG_tunnelList{flex-direction:column;flex:none;gap:8px;display:flex}.GAuiuG_tunnelRow{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;align-items:center;gap:10px;padding:8px 12px;display:flex}.GAuiuG_tunnelRow[data-state=forwarding]{border-color:var(--dsw-alias-state-success-primary)}.GAuiuG_tunnelRow[data-state=connecting]{border-color:var(--dsw-alias-state-warn-primary)}.GAuiuG_tunnelRow[data-state=failed]{border-color:var(--dsw-alias-state-error-primary)}.GAuiuG_tunnelLabel{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;overflow:hidden}.GAuiuG_formCard{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}.GAuiuG_clusterForm{flex-direction:column;flex:none;gap:8px;display:flex}.GAuiuG_clusterFilters{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;display:grid}.GAuiuG_commandInput{min-height:96px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.GAuiuG_cellDetails summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px}.GAuiuG_cellPre{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;border-radius:6px;max-height:180px;margin:4px 0 0;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow:auto}@media (max-width:420px){.GAuiuG_formRow{grid-template-columns:1fr}}.GAuiuG_fingerprintIntro{color:#000;margin:0 0 10px;font-size:13px;line-height:1.5}.GAuiuG_fingerprintCode{border:1px solid var(--dsw-alias-border-l2,#0000001f);background:var(--dsw-specific-input-major,#f2f3f5);color:#1f2229;word-break:break-all;-webkit-user-select:text;user-select:text;border-radius:6px;padding:10px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;display:block}.GAuiuG_fingerprintWarn{color:var(--dsw-alias-state-error-primary,#d03050);margin:10px 0 0;font-size:12px;line-height:1.5}";
|
|
1287
|
+
const css = ".GAuiuG_panel{background:var(--dsw-alias-bg-base);min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex-direction:column;gap:10px;padding:14px 16px 16px;display:flex;position:relative}.GAuiuG_tabBar{border-bottom:1px solid var(--dsw-alias-border-l1);flex:none;gap:2px;display:flex}.GAuiuG_tab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-bottom:2px solid #0000;border-radius:6px 6px 0 0;padding:7px 14px;font-size:13px}.GAuiuG_tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.GAuiuG_tab[data-active]{color:var(--dsw-alias-label-primary);border-bottom-color:var(--dsw-alias-state-business-primary);font-weight:600}.GAuiuG_panelContent{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.GAuiuG_sessionTarget{color:var(--dsw-alias-label-secondary);text-overflow:ellipsis;white-space:nowrap;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:7px;flex:none;padding:5px 9px;font-size:12px;line-height:18px;overflow:hidden}.GAuiuG_targetBadge{min-height:30px;color:var(--dsw-alias-state-business-primary);white-space:nowrap;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-state-business-primary);border-radius:8px;align-items:center;padding:4px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;display:inline-flex}.GAuiuG_disabledSurface{opacity:.4;filter:blur(3px);pointer-events:none;-webkit-user-select:none;user-select:none;flex-direction:column;flex:1;gap:10px;min-height:0;display:flex}.GAuiuG_disabledPlaceholder{background:linear-gradient(var(--dsw-alias-bg-layer-2), var(--dsw-alias-bg-layer-2)) 0 0 / 100% 34px no-repeat, repeating-linear-gradient(180deg, transparent 0 34px, var(--dsw-alias-separator-primary) 34px 35px);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex:1;min-height:180px}.GAuiuG_localSessionMask{z-index:2;color:var(--dsw-alias-label-primary);text-align:center;background:color-mix(in srgb, var(--dsw-alias-bg-base) 72%, transparent);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);flex-direction:column;justify-content:center;align-items:center;gap:8px;padding:32px;display:flex;position:absolute;inset:0}.GAuiuG_localSessionMask strong{font-size:15px}.GAuiuG_localSessionMask span{max-width:340px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.55}.GAuiuG_tabBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow:hidden auto}.GAuiuG_fillBody{flex-direction:column;flex:1;gap:10px;min-height:0;display:flex;overflow:hidden}.GAuiuG_toolbar,.GAuiuG_controls{flex-wrap:wrap;flex:none;align-items:center;gap:8px;display:flex}.GAuiuG_controls .GAuiuG_input{flex:0 260px;min-width:150px}.GAuiuG_toolbarSpacer{flex:1}.GAuiuG_search{min-width:120px;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;outline:none;flex:0 260px;padding:6px 10px;font-size:13px}.GAuiuG_search::placeholder{color:var(--dsw-alias-label-tertiary)}.GAuiuG_tableWrap{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex:1;min-height:0;overflow:auto}.GAuiuG_table{border-collapse:collapse;width:100%;font-size:12.5px}.GAuiuG_table th{z-index:1;text-align:left;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1);white-space:nowrap;padding:8px 10px;font-weight:600;position:sticky;top:0}.GAuiuG_table td{border-bottom:1px solid var(--dsw-alias-separator-primary);vertical-align:top;padding:7px 10px}.GAuiuG_table tbody tr:last-child td{border-bottom:none}.GAuiuG_table tbody tr:hover td{background:var(--dsw-alias-interactive-bg-hover)}.GAuiuG_mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.GAuiuG_cellMuted{color:var(--dsw-alias-label-tertiary)}.GAuiuG_actions{white-space:nowrap;align-items:center;gap:8px;display:flex}.GAuiuG_inlineTest{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:11.5px;display:inline-flex}.GAuiuG_inlineTest[data-status=ok]{color:var(--dsw-alias-state-success-primary)}.GAuiuG_inlineTest[data-status=fail]{color:var(--dsw-alias-state-error-primary)}.GAuiuG_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;line-height:1.6;display:inline-block}.GAuiuG_badge[data-kind=key]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.GAuiuG_badge[data-kind=password]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.GAuiuG_badge[data-status=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.GAuiuG_badge[data-status=fail]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.GAuiuG_badge[data-status=timeout]{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.GAuiuG_primaryButton{color:var(--dsw-alias-label-primary-foreground);background:var(--dsw-alias-button-info-fill);cursor:pointer;white-space:nowrap;border:none;border-radius:8px;padding:6px 14px;font-size:13px;font-weight:600}.GAuiuG_primaryButton:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.GAuiuG_primaryButton:disabled{opacity:.5;cursor:default}.GAuiuG_ghostButton{color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;white-space:nowrap;background:0 0;border-radius:8px;padding:5px 12px;font-size:12px}.GAuiuG_ghostButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.GAuiuG_ghostButton:disabled{opacity:.45;cursor:default}.GAuiuG_linkButton{color:var(--dsw-alias-state-business-primary);cursor:pointer;white-space:nowrap;background:0 0;border:none;padding:0;font-size:12px}.GAuiuG_linkButton:hover:not(:disabled){text-decoration:underline}.GAuiuG_linkButton:disabled{opacity:.45;cursor:default}.GAuiuG_linkButton[data-danger]{color:var(--dsw-alias-state-error-primary)}.GAuiuG_spinner{border:2px solid var(--dsw-alias-state-business-primary);vertical-align:-1px;border-top-color:#0000;border-radius:50%;flex:none;width:11px;height:11px;animation:.8s linear infinite GAuiuG_dshSshSpin;display:inline-block}@keyframes GAuiuG_dshSshSpin{to{transform:rotate(360deg)}}.GAuiuG_banner{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere;border-radius:8px;padding:8px 12px;font-size:12.5px;line-height:1.5}.GAuiuG_banner[data-kind=ok]{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.GAuiuG_banner[data-kind=error]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.GAuiuG_banner[data-kind=info]{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.GAuiuG_empty,.GAuiuG_loading{text-align:center;color:var(--dsw-alias-label-tertiary);padding:28px 12px;font-size:12.5px}.GAuiuG_modalBackdrop{z-index:40;background:var(--dsw-alias-bg-mask-1,#00000073);justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}.GAuiuG_modal{box-sizing:border-box;background:var(--dsw-alias-bg-base,#fff);border:1px solid var(--dsw-alias-border-l1,#0000000a);width:min(560px,100vw - 48px);min-width:0;min-height:0;max-height:calc(100vh - 48px);color:var(--dsw-alias-label-primary,#0f1115);font-family:var(--dsw-font-family,-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, system-ui, \"PingFang SC\", \"Microsoft YaHei\", sans-serif);border-radius:12px;flex-direction:column;display:flex;position:relative;overflow:hidden}.GAuiuG_modalHeader{box-sizing:border-box;flex:none;align-items:center;gap:10px;min-width:0;padding:14px 18px 10px;display:flex}.GAuiuG_modalTitle{color:var(--dsw-alias-label-primary,#0f1115);flex:1;align-items:center;gap:8px;margin:0;font-size:15px;font-weight:600;display:flex}.GAuiuG_whaleMark{background:radial-gradient(circle at 32% 30%,#ffffffb3 0 12%,#0000 13%),radial-gradient(circle at 50% 60%,#4d6bfe 0 42%,#0000 43%);border-radius:50% 50% 38% 38%;flex:none;width:14px;height:14px;box-shadow:0 0 0 1px #4d6bfe59}.GAuiuG_modalBody{overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-label-caption,#80808080) transparent;flex-direction:column;flex:auto;gap:12px;min-width:0;min-height:0;padding:4px 18px 8px;display:flex;overflow:hidden auto}.GAuiuG_modalBody::-webkit-scrollbar{width:6px}.GAuiuG_modalBody::-webkit-scrollbar-thumb{background:var(--dsw-alias-label-caption,#80808080);border-radius:6px}.GAuiuG_modalBody::-webkit-scrollbar-track{background:0 0}.GAuiuG_modalFooter{box-sizing:border-box;border-top:1px solid var(--dsw-alias-border-l2,#00000014);flex:none;justify-content:flex-end;align-items:center;gap:10px;min-width:0;padding:12px 18px 14px;display:flex}.GAuiuG_modalPrimary{background:var(--dsw-alias-button-primary-fill,#0f1115);color:var(--dsw-alias-label-primary-foreground,#fff);cursor:pointer;border:none;border-radius:8px;padding:7px 18px;font-family:inherit;font-size:13px;font-weight:600;transition:background .15s}.GAuiuG_modalPrimary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,#2b2f36)}.GAuiuG_modalPrimary:disabled{opacity:.5;cursor:default}.GAuiuG_modalGhost{border:1px solid var(--dsw-alias-border-l2,#00000026);color:var(--dsw-alias-label-primary,#0f1115);cursor:pointer;background:0 0;border-radius:8px;padding:7px 15px;font-family:inherit;font-size:13px;transition:background .15s}.GAuiuG_modalGhost:hover:not(:disabled){background:var(--dsw-alias-bg-overlay,#0000000d)}.GAuiuG_modalGhost:disabled{opacity:.5;cursor:default}.GAuiuG_formError{background:var(--dsw-alias-state-error-bg,#f53f3f1a);border:1px solid var(--dsw-alias-state-error-primary,#f53f3f66);color:var(--dsw-alias-state-error-primary,#d03050);border-radius:6px;margin:0 18px 2px;padding:8px 12px;font-size:12px}.GAuiuG_hint{color:var(--dsw-alias-label-tertiary,#81858c);font-size:11.5px;line-height:1.45}.GAuiuG_field{flex-direction:column;gap:5px;min-width:0;display:flex}.GAuiuG_fieldLabel{color:var(--dsw-alias-label-primary,#0f1115);font-size:13px;font-weight:500}.GAuiuG_input{box-sizing:border-box;width:100%;min-width:0;min-height:36px;color:var(--dsw-alias-label-primary,#0f1115);background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#00000024);border-radius:8px;outline:none;padding:7px 10px;font-family:inherit;font-size:13px;transition:border-color .15s,box-shadow .15s}.GAuiuG_input:focus{border-color:var(--dsw-alias-brand-primary-new-colorprimary-new-color,#4176e6);box-shadow:0 0 0 2px #4176e626}.GAuiuG_input::placeholder{color:var(--dsw-alias-label-tertiary,#81858c)}.GAuiuG_input:disabled{opacity:.5}.GAuiuG_formRow{grid-template-columns:minmax(0,1.25fr) minmax(0,1fr);gap:12px;min-width:0;display:grid}.GAuiuG_radioRow{align-items:center;gap:18px;display:flex}.GAuiuG_radioLabel{color:var(--dsw-alias-label-primary,#0f1115);cursor:pointer;align-items:center;gap:7px;font-size:13px;display:inline-flex}.GAuiuG_radioLabel input[type=radio]{accent-color:var(--dsw-alias-brand-primary-new-colorprimary-new-color,#4176e6)}.GAuiuG_termBody{flex-direction:column;flex:1;gap:8px;min-height:0;display:flex;overflow:hidden}.GAuiuG_termWrap{border:1px solid var(--dsw-alias-border-l1);background:#0b0e14;border-radius:10px;flex:1;min-height:0;position:relative;overflow:hidden}.GAuiuG_termContainer{padding:8px 10px;position:absolute;inset:0}.GAuiuG_termPlaceholder{z-index:2;text-align:center;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-base);justify-content:center;align-items:center;padding:0 24px;font-size:12.5px;display:flex;position:absolute;inset:0}.GAuiuG_hiddenFile{display:none}.GAuiuG_browsePanel{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;display:flex;overflow:hidden}.GAuiuG_browseHeader{background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;gap:8px;padding:8px 10px;display:flex}.GAuiuG_browsePath{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-secondary);flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;overflow:hidden}.GAuiuG_browseList{max-height:240px;overflow:hidden auto}.GAuiuG_dirRow{text-align:left;width:100%;min-width:0;font:inherit;color:var(--dsw-alias-label-primary);border:none;border-bottom:1px solid var(--dsw-alias-separator-primary);cursor:pointer;background:0 0;grid-template-columns:minmax(0,1fr) auto auto;gap:12px;padding:5px 10px;font-size:12.5px;display:grid}.GAuiuG_dirRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.GAuiuG_dirRow[data-type=dir] .GAuiuG_dirName{color:var(--dsw-alias-state-business-primary)}.GAuiuG_dirRow[data-up]{color:var(--dsw-alias-label-secondary)}.GAuiuG_dirName{text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow:hidden}.GAuiuG_dirType{color:var(--dsw-alias-label-tertiary);white-space:nowrap}.GAuiuG_dirSize{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-align:right}.GAuiuG_transferBlock{flex-direction:column;flex:none;gap:6px;display:flex}.GAuiuG_progressMeta{min-width:0;color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.GAuiuG_progressTrack{background:var(--dsw-alias-interactive-bg-hover);border-radius:999px;height:8px;overflow:hidden}.GAuiuG_progressBar{background:var(--dsw-alias-state-business-primary);border-radius:999px;height:100%;transition:width .12s linear}.GAuiuG_tunnelList{flex-direction:column;flex:none;gap:8px;display:flex}.GAuiuG_tunnelRow{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;align-items:center;gap:10px;padding:8px 12px;display:flex}.GAuiuG_tunnelRow[data-state=forwarding]{border-color:var(--dsw-alias-state-success-primary)}.GAuiuG_tunnelRow[data-state=connecting]{border-color:var(--dsw-alias-state-warn-primary)}.GAuiuG_tunnelRow[data-state=failed]{border-color:var(--dsw-alias-state-error-primary)}.GAuiuG_tunnelLabel{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12.5px;overflow:hidden}.GAuiuG_formCard{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;flex:none;gap:10px;padding:12px;display:flex}.GAuiuG_clusterForm{flex-direction:column;flex:none;gap:8px;display:flex}.GAuiuG_clusterFilters{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px;display:grid}.GAuiuG_commandInput{min-height:96px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.GAuiuG_cellDetails summary{cursor:pointer;color:var(--dsw-alias-label-secondary);font-size:12px}.GAuiuG_cellPre{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;word-break:break-all;border-radius:6px;max-height:180px;margin:4px 0 0;padding:6px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;overflow:auto}@media (max-width:420px){.GAuiuG_formRow{grid-template-columns:1fr}}.GAuiuG_fingerprintIntro{color:var(--dsw-alias-label-primary,#0f1115);margin:0 0 10px;font-size:13px;line-height:1.5}.GAuiuG_fingerprintCode{border:1px solid var(--dsw-alias-border-l2,#0000001f);background:var(--dsw-specific-input-major,#f2f3f5);color:var(--dsw-alias-label-primary,#0f1115);word-break:break-all;-webkit-user-select:text;user-select:text;border-radius:6px;padding:10px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;display:block}.GAuiuG_fingerprintWarn{color:var(--dsw-alias-state-error-primary,#d03050);margin:10px 0 0;font-size:12px;line-height:1.5}";
|
|
1096
1288
|
const tagId = "@tiphareth/dsh-hardssh/panel.module.css";
|
|
1097
1289
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1098
1290
|
const tag = document.createElement("style");
|
|
@@ -1120,11 +1312,10 @@ window.__ModuleLoader__.load({
|
|
|
1120
1312
|
"dirRow": "GAuiuG_dirRow",
|
|
1121
1313
|
"dirSize": "GAuiuG_dirSize",
|
|
1122
1314
|
"dirType": "GAuiuG_dirType",
|
|
1315
|
+
"disabledPlaceholder": "GAuiuG_disabledPlaceholder",
|
|
1316
|
+
"disabledSurface": "GAuiuG_disabledSurface",
|
|
1123
1317
|
"dshSshSpin": "GAuiuG_dshSshSpin",
|
|
1124
1318
|
"empty": "GAuiuG_empty",
|
|
1125
|
-
"entry": "GAuiuG_entry",
|
|
1126
|
-
"entryIcon": "GAuiuG_entryIcon",
|
|
1127
|
-
"entryLabel": "GAuiuG_entryLabel",
|
|
1128
1319
|
"field": "GAuiuG_field",
|
|
1129
1320
|
"fieldLabel": "GAuiuG_fieldLabel",
|
|
1130
1321
|
"fillBody": "GAuiuG_fillBody",
|
|
@@ -1137,11 +1328,11 @@ window.__ModuleLoader__.load({
|
|
|
1137
1328
|
"ghostButton": "GAuiuG_ghostButton",
|
|
1138
1329
|
"hiddenFile": "GAuiuG_hiddenFile",
|
|
1139
1330
|
"hint": "GAuiuG_hint",
|
|
1140
|
-
"iconButton": "GAuiuG_iconButton",
|
|
1141
1331
|
"inlineTest": "GAuiuG_inlineTest",
|
|
1142
1332
|
"input": "GAuiuG_input",
|
|
1143
1333
|
"linkButton": "GAuiuG_linkButton",
|
|
1144
1334
|
"loading": "GAuiuG_loading",
|
|
1335
|
+
"localSessionMask": "GAuiuG_localSessionMask",
|
|
1145
1336
|
"modal": "GAuiuG_modal",
|
|
1146
1337
|
"modalBackdrop": "GAuiuG_modalBackdrop",
|
|
1147
1338
|
"modalBody": "GAuiuG_modalBody",
|
|
@@ -1153,8 +1344,6 @@ window.__ModuleLoader__.load({
|
|
|
1153
1344
|
"mono": "GAuiuG_mono",
|
|
1154
1345
|
"panel": "GAuiuG_panel",
|
|
1155
1346
|
"panelContent": "GAuiuG_panelContent",
|
|
1156
|
-
"panelHeader": "GAuiuG_panelHeader",
|
|
1157
|
-
"panelTitle": "GAuiuG_panelTitle",
|
|
1158
1347
|
"primaryButton": "GAuiuG_primaryButton",
|
|
1159
1348
|
"progressBar": "GAuiuG_progressBar",
|
|
1160
1349
|
"progressMeta": "GAuiuG_progressMeta",
|
|
@@ -1162,12 +1351,14 @@ window.__ModuleLoader__.load({
|
|
|
1162
1351
|
"radioLabel": "GAuiuG_radioLabel",
|
|
1163
1352
|
"radioRow": "GAuiuG_radioRow",
|
|
1164
1353
|
"search": "GAuiuG_search",
|
|
1354
|
+
"sessionTarget": "GAuiuG_sessionTarget",
|
|
1165
1355
|
"spinner": "GAuiuG_spinner",
|
|
1166
1356
|
"tab": "GAuiuG_tab",
|
|
1167
1357
|
"tabBar": "GAuiuG_tabBar",
|
|
1168
1358
|
"tabBody": "GAuiuG_tabBody",
|
|
1169
1359
|
"table": "GAuiuG_table",
|
|
1170
1360
|
"tableWrap": "GAuiuG_tableWrap",
|
|
1361
|
+
"targetBadge": "GAuiuG_targetBadge",
|
|
1171
1362
|
"termBody": "GAuiuG_termBody",
|
|
1172
1363
|
"termContainer": "GAuiuG_termContainer",
|
|
1173
1364
|
"termPlaceholder": "GAuiuG_termPlaceholder",
|
|
@@ -1178,7 +1369,6 @@ window.__ModuleLoader__.load({
|
|
|
1178
1369
|
"tunnelLabel": "GAuiuG_tunnelLabel",
|
|
1179
1370
|
"tunnelList": "GAuiuG_tunnelList",
|
|
1180
1371
|
"tunnelRow": "GAuiuG_tunnelRow",
|
|
1181
|
-
"view": "GAuiuG_view",
|
|
1182
1372
|
"whaleMark": "GAuiuG_whaleMark"
|
|
1183
1373
|
};
|
|
1184
1374
|
//#endregion
|
|
@@ -1190,7 +1380,7 @@ window.__ModuleLoader__.load({
|
|
|
1190
1380
|
* exposes the secret-free summary).
|
|
1191
1381
|
*/
|
|
1192
1382
|
/** Split a comma-separated input into a trimmed, non-empty string list. */
|
|
1193
|
-
function splitList
|
|
1383
|
+
function splitList(text) {
|
|
1194
1384
|
return text.split(",").map((part) => part.trim()).filter((part) => part !== "");
|
|
1195
1385
|
}
|
|
1196
1386
|
/** Initial form state: the summary's public fields plus empty secrets. */
|
|
@@ -1257,10 +1447,10 @@ window.__ModuleLoader__.load({
|
|
|
1257
1447
|
keyPath: form.keyPath.trim(),
|
|
1258
1448
|
passphrase: form.passphrase === "" ? void 0 : form.passphrase
|
|
1259
1449
|
},
|
|
1260
|
-
proxyJump: splitList
|
|
1450
|
+
proxyJump: splitList(form.proxyJump),
|
|
1261
1451
|
description: form.description.trim() === "" ? void 0 : form.description.trim(),
|
|
1262
1452
|
environment: form.environment.trim() === "" ? void 0 : form.environment.trim(),
|
|
1263
|
-
tags: splitList
|
|
1453
|
+
tags: splitList(form.tags),
|
|
1264
1454
|
location: form.location.trim() === "" ? void 0 : form.location.trim()
|
|
1265
1455
|
};
|
|
1266
1456
|
setSaving(true);
|
|
@@ -1576,99 +1766,36 @@ window.__ModuleLoader__.load({
|
|
|
1576
1766
|
});
|
|
1577
1767
|
}
|
|
1578
1768
|
//#endregion
|
|
1579
|
-
//#region src/client/
|
|
1769
|
+
//#region src/client/workspace-panel.tsx
|
|
1580
1770
|
/**
|
|
1581
|
-
* The SSH workspace manager
|
|
1582
|
-
*
|
|
1583
|
-
* workspaces
|
|
1584
|
-
*
|
|
1585
|
-
*
|
|
1586
|
-
*
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
1589
|
-
*
|
|
1590
|
-
*
|
|
1771
|
+
* The SSH workspace manager as a CENTER panel (left sidebar global entry →
|
|
1772
|
+
* `main` slot). Content is unchanged from the old session-header dropdown:
|
|
1773
|
+
* server rows with their SSH-bound workspaces underneath, server edit/delete
|
|
1774
|
+
* (delete is refused while the server still backs a workspace), and a
|
|
1775
|
+
* "new server" action at the bottom.
|
|
1776
|
+
*
|
|
1777
|
+
* This is a plain panel body: no portal, no anchoring, no click-outside
|
|
1778
|
+
* dismissal. The left sidebar owns the entry row and the center column owns
|
|
1779
|
+
* the frame; closing the panel is the shell's business (select the
|
|
1780
|
+
* Conversation row back), not ours.
|
|
1591
1781
|
*/
|
|
1592
|
-
/** The
|
|
1593
|
-
function
|
|
1594
|
-
|
|
1595
|
-
type: "button",
|
|
1596
|
-
className: workspace_module_css_default.managerButton,
|
|
1597
|
-
"data-ssh-workspace-manager": "",
|
|
1598
|
-
title: tt$1("manager.tooltip"),
|
|
1599
|
-
onClick: (event) => openManager(props.manager, props.sshApi, event.currentTarget),
|
|
1600
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1601
|
-
viewBox: "0 0 16 16",
|
|
1602
|
-
width: "12",
|
|
1603
|
-
height: "12",
|
|
1604
|
-
fill: "none",
|
|
1605
|
-
stroke: "currentColor",
|
|
1606
|
-
strokeWidth: "1.4",
|
|
1607
|
-
strokeLinecap: "round",
|
|
1608
|
-
strokeLinejoin: "round",
|
|
1609
|
-
"aria-hidden": "true",
|
|
1610
|
-
children: [
|
|
1611
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
1612
|
-
x: "2",
|
|
1613
|
-
y: "3",
|
|
1614
|
-
width: "12",
|
|
1615
|
-
height: "10",
|
|
1616
|
-
rx: "2"
|
|
1617
|
-
}),
|
|
1618
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M6 6.5l2.2 1.6L6 9.7" }),
|
|
1619
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M9.5 9.5h2" })
|
|
1620
|
-
]
|
|
1621
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tt$1("manager.label") })]
|
|
1622
|
-
});
|
|
1623
|
-
}
|
|
1624
|
-
/** Open the manager dropdown, anchored under the trigger button. */
|
|
1625
|
-
function openManager(manager, sshApi, trigger) {
|
|
1626
|
-
const element = document.createElement("div");
|
|
1627
|
-
element.dataset.sshWorkspaceManagerOverlay = "";
|
|
1628
|
-
document.body.appendChild(element);
|
|
1629
|
-
const root = (0, react_dom_client.createRoot)(element);
|
|
1630
|
-
const close = () => {
|
|
1631
|
-
root.unmount();
|
|
1632
|
-
element.remove();
|
|
1633
|
-
};
|
|
1634
|
-
let anchor;
|
|
1635
|
-
if (trigger !== void 0) {
|
|
1636
|
-
const rect = trigger.getBoundingClientRect();
|
|
1637
|
-
anchor = {
|
|
1638
|
-
bottom: rect.bottom,
|
|
1639
|
-
left: rect.left
|
|
1640
|
-
};
|
|
1641
|
-
}
|
|
1642
|
-
root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ManagerOverlay, {
|
|
1643
|
-
manager,
|
|
1644
|
-
sshApi,
|
|
1645
|
-
onClose: close,
|
|
1646
|
-
anchor
|
|
1647
|
-
}));
|
|
1648
|
-
}
|
|
1649
|
-
/** The manager dropdown: server + workspace rows, with server CRUD. */
|
|
1650
|
-
function ManagerOverlay(props) {
|
|
1651
|
-
const [workspaces, setWorkspaces] = (0, react.useState)(null);
|
|
1782
|
+
/** The workspace manager panel body. */
|
|
1783
|
+
function WorkspaceManagerPanel({ manager, sshApi }) {
|
|
1784
|
+
const [workspaces, setWorkspaces] = (0, react.useState)(() => manager.getSnapshot().workspaces);
|
|
1652
1785
|
const [hosts, setHosts] = (0, react.useState)(null);
|
|
1786
|
+
const [connectedAliases, setConnectedAliases] = (0, react.useState)(null);
|
|
1787
|
+
/** Why connection state is unavailable — surfaced, never silently degraded. */
|
|
1788
|
+
const [connectionError, setConnectionError] = (0, react.useState)(null);
|
|
1653
1789
|
const [error, setError] = (0, react.useState)(null);
|
|
1654
1790
|
const [deleting, setDeleting] = (0, react.useState)(null);
|
|
1655
1791
|
const [deletingAlias, setDeletingAlias] = (0, react.useState)(null);
|
|
1656
1792
|
const [hostDialog, setHostDialog] = (0, react.useState)(null);
|
|
1657
|
-
const menuRef = (0, react.useRef)(null);
|
|
1658
|
-
(0, react.useEffect)(() => {
|
|
1659
|
-
const onMouseDown = (event) => {
|
|
1660
|
-
if (menuRef.current !== null && menuRef.current.contains(event.target)) return;
|
|
1661
|
-
props.onClose();
|
|
1662
|
-
};
|
|
1663
|
-
document.addEventListener("mousedown", onMouseDown);
|
|
1664
|
-
return () => document.removeEventListener("mousedown", onMouseDown);
|
|
1665
|
-
}, [props]);
|
|
1666
|
-
const load = () => {
|
|
1667
|
-
setWorkspaces(props.manager.getSnapshot().workspaces);
|
|
1668
|
-
};
|
|
1669
1793
|
const loadHosts = () => {
|
|
1670
|
-
if (
|
|
1671
|
-
|
|
1794
|
+
if (sshApi === void 0) {
|
|
1795
|
+
setHosts([]);
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
sshApi.listHosts().then((list) => {
|
|
1672
1799
|
setHosts(list);
|
|
1673
1800
|
}, (cause) => {
|
|
1674
1801
|
setError(cause instanceof Error ? cause.message : String(cause));
|
|
@@ -1676,74 +1803,89 @@ window.__ModuleLoader__.load({
|
|
|
1676
1803
|
});
|
|
1677
1804
|
};
|
|
1678
1805
|
(0, react.useEffect)(() => {
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
const unsubscribe =
|
|
1682
|
-
|
|
1806
|
+
setWorkspaces(manager.getSnapshot().workspaces);
|
|
1807
|
+
manager.refresh();
|
|
1808
|
+
const unsubscribe = manager.subscribe(() => {
|
|
1809
|
+
setWorkspaces(manager.getSnapshot().workspaces);
|
|
1683
1810
|
});
|
|
1684
1811
|
loadHosts();
|
|
1812
|
+
let disposed = false;
|
|
1813
|
+
const refreshConnections = async () => {
|
|
1814
|
+
if (sshApi === void 0) {
|
|
1815
|
+
if (!disposed) setConnectedAliases(/* @__PURE__ */ new Set());
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
try {
|
|
1819
|
+
const aliases = await sshApi.connectedAliases();
|
|
1820
|
+
if (disposed) return;
|
|
1821
|
+
setConnectedAliases(new Set(aliases));
|
|
1822
|
+
setConnectionError(null);
|
|
1823
|
+
} catch (cause) {
|
|
1824
|
+
if (disposed) return;
|
|
1825
|
+
setConnectedAliases(null);
|
|
1826
|
+
setConnectionError(cause instanceof Error ? cause.message : String(cause));
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
refreshConnections();
|
|
1830
|
+
const timer = window.setInterval(() => {
|
|
1831
|
+
refreshConnections();
|
|
1832
|
+
}, 3e3);
|
|
1685
1833
|
return () => {
|
|
1834
|
+
disposed = true;
|
|
1835
|
+
window.clearInterval(timer);
|
|
1686
1836
|
unsubscribe();
|
|
1687
1837
|
};
|
|
1688
|
-
}, []);
|
|
1838
|
+
}, [manager, sshApi]);
|
|
1689
1839
|
const remove = (id) => {
|
|
1690
1840
|
setDeleting(id);
|
|
1691
|
-
|
|
1841
|
+
manager.remove(id).catch((cause) => setError(cause instanceof Error ? cause.message : String(cause))).finally(() => setDeleting(null));
|
|
1692
1842
|
};
|
|
1693
1843
|
const deleteHost = (alias) => {
|
|
1694
|
-
if (
|
|
1844
|
+
if (sshApi === void 0) return;
|
|
1695
1845
|
setDeletingAlias(alias);
|
|
1696
|
-
|
|
1846
|
+
sshApi.deleteHost(alias).then(() => {
|
|
1697
1847
|
loadHosts();
|
|
1698
|
-
}).catch((
|
|
1848
|
+
}).catch((cause) => setError(cause instanceof Error ? cause.message : String(cause))).finally(() => setDeletingAlias(null));
|
|
1699
1849
|
};
|
|
1700
1850
|
const savedHost = () => {
|
|
1701
1851
|
setHostDialog(null);
|
|
1702
1852
|
loadHosts();
|
|
1703
|
-
|
|
1853
|
+
manager.refresh();
|
|
1704
1854
|
};
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
const top = Math.max(8, Math.min(anchorTop, window.innerHeight - 340));
|
|
1708
|
-
const left = Math.max(8, Math.min(anchorLeft, window.innerWidth - 380));
|
|
1709
|
-
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1710
|
-
ref: menuRef,
|
|
1711
|
-
className: workspace_module_css_default.menuDropdown,
|
|
1712
|
-
"data-ssh-workspace-manager-menu": "",
|
|
1713
|
-
style: {
|
|
1714
|
-
top,
|
|
1715
|
-
left,
|
|
1716
|
-
width: 380
|
|
1717
|
-
},
|
|
1718
|
-
onClick: (event) => event.stopPropagation(),
|
|
1855
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1856
|
+
className: workspace_module_css_default.managerPanel,
|
|
1719
1857
|
children: [
|
|
1720
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1721
|
-
className: workspace_module_css_default.
|
|
1722
|
-
children:
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
"aria-label": tt$1("create.cancel"),
|
|
1727
|
-
children: "×"
|
|
1728
|
-
})]
|
|
1858
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1859
|
+
className: workspace_module_css_default.managerPanelHeader,
|
|
1860
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1861
|
+
className: workspace_module_css_default.managerPanelTitle,
|
|
1862
|
+
children: tt$1("manager.title")
|
|
1863
|
+
})
|
|
1729
1864
|
}),
|
|
1730
1865
|
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1731
1866
|
className: workspace_module_css_default.dialogFail,
|
|
1732
1867
|
children: error
|
|
1733
1868
|
}),
|
|
1734
|
-
|
|
1869
|
+
connectionError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1870
|
+
className: workspace_module_css_default.dialogFail,
|
|
1871
|
+
"data-test": "connection-state-error",
|
|
1872
|
+
children: tt$1("manager.connectionError", connectionError)
|
|
1873
|
+
}),
|
|
1874
|
+
hosts === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1735
1875
|
className: workspace_module_css_default.dialogInfo,
|
|
1736
1876
|
children: tt$1("panel.loading")
|
|
1737
1877
|
}),
|
|
1738
|
-
workspaces !== null &&
|
|
1878
|
+
workspaces.length === 0 && hosts !== null && hosts.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1739
1879
|
className: workspace_module_css_default.dialogHint,
|
|
1740
1880
|
children: tt$1("manager.empty")
|
|
1741
1881
|
}),
|
|
1742
1882
|
hosts !== null && hosts.map((host) => {
|
|
1743
|
-
const hostWorkspaces = workspaces
|
|
1883
|
+
const hostWorkspaces = workspaces.filter((workspace) => workspace.alias === host.alias);
|
|
1744
1884
|
const inUse = hostWorkspaces.length > 0;
|
|
1885
|
+
const connectionState = connectedAliases === null ? "unknown" : connectedAliases.has(host.alias) ? "connected" : "disconnected";
|
|
1886
|
+
const connectionLabel = connectionState === "connected" ? tt$1("manager.connected") : connectionState === "disconnected" ? tt$1("manager.disconnected") : tt$1("manager.connectionUnknown");
|
|
1745
1887
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1746
|
-
|
|
1888
|
+
className: workspace_module_css_default.managerPanelGroup,
|
|
1747
1889
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1748
1890
|
className: workspace_module_css_default.managerGroupHeader,
|
|
1749
1891
|
style: {
|
|
@@ -1766,6 +1908,15 @@ window.__ModuleLoader__.load({
|
|
|
1766
1908
|
},
|
|
1767
1909
|
children: host.alias
|
|
1768
1910
|
}),
|
|
1911
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1912
|
+
className: workspace_module_css_default.connectionBadge,
|
|
1913
|
+
"data-state": connectionState,
|
|
1914
|
+
title: connectionLabel,
|
|
1915
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1916
|
+
className: workspace_module_css_default.connectionDot,
|
|
1917
|
+
"aria-hidden": "true"
|
|
1918
|
+
}), connectionLabel]
|
|
1919
|
+
}),
|
|
1769
1920
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1770
1921
|
className: workspace_module_css_default.hostMeta,
|
|
1771
1922
|
style: { fontSize: 11 },
|
|
@@ -1831,7 +1982,7 @@ window.__ModuleLoader__.load({
|
|
|
1831
1982
|
overflow: "hidden",
|
|
1832
1983
|
textOverflow: "ellipsis",
|
|
1833
1984
|
whiteSpace: "nowrap",
|
|
1834
|
-
maxWidth:
|
|
1985
|
+
maxWidth: 220
|
|
1835
1986
|
},
|
|
1836
1987
|
children: workspace.remoteRoot
|
|
1837
1988
|
}),
|
|
@@ -1845,7 +1996,7 @@ window.__ModuleLoader__.load({
|
|
|
1845
1996
|
},
|
|
1846
1997
|
disabled: deleting === workspace.id,
|
|
1847
1998
|
onClick: () => {
|
|
1848
|
-
if (
|
|
1999
|
+
if (deleting === null) remove(workspace.id);
|
|
1849
2000
|
},
|
|
1850
2001
|
children: deleting === workspace.id ? "…" : tt$1("manager.delete")
|
|
1851
2002
|
})
|
|
@@ -1853,12 +2004,8 @@ window.__ModuleLoader__.load({
|
|
|
1853
2004
|
}, workspace.id))]
|
|
1854
2005
|
}, host.alias);
|
|
1855
2006
|
}),
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
marginTop: 4,
|
|
1859
|
-
borderTop: `1px solid var(--dsw-alias-border-l2, rgba(0,0,0,0.1))`,
|
|
1860
|
-
paddingTop: 6
|
|
1861
|
-
},
|
|
2007
|
+
sshApi !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2008
|
+
className: workspace_module_css_default.managerPanelFooter,
|
|
1862
2009
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1863
2010
|
type: "button",
|
|
1864
2011
|
className: workspace_module_css_default.menuItem,
|
|
@@ -1871,8 +2018,8 @@ window.__ModuleLoader__.load({
|
|
|
1871
2018
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tt$1("manager.newHost") })]
|
|
1872
2019
|
})
|
|
1873
2020
|
}),
|
|
1874
|
-
hostDialog !== null &&
|
|
1875
|
-
api:
|
|
2021
|
+
hostDialog !== null && sshApi !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostFormDialog, {
|
|
2022
|
+
api: sshApi,
|
|
1876
2023
|
editing: hostDialog.mode === "edit" ? hostDialog.host : null,
|
|
1877
2024
|
onClose: () => {
|
|
1878
2025
|
setHostDialog(null);
|
|
@@ -1880,7 +2027,60 @@ window.__ModuleLoader__.load({
|
|
|
1880
2027
|
onSaved: savedHost
|
|
1881
2028
|
})
|
|
1882
2029
|
]
|
|
1883
|
-
})
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
//#endregion
|
|
2033
|
+
//#region src/client/workspace-panel-entry.tsx
|
|
2034
|
+
/** The main-panel key AND the sidebar row id: one identity addresses both. */
|
|
2035
|
+
const WORKSPACE_PANEL_ID = "dsh-hardssh-workspaces";
|
|
2036
|
+
/** Order among the global panel rows; the shipped rows sit around 0. */
|
|
2037
|
+
const WORKSPACE_PANEL_ORDER = 100;
|
|
2038
|
+
/** The cloud/terminal glyph the workspace row shows in both the wide and rail sidebar. */
|
|
2039
|
+
function WorkspacePanelIcon({ size }) {
|
|
2040
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2041
|
+
viewBox: "0 0 16 16",
|
|
2042
|
+
width: size,
|
|
2043
|
+
height: size,
|
|
2044
|
+
fill: "none",
|
|
2045
|
+
stroke: "currentColor",
|
|
2046
|
+
strokeWidth: "1.3",
|
|
2047
|
+
strokeLinecap: "round",
|
|
2048
|
+
strokeLinejoin: "round",
|
|
2049
|
+
"aria-hidden": "true",
|
|
2050
|
+
children: [
|
|
2051
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
2052
|
+
x: "2",
|
|
2053
|
+
y: "2.5",
|
|
2054
|
+
width: "12",
|
|
2055
|
+
height: "11",
|
|
2056
|
+
rx: "1.5"
|
|
2057
|
+
}),
|
|
2058
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4.5 5.5l2.5 2.5-2.5 2.5" }),
|
|
2059
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8.5 10.5h3" })
|
|
2060
|
+
]
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Register the left-sidebar row and the center panel body.
|
|
2065
|
+
* @param ctx - client root context carrying the slot registry.
|
|
2066
|
+
* @param deps - the workspace manager and the host API the panel uses.
|
|
2067
|
+
*/
|
|
2068
|
+
function registerWorkspacePanel(ctx, deps) {
|
|
2069
|
+
const inject = () => ({
|
|
2070
|
+
manager: deps.manager,
|
|
2071
|
+
sshApi: deps.sshApi
|
|
2072
|
+
});
|
|
2073
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
2074
|
+
name: "sidebar.panellist",
|
|
2075
|
+
id: WORKSPACE_PANEL_ID,
|
|
2076
|
+
order: WORKSPACE_PANEL_ORDER,
|
|
2077
|
+
label: () => tt$1("panel.workspaces")
|
|
2078
|
+
}, WorkspacePanelIcon));
|
|
2079
|
+
ctx.slots.inject("main", () => ctx.slots.register({
|
|
2080
|
+
name: "main",
|
|
2081
|
+
key: WORKSPACE_PANEL_ID,
|
|
2082
|
+
inject
|
|
2083
|
+
}, WorkspaceManagerPanel));
|
|
1884
2084
|
}
|
|
1885
2085
|
//#endregion
|
|
1886
2086
|
//#region src/client/migrate.ts
|
|
@@ -2043,6 +2243,13 @@ window.__ModuleLoader__.load({
|
|
|
2043
2243
|
password: ""
|
|
2044
2244
|
});
|
|
2045
2245
|
const [savingHost, setSavingHost] = (0, react.useState)(false);
|
|
2246
|
+
const openRef = (0, react.useRef)(open);
|
|
2247
|
+
openRef.current = open;
|
|
2248
|
+
const localPickRequestSequence = (0, react.useRef)(0);
|
|
2249
|
+
const hostsRequestSequence = (0, react.useRef)(0);
|
|
2250
|
+
const directoryRequestSequence = (0, react.useRef)(0);
|
|
2251
|
+
const createWorkspaceRequestSequence = (0, react.useRef)(0);
|
|
2252
|
+
const createHostRequestSequence = (0, react.useRef)(0);
|
|
2046
2253
|
(0, react.useEffect)(() => {
|
|
2047
2254
|
if (!open) return;
|
|
2048
2255
|
setChoice("menu");
|
|
@@ -2050,6 +2257,9 @@ window.__ModuleLoader__.load({
|
|
|
2050
2257
|
setDirPath("");
|
|
2051
2258
|
setTitle("");
|
|
2052
2259
|
setDirEntries([]);
|
|
2260
|
+
setLoading(false);
|
|
2261
|
+
setCreating(false);
|
|
2262
|
+
setSavingHost(false);
|
|
2053
2263
|
setError(null);
|
|
2054
2264
|
setAddingHost(false);
|
|
2055
2265
|
setNewHost({
|
|
@@ -2060,7 +2270,19 @@ window.__ModuleLoader__.load({
|
|
|
2060
2270
|
password: ""
|
|
2061
2271
|
});
|
|
2062
2272
|
setAnchorRect();
|
|
2063
|
-
|
|
2273
|
+
const request = ++hostsRequestSequence.current;
|
|
2274
|
+
props.listHosts().then((list) => {
|
|
2275
|
+
if (request === hostsRequestSequence.current && openRef.current) setHosts(list);
|
|
2276
|
+
}, () => {
|
|
2277
|
+
if (request === hostsRequestSequence.current && openRef.current) setHosts([]);
|
|
2278
|
+
});
|
|
2279
|
+
return () => {
|
|
2280
|
+
localPickRequestSequence.current += 1;
|
|
2281
|
+
hostsRequestSequence.current += 1;
|
|
2282
|
+
directoryRequestSequence.current += 1;
|
|
2283
|
+
createWorkspaceRequestSequence.current += 1;
|
|
2284
|
+
createHostRequestSequence.current += 1;
|
|
2285
|
+
};
|
|
2064
2286
|
}, [open]);
|
|
2065
2287
|
/** Locate the trigger element that just opened the flow and anchor the menu
|
|
2066
2288
|
* directly under it. Priority: the focused element (the button the operator
|
|
@@ -2105,42 +2327,50 @@ window.__ModuleLoader__.load({
|
|
|
2105
2327
|
if (!open) return null;
|
|
2106
2328
|
/** Choose "Local workspace": drive the native chooser once. */
|
|
2107
2329
|
const pickLocal = () => {
|
|
2330
|
+
const request = ++localPickRequestSequence.current;
|
|
2108
2331
|
setChoice("picking-local");
|
|
2109
2332
|
props.pickDirectory().then((path) => {
|
|
2333
|
+
if (request !== localPickRequestSequence.current || !openRef.current) return;
|
|
2110
2334
|
if (path === null) outcome.current.onCancel();
|
|
2111
2335
|
else outcome.current.onPicked(path);
|
|
2112
|
-
}, (reason) =>
|
|
2336
|
+
}, (reason) => {
|
|
2337
|
+
if (request === localPickRequestSequence.current && openRef.current) outcome.current.onError(reason instanceof Error ? reason.message : String(reason));
|
|
2338
|
+
});
|
|
2113
2339
|
};
|
|
2114
2340
|
/** Browse a remote dir level for the SSH branch. When the connection needs
|
|
2115
2341
|
* a credential (host key untrusted / session password), run the interactive
|
|
2116
2342
|
* gate first, then retry — the operator never sees a raw "connect" error. */
|
|
2117
2343
|
const browse = async (alias, path) => {
|
|
2344
|
+
const request = ++directoryRequestSequence.current;
|
|
2118
2345
|
if (alias === "") {
|
|
2119
2346
|
setError(tt$1("create.needHost"));
|
|
2120
2347
|
return;
|
|
2121
2348
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
try {
|
|
2349
|
+
setLoading(true);
|
|
2350
|
+
setError(null);
|
|
2351
|
+
try {
|
|
2352
|
+
for (let attempt = 0; attempt < 3; attempt++) try {
|
|
2126
2353
|
const result = await props.listRemoteDir(alias, path);
|
|
2354
|
+
if (request !== directoryRequestSequence.current || !openRef.current) return;
|
|
2127
2355
|
setDirPath(result.path);
|
|
2128
2356
|
setDirEntries(result.entries);
|
|
2129
2357
|
setChoice("browsing");
|
|
2130
|
-
setLoading(false);
|
|
2131
2358
|
return;
|
|
2132
2359
|
} catch (e) {
|
|
2360
|
+
if (request !== directoryRequestSequence.current || !openRef.current) return;
|
|
2133
2361
|
const code = e?.code;
|
|
2134
|
-
|
|
2135
|
-
setLoading(false);
|
|
2136
|
-
if (!interactive) {
|
|
2362
|
+
if (!(code === "NEEDS_PASSWORD" || code === "HOST_KEY_UNKNOWN" || code === "HOST_KEY_MISMATCH")) {
|
|
2137
2363
|
setError(e instanceof Error ? e.message : String(e));
|
|
2138
2364
|
return;
|
|
2139
2365
|
}
|
|
2140
|
-
|
|
2366
|
+
const connected = await props.ensureConnected(alias);
|
|
2367
|
+
if (request !== directoryRequestSequence.current || !openRef.current) return;
|
|
2368
|
+
if (!connected) return;
|
|
2141
2369
|
}
|
|
2370
|
+
if (request === directoryRequestSequence.current && openRef.current) setError(tt$1("create.needConnect"));
|
|
2371
|
+
} finally {
|
|
2372
|
+
if (request === directoryRequestSequence.current && openRef.current) setLoading(false);
|
|
2142
2373
|
}
|
|
2143
|
-
setError(tt$1("create.needConnect"));
|
|
2144
2374
|
};
|
|
2145
2375
|
/** Create the SSH workspace; on success hand the anchor to the owner. */
|
|
2146
2376
|
const createSsh = async () => {
|
|
@@ -2148,6 +2378,7 @@ window.__ModuleLoader__.load({
|
|
|
2148
2378
|
setError(host === "" ? tt$1("create.needHost") : dirPath === "" ? tt$1("create.needDir") : tt$1("create.needTitle"));
|
|
2149
2379
|
return;
|
|
2150
2380
|
}
|
|
2381
|
+
const request = ++createWorkspaceRequestSequence.current;
|
|
2151
2382
|
setCreating(true);
|
|
2152
2383
|
setError(null);
|
|
2153
2384
|
try {
|
|
@@ -2156,11 +2387,12 @@ window.__ModuleLoader__.load({
|
|
|
2156
2387
|
alias: host,
|
|
2157
2388
|
remoteRoot: dirPath
|
|
2158
2389
|
});
|
|
2390
|
+
if (request !== createWorkspaceRequestSequence.current || !openRef.current) return;
|
|
2159
2391
|
outcome.current.onPicked(record.anchorPath);
|
|
2160
2392
|
} catch (e) {
|
|
2161
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
2393
|
+
if (request === createWorkspaceRequestSequence.current && openRef.current) setError(e instanceof Error ? e.message : String(e));
|
|
2162
2394
|
} finally {
|
|
2163
|
-
setCreating(false);
|
|
2395
|
+
if (request === createWorkspaceRequestSequence.current && openRef.current) setCreating(false);
|
|
2164
2396
|
}
|
|
2165
2397
|
};
|
|
2166
2398
|
/** Save a newly-added SSH host, refresh the list, select it, and close the
|
|
@@ -2171,6 +2403,7 @@ window.__ModuleLoader__.load({
|
|
|
2171
2403
|
setError(tt$1("host.needFields"));
|
|
2172
2404
|
return;
|
|
2173
2405
|
}
|
|
2406
|
+
const request = ++createHostRequestSequence.current;
|
|
2174
2407
|
setSavingHost(true);
|
|
2175
2408
|
setError(null);
|
|
2176
2409
|
try {
|
|
@@ -2184,7 +2417,10 @@ window.__ModuleLoader__.load({
|
|
|
2184
2417
|
password: newHost.password
|
|
2185
2418
|
}
|
|
2186
2419
|
});
|
|
2420
|
+
if (request !== createHostRequestSequence.current || !openRef.current) return;
|
|
2421
|
+
const hostsRequest = ++hostsRequestSequence.current;
|
|
2187
2422
|
const list = await props.listHosts();
|
|
2423
|
+
if (request !== createHostRequestSequence.current || hostsRequest !== hostsRequestSequence.current || !openRef.current) return;
|
|
2188
2424
|
setHosts(list);
|
|
2189
2425
|
setHost(created.alias);
|
|
2190
2426
|
setAddingHost(false);
|
|
@@ -2196,9 +2432,9 @@ window.__ModuleLoader__.load({
|
|
|
2196
2432
|
password: ""
|
|
2197
2433
|
});
|
|
2198
2434
|
} catch (e) {
|
|
2199
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
2435
|
+
if (request === createHostRequestSequence.current && openRef.current) setError(e instanceof Error ? e.message : String(e));
|
|
2200
2436
|
} finally {
|
|
2201
|
-
setSavingHost(false);
|
|
2437
|
+
if (request === createHostRequestSequence.current && openRef.current) setSavingHost(false);
|
|
2202
2438
|
}
|
|
2203
2439
|
};
|
|
2204
2440
|
const menuWidth = choice === "menu" ? 200 : 360;
|
|
@@ -2358,6 +2594,7 @@ window.__ModuleLoader__.load({
|
|
|
2358
2594
|
style: { gridColumn: "1 / -1" },
|
|
2359
2595
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tt$1("create.host") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
2360
2596
|
value: host,
|
|
2597
|
+
disabled: loading,
|
|
2361
2598
|
onChange: (event) => setHost(event.target.value),
|
|
2362
2599
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
2363
2600
|
value: "",
|
|
@@ -2384,6 +2621,7 @@ window.__ModuleLoader__.load({
|
|
|
2384
2621
|
gridColumn: "1 / -1",
|
|
2385
2622
|
padding: "5px 8px"
|
|
2386
2623
|
},
|
|
2624
|
+
disabled: loading,
|
|
2387
2625
|
onClick: () => setAddingHost(true),
|
|
2388
2626
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2389
2627
|
className: workspace_module_css_default.menuItemIcon,
|
|
@@ -2395,6 +2633,7 @@ window.__ModuleLoader__.load({
|
|
|
2395
2633
|
style: { gridColumn: "1 / -1" },
|
|
2396
2634
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tt$1("create.dir") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2397
2635
|
value: dirPath,
|
|
2636
|
+
disabled: loading,
|
|
2398
2637
|
onChange: (event) => setDirPath(event.target.value),
|
|
2399
2638
|
placeholder: tt$1("create.dirPlaceholder"),
|
|
2400
2639
|
spellCheck: false
|
|
@@ -2428,12 +2667,12 @@ window.__ModuleLoader__.load({
|
|
|
2428
2667
|
type: "button",
|
|
2429
2668
|
className: workspace_module_css_default.button,
|
|
2430
2669
|
onClick: () => setChoice("menu"),
|
|
2431
|
-
disabled: busy || creating,
|
|
2670
|
+
disabled: busy || creating || loading,
|
|
2432
2671
|
children: tt$1("create.cancel")
|
|
2433
2672
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2434
2673
|
type: "button",
|
|
2435
2674
|
className: `${workspace_module_css_default.button} ${workspace_module_css_default.buttonPrimary}`,
|
|
2436
|
-
disabled: busy || creating,
|
|
2675
|
+
disabled: busy || creating || loading,
|
|
2437
2676
|
onClick: () => void createSsh(),
|
|
2438
2677
|
children: creating ? tt$1("create.connecting") : tt$1("create.submit")
|
|
2439
2678
|
})]
|
|
@@ -2491,6 +2730,7 @@ window.__ModuleLoader__.load({
|
|
|
2491
2730
|
}), dirEntries.filter((entry) => entry.type === "dir").map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2492
2731
|
type: "button",
|
|
2493
2732
|
className: workspace_module_css_default.hostRow,
|
|
2733
|
+
disabled: loading,
|
|
2494
2734
|
style: {
|
|
2495
2735
|
width: "100%",
|
|
2496
2736
|
textAlign: "left",
|
|
@@ -2499,7 +2739,7 @@ window.__ModuleLoader__.load({
|
|
|
2499
2739
|
alignItems: "center",
|
|
2500
2740
|
border: "none",
|
|
2501
2741
|
background: "transparent",
|
|
2502
|
-
cursor: "pointer",
|
|
2742
|
+
cursor: loading ? "default" : "pointer",
|
|
2503
2743
|
padding: "4px 6px"
|
|
2504
2744
|
},
|
|
2505
2745
|
onClick: () => {
|
|
@@ -2517,11 +2757,13 @@ window.__ModuleLoader__.load({
|
|
|
2517
2757
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2518
2758
|
type: "button",
|
|
2519
2759
|
className: workspace_module_css_default.button,
|
|
2760
|
+
disabled: loading,
|
|
2520
2761
|
onClick: () => setChoice("ssh"),
|
|
2521
2762
|
children: tt$1("create.cancel")
|
|
2522
2763
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2523
2764
|
type: "button",
|
|
2524
2765
|
className: `${workspace_module_css_default.button} ${workspace_module_css_default.buttonPrimary}`,
|
|
2766
|
+
disabled: loading,
|
|
2525
2767
|
onClick: () => setChoice("ssh"),
|
|
2526
2768
|
children: tt$1("flow.useThisDir")
|
|
2527
2769
|
})]
|
|
@@ -2532,98 +2774,58 @@ window.__ModuleLoader__.load({
|
|
|
2532
2774
|
}), document.body);
|
|
2533
2775
|
}
|
|
2534
2776
|
//#endregion
|
|
2535
|
-
//#region src/client/
|
|
2777
|
+
//#region src/client/ssh/panel/ConnectionErrorDialog.tsx
|
|
2778
|
+
/** User-visible terminal connection failure surfaced by the Session connect gate. */
|
|
2779
|
+
/** A small acknowledgement dialog for non-interactive SSH connection failures. */
|
|
2780
|
+
function ConnectionErrorDialog({ alias, detail, onClose }) {
|
|
2781
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2782
|
+
className: panel_module_css_default.modalBackdrop,
|
|
2783
|
+
onClick: onClose,
|
|
2784
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2785
|
+
className: panel_module_css_default.modal,
|
|
2786
|
+
role: "alertdialog",
|
|
2787
|
+
"aria-modal": "true",
|
|
2788
|
+
"aria-labelledby": "dsh-ssh-connection-error-title",
|
|
2789
|
+
onClick: (event) => {
|
|
2790
|
+
event.stopPropagation();
|
|
2791
|
+
},
|
|
2792
|
+
children: [
|
|
2793
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2794
|
+
className: panel_module_css_default.modalHeader,
|
|
2795
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2796
|
+
id: "dsh-ssh-connection-error-title",
|
|
2797
|
+
className: panel_module_css_default.modalTitle,
|
|
2798
|
+
children: tt("connectionError.title", { alias })
|
|
2799
|
+
})
|
|
2800
|
+
}),
|
|
2801
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2802
|
+
className: panel_module_css_default.modalBody,
|
|
2803
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: tt("connectionError.intro") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2804
|
+
className: panel_module_css_default.formError,
|
|
2805
|
+
children: detail
|
|
2806
|
+
})]
|
|
2807
|
+
}),
|
|
2808
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2809
|
+
className: panel_module_css_default.modalFooter,
|
|
2810
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2811
|
+
type: "button",
|
|
2812
|
+
className: panel_module_css_default.modalPrimary,
|
|
2813
|
+
autoFocus: true,
|
|
2814
|
+
onClick: onClose,
|
|
2815
|
+
children: tt("connectionError.close")
|
|
2816
|
+
})
|
|
2817
|
+
})
|
|
2818
|
+
]
|
|
2819
|
+
})
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
//#endregion
|
|
2823
|
+
//#region src/client/ssh/panel/HostFingerprintDialog.tsx
|
|
2536
2824
|
/**
|
|
2537
|
-
*
|
|
2538
|
-
*
|
|
2539
|
-
* (
|
|
2540
|
-
*
|
|
2541
|
-
* decoration), following the dsh-ssh sidebar-entry precedent: a
|
|
2542
|
-
* MutationObserver self-heals when React re-renders displace the injected
|
|
2543
|
-
* badge.
|
|
2544
|
-
*
|
|
2545
|
-
* The row title alone matches: SSH-bound workspace titles are unique enough
|
|
2546
|
-
* in practice (the ledger title is what the row shows). A title match against
|
|
2547
|
-
* `~/.dsh/ssh-workspaces/<id>` anchors also works when the host picks that
|
|
2548
|
-
* default title.
|
|
2549
|
-
*
|
|
2550
|
-
* @module dsh-hardssh/client/workspace-badges
|
|
2551
|
-
*/
|
|
2552
|
-
const PROJECT_ROW_SELECTOR$1 = "[class*=\"projectRow\"]";
|
|
2553
|
-
const TITLE_SELECTOR$1 = "[class*=\"title\"]";
|
|
2554
|
-
/** Render a small remote badge element. The cloud icon marks "remote";
|
|
2555
|
-
* connected servers render in DeepSeek blue, disconnected stay gray. The
|
|
2556
|
-
* tooltip shows the REMOTE directory plus the server, e.g.
|
|
2557
|
-
* `/data/app (prod-01)`. */
|
|
2558
|
-
function makeBadge(alias, remoteRoot, connected) {
|
|
2559
|
-
const badge = document.createElement("span");
|
|
2560
|
-
badge.dataset.sshBadge = connected ? "connected" : "disconnected";
|
|
2561
|
-
badge.title = `${remoteRoot}(${alias})`;
|
|
2562
|
-
badge.setAttribute("style", connected ? "display:inline-flex;align-items:center;gap:3px;margin-left:6px;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:16px;color:#1476E6;border:1px solid #1476E6;border-radius:999px;padding:0 6px;background:#D4E0F7;flex:none" : "display:inline-flex;align-items:center;gap:3px;margin-left:6px;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,rgba(128,128,128,.75));border:1px solid var(--dsw-alias-line-secondary,rgba(128,128,128,.28));border-radius:999px;padding:0 6px;background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.10));flex:none");
|
|
2563
|
-
const icon = document.createElement("span");
|
|
2564
|
-
icon.setAttribute("style", "display:inline-flex;flex:none;line-height:1");
|
|
2565
|
-
icon.innerHTML = "<svg viewBox=\"0 0 1024 1024\" width=\"11\" height=\"11\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M743.936 758.499556h-273.066667c-17.066667 0-34.133333-17.066667-34.133333-34.133334s17.066667-34.133333 34.133333-34.133333h273.066667c28.444444 0 153.6-5.688889 153.6-130.844445 0-142.222222-153.6-164.977778-159.288889-164.977777-17.066667 0-28.444444-11.377778-28.444444-22.755556-22.755556-85.333333-108.088889-142.222222-193.422223-130.844444-136.533333 0-176.355556 79.644444-199.111111 164.977777-5.688889 5.688889-22.755556 17.066667-39.822222 17.066667 0 0-153.6 5.688889-153.6 142.222222 0 119.466667 130.844444 125.155556 153.6 125.155556 17.066667 0 34.133333 17.066667 34.133333 34.133333s-17.066667 34.133333-34.133333 34.133334c-113.777778 0-227.555556-62.577778-227.555555-193.422223 0-142.222222 119.466667-204.8 199.111111-210.488889 22.755556-68.266667 79.644444-193.422222 261.688889-193.422222 113.777778-11.377778 216.177778 56.888889 256 164.977778 79.644444 17.066667 199.111111 79.644444 199.111111 233.244444 5.688889 136.533333-108.088889 199.111111-221.866667 199.111112z\"/><path d=\"M573.269333 866.588444c-11.377778 0-17.066667-5.688889-22.755555-11.377777l-108.088889-108.088889c-5.688889-5.688889-11.377778-17.066667-11.377778-22.755556s5.688889-17.066667 11.377778-22.755555l108.088889-108.088889c11.377778-11.377778 34.133333-11.377778 51.2 0 11.377778 11.377778 11.377778 34.133333 0 51.2l-79.644445 79.644444 79.644445 79.644445c11.377778 11.377778 11.377778 34.133333 0 51.2-5.688889 5.688889-17.066667 11.377778-28.444445 11.377777z\"/></svg>";
|
|
2566
|
-
const label = document.createElement("span");
|
|
2567
|
-
label.textContent = alias;
|
|
2568
|
-
label.setAttribute("style", "font-weight:400");
|
|
2569
|
-
badge.append(icon, label);
|
|
2570
|
-
return badge;
|
|
2571
|
-
}
|
|
2572
|
-
/**
|
|
2573
|
-
* Mount the row-decoration pass. Queries the sidebar for project rows and
|
|
2574
|
-
* appends a remote badge when the row's title belongs to an SSH workspace.
|
|
2575
|
-
* Badges of aliases in `connected` render blue, others gray. Self-heals via
|
|
2576
|
-
* MutationObserver on the sidebar container (rows re-render on
|
|
2577
|
-
* session/workspace changes).
|
|
2578
|
-
* @param workspaces - the SSH workspace records (id/title/alias/remoteRoot).
|
|
2579
|
-
* @param connected - aliases with a live pooled connection (badge coloring).
|
|
2580
|
-
* @returns disposer.
|
|
2581
|
-
*/
|
|
2582
|
-
function mountWorkspaceBadges(workspaces, connected) {
|
|
2583
|
-
const byTitle = /* @__PURE__ */ new Map();
|
|
2584
|
-
for (const workspace of workspaces) byTitle.set(workspace.title, workspace);
|
|
2585
|
-
/** Decorate one project row: ALWAYS keep the row tooltip = the REMOTE
|
|
2586
|
-
* directory (server), never the local anchor path the shell would show;
|
|
2587
|
-
* mount the badge once (idempotent via the guard). Re-runs on every scan,
|
|
2588
|
-
* so a shell re-render that resets <title> gets corrected. */
|
|
2589
|
-
const decorate = (row) => {
|
|
2590
|
-
const titleEl = row.querySelector(TITLE_SELECTOR$1);
|
|
2591
|
-
if (titleEl === null || titleEl === void 0) return;
|
|
2592
|
-
const title = titleEl.textContent?.trim() ?? "";
|
|
2593
|
-
const target = byTitle.get(title);
|
|
2594
|
-
if (target === void 0) return;
|
|
2595
|
-
const desiredTitle = `${target.remoteRoot}(${target.alias})`;
|
|
2596
|
-
if (row.title !== desiredTitle) row.title = desiredTitle;
|
|
2597
|
-
if (row.querySelector("[data-ssh-badge]") !== null) return;
|
|
2598
|
-
const isConnected = connected?.has(target.alias) ?? false;
|
|
2599
|
-
titleEl.appendChild(makeBadge(target.alias, target.remoteRoot, isConnected));
|
|
2600
|
-
};
|
|
2601
|
-
/** Scan the sidebar column for project rows and decorate each. */
|
|
2602
|
-
const scan = () => {
|
|
2603
|
-
for (const row of document.querySelectorAll(PROJECT_ROW_SELECTOR$1)) decorate(row);
|
|
2604
|
-
};
|
|
2605
|
-
const observer = new MutationObserver(() => {
|
|
2606
|
-
scan();
|
|
2607
|
-
});
|
|
2608
|
-
observer.observe(document.body, {
|
|
2609
|
-
childList: true,
|
|
2610
|
-
subtree: true,
|
|
2611
|
-
attributes: true,
|
|
2612
|
-
attributeFilter: ["title"]
|
|
2613
|
-
});
|
|
2614
|
-
scan();
|
|
2615
|
-
return () => {
|
|
2616
|
-
observer.disconnect();
|
|
2617
|
-
for (const badge of document.querySelectorAll("[data-ssh-badge]")) badge.remove();
|
|
2618
|
-
};
|
|
2619
|
-
}
|
|
2620
|
-
//#endregion
|
|
2621
|
-
//#region src/client/ssh/panel/HostFingerprintDialog.tsx
|
|
2622
|
-
/**
|
|
2623
|
-
* Host fingerprint confirm dialog: shown when a connection is refused because
|
|
2624
|
-
* the server's host key is not trusted yet (TOFU first encounter) or changed
|
|
2625
|
-
* (possible MITM / key rotation). Displays the canonical SHA256 fingerprint
|
|
2626
|
-
* and lets the operator confirm (trust) or reset (forget) the record.
|
|
2825
|
+
* Host fingerprint confirm dialog: shown when a connection is refused because
|
|
2826
|
+
* the server's host key is not trusted yet (TOFU first encounter) or changed
|
|
2827
|
+
* (possible MITM / key rotation). Displays the canonical SHA256 fingerprint
|
|
2828
|
+
* and lets the operator confirm (trust) or reset (forget) the record.
|
|
2627
2829
|
*/
|
|
2628
2830
|
/** Confirm (or reset) one host key. */
|
|
2629
2831
|
function HostFingerprintDialog({ api, alias, fingerprintSha256, mismatch, onClose, onTrusted }) {
|
|
@@ -2735,7 +2937,7 @@ window.__ModuleLoader__.load({
|
|
|
2735
2937
|
* process exit. Nothing is written to disk.
|
|
2736
2938
|
*/
|
|
2737
2939
|
/** Prompt for a session-only credential. */
|
|
2738
|
-
function SessionSecretDialog({ api, alias, user, secret, onClose, onProvided }) {
|
|
2940
|
+
function SessionSecretDialog({ api, alias, user, secret, reason, onClose, onProvided }) {
|
|
2739
2941
|
const [value, setValue] = (0, react.useState)("");
|
|
2740
2942
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
2741
2943
|
const [error, setError] = (0, react.useState)(null);
|
|
@@ -2776,22 +2978,29 @@ window.__ModuleLoader__.load({
|
|
|
2776
2978
|
}),
|
|
2777
2979
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2778
2980
|
className: panel_module_css_default.modalBody,
|
|
2779
|
-
children: [
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2981
|
+
children: [
|
|
2982
|
+
reason !== void 0 && reason !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2983
|
+
className: panel_module_css_default.formError,
|
|
2984
|
+
children: reason
|
|
2985
|
+
}),
|
|
2986
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2987
|
+
className: panel_module_css_default.input,
|
|
2988
|
+
type: "password",
|
|
2989
|
+
value,
|
|
2990
|
+
autoFocus: true,
|
|
2991
|
+
placeholder: secret === "password" ? tt("sessionSecret.placeholder") : tt("sessionSecret.placeholderPassphrase"),
|
|
2992
|
+
onChange: (event) => {
|
|
2993
|
+
setValue(event.target.value);
|
|
2994
|
+
},
|
|
2995
|
+
onKeyDown: (event) => {
|
|
2996
|
+
if (event.key === "Enter") submit();
|
|
2997
|
+
}
|
|
2998
|
+
}),
|
|
2999
|
+
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3000
|
+
className: panel_module_css_default.formError,
|
|
3001
|
+
children: error
|
|
3002
|
+
})
|
|
3003
|
+
]
|
|
2795
3004
|
}),
|
|
2796
3005
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2797
3006
|
className: panel_module_css_default.modalFooter,
|
|
@@ -2816,61 +3025,58 @@ window.__ModuleLoader__.load({
|
|
|
2816
3025
|
});
|
|
2817
3026
|
}
|
|
2818
3027
|
//#endregion
|
|
2819
|
-
//#region src/client/
|
|
3028
|
+
//#region src/client/connect-host.ts
|
|
2820
3029
|
/**
|
|
2821
|
-
*
|
|
2822
|
-
* workspace row in the session sidebar, ensure the server connection can
|
|
2823
|
-
* actually be established BEFORE the shell proceeds with the native open —
|
|
2824
|
-
* otherwise the shell surfaces raw SSH errors (host key untrusted, session
|
|
2825
|
-
* password missing) and the workspace appears "dead" with no prompt.
|
|
3030
|
+
* Probe-and-prompt for an SSH alias, with a browser-session pass cache.
|
|
2826
3031
|
*
|
|
2827
|
-
*
|
|
2828
|
-
*
|
|
2829
|
-
*
|
|
2830
|
-
* - NEEDS_PASSWORD / NEEDS_PASSPHRASE → the VSCode-style session password
|
|
2831
|
-
* dialog (SessionSecretDialog; never persisted, session-only);
|
|
2832
|
-
* - ok → mark the alias as gated (5-minute TTL — trust is durable and the
|
|
2833
|
-
* session password survives in the engine table) and re-dispatch the click.
|
|
3032
|
+
* This is the interactive connection gate on its own — no row, no banner, no
|
|
3033
|
+
* DOM decoration. Flows that surface their own errors (the workspace creation
|
|
3034
|
+
* directory browser) call `connectHost` and get a plain boolean.
|
|
2834
3035
|
*
|
|
2835
|
-
*
|
|
2836
|
-
*
|
|
2837
|
-
*
|
|
2838
|
-
*
|
|
2839
|
-
*
|
|
3036
|
+
* The probe is the engine's `/test` route (a real, lightweight connect):
|
|
3037
|
+
* - `HOST_KEY_UNKNOWN` / `HOST_KEY_MISMATCH` → the host fingerprint dialog
|
|
3038
|
+
* (TOFU first encounter / key rotation);
|
|
3039
|
+
* - `NEEDS_PASSWORD` / `NEEDS_PASSPHRASE` → the VSCode-style session password
|
|
3040
|
+
* dialog (never persisted, session-only);
|
|
3041
|
+
* - ok → the alias is marked passed for a TTL (trust is durable and the session
|
|
3042
|
+
* password survives in the engine's session table until the process exits).
|
|
2840
3043
|
*
|
|
2841
|
-
* @module dsh-hardssh/client/
|
|
3044
|
+
* @module dsh-hardssh/client/connect-host
|
|
2842
3045
|
*/
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
/** A passed gate stays valid this long (trust is durable; the session
|
|
2846
|
-
* password table lives in the host process for the session). */
|
|
3046
|
+
/** A passed alias stays valid this long (trust is durable; the session password
|
|
3047
|
+
* table lives in the host process for the session). */
|
|
2847
3048
|
const GATE_TTL_MS = 3e5;
|
|
2848
3049
|
/** Cap on interactive retry rounds (wrong password loops etc.). */
|
|
2849
3050
|
const MAX_ATTEMPTS = 3;
|
|
2850
|
-
/** alias → last successful
|
|
2851
|
-
const
|
|
2852
|
-
/** alias → in-flight
|
|
3051
|
+
/** alias → last successful probe timestamp (browser-session scope). */
|
|
3052
|
+
const passedAt = /* @__PURE__ */ new Map();
|
|
3053
|
+
/** alias → in-flight probe promise (no duplicate dialogs on multi-click). */
|
|
2853
3054
|
const pending = /* @__PURE__ */ new Map();
|
|
2854
3055
|
/** alias → SSH login name (fetched once for the user@alias prompt). */
|
|
2855
3056
|
let hostsCache = null;
|
|
3057
|
+
/** At most one visible non-interactive failure dialog per alias. */
|
|
3058
|
+
const failureDialogs = /* @__PURE__ */ new Map();
|
|
2856
3059
|
async function hostsUser(api, alias) {
|
|
2857
3060
|
if (hostsCache === null) try {
|
|
2858
3061
|
const list = await api.listHosts();
|
|
2859
3062
|
hostsCache = new Map(list.map((host) => [host.alias, host.user]));
|
|
2860
3063
|
} catch {
|
|
2861
|
-
|
|
3064
|
+
return;
|
|
2862
3065
|
}
|
|
2863
3066
|
return hostsCache.get(alias);
|
|
2864
3067
|
}
|
|
3068
|
+
/** Whether the alias passed the gate within its TTL. */
|
|
2865
3069
|
function gateActive(alias, now) {
|
|
2866
|
-
const at =
|
|
3070
|
+
const at = passedAt.get(alias);
|
|
2867
3071
|
return at !== void 0 && now - at < GATE_TTL_MS;
|
|
2868
3072
|
}
|
|
2869
|
-
/**
|
|
2870
|
-
*
|
|
2871
|
-
*
|
|
2872
|
-
*
|
|
2873
|
-
*
|
|
3073
|
+
/**
|
|
3074
|
+
* Mount a dialog component into a fresh root; returns its closer. The dialog's
|
|
3075
|
+
* `onClose` prop is CHAINED: the caller's handler (resolving its promise) runs
|
|
3076
|
+
* first, then the root is torn down. Without the chaining a cancel would
|
|
3077
|
+
* unmount the dialog but never resolve the caller's promise, leaving the alias
|
|
3078
|
+
* stuck "in flight" so later calls stop prompting.
|
|
3079
|
+
*/
|
|
2874
3080
|
function mountModal(node) {
|
|
2875
3081
|
const host = document.createElement("div");
|
|
2876
3082
|
host.dataset.sshGateDialog = "";
|
|
@@ -2891,18 +3097,20 @@ window.__ModuleLoader__.load({
|
|
|
2891
3097
|
}));
|
|
2892
3098
|
return close;
|
|
2893
3099
|
}
|
|
2894
|
-
/**
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
if (
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
}
|
|
3100
|
+
/** Log and surface one concrete non-interactive connection failure. */
|
|
3101
|
+
function reportConnectionFailure(alias, detail) {
|
|
3102
|
+
console.warn(`[dsh-hardssh] SSH probe failed for '${alias}': ${detail}`);
|
|
3103
|
+
if (typeof document === "undefined") return false;
|
|
3104
|
+
failureDialogs.get(alias)?.();
|
|
3105
|
+
const close = mountModal((0, react.createElement)(ConnectionErrorDialog, {
|
|
3106
|
+
alias,
|
|
3107
|
+
detail,
|
|
3108
|
+
onClose: () => {
|
|
3109
|
+
failureDialogs.delete(alias);
|
|
3110
|
+
}
|
|
3111
|
+
}));
|
|
3112
|
+
failureDialogs.set(alias, close);
|
|
3113
|
+
return false;
|
|
2906
3114
|
}
|
|
2907
3115
|
/** Interactive prompts, resolving true when the user finished the action. */
|
|
2908
3116
|
function promptFingerprint(api, alias, fingerprintSha256, mismatch) {
|
|
@@ -2922,7 +3130,7 @@ window.__ModuleLoader__.load({
|
|
|
2922
3130
|
}));
|
|
2923
3131
|
});
|
|
2924
3132
|
}
|
|
2925
|
-
function promptSecret(api, alias, secret) {
|
|
3133
|
+
function promptSecret(api, alias, secret, reason) {
|
|
2926
3134
|
return new Promise((resolve) => {
|
|
2927
3135
|
let closed = false;
|
|
2928
3136
|
const finish = (ok) => {
|
|
@@ -2936,6 +3144,7 @@ window.__ModuleLoader__.load({
|
|
|
2936
3144
|
alias,
|
|
2937
3145
|
user,
|
|
2938
3146
|
secret,
|
|
3147
|
+
reason,
|
|
2939
3148
|
onClose: () => {
|
|
2940
3149
|
finish(false);
|
|
2941
3150
|
},
|
|
@@ -2948,39 +3157,56 @@ window.__ModuleLoader__.load({
|
|
|
2948
3157
|
});
|
|
2949
3158
|
}
|
|
2950
3159
|
/**
|
|
2951
|
-
* Probe + prompt until connected. Resolves true when the alias may proceed
|
|
2952
|
-
*
|
|
2953
|
-
*
|
|
3160
|
+
* Probe + prompt until connected. Resolves true when the alias may proceed;
|
|
3161
|
+
* false when the user cancelled or the failure is not interactive. When a
|
|
3162
|
+
* credential was already supplied and the retry still fails (wrong password,
|
|
3163
|
+
* unreachable host, network drop, …), the password dialog re-opens WITH the
|
|
3164
|
+
* failure reason shown, VSCode style — no silent "stuck" states.
|
|
3165
|
+
*
|
|
3166
|
+
* @param api - the SSH API client.
|
|
3167
|
+
* @param alias - the host alias to gate.
|
|
2954
3168
|
*/
|
|
2955
|
-
function
|
|
3169
|
+
function connectHost(api, alias) {
|
|
2956
3170
|
if (gateActive(alias, Date.now())) return Promise.resolve(true);
|
|
2957
3171
|
const inFlight = pending.get(alias);
|
|
2958
3172
|
if (inFlight !== void 0) return inFlight;
|
|
2959
3173
|
const attempt = (async () => {
|
|
3174
|
+
let secretKind;
|
|
3175
|
+
let lastFailure = null;
|
|
2960
3176
|
for (let i = 0; i < MAX_ATTEMPTS; i++) try {
|
|
2961
3177
|
const result = await api.testHost(alias);
|
|
2962
3178
|
if (result.ok) {
|
|
2963
|
-
|
|
3179
|
+
passedAt.set(alias, Date.now());
|
|
2964
3180
|
return true;
|
|
2965
3181
|
}
|
|
2966
3182
|
if (result.code === "NEEDS_PASSWORD" && (result.secret === "password" || result.secret === "passphrase")) {
|
|
2967
|
-
|
|
3183
|
+
secretKind = result.secret;
|
|
3184
|
+
if (await promptSecret(api, alias, result.secret, lastFailure ?? void 0)) {
|
|
3185
|
+
lastFailure = null;
|
|
3186
|
+
continue;
|
|
3187
|
+
}
|
|
2968
3188
|
return false;
|
|
2969
3189
|
}
|
|
2970
|
-
|
|
2971
|
-
return
|
|
3190
|
+
lastFailure = result.error ?? "connection failed";
|
|
3191
|
+
return reportConnectionFailure(alias, lastFailure);
|
|
2972
3192
|
} catch (cause) {
|
|
2973
|
-
if (cause instanceof
|
|
3193
|
+
if (cause instanceof HttpApiError && (cause.code === "HOST_KEY_UNKNOWN" || cause.code === "HOST_KEY_MISMATCH")) {
|
|
2974
3194
|
const fingerprint = cause.hostKeyFingerprint ?? "";
|
|
2975
3195
|
if (fingerprint !== "" && await promptFingerprint(api, alias, fingerprint, cause.code === "HOST_KEY_MISMATCH")) continue;
|
|
2976
|
-
if (fingerprint === "")
|
|
3196
|
+
if (fingerprint === "") return reportConnectionFailure(alias, cause.message);
|
|
3197
|
+
return false;
|
|
3198
|
+
}
|
|
3199
|
+
lastFailure = cause instanceof Error ? cause.message : String(cause);
|
|
3200
|
+
if (secretKind !== void 0) {
|
|
3201
|
+
if (await promptSecret(api, alias, secretKind, lastFailure)) {
|
|
3202
|
+
lastFailure = null;
|
|
3203
|
+
continue;
|
|
3204
|
+
}
|
|
2977
3205
|
return false;
|
|
2978
3206
|
}
|
|
2979
|
-
|
|
2980
|
-
return false;
|
|
3207
|
+
return reportConnectionFailure(alias, lastFailure);
|
|
2981
3208
|
}
|
|
2982
|
-
|
|
2983
|
-
return false;
|
|
3209
|
+
return reportConnectionFailure(alias, lastFailure ?? tt$1("gate.retryLimit"));
|
|
2984
3210
|
})();
|
|
2985
3211
|
pending.set(alias, attempt);
|
|
2986
3212
|
attempt.finally(() => {
|
|
@@ -2988,68 +3214,199 @@ window.__ModuleLoader__.load({
|
|
|
2988
3214
|
});
|
|
2989
3215
|
return attempt;
|
|
2990
3216
|
}
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
3217
|
+
//#endregion
|
|
3218
|
+
//#region src/client/workspace-badges.ts
|
|
3219
|
+
/**
|
|
3220
|
+
* Sidebar workspace-row augmentation: labels every host workspace row whose
|
|
3221
|
+
* title matches an SSH-bound workspace with a compact remote badge
|
|
3222
|
+
* (`alias @ remoteRoot`), so a glance tells a local workspace from a remote
|
|
3223
|
+
* one. Pure DOM-level extension (the sidebar shell exposes no slot for row
|
|
3224
|
+
* decoration), following the dsh-ssh sidebar-entry precedent: a
|
|
3225
|
+
* MutationObserver self-heals when React re-renders displace the injected
|
|
3226
|
+
* badge.
|
|
3227
|
+
*
|
|
3228
|
+
* The row title alone matches: SSH-bound workspace titles are unique enough
|
|
3229
|
+
* in practice (the ledger title is what the row shows). A title match against
|
|
3230
|
+
* `~/.dsh/ssh-workspaces/<id>` anchors also works when the host picks that
|
|
3231
|
+
* default title.
|
|
3232
|
+
*
|
|
3233
|
+
* @module dsh-hardssh/client/workspace-badges
|
|
3234
|
+
*/
|
|
3235
|
+
const PROJECT_ROW_SELECTOR = "[class*=\"projectRow\"]";
|
|
3236
|
+
const TITLE_SELECTOR = "[class*=\"title\"]";
|
|
3237
|
+
/** Render a small remote badge element. The cloud icon marks "remote";
|
|
3238
|
+
* connected servers render in DeepSeek blue, disconnected stay gray. The
|
|
3239
|
+
* tooltip shows the REMOTE directory plus the server, e.g.
|
|
3240
|
+
* `/data/app (prod-01)`. */
|
|
3241
|
+
function makeBadge(alias, remoteRoot, connected) {
|
|
3242
|
+
const badge = document.createElement("span");
|
|
3243
|
+
badge.dataset.sshBadge = connected ? "connected" : "disconnected";
|
|
3244
|
+
badge.title = `${remoteRoot}(${alias})`;
|
|
3245
|
+
badge.setAttribute("style", connected ? "display:inline-flex;align-items:center;gap:3px;margin-left:6px;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:16px;color:#1476E6;border:1px solid #1476E6;border-radius:999px;padding:0 6px;background:#D4E0F7;flex:none" : "display:inline-flex;align-items:center;gap:3px;margin-left:6px;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,rgba(128,128,128,.75));border:1px solid var(--dsw-alias-line-secondary,rgba(128,128,128,.28));border-radius:999px;padding:0 6px;background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.10));flex:none");
|
|
3246
|
+
const icon = document.createElement("span");
|
|
3247
|
+
icon.setAttribute("style", "display:inline-flex;flex:none;line-height:1");
|
|
3248
|
+
icon.innerHTML = "<svg viewBox=\"0 0 1024 1024\" width=\"11\" height=\"11\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M743.936 758.499556h-273.066667c-17.066667 0-34.133333-17.066667-34.133333-34.133334s17.066667-34.133333 34.133333-34.133333h273.066667c28.444444 0 153.6-5.688889 153.6-130.844445 0-142.222222-153.6-164.977778-159.288889-164.977777-17.066667 0-28.444444-11.377778-28.444444-22.755556-22.755556-85.333333-108.088889-142.222222-193.422223-130.844444-136.533333 0-176.355556 79.644444-199.111111 164.977777-5.688889 5.688889-22.755556 17.066667-39.822222 17.066667 0 0-153.6 5.688889-153.6 142.222222 0 119.466667 130.844444 125.155556 153.6 125.155556 17.066667 0 34.133333 17.066667 34.133333 34.133333s-17.066667 34.133333-34.133333 34.133334c-113.777778 0-227.555556-62.577778-227.555555-193.422223 0-142.222222 119.466667-204.8 199.111111-210.488889 22.755556-68.266667 79.644444-193.422222 261.688889-193.422222 113.777778-11.377778 216.177778 56.888889 256 164.977778 79.644444 17.066667 199.111111 79.644444 199.111111 233.244444 5.688889 136.533333-108.088889 199.111111-221.866667 199.111112z\"/><path d=\"M573.269333 866.588444c-11.377778 0-17.066667-5.688889-22.755555-11.377777l-108.088889-108.088889c-5.688889-5.688889-11.377778-17.066667-11.377778-22.755556s5.688889-17.066667 11.377778-22.755555l108.088889-108.088889c11.377778-11.377778 34.133333-11.377778 51.2 0 11.377778 11.377778 11.377778 34.133333 0 51.2l-79.644445 79.644444 79.644445 79.644445c11.377778 11.377778 11.377778 34.133333 0 51.2-5.688889 5.688889-17.066667 11.377778-28.444445 11.377777z\"/></svg>";
|
|
3249
|
+
const label = document.createElement("span");
|
|
3250
|
+
label.textContent = alias;
|
|
3251
|
+
label.setAttribute("style", "font-weight:400");
|
|
3252
|
+
badge.append(icon, label);
|
|
3253
|
+
return badge;
|
|
2995
3254
|
}
|
|
2996
3255
|
/**
|
|
2997
|
-
* Mount the
|
|
2998
|
-
* to an SSH
|
|
2999
|
-
*
|
|
3256
|
+
* Mount the row-decoration pass. Queries the sidebar for project rows and
|
|
3257
|
+
* appends a remote badge when the row's title belongs to an SSH workspace.
|
|
3258
|
+
* Badges of aliases in `connected` render blue, others gray. Self-heals via
|
|
3259
|
+
* MutationObserver on the sidebar container (rows re-render on
|
|
3260
|
+
* session/workspace changes).
|
|
3261
|
+
* @param workspaces - the SSH workspace records (id/title/alias/remoteRoot).
|
|
3262
|
+
* @param connected - aliases with a live pooled connection (badge coloring).
|
|
3263
|
+
* @returns disposer.
|
|
3000
3264
|
*/
|
|
3001
|
-
function
|
|
3265
|
+
function mountWorkspaceBadges(workspaces, connected) {
|
|
3002
3266
|
const byTitle = /* @__PURE__ */ new Map();
|
|
3003
|
-
for (const workspace of workspaces) byTitle.set(workspace.title, workspace
|
|
3004
|
-
|
|
3005
|
-
|
|
3267
|
+
for (const workspace of workspaces) byTitle.set(workspace.title, workspace);
|
|
3268
|
+
/** Decorate one project row: ALWAYS keep the row tooltip = the REMOTE
|
|
3269
|
+
* directory (server), never the local anchor path the shell would show;
|
|
3270
|
+
* mount the badge once (idempotent via the guard). Re-runs on every scan,
|
|
3271
|
+
* so a shell re-render that resets <title> gets corrected. */
|
|
3272
|
+
const decorate = (row) => {
|
|
3006
3273
|
const titleEl = row.querySelector(TITLE_SELECTOR);
|
|
3007
3274
|
if (titleEl === null || titleEl === void 0) return;
|
|
3008
|
-
const
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
if (ok) row.click();
|
|
3017
|
-
});
|
|
3018
|
-
}, true);
|
|
3275
|
+
const title = titleEl.textContent?.trim() ?? "";
|
|
3276
|
+
const target = byTitle.get(title);
|
|
3277
|
+
if (target === void 0) return;
|
|
3278
|
+
const desiredTitle = `${target.remoteRoot}(${target.alias})`;
|
|
3279
|
+
if (row.title !== desiredTitle) row.title = desiredTitle;
|
|
3280
|
+
if (row.querySelector("[data-ssh-badge]") !== null) return;
|
|
3281
|
+
const isConnected = connected?.has(target.alias) ?? false;
|
|
3282
|
+
titleEl.appendChild(makeBadge(target.alias, target.remoteRoot, isConnected));
|
|
3019
3283
|
};
|
|
3284
|
+
/** Scan the sidebar column for project rows and decorate each. */
|
|
3020
3285
|
const scan = () => {
|
|
3021
|
-
for (const row of document.querySelectorAll(PROJECT_ROW_SELECTOR))
|
|
3286
|
+
for (const row of document.querySelectorAll(PROJECT_ROW_SELECTOR)) decorate(row);
|
|
3022
3287
|
};
|
|
3023
3288
|
const observer = new MutationObserver(() => {
|
|
3024
3289
|
scan();
|
|
3025
3290
|
});
|
|
3026
3291
|
observer.observe(document.body, {
|
|
3027
3292
|
childList: true,
|
|
3028
|
-
subtree: true
|
|
3293
|
+
subtree: true,
|
|
3294
|
+
attributes: true,
|
|
3295
|
+
attributeFilter: ["title"]
|
|
3029
3296
|
});
|
|
3030
3297
|
scan();
|
|
3031
3298
|
return () => {
|
|
3032
3299
|
observer.disconnect();
|
|
3033
|
-
for (const
|
|
3300
|
+
for (const badge of document.querySelectorAll("[data-ssh-badge]")) badge.remove();
|
|
3301
|
+
};
|
|
3302
|
+
}
|
|
3303
|
+
//#endregion
|
|
3304
|
+
//#region src/client/session-connect-gate.ts
|
|
3305
|
+
/** Normalize one path for anchor comparison: unified separators, no trailing
|
|
3306
|
+
* separator, and case-folded for Windows-shaped paths (Windows anchors are
|
|
3307
|
+
* case-insensitive; POSIX anchors are not).
|
|
3308
|
+
*
|
|
3309
|
+
* Deliberately a SECOND implementation of `base/ledger.ts`'s
|
|
3310
|
+
* `normalizeAnchorPath`: the base module imports `node:fs` for its own
|
|
3311
|
+
* persistence and therefore cannot be pulled into the browser bundle. Keep the
|
|
3312
|
+
* two in step — the host-side duplicates were consolidated for exactly this
|
|
3313
|
+
* reason, and this one is the documented exception. */
|
|
3314
|
+
function normalizeAnchor(path) {
|
|
3315
|
+
const unified = path.replace(/\\/gu, "/").replace(/\/+$/u, "");
|
|
3316
|
+
if (/^[a-zA-Z]:\//u.test(unified) || unified.startsWith("//")) return unified.toLowerCase();
|
|
3317
|
+
return unified;
|
|
3318
|
+
}
|
|
3319
|
+
/** True when `cwd` is the anchor itself or sits inside it. */
|
|
3320
|
+
function isUnderAnchor(anchor, cwd) {
|
|
3321
|
+
return cwd === anchor || cwd.startsWith(`${anchor}/`);
|
|
3322
|
+
}
|
|
3323
|
+
/**
|
|
3324
|
+
* Build the cwd → workspace resolver over a live workspace list. The LONGEST
|
|
3325
|
+
* matching anchor wins, so a workspace nested under another one still routes to
|
|
3326
|
+
* its own server.
|
|
3327
|
+
* @param workspaces - live records carrying an anchor path.
|
|
3328
|
+
* @returns resolver returning the owning record, or undefined when unbound.
|
|
3329
|
+
*/
|
|
3330
|
+
function makeAnchorWorkspaceResolver(workspaces) {
|
|
3331
|
+
return (cwd) => {
|
|
3332
|
+
if (cwd === void 0 || cwd === "") return void 0;
|
|
3333
|
+
const target = normalizeAnchor(cwd);
|
|
3334
|
+
let best;
|
|
3335
|
+
let bestLength = -1;
|
|
3336
|
+
for (const workspace of workspaces()) {
|
|
3337
|
+
const anchor = normalizeAnchor(workspace.anchorPath);
|
|
3338
|
+
if (anchor === "" || !isUnderAnchor(anchor, target)) continue;
|
|
3339
|
+
if (anchor.length > bestLength) {
|
|
3340
|
+
bestLength = anchor.length;
|
|
3341
|
+
best = workspace;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
return best;
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
/** Alias-only compatibility face used by the connection gate. */
|
|
3348
|
+
function makeAnchorAliasResolver(workspaces) {
|
|
3349
|
+
const resolve = makeAnchorWorkspaceResolver(workspaces);
|
|
3350
|
+
return (cwd) => resolve(cwd)?.alias;
|
|
3351
|
+
}
|
|
3352
|
+
/**
|
|
3353
|
+
* Watch the session list and probe the server behind every SSH-bound session
|
|
3354
|
+
* the operator opens or creates.
|
|
3355
|
+
*
|
|
3356
|
+
* The first pass ADOPTS the existing list without probing: a GUI restore must
|
|
3357
|
+
* not fire one connection dialog per remembered session. Only the session the
|
|
3358
|
+
* shell actually selects (including the restored current one) and sessions
|
|
3359
|
+
* created after that pass trigger a probe.
|
|
3360
|
+
*
|
|
3361
|
+
* @param deps - session list view, anchor resolver and probe.
|
|
3362
|
+
* @returns disposer removing the subscription.
|
|
3363
|
+
*/
|
|
3364
|
+
function mountSessionConnectGate(deps) {
|
|
3365
|
+
const adopted = /* @__PURE__ */ new Set();
|
|
3366
|
+
let primed = false;
|
|
3367
|
+
let lastCurrent;
|
|
3368
|
+
/** Probe the server owning one session's workspace (best-effort). */
|
|
3369
|
+
const probe = (sessionId) => {
|
|
3370
|
+
if (sessionId === void 0) return;
|
|
3371
|
+
const alias = deps.aliasForCwd(deps.sessions.sessionCwd(sessionId));
|
|
3372
|
+
if (alias === void 0) return;
|
|
3373
|
+
deps.ensureConnected(alias).catch((error) => {
|
|
3374
|
+
console.warn("[dsh-hardssh] session connect gate failed:", error);
|
|
3375
|
+
});
|
|
3376
|
+
};
|
|
3377
|
+
const pass = () => {
|
|
3378
|
+
if (!primed && deps.sessions.phase() !== "ready") return;
|
|
3379
|
+
const ids = deps.sessions.sessionIds();
|
|
3380
|
+
const probed = /* @__PURE__ */ new Set();
|
|
3381
|
+
if (!primed) {
|
|
3382
|
+
for (const id of ids) adopted.add(id);
|
|
3383
|
+
primed = true;
|
|
3384
|
+
} else for (const id of ids) {
|
|
3385
|
+
if (adopted.has(id)) continue;
|
|
3386
|
+
adopted.add(id);
|
|
3387
|
+
probed.add(id);
|
|
3388
|
+
probe(id);
|
|
3389
|
+
}
|
|
3390
|
+
const current = deps.sessions.currentSessionId();
|
|
3391
|
+
if (current !== lastCurrent) {
|
|
3392
|
+
lastCurrent = current;
|
|
3393
|
+
if (current !== void 0 && !probed.has(current)) probe(current);
|
|
3394
|
+
}
|
|
3034
3395
|
};
|
|
3396
|
+
const dispose = deps.sessions.subscribe(pass);
|
|
3397
|
+
pass();
|
|
3398
|
+
return dispose;
|
|
3035
3399
|
}
|
|
3036
3400
|
//#endregion
|
|
3037
3401
|
//#region src/client/ssh/panel/ClusterTab.tsx
|
|
3038
3402
|
/**
|
|
3039
|
-
* Cluster tab: run one command
|
|
3040
|
-
*
|
|
3403
|
+
* Cluster tab: run one command against the selected Session's fixed server and
|
|
3404
|
+
* inspect its result. Stdout/stderr render in
|
|
3041
3405
|
* collapsed <details> blocks; status renders as a colored badge.
|
|
3042
3406
|
*/
|
|
3043
|
-
/** Split a comma-separated input into a trimmed, non-empty string list. */
|
|
3044
|
-
function splitList(text) {
|
|
3045
|
-
return text.split(",").map((part) => part.trim()).filter((part) => part !== "");
|
|
3046
|
-
}
|
|
3047
3407
|
/** The cluster execution tab. */
|
|
3048
|
-
function ClusterTab({ api }) {
|
|
3408
|
+
function ClusterTab({ api, alias }) {
|
|
3049
3409
|
const [command, setCommand] = (0, react.useState)("");
|
|
3050
|
-
const [aliases, setAliases] = (0, react.useState)("");
|
|
3051
|
-
const [environment, setEnvironment] = (0, react.useState)("");
|
|
3052
|
-
const [tags, setTags] = (0, react.useState)("");
|
|
3053
3410
|
const [running, setRunning] = (0, react.useState)(false);
|
|
3054
3411
|
const [results, setResults] = (0, react.useState)(null);
|
|
3055
3412
|
const [error, setError] = (0, react.useState)(null);
|
|
@@ -3059,13 +3416,9 @@ window.__ModuleLoader__.load({
|
|
|
3059
3416
|
setRunning(true);
|
|
3060
3417
|
setError(null);
|
|
3061
3418
|
try {
|
|
3062
|
-
const aliasList = splitList(aliases);
|
|
3063
|
-
const tagList = splitList(tags);
|
|
3064
3419
|
const outcome = await api.cluster({
|
|
3065
3420
|
command: command.trim(),
|
|
3066
|
-
aliases:
|
|
3067
|
-
environment: environment.trim() === "" ? void 0 : environment.trim(),
|
|
3068
|
-
tags: tagList.length > 0 ? tagList : void 0
|
|
3421
|
+
aliases: [alias]
|
|
3069
3422
|
});
|
|
3070
3423
|
setResults(outcome);
|
|
3071
3424
|
} catch (cause) {
|
|
@@ -3094,421 +3447,93 @@ window.__ModuleLoader__.load({
|
|
|
3094
3447
|
})]
|
|
3095
3448
|
}),
|
|
3096
3449
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3097
|
-
className: panel_module_css_default.
|
|
3098
|
-
children: [
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
}
|
|
3106
|
-
}),
|
|
3107
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3108
|
-
className: panel_module_css_default.input,
|
|
3109
|
-
placeholder: tt("cluster.environment"),
|
|
3110
|
-
value: environment,
|
|
3111
|
-
onChange: (event) => {
|
|
3112
|
-
setEnvironment(event.target.value);
|
|
3113
|
-
}
|
|
3114
|
-
}),
|
|
3115
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3116
|
-
className: panel_module_css_default.input,
|
|
3117
|
-
placeholder: tt("cluster.tags"),
|
|
3118
|
-
value: tags,
|
|
3119
|
-
onChange: (event) => {
|
|
3120
|
-
setTags(event.target.value);
|
|
3121
|
-
}
|
|
3122
|
-
})
|
|
3123
|
-
]
|
|
3124
|
-
}),
|
|
3125
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3126
|
-
type: "button",
|
|
3127
|
-
className: panel_module_css_default.primaryButton,
|
|
3128
|
-
disabled: running || command.trim() === "",
|
|
3129
|
-
onClick: () => {
|
|
3130
|
-
run();
|
|
3131
|
-
},
|
|
3132
|
-
children: tt("cluster.run")
|
|
3133
|
-
}) })
|
|
3134
|
-
]
|
|
3135
|
-
}),
|
|
3136
|
-
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3137
|
-
className: panel_module_css_default.banner,
|
|
3138
|
-
"data-kind": "error",
|
|
3139
|
-
children: tt("common.error", { error })
|
|
3140
|
-
}),
|
|
3141
|
-
results === null && error === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3142
|
-
className: panel_module_css_default.empty,
|
|
3143
|
-
children: tt("cluster.empty")
|
|
3144
|
-
}),
|
|
3145
|
-
results !== null && results.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3146
|
-
className: panel_module_css_default.empty,
|
|
3147
|
-
children: tt("cluster.noMatch")
|
|
3148
|
-
}),
|
|
3149
|
-
results !== null && results.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3150
|
-
className: panel_module_css_default.tableWrap,
|
|
3151
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
3152
|
-
className: panel_module_css_default.table,
|
|
3153
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
3154
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.alias") }),
|
|
3155
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.status") }),
|
|
3156
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.exit") }),
|
|
3157
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.stdout") }),
|
|
3158
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.stderr") }),
|
|
3159
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.error") }),
|
|
3160
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.duration") })
|
|
3161
|
-
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: results.map((result) => {
|
|
3162
|
-
const status = result.ok ? "ok" : result.timedOut === true ? "timeout" : "fail";
|
|
3163
|
-
const label = result.ok ? tt("cluster.ok") : result.timedOut === true ? tt("cluster.timeout") : tt("cluster.fail");
|
|
3164
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
3165
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3166
|
-
className: panel_module_css_default.mono,
|
|
3167
|
-
children: result.alias
|
|
3168
|
-
}),
|
|
3169
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3170
|
-
className: panel_module_css_default.badge,
|
|
3171
|
-
"data-status": status,
|
|
3172
|
-
children: label
|
|
3173
|
-
}) }),
|
|
3174
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3175
|
-
className: panel_module_css_default.mono,
|
|
3176
|
-
children: result.exitCode ?? "-"
|
|
3177
|
-
}),
|
|
3178
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: result.stdout !== void 0 && result.stdout !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
3179
|
-
className: panel_module_css_default.cellDetails,
|
|
3180
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: tt("cluster.col.stdout") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
3181
|
-
className: panel_module_css_default.cellPre,
|
|
3182
|
-
children: result.stdout
|
|
3183
|
-
})]
|
|
3184
|
-
}) }),
|
|
3185
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: result.stderr !== void 0 && result.stderr !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
3186
|
-
className: panel_module_css_default.cellDetails,
|
|
3187
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: tt("cluster.col.stderr") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
3188
|
-
className: panel_module_css_default.cellPre,
|
|
3189
|
-
children: result.stderr
|
|
3190
|
-
})]
|
|
3191
|
-
}) }),
|
|
3192
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3193
|
-
className: panel_module_css_default.cellMuted,
|
|
3194
|
-
children: result.error ?? ""
|
|
3195
|
-
}),
|
|
3196
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3197
|
-
className: panel_module_css_default.mono,
|
|
3198
|
-
children: result.durationMs !== void 0 ? result.durationMs + " ms" : "-"
|
|
3199
|
-
})
|
|
3200
|
-
] }, result.alias);
|
|
3201
|
-
}) })]
|
|
3202
|
-
})
|
|
3203
|
-
})
|
|
3204
|
-
]
|
|
3205
|
-
});
|
|
3206
|
-
}
|
|
3207
|
-
//#endregion
|
|
3208
|
-
//#region src/client/ssh/panel/HostsTab.tsx
|
|
3209
|
-
/**
|
|
3210
|
-
* Hosts tab: the host table with search (debounced through listHosts),
|
|
3211
|
-
* add/edit/delete/test actions, ~/.ssh/config import, and a connect action
|
|
3212
|
-
* that hands the alias to the terminal tab via onConnect.
|
|
3213
|
-
*/
|
|
3214
|
-
/** The hosts table plus its toolbar and dialogs. */
|
|
3215
|
-
function HostsTab({ api, onConnect }) {
|
|
3216
|
-
const [hosts, setHosts] = (0, react.useState)(null);
|
|
3217
|
-
const [error, setError] = (0, react.useState)(null);
|
|
3218
|
-
const [search, setSearch] = (0, react.useState)("");
|
|
3219
|
-
const [testingAlias, setTestingAlias] = (0, react.useState)(null);
|
|
3220
|
-
const [testResults, setTestResults] = (0, react.useState)({});
|
|
3221
|
-
const [importing, setImporting] = (0, react.useState)(false);
|
|
3222
|
-
const [notice, setNotice] = (0, react.useState)(null);
|
|
3223
|
-
const [dialog, setDialog] = (0, react.useState)(null);
|
|
3224
|
-
const [hostKeyConfirm, setHostKeyConfirm] = (0, react.useState)(null);
|
|
3225
|
-
const [secretConfirm, setSecretConfirm] = (0, react.useState)(null);
|
|
3226
|
-
const seqRef = (0, react.useRef)(0);
|
|
3227
|
-
const load = (0, react.useCallback)(async (query) => {
|
|
3228
|
-
const seq = ++seqRef.current;
|
|
3229
|
-
try {
|
|
3230
|
-
const list = await api.listHosts(query);
|
|
3231
|
-
if (seq !== seqRef.current) return;
|
|
3232
|
-
setHosts(list);
|
|
3233
|
-
setError(null);
|
|
3234
|
-
} catch (cause) {
|
|
3235
|
-
if (seq !== seqRef.current) return;
|
|
3236
|
-
setError(errorMessage(cause));
|
|
3237
|
-
}
|
|
3238
|
-
}, [api]);
|
|
3239
|
-
(0, react.useEffect)(() => {
|
|
3240
|
-
load();
|
|
3241
|
-
}, [load]);
|
|
3242
|
-
(0, react.useEffect)(() => {
|
|
3243
|
-
const timer = setTimeout(() => {
|
|
3244
|
-
const query = search.trim();
|
|
3245
|
-
load(query === "" ? void 0 : query);
|
|
3246
|
-
}, 300);
|
|
3247
|
-
return () => {
|
|
3248
|
-
clearTimeout(timer);
|
|
3249
|
-
};
|
|
3250
|
-
}, [search, load]);
|
|
3251
|
-
const runTest = async (alias) => {
|
|
3252
|
-
setTestingAlias(alias);
|
|
3253
|
-
try {
|
|
3254
|
-
const result = await api.testHost(alias);
|
|
3255
|
-
if (result.code === "NEEDS_PASSWORD" && (result.secret === "password" || result.secret === "passphrase")) {
|
|
3256
|
-
setSecretConfirm({
|
|
3257
|
-
alias,
|
|
3258
|
-
secret: result.secret,
|
|
3259
|
-
user: hosts?.find((host) => host.alias === alias)?.user
|
|
3260
|
-
});
|
|
3261
|
-
setTestingAlias(null);
|
|
3262
|
-
return;
|
|
3263
|
-
}
|
|
3264
|
-
setTestResults((prev) => ({
|
|
3265
|
-
...prev,
|
|
3266
|
-
[alias]: result
|
|
3267
|
-
}));
|
|
3268
|
-
} catch (cause) {
|
|
3269
|
-
if (cause instanceof SshApiError && (cause.code === "HOST_KEY_UNKNOWN" || cause.code === "HOST_KEY_MISMATCH")) {
|
|
3270
|
-
setHostKeyConfirm({
|
|
3271
|
-
alias,
|
|
3272
|
-
fingerprintSha256: cause.hostKeyFingerprint ?? "",
|
|
3273
|
-
mismatch: cause.code === "HOST_KEY_MISMATCH"
|
|
3274
|
-
});
|
|
3275
|
-
setTestingAlias(null);
|
|
3276
|
-
return;
|
|
3277
|
-
}
|
|
3278
|
-
setTestResults((prev) => ({
|
|
3279
|
-
...prev,
|
|
3280
|
-
[alias]: {
|
|
3281
|
-
ok: false,
|
|
3282
|
-
error: errorMessage(cause)
|
|
3283
|
-
}
|
|
3284
|
-
}));
|
|
3285
|
-
} finally {
|
|
3286
|
-
setTestingAlias(null);
|
|
3287
|
-
}
|
|
3288
|
-
};
|
|
3289
|
-
/** After the fingerprint is confirmed (or reset), retry the test. */
|
|
3290
|
-
const retryAfterFingerprint = async (alias) => {
|
|
3291
|
-
setHostKeyConfirm(null);
|
|
3292
|
-
await runTest(alias);
|
|
3293
|
-
};
|
|
3294
|
-
/** After the session secret is provided, retry the test. */
|
|
3295
|
-
const retryAfterSecret = async (alias) => {
|
|
3296
|
-
setSecretConfirm(null);
|
|
3297
|
-
await runTest(alias);
|
|
3298
|
-
};
|
|
3299
|
-
const deleteHost = async (alias) => {
|
|
3300
|
-
if (!window.confirm(tt("hosts.deleteConfirm", { alias }))) return;
|
|
3301
|
-
try {
|
|
3302
|
-
await api.deleteHost(alias);
|
|
3303
|
-
load();
|
|
3304
|
-
} catch (cause) {
|
|
3305
|
-
setError(errorMessage(cause));
|
|
3306
|
-
}
|
|
3307
|
-
};
|
|
3308
|
-
const importConfig = async () => {
|
|
3309
|
-
setImporting(true);
|
|
3310
|
-
try {
|
|
3311
|
-
const result = await api.importSshConfig();
|
|
3312
|
-
setNotice(tt("hosts.imported", {
|
|
3313
|
-
parsed: result.parsed,
|
|
3314
|
-
added: result.added,
|
|
3315
|
-
skipped: result.skipped
|
|
3316
|
-
}));
|
|
3317
|
-
load();
|
|
3318
|
-
} catch (cause) {
|
|
3319
|
-
setError(errorMessage(cause));
|
|
3320
|
-
} finally {
|
|
3321
|
-
setImporting(false);
|
|
3322
|
-
}
|
|
3323
|
-
};
|
|
3324
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3325
|
-
className: panel_module_css_default.fillBody,
|
|
3326
|
-
children: [
|
|
3327
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3328
|
-
className: panel_module_css_default.toolbar,
|
|
3329
|
-
children: [
|
|
3330
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3331
|
-
className: panel_module_css_default.search,
|
|
3332
|
-
type: "search",
|
|
3333
|
-
placeholder: tt("hosts.search"),
|
|
3334
|
-
value: search,
|
|
3335
|
-
onChange: (event) => {
|
|
3336
|
-
setSearch(event.target.value);
|
|
3337
|
-
}
|
|
3338
|
-
}),
|
|
3339
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: panel_module_css_default.toolbarSpacer }),
|
|
3340
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3341
|
-
type: "button",
|
|
3342
|
-
className: panel_module_css_default.primaryButton,
|
|
3343
|
-
onClick: () => {
|
|
3344
|
-
setDialog({ mode: "create" });
|
|
3345
|
-
},
|
|
3346
|
-
children: tt("hosts.add")
|
|
3450
|
+
className: panel_module_css_default.controls,
|
|
3451
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3452
|
+
className: panel_module_css_default.fieldLabel,
|
|
3453
|
+
children: tt("session.server")
|
|
3454
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3455
|
+
className: panel_module_css_default.targetBadge,
|
|
3456
|
+
children: alias
|
|
3457
|
+
})]
|
|
3347
3458
|
}),
|
|
3348
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3459
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3349
3460
|
type: "button",
|
|
3350
|
-
className: panel_module_css_default.
|
|
3351
|
-
disabled:
|
|
3461
|
+
className: panel_module_css_default.primaryButton,
|
|
3462
|
+
disabled: running || command.trim() === "",
|
|
3352
3463
|
onClick: () => {
|
|
3353
|
-
|
|
3464
|
+
run();
|
|
3354
3465
|
},
|
|
3355
|
-
children:
|
|
3356
|
-
})
|
|
3466
|
+
children: tt("cluster.run")
|
|
3467
|
+
}) })
|
|
3357
3468
|
]
|
|
3358
3469
|
}),
|
|
3359
|
-
notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3360
|
-
className: panel_module_css_default.banner,
|
|
3361
|
-
"data-kind": "ok",
|
|
3362
|
-
children: notice
|
|
3363
|
-
}),
|
|
3364
3470
|
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3365
3471
|
className: panel_module_css_default.banner,
|
|
3366
3472
|
"data-kind": "error",
|
|
3367
3473
|
children: tt("common.error", { error })
|
|
3368
3474
|
}),
|
|
3369
|
-
|
|
3370
|
-
className: panel_module_css_default.
|
|
3371
|
-
children: tt("
|
|
3475
|
+
results === null && error === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3476
|
+
className: panel_module_css_default.empty,
|
|
3477
|
+
children: tt("cluster.empty")
|
|
3372
3478
|
}),
|
|
3373
|
-
|
|
3479
|
+
results !== null && results.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3374
3480
|
className: panel_module_css_default.empty,
|
|
3375
|
-
children: tt("
|
|
3481
|
+
children: tt("cluster.noMatch")
|
|
3376
3482
|
}),
|
|
3377
|
-
|
|
3483
|
+
results !== null && results.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3378
3484
|
className: panel_module_css_default.tableWrap,
|
|
3379
3485
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
3380
3486
|
className: panel_module_css_default.table,
|
|
3381
3487
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
3382
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3383
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3384
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3385
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3386
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3387
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3388
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
const
|
|
3488
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.alias") }),
|
|
3489
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.status") }),
|
|
3490
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.exit") }),
|
|
3491
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.stdout") }),
|
|
3492
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.stderr") }),
|
|
3493
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.error") }),
|
|
3494
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.duration") })
|
|
3495
|
+
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: results.map((result) => {
|
|
3496
|
+
const status = result.ok ? "ok" : result.timedOut === true ? "timeout" : "fail";
|
|
3497
|
+
const label = result.ok ? tt("cluster.ok") : result.timedOut === true ? tt("cluster.timeout") : tt("cluster.fail");
|
|
3392
3498
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
3393
3499
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3394
3500
|
className: panel_module_css_default.mono,
|
|
3395
|
-
children:
|
|
3396
|
-
}),
|
|
3397
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
|
|
3398
|
-
className: panel_module_css_default.mono,
|
|
3399
|
-
children: [
|
|
3400
|
-
host.host,
|
|
3401
|
-
":",
|
|
3402
|
-
host.port
|
|
3403
|
-
]
|
|
3501
|
+
children: result.alias
|
|
3404
3502
|
}),
|
|
3405
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: host.user }),
|
|
3406
3503
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3407
3504
|
className: panel_module_css_default.badge,
|
|
3408
|
-
"data-
|
|
3409
|
-
children:
|
|
3505
|
+
"data-status": status,
|
|
3506
|
+
children: label
|
|
3410
3507
|
}) }),
|
|
3411
3508
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3412
|
-
className: panel_module_css_default.
|
|
3413
|
-
children:
|
|
3509
|
+
className: panel_module_css_default.mono,
|
|
3510
|
+
children: result.exitCode ?? "-"
|
|
3414
3511
|
}),
|
|
3512
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: result.stdout !== void 0 && result.stdout !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
3513
|
+
className: panel_module_css_default.cellDetails,
|
|
3514
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: tt("cluster.col.stdout") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
3515
|
+
className: panel_module_css_default.cellPre,
|
|
3516
|
+
children: result.stdout
|
|
3517
|
+
})]
|
|
3518
|
+
}) }),
|
|
3519
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: result.stderr !== void 0 && result.stderr !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
3520
|
+
className: panel_module_css_default.cellDetails,
|
|
3521
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: tt("cluster.col.stderr") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
3522
|
+
className: panel_module_css_default.cellPre,
|
|
3523
|
+
children: result.stderr
|
|
3524
|
+
})]
|
|
3525
|
+
}) }),
|
|
3415
3526
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3416
3527
|
className: panel_module_css_default.cellMuted,
|
|
3417
|
-
children:
|
|
3528
|
+
children: result.error ?? ""
|
|
3418
3529
|
}),
|
|
3419
3530
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3420
|
-
className: panel_module_css_default.
|
|
3421
|
-
children:
|
|
3422
|
-
})
|
|
3423
|
-
|
|
3424
|
-
className: panel_module_css_default.actions,
|
|
3425
|
-
children: [
|
|
3426
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3427
|
-
type: "button",
|
|
3428
|
-
className: panel_module_css_default.linkButton,
|
|
3429
|
-
disabled: testingAlias === host.alias,
|
|
3430
|
-
onClick: () => {
|
|
3431
|
-
runTest(host.alias);
|
|
3432
|
-
},
|
|
3433
|
-
children: testingAlias === host.alias ? tt("hosts.testing") : tt("hosts.test")
|
|
3434
|
-
}),
|
|
3435
|
-
testingAlias === host.alias && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3436
|
-
className: panel_module_css_default.spinner,
|
|
3437
|
-
"aria-hidden": "true"
|
|
3438
|
-
}),
|
|
3439
|
-
test !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3440
|
-
className: panel_module_css_default.inlineTest,
|
|
3441
|
-
"data-status": test.ok ? "ok" : "fail",
|
|
3442
|
-
children: test.ok ? tt("hosts.testOk", { latency: test.latencyMs ?? 0 }) : tt("hosts.testFail", { error: test.error ?? "" })
|
|
3443
|
-
}),
|
|
3444
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3445
|
-
type: "button",
|
|
3446
|
-
className: panel_module_css_default.linkButton,
|
|
3447
|
-
onClick: () => {
|
|
3448
|
-
setDialog({
|
|
3449
|
-
mode: "edit",
|
|
3450
|
-
host
|
|
3451
|
-
});
|
|
3452
|
-
},
|
|
3453
|
-
children: tt("hosts.edit")
|
|
3454
|
-
}),
|
|
3455
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3456
|
-
type: "button",
|
|
3457
|
-
className: panel_module_css_default.linkButton,
|
|
3458
|
-
"data-danger": true,
|
|
3459
|
-
onClick: () => {
|
|
3460
|
-
deleteHost(host.alias);
|
|
3461
|
-
},
|
|
3462
|
-
children: tt("hosts.delete")
|
|
3463
|
-
}),
|
|
3464
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3465
|
-
type: "button",
|
|
3466
|
-
className: panel_module_css_default.ghostButton,
|
|
3467
|
-
onClick: () => {
|
|
3468
|
-
onConnect(host.alias);
|
|
3469
|
-
},
|
|
3470
|
-
children: tt("hosts.connected")
|
|
3471
|
-
})
|
|
3472
|
-
]
|
|
3473
|
-
}) })
|
|
3474
|
-
] }, host.alias);
|
|
3531
|
+
className: panel_module_css_default.mono,
|
|
3532
|
+
children: result.durationMs !== void 0 ? result.durationMs + " ms" : "-"
|
|
3533
|
+
})
|
|
3534
|
+
] }, result.alias);
|
|
3475
3535
|
}) })]
|
|
3476
3536
|
})
|
|
3477
|
-
}),
|
|
3478
|
-
dialog !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostFormDialog, {
|
|
3479
|
-
api,
|
|
3480
|
-
editing: dialog.mode === "edit" ? dialog.host : null,
|
|
3481
|
-
onClose: () => {
|
|
3482
|
-
setDialog(null);
|
|
3483
|
-
},
|
|
3484
|
-
onSaved: () => {
|
|
3485
|
-
setDialog(null);
|
|
3486
|
-
load();
|
|
3487
|
-
}
|
|
3488
|
-
}),
|
|
3489
|
-
hostKeyConfirm !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostFingerprintDialog, {
|
|
3490
|
-
api,
|
|
3491
|
-
alias: hostKeyConfirm.alias,
|
|
3492
|
-
fingerprintSha256: hostKeyConfirm.fingerprintSha256,
|
|
3493
|
-
mismatch: hostKeyConfirm.mismatch,
|
|
3494
|
-
onClose: () => {
|
|
3495
|
-
setHostKeyConfirm(null);
|
|
3496
|
-
},
|
|
3497
|
-
onTrusted: () => {
|
|
3498
|
-
retryAfterFingerprint(hostKeyConfirm.alias);
|
|
3499
|
-
}
|
|
3500
|
-
}),
|
|
3501
|
-
secretConfirm !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionSecretDialog, {
|
|
3502
|
-
api,
|
|
3503
|
-
alias: secretConfirm.alias,
|
|
3504
|
-
user: secretConfirm.user,
|
|
3505
|
-
secret: secretConfirm.secret,
|
|
3506
|
-
onClose: () => {
|
|
3507
|
-
setSecretConfirm(null);
|
|
3508
|
-
},
|
|
3509
|
-
onProvided: () => {
|
|
3510
|
-
retryAfterSecret(secretConfirm.alias);
|
|
3511
|
-
}
|
|
3512
3537
|
})
|
|
3513
3538
|
]
|
|
3514
3539
|
});
|
|
@@ -16557,8 +16582,9 @@ window.__ModuleLoader__.load({
|
|
|
16557
16582
|
//#region src/client/ssh/panel/TerminalTab.tsx
|
|
16558
16583
|
/**
|
|
16559
16584
|
* Terminal tab: an xterm.js PTY view over the host's WebSocket terminal route.
|
|
16560
|
-
*
|
|
16561
|
-
*
|
|
16585
|
+
* The target alias is inherited from the selected Session; only connect /
|
|
16586
|
+
* disconnect controls remain. The terminal container is sized by FitAddon
|
|
16587
|
+
* (default 80x24 before first fit). On remote exit the last
|
|
16562
16588
|
* output stays visible and input is disabled. xterm's stylesheet is injected
|
|
16563
16589
|
* once per page load (module-level guard).
|
|
16564
16590
|
*/
|
|
@@ -16574,9 +16600,7 @@ window.__ModuleLoader__.load({
|
|
|
16574
16600
|
document.head.appendChild(style);
|
|
16575
16601
|
}
|
|
16576
16602
|
/** The xterm terminal view. */
|
|
16577
|
-
function TerminalTab({ api,
|
|
16578
|
-
const [hosts, setHosts] = (0, react.useState)([]);
|
|
16579
|
-
const [alias, setAlias] = (0, react.useState)(presetAlias ?? "");
|
|
16603
|
+
function TerminalTab({ api, alias }) {
|
|
16580
16604
|
const [status, setStatus] = (0, react.useState)({ kind: "idle" });
|
|
16581
16605
|
const containerRef = (0, react.useRef)(null);
|
|
16582
16606
|
const termRef = (0, react.useRef)(null);
|
|
@@ -16585,26 +16609,6 @@ window.__ModuleLoader__.load({
|
|
|
16585
16609
|
(0, react.useEffect)(() => {
|
|
16586
16610
|
ensureXtermCss();
|
|
16587
16611
|
}, []);
|
|
16588
|
-
(0, react.useEffect)(() => {
|
|
16589
|
-
let disposed = false;
|
|
16590
|
-
(async () => {
|
|
16591
|
-
try {
|
|
16592
|
-
const list = await api.listHosts();
|
|
16593
|
-
if (!disposed) setHosts(list);
|
|
16594
|
-
} catch (cause) {
|
|
16595
|
-
if (!disposed) setStatus({
|
|
16596
|
-
kind: "error",
|
|
16597
|
-
detail: errorMessage(cause)
|
|
16598
|
-
});
|
|
16599
|
-
}
|
|
16600
|
-
})();
|
|
16601
|
-
return () => {
|
|
16602
|
-
disposed = true;
|
|
16603
|
-
};
|
|
16604
|
-
}, [api]);
|
|
16605
|
-
(0, react.useEffect)(() => {
|
|
16606
|
-
if (presetAlias !== void 0) setAlias(presetAlias);
|
|
16607
|
-
}, [presetAlias, requestId]);
|
|
16608
16612
|
const teardown = () => {
|
|
16609
16613
|
const connection = connRef.current;
|
|
16610
16614
|
connRef.current = null;
|
|
@@ -16656,6 +16660,7 @@ window.__ModuleLoader__.load({
|
|
|
16656
16660
|
term.loadAddon(fit);
|
|
16657
16661
|
term.open(container);
|
|
16658
16662
|
fit.fit();
|
|
16663
|
+
term.options.disableStdin = true;
|
|
16659
16664
|
const connection = api.openTerminal(target, term.cols, term.rows);
|
|
16660
16665
|
termRef.current = term;
|
|
16661
16666
|
fitRef.current = fit;
|
|
@@ -16665,6 +16670,7 @@ window.__ModuleLoader__.load({
|
|
|
16665
16670
|
connection.send(data);
|
|
16666
16671
|
});
|
|
16667
16672
|
connection.onReady = () => {
|
|
16673
|
+
term.options.disableStdin = false;
|
|
16668
16674
|
setStatus({
|
|
16669
16675
|
kind: "connected",
|
|
16670
16676
|
alias: target
|
|
@@ -16697,29 +16703,14 @@ window.__ModuleLoader__.load({
|
|
|
16697
16703
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
16698
16704
|
className: panel_module_css_default.controls,
|
|
16699
16705
|
children: [
|
|
16700
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
16701
|
-
className: panel_module_css_default.
|
|
16702
|
-
|
|
16703
|
-
onChange: (event) => {
|
|
16704
|
-
setAlias(event.target.value);
|
|
16705
|
-
},
|
|
16706
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
16707
|
-
value: "",
|
|
16708
|
-
children: tt("terminal.selectHost")
|
|
16709
|
-
}), hosts.map((host) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
16710
|
-
value: host.alias,
|
|
16711
|
-
children: [
|
|
16712
|
-
host.alias,
|
|
16713
|
-
" (",
|
|
16714
|
-
host.host,
|
|
16715
|
-
")"
|
|
16716
|
-
]
|
|
16717
|
-
}, host.alias))]
|
|
16706
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16707
|
+
className: panel_module_css_default.targetBadge,
|
|
16708
|
+
children: alias
|
|
16718
16709
|
}),
|
|
16719
16710
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
16720
16711
|
type: "button",
|
|
16721
16712
|
className: panel_module_css_default.primaryButton,
|
|
16722
|
-
disabled:
|
|
16713
|
+
disabled: active,
|
|
16723
16714
|
onClick: connect,
|
|
16724
16715
|
children: tt("terminal.connect")
|
|
16725
16716
|
}),
|
|
@@ -16759,7 +16750,7 @@ window.__ModuleLoader__.load({
|
|
|
16759
16750
|
className: panel_module_css_default.termContainer
|
|
16760
16751
|
}), status.kind === "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
16761
16752
|
className: panel_module_css_default.termPlaceholder,
|
|
16762
|
-
children:
|
|
16753
|
+
children: tt("terminal.placeholder")
|
|
16763
16754
|
})]
|
|
16764
16755
|
})
|
|
16765
16756
|
]
|
|
@@ -16791,33 +16782,22 @@ window.__ModuleLoader__.load({
|
|
|
16791
16782
|
return tt("transfer.gib", { value: (bytes / 1073741824).toFixed(1) });
|
|
16792
16783
|
}
|
|
16793
16784
|
/** The upload/download tab. */
|
|
16794
|
-
function TransferTab({ api }) {
|
|
16795
|
-
const [hosts, setHosts] = (0, react.useState)([]);
|
|
16785
|
+
function TransferTab({ api, alias, remoteRoot }) {
|
|
16796
16786
|
const [listError, setListError] = (0, react.useState)(null);
|
|
16797
|
-
const [
|
|
16798
|
-
const [remotePath, setRemotePath] = (0, react.useState)("");
|
|
16787
|
+
const [remotePath, setRemotePath] = (0, react.useState)(remoteRoot);
|
|
16799
16788
|
const [browseOpen, setBrowseOpen] = (0, react.useState)(false);
|
|
16800
|
-
const [browseDir, setBrowseDir] = (0, react.useState)(
|
|
16789
|
+
const [browseDir, setBrowseDir] = (0, react.useState)(remoteRoot);
|
|
16801
16790
|
const [entries, setEntries] = (0, react.useState)([]);
|
|
16802
16791
|
const [browsing, setBrowsing] = (0, react.useState)(false);
|
|
16803
16792
|
const [transfer, setTransfer] = (0, react.useState)(null);
|
|
16804
16793
|
const [status, setStatus] = (0, react.useState)(null);
|
|
16805
16794
|
const fileRef = (0, react.useRef)(null);
|
|
16806
16795
|
const seqRef = (0, react.useRef)(0);
|
|
16807
|
-
(0, react.
|
|
16808
|
-
|
|
16809
|
-
(
|
|
16810
|
-
|
|
16811
|
-
|
|
16812
|
-
if (!disposed) setHosts(list);
|
|
16813
|
-
} catch (cause) {
|
|
16814
|
-
if (!disposed) setListError(errorMessage(cause));
|
|
16815
|
-
}
|
|
16816
|
-
})();
|
|
16817
|
-
return () => {
|
|
16818
|
-
disposed = true;
|
|
16819
|
-
};
|
|
16820
|
-
}, [api]);
|
|
16796
|
+
const transferAbortRef = (0, react.useRef)(null);
|
|
16797
|
+
(0, react.useEffect)(() => () => {
|
|
16798
|
+
transferAbortRef.current?.abort(Object.assign(/* @__PURE__ */ new Error("transfer tab unmounted"), { name: "AbortError" }));
|
|
16799
|
+
transferAbortRef.current = null;
|
|
16800
|
+
}, []);
|
|
16821
16801
|
const loadDir = async (path) => {
|
|
16822
16802
|
if (alias === "") return;
|
|
16823
16803
|
const seq = ++seqRef.current;
|
|
@@ -16839,15 +16819,16 @@ window.__ModuleLoader__.load({
|
|
|
16839
16819
|
}
|
|
16840
16820
|
};
|
|
16841
16821
|
const openBrowse = () => {
|
|
16842
|
-
if (alias === "") return;
|
|
16843
16822
|
setBrowseOpen(true);
|
|
16844
|
-
const path = remotePath.trim() === "" ?
|
|
16823
|
+
const path = remotePath.trim() === "" ? remoteRoot : remotePath.trim();
|
|
16845
16824
|
loadDir(path);
|
|
16846
16825
|
};
|
|
16847
16826
|
const handleFile = async (event) => {
|
|
16848
16827
|
const file = event.target.files?.[0];
|
|
16849
16828
|
event.target.value = "";
|
|
16850
16829
|
if (file === void 0 || alias === "" || remotePath.trim() === "" || transfer !== null) return;
|
|
16830
|
+
const controller = new AbortController();
|
|
16831
|
+
transferAbortRef.current = controller;
|
|
16851
16832
|
setStatus(null);
|
|
16852
16833
|
setTransfer({
|
|
16853
16834
|
kind: "upload",
|
|
@@ -16857,6 +16838,7 @@ window.__ModuleLoader__.load({
|
|
|
16857
16838
|
});
|
|
16858
16839
|
try {
|
|
16859
16840
|
const outcome = await api.uploadFile(file, alias, remotePath.trim(), (progress) => {
|
|
16841
|
+
if (transferAbortRef.current !== controller || controller.signal.aborted) return;
|
|
16860
16842
|
setTransfer((prev) => prev === null ? prev : {
|
|
16861
16843
|
kind: "upload",
|
|
16862
16844
|
phase: progress.phase,
|
|
@@ -16864,22 +16846,27 @@ window.__ModuleLoader__.load({
|
|
|
16864
16846
|
speedBps: progress.speedBps,
|
|
16865
16847
|
file: progress.file
|
|
16866
16848
|
});
|
|
16867
|
-
});
|
|
16868
|
-
setStatus({
|
|
16849
|
+
}, controller.signal);
|
|
16850
|
+
if (transferAbortRef.current === controller && !controller.signal.aborted) setStatus({
|
|
16869
16851
|
kind: "ok",
|
|
16870
16852
|
bytes: outcome.transferredBytes
|
|
16871
16853
|
});
|
|
16872
16854
|
} catch (cause) {
|
|
16873
|
-
setStatus({
|
|
16855
|
+
if (transferAbortRef.current === controller && !controller.signal.aborted) setStatus({
|
|
16874
16856
|
kind: "error",
|
|
16875
16857
|
error: errorMessage(cause)
|
|
16876
16858
|
});
|
|
16877
16859
|
} finally {
|
|
16878
|
-
|
|
16860
|
+
if (transferAbortRef.current === controller) {
|
|
16861
|
+
transferAbortRef.current = null;
|
|
16862
|
+
if (!controller.signal.aborted) setTransfer(null);
|
|
16863
|
+
}
|
|
16879
16864
|
}
|
|
16880
16865
|
};
|
|
16881
16866
|
const handleDownload = async () => {
|
|
16882
16867
|
if (alias === "" || remotePath.trim() === "" || transfer !== null) return;
|
|
16868
|
+
const controller = new AbortController();
|
|
16869
|
+
transferAbortRef.current = controller;
|
|
16883
16870
|
setStatus(null);
|
|
16884
16871
|
setTransfer({
|
|
16885
16872
|
kind: "download",
|
|
@@ -16889,6 +16876,7 @@ window.__ModuleLoader__.load({
|
|
|
16889
16876
|
});
|
|
16890
16877
|
try {
|
|
16891
16878
|
const result = await api.downloadFile(alias, remotePath.trim(), (progress) => {
|
|
16879
|
+
if (transferAbortRef.current !== controller || controller.signal.aborted) return;
|
|
16892
16880
|
setTransfer((prev) => prev === null ? prev : {
|
|
16893
16881
|
kind: "download",
|
|
16894
16882
|
phase: progress.phase,
|
|
@@ -16896,7 +16884,8 @@ window.__ModuleLoader__.load({
|
|
|
16896
16884
|
speedBps: progress.speedBps,
|
|
16897
16885
|
file: progress.file
|
|
16898
16886
|
});
|
|
16899
|
-
});
|
|
16887
|
+
}, controller.signal);
|
|
16888
|
+
if (transferAbortRef.current !== controller || controller.signal.aborted) return;
|
|
16900
16889
|
if (!result.streamed && result.blob !== void 0) {
|
|
16901
16890
|
const url = URL.createObjectURL(result.blob);
|
|
16902
16891
|
const anchor = document.createElement("a");
|
|
@@ -16914,39 +16903,27 @@ window.__ModuleLoader__.load({
|
|
|
16914
16903
|
bytes: result.bytes
|
|
16915
16904
|
});
|
|
16916
16905
|
} catch (cause) {
|
|
16917
|
-
setStatus({
|
|
16906
|
+
if (transferAbortRef.current === controller && !controller.signal.aborted) setStatus({
|
|
16918
16907
|
kind: "error",
|
|
16919
16908
|
error: errorMessage(cause)
|
|
16920
16909
|
});
|
|
16921
16910
|
} finally {
|
|
16922
|
-
|
|
16911
|
+
if (transferAbortRef.current === controller) {
|
|
16912
|
+
transferAbortRef.current = null;
|
|
16913
|
+
if (!controller.signal.aborted) setTransfer(null);
|
|
16914
|
+
}
|
|
16923
16915
|
}
|
|
16924
16916
|
};
|
|
16925
|
-
const ready =
|
|
16917
|
+
const ready = remotePath.trim() !== "" && transfer === null;
|
|
16926
16918
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
16927
16919
|
className: panel_module_css_default.tabBody,
|
|
16928
16920
|
children: [
|
|
16929
16921
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
16930
16922
|
className: panel_module_css_default.controls,
|
|
16931
16923
|
children: [
|
|
16932
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
16933
|
-
className: panel_module_css_default.
|
|
16934
|
-
|
|
16935
|
-
onChange: (event) => {
|
|
16936
|
-
setAlias(event.target.value);
|
|
16937
|
-
},
|
|
16938
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
16939
|
-
value: "",
|
|
16940
|
-
children: tt("transfer.selectHost")
|
|
16941
|
-
}), hosts.map((host) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
16942
|
-
value: host.alias,
|
|
16943
|
-
children: [
|
|
16944
|
-
host.alias,
|
|
16945
|
-
" (",
|
|
16946
|
-
host.host,
|
|
16947
|
-
")"
|
|
16948
|
-
]
|
|
16949
|
-
}, host.alias))]
|
|
16924
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16925
|
+
className: panel_module_css_default.targetBadge,
|
|
16926
|
+
children: alias
|
|
16950
16927
|
}),
|
|
16951
16928
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
16952
16929
|
className: panel_module_css_default.input,
|
|
@@ -16959,7 +16936,6 @@ window.__ModuleLoader__.load({
|
|
|
16959
16936
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
16960
16937
|
type: "button",
|
|
16961
16938
|
className: panel_module_css_default.ghostButton,
|
|
16962
|
-
disabled: alias === "",
|
|
16963
16939
|
onClick: openBrowse,
|
|
16964
16940
|
children: tt("transfer.browseRemote")
|
|
16965
16941
|
}),
|
|
@@ -17088,28 +17064,14 @@ window.__ModuleLoader__.load({
|
|
|
17088
17064
|
* and a new-tunnel form.
|
|
17089
17065
|
*/
|
|
17090
17066
|
/** The tunnels tab. */
|
|
17091
|
-
function TunnelsTab({ api }) {
|
|
17092
|
-
const [hosts, setHosts] = (0, react.useState)([]);
|
|
17067
|
+
function TunnelsTab({ api, alias }) {
|
|
17093
17068
|
const [tunnels, setTunnels] = (0, react.useState)(null);
|
|
17094
17069
|
const [error, setError] = (0, react.useState)(null);
|
|
17095
17070
|
const [notice, setNotice] = (0, react.useState)(null);
|
|
17096
|
-
const [alias, setAlias] = (0, react.useState)("");
|
|
17097
17071
|
const [remotePort, setRemotePort] = (0, react.useState)("");
|
|
17098
17072
|
const [remoteHost, setRemoteHost] = (0, react.useState)("");
|
|
17099
17073
|
const [localPort, setLocalPort] = (0, react.useState)("");
|
|
17100
17074
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
17101
|
-
(0, react.useEffect)(() => {
|
|
17102
|
-
let disposed = false;
|
|
17103
|
-
(async () => {
|
|
17104
|
-
try {
|
|
17105
|
-
const list = await api.listHosts();
|
|
17106
|
-
if (!disposed) setHosts(list);
|
|
17107
|
-
} catch {}
|
|
17108
|
-
})();
|
|
17109
|
-
return () => {
|
|
17110
|
-
disposed = true;
|
|
17111
|
-
};
|
|
17112
|
-
}, [api]);
|
|
17113
17075
|
const seqRef = (0, react.useRef)(0);
|
|
17114
17076
|
(0, react.useEffect)(() => {
|
|
17115
17077
|
const load = async () => {
|
|
@@ -17117,7 +17079,7 @@ window.__ModuleLoader__.load({
|
|
|
17117
17079
|
try {
|
|
17118
17080
|
const list = await api.listTunnels();
|
|
17119
17081
|
if (seq !== seqRef.current) return;
|
|
17120
|
-
setTunnels(list);
|
|
17082
|
+
setTunnels(list.filter((tunnel) => tunnel.alias === alias));
|
|
17121
17083
|
setError(null);
|
|
17122
17084
|
} catch (cause) {
|
|
17123
17085
|
if (seq !== seqRef.current) return;
|
|
@@ -17131,7 +17093,7 @@ window.__ModuleLoader__.load({
|
|
|
17131
17093
|
return () => {
|
|
17132
17094
|
clearInterval(timer);
|
|
17133
17095
|
};
|
|
17134
|
-
}, [api]);
|
|
17096
|
+
}, [api, alias]);
|
|
17135
17097
|
const refresh = async () => {
|
|
17136
17098
|
const seq = ++seqRef.current;
|
|
17137
17099
|
try {
|
|
@@ -17156,7 +17118,7 @@ window.__ModuleLoader__.load({
|
|
|
17156
17118
|
if (!window.confirm(tt("tunnel.stopAllConfirm"))) return;
|
|
17157
17119
|
setBusy(true);
|
|
17158
17120
|
try {
|
|
17159
|
-
await api.stopAllTunnels(alias
|
|
17121
|
+
await api.stopAllTunnels(alias);
|
|
17160
17122
|
await refresh();
|
|
17161
17123
|
} catch (cause) {
|
|
17162
17124
|
setError(errorMessage(cause));
|
|
@@ -17265,19 +17227,9 @@ window.__ModuleLoader__.load({
|
|
|
17265
17227
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17266
17228
|
className: panel_module_css_default.fieldLabel,
|
|
17267
17229
|
children: tt("tunnel.alias")
|
|
17268
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
17269
|
-
className: panel_module_css_default.
|
|
17270
|
-
|
|
17271
|
-
onChange: (event) => {
|
|
17272
|
-
setAlias(event.target.value);
|
|
17273
|
-
},
|
|
17274
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
17275
|
-
value: "",
|
|
17276
|
-
children: tt("terminal.selectHost")
|
|
17277
|
-
}), hosts.map((host) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
17278
|
-
value: host.alias,
|
|
17279
|
-
children: host.alias
|
|
17280
|
-
}, host.alias))]
|
|
17230
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17231
|
+
className: panel_module_css_default.targetBadge,
|
|
17232
|
+
children: alias
|
|
17281
17233
|
})]
|
|
17282
17234
|
}),
|
|
17283
17235
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
@@ -17331,7 +17283,7 @@ window.__ModuleLoader__.load({
|
|
|
17331
17283
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17332
17284
|
type: "button",
|
|
17333
17285
|
className: panel_module_css_default.primaryButton,
|
|
17334
|
-
disabled: busy ||
|
|
17286
|
+
disabled: busy || remotePort.trim() === "",
|
|
17335
17287
|
onClick: () => {
|
|
17336
17288
|
start();
|
|
17337
17289
|
},
|
|
@@ -17344,18 +17296,13 @@ window.__ModuleLoader__.load({
|
|
|
17344
17296
|
//#endregion
|
|
17345
17297
|
//#region src/client/ssh/panel/SshPanel.tsx
|
|
17346
17298
|
/**
|
|
17347
|
-
*
|
|
17348
|
-
*
|
|
17349
|
-
*
|
|
17350
|
-
*
|
|
17351
|
-
* tab with the chosen alias preselected.
|
|
17299
|
+
* Session-scoped SSH operations console. The right sidebar follows the selected
|
|
17300
|
+
* Session, so every operation inherits that Session's SSH workspace target.
|
|
17301
|
+
* Individual tabs never offer a host picker. A local Session mounts no operation
|
|
17302
|
+
* body and shows a blurred unavailable mask instead.
|
|
17352
17303
|
*/
|
|
17353
17304
|
/** The tab bar definition (labels resolved at render time). */
|
|
17354
17305
|
const TABS = [
|
|
17355
|
-
{
|
|
17356
|
-
id: "hosts",
|
|
17357
|
-
label: () => tt("tab.hosts")
|
|
17358
|
-
},
|
|
17359
17306
|
{
|
|
17360
17307
|
id: "terminal",
|
|
17361
17308
|
label: () => tt("tab.terminal")
|
|
@@ -17373,36 +17320,39 @@ window.__ModuleLoader__.load({
|
|
|
17373
17320
|
label: () => tt("tab.cluster")
|
|
17374
17321
|
}
|
|
17375
17322
|
];
|
|
17376
|
-
/** The tabbed SSH
|
|
17377
|
-
function SshPanel({
|
|
17378
|
-
const [activeTab, setActiveTab] = (0, react.useState)("
|
|
17379
|
-
const
|
|
17380
|
-
|
|
17381
|
-
|
|
17382
|
-
|
|
17383
|
-
|
|
17384
|
-
|
|
17385
|
-
|
|
17386
|
-
|
|
17323
|
+
/** The tabbed SSH operations console. */
|
|
17324
|
+
function SshPanel({ api, target }) {
|
|
17325
|
+
const [activeTab, setActiveTab] = (0, react.useState)("terminal");
|
|
17326
|
+
const sessionTarget = (0, react.useSyncExternalStore)(target.subscribe, target.getSnapshot, target.getSnapshot);
|
|
17327
|
+
if (sessionTarget === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17328
|
+
className: panel_module_css_default.panel,
|
|
17329
|
+
"data-session-mode": "local",
|
|
17330
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17331
|
+
className: panel_module_css_default.disabledSurface,
|
|
17332
|
+
"aria-hidden": "true",
|
|
17333
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17334
|
+
className: panel_module_css_default.tabBar,
|
|
17335
|
+
role: "presentation",
|
|
17336
|
+
children: TABS.map((tab) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17337
|
+
className: panel_module_css_default.tab,
|
|
17338
|
+
children: tab.label()
|
|
17339
|
+
}, tab.id))
|
|
17340
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17341
|
+
className: panel_module_css_default.panelContent,
|
|
17342
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: panel_module_css_default.disabledPlaceholder })
|
|
17343
|
+
})]
|
|
17344
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17345
|
+
className: panel_module_css_default.localSessionMask,
|
|
17346
|
+
role: "status",
|
|
17347
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: tt("session.localTitle") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: tt("session.localUnavailable") })]
|
|
17348
|
+
})]
|
|
17349
|
+
});
|
|
17350
|
+
const targetKey = `${sessionTarget.sessionId}:${sessionTarget.workspaceId}:${sessionTarget.alias}`;
|
|
17387
17351
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17388
17352
|
className: panel_module_css_default.panel,
|
|
17353
|
+
"data-session-mode": "ssh",
|
|
17354
|
+
"data-session-alias": sessionTarget.alias,
|
|
17389
17355
|
children: [
|
|
17390
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17391
|
-
className: panel_module_css_default.panelHeader,
|
|
17392
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
17393
|
-
className: panel_module_css_default.panelTitle,
|
|
17394
|
-
children: tt("panel.title")
|
|
17395
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17396
|
-
type: "button",
|
|
17397
|
-
className: panel_module_css_default.iconButton,
|
|
17398
|
-
title: tt("common.close"),
|
|
17399
|
-
"aria-label": tt("common.close"),
|
|
17400
|
-
onClick: () => {
|
|
17401
|
-
controller.close();
|
|
17402
|
-
},
|
|
17403
|
-
children: "x"
|
|
17404
|
-
})]
|
|
17405
|
-
}),
|
|
17406
17356
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17407
17357
|
className: panel_module_css_default.tabBar,
|
|
17408
17358
|
role: "tablist",
|
|
@@ -17418,271 +17368,172 @@ window.__ModuleLoader__.load({
|
|
|
17418
17368
|
children: tab.label()
|
|
17419
17369
|
}, tab.id))
|
|
17420
17370
|
}),
|
|
17371
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17372
|
+
className: panel_module_css_default.sessionTarget,
|
|
17373
|
+
title: sessionTarget.remoteRoot,
|
|
17374
|
+
children: tt("session.target", {
|
|
17375
|
+
alias: sessionTarget.alias,
|
|
17376
|
+
remoteRoot: sessionTarget.remoteRoot
|
|
17377
|
+
})
|
|
17378
|
+
}),
|
|
17421
17379
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17422
17380
|
className: panel_module_css_default.panelContent,
|
|
17423
17381
|
children: [
|
|
17424
|
-
activeTab === "
|
|
17382
|
+
activeTab === "terminal" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TerminalTab, {
|
|
17425
17383
|
api,
|
|
17426
|
-
|
|
17384
|
+
alias: sessionTarget.alias
|
|
17427
17385
|
}),
|
|
17428
|
-
activeTab === "
|
|
17386
|
+
activeTab === "transfer" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransferTab, {
|
|
17387
|
+
api,
|
|
17388
|
+
alias: sessionTarget.alias,
|
|
17389
|
+
remoteRoot: sessionTarget.remoteRoot
|
|
17390
|
+
}),
|
|
17391
|
+
activeTab === "tunnels" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TunnelsTab, {
|
|
17429
17392
|
api,
|
|
17430
|
-
|
|
17431
|
-
requestId: connectRequest?.nonce
|
|
17393
|
+
alias: sessionTarget.alias
|
|
17432
17394
|
}),
|
|
17433
|
-
activeTab === "
|
|
17434
|
-
|
|
17435
|
-
|
|
17395
|
+
activeTab === "cluster" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClusterTab, {
|
|
17396
|
+
api,
|
|
17397
|
+
alias: sessionTarget.alias
|
|
17398
|
+
})
|
|
17436
17399
|
]
|
|
17437
|
-
})
|
|
17400
|
+
}, targetKey)
|
|
17438
17401
|
]
|
|
17439
17402
|
});
|
|
17440
17403
|
}
|
|
17441
17404
|
//#endregion
|
|
17442
|
-
//#region src/client/ssh/
|
|
17443
|
-
/**
|
|
17444
|
-
|
|
17445
|
-
|
|
17446
|
-
|
|
17447
|
-
* plugins cannot declare slots, so the panel takes over the center column at
|
|
17448
|
-
* the DOM level: a container is appended inside the `[data-pane="conversation"]`
|
|
17449
|
-
* grid item (an extra trailing child React never manages), and a stylesheet
|
|
17450
|
-
* rule hides the conversation content while the panel is active. Toggling is
|
|
17451
|
-
* a data attribute on <html> — no React involvement, so the conversation
|
|
17452
|
-
* subtree underneath stays mounted and stateful.
|
|
17453
|
-
*/
|
|
17454
|
-
const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"]";
|
|
17455
|
-
const ACTIVE_ATTR = "data-dsh-ssh-active";
|
|
17456
|
-
/** Find the center column, or undefined while the frame is not mounted. */
|
|
17457
|
-
function conversationColumn() {
|
|
17458
|
-
return document.querySelector(CONVERSATION_COLUMN_SELECTOR) ?? void 0;
|
|
17459
|
-
}
|
|
17405
|
+
//#region src/client/ssh/ops-tab.tsx
|
|
17406
|
+
/** This implementation's identity in the tab system; also the body/title key. */
|
|
17407
|
+
const OPS_TAB_ID = "@tiphareth/dsh-hardssh/operations";
|
|
17408
|
+
/** The page kind `ctx.sidebarRight.openTab(kind)` names. */
|
|
17409
|
+
const OPS_TAB_KIND = "hardssh-operations";
|
|
17460
17410
|
/**
|
|
17461
|
-
*
|
|
17462
|
-
*
|
|
17463
|
-
* @param
|
|
17464
|
-
* @param
|
|
17465
|
-
* @returns disposer unmounting the tree and restoring the column.
|
|
17411
|
+
* Register the regression type, its body, and its chip title.
|
|
17412
|
+
* @param ctx - client root context carrying the tab registry and slot registry.
|
|
17413
|
+
* @param api - the SSH API client the panel's tabs operate through.
|
|
17414
|
+
* @param target - live target inherited from the selected Session.
|
|
17466
17415
|
*/
|
|
17467
|
-
function
|
|
17468
|
-
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
17472
|
-
|
|
17473
|
-
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
|
|
17482
|
-
|
|
17483
|
-
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
17487
|
-
|
|
17488
|
-
|
|
17489
|
-
};
|
|
17490
|
-
const waitObserver = new MutationObserver(() => {
|
|
17491
|
-
ensure();
|
|
17492
|
-
});
|
|
17493
|
-
waitObserver.observe(document.body, {
|
|
17494
|
-
childList: true,
|
|
17495
|
-
subtree: true
|
|
17496
|
-
});
|
|
17497
|
-
const applyActive = () => {
|
|
17498
|
-
if (controller.getSnapshot().panelOpen) document.documentElement.setAttribute(ACTIVE_ATTR, "");
|
|
17499
|
-
else document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
17500
|
-
};
|
|
17501
|
-
const unsubscribe = controller.subscribe(applyActive);
|
|
17502
|
-
applyActive();
|
|
17503
|
-
ensure();
|
|
17504
|
-
return () => {
|
|
17505
|
-
waitObserver.disconnect();
|
|
17506
|
-
unsubscribe();
|
|
17507
|
-
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
17508
|
-
root?.unmount();
|
|
17509
|
-
root = void 0;
|
|
17510
|
-
container?.remove();
|
|
17511
|
-
container = void 0;
|
|
17512
|
-
};
|
|
17513
|
-
}
|
|
17514
|
-
//#endregion
|
|
17515
|
-
//#region src/client/ssh/panel/controller.ts
|
|
17516
|
-
/** The panel state owner the sidebar entry toggles and the view renders from. */
|
|
17517
|
-
var PanelController = class {
|
|
17518
|
-
panelOpen = false;
|
|
17519
|
-
listeners = /* @__PURE__ */ new Set();
|
|
17520
|
-
getSnapshot() {
|
|
17521
|
-
return { panelOpen: this.panelOpen };
|
|
17522
|
-
}
|
|
17523
|
-
subscribe(fn) {
|
|
17524
|
-
this.listeners.add(fn);
|
|
17525
|
-
return () => {
|
|
17526
|
-
this.listeners.delete(fn);
|
|
17527
|
-
};
|
|
17528
|
-
}
|
|
17529
|
-
open() {
|
|
17530
|
-
if (this.panelOpen) return;
|
|
17531
|
-
this.panelOpen = true;
|
|
17532
|
-
this.notify();
|
|
17533
|
-
}
|
|
17534
|
-
close() {
|
|
17535
|
-
if (!this.panelOpen) return;
|
|
17536
|
-
this.panelOpen = false;
|
|
17537
|
-
this.notify();
|
|
17538
|
-
}
|
|
17539
|
-
toggle() {
|
|
17540
|
-
if (this.panelOpen) this.close();
|
|
17541
|
-
else this.open();
|
|
17542
|
-
}
|
|
17543
|
-
notify() {
|
|
17544
|
-
for (const fn of [...this.listeners]) fn();
|
|
17545
|
-
}
|
|
17546
|
-
};
|
|
17547
|
-
//#endregion
|
|
17548
|
-
//#region src/client/ssh/sidebar-entry.ts
|
|
17549
|
-
/** The sidebar column is the grid item AppFrame renders with this pane marker. */
|
|
17550
|
-
const SIDEBAR_COLUMN_SELECTOR = "[data-pane=\"sidebar\"]";
|
|
17551
|
-
/** Find the sidebar shell root element, or undefined while not yet mounted. */
|
|
17552
|
-
function sidebarRoot() {
|
|
17553
|
-
return document.querySelector(SIDEBAR_COLUMN_SELECTOR)?.firstElementChild ?? void 0;
|
|
17554
|
-
}
|
|
17555
|
-
/** The New Session button: the shell's only direct-child button of the root. */
|
|
17556
|
-
function newSessionButton(root) {
|
|
17557
|
-
for (const child of root.children) if (child.tagName === "BUTTON") return child;
|
|
17558
|
-
}
|
|
17559
|
-
/** Build the entry row (a detached button; insert once the shell is up). */
|
|
17560
|
-
function createEntry(controller) {
|
|
17561
|
-
const entry = document.createElement("button");
|
|
17562
|
-
entry.type = "button";
|
|
17563
|
-
entry.dataset.dshSshEntry = "";
|
|
17564
|
-
entry.className = panel_module_css_default.entry;
|
|
17565
|
-
entry.setAttribute("aria-label", tt("entry.label"));
|
|
17566
|
-
entry.setAttribute("title", tt("entry.tooltip"));
|
|
17567
|
-
entry.innerHTML = "<span class=\"" + panel_module_css_default.entryIcon + "\"><svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"2\" y=\"2.5\" width=\"12\" height=\"11\" rx=\"1.5\"/><path d=\"M4.5 5.5l2.5 2.5-2.5 2.5\"/><path d=\"M8.5 10.5h3\"/></svg></span><span class=\"" + panel_module_css_default.entryLabel + "\">" + tt("entry.label") + "</span>";
|
|
17568
|
-
entry.addEventListener("click", () => {
|
|
17569
|
-
controller.toggle();
|
|
17570
|
-
});
|
|
17571
|
-
return entry;
|
|
17572
|
-
}
|
|
17573
|
-
/** Re-insert the entry after the New Session button (before the browser region). */
|
|
17574
|
-
function placeEntry(root, entry) {
|
|
17575
|
-
const button = newSessionButton(root);
|
|
17576
|
-
if (button === void 0) return false;
|
|
17577
|
-
if (entry.parentElement !== root) root.insertBefore(entry, button.nextElementSibling);
|
|
17578
|
-
return true;
|
|
17416
|
+
function registerOperationsTab(ctx, api, target) {
|
|
17417
|
+
ctx.effect(() => ctx.sidebarRightTabs.register({
|
|
17418
|
+
id: OPS_TAB_ID,
|
|
17419
|
+
kind: OPS_TAB_KIND,
|
|
17420
|
+
title: () => tt$1("ops.tab"),
|
|
17421
|
+
guide: [{
|
|
17422
|
+
order: 100,
|
|
17423
|
+
title: () => tt$1("ops.tab"),
|
|
17424
|
+
description: () => tt$1("ops.guide")
|
|
17425
|
+
}]
|
|
17426
|
+
}), "dsh-hardssh: operations tab type");
|
|
17427
|
+
ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
|
|
17428
|
+
name: "sidebar.right.pane.tab",
|
|
17429
|
+
key: OPS_TAB_ID,
|
|
17430
|
+
inject: () => ({
|
|
17431
|
+
api,
|
|
17432
|
+
target
|
|
17433
|
+
})
|
|
17434
|
+
}, SshPanel));
|
|
17435
|
+
ctx.slots.inject("sidebar.right.pane.tab.title", () => ctx.slots.register({
|
|
17436
|
+
name: "sidebar.right.pane.tab.title",
|
|
17437
|
+
key: OPS_TAB_ID
|
|
17438
|
+
}, OpsTabTitle));
|
|
17579
17439
|
}
|
|
17580
|
-
/**
|
|
17581
|
-
|
|
17582
|
-
|
|
17583
|
-
* @param controller - the panel controller the entry toggles.
|
|
17584
|
-
* @returns disposer removing the entry and its observers.
|
|
17585
|
-
*/
|
|
17586
|
-
function mountSidebarEntry(controller) {
|
|
17587
|
-
const entry = createEntry(controller);
|
|
17588
|
-
let root;
|
|
17589
|
-
let placed = false;
|
|
17590
|
-
const tryPlace = () => {
|
|
17591
|
-
if (placed) return;
|
|
17592
|
-
if (root !== void 0 && !root.isConnected) {
|
|
17593
|
-
rootObserver.disconnect();
|
|
17594
|
-
root = void 0;
|
|
17595
|
-
}
|
|
17596
|
-
root ??= sidebarRoot();
|
|
17597
|
-
if (root === void 0) return;
|
|
17598
|
-
placed = placeEntry(root, entry);
|
|
17599
|
-
if (placed) rootObserver.observe(root, {
|
|
17600
|
-
childList: true,
|
|
17601
|
-
subtree: true
|
|
17602
|
-
});
|
|
17603
|
-
};
|
|
17604
|
-
const waitObserver = new MutationObserver(() => {
|
|
17605
|
-
tryPlace();
|
|
17606
|
-
});
|
|
17607
|
-
waitObserver.observe(document.body, {
|
|
17608
|
-
childList: true,
|
|
17609
|
-
subtree: true
|
|
17610
|
-
});
|
|
17611
|
-
const rootObserver = new MutationObserver(() => {
|
|
17612
|
-
if (root === void 0 || !root.isConnected) {
|
|
17613
|
-
placed = false;
|
|
17614
|
-
tryPlace();
|
|
17615
|
-
return;
|
|
17616
|
-
}
|
|
17617
|
-
if (!root.contains(entry)) placed = placeEntry(root, entry);
|
|
17618
|
-
});
|
|
17619
|
-
const unsubscribe = controller.subscribe(() => {
|
|
17620
|
-
entry.dataset.active = controller.getSnapshot().panelOpen ? "true" : void 0;
|
|
17621
|
-
});
|
|
17622
|
-
entry.dataset.active = controller.getSnapshot().panelOpen ? "true" : void 0;
|
|
17623
|
-
tryPlace();
|
|
17624
|
-
return () => {
|
|
17625
|
-
waitObserver.disconnect();
|
|
17626
|
-
rootObserver.disconnect();
|
|
17627
|
-
unsubscribe();
|
|
17628
|
-
entry.remove();
|
|
17629
|
-
};
|
|
17440
|
+
/** The tab chip and floating-panel header text. */
|
|
17441
|
+
function OpsTabTitle() {
|
|
17442
|
+
return tt$1("ops.tab");
|
|
17630
17443
|
}
|
|
17631
17444
|
//#endregion
|
|
17632
17445
|
//#region src/client/ssh/apply.ts
|
|
17633
17446
|
/** Locale namespace this capability owns (kept as 'dsh-ssh'). */
|
|
17634
17447
|
const NS = "dsh-ssh";
|
|
17635
17448
|
/**
|
|
17636
|
-
* Mount the SSH operations surfaces (
|
|
17637
|
-
* @param ctx - client root context (locale service).
|
|
17449
|
+
* Mount the SSH operations surfaces (locale dictionaries + the right-Sidebar tab).
|
|
17450
|
+
* @param ctx - client root context (locale service, slot registry, tab registry).
|
|
17451
|
+
* @param api - shared SSH API client.
|
|
17452
|
+
* @param target - selected Session's fixed SSH target, or null for local Sessions.
|
|
17638
17453
|
*/
|
|
17639
|
-
function mountSshOperations(ctx) {
|
|
17454
|
+
function mountSshOperations(ctx, api, target) {
|
|
17455
|
+
try {
|
|
17456
|
+
registerOperationsTab(ctx, api, target);
|
|
17457
|
+
} catch (error) {
|
|
17458
|
+
console.warn("[dsh-ssh] operations tab registration failed:", error);
|
|
17459
|
+
}
|
|
17640
17460
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
17641
17461
|
zh,
|
|
17642
17462
|
en
|
|
17643
17463
|
}), "dsh-ssh: dictionaries");
|
|
17644
|
-
|
|
17645
|
-
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
|
|
17464
|
+
}
|
|
17465
|
+
//#endregion
|
|
17466
|
+
//#region src/client/ssh/session-target.ts
|
|
17467
|
+
/** Build one stable Session → SSH target source over the public session list and workspace manager. */
|
|
17468
|
+
function createSessionSshTargetSource(sessions, manager) {
|
|
17469
|
+
const workspaceForCwd = makeAnchorWorkspaceResolver(() => manager.getSnapshot().workspaces);
|
|
17470
|
+
let cachedKey;
|
|
17471
|
+
let cachedValue = null;
|
|
17472
|
+
const getSnapshot = () => {
|
|
17473
|
+
const sessionId = sessions.currentSessionId();
|
|
17474
|
+
const cwd = sessionId === void 0 ? void 0 : sessions.sessionCwd(sessionId);
|
|
17475
|
+
const workspace = workspaceForCwd(cwd);
|
|
17476
|
+
const key = workspace === void 0 || sessionId === void 0 ? `local\u0000${sessionId ?? ""}\u0000${cwd ?? ""}` : `ssh\u0000${sessionId}\u0000${workspace.id}\u0000${workspace.alias}\u0000${workspace.remoteRoot}`;
|
|
17477
|
+
if (key === cachedKey) return cachedValue;
|
|
17478
|
+
cachedKey = key;
|
|
17479
|
+
cachedValue = workspace === void 0 || sessionId === void 0 ? null : {
|
|
17480
|
+
sessionId,
|
|
17481
|
+
workspaceId: workspace.id,
|
|
17482
|
+
alias: workspace.alias,
|
|
17483
|
+
remoteRoot: workspace.remoteRoot
|
|
17484
|
+
};
|
|
17485
|
+
return cachedValue;
|
|
17486
|
+
};
|
|
17487
|
+
return {
|
|
17488
|
+
getSnapshot,
|
|
17489
|
+
subscribe: (listener) => {
|
|
17490
|
+
const disposeSessions = sessions.subscribe(listener);
|
|
17491
|
+
const disposeWorkspaces = manager.subscribe(listener);
|
|
17492
|
+
return () => {
|
|
17493
|
+
disposeSessions();
|
|
17494
|
+
disposeWorkspaces();
|
|
17495
|
+
};
|
|
17496
|
+
}
|
|
17497
|
+
};
|
|
17656
17498
|
}
|
|
17657
17499
|
//#endregion
|
|
17658
17500
|
//#region src/client/index.ts
|
|
17659
|
-
/** Required services: slots for the
|
|
17660
|
-
*
|
|
17501
|
+
/** Required services: slots for the extension seats, locale for copy,
|
|
17502
|
+
* workspaces for the native local-directory chooser, sessions for the
|
|
17503
|
+
* open/new-session connection gate, and sidebarRightTabs for the SSH
|
|
17504
|
+
* operations console type. Keep the registry explicit: without it Cordis may
|
|
17505
|
+
* activate this client before ui-sidebar-right provides the service; the
|
|
17506
|
+
* guarded registration then degrades silently and no HardSSH tab exists. */
|
|
17661
17507
|
const inject = [
|
|
17662
17508
|
"slots",
|
|
17663
17509
|
"locale",
|
|
17664
|
-
"workspaces"
|
|
17510
|
+
"workspaces",
|
|
17511
|
+
"sessions",
|
|
17512
|
+
"sidebarRightTabs"
|
|
17665
17513
|
];
|
|
17666
17514
|
/** Apply the browser half. */
|
|
17667
17515
|
function apply(ctx) {
|
|
17668
|
-
mountSshOperations(ctx);
|
|
17669
|
-
ctx.effect(() => ctx.locale.register(NS$1, dictionaries), "dsh-hardssh: dictionaries");
|
|
17670
17516
|
const api = new WorkspaceApi();
|
|
17671
17517
|
const hostsApi = new SshHostsApi();
|
|
17672
17518
|
const sshApi = new SshApi();
|
|
17673
17519
|
const manager = new WorkspaceManager(api);
|
|
17520
|
+
const sessionList = ctx.sessions.list;
|
|
17521
|
+
const sessions = {
|
|
17522
|
+
subscribe: (listener) => sessionList.subscribe(listener),
|
|
17523
|
+
phase: () => sessionList.getSnapshot().phase,
|
|
17524
|
+
currentSessionId: () => sessionList.getSnapshot().current,
|
|
17525
|
+
sessionIds: () => sessionList.getSnapshot().ids,
|
|
17526
|
+
sessionCwd: (id) => {
|
|
17527
|
+
return sessionList.getSnapshot().byId[id]?.cwd;
|
|
17528
|
+
}
|
|
17529
|
+
};
|
|
17530
|
+
mountSshOperations(ctx, sshApi, createSessionSshTargetSource(sessions, manager));
|
|
17531
|
+
ctx.effect(() => ctx.locale.register(NS$1, dictionaries), "dsh-hardssh: dictionaries");
|
|
17674
17532
|
const disposers = [];
|
|
17675
17533
|
try {
|
|
17676
|
-
ctx
|
|
17677
|
-
|
|
17678
|
-
|
|
17679
|
-
id: "ssh-workspace-manager",
|
|
17680
|
-
order: -10,
|
|
17681
|
-
inject: () => ({
|
|
17682
|
-
manager,
|
|
17683
|
-
sshApi
|
|
17684
|
-
})
|
|
17685
|
-
}, WorkspaceManagerButton);
|
|
17534
|
+
registerWorkspacePanel(ctx, {
|
|
17535
|
+
manager,
|
|
17536
|
+
sshApi
|
|
17686
17537
|
});
|
|
17687
17538
|
const flowInject = () => ({
|
|
17688
17539
|
pickDirectory: () => ctx.workspaces.pickDirectory(),
|
|
@@ -17743,25 +17594,10 @@ window.__ModuleLoader__.load({
|
|
|
17743
17594
|
window.clearInterval(connTimer);
|
|
17744
17595
|
});
|
|
17745
17596
|
refreshConnections();
|
|
17746
|
-
|
|
17747
|
-
|
|
17748
|
-
|
|
17749
|
-
|
|
17750
|
-
gateDispose?.();
|
|
17751
|
-
gateDispose = mountWorkspaceGates(workspaces.map((workspace) => ({
|
|
17752
|
-
id: workspace.id,
|
|
17753
|
-
title: workspace.title,
|
|
17754
|
-
alias: workspace.alias
|
|
17755
|
-
})), sshApi);
|
|
17756
|
-
} catch (error) {
|
|
17757
|
-
console.warn("[dsh-hardssh] gate refresh failed:", error);
|
|
17758
|
-
}
|
|
17759
|
-
};
|
|
17760
|
-
disposers.push(() => {
|
|
17761
|
-
gateDispose?.();
|
|
17762
|
-
});
|
|
17763
|
-
disposers.push(manager.subscribe(() => {
|
|
17764
|
-
refreshGates();
|
|
17597
|
+
disposers.push(mountSessionConnectGate({
|
|
17598
|
+
sessions,
|
|
17599
|
+
aliasForCwd: makeAnchorAliasResolver(() => manager.getSnapshot().workspaces),
|
|
17600
|
+
ensureConnected: (alias) => connectHost(sshApi, alias)
|
|
17765
17601
|
}));
|
|
17766
17602
|
} catch (error) {
|
|
17767
17603
|
console.warn("[dsh-hardssh] mount failed:", error);
|