@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.
Files changed (174) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +77 -50
  3. package/SKILLS.md +140 -0
  4. package/lib/base/index.js +95 -0
  5. package/lib/client.js +1171 -1321
  6. package/lib/fs.js +333 -416
  7. package/lib/index.js +5428 -2991
  8. package/lib/ledger-B-LXlftp.js +43 -0
  9. package/lib/ledger-D2ezq1iW.js +376 -0
  10. package/lib/model-Cp7f70Mb.js +5 -0
  11. package/lib/registry-CViuzKYI.js +421 -0
  12. package/lib/subprocess.js +92 -549
  13. package/lib/types/backend.d.ts +73 -78
  14. package/lib/types/base/capability.d.ts +4 -0
  15. package/lib/types/base/ledger-router.d.ts +42 -26
  16. package/lib/types/base/ledger.d.ts +30 -19
  17. package/lib/types/base/model.d.ts +17 -8
  18. package/lib/types/base/plugin.d.ts +11 -0
  19. package/lib/types/base/registry.d.ts +28 -31
  20. package/lib/types/base/router.d.ts +0 -7
  21. package/lib/types/client/api.d.ts +8 -8
  22. package/lib/types/client/connect-host.d.ts +13 -0
  23. package/lib/types/client/index.d.ts +6 -4
  24. package/lib/types/client/locales.d.ts +15 -1
  25. package/lib/types/client/session-connect-gate.d.ts +71 -0
  26. package/lib/types/client/ssh/api.d.ts +11 -9
  27. package/lib/types/client/ssh/apply.d.ts +14 -8
  28. package/lib/types/client/ssh/locales.d.ts +8 -1
  29. package/lib/types/client/ssh/ops-tab.d.ts +34 -0
  30. package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
  31. package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
  32. package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
  33. package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
  34. package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
  35. package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
  36. package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
  37. package/lib/types/client/ssh/session-target.d.ts +17 -0
  38. package/lib/types/client/state.d.ts +4 -1
  39. package/lib/types/client/workspace-panel-entry.d.ts +30 -0
  40. package/lib/types/client/workspace-panel.d.ts +24 -0
  41. package/lib/types/client-http.d.ts +77 -6
  42. package/lib/types/core.d.ts +5 -16
  43. package/lib/types/fs.d.ts +42 -25
  44. package/lib/types/index.d.ts +138 -9
  45. package/lib/types/ledger.d.ts +16 -111
  46. package/lib/types/protocol.d.ts +0 -5
  47. package/lib/types/providers/index.d.ts +6 -3
  48. package/lib/types/providers/local/provider.d.ts +83 -23
  49. package/lib/types/providers/ssh/provider.d.ts +24 -57
  50. package/lib/types/remote/environment.d.ts +2 -0
  51. package/lib/types/remote/remote-fs.d.ts +12 -1
  52. package/lib/types/remote/remote-process.d.ts +22 -1
  53. package/lib/types/remote/remote-subprocess.d.ts +13 -1
  54. package/lib/types/remote/remote-terminal.d.ts +4 -0
  55. package/lib/types/remote-search.d.ts +3 -3
  56. package/lib/types/routes.d.ts +36 -8
  57. package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
  58. package/lib/types/runtime/workspace-core.d.ts +73 -38
  59. package/lib/types/runtime/workspace-migration.d.ts +113 -0
  60. package/lib/types/ssh/connection/lease.d.ts +23 -0
  61. package/lib/types/ssh/connection/manager.d.ts +359 -0
  62. package/lib/types/ssh/connection/pool.d.ts +8 -1
  63. package/lib/types/ssh/engine.d.ts +97 -267
  64. package/lib/types/ssh/known-hosts.d.ts +22 -5
  65. package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
  66. package/lib/types/ssh/plugin.d.ts +10 -15
  67. package/lib/types/ssh/protocol.d.ts +18 -0
  68. package/lib/types/ssh/routes.d.ts +15 -7
  69. package/lib/types/ssh/sftp/service.d.ts +216 -0
  70. package/lib/types/ssh/store.d.ts +20 -8
  71. package/lib/types/ssh/terminal/service.d.ts +49 -0
  72. package/lib/types/ssh/tunnel/service.d.ts +26 -0
  73. package/lib/types/ssh/vault.d.ts +51 -4
  74. package/lib/types/subprocess.d.ts +13 -21
  75. package/lib/types/switch/switch-fs.d.ts +52 -2
  76. package/lib/types/switch/switch-subprocess.d.ts +32 -3
  77. package/lib/types/tools.d.ts +7 -6
  78. package/lib/types/workspace-tool-ops.d.ts +33 -0
  79. package/lib/types/workspace.d.ts +18 -0
  80. package/lib/vault-3gpWct2Q.js +559 -0
  81. package/lib/workspace.js +2 -0
  82. package/package.json +59 -26
  83. package/scripts/export-legacy-workspaces.mjs +136 -0
  84. package/src/backend.ts +155 -483
  85. package/src/base/capability.ts +4 -0
  86. package/src/base/ledger-router.ts +214 -65
  87. package/src/base/ledger.ts +175 -48
  88. package/src/base/model.ts +18 -8
  89. package/src/base/plugin.ts +15 -4
  90. package/src/base/registry.ts +49 -41
  91. package/src/base/router.ts +0 -8
  92. package/src/client/api.ts +8 -36
  93. package/src/client/connect-host.ts +199 -0
  94. package/src/client/directory-flow.tsx +91 -36
  95. package/src/client/index.ts +56 -53
  96. package/src/client/locales.ts +14 -0
  97. package/src/client/session-connect-gate.ts +163 -0
  98. package/src/client/ssh/api.ts +245 -108
  99. package/src/client/ssh/apply.ts +23 -24
  100. package/src/client/ssh/locales.ts +16 -2
  101. package/src/client/ssh/ops-tab.tsx +79 -0
  102. package/src/client/ssh/panel/ClusterTab.tsx +9 -20
  103. package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
  104. package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
  105. package/src/client/ssh/panel/SshPanel.tsx +81 -77
  106. package/src/client/ssh/panel/TerminalTab.tsx +18 -37
  107. package/src/client/ssh/panel/TransferTab.tsx +43 -33
  108. package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
  109. package/src/client/ssh/panel/panel.module.css +90 -127
  110. package/src/client/ssh/session-target.ts +69 -0
  111. package/src/client/state.ts +9 -1
  112. package/src/client/workspace-badges.ts +99 -96
  113. package/src/client/workspace-panel-entry.tsx +91 -0
  114. package/src/client/workspace-panel.tsx +227 -0
  115. package/src/client/workspace.module.css +76 -2
  116. package/src/client-http.ts +118 -18
  117. package/src/core.ts +37 -47
  118. package/src/fs.ts +171 -85
  119. package/src/index.ts +535 -244
  120. package/src/ledger.ts +62 -416
  121. package/src/protocol.ts +0 -6
  122. package/src/providers/index.ts +8 -4
  123. package/src/providers/local/provider.ts +259 -87
  124. package/src/providers/ssh/provider.ts +158 -155
  125. package/src/remote/environment.ts +22 -1
  126. package/src/remote/remote-fs.ts +123 -34
  127. package/src/remote/remote-process.ts +265 -203
  128. package/src/remote/remote-subprocess.ts +61 -18
  129. package/src/remote/remote-terminal.ts +106 -21
  130. package/src/remote-search.ts +122 -26
  131. package/src/routes.ts +416 -395
  132. package/src/runtime/dsh-capabilities.ts +19 -0
  133. package/src/runtime/workspace-core.ts +171 -88
  134. package/src/runtime/workspace-migration.ts +472 -0
  135. package/src/ssh/connection/lease.ts +35 -0
  136. package/src/ssh/connection/manager.ts +1083 -0
  137. package/src/ssh/connection/pool.ts +341 -275
  138. package/src/ssh/engine.ts +269 -1477
  139. package/src/ssh/known-hosts.ts +42 -18
  140. package/src/ssh/local-transfer-policy.ts +83 -0
  141. package/src/ssh/plugin.ts +20 -19
  142. package/src/ssh/protocol.ts +22 -1
  143. package/src/ssh/routes.ts +419 -187
  144. package/src/ssh/sftp/service.ts +967 -0
  145. package/src/ssh/store.ts +169 -72
  146. package/src/ssh/terminal/service.ts +177 -0
  147. package/src/ssh/tools.ts +42 -20
  148. package/src/ssh/tunnel/service.ts +217 -0
  149. package/src/ssh/vault.ts +245 -91
  150. package/src/subprocess.ts +88 -71
  151. package/src/switch/switch-fs.ts +141 -10
  152. package/src/switch/switch-subprocess.ts +80 -6
  153. package/src/tools.ts +217 -221
  154. package/src/workspace-tool-ops.ts +101 -0
  155. package/src/workspace.ts +52 -0
  156. package/lib/environment-BL1jddfB.js +0 -449
  157. package/lib/switch-fs-CAJpFY9C.js +0 -193
  158. package/lib/switch-fs-RrZtG2gv.js +0 -210
  159. package/lib/types/client/manager-button.d.ts +0 -32
  160. package/lib/types/client/ssh/mount.d.ts +0 -13
  161. package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
  162. package/lib/types/client/ssh/panel/controller.d.ts +0 -23
  163. package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
  164. package/lib/types/client/workspace-gate.d.ts +0 -15
  165. package/lib/types/remote-runner.d.ts +0 -83
  166. package/lib/types/seam-state.d.ts +0 -69
  167. package/src/client/manager-button.tsx +0 -269
  168. package/src/client/ssh/mount.tsx +0 -83
  169. package/src/client/ssh/panel/HostsTab.tsx +0 -236
  170. package/src/client/ssh/panel/controller.ts +0 -48
  171. package/src/client/ssh/sidebar-entry.ts +0 -123
  172. package/src/client/workspace-gate.ts +0 -225
  173. package/src/remote-runner.ts +0 -201
  174. 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 { SshHostSummary, TunnelInfo } from '../../../ssh/protocol.ts'
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 === '' ? undefined : 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
- <select className={css.input} value={alias} onChange={event => { setAlias(event.target.value) }}>
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 || alias === '' || remotePort.trim() === ''} onClick={() => { void start() }}>{tt('tunnel.start')}</button>
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 panel styles. Scoped by the plugin's own data attributes so nothing
3
- * leaks into the rest of the GUI; colors ride the dsh --dsw-* tokens so the
4
- * panel follows the active theme (light/dark and skins). The center-column
5
- * hide rules are attribute-scoped and must stay in this stylesheet (it is
6
- * imported by mount.tsx, so the styles load with the plugin).
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: #000000;
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: #000000;
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: #000000;
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: #000;
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: #1f2229;
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
+ }
@@ -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 title annotations.
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