dsh-remote-plugin 0.6.23 → 0.6.24

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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-plugin",
3
- "version": "0.6.23",
3
+ "version": "0.6.24",
4
4
  "description": "DSH Remote 官方 bundle 插件:DSH 左侧原生边栏入口 + 右侧抽屉内嵌管理控制台;内置网关随 DSH 自动启停(systemd 独立单元),抽屉直显令牌与设备监控;网关提供 /fs/* 文件传输端点(列表/断点下载/上传),配合 Android App 远程操控会话/审批/提问/goal 与文件互传(多服务器测速切换、聊天记录离线缓存)。",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",
package/public/app.js CHANGED
@@ -1837,7 +1837,17 @@ function resyncAfterStreamOpen() {
1837
1837
  state.lastStreamResyncAt = Date.now()
1838
1838
  void refreshAll().then(() => resyncCurrentSession())
1839
1839
  }
1840
- function titleOf(s) { return proj(s, 'title') || (s?.sessionId ? short(s.sessionId) : t('session.unknown')) }
1840
+ function sessionTitleValue(s) {
1841
+ const value = proj(s, 'title', '')
1842
+ return value == null ? '' : String(value).trim()
1843
+ }
1844
+ function hasSessionTitle(s) { return !!sessionTitleValue(s) }
1845
+ function titleOf(s) { return sessionTitleValue(s) || (s?.sessionId ? t('session.untitled') : t('session.unknown')) }
1846
+ function sessionLabelOf(s) {
1847
+ const title = titleOf(s)
1848
+ if (!s?.sessionId || hasSessionTitle(s)) return title
1849
+ return `${title} · ${String(s.sessionId).slice(-8)}`
1850
+ }
1841
1851
  function short(id) { return '…' + String(id).slice(-8) }
1842
1852
  function isTopLevelSession(session) {
1843
1853
  return !!session && !session.parentSessionId && session.origin !== 'subagent'
@@ -2110,7 +2120,7 @@ function renderWorkbench() {
2110
2120
  <div class="session-swipe" data-session-swipe data-id="${esc(s.sessionId)}">
2111
2121
  <button class="wb-session" type="button" data-wb-session="${esc(s.sessionId)}" data-motion-key="${esc(s.sessionId)}">
2112
2122
  <span class="wb-session-drag-handle" data-reorder-handle aria-hidden="true">⠿</span>
2113
- <span class="wb-session-title">${esc(titleOf(s))}</span>
2123
+ <span class="wb-session-title">${esc(sessionLabelOf(s))}</span>
2114
2124
  <span class="wb-session-meta">${s.running ? esc(t('sessions.running')) : esc(fmtTime(sessionSortTime(s)))}</span>
2115
2125
  </button>
2116
2126
  <button type="button" class="sc-archive-btn" data-archive-session="${esc(s.sessionId)}">${esc(t('session.archive'))}</button>
@@ -2202,7 +2212,7 @@ function renderSessions() {
2202
2212
  const renderSession = s => {
2203
2213
  const workspace = sessionWorkspaceLabel(s)
2204
2214
  const workspaceTitle = sessionWorkspaceName(s)
2205
- const title = titleOf(s)
2215
+ const title = sessionLabelOf(s)
2206
2216
  const goal = goalOf(s)
2207
2217
  const pending = (state.approvals.some(a => a.sessionId === s.sessionId) || state.questions.some(q => q.sessionId === s.sessionId)) ? 'pending' : ''
2208
2218
  const queueN = (state.queues[s.sessionId] || []).filter(i => i.placement === 'queued').length
@@ -3410,7 +3420,7 @@ function renameSession(sessionId = state.current) {
3410
3420
  const session = state.byId.get(sessionId)
3411
3421
  if (!session) return
3412
3422
  renamePendingSessionId = sessionId
3413
- $('rename-session-input').value = titleOf(session)
3423
+ $('rename-session-input').value = sessionTitleValue(session)
3414
3424
  $('modal-rename').classList.remove('hidden')
3415
3425
  setTimeout(() => { $('rename-session-input').focus(); $('rename-session-input').select() }, 40)
3416
3426
  }
@@ -3601,7 +3611,7 @@ function renderOverview() {
3601
3611
  ]
3602
3612
  $('overview-attention-count').textContent = pending.length ? t('overview.pendingCount', { n: pending.length }) : '—'
3603
3613
  $('overview-attention-list').innerHTML = pending.length ? pending.slice(0, 3).map(({ kind, item }) => {
3604
- const title = titleOf(state.byId.get(item.sessionId))
3614
+ const title = sessionLabelOf(state.byId.get(item.sessionId))
3605
3615
  if (kind === 'approval') return `<div class="overview-attention-item" data-overview-approval="${esc(item.approvalId)}">
3606
3616
  <span class="overview-item-mark">⌁</span><span class="overview-item-copy"><span class="overview-item-title">${esc(item.toolName || t('tool.default'))}</span><span class="overview-item-desc">${esc(approvalDetail(item))} · ${esc(title)}</span></span>
3607
3617
  <span class="overview-item-actions"><button type="button" class="mini-btn" data-overview-approve="1">${t('pending.allow')}</button><button type="button" class="mini-btn" data-overview-approve="0">${t('pending.reject')}</button></span>
@@ -3650,7 +3660,7 @@ function renderOverview() {
3650
3660
  $('overview-connection-mode').textContent = state.token ? t(state.streamMode === 'poll' ? 'overview.poll' : 'overview.ws') : '—'
3651
3661
  $('overview-active-count').textContent = running ? t('overview.activeCount', { n: running }) : ''
3652
3662
  $('overview-session-list').innerHTML = sessions.length ? sessions.map(s => `<button type="button" class="overview-session-item ${s.running ? 'running' : ''}" data-overview-session="${esc(s.sessionId)}">
3653
- <span class="overview-item-mark">${s.running ? '●' : '○'}</span><span class="overview-item-copy"><span class="overview-item-title">${esc(titleOf(s))}</span><span class="overview-item-desc">${s.running ? esc(t('sessions.running')) + ' · ' : ''}${esc(fmtTime(sessionSortTime(s)))}</span></span><span class="overview-item-arrow">›</span>
3663
+ <span class="overview-item-mark">${s.running ? '●' : '○'}</span><span class="overview-item-copy"><span class="overview-item-title">${esc(sessionLabelOf(s))}</span><span class="overview-item-desc">${s.running ? esc(t('sessions.running')) + ' · ' : ''}${esc(fmtTime(sessionSortTime(s)))}</span></span><span class="overview-item-arrow">›</span>
3654
3664
  </button>`).join('') : `<div class="overview-empty">${t('overview.noSession')}</div>`
3655
3665
  $('overview-session-list').querySelectorAll('[data-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.overviewSession)))
3656
3666
  }
@@ -528,6 +528,13 @@ button.ds-overview-attention-item, button.ds-overview-session-item { cursor:poin
528
528
  .ds-workspace-create-location code { min-width: 0; flex: 1; padding: 7px 9px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsr-text); background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 9px; }
529
529
  .ds-workspace-name { width: 100%; box-sizing: border-box; background: var(--dsr-bg-2); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 9px; padding: 8px 10px; font: inherit; outline: none; }
530
530
  .ds-workspace-name:focus { border-color: var(--dsr-accent-line); }
531
+ .ds-new-session-desc { margin: 0; color: var(--dsr-muted); line-height: 1.55; }
532
+ .ds-new-session-label { font-size: 12px; color: var(--dsr-muted); }
533
+ .ds-new-session-select { width: 100%; box-sizing: border-box; background: var(--dsr-bg-2); color: var(--dsr-text); border: 1px solid var(--dsr-line); border-radius: 9px; padding: 8px 10px; font: inherit; outline: none; }
534
+ .ds-new-session-select:focus { border-color: var(--dsr-accent-line); }
535
+ .ds-new-session-details { display: flex; flex-direction: column; gap: 4px; min-width: 0; padding: 8px 10px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 9px; }
536
+ .ds-new-session-name { font-size: 13px; font-weight: 600; }
537
+ .ds-new-session-path { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsr-muted); font-size: 11.5px; }
531
538
  .ds-modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
532
539
  .ds-q-item { border: 1px solid var(--dsr-line); border-radius: 10px; padding: 9px 11px; }
533
540
  .ds-q-text { font-size: 13.5px; margin-bottom: 6px; }
@@ -393,6 +393,27 @@
393
393
  </div>
394
394
  </div>
395
395
 
396
+ <!-- 新建会话工作区模态 -->
397
+ <div id="modal-new-session" class="ds-modal hidden" role="dialog" aria-modal="true">
398
+ <div class="ds-modal-card">
399
+ <div class="ds-modal-title" data-i18n="ds.newSessionTitle">新建会话</div>
400
+ <div class="ds-modal-body">
401
+ <p class="ds-new-session-desc" data-i18n="ds.newSessionDesc">选择一个 DSH 工作区,新会话将在该工作区中创建。</p>
402
+ <label class="ds-new-session-label" for="new-session-workspace" data-i18n="ds.newSessionWorkspace">工作区</label>
403
+ <select id="new-session-workspace" class="ds-new-session-select"></select>
404
+ <div id="new-session-workspace-details" class="ds-new-session-details">
405
+ <span id="new-session-workspace-name" class="ds-new-session-name"></span>
406
+ <code id="new-session-workspace-path" class="ds-new-session-path"></code>
407
+ </div>
408
+ <div id="new-session-empty" class="ds-empty hidden" data-i18n="ds.newSessionNoWorkspaces">暂无可用工作区,请先创建一个工作区。</div>
409
+ </div>
410
+ <div class="ds-modal-actions">
411
+ <button id="new-session-cancel" class="ds-btn" data-i18n="ds.cancel">取消</button>
412
+ <button id="new-session-create" class="ds-btn primary" data-i18n="ds.newSessionCreate">创建</button>
413
+ </div>
414
+ </div>
415
+ </div>
416
+
396
417
  <!-- 预设提示词管理模态 -->
397
418
  <div id="modal-presets" class="ds-modal hidden" role="dialog" aria-modal="true">
398
419
  <div class="ds-modal-card">
@@ -470,7 +491,7 @@
470
491
  <script>
471
492
  window.DESKTOP_STR = {
472
493
  zh: {
473
- 'ds.repo': 'GitHub 仓库', 'ds.newSession': '+ 新会话', 'ds.newWorkspace': '新建工作区', 'ds.sessions': '会话', 'ds.overview': '主页', 'ds.overviewEyebrow': 'SYSTEM OVERVIEW', 'ds.overviewSubtitle': '实时状态与需要你处理的事项',
494
+ 'ds.repo': 'GitHub 仓库', 'ds.newSession': '+ 新会话', 'ds.sessionUntitled': '新会话', 'ds.newWorkspace': '新建工作区', 'ds.sessions': '会话', 'ds.overview': '主页', 'ds.overviewEyebrow': 'SYSTEM OVERVIEW', 'ds.overviewSubtitle': '实时状态与需要你处理的事项',
474
495
  'ds.systemPulse': '系统链路', 'ds.systemNominal': '系统链路正常', 'ds.systemDegraded': '部分链路需要关注', 'ds.systemOffline': '连接已断开', 'ds.checking': '检查中…', 'ds.components': '{n}/4 个模块在线', 'ds.live': 'LIVE', 'ds.allLinked': '全部链路', 'ds.offlineCore': 'OFFLINE',
475
496
  'ds.gateway': '网关', 'ds.dshUpstream': 'DSH 上游', 'ds.mux': '实时 mux', 'ds.host': '实时 host', 'ds.online': '在线', 'ds.offlineShort': '离线',
476
497
  'ds.attention': '需要你处理', 'ds.runtime': '运行指标', 'ds.recentActivity': '近期活动', 'ds.dshVersion': 'DSH 版本', 'ds.gatewayStatus': '网关状态', 'ds.activeSessions': '活跃会话', 'ds.connectionMode': '连接模式', 'ds.liveWs': '实时 WS', 'ds.poll': '降级轮询', 'ds.running': '运行中', 'ds.noReason': '无说明', 'ds.noActivity': '暂无后台活动', 'ds.noSessions': '暂无会话', 'ds.refreshOverview': '刷新总览', 'ds.pendingCount': '{n} 项待处理', 'ds.activeCount': '{n} 个活跃会话', 'ds.nothingPending': '暂无事项', 'ds.action.openSession': '打开最近会话', 'ds.action.newSession': '新建会话', 'ds.action.attention': '处理待办', 'ds.action.connect': '去设置连接', 'ds.action.refresh': '重新检查连接',
@@ -482,7 +503,7 @@
482
503
  'ds.cmdFeedback': '/feedback 反馈当前会话', 'ds.cmdGoal': '/goal 设置/查看任务目标',
483
504
  'ds.cmdPermission': '/permission 切换权限预设', 'ds.cmdPlan': '/plan 进入/退出计划模式',
484
505
  'ds.fsUp': '上级', 'ds.fsRoot': '允许根目录', 'ds.fsNewWorkspace': '新建工作区', 'ds.fsRefresh': '刷新', 'ds.fsEmpty': '目录为空',
485
- 'ds.workspaceCreateTitle': '新建工作区', 'ds.workspaceCreateDesc': '将在当前文件目录下创建文件夹,并自动打开新会话。', 'ds.workspaceParent': '父目录', 'ds.workspaceNamePlaceholder': '例如:my-project', 'ds.workspaceCreate': '创建并打开', 'ds.cancel': '取消', 'ds.workspaceNameRequired': '请输入工作区名称', 'ds.workspaceExists': '该目录已存在', 'ds.workspaceInvalidName': '名称不能包含路径分隔符', 'ds.workspaceCreateFailed': '创建工作区失败', 'ds.workspaceCreated': '工作区已创建', 'ds.workspaceCreatedNoSession': '工作区已创建,但新会话未能打开',
506
+ 'ds.workspaceCreateTitle': '新建工作区', 'ds.workspaceCreateDesc': '将在当前文件目录下创建文件夹,并自动打开新会话。', 'ds.workspaceParent': '父目录', 'ds.workspaceNamePlaceholder': '例如:my-project', 'ds.workspaceCreate': '创建并打开', 'ds.cancel': '取消', 'ds.workspaceNameRequired': '请输入工作区名称', 'ds.workspaceExists': '该目录已存在', 'ds.workspaceInvalidName': '名称不能包含路径分隔符', 'ds.workspaceCreateFailed': '创建工作区失败', 'ds.workspaceCreated': '工作区已创建', 'ds.workspaceCreatedNoSession': '工作区已创建,但新会话未能打开', 'ds.newSessionTitle': '新建会话', 'ds.newSessionDesc': '选择一个 DSH 工作区,新会话将在该工作区中创建。', 'ds.newSessionWorkspace': '工作区', 'ds.newSessionNoWorkspaces': '暂无可用工作区,请先创建一个工作区。', 'ds.newSessionCreate': '创建', 'ds.newSessionChooseWorkspace': '请选择一个工作区',
486
507
  'ds.groupGeneral': '通用', 'ds.groupGeneralDesc': '工具调用、预设提示词',
487
508
  'ds.groupServers': '服务器', 'ds.groupServersDesc': '服务器地址与令牌',
488
509
  'ds.groupNotify': '通知', 'ds.groupNotifyDesc': '通知与提醒',
@@ -575,7 +596,7 @@
575
596
  'menu.modelTitle': '模型切换', 'menu.effortTitle': '思考深度',
576
597
  },
577
598
  en: {
578
- 'ds.repo': 'GitHub repo', 'ds.newSession': '+ New session', 'ds.newWorkspace': 'New workspace', 'ds.sessions': 'Sessions', 'ds.overview': 'Home', 'ds.overviewEyebrow': 'SYSTEM OVERVIEW', 'ds.overviewSubtitle': 'Live status and items needing your attention',
599
+ 'ds.repo': 'GitHub repo', 'ds.newSession': '+ New session', 'ds.sessionUntitled': 'New session', 'ds.newWorkspace': 'New workspace', 'ds.sessions': 'Sessions', 'ds.overview': 'Home', 'ds.overviewEyebrow': 'SYSTEM OVERVIEW', 'ds.overviewSubtitle': 'Live status and items needing your attention',
579
600
  'ds.systemPulse': 'System pulse', 'ds.systemNominal': 'System nominal', 'ds.systemDegraded': 'Some links need attention', 'ds.systemOffline': 'Connection offline', 'ds.checking': 'Checking…', 'ds.components': '{n}/4 components online', 'ds.live': 'LIVE', 'ds.allLinked': 'ALL LINKED', 'ds.offlineCore': 'OFFLINE',
580
601
  'ds.gateway': 'Gateway', 'ds.dshUpstream': 'DSH upstream', 'ds.mux': 'Live mux', 'ds.host': 'Live host', 'ds.online': 'Online', 'ds.offlineShort': 'Offline',
581
602
  'ds.attention': 'Needs attention', 'ds.runtime': 'Runtime metrics', 'ds.recentActivity': 'Recent activity', 'ds.dshVersion': 'DSH version', 'ds.gatewayStatus': 'Gateway status', 'ds.activeSessions': 'Active sessions', 'ds.connectionMode': 'Connection mode', 'ds.liveWs': 'Live WS', 'ds.poll': 'Degraded polling', 'ds.running': 'Running', 'ds.noReason': 'No reason given', 'ds.noActivity': 'No background activity', 'ds.noSessions': 'No sessions yet', 'ds.refreshOverview': 'Refresh overview', 'ds.pendingCount': '{n} pending', 'ds.activeCount': '{n} active sessions', 'ds.nothingPending': 'Nothing pending', 'ds.action.openSession': 'Open recent session', 'ds.action.newSession': 'New session', 'ds.action.attention': 'Review pending', 'ds.action.connect': 'Set up connection', 'ds.action.refresh': 'Check connection',
@@ -587,7 +608,7 @@
587
608
  'ds.cmdFeedback': '/feedback Feedback current session', 'ds.cmdGoal': '/goal Set/view task goal',
588
609
  'ds.cmdPermission': '/permission Switch permission preset', 'ds.cmdPlan': '/plan Enter/exit plan mode',
589
610
  'ds.fsUp': 'Up', 'ds.fsRoot': 'Allowed root', 'ds.fsNewWorkspace': 'New workspace', 'ds.fsRefresh': 'Refresh', 'ds.fsEmpty': 'Empty directory',
590
- 'ds.workspaceCreateTitle': 'New workspace', 'ds.workspaceCreateDesc': 'Create a folder in the current file directory and open a new session there.', 'ds.workspaceParent': 'Parent folder', 'ds.workspaceNamePlaceholder': 'For example: my-project', 'ds.workspaceCreate': 'Create & open', 'ds.cancel': 'Cancel', 'ds.workspaceNameRequired': 'Enter a workspace name', 'ds.workspaceExists': 'That folder already exists', 'ds.workspaceInvalidName': 'The name cannot contain path separators', 'ds.workspaceCreateFailed': 'Could not create workspace', 'ds.workspaceCreated': 'Workspace created', 'ds.workspaceCreatedNoSession': 'Workspace created, but the new session could not be opened',
611
+ 'ds.workspaceCreateTitle': 'New workspace', 'ds.workspaceCreateDesc': 'Create a folder in the current file directory and open a new session there.', 'ds.workspaceParent': 'Parent folder', 'ds.workspaceNamePlaceholder': 'For example: my-project', 'ds.workspaceCreate': 'Create & open', 'ds.cancel': 'Cancel', 'ds.workspaceNameRequired': 'Enter a workspace name', 'ds.workspaceExists': 'That folder already exists', 'ds.workspaceInvalidName': 'The name cannot contain path separators', 'ds.workspaceCreateFailed': 'Could not create workspace', 'ds.workspaceCreated': 'Workspace created', 'ds.workspaceCreatedNoSession': 'Workspace created, but the new session could not be opened', 'ds.newSessionTitle': 'New session', 'ds.newSessionDesc': 'Choose a DSH workspace for the new session.', 'ds.newSessionWorkspace': 'Workspace', 'ds.newSessionNoWorkspaces': 'No workspaces are available. Create a workspace first.', 'ds.newSessionCreate': 'Create', 'ds.newSessionChooseWorkspace': 'Choose a workspace',
591
612
  'ds.groupGeneral': 'General', 'ds.groupGeneralDesc': 'Tool calls, prompt presets',
592
613
  'ds.groupServers': 'Servers', 'ds.groupServersDesc': 'Server address and token',
593
614
  'ds.groupNotify': 'Notifications', 'ds.groupNotifyDesc': 'Notifications & reminders',
@@ -95,6 +95,7 @@ const state = {
95
95
  pollSeq: { mux: 0, host: 0 },
96
96
  fs: { path: null, initial: null, loaded: false, roots: [], rootIndex: 0 },
97
97
  models: { loaded: false, loading: false, groups: [], current: null, failures: [] },
98
+ workspaces: [],
98
99
  wb: { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false },
99
100
  archivedIds: [],
100
101
  view: 'sessions',
@@ -1530,7 +1531,17 @@ function resyncAfterStreamOpen() {
1530
1531
  void refreshSessions().then(() => resyncCurrentSession())
1531
1532
  }
1532
1533
  function proj(s, key, d) { return s?.projections?.values?.[key] ?? d }
1533
- function titleOf(s) { return proj(s, 'title') || (s?.sessionId ? short(s.sessionId) : t('ds.sessions')) }
1534
+ function sessionTitleValue(s) {
1535
+ const value = proj(s, 'title', '')
1536
+ return value == null ? '' : String(value).trim()
1537
+ }
1538
+ function hasSessionTitle(s) { return !!sessionTitleValue(s) }
1539
+ function titleOf(s) { return sessionTitleValue(s) || (s?.sessionId ? t('ds.sessionUntitled') : t('ds.sessions')) }
1540
+ function sessionLabelOf(s) {
1541
+ const title = titleOf(s)
1542
+ if (!s?.sessionId || hasSessionTitle(s)) return title
1543
+ return `${title} · ${String(s.sessionId).slice(-8)}`
1544
+ }
1534
1545
  function isTopLevelSession(session) {
1535
1546
  return !!session && !session.parentSessionId && session.origin !== 'subagent'
1536
1547
  }
@@ -1660,7 +1671,7 @@ function renderSessions() {
1660
1671
  const renderSession = s => {
1661
1672
  const workspace = sessionWorkspaceLabel(s)
1662
1673
  const workspaceName = workspaceDisplayName(workspace)
1663
- const title = titleOf(s)
1674
+ const title = sessionLabelOf(s)
1664
1675
  return `<button class="ds-session-item ${state.current === s.sessionId ? 'current' : ''}" data-id="${esc(s.sessionId)}">
1665
1676
  <span class="ds-session-title">${esc(title)}</span>
1666
1677
  <span class="ds-session-workspace" title="${esc(workspace)}">⌂ ${esc(workspaceName)}</span>
@@ -2146,7 +2157,7 @@ function renameSession(sessionId = state.current) {
2146
2157
  const session = state.byId.get(sessionId)
2147
2158
  if (!session) return
2148
2159
  renamePendingSessionId = sessionId
2149
- $('rename-session-input').value = titleOf(session)
2160
+ $('rename-session-input').value = sessionTitleValue(session)
2150
2161
  $('modal-rename').classList.remove('hidden')
2151
2162
  setTimeout(() => { $('rename-session-input').focus(); $('rename-session-input').select() }, 40)
2152
2163
  }
@@ -2480,9 +2491,19 @@ async function createWorkspace() {
2480
2491
  }
2481
2492
  closeWorkspaceModal()
2482
2493
  await loadFs(parent || null, true)
2483
- const v = await safeRpc('session.create', { cwd: data.path }, t('ds.toastOpFailed'))
2494
+ let workspace = null
2495
+ try {
2496
+ const created = await rpc('workspace.create', { path: data.path })
2497
+ workspace = created?.workspace || null
2498
+ } catch {}
2499
+ if (workspace?.workspaceId) {
2500
+ state.workspaces = [...state.workspaces.filter(item => item.workspaceId !== workspace.workspaceId), workspace]
2501
+ }
2502
+ const sessionPayload = workspace?.workspaceId ? { workspaceId: workspace.workspaceId } : { cwd: data.path }
2503
+ const v = await safeRpc('session.create', sessionPayload, t('ds.toastOpFailed'))
2484
2504
  await refreshSessions()
2485
2505
  if (v?.sessionId) {
2506
+ if (workspace?.workspaceId) LS.set('lastNewSessionWorkspaceV1', workspace.workspaceId)
2486
2507
  toast(t('ds.workspaceCreated'), 'ok')
2487
2508
  openSession(v.sessionId)
2488
2509
  } else {
@@ -2566,6 +2587,22 @@ function wbBaseName(p) {
2566
2587
  const value = String(p || '').replace(/[\\/]+$/, '')
2567
2588
  return value.split(/[\\/]/).pop() || value
2568
2589
  }
2590
+ function workspaceItems() {
2591
+ return (state.workspaces || []).filter(w => w && typeof w.workspaceId === 'string' && w.workspaceId && typeof w.path === 'string' && w.path)
2592
+ }
2593
+ function workspaceById(workspaceId) {
2594
+ return workspaceItems().find(w => w.workspaceId === workspaceId) || null
2595
+ }
2596
+ function workspaceName(workspace) {
2597
+ return String(workspace?.title || wbBaseName(workspace?.path) || workspace?.path || '').trim()
2598
+ }
2599
+ function workspaceOptionLabel(workspace) {
2600
+ const name = workspaceName(workspace)
2601
+ return workspace.path && workspace.path !== name ? `${name} — ${workspace.path}` : name
2602
+ }
2603
+ function workspaceOptionsHtml(selected = '') {
2604
+ return workspaceItems().map(workspace => `<option value="${esc(workspace.workspaceId)}"${workspace.workspaceId === selected ? ' selected' : ''}>${esc(workspaceOptionLabel(workspace))}</option>`).join('')
2605
+ }
2569
2606
  function wbStrictInside(pathValue, rootValue) {
2570
2607
  const pathKey = wbPathKey(pathValue)
2571
2608
  const rootKey = wbPathKey(rootValue)
@@ -2668,6 +2705,7 @@ async function refreshWorkbench({ silent = false } = {}) {
2668
2705
  }
2669
2706
  const wl = await safeRpc('workspace.list', {}, '')
2670
2707
  state.archivedIds = wl && Array.isArray(wl.archivedSessionIds) ? wl.archivedSessionIds : []
2708
+ state.workspaces = wl && Array.isArray(wl.items) ? wl.items.slice() : []
2671
2709
  if (!wb) { renderWorkbench(); renderSessions(); return }
2672
2710
  if (!wb.bound) {
2673
2711
  state.wb = { bound: false, path: '', title: '', expanded: false, projects: null, open: null, apiMissing: false }
@@ -2696,7 +2734,11 @@ async function refreshWorkbench({ silent = false } = {}) {
2696
2734
  if (have.has(wbPathKey(projectPath))) continue
2697
2735
  try {
2698
2736
  const created = await rpc('workspace.create', { path: projectPath })
2699
- if (created?.workspace) { items.push(created.workspace); have.add(wbPathKey(projectPath)) }
2737
+ if (created?.workspace) {
2738
+ items.push(created.workspace)
2739
+ state.workspaces.push(created.workspace)
2740
+ have.add(wbPathKey(projectPath))
2741
+ }
2700
2742
  } catch {}
2701
2743
  }
2702
2744
  }
@@ -2744,7 +2786,7 @@ function renderWorkbench() {
2744
2786
  </button>
2745
2787
  <div class="ds-wb-project-body ${open ? '' : 'hidden'}">
2746
2788
  <button type="button" class="ds-mini-btn ds-wb-new-session" data-wb-new="${esc(id)}">+ ${esc(t('wb.newSession'))}</button>
2747
- ${sessions.length ? sessions.map(s => `<button type="button" class="ds-wb-session ${state.current === s.sessionId ? 'current' : ''}" data-wb-session="${esc(s.sessionId)}" data-motion-key="${esc(s.sessionId)}"><span class="ds-wb-session-drag-handle" data-reorder-handle aria-hidden="true">⠿</span><span class="ds-wb-session-dot ${s.running ? 'running' : ''}"></span><span class="ds-wb-session-title">${esc(titleOf(s))}</span></button>`).join('') : `<div class="ds-wb-session-empty">${esc(t('wb.noSessions'))}</div>`}
2789
+ ${sessions.length ? sessions.map(s => `<button type="button" class="ds-wb-session ${state.current === s.sessionId ? 'current' : ''}" data-wb-session="${esc(s.sessionId)}" data-motion-key="${esc(s.sessionId)}"><span class="ds-wb-session-drag-handle" data-reorder-handle aria-hidden="true">⠿</span><span class="ds-wb-session-dot ${s.running ? 'running' : ''}"></span><span class="ds-wb-session-title">${esc(sessionLabelOf(s))}</span></button>`).join('') : `<div class="ds-wb-session-empty">${esc(t('wb.noSessions'))}</div>`}
2748
2790
  </div>
2749
2791
  </div>`
2750
2792
  }).join('') : `<div class="ds-wb-empty">${esc(t('wb.noProjects'))}</div>`
@@ -2774,6 +2816,46 @@ function renderWorkbench() {
2774
2816
  panel.querySelectorAll('[data-wb-session]').forEach(button => button.addEventListener('click', () => openSession(button.dataset.wbSession)))
2775
2817
  panel.querySelectorAll('[data-wb-unbind-panel]').forEach(button => button.addEventListener('click', unbindWorkbench))
2776
2818
  }
2819
+ function renderNewSessionWorkspace(preferredId = '') {
2820
+ const items = workspaceItems()
2821
+ const select = $('new-session-workspace')
2822
+ if (!select) return
2823
+ const current = workspaceById(preferredId || select.value)?.workspaceId || items[0]?.workspaceId || ''
2824
+ select.innerHTML = workspaceOptionsHtml(current)
2825
+ select.disabled = !items.length
2826
+ const workspace = workspaceById(select.value)
2827
+ $('new-session-workspace-name').textContent = workspace ? workspaceName(workspace) : ''
2828
+ $('new-session-workspace-path').textContent = workspace?.path || ''
2829
+ $('new-session-empty').classList.toggle('hidden', !!items.length)
2830
+ $('new-session-create').disabled = !workspace
2831
+ }
2832
+ async function openNewSessionModal() {
2833
+ if (!state.token) return showView('view-settings')
2834
+ if (!workspaceItems().length) await refreshWorkbench({ silent: true })
2835
+ renderNewSessionWorkspace(LS.get('lastNewSessionWorkspaceV1', ''))
2836
+ $('modal-new-session').classList.remove('hidden')
2837
+ setTimeout(() => $('new-session-workspace')?.focus(), 50)
2838
+ }
2839
+ function closeNewSessionModal() { $('modal-new-session').classList.add('hidden') }
2840
+ async function createSessionInWorkspace() {
2841
+ if (createSessionInWorkspace.busy) return
2842
+ const workspaceId = $('new-session-workspace').value
2843
+ if (!workspaceById(workspaceId)) return toast(t('ds.newSessionChooseWorkspace'), 'err')
2844
+ createSessionInWorkspace.busy = true
2845
+ const button = $('new-session-create')
2846
+ button.disabled = true
2847
+ try {
2848
+ const value = await safeRpc('session.create', { workspaceId }, '')
2849
+ if (!value?.sessionId) return
2850
+ LS.set('lastNewSessionWorkspaceV1', workspaceId)
2851
+ closeNewSessionModal()
2852
+ await refreshSessions()
2853
+ openSession(value.sessionId)
2854
+ } finally {
2855
+ createSessionInWorkspace.busy = false
2856
+ button.disabled = false
2857
+ }
2858
+ }
2777
2859
  const wbFs = { path: null, initial: null }
2778
2860
  function openWorkbenchModal() {
2779
2861
  $('modal-workbench').classList.remove('hidden')
@@ -2975,7 +3057,7 @@ function renderOverviewDesktop() {
2975
3057
  ]
2976
3058
  $('ds-overview-attention-count').textContent = pending.length ? t('ds.pendingCount', { n: pending.length }) : '—'
2977
3059
  $('ds-overview-attention-list').innerHTML = pending.length ? pending.slice(0, 4).map(({ kind, item }) => {
2978
- const title = titleOf(state.byId.get(item.sessionId))
3060
+ const title = sessionLabelOf(state.byId.get(item.sessionId))
2979
3061
  if (kind === 'approval') return `<div class="ds-overview-attention-item" data-ds-overview-approval="${esc(item.approvalId)}">
2980
3062
  <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(desktopApprovalDetail(item))} · ${esc(title)}</span></span>
2981
3063
  <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>
@@ -3019,7 +3101,7 @@ function renderOverviewDesktop() {
3019
3101
  $('ds-overview-connection-mode').textContent = state.token ? t(state.streamMode === 'poll' ? 'ds.poll' : 'ds.liveWs') : '—'
3020
3102
  $('ds-overview-active-count').textContent = running ? t('ds.activeCount', { n: running }) : ''
3021
3103
  $('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)}">
3022
- <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(sessionSortTime(s)))}</span></span><span class="ds-overview-arrow">›</span>
3104
+ <span class="ds-overview-mark">${s.running ? '●' : '○'}</span><span class="ds-overview-copy"><span class="ds-overview-item-title">${esc(sessionLabelOf(s))}</span><span class="ds-overview-item-desc">${s.running ? esc(t('ds.running')) + ' · ' : ''}${esc(fmtTime(sessionSortTime(s)))}</span></span><span class="ds-overview-arrow">›</span>
3023
3105
  </button>`).join('') : `<div class="ds-overview-empty">${t('ds.noSessions')}</div>`
3024
3106
  $('ds-overview-session-list').querySelectorAll('[data-ds-overview-session]').forEach(btn => btn.addEventListener('click', () => openSession(btn.dataset.dsOverviewSession)))
3025
3107
  }
@@ -3128,17 +3210,7 @@ function updateConn() {
3128
3210
 
3129
3211
  /* ---------------- 初始化 ---------------- */
3130
3212
  function bindUi() {
3131
- $('btn-new-session').addEventListener('click', async () => {
3132
- let payload = {}
3133
- // 与移动端保持一致:新会话继承 DSH 当前工作目录;查询失败时兼容回退。
3134
- try {
3135
- const host = await rpc('host.describe', {}, 5000)
3136
- const cwd = typeof host?.cwd === 'string' ? host.cwd.trim() : ''
3137
- if (cwd) payload = { cwd }
3138
- } catch {}
3139
- const v = await safeRpc('session.create', payload, '')
3140
- if (v?.sessionId) { await refreshSessions(); openSession(v.sessionId) }
3141
- })
3213
+ $('btn-new-session').addEventListener('click', openNewSessionModal)
3142
3214
  $('btn-new-workspace').addEventListener('click', openWorkspaceModal)
3143
3215
  $('session-sort')?.addEventListener('change', (e) => {
3144
3216
  state.sessionSort = e.target.value === 'workspace' ? 'workspace' : 'time'
@@ -3261,6 +3333,10 @@ function bindUi() {
3261
3333
  $('workspace-create').addEventListener('click', createWorkspace)
3262
3334
  $('workspace-name').addEventListener('keydown', (e) => { if (e.key === 'Enter') createWorkspace() })
3263
3335
  $('modal-workspace').addEventListener('click', (e) => { if (e.target === $('modal-workspace')) closeWorkspaceModal() })
3336
+ $('new-session-workspace').addEventListener('change', () => renderNewSessionWorkspace())
3337
+ $('new-session-cancel').addEventListener('click', closeNewSessionModal)
3338
+ $('new-session-create').addEventListener('click', createSessionInWorkspace)
3339
+ $('modal-new-session').addEventListener('click', (e) => { if (e.target === $('modal-new-session')) closeNewSessionModal() })
3264
3340
  $('modal-notes').addEventListener('click', (e) => { if (e.target === $('modal-notes')) closeNotesModal() })
3265
3341
  // 反馈
3266
3342
  $('btn-feedback').addEventListener('click', (e) => { e.stopPropagation(); toggleFeedbackMenu() })
package/public/index.html CHANGED
@@ -982,7 +982,7 @@
982
982
  'groups.drawerOpen': '切换服务器组', 'groups.drawerClose': '关闭组列表', 'groups.drawerEyebrow': '快速切换', 'groups.drawerTitle': '切换服务器组', 'groups.drawerCurrent': '当前连接组',
983
983
  'groups.drawerManage': '管理服务器与组', 'groups.drawerManageDesc': '编辑、测速和调整组', 'groups.serverCount': '{count} 台服务器',
984
984
  'stream.error': '事件流错误:{msg}',
985
- 'session.loading': '加载中…', 'session.unknown': '未知会话', 'session.running': '运行中', 'session.interrupted': '已中断',
985
+ 'session.loading': '加载中…', 'session.unknown': '未知会话', 'session.untitled': '新会话', 'session.running': '运行中', 'session.interrupted': '已中断',
986
986
  'session.stop': '停止本轮', 'session.history': '对话', 'session.errorMsg': '会话出错:{msg}', 'session.recovering': '恢复会话中…', 'session.recoveryReady': '会话已恢复', 'session.recoveryFailed': '会话恢复失败', 'session.recoveryCached': '正在查看缓存历史',
987
987
  'session.confirmStop': '停止当前回合?排队中的消息不会被删除。', 'session.stopFailed': '停止失败', 'session.stopRequested': '已请求停止本轮', 'session.archive': '归档', 'session.archiveConfirm': '归档这个会话?归档后可在“显示已归档会话”中打开。', 'session.archiveFailed': '归档失败:{msg}', 'session.archived': '会话已归档', 'session.archiveTitle': '确认归档会话', 'session.archiveDesc': '归档后会话将从普通列表隐藏,但不会删除对话记录。', 'session.archiveConversation': '对话', 'session.archiveWorkspace': '工作区', 'session.archiveCancel': '取消', 'session.archiveConfirmAction': '确认归档', 'session.rename': '重命名会话', 'session.renameTitle': '重命名会话', 'session.renamePlaceholder': '输入新的会话名称', 'session.renameCancel': '取消', 'session.renameConfirm': '保存', 'session.renameEmpty': '会话名称不能为空', 'session.renameFailed': '重命名失败', 'session.renamed': '会话名称已更新',
988
988
  'notify.approvalTitle': '工具审批', 'notify.approvalBody': '{tool} 需要批准', 'notify.questionTitle': 'DSH 提问', 'notify.questionBody': '需要你回答',
@@ -1215,7 +1215,7 @@
1215
1215
  'groups.drawerOpen': 'Switch server group', 'groups.drawerClose': 'Close group list', 'groups.drawerEyebrow': 'QUICK SWITCH', 'groups.drawerTitle': 'Switch server group', 'groups.drawerCurrent': 'Current connection group',
1216
1216
  'groups.drawerManage': 'Manage servers and groups', 'groups.drawerManageDesc': 'Edit, test, and organize groups', 'groups.serverCount': '{count} servers',
1217
1217
  'stream.error': 'Event stream error: {msg}',
1218
- 'session.loading': 'Loading…', 'session.unknown': 'Unknown session', 'session.running': 'Running', 'session.interrupted': 'Interrupted',
1218
+ 'session.loading': 'Loading…', 'session.unknown': 'Unknown session', 'session.untitled': 'New session', 'session.running': 'Running', 'session.interrupted': 'Interrupted',
1219
1219
  'session.stop': 'Stop turn', 'session.history': 'Conversation', 'session.errorMsg': 'Session error: {msg}', 'session.recovering': 'Restoring session…', 'session.recoveryReady': 'Session ready', 'session.recoveryFailed': 'Session restore failed', 'session.recoveryCached': 'Viewing cached history',
1220
1220
  'session.confirmStop': 'Stop the current turn? Queued messages will be kept.', 'session.stopFailed': 'Stop failed', 'session.stopRequested': 'Stop requested', 'session.archive': 'Archive', 'session.archiveConfirm': 'Archive this session? You can open it from “Show archived”.', 'session.archiveFailed': 'Archive failed: {msg}', 'session.archived': 'Session archived', 'session.archiveTitle': 'Confirm archive', 'session.archiveDesc': 'The session will be hidden from the normal list, but its conversation will not be deleted.', 'session.archiveConversation': 'Conversation', 'session.archiveWorkspace': 'Workspace', 'session.archiveCancel': 'Cancel', 'session.archiveConfirmAction': 'Archive session', 'session.rename': 'Rename session', 'session.renameTitle': 'Rename session', 'session.renamePlaceholder': 'Enter a new session name', 'session.renameCancel': 'Cancel', 'session.renameConfirm': 'Save', 'session.renameEmpty': 'Session name cannot be empty', 'session.renameFailed': 'Rename failed', 'session.renamed': 'Session name updated',
1221
1221
  'notify.approvalTitle': 'Tool approval', 'notify.approvalBody': '{tool} needs approval', 'notify.questionTitle': 'DSH question', 'notify.questionBody': 'Needs your answer',
@@ -1,10 +1,14 @@
1
1
  {
2
- "version": "0.6.23",
2
+ "version": "0.6.24",
3
3
  "apkUrl": "dsh-remote.apk",
4
- "sha256": "947d138637edf0b300e9db3e024ac9b983f62e825847a24327bf7399edd56c01",
5
- "releasedAt": "2026-09-03T01:19:16.166Z",
6
- "notes": "0.6.23:加强 DSH 版本兼容。旧版点号 RPC 与新版 slash Remote API 可自动协商和回退,修复部分新版 DSH 中 host.describe、创建会话等请求返回 404 的问题;DSH 重启或升级后会重新识别协议并恢复实时通道。反馈时可由用户选择附带脱敏兼容性诊断,便于定位接口问题;诊断不包含令牌、Cookie、对话内容或文件路径。",
4
+ "sha256": "524f38fd3c1f067ab1ce8bad692649366ca9b775dcf16c99ee9e21058c422529",
5
+ "releasedAt": "2026-09-05T13:01:00.792Z",
6
+ "notes": "0.6.24:同步 App 与宽屏客户端的会话命名体验;未命名会话显示为“新会话”并保留短标识,创建会话时可选择指定工作区;补充空会话清理与相关回归验证。",
7
7
  "history": [
8
+ {
9
+ "version": "0.6.24",
10
+ "notes": "0.6.24:同步 App 与宽屏客户端的会话命名体验;未命名会话显示为“新会话”并保留短标识,创建会话时可选择指定工作区;补充空会话清理与相关回归验证。"
11
+ },
8
12
  {
9
13
  "version": "0.6.23",
10
14
  "notes": "0.6.23:加强 DSH 版本兼容。旧版点号 RPC 与新版 slash Remote API 可自动协商和回退,修复部分新版 DSH 中 host.describe、创建会话等请求返回 404 的问题;DSH 重启或升级后会重新识别协议并恢复实时通道。反馈时可由用户选择附带脱敏兼容性诊断,便于定位接口问题;诊断不包含令牌、Cookie、对话内容或文件路径。"
@@ -40,10 +44,6 @@
40
44
  {
41
45
  "version": "0.6.15",
42
46
  "notes": "0.6.15:修复拖拽候选项拦截普通竖向滑动的问题,卡片中间也可自然滚动;仅在长按真正进入拖拽后锁定页面,并补充指针结束、取消、失焦和页面隐藏时的解锁清理。保留边缘自动滚动、占位符同步重排、主列表/工作台工作区与组内对话拖动。"
43
- },
44
- {
45
- "version": "0.6.14",
46
- "notes": "0.6.14-rc.7:将 Android 后台轮询服务接入与前台 App 相同的 client ID,修复 Dalvik 后台请求被显示为第二台未知设备;保留旧记录的顺序无关迁移;补齐文件上传请求的持久设备 ID;实时扫码继续使用 Worker 解码并降低取样负载;修复非用户来源的 DSH user/message 被误显示为用户输入;新增子代理折叠、排队消息插话、运行中提示和按轮次稳定排序;Markdown 支持 GFM 表格。"
47
47
  }
48
48
  ]
49
49
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.6.23"
2
+ "version": "0.6.24"
3
3
  }