dsh-remote 0.8.18 → 0.8.20

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) }
@@ -561,13 +602,83 @@ window.__ModuleLoader__.load({
561
602
  label: v('--dsw-alias-label-primary', '#e4e4e7'),
562
603
  primary: v('--dsw-alias-button-primary-fill', '#2563eb'),
563
604
  onPrimary: v('--dsw-alias-button-contrast-fill', '#fff'),
564
- hoverBg: v('--dsw-alias-interactive-bg-hover', 'rgba(128,128,128,0.14)'),
605
+ // Hover tints are a neutral translucent gray rather than a theme token:
606
+ // the harness's own interactive-bg-hover is a 6% tint that disappears on
607
+ // the settings surface, while a gray overlay darkens light themes and
608
+ // lightens dark ones.
609
+ hoverBg: 'rgba(128,128,128,0.14)',
565
610
  }
566
611
  const inputS = { flex: 1, padding: '6px 10px', borderRadius: T.radius, border: '1px solid ' + T.border, background: T.bg, color: T.label, outline: 'none' }
567
612
  const buttonS = { padding: '6px 12px', borderRadius: T.radius, border: '1px solid ' + T.border, background: T.bg, color: T.label, cursor: 'pointer' }
568
613
  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 }
614
+ const box = { border: '1px solid ' + T.border, borderRadius: 10, background: T.bg, padding: 12 }
570
615
  const boxS = box
616
+ const LABEL_W = 108
617
+ const CHROME_CSS_ID = 'dsh-remote-chrome'
618
+ function ensureChromeCss() {
619
+ try {
620
+ if (typeof document === 'undefined' || !document.head || typeof document.getElementById !== 'function') return
621
+ if (document.getElementById(CHROME_CSS_ID)) return
622
+ const el = document.createElement('style')
623
+ el.id = CHROME_CSS_ID
624
+ el.textContent = [
625
+ '.dsh-rw-btn{transition:background .12s ease,opacity .12s ease,border-color .12s ease}',
626
+ '.dsh-rw-btn:hover:not(:disabled){background:rgba(128,128,128,.18)}',
627
+ '.dsh-rw-btn:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,#2563eb);outline-offset:1px}',
628
+ '.dsh-rw-btn:disabled{opacity:.45;cursor:not-allowed}',
629
+ // A filled primary keeps its own fill on hover; opacity is the only
630
+ // cue that stays visible whether that fill is near-black or near-white.
631
+ '.dsh-rw-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-fill,#2563eb);opacity:.85}',
632
+ '.dsh-rw-input{transition:border-color .12s ease,box-shadow .12s ease}',
633
+ '.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)}',
634
+ '.dsh-rw-row{border-radius:8px;margin:0 -6px;padding-left:8px;padding-right:8px;transition:background .12s ease}',
635
+ '.dsh-rw-row:hover{background:rgba(128,128,128,.14)}',
636
+ '.dsh-rw-list-item{transition:background .1s ease}',
637
+ '.dsh-rw-list-item:hover{background:rgba(128,128,128,.18)}',
638
+ '.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))}',
639
+ '.dsh-rw-tab{border:none!important;background:transparent!important}',
640
+ '.dsh-rw-tab.is-active{background:var(--dsw-alias-button-primary-fill,#2563eb)!important;color:var(--dsw-alias-button-contrast-fill,#fff)!important;font-weight:600}',
641
+ '.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}',
642
+ '.dsh-rw-pill.is-ok{color:var(--dsw-static-green-500,#4caf7d);border-color:currentColor}',
643
+ '.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}',
644
+ '@keyframes dsh-rw-in{from{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:none}}',
645
+ '.dsh-rw-modal-panel{animation:dsh-rw-in .14s ease-out}',
646
+ ].join('')
647
+ document.head.appendChild(el)
648
+ } catch { /* vm tests / no DOM */ }
649
+ }
650
+ function PromptDialog(props) {
651
+ const { title, initial, placeholder, onSubmit, onCancel } = props
652
+ const [val, setVal] = React.useState(initial || '')
653
+ React.useEffect(() => {
654
+ try {
655
+ if (typeof document === 'undefined') return
656
+ const onKey = (e) => {
657
+ if (e.key === 'Escape') { e.preventDefault(); onCancel() }
658
+ if (e.key === 'Enter') { e.preventDefault(); const t = val.trim(); if (t) onSubmit(t) }
659
+ }
660
+ document.addEventListener('keydown', onKey)
661
+ return () => document.removeEventListener('keydown', onKey)
662
+ } catch { return undefined }
663
+ }, [val, onSubmit, onCancel])
664
+ return React.createElement('div', {
665
+ style: { position: 'fixed', inset: 0, zIndex: 1000000, background: 'rgba(0,0,0,0.45)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 },
666
+ onClick: onCancel,
667
+ },
668
+ React.createElement('div', {
669
+ className: 'dsh-rw-modal-panel',
670
+ 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)' },
671
+ onClick: (e) => e.stopPropagation(),
672
+ },
673
+ React.createElement('div', { style: { fontSize: 13, fontWeight: 600, color: T.label } }, title),
674
+ React.createElement('input', { autoFocus: true, value: val, onChange: (e) => setVal(e.target.value), placeholder: placeholder || '', className: 'dsh-rw-input', style: inputS }),
675
+ React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' } },
676
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: onCancel }, tr('file.cancel')),
677
+ 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')),
678
+ ),
679
+ ),
680
+ )
681
+ }
571
682
 
572
683
  function humanSize(n) {
573
684
  if (n == null) return ''
@@ -629,6 +740,9 @@ window.__ModuleLoader__.load({
629
740
  const [sshEntries, setSshEntries] = React.useState(null)
630
741
  const [workspaces, setWorkspaces] = React.useState([])
631
742
  const [advOpen, setAdvOpen] = React.useState(false) // 高级配置折叠
743
+ const [fwdOpen, setFwdOpen] = React.useState(true)
744
+ const [auditOpen, setAuditOpen] = React.useState(false)
745
+ React.useEffect(() => { ensureChromeCss() }, [])
632
746
 
633
747
  const testConn = () => {
634
748
  if (!form.host.trim() || tst === 'testing') return
@@ -771,52 +885,66 @@ window.__ModuleLoader__.load({
771
885
  .catch((e) => setErr(String((e && e.message) || e)))
772
886
  }
773
887
 
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)
888
+ const row = (label, ctrl, k) => React.createElement('div', { key: k, style: { display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8 } },
889
+ React.createElement('label', { style: { width: LABEL_W, fontSize: 12, opacity: 0.8, flexShrink: 0 } }, label), ctrl)
890
+ const pill = (cls, text) => React.createElement('span', { className: 'dsh-rw-pill' + (cls ? ' ' + cls : ''), style: { marginLeft: 4 } }, text)
891
+ const sectionToggle = (open, setOpen, title, extra) => React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: open ? 8 : 0 } },
892
+ React.createElement('button', {
893
+ type: 'button', className: 'dsh-rw-section-btn',
894
+ title: open ? tr('settings.sectionHide') : tr('settings.sectionShow'),
895
+ onClick: () => setOpen(!open),
896
+ style: { fontSize: 13, fontWeight: 600 },
897
+ }, React.createElement('span', { style: { opacity: 0.7, width: 12 } }, open ? '▾' : '▸'), title),
898
+ extra || null,
899
+ )
776
900
 
777
901
  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 } },
902
+ React.createElement('div', { style: { fontSize: 16, fontWeight: 650, letterSpacing: '-0.01em' } }, tr('settings.title')),
903
+ React.createElement('div', { style: { fontSize: 12, opacity: 0.8, lineHeight: 1.55 } },
780
904
  tr('settings.intro')),
781
905
  React.createElement('div', { style: boxS },
782
- React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.machines')),
906
+ React.createElement('div', { style: { marginBottom: 8, fontSize: 13, fontWeight: 600 } }, tr('settings.machines')),
783
907
  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'))))
908
+ ? 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' } },
909
+ React.createElement('div', { style: { flex: 1, fontSize: 13, minWidth: 180 } },
910
+ React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' } },
911
+ React.createElement('span', { style: { fontWeight: 600 } }, m.name),
912
+ React.createElement('code', { style: { fontSize: 12, opacity: 0.8 } }, m.username + '@' + m.host + ':' + m.port),
913
+ m.id === currentId ? pill('is-ok', tr('settings.current')) : null,
914
+ m.passwordSet ? pill('', tr('settings.badgePwd')) : null,
915
+ (m.credentialBackend && m.credentialBackend !== 'plain') ? pill('', tr('settings.badgeKeychain')) : null,
916
+ m.proxy && m.proxy.host ? pill('', tr('settings.badgeProxy')) : null,
917
+ m.latencyMs != null ? React.createElement('span', { style: { fontSize: 11, opacity: 0.6 } }, m.latencyMs + 'ms') : null,
918
+ )),
919
+ React.createElement('div', { style: { display: 'flex', gap: 6, flexShrink: 0 } },
920
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => startEdit(m) }, tr('settings.edit')),
921
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => del(m.id) }, tr('settings.delete')),
922
+ 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
923
  : React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, tr('settings.noMachines')),
797
924
  currentId
798
- ? React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', paddingTop: 6, fontSize: 12, color: T.ok } },
925
+ ? React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', paddingTop: 10, fontSize: 12, color: T.ok } },
799
926
  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')),
927
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px', fontSize: 12, marginLeft: 'auto' }, onClick: clearCurrent }, tr('settings.clearCurrent')))
928
+ : React.createElement('div', { style: { paddingTop: 10, fontSize: 12, opacity: 0.6 } }, tr('settings.noActive')),
802
929
  ),
803
930
  React.createElement('div', { style: boxS },
804
931
  React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 } },
805
932
  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'))),
933
+ 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
934
  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 } },
935
+ 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
936
  e.host + ' → ' + (e.hostName || '?') + (e.user ? ' (' + e.user + ')' : '') + (e.identityFile ? ' [key]' : '') + (e.proxyJump ? ' ⛳' : '')))
810
937
  : React.createElement('div', { style: { padding: 8, fontSize: 12, opacity: 0.6 } }, tr('settings.sshNoEntries')))
811
938
  : 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'),
939
+ 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'),
940
+ row(tr('settings.host'), React.createElement('input', { className: 'dsh-rw-input', value: form.host, onChange: setF('host'), placeholder: 'IP or hostname', style: inputS }), 'h'),
941
+ row(tr('settings.port'), React.createElement('input', { className: 'dsh-rw-input', value: form.port, onChange: setF('port'), placeholder: '22', style: { ...inputS, width: 70 } }), 'p'),
942
+ row(tr('settings.user'), React.createElement('input', { className: 'dsh-rw-input', value: form.username, onChange: setF('username'), placeholder: 'root', style: inputS }), 'u'),
943
+ 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
944
  // 高级配置折叠开关
818
945
  React.createElement('button', {
819
946
  type: 'button',
947
+ className: 'dsh-rw-btn',
820
948
  onClick: () => setAdvOpen(!advOpen),
821
949
  style: { ...buttonS, width: '100%', marginBottom: advOpen ? 10 : 0, color: advOpen ? T.primary : 'inherit', fontWeight: advOpen ? 600 : 400, borderStyle: 'dashed' },
822
950
  }, advOpen ? tr('settings.advCollapse') : tr('settings.advExpand')),
@@ -829,7 +957,7 @@ window.__ModuleLoader__.load({
829
957
  React.createElement('option', { value: 'accept-new' }, tr('settings.hkAccept')),
830
958
  React.createElement('option', { value: 'verify' }, tr('settings.hkVerify')),
831
959
  React.createElement('option', { value: 'off' }, tr('settings.hkOff'))), 'hkm'),
832
- React.createElement('div', { style: { display: 'flex', gap: 16, paddingLeft: 90, flexWrap: 'wrap', marginBottom: 8 } },
960
+ React.createElement('div', { style: { display: 'flex', gap: 16, paddingLeft: LABEL_W + 8, flexWrap: 'wrap', marginBottom: 8 } },
833
961
  React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
834
962
  React.createElement('input', { type: 'checkbox', checked: form.useAgent, onChange: setFb('useAgent') }), tr('settings.agent')),
835
963
  React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
@@ -843,13 +971,13 @@ window.__ModuleLoader__.load({
843
971
  React.createElement('input', { type: 'password', value: form.proxyPassword, onChange: setF('proxyPassword'), placeholder: 'Jump password', style: { ...inputS, minWidth: 140 } })), 'pu'),
844
972
  row(tr('settings.proxyKey'), React.createElement('input', { value: form.proxyKey, onChange: setF('proxyKey'), placeholder: 'Jump private key path (optional)', style: inputS }), 'pk'),
845
973
  ) : null,
846
- React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center', flexWrap: 'wrap', marginTop: 4 } },
974
+ React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center', flexWrap: 'wrap', marginTop: 8 } },
847
975
  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' },
976
+ form.id ? React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: buttonS, onClick: () => save('delete') }, tr('settings.delete')) : null,
977
+ 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')),
978
+ 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
979
  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')),
980
+ 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
981
  ),
854
982
  ),
855
983
  // 快捷切换工作区(该机器的最近工作区)
@@ -857,35 +985,40 @@ window.__ModuleLoader__.load({
857
985
  React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.recentWorkspaces')),
858
986
  workspaces.map((w, i) => React.createElement('div', { key: w + i, style: { display: 'flex', gap: 8, alignItems: 'center', padding: '4px 0' } },
859
987
  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,
988
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px' }, onClick: () => setWorkspaceNow(w) }, tr('settings.switch'))))) : null,
861
989
  // 端口转发面板
862
990
  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'))),
991
+ sectionToggle(fwdOpen, setFwdOpen, tr('settings.forwards')),
992
+ fwdOpen ? React.createElement(React.Fragment, null,
993
+ 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' } },
994
+ React.createElement('span', { style: { color: f.active ? T.ok : T.muted } }, f.active ? '●' : '○'),
995
+ React.createElement('span', { style: { flex: 1 } }, `${f.direction} 127.0.0.1:${f.listenPort} → ${f.targetHost}:${f.targetPort}${f.autoStart ? ' [auto]' : ''}`),
996
+ f.active
997
+ ? React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('stop', f.id) }, tr('settings.fwdStop'))
998
+ : React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('start', f.id) }, tr('settings.fwdStart')),
999
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('remove', f.id) }, tr('settings.fwdRemove')))) : null,
1000
+ React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', marginTop: 6, flexWrap: 'wrap' } },
1001
+ React.createElement('select', { className: 'dsh-rw-input', value: fwdForm.direction, onChange: (e) => setFwdForm({ ...fwdForm, direction: e.target.value }), style: { ...inputS, maxWidth: 110 } },
1002
+ React.createElement('option', { value: 'local' }, tr('settings.fwdDirLocal')),
1003
+ React.createElement('option', { value: 'reverse' }, tr('settings.fwdDirReverse'))),
1004
+ 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 } }),
1005
+ 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 } }),
1006
+ 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 } }),
1007
+ React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
1008
+ React.createElement('input', { type: 'checkbox', checked: fwdForm.autoStart, onChange: (e) => setFwdForm({ ...fwdForm, autoStart: e.target.checked }) }), tr('settings.fwdAuto')),
1009
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: fwdDefine, disabled: !fwdForm.listenPort }, tr('settings.fwdAdd'))),
1010
+ ) : null,
881
1011
  ),
882
1012
  // 审计日志
883
1013
  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')),
1014
+ sectionToggle(auditOpen, setAuditOpen, tr('settings.audit'),
1015
+ 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'))),
1016
+ auditOpen
1017
+ ? (auditLines.length
1018
+ ? React.createElement('div', { style: { maxHeight: 180, overflowY: 'auto', fontSize: 11, fontFamily: 'monospace', lineHeight: 1.6 } },
1019
+ auditLines.map((l, i) => React.createElement('div', { key: i, style: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, l)))
1020
+ : React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, tr('settings.auditEmpty')))
1021
+ : React.createElement('div', { style: { fontSize: 11, opacity: 0.55 } }, String(auditLines.length)),
889
1022
  ),
890
1023
  err ? React.createElement('div', { style: { color: T.danger, fontSize: 13 } }, err) : null,
891
1024
  React.createElement('div', { style: boxS },
@@ -896,9 +1029,9 @@ window.__ModuleLoader__.load({
896
1029
  'v' + upd.current,
897
1030
  upd.updateAvailable ? React.createElement('b', { style: { color: T.ok, marginLeft: 6 } }, '→ v' + upd.latest) : null)
898
1031
  : 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')),
1032
+ 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
1033
  upd && upd.updateAvailable
901
- ? React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontWeight: 700, color: T.ok }, onClick: applyUpdateNow, disabled: updBusy }, tr('settings.updateNow'))
1034
+ ? 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
1035
  : null,
903
1036
  ),
904
1037
  React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 6, fontSize: 12 } },
@@ -993,6 +1126,7 @@ window.__ModuleLoader__.load({
993
1126
  // 本机浏览浮层(browse 后端):{ path, home, crumbs, entries, truncated }。
994
1127
  const [localPop, setLocalPop] = React.useState(null)
995
1128
  const [newDirName, setNewDirName] = React.useState('')
1129
+ const [mkdirPrompt, setMkdirPrompt] = React.useState(false)
996
1130
  const suggestTimer = React.useRef(null)
997
1131
  const suggestRef = React.useRef(null)
998
1132
  React.useEffect(() => {
@@ -1009,6 +1143,7 @@ window.__ModuleLoader__.load({
1009
1143
  document.addEventListener('mousedown', onDown)
1010
1144
  return () => document.removeEventListener('mousedown', onDown)
1011
1145
  }, [mOpen])
1146
+ React.useEffect(() => { if (open) ensureChromeCss() }, [open])
1012
1147
 
1013
1148
  const sortItems = (list) => {
1014
1149
  const dirs = list.filter((it) => it.dir)
@@ -1235,10 +1370,16 @@ window.__ModuleLoader__.load({
1235
1370
  const mkdirHere = () => {
1236
1371
  const last = levels && levels.length ? levels[levels.length - 1] : null
1237
1372
  const base = (last && last.path) || '/'
1238
- const nm = window.prompt(tr('picker.newDirName', { path: base }))
1239
- if (!nm || !nm.trim()) return
1373
+ setMkdirPrompt({ base })
1374
+ }
1375
+ const confirmMkdirHere = (nm) => {
1376
+ const last = levels && levels.length ? levels[levels.length - 1] : null
1377
+ const base = (mkdirPrompt && mkdirPrompt.base) || (last && last.path) || '/'
1378
+ setMkdirPrompt(false)
1379
+ const name = String(nm || '').trim()
1380
+ if (!name) return
1240
1381
  const sep = base.includes('\\') ? '\\' : '/'
1241
- const full = base === '/' ? '/' + nm.trim() : base + sep + nm.trim()
1382
+ const full = base === '/' ? '/' + name : base + sep + name
1242
1383
  api('POST', '/dsh-remote/fs', { op: 'mkdir', path: full })
1243
1384
  .then(() => { setErr(''); loadLevels(machineId, base, (levels ? levels.length - 1 : 0)) })
1244
1385
  .catch((e) => setErr(String((e && e.message) || e)))
@@ -1297,6 +1438,7 @@ window.__ModuleLoader__.load({
1297
1438
  : (cur.entries.length ? cur.entries.slice(0, 400).map((it, i) => React.createElement('div', {
1298
1439
  key: it.path || i, title: tr('picker.enterDir', { name: it.name }),
1299
1440
  onClick: () => loadLocal(it.path),
1441
+ className: 'dsh-rw-list-item',
1300
1442
  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
1443
  },
1302
1444
  React.createElement('span', null, '📁'),
@@ -1306,10 +1448,10 @@ window.__ModuleLoader__.load({
1306
1448
  ),
1307
1449
  err ? React.createElement('div', { style: { color: T.danger, fontSize: 12, padding: '4px 12px' } }, err) : null,
1308
1450
  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')),
1451
+ 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 } }),
1452
+ 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
1453
  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')),
1454
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: { ...primaryBtnS, whiteSpace: 'nowrap' }, onClick: () => acceptLocalPick(cur.path) }, tr('picker.chooseThis')),
1313
1455
  ),
1314
1456
  ),
1315
1457
  )
@@ -1333,6 +1475,7 @@ window.__ModuleLoader__.load({
1333
1475
  : (entries.length ? entries.slice(0, 400).map((it, i) => React.createElement('div', {
1334
1476
  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
1477
  onClick: it.dir ? () => enterDir(it) : undefined,
1478
+ className: it.dir ? 'dsh-rw-list-item' : undefined,
1336
1479
  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
1480
  },
1338
1481
  React.createElement('span', { style: { flexShrink: 0, display: 'inline-flex', color: T.label, fontSize: 13 } }, it.drive ? '💽' : (it.dir
@@ -1344,31 +1487,59 @@ window.__ModuleLoader__.load({
1344
1487
  ),
1345
1488
  React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center', padding: '6px 10px', borderTop: '1px solid ' + T.border } },
1346
1489
  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')),
1490
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn dsh-rw-primary', style: primaryBtnS, onClick: () => { acceptBrowserPick(last.path); setPopOpen(false) } }, tr('picker.chooseThis')),
1348
1491
  ),
1349
1492
  )
1350
1493
  }
1351
1494
 
1495
+ React.useEffect(() => {
1496
+ if (!open) return
1497
+ try {
1498
+ if (typeof document === 'undefined') return
1499
+ const onKey = (e) => {
1500
+ if (mkdirPrompt) return
1501
+ if (e.key === 'Escape') {
1502
+ if (popOpen) { e.preventDefault(); setPopOpen(false); return }
1503
+ if (suggestOpen) { e.preventDefault(); setSuggestOpen(false); return }
1504
+ if (!busy) { e.preventDefault(); onCancel() }
1505
+ return
1506
+ }
1507
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
1508
+ e.preventDefault()
1509
+ if (tab === 'local' && path.trim()) onPicked(path)
1510
+ else if (tab === 'remote' && path.trim()) commitPath(path)
1511
+ }
1512
+ }
1513
+ document.addEventListener('keydown', onKey)
1514
+ return () => document.removeEventListener('keydown', onKey)
1515
+ } catch { return undefined }
1516
+ }, [open, busy, tab, path, machineId, popOpen, suggestOpen, mkdirPrompt])
1517
+
1352
1518
  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')),
1519
+ const tabBtn = (t, lbl) => React.createElement('button', {
1520
+ type: 'button',
1521
+ className: 'dsh-rw-btn dsh-rw-tab' + (tab === t ? ' is-active' : ''),
1522
+ onClick: () => switchTab(t),
1523
+ style: { ...buttonS, fontWeight: tab === t ? 700 : 400, padding: '5px 14px' },
1524
+ }, lbl)
1525
+ 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() } },
1526
+ 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() },
1527
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 } },
1528
+ React.createElement('div', { style: { fontSize: 15, fontWeight: 650 } }, tr('picker.title')),
1529
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 8px' }, onClick: () => { if (!busy) onCancel() }, disabled: busy }, tr('picker.close')),
1359
1530
  ),
1360
- React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 8 } },
1531
+ React.createElement('div', { className: 'dsh-rw-tabs', style: { marginBottom: 10, alignSelf: 'flex-start' } },
1361
1532
  tabBtn('local', tr('picker.tabLocal')),
1362
1533
  tabBtn('remote', tr('picker.tabRemote')),
1363
1534
  ),
1364
1535
  tab === 'local'
1365
1536
  ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
1366
- React.createElement('div', { style: { fontSize: 12, opacity: 0.8 } }, tr('picker.localIntro')),
1537
+ React.createElement('div', { style: { fontSize: 12, opacity: 0.8, lineHeight: 1.5 } }, tr('picker.localIntro')),
1367
1538
  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')),
1539
+ 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 }),
1540
+ 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
1541
  ),
1371
- React.createElement('button', { style: { ...buttonS, alignSelf: 'flex-start' }, onClick: chooseLocal, disabled: loading }, loading ? tr('picker.opening') : tr('picker.openSysPicker')),
1542
+ 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
1543
  err ? React.createElement('div', { style: { color: T.danger, fontSize: 12 } }, err) : null,
1373
1544
  localPop ? renderLocalPopup() : null,
1374
1545
  )
@@ -1376,31 +1547,44 @@ window.__ModuleLoader__.load({
1376
1547
  React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
1377
1548
  React.createElement('label', { style: { fontSize: 12, opacity: 0.8, whiteSpace: 'nowrap' } }, tr('picker.remoteMachine')),
1378
1549
  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) },
1550
+ 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
1551
  (machines.find((m) => m.id === machineId) || {}).name || (machineId ? '…' : tr('picker.chooseMachine')),
1381
1552
  ),
1382
1553
  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 } },
1554
+ ? 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
1555
  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 + ')'))
1556
+ ? 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
1557
  : React.createElement('div', { style: { padding: 6, opacity: 0.6 } }, tr('picker.noMachines')),
1387
1558
  )
1388
1559
  : null,
1389
1560
  ),
1390
1561
  ),
1391
- // 最近工作区快捷入口
1392
1562
  recent.length ? React.createElement('div', { style: { display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' } },
1393
1563
  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,
1564
+ 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
1565
  React.createElement('div', { ref: suggestRef, style: { position: 'relative' } },
1396
1566
  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')),
1567
+ React.createElement('input', {
1568
+ className: 'dsh-rw-input',
1569
+ value: path,
1570
+ onChange: (e) => onPathChange(e.target.value),
1571
+ onFocus: () => loadSuggestions(path),
1572
+ onKeyDown: (e) => {
1573
+ if (e.key === 'Enter') {
1574
+ e.preventDefault()
1575
+ if (suggestOpen && suggest.length) selectSuggestion(suggest[0])
1576
+ else if (path.trim()) commitPath(path)
1577
+ }
1578
+ },
1579
+ placeholder: (machineId ? (win ? tr('picker.pathPlaceholderWin') : tr('picker.pathPlaceholder')) : tr('picker.chooseMachineFirst')),
1580
+ disabled: !machineId, style: { ...inputS, flex: 1, minWidth: 120 },
1581
+ }),
1582
+ 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')),
1583
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, whiteSpace: 'nowrap' }, onClick: pickHome, disabled: !machineId }, tr('picker.home')),
1400
1584
  ),
1401
1585
  (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)),
1586
+ ? 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)' } },
1587
+ 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
1588
  )
1405
1589
  : null,
1406
1590
  ),
@@ -1408,15 +1592,22 @@ window.__ModuleLoader__.load({
1408
1592
  err ? React.createElement('div', { style: { color: T.danger, fontSize: 12 } }, err) : null,
1409
1593
  React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center' } },
1410
1594
  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')),
1595
+ 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
1596
  ),
1413
1597
  ),
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,
1598
+ React.createElement('div', { style: { fontSize: 11, opacity: 0.55, paddingTop: 8 } }, tr('picker.hintKeys')),
1599
+ (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
1600
  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'),
1601
+ React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, tr('picker.star')),
1417
1602
  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'),
1603
+ React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote/issues/new', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, tr('picker.feedback')),
1419
1604
  ),
1605
+ mkdirPrompt ? React.createElement(PromptDialog, {
1606
+ title: tr('picker.newDirName', { path: mkdirPrompt.base || '/' }),
1607
+ placeholder: tr('explorer.namePlaceholder'),
1608
+ onSubmit: confirmMkdirHere,
1609
+ onCancel: () => setMkdirPrompt(false),
1610
+ }) : null,
1420
1611
  ),
1421
1612
  )
1422
1613
  }
@@ -1425,8 +1616,8 @@ window.__ModuleLoader__.load({
1425
1616
  const SIDEBAR_EXPLORER_ID = 'dsh-remote:explorer'
1426
1617
  const SIDEBAR_FILE_ID = 'dsh-remote:file'
1427
1618
 
1428
- function fetchRemoteStatus() {
1429
- return api('GET', '/dsh-remote/status').catch(() => null)
1619
+ function fetchRemoteStatus(sessionId) {
1620
+ return api('GET', withSessionQuery('/dsh-remote/status', sessionId)).catch(() => null)
1430
1621
  }
1431
1622
 
1432
1623
  // Resolve the remote path this session shows. Issue #13: the ONLY source
@@ -1458,14 +1649,14 @@ window.__ModuleLoader__.load({
1458
1649
  .catch(() => '')
1459
1650
  }
1460
1651
 
1461
- function readRemoteFile(path, maxBytes) {
1462
- return api('POST', '/dsh-remote/read', { path, maxBytes }).catch((e) => {
1652
+ function readRemoteFile(path, maxBytes, sessionId) {
1653
+ return api('POST', '/dsh-remote/read', withSessionBody({ path, maxBytes }, sessionId)).catch((e) => {
1463
1654
  throw new Error(tr('file.readFail', { msg: ((e && e.message) || e) }))
1464
1655
  })
1465
1656
  }
1466
1657
 
1467
- function listRemoteDir(path) {
1468
- return api('GET', '/dsh-remote/ls?path=' + encodeURIComponent(path || '')).catch((e) => {
1658
+ function listRemoteDir(path, sessionId) {
1659
+ return api('GET', withSessionQuery('/dsh-remote/ls?path=' + encodeURIComponent(path || ''), sessionId)).catch((e) => {
1469
1660
  throw new Error(tr('file.listFail', { msg: ((e && e.message) || e) }))
1470
1661
  })
1471
1662
  }
@@ -1675,7 +1866,11 @@ window.__ModuleLoader__.load({
1675
1866
  const [hoverPath, setHoverPath] = React.useState('') // row hover highlight
1676
1867
  /** Row whose path was just copied — shows a brief「已复制」label. */
1677
1868
  const [copiedPath, setCopiedPath] = React.useState('')
1869
+ const [namePrompt, setNamePrompt] = React.useState(null) // { kind, path, name }
1870
+ React.useEffect(() => { ensureChromeCss() }, [])
1678
1871
  const expanded = Array.isArray(props.expanded) ? props.expanded : []
1872
+ const scopeCwd = props.scope && props.scope.cwd
1873
+ const sessionId = props.scope && props.scope.sessionId
1679
1874
 
1680
1875
  const storeLevel = (path, level) => {
1681
1876
  dataRef.current = { ...dataRef.current, [path]: level }
@@ -1691,7 +1886,7 @@ window.__ModuleLoader__.load({
1691
1886
  if (existing && !existing.loading && opts && opts.keep) return
1692
1887
  if (cur[dir] !== undefined && !cur[dir].loading) return
1693
1888
  storeLevel(dir, { loading: true })
1694
- listRemoteDir(dir)
1889
+ listRemoteDir(dir, sessionId)
1695
1890
  .then((res) => {
1696
1891
  const items = Array.isArray(res && res.items) ? res.items : []
1697
1892
  const sorted = items.slice().sort((a, b) => {
@@ -1712,14 +1907,12 @@ window.__ModuleLoader__.load({
1712
1907
  storeLevel(dir, { error: String((e && e.message) || e) })
1713
1908
  }
1714
1909
  })
1715
- }, [])
1910
+ }, [sessionId])
1716
1911
 
1717
1912
  // Resolve this session's remote root, then load it and every expanded dir.
1718
1913
  // Re-runs whenever the session scope changes (switching conversations, or
1719
1914
  // the async sessionCwd fetch landing), so the sidebar follows the session's
1720
1915
  // own workspace dir.
1721
- const scopeCwd = props.scope && props.scope.cwd
1722
- const sessionId = props.scope && props.scope.sessionId
1723
1916
  const rootRef = React.useRef('')
1724
1917
  const refreshStatus = React.useCallback((opts) => {
1725
1918
  const applyRoot = (r, connected) => {
@@ -1739,14 +1932,17 @@ window.__ModuleLoader__.load({
1739
1932
  // Not connected (e.g. freshly restarted, machine no longer active):
1740
1933
  // show a clear disconnected state instead of firing ls requests
1741
1934
  // that all fail with AggregateError / 500 in a loop.
1742
- if (!connected) {
1935
+ // A bound remote session lists on demand even if this process has
1936
+ // not yet opened the SSH socket; only an unbound/disconnected
1937
+ // machine-scoped view should show the empty "未连接" state.
1938
+ if (!connected && !sessionId) {
1743
1939
  storeLevel(r, { error: tr('explorer.notConnected') })
1744
1940
  return
1745
1941
  }
1746
1942
  if (!sameRoot) {
1747
1943
  storeLevel(r, { loading: true })
1748
1944
  }
1749
- listRemoteDir(r)
1945
+ listRemoteDir(r, sessionId)
1750
1946
  .then((res) => {
1751
1947
  const items = Array.isArray(res && res.items) ? res.items : []
1752
1948
  const sorted = items.slice().sort((a, b) => {
@@ -1779,7 +1975,7 @@ window.__ModuleLoader__.load({
1779
1975
  return Promise.resolve('')
1780
1976
  }
1781
1977
  resolveRoot().then((r) => {
1782
- fetchRemoteStatus().then((s) => {
1978
+ fetchRemoteStatus(sessionId).then((s) => {
1783
1979
  setStatus(s)
1784
1980
  // Issue #13: NEVER fall back to the machine-level workspace here —
1785
1981
  // a session whose cwd is not a mirror is a LOCAL session and shows
@@ -1807,12 +2003,12 @@ window.__ModuleLoader__.load({
1807
2003
  const host = document.createElement('div')
1808
2004
  host.style.cssText = 'position:fixed;z-index:999999;pointer-events:auto'
1809
2005
  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'
2006
+ 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
2007
  const mkItem = (label, danger, fn) => {
1812
2008
  const d = document.createElement('div')
1813
2009
  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)' }
2010
+ 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'
2011
+ d.onmouseenter = () => { d.style.background = 'var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,0.14))' }
1816
2012
  d.onmouseleave = () => { d.style.background = 'transparent' }
1817
2013
  d.addEventListener('click', (e) => { e.stopPropagation(); setMenu(null); fn() })
1818
2014
  return d
@@ -1821,7 +2017,7 @@ window.__ModuleLoader__.load({
1821
2017
  box.appendChild(mkItem(tr('explorer.menuDownload'), false, () => doFs('download', { path: menu.path })))
1822
2018
  box.appendChild(mkItem(tr('explorer.menuCopyRel'), false, () => copyPath(relPath(menu.path), menu.path)))
1823
2019
  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) }))
2020
+ box.appendChild(mkItem(tr('explorer.menuRename'), false, () => { setNamePrompt({ kind: 'rename', path: menu.path, name: menu.name }) }))
1825
2021
  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
2022
  host.appendChild(box)
1827
2023
  document.body.appendChild(host)
@@ -1839,7 +2035,7 @@ window.__ModuleLoader__.load({
1839
2035
 
1840
2036
  const doFs = (op, payload, refreshParent) => {
1841
2037
  setBusy(op)
1842
- return api('POST', '/dsh-remote/fs', { op, ...payload })
2038
+ return api('POST', '/dsh-remote/fs', withSessionBody({ op, ...payload }, sessionId))
1843
2039
  .then(() => {
1844
2040
  setErr('')
1845
2041
  if (refreshParent) {
@@ -1859,7 +2055,8 @@ window.__ModuleLoader__.load({
1859
2055
  }
1860
2056
  const ctxItem = (label, danger, fn) => React.createElement('div', {
1861
2057
  onClick: (e) => { e.stopPropagation(); setMenu(null); fn() },
1862
- style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, color: danger ? '#e06c75' : '#e4e4e7', borderRadius: 4, whiteSpace: 'nowrap' },
2058
+ className: 'dsh-rw-list-item',
2059
+ style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, color: danger ? T.danger : T.label, borderRadius: 4, whiteSpace: 'nowrap' },
1863
2060
  }, label)
1864
2061
 
1865
2062
  // The path shown by「复制相对地址」: relative to this session's remote
@@ -1949,11 +2146,11 @@ window.__ModuleLoader__.load({
1949
2146
  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
2147
  }
1951
2148
  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)
2149
+ 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
2150
  }
1954
2151
  const items = level.entries || []
1955
2152
  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 }))
2153
+ ? 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
2154
  : null
1958
2155
  if (!items.length) return soft || null
1959
2156
  return React.createElement('div', { key: dir + ':list' },
@@ -1966,13 +2163,15 @@ window.__ModuleLoader__.load({
1966
2163
  const rootLevel = data[root]
1967
2164
 
1968
2165
  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') }, '↻'),
2166
+ React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', padding: '6px 8px', borderBottom: '1px solid ' + T.border } },
2167
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: refreshStatus, title: tr('explorer.refresh') }, '↻'),
1971
2168
  React.createElement('button', {
2169
+ type: 'button',
2170
+ className: 'dsh-rw-btn',
1972
2171
  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) },
2172
+ onClick: () => setNamePrompt({ kind: 'mkdir', path: root || '/', name: '' }),
1974
2173
  }, tr('explorer.newDir')),
1975
- React.createElement('button', { style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: () => setPicker(true) }, tr('explorer.more')),
2174
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: () => setPicker(true) }, tr('explorer.more')),
1976
2175
  ),
1977
2176
  status && status.workspace && root
1978
2177
  ? 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 +2199,25 @@ window.__ModuleLoader__.load({
2000
2199
  ),
2001
2200
  // context menu — now rendered via DOM portal in useEffect above
2002
2201
  null,
2003
- err ? React.createElement('div', { style: { color: '#e06c75', fontSize: 12, padding: '4px 8px' } }, (busy ? tr('explorer.busy') : '') + err) : null,
2202
+ err ? React.createElement('div', { style: { color: T.danger, fontSize: 12, padding: '4px 8px' } }, (busy ? tr('explorer.busy') : '') + err) : null,
2203
+ namePrompt ? React.createElement(PromptDialog, {
2204
+ title: namePrompt.kind === 'rename' ? tr('explorer.renameTitle') : tr('explorer.mkdirTitle'),
2205
+ initial: namePrompt.name || '',
2206
+ placeholder: namePrompt.kind === 'rename' ? tr('explorer.renamePrompt') : tr('explorer.namePlaceholder'),
2207
+ onCancel: () => setNamePrompt(null),
2208
+ onSubmit: (nm) => {
2209
+ const name = String(nm || '').trim()
2210
+ const cur = namePrompt
2211
+ setNamePrompt(null)
2212
+ if (!name) return
2213
+ if (cur.kind === 'rename') {
2214
+ if (name === cur.name) return
2215
+ doFs('rename', { path: cur.path, dest: joinRemote(String(cur.path).replace(/[\\/][^\\/]*$/, ''), name) }, true)
2216
+ } else {
2217
+ doFs('mkdir', { path: joinRemote(cur.path, name) }, true)
2218
+ }
2219
+ },
2220
+ }) : null,
2004
2221
  )
2005
2222
  }
2006
2223
 
@@ -2008,18 +2225,23 @@ window.__ModuleLoader__.load({
2008
2225
  function RemoteFileTab(props) {
2009
2226
  const { tab, scope } = props
2010
2227
  const path = tab.path || ''
2228
+ const fileSessionId = scope && scope.sessionId
2011
2229
  const [data, setData] = React.useState(null)
2012
2230
  const [err, setErr] = React.useState('')
2013
2231
  const [loading, setLoading] = React.useState(true)
2014
2232
  const [edit, setEdit] = React.useState(false)
2015
2233
  const [draft, setDraft] = React.useState('')
2016
2234
  const [saving, setSaving] = React.useState(false)
2235
+ React.useEffect(() => { ensureChromeCss() }, [])
2236
+
2237
+ const originalText = (data && data.content != null ? data.content : '').replace(/\n…\[truncated:.*$/, '')
2238
+ const dirty = edit && draft !== originalText
2017
2239
 
2018
2240
  const load = () => {
2019
2241
  if (!path) { setLoading(false); return }
2020
2242
  let cancelled = false
2021
2243
  setLoading(true); setErr(''); setData(null); setEdit(false)
2022
- readRemoteFile(path, 256 * 1024)
2244
+ readRemoteFile(path, 256 * 1024, fileSessionId)
2023
2245
  .then((d) => { if (!cancelled) { setData(d); setLoading(false) } })
2024
2246
  .catch((e) => { if (!cancelled) { setErr(String((e && e.message) || e)); setLoading(false) } })
2025
2247
  return () => { cancelled = true }
@@ -2033,7 +2255,7 @@ window.__ModuleLoader__.load({
2033
2255
  }
2034
2256
  const saveRemote = () => {
2035
2257
  setSaving(true); setErr('')
2036
- apiRaw('POST', '/dsh-remote/write', { path, content: draft })
2258
+ apiRaw('POST', '/dsh-remote/write', withSessionBody({ path, content: draft, expectedMtime: data && data.mtime }, fileSessionId))
2037
2259
  .then((r) => {
2038
2260
  if (r.status === 409) {
2039
2261
  setErr(String((r.data && r.data.error) || tr('file.conflict')))
@@ -2048,38 +2270,48 @@ window.__ModuleLoader__.load({
2048
2270
  }
2049
2271
  const downloadMirror = () => {
2050
2272
  setSaving(true); setErr('')
2051
- api('POST', '/dsh-remote/fs', { op: 'download', path })
2273
+ api('POST', '/dsh-remote/fs', withSessionBody({ op: 'download', path }, fileSessionId))
2052
2274
  .then((r) => setErr(tr('file.downloaded', { local: (r && r.local ? tr('file.downloadedAt', { path: r.local }) : '') })))
2053
2275
  .catch((e) => setErr(String((e && e.message) || e)))
2054
2276
  .finally(() => setSaving(false))
2055
2277
  }
2056
2278
 
2057
2279
  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 } },
2280
+ return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0, color: T.label } },
2281
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', borderBottom: '1px solid ' + T.border, fontSize: 12 } },
2060
2282
  React.createElement('span', { style: { fontWeight: 600, whiteSpace: 'nowrap' } }, '📄 ' + baseName),
2061
2283
  React.createElement('code', { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', opacity: 0.75 } }, path),
2284
+ dirty ? React.createElement('span', { className: 'dsh-rw-pill', style: { color: T.warn } }, tr('file.dirty')) : null,
2062
2285
  React.createElement('span', { style: { opacity: 0.7, fontSize: 11, flexShrink: 0 } }, edit ? tr('file.editing') : tr('file.readonly')),
2063
2286
  edit
2064
2287
  ? 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')))
2288
+ 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')),
2289
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: () => { setEdit(false); setErr('') } }, tr('file.cancel')))
2067
2290
  : 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'))),
2291
+ 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')),
2292
+ React.createElement('button', { type: 'button', className: 'dsh-rw-btn', style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: downloadMirror, disabled: saving }, tr('file.download'))),
2070
2293
  ),
2071
- React.createElement('div', { style: { flex: 1, overflow: 'auto', minHeight: 0 } },
2294
+ React.createElement('div', { style: { flex: 1, overflow: 'auto', minHeight: 0, display: 'flex', flexDirection: 'column' } },
2295
+ err ? React.createElement('div', { style: { color: T.danger, padding: '8px 12px', fontSize: 13, flexShrink: 0 } }, err) : null,
2072
2296
  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' } },
2297
+ : (data && data.binary
2298
+ ? React.createElement('div', { style: { padding: 12, fontSize: 13, color: T.warn } },
2076
2299
  tr('file.binary', { size: (data.size != null ? data.size + ' bytes' : tr('file.unknownSize')) }))
2077
2300
  : (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'),
2301
+ ? React.createElement('textarea', {
2302
+ value: draft,
2303
+ onChange: (e) => setDraft(e.target.value),
2304
+ onKeyDown: (e) => {
2305
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); if (!saving && dirty) saveRemote() }
2306
+ if (e.key === 'Escape') { e.preventDefault(); setEdit(false); setErr('') }
2307
+ },
2308
+ spellCheck: false,
2309
+ 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' },
2310
+ })
2311
+ : 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 } },
2312
+ (data && data.content != null) ? data.content : (err ? '' : tr('file.empty')),
2081
2313
  (data && data.truncated)
2082
- ? React.createElement('div', { style: { color: '#e6c07b', fontSize: 12, marginTop: 8 } }, tr('file.truncated'))
2314
+ ? React.createElement('div', { style: { color: T.warn, fontSize: 12, marginTop: 8 } }, tr('file.truncated'))
2083
2315
  : null,
2084
2316
  ))),
2085
2317
  ),
@@ -2201,6 +2433,7 @@ window.__ModuleLoader__.load({
2201
2433
  }
2202
2434
 
2203
2435
  function apply(ctx) {
2436
+ ensureChromeCss()
2204
2437
  const slots = ctx.get('slots')
2205
2438
  wireLocale(ctx)
2206
2439
  // Optional integrations must not gate the settings page. In particular,