@tiphareth/dsh-hardssh 0.1.2-alpha → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +30 -0
- package/README.md +49 -25
- package/SKILLS.md +140 -0
- package/lib/base/index.js +95 -0
- package/lib/client.js +1165 -1329
- package/lib/fs.js +307 -416
- package/lib/index.js +5428 -2991
- package/lib/ledger-B-LXlftp.js +43 -0
- package/lib/ledger-D2ezq1iW.js +376 -0
- package/lib/model-Cp7f70Mb.js +5 -0
- package/lib/registry-CViuzKYI.js +421 -0
- package/lib/subprocess.js +92 -549
- package/lib/types/backend.d.ts +73 -78
- package/lib/types/base/capability.d.ts +4 -0
- package/lib/types/base/ledger-router.d.ts +42 -26
- package/lib/types/base/ledger.d.ts +30 -19
- package/lib/types/base/model.d.ts +17 -8
- package/lib/types/base/plugin.d.ts +11 -0
- package/lib/types/base/registry.d.ts +28 -31
- package/lib/types/base/router.d.ts +0 -7
- package/lib/types/client/api.d.ts +8 -8
- package/lib/types/client/connect-host.d.ts +13 -0
- package/lib/types/client/index.d.ts +6 -4
- package/lib/types/client/locales.d.ts +15 -1
- package/lib/types/client/session-connect-gate.d.ts +71 -0
- package/lib/types/client/ssh/api.d.ts +11 -9
- package/lib/types/client/ssh/apply.d.ts +14 -8
- package/lib/types/client/ssh/locales.d.ts +8 -1
- package/lib/types/client/ssh/ops-tab.d.ts +34 -0
- package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
- package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
- package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
- package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
- package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
- package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
- package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
- package/lib/types/client/ssh/session-target.d.ts +17 -0
- package/lib/types/client/state.d.ts +4 -1
- package/lib/types/client/workspace-panel-entry.d.ts +30 -0
- package/lib/types/client/workspace-panel.d.ts +24 -0
- package/lib/types/client-http.d.ts +77 -6
- package/lib/types/core.d.ts +5 -16
- package/lib/types/fs.d.ts +18 -25
- package/lib/types/index.d.ts +138 -9
- package/lib/types/ledger.d.ts +16 -111
- package/lib/types/protocol.d.ts +0 -5
- package/lib/types/providers/index.d.ts +6 -3
- package/lib/types/providers/local/provider.d.ts +83 -23
- package/lib/types/providers/ssh/provider.d.ts +24 -57
- package/lib/types/remote/environment.d.ts +2 -0
- package/lib/types/remote/remote-fs.d.ts +12 -1
- package/lib/types/remote/remote-process.d.ts +22 -1
- package/lib/types/remote/remote-subprocess.d.ts +13 -1
- package/lib/types/remote/remote-terminal.d.ts +4 -0
- package/lib/types/remote-search.d.ts +3 -3
- package/lib/types/routes.d.ts +36 -8
- package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
- package/lib/types/runtime/workspace-core.d.ts +73 -38
- package/lib/types/runtime/workspace-migration.d.ts +113 -0
- package/lib/types/ssh/connection/lease.d.ts +23 -0
- package/lib/types/ssh/connection/manager.d.ts +359 -0
- package/lib/types/ssh/connection/pool.d.ts +8 -1
- package/lib/types/ssh/engine.d.ts +97 -267
- package/lib/types/ssh/known-hosts.d.ts +22 -5
- package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
- package/lib/types/ssh/plugin.d.ts +10 -15
- package/lib/types/ssh/protocol.d.ts +18 -0
- package/lib/types/ssh/routes.d.ts +15 -7
- package/lib/types/ssh/sftp/service.d.ts +216 -0
- package/lib/types/ssh/store.d.ts +20 -8
- package/lib/types/ssh/terminal/service.d.ts +49 -0
- package/lib/types/ssh/tunnel/service.d.ts +26 -0
- package/lib/types/ssh/vault.d.ts +51 -4
- package/lib/types/subprocess.d.ts +13 -21
- package/lib/types/switch/switch-fs.d.ts +52 -2
- package/lib/types/switch/switch-subprocess.d.ts +32 -3
- package/lib/types/tools.d.ts +7 -6
- package/lib/types/workspace-tool-ops.d.ts +33 -0
- package/lib/types/workspace.d.ts +18 -0
- package/lib/vault-3gpWct2Q.js +559 -0
- package/lib/workspace.js +2 -0
- package/package.json +36 -11
- package/scripts/export-legacy-workspaces.mjs +136 -0
- package/src/backend.ts +155 -483
- package/src/base/capability.ts +4 -0
- package/src/base/ledger-router.ts +214 -65
- package/src/base/ledger.ts +175 -48
- package/src/base/model.ts +18 -8
- package/src/base/plugin.ts +15 -4
- package/src/base/registry.ts +49 -41
- package/src/base/router.ts +0 -8
- package/src/client/api.ts +8 -36
- package/src/client/connect-host.ts +199 -0
- package/src/client/directory-flow.tsx +91 -36
- package/src/client/index.ts +56 -53
- package/src/client/locales.ts +14 -0
- package/src/client/session-connect-gate.ts +163 -0
- package/src/client/ssh/api.ts +245 -108
- package/src/client/ssh/apply.ts +23 -24
- package/src/client/ssh/locales.ts +16 -2
- package/src/client/ssh/ops-tab.tsx +79 -0
- package/src/client/ssh/panel/ClusterTab.tsx +9 -20
- package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
- package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
- package/src/client/ssh/panel/SshPanel.tsx +81 -77
- package/src/client/ssh/panel/TerminalTab.tsx +18 -37
- package/src/client/ssh/panel/TransferTab.tsx +43 -33
- package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
- package/src/client/ssh/panel/panel.module.css +90 -127
- package/src/client/ssh/session-target.ts +69 -0
- package/src/client/state.ts +9 -1
- package/src/client/workspace-badges.ts +99 -99
- package/src/client/workspace-panel-entry.tsx +91 -0
- package/src/client/workspace-panel.tsx +227 -0
- package/src/client/workspace.module.css +76 -2
- package/src/client-http.ts +118 -18
- package/src/core.ts +37 -47
- package/src/fs.ts +137 -85
- package/src/index.ts +535 -244
- package/src/ledger.ts +62 -416
- package/src/protocol.ts +0 -6
- package/src/providers/index.ts +8 -4
- package/src/providers/local/provider.ts +259 -87
- package/src/providers/ssh/provider.ts +158 -155
- package/src/remote/environment.ts +22 -1
- package/src/remote/remote-fs.ts +123 -34
- package/src/remote/remote-process.ts +265 -203
- package/src/remote/remote-subprocess.ts +61 -18
- package/src/remote/remote-terminal.ts +106 -21
- package/src/remote-search.ts +122 -26
- package/src/routes.ts +416 -395
- package/src/runtime/dsh-capabilities.ts +19 -0
- package/src/runtime/workspace-core.ts +171 -88
- package/src/runtime/workspace-migration.ts +472 -0
- package/src/ssh/connection/lease.ts +35 -0
- package/src/ssh/connection/manager.ts +1083 -0
- package/src/ssh/connection/pool.ts +341 -275
- package/src/ssh/engine.ts +269 -1477
- package/src/ssh/known-hosts.ts +42 -18
- package/src/ssh/local-transfer-policy.ts +83 -0
- package/src/ssh/plugin.ts +20 -19
- package/src/ssh/protocol.ts +22 -1
- package/src/ssh/routes.ts +419 -187
- package/src/ssh/sftp/service.ts +967 -0
- package/src/ssh/store.ts +169 -72
- package/src/ssh/terminal/service.ts +177 -0
- package/src/ssh/tools.ts +42 -20
- package/src/ssh/tunnel/service.ts +217 -0
- package/src/ssh/vault.ts +245 -91
- package/src/subprocess.ts +88 -71
- package/src/switch/switch-fs.ts +141 -10
- package/src/switch/switch-subprocess.ts +80 -6
- package/src/tools.ts +217 -221
- package/src/workspace-tool-ops.ts +101 -0
- package/src/workspace.ts +52 -0
- package/lib/environment-BL1jddfB.js +0 -449
- package/lib/switch-fs-CAJpFY9C.js +0 -193
- package/lib/switch-fs-RrZtG2gv.js +0 -210
- package/lib/types/client/manager-button.d.ts +0 -32
- package/lib/types/client/ssh/mount.d.ts +0 -13
- package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
- package/lib/types/client/ssh/panel/controller.d.ts +0 -23
- package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
- package/lib/types/client/workspace-gate.d.ts +0 -15
- package/lib/types/remote-runner.d.ts +0 -83
- package/lib/types/seam-state.d.ts +0 -69
- package/src/client/manager-button.tsx +0 -269
- package/src/client/ssh/mount.tsx +0 -83
- package/src/client/ssh/panel/HostsTab.tsx +0 -236
- package/src/client/ssh/panel/controller.ts +0 -48
- package/src/client/ssh/sidebar-entry.ts +0 -123
- package/src/client/workspace-gate.ts +0 -232
- package/src/remote-runner.ts +0 -201
- package/src/seam-state.ts +0 -185
|
@@ -5,41 +5,27 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { useEffect, useRef, useState } from 'react'
|
|
7
7
|
import type { SshApi } from '../api.ts'
|
|
8
|
-
import type {
|
|
8
|
+
import type { TunnelInfo } from '../../../ssh/protocol.ts'
|
|
9
9
|
import { errorMessage, tt } from './helpers.ts'
|
|
10
10
|
import css from './panel.module.css'
|
|
11
11
|
|
|
12
12
|
/** Tunnels tab props. */
|
|
13
13
|
export interface TunnelsTabProps {
|
|
14
14
|
api: SshApi
|
|
15
|
+
/** Fixed alias inherited from the selected Session. */
|
|
16
|
+
alias: string
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/** The tunnels tab. */
|
|
18
|
-
export function TunnelsTab({ api }: TunnelsTabProps) {
|
|
19
|
-
const [hosts, setHosts] = useState<SshHostSummary[]>([])
|
|
20
|
+
export function TunnelsTab({ api, alias }: TunnelsTabProps) {
|
|
20
21
|
const [tunnels, setTunnels] = useState<TunnelInfo[] | null>(null)
|
|
21
22
|
const [error, setError] = useState<string | null>(null)
|
|
22
23
|
const [notice, setNotice] = useState<string | null>(null)
|
|
23
|
-
const [alias, setAlias] = useState('')
|
|
24
24
|
const [remotePort, setRemotePort] = useState('')
|
|
25
25
|
const [remoteHost, setRemoteHost] = useState('')
|
|
26
26
|
const [localPort, setLocalPort] = useState('')
|
|
27
27
|
const [busy, setBusy] = useState(false)
|
|
28
28
|
|
|
29
|
-
// Hosts for the new-tunnel form (failure does not block tunnel listing).
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
let disposed = false
|
|
32
|
-
void (async () => {
|
|
33
|
-
try {
|
|
34
|
-
const list = await api.listHosts()
|
|
35
|
-
if (!disposed) setHosts(list)
|
|
36
|
-
} catch {
|
|
37
|
-
// Tunnels may still exist; keep the list usable.
|
|
38
|
-
}
|
|
39
|
-
})()
|
|
40
|
-
return () => { disposed = true }
|
|
41
|
-
}, [api])
|
|
42
|
-
|
|
43
29
|
// Live list with a 5s heartbeat while mounted. Every load carries a
|
|
44
30
|
// sequence number so stale responses never overwrite newer state.
|
|
45
31
|
const seqRef = useRef(0)
|
|
@@ -49,7 +35,7 @@ export function TunnelsTab({ api }: TunnelsTabProps) {
|
|
|
49
35
|
try {
|
|
50
36
|
const list = await api.listTunnels()
|
|
51
37
|
if (seq !== seqRef.current) return
|
|
52
|
-
setTunnels(list)
|
|
38
|
+
setTunnels(list.filter(tunnel => tunnel.alias === alias))
|
|
53
39
|
setError(null)
|
|
54
40
|
} catch (cause) {
|
|
55
41
|
if (seq !== seqRef.current) return
|
|
@@ -59,7 +45,7 @@ export function TunnelsTab({ api }: TunnelsTabProps) {
|
|
|
59
45
|
void load()
|
|
60
46
|
const timer = setInterval(() => { void load() }, 5000)
|
|
61
47
|
return () => { clearInterval(timer) }
|
|
62
|
-
}, [api])
|
|
48
|
+
}, [api, alias])
|
|
63
49
|
|
|
64
50
|
const refresh = async (): Promise<void> => {
|
|
65
51
|
const seq = ++seqRef.current
|
|
@@ -87,7 +73,7 @@ export function TunnelsTab({ api }: TunnelsTabProps) {
|
|
|
87
73
|
if (!window.confirm(tt('tunnel.stopAllConfirm'))) return
|
|
88
74
|
setBusy(true)
|
|
89
75
|
try {
|
|
90
|
-
await api.stopAllTunnels(alias
|
|
76
|
+
await api.stopAllTunnels(alias)
|
|
91
77
|
await refresh()
|
|
92
78
|
} catch (cause) {
|
|
93
79
|
setError(errorMessage(cause))
|
|
@@ -150,10 +136,7 @@ export function TunnelsTab({ api }: TunnelsTabProps) {
|
|
|
150
136
|
<div className={css.formRow}>
|
|
151
137
|
<label className={css.field}>
|
|
152
138
|
<span className={css.fieldLabel}>{tt('tunnel.alias')}</span>
|
|
153
|
-
<
|
|
154
|
-
<option value="">{tt('terminal.selectHost')}</option>
|
|
155
|
-
{hosts.map(host => <option key={host.alias} value={host.alias}>{host.alias}</option>)}
|
|
156
|
-
</select>
|
|
139
|
+
<span className={css.targetBadge}>{alias}</span>
|
|
157
140
|
</label>
|
|
158
141
|
<label className={css.field}>
|
|
159
142
|
<span className={css.fieldLabel}>{tt('tunnel.remotePort')}</span>
|
|
@@ -169,7 +152,7 @@ export function TunnelsTab({ api }: TunnelsTabProps) {
|
|
|
169
152
|
</label>
|
|
170
153
|
</div>
|
|
171
154
|
<div>
|
|
172
|
-
<button type="button" className={css.primaryButton} disabled={busy ||
|
|
155
|
+
<button type="button" className={css.primaryButton} disabled={busy || remotePort.trim() === ''} onClick={() => { void start() }}>{tt('tunnel.start')}</button>
|
|
173
156
|
</div>
|
|
174
157
|
</div>
|
|
175
158
|
</div>
|
|
@@ -1,95 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-ssh
|
|
3
|
-
* leaks into the rest of the GUI; colors ride the dsh --dsw-* tokens so
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* dsh-ssh operations console styles. Scoped by the plugin's own class names so
|
|
3
|
+
* nothing leaks into the rest of the GUI; colors ride the dsh --dsw-* tokens so
|
|
4
|
+
* the console follows the active theme (light/dark and skins).
|
|
5
|
+
*
|
|
6
|
+
* The console is a RIGHT-Sidebar page tab now: the tab record owns visibility
|
|
7
|
+
* and the chip owns closing, so there is no column-takeover CSS here and no
|
|
8
|
+
* entry row — the sidebar shell renders both from the plugin's registrations.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
/* --- center-column takeover (global rules, attribute-scoped) ----------------- */
|
|
10
|
-
|
|
11
|
-
[data-pane='conversation'] {
|
|
12
|
-
position: relative;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/* The panel container rides inside the conversation grid item as an extra
|
|
16
|
-
trailing child; hidden unless the panel is active. */
|
|
17
|
-
[data-dsh-ssh-view] {
|
|
18
|
-
position: absolute;
|
|
19
|
-
inset: 0;
|
|
20
|
-
display: none;
|
|
21
|
-
z-index: 5;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
html[data-dsh-ssh-active] [data-dsh-ssh-view] {
|
|
25
|
-
display: block;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/* While the panel is active, the conversation content underneath is hidden
|
|
29
|
-
(it stays mounted and stateful). */
|
|
30
|
-
html[data-dsh-ssh-active] [data-pane='conversation'] > :not([data-dsh-ssh-view]) {
|
|
31
|
-
display: none;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/* --- sidebar entry row ------------------------------------------------------- */
|
|
35
|
-
|
|
36
|
-
.entry {
|
|
37
|
-
display: flex;
|
|
38
|
-
align-items: center;
|
|
39
|
-
gap: 8px;
|
|
40
|
-
width: 100%;
|
|
41
|
-
height: 32px;
|
|
42
|
-
padding: 0 12px;
|
|
43
|
-
background: transparent;
|
|
44
|
-
border: none;
|
|
45
|
-
border-radius: 8px;
|
|
46
|
-
color: var(--dsw-alias-label-secondary);
|
|
47
|
-
cursor: pointer;
|
|
48
|
-
font-size: 13px;
|
|
49
|
-
white-space: nowrap;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.entry:hover {
|
|
53
|
-
background: var(--dsw-specific-sidebar-nav-item-hover);
|
|
54
|
-
color: var(--dsw-alias-label-primary);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.entry[data-active] {
|
|
58
|
-
background: var(--dsw-specific-sidebar-nav-item-active);
|
|
59
|
-
color: var(--dsw-alias-label-primary);
|
|
60
|
-
font-weight: 600;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.entryIcon {
|
|
64
|
-
display: inline-flex;
|
|
65
|
-
align-items: center;
|
|
66
|
-
justify-content: center;
|
|
67
|
-
flex: none;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.entryLabel {
|
|
71
|
-
overflow: hidden;
|
|
72
|
-
text-overflow: ellipsis;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/* Collapsed rail: icon-only, centered, matching the shell's 56px rail. */
|
|
76
|
-
[data-dsh-frame][data-sidebar-collapsed] .entry {
|
|
77
|
-
justify-content: center;
|
|
78
|
-
padding: 0;
|
|
79
|
-
width: 100%;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
[data-dsh-frame][data-sidebar-collapsed] .entryLabel {
|
|
83
|
-
display: none;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
11
|
/* --- panel frame -------------------------------------------------------------- */
|
|
87
12
|
|
|
88
|
-
.view {
|
|
89
|
-
overflow: hidden;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
13
|
.panel {
|
|
14
|
+
position: relative;
|
|
93
15
|
display: flex;
|
|
94
16
|
flex-direction: column;
|
|
95
17
|
height: 100%;
|
|
@@ -102,22 +24,6 @@ html[data-dsh-ssh-active] [data-pane='conversation'] > :not([data-dsh-ssh-view])
|
|
|
102
24
|
font-family: var(--dsw-font-family);
|
|
103
25
|
}
|
|
104
26
|
|
|
105
|
-
.panelHeader {
|
|
106
|
-
display: flex;
|
|
107
|
-
align-items: center;
|
|
108
|
-
gap: 10px;
|
|
109
|
-
flex: none;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.panelTitle {
|
|
113
|
-
margin: 0;
|
|
114
|
-
flex: 1;
|
|
115
|
-
font-size: 16px;
|
|
116
|
-
font-weight: 700;
|
|
117
|
-
color: var(--dsw-alias-label-primary);
|
|
118
|
-
white-space: nowrap;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
27
|
/* --- tab bar ------------------------------------------------------------------ */
|
|
122
28
|
|
|
123
29
|
.tabBar {
|
|
@@ -158,6 +64,83 @@ html[data-dsh-ssh-active] [data-pane='conversation'] > :not([data-dsh-ssh-view])
|
|
|
158
64
|
overflow: hidden;
|
|
159
65
|
}
|
|
160
66
|
|
|
67
|
+
.sessionTarget {
|
|
68
|
+
flex: none;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
padding: 5px 9px;
|
|
71
|
+
color: var(--dsw-alias-label-secondary);
|
|
72
|
+
font-size: 12px;
|
|
73
|
+
line-height: 18px;
|
|
74
|
+
text-overflow: ellipsis;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
77
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
78
|
+
border-radius: 7px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.targetBadge {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
min-height: 30px;
|
|
84
|
+
align-items: center;
|
|
85
|
+
padding: 4px 10px;
|
|
86
|
+
color: var(--dsw-alias-state-business-primary);
|
|
87
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
88
|
+
font-size: 12px;
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
91
|
+
border: 1px solid var(--dsw-alias-state-business-primary);
|
|
92
|
+
border-radius: 8px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.disabledSurface {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex: 1;
|
|
98
|
+
min-height: 0;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: 10px;
|
|
101
|
+
opacity: 0.4;
|
|
102
|
+
filter: blur(3px);
|
|
103
|
+
pointer-events: none;
|
|
104
|
+
user-select: none;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.disabledPlaceholder {
|
|
108
|
+
flex: 1;
|
|
109
|
+
min-height: 180px;
|
|
110
|
+
background:
|
|
111
|
+
linear-gradient(var(--dsw-alias-bg-layer-2), var(--dsw-alias-bg-layer-2)) 0 0 / 100% 34px no-repeat,
|
|
112
|
+
repeating-linear-gradient(180deg, transparent 0 34px, var(--dsw-alias-separator-primary) 34px 35px);
|
|
113
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
114
|
+
border-radius: 10px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.localSessionMask {
|
|
118
|
+
position: absolute;
|
|
119
|
+
inset: 0;
|
|
120
|
+
z-index: 2;
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
gap: 8px;
|
|
126
|
+
padding: 32px;
|
|
127
|
+
color: var(--dsw-alias-label-primary);
|
|
128
|
+
text-align: center;
|
|
129
|
+
background: color-mix(in srgb, var(--dsw-alias-bg-base) 72%, transparent);
|
|
130
|
+
backdrop-filter: blur(2px);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.localSessionMask strong {
|
|
134
|
+
font-size: 15px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.localSessionMask span {
|
|
138
|
+
max-width: 340px;
|
|
139
|
+
color: var(--dsw-alias-label-secondary);
|
|
140
|
+
font-size: 13px;
|
|
141
|
+
line-height: 1.55;
|
|
142
|
+
}
|
|
143
|
+
|
|
161
144
|
/* Scrollable tab body (transfer / tunnels); hosts and cluster use fillBody so
|
|
162
145
|
their toolbars stay put while the table scrolls internally. */
|
|
163
146
|
.tabBody {
|
|
@@ -377,26 +360,6 @@ html[data-dsh-ssh-active] [data-pane='conversation'] > :not([data-dsh-ssh-view])
|
|
|
377
360
|
cursor: default;
|
|
378
361
|
}
|
|
379
362
|
|
|
380
|
-
.iconButton {
|
|
381
|
-
display: inline-flex;
|
|
382
|
-
align-items: center;
|
|
383
|
-
justify-content: center;
|
|
384
|
-
width: 26px;
|
|
385
|
-
height: 26px;
|
|
386
|
-
padding: 0;
|
|
387
|
-
background: transparent;
|
|
388
|
-
border: none;
|
|
389
|
-
border-radius: 6px;
|
|
390
|
-
color: var(--dsw-alias-label-secondary);
|
|
391
|
-
cursor: pointer;
|
|
392
|
-
font-size: 13px;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
.iconButton:hover {
|
|
396
|
-
background: var(--dsw-alias-interactive-bg-hover);
|
|
397
|
-
color: var(--dsw-alias-label-primary);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
363
|
.linkButton {
|
|
401
364
|
padding: 0;
|
|
402
365
|
font-size: 12px;
|
|
@@ -509,7 +472,7 @@ html[data-dsh-ssh-active] [data-pane='conversation'] > :not([data-dsh-ssh-view])
|
|
|
509
472
|
background: var(--dsw-alias-bg-base, #fff);
|
|
510
473
|
border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.04));
|
|
511
474
|
border-radius: 12px;
|
|
512
|
-
color: #
|
|
475
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
513
476
|
font-family: var(--dsw-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
|
514
477
|
Roboto, system-ui, 'PingFang SC', 'Microsoft YaHei', sans-serif);
|
|
515
478
|
}
|
|
@@ -533,7 +496,7 @@ html[data-dsh-ssh-active] [data-pane='conversation'] > :not([data-dsh-ssh-view])
|
|
|
533
496
|
gap: 8px;
|
|
534
497
|
font-size: 15px;
|
|
535
498
|
font-weight: 600;
|
|
536
|
-
color: #
|
|
499
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
537
500
|
}
|
|
538
501
|
|
|
539
502
|
/* tiny whale glyph: DeepSeek's friendly blue-whale mark */
|
|
@@ -709,7 +672,7 @@ html[data-dsh-ssh-active] [data-pane='conversation'] > :not([data-dsh-ssh-view])
|
|
|
709
672
|
gap: 7px;
|
|
710
673
|
align-items: center;
|
|
711
674
|
font-size: 13px;
|
|
712
|
-
color: #
|
|
675
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
713
676
|
cursor: pointer;
|
|
714
677
|
}
|
|
715
678
|
|
|
@@ -982,7 +945,7 @@ html[data-dsh-ssh-active] [data-pane='conversation'] > :not([data-dsh-ssh-view])
|
|
|
982
945
|
margin: 0 0 10px;
|
|
983
946
|
font-size: 13px;
|
|
984
947
|
line-height: 1.5;
|
|
985
|
-
color: #
|
|
948
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
986
949
|
}
|
|
987
950
|
|
|
988
951
|
.fingerprintCode {
|
|
@@ -991,7 +954,7 @@ html[data-dsh-ssh-active] [data-pane='conversation'] > :not([data-dsh-ssh-view])
|
|
|
991
954
|
border: 1px solid var(--dsw-alias-border-l2, rgba(0, 0, 0, 0.12));
|
|
992
955
|
border-radius: 6px;
|
|
993
956
|
background: var(--dsw-specific-input-major, #f2f3f5);
|
|
994
|
-
color: #
|
|
957
|
+
color: var(--dsw-alias-label-primary, #0f1115);
|
|
995
958
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
996
959
|
font-size: 13px;
|
|
997
960
|
word-break: break-all;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped SSH target for the right-sidebar operations console.
|
|
3
|
+
*
|
|
4
|
+
* The right sidebar follows the selected Session. Its operations must therefore
|
|
5
|
+
* resolve from that Session's cwd → SSH workspace anchor, never from a host
|
|
6
|
+
* picker inside an individual operation tab.
|
|
7
|
+
*/
|
|
8
|
+
import type { SshWorkspaceRecord } from '../../protocol.ts'
|
|
9
|
+
import type { SessionGateList } from '../session-connect-gate.ts'
|
|
10
|
+
import { makeAnchorWorkspaceResolver } from '../session-connect-gate.ts'
|
|
11
|
+
import type { WorkspaceManager } from '../state.ts'
|
|
12
|
+
|
|
13
|
+
/** Fixed SSH identity inherited from the currently selected Session. */
|
|
14
|
+
export interface SessionSshTarget {
|
|
15
|
+
sessionId: string
|
|
16
|
+
workspaceId: string
|
|
17
|
+
alias: string
|
|
18
|
+
remoteRoot: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** React-compatible external store; null means the Session is local/unbound. */
|
|
22
|
+
export interface SessionSshTargetSource {
|
|
23
|
+
subscribe(listener: () => void): () => void
|
|
24
|
+
getSnapshot(): SessionSshTarget | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Build one stable Session → SSH target source over the public session list and workspace manager. */
|
|
28
|
+
export function createSessionSshTargetSource(
|
|
29
|
+
sessions: SessionGateList,
|
|
30
|
+
manager: Pick<WorkspaceManager, 'getSnapshot' | 'subscribe'>,
|
|
31
|
+
): SessionSshTargetSource {
|
|
32
|
+
const workspaceForCwd = makeAnchorWorkspaceResolver<SshWorkspaceRecord>(
|
|
33
|
+
() => manager.getSnapshot().workspaces,
|
|
34
|
+
)
|
|
35
|
+
let cachedKey: string | undefined
|
|
36
|
+
let cachedValue: SessionSshTarget | null = null
|
|
37
|
+
|
|
38
|
+
const getSnapshot = (): SessionSshTarget | null => {
|
|
39
|
+
const sessionId = sessions.currentSessionId()
|
|
40
|
+
const cwd = sessionId === undefined ? undefined : sessions.sessionCwd(sessionId)
|
|
41
|
+
const workspace = workspaceForCwd(cwd)
|
|
42
|
+
const key = workspace === undefined || sessionId === undefined
|
|
43
|
+
? `local\u0000${sessionId ?? ''}\u0000${cwd ?? ''}`
|
|
44
|
+
: `ssh\u0000${sessionId}\u0000${workspace.id}\u0000${workspace.alias}\u0000${workspace.remoteRoot}`
|
|
45
|
+
if (key === cachedKey) return cachedValue
|
|
46
|
+
cachedKey = key
|
|
47
|
+
cachedValue = workspace === undefined || sessionId === undefined
|
|
48
|
+
? null
|
|
49
|
+
: {
|
|
50
|
+
sessionId,
|
|
51
|
+
workspaceId: workspace.id,
|
|
52
|
+
alias: workspace.alias,
|
|
53
|
+
remoteRoot: workspace.remoteRoot,
|
|
54
|
+
}
|
|
55
|
+
return cachedValue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
getSnapshot,
|
|
60
|
+
subscribe: (listener) => {
|
|
61
|
+
const disposeSessions = sessions.subscribe(listener)
|
|
62
|
+
const disposeWorkspaces = manager.subscribe(listener)
|
|
63
|
+
return () => {
|
|
64
|
+
disposeSessions()
|
|
65
|
+
disposeWorkspaces()
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/client/state.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* between polls. There is NO global local/remote mode anymore — a session's
|
|
6
6
|
* execution world is decided host-side by its cwd (an anchor path of an SSH
|
|
7
7
|
* workspace routes remote); this client state only drives the management UI
|
|
8
|
-
* (list / create / delete) and the sidebar
|
|
8
|
+
* (list / create / delete) and the sidebar row decoration (remote badge + the
|
|
9
|
+
* shell's hover card path rewrite).
|
|
9
10
|
*/
|
|
10
11
|
import type { SshWorkspaceRecord } from '../protocol.ts'
|
|
11
12
|
import type { WorkspaceApi } from './api.ts'
|
|
@@ -42,6 +43,8 @@ export class WorkspaceManager {
|
|
|
42
43
|
private state: WorkspaceManagerState = { workspaces: [], error: null }
|
|
43
44
|
private readonly listeners = new Set<() => void>()
|
|
44
45
|
private timer: number | undefined
|
|
46
|
+
/** Monotonic list-workspaces request id; only the newest request may commit. */
|
|
47
|
+
private refreshSequence = 0
|
|
45
48
|
|
|
46
49
|
constructor(private readonly api: WorkspaceApi) {}
|
|
47
50
|
|
|
@@ -61,8 +64,10 @@ export class WorkspaceManager {
|
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
async refresh(): Promise<void> {
|
|
67
|
+
const request = ++this.refreshSequence
|
|
64
68
|
try {
|
|
65
69
|
const workspaces = await this.api.listWorkspaces()
|
|
70
|
+
if (request !== this.refreshSequence) return
|
|
66
71
|
const next: WorkspaceManagerState = { workspaces, error: null }
|
|
67
72
|
// Emit only on real change: identical polls keep the same snapshot
|
|
68
73
|
// reference, so subscribers (badges, the manager menu) don't re-run
|
|
@@ -72,6 +77,7 @@ export class WorkspaceManager {
|
|
|
72
77
|
this.emit()
|
|
73
78
|
}
|
|
74
79
|
} catch (error) {
|
|
80
|
+
if (request !== this.refreshSequence) return
|
|
75
81
|
const message = error instanceof Error ? error.message : String(error)
|
|
76
82
|
if (this.state.error !== message) {
|
|
77
83
|
this.state = { ...this.state, error: message }
|
|
@@ -86,6 +92,8 @@ export class WorkspaceManager {
|
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
stop(): void {
|
|
95
|
+
// Invalidate an in-flight poll so a stopped manager cannot publish later.
|
|
96
|
+
this.refreshSequence += 1
|
|
89
97
|
if (this.timer !== undefined) {
|
|
90
98
|
window.clearInterval(this.timer)
|
|
91
99
|
this.timer = undefined
|