dsh-remote-plugin 0.6.13 → 0.6.15

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/public/index.html CHANGED
@@ -93,6 +93,8 @@
93
93
  <div id="session-title" class="session-title" data-i18n="session.loading">加载中…</div>
94
94
  <div id="session-sub" class="session-sub"></div>
95
95
  </div>
96
+ <button id="btn-rename-session" class="icon-btn hidden" data-i18n-title="session.rename" data-i18n-aria="session.rename">✎</button>
97
+ <button id="btn-archive-session" class="mini-btn hidden" data-i18n="session.archive">归档</button>
96
98
  <button id="btn-stats" class="icon-btn" data-i18n-title="stats.title" data-i18n-aria="stats.title"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 19V9M12 19V5M19 19v-7M3 19h18"/></svg></button>
97
99
  <button id="btn-fs-send" class="session-send-btn hidden" type="button" data-i18n="composer.send" data-i18n-title="composer.send" data-i18n-aria="composer.send">发送</button>
98
100
  <button id="btn-cancel" class="danger-btn hidden" data-i18n="session.stop">停止</button>
@@ -100,6 +102,8 @@
100
102
 
101
103
  <div id="session-cards" class="cards"></div>
102
104
 
105
+ <div id="queue-dock" class="queue-dock hidden" aria-live="polite"></div>
106
+
103
107
  <div class="section-head small">
104
108
  <span data-i18n="session.history">对话</span>
105
109
  <span id="history-hint" class="muted"></span>
@@ -114,6 +118,7 @@
114
118
  </div>
115
119
 
116
120
  <div id="composer-wrap" class="composer-wrap">
121
+ <div id="composer-status" class="composer-status hidden" role="status" aria-live="polite"><span class="composer-status-dot" aria-hidden="true"></span><span data-i18n="composer.running">运行中…</span></div>
117
122
  <div id="composer-menu" class="composer-menu hidden">
118
123
  <div class="menu-group">
119
124
  <div class="menu-title" data-i18n="menu.commandsTitle">输入指令</div>
@@ -155,8 +160,7 @@
155
160
  <div id="composer-input-wrap" class="composer-input-wrap">
156
161
  <textarea id="composer-input" rows="1" data-i18n-placeholder="composer.placeholder" placeholder="给 DSH 发消息…"></textarea>
157
162
  <button id="btn-fs-toggle" class="composer-fs-btn hidden" type="button" data-i18n-title="composer.fullscreen" data-i18n-aria="composer.fullscreen">
158
- <svg id="fs-ico-expand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M8 3H3v5M16 3h5v5M8 21H3v-5M21 16v5h-5"/></svg>
159
- <svg id="fs-ico-collapse" class="hidden" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M9 3v6H3M15 3v6h6M9 21v-6H3M21 15h-6v6"/></svg>
163
+ <morph-icon id="fs-ico" data-morph-state="closed" data-morph-closed="M8 3H3v5M16 3h5v5M8 21H3v-5M21 16v5h-5" data-morph-open="M9 3v6H3M15 3v6h6M9 21v-6H3M21 15h-6v6" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M8 3H3v5M16 3h5v5M8 21H3v-5M21 16v5h-5"/></svg></morph-icon>
160
164
  </button>
161
165
  </div>
162
166
  <button id="btn-image" class="composer-image-btn" type="button" data-i18n-title="composer.addImage" data-i18n-aria="composer.addImage"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="4" width="17" height="16" rx="2"/><circle cx="8.5" cy="9" r="1.2"/><path d="m5 17 4.5-4.5 3 3 2-2L19 17"/></svg></button>
@@ -346,9 +350,9 @@
346
350
  <button id="btn-server-add" class="mini-btn" data-i18n="settings.add">添加</button>
347
351
  </div>
348
352
  <div class="setting-row">
349
- <div><div class="setting-name" data-i18n="settings.scanTitle">扫码连接</div><div class="setting-desc" data-i18n="settings.scanDesc">拍照/相册本地解码(无谷歌服务依赖);也可系统相机扫码或手动粘贴</div></div>
353
+ <div><div class="setting-name" data-i18n="settings.scanTitle">扫码连接</div><div class="setting-desc" data-i18n="settings.scanDesc">实时相机识别二维码,也可拍照/相册本地解码;无需谷歌服务</div></div>
350
354
  <div class="setting-actions">
351
- <button id="btn-scan-camera" class="mini-btn" data-i18n="scan.promptPhoto">拍照扫描</button>
355
+ <button id="btn-scan-camera" class="mini-btn" data-i18n="scan.promptLive">实时扫码</button>
352
356
  <button id="btn-scan-gallery" class="mini-btn" data-i18n="scan.promptGallery">从相册选择</button>
353
357
  </div>
354
358
  </div>
@@ -650,6 +654,19 @@
650
654
  </div>
651
655
  </div>
652
656
 
657
+ <div id="modal-rename" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="rename-session-title">
658
+ <div class="modal-card">
659
+ <div id="rename-session-title" class="modal-title" data-i18n="session.renameTitle">重命名会话</div>
660
+ <div class="modal-body">
661
+ <input id="rename-session-input" class="text-input" maxlength="160" autocomplete="off" data-i18n-placeholder="session.renamePlaceholder" placeholder="输入新的会话名称">
662
+ </div>
663
+ <div class="modal-actions">
664
+ <button id="rename-cancel" class="btn subtle" data-i18n="session.renameCancel">取消</button>
665
+ <button id="rename-confirm" class="btn primary" data-i18n="session.renameConfirm">保存</button>
666
+ </div>
667
+ </div>
668
+ </div>
669
+
653
670
  <!-- goal 模态 -->
654
671
  <div id="modal-goal" class="modal hidden" role="dialog" aria-modal="true">
655
672
  <div class="modal-card">
@@ -717,6 +734,21 @@
717
734
  </div>
718
735
  </div>
719
736
 
737
+ <!-- 实时扫码模态 -->
738
+ <div id="modal-scan-live" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="scan-live-title">
739
+ <div class="modal-card scan-live-card">
740
+ <div id="scan-live-title" class="modal-title" data-i18n="scan.liveTitle">实时扫码连接</div>
741
+ <div class="scan-live-preview">
742
+ <video id="scan-live-video" autoplay playsinline muted></video>
743
+ <div class="scan-live-frame" aria-hidden="true"><span></span></div>
744
+ </div>
745
+ <div id="scan-live-status" class="scan-live-status" data-i18n="scan.liveHint">将二维码放入框内,识别后自动连接</div>
746
+ <div class="modal-actions">
747
+ <button id="scan-live-cancel" class="btn subtle" data-i18n="scan.liveCancel">取消</button>
748
+ </div>
749
+ </div>
750
+ </div>
751
+
720
752
  <!-- 更新内容模态 -->
721
753
  <div id="modal-notes" class="modal hidden" role="dialog" aria-modal="true">
722
754
  <div class="modal-card">
@@ -794,7 +826,7 @@
794
826
  'statsPage.gatewayDown': '统计需要网关运行', 'statsPage.empty': '暂无统计,产生会话后自动聚合',
795
827
  'statsPage.note': '注:本数据仅在使用 DeepSeek 官方 API 时估算;基于 token 计算,与官网账单可能有出入,一切以官网为准。统计自 2026-08-17 定价生效日起。',
796
828
  'home.newSession': '+ 新会话', 'home.newWorkspace': '+ 工作区', 'home.empty': '暂无会话', 'home.createFailed': '新建会话失败', 'home.created': '会话已创建',
797
- 'wb.unbound': '未绑定工作台', 'wb.bound': '绑定 {title}', 'wb.noProjects': '暂无项目', 'wb.noSessions': '暂无会话', 'wb.newSession': '新会话', 'wb.flatHidden': '工作台会话已收起({n} 个)', 'wb.archivedHidden': '------隐藏已归档会话------', 'wb.archivedShown': '------显示已归档会话------',
829
+ 'wb.unbound': '未绑定工作台', 'wb.bound': '绑定 {title}', 'wb.noProjects': '暂无项目', 'wb.noSessions': '暂无会话', 'wb.newSession': '新会话', 'wb.orderSaved': '顺序已保存到当前设备', 'wb.flatHidden': '工作台会话已收起({n} 个)', 'wb.archivedHidden': '------隐藏已归档会话------', 'wb.archivedShown': '------显示已归档会话------',
798
830
  'workspace.createTitle': '新建工作区', 'workspace.createDesc': '将在当前文件目录下创建文件夹,并自动打开新会话。', 'workspace.parent': '父目录', 'workspace.namePlaceholder': '例如:my-project', 'workspace.cancel': '取消', 'workspace.create': '创建并打开', 'workspace.nameRequired': '请输入工作区名称', 'workspace.exists': '该目录已存在', 'workspace.invalidName': '名称不能包含路径分隔符', 'workspace.createFailed': '创建工作区失败:{msg}', 'workspace.created': '工作区已创建', 'workspace.createdNoSession': '工作区已创建,但新会话未能打开',
799
831
  'workspace.createTitle': '新建工作区', 'workspace.createDesc': '将在当前文件目录下创建文件夹,并自动打开新会话。', 'workspace.parent': '父目录', 'workspace.namePlaceholder': '例如:my-project', 'workspace.cancel': '取消', 'workspace.create': '创建并打开', 'workspace.nameRequired': '请输入工作区名称', 'workspace.created': '工作区已创建', 'workspace.sessionFailed': '目录已创建,但新会话打开失败:{msg}',
800
832
  'workspace.current': '当前工作区', 'workspace.select': '选择工作区', 'workspace.all': '全部工作区', 'workspace.ungrouped': '未分组会话', 'workspace.files': '查看文件', 'workspace.allPath': '显示全部 DSH 工作区中的会话',
@@ -831,8 +863,8 @@
831
863
  'groups.confirmDelete': '删除组「{group}」?组内服务器将归入默认组。', 'groups.deleted': '组已删除',
832
864
  'stream.error': '事件流错误:{msg}',
833
865
  'session.loading': '加载中…', 'session.unknown': '未知会话', 'session.running': '运行中', 'session.interrupted': '已中断',
834
- 'session.stop': '停止', 'session.history': '对话', 'session.errorMsg': '会话出错:{msg}',
835
- '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': '确认归档',
866
+ 'session.stop': '停止本轮', 'session.history': '对话', 'session.errorMsg': '会话出错:{msg}', 'session.recovering': '恢复会话中…', 'session.recoveryReady': '会话已恢复', 'session.recoveryFailed': '会话恢复失败', 'session.recoveryCached': '正在查看缓存历史',
867
+ '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': '会话名称已更新',
836
868
  'notify.approvalTitle': '工具审批', 'notify.approvalBody': '{tool} 需要批准', 'notify.questionTitle': 'DSH 提问', 'notify.questionBody': '需要你回答',
837
869
  'notify.permissionFailed': '通知权限申请失败:{msg}',
838
870
  'history.loading': '加载历史…', 'history.cacheFallback': '网络不可用:显示本地缓存的历史', 'history.loadFailed': '加载历史失败:{msg}', 'history.retry': '重试',
@@ -852,8 +884,9 @@
852
884
  'goal.actionFailed': '目标操作失败', 'goal.actionSubmitted': '目标操作已提交',
853
885
  'goal.cannotEmpty': '目标不能为空', 'goal.updateFailed': '更新失败', 'goal.updated': '目标已更新',
854
886
  'todos.title': '任务清单',
855
- 'subagent.title': '子代理', 'subagent.diagnostic': '诊断项', 'subagent.running': '· 运行中', 'subagent.interrupt': '中断',
887
+ 'subagent.title': '子代理', 'subagent.count': '子代理 · {n} 个', 'subagent.expand': '展开子代理', 'subagent.collapse': '收起子代理', 'subagent.diagnostic': '诊断项', 'subagent.running': '· 运行中', 'subagent.interrupt': '中断',
856
888
  'subagent.confirmInterrupt': '中断这个子代理当前回合?', 'subagent.interruptFailed': '中断失败', 'subagent.interruptSubmitted': '中断请求已提交',
889
+ 'queue.title': '排队消息', 'queue.steer': '插话', 'queue.steerUnavailable': '仅运行中可插话', 'queue.steerSubmitted': '插话请求已提交', 'queue.steerFailed': '插话失败:{msg}', 'queue.image': '图片消息',
857
890
  'send.failed': '发送失败', 'send.commandSent': '指令已发送', 'send.sent': '已发送', 'send.imageSent': '图片已发送', 'send.commandExecuted': '命令已执行',
858
891
  'models.loading': '模型加载中…', 'models.loadFailed': '模型列表加载失败:{msg}', 'models.unavailable': '不可用', 'models.none': '没有可用模型',
859
892
  'models.switchFailed': '切换模型失败', 'models.switched': '已切换模型:{model}',
@@ -864,7 +897,7 @@
864
897
  'menu.permission': '/permission 切换权限预设', 'menu.plan': '/plan 进入/退出计划模式',
865
898
  'perm.readOnly': 'read-only', 'perm.workspaceWrite': 'workspace-write', 'perm.dangerFullAccess': 'danger-full-access',
866
899
  'menu.modelTitle': '模型切换', 'menu.refresh': '刷新', 'menu.modelAuto': '点开时自动加载', 'menu.effortTitle': '思考深度',
867
- 'composer.placeholder': '给 DSH 发消息…', 'composer.send': '发送', 'composer.fullscreen': '全屏编辑', 'composer.exitFullscreen': '退出全屏编辑', 'composer.addImage': '添加图片', 'composer.takePhoto': '拍照', 'composer.choosePhoto': '相册', 'composer.imageAdded': '已添加图片', 'composer.imageRemoved': '已移除图片', 'composer.removeImage': '移除图片', 'composer.imageUnsupported': '仅支持 PNG、JPEG、WebP 或 GIF 图片', 'composer.imageTooLarge': '单张图片不能超过 {size}', 'composer.imageLimit': '最多添加 {count} 张图片', 'composer.imageSlashUnsupported': '带图片时不能发送斜杠命令', 'composer.imageReadFailed': '图片读取失败:{msg}',
900
+ 'composer.placeholder': '给 DSH 发消息…', 'composer.running': '运行中…', 'composer.send': '发送', 'composer.fullscreen': '全屏编辑', 'composer.exitFullscreen': '退出全屏编辑', 'composer.addImage': '添加图片', 'composer.takePhoto': '拍照', 'composer.choosePhoto': '相册', 'composer.imageAdded': '已添加图片', 'composer.imageRemoved': '已移除图片', 'composer.removeImage': '移除图片', 'composer.imageUnsupported': '仅支持 PNG、JPEG、WebP 或 GIF 图片', 'composer.imageTooLarge': '单张图片不能超过 {size}', 'composer.imageLimit': '最多添加 {count} 张图片', 'composer.imageSlashUnsupported': '带图片时不能发送斜杠命令', 'composer.imageReadFailed': '图片读取失败:{msg}',
868
901
  'pending.title': '待处理', 'pending.count': '{n} 项', 'pending.approvalTitle': '🔧 {tool} 请求批准', 'pending.noReason': '无说明',
869
902
  'pending.allow': '允许', 'pending.reject': '拒绝', 'pending.question': '❓ {question}',
870
903
  'pending.questionCount': '共 {n} 个问题', 'pending.answer': '去回答', 'pending.empty': '暂无待处理事项',
@@ -903,10 +936,10 @@
903
936
  'update.appVersion': 'App 版本', 'update.gatewayVersion': '网关版本', 'update.gatewayAheadHint': '建议同步更新 App,以保持与网关功能一致。',
904
937
  'update.remindLater': '稍后提醒', 'update.checkNow': '立即检查更新',
905
938
  'notes.title': '更新内容', 'notes.close': '关闭', 'notes.page': '版本号 {current}/{total}',
906
- 'scan.imageLoadFailed': '图片加载失败', 'scan.decodeUnsupported': '当前设备不支持图片解码',
939
+ 'scan.imageLoadFailed': '图片加载失败', 'scan.decodeUnsupported': '当前设备不支持图片解码', 'scan.liveTitle': '实时扫码连接', 'scan.liveHint': '将二维码放入框内,识别后自动连接', 'scan.liveStarting': '正在打开摄像头…', 'scan.liveCancel': '取消',
907
940
  'scan.browserHint': '浏览器请打开主机管理页,用手机相机扫码', 'scan.unsupported': '当前 App 版本不支持拍照扫码,请先更新 App',
908
941
  'scan.permissionDenied': '未授予相机权限',
909
- 'scan.promptHeader': '扫描 DSH Remote 配对二维码', 'scan.promptPhoto': '拍照扫描', 'scan.promptGallery': '从相册选择',
942
+ 'scan.promptHeader': '扫描 DSH Remote 配对二维码', 'scan.promptLive': '实时扫码', 'scan.promptPhoto': '拍照扫描', 'scan.promptGallery': '从相册选择',
910
943
  'scan.noPhoto': '没有拿到图片,请重试', 'scan.noQr': '未识别到二维码,请对准后重试',
911
944
  'scan.paired': '配对成功,正在连接', 'scan.pairedLink': '已通过二维码配对', 'scan.notPair': '这不是 DSH Remote 的配对二维码',
912
945
  'scan.cancelled': '已取消扫码', 'scan.failed': '扫码失败:{msg}',
@@ -918,7 +951,7 @@
918
951
  'settings.donate': '赞赏支持', 'settings.donateDesc': '如果 dsh-remote 帮到了你,欢迎赞赏支持开发 ☕', 'settings.donateThanks': '感谢你的支持 ☕',
919
952
  'settings.back': '‹ 返回',
920
953
  'settings.servers': '服务器地址(可多个)', 'settings.speedTest': '测速', 'settings.add': '添加',
921
- 'settings.scanTitle': '扫码连接', 'settings.scanDesc': '拍照/相册本地解码(无谷歌服务依赖);也可系统相机扫码或手动粘贴', 'settings.scan': '扫码',
954
+ 'settings.scanTitle': '扫码连接', 'settings.scanDesc': '实时相机识别二维码,也可拍照/相册本地解码;无需谷歌服务', 'settings.scan': '扫码',
922
955
  'settings.notifyTitle': '通知', 'settings.notifyDesc': '收到审批/提问时推送', 'settings.announcementHistoryTitle': '历史公告', 'settings.announcementHistoryDesc': '查看以前收到的公告', 'settings.announcementHistory': '查看记录',
923
956
  'settings.testNotifyTitle': '测试通知', 'settings.testNotifyDesc': '立即排一条 3 秒后的本地通知,用于排查系统通知限制', 'settings.testNotify': '发送测试通知',
924
957
  'settings.testNotifySent': '测试通知已排定,3 秒后到达', 'settings.testNotifyFailed': '发送测试通知失败:{msg}', 'settings.testNotifyUnavailable': '测试通知仅在 App 内可用',
@@ -1005,7 +1038,7 @@
1005
1038
  'statsPage.gatewayDown': 'Stats require the gateway', 'statsPage.empty': 'No stats yet — they aggregate as sessions happen',
1006
1039
  'statsPage.note': 'Note: estimates assume the official DeepSeek API. Token-based calculation may differ from the official bill; always defer to deepseek.com. Stats start from the 2026-08-17 pricing date.',
1007
1040
  'home.newSession': '+ New session', 'home.newWorkspace': '+ Workspace', 'home.empty': 'No sessions yet', 'home.createFailed': 'Failed to create session', 'home.created': 'Session created',
1008
- 'wb.unbound': 'No workbench bound', 'wb.bound': 'Bound {title}', 'wb.noProjects': 'No projects', 'wb.noSessions': 'No sessions', 'wb.newSession': 'New session', 'wb.flatHidden': 'Workbench sessions folded ({n})', 'wb.archivedHidden': '------ Hide archived ------', 'wb.archivedShown': '------ Show archived ------',
1041
+ 'wb.unbound': 'No workbench bound', 'wb.bound': 'Bound {title}', 'wb.noProjects': 'No projects', 'wb.noSessions': 'No sessions', 'wb.newSession': 'New session', 'wb.orderSaved': 'Order saved on this device', 'wb.flatHidden': 'Workbench sessions folded ({n})', 'wb.archivedHidden': '------ Hide archived ------', 'wb.archivedShown': '------ Show archived ------',
1009
1042
  'workspace.createTitle': 'New workspace', 'workspace.createDesc': 'Create a folder in the current file directory and open a new session there.', 'workspace.parent': 'Parent folder', 'workspace.namePlaceholder': 'For example: my-project', 'workspace.cancel': 'Cancel', 'workspace.create': 'Create & open', 'workspace.nameRequired': 'Enter a workspace name', 'workspace.exists': 'That folder already exists', 'workspace.invalidName': 'The name cannot contain path separators', 'workspace.createFailed': 'Could not create workspace: {msg}', 'workspace.created': 'Workspace created', 'workspace.createdNoSession': 'Workspace created, but the new session could not be opened',
1010
1043
  'workspace.createTitle': 'New workspace', 'workspace.createDesc': 'Create a folder in the current file directory and open a new session there.', 'workspace.parent': 'Parent folder', 'workspace.namePlaceholder': 'For example: my-project', 'workspace.cancel': 'Cancel', 'workspace.create': 'Create & open', 'workspace.nameRequired': 'Enter a workspace name', 'workspace.created': 'Workspace created', 'workspace.sessionFailed': 'Folder created, but the new session could not be opened: {msg}',
1011
1044
  'workspace.current': 'Current workspace', 'workspace.select': 'Select workspace', 'workspace.all': 'All workspaces', 'workspace.ungrouped': 'Ungrouped sessions', 'workspace.files': 'View files', 'workspace.allPath': 'Showing sessions from every DSH workspace',
@@ -1042,8 +1075,8 @@
1042
1075
  'groups.confirmDelete': 'Delete group "{group}"? Its servers move to the default group.', 'groups.deleted': 'Group deleted',
1043
1076
  'stream.error': 'Event stream error: {msg}',
1044
1077
  'session.loading': 'Loading…', 'session.unknown': 'Unknown session', 'session.running': 'Running', 'session.interrupted': 'Interrupted',
1045
- 'session.stop': 'Stop', 'session.history': 'Conversation', 'session.errorMsg': 'Session error: {msg}',
1046
- 'session.confirmStop': 'Stop the currently running task in this session?', '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',
1078
+ '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',
1079
+ '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',
1047
1080
  'notify.approvalTitle': 'Tool approval', 'notify.approvalBody': '{tool} needs approval', 'notify.questionTitle': 'DSH question', 'notify.questionBody': 'Needs your answer',
1048
1081
  'notify.permissionFailed': 'Notification permission failed: {msg}',
1049
1082
  'history.loading': 'Loading history…', 'history.cacheFallback': 'Network unavailable: showing cached history', 'history.loadFailed': 'Failed to load history: {msg}', 'history.retry': 'Retry',
@@ -1063,8 +1096,9 @@
1063
1096
  'goal.actionFailed': 'Goal action failed', 'goal.actionSubmitted': 'Goal action submitted',
1064
1097
  'goal.cannotEmpty': 'Goal cannot be empty', 'goal.updateFailed': 'Update failed', 'goal.updated': 'Goal updated',
1065
1098
  'todos.title': 'Todos',
1066
- 'subagent.title': 'Subagents', 'subagent.diagnostic': 'Diagnostic', 'subagent.running': '· running', 'subagent.interrupt': 'Interrupt',
1099
+ 'subagent.title': 'Subagents', 'subagent.count': 'Subagents · {n}', 'subagent.expand': 'Expand subagents', 'subagent.collapse': 'Collapse subagents', 'subagent.diagnostic': 'Diagnostic', 'subagent.running': '· running', 'subagent.interrupt': 'Interrupt',
1067
1100
  'subagent.confirmInterrupt': 'Interrupt this subagent\'s current turn?', 'subagent.interruptFailed': 'Interrupt failed', 'subagent.interruptSubmitted': 'Interrupt requested',
1101
+ 'queue.title': 'Queued messages', 'queue.steer': 'Interject', 'queue.steerUnavailable': 'Only available while running', 'queue.steerSubmitted': 'Interjection requested', 'queue.steerFailed': 'Interjection failed: {msg}', 'queue.image': 'Image message',
1068
1102
  'send.failed': 'Send failed', 'send.commandSent': 'Command sent', 'send.sent': 'Sent', 'send.imageSent': 'Image sent', 'send.commandExecuted': 'Command executed',
1069
1103
  'models.loading': 'Loading models…', 'models.loadFailed': 'Failed to load models: {msg}', 'models.unavailable': 'unavailable', 'models.none': 'No models available',
1070
1104
  'models.switchFailed': 'Model switch failed', 'models.switched': 'Switched model: {model}',
@@ -1075,7 +1109,7 @@
1075
1109
  'menu.permission': '/permission Switch permission preset', 'menu.plan': '/plan Enter/exit plan mode',
1076
1110
  'perm.readOnly': 'read-only', 'perm.workspaceWrite': 'workspace-write', 'perm.dangerFullAccess': 'danger-full-access',
1077
1111
  'menu.modelTitle': 'Model', 'menu.refresh': 'Refresh', 'menu.modelAuto': 'Auto-loads when opened', 'menu.effortTitle': 'Reasoning effort',
1078
- 'composer.placeholder': 'Message DSH…', 'composer.send': 'Send', 'composer.fullscreen': 'Fullscreen editor', 'composer.exitFullscreen': 'Exit fullscreen editor', 'composer.addImage': 'Add image', 'composer.takePhoto': 'Camera', 'composer.choosePhoto': 'Gallery', 'composer.imageAdded': 'Image added', 'composer.imageRemoved': 'Image removed', 'composer.removeImage': 'Remove image', 'composer.imageUnsupported': 'Only PNG, JPEG, WebP or GIF images are supported', 'composer.imageTooLarge': 'Each image must be under {size}', 'composer.imageLimit': 'You can add up to {count} images', 'composer.imageSlashUnsupported': 'Slash commands cannot include images', 'composer.imageReadFailed': 'Could not read image: {msg}',
1112
+ 'composer.placeholder': 'Message DSH…', 'composer.running': 'Running…', 'composer.send': 'Send', 'composer.fullscreen': 'Fullscreen editor', 'composer.exitFullscreen': 'Exit fullscreen editor', 'composer.addImage': 'Add image', 'composer.takePhoto': 'Camera', 'composer.choosePhoto': 'Gallery', 'composer.imageAdded': 'Image added', 'composer.imageRemoved': 'Image removed', 'composer.removeImage': 'Remove image', 'composer.imageUnsupported': 'Only PNG, JPEG, WebP or GIF images are supported', 'composer.imageTooLarge': 'Each image must be under {size}', 'composer.imageLimit': 'You can add up to {count} images', 'composer.imageSlashUnsupported': 'Slash commands cannot include images', 'composer.imageReadFailed': 'Could not read image: {msg}',
1079
1113
  'pending.title': 'Inbox', 'pending.count': '{n} items', 'pending.approvalTitle': '🔧 {tool} requests approval', 'pending.noReason': 'No reason given',
1080
1114
  'pending.allow': 'Allow', 'pending.reject': 'Reject', 'pending.question': '❓ {question}',
1081
1115
  'pending.questionCount': '{n} questions', 'pending.answer': 'Answer', 'pending.empty': 'Nothing pending',
@@ -1114,10 +1148,10 @@
1114
1148
  'update.appVersion': 'App version', 'update.gatewayVersion': 'Gateway version', 'update.gatewayAheadHint': 'Update the App to keep its features aligned with the gateway.',
1115
1149
  'update.remindLater': 'Remind me later', 'update.checkNow': 'Check for updates',
1116
1150
  'notes.title': 'What\'s New', 'notes.close': 'Close', 'notes.page': 'Version {current}/{total}',
1117
- 'scan.imageLoadFailed': 'Image failed to load', 'scan.decodeUnsupported': 'This device cannot decode images',
1151
+ 'scan.imageLoadFailed': 'Image failed to load', 'scan.decodeUnsupported': 'This device cannot decode images', 'scan.liveTitle': 'Live QR scanner', 'scan.liveHint': 'Place the QR code inside the frame to connect automatically', 'scan.liveStarting': 'Opening camera…', 'scan.liveCancel': 'Cancel',
1118
1152
  'scan.browserHint': 'In a browser, open the host admin page and scan with your phone camera', 'scan.unsupported': 'This app version cannot scan, please update first',
1119
1153
  'scan.permissionDenied': 'Camera permission not granted',
1120
- 'scan.promptHeader': 'Scan DSH Remote pairing QR', 'scan.promptPhoto': 'Take photo', 'scan.promptGallery': 'Choose from gallery',
1154
+ 'scan.promptHeader': 'Scan DSH Remote pairing QR', 'scan.promptLive': 'Live scan', 'scan.promptPhoto': 'Take photo', 'scan.promptGallery': 'Choose from gallery',
1121
1155
  'scan.noPhoto': 'No image received, try again', 'scan.noQr': 'No QR code detected, aim and try again',
1122
1156
  'scan.paired': 'Paired, connecting', 'scan.pairedLink': 'Paired via QR code', 'scan.notPair': 'Not a DSH Remote pairing QR code',
1123
1157
  'scan.cancelled': 'Scan cancelled', 'scan.failed': 'Scan failed: {msg}',
@@ -1129,7 +1163,7 @@
1129
1163
  'settings.donate': 'Support', 'settings.donateDesc': 'If dsh-remote has been helpful to you, feel free to support development with a coffee ☕', 'settings.donateThanks': 'Thanks for your support ☕',
1130
1164
  'settings.back': '‹ Back',
1131
1165
  'settings.servers': 'Servers (one or more)', 'settings.speedTest': 'Test', 'settings.add': 'Add',
1132
- 'settings.scanTitle': 'Scan to pair', 'settings.scanDesc': 'Photo/gallery QR decoding on device (no Google services); system camera or manual paste also work', 'settings.scan': 'Scan',
1166
+ 'settings.scanTitle': 'Scan to pair', 'settings.scanDesc': 'Use the live camera or photo/gallery QR decoding; no Google services required', 'settings.scan': 'Scan',
1133
1167
  'settings.notifyTitle': 'Notifications', 'settings.notifyDesc': 'Push on approvals / questions', 'settings.announcementHistoryTitle': 'Announcement history', 'settings.announcementHistoryDesc': 'Review announcements you received before', 'settings.announcementHistory': 'View history',
1134
1168
  'settings.testNotifyTitle': 'Test notification', 'settings.testNotifyDesc': 'Schedule a local notification in 3 seconds to verify the notification pipeline', 'settings.testNotify': 'Send test notification',
1135
1169
  'settings.testNotifySent': 'Test notification scheduled, arriving in 3s', 'settings.testNotifyFailed': 'Failed to send test notification: {msg}', 'settings.testNotifyUnavailable': 'Test notification is only available in the app',
@@ -1199,6 +1233,9 @@
1199
1233
  <script src="i18n.js"></script>
1200
1234
  <script src="theme.js"></script>
1201
1235
  <script src="md.js"></script>
1236
+ <script src="vendor/gsap/gsap.min.js"></script>
1237
+ <script src="motion.js"></script>
1238
+ <script type="module" src="morphicons-init.js"></script>
1202
1239
  <script src="app.js"></script>
1203
1240
  </body>
1204
1241
  </html>
package/public/md.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * 用法: mdToHtml(text) -> HTML 字符串
3
3
  * 安全: 先 HTML 转义再转标记; 链接仅允许 http/https, 其余保留为纯文本。
4
4
  * 支持: 代码块 / 行内代码 / #~### 标题 / **粗体** / *斜体* / - 无序列表 /
5
- * 1. 有序列表 / > 引用 / [text](url) 链接 / 换行。
5
+ * 1. 有序列表 / > 引用 / GFM 表格 / [text](url) 链接 / 换行。
6
6
  * 同时支持浏览器全局 window.mdToHtml 与 Node CommonJS module.exports。
7
7
  */
8
8
  (function (root, factory) {
@@ -35,6 +35,70 @@
35
35
  return s
36
36
  }
37
37
 
38
+ function tableCells(line) {
39
+ var s = String(line || '').trim()
40
+ if (s.charAt(0) === '|') s = s.slice(1)
41
+ if (s.charAt(s.length - 1) === '|') s = s.slice(0, -1)
42
+ var cells = []
43
+ var cell = ''
44
+ var escaped = false
45
+ for (var i = 0; i < s.length; i++) {
46
+ var c = s.charAt(i)
47
+ if (escaped) { cell += c; escaped = false; continue }
48
+ if (c === '\\') { escaped = true; continue }
49
+ if (c === '|') { cells.push(cell.trim()); cell = ''; continue }
50
+ cell += c
51
+ }
52
+ if (escaped) cell += '\\'
53
+ cells.push(cell.trim())
54
+ return cells
55
+ }
56
+
57
+ function tableAlignment(cell) {
58
+ var s = String(cell || '').trim()
59
+ if (!/^:?-{3,}:?$/.test(s)) return null
60
+ if (s.charAt(0) === ':' && s.charAt(s.length - 1) === ':') return 'center'
61
+ if (s.charAt(0) === ':') return 'left'
62
+ if (s.charAt(s.length - 1) === ':') return 'right'
63
+ return null
64
+ }
65
+
66
+ function tableAt(lines, start) {
67
+ if (start + 1 >= lines.length || !/\|/.test(lines[start]) || !/\|/.test(lines[start + 1])) return null
68
+ var headers = tableCells(lines[start])
69
+ var separators = tableCells(lines[start + 1])
70
+ if (headers.length < 2 || separators.length !== headers.length) return null
71
+ if (!separators.every(function (cell) { return /^:?-{3,}:?$/.test(String(cell || '').trim()) })) return null
72
+ var alignments = separators.map(tableAlignment)
73
+ var rows = []
74
+ var i = start + 2
75
+ while (i < lines.length && lines[i].trim() && /\|/.test(lines[i])) {
76
+ var cells = tableCells(lines[i])
77
+ if (cells.length !== headers.length) break
78
+ rows.push(cells)
79
+ i++
80
+ }
81
+ return { headers: headers, alignments: alignments, rows: rows, next: i }
82
+ }
83
+
84
+ function tableHtml(table) {
85
+ function cellTag(tag, value, align) {
86
+ var style = align ? ' style="text-align:' + align + '"' : ''
87
+ return '<' + tag + style + '>' + inline(value) + '</' + tag + '>'
88
+ }
89
+ var head = '<thead><tr>' + table.headers.map(function (value, i) {
90
+ return cellTag('th', value, table.alignments[i])
91
+ }).join('') + '</tr></thead>'
92
+ var body = table.rows.length
93
+ ? '<tbody>' + table.rows.map(function (row) {
94
+ return '<tr>' + row.map(function (value, i) {
95
+ return cellTag('td', value, table.alignments[i])
96
+ }).join('') + '</tr>'
97
+ }).join('') + '</tbody>'
98
+ : ''
99
+ return '<div class="md-table-wrap"><table>' + head + body + '</table></div>'
100
+ }
101
+
38
102
  function renderLines(lines) {
39
103
  var html = ''
40
104
  var i = 0
@@ -49,6 +113,12 @@
49
113
  i++
50
114
  continue
51
115
  }
116
+ var table = tableAt(lines, i)
117
+ if (table) {
118
+ html += tableHtml(table)
119
+ i = table.next
120
+ continue
121
+ }
52
122
  var q = line.match(/^&gt;\s?(.*)$/)
53
123
  if (q) {
54
124
  html += '<blockquote>' + inline(q[1]) + '</blockquote>'
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Local morphicons bridge for the zero-build WebUI.
3
+ *
4
+ * The vendored element keeps a static SVG child as its first-paint fallback.
5
+ * UI code only changes data-morph-state; this bridge turns that state change
6
+ * into a morph without coupling the application code to the vendor API.
7
+ */
8
+ import { defineMorphIcon } from './vendor/morphicons/element.js'
9
+
10
+ defineMorphIcon()
11
+
12
+ function syncMorphIcon(element) {
13
+ if (!element || !('icon' in element)) return
14
+ const state = element.dataset.morphState === 'open' ? 'open' : 'closed'
15
+ const next = state === 'open' ? element.dataset.morphOpen : element.dataset.morphClosed
16
+ if (!next || element.icon === next) return
17
+ element.icon = next
18
+ }
19
+
20
+ function syncMorphIcons(root = document) {
21
+ if (root.matches?.('morph-icon[data-morph-closed][data-morph-open]')) syncMorphIcon(root)
22
+ root.querySelectorAll?.('morph-icon[data-morph-closed][data-morph-open]').forEach(syncMorphIcon)
23
+ }
24
+
25
+ function bootMorphIcons() {
26
+ syncMorphIcons()
27
+ const observer = new MutationObserver(records => {
28
+ for (const record of records) syncMorphIcon(record.target)
29
+ })
30
+ observer.observe(document.documentElement, {
31
+ subtree: true,
32
+ attributes: true,
33
+ attributeFilter: ['data-morph-state']
34
+ })
35
+ }
36
+
37
+ if (document.readyState === 'loading') {
38
+ document.addEventListener('DOMContentLoaded', bootMorphIcons, { once: true })
39
+ } else {
40
+ bootMorphIcons()
41
+ }