@goodandready/dsh-remote-workspace 0.3.8 → 0.3.9

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/lib/client.js CHANGED
@@ -77,10 +77,10 @@ window.__ModuleLoader__.load({
77
77
  .drw-tab-btn.active{background:var(--dsw-alias-bg-layer-4);border-color:var(--dsw-alias-state-brand-primary);color:var(--dsw-alias-state-brand-primary)}
78
78
 
79
79
  .drw-term-window{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;display:flex;flex-direction:column;height:340px;overflow:hidden;box-shadow:0 4px 14px var(--dsw-alias-border-l1)}
80
- .drw-term-header{background:var(--dsw-alias-bg-layer-2);padding:6px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:12px;color:var(--dsw-alias-label-secondary);font-family:monospace}
81
- .drw-term-body{flex:1;padding:10px 12px;overflow-y:auto;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12.5px;line-height:1.45;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-all}
80
+ .drw-term-header{background:var(--dsw-alias-bg-layer-2);padding:6px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:12px;color:var(--dsw-alias-label-secondary);font-family:var(--drw-terminal-font,monospace)}
81
+ .drw-term-body{flex:1;padding:10px 12px;overflow-y:auto;font-family:var(--drw-terminal-font,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:12.5px;line-height:1.45;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-all}
82
82
  .drw-term-footer{display:flex;background:var(--dsw-alias-bg-layer-2);border-top:1px solid var(--dsw-alias-border-l2);padding:6px}
83
- .drw-term-input{flex:1;background:transparent;border:none;color:var(--dsw-alias-link-primary, var(--dsw-alias-label-primary));font-family:monospace;font-size:13px;padding:4px 8px;outline:none}
83
+ .drw-term-input{flex:1;background:transparent;border:none;color:var(--dsw-alias-link-primary, var(--dsw-alias-label-primary));font-family:var(--drw-terminal-font,monospace);font-size:13px;padding:4px 8px;outline:none}
84
84
 
85
85
  .drw-explorer-box{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:12px;display:flex;flex-direction:column;gap:10px}
86
86
  .drw-explorer-nav{display:flex;align-items:center;gap:8px;font-family:monospace;font-size:12px;background:var(--dsw-alias-bg-layer-1);padding:6px 10px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2)}
@@ -109,12 +109,32 @@ window.__ModuleLoader__.load({
109
109
  .drw-alert-warning{background:var(--dsw-alias-state-warning-bg, var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-warning-primary);border:1px solid var(--dsw-alias-state-warning-border, var(--dsw-alias-border-l2))}
110
110
 
111
111
 
112
+ .drw-ws{min-height:100%;box-sizing:border-box;color:var(--dsw-alias-label-primary)}
113
+ .drw-ws-bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;padding:12px 16px;position:sticky;top:0;background:var(--dsw-alias-bg-base,#171717);z-index:2}
114
+ .drw-ws-panel{padding:0 16px 24px}
115
+ .drw-ws-panel[hidden]{display:none!important}
116
+ [data-pane=conversation],[class*=centerCol]{position:relative!important}
117
+ [data-drw-view]{display:none;position:absolute;inset:0;z-index:60;overflow:auto;background:var(--dsw-alias-bg-base,#171717)}
118
+ html[data-drw-active] [data-drw-view]{display:block!important}
119
+ html[data-drw-active] [data-pane=conversation] > *:not([data-drw-view]),html[data-drw-active] [class*=centerCol] > *:not([data-drw-view]){display:none!important}
120
+ .drw-entry-clone{display:flex;align-items:center;gap:8px}
121
+ .drw-entry-icon{display:flex;align-items:center;justify-content:center;width:16px;height:16px;flex:none}
122
+ [data-dsh-frame][data-sidebar-collapsed] .drw-entry-label{display:none}
123
+
112
124
  `
113
125
  document.head.appendChild(style)
114
126
  }
115
127
 
116
128
  const en = {
117
129
  title: 'Remote Workspace',
130
+ navLabel: 'Remote',
131
+ wsBack: 'Back',
132
+ wsHosts: 'Hosts',
133
+ wsTerminal: 'Terminal',
134
+ wsFiles: 'Files',
135
+ wsDocker: 'Containers',
136
+ wsTunnels: 'Tunnels',
137
+ wsCluster: 'Cluster',
118
138
  subtitle: 'Native SSH connections, SFTP file access, 3-way mirror sync, and port forwarding tunnels.',
119
139
  status: 'Ready',
120
140
  actionFailed: 'The request failed.',
@@ -141,6 +161,11 @@ window.__ModuleLoader__.load({
141
161
 
142
162
  tabEnv: '🗃️ .env Manager',
143
163
  btnImportSsh: 'Import ~/.ssh/config',
164
+ importAdded: 'Imported {count} hosts.',
165
+ skipWildcard: 'pattern is not a single host',
166
+ skipMatch: 'Match blocks are not imported',
167
+ skipDuplicate: 'already imported',
168
+ skipMissing: 'included file was not found',
144
169
  envTitle: 'Remote Environment Variables (.env)',
145
170
  envKey: 'Key',
146
171
  envVal: 'Value',
@@ -225,6 +250,47 @@ window.__ModuleLoader__.load({
225
250
  fAuthType: 'Authentication Method',
226
251
  fAuthKey: 'SSH Private Key',
227
252
  fAuthPass: 'Password',
253
+ fEnvironment: 'Environment',
254
+ fLocation: 'Location',
255
+ fTags: 'Tags',
256
+ fDescription: 'Description',
257
+ groupFlat: 'Flat',
258
+ groupEnvironment: 'By environment',
259
+ groupTag: 'By tag',
260
+ groupUngrouped: 'Ungrouped',
261
+ testGroup: 'Test group',
262
+ groupOnline: 'online, {latency} ms',
263
+ groupOffline: 'offline: {err}',
264
+ tabProfiles: 'Hosts',
265
+ tabCluster: 'Cluster',
266
+ tabFiles: 'Files',
267
+ btnUpload: 'Upload',
268
+ btnDownload: 'Download',
269
+ transferProgress: '{name}: {percent}%',
270
+ transferFailed: 'Transfer failed: {name}',
271
+ transferCancel: 'Cancel',
272
+ clusterTitle: 'Run on a group',
273
+ clusterCommand: 'Command',
274
+ clusterRun: 'Run',
275
+ clusterRunning: 'Running...',
276
+ clusterCount: '{count} hosts configured',
277
+ clusterOk: 'ok',
278
+ clusterFail: 'failed',
279
+ fAuthAgent: 'SSH Agent',
280
+ authPromptTitle: 'Sign-in code',
281
+ authPromptCode: 'One-time code',
282
+ authPromptSubmit: 'Continue',
283
+ fAuthKeyShort: 'key',
284
+ fAuthPassShort: 'pwd',
285
+ fAuthAgentShort: 'agent',
286
+ fAgentPath: 'Agent socket',
287
+ fAgentPathHint: 'Empty uses SSH_AUTH_SOCK. Use pageant on Windows.',
288
+ fJumpHosts: 'Jump hosts',
289
+ fJumpHostsHint: 'Profile ids in order, separated by commas. The first host is the entry bastion.',
290
+ fProxyCommand: 'Proxy command',
291
+ fProxyCommandHint: 'OpenSSH ProxyCommand. Tokens: %h host, %p port, %r user, %n profile name.',
292
+ fTerminalFont: 'Terminal font',
293
+ fTerminalFontHint: 'Font family for the terminal. Leave empty to keep the default monospace stack.',
228
294
  fKeyPath: 'Private Key Path',
229
295
  fKeyPathHint: 'Path to ~/.ssh/id_rsa or /home/user/.ssh/id_ed25519',
230
296
  fPassphrase: 'Key Passphrase (optional)',
@@ -271,6 +337,14 @@ window.__ModuleLoader__.load({
271
337
 
272
338
  const zh = {
273
339
  title: '远程开发工作区',
340
+ navLabel: '远程',
341
+ wsBack: '返回',
342
+ wsHosts: '主机',
343
+ wsTerminal: '终端',
344
+ wsFiles: '文件',
345
+ wsDocker: '容器',
346
+ wsTunnels: '隧道',
347
+ wsCluster: '集群',
274
348
  subtitle: '支持原生 SSH 连接、SFTP 文件访问、3-way 镜像同步与端口转发隧道。',
275
349
  status: '就绪',
276
350
  actionFailed: '请求失败。',
@@ -284,6 +358,11 @@ window.__ModuleLoader__.load({
284
358
  tabContainers: '🐳 容器管理',
285
359
  tabEnv: '🗃️ 环境变量',
286
360
  btnImportSsh: '导入 ~/.ssh/config',
361
+ importAdded: '已导入 {count} 台主机。',
362
+ skipWildcard: '通配主机不会导入',
363
+ skipMatch: '不导入 Match 块',
364
+ skipDuplicate: '已经导入',
365
+ skipMissing: '找不到被包含的文件',
287
366
  envTitle: '远程环境变量配置 (.env)',
288
367
  envKey: '变量名',
289
368
  envVal: '变量值',
@@ -379,6 +458,47 @@ window.__ModuleLoader__.load({
379
458
  fAuthType: '身份认证方式',
380
459
  fAuthKey: 'SSH 私钥',
381
460
  fAuthPass: '账户密码',
461
+ fEnvironment: '环境',
462
+ fLocation: '位置',
463
+ fTags: '标签',
464
+ fDescription: '说明',
465
+ groupFlat: '平铺',
466
+ groupEnvironment: '按环境',
467
+ groupTag: '按标签',
468
+ groupUngrouped: '未分组',
469
+ testGroup: '测试本组',
470
+ groupOnline: '在线,{latency} 毫秒',
471
+ groupOffline: '离线:{err}',
472
+ tabProfiles: '主机',
473
+ tabCluster: '集群',
474
+ tabFiles: '文件',
475
+ btnUpload: '上传',
476
+ btnDownload: '下载',
477
+ transferProgress: '{name}:{percent}%',
478
+ transferFailed: '传输失败:{name}',
479
+ transferCancel: '取消',
480
+ clusterTitle: '在一组主机上运行',
481
+ clusterCommand: '命令',
482
+ clusterRun: '运行',
483
+ clusterRunning: '正在运行...',
484
+ clusterCount: '已配置 {count} 台主机',
485
+ clusterOk: '成功',
486
+ clusterFail: '失败',
487
+ fAuthAgent: 'SSH 代理',
488
+ authPromptTitle: '登录验证码',
489
+ authPromptCode: '一次性验证码',
490
+ authPromptSubmit: '继续',
491
+ fAuthKeyShort: '密钥',
492
+ fAuthPassShort: '密码',
493
+ fAuthAgentShort: '代理',
494
+ fAgentPath: '代理套接字',
495
+ fAgentPathHint: '留空则使用 SSH_AUTH_SOCK。Windows 上填写 pageant。',
496
+ fJumpHosts: '跳板主机',
497
+ fJumpHostsHint: '按经过顺序填写配置编号,用逗号分隔。第一台是入口堡垒机。',
498
+ fProxyCommand: '代理命令',
499
+ fProxyCommandHint: 'OpenSSH ProxyCommand。令牌:%h 主机,%p 端口,%r 用户,%n 配置名称。',
500
+ fTerminalFont: '终端字体',
501
+ fTerminalFontHint: '终端使用的字体族。留空则使用默认等宽字体。',
382
502
  fKeyPath: '私钥文件路径',
383
503
  fKeyPathHint: '例如 ~/.ssh/id_rsa 或 /home/user/.ssh/id_ed25519',
384
504
  fPassphrase: '私钥口令 (可选)',
@@ -717,6 +837,13 @@ window.__ModuleLoader__.load({
717
837
 
718
838
  // --- Sub-component: Web Terminal Tab ---
719
839
 
840
+ function applyTerminalFont(font) {
841
+ if (typeof document === 'undefined') return
842
+ const value = String(font || '').trim()
843
+ if (value) document.documentElement.style.setProperty('--drw-terminal-font', value)
844
+ else document.documentElement.style.removeProperty('--drw-terminal-font')
845
+ }
846
+
720
847
  function TerminalTab(props) {
721
848
  const { activeProfile, t } = props
722
849
  const [connected, setConnected] = React.useState(false)
@@ -902,7 +1029,7 @@ window.__ModuleLoader__.load({
902
1029
  ? React.createElement(
903
1030
  'form',
904
1031
  { className: 'drw-term-footer', onSubmit: sendCommand },
905
- React.createElement('span', { style: { color: 'var(--dsw-alias-link-primary)', padding: '4px 6px', fontFamily: 'monospace' } }, '$'),
1032
+ React.createElement('span', { style: { color: 'var(--dsw-alias-link-primary)', padding: '4px 6px', fontFamily: 'inherit' } }, '$'),
906
1033
  React.createElement('input', {
907
1034
  className: 'drw-term-input',
908
1035
  type: 'text',
@@ -933,6 +1060,8 @@ window.__ModuleLoader__.load({
933
1060
  const [editContent, setEditContent] = React.useState('')
934
1061
  const [previewLoading, setPreviewLoading] = React.useState(false)
935
1062
  const [saveStatus, setSaveStatus] = React.useState(null)
1063
+ const [transfer, setTransfer] = React.useState(null)
1064
+ const transferAbort = React.useRef(null)
936
1065
 
937
1066
  const loadDir = async (dirPath) => {
938
1067
  if (!activeProfile) return
@@ -990,6 +1119,75 @@ window.__ModuleLoader__.load({
990
1119
  setPreviewLoading(false)
991
1120
  }
992
1121
 
1122
+
1123
+ const filePathFor = (entry) => currentPath.endsWith('/') ? currentPath + entry.filename : currentPath + '/' + entry.filename
1124
+
1125
+ const downloadFile = async (entry, event) => {
1126
+ if (event) event.stopPropagation()
1127
+ if (!activeProfile) return
1128
+ const ctrl = new AbortController()
1129
+ transferAbort.current = () => ctrl.abort()
1130
+ setTransfer({ name: entry.filename, percent: 0 })
1131
+ try {
1132
+ const params = new URLSearchParams({ profileId: activeProfile.id, filePath: filePathFor(entry) })
1133
+ const res = await fetch('/dsh-remote-workspace/file/download?' + params.toString(), { signal: ctrl.signal })
1134
+ if (!res.ok || !res.body) {
1135
+ setTransfer({ name: entry.filename, error: true })
1136
+ return
1137
+ }
1138
+ const total = Number(res.headers.get('content-length')) || 0
1139
+ const reader = res.body.getReader()
1140
+ const chunks = []
1141
+ let loaded = 0
1142
+ while (true) {
1143
+ const step = await reader.read()
1144
+ if (step.done) break
1145
+ chunks.push(step.value)
1146
+ loaded += step.value.length
1147
+ setTransfer({ name: entry.filename, percent: total ? Math.min(100, Math.round((loaded / total) * 100)) : 0 })
1148
+ }
1149
+ const blob = new Blob(chunks)
1150
+ const link = document.createElement('a')
1151
+ link.href = URL.createObjectURL(blob)
1152
+ link.download = entry.filename
1153
+ link.click()
1154
+ URL.revokeObjectURL(link.href)
1155
+ setTransfer(null)
1156
+ } catch (err) {
1157
+ setTransfer({ name: entry.filename, error: err && err.name === 'AbortError' })
1158
+ }
1159
+ }
1160
+
1161
+ const uploadFile = () => {
1162
+ if (!activeProfile) return
1163
+ const input = document.createElement('input')
1164
+ input.type = 'file'
1165
+ input.onchange = () => {
1166
+ const file = input.files && input.files[0]
1167
+ if (!file) return
1168
+ const remotePath = currentPath.endsWith('/') ? currentPath + file.name : currentPath + '/' + file.name
1169
+ const xhr = new XMLHttpRequest()
1170
+ const params = new URLSearchParams({ profileId: activeProfile.id, filePath: remotePath })
1171
+ xhr.open('POST', '/dsh-remote-workspace/file/upload?' + params.toString())
1172
+ transferAbort.current = () => xhr.abort()
1173
+ setTransfer({ name: file.name, percent: 0 })
1174
+ xhr.upload.onprogress = (event) => {
1175
+ if (event.lengthComputable) {
1176
+ setTransfer({ name: file.name, percent: Math.min(100, Math.round((event.loaded / event.total) * 100)) })
1177
+ }
1178
+ }
1179
+ xhr.onload = () => {
1180
+ setTransfer(null)
1181
+ if (xhr.status >= 200 && xhr.status < 300) loadDir(currentPath)
1182
+ else setTransfer({ name: file.name, error: true })
1183
+ }
1184
+ xhr.onerror = () => setTransfer({ name: file.name, error: true })
1185
+ xhr.onabort = () => setTransfer({ name: file.name, error: true })
1186
+ xhr.send(file)
1187
+ }
1188
+ input.click()
1189
+ }
1190
+
993
1191
  const handleSaveFile = async () => {
994
1192
  if (!previewFile || !activeProfile) return
995
1193
  setSaveStatus('saving')
@@ -1027,8 +1225,15 @@ window.__ModuleLoader__.load({
1027
1225
  disabled: loading
1028
1226
  },
1029
1227
  loading ? 'Refreshing...' : '🔄 Refresh'
1030
- )
1228
+ ),
1229
+ React.createElement('button', { type: 'button', className: 'drw-btn', onClick: uploadFile }, t('btnUpload'))
1031
1230
  ),
1231
+ transfer
1232
+ ? React.createElement('div', { className: 'drw-hint', role: 'status' },
1233
+ transfer.error ? t('transferFailed', { name: transfer.name }) : t('transferProgress', { name: transfer.name, percent: transfer.percent || 0 }),
1234
+ React.createElement('button', { type: 'button', className: 'drw-btn', onClick: () => transferAbort.current && transferAbort.current() }, t('transferCancel'))
1235
+ )
1236
+ : null,
1032
1237
  React.createElement(
1033
1238
  'div',
1034
1239
  { className: 'drw-explorer-nav' },
@@ -1069,7 +1274,8 @@ window.__ModuleLoader__.load({
1069
1274
  'div',
1070
1275
  { className: 'drw-file-meta' },
1071
1276
  e.size !== undefined ? React.createElement('span', null, `${Math.round(e.size / 1024)} KB`) : null,
1072
- e.permissions ? React.createElement('span', { style: { opacity: 0.6 } }, e.permissions) : null
1277
+ e.permissions ? React.createElement('span', { style: { opacity: 0.6 } }, e.permissions) : null,
1278
+ e.isDirectory ? null : React.createElement('button', { type: 'button', className: 'drw-btn', onClick: (event) => downloadFile(e, event) }, t('btnDownload'))
1073
1279
  )
1074
1280
  )
1075
1281
  )
@@ -1158,6 +1364,31 @@ window.__ModuleLoader__.load({
1158
1364
  )
1159
1365
  }
1160
1366
 
1367
+
1368
+ function groupProfiles(list, mode) {
1369
+ const profiles = Array.isArray(list) ? list : []
1370
+ const bucket = (map, key, profile) => {
1371
+ const name = key || 'ungrouped'
1372
+ if (!map.has(name)) map.set(name, [])
1373
+ map.get(name).push(profile)
1374
+ }
1375
+ if (mode === 'environment') {
1376
+ const map = new Map()
1377
+ profiles.forEach((profile) => bucket(map, String(profile.environment || '').trim(), profile))
1378
+ return Array.from(map.entries()).map(([key, items]) => ({ key, items }))
1379
+ }
1380
+ if (mode === 'tag') {
1381
+ const map = new Map()
1382
+ profiles.forEach((profile) => {
1383
+ const tags = Array.isArray(profile.tags) ? profile.tags.map((item) => String(item).trim()).filter(Boolean) : []
1384
+ if (!tags.length) bucket(map, '', profile)
1385
+ else tags.forEach((tag) => bucket(map, tag, profile))
1386
+ })
1387
+ return Array.from(map.entries()).map(([key, items]) => ({ key, items }))
1388
+ }
1389
+ return [{ key: 'all', items: profiles }]
1390
+ }
1391
+
1161
1392
  function ProfilesPanel(panel) {
1162
1393
  const {
1163
1394
  t,
@@ -1182,6 +1413,22 @@ window.__ModuleLoader__.load({
1182
1413
  showPassword,
1183
1414
  setShowPassword,
1184
1415
  } = panel
1416
+ const [groupMode, setGroupMode] = React.useState('flat')
1417
+ const [groupReport, setGroupReport] = React.useState([])
1418
+ const sections = groupProfiles(profiles, groupMode)
1419
+ const testGroup = async (items) => {
1420
+ try {
1421
+ const res = await fetch('/dsh-remote-workspace/profiles/test-group', {
1422
+ method: 'POST',
1423
+ headers: { 'Content-Type': 'application/json' },
1424
+ body: JSON.stringify({ ids: items.map((item) => item.id) })
1425
+ })
1426
+ const data = await res.json()
1427
+ if (data && data.ok) setGroupReport(data.results || [])
1428
+ } catch (err) {
1429
+ setGroupReport([])
1430
+ }
1431
+ }
1185
1432
  return (
1186
1433
  React.createElement(
1187
1434
  'div',
@@ -1208,7 +1455,11 @@ window.__ModuleLoader__.load({
1208
1455
  passphrase: '',
1209
1456
  password: '',
1210
1457
  remoteWorkspace: '',
1211
- localMirrorPath: ''
1458
+ localMirrorPath: '',
1459
+ environment: '',
1460
+ tags: [],
1461
+ location: '',
1462
+ description: ''
1212
1463
  })
1213
1464
  setTestResult(null)
1214
1465
  }
@@ -1286,6 +1537,62 @@ window.__ModuleLoader__.load({
1286
1537
  )
1287
1538
  ),
1288
1539
 
1540
+ React.createElement(
1541
+ 'div',
1542
+ { className: 'drw-field' },
1543
+ React.createElement('span', { className: 'drw-label' }, t('fJumpHosts')),
1544
+ React.createElement('input', {
1545
+ className: 'drw-input',
1546
+ placeholder: 'bastion-a, bastion-b',
1547
+ value: editing.jumpHostId || ((editing.jumpHosts || []).join(', ')),
1548
+ onChange: (e) => {
1549
+ const raw = e.target.value;
1550
+ const jumpHosts = raw.split(',').map((item) => item.trim()).filter(Boolean);
1551
+ setEditing({ ...editing, jumpHostId: raw, jumpHosts });
1552
+ }
1553
+ }),
1554
+ React.createElement('span', { className: 'drw-hint' }, t('fJumpHostsHint'))
1555
+ ),
1556
+
1557
+ React.createElement(
1558
+ 'div',
1559
+ { className: 'drw-field' },
1560
+ React.createElement('span', { className: 'drw-label' }, t('fProxyCommand')),
1561
+ React.createElement('input', {
1562
+ className: 'drw-input',
1563
+ placeholder: 'cloudflared access ssh --hostname %h',
1564
+ value: editing.proxyCommand || '',
1565
+ onChange: (e) => setEditing({ ...editing, proxyCommand: e.target.value })
1566
+ }),
1567
+ React.createElement('span', { className: 'drw-hint' }, t('fProxyCommandHint'))
1568
+ ),
1569
+
1570
+
1571
+ React.createElement(
1572
+ 'div',
1573
+ { className: 'drw-grid-2' },
1574
+ React.createElement('div', { className: 'drw-field' },
1575
+ React.createElement('span', { className: 'drw-label' }, t('fEnvironment')),
1576
+ React.createElement('input', { className: 'drw-input', value: editing.environment || '', onChange: (e) => setEditing({ ...editing, environment: e.target.value }) })
1577
+ ),
1578
+ React.createElement('div', { className: 'drw-field' },
1579
+ React.createElement('span', { className: 'drw-label' }, t('fLocation')),
1580
+ React.createElement('input', { className: 'drw-input', value: editing.location || '', onChange: (e) => setEditing({ ...editing, location: e.target.value }) })
1581
+ ),
1582
+ React.createElement('div', { className: 'drw-field' },
1583
+ React.createElement('span', { className: 'drw-label' }, t('fTags')),
1584
+ React.createElement('input', {
1585
+ className: 'drw-input',
1586
+ value: Array.isArray(editing.tags) ? editing.tags.join(', ') : (editing.tags || ''),
1587
+ onChange: (e) => setEditing({ ...editing, tags: e.target.value.split(',').map((item) => item.trim()).filter(Boolean) })
1588
+ })
1589
+ ),
1590
+ React.createElement('div', { className: 'drw-field' },
1591
+ React.createElement('span', { className: 'drw-label' }, t('fDescription')),
1592
+ React.createElement('input', { className: 'drw-input', value: editing.description || '', onChange: (e) => setEditing({ ...editing, description: e.target.value }) })
1593
+ )
1594
+ ),
1595
+
1289
1596
  // Auth Method Segmented Switcher (Key vs Password)
1290
1597
  React.createElement(
1291
1598
  'div',
@@ -1298,11 +1605,20 @@ window.__ModuleLoader__.load({
1298
1605
  'button',
1299
1606
  {
1300
1607
  type: 'button',
1301
- className: `drw-segmented-item ${editing.authType !== 'password' ? 'drw-segmented-item-active' : ''}`,
1608
+ className: `drw-segmented-item ${editing.authType !== 'password' && editing.authType !== 'agent' ? 'drw-segmented-item-active' : ''}`,
1302
1609
  onClick: () => setEditing({ ...editing, authType: 'key' })
1303
1610
  },
1304
1611
  '🔑 ' + t('fAuthKey')
1305
1612
  ),
1613
+ React.createElement(
1614
+ 'button',
1615
+ {
1616
+ type: 'button',
1617
+ className: `drw-segmented-item ${editing.authType === 'agent' ? 'drw-segmented-item-active' : ''}`,
1618
+ onClick: () => setEditing({ ...editing, authType: 'agent' })
1619
+ },
1620
+ t('fAuthAgent')
1621
+ ),
1306
1622
  React.createElement(
1307
1623
  'button',
1308
1624
  {
@@ -1316,7 +1632,19 @@ window.__ModuleLoader__.load({
1316
1632
  ),
1317
1633
 
1318
1634
  // Conditional Auth Inputs
1319
- editing.authType === 'password'
1635
+ editing.authType === 'agent'
1636
+ ? React.createElement(
1637
+ 'div',
1638
+ { className: 'drw-field' },
1639
+ React.createElement('span', { className: 'drw-label' }, t('fAgentPath')),
1640
+ React.createElement('input', {
1641
+ className: 'drw-input',
1642
+ placeholder: t('fAgentPathHint'),
1643
+ value: editing.agentPath || '',
1644
+ onChange: (e) => setEditing({ ...editing, agentPath: e.target.value })
1645
+ })
1646
+ )
1647
+ : editing.authType === 'password'
1320
1648
  ? React.createElement(
1321
1649
  'div',
1322
1650
  { className: 'drw-field' },
@@ -1551,6 +1879,23 @@ window.__ModuleLoader__.load({
1551
1879
  )
1552
1880
  : null,
1553
1881
 
1882
+
1883
+ React.createElement('div', { style: { display: 'flex', gap: '8px', marginBottom: '8px' } },
1884
+ ['flat', 'environment', 'tag'].map((mode) => React.createElement('button', {
1885
+ key: mode,
1886
+ type: 'button',
1887
+ className: groupMode === mode ? 'drw-btn drw-btn-primary' : 'drw-btn',
1888
+ onClick: () => setGroupMode(mode)
1889
+ }, t(mode === 'flat' ? 'groupFlat' : mode === 'environment' ? 'groupEnvironment' : 'groupTag')))
1890
+ ),
1891
+ groupReport.length
1892
+ ? React.createElement('div', { className: 'drw-hint', role: 'status' },
1893
+ groupReport.map((item) => React.createElement('div', { key: item.id + (item.name || '') },
1894
+ (item.name || item.id) + ': ' + (item.ok ? t('groupOnline', { latency: item.latencyMs || 0 }) : t('groupOffline', { err: item.error || '' }))
1895
+ ))
1896
+ )
1897
+ : null,
1898
+
1554
1899
  // Profiles List Table
1555
1900
  profiles.length > 0
1556
1901
  ? React.createElement(
@@ -1572,7 +1917,17 @@ window.__ModuleLoader__.load({
1572
1917
  React.createElement(
1573
1918
  'tbody',
1574
1919
  null,
1575
- profiles.map((p) => {
1920
+ sections.flatMap((section) => {
1921
+ const header = groupMode === 'flat' ? [] : [
1922
+ React.createElement('tr', { key: 'group-' + section.key },
1923
+ React.createElement('td', { colSpan: 5 },
1924
+ React.createElement('strong', null, section.key === 'ungrouped' ? t('groupUngrouped') : section.key),
1925
+ ' ',
1926
+ React.createElement('button', { type: 'button', className: 'drw-btn', onClick: () => testGroup(section.items) }, t('testGroup'))
1927
+ )
1928
+ )
1929
+ ]
1930
+ return header.concat(section.items.map((p) => {
1576
1931
  const isAct = p.id === activeId
1577
1932
  return React.createElement(
1578
1933
  'tr',
@@ -1584,7 +1939,7 @@ window.__ModuleLoader__.load({
1584
1939
  React.createElement(
1585
1940
  'span',
1586
1941
  { className: 'drw-badge', style: { marginLeft: '8px' } },
1587
- p.authType === 'password' ? '🔒 pwd' : '🔑 key'
1942
+ p.authType === 'password' ? '🔒 ' + t('fAuthPassShort') : p.authType === 'agent' ? t('fAuthAgentShort') : '🔑 ' + t('fAuthKeyShort')
1588
1943
  ),
1589
1944
  isAct
1590
1945
  ? React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginLeft: '6px' } }, t('activeBadge'))
@@ -1637,6 +1992,7 @@ window.__ModuleLoader__.load({
1637
1992
  )
1638
1993
  )
1639
1994
  )
1995
+ }))
1640
1996
  })
1641
1997
  )
1642
1998
  )
@@ -1873,6 +2229,57 @@ window.__ModuleLoader__.load({
1873
2229
  )
1874
2230
  }
1875
2231
 
2232
+ function ClusterPanel(props) {
2233
+ const t = props.t
2234
+ const profiles = props.profiles || []
2235
+ const [command, setCommand] = React.useState('')
2236
+ const [environment, setEnvironment] = React.useState('')
2237
+ const [tag, setTag] = React.useState('')
2238
+ const [rows, setRows] = React.useState([])
2239
+ const [open, setOpen] = React.useState('')
2240
+ const [running, setRunning] = React.useState(false)
2241
+ const run = async () => {
2242
+ setRunning(true)
2243
+ try {
2244
+ const res = await fetch('/dsh-remote-workspace/cluster', {
2245
+ method: 'POST',
2246
+ headers: { 'Content-Type': 'application/json' },
2247
+ body: JSON.stringify({
2248
+ command,
2249
+ environment,
2250
+ tags: tag ? [tag] : [],
2251
+ maxWorkers: 8
2252
+ })
2253
+ })
2254
+ const data = await res.json()
2255
+ setRows(data && data.results ? data.results : [])
2256
+ } catch (err) {
2257
+ setRows([])
2258
+ }
2259
+ setRunning(false)
2260
+ }
2261
+ return React.createElement('div', { className: 'drw-card' },
2262
+ React.createElement('div', { className: 'drw-card-title' }, t('clusterTitle')),
2263
+ React.createElement('div', { className: 'drw-grid-2' },
2264
+ React.createElement('input', { className: 'drw-input', placeholder: t('fEnvironment'), value: environment, onChange: (e) => setEnvironment(e.target.value) }),
2265
+ React.createElement('input', { className: 'drw-input', placeholder: t('fTags'), value: tag, onChange: (e) => setTag(e.target.value) })
2266
+ ),
2267
+ React.createElement('div', { style: { display: 'flex', gap: '8px', marginTop: '8px' } },
2268
+ React.createElement('input', { className: 'drw-input', placeholder: t('clusterCommand'), value: command, onChange: (e) => setCommand(e.target.value) }),
2269
+ React.createElement('button', { type: 'button', className: 'drw-btn drw-btn-primary', disabled: running || !command.trim(), onClick: run }, running ? t('clusterRunning') : t('clusterRun'))
2270
+ ),
2271
+ React.createElement('div', { className: 'drw-hint' }, t('clusterCount', { count: profiles.length })),
2272
+ rows.map((row, index) => React.createElement('div', { key: (row.alias || '') + index, className: 'drw-field' },
2273
+ React.createElement('button', { type: 'button', className: 'drw-btn', onClick: () => setOpen(open === String(index) ? '' : String(index)) },
2274
+ (row.alias || row.host) + ' ' + (row.success ? t('clusterOk') : t('clusterFail')) + ' ' + (row.durationMs || 0) + ' ms'
2275
+ ),
2276
+ open === String(index)
2277
+ ? React.createElement('pre', { className: 'drw-term-body' }, row.error || row.stdout || row.stderr || '')
2278
+ : null
2279
+ ))
2280
+ )
2281
+ }
2282
+
1876
2283
  function SettingsView(props) {
1877
2284
  const [activeTab, setActiveTab] = React.useState('profiles')
1878
2285
  const [healthData, setHealthData] = React.useState(null)
@@ -1886,9 +2293,13 @@ window.__ModuleLoader__.load({
1886
2293
  const [busy, setBusy] = React.useState(null)
1887
2294
  const [testResult, setTestResult] = React.useState(null)
1888
2295
  const [actionError, setActionError] = React.useState('')
2296
+ const [authPrompts, setAuthPrompts] = React.useState([])
2297
+ const [authAnswers, setAuthAnswers] = React.useState({})
2298
+ const [importReport, setImportReport] = React.useState(null)
1889
2299
  const [syncMsg, setSyncMsg] = React.useState(null)
1890
2300
  const [tunnelMsg, setTunnelMsg] = React.useState(null)
1891
2301
  const [showPassword, setShowPassword] = React.useState(false)
2302
+ const [terminalFont, setTerminalFont] = React.useState('')
1892
2303
 
1893
2304
  // Tunnel creation inputs
1894
2305
  const [newTunnelLocal, setNewTunnelLocal] = React.useState('3000')
@@ -1915,7 +2326,12 @@ window.__ModuleLoader__.load({
1915
2326
  return text ? null : data
1916
2327
  }
1917
2328
 
1918
- const loadState = async () => {
2329
+ const loadInFlight = React.useRef(false)
2330
+
2331
+ const loadState = async (force) => {
2332
+ const hidden = typeof document !== 'undefined' && document.hidden
2333
+ if (!force && (hidden || loadInFlight.current)) return
2334
+ loadInFlight.current = true
1919
2335
  try {
1920
2336
  const res = await fetch('/dsh-remote-workspace/state')
1921
2337
  const data = await reportAction(res)
@@ -1924,20 +2340,67 @@ window.__ModuleLoader__.load({
1924
2340
  setActiveId(data.activeId || null)
1925
2341
  setTunnels(data.tunnels || [])
1926
2342
  setAutoSync(Boolean(data.autoSync))
2343
+ setAuthPrompts(data.authPrompts || [])
2344
+ setTerminalFont(data.terminalFontFamily || '')
2345
+ applyTerminalFont(data.terminalFontFamily || '')
1927
2346
  if (data.activeId) {
1928
2347
  fetch(`/dsh-remote-workspace/health?profileId=${data.activeId}`).then(r => r.json()).then(h => { if (h.ok) setHealthData(h.health) }).catch(() => {})
1929
2348
  }
1930
2349
  }
1931
2350
  } catch (err) {
1932
2351
  setActionError(err && err.message ? err.message : t('actionFailed'))
2352
+ } finally {
2353
+ loadInFlight.current = false
1933
2354
  }
1934
2355
  }
1935
2356
 
1936
2357
  React.useEffect(() => {
1937
2358
  ensureCss()
1938
- loadState()
2359
+ loadState(true)
2360
+ const timer = setInterval(() => loadState(false), 2000)
2361
+ const onVisible = () => {
2362
+ if (typeof document !== 'undefined' && !document.hidden) loadState(true)
2363
+ }
2364
+ if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVisible)
2365
+ return () => {
2366
+ clearInterval(timer)
2367
+ if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVisible)
2368
+ }
1939
2369
  }, [])
1940
2370
 
2371
+
2372
+ const saveTerminalFont = async () => {
2373
+ try {
2374
+ const res = await fetch('/dsh-remote-workspace/terminal/font', {
2375
+ method: 'POST',
2376
+ headers: { 'Content-Type': 'application/json' },
2377
+ body: JSON.stringify({ fontFamily: terminalFont })
2378
+ })
2379
+ const data = await reportAction(res)
2380
+ if (data && data.ok) {
2381
+ setTerminalFont(data.terminalFontFamily || '')
2382
+ applyTerminalFont(data.terminalFontFamily || '')
2383
+ }
2384
+ } catch (err) {
2385
+ setActionError(err && err.message ? err.message : t('actionFailed'))
2386
+ }
2387
+ }
2388
+
2389
+ const submitAuth = async (prompt) => {
2390
+ const answers = (prompt.prompts || []).map((_, index) => authAnswers[prompt.profileId + ':' + index] || '')
2391
+ try {
2392
+ const res = await fetch('/dsh-remote-workspace/auth/keyboard', {
2393
+ method: 'POST',
2394
+ headers: { 'Content-Type': 'application/json' },
2395
+ body: JSON.stringify({ id: prompt.profileId, answers })
2396
+ })
2397
+ const data = await reportAction(res)
2398
+ if (data) await loadState()
2399
+ } catch (err) {
2400
+ setActionError(err && err.message ? err.message : t('actionFailed'))
2401
+ }
2402
+ }
2403
+
1941
2404
  const handleSave = async (profile) => {
1942
2405
  setBusy('saving')
1943
2406
  try {
@@ -2102,6 +2565,31 @@ window.__ModuleLoader__.load({
2102
2565
 
2103
2566
  const activeProfile = profiles.find((p) => p.id === activeId)
2104
2567
 
2568
+ const handleImport = async () => {
2569
+ try {
2570
+ const res = await fetch('/dsh-remote-workspace/profiles/import-ssh-config', {
2571
+ method: 'POST',
2572
+ headers: { 'Content-Type': 'application/json' },
2573
+ body: '{}'
2574
+ })
2575
+ const data = await reportAction(res)
2576
+ if (data) {
2577
+ setImportReport(data)
2578
+ await loadState()
2579
+ }
2580
+ } catch (err) {
2581
+ setActionError(err && err.message ? err.message : t('actionFailed'))
2582
+ }
2583
+ }
2584
+
2585
+ const skipReason = (reason) => {
2586
+ if (reason === 'wildcard') return t('skipWildcard')
2587
+ if (reason === 'match') return t('skipMatch')
2588
+ if (reason === 'duplicate') return t('skipDuplicate')
2589
+ if (reason === 'missing-include') return t('skipMissing')
2590
+ return reason
2591
+ }
2592
+
2105
2593
  return React.createElement(
2106
2594
  'div',
2107
2595
  { className: 'drw-page' },
@@ -2133,7 +2621,76 @@ window.__ModuleLoader__.load({
2133
2621
  ),
2134
2622
 
2135
2623
 
2136
- React.createElement(ProfilesPanel, { t, editing, setEditing, setTestResult, profiles, activeId, busy, handleSave, testResult, handleTest, handleSetActive, handleDelete, browserOpen, setBrowserOpen, browserPath, browserEntries, browserLoading, openDirectoryBrowser, fetchRemoteDir, showPassword, setShowPassword }),
2624
+
2625
+ authPrompts.length
2626
+ ? React.createElement(
2627
+ 'div',
2628
+ { className: 'drw-card', role: 'dialog', 'aria-label': t('authPromptTitle') },
2629
+ React.createElement('div', { className: 'drw-page-title' }, t('authPromptTitle')),
2630
+ authPrompts.map((prompt) => React.createElement(
2631
+ 'div',
2632
+ { key: prompt.profileId, className: 'drw-field' },
2633
+ prompt.instructions
2634
+ ? React.createElement('span', { className: 'drw-hint' }, prompt.instructions)
2635
+ : null,
2636
+ (prompt.prompts || []).map((field, index) => React.createElement('input', {
2637
+ key: index,
2638
+ className: 'drw-input',
2639
+ type: field.echo ? 'text' : 'password',
2640
+ placeholder: field.prompt || t('authPromptCode'),
2641
+ value: authAnswers[prompt.profileId + ':' + index] || '',
2642
+ onChange: (e) => setAuthAnswers({ ...authAnswers, [prompt.profileId + ':' + index]: e.target.value })
2643
+ })),
2644
+ React.createElement('button', {
2645
+ type: 'button',
2646
+ className: 'drw-btn drw-btn-primary',
2647
+ onClick: () => submitAuth(prompt)
2648
+ }, t('authPromptSubmit'))
2649
+ ))
2650
+ )
2651
+ : null,
2652
+
2653
+
2654
+ React.createElement(
2655
+ 'div',
2656
+ { className: 'drw-card' },
2657
+ React.createElement('button', { type: 'button', className: 'drw-btn', onClick: handleImport }, t('btnImportSsh')),
2658
+ importReport
2659
+ ? React.createElement(
2660
+ 'div',
2661
+ { className: 'drw-hint', role: 'status' },
2662
+ t('importAdded', { count: importReport.count || 0 }),
2663
+ (importReport.skipped || []).map((item, index) => React.createElement('div', { key: index }, (item.name || '') + ': ' + skipReason(item.reason)))
2664
+ )
2665
+ : null
2666
+ ),
2667
+
2668
+
2669
+ React.createElement('div', { className: 'drw-card' },
2670
+ React.createElement('label', { className: 'drw-label' }, t('fTerminalFont')),
2671
+ React.createElement('div', { style: { display: 'flex', gap: '8px' } },
2672
+ React.createElement('input', {
2673
+ className: 'drw-input',
2674
+ type: 'text',
2675
+ value: terminalFont,
2676
+ placeholder: 'ui-monospace, Consolas, monospace',
2677
+ onChange: (e) => setTerminalFont(e.target.value)
2678
+ }),
2679
+ React.createElement('button', { type: 'button', className: 'drw-btn', onClick: saveTerminalFont }, t('btnSave'))
2680
+ ),
2681
+ React.createElement('div', { className: 'drw-hint' }, t('fTerminalFontHint'))
2682
+ ),
2683
+
2684
+ React.createElement('div', { style: { display: 'flex', gap: '8px' } },
2685
+ React.createElement('button', { type: 'button', className: activeTab === 'profiles' ? 'drw-btn drw-btn-primary' : 'drw-btn', onClick: () => setActiveTab('profiles') }, t('tabProfiles')),
2686
+ React.createElement('button', { type: 'button', className: activeTab === 'cluster' ? 'drw-btn drw-btn-primary' : 'drw-btn', onClick: () => setActiveTab('cluster') }, t('tabCluster'))
2687
+ , React.createElement('button', { type: 'button', className: activeTab === 'files' ? 'drw-btn drw-btn-primary' : 'drw-btn', onClick: () => setActiveTab('files') }, t('tabFiles'))
2688
+ ),
2689
+ activeTab === 'cluster'
2690
+ ? React.createElement(ClusterPanel, { t, profiles })
2691
+ : activeTab === 'files'
2692
+ ? React.createElement(ExplorerTab, { t, activeProfile })
2693
+ : React.createElement(ProfilesPanel, { t, editing, setEditing, setTestResult, profiles, activeId, busy, handleSave, testResult, handleTest, handleSetActive, handleDelete, browserOpen, setBrowserOpen, browserPath, browserEntries, browserLoading, openDirectoryBrowser, fetchRemoteDir, showPassword, setShowPassword }),
2137
2694
 
2138
2695
 
2139
2696
  // Section 2: Active Connection Diagnostics (ClineBot Card 4 style)
@@ -2445,7 +3002,232 @@ window.__ModuleLoader__.load({
2445
3002
  )
2446
3003
  }
2447
3004
 
3005
+ const DRW_ACTIVE = 'data-drw-active'
3006
+ const DRW_VIEW = 'data-drw-view'
3007
+ const DRW_ENTRY = 'data-drw-entry'
3008
+ const DRW_PANEL = 'remote-workspace'
3009
+ const DRW_ICON = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="12" rx="2"></rect><path d="M8 20h8"></path><path d="M12 16v4"></path></svg>'
3010
+
3011
+ function workspaceTranslator(ctx) {
3012
+ let lang = ''
3013
+ try {
3014
+ lang = ctx && ctx.locale && typeof ctx.locale.getLocale === 'function' ? ctx.locale.getLocale() : ''
3015
+ } catch (err) { lang = '' }
3016
+ if (typeof lang === 'string' && (lang === 'zh' || lang.indexOf('zh') === 0)) return makeT(zh, en)
3017
+ return makeT(en, zh)
3018
+ }
3019
+
3020
+ function WorkspaceScreen(props) {
3021
+ const t = workspaceTranslator(props.ctx)
3022
+ const [tab, setTab] = React.useState('hosts')
3023
+ const [profiles, setProfiles] = React.useState([])
3024
+ const [activeId, setActiveId] = React.useState('')
3025
+ const [tunnels, setTunnels] = React.useState([])
3026
+ const [localPort, setLocalPort] = React.useState('3000')
3027
+ const [remotePort, setRemotePort] = React.useState('3000')
3028
+ const [tunnelMsg, setTunnelMsg] = React.useState(null)
3029
+ const [busy, setBusy] = React.useState(null)
3030
+ const activeProfile = profiles.find((item) => item.id === activeId) || null
3031
+
3032
+ const loadState = async () => {
3033
+ try {
3034
+ const res = await fetch('/dsh-remote-workspace/state')
3035
+ const data = await res.json()
3036
+ if (data && data.ok) {
3037
+ setProfiles(data.profiles || [])
3038
+ setActiveId(data.activeId || '')
3039
+ setTunnels(data.tunnels || [])
3040
+ }
3041
+ } catch (err) { /* state refresh is best effort */ }
3042
+ }
3043
+
3044
+ React.useEffect(() => {
3045
+ ensureCss()
3046
+ loadState()
3047
+ }, [])
3048
+
3049
+ const startTunnel = async () => {
3050
+ if (!activeId) return
3051
+ setBusy('tunnel_open')
3052
+ try {
3053
+ const res = await fetch('/dsh-remote-workspace/tunnels/start', {
3054
+ method: 'POST',
3055
+ headers: { 'Content-Type': 'application/json' },
3056
+ body: JSON.stringify({ profileId: activeId, localPort, remotePort })
3057
+ })
3058
+ const data = await res.json()
3059
+ setTunnelMsg(data && data.ok
3060
+ ? { ok: true, text: t('tOpenOk', { localPort, remotePort }) }
3061
+ : { ok: false, text: t('tOpenErr', { err: (data && data.error) || 'Failed' }) })
3062
+ if (data && data.ok) await loadState()
3063
+ } catch (err) {
3064
+ setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
3065
+ }
3066
+ setBusy(null)
3067
+ }
3068
+
3069
+ const stopTunnel = async (tunnelId) => {
3070
+ setBusy('tunnel_stop')
3071
+ try {
3072
+ await fetch('/dsh-remote-workspace/tunnels/stop', {
3073
+ method: 'POST',
3074
+ headers: { 'Content-Type': 'application/json' },
3075
+ body: JSON.stringify({ tunnelId })
3076
+ })
3077
+ await loadState()
3078
+ } catch (err) { /* stop is reported by the next state load */ }
3079
+ setBusy(null)
3080
+ }
3081
+
3082
+ const tabs = [
3083
+ ['hosts', 'wsHosts'],
3084
+ ['terminal', 'wsTerminal'],
3085
+ ['files', 'wsFiles'],
3086
+ ['docker', 'wsDocker'],
3087
+ ['tunnels', 'wsTunnels'],
3088
+ ['cluster', 'wsCluster']
3089
+ ]
3090
+
3091
+ return React.createElement('div', { className: 'drw-ws' },
3092
+ React.createElement('div', { className: 'drw-ws-bar' },
3093
+ React.createElement('button', { type: 'button', className: 'drw-btn', onClick: () => props.onClose && props.onClose() }, t('wsBack')),
3094
+ tabs.map(([id, key]) => React.createElement('button', {
3095
+ key: id,
3096
+ type: 'button',
3097
+ className: tab === id ? 'drw-btn drw-btn-primary' : 'drw-btn',
3098
+ onClick: () => setTab(id)
3099
+ }, t(key)))
3100
+ ),
3101
+ React.createElement('div', { className: 'drw-ws-panel', hidden: tab !== 'hosts' }, React.createElement(SettingsView, props)),
3102
+ React.createElement('div', { className: 'drw-ws-panel', hidden: tab !== 'terminal' }, React.createElement(TerminalTab, { t, activeProfile })),
3103
+ React.createElement('div', { className: 'drw-ws-panel', hidden: tab !== 'files' }, React.createElement(ExplorerTab, { t, activeProfile })),
3104
+ React.createElement('div', { className: 'drw-ws-panel', hidden: tab !== 'docker' }, React.createElement(ContainersTab, { t, activeProfile })),
3105
+ React.createElement('div', { className: 'drw-ws-panel', hidden: tab !== 'tunnels' }, React.createElement(TunnelsPanel, {
3106
+ t, busy, activeProfile, tunnelMsg, tunnels,
3107
+ newTunnelLocal: localPort, setNewTunnelLocal: setLocalPort,
3108
+ newTunnelRemote: remotePort, setNewTunnelRemote: setRemotePort,
3109
+ handleStartTunnel: startTunnel, handleStopTunnel: stopTunnel
3110
+ })),
3111
+ React.createElement('div', { className: 'drw-ws-panel', hidden: tab !== 'cluster' }, React.createElement(ClusterPanel, { t, profiles }))
3112
+ )
3113
+ }
3114
+
3115
+ function createWorkspaceToggle() {
3116
+ let open = false
3117
+ const listeners = new Set()
3118
+ return {
3119
+ isOpen() { return open },
3120
+ set(next) {
3121
+ open = Boolean(next)
3122
+ listeners.forEach((fn) => fn(open))
3123
+ },
3124
+ toggle() { this.set(!open) },
3125
+ subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn) }
3126
+ }
3127
+ }
3128
+
3129
+ function nativeSidebarButton() {
3130
+ const nodes = document.querySelectorAll('button[class*="newSession"]')
3131
+ for (const node of nodes) {
3132
+ if (String(node.className).indexOf('newSessionLabel') === -1) return node
3133
+ }
3134
+ return null
3135
+ }
3136
+
3137
+ function mountRemoteWorkspace(ctx) {
3138
+ if (typeof document === 'undefined') return function () {}
3139
+ ensureCss()
3140
+ const toggle = createWorkspaceToggle()
3141
+ const t = workspaceTranslator(ctx)
3142
+ let entry = null
3143
+ let container = null
3144
+ let root = null
3145
+
3146
+ const placeEntry = () => {
3147
+ if (entry && entry.isConnected) return
3148
+ const target = nativeSidebarButton()
3149
+ if (!target || !target.parentElement) return
3150
+ if (target.parentElement.querySelector('[' + DRW_ENTRY + ']')) return
3151
+ entry = target.cloneNode(true)
3152
+ entry.className = String(target.className) + ' drw-entry-clone'
3153
+ entry.setAttribute(DRW_ENTRY, '')
3154
+ entry.setAttribute('data-dsh-plugin', 'dsh-remote-workspace')
3155
+ entry.setAttribute('aria-label', t('navLabel'))
3156
+ entry.removeAttribute('id')
3157
+ entry.innerHTML = ''
3158
+ const icon = document.createElement('span')
3159
+ icon.className = 'drw-entry-icon'
3160
+ icon.innerHTML = DRW_ICON
3161
+ const label = document.createElement('span')
3162
+ label.className = 'drw-entry-label'
3163
+ label.textContent = t('navLabel')
3164
+ entry.appendChild(icon)
3165
+ entry.appendChild(label)
3166
+ entry.addEventListener('click', (event) => {
3167
+ event.preventDefault()
3168
+ event.stopPropagation()
3169
+ toggle.toggle()
3170
+ })
3171
+ target.insertAdjacentElement('afterend', entry)
3172
+ }
3173
+
3174
+ const reactDom = () => {
3175
+ if (typeof window !== 'undefined' && window.ReactDOM) return window.ReactDOM
3176
+ try { return require('react-dom/client') } catch (err) { return null }
3177
+ }
3178
+
3179
+ const placeView = () => {
3180
+ const column = document.querySelector('[data-pane=conversation], [class*=centerCol]')
3181
+ if (!column) return
3182
+ if (!container) {
3183
+ container = document.createElement('div')
3184
+ container.setAttribute(DRW_VIEW, '')
3185
+ container.setAttribute('data-dsh-plugin', 'dsh-remote-workspace')
3186
+ column.appendChild(container)
3187
+ const ReactDOM = reactDom()
3188
+ if (ReactDOM && typeof ReactDOM.createRoot === 'function') {
3189
+ root = ReactDOM.createRoot(container)
3190
+ root.render(React.createElement(WorkspaceScreen, { ctx, onClose: () => toggle.set(false) }))
3191
+ }
3192
+ return
3193
+ }
3194
+ if (container.parentElement !== column) column.appendChild(container)
3195
+ }
3196
+
3197
+ const applyOpen = () => {
3198
+ if (toggle.isOpen()) {
3199
+ document.documentElement.setAttribute(DRW_ACTIVE, '')
3200
+ document.dispatchEvent(new CustomEvent('dsh-panel-activate', { detail: DRW_PANEL }))
3201
+ if (entry) entry.dataset.active = 'true'
3202
+ } else {
3203
+ document.documentElement.removeAttribute(DRW_ACTIVE)
3204
+ if (entry) delete entry.dataset.active
3205
+ }
3206
+ }
3207
+
3208
+ const onOther = (event) => {
3209
+ if (event.detail !== DRW_PANEL && toggle.isOpen()) toggle.set(false)
3210
+ }
3211
+ const observer = new MutationObserver(() => { placeEntry(); placeView() })
3212
+ observer.observe(document.body, { childList: true, subtree: true })
3213
+ document.addEventListener('dsh-panel-activate', onOther)
3214
+ const unsubscribe = toggle.subscribe(applyOpen)
3215
+ placeEntry()
3216
+ placeView()
3217
+
3218
+ return () => {
3219
+ observer.disconnect()
3220
+ document.removeEventListener('dsh-panel-activate', onOther)
3221
+ unsubscribe()
3222
+ document.documentElement.removeAttribute(DRW_ACTIVE)
3223
+ if (root && root.unmount) root.unmount()
3224
+ if (container) container.remove()
3225
+ if (entry) entry.remove()
3226
+ }
3227
+ }
3228
+
2448
3229
  function apply(ctx) {
3230
+ try { mountRemoteWorkspace(ctx) } catch (err) { console.warn('[dsh-remote-workspace] workspace mount skipped', err && err.message) }
2449
3231
  if (ctx.locale && typeof ctx.locale.register === 'function') {
2450
3232
  if (typeof ctx.effect === 'function') {
2451
3233
  ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-remote-workspace: dictionaries');