@tiphareth/dsh-hardssh 0.1.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +30 -0
- package/README.md +77 -50
- package/SKILLS.md +140 -0
- package/lib/base/index.js +95 -0
- package/lib/client.js +1171 -1321
- package/lib/fs.js +333 -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 +42 -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 +59 -26
- 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 -96
- 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 +171 -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 -225
- 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,92 +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 (idempotent: skip rows already tagged). */
|
|
2586
|
-
const decorate = (row) => {
|
|
2587
|
-
if (row.querySelector("[data-ssh-badge]") !== null) return;
|
|
2588
|
-
const titleEl = row.querySelector(TITLE_SELECTOR$1);
|
|
2589
|
-
if (titleEl === null || titleEl === void 0) return;
|
|
2590
|
-
const title = titleEl.textContent?.trim() ?? "";
|
|
2591
|
-
const target = byTitle.get(title);
|
|
2592
|
-
if (target === void 0) return;
|
|
2593
|
-
const isConnected = connected?.has(target.alias) ?? false;
|
|
2594
|
-
row.title = `${target.remoteRoot}(${target.alias})`;
|
|
2595
|
-
titleEl.appendChild(makeBadge(target.alias, target.remoteRoot, isConnected));
|
|
2596
|
-
};
|
|
2597
|
-
/** Scan the sidebar column for project rows and decorate each. */
|
|
2598
|
-
const scan = () => {
|
|
2599
|
-
for (const row of document.querySelectorAll(PROJECT_ROW_SELECTOR$1)) decorate(row);
|
|
2600
|
-
};
|
|
2601
|
-
const observer = new MutationObserver(() => {
|
|
2602
|
-
scan();
|
|
2603
|
-
});
|
|
2604
|
-
observer.observe(document.body, {
|
|
2605
|
-
childList: true,
|
|
2606
|
-
subtree: true
|
|
2607
|
-
});
|
|
2608
|
-
scan();
|
|
2609
|
-
return () => {
|
|
2610
|
-
observer.disconnect();
|
|
2611
|
-
for (const badge of document.querySelectorAll("[data-ssh-badge]")) badge.remove();
|
|
2612
|
-
};
|
|
2613
|
-
}
|
|
2614
|
-
//#endregion
|
|
2615
|
-
//#region src/client/ssh/panel/HostFingerprintDialog.tsx
|
|
2616
|
-
/**
|
|
2617
|
-
* Host fingerprint confirm dialog: shown when a connection is refused because
|
|
2618
|
-
* the server's host key is not trusted yet (TOFU first encounter) or changed
|
|
2619
|
-
* (possible MITM / key rotation). Displays the canonical SHA256 fingerprint
|
|
2620
|
-
* 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.
|
|
2621
2829
|
*/
|
|
2622
2830
|
/** Confirm (or reset) one host key. */
|
|
2623
2831
|
function HostFingerprintDialog({ api, alias, fingerprintSha256, mismatch, onClose, onTrusted }) {
|
|
@@ -2729,7 +2937,7 @@ window.__ModuleLoader__.load({
|
|
|
2729
2937
|
* process exit. Nothing is written to disk.
|
|
2730
2938
|
*/
|
|
2731
2939
|
/** Prompt for a session-only credential. */
|
|
2732
|
-
function SessionSecretDialog({ api, alias, user, secret, onClose, onProvided }) {
|
|
2940
|
+
function SessionSecretDialog({ api, alias, user, secret, reason, onClose, onProvided }) {
|
|
2733
2941
|
const [value, setValue] = (0, react.useState)("");
|
|
2734
2942
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
2735
2943
|
const [error, setError] = (0, react.useState)(null);
|
|
@@ -2770,22 +2978,29 @@ window.__ModuleLoader__.load({
|
|
|
2770
2978
|
}),
|
|
2771
2979
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2772
2980
|
className: panel_module_css_default.modalBody,
|
|
2773
|
-
children: [
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
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
|
+
]
|
|
2789
3004
|
}),
|
|
2790
3005
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2791
3006
|
className: panel_module_css_default.modalFooter,
|
|
@@ -2810,58 +3025,58 @@ window.__ModuleLoader__.load({
|
|
|
2810
3025
|
});
|
|
2811
3026
|
}
|
|
2812
3027
|
//#endregion
|
|
2813
|
-
//#region src/client/
|
|
3028
|
+
//#region src/client/connect-host.ts
|
|
2814
3029
|
/**
|
|
2815
|
-
*
|
|
2816
|
-
* workspace row in the session sidebar, ensure the server connection can
|
|
2817
|
-
* actually be established BEFORE the shell proceeds with the native open —
|
|
2818
|
-
* otherwise the shell surfaces raw SSH errors (host key untrusted, session
|
|
2819
|
-
* password missing) and the workspace appears "dead" with no prompt.
|
|
3030
|
+
* Probe-and-prompt for an SSH alias, with a browser-session pass cache.
|
|
2820
3031
|
*
|
|
2821
|
-
*
|
|
2822
|
-
*
|
|
2823
|
-
*
|
|
2824
|
-
* - NEEDS_PASSWORD / NEEDS_PASSPHRASE → the VSCode-style session password
|
|
2825
|
-
* dialog (SessionSecretDialog; never persisted, session-only);
|
|
2826
|
-
* - ok → mark the alias as gated (5-minute TTL — trust is durable and the
|
|
2827
|
-
* 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.
|
|
2828
3035
|
*
|
|
2829
|
-
*
|
|
2830
|
-
*
|
|
2831
|
-
*
|
|
2832
|
-
*
|
|
2833
|
-
*
|
|
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).
|
|
2834
3043
|
*
|
|
2835
|
-
* @module dsh-hardssh/client/
|
|
3044
|
+
* @module dsh-hardssh/client/connect-host
|
|
2836
3045
|
*/
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
/** A passed gate stays valid this long (trust is durable; the session
|
|
2840
|
-
* 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). */
|
|
2841
3048
|
const GATE_TTL_MS = 3e5;
|
|
2842
3049
|
/** Cap on interactive retry rounds (wrong password loops etc.). */
|
|
2843
3050
|
const MAX_ATTEMPTS = 3;
|
|
2844
|
-
/** alias → last successful
|
|
2845
|
-
const
|
|
2846
|
-
/** 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). */
|
|
2847
3054
|
const pending = /* @__PURE__ */ new Map();
|
|
2848
3055
|
/** alias → SSH login name (fetched once for the user@alias prompt). */
|
|
2849
3056
|
let hostsCache = null;
|
|
3057
|
+
/** At most one visible non-interactive failure dialog per alias. */
|
|
3058
|
+
const failureDialogs = /* @__PURE__ */ new Map();
|
|
2850
3059
|
async function hostsUser(api, alias) {
|
|
2851
3060
|
if (hostsCache === null) try {
|
|
2852
3061
|
const list = await api.listHosts();
|
|
2853
3062
|
hostsCache = new Map(list.map((host) => [host.alias, host.user]));
|
|
2854
3063
|
} catch {
|
|
2855
|
-
|
|
3064
|
+
return;
|
|
2856
3065
|
}
|
|
2857
3066
|
return hostsCache.get(alias);
|
|
2858
3067
|
}
|
|
3068
|
+
/** Whether the alias passed the gate within its TTL. */
|
|
2859
3069
|
function gateActive(alias, now) {
|
|
2860
|
-
const at =
|
|
3070
|
+
const at = passedAt.get(alias);
|
|
2861
3071
|
return at !== void 0 && now - at < GATE_TTL_MS;
|
|
2862
3072
|
}
|
|
2863
|
-
/**
|
|
2864
|
-
*
|
|
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
|
+
*/
|
|
2865
3080
|
function mountModal(node) {
|
|
2866
3081
|
const host = document.createElement("div");
|
|
2867
3082
|
host.dataset.sshGateDialog = "";
|
|
@@ -2871,24 +3086,31 @@ window.__ModuleLoader__.load({
|
|
|
2871
3086
|
root.unmount();
|
|
2872
3087
|
host.remove();
|
|
2873
3088
|
};
|
|
3089
|
+
const callerOnClose = node.props.onClose;
|
|
3090
|
+
const chainedOnClose = callerOnClose !== void 0 ? () => {
|
|
3091
|
+
callerOnClose();
|
|
3092
|
+
close();
|
|
3093
|
+
} : close;
|
|
2874
3094
|
root.render((0, react.createElement)(node.type, {
|
|
2875
3095
|
...node.props,
|
|
2876
|
-
onClose:
|
|
3096
|
+
onClose: chainedOnClose
|
|
2877
3097
|
}));
|
|
2878
3098
|
return close;
|
|
2879
3099
|
}
|
|
2880
|
-
/**
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
if (
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
}
|
|
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;
|
|
2892
3114
|
}
|
|
2893
3115
|
/** Interactive prompts, resolving true when the user finished the action. */
|
|
2894
3116
|
function promptFingerprint(api, alias, fingerprintSha256, mismatch) {
|
|
@@ -2908,7 +3130,7 @@ window.__ModuleLoader__.load({
|
|
|
2908
3130
|
}));
|
|
2909
3131
|
});
|
|
2910
3132
|
}
|
|
2911
|
-
function promptSecret(api, alias, secret) {
|
|
3133
|
+
function promptSecret(api, alias, secret, reason) {
|
|
2912
3134
|
return new Promise((resolve) => {
|
|
2913
3135
|
let closed = false;
|
|
2914
3136
|
const finish = (ok) => {
|
|
@@ -2922,6 +3144,7 @@ window.__ModuleLoader__.load({
|
|
|
2922
3144
|
alias,
|
|
2923
3145
|
user,
|
|
2924
3146
|
secret,
|
|
3147
|
+
reason,
|
|
2925
3148
|
onClose: () => {
|
|
2926
3149
|
finish(false);
|
|
2927
3150
|
},
|
|
@@ -2934,39 +3157,56 @@ window.__ModuleLoader__.load({
|
|
|
2934
3157
|
});
|
|
2935
3158
|
}
|
|
2936
3159
|
/**
|
|
2937
|
-
* Probe + prompt until connected. Resolves true when the alias may proceed
|
|
2938
|
-
*
|
|
2939
|
-
*
|
|
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.
|
|
2940
3168
|
*/
|
|
2941
|
-
function
|
|
3169
|
+
function connectHost(api, alias) {
|
|
2942
3170
|
if (gateActive(alias, Date.now())) return Promise.resolve(true);
|
|
2943
3171
|
const inFlight = pending.get(alias);
|
|
2944
3172
|
if (inFlight !== void 0) return inFlight;
|
|
2945
3173
|
const attempt = (async () => {
|
|
3174
|
+
let secretKind;
|
|
3175
|
+
let lastFailure = null;
|
|
2946
3176
|
for (let i = 0; i < MAX_ATTEMPTS; i++) try {
|
|
2947
3177
|
const result = await api.testHost(alias);
|
|
2948
3178
|
if (result.ok) {
|
|
2949
|
-
|
|
3179
|
+
passedAt.set(alias, Date.now());
|
|
2950
3180
|
return true;
|
|
2951
3181
|
}
|
|
2952
3182
|
if (result.code === "NEEDS_PASSWORD" && (result.secret === "password" || result.secret === "passphrase")) {
|
|
2953
|
-
|
|
3183
|
+
secretKind = result.secret;
|
|
3184
|
+
if (await promptSecret(api, alias, result.secret, lastFailure ?? void 0)) {
|
|
3185
|
+
lastFailure = null;
|
|
3186
|
+
continue;
|
|
3187
|
+
}
|
|
2954
3188
|
return false;
|
|
2955
3189
|
}
|
|
2956
|
-
|
|
2957
|
-
return
|
|
3190
|
+
lastFailure = result.error ?? "connection failed";
|
|
3191
|
+
return reportConnectionFailure(alias, lastFailure);
|
|
2958
3192
|
} catch (cause) {
|
|
2959
|
-
if (cause instanceof
|
|
3193
|
+
if (cause instanceof HttpApiError && (cause.code === "HOST_KEY_UNKNOWN" || cause.code === "HOST_KEY_MISMATCH")) {
|
|
2960
3194
|
const fingerprint = cause.hostKeyFingerprint ?? "";
|
|
2961
3195
|
if (fingerprint !== "" && await promptFingerprint(api, alias, fingerprint, cause.code === "HOST_KEY_MISMATCH")) continue;
|
|
2962
|
-
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
|
+
}
|
|
2963
3205
|
return false;
|
|
2964
3206
|
}
|
|
2965
|
-
|
|
2966
|
-
return false;
|
|
3207
|
+
return reportConnectionFailure(alias, lastFailure);
|
|
2967
3208
|
}
|
|
2968
|
-
|
|
2969
|
-
return false;
|
|
3209
|
+
return reportConnectionFailure(alias, lastFailure ?? tt$1("gate.retryLimit"));
|
|
2970
3210
|
})();
|
|
2971
3211
|
pending.set(alias, attempt);
|
|
2972
3212
|
attempt.finally(() => {
|
|
@@ -2974,68 +3214,199 @@ window.__ModuleLoader__.load({
|
|
|
2974
3214
|
});
|
|
2975
3215
|
return attempt;
|
|
2976
3216
|
}
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
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;
|
|
2981
3254
|
}
|
|
2982
3255
|
/**
|
|
2983
|
-
* Mount the
|
|
2984
|
-
* to an SSH
|
|
2985
|
-
*
|
|
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.
|
|
2986
3264
|
*/
|
|
2987
|
-
function
|
|
3265
|
+
function mountWorkspaceBadges(workspaces, connected) {
|
|
2988
3266
|
const byTitle = /* @__PURE__ */ new Map();
|
|
2989
|
-
for (const workspace of workspaces) byTitle.set(workspace.title, workspace
|
|
2990
|
-
|
|
2991
|
-
|
|
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) => {
|
|
2992
3273
|
const titleEl = row.querySelector(TITLE_SELECTOR);
|
|
2993
3274
|
if (titleEl === null || titleEl === void 0) return;
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
if (ok) row.click();
|
|
3003
|
-
});
|
|
3004
|
-
}, 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));
|
|
3005
3283
|
};
|
|
3284
|
+
/** Scan the sidebar column for project rows and decorate each. */
|
|
3006
3285
|
const scan = () => {
|
|
3007
|
-
for (const row of document.querySelectorAll(PROJECT_ROW_SELECTOR))
|
|
3286
|
+
for (const row of document.querySelectorAll(PROJECT_ROW_SELECTOR)) decorate(row);
|
|
3008
3287
|
};
|
|
3009
3288
|
const observer = new MutationObserver(() => {
|
|
3010
3289
|
scan();
|
|
3011
3290
|
});
|
|
3012
3291
|
observer.observe(document.body, {
|
|
3013
3292
|
childList: true,
|
|
3014
|
-
subtree: true
|
|
3293
|
+
subtree: true,
|
|
3294
|
+
attributes: true,
|
|
3295
|
+
attributeFilter: ["title"]
|
|
3015
3296
|
});
|
|
3016
3297
|
scan();
|
|
3017
3298
|
return () => {
|
|
3018
3299
|
observer.disconnect();
|
|
3019
|
-
for (const
|
|
3300
|
+
for (const badge of document.querySelectorAll("[data-ssh-badge]")) badge.remove();
|
|
3020
3301
|
};
|
|
3021
3302
|
}
|
|
3022
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
|
+
}
|
|
3395
|
+
};
|
|
3396
|
+
const dispose = deps.sessions.subscribe(pass);
|
|
3397
|
+
pass();
|
|
3398
|
+
return dispose;
|
|
3399
|
+
}
|
|
3400
|
+
//#endregion
|
|
3023
3401
|
//#region src/client/ssh/panel/ClusterTab.tsx
|
|
3024
3402
|
/**
|
|
3025
|
-
* Cluster tab: run one command
|
|
3026
|
-
*
|
|
3403
|
+
* Cluster tab: run one command against the selected Session's fixed server and
|
|
3404
|
+
* inspect its result. Stdout/stderr render in
|
|
3027
3405
|
* collapsed <details> blocks; status renders as a colored badge.
|
|
3028
3406
|
*/
|
|
3029
|
-
/** Split a comma-separated input into a trimmed, non-empty string list. */
|
|
3030
|
-
function splitList(text) {
|
|
3031
|
-
return text.split(",").map((part) => part.trim()).filter((part) => part !== "");
|
|
3032
|
-
}
|
|
3033
3407
|
/** The cluster execution tab. */
|
|
3034
|
-
function ClusterTab({ api }) {
|
|
3408
|
+
function ClusterTab({ api, alias }) {
|
|
3035
3409
|
const [command, setCommand] = (0, react.useState)("");
|
|
3036
|
-
const [aliases, setAliases] = (0, react.useState)("");
|
|
3037
|
-
const [environment, setEnvironment] = (0, react.useState)("");
|
|
3038
|
-
const [tags, setTags] = (0, react.useState)("");
|
|
3039
3410
|
const [running, setRunning] = (0, react.useState)(false);
|
|
3040
3411
|
const [results, setResults] = (0, react.useState)(null);
|
|
3041
3412
|
const [error, setError] = (0, react.useState)(null);
|
|
@@ -3045,13 +3416,9 @@ window.__ModuleLoader__.load({
|
|
|
3045
3416
|
setRunning(true);
|
|
3046
3417
|
setError(null);
|
|
3047
3418
|
try {
|
|
3048
|
-
const aliasList = splitList(aliases);
|
|
3049
|
-
const tagList = splitList(tags);
|
|
3050
3419
|
const outcome = await api.cluster({
|
|
3051
3420
|
command: command.trim(),
|
|
3052
|
-
aliases:
|
|
3053
|
-
environment: environment.trim() === "" ? void 0 : environment.trim(),
|
|
3054
|
-
tags: tagList.length > 0 ? tagList : void 0
|
|
3421
|
+
aliases: [alias]
|
|
3055
3422
|
});
|
|
3056
3423
|
setResults(outcome);
|
|
3057
3424
|
} catch (cause) {
|
|
@@ -3080,421 +3447,93 @@ window.__ModuleLoader__.load({
|
|
|
3080
3447
|
})]
|
|
3081
3448
|
}),
|
|
3082
3449
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3083
|
-
className: panel_module_css_default.
|
|
3084
|
-
children: [
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
}
|
|
3092
|
-
}),
|
|
3093
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3094
|
-
className: panel_module_css_default.input,
|
|
3095
|
-
placeholder: tt("cluster.environment"),
|
|
3096
|
-
value: environment,
|
|
3097
|
-
onChange: (event) => {
|
|
3098
|
-
setEnvironment(event.target.value);
|
|
3099
|
-
}
|
|
3100
|
-
}),
|
|
3101
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3102
|
-
className: panel_module_css_default.input,
|
|
3103
|
-
placeholder: tt("cluster.tags"),
|
|
3104
|
-
value: tags,
|
|
3105
|
-
onChange: (event) => {
|
|
3106
|
-
setTags(event.target.value);
|
|
3107
|
-
}
|
|
3108
|
-
})
|
|
3109
|
-
]
|
|
3110
|
-
}),
|
|
3111
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3112
|
-
type: "button",
|
|
3113
|
-
className: panel_module_css_default.primaryButton,
|
|
3114
|
-
disabled: running || command.trim() === "",
|
|
3115
|
-
onClick: () => {
|
|
3116
|
-
run();
|
|
3117
|
-
},
|
|
3118
|
-
children: tt("cluster.run")
|
|
3119
|
-
}) })
|
|
3120
|
-
]
|
|
3121
|
-
}),
|
|
3122
|
-
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3123
|
-
className: panel_module_css_default.banner,
|
|
3124
|
-
"data-kind": "error",
|
|
3125
|
-
children: tt("common.error", { error })
|
|
3126
|
-
}),
|
|
3127
|
-
results === null && error === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3128
|
-
className: panel_module_css_default.empty,
|
|
3129
|
-
children: tt("cluster.empty")
|
|
3130
|
-
}),
|
|
3131
|
-
results !== null && results.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3132
|
-
className: panel_module_css_default.empty,
|
|
3133
|
-
children: tt("cluster.noMatch")
|
|
3134
|
-
}),
|
|
3135
|
-
results !== null && results.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3136
|
-
className: panel_module_css_default.tableWrap,
|
|
3137
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
3138
|
-
className: panel_module_css_default.table,
|
|
3139
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
3140
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.alias") }),
|
|
3141
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.status") }),
|
|
3142
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.exit") }),
|
|
3143
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.stdout") }),
|
|
3144
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.stderr") }),
|
|
3145
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.error") }),
|
|
3146
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("cluster.col.duration") })
|
|
3147
|
-
] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: results.map((result) => {
|
|
3148
|
-
const status = result.ok ? "ok" : result.timedOut === true ? "timeout" : "fail";
|
|
3149
|
-
const label = result.ok ? tt("cluster.ok") : result.timedOut === true ? tt("cluster.timeout") : tt("cluster.fail");
|
|
3150
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
3151
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3152
|
-
className: panel_module_css_default.mono,
|
|
3153
|
-
children: result.alias
|
|
3154
|
-
}),
|
|
3155
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3156
|
-
className: panel_module_css_default.badge,
|
|
3157
|
-
"data-status": status,
|
|
3158
|
-
children: label
|
|
3159
|
-
}) }),
|
|
3160
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3161
|
-
className: panel_module_css_default.mono,
|
|
3162
|
-
children: result.exitCode ?? "-"
|
|
3163
|
-
}),
|
|
3164
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: result.stdout !== void 0 && result.stdout !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
3165
|
-
className: panel_module_css_default.cellDetails,
|
|
3166
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: tt("cluster.col.stdout") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
3167
|
-
className: panel_module_css_default.cellPre,
|
|
3168
|
-
children: result.stdout
|
|
3169
|
-
})]
|
|
3170
|
-
}) }),
|
|
3171
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: result.stderr !== void 0 && result.stderr !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
3172
|
-
className: panel_module_css_default.cellDetails,
|
|
3173
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: tt("cluster.col.stderr") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
3174
|
-
className: panel_module_css_default.cellPre,
|
|
3175
|
-
children: result.stderr
|
|
3176
|
-
})]
|
|
3177
|
-
}) }),
|
|
3178
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3179
|
-
className: panel_module_css_default.cellMuted,
|
|
3180
|
-
children: result.error ?? ""
|
|
3181
|
-
}),
|
|
3182
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3183
|
-
className: panel_module_css_default.mono,
|
|
3184
|
-
children: result.durationMs !== void 0 ? result.durationMs + " ms" : "-"
|
|
3185
|
-
})
|
|
3186
|
-
] }, result.alias);
|
|
3187
|
-
}) })]
|
|
3188
|
-
})
|
|
3189
|
-
})
|
|
3190
|
-
]
|
|
3191
|
-
});
|
|
3192
|
-
}
|
|
3193
|
-
//#endregion
|
|
3194
|
-
//#region src/client/ssh/panel/HostsTab.tsx
|
|
3195
|
-
/**
|
|
3196
|
-
* Hosts tab: the host table with search (debounced through listHosts),
|
|
3197
|
-
* add/edit/delete/test actions, ~/.ssh/config import, and a connect action
|
|
3198
|
-
* that hands the alias to the terminal tab via onConnect.
|
|
3199
|
-
*/
|
|
3200
|
-
/** The hosts table plus its toolbar and dialogs. */
|
|
3201
|
-
function HostsTab({ api, onConnect }) {
|
|
3202
|
-
const [hosts, setHosts] = (0, react.useState)(null);
|
|
3203
|
-
const [error, setError] = (0, react.useState)(null);
|
|
3204
|
-
const [search, setSearch] = (0, react.useState)("");
|
|
3205
|
-
const [testingAlias, setTestingAlias] = (0, react.useState)(null);
|
|
3206
|
-
const [testResults, setTestResults] = (0, react.useState)({});
|
|
3207
|
-
const [importing, setImporting] = (0, react.useState)(false);
|
|
3208
|
-
const [notice, setNotice] = (0, react.useState)(null);
|
|
3209
|
-
const [dialog, setDialog] = (0, react.useState)(null);
|
|
3210
|
-
const [hostKeyConfirm, setHostKeyConfirm] = (0, react.useState)(null);
|
|
3211
|
-
const [secretConfirm, setSecretConfirm] = (0, react.useState)(null);
|
|
3212
|
-
const seqRef = (0, react.useRef)(0);
|
|
3213
|
-
const load = (0, react.useCallback)(async (query) => {
|
|
3214
|
-
const seq = ++seqRef.current;
|
|
3215
|
-
try {
|
|
3216
|
-
const list = await api.listHosts(query);
|
|
3217
|
-
if (seq !== seqRef.current) return;
|
|
3218
|
-
setHosts(list);
|
|
3219
|
-
setError(null);
|
|
3220
|
-
} catch (cause) {
|
|
3221
|
-
if (seq !== seqRef.current) return;
|
|
3222
|
-
setError(errorMessage(cause));
|
|
3223
|
-
}
|
|
3224
|
-
}, [api]);
|
|
3225
|
-
(0, react.useEffect)(() => {
|
|
3226
|
-
load();
|
|
3227
|
-
}, [load]);
|
|
3228
|
-
(0, react.useEffect)(() => {
|
|
3229
|
-
const timer = setTimeout(() => {
|
|
3230
|
-
const query = search.trim();
|
|
3231
|
-
load(query === "" ? void 0 : query);
|
|
3232
|
-
}, 300);
|
|
3233
|
-
return () => {
|
|
3234
|
-
clearTimeout(timer);
|
|
3235
|
-
};
|
|
3236
|
-
}, [search, load]);
|
|
3237
|
-
const runTest = async (alias) => {
|
|
3238
|
-
setTestingAlias(alias);
|
|
3239
|
-
try {
|
|
3240
|
-
const result = await api.testHost(alias);
|
|
3241
|
-
if (result.code === "NEEDS_PASSWORD" && (result.secret === "password" || result.secret === "passphrase")) {
|
|
3242
|
-
setSecretConfirm({
|
|
3243
|
-
alias,
|
|
3244
|
-
secret: result.secret,
|
|
3245
|
-
user: hosts?.find((host) => host.alias === alias)?.user
|
|
3246
|
-
});
|
|
3247
|
-
setTestingAlias(null);
|
|
3248
|
-
return;
|
|
3249
|
-
}
|
|
3250
|
-
setTestResults((prev) => ({
|
|
3251
|
-
...prev,
|
|
3252
|
-
[alias]: result
|
|
3253
|
-
}));
|
|
3254
|
-
} catch (cause) {
|
|
3255
|
-
if (cause instanceof SshApiError && (cause.code === "HOST_KEY_UNKNOWN" || cause.code === "HOST_KEY_MISMATCH")) {
|
|
3256
|
-
setHostKeyConfirm({
|
|
3257
|
-
alias,
|
|
3258
|
-
fingerprintSha256: cause.hostKeyFingerprint ?? "",
|
|
3259
|
-
mismatch: cause.code === "HOST_KEY_MISMATCH"
|
|
3260
|
-
});
|
|
3261
|
-
setTestingAlias(null);
|
|
3262
|
-
return;
|
|
3263
|
-
}
|
|
3264
|
-
setTestResults((prev) => ({
|
|
3265
|
-
...prev,
|
|
3266
|
-
[alias]: {
|
|
3267
|
-
ok: false,
|
|
3268
|
-
error: errorMessage(cause)
|
|
3269
|
-
}
|
|
3270
|
-
}));
|
|
3271
|
-
} finally {
|
|
3272
|
-
setTestingAlias(null);
|
|
3273
|
-
}
|
|
3274
|
-
};
|
|
3275
|
-
/** After the fingerprint is confirmed (or reset), retry the test. */
|
|
3276
|
-
const retryAfterFingerprint = async (alias) => {
|
|
3277
|
-
setHostKeyConfirm(null);
|
|
3278
|
-
await runTest(alias);
|
|
3279
|
-
};
|
|
3280
|
-
/** After the session secret is provided, retry the test. */
|
|
3281
|
-
const retryAfterSecret = async (alias) => {
|
|
3282
|
-
setSecretConfirm(null);
|
|
3283
|
-
await runTest(alias);
|
|
3284
|
-
};
|
|
3285
|
-
const deleteHost = async (alias) => {
|
|
3286
|
-
if (!window.confirm(tt("hosts.deleteConfirm", { alias }))) return;
|
|
3287
|
-
try {
|
|
3288
|
-
await api.deleteHost(alias);
|
|
3289
|
-
load();
|
|
3290
|
-
} catch (cause) {
|
|
3291
|
-
setError(errorMessage(cause));
|
|
3292
|
-
}
|
|
3293
|
-
};
|
|
3294
|
-
const importConfig = async () => {
|
|
3295
|
-
setImporting(true);
|
|
3296
|
-
try {
|
|
3297
|
-
const result = await api.importSshConfig();
|
|
3298
|
-
setNotice(tt("hosts.imported", {
|
|
3299
|
-
parsed: result.parsed,
|
|
3300
|
-
added: result.added,
|
|
3301
|
-
skipped: result.skipped
|
|
3302
|
-
}));
|
|
3303
|
-
load();
|
|
3304
|
-
} catch (cause) {
|
|
3305
|
-
setError(errorMessage(cause));
|
|
3306
|
-
} finally {
|
|
3307
|
-
setImporting(false);
|
|
3308
|
-
}
|
|
3309
|
-
};
|
|
3310
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3311
|
-
className: panel_module_css_default.fillBody,
|
|
3312
|
-
children: [
|
|
3313
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3314
|
-
className: panel_module_css_default.toolbar,
|
|
3315
|
-
children: [
|
|
3316
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3317
|
-
className: panel_module_css_default.search,
|
|
3318
|
-
type: "search",
|
|
3319
|
-
placeholder: tt("hosts.search"),
|
|
3320
|
-
value: search,
|
|
3321
|
-
onChange: (event) => {
|
|
3322
|
-
setSearch(event.target.value);
|
|
3323
|
-
}
|
|
3324
|
-
}),
|
|
3325
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: panel_module_css_default.toolbarSpacer }),
|
|
3326
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3327
|
-
type: "button",
|
|
3328
|
-
className: panel_module_css_default.primaryButton,
|
|
3329
|
-
onClick: () => {
|
|
3330
|
-
setDialog({ mode: "create" });
|
|
3331
|
-
},
|
|
3332
|
-
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
|
+
})]
|
|
3333
3458
|
}),
|
|
3334
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3459
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3335
3460
|
type: "button",
|
|
3336
|
-
className: panel_module_css_default.
|
|
3337
|
-
disabled:
|
|
3461
|
+
className: panel_module_css_default.primaryButton,
|
|
3462
|
+
disabled: running || command.trim() === "",
|
|
3338
3463
|
onClick: () => {
|
|
3339
|
-
|
|
3464
|
+
run();
|
|
3340
3465
|
},
|
|
3341
|
-
children:
|
|
3342
|
-
})
|
|
3466
|
+
children: tt("cluster.run")
|
|
3467
|
+
}) })
|
|
3343
3468
|
]
|
|
3344
3469
|
}),
|
|
3345
|
-
notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3346
|
-
className: panel_module_css_default.banner,
|
|
3347
|
-
"data-kind": "ok",
|
|
3348
|
-
children: notice
|
|
3349
|
-
}),
|
|
3350
3470
|
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3351
3471
|
className: panel_module_css_default.banner,
|
|
3352
3472
|
"data-kind": "error",
|
|
3353
3473
|
children: tt("common.error", { error })
|
|
3354
3474
|
}),
|
|
3355
|
-
|
|
3356
|
-
className: panel_module_css_default.
|
|
3357
|
-
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")
|
|
3358
3478
|
}),
|
|
3359
|
-
|
|
3479
|
+
results !== null && results.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3360
3480
|
className: panel_module_css_default.empty,
|
|
3361
|
-
children: tt("
|
|
3481
|
+
children: tt("cluster.noMatch")
|
|
3362
3482
|
}),
|
|
3363
|
-
|
|
3483
|
+
results !== null && results.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3364
3484
|
className: panel_module_css_default.tableWrap,
|
|
3365
3485
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
3366
3486
|
className: panel_module_css_default.table,
|
|
3367
3487
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
3368
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3369
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3370
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3371
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3372
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3373
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3374
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: tt("
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
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");
|
|
3378
3498
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
|
|
3379
3499
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3380
3500
|
className: panel_module_css_default.mono,
|
|
3381
|
-
children:
|
|
3382
|
-
}),
|
|
3383
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
|
|
3384
|
-
className: panel_module_css_default.mono,
|
|
3385
|
-
children: [
|
|
3386
|
-
host.host,
|
|
3387
|
-
":",
|
|
3388
|
-
host.port
|
|
3389
|
-
]
|
|
3501
|
+
children: result.alias
|
|
3390
3502
|
}),
|
|
3391
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: host.user }),
|
|
3392
3503
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3393
3504
|
className: panel_module_css_default.badge,
|
|
3394
|
-
"data-
|
|
3395
|
-
children:
|
|
3505
|
+
"data-status": status,
|
|
3506
|
+
children: label
|
|
3396
3507
|
}) }),
|
|
3397
3508
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3398
|
-
className: panel_module_css_default.
|
|
3399
|
-
children:
|
|
3509
|
+
className: panel_module_css_default.mono,
|
|
3510
|
+
children: result.exitCode ?? "-"
|
|
3400
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
|
+
}) }),
|
|
3401
3526
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3402
3527
|
className: panel_module_css_default.cellMuted,
|
|
3403
|
-
children:
|
|
3528
|
+
children: result.error ?? ""
|
|
3404
3529
|
}),
|
|
3405
3530
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
3406
|
-
className: panel_module_css_default.
|
|
3407
|
-
children:
|
|
3408
|
-
})
|
|
3409
|
-
|
|
3410
|
-
className: panel_module_css_default.actions,
|
|
3411
|
-
children: [
|
|
3412
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3413
|
-
type: "button",
|
|
3414
|
-
className: panel_module_css_default.linkButton,
|
|
3415
|
-
disabled: testingAlias === host.alias,
|
|
3416
|
-
onClick: () => {
|
|
3417
|
-
runTest(host.alias);
|
|
3418
|
-
},
|
|
3419
|
-
children: testingAlias === host.alias ? tt("hosts.testing") : tt("hosts.test")
|
|
3420
|
-
}),
|
|
3421
|
-
testingAlias === host.alias && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3422
|
-
className: panel_module_css_default.spinner,
|
|
3423
|
-
"aria-hidden": "true"
|
|
3424
|
-
}),
|
|
3425
|
-
test !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3426
|
-
className: panel_module_css_default.inlineTest,
|
|
3427
|
-
"data-status": test.ok ? "ok" : "fail",
|
|
3428
|
-
children: test.ok ? tt("hosts.testOk", { latency: test.latencyMs ?? 0 }) : tt("hosts.testFail", { error: test.error ?? "" })
|
|
3429
|
-
}),
|
|
3430
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3431
|
-
type: "button",
|
|
3432
|
-
className: panel_module_css_default.linkButton,
|
|
3433
|
-
onClick: () => {
|
|
3434
|
-
setDialog({
|
|
3435
|
-
mode: "edit",
|
|
3436
|
-
host
|
|
3437
|
-
});
|
|
3438
|
-
},
|
|
3439
|
-
children: tt("hosts.edit")
|
|
3440
|
-
}),
|
|
3441
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3442
|
-
type: "button",
|
|
3443
|
-
className: panel_module_css_default.linkButton,
|
|
3444
|
-
"data-danger": true,
|
|
3445
|
-
onClick: () => {
|
|
3446
|
-
deleteHost(host.alias);
|
|
3447
|
-
},
|
|
3448
|
-
children: tt("hosts.delete")
|
|
3449
|
-
}),
|
|
3450
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3451
|
-
type: "button",
|
|
3452
|
-
className: panel_module_css_default.ghostButton,
|
|
3453
|
-
onClick: () => {
|
|
3454
|
-
onConnect(host.alias);
|
|
3455
|
-
},
|
|
3456
|
-
children: tt("hosts.connected")
|
|
3457
|
-
})
|
|
3458
|
-
]
|
|
3459
|
-
}) })
|
|
3460
|
-
] }, host.alias);
|
|
3531
|
+
className: panel_module_css_default.mono,
|
|
3532
|
+
children: result.durationMs !== void 0 ? result.durationMs + " ms" : "-"
|
|
3533
|
+
})
|
|
3534
|
+
] }, result.alias);
|
|
3461
3535
|
}) })]
|
|
3462
3536
|
})
|
|
3463
|
-
}),
|
|
3464
|
-
dialog !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostFormDialog, {
|
|
3465
|
-
api,
|
|
3466
|
-
editing: dialog.mode === "edit" ? dialog.host : null,
|
|
3467
|
-
onClose: () => {
|
|
3468
|
-
setDialog(null);
|
|
3469
|
-
},
|
|
3470
|
-
onSaved: () => {
|
|
3471
|
-
setDialog(null);
|
|
3472
|
-
load();
|
|
3473
|
-
}
|
|
3474
|
-
}),
|
|
3475
|
-
hostKeyConfirm !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostFingerprintDialog, {
|
|
3476
|
-
api,
|
|
3477
|
-
alias: hostKeyConfirm.alias,
|
|
3478
|
-
fingerprintSha256: hostKeyConfirm.fingerprintSha256,
|
|
3479
|
-
mismatch: hostKeyConfirm.mismatch,
|
|
3480
|
-
onClose: () => {
|
|
3481
|
-
setHostKeyConfirm(null);
|
|
3482
|
-
},
|
|
3483
|
-
onTrusted: () => {
|
|
3484
|
-
retryAfterFingerprint(hostKeyConfirm.alias);
|
|
3485
|
-
}
|
|
3486
|
-
}),
|
|
3487
|
-
secretConfirm !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionSecretDialog, {
|
|
3488
|
-
api,
|
|
3489
|
-
alias: secretConfirm.alias,
|
|
3490
|
-
user: secretConfirm.user,
|
|
3491
|
-
secret: secretConfirm.secret,
|
|
3492
|
-
onClose: () => {
|
|
3493
|
-
setSecretConfirm(null);
|
|
3494
|
-
},
|
|
3495
|
-
onProvided: () => {
|
|
3496
|
-
retryAfterSecret(secretConfirm.alias);
|
|
3497
|
-
}
|
|
3498
3537
|
})
|
|
3499
3538
|
]
|
|
3500
3539
|
});
|
|
@@ -16543,8 +16582,9 @@ window.__ModuleLoader__.load({
|
|
|
16543
16582
|
//#region src/client/ssh/panel/TerminalTab.tsx
|
|
16544
16583
|
/**
|
|
16545
16584
|
* Terminal tab: an xterm.js PTY view over the host's WebSocket terminal route.
|
|
16546
|
-
*
|
|
16547
|
-
*
|
|
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
|
|
16548
16588
|
* output stays visible and input is disabled. xterm's stylesheet is injected
|
|
16549
16589
|
* once per page load (module-level guard).
|
|
16550
16590
|
*/
|
|
@@ -16560,9 +16600,7 @@ window.__ModuleLoader__.load({
|
|
|
16560
16600
|
document.head.appendChild(style);
|
|
16561
16601
|
}
|
|
16562
16602
|
/** The xterm terminal view. */
|
|
16563
|
-
function TerminalTab({ api,
|
|
16564
|
-
const [hosts, setHosts] = (0, react.useState)([]);
|
|
16565
|
-
const [alias, setAlias] = (0, react.useState)(presetAlias ?? "");
|
|
16603
|
+
function TerminalTab({ api, alias }) {
|
|
16566
16604
|
const [status, setStatus] = (0, react.useState)({ kind: "idle" });
|
|
16567
16605
|
const containerRef = (0, react.useRef)(null);
|
|
16568
16606
|
const termRef = (0, react.useRef)(null);
|
|
@@ -16571,26 +16609,6 @@ window.__ModuleLoader__.load({
|
|
|
16571
16609
|
(0, react.useEffect)(() => {
|
|
16572
16610
|
ensureXtermCss();
|
|
16573
16611
|
}, []);
|
|
16574
|
-
(0, react.useEffect)(() => {
|
|
16575
|
-
let disposed = false;
|
|
16576
|
-
(async () => {
|
|
16577
|
-
try {
|
|
16578
|
-
const list = await api.listHosts();
|
|
16579
|
-
if (!disposed) setHosts(list);
|
|
16580
|
-
} catch (cause) {
|
|
16581
|
-
if (!disposed) setStatus({
|
|
16582
|
-
kind: "error",
|
|
16583
|
-
detail: errorMessage(cause)
|
|
16584
|
-
});
|
|
16585
|
-
}
|
|
16586
|
-
})();
|
|
16587
|
-
return () => {
|
|
16588
|
-
disposed = true;
|
|
16589
|
-
};
|
|
16590
|
-
}, [api]);
|
|
16591
|
-
(0, react.useEffect)(() => {
|
|
16592
|
-
if (presetAlias !== void 0) setAlias(presetAlias);
|
|
16593
|
-
}, [presetAlias, requestId]);
|
|
16594
16612
|
const teardown = () => {
|
|
16595
16613
|
const connection = connRef.current;
|
|
16596
16614
|
connRef.current = null;
|
|
@@ -16642,6 +16660,7 @@ window.__ModuleLoader__.load({
|
|
|
16642
16660
|
term.loadAddon(fit);
|
|
16643
16661
|
term.open(container);
|
|
16644
16662
|
fit.fit();
|
|
16663
|
+
term.options.disableStdin = true;
|
|
16645
16664
|
const connection = api.openTerminal(target, term.cols, term.rows);
|
|
16646
16665
|
termRef.current = term;
|
|
16647
16666
|
fitRef.current = fit;
|
|
@@ -16651,6 +16670,7 @@ window.__ModuleLoader__.load({
|
|
|
16651
16670
|
connection.send(data);
|
|
16652
16671
|
});
|
|
16653
16672
|
connection.onReady = () => {
|
|
16673
|
+
term.options.disableStdin = false;
|
|
16654
16674
|
setStatus({
|
|
16655
16675
|
kind: "connected",
|
|
16656
16676
|
alias: target
|
|
@@ -16683,29 +16703,14 @@ window.__ModuleLoader__.load({
|
|
|
16683
16703
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
16684
16704
|
className: panel_module_css_default.controls,
|
|
16685
16705
|
children: [
|
|
16686
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
16687
|
-
className: panel_module_css_default.
|
|
16688
|
-
|
|
16689
|
-
onChange: (event) => {
|
|
16690
|
-
setAlias(event.target.value);
|
|
16691
|
-
},
|
|
16692
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
16693
|
-
value: "",
|
|
16694
|
-
children: tt("terminal.selectHost")
|
|
16695
|
-
}), hosts.map((host) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
16696
|
-
value: host.alias,
|
|
16697
|
-
children: [
|
|
16698
|
-
host.alias,
|
|
16699
|
-
" (",
|
|
16700
|
-
host.host,
|
|
16701
|
-
")"
|
|
16702
|
-
]
|
|
16703
|
-
}, host.alias))]
|
|
16706
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16707
|
+
className: panel_module_css_default.targetBadge,
|
|
16708
|
+
children: alias
|
|
16704
16709
|
}),
|
|
16705
16710
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
16706
16711
|
type: "button",
|
|
16707
16712
|
className: panel_module_css_default.primaryButton,
|
|
16708
|
-
disabled:
|
|
16713
|
+
disabled: active,
|
|
16709
16714
|
onClick: connect,
|
|
16710
16715
|
children: tt("terminal.connect")
|
|
16711
16716
|
}),
|
|
@@ -16745,7 +16750,7 @@ window.__ModuleLoader__.load({
|
|
|
16745
16750
|
className: panel_module_css_default.termContainer
|
|
16746
16751
|
}), status.kind === "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
16747
16752
|
className: panel_module_css_default.termPlaceholder,
|
|
16748
|
-
children:
|
|
16753
|
+
children: tt("terminal.placeholder")
|
|
16749
16754
|
})]
|
|
16750
16755
|
})
|
|
16751
16756
|
]
|
|
@@ -16777,33 +16782,22 @@ window.__ModuleLoader__.load({
|
|
|
16777
16782
|
return tt("transfer.gib", { value: (bytes / 1073741824).toFixed(1) });
|
|
16778
16783
|
}
|
|
16779
16784
|
/** The upload/download tab. */
|
|
16780
|
-
function TransferTab({ api }) {
|
|
16781
|
-
const [hosts, setHosts] = (0, react.useState)([]);
|
|
16785
|
+
function TransferTab({ api, alias, remoteRoot }) {
|
|
16782
16786
|
const [listError, setListError] = (0, react.useState)(null);
|
|
16783
|
-
const [
|
|
16784
|
-
const [remotePath, setRemotePath] = (0, react.useState)("");
|
|
16787
|
+
const [remotePath, setRemotePath] = (0, react.useState)(remoteRoot);
|
|
16785
16788
|
const [browseOpen, setBrowseOpen] = (0, react.useState)(false);
|
|
16786
|
-
const [browseDir, setBrowseDir] = (0, react.useState)(
|
|
16789
|
+
const [browseDir, setBrowseDir] = (0, react.useState)(remoteRoot);
|
|
16787
16790
|
const [entries, setEntries] = (0, react.useState)([]);
|
|
16788
16791
|
const [browsing, setBrowsing] = (0, react.useState)(false);
|
|
16789
16792
|
const [transfer, setTransfer] = (0, react.useState)(null);
|
|
16790
16793
|
const [status, setStatus] = (0, react.useState)(null);
|
|
16791
16794
|
const fileRef = (0, react.useRef)(null);
|
|
16792
16795
|
const seqRef = (0, react.useRef)(0);
|
|
16793
|
-
(0, react.
|
|
16794
|
-
|
|
16795
|
-
(
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
if (!disposed) setHosts(list);
|
|
16799
|
-
} catch (cause) {
|
|
16800
|
-
if (!disposed) setListError(errorMessage(cause));
|
|
16801
|
-
}
|
|
16802
|
-
})();
|
|
16803
|
-
return () => {
|
|
16804
|
-
disposed = true;
|
|
16805
|
-
};
|
|
16806
|
-
}, [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
|
+
}, []);
|
|
16807
16801
|
const loadDir = async (path) => {
|
|
16808
16802
|
if (alias === "") return;
|
|
16809
16803
|
const seq = ++seqRef.current;
|
|
@@ -16825,15 +16819,16 @@ window.__ModuleLoader__.load({
|
|
|
16825
16819
|
}
|
|
16826
16820
|
};
|
|
16827
16821
|
const openBrowse = () => {
|
|
16828
|
-
if (alias === "") return;
|
|
16829
16822
|
setBrowseOpen(true);
|
|
16830
|
-
const path = remotePath.trim() === "" ?
|
|
16823
|
+
const path = remotePath.trim() === "" ? remoteRoot : remotePath.trim();
|
|
16831
16824
|
loadDir(path);
|
|
16832
16825
|
};
|
|
16833
16826
|
const handleFile = async (event) => {
|
|
16834
16827
|
const file = event.target.files?.[0];
|
|
16835
16828
|
event.target.value = "";
|
|
16836
16829
|
if (file === void 0 || alias === "" || remotePath.trim() === "" || transfer !== null) return;
|
|
16830
|
+
const controller = new AbortController();
|
|
16831
|
+
transferAbortRef.current = controller;
|
|
16837
16832
|
setStatus(null);
|
|
16838
16833
|
setTransfer({
|
|
16839
16834
|
kind: "upload",
|
|
@@ -16843,6 +16838,7 @@ window.__ModuleLoader__.load({
|
|
|
16843
16838
|
});
|
|
16844
16839
|
try {
|
|
16845
16840
|
const outcome = await api.uploadFile(file, alias, remotePath.trim(), (progress) => {
|
|
16841
|
+
if (transferAbortRef.current !== controller || controller.signal.aborted) return;
|
|
16846
16842
|
setTransfer((prev) => prev === null ? prev : {
|
|
16847
16843
|
kind: "upload",
|
|
16848
16844
|
phase: progress.phase,
|
|
@@ -16850,22 +16846,27 @@ window.__ModuleLoader__.load({
|
|
|
16850
16846
|
speedBps: progress.speedBps,
|
|
16851
16847
|
file: progress.file
|
|
16852
16848
|
});
|
|
16853
|
-
});
|
|
16854
|
-
setStatus({
|
|
16849
|
+
}, controller.signal);
|
|
16850
|
+
if (transferAbortRef.current === controller && !controller.signal.aborted) setStatus({
|
|
16855
16851
|
kind: "ok",
|
|
16856
16852
|
bytes: outcome.transferredBytes
|
|
16857
16853
|
});
|
|
16858
16854
|
} catch (cause) {
|
|
16859
|
-
setStatus({
|
|
16855
|
+
if (transferAbortRef.current === controller && !controller.signal.aborted) setStatus({
|
|
16860
16856
|
kind: "error",
|
|
16861
16857
|
error: errorMessage(cause)
|
|
16862
16858
|
});
|
|
16863
16859
|
} finally {
|
|
16864
|
-
|
|
16860
|
+
if (transferAbortRef.current === controller) {
|
|
16861
|
+
transferAbortRef.current = null;
|
|
16862
|
+
if (!controller.signal.aborted) setTransfer(null);
|
|
16863
|
+
}
|
|
16865
16864
|
}
|
|
16866
16865
|
};
|
|
16867
16866
|
const handleDownload = async () => {
|
|
16868
16867
|
if (alias === "" || remotePath.trim() === "" || transfer !== null) return;
|
|
16868
|
+
const controller = new AbortController();
|
|
16869
|
+
transferAbortRef.current = controller;
|
|
16869
16870
|
setStatus(null);
|
|
16870
16871
|
setTransfer({
|
|
16871
16872
|
kind: "download",
|
|
@@ -16875,6 +16876,7 @@ window.__ModuleLoader__.load({
|
|
|
16875
16876
|
});
|
|
16876
16877
|
try {
|
|
16877
16878
|
const result = await api.downloadFile(alias, remotePath.trim(), (progress) => {
|
|
16879
|
+
if (transferAbortRef.current !== controller || controller.signal.aborted) return;
|
|
16878
16880
|
setTransfer((prev) => prev === null ? prev : {
|
|
16879
16881
|
kind: "download",
|
|
16880
16882
|
phase: progress.phase,
|
|
@@ -16882,7 +16884,8 @@ window.__ModuleLoader__.load({
|
|
|
16882
16884
|
speedBps: progress.speedBps,
|
|
16883
16885
|
file: progress.file
|
|
16884
16886
|
});
|
|
16885
|
-
});
|
|
16887
|
+
}, controller.signal);
|
|
16888
|
+
if (transferAbortRef.current !== controller || controller.signal.aborted) return;
|
|
16886
16889
|
if (!result.streamed && result.blob !== void 0) {
|
|
16887
16890
|
const url = URL.createObjectURL(result.blob);
|
|
16888
16891
|
const anchor = document.createElement("a");
|
|
@@ -16900,39 +16903,27 @@ window.__ModuleLoader__.load({
|
|
|
16900
16903
|
bytes: result.bytes
|
|
16901
16904
|
});
|
|
16902
16905
|
} catch (cause) {
|
|
16903
|
-
setStatus({
|
|
16906
|
+
if (transferAbortRef.current === controller && !controller.signal.aborted) setStatus({
|
|
16904
16907
|
kind: "error",
|
|
16905
16908
|
error: errorMessage(cause)
|
|
16906
16909
|
});
|
|
16907
16910
|
} finally {
|
|
16908
|
-
|
|
16911
|
+
if (transferAbortRef.current === controller) {
|
|
16912
|
+
transferAbortRef.current = null;
|
|
16913
|
+
if (!controller.signal.aborted) setTransfer(null);
|
|
16914
|
+
}
|
|
16909
16915
|
}
|
|
16910
16916
|
};
|
|
16911
|
-
const ready =
|
|
16917
|
+
const ready = remotePath.trim() !== "" && transfer === null;
|
|
16912
16918
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
16913
16919
|
className: panel_module_css_default.tabBody,
|
|
16914
16920
|
children: [
|
|
16915
16921
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
16916
16922
|
className: panel_module_css_default.controls,
|
|
16917
16923
|
children: [
|
|
16918
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
16919
|
-
className: panel_module_css_default.
|
|
16920
|
-
|
|
16921
|
-
onChange: (event) => {
|
|
16922
|
-
setAlias(event.target.value);
|
|
16923
|
-
},
|
|
16924
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
16925
|
-
value: "",
|
|
16926
|
-
children: tt("transfer.selectHost")
|
|
16927
|
-
}), hosts.map((host) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
16928
|
-
value: host.alias,
|
|
16929
|
-
children: [
|
|
16930
|
-
host.alias,
|
|
16931
|
-
" (",
|
|
16932
|
-
host.host,
|
|
16933
|
-
")"
|
|
16934
|
-
]
|
|
16935
|
-
}, host.alias))]
|
|
16924
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
16925
|
+
className: panel_module_css_default.targetBadge,
|
|
16926
|
+
children: alias
|
|
16936
16927
|
}),
|
|
16937
16928
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
16938
16929
|
className: panel_module_css_default.input,
|
|
@@ -16945,7 +16936,6 @@ window.__ModuleLoader__.load({
|
|
|
16945
16936
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
16946
16937
|
type: "button",
|
|
16947
16938
|
className: panel_module_css_default.ghostButton,
|
|
16948
|
-
disabled: alias === "",
|
|
16949
16939
|
onClick: openBrowse,
|
|
16950
16940
|
children: tt("transfer.browseRemote")
|
|
16951
16941
|
}),
|
|
@@ -17074,28 +17064,14 @@ window.__ModuleLoader__.load({
|
|
|
17074
17064
|
* and a new-tunnel form.
|
|
17075
17065
|
*/
|
|
17076
17066
|
/** The tunnels tab. */
|
|
17077
|
-
function TunnelsTab({ api }) {
|
|
17078
|
-
const [hosts, setHosts] = (0, react.useState)([]);
|
|
17067
|
+
function TunnelsTab({ api, alias }) {
|
|
17079
17068
|
const [tunnels, setTunnels] = (0, react.useState)(null);
|
|
17080
17069
|
const [error, setError] = (0, react.useState)(null);
|
|
17081
17070
|
const [notice, setNotice] = (0, react.useState)(null);
|
|
17082
|
-
const [alias, setAlias] = (0, react.useState)("");
|
|
17083
17071
|
const [remotePort, setRemotePort] = (0, react.useState)("");
|
|
17084
17072
|
const [remoteHost, setRemoteHost] = (0, react.useState)("");
|
|
17085
17073
|
const [localPort, setLocalPort] = (0, react.useState)("");
|
|
17086
17074
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
17087
|
-
(0, react.useEffect)(() => {
|
|
17088
|
-
let disposed = false;
|
|
17089
|
-
(async () => {
|
|
17090
|
-
try {
|
|
17091
|
-
const list = await api.listHosts();
|
|
17092
|
-
if (!disposed) setHosts(list);
|
|
17093
|
-
} catch {}
|
|
17094
|
-
})();
|
|
17095
|
-
return () => {
|
|
17096
|
-
disposed = true;
|
|
17097
|
-
};
|
|
17098
|
-
}, [api]);
|
|
17099
17075
|
const seqRef = (0, react.useRef)(0);
|
|
17100
17076
|
(0, react.useEffect)(() => {
|
|
17101
17077
|
const load = async () => {
|
|
@@ -17103,7 +17079,7 @@ window.__ModuleLoader__.load({
|
|
|
17103
17079
|
try {
|
|
17104
17080
|
const list = await api.listTunnels();
|
|
17105
17081
|
if (seq !== seqRef.current) return;
|
|
17106
|
-
setTunnels(list);
|
|
17082
|
+
setTunnels(list.filter((tunnel) => tunnel.alias === alias));
|
|
17107
17083
|
setError(null);
|
|
17108
17084
|
} catch (cause) {
|
|
17109
17085
|
if (seq !== seqRef.current) return;
|
|
@@ -17117,7 +17093,7 @@ window.__ModuleLoader__.load({
|
|
|
17117
17093
|
return () => {
|
|
17118
17094
|
clearInterval(timer);
|
|
17119
17095
|
};
|
|
17120
|
-
}, [api]);
|
|
17096
|
+
}, [api, alias]);
|
|
17121
17097
|
const refresh = async () => {
|
|
17122
17098
|
const seq = ++seqRef.current;
|
|
17123
17099
|
try {
|
|
@@ -17142,7 +17118,7 @@ window.__ModuleLoader__.load({
|
|
|
17142
17118
|
if (!window.confirm(tt("tunnel.stopAllConfirm"))) return;
|
|
17143
17119
|
setBusy(true);
|
|
17144
17120
|
try {
|
|
17145
|
-
await api.stopAllTunnels(alias
|
|
17121
|
+
await api.stopAllTunnels(alias);
|
|
17146
17122
|
await refresh();
|
|
17147
17123
|
} catch (cause) {
|
|
17148
17124
|
setError(errorMessage(cause));
|
|
@@ -17251,19 +17227,9 @@ window.__ModuleLoader__.load({
|
|
|
17251
17227
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17252
17228
|
className: panel_module_css_default.fieldLabel,
|
|
17253
17229
|
children: tt("tunnel.alias")
|
|
17254
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
17255
|
-
className: panel_module_css_default.
|
|
17256
|
-
|
|
17257
|
-
onChange: (event) => {
|
|
17258
|
-
setAlias(event.target.value);
|
|
17259
|
-
},
|
|
17260
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
17261
|
-
value: "",
|
|
17262
|
-
children: tt("terminal.selectHost")
|
|
17263
|
-
}), hosts.map((host) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
17264
|
-
value: host.alias,
|
|
17265
|
-
children: host.alias
|
|
17266
|
-
}, host.alias))]
|
|
17230
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
17231
|
+
className: panel_module_css_default.targetBadge,
|
|
17232
|
+
children: alias
|
|
17267
17233
|
})]
|
|
17268
17234
|
}),
|
|
17269
17235
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
@@ -17317,7 +17283,7 @@ window.__ModuleLoader__.load({
|
|
|
17317
17283
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17318
17284
|
type: "button",
|
|
17319
17285
|
className: panel_module_css_default.primaryButton,
|
|
17320
|
-
disabled: busy ||
|
|
17286
|
+
disabled: busy || remotePort.trim() === "",
|
|
17321
17287
|
onClick: () => {
|
|
17322
17288
|
start();
|
|
17323
17289
|
},
|
|
@@ -17330,18 +17296,13 @@ window.__ModuleLoader__.load({
|
|
|
17330
17296
|
//#endregion
|
|
17331
17297
|
//#region src/client/ssh/panel/SshPanel.tsx
|
|
17332
17298
|
/**
|
|
17333
|
-
*
|
|
17334
|
-
*
|
|
17335
|
-
*
|
|
17336
|
-
*
|
|
17337
|
-
* 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.
|
|
17338
17303
|
*/
|
|
17339
17304
|
/** The tab bar definition (labels resolved at render time). */
|
|
17340
17305
|
const TABS = [
|
|
17341
|
-
{
|
|
17342
|
-
id: "hosts",
|
|
17343
|
-
label: () => tt("tab.hosts")
|
|
17344
|
-
},
|
|
17345
17306
|
{
|
|
17346
17307
|
id: "terminal",
|
|
17347
17308
|
label: () => tt("tab.terminal")
|
|
@@ -17359,36 +17320,39 @@ window.__ModuleLoader__.load({
|
|
|
17359
17320
|
label: () => tt("tab.cluster")
|
|
17360
17321
|
}
|
|
17361
17322
|
];
|
|
17362
|
-
/** The tabbed SSH
|
|
17363
|
-
function SshPanel({
|
|
17364
|
-
const [activeTab, setActiveTab] = (0, react.useState)("
|
|
17365
|
-
const
|
|
17366
|
-
|
|
17367
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
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}`;
|
|
17373
17351
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17374
17352
|
className: panel_module_css_default.panel,
|
|
17353
|
+
"data-session-mode": "ssh",
|
|
17354
|
+
"data-session-alias": sessionTarget.alias,
|
|
17375
17355
|
children: [
|
|
17376
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17377
|
-
className: panel_module_css_default.panelHeader,
|
|
17378
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
17379
|
-
className: panel_module_css_default.panelTitle,
|
|
17380
|
-
children: tt("panel.title")
|
|
17381
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
17382
|
-
type: "button",
|
|
17383
|
-
className: panel_module_css_default.iconButton,
|
|
17384
|
-
title: tt("common.close"),
|
|
17385
|
-
"aria-label": tt("common.close"),
|
|
17386
|
-
onClick: () => {
|
|
17387
|
-
controller.close();
|
|
17388
|
-
},
|
|
17389
|
-
children: "x"
|
|
17390
|
-
})]
|
|
17391
|
-
}),
|
|
17392
17356
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
17393
17357
|
className: panel_module_css_default.tabBar,
|
|
17394
17358
|
role: "tablist",
|
|
@@ -17404,271 +17368,172 @@ window.__ModuleLoader__.load({
|
|
|
17404
17368
|
children: tab.label()
|
|
17405
17369
|
}, tab.id))
|
|
17406
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
|
+
}),
|
|
17407
17379
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
17408
17380
|
className: panel_module_css_default.panelContent,
|
|
17409
17381
|
children: [
|
|
17410
|
-
activeTab === "
|
|
17382
|
+
activeTab === "terminal" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TerminalTab, {
|
|
17411
17383
|
api,
|
|
17412
|
-
|
|
17384
|
+
alias: sessionTarget.alias
|
|
17413
17385
|
}),
|
|
17414
|
-
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, {
|
|
17415
17392
|
api,
|
|
17416
|
-
|
|
17417
|
-
requestId: connectRequest?.nonce
|
|
17393
|
+
alias: sessionTarget.alias
|
|
17418
17394
|
}),
|
|
17419
|
-
activeTab === "
|
|
17420
|
-
|
|
17421
|
-
|
|
17395
|
+
activeTab === "cluster" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClusterTab, {
|
|
17396
|
+
api,
|
|
17397
|
+
alias: sessionTarget.alias
|
|
17398
|
+
})
|
|
17422
17399
|
]
|
|
17423
|
-
})
|
|
17400
|
+
}, targetKey)
|
|
17424
17401
|
]
|
|
17425
17402
|
});
|
|
17426
17403
|
}
|
|
17427
17404
|
//#endregion
|
|
17428
|
-
//#region src/client/ssh/
|
|
17429
|
-
/**
|
|
17430
|
-
|
|
17431
|
-
|
|
17432
|
-
|
|
17433
|
-
* plugins cannot declare slots, so the panel takes over the center column at
|
|
17434
|
-
* the DOM level: a container is appended inside the `[data-pane="conversation"]`
|
|
17435
|
-
* grid item (an extra trailing child React never manages), and a stylesheet
|
|
17436
|
-
* rule hides the conversation content while the panel is active. Toggling is
|
|
17437
|
-
* a data attribute on <html> — no React involvement, so the conversation
|
|
17438
|
-
* subtree underneath stays mounted and stateful.
|
|
17439
|
-
*/
|
|
17440
|
-
const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"]";
|
|
17441
|
-
const ACTIVE_ATTR = "data-dsh-ssh-active";
|
|
17442
|
-
/** Find the center column, or undefined while the frame is not mounted. */
|
|
17443
|
-
function conversationColumn() {
|
|
17444
|
-
return document.querySelector(CONVERSATION_COLUMN_SELECTOR) ?? void 0;
|
|
17445
|
-
}
|
|
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";
|
|
17446
17410
|
/**
|
|
17447
|
-
*
|
|
17448
|
-
*
|
|
17449
|
-
* @param
|
|
17450
|
-
* @param
|
|
17451
|
-
* @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.
|
|
17452
17415
|
*/
|
|
17453
|
-
function
|
|
17454
|
-
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
|
|
17462
|
-
|
|
17463
|
-
|
|
17464
|
-
|
|
17465
|
-
|
|
17466
|
-
|
|
17467
|
-
|
|
17468
|
-
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
17472
|
-
|
|
17473
|
-
|
|
17474
|
-
|
|
17475
|
-
};
|
|
17476
|
-
const waitObserver = new MutationObserver(() => {
|
|
17477
|
-
ensure();
|
|
17478
|
-
});
|
|
17479
|
-
waitObserver.observe(document.body, {
|
|
17480
|
-
childList: true,
|
|
17481
|
-
subtree: true
|
|
17482
|
-
});
|
|
17483
|
-
const applyActive = () => {
|
|
17484
|
-
if (controller.getSnapshot().panelOpen) document.documentElement.setAttribute(ACTIVE_ATTR, "");
|
|
17485
|
-
else document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
17486
|
-
};
|
|
17487
|
-
const unsubscribe = controller.subscribe(applyActive);
|
|
17488
|
-
applyActive();
|
|
17489
|
-
ensure();
|
|
17490
|
-
return () => {
|
|
17491
|
-
waitObserver.disconnect();
|
|
17492
|
-
unsubscribe();
|
|
17493
|
-
document.documentElement.removeAttribute(ACTIVE_ATTR);
|
|
17494
|
-
root?.unmount();
|
|
17495
|
-
root = void 0;
|
|
17496
|
-
container?.remove();
|
|
17497
|
-
container = void 0;
|
|
17498
|
-
};
|
|
17499
|
-
}
|
|
17500
|
-
//#endregion
|
|
17501
|
-
//#region src/client/ssh/panel/controller.ts
|
|
17502
|
-
/** The panel state owner the sidebar entry toggles and the view renders from. */
|
|
17503
|
-
var PanelController = class {
|
|
17504
|
-
panelOpen = false;
|
|
17505
|
-
listeners = /* @__PURE__ */ new Set();
|
|
17506
|
-
getSnapshot() {
|
|
17507
|
-
return { panelOpen: this.panelOpen };
|
|
17508
|
-
}
|
|
17509
|
-
subscribe(fn) {
|
|
17510
|
-
this.listeners.add(fn);
|
|
17511
|
-
return () => {
|
|
17512
|
-
this.listeners.delete(fn);
|
|
17513
|
-
};
|
|
17514
|
-
}
|
|
17515
|
-
open() {
|
|
17516
|
-
if (this.panelOpen) return;
|
|
17517
|
-
this.panelOpen = true;
|
|
17518
|
-
this.notify();
|
|
17519
|
-
}
|
|
17520
|
-
close() {
|
|
17521
|
-
if (!this.panelOpen) return;
|
|
17522
|
-
this.panelOpen = false;
|
|
17523
|
-
this.notify();
|
|
17524
|
-
}
|
|
17525
|
-
toggle() {
|
|
17526
|
-
if (this.panelOpen) this.close();
|
|
17527
|
-
else this.open();
|
|
17528
|
-
}
|
|
17529
|
-
notify() {
|
|
17530
|
-
for (const fn of [...this.listeners]) fn();
|
|
17531
|
-
}
|
|
17532
|
-
};
|
|
17533
|
-
//#endregion
|
|
17534
|
-
//#region src/client/ssh/sidebar-entry.ts
|
|
17535
|
-
/** The sidebar column is the grid item AppFrame renders with this pane marker. */
|
|
17536
|
-
const SIDEBAR_COLUMN_SELECTOR = "[data-pane=\"sidebar\"]";
|
|
17537
|
-
/** Find the sidebar shell root element, or undefined while not yet mounted. */
|
|
17538
|
-
function sidebarRoot() {
|
|
17539
|
-
return document.querySelector(SIDEBAR_COLUMN_SELECTOR)?.firstElementChild ?? void 0;
|
|
17540
|
-
}
|
|
17541
|
-
/** The New Session button: the shell's only direct-child button of the root. */
|
|
17542
|
-
function newSessionButton(root) {
|
|
17543
|
-
for (const child of root.children) if (child.tagName === "BUTTON") return child;
|
|
17544
|
-
}
|
|
17545
|
-
/** Build the entry row (a detached button; insert once the shell is up). */
|
|
17546
|
-
function createEntry(controller) {
|
|
17547
|
-
const entry = document.createElement("button");
|
|
17548
|
-
entry.type = "button";
|
|
17549
|
-
entry.dataset.dshSshEntry = "";
|
|
17550
|
-
entry.className = panel_module_css_default.entry;
|
|
17551
|
-
entry.setAttribute("aria-label", tt("entry.label"));
|
|
17552
|
-
entry.setAttribute("title", tt("entry.tooltip"));
|
|
17553
|
-
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>";
|
|
17554
|
-
entry.addEventListener("click", () => {
|
|
17555
|
-
controller.toggle();
|
|
17556
|
-
});
|
|
17557
|
-
return entry;
|
|
17558
|
-
}
|
|
17559
|
-
/** Re-insert the entry after the New Session button (before the browser region). */
|
|
17560
|
-
function placeEntry(root, entry) {
|
|
17561
|
-
const button = newSessionButton(root);
|
|
17562
|
-
if (button === void 0) return false;
|
|
17563
|
-
if (entry.parentElement !== root) root.insertBefore(entry, button.nextElementSibling);
|
|
17564
|
-
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));
|
|
17565
17439
|
}
|
|
17566
|
-
/**
|
|
17567
|
-
|
|
17568
|
-
|
|
17569
|
-
* @param controller - the panel controller the entry toggles.
|
|
17570
|
-
* @returns disposer removing the entry and its observers.
|
|
17571
|
-
*/
|
|
17572
|
-
function mountSidebarEntry(controller) {
|
|
17573
|
-
const entry = createEntry(controller);
|
|
17574
|
-
let root;
|
|
17575
|
-
let placed = false;
|
|
17576
|
-
const tryPlace = () => {
|
|
17577
|
-
if (placed) return;
|
|
17578
|
-
if (root !== void 0 && !root.isConnected) {
|
|
17579
|
-
rootObserver.disconnect();
|
|
17580
|
-
root = void 0;
|
|
17581
|
-
}
|
|
17582
|
-
root ??= sidebarRoot();
|
|
17583
|
-
if (root === void 0) return;
|
|
17584
|
-
placed = placeEntry(root, entry);
|
|
17585
|
-
if (placed) rootObserver.observe(root, {
|
|
17586
|
-
childList: true,
|
|
17587
|
-
subtree: true
|
|
17588
|
-
});
|
|
17589
|
-
};
|
|
17590
|
-
const waitObserver = new MutationObserver(() => {
|
|
17591
|
-
tryPlace();
|
|
17592
|
-
});
|
|
17593
|
-
waitObserver.observe(document.body, {
|
|
17594
|
-
childList: true,
|
|
17595
|
-
subtree: true
|
|
17596
|
-
});
|
|
17597
|
-
const rootObserver = new MutationObserver(() => {
|
|
17598
|
-
if (root === void 0 || !root.isConnected) {
|
|
17599
|
-
placed = false;
|
|
17600
|
-
tryPlace();
|
|
17601
|
-
return;
|
|
17602
|
-
}
|
|
17603
|
-
if (!root.contains(entry)) placed = placeEntry(root, entry);
|
|
17604
|
-
});
|
|
17605
|
-
const unsubscribe = controller.subscribe(() => {
|
|
17606
|
-
entry.dataset.active = controller.getSnapshot().panelOpen ? "true" : void 0;
|
|
17607
|
-
});
|
|
17608
|
-
entry.dataset.active = controller.getSnapshot().panelOpen ? "true" : void 0;
|
|
17609
|
-
tryPlace();
|
|
17610
|
-
return () => {
|
|
17611
|
-
waitObserver.disconnect();
|
|
17612
|
-
rootObserver.disconnect();
|
|
17613
|
-
unsubscribe();
|
|
17614
|
-
entry.remove();
|
|
17615
|
-
};
|
|
17440
|
+
/** The tab chip and floating-panel header text. */
|
|
17441
|
+
function OpsTabTitle() {
|
|
17442
|
+
return tt$1("ops.tab");
|
|
17616
17443
|
}
|
|
17617
17444
|
//#endregion
|
|
17618
17445
|
//#region src/client/ssh/apply.ts
|
|
17619
17446
|
/** Locale namespace this capability owns (kept as 'dsh-ssh'). */
|
|
17620
17447
|
const NS = "dsh-ssh";
|
|
17621
17448
|
/**
|
|
17622
|
-
* Mount the SSH operations surfaces (
|
|
17623
|
-
* @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.
|
|
17624
17453
|
*/
|
|
17625
|
-
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
|
+
}
|
|
17626
17460
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
17627
17461
|
zh,
|
|
17628
17462
|
en
|
|
17629
17463
|
}), "dsh-ssh: dictionaries");
|
|
17630
|
-
|
|
17631
|
-
|
|
17632
|
-
|
|
17633
|
-
|
|
17634
|
-
|
|
17635
|
-
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
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
|
+
};
|
|
17642
17498
|
}
|
|
17643
17499
|
//#endregion
|
|
17644
17500
|
//#region src/client/index.ts
|
|
17645
|
-
/** Required services: slots for the
|
|
17646
|
-
*
|
|
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. */
|
|
17647
17507
|
const inject = [
|
|
17648
17508
|
"slots",
|
|
17649
17509
|
"locale",
|
|
17650
|
-
"workspaces"
|
|
17510
|
+
"workspaces",
|
|
17511
|
+
"sessions",
|
|
17512
|
+
"sidebarRightTabs"
|
|
17651
17513
|
];
|
|
17652
17514
|
/** Apply the browser half. */
|
|
17653
17515
|
function apply(ctx) {
|
|
17654
|
-
mountSshOperations(ctx);
|
|
17655
|
-
ctx.effect(() => ctx.locale.register(NS$1, dictionaries), "dsh-hardssh: dictionaries");
|
|
17656
17516
|
const api = new WorkspaceApi();
|
|
17657
17517
|
const hostsApi = new SshHostsApi();
|
|
17658
17518
|
const sshApi = new SshApi();
|
|
17659
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");
|
|
17660
17532
|
const disposers = [];
|
|
17661
17533
|
try {
|
|
17662
|
-
ctx
|
|
17663
|
-
|
|
17664
|
-
|
|
17665
|
-
id: "ssh-workspace-manager",
|
|
17666
|
-
order: -10,
|
|
17667
|
-
inject: () => ({
|
|
17668
|
-
manager,
|
|
17669
|
-
sshApi
|
|
17670
|
-
})
|
|
17671
|
-
}, WorkspaceManagerButton);
|
|
17534
|
+
registerWorkspacePanel(ctx, {
|
|
17535
|
+
manager,
|
|
17536
|
+
sshApi
|
|
17672
17537
|
});
|
|
17673
17538
|
const flowInject = () => ({
|
|
17674
17539
|
pickDirectory: () => ctx.workspaces.pickDirectory(),
|
|
@@ -17729,25 +17594,10 @@ window.__ModuleLoader__.load({
|
|
|
17729
17594
|
window.clearInterval(connTimer);
|
|
17730
17595
|
});
|
|
17731
17596
|
refreshConnections();
|
|
17732
|
-
|
|
17733
|
-
|
|
17734
|
-
|
|
17735
|
-
|
|
17736
|
-
gateDispose?.();
|
|
17737
|
-
gateDispose = mountWorkspaceGates(workspaces.map((workspace) => ({
|
|
17738
|
-
id: workspace.id,
|
|
17739
|
-
title: workspace.title,
|
|
17740
|
-
alias: workspace.alias
|
|
17741
|
-
})), sshApi);
|
|
17742
|
-
} catch (error) {
|
|
17743
|
-
console.warn("[dsh-hardssh] gate refresh failed:", error);
|
|
17744
|
-
}
|
|
17745
|
-
};
|
|
17746
|
-
disposers.push(() => {
|
|
17747
|
-
gateDispose?.();
|
|
17748
|
-
});
|
|
17749
|
-
disposers.push(manager.subscribe(() => {
|
|
17750
|
-
refreshGates();
|
|
17597
|
+
disposers.push(mountSessionConnectGate({
|
|
17598
|
+
sessions,
|
|
17599
|
+
aliasForCwd: makeAnchorAliasResolver(() => manager.getSnapshot().workspaces),
|
|
17600
|
+
ensureConnected: (alias) => connectHost(sshApi, alias)
|
|
17751
17601
|
}));
|
|
17752
17602
|
} catch (error) {
|
|
17753
17603
|
console.warn("[dsh-hardssh] mount failed:", error);
|