dsh-remote 0.8.18 → 0.8.21
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/README.md +13 -15
- package/README.zh.md +18 -12
- package/lib/binding.js +69 -0
- package/lib/client.js +382 -137
- package/lib/hostkey.js +60 -0
- package/lib/index.js +104 -924
- package/lib/pool.js +511 -0
- package/lib/remote-fs.js +143 -0
- package/lib/routes-fs.js +221 -0
- package/lib/search.js +59 -1
- package/lib/sync.js +15 -10
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -94,6 +94,12 @@ window.__ModuleLoader__.load({
|
|
|
94
94
|
'settings.fwdAdd': '添加转发',
|
|
95
95
|
'settings.audit': '最近执行的远程命令(审计日志)',
|
|
96
96
|
'settings.auditEmpty': '暂无记录(在远端执行的命令会记录到 $DSH_HOME/remote-workspaces/audit.log)',
|
|
97
|
+
'settings.auditRefresh': '刷新日志',
|
|
98
|
+
'settings.badgePwd': '密码',
|
|
99
|
+
'settings.badgeKeychain': '钥匙串',
|
|
100
|
+
'settings.badgeProxy': '跳板',
|
|
101
|
+
'settings.sectionHide': '收起此栏',
|
|
102
|
+
'settings.sectionShow': '展开此栏',
|
|
97
103
|
'settings.update': '更新',
|
|
98
104
|
'settings.updateChecking': '版本信息加载中…',
|
|
99
105
|
'settings.checking': '检查中…',
|
|
@@ -188,6 +194,9 @@ window.__ModuleLoader__.load({
|
|
|
188
194
|
'picker.chooseDrive': ' 选择驱动器',
|
|
189
195
|
'picker.homeBtn': '🏠',
|
|
190
196
|
'picker.goHome': '回家目录',
|
|
197
|
+
'picker.hintKeys': 'Enter 确认 · Esc 关闭 · Ctrl/⌘+Enter 设为工作区',
|
|
198
|
+
'picker.star': '⭐ Star dsh-remote',
|
|
199
|
+
'picker.feedback': '反馈 / 提 issue',
|
|
191
200
|
'explorer.refresh': '刷新',
|
|
192
201
|
'explorer.newDir': '新建目录',
|
|
193
202
|
'explorer.more': '…',
|
|
@@ -210,6 +219,9 @@ window.__ModuleLoader__.load({
|
|
|
210
219
|
'explorer.menuDelete': '🗑 删除',
|
|
211
220
|
'explorer.menuDeleteDir': '🗑 删除(含子目录)',
|
|
212
221
|
'explorer.renamePrompt': '重命名为(新名字)',
|
|
222
|
+
'explorer.renameTitle': '重命名',
|
|
223
|
+
'explorer.mkdirTitle': '新建目录',
|
|
224
|
+
'explorer.namePlaceholder': '名称',
|
|
213
225
|
'explorer.deleteConfirm': '确认删除 {path}?',
|
|
214
226
|
'explorer.copied': '已复制',
|
|
215
227
|
'explorer.tabTitle': '远程文件',
|
|
@@ -226,6 +238,8 @@ window.__ModuleLoader__.load({
|
|
|
226
238
|
'file.empty': '(空文件)',
|
|
227
239
|
'file.truncated': '… 内容已截断(只读远程查看,完整文件请用 rw_read_file / 下载)',
|
|
228
240
|
'file.conflict': '远端文件已变化,请重新读取',
|
|
241
|
+
'file.dirty': '未保存',
|
|
242
|
+
'file.saveHint': 'Ctrl/⌘+S 保存',
|
|
229
243
|
'file.downloaded': '已下载到本地镜像{local}',
|
|
230
244
|
'file.downloadedAt': ': {path}',
|
|
231
245
|
'file.readFail': '远程读取失败: {msg}',
|
|
@@ -294,6 +308,12 @@ window.__ModuleLoader__.load({
|
|
|
294
308
|
'settings.fwdAdd': 'Add forward',
|
|
295
309
|
'settings.audit': 'Recent remote commands (audit log)',
|
|
296
310
|
'settings.auditEmpty': 'No records yet (commands run remotely are logged to $DSH_HOME/remote-workspaces/audit.log)',
|
|
311
|
+
'settings.auditRefresh': 'Refresh log',
|
|
312
|
+
'settings.badgePwd': 'password',
|
|
313
|
+
'settings.badgeKeychain': 'keychain',
|
|
314
|
+
'settings.badgeProxy': 'jump',
|
|
315
|
+
'settings.sectionHide': 'Collapse section',
|
|
316
|
+
'settings.sectionShow': 'Expand section',
|
|
297
317
|
'settings.update': 'Update',
|
|
298
318
|
'settings.updateChecking': 'Loading version info…',
|
|
299
319
|
'settings.checking': 'Checking…',
|
|
@@ -388,6 +408,9 @@ window.__ModuleLoader__.load({
|
|
|
388
408
|
'picker.chooseDrive': ' choose a drive',
|
|
389
409
|
'picker.homeBtn': '🏠',
|
|
390
410
|
'picker.goHome': 'Go to home',
|
|
411
|
+
'picker.hintKeys': 'Enter confirms · Esc closes · Ctrl/⌘+Enter sets workspace',
|
|
412
|
+
'picker.star': '⭐ Star dsh-remote',
|
|
413
|
+
'picker.feedback': 'Feedback / report issue',
|
|
391
414
|
'explorer.refresh': 'Refresh',
|
|
392
415
|
'explorer.newDir': 'New directory',
|
|
393
416
|
'explorer.more': '…',
|
|
@@ -410,6 +433,9 @@ window.__ModuleLoader__.load({
|
|
|
410
433
|
'explorer.menuDelete': '🗑 Delete',
|
|
411
434
|
'explorer.menuDeleteDir': '🗑 Delete (with contents)',
|
|
412
435
|
'explorer.renamePrompt': 'Rename to (new name)',
|
|
436
|
+
'explorer.renameTitle': 'Rename',
|
|
437
|
+
'explorer.mkdirTitle': 'New directory',
|
|
438
|
+
'explorer.namePlaceholder': 'Name',
|
|
413
439
|
'explorer.deleteConfirm': 'Delete {path}?',
|
|
414
440
|
'explorer.copied': 'Copied',
|
|
415
441
|
'explorer.tabTitle': 'Remote Files',
|
|
@@ -426,6 +452,8 @@ window.__ModuleLoader__.load({
|
|
|
426
452
|
'file.empty': ' (empty file)',
|
|
427
453
|
'file.truncated': '… content truncated (read-only remote view; use rw_read_file / Download for the full file)',
|
|
428
454
|
'file.conflict': 'The remote file changed, please re-read it',
|
|
455
|
+
'file.dirty': 'unsaved',
|
|
456
|
+
'file.saveHint': 'Ctrl/⌘+S to save',
|
|
429
457
|
'file.downloaded': 'Downloaded to local mirror{local}',
|
|
430
458
|
'file.downloadedAt': ': {path}',
|
|
431
459
|
'file.readFail': 'Failed to read remote: {msg}',
|
|
@@ -525,6 +553,19 @@ window.__ModuleLoader__.load({
|
|
|
525
553
|
? '/api' + path : path
|
|
526
554
|
}
|
|
527
555
|
|
|
556
|
+
function sessionQuery(sessionId) {
|
|
557
|
+
return sessionId ? 'sessionId=' + encodeURIComponent(sessionId) : ''
|
|
558
|
+
}
|
|
559
|
+
function withSessionQuery(path, sessionId) {
|
|
560
|
+
const q = sessionQuery(sessionId)
|
|
561
|
+
if (!q) return path
|
|
562
|
+
return path + (path.includes('?') ? '&' : '?') + q
|
|
563
|
+
}
|
|
564
|
+
function withSessionBody(body, sessionId) {
|
|
565
|
+
if (!sessionId) return body
|
|
566
|
+
return Object.assign({}, body || {}, { sessionId })
|
|
567
|
+
}
|
|
568
|
+
|
|
528
569
|
async function api(method, path, body) {
|
|
529
570
|
const opts = { method, headers: {} }
|
|
530
571
|
if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body) }
|
|
@@ -560,14 +601,96 @@ window.__ModuleLoader__.load({
|
|
|
560
601
|
muted: v('--dsw-alias-label-tertiary', 'rgba(128,128,128,0.7)'),
|
|
561
602
|
label: v('--dsw-alias-label-primary', '#e4e4e7'),
|
|
562
603
|
primary: v('--dsw-alias-button-primary-fill', '#2563eb'),
|
|
563
|
-
|
|
564
|
-
|
|
604
|
+
// Text ON a --dsw-alias-button-primary-fill surface must use the *label*
|
|
605
|
+
// token, which is the pairing the harness's own primary button uses
|
|
606
|
+
// (._primary{background:button-primary-fill;color:label-primary-foreground}).
|
|
607
|
+
// --dsw-alias-button-contrast-fill is a FILL token (the harness uses it as a
|
|
608
|
+
// `background`), so it is NOT the light-on-dark colour: in the light theme it
|
|
609
|
+
// is #61666b (gray on #0f1115 = 3.3:1, and 1.6:1 once :disabled fades it),
|
|
610
|
+
// and in the dark theme it collapses to #f9fafb on a #f9fafb fill (invisible).
|
|
611
|
+
onPrimary: v('--dsw-alias-label-primary-foreground', '#fff'),
|
|
612
|
+
// Hover tints are a neutral translucent gray rather than a theme token:
|
|
613
|
+
// the harness's own interactive-bg-hover is a 6% tint that disappears on
|
|
614
|
+
// the settings surface, while a gray overlay darkens light themes and
|
|
615
|
+
// lightens dark ones.
|
|
616
|
+
hoverBg: 'rgba(128,128,128,0.14)',
|
|
565
617
|
}
|
|
566
618
|
const inputS = { flex: 1, padding: '6px 10px', borderRadius: T.radius, border: '1px solid ' + T.border, background: T.bg, color: T.label, outline: 'none' }
|
|
567
619
|
const buttonS = { padding: '6px 12px', borderRadius: T.radius, border: '1px solid ' + T.border, background: T.bg, color: T.label, cursor: 'pointer' }
|
|
568
620
|
const primaryBtnS = { padding: '6px 12px', borderRadius: T.radius, border: 'none', background: T.primary, color: T.onPrimary, cursor: 'pointer', fontWeight: 600 }
|
|
569
|
-
const box = { border: '1px solid ' + T.border, borderRadius:
|
|
621
|
+
const box = { border: '1px solid ' + T.border, borderRadius: 10, background: T.bg, padding: 12 }
|
|
570
622
|
const boxS = box
|
|
623
|
+
const LABEL_W = 108
|
|
624
|
+
const CHROME_CSS_ID = 'dsh-remote-chrome'
|
|
625
|
+
function ensureChromeCss() {
|
|
626
|
+
try {
|
|
627
|
+
if (typeof document === 'undefined' || !document.head || typeof document.getElementById !== 'function') return
|
|
628
|
+
if (document.getElementById(CHROME_CSS_ID)) return
|
|
629
|
+
const el = document.createElement('style')
|
|
630
|
+
el.id = CHROME_CSS_ID
|
|
631
|
+
el.textContent = [
|
|
632
|
+
'.dsh-rw-btn{transition:background .12s ease,opacity .12s ease,border-color .12s ease}',
|
|
633
|
+
'.dsh-rw-btn:hover:not(:disabled){background:rgba(128,128,128,.18)}',
|
|
634
|
+
'.dsh-rw-btn:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,#2563eb);outline-offset:1px}',
|
|
635
|
+
// A disabled FILLED primary keeps its solid fill. Fading the whole
|
|
636
|
+
// button (opacity) turned the near-black primary fill into a muddy
|
|
637
|
+
// gray and dragged the white label down to ~1.5:1 — the fill is the
|
|
638
|
+
// affordance here, so keep it pure; the cursor carries "disabled".
|
|
639
|
+
'.dsh-rw-btn:disabled{cursor:not-allowed}',
|
|
640
|
+
'.dsh-rw-btn:disabled:not(.dsh-rw-primary){opacity:.45}',
|
|
641
|
+
// A filled primary keeps its own fill on hover; opacity is the only
|
|
642
|
+
// cue that stays visible whether that fill is near-black or near-white.
|
|
643
|
+
'.dsh-rw-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-fill,#2563eb);opacity:.85}',
|
|
644
|
+
'.dsh-rw-input{transition:border-color .12s ease,box-shadow .12s ease}',
|
|
645
|
+
'.dsh-rw-input:focus{border-color:var(--dsw-alias-button-primary-fill,#2563eb)!important;box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-button-primary-fill,#2563eb) 28%,transparent)}',
|
|
646
|
+
'.dsh-rw-row{border-radius:8px;margin:0 -6px;padding-left:8px;padding-right:8px;transition:background .12s ease}',
|
|
647
|
+
'.dsh-rw-row:hover{background:rgba(128,128,128,.14)}',
|
|
648
|
+
'.dsh-rw-list-item{transition:background .1s ease}',
|
|
649
|
+
'.dsh-rw-list-item:hover{background:rgba(128,128,128,.18)}',
|
|
650
|
+
'.dsh-rw-tabs{display:inline-flex;padding:3px;gap:2px;border-radius:10px;background:rgba(128,128,128,.14);border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35))}',
|
|
651
|
+
'.dsh-rw-tab{border:none!important;background:transparent!important}',
|
|
652
|
+
'.dsh-rw-tab.is-active{background:var(--dsw-alias-button-primary-fill,#2563eb)!important;color:var(--dsw-alias-label-primary-foreground,#fff)!important;font-weight:600}',
|
|
653
|
+
'.dsh-rw-pill{display:inline-flex;align-items:center;font-size:11px;line-height:1.3;padding:1px 7px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.35));opacity:.9;white-space:nowrap}',
|
|
654
|
+
'.dsh-rw-pill.is-ok{color:var(--dsw-static-green-500,#4caf7d);border-color:currentColor}',
|
|
655
|
+
'.dsh-rw-section-btn{width:100%;text-align:left;display:flex;align-items:center;gap:8px;background:transparent;border:none;cursor:pointer;padding:0;color:inherit}',
|
|
656
|
+
'@keyframes dsh-rw-in{from{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:none}}',
|
|
657
|
+
'.dsh-rw-modal-panel{animation:dsh-rw-in .14s ease-out}',
|
|
658
|
+
].join('')
|
|
659
|
+
document.head.appendChild(el)
|
|
660
|
+
} catch { /* vm tests / no DOM */ }
|
|
661
|
+
}
|
|
662
|
+
function PromptDialog(props) {
|
|
663
|
+
const { title, initial, placeholder, onSubmit, onCancel } = props
|
|
664
|
+
const [val, setVal] = React.useState(initial || '')
|
|
665
|
+
React.useEffect(() => {
|
|
666
|
+
try {
|
|
667
|
+
if (typeof document === 'undefined') return
|
|
668
|
+
const onKey = (e) => {
|
|
669
|
+
if (e.key === 'Escape') { e.preventDefault(); onCancel() }
|
|
670
|
+
if (e.key === 'Enter') { e.preventDefault(); const t = val.trim(); if (t) onSubmit(t) }
|
|
671
|
+
}
|
|
672
|
+
document.addEventListener('keydown', onKey)
|
|
673
|
+
return () => document.removeEventListener('keydown', onKey)
|
|
674
|
+
} catch { return undefined }
|
|
675
|
+
}, [val, onSubmit, onCancel])
|
|
676
|
+
return React.createElement('div', {
|
|
677
|
+
style: { position: 'fixed', inset: 0, zIndex: 1000000, background: 'rgba(0,0,0,0.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 },
|
|
678
|
+
onClick: onCancel,
|
|
679
|
+
},
|
|
680
|
+
React.createElement('div', {
|
|
681
|
+
className: 'dsh-rw-modal-panel',
|
|
682
|
+
style: { background: v('--dsw-alias-bg-overlay', '#1e1e1e'), border: '1px solid ' + T.borderStrong, borderRadius: 10, padding: 14, width: 'min(420px, 92vw)', display: 'flex', flexDirection: 'column', gap: 10, boxShadow: '0 12px 40px rgba(0,0,0,0.4)' },
|
|
683
|
+
onClick: (e) => e.stopPropagation(),
|
|
684
|
+
},
|
|
685
|
+
React.createElement('div', { style: { fontSize: 13, fontWeight: 600, color: T.label } }, title),
|
|
686
|
+
React.createElement('input', { autoFocus: true, value: val, onChange: (e) => setVal(e.target.value), placeholder: placeholder || '', className: 'dsh-rw-input', style: inputS }),
|
|
687
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' } },
|
|
688
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: onCancel }, tr('file.cancel')),
|
|
689
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: () => { const t = val.trim(); if (t) onSubmit(t) }, disabled: !val.trim() }, tr('common.confirm')),
|
|
690
|
+
),
|
|
691
|
+
),
|
|
692
|
+
)
|
|
693
|
+
}
|
|
571
694
|
|
|
572
695
|
function humanSize(n) {
|
|
573
696
|
if (n == null) return ''
|
|
@@ -629,6 +752,9 @@ window.__ModuleLoader__.load({
|
|
|
629
752
|
const [sshEntries, setSshEntries] = React.useState(null)
|
|
630
753
|
const [workspaces, setWorkspaces] = React.useState([])
|
|
631
754
|
const [advOpen, setAdvOpen] = React.useState(false) // 高级配置折叠
|
|
755
|
+
const [fwdOpen, setFwdOpen] = React.useState(true)
|
|
756
|
+
const [auditOpen, setAuditOpen] = React.useState(false)
|
|
757
|
+
React.useEffect(() => { ensureChromeCss() }, [])
|
|
632
758
|
|
|
633
759
|
const testConn = () => {
|
|
634
760
|
if (!form.host.trim() || tst === 'testing') return
|
|
@@ -771,52 +897,66 @@ window.__ModuleLoader__.load({
|
|
|
771
897
|
.catch((e) => setErr(String((e && e.message) || e)))
|
|
772
898
|
}
|
|
773
899
|
|
|
774
|
-
const row = (label, ctrl, k) => React.createElement('div', { key: k, style: { display: 'flex', gap:
|
|
775
|
-
React.createElement('label', { style: { width:
|
|
900
|
+
const row = (label, ctrl, k) => React.createElement('div', { key: k, style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8 } },
|
|
901
|
+
React.createElement('label', { style: { width: LABEL_W, fontSize: 12, opacity: 0.8, flexShrink: 0 } }, label), ctrl)
|
|
902
|
+
const pill = (cls, text) => React.createElement('span', { className: 'dsh-rw-pill' + (cls ? ' ' + cls : ''), style: { marginLeft: 4 } }, text)
|
|
903
|
+
const sectionToggle = (open, setOpen, title, extra) => React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: open ? 8 : 0 } },
|
|
904
|
+
React.createElement('button', {
|
|
905
|
+
type: 'button', className: 'dsh-rw-section-btn',
|
|
906
|
+
title: open ? tr('settings.sectionHide') : tr('settings.sectionShow'),
|
|
907
|
+
onClick: () => setOpen(!open),
|
|
908
|
+
style: { fontSize: 13, fontWeight: 600 },
|
|
909
|
+
}, React.createElement('span', { style: { opacity: 0.7, width: 12 } }, open ? '▾' : '▸'), title),
|
|
910
|
+
extra || null,
|
|
911
|
+
)
|
|
776
912
|
|
|
777
913
|
return React.createElement('div', { style: { padding: 16, display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 860 } },
|
|
778
|
-
React.createElement('div', { style: { fontSize:
|
|
779
|
-
React.createElement('div', { style: { fontSize: 12, opacity: 0.8 } },
|
|
914
|
+
React.createElement('div', { style: { fontSize: 16, fontWeight: 650, letterSpacing: '-0.01em' } }, tr('settings.title')),
|
|
915
|
+
React.createElement('div', { style: { fontSize: 12, opacity: 0.8, lineHeight: 1.55 } },
|
|
780
916
|
tr('settings.intro')),
|
|
781
917
|
React.createElement('div', { style: boxS },
|
|
782
|
-
React.createElement('div', { style: { marginBottom:
|
|
918
|
+
React.createElement('div', { style: { marginBottom: 8, fontSize: 13, fontWeight: 600 } }, tr('settings.machines')),
|
|
783
919
|
machines.length
|
|
784
|
-
? machines.map((m) => React.createElement('div', { key: m.id, style: { display: 'flex', gap: 8, alignItems: 'center',
|
|
785
|
-
React.createElement('div', { style: { flex: 1, fontSize: 13 } },
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
React.createElement('
|
|
920
|
+
? machines.map((m) => React.createElement('div', { key: m.id, className: 'dsh-rw-row', style: { display: 'flex', gap: 8, alignItems: 'center', paddingTop: 8, paddingBottom: 8, borderBottom: '1px solid ' + T.border, flexWrap: 'wrap' } },
|
|
921
|
+
React.createElement('div', { style: { flex: 1, fontSize: 13, minWidth: 180 } },
|
|
922
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' } },
|
|
923
|
+
React.createElement('span', { style: { fontWeight: 600 } }, m.name),
|
|
924
|
+
React.createElement('code', { style: { fontSize: 12, opacity: 0.8 } }, m.username + '@' + m.host + ':' + m.port),
|
|
925
|
+
m.id === currentId ? pill('is-ok', tr('settings.current')) : null,
|
|
926
|
+
m.passwordSet ? pill('', tr('settings.badgePwd')) : null,
|
|
927
|
+
(m.credentialBackend && m.credentialBackend !== 'plain') ? pill('', tr('settings.badgeKeychain')) : null,
|
|
928
|
+
m.proxy && m.proxy.host ? pill('', tr('settings.badgeProxy')) : null,
|
|
929
|
+
m.latencyMs != null ? React.createElement('span', { style: { fontSize: 11, opacity: 0.6 } }, m.latencyMs + 'ms') : null,
|
|
930
|
+
)),
|
|
931
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, flexShrink: 0 } },
|
|
932
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => startEdit(m) }, tr('settings.edit')),
|
|
933
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => del(m.id) }, tr('settings.delete')),
|
|
934
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn' + (m.id === currentId ? '' : ' dsh-rw-primary'), style: { ...(m.id === currentId ? buttonS : primaryBtnS), padding: '6px 10px', whiteSpace: 'nowrap' }, onClick: () => useNow(m.id), disabled: m.id === currentId }, tr('settings.setCurrent')))))
|
|
796
935
|
: React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, tr('settings.noMachines')),
|
|
797
936
|
currentId
|
|
798
|
-
? React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', paddingTop:
|
|
937
|
+
? React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', paddingTop: 10, fontSize: 12, color: T.ok } },
|
|
799
938
|
tr('settings.currentMachine', { name: ((machines.find((m) => m.id === currentId) || {}).name || currentId) }),
|
|
800
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 8px', fontSize: 12, marginLeft: 'auto' }, onClick: clearCurrent }, tr('settings.clearCurrent')))
|
|
801
|
-
: React.createElement('div', { style: { paddingTop:
|
|
939
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px', fontSize: 12, marginLeft: 'auto' }, onClick: clearCurrent }, tr('settings.clearCurrent')))
|
|
940
|
+
: React.createElement('div', { style: { paddingTop: 10, fontSize: 12, opacity: 0.6 } }, tr('settings.noActive')),
|
|
802
941
|
),
|
|
803
942
|
React.createElement('div', { style: boxS },
|
|
804
943
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 } },
|
|
805
944
|
React.createElement('div', { style: { fontSize: 13, fontWeight: 600 } }, form.id ? tr('settings.editMachine') : tr('settings.addMachine')),
|
|
806
|
-
React.createElement('button', { style: { ...buttonS, fontSize: 12, padding: '3px 8px' }, onClick: () => (sshEntries ? setSshEntries(null) : importSsh()) }, sshEntries ? tr('settings.sshCollapse') : tr('settings.sshImport'))),
|
|
945
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, fontSize: 12, padding: '3px 8px' }, onClick: () => (sshEntries ? setSshEntries(null) : importSsh()) }, sshEntries ? tr('settings.sshCollapse') : tr('settings.sshImport'))),
|
|
807
946
|
sshEntries ? React.createElement('div', { style: { marginBottom: 8, maxHeight: 160, overflowY: 'auto', border: '1px solid ' + T.border, borderRadius: T.radius } },
|
|
808
|
-
sshEntries.length ? sshEntries.map((e) => React.createElement('div', { key: e.host, onClick: () => applySshEntry(e), style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, borderBottom: '1px solid ' + T.border } },
|
|
947
|
+
sshEntries.length ? sshEntries.map((e) => React.createElement('div', { key: e.host, className: 'dsh-rw-list-item', onClick: () => applySshEntry(e), style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, borderBottom: '1px solid ' + T.border } },
|
|
809
948
|
e.host + ' → ' + (e.hostName || '?') + (e.user ? ' (' + e.user + ')' : '') + (e.identityFile ? ' [key]' : '') + (e.proxyJump ? ' ⛳' : '')))
|
|
810
949
|
: React.createElement('div', { style: { padding: 8, fontSize: 12, opacity: 0.6 } }, tr('settings.sshNoEntries')))
|
|
811
950
|
: null,
|
|
812
|
-
row(tr('settings.name'), React.createElement('input', { value: form.name, onChange: setF('name'), placeholder: tr('settings.name') + ' (e.g. build-server)', style: inputS }), 'n'),
|
|
813
|
-
row(tr('settings.host'), React.createElement('input', { value: form.host, onChange: setF('host'), placeholder: 'IP or hostname', style: inputS }), 'h'),
|
|
814
|
-
row(tr('settings.port'), React.createElement('input', { value: form.port, onChange: setF('port'), placeholder: '22', style: { ...inputS, width: 70 } }), 'p'),
|
|
815
|
-
row(tr('settings.user'), React.createElement('input', { value: form.username, onChange: setF('username'), placeholder: 'root', style: inputS }), 'u'),
|
|
816
|
-
row(tr('settings.password'), React.createElement('input', { type: 'password', value: form.password, onChange: setF('password'), placeholder: 'SSH password (hidden)', style: inputS }), 'w'),
|
|
951
|
+
row(tr('settings.name'), React.createElement('input', { className: 'dsh-rw-input', value: form.name, onChange: setF('name'), placeholder: tr('settings.name') + ' (e.g. build-server)', style: inputS }), 'n'),
|
|
952
|
+
row(tr('settings.host'), React.createElement('input', { className: 'dsh-rw-input', value: form.host, onChange: setF('host'), placeholder: 'IP or hostname', style: inputS }), 'h'),
|
|
953
|
+
row(tr('settings.port'), React.createElement('input', { className: 'dsh-rw-input', value: form.port, onChange: setF('port'), placeholder: '22', style: { ...inputS, width: 70 } }), 'p'),
|
|
954
|
+
row(tr('settings.user'), React.createElement('input', { className: 'dsh-rw-input', value: form.username, onChange: setF('username'), placeholder: 'root', style: inputS }), 'u'),
|
|
955
|
+
row(tr('settings.password'), React.createElement('input', { className: 'dsh-rw-input', type: 'password', value: form.password, onChange: setF('password'), placeholder: 'SSH password (hidden)', style: inputS }), 'w'),
|
|
817
956
|
// 高级配置折叠开关
|
|
818
957
|
React.createElement('button', {
|
|
819
958
|
type: 'button',
|
|
959
|
+
className: 'dsh-rw-btn',
|
|
820
960
|
onClick: () => setAdvOpen(!advOpen),
|
|
821
961
|
style: { ...buttonS, width: '100%', marginBottom: advOpen ? 10 : 0, color: advOpen ? T.primary : 'inherit', fontWeight: advOpen ? 600 : 400, borderStyle: 'dashed' },
|
|
822
962
|
}, advOpen ? tr('settings.advCollapse') : tr('settings.advExpand')),
|
|
@@ -829,7 +969,7 @@ window.__ModuleLoader__.load({
|
|
|
829
969
|
React.createElement('option', { value: 'accept-new' }, tr('settings.hkAccept')),
|
|
830
970
|
React.createElement('option', { value: 'verify' }, tr('settings.hkVerify')),
|
|
831
971
|
React.createElement('option', { value: 'off' }, tr('settings.hkOff'))), 'hkm'),
|
|
832
|
-
React.createElement('div', { style: { display: 'flex', gap: 16, paddingLeft:
|
|
972
|
+
React.createElement('div', { style: { display: 'flex', gap: 16, paddingLeft: LABEL_W + 8, flexWrap: 'wrap', marginBottom: 8 } },
|
|
833
973
|
React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
|
|
834
974
|
React.createElement('input', { type: 'checkbox', checked: form.useAgent, onChange: setFb('useAgent') }), tr('settings.agent')),
|
|
835
975
|
React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
|
|
@@ -843,13 +983,13 @@ window.__ModuleLoader__.load({
|
|
|
843
983
|
React.createElement('input', { type: 'password', value: form.proxyPassword, onChange: setF('proxyPassword'), placeholder: 'Jump password', style: { ...inputS, minWidth: 140 } })), 'pu'),
|
|
844
984
|
row(tr('settings.proxyKey'), React.createElement('input', { value: form.proxyKey, onChange: setF('proxyKey'), placeholder: 'Jump private key path (optional)', style: inputS }), 'pk'),
|
|
845
985
|
) : null,
|
|
846
|
-
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center', flexWrap: 'wrap', marginTop:
|
|
986
|
+
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center', flexWrap: 'wrap', marginTop: 8 } },
|
|
847
987
|
msg ? React.createElement('span', { style: { color: T.ok, fontSize: 12, marginRight: 'auto' } }, msg) : null,
|
|
848
|
-
form.id ? React.createElement('button', { style: buttonS, onClick: () => save('delete') }, tr('settings.delete')) : null,
|
|
849
|
-
React.createElement('button', { style: buttonS, onClick: () => { clearForm(); setErr(''); setTst('idle'); setAdvOpen(false) } }, form.id ? tr('settings.cancelEdit') : tr('settings.clear')),
|
|
850
|
-
React.createElement('button', { style: { ...buttonS, fontFamily: 'monospace', whiteSpace: 'nowrap' }, onClick: testConn, disabled: busy || !form.host.trim() || tst === 'testing' },
|
|
988
|
+
form.id ? React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => save('delete') }, tr('settings.delete')) : null,
|
|
989
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => { clearForm(); setErr(''); setTst('idle'); setAdvOpen(false) } }, form.id ? tr('settings.cancelEdit') : tr('settings.clear')),
|
|
990
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, fontFamily: 'monospace', whiteSpace: 'nowrap' }, onClick: testConn, disabled: busy || !form.host.trim() || tst === 'testing' },
|
|
851
991
|
tst === 'testing' ? tr('settings.testing') : tst === 'ok' ? tr('settings.testOk') : tr('settings.testConn')),
|
|
852
|
-
React.createElement('button', {
|
|
992
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: () => save(form.id ? 'update' : 'add'), disabled: busy || !form.host.trim() }, busy ? tr('settings.saving') : tr('settings.save')),
|
|
853
993
|
),
|
|
854
994
|
),
|
|
855
995
|
// 快捷切换工作区(该机器的最近工作区)
|
|
@@ -857,35 +997,40 @@ window.__ModuleLoader__.load({
|
|
|
857
997
|
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.recentWorkspaces')),
|
|
858
998
|
workspaces.map((w, i) => React.createElement('div', { key: w + i, style: { display: 'flex', gap: 8, alignItems: 'center', padding: '4px 0' } },
|
|
859
999
|
React.createElement('code', { style: { flex: 1, fontSize: 12, fontFamily: 'monospace' } }, w),
|
|
860
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px' }, onClick: () => setWorkspaceNow(w) }, tr('settings.switch'))))) : null,
|
|
1000
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px' }, onClick: () => setWorkspaceNow(w) }, tr('settings.switch'))))) : null,
|
|
861
1001
|
// 端口转发面板
|
|
862
1002
|
React.createElement('div', { style: boxS },
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
React.createElement('
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
React.createElement('
|
|
873
|
-
React.createElement('
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
React.createElement('
|
|
880
|
-
|
|
1003
|
+
sectionToggle(fwdOpen, setFwdOpen, tr('settings.forwards')),
|
|
1004
|
+
fwdOpen ? React.createElement(React.Fragment, null,
|
|
1005
|
+
forwards.length ? forwards.map((f) => React.createElement('div', { key: f.id, className: 'dsh-rw-row', style: { display: 'flex', gap: 8, alignItems: 'center', padding: '6px 0', fontSize: 12, fontFamily: 'monospace' } },
|
|
1006
|
+
React.createElement('span', { style: { color: f.active ? T.ok : T.muted } }, f.active ? '●' : '○'),
|
|
1007
|
+
React.createElement('span', { style: { flex: 1 } }, `${f.direction} 127.0.0.1:${f.listenPort} → ${f.targetHost}:${f.targetPort}${f.autoStart ? ' [auto]' : ''}`),
|
|
1008
|
+
f.active
|
|
1009
|
+
? React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('stop', f.id) }, tr('settings.fwdStop'))
|
|
1010
|
+
: React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('start', f.id) }, tr('settings.fwdStart')),
|
|
1011
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('remove', f.id) }, tr('settings.fwdRemove')))) : null,
|
|
1012
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', marginTop: 6, flexWrap: 'wrap' } },
|
|
1013
|
+
React.createElement('select', { className: 'dsh-rw-input', value: fwdForm.direction, onChange: (e) => setFwdForm({ ...fwdForm, direction: e.target.value }), style: { ...inputS, maxWidth: 110 } },
|
|
1014
|
+
React.createElement('option', { value: 'local' }, tr('settings.fwdDirLocal')),
|
|
1015
|
+
React.createElement('option', { value: 'reverse' }, tr('settings.fwdDirReverse'))),
|
|
1016
|
+
React.createElement('input', { className: 'dsh-rw-input', value: fwdForm.listenPort, onChange: (e) => setFwdForm({ ...fwdForm, listenPort: e.target.value }), placeholder: tr('settings.fwdListen'), style: { ...inputS, maxWidth: 90 } }),
|
|
1017
|
+
React.createElement('input', { className: 'dsh-rw-input', value: fwdForm.targetHost, onChange: (e) => setFwdForm({ ...fwdForm, targetHost: e.target.value }), placeholder: tr('settings.fwdTargetHost'), style: { ...inputS, maxWidth: 120 } }),
|
|
1018
|
+
React.createElement('input', { className: 'dsh-rw-input', value: fwdForm.targetPort, onChange: (e) => setFwdForm({ ...fwdForm, targetPort: e.target.value }), placeholder: tr('settings.fwdTargetPort'), style: { ...inputS, maxWidth: 90 } }),
|
|
1019
|
+
React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
|
|
1020
|
+
React.createElement('input', { type: 'checkbox', checked: fwdForm.autoStart, onChange: (e) => setFwdForm({ ...fwdForm, autoStart: e.target.checked }) }), tr('settings.fwdAuto')),
|
|
1021
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: fwdDefine, disabled: !fwdForm.listenPort }, tr('settings.fwdAdd'))),
|
|
1022
|
+
) : null,
|
|
881
1023
|
),
|
|
882
1024
|
// 审计日志
|
|
883
1025
|
React.createElement('div', { style: boxS },
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1026
|
+
sectionToggle(auditOpen, setAuditOpen, tr('settings.audit'),
|
|
1027
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px', fontSize: 12, marginLeft: 'auto' }, onClick: (e) => { e.stopPropagation(); api('GET', '/dsh-remote/audit?limit=30').then((r) => setAuditLines(r.lines || [])).catch(() => {}) } }, tr('settings.auditRefresh'))),
|
|
1028
|
+
auditOpen
|
|
1029
|
+
? (auditLines.length
|
|
1030
|
+
? React.createElement('div', { style: { maxHeight: 180, overflowY: 'auto', fontSize: 11, fontFamily: 'monospace', lineHeight: 1.6 } },
|
|
1031
|
+
auditLines.map((l, i) => React.createElement('div', { key: i, style: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, l)))
|
|
1032
|
+
: React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, tr('settings.auditEmpty')))
|
|
1033
|
+
: React.createElement('div', { style: { fontSize: 11, opacity: 0.55 } }, String(auditLines.length)),
|
|
889
1034
|
),
|
|
890
1035
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 13 } }, err) : null,
|
|
891
1036
|
React.createElement('div', { style: boxS },
|
|
@@ -896,9 +1041,9 @@ window.__ModuleLoader__.load({
|
|
|
896
1041
|
'v' + upd.current,
|
|
897
1042
|
upd.updateAvailable ? React.createElement('b', { style: { color: T.ok, marginLeft: 6 } }, '→ v' + upd.latest) : null)
|
|
898
1043
|
: React.createElement('span', { style: { opacity: 0.6 } }, tr('settings.updateChecking')),
|
|
899
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px' }, onClick: () => checkUpdate(false), disabled: updBusy }, updBusy ? tr('settings.checking') : tr('settings.checkUpdate')),
|
|
1044
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px' }, onClick: () => checkUpdate(false), disabled: updBusy }, updBusy ? tr('settings.checking') : tr('settings.checkUpdate')),
|
|
900
1045
|
upd && upd.updateAvailable
|
|
901
|
-
? React.createElement('button', {
|
|
1046
|
+
? React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: { ...primaryBtnS, padding: '2px 10px' }, onClick: applyUpdateNow, disabled: updBusy }, tr('settings.updateNow'))
|
|
902
1047
|
: null,
|
|
903
1048
|
),
|
|
904
1049
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 6, fontSize: 12 } },
|
|
@@ -993,6 +1138,7 @@ window.__ModuleLoader__.load({
|
|
|
993
1138
|
// 本机浏览浮层(browse 后端):{ path, home, crumbs, entries, truncated }。
|
|
994
1139
|
const [localPop, setLocalPop] = React.useState(null)
|
|
995
1140
|
const [newDirName, setNewDirName] = React.useState('')
|
|
1141
|
+
const [mkdirPrompt, setMkdirPrompt] = React.useState(false)
|
|
996
1142
|
const suggestTimer = React.useRef(null)
|
|
997
1143
|
const suggestRef = React.useRef(null)
|
|
998
1144
|
React.useEffect(() => {
|
|
@@ -1009,6 +1155,7 @@ window.__ModuleLoader__.load({
|
|
|
1009
1155
|
document.addEventListener('mousedown', onDown)
|
|
1010
1156
|
return () => document.removeEventListener('mousedown', onDown)
|
|
1011
1157
|
}, [mOpen])
|
|
1158
|
+
React.useEffect(() => { if (open) ensureChromeCss() }, [open])
|
|
1012
1159
|
|
|
1013
1160
|
const sortItems = (list) => {
|
|
1014
1161
|
const dirs = list.filter((it) => it.dir)
|
|
@@ -1235,10 +1382,16 @@ window.__ModuleLoader__.load({
|
|
|
1235
1382
|
const mkdirHere = () => {
|
|
1236
1383
|
const last = levels && levels.length ? levels[levels.length - 1] : null
|
|
1237
1384
|
const base = (last && last.path) || '/'
|
|
1238
|
-
|
|
1239
|
-
|
|
1385
|
+
setMkdirPrompt({ base })
|
|
1386
|
+
}
|
|
1387
|
+
const confirmMkdirHere = (nm) => {
|
|
1388
|
+
const last = levels && levels.length ? levels[levels.length - 1] : null
|
|
1389
|
+
const base = (mkdirPrompt && mkdirPrompt.base) || (last && last.path) || '/'
|
|
1390
|
+
setMkdirPrompt(false)
|
|
1391
|
+
const name = String(nm || '').trim()
|
|
1392
|
+
if (!name) return
|
|
1240
1393
|
const sep = base.includes('\\') ? '\\' : '/'
|
|
1241
|
-
const full = base === '/' ? '/' +
|
|
1394
|
+
const full = base === '/' ? '/' + name : base + sep + name
|
|
1242
1395
|
api('POST', '/dsh-remote/fs', { op: 'mkdir', path: full })
|
|
1243
1396
|
.then(() => { setErr(''); loadLevels(machineId, base, (levels ? levels.length - 1 : 0)) })
|
|
1244
1397
|
.catch((e) => setErr(String((e && e.message) || e)))
|
|
@@ -1297,6 +1450,7 @@ window.__ModuleLoader__.load({
|
|
|
1297
1450
|
: (cur.entries.length ? cur.entries.slice(0, 400).map((it, i) => React.createElement('div', {
|
|
1298
1451
|
key: it.path || i, title: tr('picker.enterDir', { name: it.name }),
|
|
1299
1452
|
onClick: () => loadLocal(it.path),
|
|
1453
|
+
className: 'dsh-rw-list-item',
|
|
1300
1454
|
style: { padding: '7px 12px', cursor: 'pointer', color: T.label, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: 'flex', gap: 8, alignItems: 'center', fontSize: 13, borderBottom: '1px solid ' + T.border },
|
|
1301
1455
|
},
|
|
1302
1456
|
React.createElement('span', null, '📁'),
|
|
@@ -1306,10 +1460,10 @@ window.__ModuleLoader__.load({
|
|
|
1306
1460
|
),
|
|
1307
1461
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 12, padding: '4px 12px' } }, err) : null,
|
|
1308
1462
|
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', padding: '8px 12px', borderTop: '1px solid ' + T.border } },
|
|
1309
|
-
React.createElement('input', { value: newDirName, onChange: (e) => setNewDirName(e.target.value), placeholder: tr('picker.newFolder'), style: { ...inputS, width: 110, flex: '0 0 auto', padding: '4px 8px', fontSize: 12 } }),
|
|
1310
|
-
React.createElement('button', { style: { ...buttonS, padding: '4px 10px', whiteSpace: 'nowrap' }, onClick: mkdirLocal, disabled: loading || !newDirName.trim() || !cur.path }, tr('picker.create')),
|
|
1463
|
+
React.createElement('input', { className: 'dsh-rw-input', value: newDirName, onChange: (e) => setNewDirName(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') mkdirLocal() }, placeholder: tr('picker.newFolder'), style: { ...inputS, width: 110, flex: '0 0 auto', padding: '4px 8px', fontSize: 12 } }),
|
|
1464
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '4px 10px', whiteSpace: 'nowrap' }, onClick: mkdirLocal, disabled: loading || !newDirName.trim() || !cur.path }, tr('picker.create')),
|
|
1311
1465
|
React.createElement('span', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, tr('picker.selected', { path: cur.path })),
|
|
1312
|
-
React.createElement('button', {
|
|
1466
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: { ...primaryBtnS, whiteSpace: 'nowrap' }, onClick: () => acceptLocalPick(cur.path) }, tr('picker.chooseThis')),
|
|
1313
1467
|
),
|
|
1314
1468
|
),
|
|
1315
1469
|
)
|
|
@@ -1333,6 +1487,7 @@ window.__ModuleLoader__.load({
|
|
|
1333
1487
|
: (entries.length ? entries.slice(0, 400).map((it, i) => React.createElement('div', {
|
|
1334
1488
|
key: i, title: it.drive ? tr('picker.enterDrive', { name: it.name }) : (it.dir ? tr('picker.enterDir', { name: it.name }) : tr('picker.fileAt', { name: it.name })),
|
|
1335
1489
|
onClick: it.dir ? () => enterDir(it) : undefined,
|
|
1490
|
+
className: it.dir ? 'dsh-rw-list-item' : undefined,
|
|
1336
1491
|
style: { padding: '6px 10px', cursor: it.dir ? 'pointer' : 'default', color: T.label, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: 'flex', gap: 8, alignItems: 'center', fontSize: 13, borderBottom: '1px solid ' + T.border },
|
|
1337
1492
|
},
|
|
1338
1493
|
React.createElement('span', { style: { flexShrink: 0, display: 'inline-flex', color: T.label, fontSize: 13 } }, it.drive ? '💽' : (it.dir
|
|
@@ -1344,31 +1499,59 @@ window.__ModuleLoader__.load({
|
|
|
1344
1499
|
),
|
|
1345
1500
|
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center', padding: '6px 10px', borderTop: '1px solid ' + T.border } },
|
|
1346
1501
|
React.createElement('span', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, tr('picker.selected', { path: (last.path || (win ? tr('picker.driveRoot') : '/')) })),
|
|
1347
|
-
React.createElement('button', {
|
|
1502
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: () => { acceptBrowserPick(last.path); setPopOpen(false) } }, tr('picker.chooseThis')),
|
|
1348
1503
|
),
|
|
1349
1504
|
)
|
|
1350
1505
|
}
|
|
1351
1506
|
|
|
1507
|
+
React.useEffect(() => {
|
|
1508
|
+
if (!open) return
|
|
1509
|
+
try {
|
|
1510
|
+
if (typeof document === 'undefined') return
|
|
1511
|
+
const onKey = (e) => {
|
|
1512
|
+
if (mkdirPrompt) return
|
|
1513
|
+
if (e.key === 'Escape') {
|
|
1514
|
+
if (popOpen) { e.preventDefault(); setPopOpen(false); return }
|
|
1515
|
+
if (suggestOpen) { e.preventDefault(); setSuggestOpen(false); return }
|
|
1516
|
+
if (!busy) { e.preventDefault(); onCancel() }
|
|
1517
|
+
return
|
|
1518
|
+
}
|
|
1519
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
1520
|
+
e.preventDefault()
|
|
1521
|
+
if (tab === 'local' && path.trim()) onPicked(path)
|
|
1522
|
+
else if (tab === 'remote' && path.trim()) commitPath(path)
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
document.addEventListener('keydown', onKey)
|
|
1526
|
+
return () => document.removeEventListener('keydown', onKey)
|
|
1527
|
+
} catch { return undefined }
|
|
1528
|
+
}, [open, busy, tab, path, machineId, popOpen, suggestOpen, mkdirPrompt])
|
|
1529
|
+
|
|
1352
1530
|
if (!open) return null
|
|
1353
|
-
const tabBtn = (t, lbl) => React.createElement('button', {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1531
|
+
const tabBtn = (t, lbl) => React.createElement('button', {
|
|
1532
|
+
type: 'button',
|
|
1533
|
+
className: 'dsh-rw-btn dsh-rw-tab' + (tab === t ? ' is-active' : ''),
|
|
1534
|
+
onClick: () => switchTab(t),
|
|
1535
|
+
style: { ...buttonS, fontWeight: tab === t ? 700 : 400, padding: '5px 14px' },
|
|
1536
|
+
}, lbl)
|
|
1537
|
+
return React.createElement('div', { style: { position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }, onClick: () => { if (!busy && !mkdirPrompt) onCancel() } },
|
|
1538
|
+
React.createElement('div', { className: 'dsh-rw-modal-panel', style: { background: v('--dsw-alias-bg-layer-1', '#18181b'), color: T.label, border: '1px solid ' + T.borderStrong, borderRadius: 12, boxShadow: '0 12px 48px rgba(0,0,0,0.5)', width: 'min(600px, 94vw)', maxHeight: 'min(680px, 92vh)', display: 'flex', flexDirection: 'column', padding: 16, boxSizing: 'border-box' }, onClick: (e) => e.stopPropagation() },
|
|
1539
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 } },
|
|
1540
|
+
React.createElement('div', { style: { fontSize: 15, fontWeight: 650 } }, tr('picker.title')),
|
|
1541
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' }, onClick: () => { if (!busy) onCancel() }, disabled: busy }, tr('picker.close')),
|
|
1359
1542
|
),
|
|
1360
|
-
React.createElement('div', {
|
|
1543
|
+
React.createElement('div', { className: 'dsh-rw-tabs', style: { marginBottom: 10, alignSelf: 'flex-start' } },
|
|
1361
1544
|
tabBtn('local', tr('picker.tabLocal')),
|
|
1362
1545
|
tabBtn('remote', tr('picker.tabRemote')),
|
|
1363
1546
|
),
|
|
1364
1547
|
tab === 'local'
|
|
1365
1548
|
? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
|
|
1366
|
-
React.createElement('div', { style: { fontSize: 12, opacity: 0.8 } }, tr('picker.localIntro')),
|
|
1549
|
+
React.createElement('div', { style: { fontSize: 12, opacity: 0.8, lineHeight: 1.5 } }, tr('picker.localIntro')),
|
|
1367
1550
|
React.createElement('div', { style: { display: 'flex', gap: 6 } },
|
|
1368
|
-
React.createElement('input', { value: path, onChange: (e) => setPath(e.target.value), placeholder: tr('picker.localPlaceholder'), style: inputS }),
|
|
1369
|
-
React.createElement('button', { style:
|
|
1551
|
+
React.createElement('input', { className: 'dsh-rw-input', value: path, onChange: (e) => setPath(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter' && path.trim()) { e.preventDefault(); onPicked(path) } }, placeholder: tr('picker.localPlaceholder'), style: inputS }),
|
|
1552
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: () => (path.trim() ? onPicked(path) : undefined), disabled: !path.trim() }, tr('picker.useLocalPath')),
|
|
1370
1553
|
),
|
|
1371
|
-
React.createElement('button', { style: { ...buttonS, alignSelf: 'flex-start' }, onClick: chooseLocal, disabled: loading }, loading ? tr('picker.opening') : tr('picker.openSysPicker')),
|
|
1554
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, alignSelf: 'flex-start' }, onClick: chooseLocal, disabled: loading }, loading ? tr('picker.opening') : tr('picker.openSysPicker')),
|
|
1372
1555
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 12 } }, err) : null,
|
|
1373
1556
|
localPop ? renderLocalPopup() : null,
|
|
1374
1557
|
)
|
|
@@ -1376,31 +1559,44 @@ window.__ModuleLoader__.load({
|
|
|
1376
1559
|
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
|
|
1377
1560
|
React.createElement('label', { style: { fontSize: 12, opacity: 0.8, whiteSpace: 'nowrap' } }, tr('picker.remoteMachine')),
|
|
1378
1561
|
React.createElement('div', { ref: ddRef, style: { position: 'relative', display: 'inline-block' } },
|
|
1379
|
-
React.createElement('button', { style: { ...inputS, textAlign: 'left', cursor: 'pointer', minWidth: 200, maxWidth: 320, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, onClick: () => setMOpen((x) => !x) },
|
|
1562
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...inputS, textAlign: 'left', cursor: 'pointer', minWidth: 200, maxWidth: 320, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, onClick: () => setMOpen((x) => !x) },
|
|
1380
1563
|
(machines.find((m) => m.id === machineId) || {}).name || (machineId ? '…' : tr('picker.chooseMachine')),
|
|
1381
1564
|
),
|
|
1382
1565
|
mOpen
|
|
1383
|
-
? React.createElement('div', { style: { position: 'absolute', top: '100%', left: 0, zIndex: 1000, marginTop: 2, minWidth: 300, maxHeight: 260, overflow: 'auto', background: v('--dsw-alias-bg-overlay', '#1f1f23'), border: '1px solid ' + T.border, borderRadius: T.radius, padding: 4 } },
|
|
1566
|
+
? React.createElement('div', { style: { position: 'absolute', top: '100%', left: 0, zIndex: 1000, marginTop: 2, minWidth: 300, maxHeight: 260, overflow: 'auto', background: v('--dsw-alias-bg-overlay', '#1f1f23'), color: T.label, border: '1px solid ' + T.border, borderRadius: T.radius, padding: 4 } },
|
|
1384
1567
|
machines.length
|
|
1385
|
-
? machines.map((m) => React.createElement('div', { key: m.id, onClick: () => { setMachineId(m.id); setRecent(m.recentWorkspaces || []); setLevels(null); setPath(''); setMOpen(false); if (m.id) loadLevels(m.id, '', 0) }, style: { padding: '6px 8px', borderRadius: 4, cursor: 'pointer', color: m.id === machineId ? T.ok : 'inherit' } }, m.name + ' (' + m.username + '@' + m.host + ':' + m.port + ')'))
|
|
1568
|
+
? machines.map((m) => React.createElement('div', { key: m.id, className: 'dsh-rw-list-item', onClick: () => { setMachineId(m.id); setRecent(m.recentWorkspaces || []); setLevels(null); setPath(''); setMOpen(false); if (m.id) loadLevels(m.id, '', 0) }, style: { padding: '6px 8px', borderRadius: 4, cursor: 'pointer', color: m.id === machineId ? T.ok : 'inherit' } }, m.name + ' (' + m.username + '@' + m.host + ':' + m.port + ')'))
|
|
1386
1569
|
: React.createElement('div', { style: { padding: 6, opacity: 0.6 } }, tr('picker.noMachines')),
|
|
1387
1570
|
)
|
|
1388
1571
|
: null,
|
|
1389
1572
|
),
|
|
1390
1573
|
),
|
|
1391
|
-
// 最近工作区快捷入口
|
|
1392
1574
|
recent.length ? React.createElement('div', { style: { display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' } },
|
|
1393
1575
|
React.createElement('span', { style: { fontSize: 11, opacity: 0.6, whiteSpace: 'nowrap' } }, tr('picker.recent')),
|
|
1394
|
-
recent.map((w, i) => React.createElement('button', { key: w + i, onClick: () => { setPath(w); setErr(''); continueSuggest(w) }, style: { ...buttonS, padding: '2px 8px', fontSize: 11, fontFamily: 'monospace' } }, w))) : null,
|
|
1576
|
+
recent.map((w, i) => React.createElement('button', { type: 'button', className: 'dsh-rw-btn', key: w + i, onClick: () => { setPath(w); setErr(''); continueSuggest(w) }, style: { ...buttonS, padding: '2px 8px', fontSize: 11, fontFamily: 'monospace' } }, w))) : null,
|
|
1395
1577
|
React.createElement('div', { ref: suggestRef, style: { position: 'relative' } },
|
|
1396
1578
|
React.createElement('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
1397
|
-
React.createElement('input', {
|
|
1398
|
-
|
|
1399
|
-
|
|
1579
|
+
React.createElement('input', {
|
|
1580
|
+
className: 'dsh-rw-input',
|
|
1581
|
+
value: path,
|
|
1582
|
+
onChange: (e) => onPathChange(e.target.value),
|
|
1583
|
+
onFocus: () => loadSuggestions(path),
|
|
1584
|
+
onKeyDown: (e) => {
|
|
1585
|
+
if (e.key === 'Enter') {
|
|
1586
|
+
e.preventDefault()
|
|
1587
|
+
if (suggestOpen && suggest.length) selectSuggestion(suggest[0])
|
|
1588
|
+
else if (path.trim()) commitPath(path)
|
|
1589
|
+
}
|
|
1590
|
+
},
|
|
1591
|
+
placeholder: (machineId ? (win ? tr('picker.pathPlaceholderWin') : tr('picker.pathPlaceholder')) : tr('picker.chooseMachineFirst')),
|
|
1592
|
+
disabled: !machineId, style: { ...inputS, flex: 1, minWidth: 120 },
|
|
1593
|
+
}),
|
|
1594
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, whiteSpace: 'nowrap' }, onClick: () => { if (!machineId) return; const p = path.trim(); loadLevels(machineId, p || '', 0); setPopOpen(true) }, disabled: !machineId }, tr('picker.browse')),
|
|
1595
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, whiteSpace: 'nowrap' }, onClick: pickHome, disabled: !machineId }, tr('picker.home')),
|
|
1400
1596
|
),
|
|
1401
1597
|
(suggestOpen && suggest.length)
|
|
1402
|
-
? React.createElement('div', { style: { marginTop: 6, background: v('--dsw-alias-bg-overlay', '#1e1e1e'), border: '1px solid ' + T.borderStrong, borderRadius: 8, maxHeight: 200, overflowY: 'auto', boxShadow: '0 6px 24px rgba(0,0,0,0.25)' } },
|
|
1403
|
-
suggest.map((s, i) => React.createElement('div', { key: s + i, onMouseDown: () => selectSuggestion(s), style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, fontFamily: 'monospace', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, s)),
|
|
1598
|
+
? React.createElement('div', { style: { marginTop: 6, background: v('--dsw-alias-bg-overlay', '#1e1e1e'), color: T.label, border: '1px solid ' + T.borderStrong, borderRadius: 8, maxHeight: 200, overflowY: 'auto', boxShadow: '0 6px 24px rgba(0,0,0,0.25)' } },
|
|
1599
|
+
suggest.map((s, i) => React.createElement('div', { key: s + i, className: 'dsh-rw-list-item', onMouseDown: () => selectSuggestion(s), style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, fontFamily: 'monospace', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, s)),
|
|
1404
1600
|
)
|
|
1405
1601
|
: null,
|
|
1406
1602
|
),
|
|
@@ -1408,15 +1604,22 @@ window.__ModuleLoader__.load({
|
|
|
1408
1604
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 12 } }, err) : null,
|
|
1409
1605
|
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center' } },
|
|
1410
1606
|
React.createElement('span', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, path ? tr('picker.selected', { path }) : ''),
|
|
1411
|
-
React.createElement('button', {
|
|
1607
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: () => commitPath(path), disabled: busy || !machineId || !path.trim() }, busy ? tr('picker.mirroring') : tr('picker.setRemoteWs')),
|
|
1412
1608
|
),
|
|
1413
1609
|
),
|
|
1414
|
-
|
|
1610
|
+
React.createElement('div', { style: { fontSize: 11, opacity: 0.55, paddingTop: 8 } }, tr('picker.hintKeys')),
|
|
1611
|
+
(tab === 'local') ? React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' } }, React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: onCancel }, tr('picker.cancel'))) : null,
|
|
1415
1612
|
React.createElement('div', { style: { display: 'flex', gap: 10, justifyContent: 'center', alignItems: 'center', paddingTop: 6, fontSize: 10, opacity: 0.5 } },
|
|
1416
|
-
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, '
|
|
1613
|
+
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, tr('picker.star')),
|
|
1417
1614
|
React.createElement('span', {}, '·'),
|
|
1418
|
-
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote/issues/new', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, '
|
|
1615
|
+
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote/issues/new', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, tr('picker.feedback')),
|
|
1419
1616
|
),
|
|
1617
|
+
mkdirPrompt ? React.createElement(PromptDialog, {
|
|
1618
|
+
title: tr('picker.newDirName', { path: mkdirPrompt.base || '/' }),
|
|
1619
|
+
placeholder: tr('explorer.namePlaceholder'),
|
|
1620
|
+
onSubmit: confirmMkdirHere,
|
|
1621
|
+
onCancel: () => setMkdirPrompt(false),
|
|
1622
|
+
}) : null,
|
|
1420
1623
|
),
|
|
1421
1624
|
)
|
|
1422
1625
|
}
|
|
@@ -1425,8 +1628,8 @@ window.__ModuleLoader__.load({
|
|
|
1425
1628
|
const SIDEBAR_EXPLORER_ID = 'dsh-remote:explorer'
|
|
1426
1629
|
const SIDEBAR_FILE_ID = 'dsh-remote:file'
|
|
1427
1630
|
|
|
1428
|
-
function fetchRemoteStatus() {
|
|
1429
|
-
return api('GET', '/dsh-remote/status').catch(() => null)
|
|
1631
|
+
function fetchRemoteStatus(sessionId) {
|
|
1632
|
+
return api('GET', withSessionQuery('/dsh-remote/status', sessionId)).catch(() => null)
|
|
1430
1633
|
}
|
|
1431
1634
|
|
|
1432
1635
|
// Resolve the remote path this session shows. Issue #13: the ONLY source
|
|
@@ -1458,14 +1661,14 @@ window.__ModuleLoader__.load({
|
|
|
1458
1661
|
.catch(() => '')
|
|
1459
1662
|
}
|
|
1460
1663
|
|
|
1461
|
-
function readRemoteFile(path, maxBytes) {
|
|
1462
|
-
return api('POST', '/dsh-remote/read', { path, maxBytes }).catch((e) => {
|
|
1664
|
+
function readRemoteFile(path, maxBytes, sessionId) {
|
|
1665
|
+
return api('POST', '/dsh-remote/read', withSessionBody({ path, maxBytes }, sessionId)).catch((e) => {
|
|
1463
1666
|
throw new Error(tr('file.readFail', { msg: ((e && e.message) || e) }))
|
|
1464
1667
|
})
|
|
1465
1668
|
}
|
|
1466
1669
|
|
|
1467
|
-
function listRemoteDir(path) {
|
|
1468
|
-
return api('GET', '/dsh-remote/ls?path=' + encodeURIComponent(path || '')).catch((e) => {
|
|
1670
|
+
function listRemoteDir(path, sessionId) {
|
|
1671
|
+
return api('GET', withSessionQuery('/dsh-remote/ls?path=' + encodeURIComponent(path || ''), sessionId)).catch((e) => {
|
|
1469
1672
|
throw new Error(tr('file.listFail', { msg: ((e && e.message) || e) }))
|
|
1470
1673
|
})
|
|
1471
1674
|
}
|
|
@@ -1675,7 +1878,11 @@ window.__ModuleLoader__.load({
|
|
|
1675
1878
|
const [hoverPath, setHoverPath] = React.useState('') // row hover highlight
|
|
1676
1879
|
/** Row whose path was just copied — shows a brief「已复制」label. */
|
|
1677
1880
|
const [copiedPath, setCopiedPath] = React.useState('')
|
|
1881
|
+
const [namePrompt, setNamePrompt] = React.useState(null) // { kind, path, name }
|
|
1882
|
+
React.useEffect(() => { ensureChromeCss() }, [])
|
|
1678
1883
|
const expanded = Array.isArray(props.expanded) ? props.expanded : []
|
|
1884
|
+
const scopeCwd = props.scope && props.scope.cwd
|
|
1885
|
+
const sessionId = props.scope && props.scope.sessionId
|
|
1679
1886
|
|
|
1680
1887
|
const storeLevel = (path, level) => {
|
|
1681
1888
|
dataRef.current = { ...dataRef.current, [path]: level }
|
|
@@ -1691,7 +1898,7 @@ window.__ModuleLoader__.load({
|
|
|
1691
1898
|
if (existing && !existing.loading && opts && opts.keep) return
|
|
1692
1899
|
if (cur[dir] !== undefined && !cur[dir].loading) return
|
|
1693
1900
|
storeLevel(dir, { loading: true })
|
|
1694
|
-
listRemoteDir(dir)
|
|
1901
|
+
listRemoteDir(dir, sessionId)
|
|
1695
1902
|
.then((res) => {
|
|
1696
1903
|
const items = Array.isArray(res && res.items) ? res.items : []
|
|
1697
1904
|
const sorted = items.slice().sort((a, b) => {
|
|
@@ -1712,14 +1919,12 @@ window.__ModuleLoader__.load({
|
|
|
1712
1919
|
storeLevel(dir, { error: String((e && e.message) || e) })
|
|
1713
1920
|
}
|
|
1714
1921
|
})
|
|
1715
|
-
}, [])
|
|
1922
|
+
}, [sessionId])
|
|
1716
1923
|
|
|
1717
1924
|
// Resolve this session's remote root, then load it and every expanded dir.
|
|
1718
1925
|
// Re-runs whenever the session scope changes (switching conversations, or
|
|
1719
1926
|
// the async sessionCwd fetch landing), so the sidebar follows the session's
|
|
1720
1927
|
// own workspace dir.
|
|
1721
|
-
const scopeCwd = props.scope && props.scope.cwd
|
|
1722
|
-
const sessionId = props.scope && props.scope.sessionId
|
|
1723
1928
|
const rootRef = React.useRef('')
|
|
1724
1929
|
const refreshStatus = React.useCallback((opts) => {
|
|
1725
1930
|
const applyRoot = (r, connected) => {
|
|
@@ -1739,14 +1944,17 @@ window.__ModuleLoader__.load({
|
|
|
1739
1944
|
// Not connected (e.g. freshly restarted, machine no longer active):
|
|
1740
1945
|
// show a clear disconnected state instead of firing ls requests
|
|
1741
1946
|
// that all fail with AggregateError / 500 in a loop.
|
|
1742
|
-
if
|
|
1947
|
+
// A bound remote session lists on demand even if this process has
|
|
1948
|
+
// not yet opened the SSH socket; only an unbound/disconnected
|
|
1949
|
+
// machine-scoped view should show the empty "未连接" state.
|
|
1950
|
+
if (!connected && !sessionId) {
|
|
1743
1951
|
storeLevel(r, { error: tr('explorer.notConnected') })
|
|
1744
1952
|
return
|
|
1745
1953
|
}
|
|
1746
1954
|
if (!sameRoot) {
|
|
1747
1955
|
storeLevel(r, { loading: true })
|
|
1748
1956
|
}
|
|
1749
|
-
listRemoteDir(r)
|
|
1957
|
+
listRemoteDir(r, sessionId)
|
|
1750
1958
|
.then((res) => {
|
|
1751
1959
|
const items = Array.isArray(res && res.items) ? res.items : []
|
|
1752
1960
|
const sorted = items.slice().sort((a, b) => {
|
|
@@ -1779,7 +1987,7 @@ window.__ModuleLoader__.load({
|
|
|
1779
1987
|
return Promise.resolve('')
|
|
1780
1988
|
}
|
|
1781
1989
|
resolveRoot().then((r) => {
|
|
1782
|
-
fetchRemoteStatus().then((s) => {
|
|
1990
|
+
fetchRemoteStatus(sessionId).then((s) => {
|
|
1783
1991
|
setStatus(s)
|
|
1784
1992
|
// Issue #13: NEVER fall back to the machine-level workspace here —
|
|
1785
1993
|
// a session whose cwd is not a mirror is a LOCAL session and shows
|
|
@@ -1807,12 +2015,12 @@ window.__ModuleLoader__.load({
|
|
|
1807
2015
|
const host = document.createElement('div')
|
|
1808
2016
|
host.style.cssText = 'position:fixed;z-index:999999;pointer-events:auto'
|
|
1809
2017
|
const box = document.createElement('div')
|
|
1810
|
-
box.style.cssText = 'position:fixed;left:' + menu.x + 'px;top:' + menu.y + 'px;z-index:999999;background
|
|
2018
|
+
box.style.cssText = 'position:fixed;left:' + menu.x + 'px;top:' + menu.y + 'px;z-index:999999;background:var(--dsw-alias-bg-overlay,#1f1f23);border:1px solid var(--dsw-alias-border-l3,rgba(128,128,128,0.5));border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,0.4);min-width:170px;font-family:system-ui,sans-serif;color:var(--dsw-alias-label-primary,#e4e4e7)'
|
|
1811
2019
|
const mkItem = (label, danger, fn) => {
|
|
1812
2020
|
const d = document.createElement('div')
|
|
1813
2021
|
d.textContent = label
|
|
1814
|
-
d.style.cssText = 'padding:6px 10px;cursor:pointer;font-size:12px;color:' + (danger ? '
|
|
1815
|
-
d.onmouseenter = () => { d.style.background = 'rgba(128,128,128,0.14)' }
|
|
2022
|
+
d.style.cssText = 'padding:6px 10px;cursor:pointer;font-size:12px;color:' + (danger ? 'var(--dsw-static-red-400,#e06c75)' : 'inherit') + ';border-radius:4px;white-space:nowrap;transition:background 0.1s'
|
|
2023
|
+
d.onmouseenter = () => { d.style.background = 'var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,0.14))' }
|
|
1816
2024
|
d.onmouseleave = () => { d.style.background = 'transparent' }
|
|
1817
2025
|
d.addEventListener('click', (e) => { e.stopPropagation(); setMenu(null); fn() })
|
|
1818
2026
|
return d
|
|
@@ -1821,7 +2029,7 @@ window.__ModuleLoader__.load({
|
|
|
1821
2029
|
box.appendChild(mkItem(tr('explorer.menuDownload'), false, () => doFs('download', { path: menu.path })))
|
|
1822
2030
|
box.appendChild(mkItem(tr('explorer.menuCopyRel'), false, () => copyPath(relPath(menu.path), menu.path)))
|
|
1823
2031
|
box.appendChild(mkItem(tr('explorer.menuCopyAbs'), false, () => copyPath(menu.path, menu.path)))
|
|
1824
|
-
box.appendChild(mkItem(tr('explorer.menuRename'), false, () => {
|
|
2032
|
+
box.appendChild(mkItem(tr('explorer.menuRename'), false, () => { setNamePrompt({ kind: 'rename', path: menu.path, name: menu.name }) }))
|
|
1825
2033
|
box.appendChild(mkItem(menu.isDir ? tr('explorer.menuDeleteDir') : tr('explorer.menuDelete'), true, () => { if (window.confirm(tr('explorer.deleteConfirm', { path: menu.path }))) doFs('remove', { path: menu.path }, true) }))
|
|
1826
2034
|
host.appendChild(box)
|
|
1827
2035
|
document.body.appendChild(host)
|
|
@@ -1839,7 +2047,7 @@ window.__ModuleLoader__.load({
|
|
|
1839
2047
|
|
|
1840
2048
|
const doFs = (op, payload, refreshParent) => {
|
|
1841
2049
|
setBusy(op)
|
|
1842
|
-
return api('POST', '/dsh-remote/fs', { op, ...payload })
|
|
2050
|
+
return api('POST', '/dsh-remote/fs', withSessionBody({ op, ...payload }, sessionId))
|
|
1843
2051
|
.then(() => {
|
|
1844
2052
|
setErr('')
|
|
1845
2053
|
if (refreshParent) {
|
|
@@ -1859,7 +2067,8 @@ window.__ModuleLoader__.load({
|
|
|
1859
2067
|
}
|
|
1860
2068
|
const ctxItem = (label, danger, fn) => React.createElement('div', {
|
|
1861
2069
|
onClick: (e) => { e.stopPropagation(); setMenu(null); fn() },
|
|
1862
|
-
|
|
2070
|
+
className: 'dsh-rw-list-item',
|
|
2071
|
+
style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, color: danger ? T.danger : T.label, borderRadius: 4, whiteSpace: 'nowrap' },
|
|
1863
2072
|
}, label)
|
|
1864
2073
|
|
|
1865
2074
|
// The path shown by「复制相对地址」: relative to this session's remote
|
|
@@ -1949,11 +2158,11 @@ window.__ModuleLoader__.load({
|
|
|
1949
2158
|
return React.createElement('div', { key: dir + ':missing', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 13, opacity: 0.6 } }, tr('explorer.dirMissing'))
|
|
1950
2159
|
}
|
|
1951
2160
|
if (level.error !== undefined) {
|
|
1952
|
-
return React.createElement('div', { key: dir + ':err', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 13, color:
|
|
2161
|
+
return React.createElement('div', { key: dir + ':err', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 13, color: T.danger } }, level.error)
|
|
1953
2162
|
}
|
|
1954
2163
|
const items = level.entries || []
|
|
1955
2164
|
const soft = level.softError
|
|
1956
|
-
|
|
2165
|
+
? React.createElement('div', { key: dir + ':softerr', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 11, opacity: 0.6, color: T.warn } }, tr('explorer.softError', { msg: level.softError }))
|
|
1957
2166
|
: null
|
|
1958
2167
|
if (!items.length) return soft || null
|
|
1959
2168
|
return React.createElement('div', { key: dir + ':list' },
|
|
@@ -1966,13 +2175,15 @@ window.__ModuleLoader__.load({
|
|
|
1966
2175
|
const rootLevel = data[root]
|
|
1967
2176
|
|
|
1968
2177
|
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 } },
|
|
1969
|
-
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', padding: '6px 8px', borderBottom: '1px solid
|
|
1970
|
-
React.createElement('button', { style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: refreshStatus, title: tr('explorer.refresh') }, '↻'),
|
|
2178
|
+
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', padding: '6px 8px', borderBottom: '1px solid ' + T.border } },
|
|
2179
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: refreshStatus, title: tr('explorer.refresh') }, '↻'),
|
|
1971
2180
|
React.createElement('button', {
|
|
2181
|
+
type: 'button',
|
|
2182
|
+
className: 'dsh-rw-btn',
|
|
1972
2183
|
style: { ...buttonS, padding: '3px 8px', fontSize: 12 },
|
|
1973
|
-
onClick: () => {
|
|
2184
|
+
onClick: () => setNamePrompt({ kind: 'mkdir', path: root || '/', name: '' }),
|
|
1974
2185
|
}, tr('explorer.newDir')),
|
|
1975
|
-
React.createElement('button', { style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: () => setPicker(true) }, tr('explorer.more')),
|
|
2186
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: () => setPicker(true) }, tr('explorer.more')),
|
|
1976
2187
|
),
|
|
1977
2188
|
status && status.workspace && root
|
|
1978
2189
|
? React.createElement('div', { style: { fontSize: 11, opacity: 0.7, padding: '4px 8px', wordBreak: 'break-all' } }, tr('explorer.remoteWs', { path: root, host: (status.host ? tr('explorer.hostTag', { user: status.username, host: status.host }) : '') }))
|
|
@@ -2000,7 +2211,25 @@ window.__ModuleLoader__.load({
|
|
|
2000
2211
|
),
|
|
2001
2212
|
// context menu — now rendered via DOM portal in useEffect above
|
|
2002
2213
|
null,
|
|
2003
|
-
err ? React.createElement('div', { style: { color:
|
|
2214
|
+
err ? React.createElement('div', { style: { color: T.danger, fontSize: 12, padding: '4px 8px' } }, (busy ? tr('explorer.busy') : '') + err) : null,
|
|
2215
|
+
namePrompt ? React.createElement(PromptDialog, {
|
|
2216
|
+
title: namePrompt.kind === 'rename' ? tr('explorer.renameTitle') : tr('explorer.mkdirTitle'),
|
|
2217
|
+
initial: namePrompt.name || '',
|
|
2218
|
+
placeholder: namePrompt.kind === 'rename' ? tr('explorer.renamePrompt') : tr('explorer.namePlaceholder'),
|
|
2219
|
+
onCancel: () => setNamePrompt(null),
|
|
2220
|
+
onSubmit: (nm) => {
|
|
2221
|
+
const name = String(nm || '').trim()
|
|
2222
|
+
const cur = namePrompt
|
|
2223
|
+
setNamePrompt(null)
|
|
2224
|
+
if (!name) return
|
|
2225
|
+
if (cur.kind === 'rename') {
|
|
2226
|
+
if (name === cur.name) return
|
|
2227
|
+
doFs('rename', { path: cur.path, dest: joinRemote(String(cur.path).replace(/[\\/][^\\/]*$/, ''), name) }, true)
|
|
2228
|
+
} else {
|
|
2229
|
+
doFs('mkdir', { path: joinRemote(cur.path, name) }, true)
|
|
2230
|
+
}
|
|
2231
|
+
},
|
|
2232
|
+
}) : null,
|
|
2004
2233
|
)
|
|
2005
2234
|
}
|
|
2006
2235
|
|
|
@@ -2008,18 +2237,23 @@ window.__ModuleLoader__.load({
|
|
|
2008
2237
|
function RemoteFileTab(props) {
|
|
2009
2238
|
const { tab, scope } = props
|
|
2010
2239
|
const path = tab.path || ''
|
|
2240
|
+
const fileSessionId = scope && scope.sessionId
|
|
2011
2241
|
const [data, setData] = React.useState(null)
|
|
2012
2242
|
const [err, setErr] = React.useState('')
|
|
2013
2243
|
const [loading, setLoading] = React.useState(true)
|
|
2014
2244
|
const [edit, setEdit] = React.useState(false)
|
|
2015
2245
|
const [draft, setDraft] = React.useState('')
|
|
2016
2246
|
const [saving, setSaving] = React.useState(false)
|
|
2247
|
+
React.useEffect(() => { ensureChromeCss() }, [])
|
|
2248
|
+
|
|
2249
|
+
const originalText = (data && data.content != null ? data.content : '').replace(/\n…\[truncated:.*$/, '')
|
|
2250
|
+
const dirty = edit && draft !== originalText
|
|
2017
2251
|
|
|
2018
2252
|
const load = () => {
|
|
2019
2253
|
if (!path) { setLoading(false); return }
|
|
2020
2254
|
let cancelled = false
|
|
2021
2255
|
setLoading(true); setErr(''); setData(null); setEdit(false)
|
|
2022
|
-
readRemoteFile(path, 256 * 1024)
|
|
2256
|
+
readRemoteFile(path, 256 * 1024, fileSessionId)
|
|
2023
2257
|
.then((d) => { if (!cancelled) { setData(d); setLoading(false) } })
|
|
2024
2258
|
.catch((e) => { if (!cancelled) { setErr(String((e && e.message) || e)); setLoading(false) } })
|
|
2025
2259
|
return () => { cancelled = true }
|
|
@@ -2033,7 +2267,7 @@ window.__ModuleLoader__.load({
|
|
|
2033
2267
|
}
|
|
2034
2268
|
const saveRemote = () => {
|
|
2035
2269
|
setSaving(true); setErr('')
|
|
2036
|
-
apiRaw('POST', '/dsh-remote/write', { path, content: draft })
|
|
2270
|
+
apiRaw('POST', '/dsh-remote/write', withSessionBody({ path, content: draft, expectedMtime: data && data.mtime }, fileSessionId))
|
|
2037
2271
|
.then((r) => {
|
|
2038
2272
|
if (r.status === 409) {
|
|
2039
2273
|
setErr(String((r.data && r.data.error) || tr('file.conflict')))
|
|
@@ -2048,38 +2282,48 @@ window.__ModuleLoader__.load({
|
|
|
2048
2282
|
}
|
|
2049
2283
|
const downloadMirror = () => {
|
|
2050
2284
|
setSaving(true); setErr('')
|
|
2051
|
-
api('POST', '/dsh-remote/fs', { op: 'download', path })
|
|
2285
|
+
api('POST', '/dsh-remote/fs', withSessionBody({ op: 'download', path }, fileSessionId))
|
|
2052
2286
|
.then((r) => setErr(tr('file.downloaded', { local: (r && r.local ? tr('file.downloadedAt', { path: r.local }) : '') })))
|
|
2053
2287
|
.catch((e) => setErr(String((e && e.message) || e)))
|
|
2054
2288
|
.finally(() => setSaving(false))
|
|
2055
2289
|
}
|
|
2056
2290
|
|
|
2057
2291
|
const baseName = path.split(/[\\/]/).pop() || path
|
|
2058
|
-
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 } },
|
|
2059
|
-
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', borderBottom: '1px solid
|
|
2292
|
+
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0, color: T.label } },
|
|
2293
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', borderBottom: '1px solid ' + T.border, fontSize: 12 } },
|
|
2060
2294
|
React.createElement('span', { style: { fontWeight: 600, whiteSpace: 'nowrap' } }, '📄 ' + baseName),
|
|
2061
2295
|
React.createElement('code', { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', opacity: 0.75 } }, path),
|
|
2296
|
+
dirty ? React.createElement('span', { className: 'dsh-rw-pill', style: { color: T.warn } }, tr('file.dirty')) : null,
|
|
2062
2297
|
React.createElement('span', { style: { opacity: 0.7, fontSize: 11, flexShrink: 0 } }, edit ? tr('file.editing') : tr('file.readonly')),
|
|
2063
2298
|
edit
|
|
2064
2299
|
? React.createElement(React.Fragment, null,
|
|
2065
|
-
React.createElement('button', { style: { ...
|
|
2066
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: () => { setEdit(false); setErr('') } }, tr('file.cancel')))
|
|
2300
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: { ...primaryBtnS, padding: '2px 10px', fontSize: 12 }, onClick: saveRemote, disabled: saving || !dirty, title: tr('file.saveHint') }, saving ? tr('file.saving') : tr('file.saveToRemote')),
|
|
2301
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: () => { setEdit(false); setErr('') } }, tr('file.cancel')))
|
|
2067
2302
|
: React.createElement(React.Fragment, null,
|
|
2068
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: startEdit, disabled: !data || data.binary }, tr('file.edit')),
|
|
2069
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: downloadMirror, disabled: saving }, tr('file.download'))),
|
|
2303
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: startEdit, disabled: !data || data.binary }, tr('file.edit')),
|
|
2304
|
+
React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: downloadMirror, disabled: saving }, tr('file.download'))),
|
|
2070
2305
|
),
|
|
2071
|
-
React.createElement('div', { style: { flex: 1, overflow: 'auto', minHeight: 0 } },
|
|
2306
|
+
React.createElement('div', { style: { flex: 1, overflow: 'auto', minHeight: 0, display: 'flex', flexDirection: 'column' } },
|
|
2307
|
+
err ? React.createElement('div', { style: { color: T.danger, padding: '8px 12px', fontSize: 13, flexShrink: 0 } }, err) : null,
|
|
2072
2308
|
loading ? React.createElement('div', { style: { opacity: 0.7, padding: 12, fontSize: 13 } }, tr('file.loading'))
|
|
2073
|
-
:
|
|
2074
|
-
|
|
2075
|
-
? React.createElement('div', { style: { padding: 12, fontSize: 13, color: '#e6c07b' } },
|
|
2309
|
+
: (data && data.binary
|
|
2310
|
+
? React.createElement('div', { style: { padding: 12, fontSize: 13, color: T.warn } },
|
|
2076
2311
|
tr('file.binary', { size: (data.size != null ? data.size + ' bytes' : tr('file.unknownSize')) }))
|
|
2077
2312
|
: (edit
|
|
2078
|
-
? React.createElement('textarea', {
|
|
2079
|
-
|
|
2080
|
-
(
|
|
2313
|
+
? React.createElement('textarea', {
|
|
2314
|
+
value: draft,
|
|
2315
|
+
onChange: (e) => setDraft(e.target.value),
|
|
2316
|
+
onKeyDown: (e) => {
|
|
2317
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); if (!saving && dirty) saveRemote() }
|
|
2318
|
+
if (e.key === 'Escape') { e.preventDefault(); setEdit(false); setErr('') }
|
|
2319
|
+
},
|
|
2320
|
+
spellCheck: false,
|
|
2321
|
+
style: { width: '100%', height: '100%', flex: 1, boxSizing: 'border-box', border: 'none', outline: 'none', resize: 'none', background: 'transparent', color: T.label, fontSize: 13, lineHeight: 1.55, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', padding: '10px 12px', whiteSpace: 'pre-wrap', wordBreak: 'break-word' },
|
|
2322
|
+
})
|
|
2323
|
+
: React.createElement('div', { style: { fontSize: 13, lineHeight: 1.55, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', padding: '10px 12px', whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: T.label } },
|
|
2324
|
+
(data && data.content != null) ? data.content : (err ? '' : tr('file.empty')),
|
|
2081
2325
|
(data && data.truncated)
|
|
2082
|
-
? React.createElement('div', { style: { color:
|
|
2326
|
+
? React.createElement('div', { style: { color: T.warn, fontSize: 12, marginTop: 8 } }, tr('file.truncated'))
|
|
2083
2327
|
: null,
|
|
2084
2328
|
))),
|
|
2085
2329
|
),
|
|
@@ -2201,6 +2445,7 @@ window.__ModuleLoader__.load({
|
|
|
2201
2445
|
}
|
|
2202
2446
|
|
|
2203
2447
|
function apply(ctx) {
|
|
2448
|
+
ensureChromeCss()
|
|
2204
2449
|
const slots = ctx.get('slots')
|
|
2205
2450
|
wireLocale(ctx)
|
|
2206
2451
|
// Optional integrations must not gate the settings page. In particular,
|