dsh-ssh-workbench 1.6.4 → 1.7.8
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/CHANGELOG.md +133 -1
- package/CONTRIBUTING.md +5 -4
- package/README.md +47 -25
- package/SECURITY.md +2 -0
- package/docs/architecture.md +60 -38
- package/docs/execution-model.md +83 -0
- package/docs/images/chat-at-resource.png +0 -0
- package/docs/images/plugin.png +0 -0
- package/docs/images/remote-workbench.png +0 -0
- package/docs/images/workbench.png +0 -0
- package/docs/quickstart.md +24 -9
- package/lib/client.js +1249 -218
- package/lib/core/activity.js +5 -0
- package/lib/core/background.js +18 -4
- package/lib/core/broker.js +10 -7
- package/lib/core/dialect.js +1 -1
- package/lib/core/execution-stats.js +88 -0
- package/lib/core/gate-pwsh.js +1 -1
- package/lib/core/guard.js +86 -34
- package/lib/core/live-executions.js +101 -0
- package/lib/core/managed-context.js +287 -0
- package/lib/core/{targets.js → mentions.js} +49 -83
- package/lib/core/posix-shell.js +89 -0
- package/lib/core/prefs.js +36 -3
- package/lib/core/remote-exec-tool.js +441 -0
- package/lib/core/remote-file-tools.js +642 -0
- package/lib/core/{remote-bash-tool.js → remote-shell-tool.js} +58 -69
- package/lib/core/secrets.js +0 -1
- package/lib/core/server-ref.js +61 -0
- package/lib/core/shell-adapters.js +1 -1
- package/lib/core/ssh.js +22 -0
- package/lib/core/win32-adapter.js +1 -1
- package/lib/index.js +121 -157
- package/lib/routes.js +45 -5
- package/package.json +23 -7
- package/lib/core/remote-bash.js +0 -337
- package/lib/core/server-reference.js +0 -299
package/lib/client.js
CHANGED
|
@@ -23,6 +23,10 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
// don't expose the primitives table.
|
|
24
24
|
let primitives = null
|
|
25
25
|
try { primitives = require('@deepseek-ai/dsh-client-ui-primitives') } catch { primitives = null }
|
|
26
|
+
// Portal the dock panel to document.body so the composer's stacking context
|
|
27
|
+
// cannot clip or cover it. Optional: fall back to inline when unavailable.
|
|
28
|
+
let ReactDOM = null
|
|
29
|
+
try { ReactDOM = require('react-dom') } catch { ReactDOM = null }
|
|
26
30
|
|
|
27
31
|
// The merged SSH guide card remembers which page (server manager or remote
|
|
28
32
|
// files) its main button should open; the chevron always offers both.
|
|
@@ -79,6 +83,17 @@ window.__ModuleLoader__.load({
|
|
|
79
83
|
'workbench.disabled': '已关闭',
|
|
80
84
|
'workbench.sidebarLoadFailed': '读取侧边栏设置失败:{msg}',
|
|
81
85
|
'workbench.sidebarSaveFailed': '保存失败:{msg}',
|
|
86
|
+
'workbench.writeTitle': '允许远程写入',
|
|
87
|
+
'workbench.writeHint': '开启后模型可以用 remote_write / remote_edit 修改远程文件。关闭时远程读取、remote_glob、remote_grep 仍然可用,写入会被明确拒绝。',
|
|
88
|
+
'workbench.writeOn': '已允许',
|
|
89
|
+
'workbench.writeOff': '已禁止',
|
|
90
|
+
'workbench.writeLoadFailed': '读取远程写入设置失败:{msg}',
|
|
91
|
+
'workbench.writeNeedRestart': '已保存,但当前运行的后端还不支持这个开关,重启 DSH 后生效。',
|
|
92
|
+
'workbench.hierarchyTitle': '系统文件浏览使用层级模式',
|
|
93
|
+
'workbench.hierarchyHint': '开启后右侧栏「工作区文件」改为带面包屑和向上导航的层级浏览器,只浏览当前会话目录;关闭时保持 DSH 原生文件页。',
|
|
94
|
+
'workbench.hierarchyOn': '层级模式',
|
|
95
|
+
'workbench.hierarchyOff': '原生模式',
|
|
96
|
+
'workbench.hierarchyLoadFailed': '读取系统文件浏览设置失败:{msg}',
|
|
82
97
|
'workbench.saving': '保存中…',
|
|
83
98
|
'workbench.saved': '已保存',
|
|
84
99
|
'workbench.split': '拖动调整终端与文件宽度',
|
|
@@ -153,6 +168,10 @@ window.__ModuleLoader__.load({
|
|
|
153
168
|
'panel.keyRequired': '密钥文件路径不能为空',
|
|
154
169
|
'source.section': '纳管资源',
|
|
155
170
|
'files.title': '远程文件',
|
|
171
|
+
'files.systemTitle': '工作区文件',
|
|
172
|
+
'files.systemDescription': '层级浏览当前会话目录',
|
|
173
|
+
'files.noSessionDir': '当前会话还没有工作目录',
|
|
174
|
+
'files.outsideRoot': '只能在当前会话目录内浏览',
|
|
156
175
|
'files.up': '上一级',
|
|
157
176
|
'files.refresh': '刷新',
|
|
158
177
|
'files.empty': '目录为空',
|
|
@@ -167,8 +186,22 @@ window.__ModuleLoader__.load({
|
|
|
167
186
|
'files.enterPath': '输入路径,回车跳转',
|
|
168
187
|
'files.created': '已创建文件夹 {name}',
|
|
169
188
|
'files.more': '更多操作',
|
|
189
|
+
'files.openFile': '打开',
|
|
190
|
+
'files.actionFailed': '操作失败:{message}',
|
|
170
191
|
'files.downloadFile': '下载',
|
|
171
192
|
'files.copyFile': '复制文件',
|
|
193
|
+
'files.openInApp': '在应用中打开',
|
|
194
|
+
'files.openInAppWith': '用 {app} 打开',
|
|
195
|
+
'files.openInAppMore': '选择其他应用打开',
|
|
196
|
+
'files.openInAppDefault': '{app}(默认)',
|
|
197
|
+
'files.search': '搜索文件',
|
|
198
|
+
'files.searchPlaceholder': '搜索文件名或路径',
|
|
199
|
+
'files.searchClear': '清空搜索',
|
|
200
|
+
'files.searching': '正在搜索…',
|
|
201
|
+
'files.searchFailed': '搜索失败,请稍后重试',
|
|
202
|
+
'files.searchEmpty': '没有匹配的文件',
|
|
203
|
+
'files.searchTruncated': '结果较多,只显示前面一部分',
|
|
204
|
+
'files.searchIncomplete': '搜索未完成,结果可能不全',
|
|
172
205
|
'files.copyPathEntry': '复制路径',
|
|
173
206
|
'files.rename': '重命名',
|
|
174
207
|
'files.delete': '删除',
|
|
@@ -229,6 +262,17 @@ window.__ModuleLoader__.load({
|
|
|
229
262
|
'files.sourceMode': '源码',
|
|
230
263
|
'files.viewerLoading': '读取文件…',
|
|
231
264
|
'files.viewerBinary': '二进制文件,暂不支持在线阅读',
|
|
265
|
+
'files.truncated': '已截断',
|
|
266
|
+
'files.viewerTruncated': '文件较大,这里只显示开头一部分。',
|
|
267
|
+
'files.viewerImageTruncated': '图片超过预览上限,无法完整显示;请下载后查看。',
|
|
268
|
+
'files.tableMode': '表格',
|
|
269
|
+
'files.imageMode': '图片',
|
|
270
|
+
'files.tableRows': '{n} 行 · {columns} 列',
|
|
271
|
+
'files.tableRowsPartial': '前 {n} 行 · {columns} 列',
|
|
272
|
+
'files.linesPartial': '前 {n} 行',
|
|
273
|
+
'files.tableTruncatedRows': '表格较大,只显示前 {rows} 行。',
|
|
274
|
+
'files.tableTruncatedColumns': '表格较宽,只显示前 {columns} 列。',
|
|
275
|
+
'files.tableTruncatedBoth': '表格较大,只显示前 {rows} 行、前 {columns} 列。',
|
|
232
276
|
'files.viewerDirectory': '该条目是目录,无法预览',
|
|
233
277
|
'files.viewerNoParams': '预览参数缺失,请从远程文件重新打开',
|
|
234
278
|
'files.openUnavailable': '当前宿主不支持打开预览页签',
|
|
@@ -247,7 +291,6 @@ window.__ModuleLoader__.load({
|
|
|
247
291
|
'overview.kind.server-update': '更新资源',
|
|
248
292
|
'overview.kind.server-remove': '删除资源',
|
|
249
293
|
'common.loading': '加载中…',
|
|
250
|
-
'tool.target': '切换远程目标',
|
|
251
294
|
'tool.running': '执行中…',
|
|
252
295
|
'tool.noOutput': '(无输出)',
|
|
253
296
|
'tool.input': '参数',
|
|
@@ -265,8 +308,15 @@ window.__ModuleLoader__.load({
|
|
|
265
308
|
'files.chunkIncomplete': '文件分块读取不完整',
|
|
266
309
|
'settings.tabServers': '纳管资源',
|
|
267
310
|
'settings.tabActivity': '操作日志',
|
|
268
|
-
'target.
|
|
269
|
-
'target.
|
|
311
|
+
'target.popTitle': '远程执行',
|
|
312
|
+
'target.popCount': '{n} 次',
|
|
313
|
+
'target.rowCount': '{n} 次',
|
|
314
|
+
'target.execChip': '远程执行 · {n} 次',
|
|
315
|
+
'target.execTitle': '本会话远程执行次数',
|
|
316
|
+
'target.runningOne': '{name} · 执行中',
|
|
317
|
+
'target.runningMany': '{n} 个目标 · 执行中',
|
|
318
|
+
'target.runningValue': '{n} 个目标执行中',
|
|
319
|
+
'target.runningLabel': '执行中',
|
|
270
320
|
'common.close': '关闭',
|
|
271
321
|
}
|
|
272
322
|
|
|
@@ -299,6 +349,17 @@ window.__ModuleLoader__.load({
|
|
|
299
349
|
'workbench.disabled': 'Off',
|
|
300
350
|
'workbench.sidebarLoadFailed': 'Failed to read sidebar settings: {msg}',
|
|
301
351
|
'workbench.sidebarSaveFailed': 'Save failed: {msg}',
|
|
352
|
+
'workbench.writeTitle': 'Allow remote writes',
|
|
353
|
+
'workbench.writeHint': 'When on, the model can modify remote files with remote_write / remote_edit. When off, remote_read, remote_glob and remote_grep still work and writes are refused explicitly.',
|
|
354
|
+
'workbench.writeOn': 'Allowed',
|
|
355
|
+
'workbench.writeOff': 'Blocked',
|
|
356
|
+
'workbench.writeLoadFailed': 'Failed to read the remote write setting: {msg}',
|
|
357
|
+
'workbench.writeNeedRestart': 'Saved, but the running backend does not support this switch yet. Restart DSH to apply it.',
|
|
358
|
+
'workbench.hierarchyTitle': 'Hierarchical system file browser',
|
|
359
|
+
'workbench.hierarchyHint': 'Replaces the right-sidebar Workspace Files page with the breadcrumb-and-up hierarchy browser, limited to the current session directory. Off keeps the native DSH file page.',
|
|
360
|
+
'workbench.hierarchyOn': 'Hierarchical',
|
|
361
|
+
'workbench.hierarchyOff': 'Native',
|
|
362
|
+
'workbench.hierarchyLoadFailed': 'Could not read the system file browser setting: {msg}',
|
|
302
363
|
'workbench.saving': 'Saving…',
|
|
303
364
|
'workbench.saved': 'Saved',
|
|
304
365
|
'workbench.split': 'Drag to resize the terminal and file panes',
|
|
@@ -373,6 +434,10 @@ window.__ModuleLoader__.load({
|
|
|
373
434
|
'panel.keyRequired': 'Key file path is required',
|
|
374
435
|
'source.section': 'Managed resources',
|
|
375
436
|
'files.title': 'Remote files',
|
|
437
|
+
'files.systemTitle': 'Workspace files',
|
|
438
|
+
'files.systemDescription': 'Browse the current session directory hierarchically',
|
|
439
|
+
'files.noSessionDir': 'This session has no working directory yet',
|
|
440
|
+
'files.outsideRoot': 'Only the current session directory can be browsed',
|
|
376
441
|
'files.up': 'Up',
|
|
377
442
|
'files.refresh': 'Refresh',
|
|
378
443
|
'files.empty': 'Empty directory',
|
|
@@ -387,8 +452,22 @@ window.__ModuleLoader__.load({
|
|
|
387
452
|
'files.enterPath': 'Enter a path and press Enter',
|
|
388
453
|
'files.created': 'Folder created: {name}',
|
|
389
454
|
'files.more': 'More actions',
|
|
455
|
+
'files.openFile': 'Open',
|
|
456
|
+
'files.actionFailed': 'Action failed: {message}',
|
|
390
457
|
'files.downloadFile': 'Download',
|
|
391
458
|
'files.copyFile': 'Copy file',
|
|
459
|
+
'files.openInApp': 'Open in app',
|
|
460
|
+
'files.openInAppWith': 'Open with {app}',
|
|
461
|
+
'files.openInAppMore': 'Open with another app',
|
|
462
|
+
'files.openInAppDefault': '{app} (default)',
|
|
463
|
+
'files.search': 'Search files',
|
|
464
|
+
'files.searchPlaceholder': 'Search by name or path',
|
|
465
|
+
'files.searchClear': 'Clear search',
|
|
466
|
+
'files.searching': 'Searching…',
|
|
467
|
+
'files.searchFailed': 'Search failed, please try again',
|
|
468
|
+
'files.searchEmpty': 'No matching files',
|
|
469
|
+
'files.searchTruncated': 'Too many matches; only the first ones are shown',
|
|
470
|
+
'files.searchIncomplete': 'Search incomplete; results may be partial',
|
|
392
471
|
'files.copyPathEntry': 'Copy path',
|
|
393
472
|
'files.rename': 'Rename',
|
|
394
473
|
'files.delete': 'Delete',
|
|
@@ -447,6 +526,17 @@ window.__ModuleLoader__.load({
|
|
|
447
526
|
'files.sourceMode': 'Source',
|
|
448
527
|
'files.viewerLoading': 'Reading file…',
|
|
449
528
|
'files.viewerBinary': 'Binary file; inline reading is not available',
|
|
529
|
+
'files.truncated': 'Truncated',
|
|
530
|
+
'files.viewerTruncated': 'This file is large; only the beginning is shown.',
|
|
531
|
+
'files.viewerImageTruncated': 'This image is larger than the preview limit and cannot be shown in full; download it to view.',
|
|
532
|
+
'files.tableMode': 'Table',
|
|
533
|
+
'files.imageMode': 'Image',
|
|
534
|
+
'files.tableRows': '{n} rows · {columns} columns',
|
|
535
|
+
'files.tableRowsPartial': 'first {n} rows · {columns} columns',
|
|
536
|
+
'files.linesPartial': 'first {n} lines',
|
|
537
|
+
'files.tableTruncatedRows': 'This table is large; only the first {rows} rows are shown.',
|
|
538
|
+
'files.tableTruncatedColumns': 'This table is wide; only the first {columns} columns are shown.',
|
|
539
|
+
'files.tableTruncatedBoth': 'This table is large; only the first {rows} rows and {columns} columns are shown.',
|
|
450
540
|
'files.viewerDirectory': 'This entry is a directory; it cannot be previewed',
|
|
451
541
|
'files.viewerNoParams': 'Missing preview parameters; reopen from Remote files',
|
|
452
542
|
'files.openUnavailable': 'This host cannot open preview tabs',
|
|
@@ -465,7 +555,6 @@ window.__ModuleLoader__.load({
|
|
|
465
555
|
'overview.kind.server-update': 'Resource updated',
|
|
466
556
|
'overview.kind.server-remove': 'Resource removed',
|
|
467
557
|
'common.loading': 'Loading…',
|
|
468
|
-
'tool.target': 'Switch remote target',
|
|
469
558
|
'tool.running': 'Running…',
|
|
470
559
|
'tool.noOutput': '(no output)',
|
|
471
560
|
'tool.input': 'Input',
|
|
@@ -483,8 +572,15 @@ window.__ModuleLoader__.load({
|
|
|
483
572
|
'files.chunkIncomplete': 'File chunk read was incomplete',
|
|
484
573
|
'settings.tabServers': 'Managed resources',
|
|
485
574
|
'settings.tabActivity': 'Activity log',
|
|
486
|
-
'target.
|
|
487
|
-
'target.
|
|
575
|
+
'target.popTitle': 'Remote executions',
|
|
576
|
+
'target.popCount': '{n} runs',
|
|
577
|
+
'target.rowCount': '{n} runs',
|
|
578
|
+
'target.execChip': 'Remote · {n} runs',
|
|
579
|
+
'target.execTitle': 'Remote executions in this session',
|
|
580
|
+
'target.runningOne': '{name} · running',
|
|
581
|
+
'target.runningMany': '{n} targets · running',
|
|
582
|
+
'target.runningValue': '{n} targets running',
|
|
583
|
+
'target.runningLabel': 'running',
|
|
488
584
|
'common.close': 'Close',
|
|
489
585
|
'files.transferUploading': 'Uploading {name}',
|
|
490
586
|
'files.transferDownloading': 'Downloading {name}',
|
|
@@ -563,8 +659,14 @@ window.__ModuleLoader__.load({
|
|
|
563
659
|
let composerBridge = null
|
|
564
660
|
/** Client `sessions` service: gives the session-scoped ctx that owns composer events. */
|
|
565
661
|
let sessionsService = null
|
|
662
|
+
/** Set while the sidebar tab service is available; toggled from the settings form. */
|
|
663
|
+
let systemFilesSync = null
|
|
566
664
|
/** Owner client ctx, set in apply(); used by the composer target chip. */
|
|
567
665
|
let clientCtx = null
|
|
666
|
+
/** Namespace service (`remote.session`) used to open Host-local paths in their app. */
|
|
667
|
+
let sessionRemote = null
|
|
668
|
+
/** Namespace service (`remote.workspaceFiles`) used by the local files search. */
|
|
669
|
+
let workspaceFilesRemote = null
|
|
568
670
|
|
|
569
671
|
|
|
570
672
|
/** The open conversation's session id, exposed on the chat region for a11y/debugging. */
|
|
@@ -590,13 +692,19 @@ window.__ModuleLoader__.load({
|
|
|
590
692
|
}
|
|
591
693
|
|
|
592
694
|
/**
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
695
|
+
* Composer dock for the Session's remote resources. The popover deliberately
|
|
696
|
+
* mirrors the shipped usage counter: one header value, a hairline divider,
|
|
697
|
+
* and a label/value grid. It is read-only: targets are shown with their
|
|
698
|
+
* plugin-local execution count for this Session.
|
|
596
699
|
*/
|
|
597
700
|
function TargetDock(props) {
|
|
598
701
|
const sessionId = props && props.sessionId
|
|
599
|
-
const [
|
|
702
|
+
const [stats, setStats] = useState(null)
|
|
703
|
+
const [running, setRunning] = useState(null)
|
|
704
|
+
const [open, setOpen] = useState(false)
|
|
705
|
+
const [rect, setRect] = useState(null)
|
|
706
|
+
const rootRef = useRef(null)
|
|
707
|
+
const panelRef = useRef(null)
|
|
600
708
|
useEffect(() => {
|
|
601
709
|
ensurePanelStyle()
|
|
602
710
|
}, [])
|
|
@@ -606,56 +714,147 @@ window.__ModuleLoader__.load({
|
|
|
606
714
|
const load = async () => {
|
|
607
715
|
try {
|
|
608
716
|
const body = await api('/target?session=' + encodeURIComponent(sessionId))
|
|
609
|
-
if (alive)
|
|
717
|
+
if (!alive) return
|
|
718
|
+
setStats(body && body.stats && typeof body.stats === 'object' ? body.stats : null)
|
|
719
|
+
setRunning(body && body.running && typeof body.running === 'object' ? body.running : null)
|
|
610
720
|
} catch {
|
|
611
|
-
if (alive)
|
|
721
|
+
if (!alive) return
|
|
722
|
+
setStats(null)
|
|
723
|
+
setRunning(null)
|
|
612
724
|
}
|
|
613
725
|
}
|
|
614
726
|
void load()
|
|
615
|
-
|
|
727
|
+
// The dock is a read-only projection of the execution ledger. Tool
|
|
728
|
+
// events already trigger a refresh on the next turn; this low-frequency
|
|
729
|
+
// fallback keeps a long turn from showing stale counts.
|
|
730
|
+
const timer = setInterval(load, 1500)
|
|
616
731
|
return () => {
|
|
617
732
|
alive = false
|
|
618
733
|
clearInterval(timer)
|
|
619
734
|
}
|
|
620
735
|
}, [sessionId])
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
return
|
|
736
|
+
useEffect(() => {
|
|
737
|
+
if (!open || typeof document === 'undefined') return undefined
|
|
738
|
+
const onPointerDown = (event) => {
|
|
739
|
+
const node = event.target
|
|
740
|
+
if (node && typeof node.closest === 'function' && node.closest('[data-sdh-target-dock]')) return
|
|
741
|
+
if (rootRef.current && node && rootRef.current.contains(node)) return
|
|
742
|
+
if (panelRef.current && node && panelRef.current.contains(node)) return
|
|
743
|
+
setOpen(false)
|
|
627
744
|
}
|
|
628
|
-
|
|
629
|
-
|
|
745
|
+
const onKeyDown = (event) => {
|
|
746
|
+
if (event.key === 'Escape') {
|
|
747
|
+
event.stopPropagation()
|
|
748
|
+
setOpen(false)
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
document.addEventListener('mousedown', onPointerDown, true)
|
|
752
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
753
|
+
return () => {
|
|
754
|
+
document.removeEventListener('mousedown', onPointerDown, true)
|
|
755
|
+
document.removeEventListener('keydown', onKeyDown, true)
|
|
756
|
+
}
|
|
757
|
+
}, [open])
|
|
758
|
+
|
|
759
|
+
const tr = (key, fallback) => {
|
|
630
760
|
try {
|
|
631
|
-
|
|
761
|
+
const value = String(translate(key))
|
|
762
|
+
return value === key ? fallback : value
|
|
632
763
|
} catch {
|
|
633
|
-
return
|
|
764
|
+
return fallback
|
|
634
765
|
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
766
|
+
}
|
|
767
|
+
const statsServers = stats && Array.isArray(stats.servers) ? stats.servers : []
|
|
768
|
+
const rows = statsServers.filter((bucket) => bucket && Number.isFinite(bucket.count) && bucket.count > 0)
|
|
769
|
+
const total = stats && Number.isFinite(stats.count) ? stats.count : 0
|
|
770
|
+
const runningServers = running && Array.isArray(running.servers) ? running.servers.filter((entry) => entry && entry.name) : []
|
|
771
|
+
const runningNames = [...new Set(runningServers.map((entry) => entry.name))]
|
|
772
|
+
// A session with no remote execution has no dock. While a call is in
|
|
773
|
+
// flight we still render: the live target is the point of the indicator.
|
|
774
|
+
if (rows.length === 0 && total === 0 && runningNames.length === 0) return null
|
|
775
|
+
const togglePanel = (event) => {
|
|
776
|
+
if (open) {
|
|
777
|
+
setOpen(false)
|
|
778
|
+
return
|
|
779
|
+
}
|
|
780
|
+
const bounds = event.currentTarget.getBoundingClientRect()
|
|
781
|
+
const viewportWidth = typeof window !== 'undefined' && Number.isFinite(window.innerWidth) ? window.innerWidth : 320
|
|
782
|
+
const viewportHeight = typeof window !== 'undefined' && Number.isFinite(window.innerHeight) ? window.innerHeight : 600
|
|
783
|
+
const right = Math.max(8, viewportWidth - bounds.right)
|
|
784
|
+
const bottom = Math.max(8, viewportHeight - bounds.top + 6)
|
|
785
|
+
setRect({ right, bottom })
|
|
786
|
+
setOpen(true)
|
|
787
|
+
}
|
|
788
|
+
const runningOne = runningNames.length === 1
|
|
789
|
+
const label = runningOne
|
|
790
|
+
? tr('target.runningOne', '{name} · running').replace('{name}', runningNames[0])
|
|
791
|
+
: runningNames.length > 1
|
|
792
|
+
? tr('target.runningMany', '{n} targets · running').replace('{n}', String(runningNames.length))
|
|
793
|
+
: tr('target.execChip', 'Remote · {n}').replace('{n}', String(total))
|
|
794
|
+
const title = runningNames.length > 0
|
|
795
|
+
? runningOne
|
|
796
|
+
? tr('target.runningOne', '{name} · running').replace('{name}', runningNames[0])
|
|
797
|
+
: tr('target.runningMany', '{n} targets · running').replace('{n}', String(runningNames.length))
|
|
798
|
+
: tr('target.execTitle', 'Remote executions in this session')
|
|
799
|
+
const icon = h('svg', {
|
|
800
|
+
viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
801
|
+
strokeWidth: 1.7, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true,
|
|
802
|
+
},
|
|
803
|
+
h('rect', { x: 3, y: 4, width: 18, height: 7, rx: 1.5 }),
|
|
804
|
+
h('rect', { x: 3, y: 13, width: 18, height: 7, rx: 1.5 }),
|
|
805
|
+
h('path', { d: 'M6.5 7.5h.01M6.5 16.5h.01M10 7.5h7M10 16.5h7' }))
|
|
806
|
+
const button = h('button', {
|
|
807
|
+
type: 'button',
|
|
808
|
+
className: 'dsh-ssh-workbench-target-pill',
|
|
809
|
+
title,
|
|
810
|
+
'aria-label': title,
|
|
811
|
+
'aria-haspopup': 'dialog',
|
|
812
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
813
|
+
onClick: togglePanel,
|
|
814
|
+
},
|
|
815
|
+
icon,
|
|
816
|
+
h('span', { className: 'dsh-ssh-workbench-target-label' }, label))
|
|
817
|
+
|
|
818
|
+
const rowByName = new Map(rows.map((bucket) => [bucket.name, bucket]))
|
|
819
|
+
const detailNames = [
|
|
820
|
+
...runningNames,
|
|
821
|
+
...rows.map((bucket) => bucket.name).filter((name) => !runningNames.includes(name)),
|
|
822
|
+
]
|
|
823
|
+
const details = []
|
|
824
|
+
for (const name of detailNames) {
|
|
825
|
+
const bucket = rowByName.get(name)
|
|
826
|
+
const active = runningNames.includes(name)
|
|
827
|
+
const parts = []
|
|
828
|
+
if (active) parts.push(tr('target.runningLabel', 'running'))
|
|
829
|
+
if (bucket) parts.push(tr('target.rowCount', '{n} runs').replace('{n}', String(bucket.count)))
|
|
830
|
+
details.push(h('dt', { key: name + ':label' }, name))
|
|
831
|
+
details.push(h('dd', {
|
|
832
|
+
key: name + ':value',
|
|
833
|
+
...(active ? { className: 'dsh-ssh-workbench-target-row-running' } : {}),
|
|
834
|
+
}, parts.join(' · ')))
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const panel = open && rect
|
|
838
|
+
? h('div', {
|
|
839
|
+
ref: panelRef,
|
|
840
|
+
className: 'dsh-ssh-workbench-target-popover',
|
|
841
|
+
role: 'dialog',
|
|
842
|
+
'aria-label': tr('target.popTitle', 'Remote executions'),
|
|
843
|
+
'data-sdh-target-dock': '',
|
|
844
|
+
style: { right: rect.right + 'px', bottom: rect.bottom + 'px' },
|
|
654
845
|
},
|
|
655
|
-
h('
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
846
|
+
h('div', { className: 'dsh-ssh-workbench-target-title' },
|
|
847
|
+
h('span', { className: 'dsh-ssh-workbench-target-title-label' }, icon, tr('target.popTitle', 'Remote executions')),
|
|
848
|
+
h('span', { className: 'dsh-ssh-workbench-target-title-value' }, runningNames.length > 0
|
|
849
|
+
? tr('target.runningValue', '{n} targets running').replace('{n}', String(runningNames.length))
|
|
850
|
+
: tr('target.popCount', '{n} runs').replace('{n}', String(total)))),
|
|
851
|
+
h('div', { className: 'dsh-ssh-workbench-target-rule', 'aria-hidden': true }),
|
|
852
|
+
h('dl', { className: 'dsh-ssh-workbench-target-details' }, ...details))
|
|
853
|
+
: null
|
|
854
|
+
const mountedPanel = panel && ReactDOM && typeof document !== 'undefined'
|
|
855
|
+
? ReactDOM.createPortal(panel, document.body)
|
|
856
|
+
: panel
|
|
857
|
+
return h('span', { ref: rootRef, className: 'dsh-ssh-workbench-target-anchor', 'data-sdh-target-dock': '' }, button, mountedPanel)
|
|
659
858
|
}
|
|
660
859
|
|
|
661
860
|
/**
|
|
@@ -804,6 +1003,17 @@ window.__ModuleLoader__.load({
|
|
|
804
1003
|
'.dsh-ssh-workbench-files-split button:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
805
1004
|
'.dsh-ssh-workbench-files-split button:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-1px}',
|
|
806
1005
|
'.dsh-ssh-workbench-files-split svg{width:14px;height:14px}',
|
|
1006
|
+
'.dsh-ssh-workbench-files-search{box-sizing:border-box;height:42px;padding:8px 8px 4px;flex:none}',
|
|
1007
|
+
'.dsh-ssh-workbench-files-search-field{display:flex;gap:2px;align-items:center;height:100%;padding:0 2px;background:var(--dsw-alias-bg-layer-1);border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md)}',
|
|
1008
|
+
'.dsh-ssh-workbench-files-search-field:focus-within{border-color:var(--dsw-alias-state-business-primary)}',
|
|
1009
|
+
'.dsh-ssh-workbench-files-search-glyph{display:inline-flex;flex:none;width:24px;height:24px;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);padding:4px}',
|
|
1010
|
+
'.dsh-ssh-workbench-files-search-glyph svg{width:14px;height:14px}',
|
|
1011
|
+
'.dsh-ssh-workbench-files-search-input{flex:1 1 auto;min-width:0;padding:0 4px;color:var(--dsw-alias-label-primary);background:0 0;border:0;outline:0;font:inherit}',
|
|
1012
|
+
'.dsh-ssh-workbench-files-search-clear{display:inline-flex;flex:none;width:24px;height:24px;padding:4px;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);background:0 0;border:0;border-radius:var(--dsw-radius-sm);cursor:pointer}',
|
|
1013
|
+
'.dsh-ssh-workbench-files-search-clear:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
1014
|
+
'.dsh-ssh-workbench-files-search-clear svg{width:14px;height:14px}',
|
|
1015
|
+
'.dsh-ssh-workbench-files-search-dir{flex:none;max-width:45%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:12px}',
|
|
1016
|
+
'.dsh-ssh-workbench-files-search-note{padding:6px 10px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}',
|
|
807
1017
|
'.dsh-ssh-workbench-tool-row{box-sizing:border-box;display:flex;align-items:center;width:100%;min-width:0;height:calc(24px + var(--dsh-content-font-delta,0px));padding:0;border:0;background:0 0;color:var(--dsw-alias-label-tertiary);font:inherit;text-align:left;cursor:pointer;transition:color .1s}',
|
|
808
1018
|
'.dsh-ssh-workbench-tool-row:disabled{cursor:default}',
|
|
809
1019
|
'.dsh-ssh-workbench-tool-row:hover:not(:disabled){color:var(--dsw-alias-label-secondary)}',
|
|
@@ -867,7 +1077,18 @@ window.__ModuleLoader__.load({
|
|
|
867
1077
|
'@media (hover:hover){.dsh-ssh-workbench-target-pill:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}}',
|
|
868
1078
|
'.dsh-ssh-workbench-target-pill:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-1px}',
|
|
869
1079
|
'.dsh-ssh-workbench-target-label{min-width:0;overflow:hidden;text-overflow:ellipsis}',
|
|
870
|
-
'.dsh-ssh-workbench-
|
|
1080
|
+
'.dsh-ssh-workbench-target-popover{position:fixed;z-index:1100;box-sizing:border-box;width:max-content;min-width:min(300px,calc(100vw - 24px),var(--dsh-anchored-max-width,100vw));max-width:min(440px,calc(100vw - 24px),var(--dsh-anchored-max-width,100vw));max-height:var(--dsh-anchored-max-height,70vh);overflow:hidden auto;padding:16px;border:0;border-radius:var(--dsw-radius-lg);background:var(--dsw-specific-menu,var(--dsw-menu-surface-fill));backdrop-filter:var(--dsw-menu-backdrop-filter);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-secondary);cursor:default;font-size:12px;line-height:18px;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}',
|
|
1081
|
+
'.dsh-ssh-workbench-target-title{display:flex;justify-content:space-between;gap:16px;margin-bottom:8px;color:var(--dsw-alias-label-primary);font-weight:500}',
|
|
1082
|
+
'.dsh-ssh-workbench-target-title-label{display:inline-flex;align-items:center;gap:6px;min-width:0}',
|
|
1083
|
+
'.dsh-ssh-workbench-target-title-label svg{flex:none;width:14px;height:14px}',
|
|
1084
|
+
'.dsh-ssh-workbench-target-title-value{font-variant-numeric:tabular-nums}',
|
|
1085
|
+
'.dsh-ssh-workbench-target-rule{border-top:.5px solid var(--dsw-alias-border-l2);margin-bottom:10px}',
|
|
1086
|
+
'.dsh-ssh-workbench-target-details{display:grid;grid-template-columns:minmax(76px,auto) minmax(0,1fr);gap:6px 16px;margin:0;color:var(--dsw-alias-label-tertiary)}',
|
|
1087
|
+
'.dsh-ssh-workbench-target-details dt,.dsh-ssh-workbench-target-details dd{min-width:0;margin:0}',
|
|
1088
|
+
'.dsh-ssh-workbench-target-details dt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
1089
|
+
'.dsh-ssh-workbench-target-details dd{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;text-align:right}',
|
|
1090
|
+
'.dsh-ssh-workbench-target-details dd.dsh-ssh-workbench-target-row-running{color:var(--dsw-alias-label-primary);font-weight:500}',
|
|
1091
|
+
'.dsh-ssh-workbench-input{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;padding:0 12px;min-width:0}',
|
|
871
1092
|
'.dsh-ssh-workbench-input:focus-visible{outline:none;border-color:var(--dsw-alias-state-business-primary)}',
|
|
872
1093
|
'.dsh-ssh-workbench-input-compact{height:28px;padding:0 8px;border-radius:var(--dsw-radius-sm);border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);font-size:12px;line-height:18px}',
|
|
873
1094
|
'.dsh-ssh-workbench-select-trigger{display:flex;align-items:center;justify-content:space-between;gap:8px;text-align:left;cursor:pointer}',
|
|
@@ -2013,106 +2234,6 @@ window.__ModuleLoader__.load({
|
|
|
2013
2234
|
: tr('tool.failed', '调用失败')
|
|
2014
2235
|
}
|
|
2015
2236
|
|
|
2016
|
-
function SshOpsToolRow(props) {
|
|
2017
|
-
const phase = props?.phase ?? 'result'
|
|
2018
|
-
const block = props?.block
|
|
2019
|
-
const args = toolArgs(block)
|
|
2020
|
-
const toolName = props?.toolName ?? block?.call?.name ?? 'ssh_workbench'
|
|
2021
|
-
const command = typeof args?.command === 'string'
|
|
2022
|
-
? args.command
|
|
2023
|
-
: typeof args?.commands === 'string'
|
|
2024
|
-
? args.commands
|
|
2025
|
-
: typeof args?.pack === 'string'
|
|
2026
|
-
? args.pack
|
|
2027
|
-
: typeof args?.text === 'string'
|
|
2028
|
-
? args.text
|
|
2029
|
-
: ''
|
|
2030
|
-
const target = Array.isArray(args?.servers)
|
|
2031
|
-
? args.servers.join(', ')
|
|
2032
|
-
: args?.tag
|
|
2033
|
-
? 'tag:' + args.tag
|
|
2034
|
-
: args?.server ?? args?.session ?? ''
|
|
2035
|
-
const body = phase === 'result' ? textFromToolContent(block) : ''
|
|
2036
|
-
const bodyText = body.trim()
|
|
2037
|
-
const errorCode = phase === 'result' ? toolErrorCode(block) : ''
|
|
2038
|
-
const failed = phase === 'result' && (block?.isError || block?.error)
|
|
2039
|
-
const running = phase === 'preparing' || phase === 'start'
|
|
2040
|
-
const [expanded, setExpanded] = useState(false)
|
|
2041
|
-
const labels = {
|
|
2042
|
-
ssh_workbench_target: tr('tool.target', '切换远程目标'),
|
|
2043
|
-
}
|
|
2044
|
-
const label = labels[toolName] ?? tr('tool.target', '切换远程目标')
|
|
2045
|
-
const argsText = args ? JSON.stringify(args, null, 2) : ''
|
|
2046
|
-
const canExpand = Boolean(bodyText || argsText)
|
|
2047
|
-
const summary = failed
|
|
2048
|
-
? toolRowFailureText(bodyText, errorCode)
|
|
2049
|
-
: target && command
|
|
2050
|
-
? target + ' · ' + command
|
|
2051
|
-
: target || command || (running ? tr('tool.running', '执行中…') : tr('tool.noOutput', '(无输出)'))
|
|
2052
|
-
const state = failed ? 'error' : running ? 'running' : 'ok'
|
|
2053
|
-
const leading = running
|
|
2054
|
-
? spinner(14)
|
|
2055
|
-
: h('svg', {
|
|
2056
|
-
width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none',
|
|
2057
|
-
'aria-hidden': 'true', style: { display: 'block' },
|
|
2058
|
-
},
|
|
2059
|
-
state === 'error'
|
|
2060
|
-
? [
|
|
2061
|
-
h('circle', { key: 'ring', cx: 8, cy: 8, r: 6, stroke: 'currentColor', strokeWidth: 1.2 }),
|
|
2062
|
-
h('line', { key: 'a', x1: 5.8, y1: 5.8, x2: 10.2, y2: 10.2, stroke: 'currentColor', strokeWidth: 1.2 }),
|
|
2063
|
-
h('line', { key: 'b', x1: 10.2, y1: 5.8, x2: 5.8, y2: 10.2, stroke: 'currentColor', strokeWidth: 1.2 }),
|
|
2064
|
-
]
|
|
2065
|
-
: h('polyline', {
|
|
2066
|
-
points: '3.5 8.5 6.5 11.5 12.5 5',
|
|
2067
|
-
stroke: 'currentColor', strokeWidth: 1.2, strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
2068
|
-
}))
|
|
2069
|
-
const ioSection = (labelText, text) => h('div', { className: 'dsh-ssh-workbench-io-section' },
|
|
2070
|
-
h('span', { className: 'dsh-ssh-workbench-io-label' }, labelText),
|
|
2071
|
-
h('span', { className: 'dsh-ssh-workbench-io-text' }, text))
|
|
2072
|
-
const ioDivider = (key) => h('div', { key, className: 'dsh-ssh-workbench-io-divider' })
|
|
2073
|
-
const sections = []
|
|
2074
|
-
if (argsText) sections.push(ioSection(tr('tool.input', '参数'), argsText))
|
|
2075
|
-
if (bodyText) sections.push(ioSection(tr('tool.output', '输出'), bodyText))
|
|
2076
|
-
const bodyBlocks = sections.reduce((accumulator, section, index) => {
|
|
2077
|
-
if (index > 0) accumulator.push(ioDivider('divider-' + index))
|
|
2078
|
-
accumulator.push(section)
|
|
2079
|
-
return accumulator
|
|
2080
|
-
}, [])
|
|
2081
|
-
return h('div', {
|
|
2082
|
-
'data-sdh-tool-card': toolName,
|
|
2083
|
-
style: { display: 'flex', flexDirection: 'column', minWidth: 0 },
|
|
2084
|
-
},
|
|
2085
|
-
h('button', {
|
|
2086
|
-
type: 'button',
|
|
2087
|
-
className: 'dsh-ssh-workbench-tool-row',
|
|
2088
|
-
'data-sdh-tool-details-toggle': '',
|
|
2089
|
-
'aria-expanded': expanded ? 'true' : 'false',
|
|
2090
|
-
disabled: canExpand !== true,
|
|
2091
|
-
onClick: () => setExpanded((value) => !value),
|
|
2092
|
-
},
|
|
2093
|
-
h('span', { className: 'dsh-ssh-workbench-tool-leading', 'data-state': state }, leading),
|
|
2094
|
-
h('span', { className: 'dsh-ssh-workbench-tool-title' }, label),
|
|
2095
|
-
h('span', { className: 'dsh-ssh-workbench-tool-sep', 'aria-hidden': 'true' }),
|
|
2096
|
-
h('span', {
|
|
2097
|
-
className: 'dsh-ssh-workbench-tool-summary',
|
|
2098
|
-
'data-error': failed ? '' : undefined,
|
|
2099
|
-
'data-stopped': !failed && !running && /stop|cancel/i.test(errorCode) ? '' : undefined,
|
|
2100
|
-
}, summary),
|
|
2101
|
-
canExpand
|
|
2102
|
-
? h('span', { className: 'dsh-ssh-workbench-tool-action' }, expanded ? tr('tool.collapse', '收起详情') : tr('tool.expand', '查看详情'))
|
|
2103
|
-
: null,
|
|
2104
|
-
),
|
|
2105
|
-
expanded && canExpand
|
|
2106
|
-
? h('div', { className: 'dsh-ssh-workbench-tool-body' },
|
|
2107
|
-
h('div', { className: 'dsh-ssh-workbench-io-card' },
|
|
2108
|
-
bodyBlocks.length > 0
|
|
2109
|
-
? bodyBlocks
|
|
2110
|
-
: h('div', { className: 'dsh-ssh-workbench-io-section' },
|
|
2111
|
-
h('span', { className: 'dsh-ssh-workbench-io-label' }, tr('tool.output', '输出')),
|
|
2112
|
-
h('span', { className: 'dsh-ssh-workbench-io-text' }, tr('tool.noOutput', '(无输出)')))))
|
|
2113
|
-
: null,
|
|
2114
|
-
)
|
|
2115
|
-
}
|
|
2116
2237
|
let translate = (key) => key
|
|
2117
2238
|
/** Locale lookup with a Chinese fallback for hosts/tests where the locale service is not bound. */
|
|
2118
2239
|
const tr = (key, fallback) => {
|
|
@@ -2185,9 +2306,24 @@ window.__ModuleLoader__.load({
|
|
|
2185
2306
|
|
|
2186
2307
|
const FILES_IMAGE_TYPES = {
|
|
2187
2308
|
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
|
|
2188
|
-
webp: 'image/webp', bmp: 'image/bmp', ico: 'image/x-icon',
|
|
2309
|
+
webp: 'image/webp', bmp: 'image/bmp', ico: 'image/x-icon', svg: 'image/svg+xml',
|
|
2189
2310
|
}
|
|
2190
2311
|
|
|
2312
|
+
/** Delimited text rendered as a grid; the value is the field separator. */
|
|
2313
|
+
const FILES_TABLE_TYPES = { csv: ',', tsv: '\t' }
|
|
2314
|
+
const FILES_TABLE_MAX_ROWS = 500
|
|
2315
|
+
const FILES_TABLE_MAX_COLUMNS = 40
|
|
2316
|
+
|
|
2317
|
+
/**
|
|
2318
|
+
* The exclusion list DSH's own workspace search ships with. The rules are
|
|
2319
|
+
* literal names/paths, so the local page keeps the same results as the
|
|
2320
|
+
* native files page instead of searching `node_modules` or `.git`.
|
|
2321
|
+
*/
|
|
2322
|
+
const FILES_SEARCH_EXCLUDED = [
|
|
2323
|
+
'.git', '.hg', '.next', '.pnpm-store', '.svn', '.turbo', '.yarn', 'build', 'coverage', 'dist', 'node_modules',
|
|
2324
|
+
'.DS_Store', '.Spotlight-V100', '.Trashes', '.fseventsd', 'Thumbs.db', 'desktop.ini', '$RECYCLE.BIN', 'System Volume Information',
|
|
2325
|
+
]
|
|
2326
|
+
|
|
2191
2327
|
function filesExtension(name) {
|
|
2192
2328
|
const text = String(name || '')
|
|
2193
2329
|
const dot = text.lastIndexOf('.')
|
|
@@ -2261,6 +2397,15 @@ window.__ModuleLoader__.load({
|
|
|
2261
2397
|
return bytes
|
|
2262
2398
|
}
|
|
2263
2399
|
|
|
2400
|
+
/** Decode a UTF-8 payload from base64; used to show SVG source as text. */
|
|
2401
|
+
function decodeBase64Text(base64) {
|
|
2402
|
+
try {
|
|
2403
|
+
return new TextDecoder('utf-8', { fatal: false }).decode(filesBase64ToBytes(base64))
|
|
2404
|
+
} catch {
|
|
2405
|
+
return ''
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2264
2409
|
function filesLoadTransferRecords() {
|
|
2265
2410
|
try {
|
|
2266
2411
|
const raw = window.localStorage.getItem(FILES_TRANSFER_STORAGE_KEY)
|
|
@@ -2414,6 +2559,75 @@ window.__ModuleLoader__.load({
|
|
|
2414
2559
|
return { body: source.slice(match[0].length), source: match[1] }
|
|
2415
2560
|
}
|
|
2416
2561
|
|
|
2562
|
+
/**
|
|
2563
|
+
* Split delimited text into rows and cells. Handles quoted fields (with
|
|
2564
|
+
* doubled quotes and embedded newlines), CRLF, and a trailing newline. The
|
|
2565
|
+
* reader is bounded so a pathological file cannot stall the renderer.
|
|
2566
|
+
*/
|
|
2567
|
+
function parseDelimitedTable(source, separator) {
|
|
2568
|
+
const text = String(source || '').replace(/^\uFEFF/, '')
|
|
2569
|
+
const rows = []
|
|
2570
|
+
let row = []
|
|
2571
|
+
let cell = ''
|
|
2572
|
+
let quoted = false
|
|
2573
|
+
let index = 0
|
|
2574
|
+
const pushCell = () => { row.push(cell); cell = '' }
|
|
2575
|
+
const pushRow = () => {
|
|
2576
|
+
pushCell()
|
|
2577
|
+
rows.push(row)
|
|
2578
|
+
row = []
|
|
2579
|
+
return rows.length <= FILES_TABLE_MAX_ROWS
|
|
2580
|
+
}
|
|
2581
|
+
while (index < text.length) {
|
|
2582
|
+
const char = text[index]
|
|
2583
|
+
if (quoted) {
|
|
2584
|
+
if (char === '"') {
|
|
2585
|
+
if (text[index + 1] === '"') { cell += '"'; index += 2; continue }
|
|
2586
|
+
quoted = false
|
|
2587
|
+
index += 1
|
|
2588
|
+
continue
|
|
2589
|
+
}
|
|
2590
|
+
cell += char
|
|
2591
|
+
index += 1
|
|
2592
|
+
continue
|
|
2593
|
+
}
|
|
2594
|
+
if (char === '"' && cell === '') { quoted = true; index += 1; continue }
|
|
2595
|
+
if (char === separator) { pushCell(); index += 1; continue }
|
|
2596
|
+
if (char === '\r') { index += 1; continue }
|
|
2597
|
+
if (char === '\n') {
|
|
2598
|
+
if (!pushRow()) return { rows, truncated: true }
|
|
2599
|
+
index += 1
|
|
2600
|
+
continue
|
|
2601
|
+
}
|
|
2602
|
+
cell += char
|
|
2603
|
+
index += 1
|
|
2604
|
+
}
|
|
2605
|
+
if (cell !== '' || row.length > 0) pushRow()
|
|
2606
|
+
return { rows, truncated: rows.length > FILES_TABLE_MAX_ROWS }
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
/** Present a parsed table: header row plus a bounded body for rendering. */
|
|
2610
|
+
function buildDelimitedTable(source, separator) {
|
|
2611
|
+
const parsed = parseDelimitedTable(source, separator)
|
|
2612
|
+
const rows = parsed.rows
|
|
2613
|
+
if (rows.length === 0) return null
|
|
2614
|
+
const columnCount = Math.min(FILES_TABLE_MAX_COLUMNS, rows.reduce((max, cells) => Math.max(max, cells.length), 0))
|
|
2615
|
+
const visible = rows.length > FILES_TABLE_MAX_ROWS ? rows.slice(0, FILES_TABLE_MAX_ROWS) : rows
|
|
2616
|
+
const rowsTruncated = parsed.truncated || rows.length > visible.length
|
|
2617
|
+
const columnsTruncated = rows.some((cells) => cells.length > columnCount)
|
|
2618
|
+
return {
|
|
2619
|
+
header: visible[0].slice(0, columnCount),
|
|
2620
|
+
body: visible.slice(1).map((cells) => cells.slice(0, columnCount)),
|
|
2621
|
+
columnCount,
|
|
2622
|
+
rowCount: rows.length,
|
|
2623
|
+
/** Data rows, i.e. everything after the header line. */
|
|
2624
|
+
dataRowCount: Math.max(0, rows.length - 1),
|
|
2625
|
+
rowsTruncated,
|
|
2626
|
+
columnsTruncated,
|
|
2627
|
+
truncated: rowsTruncated || columnsTruncated,
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2417
2631
|
/** Read the opener's navigation params from a tab's framework information. */
|
|
2418
2632
|
function tabParamsOf(info) {
|
|
2419
2633
|
if (!info || !info.tab) return undefined
|
|
@@ -2442,6 +2656,8 @@ window.__ModuleLoader__.load({
|
|
|
2442
2656
|
const [meta, setMeta] = useState(null)
|
|
2443
2657
|
const [wrap, setWrap] = useState(true)
|
|
2444
2658
|
const [markdownSource, setMarkdownSource] = useState(false)
|
|
2659
|
+
const [tableSource, setTableSource] = useState(false)
|
|
2660
|
+
const [imageSource, setImageSource] = useState(false)
|
|
2445
2661
|
/** Workbench preview passes onClose; the sidebar preview tab does not. */
|
|
2446
2662
|
const onClose = typeof props.onClose === 'function' ? props.onClose : null
|
|
2447
2663
|
|
|
@@ -2456,8 +2672,14 @@ window.__ModuleLoader__.load({
|
|
|
2456
2672
|
const body = await post('/sftp/read', { ref: server, path, base64: true, maxBytes: 5 * 1024 * 1024 })
|
|
2457
2673
|
const result = body.result || {}
|
|
2458
2674
|
setMeta({ size: result.size, truncated: result.truncated === true })
|
|
2459
|
-
if (result.base64) {
|
|
2460
|
-
|
|
2675
|
+
if (result.base64) {
|
|
2676
|
+
setImageUrl(`data:${mime};base64,${result.base64}`)
|
|
2677
|
+
setBinary(false)
|
|
2678
|
+
// SVG is text, so keep its source available for the source view;
|
|
2679
|
+
// raster payloads are never decoded into text.
|
|
2680
|
+
setContent(filesExtension(name) === 'svg' ? decodeBase64Text(result.base64) : '')
|
|
2681
|
+
setImageSource(false)
|
|
2682
|
+
} else { setBinary(true); setContent(''); setError(t('files.viewerBinary')) }
|
|
2461
2683
|
} else {
|
|
2462
2684
|
const body = await post('/sftp/read', { ref: server, path, maxBytes: 1024 * 1024 })
|
|
2463
2685
|
const result = body.result || {}
|
|
@@ -2482,6 +2704,17 @@ window.__ModuleLoader__.load({
|
|
|
2482
2704
|
const lines = content ? content.split('\n') : []
|
|
2483
2705
|
const extension = filesExtension(name)
|
|
2484
2706
|
const isMarkdown = FILES_MARKDOWN_EXTENSIONS.includes(extension)
|
|
2707
|
+
const tableSeparator = FILES_TABLE_TYPES[extension]
|
|
2708
|
+
const isTable = Boolean(tableSeparator) && !binary && Boolean(content)
|
|
2709
|
+
const table = isTable && !tableSource ? buildDelimitedTable(content, tableSeparator) : null
|
|
2710
|
+
const isTextImage = extension === 'svg'
|
|
2711
|
+
const showImage = Boolean(imageUrl) && !(isTextImage && imageSource)
|
|
2712
|
+
const byteTruncated = meta && meta.truncated === true
|
|
2713
|
+
const truncatedHint = byteTruncated
|
|
2714
|
+
? tr('files.viewerTruncated', '文件较大,这里只显示开头一部分。')
|
|
2715
|
+
: ''
|
|
2716
|
+
/** A clipped bitmap/svg payload cannot be decoded, so say so instead of rendering a broken image. */
|
|
2717
|
+
const imageTruncated = showImage && byteTruncated
|
|
2485
2718
|
const MarkdownText = primitives && primitives.MarkdownText
|
|
2486
2719
|
const CodeBlock = primitives && primitives.CodeBlock
|
|
2487
2720
|
const markdownLabels = {
|
|
@@ -2516,13 +2749,31 @@ window.__ModuleLoader__.load({
|
|
|
2516
2749
|
style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 13 },
|
|
2517
2750
|
}, name),
|
|
2518
2751
|
meta && meta.size != null ? h('span', { style: { ...panelUi.fieldLabel, flexShrink: 0 } }, filesFormatBytes(meta.size)) : null,
|
|
2752
|
+
byteTruncated
|
|
2753
|
+
? h('span', {
|
|
2754
|
+
title: truncatedHint,
|
|
2755
|
+
style: { ...panelUi.fieldLabel, flexShrink: 0, color: 'var(--dsw-alias-state-warning-primary, inherit)' },
|
|
2756
|
+
}, t('files.truncated'))
|
|
2757
|
+
: null,
|
|
2519
2758
|
isMarkdown && !binary && !imageUrl && MarkdownText
|
|
2520
2759
|
? h('button', {
|
|
2521
2760
|
className: 'dsh-ssh-workbench-button dsh-ssh-workbench-button-ghost', style: { ...panelUi.ghostButton, flexShrink: 0 },
|
|
2522
2761
|
onClick: () => setMarkdownSource((value) => !value),
|
|
2523
2762
|
}, markdownSource ? t('files.previewMode') : t('files.sourceMode'))
|
|
2524
2763
|
: null,
|
|
2525
|
-
|
|
2764
|
+
isTable
|
|
2765
|
+
? h('button', {
|
|
2766
|
+
className: 'dsh-ssh-workbench-button dsh-ssh-workbench-button-ghost', style: { ...panelUi.ghostButton, flexShrink: 0 },
|
|
2767
|
+
onClick: () => setTableSource((value) => !value),
|
|
2768
|
+
}, tableSource ? t('files.tableMode') : t('files.sourceMode'))
|
|
2769
|
+
: null,
|
|
2770
|
+
isTextImage && imageUrl
|
|
2771
|
+
? h('button', {
|
|
2772
|
+
className: 'dsh-ssh-workbench-button dsh-ssh-workbench-button-ghost', style: { ...panelUi.ghostButton, flexShrink: 0 },
|
|
2773
|
+
onClick: () => setImageSource((value) => !value),
|
|
2774
|
+
}, imageSource ? t('files.imageMode') : t('files.sourceMode'))
|
|
2775
|
+
: null,
|
|
2776
|
+
!isMarkdown && !binary && !showImage && !table ? toolbar(wrap ? t('files.unwrap') : t('files.wrap'), icon(12, [h('line', { x1: 3, y1: 6, x2: 21, y2: 6 }), h('line', { x1: 3, y1: 12, x2: 21, y2: 12 }), h('line', { x1: 3, y1: 18, x2: 15, y2: 18 })]), () => setWrap((value) => !value)) : null,
|
|
2526
2777
|
toolbar(t('files.refresh'), refreshingIcon(), load),
|
|
2527
2778
|
onClose
|
|
2528
2779
|
? toolbar(t('common.close'), icon(12, [h('path', { d: 'M18 6L6 18' }), h('path', { d: 'M6 6l12 12' })]), onClose)
|
|
@@ -2531,9 +2782,58 @@ window.__ModuleLoader__.load({
|
|
|
2531
2782
|
h('div', { style: { flex: 1, minHeight: 0, overflow: 'auto', background: 'transparent' } },
|
|
2532
2783
|
loading ? h('div', { style: { padding: '24px 12px', color: 'var(--dsw-alias-label-tertiary, inherit)' } }, t('files.viewerLoading')) : null,
|
|
2533
2784
|
error ? h('div', { role: 'alert', style: { padding: '18px 12px', color: 'var(--dsw-alias-state-error-primary)' } }, error) : null,
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2785
|
+
imageTruncated
|
|
2786
|
+
? h('div', { style: { padding: '18px 14px', color: 'var(--dsw-alias-label-secondary, inherit)' } }, tr('files.viewerImageTruncated', '图片超过预览上限,无法完整显示;请下载后查看。'))
|
|
2787
|
+
: showImage
|
|
2788
|
+
? h('div', { style: { padding: 12, display: 'flex', justifyContent: 'center' } }, h('img', { src: imageUrl, alt: name, style: { maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' } }))
|
|
2789
|
+
: null,
|
|
2790
|
+
!loading && !error && !imageTruncated && !showImage && !binary
|
|
2791
|
+
? (table
|
|
2792
|
+
? h('div', { style: { padding: 12, overflow: 'auto' }, 'data-sdh-table-preview': path },
|
|
2793
|
+
h('table', {
|
|
2794
|
+
style: {
|
|
2795
|
+
borderCollapse: 'collapse', fontSize: 'var(--dsh-code-font-size, 12px)',
|
|
2796
|
+
fontFamily: 'var(--ds-font-family-code)', width: '100%',
|
|
2797
|
+
},
|
|
2798
|
+
},
|
|
2799
|
+
h('thead', null,
|
|
2800
|
+
h('tr', null, table.header.map((cell, index) => h('th', {
|
|
2801
|
+
key: index,
|
|
2802
|
+
style: {
|
|
2803
|
+
textAlign: 'left', fontWeight: 500, whiteSpace: 'nowrap', padding: '4px 10px',
|
|
2804
|
+
borderBottom: '0.5px solid var(--dsw-alias-border-l2, #ddd)',
|
|
2805
|
+
color: 'var(--dsw-alias-label-primary, inherit)',
|
|
2806
|
+
position: 'sticky', top: 0, background: 'var(--dsw-specific-menu, transparent)',
|
|
2807
|
+
},
|
|
2808
|
+
}, cell))),
|
|
2809
|
+
),
|
|
2810
|
+
h('tbody', null,
|
|
2811
|
+
table.body.map((cells, rowIndex) => h('tr', { key: rowIndex },
|
|
2812
|
+
cells.map((cell, cellIndex) => h('td', {
|
|
2813
|
+
key: cellIndex,
|
|
2814
|
+
style: {
|
|
2815
|
+
padding: '3px 10px', whiteSpace: 'nowrap', maxWidth: 320, overflow: 'hidden',
|
|
2816
|
+
textOverflow: 'ellipsis', borderBottom: '0.5px solid var(--dsw-alias-border-l3, #eee)',
|
|
2817
|
+
color: 'var(--dsw-alias-label-secondary, inherit)',
|
|
2818
|
+
},
|
|
2819
|
+
}, cell))),
|
|
2820
|
+
),
|
|
2821
|
+
),
|
|
2822
|
+
),
|
|
2823
|
+
table.truncated
|
|
2824
|
+
? h('div', { style: { paddingTop: 8, ...panelUi.fieldLabel } },
|
|
2825
|
+
table.rowsTruncated && table.columnsTruncated
|
|
2826
|
+
? tr('files.tableTruncatedBoth', '表格较大,只显示前 {rows} 行、前 {columns} 列。')
|
|
2827
|
+
.replace('{rows}', String(FILES_TABLE_MAX_ROWS))
|
|
2828
|
+
.replace('{columns}', String(FILES_TABLE_MAX_COLUMNS))
|
|
2829
|
+
: table.rowsTruncated
|
|
2830
|
+
? tr('files.tableTruncatedRows', '表格较大,只显示前 {rows} 行。')
|
|
2831
|
+
.replace('{rows}', String(FILES_TABLE_MAX_ROWS))
|
|
2832
|
+
: tr('files.tableTruncatedColumns', '表格较宽,只显示前 {columns} 列。')
|
|
2833
|
+
.replace('{columns}', String(FILES_TABLE_MAX_COLUMNS)))
|
|
2834
|
+
: null,
|
|
2835
|
+
)
|
|
2836
|
+
: isMarkdown && !markdownSource && MarkdownText != null
|
|
2537
2837
|
? h('div', {
|
|
2538
2838
|
style: { padding: '12px 16px', fontSize: 'var(--dsh-content-font-size, 14px)', lineHeight: 1.6, color: 'var(--dsw-alias-label-primary, inherit)' },
|
|
2539
2839
|
'data-sdh-markdown-preview': '',
|
|
@@ -2579,7 +2879,20 @@ window.__ModuleLoader__.load({
|
|
|
2579
2879
|
},
|
|
2580
2880
|
},
|
|
2581
2881
|
h('span', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, path),
|
|
2582
|
-
|
|
2882
|
+
// A clipped image has no meaningful line count, so the footer stays bare.
|
|
2883
|
+
imageTruncated
|
|
2884
|
+
? null
|
|
2885
|
+
: h('span', { style: { flexShrink: 0 } }, table
|
|
2886
|
+
? (byteTruncated
|
|
2887
|
+
? tr('files.tableRowsPartial', '前 {n} 行 · {columns} 列')
|
|
2888
|
+
.replace('{n}', String(table.dataRowCount))
|
|
2889
|
+
.replace('{columns}', String(table.columnCount))
|
|
2890
|
+
: tr('files.tableRows', '{n} 行 · {columns} 列')
|
|
2891
|
+
.replace('{n}', String(table.dataRowCount))
|
|
2892
|
+
.replace('{columns}', String(table.columnCount)))
|
|
2893
|
+
: truncatedHint
|
|
2894
|
+
? tr('files.linesPartial', '前 {n} 行').replace('{n}', String(lines.length))
|
|
2895
|
+
: tr('files.lines', '{n} 行').replace('{n}', String(lines.length))),
|
|
2583
2896
|
),
|
|
2584
2897
|
)
|
|
2585
2898
|
}
|
|
@@ -2624,6 +2937,10 @@ window.__ModuleLoader__.load({
|
|
|
2624
2937
|
const iconArrowLeft = () => icon(14, [h('line', { x1: 19, y1: 12, x2: 5, y2: 12 }), h('polyline', { points: '12 5 5 12 12 19' })])
|
|
2625
2938
|
const iconHome = () => icon(14, [h('path', { d: 'M4 10.5 12 4l8 6.5' }), h('path', { d: 'M6.5 9.5V20h11V9.5' })])
|
|
2626
2939
|
const iconArrowUp16 = () => icon(16, [h('line', { x1: 12, y1: 19, x2: 12, y2: 5 }), h('polyline', { points: '5 12 12 5 19 12' })])
|
|
2940
|
+
const iconRightUp = () => icon(14, [
|
|
2941
|
+
h('path', { d: 'M8 17 17 8' }),
|
|
2942
|
+
h('polyline', { points: '9.5 8 17 8 17 15.5' }),
|
|
2943
|
+
])
|
|
2627
2944
|
const iconMore = () => icon(14, [
|
|
2628
2945
|
h('circle', { cx: 5, cy: 12, r: 1.4, fill: 'currentColor', stroke: 'none' }),
|
|
2629
2946
|
h('circle', { cx: 12, cy: 12, r: 1.4, fill: 'currentColor', stroke: 'none' }),
|
|
@@ -2649,6 +2966,93 @@ window.__ModuleLoader__.load({
|
|
|
2649
2966
|
|
|
2650
2967
|
let filesPendingConflictResolve = null
|
|
2651
2968
|
|
|
2969
|
+
/**
|
|
2970
|
+
* Hand one Host-local path to the OS: `open` starts the associated app and
|
|
2971
|
+
* `reveal` shows it in Finder/Explorer. Only paths the Host can map back to
|
|
2972
|
+
* its own filesystem work, so callers must gate this to local browsing.
|
|
2973
|
+
*/
|
|
2974
|
+
async function openHostPath(path, action = 'open') {
|
|
2975
|
+
if (typeof path !== 'string' || !path) return false
|
|
2976
|
+
const session = sessionRemote
|
|
2977
|
+
if (!session || typeof session.openWorkspacePath !== 'function') return false
|
|
2978
|
+
try {
|
|
2979
|
+
const result = action === 'reveal'
|
|
2980
|
+
? await session.openWorkspacePath({ path, action: 'reveal' })
|
|
2981
|
+
: await session.openWorkspacePath({ path })
|
|
2982
|
+
return Boolean(result && result.ok)
|
|
2983
|
+
} catch {
|
|
2984
|
+
return false
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
/**
|
|
2989
|
+
* List the apps the Host would use for one path: the same call the official
|
|
2990
|
+
* "open in app" split button makes, so the menu matches DSH exactly.
|
|
2991
|
+
*/
|
|
2992
|
+
async function loadHostApplications(path) {
|
|
2993
|
+
const session = sessionRemote
|
|
2994
|
+
if (!session || typeof session.workspacePathApplications !== 'function') return []
|
|
2995
|
+
try {
|
|
2996
|
+
const result = await session.workspacePathApplications({ path })
|
|
2997
|
+
if (!result || result.ok !== true || !Array.isArray(result.value)) return []
|
|
2998
|
+
return result.value.filter((item) => item && typeof item.id === 'string' && typeof item.name === 'string')
|
|
2999
|
+
} catch {
|
|
3000
|
+
return []
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
/**
|
|
3005
|
+
* Workspace-wide search backed by the Host's ripgrep pass. Matches are
|
|
3006
|
+
* `/`-joined paths relative to the session workspace root.
|
|
3007
|
+
*/
|
|
3008
|
+
async function searchHostWorkspace(sessionId, request) {
|
|
3009
|
+
const files = workspaceFilesRemote
|
|
3010
|
+
if (!files || typeof files.search !== 'function') return null
|
|
3011
|
+
try {
|
|
3012
|
+
const result = await files.search(sessionId, request)
|
|
3013
|
+
if (!result || result.ok !== true) return null
|
|
3014
|
+
return result.value || null
|
|
3015
|
+
} catch {
|
|
3016
|
+
return null
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
/**
|
|
3021
|
+
* Native file rows expose the official `contextMenu` target family, so the
|
|
3022
|
+
* DSH context menu service resolves our actions the same way it resolves
|
|
3023
|
+
* workspace and document-preview files. The captured value carries only
|
|
3024
|
+
* JSON-safe row facts plus the id of the live panel that owns the handlers.
|
|
3025
|
+
*/
|
|
3026
|
+
const FILES_MENU_TARGET = `${NS}.file`
|
|
3027
|
+
let filesMenuSeq = 0
|
|
3028
|
+
/** Panel id → mutable box `{ menuActions }` filled on every render. */
|
|
3029
|
+
const filesMenuPanels = new Map()
|
|
3030
|
+
|
|
3031
|
+
function filesMenuEntryOf(value) {
|
|
3032
|
+
if (value == null || typeof value !== 'object') return null
|
|
3033
|
+
const path = typeof value.path === 'string' ? value.path : ''
|
|
3034
|
+
if (!path) return null
|
|
3035
|
+
return {
|
|
3036
|
+
path,
|
|
3037
|
+
name: typeof value.name === 'string' ? value.name : baseNameOf(path),
|
|
3038
|
+
type: value.type === 'dir' ? 'dir' : 'file',
|
|
3039
|
+
size: Number.isFinite(value.size) ? value.size : 0,
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
/** Build the menu actions for one captured row from its live panel. */
|
|
3044
|
+
function resolveFilesMenuActions(value) {
|
|
3045
|
+
const panelId = value && typeof value.panel === 'string' ? value.panel : ''
|
|
3046
|
+
// The map stores the box itself, so the handlers sit directly on it.
|
|
3047
|
+
const box = panelId ? filesMenuPanels.get(panelId) : null
|
|
3048
|
+
if (!box || typeof box.menuActions !== 'function') return []
|
|
3049
|
+
try {
|
|
3050
|
+
return box.menuActions(value) || []
|
|
3051
|
+
} catch {
|
|
3052
|
+
return []
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
|
|
2652
3056
|
function RemoteFilesPanel(props) {
|
|
2653
3057
|
// Hierarchical browser (Luminax `remote-file-tree` behaviour) rendered in
|
|
2654
3058
|
// the native Files chrome: one directory per view, breadcrumb navigation,
|
|
@@ -2663,6 +3067,21 @@ window.__ModuleLoader__.load({
|
|
|
2663
3067
|
const initialPath = typeof props.initialPath === 'string' && props.initialPath ? props.initialPath : '/'
|
|
2664
3068
|
const navRoot = typeof props.rootPath === 'string' && props.rootPath ? props.rootPath : '/'
|
|
2665
3069
|
const rootLabel = typeof props.rootLabel === 'string' && props.rootLabel ? props.rootLabel : (navRoot === '/' ? '/' : navRoot)
|
|
3070
|
+
const lockRoot = props.lockRoot === true
|
|
3071
|
+
/**
|
|
3072
|
+
* Local hierarchy page: it browses the Host's own disk, so uploads, new
|
|
3073
|
+
* folders and the transfer log — which exist for slow remote copies —
|
|
3074
|
+
* only add noise. The page keeps navigation, refresh, hidden files and
|
|
3075
|
+
* gains "open in app" instead.
|
|
3076
|
+
*/
|
|
3077
|
+
const localChrome = localMode && props.openNative === true
|
|
3078
|
+
const withinRoot = (value) => {
|
|
3079
|
+
const normalize = (input) => String(input || '/').replace(/[\\/]+/g, '/').replace(/\/+$/, '') || '/'
|
|
3080
|
+
const root = normalize(navRoot)
|
|
3081
|
+
const next = normalize(value)
|
|
3082
|
+
if (root === '/') return next.startsWith('/')
|
|
3083
|
+
return next === root || next.startsWith(`${root}/`)
|
|
3084
|
+
}
|
|
2666
3085
|
const [servers, setServers] = useState([])
|
|
2667
3086
|
const [server, setServer] = useState(localMode ? (controlledServer ?? '__local__') : (controlledServer ?? ''))
|
|
2668
3087
|
const [dir, setDir] = useState(initialPath)
|
|
@@ -2688,11 +3107,34 @@ window.__ModuleLoader__.load({
|
|
|
2688
3107
|
const [showHidden, setShowHidden] = useState(() => {
|
|
2689
3108
|
try { return window.localStorage.getItem('dsh-ssh-workbench.files.showHidden') === '1' } catch { return false }
|
|
2690
3109
|
})
|
|
3110
|
+
/** Local hierarchy page only: workspace-wide search driven by ripgrep on the Host. */
|
|
3111
|
+
const [searchOpen, setSearchOpen] = useState(false)
|
|
3112
|
+
const [query, setQuery] = useState('')
|
|
3113
|
+
const [searchState, setSearchState] = useState(null)
|
|
3114
|
+
const searchSeqRef = useRef(0)
|
|
3115
|
+
/** Local hierarchy page only: the Host's own app list for "open in app". */
|
|
3116
|
+
const [hostApps, setHostApps] = useState(null)
|
|
3117
|
+
const [appsOpen, setAppsOpen] = useState(false)
|
|
2691
3118
|
const Menu = primitives && primitives.Menu
|
|
2692
3119
|
const ChevronIcon = primitives && primitives.IconChevronDownOutlineRegular
|
|
2693
3120
|
const RefreshIcon = primitives && primitives.IconRefreshOutlineRegular
|
|
2694
3121
|
/** Newest directory request wins; older responses are discarded (M8/R2). */
|
|
2695
3122
|
const loadSeqRef = useRef(0)
|
|
3123
|
+
/**
|
|
3124
|
+
* The official context menu resolves rows by target value, so each panel
|
|
3125
|
+
* publishes its handlers under a stable id while it is mounted.
|
|
3126
|
+
*/
|
|
3127
|
+
const panelIdRef = useRef('')
|
|
3128
|
+
if (!panelIdRef.current) panelIdRef.current = `files-${++filesMenuSeq}`
|
|
3129
|
+
const menuBoxRef = useRef({ menuActions: null })
|
|
3130
|
+
// Publish during render so an already-open menu sees fresh handlers, and
|
|
3131
|
+
// re-publish from the effect because StrictMode unmounts once on mount.
|
|
3132
|
+
filesMenuPanels.set(panelIdRef.current, menuBoxRef.current)
|
|
3133
|
+
useEffect(() => {
|
|
3134
|
+
const id = panelIdRef.current
|
|
3135
|
+
filesMenuPanels.set(id, menuBoxRef.current)
|
|
3136
|
+
return () => { filesMenuPanels.delete(id) }
|
|
3137
|
+
}, [])
|
|
2696
3138
|
|
|
2697
3139
|
useEffect(() => {
|
|
2698
3140
|
if (localMode) {
|
|
@@ -2757,9 +3199,62 @@ window.__ModuleLoader__.load({
|
|
|
2757
3199
|
void loadDir(server, dir)
|
|
2758
3200
|
}, [server, dir, loadDir])
|
|
2759
3201
|
|
|
3202
|
+
// Local hierarchy page: the Host's own app list, so "open in app" can
|
|
3203
|
+
// offer the same menu as DSH's native split button.
|
|
3204
|
+
useEffect(() => {
|
|
3205
|
+
if (!localChrome) return undefined
|
|
3206
|
+
let live = true
|
|
3207
|
+
void loadHostApplications(dir).then((list) => { if (live) setHostApps(list) })
|
|
3208
|
+
return () => { live = false }
|
|
3209
|
+
}, [localChrome, dir])
|
|
3210
|
+
|
|
3211
|
+
// Local hierarchy page: one workspace-wide search per settled query; a
|
|
3212
|
+
// newer query aborts the answer of the older one.
|
|
3213
|
+
useEffect(() => {
|
|
3214
|
+
if (!localChrome || !searchOpen) return undefined
|
|
3215
|
+
const text = String(query || '').trim()
|
|
3216
|
+
if (!text) {
|
|
3217
|
+
searchSeqRef.current += 1
|
|
3218
|
+
setSearchState(null)
|
|
3219
|
+
return undefined
|
|
3220
|
+
}
|
|
3221
|
+
if (typeof props.sessionId !== 'string' || !props.sessionId) return undefined
|
|
3222
|
+
const seq = searchSeqRef.current + 1
|
|
3223
|
+
searchSeqRef.current = seq
|
|
3224
|
+
setSearchState({ loading: true, matches: [], truncated: false, incomplete: false })
|
|
3225
|
+
const timer = setTimeout(() => {
|
|
3226
|
+
void searchHostWorkspace(props.sessionId, {
|
|
3227
|
+
query: text,
|
|
3228
|
+
excludedPaths: FILES_SEARCH_EXCLUDED,
|
|
3229
|
+
includeHidden: showHidden === true,
|
|
3230
|
+
respectGitignore: true,
|
|
3231
|
+
}).then((answer) => {
|
|
3232
|
+
if (seq !== searchSeqRef.current) return
|
|
3233
|
+
if (!answer) {
|
|
3234
|
+
setSearchState({ loading: false, failed: true, matches: [], truncated: false, incomplete: false })
|
|
3235
|
+
return
|
|
3236
|
+
}
|
|
3237
|
+
setSearchState({
|
|
3238
|
+
loading: false,
|
|
3239
|
+
failed: false,
|
|
3240
|
+
matches: Array.isArray(answer.matches) ? answer.matches : [],
|
|
3241
|
+
truncated: answer.truncated === true,
|
|
3242
|
+
incomplete: answer.incomplete === true,
|
|
3243
|
+
})
|
|
3244
|
+
})
|
|
3245
|
+
}, 180)
|
|
3246
|
+
return () => { clearTimeout(timer) }
|
|
3247
|
+
}, [localChrome, searchOpen, query, showHidden, props.sessionId])
|
|
3248
|
+
|
|
3249
|
+
useEffect(() => () => { searchSeqRef.current += 1 }, [])
|
|
3250
|
+
|
|
2760
3251
|
const navigate = (next) => {
|
|
2761
3252
|
const target = String(next || '/').trim() || '/'
|
|
2762
3253
|
if (target === dir) return
|
|
3254
|
+
if (lockRoot && !withinRoot(target)) {
|
|
3255
|
+
setError(t('files.outsideRoot'))
|
|
3256
|
+
return
|
|
3257
|
+
}
|
|
2763
3258
|
setBackStack((stack) => [...stack, dir])
|
|
2764
3259
|
setDir(target)
|
|
2765
3260
|
setNotice('')
|
|
@@ -2813,7 +3308,26 @@ window.__ModuleLoader__.load({
|
|
|
2813
3308
|
})
|
|
2814
3309
|
}
|
|
2815
3310
|
const openFile = (entry) => {
|
|
2816
|
-
const actions =
|
|
3311
|
+
const actions = (props.tabInfo && props.tabInfo.tab && props.tabInfo.tab.actions) || props.actions
|
|
3312
|
+
// In the takeover of DSH's native files page, prefer the official
|
|
3313
|
+
// resource protocol so the official preview (markdown/code/image/pdf)
|
|
3314
|
+
// opens; the address has to be session-scoped and relative to cwd.
|
|
3315
|
+
if (props.openNative === true && actions && typeof actions.openResource === 'function' && typeof props.sessionId === 'string' && typeof props.rootPath === 'string') {
|
|
3316
|
+
const root = String(props.rootPath).replace(/[\\/]+$/, '')
|
|
3317
|
+
const full = String(entry.path)
|
|
3318
|
+
const rootLower = root.toLowerCase()
|
|
3319
|
+
const fullLower = full.toLowerCase()
|
|
3320
|
+
if (fullLower.startsWith(`${rootLower}/`) || fullLower.startsWith(`${rootLower}\\`)) {
|
|
3321
|
+
const relative = full.slice(root.length).replace(/^[\\/]+/, '')
|
|
3322
|
+
if (relative) {
|
|
3323
|
+
const address = `dsh-resource://file/session/${encodeURIComponent(props.sessionId)}/${relative.split(/[\\/]+/).filter(Boolean).map((part) => encodeURIComponent(part)).join('/')}`
|
|
3324
|
+
const panel = props.tabInfo && props.tabInfo.panel
|
|
3325
|
+
if (panel && panel.isFloating !== true && panel.id) actions.openResource(address, { paneId: panel.id })
|
|
3326
|
+
else actions.openResource(address)
|
|
3327
|
+
return
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
2817
3331
|
if (!actions || typeof actions.openTab !== 'function') {
|
|
2818
3332
|
setError(t('files.openUnavailable'))
|
|
2819
3333
|
return
|
|
@@ -3325,6 +3839,7 @@ window.__ModuleLoader__.load({
|
|
|
3325
3839
|
const handleRowAction = (entry, action) => {
|
|
3326
3840
|
if (action === 'download') { void downloadFile(entry); return }
|
|
3327
3841
|
if (action === 'copyFile') { void copyFileEntry(entry); return }
|
|
3842
|
+
if (action === 'openInApp') { void openHostPath(entry.path); return }
|
|
3328
3843
|
if (action === 'copyPath') { void copyPathValue(entry.path); return }
|
|
3329
3844
|
if (action === 'rename') { setRenameTarget(entry); setRenameValue(entry.name); return }
|
|
3330
3845
|
if (action === 'delete') { setDeleteTarget(entry); return }
|
|
@@ -3332,13 +3847,107 @@ window.__ModuleLoader__.load({
|
|
|
3332
3847
|
const rowMenuItems = (entry) => {
|
|
3333
3848
|
const isDir = entry.type === 'dir'
|
|
3334
3849
|
const items = []
|
|
3335
|
-
if (!isDir) items.push({ id: 'download', label: t('files.downloadFile') })
|
|
3336
|
-
if (!isDir) items.push({ id: 'copyFile', label: t('files.copyFile') })
|
|
3850
|
+
if (!isDir && !localChrome) items.push({ id: 'download', label: t('files.downloadFile') })
|
|
3851
|
+
if (!isDir && !localChrome) items.push({ id: 'copyFile', label: t('files.copyFile') })
|
|
3852
|
+
if (localChrome) items.push({ id: 'openInApp', label: t('files.openInApp') })
|
|
3337
3853
|
items.push({ id: 'copyPath', label: t('files.copyPathEntry') })
|
|
3338
3854
|
items.push({ id: 'rename', label: t('files.rename') })
|
|
3339
3855
|
items.push({ id: 'delete', label: t('files.delete') })
|
|
3340
3856
|
return items
|
|
3341
3857
|
}
|
|
3858
|
+
/** Run one row action, turning a rejection into the inline error line. */
|
|
3859
|
+
const runRowAction = (entry, action) => {
|
|
3860
|
+
try {
|
|
3861
|
+
handleRowAction(entry, action)
|
|
3862
|
+
} catch (cause) {
|
|
3863
|
+
setError(t('files.actionFailed', { message: messageOf(cause) }))
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
/**
|
|
3867
|
+
* Official `MenuAction[]` for one row. The context menu service resolves
|
|
3868
|
+
* these lazily, so labels are evaluated at open time and stay localized.
|
|
3869
|
+
*/
|
|
3870
|
+
const rowMenuActions = (entry) => {
|
|
3871
|
+
const isDir = entry.type === 'dir'
|
|
3872
|
+
const actions = []
|
|
3873
|
+
if (!isDir) {
|
|
3874
|
+
actions.push({
|
|
3875
|
+
id: 'open',
|
|
3876
|
+
label: t('files.openFile'),
|
|
3877
|
+
run: () => openFile(entry),
|
|
3878
|
+
})
|
|
3879
|
+
}
|
|
3880
|
+
if (!isDir && !localChrome) {
|
|
3881
|
+
actions.push({
|
|
3882
|
+
id: 'download',
|
|
3883
|
+
label: t('files.downloadFile'),
|
|
3884
|
+
separatorBefore: actions.length > 0,
|
|
3885
|
+
run: () => runRowAction(entry, 'download'),
|
|
3886
|
+
})
|
|
3887
|
+
actions.push({
|
|
3888
|
+
id: 'copyFile',
|
|
3889
|
+
label: t('files.copyFile'),
|
|
3890
|
+
run: () => runRowAction(entry, 'copyFile'),
|
|
3891
|
+
})
|
|
3892
|
+
}
|
|
3893
|
+
if (localChrome) {
|
|
3894
|
+
actions.push({
|
|
3895
|
+
id: 'openInApp',
|
|
3896
|
+
label: t('files.openInApp'),
|
|
3897
|
+
separatorBefore: actions.length > 0,
|
|
3898
|
+
run: () => runRowAction(entry, 'openInApp'),
|
|
3899
|
+
})
|
|
3900
|
+
}
|
|
3901
|
+
actions.push({
|
|
3902
|
+
id: 'copyPath',
|
|
3903
|
+
label: t('files.copyPathEntry'),
|
|
3904
|
+
separatorBefore: actions.length > 0,
|
|
3905
|
+
run: () => runRowAction(entry, 'copyPath'),
|
|
3906
|
+
})
|
|
3907
|
+
actions.push({
|
|
3908
|
+
id: 'rename',
|
|
3909
|
+
label: t('files.rename'),
|
|
3910
|
+
run: () => runRowAction(entry, 'rename'),
|
|
3911
|
+
})
|
|
3912
|
+
actions.push({
|
|
3913
|
+
id: 'delete',
|
|
3914
|
+
label: t('files.delete'),
|
|
3915
|
+
danger: true,
|
|
3916
|
+
separatorBefore: true,
|
|
3917
|
+
run: () => runRowAction(entry, 'delete'),
|
|
3918
|
+
})
|
|
3919
|
+
return actions
|
|
3920
|
+
}
|
|
3921
|
+
/** What the context menu service captures for one row. */
|
|
3922
|
+
const menuTargetOf = (entry) => ({
|
|
3923
|
+
kind: FILES_MENU_TARGET,
|
|
3924
|
+
value: {
|
|
3925
|
+
panel: panelIdRef.current,
|
|
3926
|
+
path: entry.path,
|
|
3927
|
+
name: entry.name,
|
|
3928
|
+
type: entry.type === 'dir' ? 'dir' : 'file',
|
|
3929
|
+
size: Number.isFinite(entry.size) ? entry.size : 0,
|
|
3930
|
+
},
|
|
3931
|
+
})
|
|
3932
|
+
const useContextMenu = props.useContextMenu
|
|
3933
|
+
const renderFactorySlot = props.renderFactorySlot
|
|
3934
|
+
const getTriggerProps = (() => {
|
|
3935
|
+
if (typeof useContextMenu !== 'function') return null
|
|
3936
|
+
try {
|
|
3937
|
+
const bindings = useContextMenu()
|
|
3938
|
+
return bindings && typeof bindings.getTriggerProps === 'function' ? bindings.getTriggerProps : null
|
|
3939
|
+
} catch {
|
|
3940
|
+
return null
|
|
3941
|
+
}
|
|
3942
|
+
})()
|
|
3943
|
+
/**
|
|
3944
|
+
* Publish row handlers for the context menu service. Reassigned on every
|
|
3945
|
+
* render so the closure always sees the latest directory and state.
|
|
3946
|
+
*/
|
|
3947
|
+
menuBoxRef.current.menuActions = (value) => {
|
|
3948
|
+
const row = filesMenuEntryOf(value)
|
|
3949
|
+
return row ? rowMenuActions(row) : []
|
|
3950
|
+
}
|
|
3342
3951
|
const renderRowMenu = (entry) => {
|
|
3343
3952
|
const moreButton = h('button', {
|
|
3344
3953
|
type: 'button',
|
|
@@ -3350,6 +3959,22 @@ window.__ModuleLoader__.load({
|
|
|
3350
3959
|
setRowMenuPath((current) => (current === entry.path ? '' : entry.path))
|
|
3351
3960
|
},
|
|
3352
3961
|
}, iconMore())
|
|
3962
|
+
const menuTarget = menuTargetOf(entry)
|
|
3963
|
+
// Prefer the official ellipsis trigger so right-click, Shift+F10 and the
|
|
3964
|
+
// button share one menu surface; fall back to our own Menu when the
|
|
3965
|
+
// renderer does not expose the factory slot.
|
|
3966
|
+
if (typeof renderFactorySlot === 'function') {
|
|
3967
|
+
const official = renderFactorySlot('contextMenu.button', {
|
|
3968
|
+
target: menuTarget,
|
|
3969
|
+
label: t('files.more'),
|
|
3970
|
+
className: 'dsh-ssh-workbench-row-more',
|
|
3971
|
+
}, {
|
|
3972
|
+
fallback: h('span', { className: 'dsh-ssh-workbench-row-actions' }, moreButton),
|
|
3973
|
+
})
|
|
3974
|
+
if (official != null) {
|
|
3975
|
+
return h('span', { className: 'dsh-ssh-workbench-row-actions' }, official)
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3353
3978
|
if (Menu == null) return null
|
|
3354
3979
|
return h(Menu, {
|
|
3355
3980
|
open: rowMenuPath === entry.path,
|
|
@@ -3460,6 +4085,167 @@ window.__ModuleLoader__.load({
|
|
|
3460
4085
|
...extra,
|
|
3461
4086
|
}, glyph)
|
|
3462
4087
|
|
|
4088
|
+
// ── local hierarchy page: search + "open in app" split button ───────
|
|
4089
|
+
// DSH's native files page ships a reload, a search toggle and an
|
|
4090
|
+
// "open in app" split button; the takeover keeps the same footprint and
|
|
4091
|
+
// only adds the hierarchy navigation around it.
|
|
4092
|
+
const searchActive = localChrome && searchOpen
|
|
4093
|
+
const searchText = String(query || '').trim()
|
|
4094
|
+
const searchResults = searchActive && searchText ? searchState : null
|
|
4095
|
+
const searchToggle = localChrome
|
|
4096
|
+
? tool(t('files.search'), iconSearch(), () => {
|
|
4097
|
+
setSearchOpen((value) => {
|
|
4098
|
+
const next = !value
|
|
4099
|
+
if (!next) setQuery('')
|
|
4100
|
+
return next
|
|
4101
|
+
})
|
|
4102
|
+
}, !server, { 'aria-pressed': searchOpen, 'data-sdh-files-search-toggle': '' })
|
|
4103
|
+
: null
|
|
4104
|
+
const searchBar = searchActive
|
|
4105
|
+
? h('div', { 'data-sdh-files-search-bar': '', className: 'dsh-ssh-workbench-files-search' },
|
|
4106
|
+
h('div', { className: 'dsh-ssh-workbench-files-search-field' },
|
|
4107
|
+
h('span', { className: 'dsh-ssh-workbench-files-search-glyph' }, iconSearch()),
|
|
4108
|
+
h('input', {
|
|
4109
|
+
autoFocus: true,
|
|
4110
|
+
className: 'dsh-ssh-workbench-files-search-input',
|
|
4111
|
+
value: query,
|
|
4112
|
+
placeholder: t('files.searchPlaceholder'),
|
|
4113
|
+
'aria-label': t('files.search'),
|
|
4114
|
+
onChange: (event) => setQuery(event.target.value),
|
|
4115
|
+
onKeyDown: (event) => {
|
|
4116
|
+
if (event.key !== 'Escape') return
|
|
4117
|
+
event.preventDefault()
|
|
4118
|
+
setSearchOpen(false)
|
|
4119
|
+
setQuery('')
|
|
4120
|
+
},
|
|
4121
|
+
}),
|
|
4122
|
+
query
|
|
4123
|
+
? h('button', {
|
|
4124
|
+
type: 'button',
|
|
4125
|
+
className: 'dsh-ssh-workbench-files-search-clear',
|
|
4126
|
+
title: t('files.searchClear'),
|
|
4127
|
+
'aria-label': t('files.searchClear'),
|
|
4128
|
+
onClick: () => setQuery(''),
|
|
4129
|
+
}, iconClose())
|
|
4130
|
+
: null,
|
|
4131
|
+
),
|
|
4132
|
+
)
|
|
4133
|
+
: null
|
|
4134
|
+
const searchResultPath = (match) => {
|
|
4135
|
+
const relative = String(match.path || '').replace(/^[\\/]+/, '')
|
|
4136
|
+
if (!relative) return dir
|
|
4137
|
+
const root = String(navRoot || '/').replace(/[\\/]+$/, '')
|
|
4138
|
+
return root === '' || root === '/' ? `/${relative}` : `${root}/${relative}`
|
|
4139
|
+
}
|
|
4140
|
+
const searchView = searchResults
|
|
4141
|
+
? h('div', { 'data-sdh-files-search-results': '', style: { display: 'flex', flexDirection: 'column' } },
|
|
4142
|
+
searchResults.loading
|
|
4143
|
+
? h('div', { className: 'dsh-ssh-workbench-empty' }, t('files.searching'))
|
|
4144
|
+
: searchResults.failed
|
|
4145
|
+
? h('div', { className: 'dsh-ssh-workbench-empty' }, t('files.searchFailed'))
|
|
4146
|
+
: searchResults.matches.length === 0
|
|
4147
|
+
? h('div', { className: 'dsh-ssh-workbench-empty' }, t('files.searchEmpty'))
|
|
4148
|
+
: searchResults.matches.map((match) => {
|
|
4149
|
+
const isDir = match.type === 'directory'
|
|
4150
|
+
const full = searchResultPath(match)
|
|
4151
|
+
const relative = String(match.path || '')
|
|
4152
|
+
const depth = relative.split('/').length - 1
|
|
4153
|
+
const name = baseNameOf(relative) || relative
|
|
4154
|
+
const entry = { path: full, name, type: isDir ? 'dir' : 'file' }
|
|
4155
|
+
return h('div', {
|
|
4156
|
+
key: relative,
|
|
4157
|
+
className: 'dsh-ssh-workbench-files-row',
|
|
4158
|
+
'data-sdh-files-search-row': isDir ? 'dir' : 'file',
|
|
4159
|
+
'data-sdh-files-path': full,
|
|
4160
|
+
role: 'button',
|
|
4161
|
+
tabIndex: 0,
|
|
4162
|
+
title: full,
|
|
4163
|
+
style: { paddingLeft: 10 + depth * 18 },
|
|
4164
|
+
onClick: () => { if (isDir) { setSearchOpen(false); setQuery(''); navigate(full) } else openFile(entry) },
|
|
4165
|
+
onKeyDown: (event) => {
|
|
4166
|
+
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
4167
|
+
event.preventDefault()
|
|
4168
|
+
if (isDir) { setSearchOpen(false); setQuery(''); navigate(full) } else openFile(entry)
|
|
4169
|
+
},
|
|
4170
|
+
},
|
|
4171
|
+
h('span', { style: { display: 'inline-flex', flexShrink: 0, color: isDir ? 'var(--dsw-alias-label-tertiary)' : undefined } }, isDir ? nativeFolderIcon(false) : nativeFileIcon(name)),
|
|
4172
|
+
h('span', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, name),
|
|
4173
|
+
h('span', { className: 'dsh-ssh-workbench-files-search-dir', title: full }, relative.includes('/') ? relative.slice(0, relative.lastIndexOf('/')) : ''),
|
|
4174
|
+
)
|
|
4175
|
+
}),
|
|
4176
|
+
searchResults.matches.length > 0 && (searchResults.truncated || searchResults.incomplete)
|
|
4177
|
+
? h('div', { className: 'dsh-ssh-workbench-files-search-note' }, searchResults.truncated ? t('files.searchTruncated') : t('files.searchIncomplete'))
|
|
4178
|
+
: null,
|
|
4179
|
+
)
|
|
4180
|
+
: null
|
|
4181
|
+
const appList = localChrome && Array.isArray(hostApps) ? hostApps : []
|
|
4182
|
+
const defaultApp = appList.find((app) => app.default === true) || appList[0] || null
|
|
4183
|
+
const openInDefault = () => {
|
|
4184
|
+
if (!appList.length) { void openHostPath(dir); return }
|
|
4185
|
+
const target = defaultApp && typeof defaultApp.id === 'string' ? defaultApp.id : null
|
|
4186
|
+
if (!sessionRemote || typeof sessionRemote.openWorkspacePath !== 'function') return
|
|
4187
|
+
const request = target ? { path: dir, application: target } : { path: dir }
|
|
4188
|
+
void Promise.resolve(sessionRemote.openWorkspacePath(request)).catch(() => false)
|
|
4189
|
+
}
|
|
4190
|
+
const appsAnchor = h('div', { style: { display: 'inline-flex' } },
|
|
4191
|
+
h('div', { className: 'dsh-ssh-workbench-files-split', 'data-sdh-open-in-app': '' },
|
|
4192
|
+
h('button', {
|
|
4193
|
+
type: 'button',
|
|
4194
|
+
className: 'main',
|
|
4195
|
+
title: defaultApp ? t('files.openInAppWith', { app: defaultApp.name }) : t('files.openInApp'),
|
|
4196
|
+
'aria-label': defaultApp ? t('files.openInAppWith', { app: defaultApp.name }) : t('files.openInApp'),
|
|
4197
|
+
disabled: !server,
|
|
4198
|
+
onClick: openInDefault,
|
|
4199
|
+
},
|
|
4200
|
+
defaultApp && defaultApp.icon
|
|
4201
|
+
? h('img', { src: defaultApp.icon, alt: '', width: 14, height: 14, style: { display: 'block', flexShrink: 0 } })
|
|
4202
|
+
: iconRightUp(),
|
|
4203
|
+
h('span', { style: { maxWidth: 92, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
|
|
4204
|
+
defaultApp ? defaultApp.name : t('files.openInApp')),
|
|
4205
|
+
),
|
|
4206
|
+
h('button', {
|
|
4207
|
+
type: 'button',
|
|
4208
|
+
className: 'chevron',
|
|
4209
|
+
title: t('files.openInAppMore'),
|
|
4210
|
+
'aria-label': t('files.openInAppMore'),
|
|
4211
|
+
'aria-haspopup': 'menu',
|
|
4212
|
+
'aria-expanded': appsOpen,
|
|
4213
|
+
disabled: !server,
|
|
4214
|
+
onClick: () => setAppsOpen((value) => !value),
|
|
4215
|
+
}, ChevronIcon ? h(ChevronIcon, { size: 12 }) : icon(12, [h('polyline', { points: '6 9 12 15 18 9' })])),
|
|
4216
|
+
),
|
|
4217
|
+
)
|
|
4218
|
+
const appsMenu = Menu != null && appsAnchor && appList.length > 0
|
|
4219
|
+
? h(Menu, {
|
|
4220
|
+
open: appsOpen,
|
|
4221
|
+
autoFocus: true,
|
|
4222
|
+
portal: true,
|
|
4223
|
+
dense: true,
|
|
4224
|
+
align: 'end',
|
|
4225
|
+
onClose: () => setAppsOpen(false),
|
|
4226
|
+
items: appList.map((app) => ({
|
|
4227
|
+
id: app.id,
|
|
4228
|
+
icon: app.icon ? h('img', { src: app.icon, alt: '', width: 16, height: 16, style: { display: 'block' } }) : undefined,
|
|
4229
|
+
label: app.default === true ? t('files.openInAppDefault', { app: app.name }) : app.name,
|
|
4230
|
+
})),
|
|
4231
|
+
onSelect: (id) => {
|
|
4232
|
+
setAppsOpen(false)
|
|
4233
|
+
if (!sessionRemote || typeof sessionRemote.openWorkspacePath !== 'function') return
|
|
4234
|
+
void Promise.resolve(sessionRemote.openWorkspacePath({ path: dir, application: id })).catch(() => false)
|
|
4235
|
+
},
|
|
4236
|
+
anchor: appsAnchor,
|
|
4237
|
+
})
|
|
4238
|
+
: null
|
|
4239
|
+
// `primitives.Menu` renders its own anchor, so hand it the split button
|
|
4240
|
+
// rather than rendering the anchor a second time next to the menu. With
|
|
4241
|
+
// no app list there is nothing to choose from, so fall back to a plain
|
|
4242
|
+
// "open in app" button that uses the system default.
|
|
4243
|
+
const appsControl = !localChrome
|
|
4244
|
+
? null
|
|
4245
|
+
: appsMenu
|
|
4246
|
+
? appsMenu
|
|
4247
|
+
: tool(t('files.openInApp'), iconRightUp(), () => { void openHostPath(dir) }, !server)
|
|
4248
|
+
|
|
3463
4249
|
const transferBar = transfer
|
|
3464
4250
|
? h('div', {
|
|
3465
4251
|
'data-sdh-transfer': transfer.kind,
|
|
@@ -3729,20 +4515,31 @@ window.__ModuleLoader__.load({
|
|
|
3729
4515
|
})
|
|
3730
4516
|
: null,
|
|
3731
4517
|
),
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
4518
|
+
localChrome
|
|
4519
|
+
? null
|
|
4520
|
+
: h('input', {
|
|
4521
|
+
id: 'dsh-ssh-workbench-files-upload',
|
|
4522
|
+
type: 'file',
|
|
4523
|
+
multiple: true,
|
|
4524
|
+
style: { display: 'none' },
|
|
4525
|
+
onChange: onPickedFiles,
|
|
4526
|
+
}),
|
|
4527
|
+
localChrome
|
|
4528
|
+
? null
|
|
4529
|
+
: tool(t('files.upload'), iconUpload(), () => { setResumeRecord(null); triggerUploadInput() }, !server || transfer != null),
|
|
4530
|
+
localChrome
|
|
4531
|
+
? null
|
|
4532
|
+
: tool(t('files.newFolder'), iconFolderPlus(), () => setDialog({ value: '' }), !server || transfer != null),
|
|
3741
4533
|
tool(showHidden ? t('files.hideHidden') : t('files.showHidden'), iconEye(showHidden), toggleHidden, !server, { 'aria-pressed': showHidden }),
|
|
3742
4534
|
tool(t('files.refresh'), RefreshIcon ? h(RefreshIcon, { size: 15 }) : refreshingIcon(), () => loadDir(server, dir, true), !server),
|
|
3743
|
-
|
|
4535
|
+
searchToggle,
|
|
4536
|
+
localChrome
|
|
4537
|
+
? null
|
|
4538
|
+
: tool(t('files.history'), iconHistory(), () => setHistoryOpen((value) => !value), !server),
|
|
3744
4539
|
hideServerPicker ? null : picker,
|
|
4540
|
+
appsControl,
|
|
3745
4541
|
),
|
|
4542
|
+
searchBar,
|
|
3746
4543
|
error ? h('div', { role: 'alert', style: { padding: '6px 10px 0', color: 'var(--dsw-alias-state-error-primary)', fontSize: 12, lineHeight: '18px', wordBreak: 'break-all' } }, error) : null,
|
|
3747
4544
|
notice ? h('div', { role: 'status', style: { padding: '6px 10px 0', color: 'var(--dsw-alias-label-secondary, inherit)', fontSize: 12, lineHeight: '18px' } }, notice) : null,
|
|
3748
4545
|
transferBar,
|
|
@@ -3756,7 +4553,9 @@ window.__ModuleLoader__.load({
|
|
|
3756
4553
|
)
|
|
3757
4554
|
: historyOpen
|
|
3758
4555
|
? historyPanel
|
|
3759
|
-
:
|
|
4556
|
+
: searchView
|
|
4557
|
+
? h('div', { style: { flex: 1, minHeight: 0, overflowY: 'auto', padding: '4px 0 8px', scrollbarGutter: 'stable' } }, searchView)
|
|
4558
|
+
: h('div', { style: { flex: 1, minHeight: 0, overflowY: 'auto', padding: '8px 0 8px 8px', scrollbarGutter: 'stable' } },
|
|
3760
4559
|
h('div', { style: { display: 'flex', flexDirection: 'column' } },
|
|
3761
4560
|
dir !== navRoot
|
|
3762
4561
|
? h('div', {
|
|
@@ -3774,13 +4573,7 @@ window.__ModuleLoader__.load({
|
|
|
3774
4573
|
? h('div', { className: 'dsh-ssh-workbench-empty' }, t('files.empty'))
|
|
3775
4574
|
: visibleEntries.map((entry) => {
|
|
3776
4575
|
const isDir = entry.type === 'dir'
|
|
3777
|
-
|
|
3778
|
-
key: entry.path,
|
|
3779
|
-
className: 'dsh-ssh-workbench-files-row',
|
|
3780
|
-
'data-sdh-files-row': isDir ? 'dir' : 'file',
|
|
3781
|
-
'data-sdh-files-path': entry.path,
|
|
3782
|
-
role: 'button',
|
|
3783
|
-
tabIndex: 0,
|
|
4576
|
+
const rowHandlers = {
|
|
3784
4577
|
onClick: () => { if (isDir) navigate(entry.path); else openFile(entry) },
|
|
3785
4578
|
onKeyDown: (event) => {
|
|
3786
4579
|
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
@@ -3788,6 +4581,19 @@ window.__ModuleLoader__.load({
|
|
|
3788
4581
|
if (isDir) navigate(entry.path)
|
|
3789
4582
|
else openFile(entry)
|
|
3790
4583
|
},
|
|
4584
|
+
}
|
|
4585
|
+
// Official trigger adds onContextMenu + Shift+F10 and
|
|
4586
|
+
// owns tabIndex; keep the row a plain button otherwise.
|
|
4587
|
+
const rowProps = getTriggerProps
|
|
4588
|
+
? getTriggerProps(menuTargetOf(entry), rowHandlers)
|
|
4589
|
+
: { ...rowHandlers, tabIndex: 0 }
|
|
4590
|
+
return h('div', {
|
|
4591
|
+
key: entry.path,
|
|
4592
|
+
className: 'dsh-ssh-workbench-files-row',
|
|
4593
|
+
'data-sdh-files-row': isDir ? 'dir' : 'file',
|
|
4594
|
+
'data-sdh-files-path': entry.path,
|
|
4595
|
+
role: 'button',
|
|
4596
|
+
...rowProps,
|
|
3791
4597
|
},
|
|
3792
4598
|
h('span', { style: { display: 'inline-flex', flexShrink: 0, color: isDir ? 'var(--dsw-alias-label-tertiary)' : undefined } }, isDir ? nativeFolderIcon(false) : nativeFileIcon(entry.name)),
|
|
3793
4599
|
h('span', { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, entry.name),
|
|
@@ -4547,6 +5353,8 @@ ${d.join(`
|
|
|
4547
5353
|
? h('div', { className: 'dsh-ssh-workbench-empty', style: { flex: 1 } }, t('panel.loading'))
|
|
4548
5354
|
: h(RemoteFilesPanel, {
|
|
4549
5355
|
t,
|
|
5356
|
+
useContextMenu: props.useContextMenu,
|
|
5357
|
+
renderFactorySlot: props.renderFactorySlot,
|
|
4550
5358
|
controlledServer: activeTab.server,
|
|
4551
5359
|
hideServerPicker: true,
|
|
4552
5360
|
initialPath: activeRemoteHome || '/',
|
|
@@ -4556,6 +5364,8 @@ ${d.join(`
|
|
|
4556
5364
|
: localRoot
|
|
4557
5365
|
? h(RemoteFilesPanel, {
|
|
4558
5366
|
t,
|
|
5367
|
+
useContextMenu: props.useContextMenu,
|
|
5368
|
+
renderFactorySlot: props.renderFactorySlot,
|
|
4559
5369
|
localMode: true,
|
|
4560
5370
|
controlledServer: '__local__',
|
|
4561
5371
|
initialPath: localRoot,
|
|
@@ -4581,46 +5391,120 @@ ${d.join(`
|
|
|
4581
5391
|
)
|
|
4582
5392
|
}
|
|
4583
5393
|
|
|
4584
|
-
/** Plugin-manager bundle configuration:
|
|
5394
|
+
/** Plugin-manager bundle configuration: sidebar entry + remote write switch. */
|
|
4585
5395
|
function SidebarSettingsForm(props) {
|
|
4586
5396
|
const t = props.t ?? ((key) => key)
|
|
4587
5397
|
ensurePanelStyle()
|
|
4588
5398
|
const [enabled, setEnabled] = useState(null)
|
|
5399
|
+
const [writeEnabled, setWriteEnabled] = useState(null)
|
|
5400
|
+
const [hierarchyEnabled, setHierarchyEnabled] = useState(null)
|
|
4589
5401
|
const [busy, setBusy] = useState(false)
|
|
4590
5402
|
const [error, setError] = useState('')
|
|
4591
5403
|
const [notice, setNotice] = useState('')
|
|
4592
5404
|
|
|
4593
5405
|
useEffect(() => {
|
|
4594
5406
|
let alive = true
|
|
4595
|
-
api('/prefs')
|
|
4596
|
-
.then((
|
|
5407
|
+
Promise.all([api('/prefs'), api('/state')])
|
|
5408
|
+
.then(([prefsBody, stateBody]) => {
|
|
5409
|
+
if (!alive) return
|
|
5410
|
+
setEnabled(prefsBody?.prefs?.sidebar !== false)
|
|
5411
|
+
setWriteEnabled(stateBody?.config?.remoteFileWrite === true)
|
|
5412
|
+
setHierarchyEnabled(stateBody?.config?.systemFileHierarchy === true)
|
|
5413
|
+
})
|
|
4597
5414
|
.catch((cause) => { if (alive) setError(t('workbench.sidebarLoadFailed', { msg: messageOf(cause) })) })
|
|
4598
5415
|
return () => { alive = false }
|
|
4599
5416
|
}, [])
|
|
4600
5417
|
|
|
4601
|
-
|
|
4602
|
-
|
|
5418
|
+
/**
|
|
5419
|
+
* Apply one switch immediately: the flip is already visible, so the Host
|
|
5420
|
+
* write runs right away and only the effective value comes back. There is
|
|
5421
|
+
* no Save button — a settings switch that needs a second click to take
|
|
5422
|
+
* effect reads as broken.
|
|
5423
|
+
*/
|
|
5424
|
+
const commit = async (patch) => {
|
|
5425
|
+
if (busy) return
|
|
4603
5426
|
setBusy(true)
|
|
4604
5427
|
setError('')
|
|
4605
5428
|
setNotice('')
|
|
4606
5429
|
try {
|
|
4607
|
-
const
|
|
4608
|
-
const value =
|
|
5430
|
+
const prefsBody = await post('/prefs', patch)
|
|
5431
|
+
const value = prefsBody?.prefs?.sidebar !== false
|
|
4609
5432
|
setEnabled(value)
|
|
5433
|
+
let effective = prefsBody?.config?.remoteFileWrite
|
|
5434
|
+
if (typeof effective !== 'boolean') {
|
|
5435
|
+
// Older Hosts answer with prefs only: read the effective value back
|
|
5436
|
+
// so the switch never reports a state the backend did not take.
|
|
5437
|
+
try {
|
|
5438
|
+
const stateBody = await api('/state')
|
|
5439
|
+
effective = stateBody?.config?.remoteFileWrite
|
|
5440
|
+
} catch {
|
|
5441
|
+
effective = undefined
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
if (typeof effective === 'boolean') setWriteEnabled(effective)
|
|
5445
|
+
let hierarchyEffective = prefsBody?.config?.systemFileHierarchy
|
|
5446
|
+
if (typeof hierarchyEffective !== 'boolean') {
|
|
5447
|
+
try {
|
|
5448
|
+
const stateBody = await api('/state')
|
|
5449
|
+
hierarchyEffective = stateBody?.config?.systemFileHierarchy
|
|
5450
|
+
} catch {
|
|
5451
|
+
hierarchyEffective = undefined
|
|
5452
|
+
}
|
|
5453
|
+
}
|
|
5454
|
+
if (typeof hierarchyEffective === 'boolean') {
|
|
5455
|
+
setHierarchyEnabled(hierarchyEffective)
|
|
5456
|
+
if (typeof systemFilesSync === 'function') systemFilesSync(hierarchyEffective === true)
|
|
5457
|
+
}
|
|
4610
5458
|
if (typeof props.onApply === 'function') props.onApply(value)
|
|
4611
|
-
|
|
5459
|
+
// An older Host silently ignores a field it does not know. Only claim
|
|
5460
|
+
// success when the effective value confirms the flip.
|
|
5461
|
+
let applied = true
|
|
5462
|
+
if ('remoteFileWrite' in patch) {
|
|
5463
|
+
applied = applied && typeof effective === 'boolean' && effective === (patch.remoteFileWrite === true)
|
|
5464
|
+
}
|
|
5465
|
+
if ('systemFileHierarchy' in patch) {
|
|
5466
|
+
applied = applied && typeof hierarchyEffective === 'boolean' && hierarchyEffective === (patch.systemFileHierarchy === true)
|
|
5467
|
+
}
|
|
5468
|
+
setNotice(applied ? t('workbench.saved') : t('workbench.writeNeedRestart'))
|
|
4612
5469
|
} catch (cause) {
|
|
4613
|
-
|
|
5470
|
+
// A Host built before this switch existed rejects the new field with a
|
|
5471
|
+
// schema error instead of ignoring it. That is a restart, not a bug.
|
|
5472
|
+
if (/prefs\.(sidebar|remoteFileWrite|systemFileHierarchy) must be a boolean/.test(messageOf(cause) || '')) {
|
|
5473
|
+
setNotice(t('workbench.writeNeedRestart'))
|
|
5474
|
+
} else {
|
|
5475
|
+
setError(t('workbench.sidebarSaveFailed', { msg: messageOf(cause) }))
|
|
5476
|
+
}
|
|
4614
5477
|
} finally {
|
|
4615
5478
|
setBusy(false)
|
|
4616
5479
|
}
|
|
4617
5480
|
}
|
|
4618
5481
|
|
|
5482
|
+
const switchRow = (options) => h('div', {
|
|
5483
|
+
style: { display: 'flex', alignItems: 'center', gap: 12, padding: options.first ? '0' : '16px 0 0' },
|
|
5484
|
+
},
|
|
5485
|
+
h('div', { style: { flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
5486
|
+
h('strong', { style: { fontSize: 14, fontWeight: 500, lineHeight: '20px', color: 'var(--dsw-alias-label-primary, inherit)' } }, options.title),
|
|
5487
|
+
h('span', { style: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary, inherit)' } }, options.hint),
|
|
5488
|
+
),
|
|
5489
|
+
h('button', {
|
|
5490
|
+
type: 'button',
|
|
5491
|
+
role: 'switch',
|
|
5492
|
+
'aria-checked': options.checked === true ? 'true' : 'false',
|
|
5493
|
+
'aria-label': options.title,
|
|
5494
|
+
'data-sdh-switch': options.key,
|
|
5495
|
+
className: 'dsh-ssh-workbench-switch',
|
|
5496
|
+
disabled: options.disabled,
|
|
5497
|
+
onClick: options.onToggle,
|
|
5498
|
+
}, h('span', { className: 'dsh-ssh-workbench-switch-thumb' })),
|
|
5499
|
+
)
|
|
5500
|
+
|
|
4619
5501
|
if (props.view === 'summary') {
|
|
5502
|
+
const parts = []
|
|
5503
|
+
if (enabled !== null) parts.push(`${t('workbench.sidebarTitle')} ${enabled ? t('workbench.enabled') : t('workbench.disabled')}`)
|
|
5504
|
+
if (writeEnabled !== null) parts.push(`${t('workbench.writeTitle')} ${writeEnabled ? t('workbench.writeOn') : t('workbench.writeOff')}`)
|
|
5505
|
+
if (hierarchyEnabled !== null) parts.push(`${t('workbench.hierarchyTitle')} ${hierarchyEnabled ? t('workbench.hierarchyOn') : t('workbench.hierarchyOff')}`)
|
|
4620
5506
|
return h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary, inherit)' } },
|
|
4621
|
-
h('span', null,
|
|
4622
|
-
h('span', { style: { color: 'var(--dsw-alias-label-tertiary, inherit)' } },
|
|
4623
|
-
enabled === null ? '' : (enabled ? t('workbench.enabled') : t('workbench.disabled'))),
|
|
5507
|
+
h('span', null, parts.join(' · ')),
|
|
4624
5508
|
)
|
|
4625
5509
|
}
|
|
4626
5510
|
|
|
@@ -4628,26 +5512,52 @@ ${d.join(`
|
|
|
4628
5512
|
'data-sdh-sidebar-pref': '',
|
|
4629
5513
|
style: { display: 'flex', flexDirection: 'column', gap: 12, width: '100%', padding: '16px 0' },
|
|
4630
5514
|
},
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
),
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
5515
|
+
switchRow({
|
|
5516
|
+
key: 'sidebar',
|
|
5517
|
+
first: true,
|
|
5518
|
+
title: t('workbench.sidebarTitle'),
|
|
5519
|
+
hint: t('workbench.sidebarHint'),
|
|
5520
|
+
checked: enabled === true,
|
|
5521
|
+
disabled: enabled === null || busy,
|
|
5522
|
+
onToggle: () => {
|
|
5523
|
+
const next = !(enabled === true)
|
|
5524
|
+
setEnabled(next)
|
|
5525
|
+
commit({ sidebar: next })
|
|
5526
|
+
},
|
|
5527
|
+
}),
|
|
5528
|
+
switchRow({
|
|
5529
|
+
key: 'remoteFileWrite',
|
|
5530
|
+
first: false,
|
|
5531
|
+
title: t('workbench.writeTitle'),
|
|
5532
|
+
hint: t('workbench.writeHint'),
|
|
5533
|
+
checked: writeEnabled === true,
|
|
5534
|
+
disabled: writeEnabled === null || busy,
|
|
5535
|
+
onToggle: () => {
|
|
5536
|
+
const next = !(writeEnabled === true)
|
|
5537
|
+
setWriteEnabled(next)
|
|
5538
|
+
setNotice('')
|
|
5539
|
+
commit({ remoteFileWrite: next })
|
|
5540
|
+
},
|
|
5541
|
+
}),
|
|
5542
|
+
switchRow({
|
|
5543
|
+
key: 'systemFileHierarchy',
|
|
5544
|
+
first: false,
|
|
5545
|
+
title: t('workbench.hierarchyTitle'),
|
|
5546
|
+
hint: t('workbench.hierarchyHint'),
|
|
5547
|
+
checked: hierarchyEnabled === true,
|
|
5548
|
+
disabled: hierarchyEnabled === null || busy,
|
|
5549
|
+
onToggle: () => {
|
|
5550
|
+
const next = !(hierarchyEnabled === true)
|
|
5551
|
+
setHierarchyEnabled(next)
|
|
5552
|
+
if (typeof systemFilesSync === 'function') systemFilesSync(next)
|
|
5553
|
+
setNotice('')
|
|
5554
|
+
commit({ systemFileHierarchy: next })
|
|
5555
|
+
},
|
|
5556
|
+
}),
|
|
4646
5557
|
error ? h('div', { className: 'dsh-ssh-workbench-error', role: 'alert' }, error) : null,
|
|
4647
|
-
|
|
4648
|
-
h(
|
|
4649
|
-
|
|
4650
|
-
),
|
|
5558
|
+
notice
|
|
5559
|
+
? h('div', { role: 'status', style: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary, inherit)' } }, notice)
|
|
5560
|
+
: null,
|
|
4651
5561
|
)
|
|
4652
5562
|
}
|
|
4653
5563
|
|
|
@@ -4675,6 +5585,34 @@ ${d.join(`
|
|
|
4675
5585
|
let layoutService = null
|
|
4676
5586
|
if (typeof ctx.inject === 'function') {
|
|
4677
5587
|
ctx.inject(['layout'], (inner) => { layoutService = inner.get('layout') ?? null })
|
|
5588
|
+
// The gateway exposes Host-local file actions (open in an app / reveal in
|
|
5589
|
+
// Finder) through `remote.session`. Only Host paths resolve, so the
|
|
5590
|
+
// hierarchy page (session cwd) uses it and remote SFTP pages never do.
|
|
5591
|
+
// The gateway exposes Host-local file actions through its namespace
|
|
5592
|
+
// service. Inject the namespace itself: reading `ctx.remote.session`
|
|
5593
|
+
// from outside an inject throws.
|
|
5594
|
+
ctx.inject(['remote.session'], (inner) => { sessionRemote = inner.get('remote.session') ?? null })
|
|
5595
|
+
// Workspace search for the local hierarchy page comes from the same
|
|
5596
|
+
// gateway namespace the native files page uses (packaged ripgrep).
|
|
5597
|
+
ctx.inject(['remote.workspaceFiles'], (inner) => { workspaceFilesRemote = inner.get('remote.workspaceFiles') ?? null })
|
|
5598
|
+
// Native file rows reuse the official context menu: right-click and
|
|
5599
|
+
// Shift+F10 come from its trigger bindings, and the actions below are
|
|
5600
|
+
// resolved when the menu opens.
|
|
5601
|
+
ctx.inject(['contextMenus'], (inner) => {
|
|
5602
|
+
const menus = inner.get('contextMenus')
|
|
5603
|
+
if (!menus || typeof menus.register !== 'function') return
|
|
5604
|
+
const register = () => menus.register({
|
|
5605
|
+
id: `${NS}.files`,
|
|
5606
|
+
target: FILES_MENU_TARGET,
|
|
5607
|
+
order: 100,
|
|
5608
|
+
resolve: (value) => resolveFilesMenuActions(value),
|
|
5609
|
+
})
|
|
5610
|
+
if (typeof inner.effect === 'function') {
|
|
5611
|
+
inner.effect(register, 'dsh-ssh-workbench: files context menu')
|
|
5612
|
+
} else {
|
|
5613
|
+
register()
|
|
5614
|
+
}
|
|
5615
|
+
})
|
|
4678
5616
|
}
|
|
4679
5617
|
// The left-rail workbench entry is the one sidebar surface the plugin
|
|
4680
5618
|
// manager switch controls. Registration is a pair: the `main` page the
|
|
@@ -4689,9 +5627,13 @@ ${d.join(`
|
|
|
4689
5627
|
own(slots.inject('main', () =>
|
|
4690
5628
|
slots.register(
|
|
4691
5629
|
{ name: 'main', key: WORKBENCH_PANEL_ID, locale: NS, inject: () => ({ t }) },
|
|
4692
|
-
function SdhSshOpsWorkbenchPage() {
|
|
5630
|
+
function SdhSshOpsWorkbenchPage(props) {
|
|
4693
5631
|
return h(WorkbenchPage, {
|
|
4694
5632
|
t,
|
|
5633
|
+
// The official file context menu is wired through slot props;
|
|
5634
|
+
// pass them down so the explorer rows can use it.
|
|
5635
|
+
useContextMenu: props.useContextMenu,
|
|
5636
|
+
renderFactorySlot: props.renderFactorySlot,
|
|
4695
5637
|
onBack: () => {
|
|
4696
5638
|
try { layoutService?.selectPanel?.(null) } catch { /* navigation is best effort */ }
|
|
4697
5639
|
},
|
|
@@ -4804,6 +5746,63 @@ ${d.join(`
|
|
|
4804
5746
|
},
|
|
4805
5747
|
),
|
|
4806
5748
|
)
|
|
5749
|
+
slots.inject('sidebar.right.pane.tab.title', () =>
|
|
5750
|
+
slots.register(
|
|
5751
|
+
{ name: 'sidebar.right.pane.tab.title', key: 'dsh-ssh-workbench/system-files', locale: NS },
|
|
5752
|
+
function SdhSshOpsSystemFilesTitle() {
|
|
5753
|
+
// Match DSH's own files tab title: the folder glyph from
|
|
5754
|
+
// `FileTypeIcon` followed by the label, no extra chrome.
|
|
5755
|
+
const FileTypeIcon = primitives && primitives.FileTypeIcon
|
|
5756
|
+
return h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6, minWidth: 0 } },
|
|
5757
|
+
typeof FileTypeIcon === 'function'
|
|
5758
|
+
? h(FileTypeIcon, { kind: 'folder', size: 16, style: { flexShrink: 0 } })
|
|
5759
|
+
: h(FilesTabArtwork, { size: 14 }),
|
|
5760
|
+
h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, t('files.systemTitle')),
|
|
5761
|
+
)
|
|
5762
|
+
},
|
|
5763
|
+
),
|
|
5764
|
+
)
|
|
5765
|
+
slots.inject('sidebar.right.pane.tab', () =>
|
|
5766
|
+
slots.register(
|
|
5767
|
+
{
|
|
5768
|
+
name: 'sidebar.right.pane.tab',
|
|
5769
|
+
key: 'dsh-ssh-workbench/system-files',
|
|
5770
|
+
inject: (sessionId, actions) => ({ actions }),
|
|
5771
|
+
},
|
|
5772
|
+
function SdhSshOpsSystemFilesBody(props) {
|
|
5773
|
+
const useTabInfo = props.useTabInfo
|
|
5774
|
+
const tabInfo = typeof useTabInfo === 'function' ? useTabInfo() : null
|
|
5775
|
+
const useSessions = props.useSessions
|
|
5776
|
+
// The session workspace is the only root the takeover may show;
|
|
5777
|
+
// the panel locks navigation to it so this page cannot escape the
|
|
5778
|
+
// same boundary as DSH's native files page.
|
|
5779
|
+
const cwd = typeof useSessions === 'function'
|
|
5780
|
+
? useSessions((sessions) => {
|
|
5781
|
+
const row = sessions && sessions.byId && sessions.byId[props.sessionId]
|
|
5782
|
+
return row && typeof row.cwd === 'string' && row.cwd ? row.cwd : undefined
|
|
5783
|
+
})
|
|
5784
|
+
: undefined
|
|
5785
|
+
if (!cwd) {
|
|
5786
|
+
return h('div', {
|
|
5787
|
+
style: { padding: 16, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary, inherit)' },
|
|
5788
|
+
}, t('files.noSessionDir'))
|
|
5789
|
+
}
|
|
5790
|
+
return h(RemoteFilesPanel, {
|
|
5791
|
+
t,
|
|
5792
|
+
...props,
|
|
5793
|
+
localMode: true,
|
|
5794
|
+
controlledServer: '__local__',
|
|
5795
|
+
rootPath: cwd,
|
|
5796
|
+
initialPath: cwd,
|
|
5797
|
+
rootLabel: baseNameOf(cwd) || cwd,
|
|
5798
|
+
lockRoot: true,
|
|
5799
|
+
openNative: true,
|
|
5800
|
+
hideServerPicker: true,
|
|
5801
|
+
tabInfo,
|
|
5802
|
+
})
|
|
5803
|
+
},
|
|
5804
|
+
),
|
|
5805
|
+
)
|
|
4807
5806
|
slots.inject('sidebar.right.pane.tab', () =>
|
|
4808
5807
|
slots.register(
|
|
4809
5808
|
{ name: 'sidebar.right.pane.tab', key: 'dsh-ssh-workbench/file' },
|
|
@@ -4836,16 +5835,6 @@ ${d.join(`
|
|
|
4836
5835
|
TargetDock,
|
|
4837
5836
|
),
|
|
4838
5837
|
)
|
|
4839
|
-
for (const toolName of ['ssh_workbench_target']) {
|
|
4840
|
-
slots.inject('tool.call.toolview', () =>
|
|
4841
|
-
slots.register(
|
|
4842
|
-
{ name: 'tool.call.toolview', key: toolName },
|
|
4843
|
-
function SdhSshOpsToolView(props) {
|
|
4844
|
-
return h(SshOpsToolRow, props)
|
|
4845
|
-
},
|
|
4846
|
-
),
|
|
4847
|
-
)
|
|
4848
|
-
}
|
|
4849
5838
|
}
|
|
4850
5839
|
|
|
4851
5840
|
// Native right sidebar: a page type opened by kind, plus a guide entry.
|
|
@@ -4881,6 +5870,48 @@ ${d.join(`
|
|
|
4881
5870
|
title: () => t('files.title'),
|
|
4882
5871
|
guide: [],
|
|
4883
5872
|
}), 'dsh-ssh-workbench: files tab')
|
|
5873
|
+
|
|
5874
|
+
// Take over the native `files` kind only when the setting is on. The
|
|
5875
|
+
// extension definition outranks the builtin one, so the official page
|
|
5876
|
+
// is shadowed without being unregistered; toggling off restores it.
|
|
5877
|
+
let systemFilesDisposer = null
|
|
5878
|
+
const syncSystemFiles = (enabled) => {
|
|
5879
|
+
if (enabled === true && !systemFilesDisposer) {
|
|
5880
|
+
systemFilesDisposer = tabs.register({
|
|
5881
|
+
id: 'dsh-ssh-workbench/system-files',
|
|
5882
|
+
kind: 'files',
|
|
5883
|
+
keepMounted: true,
|
|
5884
|
+
title: () => t('files.systemTitle'),
|
|
5885
|
+
/**
|
|
5886
|
+
* Reuse DSH's guide entry verbatim (`workspace` / `workspace.files`
|
|
5887
|
+
* / order 10 / its artwork) so the entry keeps its original slot on
|
|
5888
|
+
* the guide page instead of dropping to the bottom.
|
|
5889
|
+
*/
|
|
5890
|
+
guide: [{
|
|
5891
|
+
id: 'workspace',
|
|
5892
|
+
commandId: 'workspace.files',
|
|
5893
|
+
order: 10,
|
|
5894
|
+
title: () => t('files.systemTitle'),
|
|
5895
|
+
description: () => t('files.systemDescription'),
|
|
5896
|
+
...(primitives && primitives.GuideArtworkFiles ? { icon: primitives.GuideArtworkFiles } : {}),
|
|
5897
|
+
}],
|
|
5898
|
+
})
|
|
5899
|
+
} else if (enabled !== true && systemFilesDisposer) {
|
|
5900
|
+
systemFilesDisposer()
|
|
5901
|
+
systemFilesDisposer = null
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
systemFilesSync = syncSystemFiles
|
|
5905
|
+
inner.effect(() => () => {
|
|
5906
|
+
if (systemFilesDisposer) {
|
|
5907
|
+
systemFilesDisposer()
|
|
5908
|
+
systemFilesDisposer = null
|
|
5909
|
+
}
|
|
5910
|
+
if (systemFilesSync === syncSystemFiles) systemFilesSync = null
|
|
5911
|
+
}, 'dsh-ssh-workbench: system files tab')
|
|
5912
|
+
api('/state')
|
|
5913
|
+
.then((body) => syncSystemFiles(body?.config?.systemFileHierarchy === true))
|
|
5914
|
+
.catch(() => {})
|
|
4884
5915
|
})
|
|
4885
5916
|
|
|
4886
5917
|
// Session-scoped contexts (composer reference events) come from this service.
|