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/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
- onPrimary: v('--dsw-alias-button-contrast-fill', '#fff'),
564
- hoverBg: v('--dsw-alias-interactive-bg-hover', 'rgba(128,128,128,0.14)'),
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: T.radius, background: T.bg, padding: 10 }
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: 6, alignItems: 'center', marginBottom: 8 } },
775
- React.createElement('label', { style: { width: 84, fontSize: 12, opacity: 0.8, flexShrink: 0 } }, label), ctrl)
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: 15, fontWeight: 600 } }, tr('settings.title')),
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: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.machines')),
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', padding: '6px 0', borderBottom: '1px solid ' + T.border } },
785
- React.createElement('div', { style: { flex: 1, fontSize: 13 } },
786
- m.name + ' ',
787
- React.createElement('code', { style: { fontSize: 12, opacity: 0.8 } }, m.username + '@' + m.host + ':' + m.port),
788
- m.passwordSet ? ' 🔒' : '',
789
- (m.credentialBackend && m.credentialBackend !== 'plain') ? ' 🗝' : '',
790
- m.proxy && m.proxy.host ? ' ⛳' : '',
791
- m.latencyMs != null ? React.createElement('span', { style: { fontSize: 11, opacity: 0.6 } }, ` · ${m.latencyMs}ms`) : null,
792
- m.id === currentId ? React.createElement('span', { style: { color: T.ok, fontSize: 12 } }, ' · ' + tr('settings.current')) : null),
793
- React.createElement('button', { style: buttonS, onClick: () => startEdit(m) }, tr('settings.edit')),
794
- React.createElement('button', { style: buttonS, onClick: () => del(m.id) }, tr('settings.delete')),
795
- React.createElement('button', { style: { ...buttonS, padding: '6px 10px', whiteSpace: 'nowrap' }, onClick: () => useNow(m.id), disabled: m.id === currentId }, tr('settings.setCurrent'))))
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: 6, fontSize: 12, color: T.ok } },
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: 6, fontSize: 12, opacity: 0.6 } }, tr('settings.noActive')),
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: 90, flexWrap: 'wrap', marginBottom: 8 } },
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: 4 } },
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', { style: { ...buttonS, fontWeight: 700 }, onClick: () => save(form.id ? 'update' : 'add'), disabled: busy || !form.host.trim() }, busy ? tr('settings.saving') : tr('settings.save')),
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
- React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.forwards')),
864
- forwards.length ? forwards.map((f) => React.createElement('div', { key: f.id, style: { display: 'flex', gap: 8, alignItems: 'center', padding: '4px 0', fontSize: 12, fontFamily: 'monospace' } },
865
- React.createElement('span', { style: { color: f.active ? T.ok : T.muted } }, f.active ? '●' : '○'),
866
- React.createElement('span', { style: { flex: 1 } }, `${f.direction} 127.0.0.1:${f.listenPort} → ${f.targetHost}:${f.targetPort}${f.autoStart ? ' [auto]' : ''}`),
867
- f.active
868
- ? React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('stop', f.id) }, tr('settings.fwdStop'))
869
- : React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('start', f.id) }, tr('settings.fwdStart')),
870
- React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('remove', f.id) }, tr('settings.fwdRemove')))) : null,
871
- React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', marginTop: 6, flexWrap: 'wrap' } },
872
- React.createElement('select', { value: fwdForm.direction, onChange: (e) => setFwdForm({ ...fwdForm, direction: e.target.value }), style: { ...inputS, maxWidth: 110 } },
873
- React.createElement('option', { value: 'local' }, tr('settings.fwdDirLocal')),
874
- React.createElement('option', { value: 'reverse' }, tr('settings.fwdDirReverse'))),
875
- React.createElement('input', { value: fwdForm.listenPort, onChange: (e) => setFwdForm({ ...fwdForm, listenPort: e.target.value }), placeholder: tr('settings.fwdListen'), style: { ...inputS, maxWidth: 90 } }),
876
- React.createElement('input', { value: fwdForm.targetHost, onChange: (e) => setFwdForm({ ...fwdForm, targetHost: e.target.value }), placeholder: tr('settings.fwdTargetHost'), style: { ...inputS, maxWidth: 120 } }),
877
- React.createElement('input', { value: fwdForm.targetPort, onChange: (e) => setFwdForm({ ...fwdForm, targetPort: e.target.value }), placeholder: tr('settings.fwdTargetPort'), style: { ...inputS, maxWidth: 90 } }),
878
- React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
879
- React.createElement('input', { type: 'checkbox', checked: fwdForm.autoStart, onChange: (e) => setFwdForm({ ...fwdForm, autoStart: e.target.checked }) }), tr('settings.fwdAuto')),
880
- React.createElement('button', { style: { ...buttonS, fontWeight: 700 }, onClick: fwdDefine, disabled: !fwdForm.listenPort }, tr('settings.fwdAdd'))),
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
- React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.audit')),
885
- auditLines.length
886
- ? React.createElement('div', { style: { maxHeight: 180, overflowY: 'auto', fontSize: 11, fontFamily: 'monospace', lineHeight: 1.6 } },
887
- auditLines.map((l, i) => React.createElement('div', { key: i, style: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, l)))
888
- : React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, tr('settings.auditEmpty')),
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', { style: { ...buttonS, padding: '2px 10px', fontWeight: 700, color: T.ok }, onClick: applyUpdateNow, disabled: updBusy }, tr('settings.updateNow'))
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
- const nm = window.prompt(tr('picker.newDirName', { path: base }))
1239
- if (!nm || !nm.trim()) return
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 === '/' ? '/' + nm.trim() : base + sep + nm.trim()
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', { style: { ...buttonS, fontWeight: 600, whiteSpace: 'nowrap' }, onClick: () => acceptLocalPick(cur.path) }, tr('picker.chooseThis')),
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', { style: { ...buttonS, fontWeight: 600 }, onClick: () => { acceptBrowserPick(last.path); setPopOpen(false) } }, tr('picker.chooseThis')),
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', { onClick: () => switchTab(t), style: { ...buttonS, fontWeight: tab === t ? 700 : 400 } }, lbl)
1354
- 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) onCancel() } },
1355
- React.createElement('div', { style: { background: v('--dsw-alias-bg-layer-1', '#18181b'), 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() },
1356
- React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 } },
1357
- React.createElement('div', null, tr('picker.title')),
1358
- React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => { if (!busy) onCancel() }, disabled: busy }, tr('picker.close')),
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', { style: { display: 'flex', gap: 6, marginBottom: 8 } },
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: buttonS, onClick: () => (path.trim() ? onPicked(path) : undefined), disabled: !path.trim() }, tr('picker.useLocalPath')),
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', { value: path, onChange: (e) => onPathChange(e.target.value), onFocus: () => loadSuggestions(path), placeholder: (machineId ? (win ? tr('picker.pathPlaceholderWin') : tr('picker.pathPlaceholder')) : tr('picker.chooseMachineFirst')), disabled: !machineId, style: { ...inputS, flex: 1, minWidth: 120 } }),
1398
- React.createElement('button', { style: { ...buttonS, whiteSpace: 'nowrap' }, onClick: () => { if (!machineId) return; const p = path.trim(); loadLevels(machineId, p || '', 0); setPopOpen(true) }, disabled: !machineId }, tr('picker.browse')),
1399
- React.createElement('button', { style: { ...buttonS, whiteSpace: 'nowrap' }, onClick: pickHome, disabled: !machineId }, tr('picker.home')),
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', { style: { ...buttonS, fontWeight: 600 }, onClick: () => commitPath(path), disabled: busy || !machineId || !path.trim() }, busy ? tr('picker.mirroring') : tr('picker.setRemoteWs')),
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
- (tab === 'local') ? React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' } }, React.createElement('button', { style: { background: 'transparent' }, onClick: onCancel }, tr('picker.cancel'))) : null,
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' } }, '⭐ Star dsh-remote'),
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' } }, 'Feedback / report issue'),
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 (!connected) {
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:#1f1f23;border:1px solid rgba(128,128,128,0.5);border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,0.4);min-width:170;font-family:system-ui,sans-serif'
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 ? '#e06c75' : '#e4e4e7') + ';border-radius:4px;white-space:nowrap;transition:background 0.1s'
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, () => { const nm = window.prompt(tr('explorer.renamePrompt'), menu.name); if (nm && nm.trim() && nm.trim() !== menu.name) doFs('rename', { path: menu.path, dest: joinRemote(String(menu.path).replace(/[\\/][^\\/]*$/, ''), nm.trim()) }, true) }))
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
- style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, color: danger ? '#e06c75' : '#e4e4e7', borderRadius: 4, whiteSpace: 'nowrap' },
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: '#e06c75' } }, level.error)
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
- ? 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: '#e6c07b' } }, tr('explorer.softError', { msg: level.softError }))
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 rgba(128,128,128,0.25)' } },
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: () => { const nm = window.prompt(tr('picker.newDirName', { path: (root || '/') })); if (nm && nm.trim()) doFs('mkdir', { path: joinRemote(root, nm.trim()) }, true) },
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: '#e06c75', fontSize: 12, padding: '4px 8px' } }, (busy ? tr('explorer.busy') : '') + err) : null,
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 rgba(128,128,128,0.25)', fontSize: 12 } },
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: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: saveRemote, disabled: saving }, saving ? tr('file.saving') : tr('file.saveToRemote')),
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
- : err ? React.createElement('div', { style: { color: '#e06c75', padding: 12, fontSize: 13 } }, err)
2074
- : (data && data.binary
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', { value: draft, onChange: (e) => setDraft(e.target.value), spellCheck: false, style: { width: '100%', height: '100%', boxSizing: 'border-box', border: 'none', outline: 'none', resize: 'none', background: 'transparent', color: '#e4e4e7', fontSize: 13, lineHeight: 1.55, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', padding: '10px 12px', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } })
2079
- : 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' } },
2080
- (data && data.content != null) ? data.content : tr('file.empty'),
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: '#e6c07b', fontSize: 12, marginTop: 8 } }, tr('file.truncated'))
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,