dsh-recall-plugin 1.4.0 → 1.5.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,37 @@
2
2
 
3
3
  本文件格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
4
4
 
5
+ ## [1.5.1] - 2026-08-18
6
+
7
+ ### 修复
8
+
9
+ - 设置页冷启动优化:`exclude-get` 首次遍历工作区、逐文件 shell 读改为并行 + 30s 结果缓存(保存后失效),二次打开设置页不再重复付出首访代价。
10
+ - 冷会话标题/消息文本补齐引入通用并发限制器(`runLimited`,同时最多 4 个):首次大量冷数据时 `readSession` 整日志解压不再全量并发压垮磁盘/CPU。
11
+ - 启动预热叠加 `sessionQuery.listSessions()` 冷元数据:冷启动时 `ctx.sessions.list()` 常为空(惰性载入),此前设置页首次打开仍要现场建 store,现开机即预热全部历史工作区。
12
+ - Client 侧 `usage`/`status` 补数据延后到 `list` 返回后异步执行:首屏先渲染树形内容,磁盘占用与错误日志随后补齐;`list` 失败时仍尝试补这两个数据,避免整卡全空。
13
+
14
+ ### 变更
15
+
16
+ - Client 侧 `titles` 请求的 sessionIds 去重(`Set` 去重 + 过滤空值)。
17
+
18
+ ## [1.5.0] - 2026-08-18
19
+
20
+ ### 新增
21
+
22
+ - 快照管理改为**树形结构**展示:第一级工作区(文件夹名)→ 第二级会话(会话标题)→ 第三级快照(消息 ID/消息内容摘要);工作区与会话支持展开/折叠。
23
+ - 树形每级右侧提供删除按钮:工作区删除该工作区全部快照,会话删除该工作区内该会话全部快照,叶子单条删除;均带行内二次确认。
24
+ - 快照叶子显示**对应消息内容摘要**(取 `user/message` 事件的 text 块),悬停显示完整内容;冷会话消息文本经新增 `manage/messages` 端点按会话分组异步补齐,避免为每条消息重复解压日志。
25
+ - Host `manage/delete` 扩展 `scope=workspace/session` 批量删除:内存 + 磁盘全量收集匹配快照,按 root 分组进串行队列,tag 分块(每 100 个)规避 Windows 命令行上限;单个 root 失败 best-effort 继续并进入错误缓冲。
26
+
27
+ ### 变更
28
+
29
+ - `manage list` 同 id 去重由“首次命中即丢弃”改为字段补全(root/sessionId/time/标题/消息文本),避免磁盘先占位、内存后补全时树形节点落入「未知工作区」。
30
+ - 删除操作成功提示改为显示实际删除条数(`已删除 N 条快照`)。
31
+
32
+ ### 兼容性
33
+
34
+ - 全部改动保持纯 JS 零构建;未改存储格式与脚本模板接口,旧索引/旧快照无需迁移。
35
+
5
36
  ## [1.4.0] - 2026-08-17
6
37
 
7
38
  ### 新增
package/README.md CHANGED
@@ -25,9 +25,8 @@
25
25
 
26
26
  - 设置页 · 排除项快速编辑(设置 → 插件 → 撤回设置)
27
27
 
28
- | 设置界面 | 设置界面 |
29
- | --- | --- |
30
- | ![设置页·编辑中](docs/screenshots/settings-exclude-1.png) | ![设置页·已保存](docs/screenshots/settings-exclude-2.png) |
28
+ ![设置页](docs/screenshots/settings-exclude-2.png)
29
+
31
30
 
32
31
  ## 功能亮点
33
32
 
@@ -38,6 +37,7 @@
38
37
  - **先看清单再动手**:点撤回先弹出将变更的文件清单(修改 / 恢复 / 删除),确认后才执行,不会稀里糊涂覆盖。
39
38
  - **磁盘友好**:快照走 git delta 压缩,是增量不是整目录拷贝;超过 100MB 的大文件自动跳过。
40
39
  - **自动瘦身**:定期 `git gc` 把 loose 对象压 pack(无损,快照一个不丢);会话删除后其快照自动清理;构建产物可经 `exclude.txt` 全局排除(见下)。
40
+ - **树形快照管理**:设置页「快照管理」按 **工作区 → 会话 → 快照** 三级树形展示,支持展开/折叠;每级右侧都有删除按钮,可一次清掉整个工作区或某个会话的全部快照,叶子显示该快照对应的消息内容摘要,方便定位“这条消息当时改了什么”。
41
41
 
42
42
  ## 已知限制
43
43
 
@@ -92,6 +92,7 @@ pm2 restart <你的dsh进程名> # 若用 pm2 托管
92
92
  ```
93
93
 
94
94
  对所有项目生效(home 不可写而降级到项目内存储时,该工作区有独立的排除配置,设置页会分卡片列出)。新增排除只影响之后的快照;**回退到更早的快照时,当时尚未排除的文件仍会被恢复**(回到当时的状态,这正是回退语义)。想彻底清掉已进快照的目录,可手动删除 home 下 `dsh-recall-snapshots/` 里对应项目的哈希目录。设置页标签依赖 DSH 自带设置页(0.1.0-rc.x 均含);极旧版本看不到该标签时,直接编辑文件等效。
95
+ - **树形快照管理**:打开「**设置 → 插件 → 撤回设置 → 快照管理**」可看到树形列表——第一级工作区(文件夹名)、第二级会话(会话标题)、第三级快照(时间 + 消息内容摘要,悬停看完整内容)。工作区和会话节点可展开/折叠;每一级右侧都有删除按钮,删除前会二次确认。删除工作区 = 清掉该工作区全部快照;删除会话 = 清掉该工作区内该会话的全部快照;删除叶子 = 只删那一条快照。
95
96
 
96
97
  ## 工作原理
97
98
 
package/lib/client.js CHANGED
@@ -74,12 +74,19 @@ window.__ModuleLoader__.load({
74
74
  '.dsh-recall-ex-chip:hover{color:var(--dsw-alias-label-primary)}',
75
75
  '.dsh-recall-ex-status{margin-right:auto;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary)}',
76
76
  '.dsh-recall-ex-status-error{color:var(--dsw-alias-state-error-primary)}',
77
- '.dsh-recall-snap{display:flex;flex-direction:column;gap:2px;padding:4px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}',
78
- '.dsh-recall-snap:last-child{border-bottom:none}',
79
- '.dsh-recall-snap-main{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px}',
80
- '.dsh-recall-snap-meta{display:flex;gap:8px;align-items:baseline;font-size:11px;line-height:18px;color:var(--dsw-alias-label-tertiary)}',
81
- '.dsh-recall-snap-ws{flex:none;font-weight:600;color:var(--dsw-alias-label-secondary)}',
82
- '.dsh-recall-snap-confirm{display:flex;gap:8px;align-items:center;font-size:12px;line-height:20px;color:var(--dsw-alias-label-secondary)}'
77
+ '.dsh-recall-tree{display:flex;flex-direction:column;gap:2px;padding:4px 0}',
78
+ '.dsh-recall-tree-node{display:flex;flex-direction:column;gap:1px}',
79
+ '.dsh-recall-tree-row{display:flex;gap:6px;align-items:center;min-width:0;padding:2px 4px;border-radius:6px;cursor:default}',
80
+ '.dsh-recall-tree-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
81
+ '.dsh-recall-tree-toggle{flex:none;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:4px;font-size:11px;line-height:18px;user-select:none}',
82
+ '.dsh-recall-tree-toggle:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
83
+ '.dsh-recall-tree-toggle-placeholder{flex:none;width:18px;height:18px}',
84
+ '.dsh-recall-tree-label{flex:1;min-width:0;display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px;overflow:hidden}',
85
+ '.dsh-recall-tree-name{flex:none;font-weight:600;color:var(--dsw-alias-label-secondary)}',
86
+ '.dsh-recall-tree-title{min-width:0;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
87
+ '.dsh-recall-tree-meta{flex:none;font-size:11px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}',
88
+ '.dsh-recall-tree-children{display:flex;flex-direction:column;gap:1px;margin-left:16px;border-left:1px solid var(--dsw-alias-border-l1);padding-left:8px}',
89
+ '.dsh-recall-tree-confirm{display:flex;gap:8px;align-items:center;font-size:12px;line-height:20px;color:var(--dsw-alias-label-secondary)}'
83
90
  ].join('')
84
91
  // 静态 bundle 的 ctx 可能不提供 styles 服务,降级为直接注入 <style>
85
92
  const stylesSvc = ctx.get('styles')
@@ -605,21 +612,55 @@ window.__ModuleLoader__.load({
605
612
  }).catch(() => setTitlesPending(false))
606
613
  }
607
614
 
615
+ // 消息文本补齐:只请求 live 拿不到文本的快照;同一会话多条消息
616
+ // 在 Host 端共享一次 readSession,避免为每条消息重复解压大日志。
617
+ function fetchMessages(list) {
618
+ const requests = (list || [])
619
+ // 只补「还没拿到结论」的快照:live 没命中时不带 messageText 字段,
620
+ // 冷查补齐后字段存在(字符串或 null),null 表示确实无文本不再请求。
621
+ .filter((it) => it.sessionId && it.id && !Object.prototype.hasOwnProperty.call(it, 'messageText'))
622
+ .map((it) => ({ sessionId: it.sessionId, messageId: it.id }))
623
+ .slice(0, 200)
624
+ if (!requests.length) return
625
+ api('manage', { op: 'messages', requests }).then((res) => {
626
+ const map = res && res.ok ? res.messageTexts : null
627
+ if (map) {
628
+ setItems((prev) => (prev || []).map((it) => (
629
+ it.id && Object.prototype.hasOwnProperty.call(map, it.id) ? Object.assign({}, it, { messageText: map[it.id] }) : it
630
+ )))
631
+ }
632
+ }).catch(() => {})
633
+ }
634
+
608
635
  function refresh() {
609
636
  api('manage', { op: 'list' }).then((res) => {
610
637
  if (res && res.ok) {
611
638
  setItems(res.items || [])
612
639
  fetchTitles(res.items || [])
640
+ fetchMessages(res.items || [])
613
641
  }
614
- }).catch(() => {})
615
- if (sessionId) {
616
- api('manage', { op: 'usage', sessionId }).then((res) => {
617
- if (res && res.ok) setUsage(res.bytes || 0)
642
+ // 列表返回后再补 usage/status:首次冷启动时磁盘占用和错误日志
643
+ // 都各要一条 shell,和 list 并发会抢资源拖慢首屏;延后到列表
644
+ // 渲染后,用户先看到树形内容,占用再异步补上。
645
+ if (sessionId) {
646
+ api('manage', { op: 'usage', sessionId }).then((res) => {
647
+ if (res && res.ok) setUsage(res.bytes || 0)
648
+ }).catch(() => {})
649
+ }
650
+ api('status', {}).then((res) => {
651
+ if (res && res.ok) setErrors(res.errors || [])
618
652
  }).catch(() => {})
619
- }
620
- api('status', {}).then((res) => {
621
- if (res && res.ok) setErrors(res.errors || [])
622
- }).catch(() => {})
653
+ }).catch(() => {
654
+ // list 失败时仍尝试补 usage/status,避免整卡全空
655
+ if (sessionId) {
656
+ api('manage', { op: 'usage', sessionId }).then((res) => {
657
+ if (res && res.ok) setUsage(res.bytes || 0)
658
+ }).catch(() => {})
659
+ }
660
+ api('status', {}).then((res) => {
661
+ if (res && res.ok) setErrors(res.errors || [])
662
+ }).catch(() => {})
663
+ })
623
664
  }
624
665
 
625
666
  React.useEffect(() => { refresh() }, [sessionId])
@@ -629,7 +670,7 @@ window.__ModuleLoader__.load({
629
670
  setState({ busy: true, message: '执行中…', error: false })
630
671
  api('manage', Object.assign({ op, sessionId }, extra || {})).then((res) => {
631
672
  if (res && res.ok) {
632
- setState({ busy: false, message: doneText, error: false })
673
+ setState({ busy: false, message: typeof res.deleted === 'number' ? '已删除 ' + res.deleted + ' 条快照' : doneText, error: false })
633
674
  refresh()
634
675
  } else {
635
676
  setState({ busy: false, message: (res && (res.message || res.error)) || '操作失败', error: true })
@@ -637,39 +678,145 @@ window.__ModuleLoader__.load({
637
678
  }).catch((e) => setState({ busy: false, message: String(e), error: true }))
638
679
  }
639
680
 
640
- // 列表行:第一行工作区名 + 会话标题(已删除会话标灰)+ 时间 +
641
- // 截断 ID;第二行删除按钮。完整 ID 仍走行 title 悬停。
642
- // 删除走行内二次确认(confirmingId 记录正在确认的行):
643
- // 点「删除」先换成「确认删除?是/否」,避免误点直接清 tag。
644
- const [confirmingId, setConfirmingId] = React.useState(null)
645
- const rows = (items || []).slice(0, 50).map((it) => {
646
- const confirming = confirmingId === it.id
647
- return React.createElement('div', { className: 'dsh-recall-snap', key: it.id, title: it.id },
648
- React.createElement('div', { className: 'dsh-recall-snap-main' },
649
- React.createElement('span', { className: 'dsh-recall-snap-ws' }, it.workspace || '未知工作区'),
650
- React.createElement('span', { className: 'dsh-recall-rel' }, it.sessionTitle || (titlesPending && it.sessionId ? '…' : '(已删除会话)')),
651
- React.createElement('span', { className: 'dsh-recall-snap-meta' }, clockText(it.time) + ' ' + it.id.slice(0, 12) + '…')
681
+ // 树形管理:工作区 会话 快照三级。折叠状态用 Set<key> 记录,
682
+ // 工作区 key root 路径(可能含特殊字符,但 Set 按字符串精确匹配);
683
+ // 会话 key 用 sessionId;叶子无子节点。删除走行内二次确认:
684
+ // confirming 存 { kind, key, extra, text },行内原地展开确认。
685
+ const [expanded, setExpanded] = React.useState(() => new Set())
686
+ const [confirming, setConfirming] = React.useState(null)
687
+ function toggle(key) {
688
+ setExpanded((prev) => {
689
+ const next = new Set(prev)
690
+ if (next.has(key)) next.delete(key)
691
+ else next.add(key)
692
+ return next
693
+ })
694
+ }
695
+ // 把扁平列表组装成树。同一快照只属于一个工作区/会话,root 或
696
+ // sessionId 缺失时归入「未知」节点,避免行凭空消失。
697
+ function buildTree(list) {
698
+ const workspaces = new Map()
699
+ for (const it of list || []) {
700
+ const rootKey = it.root || 'unknown-root'
701
+ if (!workspaces.has(rootKey)) workspaces.set(rootKey, { root: it.root || null, name: it.workspace || '未知工作区', sessions: new Map() })
702
+ const ws = workspaces.get(rootKey)
703
+ const sidKey = it.sessionId || 'unknown-session'
704
+ if (!ws.sessions.has(sidKey)) ws.sessions.set(sidKey, { root: ws.root, sessionId: it.sessionId || null, title: it.sessionTitle || null, items: [] })
705
+ ws.sessions.get(sidKey).items.push(it)
706
+ }
707
+ const wsList = Array.from(workspaces.values())
708
+ wsList.sort((a, b) => (a.name || '').localeCompare(b.name || ''))
709
+ for (const ws of wsList) {
710
+ ws.sessions = Array.from(ws.sessions.values())
711
+ ws.sessions.sort((a, b) => (a.title || '').localeCompare(b.title || ''))
712
+ for (const s of ws.sessions) s.items.sort((a, b) => (b.time || 0) - (a.time || 0))
713
+ }
714
+ return wsList
715
+ }
716
+ const tree = buildTree(items)
717
+ // 每个节点的删除按钮统一走 run();workspace/session 用新端点 scope
718
+ // 批量删,叶子沿用原单条删除。二次确认文案按节点层级区分。
719
+ function confirmDelete(kind, key, extra, text) {
720
+ setConfirming({ kind, key, extra, text })
721
+ }
722
+ function renderConfirm(kind, key, extra, text) {
723
+ if (!confirming || confirming.kind !== kind || confirming.key !== key) return null
724
+ return React.createElement('div', { className: 'dsh-recall-tree-confirm' },
725
+ text,
726
+ React.createElement('button', {
727
+ type: 'button',
728
+ className: 'dsh-recall-ex-chip',
729
+ onClick: () => {
730
+ const c = confirming
731
+ setConfirming(null)
732
+ run('delete', c.extra, '已删除')
733
+ }
734
+ }, '确认'),
735
+ React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: () => setConfirming(null) }, '取消')
736
+ )
737
+ }
738
+ // 叶子节点:展开箭头占位 + 时间 + 消息内容摘要 + 截断 ID。
739
+ // 悬停 title 显示完整消息内容(优先)或完整 ID(无内容时)。
740
+ // 消息文本来自 Host live 事件或冷日志异步补齐;取到前/确实没有时
741
+ // 退化为显示 ID,避免假“加载中”占位。
742
+ function renderLeaf(it) {
743
+ const key = 'snap-' + it.id
744
+ const text = it.messageText
745
+ const title = text || it.id
746
+ const label = text
747
+ ? clockText(it.time) + ' ' + text
748
+ : clockText(it.time) + ' ' + it.id.slice(0, 12) + '…'
749
+ return React.createElement('div', { className: 'dsh-recall-tree-node', key: key },
750
+ React.createElement('div', { className: 'dsh-recall-tree-row', title: title },
751
+ React.createElement('span', { className: 'dsh-recall-tree-toggle-placeholder' }),
752
+ React.createElement('span', { className: 'dsh-recall-tree-label' },
753
+ React.createElement('span', { className: 'dsh-recall-tree-title' }, label)
754
+ ),
755
+ React.createElement('button', {
756
+ type: 'button',
757
+ className: 'dsh-recall-ex-chip',
758
+ title: '删除该快照(tag 与索引条目)',
759
+ onClick: () => confirmDelete('snapshot', key, { messageId: it.id, root: it.root || null }, '确认删除该快照?此操作不可恢复。')
760
+ }, '删除')
652
761
  ),
653
- confirming
654
- ? React.createElement('div', { className: 'dsh-recall-snap-confirm' },
655
- '确认删除该快照?此操作不可恢复。',
656
- React.createElement('button', {
657
- type: 'button',
658
- className: 'dsh-recall-ex-chip',
659
- onClick: () => { setConfirmingId(null); run('delete', { messageId: it.id, root: it.root || null }, '已删除') }
660
- }, '确认'),
661
- React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: () => setConfirmingId(null) }, '取消')
662
- )
663
- : React.createElement('div', null,
664
- React.createElement('button', {
665
- type: 'button',
666
- className: 'dsh-recall-ex-chip',
667
- title: '删除该快照(tag 与索引条目)',
668
- onClick: () => setConfirmingId(it.id)
669
- }, '删除')
670
- )
762
+ renderConfirm('snapshot', key, { messageId: it.id, root: it.root || null }, '确认删除该快照?此操作不可恢复。')
671
763
  )
672
- })
764
+ }
765
+ // 会话节点:折叠按钮 + 标题 + 快照数 + 删除按钮;子节点为叶子。
766
+ function renderSession(s) {
767
+ const key = 'session-' + (s.root || '') + '-' + s.sessionId
768
+ const open = expanded.has(key)
769
+ const label = s.title || (titlesPending && s.sessionId ? '…' : '(已删除会话)')
770
+ return React.createElement('div', { className: 'dsh-recall-tree-node', key: key },
771
+ React.createElement('div', { className: 'dsh-recall-tree-row' },
772
+ React.createElement('span', {
773
+ className: 'dsh-recall-tree-toggle',
774
+ onClick: () => toggle(key)
775
+ }, open ? '▾' : '▸'),
776
+ React.createElement('span', { className: 'dsh-recall-tree-label', title: s.sessionId || '' },
777
+ React.createElement('span', { className: 'dsh-recall-tree-title' }, label),
778
+ React.createElement('span', { className: 'dsh-recall-tree-meta' }, s.items.length + ' 条')
779
+ ),
780
+ s.sessionId ? React.createElement('button', {
781
+ type: 'button',
782
+ className: 'dsh-recall-ex-chip',
783
+ title: '删除该会话全部快照',
784
+ onClick: () => confirmDelete('session', key, { scope: 'session', sessionId: s.sessionId, root: s.root || null }, '确认删除该会话全部快照?此操作不可恢复。')
785
+ }, '删除') : null
786
+ ),
787
+ open ? React.createElement('div', { className: 'dsh-recall-tree-children' }, ...s.items.map(renderLeaf)) : null,
788
+ s.sessionId ? renderConfirm('session', key, { scope: 'session', sessionId: s.sessionId, root: s.root || null }, '确认删除该会话全部快照?此操作不可恢复。') : null
789
+ )
790
+ }
791
+ // 工作区节点:折叠按钮 + 文件夹名 + 会话数/快照数 + 删除按钮;
792
+ // 子节点为会话。叶子不展开箭头。
793
+ function renderWorkspace(ws) {
794
+ const key = 'ws-' + ws.root
795
+ const open = expanded.has(key)
796
+ const sessionCount = ws.sessions.length
797
+ const snapCount = ws.sessions.reduce((n, s) => n + s.items.length, 0)
798
+ return React.createElement('div', { className: 'dsh-recall-tree-node', key: key },
799
+ React.createElement('div', { className: 'dsh-recall-tree-row' },
800
+ React.createElement('span', {
801
+ className: 'dsh-recall-tree-toggle',
802
+ onClick: () => toggle(key)
803
+ }, open ? '▾' : '▸'),
804
+ React.createElement('span', { className: 'dsh-recall-tree-label', title: ws.root || '' },
805
+ React.createElement('span', { className: 'dsh-recall-tree-name' }, ws.name),
806
+ React.createElement('span', { className: 'dsh-recall-tree-meta' }, sessionCount + ' 会话 / ' + snapCount + ' 快照')
807
+ ),
808
+ ws.root ? React.createElement('button', {
809
+ type: 'button',
810
+ className: 'dsh-recall-ex-chip',
811
+ title: '删除该工作区全部快照',
812
+ onClick: () => confirmDelete('workspace', key, { scope: 'workspace', root: ws.root }, '确认删除该工作区全部快照?此操作不可恢复。')
813
+ }, '删除') : null
814
+ ),
815
+ open ? React.createElement('div', { className: 'dsh-recall-tree-children' }, ...ws.sessions.map(renderSession)) : null,
816
+ ws.root ? renderConfirm('workspace', key, { scope: 'workspace', root: ws.root }, '确认删除该工作区全部快照?此操作不可恢复。') : null
817
+ )
818
+ }
819
+ const treeNodes = tree.map(renderWorkspace)
673
820
 
674
821
  return React.createElement('div', { className: 'dsh-recall-ex-card' },
675
822
  React.createElement('div', { className: 'dsh-recall-ex-title' }, '快照管理'),
@@ -678,7 +825,7 @@ window.__ModuleLoader__.load({
678
825
  ? '共 ' + (items ? items.length : '…') + ' 条快照。'
679
826
  : '共 ' + (items ? items.length : '…') + ' 条快照,当前工作区快照存储占用 ' + sizeText(usage) + '。'
680
827
  ),
681
- rows.length > 0 ? React.createElement('div', { className: 'dsh-recall-list' }, ...rows) : null,
828
+ treeNodes.length > 0 ? React.createElement('div', { className: 'dsh-recall-tree' }, ...treeNodes) : null,
682
829
  React.createElement('div', { className: 'dsh-recall-panel-actions' },
683
830
  state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : '') }, state.message) : null,
684
831
  React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: state.busy, onClick: refresh }, '刷新'),
package/lib/index.js CHANGED
@@ -51,6 +51,9 @@ export function apply(ctx, config) {
51
51
  // 也不是零成本(1 条 shell + 若干日志解压)。设置页打开、删除后刷新
52
52
  // 都会重拉,30s 缓存让二次打开即时;delete 与新快照落地时失效。
53
53
  let listCache = { at: 0, payload: null }
54
+ // 排除配置枚举缓存(30s):exclude-get 首次要遍历工作区、逐文件 shell 读,
55
+ // 设置页反复打开时不该每次重算;exclude-set 成功写入后立即失效。
56
+ let excludeCache = { at: 0, payload: null }
54
57
 
55
58
  // 会话标题缓存(apply 级跨请求共享):冷会话标题要 readSession 整日志
56
59
  // 解压 + 重放校验(大日志 10 秒级),绝不能挡列表首屏——list 只查
@@ -65,6 +68,36 @@ export function apply(ctx, config) {
65
68
  }
66
69
  return null
67
70
  }
71
+ // 消息文本缓存(apply 级跨请求共享):冷会话 readSession 整日志解压很贵,
72
+ // 与标题同款两段式——live 秒回,冷会话由 Client 异步调 titles 端点补齐。
73
+ // 值为 null 表示「查过、确实没有」,同样命中缓存。
74
+ const messageTexts = new Map()
75
+ function messageTextFromEvents(events, messageId) {
76
+ if (!Array.isArray(events) || !messageId) return null
77
+ for (const e of events) {
78
+ if (e && e.type === 'user/message' && e.data && String(e.data.id) === String(messageId)) {
79
+ const blocks = Array.isArray(e.data.content) ? e.data.content : []
80
+ const text = blocks
81
+ .filter((b) => b && b.type === 'text' && typeof b.text === 'string')
82
+ .map((b) => b.text)
83
+ .join('')
84
+ return text || null
85
+ }
86
+ }
87
+ return null
88
+ }
89
+ function liveMessageTextFast(sessionId, messageId) {
90
+ if (!sessionId || !messageId) return null
91
+ const key = String(sessionId) + '\u0000' + String(messageId)
92
+ if (messageTexts.has(key)) return messageTexts.get(key)
93
+ let text = null
94
+ try {
95
+ const live = ctx.sessions.get(sessionId)
96
+ if (live) text = messageTextFromEvents(live.events, messageId)
97
+ } catch (error) { text = null }
98
+ if (text !== null) messageTexts.set(key, text)
99
+ return text
100
+ }
68
101
  function liveTitleFast(sessionId) {
69
102
  if (!sessionId) return null
70
103
  if (sessionTitles.has(sessionId)) return sessionTitles.get(sessionId)
@@ -108,7 +141,9 @@ export function apply(ctx, config) {
108
141
  if (cwd) roots.add(cwd)
109
142
  }
110
143
  const byFile = new Map()
111
- for (const root of roots) {
144
+ // 并行解析全部 root:冷启动时每个 root 首次 resolveStore 可能触发 shell
145
+ // 建目录/写 root.txt,串行会随工作区数量线性变慢;Promise.all 让它们并发。
146
+ await Promise.all(Array.from(roots).map(async (root) => {
112
147
  try {
113
148
  const store = await rt.resolveStore(root)
114
149
  if (store && !byFile.has(store.excludeFile)) byFile.set(store.excludeFile, { store, roots: [] })
@@ -116,7 +151,7 @@ export function apply(ctx, config) {
116
151
  } catch (error) {
117
152
  /* 单个根解析失败只影响它自己,不拖垮整个列表 */
118
153
  }
119
- }
154
+ }))
120
155
  // 磁盘兜底:冷启动时会话注册表为空(惰性载入),但 home 容器目录可能
121
156
  // 早已存在(历史快照)。容器在 ⇒ 共享 exclude.txt 可编辑(哪怕从未
122
157
  // 写过、内容为空);容器不在 ⇒ 全新安装,让设置页显示引导文案。
@@ -248,6 +283,112 @@ export function apply(ctx, config) {
248
283
  return run
249
284
  }
250
285
 
286
+ // 通用并发限制器:冷会话标题/消息文本补齐会 readSession 整日志解压,
287
+ // 首次大量冷数据时全量 Promise.all 会同时压垮磁盘/CPU。限制同时最多
288
+ // CONCURRENCY 个任务,剩余排队执行;这是纯内存调度,不依赖额外依赖。
289
+ async function runLimited(tasks, concurrency) {
290
+ const limit = concurrency > 0 ? concurrency : 4
291
+ let index = 0
292
+ const workers = Array.from({ length: Math.min(limit, tasks.length) }, async () => {
293
+ while (index < tasks.length) {
294
+ const task = tasks[index++]
295
+ await task()
296
+ }
297
+ })
298
+ await Promise.all(workers)
299
+ }
300
+
301
+ // 收集全量快照记录(内存 + 磁盘 dump 并集),供树形管理的按工作区/
302
+ // 会话批量删除使用。返回 Map<消息ID, {id, root, sessionId, time}>;
303
+ // 磁盘条目缺 root 时按 store 的 root.txt/内存映射补全(与 manage list
304
+ // 同一条解析链)。去重只按 id——同一消息 ID 全局唯一。
305
+ async function collectAllSnapshotRecords() {
306
+ const records = new Map()
307
+ function add(id, root, sessionId, time) {
308
+ if (!id || typeof id !== 'string') return
309
+ const old = records.get(id)
310
+ if (!old) {
311
+ records.set(id, {
312
+ id,
313
+ root: root || null,
314
+ sessionId: sessionId || null,
315
+ time: typeof time === 'number' ? time : 0
316
+ })
317
+ return
318
+ }
319
+ // 同一消息 ID 可能出现磁盘先占位、内存后补全的情况:用更全的
320
+ // root/sessionId/time 覆盖旧值,避免树形节点归到「未知」导致批量
321
+ // 删除按工作区/会话匹配不到。
322
+ if (!old.root && root) old.root = root
323
+ if (!old.sessionId && sessionId) old.sessionId = sessionId
324
+ if (!old.time && time) old.time = time
325
+ }
326
+ // 内存视图:当前会话/预热过的 store 已载入,秒回
327
+ for (const [id, s] of state.snapshots.entries()) {
328
+ if (s) add(id, s.root, s.sessionId, s.time)
329
+ }
330
+ // 磁盘全量:冷启动/非活跃工作区的快照也在这里
331
+ const dump = await dumpStores()
332
+ const hints = new Map()
333
+ for (const [root, st] of state.stores.entries()) {
334
+ if (st && st.dir) hints.set(st.dir, root)
335
+ }
336
+ for (const [dir, info] of dump) {
337
+ const baseRoot = info.root || hints.get(dir) || null
338
+ for (const e of info.entries || []) {
339
+ if (!e || typeof e.id !== 'string') continue
340
+ add(e.id, (typeof e.root === 'string' && e.root) || baseRoot, e.sessionId, e.time)
341
+ }
342
+ }
343
+ return records
344
+ }
345
+
346
+ // 按过滤条件批量删除快照(工作区/会话两个树节点共用):先收集匹配
347
+ // id 并按 root 分组,再整体进串行队列——与快照/gc 互斥,避免 git 锁
348
+ // 竞态。每个 root 先 purge tag 再补载索引后重写 index.json,防止冷启动
349
+ // 时用残缺内存覆盖同 store 其余磁盘快照。
350
+ async function deleteSnapshotsByFilter(match, sessionId) {
351
+ const records = await collectAllSnapshotRecords()
352
+ const byRoot = new Map()
353
+ for (const rec of records.values()) {
354
+ if (!match(rec) || !rec.root) continue
355
+ if (!byRoot.has(rec.root)) byRoot.set(rec.root, [])
356
+ byRoot.get(rec.root).push(rec.id)
357
+ }
358
+ let deleted = 0
359
+ await enqueue(async () => {
360
+ for (const [root, rootIds] of byRoot) {
361
+ let store = state.stores.get(root)
362
+ if (!store) {
363
+ try { store = await rt.resolveStore(root) } catch (error) { store = null }
364
+ }
365
+ if (!store) continue
366
+ try {
367
+ if (state.gitExe) {
368
+ // tag 分块删除:win32 命令行有 32767 字符上限,整批传大量 tag 会
369
+ // 在长历史工作区上爆掉;与 maintenance.purgeSession 同款 100 个/块。
370
+ const tags = rootIds.map((id) => 'snap-' + id)
371
+ for (let i = 0; i < tags.length; i += 100) {
372
+ await rt.runShell(rt.scripts.purgeTagsScript(store, state.gitExe, tags.slice(i, i + 100)), { timeoutMs: 120000, stdoutMaxBytes: 4096 })
373
+ }
374
+ }
375
+ if (!state.indexLoaded.has(root)) {
376
+ try { await snaps.loadIndex(root, sessionId) } catch (error) { /* 载入失败照常重写,退化为旧行为 */ }
377
+ }
378
+ for (const id of rootIds) state.snapshots.delete(id)
379
+ await snaps.saveIndex(root, sessionId)
380
+ deleted += rootIds.length
381
+ } catch (error) {
382
+ // 单个 root 失败不阻断其他 root:与 maintenance.purgeSession 同款
383
+ // best-effort,错误进状态页可见的错误缓冲,剩余 root 继续清理。
384
+ rt.recordError('recall batch delete failed for ' + root + ': ' + String(error))
385
+ }
386
+ }
387
+ listCache.payload = null
388
+ })
389
+ return deleted
390
+ }
391
+
251
392
  const endpoints = {
252
393
  'init': async (args) => {
253
394
  if (!supported) {
@@ -322,14 +463,19 @@ export function apply(ctx, config) {
322
463
  // 设置页「撤回设置」标签的配置读取。不支持平台照常短路:Client
323
464
  // 显示不可用提示而不是空白表单,与 init 的 notice 语义对齐。
324
465
  if (!supported) return { ok: false, unsupported: true }
466
+ // 30s 结果缓存:首次进入要并行 resolveStore + 逐文件 shell 读,
467
+ // 二次打开/切标签不应重复付出这份代价;exclude-set 写入后失效。
468
+ if (excludeCache.payload && Date.now() - excludeCache.at < 30000) return excludeCache.payload
325
469
  const byFile = await listExcludeFiles()
326
- const files = []
327
- for (const [path, info] of byFile) {
470
+ // 并行读取各 exclude 文件内容:每个文件一条 shell,串行会放大延迟
471
+ const files = await Promise.all(Array.from(byFile.entries()).map(async ([path, info]) => {
328
472
  let content = ''
329
473
  try { content = await snaps.readExclude(info.store) } catch (error) { content = '' }
330
- files.push({ path, home: Boolean(info.store.home), roots: info.roots, content })
331
- }
332
- return { ok: true, files }
474
+ return { path, home: Boolean(info.store.home), roots: info.roots, content }
475
+ }))
476
+ const payload = { ok: true, files }
477
+ excludeCache = { at: Date.now(), payload }
478
+ return payload
333
479
  },
334
480
 
335
481
  'exclude-set': async (args) => {
@@ -342,6 +488,8 @@ export function apply(ctx, config) {
342
488
  const info = byFile.get(path)
343
489
  if (!info) return { ok: false, code: 'UNKNOWN_PATH', message: '未知的排除文件路径' }
344
490
  await snaps.writeExclude(info.store, content)
491
+ // 写入后立即失效:设置页保存后刷新必须看到最新内容
492
+ excludeCache.payload = null
345
493
  return { ok: true }
346
494
  },
347
495
 
@@ -355,7 +503,6 @@ export function apply(ctx, config) {
355
503
  // 结果缓存(30s + 删除/新快照失效):设置页反复打开、删除后刷新
356
504
  // 都会重拉列表,缓存让二次打开零 shell。
357
505
  if (listCache.payload && Date.now() - listCache.at < 30000) return listCache.payload
358
- const seen = new Set()
359
506
  const allItems = []
360
507
 
361
508
  // 磁盘全量:一条 shell dump(dumpStores 见其注释——旧实现每目录
@@ -371,17 +518,34 @@ export function apply(ctx, config) {
371
518
  }
372
519
  // 去重只用 id(消息 ID 全局唯一):带 root 进 key 会让同一快照
373
520
  // 因「磁盘来源 root 缺失 / 内存来源 root 齐全」出现两条重复行
521
+ const byId = new Map()
374
522
  function push(id, time, root, sessionId) {
375
- if (!id || typeof id !== 'string' || seen.has(id)) return
376
- seen.add(id)
377
- allItems.push({
378
- id,
379
- time: typeof time === 'number' ? time : 0,
380
- root: root || null,
381
- workspace: root ? root.replace(/[\\/]+$/, '').split(/[\\/]/).pop() : null,
382
- sessionId: sessionId || null,
383
- sessionTitle: liveTitleFast(sessionId)
384
- })
523
+ if (!id || typeof id !== 'string') return
524
+ const old = byId.get(id)
525
+ if (!old) {
526
+ const rec = {
527
+ id,
528
+ time: typeof time === 'number' ? time : 0,
529
+ root: root || null,
530
+ workspace: root ? root.replace(/[\\/]+$/, '').split(/[\\/]/).pop() : null,
531
+ sessionId: sessionId || null,
532
+ sessionTitle: liveTitleFast(sessionId)
533
+ }
534
+ // 消息文本只放已确认值:live 命中字符串则带,否则不设字段。
535
+ // 客户端据此判断「还没请求冷日志」;messages 端点补齐后字符串
536
+ // 或 null 都会写入,null 表示确实无文本,避免每次刷新重复请求。
537
+ const liveText = liveMessageTextFast(sessionId, id)
538
+ if (liveText) rec.messageText = liveText
539
+ byId.set(id, rec)
540
+ allItems.push(rec)
541
+ return
542
+ }
543
+ // 与 collectAllSnapshotRecords 同款补全:磁盘先占位、内存后补全
544
+ // root 时,若按「首次命中即丢弃」会让树形一级节点落进未知工作区。
545
+ if (!old.root && root) { old.root = root; old.workspace = root.replace(/[\\/]+$/, '').split(/[\\/]/).pop() || null }
546
+ if (!old.sessionId && sessionId) { old.sessionId = sessionId; old.sessionTitle = liveTitleFast(sessionId) }
547
+ if (!old.messageText && id) { old.messageText = liveMessageTextFast(sessionId, id) }
548
+ if (!old.time && time) old.time = time
385
549
  }
386
550
  for (const [dir, info] of dump) {
387
551
  const baseRoot = info.root || hints.get(dir) || null
@@ -405,10 +569,14 @@ export function apply(ctx, config) {
405
569
  // 重放校验,大日志 10 秒级——独立于列表让首屏即时。并发交给
406
570
  // sessionQuery 自带的 inspect 并发闸,这里全量并行发车。
407
571
  if (!supported) return { ok: false, unsupported: true }
408
- const ids = Array.isArray(args && args.sessionIds) ? args.sessionIds.map(String).slice(0, 100) : []
572
+ const ids = Array.from(new Set(
573
+ (Array.isArray(args && args.sessionIds) ? args.sessionIds.map(String) : []).filter(Boolean)
574
+ )).slice(0, 100)
409
575
  const out = {}
410
- await Promise.all(ids.map(async (sid) => {
411
- if (!sid || out[sid] !== undefined) return
576
+ // 并发限 4:冷标题 readSession 是重 IO,全量并发会把首次设置页
577
+ // shell/日志解压都挤在一起;限制后列表本身不受影响,标题渐进补齐。
578
+ await runLimited(ids.map((sid) => async () => {
579
+ if (out[sid] !== undefined) return
412
580
  let title = liveTitleFast(sid)
413
581
  if (title === null) {
414
582
  const query = ctx.get('sessionQuery')
@@ -421,9 +589,55 @@ export function apply(ctx, config) {
421
589
  }
422
590
  sessionTitles.set(sid, title)
423
591
  out[sid] = title
424
- }))
592
+ }), 4)
425
593
  return { ok: true, titles: out }
426
594
  }
595
+ if (op === 'messages') {
596
+ // 冷会话消息文本补齐:与 titles 同款两段式,独立端点避免和标题
597
+ // 请求的 sessionIds/messageIds 对应关系纠缠。输入 [{sessionId, messageId}],
598
+ // 缺 live 文本的消息才需要补;同一会话多个消息共享一次 readSession。
599
+ if (!supported) return { ok: false, unsupported: true }
600
+ const reqs = Array.isArray(args && args.requests) ? args.requests.slice(0, 200) : []
601
+ const bySession = new Map()
602
+ for (const r of reqs) {
603
+ const sid = r && r.sessionId ? String(r.sessionId) : null
604
+ const mid = r && r.messageId ? String(r.messageId) : null
605
+ if (!sid || !mid) continue
606
+ if (!bySession.has(sid)) bySession.set(sid, [])
607
+ bySession.get(sid).push(mid)
608
+ }
609
+ const texts = {}
610
+ await runLimited(Array.from(bySession.entries()).map(([sid, mids]) => async () => {
611
+ // 该会话所有消息都已缓存(含 null)时,不必 readSession 冷读
612
+ const allCached = mids.every((mid) => messageTexts.has(String(sid) + '\u0000' + String(mid)))
613
+ let log = null
614
+ if (!allCached) {
615
+ const query = ctx.get('sessionQuery')
616
+ if (query && typeof query.readSession === 'function') {
617
+ try {
618
+ log = await query.readSession(sid)
619
+ } catch (error) { log = null }
620
+ }
621
+ }
622
+ for (const mid of mids) {
623
+ const key = String(sid) + '\u0000' + String(mid)
624
+ // 缓存命中(含 null)直接复用,避免已确认无文本的消息反复冷读
625
+ if (messageTexts.has(key)) {
626
+ texts[mid] = messageTexts.get(key)
627
+ continue
628
+ }
629
+ let text = liveMessageTextFast(sid, mid)
630
+ if (text === null && log && Array.isArray(log.events)) {
631
+ text = messageTextFromEvents(log.events, mid)
632
+ }
633
+ // 与 sessionTitles 同款缓存策略:null 也缓存,避免冷会话反复
634
+ // readSession 查同一个查不到文本的消息。
635
+ messageTexts.set(key, text)
636
+ texts[mid] = text
637
+ }
638
+ }), 4)
639
+ return { ok: true, messageTexts: texts }
640
+ }
427
641
  if (op === 'usage') {
428
642
  const root = await rt.resolveRoot(sessionId)
429
643
  if (!root) return { ok: false, code: 'NO_ROOT', message: '无法解析当前工作区' }
@@ -434,25 +648,46 @@ export function apply(ctx, config) {
434
648
  return { ok: true, bytes }
435
649
  }
436
650
  if (op === 'delete') {
651
+ // 统一删除入口:scope=workspace 删除整个工作区全部快照;
652
+ // scope=session 删除某会话全部快照;scope=snapshot(缺省)单条删除。
653
+ // 树形管理每级右侧都有删除按钮,三种粒度共用此端点。
654
+ const scope = args && args.scope ? String(args.scope) : 'snapshot'
655
+ const root = args && args.root ? String(args.root) : null
656
+ const targetSessionId = args && args.sessionId ? String(args.sessionId) : null
437
657
  const id = args && args.messageId ? String(args.messageId) : ''
658
+ if (scope === 'workspace') {
659
+ if (!root) return { ok: false, code: 'NO_ROOT', message: '缺少工作区路径' }
660
+ const deleted = await deleteSnapshotsByFilter((rec) => rec.root === root, sessionId)
661
+ return { ok: true, deleted }
662
+ }
663
+ if (scope === 'session') {
664
+ if (!targetSessionId) return { ok: false, code: 'NO_SESSION', message: '缺少会话 ID' }
665
+ // 树形中会话挂在具体工作区下,客户端会传 root 限定范围;不传则
666
+ // 保持旧语义(删该会话全部工作区的快照),兼容老调用方。
667
+ const deleted = await deleteSnapshotsByFilter(
668
+ (rec) => rec.sessionId === targetSessionId && (!root || rec.root === root),
669
+ sessionId
670
+ )
671
+ return { ok: true, deleted }
672
+ }
438
673
  // 管理列表来自磁盘(跨工作区全量),而内存 state.snapshots 只含
439
674
  // 当前工作区 + 预热过的——冷启动或别的会话先点开列表时,列表里有、
440
675
  // 内存里没有,只查内存会把可删的快照误报「不存在」。解析链:
441
676
  // 内存命中 → Client 透传的条目 root → 磁盘 index 反查归属 store。
442
677
  let snap = state.snapshots.get(id) || null
443
- let root = snap ? snap.root : (args && args.root ? String(args.root) : null)
678
+ let snapRoot = snap ? snap.root : root
444
679
  let store = null
445
- if (root) {
446
- try { store = await rt.resolveStore(root) } catch (error) { store = null }
680
+ if (snapRoot) {
681
+ try { store = await rt.resolveStore(snapRoot) } catch (error) { store = null }
447
682
  }
448
683
  if (!store) {
449
684
  // 兜底:扫 home 容器与降级目录的 index.json,找到含该 id 的 store
450
685
  const found = await locateSnapshotOnDisk(id)
451
- if (found) { store = found.store; root = found.root }
686
+ if (found) { store = found.store; snapRoot = found.root }
452
687
  }
453
688
  if (!store) return { ok: false, code: 'NO_SNAPSHOT', message: '该快照不存在' }
454
689
  const finalStore = store
455
- const finalRoot = root
690
+ const finalRoot = snapRoot
456
691
  await enqueue(async () => {
457
692
  if (state.gitExe) {
458
693
  await rt.runShell(rt.scripts.purgeTagsScript(finalStore, state.gitExe, ['snap-' + id]), { timeoutMs: 120000, stdoutMaxBytes: 4096 })
@@ -526,16 +761,33 @@ export function apply(ctx, config) {
526
761
  // 启动预热:所有已存在工作区解析存储、重建索引与孤儿快照,
527
762
  // 并清理旧版项目内 blobs 目录(home 可用时)。
528
763
  // 不触发维护(gc/清理):开机预热应尽量轻,重活等第一条消息再按节流来。
529
- for (const session of ctx.sessions.list()) {
530
- const cwd = session && session.header && session.header.cwd
531
- if (!cwd) continue
532
- const sessionId = session.id
533
- Promise.resolve(rt.resolveStore(cwd))
534
- .then(() => rt.tryUpgradeToHome(cwd))
535
- .then((store) => rt.ensureGit(cwd, store))
536
- .then(() => snaps.loadIndex(cwd, sessionId))
537
- .then(() => snaps.rebuildOrphans(cwd, sessionId))
538
- .then(() => rt.cleanupLegacy(cwd))
539
- .catch(() => {})
540
- }
764
+ // 冷启动时 ctx.sessions.list() 常为空(惰性载入),必须叠加
765
+ // sessionQuery.listSessions() 冷元数据,否则设置页首次打开仍要现场建 store。
766
+ // apply 不是 async,这里用 IIFE 把冷元数据扫描包成异步任务。
767
+ ;(async () => {
768
+ const warmupRoots = new Map()
769
+ for (const session of ctx.sessions.list()) {
770
+ const cwd = session && session.header && session.header.cwd
771
+ if (cwd && !warmupRoots.has(cwd)) warmupRoots.set(cwd, session.id)
772
+ }
773
+ const querySvc = ctx.get('sessionQuery')
774
+ if (querySvc && typeof querySvc.listSessions === 'function') {
775
+ try {
776
+ const records = await querySvc.listSessions()
777
+ for (const record of records || []) {
778
+ const cwd = record && record.header && record.header.cwd
779
+ if (cwd && !warmupRoots.has(cwd)) warmupRoots.set(cwd, record.id || null)
780
+ }
781
+ } catch (error) { /* 冷元数据不可用则退回 live 注册表 */ }
782
+ }
783
+ for (const [cwd, sessionId] of warmupRoots) {
784
+ Promise.resolve(rt.resolveStore(cwd))
785
+ .then(() => rt.tryUpgradeToHome(cwd))
786
+ .then((store) => rt.ensureGit(cwd, store))
787
+ .then(() => snaps.loadIndex(cwd, sessionId))
788
+ .then(() => snaps.rebuildOrphans(cwd, sessionId))
789
+ .then(() => rt.cleanupLegacy(cwd))
790
+ .catch(() => {})
791
+ }
792
+ })()
541
793
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-recall-plugin",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "DSH 消息撤回插件:在用户消息气泡旁加「撤回」按钮,把项目文件(独立影子 git 仓库快照)与对话历史(官方 fork)一并回退到该消息发送之前。",
5
5
  "type": "module",
6
6
  "repository": {
@@ -40,12 +40,12 @@
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@deepseek-ai/cordis": "^4.0.1",
43
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
44
- "@deepseek-ai/dsh-shell": "^0.1.0-rc.6",
45
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.6",
46
- "@deepseek-ai/dsh-session-query": "^0.1.0-rc.6",
47
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
48
- "@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.6",
43
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
44
+ "@deepseek-ai/dsh-shell": "^0.1.0-rc.8",
45
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.8",
46
+ "@deepseek-ai/dsh-session-query": "^0.1.0-rc.8",
47
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8",
48
+ "@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.8",
49
49
  "react": "^18.2.0"
50
50
  },
51
51
  "license": "MIT"