dsh-remote-plugin 0.6.7 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,6 +52,7 @@ const state = {
52
52
  sessionSort: LS.get('sessionSort', 'time') === 'workspace' ? 'workspace' : 'time',
53
53
  byId: new Map(),
54
54
  current: null,
55
+ hostInfo: null,
55
56
  history: { seqs: new Set(), visible: [], hasMore: false, loading: false, minSeq: Infinity },
56
57
  approvals: [],
57
58
  questions: [],
@@ -1055,7 +1056,7 @@ function onHostFrame(full) {
1055
1056
  if (['host/session-added', 'host/session-removed', 'host/workspace-changed', 'host/workspace-removed', 'host/workspace-order-changed', 'host/archived-sessions-changed'].includes(f.type)) refreshSessions()
1056
1057
  if (f.type === 'host/session-status') {
1057
1058
  const s = state.byId.get(f.sessionId)
1058
- if (s) { s.running = f.running; if (state.current === f.sessionId) renderSessions() }
1059
+ if (s) { s.running = f.running; if (state.current === f.sessionId) renderSessions(); renderOverviewDesktop() }
1059
1060
  }
1060
1061
  }
1061
1062
  function applyProjection(sessionId, key, value, seq) {
@@ -1100,11 +1101,12 @@ function onSessionEvent(sessionId, event) {
1100
1101
  /* ---------------- 会话 ---------------- */
1101
1102
  async function refreshSessions() {
1102
1103
  const v = await safeRpc('session.list', {}, '')
1103
- if (!v) { renderSessions(); return }
1104
+ if (!v) { renderSessions(); renderOverviewDesktop(); return }
1104
1105
  state.sessions = v.items || []
1105
1106
  state.byId = new Map(state.sessions.map(s => [s.sessionId, s]))
1106
1107
  renderSessions()
1107
1108
  scheduleWorkbenchRefresh()
1109
+ renderOverviewDesktop()
1108
1110
  }
1109
1111
  function sessionCwd(s) { return typeof s?.cwd === 'string' ? s.cwd.trim() : '' }
1110
1112
  function sessionWorkspaceLabel(s) {
@@ -1135,8 +1137,12 @@ function renderSessions() {
1135
1137
  const wbIds = new Set()
1136
1138
  if (state.wb.bound && state.wb.projects) for (const w of state.wb.projects) for (const id of (w.sessionIds || [])) wbIds.add(id)
1137
1139
  const root = state.wb.bound ? state.wb.path : ''
1138
- const visible = allItems.filter(s => !(state.wb.bound && (wbIds.has(s.sessionId) || wbStrictInside(s.cwd, root))))
1139
1140
  const archivedSet = new Set(state.archivedIds || [])
1141
+ const visible = allItems.filter(s => {
1142
+ if (!state.wb.bound) return true
1143
+ if (archivedSet.has(s.sessionId)) return true
1144
+ return !(wbIds.has(s.sessionId) || wbStrictInside(s.cwd, root))
1145
+ })
1140
1146
  const archived = visible.filter(s => archivedSet.has(s.sessionId))
1141
1147
  const main = visible.filter(s => !archivedSet.has(s.sessionId))
1142
1148
  const showArchived = LS.get('dsShowArchivedV1', '0') === '1'
@@ -1257,7 +1263,7 @@ function eventHtml(entry) {
1257
1263
  const sysText = type === 'user/message' ? systemReminderText(blocks) : ''
1258
1264
  if (sysText) {
1259
1265
  const shown = sysText.length > 400 ? sysText.slice(0, 400) + '…' : sysText
1260
- return `<details class="event ds-tool"><summary>${esc(t('ds.eventSystemReminder'))}</summary><pre>${esc(shown)}</pre></details>`
1266
+ return `<details class="event ds-tool ds-event-detail"><summary>${esc(t('ds.eventSystemReminder'))}</summary><pre>${esc(shown)}</pre></details>`
1261
1267
  }
1262
1268
  const text = blocks.map(blockHtml).join('')
1263
1269
  return `<div class="ds-msg ${esc(role)}"><div class="role">${esc(role === 'user' ? t('ds.role.me') : t('ds.role.dsh'))}</div>${text || '<span style="opacity:.6">…</span>'}</div>`
@@ -1457,6 +1463,7 @@ function renderNotifStack() {
1457
1463
  stack.querySelectorAll('.ds-notif-card').forEach(card => card.addEventListener('keydown', (e) => {
1458
1464
  if (e.key === 'Escape') toast(t('ds.ignored'), 'ok')
1459
1465
  }))
1466
+ renderOverviewDesktop()
1460
1467
  }
1461
1468
  async function approveApproval(id, allow) {
1462
1469
  const a = state.approvals.find(x => x.approvalId === id)
@@ -1594,7 +1601,7 @@ async function loadFs(dir, silent) {
1594
1601
  $('fs-path').textContent = data.path
1595
1602
  $('fs-list').innerHTML = (data.entries || []).map(e => `
1596
1603
  <div class="ds-fs-row" data-fs-path="${esc(e.path)}" data-fs-dir="${e.type === 'dir' ? '1' : '0'}">
1597
- <span>${e.type === 'dir' ? '📁' : '📄'}</span>
1604
+ <span class="ds-fs-type">${desktopFsIconSvg(e.type === 'dir')}</span>
1598
1605
  <span class="ds-fs-name">${esc(e.name)}</span>
1599
1606
  <span class="ds-fs-size">${e.type === 'dir' ? '' : fmtSize(e.size)}</span>
1600
1607
  </div>`).join('') || `<div class="ds-empty">${t('ds.fsEmpty')}</div>`
@@ -1607,6 +1614,12 @@ async function loadFs(dir, silent) {
1607
1614
  $('fs-list').innerHTML = `<div class="ds-empty">${esc(e.message || t('ds.toastConnFailed'))}</div>`
1608
1615
  }
1609
1616
  }
1617
+
1618
+ function desktopFsIconSvg(isDir) {
1619
+ return isDir
1620
+ ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 6.5h6l2 2H20a1 1 0 0 1 1 1v8.5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7.5a1 1 0 0 1 .5-1Z"/></svg>'
1621
+ : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3.5h8l4 4V20a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1Z"/><path d="M14 3.5v4h4M8 13h8M8 16h6"/></svg>'
1622
+ }
1610
1623
  function fsUp() {
1611
1624
  if (state.fs.path && state.fs.initial && state.fs.path !== state.fs.initial) {
1612
1625
  loadFs(fsParent(state.fs.path))
@@ -1684,15 +1697,21 @@ async function refreshWorkbench({ silent = false } = {}) {
1684
1697
  const listRes = await fetch(fsApiUrl('/list', { path: state.wb.path }), { headers: fsHeaders() })
1685
1698
  if (listRes.ok) {
1686
1699
  const listData = await listRes.json().catch(() => ({}))
1687
- const have = new Set(items.map(w => wbPathKey(w.path)))
1688
- for (const entry of listData.entries || []) {
1689
- if (entry.type !== 'dir') continue
1690
- const projectPath = wbJoin(state.wb.path, entry.name)
1691
- if (have.has(wbPathKey(projectPath))) continue
1692
- try {
1693
- const created = await rpc('workspace.create', { path: projectPath })
1694
- if (created?.workspace) { items.push(created.workspace); have.add(wbPathKey(projectPath)) }
1695
- } catch {}
1700
+ if (Array.isArray(listData.entries)) {
1701
+ const diskDirs = new Set(listData.entries.filter(e => e.type === 'dir').map(e => wbPathKey(wbJoin(state.wb.path, e.name))))
1702
+ for (let i = items.length - 1; i >= 0; i--) {
1703
+ if (!diskDirs.has(wbPathKey(items[i].path))) items.splice(i, 1)
1704
+ }
1705
+ const have = new Set(items.map(w => wbPathKey(w.path)))
1706
+ for (const entry of listData.entries) {
1707
+ if (entry.type !== 'dir') continue
1708
+ const projectPath = wbJoin(state.wb.path, entry.name)
1709
+ if (have.has(wbPathKey(projectPath))) continue
1710
+ try {
1711
+ const created = await rpc('workspace.create', { path: projectPath })
1712
+ if (created?.workspace) { items.push(created.workspace); have.add(wbPathKey(projectPath)) }
1713
+ } catch {}
1714
+ }
1696
1715
  }
1697
1716
  }
1698
1717
  } catch {}
@@ -1723,10 +1742,11 @@ function renderWorkbench() {
1723
1742
  panel.classList.toggle('hidden', !state.wb.expanded)
1724
1743
  if (!state.wb.expanded) return
1725
1744
  const projects = state.wb.projects || []
1745
+ const archivedSet = new Set(state.archivedIds || [])
1726
1746
  let html = `<div class="ds-wb-panel-title">${esc(t('wb.projects'))}</div>`
1727
1747
  html += projects.length ? projects.map(w => {
1728
1748
  const id = String(w.workspaceId || '')
1729
- const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(Boolean).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
1749
+ const sessions = (w.sessionIds || []).map(sid => state.byId.get(sid)).filter(Boolean).filter(s => !archivedSet.has(s.sessionId)).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
1730
1750
  const open = state.wb.open === id
1731
1751
  return `<div class="ds-wb-project ${open ? 'open' : ''}">
1732
1752
  <button type="button" class="ds-wb-project-head" data-wb-head="${esc(id)}">
@@ -1777,7 +1797,7 @@ async function wbFsLoad(dir) {
1777
1797
  const dirs = (data.entries || []).filter(e => e.type === 'dir')
1778
1798
  box.innerHTML = dirs.length ? dirs.map(e => {
1779
1799
  const p = wbJoin(data.path, e.name)
1780
- return `<div class="ds-wb-fs-row" data-wb-dir="${esc(p)}"><span>📁</span><span class="ds-wb-fs-name">${esc(e.name)}</span><button type="button" class="ds-btn ds-wb-select" data-wb-select="${esc(p)}">${esc(t('wb.selectDir'))}</button></div>`
1800
+ return `<div class="ds-wb-fs-row" data-wb-dir="${esc(p)}"><span class="ds-fs-type">${desktopFsIconSvg(true)}</span><span class="ds-wb-fs-name">${esc(e.name)}</span><button type="button" class="ds-btn ds-wb-select" data-wb-select="${esc(p)}">${esc(t('wb.selectDir'))}</button></div>`
1781
1801
  }).join('') : `<div class="ds-empty">${esc(t('wb.empty'))}</div>`
1782
1802
  box.querySelectorAll('[data-wb-dir]').forEach(row => row.addEventListener('click', e => { if (!e.target.closest('[data-wb-select]')) wbFsLoad(row.dataset.wbDir) }))
1783
1803
  box.querySelectorAll('[data-wb-select]').forEach(button => button.addEventListener('click', () => bindWorkbench(button.dataset.wbSelect)))
@@ -1899,13 +1919,98 @@ function renderStats(days) {
1899
1919
  }
1900
1920
 
1901
1921
  /* ---------------- 视图与连接状态 ---------------- */
1922
+ function renderOverviewDesktop() {
1923
+ const ring = $('ds-overview-pulse-ring')
1924
+ if (!ring) return
1925
+ const checks = {
1926
+ gateway: !!state.token && !!state.server,
1927
+ dsh: !!state.hostInfo,
1928
+ mux: !!state.streamsOk?.mux,
1929
+ host: !!state.streamsOk?.host
1930
+ }
1931
+ const online = Object.values(checks).filter(Boolean).length
1932
+ const status = online === 4 ? 'Nominal' : online > 0 ? 'Degraded' : 'Offline'
1933
+ const pulseCard = document.querySelector('.ds-overview-pulse-card')
1934
+ if (pulseCard) {
1935
+ pulseCard.classList.remove('status-nominal', 'status-degraded', 'status-offline')
1936
+ pulseCard.classList.add('status-' + status.toLowerCase())
1937
+ }
1938
+ ring.style.setProperty('--pulse-pct', `${online / 4 * 100}%`)
1939
+ $('ds-overview-health').textContent = online === 4 ? t('ds.live') : online ? `${online}/4` : t('ds.offlineCore')
1940
+ $('ds-overview-health-caption').textContent = online === 4 ? t('ds.allLinked') : online ? t('ds.components', { n: online }) : t('ds.offlineShort')
1941
+ $('ds-overview-status').textContent = t(`ds.system${status}`)
1942
+ $('ds-overview-status-desc').textContent = t('ds.components', { n: online })
1943
+ for (const [name, ok] of Object.entries(checks)) {
1944
+ const item = document.querySelector(`[data-ds-overview-link="${name}"]`)
1945
+ if (!item) continue
1946
+ item.classList.toggle('ok', ok)
1947
+ item.classList.toggle('off', !ok)
1948
+ const value = item.querySelector('b')
1949
+ if (value) value.textContent = ok ? t('ds.online') : t('ds.offlineShort')
1950
+ }
1951
+
1952
+ const pending = [
1953
+ ...state.approvals.map(a => ({ kind: 'approval', item: a })),
1954
+ ...state.questions.map(q => ({ kind: 'question', item: q }))
1955
+ ]
1956
+ $('ds-overview-attention-count').textContent = pending.length ? t('ds.pendingCount', { n: pending.length }) : '—'
1957
+ $('ds-overview-attention-list').innerHTML = pending.length ? pending.slice(0, 4).map(({ kind, item }) => {
1958
+ const title = titleOf(state.byId.get(item.sessionId))
1959
+ if (kind === 'approval') return `<div class="ds-overview-attention-item" data-ds-overview-approval="${esc(item.approvalId)}">
1960
+ <span class="ds-overview-mark">⌁</span><span class="ds-overview-copy"><span class="ds-overview-item-title">${esc(item.toolName || t('ds.toolDefault'))}</span><span class="ds-overview-item-desc">${esc(item.reason || t('ds.approvalReason', { reason: '' }))} · ${esc(title)}</span></span>
1961
+ <span class="ds-overview-actions"><button class="ds-btn allow" data-ds-overview-approve="1">${t('ds.allow')}</button><button class="ds-btn reject" data-ds-overview-approve="0">${t('ds.reject')}</button></span>
1962
+ </div>`
1963
+ return `<button type="button" class="ds-overview-attention-item question" data-ds-overview-question="${esc(item.rpcId)}">
1964
+ <span class="ds-overview-mark">?</span><span class="ds-overview-copy"><span class="ds-overview-item-title">${esc(item.questions?.[0]?.question || t('ds.questionNotify'))}</span><span class="ds-overview-item-desc">${esc(title)}</span></span><span class="ds-overview-arrow">›</span>
1965
+ </button>`
1966
+ }).join('') : `<div class="ds-overview-empty">${t('ds.nothingPending')}</div>`
1967
+ $('ds-overview-attention-list').querySelectorAll('[data-ds-overview-approve]').forEach(btn => btn.addEventListener('click', () => approveApproval(btn.closest('[data-ds-overview-approval]')?.dataset.dsOverviewApproval || '', btn.dataset.dsOverviewApprove === '1')))
1968
+ $('ds-overview-attention-list').querySelectorAll('[data-ds-overview-question]').forEach(btn => btn.addEventListener('click', () => openQuestionModal(state.questions.find(q => q.rpcId === btn.dataset.dsOverviewQuestion))))
1969
+
1970
+ const running = state.sessions.filter(s => s.running).length
1971
+ const sessions = [...state.sessions].sort((a, b) => Number(b.running) - Number(a.running) || (new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0))).slice(0, 6)
1972
+ const primary = $('ds-overview-primary-action')
1973
+ if (primary) {
1974
+ let action = 'new'
1975
+ let label = t('ds.action.newSession')
1976
+ let sessionId = ''
1977
+ if (!state.token) {
1978
+ action = 'settings'
1979
+ label = t('ds.action.connect')
1980
+ } else if (online > 0 && online < 4) {
1981
+ action = 'refresh'
1982
+ label = t('ds.action.refresh')
1983
+ } else if (pending.length) {
1984
+ action = 'attention'
1985
+ label = t('ds.action.attention')
1986
+ } else if (sessions.length) {
1987
+ action = 'session'
1988
+ sessionId = sessions[0].sessionId
1989
+ label = t('ds.action.openSession')
1990
+ }
1991
+ primary.textContent = label
1992
+ primary.dataset.dsOverviewAction = action
1993
+ primary.dataset.dsOverviewSession = sessionId
1994
+ }
1995
+ $('ds-overview-dsh-version').textContent = state.hostInfo?.version || '—'
1996
+ $('ds-overview-gateway-version').textContent = checks.gateway ? t('ds.online') : t('ds.offlineShort')
1997
+ $('ds-overview-active-sessions').textContent = String(running)
1998
+ $('ds-overview-connection-mode').textContent = state.token ? t(state.streamMode === 'poll' ? 'ds.poll' : 'ds.liveWs') : '—'
1999
+ $('ds-overview-active-count').textContent = running ? t('ds.activeCount', { n: running }) : ''
2000
+ $('ds-overview-session-list').innerHTML = sessions.length ? sessions.map(s => `<button type="button" class="ds-overview-session-item ${s.running ? 'running' : ''}" data-ds-overview-session="${esc(s.sessionId)}">
2001
+ <span class="ds-overview-mark">${s.running ? '●' : '○'}</span><span class="ds-overview-copy"><span class="ds-overview-item-title">${esc(titleOf(s))}</span><span class="ds-overview-item-desc">${s.running ? esc(t('ds.running')) + ' · ' : ''}${esc(fmtTime(s.updatedAt))}</span></span><span class="ds-overview-arrow">›</span>
2002
+ </button>`).join('') : `<div class="ds-overview-empty">${t('ds.noSessions')}</div>`
2003
+ $('ds-overview-session-list').querySelectorAll('[data-ds-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.dsOverviewSession)))
2004
+ }
2005
+
1902
2006
  function showView(id) {
1903
2007
  state.view = id
1904
- for (const v of ['view-sessions', 'view-chat', 'view-files', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
2008
+ for (const v of ['view-overview', 'view-sessions', 'view-chat', 'view-files', 'view-settings']) $(v).classList.toggle('hidden', v !== id)
1905
2009
  document.querySelectorAll('.ds-nav-item').forEach(b => b.classList.toggle('active', b.dataset.view === id))
1906
- const titles = { 'view-sessions': 'ds.sessions', 'view-chat': 'ds.sessions', 'view-files': 'ds.files', 'view-settings': 'ds.settings' }
2010
+ const titles = { 'view-overview': 'ds.overview', 'view-sessions': 'ds.sessions', 'view-chat': 'ds.sessions', 'view-files': 'ds.files', 'view-settings': 'ds.settings' }
1907
2011
  if (id === 'view-chat') { const s = state.byId.get(state.current); $('ds-title').textContent = s ? titleOf(s) : t('ds.sessions') }
1908
2012
  else $('ds-title').textContent = t(titles[id])
2013
+ if (id === 'view-overview') renderOverviewDesktop()
1909
2014
  if (id === 'view-files' && !state.fs.loaded) loadFs(null, true)
1910
2015
  if (id === 'view-settings') showSettingsHome()
1911
2016
  }
@@ -1991,6 +2096,28 @@ function bindUi() {
1991
2096
  list.style.display = list.style.display === 'none' ? 'flex' : 'none'
1992
2097
  })
1993
2098
  document.querySelectorAll('.ds-nav-item').forEach(b => b.addEventListener('click', () => showView(b.dataset.view)))
2099
+ $('ds-overview-refresh').addEventListener('click', async () => {
2100
+ toast(t('ds.loading'))
2101
+ if (state.token) {
2102
+ await refreshSessions()
2103
+ const host = await safeRpc('host.describe', {}, '')
2104
+ if (host) state.hostInfo = host
2105
+ }
2106
+ renderOverviewDesktop()
2107
+ })
2108
+ $('ds-overview-primary-action').addEventListener('click', () => {
2109
+ const button = $('ds-overview-primary-action')
2110
+ const action = button.dataset.dsOverviewAction
2111
+ if (action === 'session' && button.dataset.dsOverviewSession) return openSession(button.dataset.dsOverviewSession)
2112
+ if (action === 'new') return $('btn-new-session').click()
2113
+ if (action === 'settings') return showView('view-settings')
2114
+ if (action === 'refresh') return $('ds-overview-refresh').click()
2115
+ const first = document.querySelector('.ds-overview-attention-item')
2116
+ if (first) {
2117
+ first.scrollIntoView({ behavior: 'smooth', block: 'center' })
2118
+ if (first.matches('button')) first.focus({ preventScroll: true })
2119
+ }
2120
+ })
1994
2121
  $('session-list').addEventListener('click', (e) => {
1995
2122
  if (e.target.closest('[data-archived-toggle]')) {
1996
2123
  LS.set('dsShowArchivedV1', LS.get('dsShowArchivedV1', '0') === '1' ? '0' : '1')
@@ -2128,7 +2255,7 @@ function bindUi() {
2128
2255
  $('btn-lang').addEventListener('click', () => {
2129
2256
  I18N.setLang(I18N.lang === 'zh' ? 'en' : 'zh')
2130
2257
  $('btn-lang').textContent = I18N.lang === 'zh' ? 'EN' : '中文'
2131
- renderServers(); renderSessions(); updateConn(); themeApply()
2258
+ renderServers(); renderSessions(); renderNotifStack(); renderOverviewDesktop(); updateConn(); themeApply()
2132
2259
  })
2133
2260
  $('fs-up').addEventListener('click', fsUp)
2134
2261
  $('fs-new-workspace').addEventListener('click', openWorkspaceModal)
@@ -2140,7 +2267,7 @@ function bindUi() {
2140
2267
  async function start() {
2141
2268
  loadServers()
2142
2269
  renderServers()
2143
- showView('view-sessions')
2270
+ showView('view-overview')
2144
2271
  const urlToken = new URLSearchParams(location.search).get('token')
2145
2272
  if (urlToken) { state.token = urlToken; LS.set('token', urlToken); history.replaceState(null, '', location.pathname) }
2146
2273
  if (!state.token) {
@@ -2155,9 +2282,12 @@ async function start() {
2155
2282
  if (state.token) {
2156
2283
  if (state.servers.length) await selectFastestServer({ silent: true, reconnect: false })
2157
2284
  openStreams()
2158
- refreshSessions()
2285
+ await refreshSessions()
2286
+ const host = await safeRpc('host.describe', {}, '')
2287
+ if (host) state.hostInfo = host
2159
2288
  refreshWorkbench({ silent: true })
2160
2289
  }
2290
+ renderOverviewDesktop()
2161
2291
  }
2162
2292
 
2163
2293
  start()