@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
|
@@ -1,96 +1,99 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar workspace-row augmentation: labels every host workspace row whose
|
|
3
|
-
* title matches an SSH-bound workspace with a compact remote badge
|
|
4
|
-
* (`alias @ remoteRoot`), so a glance tells a local workspace from a remote
|
|
5
|
-
* one. Pure DOM-level extension (the sidebar shell exposes no slot for row
|
|
6
|
-
* decoration), following the dsh-ssh sidebar-entry precedent: a
|
|
7
|
-
* MutationObserver self-heals when React re-renders displace the injected
|
|
8
|
-
* badge.
|
|
9
|
-
*
|
|
10
|
-
* The row title alone matches: SSH-bound workspace titles are unique enough
|
|
11
|
-
* in practice (the ledger title is what the row shows). A title match against
|
|
12
|
-
* `~/.dsh/ssh-workspaces/<id>` anchors also works when the host picks that
|
|
13
|
-
* default title.
|
|
14
|
-
*
|
|
15
|
-
* @module dsh-hardssh/client/workspace-badges
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const PROJECT_ROW_SELECTOR = '[class*="projectRow"]'
|
|
19
|
-
const TITLE_SELECTOR = '[class*="title"]'
|
|
20
|
-
|
|
21
|
-
/** Render a small remote badge element. The cloud icon marks "remote";
|
|
22
|
-
* connected servers render in DeepSeek blue, disconnected stay gray. The
|
|
23
|
-
* tooltip shows the REMOTE directory plus the server, e.g.
|
|
24
|
-
* `/data/app (prod-01)`. */
|
|
25
|
-
export function makeBadge(alias: string, remoteRoot: string, connected: boolean): HTMLElement {
|
|
26
|
-
const badge = document.createElement('span')
|
|
27
|
-
badge.dataset.sshBadge = connected ? 'connected' : 'disconnected'
|
|
28
|
-
badge.title = `${remoteRoot}(${alias})`
|
|
29
|
-
badge.setAttribute(
|
|
30
|
-
'style',
|
|
31
|
-
connected
|
|
32
|
-
? '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'
|
|
33
|
-
: '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',
|
|
34
|
-
)
|
|
35
|
-
const icon = document.createElement('span')
|
|
36
|
-
icon.setAttribute('style', 'display:inline-flex;flex:none;line-height:1')
|
|
37
|
-
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>'
|
|
38
|
-
const label = document.createElement('span')
|
|
39
|
-
label.textContent = alias
|
|
40
|
-
label.setAttribute('style', 'font-weight:400')
|
|
41
|
-
badge.append(icon, label)
|
|
42
|
-
return badge
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Mount the row-decoration pass. Queries the sidebar for project rows and
|
|
47
|
-
* appends a remote badge when the row's title belongs to an SSH workspace.
|
|
48
|
-
* Badges of aliases in `connected` render blue, others gray. Self-heals via
|
|
49
|
-
* MutationObserver on the sidebar container (rows re-render on
|
|
50
|
-
* session/workspace changes).
|
|
51
|
-
* @param workspaces - the SSH workspace records (id/title/alias/remoteRoot).
|
|
52
|
-
* @param connected - aliases with a live pooled connection (badge coloring).
|
|
53
|
-
* @returns disposer.
|
|
54
|
-
*/
|
|
55
|
-
export function mountWorkspaceBadges(
|
|
56
|
-
workspaces: Array<{ id: string; title: string; alias: string; remoteRoot: string }>,
|
|
57
|
-
connected?: ReadonlySet<string>,
|
|
58
|
-
): () => void {
|
|
59
|
-
const byTitle = new Map<string, { alias: string; remoteRoot: string }>()
|
|
60
|
-
for (const workspace of workspaces) byTitle.set(workspace.title, workspace)
|
|
61
|
-
|
|
62
|
-
/** Decorate one project row
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
row.title =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar workspace-row augmentation: labels every host workspace row whose
|
|
3
|
+
* title matches an SSH-bound workspace with a compact remote badge
|
|
4
|
+
* (`alias @ remoteRoot`), so a glance tells a local workspace from a remote
|
|
5
|
+
* one. Pure DOM-level extension (the sidebar shell exposes no slot for row
|
|
6
|
+
* decoration), following the dsh-ssh sidebar-entry precedent: a
|
|
7
|
+
* MutationObserver self-heals when React re-renders displace the injected
|
|
8
|
+
* badge.
|
|
9
|
+
*
|
|
10
|
+
* The row title alone matches: SSH-bound workspace titles are unique enough
|
|
11
|
+
* in practice (the ledger title is what the row shows). A title match against
|
|
12
|
+
* `~/.dsh/ssh-workspaces/<id>` anchors also works when the host picks that
|
|
13
|
+
* default title.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-hardssh/client/workspace-badges
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const PROJECT_ROW_SELECTOR = '[class*="projectRow"]'
|
|
19
|
+
const TITLE_SELECTOR = '[class*="title"]'
|
|
20
|
+
|
|
21
|
+
/** Render a small remote badge element. The cloud icon marks "remote";
|
|
22
|
+
* connected servers render in DeepSeek blue, disconnected stay gray. The
|
|
23
|
+
* tooltip shows the REMOTE directory plus the server, e.g.
|
|
24
|
+
* `/data/app (prod-01)`. */
|
|
25
|
+
export function makeBadge(alias: string, remoteRoot: string, connected: boolean): HTMLElement {
|
|
26
|
+
const badge = document.createElement('span')
|
|
27
|
+
badge.dataset.sshBadge = connected ? 'connected' : 'disconnected'
|
|
28
|
+
badge.title = `${remoteRoot}(${alias})`
|
|
29
|
+
badge.setAttribute(
|
|
30
|
+
'style',
|
|
31
|
+
connected
|
|
32
|
+
? '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'
|
|
33
|
+
: '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',
|
|
34
|
+
)
|
|
35
|
+
const icon = document.createElement('span')
|
|
36
|
+
icon.setAttribute('style', 'display:inline-flex;flex:none;line-height:1')
|
|
37
|
+
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>'
|
|
38
|
+
const label = document.createElement('span')
|
|
39
|
+
label.textContent = alias
|
|
40
|
+
label.setAttribute('style', 'font-weight:400')
|
|
41
|
+
badge.append(icon, label)
|
|
42
|
+
return badge
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Mount the row-decoration pass. Queries the sidebar for project rows and
|
|
47
|
+
* appends a remote badge when the row's title belongs to an SSH workspace.
|
|
48
|
+
* Badges of aliases in `connected` render blue, others gray. Self-heals via
|
|
49
|
+
* MutationObserver on the sidebar container (rows re-render on
|
|
50
|
+
* session/workspace changes).
|
|
51
|
+
* @param workspaces - the SSH workspace records (id/title/alias/remoteRoot).
|
|
52
|
+
* @param connected - aliases with a live pooled connection (badge coloring).
|
|
53
|
+
* @returns disposer.
|
|
54
|
+
*/
|
|
55
|
+
export function mountWorkspaceBadges(
|
|
56
|
+
workspaces: Array<{ id: string; title: string; alias: string; remoteRoot: string }>,
|
|
57
|
+
connected?: ReadonlySet<string>,
|
|
58
|
+
): () => void {
|
|
59
|
+
const byTitle = new Map<string, { alias: string; remoteRoot: string }>()
|
|
60
|
+
for (const workspace of workspaces) byTitle.set(workspace.title, workspace)
|
|
61
|
+
|
|
62
|
+
/** Decorate one project row: ALWAYS keep the row tooltip = the REMOTE
|
|
63
|
+
* directory (server), never the local anchor path the shell would show;
|
|
64
|
+
* mount the badge once (idempotent via the guard). Re-runs on every scan,
|
|
65
|
+
* so a shell re-render that resets <title> gets corrected. */
|
|
66
|
+
const decorate = (row: HTMLElement): void => {
|
|
67
|
+
const titleEl = row.querySelector<HTMLElement>(TITLE_SELECTOR)
|
|
68
|
+
if (titleEl === null || titleEl === undefined) return
|
|
69
|
+
const title = titleEl.textContent?.trim() ?? ''
|
|
70
|
+
const target = byTitle.get(title)
|
|
71
|
+
if (target === undefined) return
|
|
72
|
+
const desiredTitle = `${target.remoteRoot}(${target.alias})`
|
|
73
|
+
if (row.title !== desiredTitle) row.title = desiredTitle
|
|
74
|
+
if (row.querySelector('[data-ssh-badge]') !== null) return
|
|
75
|
+
const isConnected = connected?.has(target.alias) ?? false
|
|
76
|
+
titleEl.appendChild(makeBadge(target.alias, target.remoteRoot, isConnected))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Scan the sidebar column for project rows and decorate each. */
|
|
80
|
+
const scan = (): void => {
|
|
81
|
+
for (const row of document.querySelectorAll<HTMLElement>(PROJECT_ROW_SELECTOR)) {
|
|
82
|
+
decorate(row)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Watch the whole document for the sidebar column arriving / re-rendering,
|
|
87
|
+
// including <title> attribute changes (the shell may reset row tooltips).
|
|
88
|
+
const observer = new MutationObserver(() => { scan() })
|
|
89
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['title'] })
|
|
90
|
+
scan()
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
observer.disconnect()
|
|
94
|
+
// Remove injected badges.
|
|
95
|
+
for (const badge of document.querySelectorAll<HTMLElement>('[data-ssh-badge]')) {
|
|
96
|
+
badge.remove()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SSH workspace manager's LEFT-sidebar global entry and its CENTER panel.
|
|
3
|
+
*
|
|
4
|
+
* Both are standard plugin extension points, not DOM injection:
|
|
5
|
+
* - `sidebar.panellist` declares the row between "New session" and the
|
|
6
|
+
* workspace browser. Its `id` IS the main-panel key the row selects, and the
|
|
7
|
+
* shell owns the button, the label, and the active highlight.
|
|
8
|
+
* - `main` hosts the panel body under that same key.
|
|
9
|
+
*
|
|
10
|
+
* The id is deliberately our own rather than a shipped one: a fresh id is
|
|
11
|
+
* added beside the shipped rows, while reusing one would replace that row.
|
|
12
|
+
*/
|
|
13
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
14
|
+
// Type-only: pulls the SlotMap augmentation for `sidebar.panellist` / `main`
|
|
15
|
+
// and their owner-prop shapes.
|
|
16
|
+
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
|
17
|
+
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
19
|
+
import type { WorkspaceManager } from './state.ts'
|
|
20
|
+
import type { SshApi } from './ssh/api.ts'
|
|
21
|
+
import { tt } from './text.ts'
|
|
22
|
+
import { WorkspaceManagerPanel } from './workspace-panel.tsx'
|
|
23
|
+
|
|
24
|
+
/** The main-panel key AND the sidebar row id: one identity addresses both. */
|
|
25
|
+
export const WORKSPACE_PANEL_ID = 'dsh-hardssh-workspaces'
|
|
26
|
+
|
|
27
|
+
/** Order among the global panel rows; the shipped rows sit around 0. */
|
|
28
|
+
const WORKSPACE_PANEL_ORDER = 100
|
|
29
|
+
|
|
30
|
+
/** Icon presentation the sidebar supplies to a global panel row. */
|
|
31
|
+
interface PanelIconProps {
|
|
32
|
+
size: number
|
|
33
|
+
active: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The cloud/terminal glyph the workspace row shows in both the wide and rail sidebar. */
|
|
37
|
+
function WorkspacePanelIcon({ size }: PanelIconProps) {
|
|
38
|
+
return (
|
|
39
|
+
<svg
|
|
40
|
+
viewBox="0 0 16 16"
|
|
41
|
+
width={size}
|
|
42
|
+
height={size}
|
|
43
|
+
fill="none"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
strokeWidth="1.3"
|
|
46
|
+
strokeLinecap="round"
|
|
47
|
+
strokeLinejoin="round"
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
>
|
|
50
|
+
<rect x="2" y="2.5" width="12" height="11" rx="1.5" />
|
|
51
|
+
<path d="M4.5 5.5l2.5 2.5-2.5 2.5" />
|
|
52
|
+
<path d="M8.5 10.5h3" />
|
|
53
|
+
</svg>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Dependencies the panel body needs at render time. */
|
|
58
|
+
export interface WorkspacePanelDeps {
|
|
59
|
+
manager: WorkspaceManager
|
|
60
|
+
/** Absent when the plugin is mounted without the SSH operations half. */
|
|
61
|
+
sshApi?: SshApi
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Register the left-sidebar row and the center panel body.
|
|
66
|
+
* @param ctx - client root context carrying the slot registry.
|
|
67
|
+
* @param deps - the workspace manager and the host API the panel uses.
|
|
68
|
+
*/
|
|
69
|
+
export function registerWorkspacePanel(ctx: ClientContext, deps: WorkspacePanelDeps): void {
|
|
70
|
+
const inject = (): Record<string, unknown> => ({ manager: deps.manager, sshApi: deps.sshApi })
|
|
71
|
+
|
|
72
|
+
// The global row: the shell renders the button and the active highlight from
|
|
73
|
+
// the list metadata, and selecting it calls layout.selectPanel(id).
|
|
74
|
+
ctx.slots.inject('sidebar.panellist', () => ctx.slots.register(
|
|
75
|
+
{
|
|
76
|
+
name: 'sidebar.panellist',
|
|
77
|
+
id: WORKSPACE_PANEL_ID,
|
|
78
|
+
order: WORKSPACE_PANEL_ORDER,
|
|
79
|
+
// A thunk is re-read on every projection, so a language change needs no
|
|
80
|
+
// re-registration.
|
|
81
|
+
label: () => tt('panel.workspaces'),
|
|
82
|
+
},
|
|
83
|
+
WorkspacePanelIcon,
|
|
84
|
+
))
|
|
85
|
+
|
|
86
|
+
// The center panel body, under the same key the row selects.
|
|
87
|
+
ctx.slots.inject('main', () => ctx.slots.register(
|
|
88
|
+
{ name: 'main', key: WORKSPACE_PANEL_ID, inject },
|
|
89
|
+
WorkspaceManagerPanel,
|
|
90
|
+
))
|
|
91
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SSH workspace manager as a CENTER panel (left sidebar global entry →
|
|
3
|
+
* `main` slot). Content is unchanged from the old session-header dropdown:
|
|
4
|
+
* server rows with their SSH-bound workspaces underneath, server edit/delete
|
|
5
|
+
* (delete is refused while the server still backs a workspace), and a
|
|
6
|
+
* "new server" action at the bottom.
|
|
7
|
+
*
|
|
8
|
+
* This is a plain panel body: no portal, no anchoring, no click-outside
|
|
9
|
+
* dismissal. The left sidebar owns the entry row and the center column owns
|
|
10
|
+
* the frame; closing the panel is the shell's business (select the
|
|
11
|
+
* Conversation row back), not ours.
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect, useState, type ReactElement } from 'react'
|
|
14
|
+
import type { WorkspaceManager } from './state.ts'
|
|
15
|
+
import type { SshWorkspaceRecord } from '../protocol.ts'
|
|
16
|
+
import { tt } from './text.ts'
|
|
17
|
+
import css from './workspace.module.css'
|
|
18
|
+
import { CloudIcon, ComputerIcon } from './icons.tsx'
|
|
19
|
+
import type { SshHostSummary } from '../ssh/protocol.ts'
|
|
20
|
+
import { HostFormDialog } from './ssh/panel/HostFormDialog.tsx'
|
|
21
|
+
import type { SshApi } from './ssh/api.ts'
|
|
22
|
+
|
|
23
|
+
/** The host-form dialog invocation state. */
|
|
24
|
+
type HostDialogState = { mode: 'create' } | { mode: 'edit'; host: SshHostSummary }
|
|
25
|
+
|
|
26
|
+
/** Panel props: the workspace snapshot owner and the host API. */
|
|
27
|
+
export interface WorkspaceManagerPanelProps {
|
|
28
|
+
manager: WorkspaceManager
|
|
29
|
+
/** Full SSH host API (list/create/update/delete) for server management. */
|
|
30
|
+
sshApi?: SshApi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The workspace manager panel body. */
|
|
34
|
+
export function WorkspaceManagerPanel({ manager, sshApi }: WorkspaceManagerPanelProps): ReactElement {
|
|
35
|
+
const [workspaces, setWorkspaces] = useState<SshWorkspaceRecord[]>(() => manager.getSnapshot().workspaces)
|
|
36
|
+
const [hosts, setHosts] = useState<SshHostSummary[] | null>(null)
|
|
37
|
+
const [connectedAliases, setConnectedAliases] = useState<ReadonlySet<string> | null>(null)
|
|
38
|
+
/** Why connection state is unavailable — surfaced, never silently degraded. */
|
|
39
|
+
const [connectionError, setConnectionError] = useState<string | null>(null)
|
|
40
|
+
const [error, setError] = useState<string | null>(null)
|
|
41
|
+
const [deleting, setDeleting] = useState<string | null>(null)
|
|
42
|
+
const [deletingAlias, setDeletingAlias] = useState<string | null>(null)
|
|
43
|
+
const [hostDialog, setHostDialog] = useState<HostDialogState | null>(null)
|
|
44
|
+
|
|
45
|
+
const loadHosts = (): void => {
|
|
46
|
+
if (sshApi === undefined) {
|
|
47
|
+
setHosts([])
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
void sshApi.listHosts().then(
|
|
51
|
+
(list) => { setHosts(list) },
|
|
52
|
+
(cause: unknown) => {
|
|
53
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
54
|
+
setHosts([])
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
// Panel activation refreshes the workspace ledger and host list. Connection
|
|
61
|
+
// status is a read-only view of the existing pool: connectedAliases() never
|
|
62
|
+
// dials a server, so opening this panel cannot create SSH connections.
|
|
63
|
+
setWorkspaces(manager.getSnapshot().workspaces)
|
|
64
|
+
void manager.refresh()
|
|
65
|
+
const unsubscribe = manager.subscribe(() => { setWorkspaces(manager.getSnapshot().workspaces) })
|
|
66
|
+
loadHosts()
|
|
67
|
+
|
|
68
|
+
let disposed = false
|
|
69
|
+
const refreshConnections = async (): Promise<void> => {
|
|
70
|
+
if (sshApi === undefined) {
|
|
71
|
+
if (!disposed) setConnectedAliases(new Set())
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const aliases = await sshApi.connectedAliases()
|
|
76
|
+
if (disposed) return
|
|
77
|
+
setConnectedAliases(new Set(aliases))
|
|
78
|
+
setConnectionError(null)
|
|
79
|
+
} catch (cause: unknown) {
|
|
80
|
+
if (disposed) return
|
|
81
|
+
// Previously an empty `catch` that only blanked the badges: a broken
|
|
82
|
+
// connection-state route looked like "no server is connected".
|
|
83
|
+
setConnectedAliases(null)
|
|
84
|
+
setConnectionError(cause instanceof Error ? cause.message : String(cause))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
void refreshConnections()
|
|
88
|
+
const timer = window.setInterval(() => { void refreshConnections() }, 3000)
|
|
89
|
+
return () => {
|
|
90
|
+
disposed = true
|
|
91
|
+
window.clearInterval(timer)
|
|
92
|
+
unsubscribe()
|
|
93
|
+
}
|
|
94
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
95
|
+
}, [manager, sshApi])
|
|
96
|
+
|
|
97
|
+
const remove = (id: string): void => {
|
|
98
|
+
setDeleting(id)
|
|
99
|
+
void manager.remove(id)
|
|
100
|
+
.catch((cause: unknown) => setError(cause instanceof Error ? cause.message : String(cause)))
|
|
101
|
+
.finally(() => setDeleting(null))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const deleteHost = (alias: string): void => {
|
|
105
|
+
if (sshApi === undefined) return
|
|
106
|
+
setDeletingAlias(alias)
|
|
107
|
+
void sshApi.deleteHost(alias)
|
|
108
|
+
.then(() => { loadHosts() })
|
|
109
|
+
.catch((cause: unknown) => setError(cause instanceof Error ? cause.message : String(cause)))
|
|
110
|
+
.finally(() => setDeletingAlias(null))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const savedHost = (): void => {
|
|
114
|
+
setHostDialog(null)
|
|
115
|
+
loadHosts()
|
|
116
|
+
void manager.refresh()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className={css.managerPanel}>
|
|
121
|
+
<div className={css.managerPanelHeader}>
|
|
122
|
+
<h2 className={css.managerPanelTitle}>{tt('manager.title')}</h2>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{error !== null && <div className={css.dialogFail}>{error}</div>}
|
|
126
|
+
|
|
127
|
+
{connectionError !== null && (
|
|
128
|
+
<div className={css.dialogFail} data-test="connection-state-error">
|
|
129
|
+
{tt('manager.connectionError', connectionError)}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{hosts === null && <div className={css.dialogInfo}>{tt('panel.loading')}</div>}
|
|
134
|
+
|
|
135
|
+
{workspaces.length === 0 && hosts !== null && hosts.length === 0 && (
|
|
136
|
+
<div className={css.dialogHint}>{tt('manager.empty')}</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{/* Server rows (edit + delete) with their workspaces underneath. */}
|
|
140
|
+
{hosts !== null && hosts.map((host) => {
|
|
141
|
+
const hostWorkspaces = workspaces.filter(workspace => workspace.alias === host.alias)
|
|
142
|
+
const inUse = hostWorkspaces.length > 0
|
|
143
|
+
const connectionState = connectedAliases === null
|
|
144
|
+
? 'unknown'
|
|
145
|
+
: connectedAliases.has(host.alias) ? 'connected' : 'disconnected'
|
|
146
|
+
const connectionLabel = connectionState === 'connected'
|
|
147
|
+
? tt('manager.connected')
|
|
148
|
+
: connectionState === 'disconnected' ? tt('manager.disconnected') : tt('manager.connectionUnknown')
|
|
149
|
+
return (
|
|
150
|
+
<div key={host.alias} className={css.managerPanelGroup}>
|
|
151
|
+
<div className={css.managerGroupHeader} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px' }}>
|
|
152
|
+
<ComputerIcon size={13} />
|
|
153
|
+
<span style={{ fontWeight: 500, fontSize: 13, flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
154
|
+
{host.alias}
|
|
155
|
+
</span>
|
|
156
|
+
<span className={css.connectionBadge} data-state={connectionState} title={connectionLabel}>
|
|
157
|
+
<span className={css.connectionDot} aria-hidden="true" />
|
|
158
|
+
{connectionLabel}
|
|
159
|
+
</span>
|
|
160
|
+
<span className={css.hostMeta} style={{ fontSize: 11 }}>{host.user}@{host.host}:{host.port}</span>
|
|
161
|
+
{/* Gear: edit server */}
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
className={css.menuItemIconButton}
|
|
165
|
+
title={tt('manager.editServer')}
|
|
166
|
+
aria-label={tt('manager.editServer')}
|
|
167
|
+
onClick={() => { setHostDialog({ mode: 'edit', host }) }}
|
|
168
|
+
>
|
|
169
|
+
⚙
|
|
170
|
+
</button>
|
|
171
|
+
{/* Minus: delete server (disabled while it backs workspaces) */}
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
className={css.menuItemIconButton}
|
|
175
|
+
title={inUse ? tt('manager.hostInUse', hostWorkspaces.length) : tt('manager.deleteServer')}
|
|
176
|
+
aria-label={tt('manager.deleteServer')}
|
|
177
|
+
disabled={inUse || deletingAlias === host.alias}
|
|
178
|
+
onClick={() => { if (!inUse) deleteHost(host.alias) }}
|
|
179
|
+
>
|
|
180
|
+
−
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
{hostWorkspaces.map((workspace) => (
|
|
184
|
+
<div key={workspace.id} className={css.hostRow} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '3px 8px' }}>
|
|
185
|
+
<CloudIcon size={12} />
|
|
186
|
+
<span className={css.hostAlias} style={{ fontSize: 12.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1, minWidth: 0 }}>
|
|
187
|
+
{workspace.title}
|
|
188
|
+
</span>
|
|
189
|
+
<span className={css.hostMeta} style={{ fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 220 }}>
|
|
190
|
+
{workspace.remoteRoot}
|
|
191
|
+
</span>
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
className={`${css.button} ${css.danger}`}
|
|
195
|
+
style={{ flex: 'none', padding: '2px 8px', fontSize: 12 }}
|
|
196
|
+
disabled={deleting === workspace.id}
|
|
197
|
+
onClick={() => { if (deleting === null) remove(workspace.id) }}
|
|
198
|
+
>
|
|
199
|
+
{deleting === workspace.id ? '…' : tt('manager.delete')}
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
})}
|
|
206
|
+
|
|
207
|
+
{/* Bottom: create a new server. */}
|
|
208
|
+
{sshApi !== undefined && (
|
|
209
|
+
<div className={css.managerPanelFooter}>
|
|
210
|
+
<button type="button" className={css.menuItem} onClick={() => { setHostDialog({ mode: 'create' }) }}>
|
|
211
|
+
<span className={css.menuItemIcon}>+</span>
|
|
212
|
+
<span>{tt('manager.newHost')}</span>
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
{hostDialog !== null && sshApi !== undefined && (
|
|
218
|
+
<HostFormDialog
|
|
219
|
+
api={sshApi}
|
|
220
|
+
editing={hostDialog.mode === 'edit' ? hostDialog.host : null}
|
|
221
|
+
onClose={() => { setHostDialog(null) }}
|
|
222
|
+
onSaved={savedHost}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
border-radius: 6px;
|
|
69
69
|
background: var(--dsw-alias-bg-base-2, rgba(0, 0, 0, 0.04));
|
|
70
70
|
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
|
|
71
|
-
color: #
|
|
71
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
72
72
|
font-size: 12px;
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.2));
|
|
164
164
|
height: 32px;
|
|
165
165
|
min-width: 111px;
|
|
166
|
-
color: #
|
|
166
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
167
167
|
font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
|
168
168
|
Roboto, system-ui, 'PingFang SC', 'Microsoft YaHei', sans-serif);
|
|
169
169
|
cursor: pointer;
|
|
@@ -255,6 +255,40 @@
|
|
|
255
255
|
margin-bottom: 5px;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
.connectionBadge {
|
|
259
|
+
flex: none;
|
|
260
|
+
display: inline-flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
gap: 5px;
|
|
263
|
+
padding: 1px 7px;
|
|
264
|
+
color: var(--dsw-alias-label-secondary, #61666b);
|
|
265
|
+
font-size: 11px;
|
|
266
|
+
line-height: 18px;
|
|
267
|
+
white-space: nowrap;
|
|
268
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
|
|
269
|
+
border-radius: 999px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.connectionDot {
|
|
273
|
+
width: 7px;
|
|
274
|
+
height: 7px;
|
|
275
|
+
background: var(--dsw-alias-label-tertiary, #81858c);
|
|
276
|
+
border-radius: 50%;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.connectionBadge[data-state='connected'] {
|
|
280
|
+
color: var(--dsw-alias-state-success-primary, #18a058);
|
|
281
|
+
border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary, #18a058) 48%, transparent);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.connectionBadge[data-state='connected'] .connectionDot {
|
|
285
|
+
background: var(--dsw-alias-state-success-primary, #18a058);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.connectionBadge[data-state='disconnected'] {
|
|
289
|
+
color: var(--dsw-alias-label-tertiary, #81858c);
|
|
290
|
+
}
|
|
291
|
+
|
|
258
292
|
/* Icon-only round buttons on server rows (edit/delete) */
|
|
259
293
|
.menuItemIconButton {
|
|
260
294
|
flex: none;
|
|
@@ -281,3 +315,43 @@
|
|
|
281
315
|
opacity: 0.4;
|
|
282
316
|
cursor: default;
|
|
283
317
|
}
|
|
318
|
+
|
|
319
|
+
/* The workspace manager as a CENTER panel (left sidebar global entry → main
|
|
320
|
+
slot). The center column owns the frame; this only fills it and scrolls. */
|
|
321
|
+
.managerPanel {
|
|
322
|
+
box-sizing: border-box;
|
|
323
|
+
width: 100%;
|
|
324
|
+
height: 100%;
|
|
325
|
+
min-height: 0;
|
|
326
|
+
overflow-y: auto;
|
|
327
|
+
padding: 16px 20px 24px;
|
|
328
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
329
|
+
font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
|
330
|
+
Roboto, system-ui, 'PingFang SC', 'Microsoft YaHei', sans-serif);
|
|
331
|
+
font-size: 13px;
|
|
332
|
+
line-height: 20px;
|
|
333
|
+
scrollbar-width: thin;
|
|
334
|
+
scrollbar-color: var(--dsw-alias-label-caption, rgba(128, 128, 128, 0.5)) transparent;
|
|
335
|
+
}
|
|
336
|
+
.managerPanelHeader {
|
|
337
|
+
display: flex;
|
|
338
|
+
align-items: center;
|
|
339
|
+
gap: 10px;
|
|
340
|
+
margin-bottom: 12px;
|
|
341
|
+
}
|
|
342
|
+
.managerPanelTitle {
|
|
343
|
+
flex: 1;
|
|
344
|
+
margin: 0;
|
|
345
|
+
font-size: 16px;
|
|
346
|
+
font-weight: 700;
|
|
347
|
+
white-space: nowrap;
|
|
348
|
+
}
|
|
349
|
+
.managerPanelGroup {
|
|
350
|
+
margin-bottom: 14px;
|
|
351
|
+
}
|
|
352
|
+
.managerPanelFooter {
|
|
353
|
+
margin-top: 8px;
|
|
354
|
+
padding-top: 8px;
|
|
355
|
+
border-top: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.1));
|
|
356
|
+
}
|
|
357
|
+
|