dsh-config-manager 0.1.71 → 0.1.73

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.
Files changed (596) hide show
  1. package/README.md +149 -98
  2. package/README.zh-CN.md +245 -88
  3. package/lib/adapters/agent-instructions.js +1 -1
  4. package/lib/adapters/file-collection.js +1 -1
  5. package/lib/adapters/index.d.ts +2 -1
  6. package/lib/adapters/index.js +6 -1
  7. package/lib/adapters/link-report.d.ts +1 -0
  8. package/lib/adapters/link-report.js +27 -3
  9. package/lib/adapters/messages.d.ts +21 -0
  10. package/lib/adapters/messages.js +57 -0
  11. package/lib/adapters/plugin-data.d.ts +114 -0
  12. package/lib/adapters/plugin-data.js +309 -0
  13. package/lib/adapters/plugin-files.js +3 -2
  14. package/lib/adapters/plugins.js +4 -3
  15. package/lib/adapters/prompts.d.ts +1 -0
  16. package/lib/adapters/prompts.js +7 -1
  17. package/lib/adapters/self.d.ts +8 -2
  18. package/lib/adapters/self.js +3 -2
  19. package/lib/adapters/sessions.d.ts +1 -0
  20. package/lib/adapters/sessions.js +4 -2
  21. package/lib/adapters/test-helpers.d.ts +9 -0
  22. package/lib/adapters/test-helpers.js +14 -0
  23. package/lib/adapters/ui.d.ts +2 -2
  24. package/lib/adapters/ui.js +25 -25
  25. package/lib/adapters/workspaces.d.ts +1 -0
  26. package/lib/adapters/workspaces.js +7 -3
  27. package/lib/cli/actions.d.ts +9 -1
  28. package/lib/cli/actions.js +5 -2
  29. package/lib/cli/help.js +23 -10
  30. package/lib/cli/index.d.ts +23 -0
  31. package/lib/cli/index.js +90 -32
  32. package/lib/cli/sessions-inspect.js +3 -1
  33. package/lib/cli/sessions-repair.js +25 -6
  34. package/lib/cli/web/page.d.ts +12 -0
  35. package/lib/cli/web/page.js +21 -12
  36. package/lib/cli/web/routes.js +2 -2
  37. package/lib/cli/web/server.js +4 -4
  38. package/lib/client.d.ts +1442 -494
  39. package/lib/client.js +15326 -8520
  40. package/lib/core/analyzer.d.ts +11 -2
  41. package/lib/core/analyzer.js +31 -8
  42. package/lib/core/backup-plan.d.ts +10 -0
  43. package/lib/core/backup-plan.js +51 -20
  44. package/lib/core/backup-verify.js +24 -23
  45. package/lib/core/backup.d.ts +25 -1
  46. package/lib/core/backup.js +99 -28
  47. package/lib/core/boot-rescue.js +4 -3
  48. package/lib/core/boot-safety.js +2 -1
  49. package/lib/core/command-rollback.d.ts +182 -0
  50. package/lib/core/command-rollback.js +293 -0
  51. package/lib/core/consult-source.js +7 -6
  52. package/lib/core/disk-usage.d.ts +74 -10
  53. package/lib/core/disk-usage.js +67 -4
  54. package/lib/core/engine-locale.d.ts +87 -0
  55. package/lib/core/engine-locale.js +125 -0
  56. package/lib/core/exporter.js +3 -3
  57. package/lib/core/fork-reconcile.d.ts +128 -0
  58. package/lib/core/fork-reconcile.js +150 -0
  59. package/lib/core/journal.js +6 -5
  60. package/lib/core/local-plugin-host.js +3 -2
  61. package/lib/core/local-plugin-pack.js +11 -6
  62. package/lib/core/messages-core.d.ts +294 -0
  63. package/lib/core/messages-core.js +619 -0
  64. package/lib/core/messages.d.ts +61 -2
  65. package/lib/core/messages.js +118 -13
  66. package/lib/core/migration-consult.js +21 -20
  67. package/lib/core/migration-history.js +19 -5
  68. package/lib/core/model-tools.d.ts +0 -22
  69. package/lib/core/model-tools.js +50 -27
  70. package/lib/core/phase3-host.js +30 -5
  71. package/lib/core/plugin-cli.js +18 -19
  72. package/lib/core/plugin-data-registry.d.ts +124 -0
  73. package/lib/core/plugin-data-registry.js +198 -0
  74. package/lib/core/reconcile.d.ts +2 -14
  75. package/lib/core/reconcile.js +87 -17
  76. package/lib/core/recovery-orchestrator.js +19 -18
  77. package/lib/core/reinstall.d.ts +4 -0
  78. package/lib/core/reinstall.js +26 -21
  79. package/lib/core/restore.js +20 -17
  80. package/lib/core/rollback-gate.d.ts +33 -0
  81. package/lib/core/rollback-gate.js +57 -0
  82. package/lib/core/rollback-ignore-rules.d.ts +31 -0
  83. package/lib/core/rollback-ignore-rules.js +140 -0
  84. package/lib/core/rollback-ledger.d.ts +248 -0
  85. package/lib/core/rollback-ledger.js +1282 -0
  86. package/lib/core/rollback-point.d.ts +1391 -0
  87. package/lib/core/rollback-point.js +3503 -0
  88. package/lib/core/rollback.js +6 -4
  89. package/lib/core/run-registry.js +6 -5
  90. package/lib/core/session-health.d.ts +15 -0
  91. package/lib/core/session-meta.d.ts +42 -2
  92. package/lib/core/session-meta.js +140 -1
  93. package/lib/core/session-title.d.ts +28 -0
  94. package/lib/core/session-title.js +185 -0
  95. package/lib/core/startup-barrier.d.ts +24 -0
  96. package/lib/core/startup-barrier.js +22 -0
  97. package/lib/core/transaction-coordinator.js +4 -3
  98. package/lib/core/types.d.ts +37 -1
  99. package/lib/core/validator.js +5 -4
  100. package/lib/core/verify-recovery.js +31 -30
  101. package/lib/foreign/bundle.js +3 -2
  102. package/lib/foreign/continue.js +2 -1
  103. package/lib/foreign/crush.js +2 -1
  104. package/lib/foreign/gemini.js +2 -1
  105. package/lib/foreign/goose.js +2 -1
  106. package/lib/foreign/kilocode.js +2 -1
  107. package/lib/foreign/kimi.js +2 -1
  108. package/lib/foreign/messages.d.ts +15 -0
  109. package/lib/foreign/messages.js +40 -0
  110. package/lib/foreign/mimocode.js +2 -1
  111. package/lib/foreign/openclaw.js +2 -1
  112. package/lib/foreign/opencode.js +2 -1
  113. package/lib/foreign/pi.js +2 -1
  114. package/lib/foreign/qoder.js +2 -1
  115. package/lib/foreign/qwen.js +2 -1
  116. package/lib/foreign/registry.d.ts +1 -1
  117. package/lib/foreign/registry.js +8 -7
  118. package/lib/foreign/teleagent.js +2 -1
  119. package/lib/foreign/workbuddy.js +2 -1
  120. package/lib/foreign/zcode.js +2 -1
  121. package/lib/foreign/zed.js +2 -1
  122. package/lib/index.d.ts +227 -7
  123. package/lib/index.js +799 -54
  124. package/lib/market/git-file-writer.js +20 -19
  125. package/lib/market/github-repos.js +35 -19
  126. package/lib/market/index-parser.js +31 -30
  127. package/lib/market/messages-market.d.ts +138 -0
  128. package/lib/market/messages-market.js +305 -0
  129. package/lib/market/my-repo.d.ts +2 -2
  130. package/lib/market/my-repo.js +32 -29
  131. package/lib/market/prepare.js +18 -15
  132. package/lib/market/security.js +20 -19
  133. package/lib/market/types.js +2 -1
  134. package/lib/market/url.js +2 -1
  135. package/lib/migrations/index.js +6 -5
  136. package/lib/migrations/messages-migrations.d.ts +32 -0
  137. package/lib/migrations/messages-migrations.js +58 -0
  138. package/lib/profiles/dsh-profile-launcher.js +2 -1
  139. package/lib/profiles/dsh-profile-runtime.js +2 -1
  140. package/lib/profiles/messages.d.ts +7 -0
  141. package/lib/profiles/messages.js +27 -0
  142. package/lib/routes/backup.js +18 -2
  143. package/lib/routes/import.js +3 -3
  144. package/lib/routes/index.d.ts +1 -1
  145. package/lib/routes/kit.d.ts +3 -2
  146. package/lib/routes/kit.js +6 -1
  147. package/lib/routes/market.js +1 -1
  148. package/lib/routes/messages.d.ts +43 -0
  149. package/lib/routes/messages.js +102 -0
  150. package/lib/routes/prefs.js +26 -3
  151. package/lib/routes/profiles.js +12 -6
  152. package/lib/routes/recovery.d.ts +21 -0
  153. package/lib/routes/recovery.js +478 -94
  154. package/lib/routes/snapshots.js +51 -4
  155. package/lib/routes/sync.js +79 -17
  156. package/lib/schema/config.d.ts +11 -1
  157. package/lib/schema/config.js +52 -20
  158. package/lib/schema/index.d.ts +8 -6
  159. package/lib/schema/index.js +8 -6
  160. package/lib/schema/manifest.js +19 -18
  161. package/lib/schema/messages.d.ts +47 -0
  162. package/lib/schema/messages.js +110 -0
  163. package/lib/schema/section-registry.d.ts +42 -15
  164. package/lib/schema/section-registry.js +67 -32
  165. package/lib/schema/types.d.ts +1 -1
  166. package/lib/schema/versions.js +6 -5
  167. package/lib/security/encryption.js +16 -15
  168. package/lib/security/integrity.js +8 -7
  169. package/lib/security/messages.d.ts +30 -0
  170. package/lib/security/messages.js +77 -0
  171. package/lib/security/redaction.js +12 -0
  172. package/lib/security/secret-scanner.js +16 -13
  173. package/lib/security/vault.js +2 -1
  174. package/lib/security/zip-security.js +4 -3
  175. package/lib/sync/ancestor.js +2 -1
  176. package/lib/sync/autosync-config.d.ts +11 -0
  177. package/lib/sync/autosync-config.js +12 -0
  178. package/lib/sync/autosync-scheduler.d.ts +11 -1
  179. package/lib/sync/autosync-scheduler.js +115 -76
  180. package/lib/sync/backup-files.js +3 -2
  181. package/lib/sync/backup-scheduler.js +6 -5
  182. package/lib/sync/blob-store.js +2 -1
  183. package/lib/sync/git/git-transport.d.ts +55 -0
  184. package/lib/sync/git/git-transport.js +152 -2
  185. package/lib/sync/github-auth.js +10 -9
  186. package/lib/sync/layout.js +13 -12
  187. package/lib/sync/messages.d.ts +63 -0
  188. package/lib/sync/messages.js +149 -0
  189. package/lib/sync/review-queue.js +6 -5
  190. package/lib/sync/s3/messages.d.ts +7 -0
  191. package/lib/sync/s3/messages.js +16 -0
  192. package/lib/sync/s3/s3-providers.d.ts +4 -1
  193. package/lib/sync/s3/s3-providers.js +5 -5
  194. package/lib/sync/s3/s3-transport.js +1 -1
  195. package/lib/sync/s3/sigv4.js +2 -1
  196. package/lib/sync/snapshot-crypto.js +6 -5
  197. package/lib/sync/snapshot-json.js +3 -2
  198. package/lib/sync/sync-engine.d.ts +58 -22
  199. package/lib/sync/sync-engine.js +119 -48
  200. package/lib/sync/sync-history.js +7 -6
  201. package/lib/sync/sync-selection.d.ts +15 -4
  202. package/lib/sync/sync-selection.js +17 -5
  203. package/lib/sync/ui-prefs.d.ts +22 -0
  204. package/lib/sync/ui-prefs.js +28 -0
  205. package/lib/sync/webdav/webdav-transport.d.ts +1 -0
  206. package/lib/sync/webdav/webdav-transport.js +4 -1
  207. package/lib/ui/artifact-view.js +4 -3
  208. package/lib/ui/commands.d.ts +1 -1
  209. package/lib/ui/commands.js +10 -0
  210. package/lib/ui/diff-view.d.ts +10 -0
  211. package/lib/ui/diff-view.js +17 -0
  212. package/lib/ui/disk-usage-view.d.ts +33 -4
  213. package/lib/ui/disk-usage-view.js +29 -1
  214. package/lib/ui/errors.d.ts +1 -1
  215. package/lib/ui/errors.js +4 -3
  216. package/lib/ui/export-flow.js +26 -20
  217. package/lib/ui/foreign-view.d.ts +5 -1
  218. package/lib/ui/foreign-view.js +26 -2
  219. package/lib/ui/i18n.d.ts +68 -1
  220. package/lib/ui/i18n.js +120 -2
  221. package/lib/ui/import-wizard.d.ts +7 -0
  222. package/lib/ui/import-wizard.js +13 -10
  223. package/lib/ui/progress.d.ts +2 -2
  224. package/lib/ui/progress.js +1 -2
  225. package/lib/ui/report.d.ts +5 -5
  226. package/lib/ui/report.js +6 -6
  227. package/lib/ui/rollback-view.d.ts +815 -0
  228. package/lib/ui/rollback-view.js +738 -0
  229. package/lib/ui/selection-model.d.ts +41 -0
  230. package/lib/ui/selection-model.js +30 -0
  231. package/lib/ui/session-inventory-view.d.ts +97 -3
  232. package/lib/ui/session-inventory-view.js +76 -2
  233. package/lib/ui/session-issue-labels.d.ts +43 -0
  234. package/lib/ui/session-issue-labels.js +48 -0
  235. package/lib/ui/sync-settings-view.d.ts +33 -0
  236. package/lib/ui/sync-settings-view.js +40 -0
  237. package/lib/ui/test-helpers.js +2 -1
  238. package/lib/ui/types.d.ts +8 -3
  239. package/lib/ui/types.js +1 -1
  240. package/lib/utils/atomic-write.js +6 -5
  241. package/lib/utils/bundle-scan.d.ts +11 -1
  242. package/lib/utils/bundle-scan.js +12 -1
  243. package/lib/utils/env-lock.d.ts +10 -0
  244. package/lib/utils/env-lock.js +73 -46
  245. package/lib/utils/json.js +6 -5
  246. package/lib/utils/messages-utils.d.ts +96 -0
  247. package/lib/utils/messages-utils.js +216 -0
  248. package/lib/utils/proxy.js +18 -5
  249. package/lib/utils/recursive-walk.d.ts +22 -0
  250. package/lib/utils/recursive-walk.js +75 -1
  251. package/lib/utils/session-log-repair.js +10 -9
  252. package/lib/utils/zip.js +27 -26
  253. package/package.json +12 -2
  254. package/src/adapters/agent-instructions.ts +1 -1
  255. package/src/adapters/file-collection.test.ts +1 -0
  256. package/src/adapters/file-collection.ts +1 -1
  257. package/src/adapters/files.test.ts +27 -0
  258. package/src/adapters/index.ts +6 -1
  259. package/src/adapters/link-report.test.ts +22 -2
  260. package/src/adapters/link-report.ts +29 -4
  261. package/src/adapters/messages.ts +61 -0
  262. package/src/adapters/plugin-data.test.ts +225 -0
  263. package/src/adapters/plugin-data.ts +351 -0
  264. package/src/adapters/plugin-files.ts +3 -2
  265. package/src/adapters/plugins.ts +4 -3
  266. package/src/adapters/prompts.ts +7 -1
  267. package/src/adapters/self.test.ts +26 -0
  268. package/src/adapters/self.ts +10 -3
  269. package/src/adapters/sessions.ts +4 -2
  270. package/src/adapters/settings.test.ts +38 -1
  271. package/src/adapters/test-helpers.ts +15 -0
  272. package/src/adapters/ui.test.ts +2 -2
  273. package/src/adapters/ui.ts +27 -26
  274. package/src/adapters/workspaces.ts +7 -3
  275. package/src/cli/actions.test.ts +28 -0
  276. package/src/cli/actions.ts +11 -2
  277. package/src/cli/help.ts +23 -10
  278. package/src/cli/index-guards.test.ts +193 -0
  279. package/src/cli/index.ts +106 -31
  280. package/src/cli/sessions-inspect.test.ts +15 -1
  281. package/src/cli/sessions-inspect.ts +3 -1
  282. package/src/cli/sessions-repair.test.ts +84 -0
  283. package/src/cli/sessions-repair.ts +33 -7
  284. package/src/cli/web/console-route-parity.test.ts +115 -0
  285. package/src/cli/web/page.ts +23 -12
  286. package/src/cli/web/routes.ts +2 -1
  287. package/src/cli/web/server.ts +4 -4
  288. package/src/cli/web/web.test.ts +2 -2
  289. package/src/client/ConfigManagerSection.tsx +30 -2
  290. package/src/client/about/AboutPanel.tsx +2 -2
  291. package/src/client/about/PluginUpdateDialog.tsx +1 -1
  292. package/src/client/about/ReleaseNotesDialog.tsx +2 -2
  293. package/src/client/about/about-view.test.ts +13 -3
  294. package/src/client/about/about-view.ts +10 -6
  295. package/src/client/about/release-notes-view.test.ts +14 -4
  296. package/src/client/about/release-notes-view.ts +4 -2
  297. package/src/client/api.ts +18 -6
  298. package/src/client/chat/ChatEntryView.tsx +411 -0
  299. package/src/client/chat/RollbackDiffTab.tsx +108 -0
  300. package/src/client/chat/RollbackFileIcon.tsx +96 -0
  301. package/src/client/chat/RollbackReviewDialog.tsx +607 -0
  302. package/src/client/chat/chat-entry-view.test.ts +1174 -0
  303. package/src/client/chat/chat-entry-view.ts +758 -0
  304. package/src/client/chat/chat-locales.ts +152 -0
  305. package/src/client/chat/chat-node-registration.ts +64 -0
  306. package/src/client/chat/chat-point-window.ts +183 -0
  307. package/src/client/chat/chat-slots.ts +107 -0
  308. package/src/client/chat/rollback-diff-tab.test.ts +105 -0
  309. package/src/client/chat/rollback-diff-tab.ts +174 -0
  310. package/src/client/chat/rollback-entry-view.test.ts +1469 -0
  311. package/src/client/chat/rollback-entry-view.ts +946 -0
  312. package/src/client/chat/sidebar-slots.ts +59 -0
  313. package/src/client/common/ConfirmDialog.tsx +7 -3
  314. package/src/client/common/ContentPicker.tsx +48 -10
  315. package/src/client/common/DiffTable.tsx +133 -0
  316. package/src/client/common/ErrorBanner.tsx +4 -4
  317. package/src/client/common/Icon.tsx +2 -0
  318. package/src/client/common/Modal.tsx +8 -1
  319. package/src/client/common/ProgressBar.tsx +8 -2
  320. package/src/client/common/ReportView.tsx +4 -4
  321. package/src/client/common/RunsCenter.tsx +1 -0
  322. package/src/client/common/client-route-parity.test.ts +84 -0
  323. package/src/client/common/info-hint-guard.test.ts +46 -13
  324. package/src/client/common/picker-lazy-units.test.ts +42 -0
  325. package/src/client/common/plan-text-redaction.test.ts +17 -6
  326. package/src/client/common/progress-view-edge.test.ts +8 -7
  327. package/src/client/common/progress-view.test.ts +9 -9
  328. package/src/client/common/progress-view.ts +2 -2
  329. package/src/client/common/routes.ts +38 -0
  330. package/src/client/common/section-labels.test.ts +1 -0
  331. package/src/client/common/section-labels.ts +2 -0
  332. package/src/client/common/ui.tsx +73 -10
  333. package/src/client/config-manager.module.css +1109 -31
  334. package/src/client/css-scale-guard.test.ts +30 -0
  335. package/src/client/environment/EnvironmentPanel.tsx +11 -4
  336. package/src/client/environment/maintenance/DiskUsageCard.tsx +23 -2
  337. package/src/client/export/ExportView.tsx +13 -8
  338. package/src/client/history/HistoryPanel.tsx +4 -3
  339. package/src/client/history/history-api.ts +2 -2
  340. package/src/client/history/history-locales.ts +5 -1
  341. package/src/client/home/HomePanel.tsx +29 -8
  342. package/src/client/import/ForeignImportView.tsx +4 -5
  343. package/src/client/import/ImportWizardView.tsx +5 -3
  344. package/src/client/import/import-wizard-split.test.ts +3 -1
  345. package/src/client/import/import-wizard-steps.tsx +9 -9
  346. package/src/client/index.ts +232 -3
  347. package/src/client/library/LibraryActions.tsx +32 -7
  348. package/src/client/library/LibraryPanel.tsx +23 -3
  349. package/src/client/library/RestorePlanView.tsx +9 -66
  350. package/src/client/locales.ts +62 -9
  351. package/src/client/lucide-icons.d.ts +1 -0
  352. package/src/client/market/MarketPanel.tsx +13 -2
  353. package/src/client/market/MyConfigsView.tsx +5 -4
  354. package/src/client/market/MyConfigsWizard.tsx +10 -8
  355. package/src/client/market/market-api.test.ts +9 -8
  356. package/src/client/market/market-api.ts +2 -2
  357. package/src/client/market/market-locales.ts +3 -0
  358. package/src/client/market/market-view.test.ts +6 -6
  359. package/src/client/market/market-view.ts +3 -3
  360. package/src/client/market/my-configs-api.ts +2 -2
  361. package/src/client/market/my-configs-view.test.ts +23 -22
  362. package/src/client/market/my-configs-view.ts +8 -8
  363. package/src/client/recovery/IncompleteCopiesSection.tsx +2 -1
  364. package/src/client/recovery/RecoveryPanel.tsx +442 -283
  365. package/src/client/recovery/RollbackCard.tsx +859 -0
  366. package/src/client/recovery/RollbackDiffPane.tsx +69 -0
  367. package/src/client/recovery/RollbackFileChanges.tsx +163 -0
  368. package/src/client/recovery/incident-api.ts +2 -2
  369. package/src/client/recovery/recovery-api.ts +144 -6
  370. package/src/client/recovery/recovery-locales.test.ts +299 -0
  371. package/src/client/recovery/recovery-locales.ts +520 -149
  372. package/src/client/recovery/recovery-panel-layout.test.ts +132 -2
  373. package/src/client/recovery/rollback-enabled.ts +41 -0
  374. package/src/client/recovery/rollback-file-changes.test.ts +114 -0
  375. package/src/client/recovery/session-dialog-mount.test.ts +54 -0
  376. package/src/client/run-store.ts +84 -21
  377. package/src/client/sync/AutosyncCard.tsx +25 -1
  378. package/src/client/sync/ChannelClearConfirmDialog.tsx +6 -3
  379. package/src/client/sync/ChannelConfigDialog.tsx +10 -10
  380. package/src/client/sync/DecryptPasswordCard.tsx +2 -2
  381. package/src/client/sync/SecurityOptionsCard.tsx +27 -17
  382. package/src/client/sync/SyncChannelEntryCard.tsx +14 -2
  383. package/src/client/sync/SyncConfirmView.tsx +46 -8
  384. package/src/client/sync/SyncLogList.tsx +1 -1
  385. package/src/client/sync/SyncPanel.tsx +31 -23
  386. package/src/client/sync/SyncRepositoryPicker.tsx +3 -5
  387. package/src/client/sync/SyncSectionPickerDialog.tsx +11 -2
  388. package/src/client/sync/SyncSettingsView.test.ts +20 -1
  389. package/src/client/sync/UnreadableSnapshotsBanner.tsx +1 -1
  390. package/src/client/sync/WorkDirCard.tsx +112 -0
  391. package/src/client/sync/history-model.test.ts +42 -9
  392. package/src/client/sync/history-model.ts +30 -15
  393. package/src/client/sync/sync-api.test.ts +57 -33
  394. package/src/client/sync/sync-api.ts +80 -6
  395. package/src/client/sync/sync-locales.ts +104 -3
  396. package/src/client/sync/sync-view-v2.test.ts +65 -13
  397. package/src/client/sync/sync-view.test.ts +107 -52
  398. package/src/client/sync/sync-view.ts +124 -41
  399. package/src/core/analyzer.ts +31 -9
  400. package/src/core/backup-plan.test.ts +48 -5
  401. package/src/core/backup-plan.ts +51 -20
  402. package/src/core/backup-verify.ts +24 -23
  403. package/src/core/backup.test.ts +76 -0
  404. package/src/core/backup.ts +105 -29
  405. package/src/core/boot-rescue.ts +4 -3
  406. package/src/core/boot-safety.ts +2 -1
  407. package/src/core/command-rollback.test.ts +536 -0
  408. package/src/core/command-rollback.ts +406 -0
  409. package/src/core/consult-source.ts +7 -6
  410. package/src/core/disk-usage.test.ts +257 -1
  411. package/src/core/disk-usage.ts +134 -14
  412. package/src/core/engine-locale.test.ts +593 -0
  413. package/src/core/engine-locale.ts +175 -0
  414. package/src/core/exporter.ts +3 -3
  415. package/src/core/fork-reconcile.test.ts +279 -0
  416. package/src/core/fork-reconcile.ts +234 -0
  417. package/src/core/journal.ts +6 -5
  418. package/src/core/local-plugin-host.ts +3 -2
  419. package/src/core/local-plugin-pack.ts +11 -6
  420. package/src/core/messages-core.ts +661 -0
  421. package/src/core/messages.ts +125 -17
  422. package/src/core/migration-consult.ts +21 -20
  423. package/src/core/migration-history.ts +19 -5
  424. package/src/core/model-tools.ts +28 -27
  425. package/src/core/phase3-host.ts +21 -5
  426. package/src/core/phase3-p2b-fingerprint.test.ts +22 -0
  427. package/src/core/plugin-cli.ts +18 -19
  428. package/src/core/plugin-data-registry.test.ts +113 -0
  429. package/src/core/plugin-data-registry.ts +261 -0
  430. package/src/core/reconcile.test.ts +117 -0
  431. package/src/core/reconcile.ts +85 -17
  432. package/src/core/recovery-orchestrator.ts +19 -18
  433. package/src/core/reinstall.ts +26 -21
  434. package/src/core/restore.ts +20 -17
  435. package/src/core/rollback-file-diff.test.ts +159 -0
  436. package/src/core/rollback-gate.test.ts +66 -0
  437. package/src/core/rollback-gate.ts +61 -0
  438. package/src/core/rollback-ignore-rules.ts +126 -0
  439. package/src/core/rollback-ledger.test.ts +655 -0
  440. package/src/core/rollback-ledger.ts +1314 -0
  441. package/src/core/rollback-point.test.ts +2853 -0
  442. package/src/core/rollback-point.ts +4340 -0
  443. package/src/core/rollback.ts +5 -3
  444. package/src/core/run-registry.ts +6 -5
  445. package/src/core/session-health.ts +15 -0
  446. package/src/core/session-meta.test.ts +117 -1
  447. package/src/core/session-meta.ts +149 -2
  448. package/src/core/session-title.test.ts +127 -0
  449. package/src/core/session-title.ts +189 -0
  450. package/src/core/startup-barrier.test.ts +69 -0
  451. package/src/core/startup-barrier.ts +27 -0
  452. package/src/core/transaction-coordinator.ts +4 -3
  453. package/src/core/types.ts +37 -1
  454. package/src/core/validator.ts +5 -4
  455. package/src/core/verify-recovery.ts +31 -30
  456. package/src/foreign/bundle.ts +3 -2
  457. package/src/foreign/continue.ts +2 -1
  458. package/src/foreign/crush.ts +2 -1
  459. package/src/foreign/file-budget.test.ts +2 -0
  460. package/src/foreign/gemini.ts +2 -1
  461. package/src/foreign/goose.ts +2 -1
  462. package/src/foreign/kilocode.ts +2 -1
  463. package/src/foreign/kimi.ts +2 -1
  464. package/src/foreign/messages.ts +44 -0
  465. package/src/foreign/mimocode.ts +2 -1
  466. package/src/foreign/openclaw.ts +2 -1
  467. package/src/foreign/opencode.ts +2 -1
  468. package/src/foreign/pi.ts +2 -1
  469. package/src/foreign/qoder.ts +2 -1
  470. package/src/foreign/qwen.ts +2 -1
  471. package/src/foreign/registry.test.ts +1 -1
  472. package/src/foreign/registry.ts +8 -7
  473. package/src/foreign/teleagent.ts +2 -1
  474. package/src/foreign/workbuddy.ts +2 -1
  475. package/src/foreign/zcode.ts +2 -1
  476. package/src/foreign/zed.ts +2 -1
  477. package/src/index.facade.test.ts +340 -1
  478. package/src/index.ts +876 -57
  479. package/src/market/git-file-writer.ts +20 -19
  480. package/src/market/github-repos.test.ts +23 -0
  481. package/src/market/github-repos.ts +37 -19
  482. package/src/market/index-parser.ts +31 -30
  483. package/src/market/messages-market.ts +325 -0
  484. package/src/market/my-repo.ts +32 -29
  485. package/src/market/prepare.ts +18 -15
  486. package/src/market/security.ts +20 -19
  487. package/src/market/types.ts +2 -1
  488. package/src/market/url.ts +2 -1
  489. package/src/migrations/index.ts +6 -5
  490. package/src/migrations/messages-migrations.ts +59 -0
  491. package/src/profiles/dsh-profile-launcher.ts +2 -1
  492. package/src/profiles/dsh-profile-runtime.ts +2 -1
  493. package/src/profiles/messages.ts +31 -0
  494. package/src/routes/backup.ts +18 -2
  495. package/src/routes/import.ts +3 -3
  496. package/src/routes/index.ts +1 -1
  497. package/src/routes/kit.ts +9 -3
  498. package/src/routes/market.ts +1 -1
  499. package/src/routes/messages.ts +106 -0
  500. package/src/routes/prefs.ts +31 -2
  501. package/src/routes/profiles.ts +12 -6
  502. package/src/routes/recovery.test.ts +405 -1
  503. package/src/routes/recovery.ts +477 -94
  504. package/src/routes/snapshots.ts +51 -4
  505. package/src/routes/sync.ts +82 -17
  506. package/src/schema/config.ts +53 -20
  507. package/src/schema/index.ts +7 -5
  508. package/src/schema/manifest-schema.test.ts +4 -4
  509. package/src/schema/manifest.ts +19 -18
  510. package/src/schema/messages.ts +120 -0
  511. package/src/schema/registry.test.ts +28 -7
  512. package/src/schema/section-registry.ts +96 -42
  513. package/src/schema/types.ts +2 -1
  514. package/src/schema/versions.ts +6 -5
  515. package/src/security/encryption.ts +16 -15
  516. package/src/security/integrity.ts +8 -7
  517. package/src/security/messages.ts +87 -0
  518. package/src/security/redaction.test.ts +17 -1
  519. package/src/security/redaction.ts +13 -0
  520. package/src/security/secret-scanner.ts +19 -12
  521. package/src/security/security.test.ts +47 -0
  522. package/src/security/vault.ts +2 -1
  523. package/src/security/zip-security.ts +4 -3
  524. package/src/sync/ancestor.ts +2 -1
  525. package/src/sync/autosync-config.ts +21 -0
  526. package/src/sync/autosync-scheduler.test.ts +63 -7
  527. package/src/sync/autosync-scheduler.ts +118 -78
  528. package/src/sync/backup-files.ts +3 -2
  529. package/src/sync/backup-scheduler.ts +6 -5
  530. package/src/sync/blob-store.ts +2 -1
  531. package/src/sync/git/git-transport.test.ts +170 -8
  532. package/src/sync/git/git-transport.ts +176 -2
  533. package/src/sync/github-auth.ts +10 -9
  534. package/src/sync/layout.ts +13 -12
  535. package/src/sync/messages.ts +153 -0
  536. package/src/sync/review-queue.ts +6 -5
  537. package/src/sync/s3/messages.ts +16 -0
  538. package/src/sync/s3/s3-providers.ts +9 -6
  539. package/src/sync/s3/s3-transport.ts +1 -1
  540. package/src/sync/s3/sigv4.ts +3 -1
  541. package/src/sync/snapshot-crypto.ts +6 -5
  542. package/src/sync/snapshot-json.ts +3 -2
  543. package/src/sync/sync-credentials.test.ts +119 -0
  544. package/src/sync/sync-engine-download-tombstone.test.ts +1 -1
  545. package/src/sync/sync-engine.test.ts +226 -8
  546. package/src/sync/sync-engine.ts +154 -52
  547. package/src/sync/sync-history.ts +7 -6
  548. package/src/sync/sync-selection.test.ts +12 -2
  549. package/src/sync/sync-selection.ts +18 -5
  550. package/src/sync/transport.test.ts +1 -0
  551. package/src/sync/ui-prefs.test.ts +44 -1
  552. package/src/sync/ui-prefs.ts +39 -0
  553. package/src/sync/webdav/webdav-transport.ts +4 -1
  554. package/src/ui/artifact-view.ts +4 -3
  555. package/src/ui/commands.test.ts +1 -0
  556. package/src/ui/commands.ts +11 -1
  557. package/src/ui/diff-view.test.ts +9 -1
  558. package/src/ui/diff-view.ts +22 -0
  559. package/src/ui/disk-usage-view.test.ts +113 -6
  560. package/src/ui/disk-usage-view.ts +63 -7
  561. package/src/ui/errors.ts +4 -3
  562. package/src/ui/export-flow.ts +26 -20
  563. package/src/ui/foreign-view.test.ts +43 -0
  564. package/src/ui/foreign-view.ts +30 -3
  565. package/src/ui/i18n.ts +131 -2
  566. package/src/ui/import-wizard.ts +18 -10
  567. package/src/ui/progress.ts +2 -2
  568. package/src/ui/report.ts +7 -6
  569. package/src/ui/rollback-view.test.ts +836 -0
  570. package/src/ui/rollback-view.ts +1405 -0
  571. package/src/ui/selection-model.test.ts +33 -1
  572. package/src/ui/selection-model.ts +53 -0
  573. package/src/ui/session-inventory-view.test.ts +109 -0
  574. package/src/ui/session-inventory-view.ts +138 -3
  575. package/src/ui/session-issue-labels.test.ts +51 -0
  576. package/src/ui/session-issue-labels.ts +70 -0
  577. package/src/ui/sync-settings-view.test.ts +36 -0
  578. package/src/ui/sync-settings-view.ts +49 -0
  579. package/src/ui/test-helpers.ts +2 -1
  580. package/src/ui/types.ts +10 -4
  581. package/src/utils/atomic-write.ts +6 -5
  582. package/src/utils/bundle-scan.test.ts +2 -2
  583. package/src/utils/bundle-scan.ts +12 -1
  584. package/src/utils/env-lock.ts +83 -49
  585. package/src/utils/json.ts +7 -5
  586. package/src/utils/messages-utils.ts +232 -0
  587. package/src/utils/proxy.test.ts +25 -0
  588. package/src/utils/proxy.ts +19 -5
  589. package/src/utils/recursive-walk.test.ts +43 -0
  590. package/src/utils/recursive-walk.ts +75 -1
  591. package/src/utils/session-log-repair.ts +10 -9
  592. package/src/utils/zip.ts +27 -26
  593. package/lib/core/checkpoint.d.ts +0 -362
  594. package/lib/core/checkpoint.js +0 -1309
  595. package/src/core/checkpoint.test.ts +0 -665
  596. package/src/core/checkpoint.ts +0 -1614
@@ -7,7 +7,7 @@
7
7
  * 如实标记 failed + manualHint(人工补录);
8
8
  * - settings 回滚仍走 expectedRevision 乐观锁,避免覆盖导入后用户的新修改。
9
9
  */
10
- import { resolveFileTarget } from './backup.ts';
10
+ import { fsForSection, resolveFileTarget } from './backup.ts';
11
11
  import { isPatchLayerRel, USER_PATCH_FILE } from './patch-layers.ts';
12
12
  import { msgOf } from './messages.ts';
13
13
  import type {
@@ -88,13 +88,15 @@ async function compensateOne(
88
88
  }
89
89
  case 'file': {
90
90
  try {
91
+ // 门面按分区选(pluginData / pluginAssets 在系统 home 根;见 backup.ts 的 fsForSection)
92
+ const fsFacade = fsForSection(ctx, entry.adapter);
91
93
  const abs = resolveFileTarget(ctx, entry.adapter, entry.ref);
92
94
  if (entry.existed && entry.copiedTo && store) {
93
95
  const data = await store.readBlob(entry.snapshotId ?? '', entry.copiedTo);
94
- await ctx.fs.writeFile(abs, data);
96
+ await fsFacade.writeFile(abs, data);
95
97
  } else if (!entry.existed) {
96
98
  // 原文件不存在 → 删除导入写入的文件
97
- if (await ctx.fs.exists(abs)) await ctx.fs.remove(abs);
99
+ if (await fsFacade.exists(abs)) await fsFacade.remove(abs);
98
100
  }
99
101
  return null;
100
102
  } catch (err) {
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { randomBytes } from 'node:crypto'
22
22
  import { zhMsg } from './messages.ts'
23
+ import { engineMsg } from './messages-core.ts'
23
24
  import type { MsgFunc } from './messages.ts'
24
25
 
25
26
  /** run 类型:导出 / 导入 / 自动同步 / 一键同步逐项应用 / 定时备份 / 快照恢复 / 配置档案切换 / recovery 恢复。 */
@@ -80,15 +81,15 @@ export class RunConflictError extends Error {
80
81
  kind === 'export'
81
82
  ? msg('run.conflict.export', { runId })
82
83
  : kind === 'autosync'
83
- ? '自动同步已在进行中'
84
+ ? engineMsg('core.run.autosyncBusy')
84
85
  : kind === 'sync-apply'
85
- ? '一键同步已在进行中'
86
+ ? engineMsg('core.run.syncApplyBusy')
86
87
  : kind === 'backup-schedule'
87
- ? '定时备份已在进行中'
88
+ ? engineMsg('core.run.backupScheduleBusy')
88
89
  : kind === 'restore'
89
- ? '快照恢复已在进行中(请等待当前恢复完成)'
90
+ ? engineMsg('core.run.restoreBusy')
90
91
  : kind === 'recovery'
91
- ? '恢复/回滚已在进行中(请等待当前恢复完成)'
92
+ ? engineMsg('core.run.recoveryBusy')
92
93
  : msg('run.conflict.import', { runId }),
93
94
  )
94
95
  this.name = 'RunConflictError'
@@ -154,12 +154,27 @@ export interface SessionHealthContext {
154
154
  export interface SessionHealthRow {
155
155
  unitId: string;
156
156
  sessionId: string;
157
+ /**
158
+ * 界面标题(**宿主侧补**,不是分类判定的一部分)。
159
+ *
160
+ * 来源 = DSH 会话缓存 `storages/session_projcache.json` 的 `rows.title.val`,由宿主在扫描后用
161
+ * `core/session-meta.ts` 的 `applySessionTitles` 统一补上(本模块零 IO,自己不读盘)。
162
+ * 读不到 = 缺省 → 界面回落显示会话目录名(绝不编造标题)。
163
+ */
164
+ title?: string;
157
165
  projectKey: string;
158
166
  severity: SessionHealthSeverity;
159
167
  issues: SessionHealthIssue[];
160
168
  version?: number;
161
169
  cwd?: string;
162
170
  origin?: string;
171
+ /**
172
+ * 空白会话(从未提问过;宿主侧从会话投影补的**事实**)。
173
+ *
174
+ * 用途:这类会话在 DSH 自己那里也没有标题(显示本地化的「新会话」)—— 界面据此显示同样的
175
+ * 占位名,而不是把 `session-<uuid>` 当标题。只下发事实,文案由界面按当前语言解析。
176
+ */
177
+ blank?: boolean;
163
178
  sizeBytes?: number;
164
179
  mtimeMs?: number;
165
180
  }
@@ -10,7 +10,7 @@ import assert from 'node:assert/strict';
10
10
 
11
11
  import { makeContext, MemFs } from '../adapters/test-helpers.ts';
12
12
  import type { ExportUnit } from './types.ts';
13
- import { applySessionMeta, applySessionMetaToPlanItems, applySessionParentLinks, clearSessionMetaCache, projectKeyOf, readSessionMeta, subagentParentMap } from './session-meta.ts';
13
+ import { applySessionMeta, applySessionMetaToPlanItems, applySessionParentLinks, applySessionTitles, clearSessionMetaCache, projectKeyOf, readSessionMeta, readSessionMetaWithTitles, sessionBareKeyOf, subagentParentMap } from './session-meta.ts';
14
14
 
15
15
  const HOME = 'C:\\Users\\alice';
16
16
  const UUID_A = '19495390-402b-463d-a4cc-882287c1f04e';
@@ -314,3 +314,119 @@ test('clearSessionMetaCache:清空后下一次调用重新读盘', async () =>
314
314
  assert.equal(fs.reads.length, 4, '清空后必须重读(宿主可在存储重载信号上调用)');
315
315
  clearSessionMetaCache();
316
316
  });
317
+
318
+ /**
319
+ * 体检行补标题(2026-10-12 用户反馈:会话体检列表只有 `session-<uuid>`,同屏十几条里认不出
320
+ * 哪条是自己的对话)。查表口径必须与导出选择器**逐字同一套**(裸键归一化)。
321
+ */
322
+ test('applySessionTitles:体检行补对话标题(两种目录名形态都命中;没有标题绝不编造)', async () => {
323
+ const meta = await readSessionMeta(await seedStorages(HOME));
324
+ const rows = [
325
+ { unitId: `${PROJECT}/session-${UUID_A}`, sessionId: `session-${UUID_A}`, severity: 'invisible' },
326
+ { unitId: `${PROJECT}/${UUID_A}`, sessionId: UUID_A, severity: 'invisible' },
327
+ { unitId: `${OTHER_PROJECT}/${UUID_B}`, sessionId: UUID_B, severity: 'invisible' },
328
+ ];
329
+ const out = applySessionTitles(rows, meta);
330
+ assert.equal(out[0]?.title, '推送更新到 GitHub 并发布新版本', '带 session- 前缀的目录名同样要命中(查表前归一化)');
331
+ assert.equal(out[1]?.title, '推送更新到 GitHub 并发布新版本', '裸 uuid 形态同样命中');
332
+ assert.equal(out[2]?.title, undefined, '缓存里没有标题 → 不加字段(界面回落目录名,不编造)');
333
+ assert.deepEqual(out.map((r) => r.unitId), rows.map((r) => r.unitId), '只加展示字段,不动行上的任何既有字段');
334
+ assert.deepEqual(out.map((r) => r.severity), rows.map((r) => r.severity), '严重级等事实字段逐字不变');
335
+
336
+ // 读不到 DSH 缓存(空索引)→ 原样返回,绝不抛错(体检本身仍然可用)
337
+ const empty = { bySessionId: new Map(), workspaceTitleByProjectKey: new Map(), workspacePathByProjectKey: new Map() };
338
+ assert.deepEqual(applySessionTitles(rows, empty).map((r) => r.title), [undefined, undefined, undefined]);
339
+ });
340
+ /* ---------------- 逐会话投影文件(全量标题来源)----------------
341
+ * 用户要求(2026-10-12):「历史对话的文本**始终**用对话标题,而不是只取缓存里的标题」。
342
+ * 本机实测(2026-10-12,1347 个会话目录):聚合缓存只覆盖 445 条,而逐会话投影文件覆盖**每一条**
343
+ * —— 1150 份带标题,其余 197 份是 blank=true 的空白会话(DSH 自己给它们显示「新会话」)。
344
+ */
345
+
346
+ /** 写一份逐会话投影文件(真机形状:`{ version, record: { identity, rows } }`)。 */
347
+ async function seedProjection(
348
+ ctx: ReturnType<typeof makeContext>,
349
+ fileName: string,
350
+ rows: Record<string, unknown>,
351
+ ): Promise<void> {
352
+ const body = { version: 7, record: { identity: { cwd: 'D:\\Projects' }, rows } };
353
+ // 文件名 = 会话目录名 + `.json`(真机形态);调用方可以省掉后缀
354
+ const name = fileName.endsWith('.json') ? fileName : fileName + '.json';
355
+ await ctx.fs.writeFile('storages/session_projcache/sessions/' + name, Buffer.from(JSON.stringify(body), 'utf8'));
356
+ }
357
+
358
+ test('readSessionMetaWithTitles:聚合缓存缺标题时读逐会话投影文件补上(两种文件名形态都认)', async () => {
359
+ clearSessionMetaCache();
360
+ const ctx = await seedStorages(HOME); // 聚合里:A 有标题、B 没有
361
+ // B 的投影文件(真机:文件名 = 会话目录名,两种形态并存)
362
+ await seedProjection(ctx, 'session-' + UUID_B, {
363
+ title: { ver: 1, seq: 3, val: '投影里的标题' },
364
+ sessionListMetadata: { val: { blank: false } },
365
+ });
366
+ // 另一条只有裸 uuid 形态的投影文件
367
+ const bareId = 'ffffffff-0000-0000-0000-000000000000';
368
+ await seedProjection(ctx, bareId + '.json', { title: { ver: 1, seq: 2, val: '裸文件名形态的标题' } });
369
+
370
+ const index = await readSessionMetaWithTitles(ctx, ['sessions:' + PROJECT + '/session-' + UUID_A, 'sessions:' + PROJECT + '/' + UUID_B, bareId, 'nonexistent-id']);
371
+ assert.equal(index.bySessionId.get(UUID_A)?.title, '推送更新到 GitHub 并发布新版本', '聚合缓存里的标题原样保留(不被投影覆盖)');
372
+ assert.equal(index.bySessionId.get(UUID_B)?.title, '投影里的标题', '聚合缺标题 → 用逐会话投影补上');
373
+ assert.equal(index.bySessionId.get(bareId)?.title, '裸文件名形态的标题', '两种投影文件名形态都要认');
374
+ assert.equal(index.bySessionId.get('nonexistent-id')?.title, undefined, '两边都没有 → 不编造');
375
+
376
+ // 只读、纯:不改原索引对象
377
+ const plain = await readSessionMeta(ctx);
378
+ assert.equal(plain.bySessionId.get(UUID_B)?.title, undefined, 'readSessionMetaWithTitles 不得污染 readSessionMeta 的缓存索引');
379
+ });
380
+
381
+ test('readSessionMetaWithTitles:空白会话下发 blank 事实(没有标题可显示,界面显示「新会话」)', async () => {
382
+ clearSessionMetaCache();
383
+ const ctx = await seedStorages(HOME);
384
+ await seedProjection(ctx, UUID_B + '.json', { sessionListMetadata: { val: { blank: true, lastPromptAt: null } } });
385
+ const index = await readSessionMetaWithTitles(ctx, [UUID_B]);
386
+ assert.equal(index.bySessionId.get(UUID_B)?.blank, true, '空白事实必须下发(界面据此显示本地化的「新会话」)');
387
+ assert.equal(index.bySessionId.get(UUID_B)?.title, undefined, '空白会话没有标题 —— 绝不编造一个');
388
+ });
389
+
390
+ test('readSessionMetaWithTitles:只读「聚合里缺标题」的那些,且 mtime 未变时第二次不再读盘', async () => {
391
+ clearSessionMetaCache();
392
+ const { ctx, fs } = await seededCounting('C:\\Users\\proj-cache', 4000);
393
+ await seedProjection(ctx, 'session-' + UUID_B, { title: { ver: 1, seq: 2, val: '投影标题 B' } });
394
+ fs.setMtime('storages/session_projcache/sessions/session-' + UUID_B + '.json', 4100);
395
+ const first = await readSessionMetaWithTitles(ctx, [UUID_A, UUID_B]);
396
+ assert.equal(first.bySessionId.get(UUID_B)?.title, '投影标题 B');
397
+ const after = fs.reads.length;
398
+ const second = await readSessionMetaWithTitles(ctx, [UUID_A, UUID_B]);
399
+ assert.equal(second.bySessionId.get(UUID_B)?.title, '投影标题 B');
400
+ assert.equal(fs.reads.length, after, '同一批会话重复查询不得重复读盘(投影文件走 mtime 缓存)');
401
+ // A 在聚合里已有标题 → 不该去读它的投影文件
402
+ assert.equal(fs.reads.filter((rel) => rel.includes('session-' + UUID_A + '.json')).length, 0, '已有标题的会话不得多读一份文件');
403
+ });
404
+
405
+ test('sessionBareKeyOf:单元 id / 目录名两种入参都归一化到裸键', () => {
406
+ assert.equal(sessionBareKeyOf('sessions:' + PROJECT + '/session-' + UUID_A), UUID_A);
407
+ assert.equal(sessionBareKeyOf('session-' + UUID_A), UUID_A);
408
+ assert.equal(sessionBareKeyOf(UUID_A), UUID_A);
409
+ });
410
+
411
+
412
+ test('blank 事实:导出单元与体检行都透传(界面据此显示本地化「新会话」而不是 uuid)', async () => {
413
+ clearSessionMetaCache();
414
+ const ctx = await seedStorages(HOME);
415
+ await seedProjection(ctx, UUID_B + '.json', { sessionListMetadata: { val: { blank: true } } });
416
+ const index = await readSessionMetaWithTitles(ctx, [UUID_B]);
417
+
418
+ const units = applySessionMeta([unit(OTHER_PROJECT + '/' + UUID_B)], index);
419
+ assert.equal(units[0]?.blank, true, '空白会话的导出单元必须带 blank(ContentPicker 显示「新会话」)');
420
+ assert.equal(units[0]?.label, OTHER_PROJECT + '/' + UUID_B, '没有标题 → label 仍是目录名(界面按 blank 显示占位名)');
421
+
422
+ const rows = applySessionTitles([{ unitId: OTHER_PROJECT + '/' + UUID_B, sessionId: UUID_B }], index);
423
+ assert.equal(rows[0]?.blank, true, '体检行同样要带 blank');
424
+ assert.equal(rows[0]?.title, undefined, '空白会话没有标题');
425
+
426
+ const plan = applySessionMetaToPlanItems(
427
+ [{ id: 'sessions:' + OTHER_PROJECT + '/' + UUID_B, unitId: 'sessions:' + OTHER_PROJECT + '/' + UUID_B, adapter: 'sessions', kind: 'Create', description: 'x' }],
428
+ index,
429
+ );
430
+ assert.equal(plan[0]?.blank, true, '导入计划项同样要带 blank');
431
+ });
432
+
@@ -17,6 +17,14 @@ import type { ExportUnit, HostContext, SessionParentRelation } from './types.ts'
17
17
 
18
18
  const SESSION_PROJCACHE = 'storages/session_projcache.json';
19
19
  const WORKSPACE_STORE = 'storages/workspace.json';
20
+ /**
21
+ * **逐会话**投影存储目录(`storages/session_projcache/sessions/<会话目录名>.json`)。
22
+ *
23
+ * 与 `session_projcache.json` 的关系:那个是**会淘汰的近期聚合**(本机实测只覆盖 445/1347 条会话),
24
+ * 这个目录才是 DSH 为**每条会话**留下的投影(本机 1349 份,其中 1150 份带标题)。只读聚合缓存会让
25
+ * 其余会话在选择器 / 体检列表里退化成 uuid 目录名 —— 用户认不出哪条是哪条。
26
+ */
27
+ const SESSION_PROJECTION_DIR = 'storages/session_projcache/sessions';
20
28
 
21
29
  // projectKeyOf 已下移到**零依赖**模块 `session-select.ts`(客户端勾选联动要按 cwd 目录键认领工作区,
22
30
  // 而本模块会用 Buffer 读 DSH 存储缓存,不能进浏览器 bundle)。这里 re-export,既有导入路径逐字不变。
@@ -78,9 +86,19 @@ function asRecord(value: unknown): Record<string, unknown> | null {
78
86
  */
79
87
  const metaCache = new Map<string, { key: string; index: SessionMetaIndex }>();
80
88
 
81
- /** 清空索引缓存(测试用;宿主在收到「DSH 存储已重载」信号时也可调用)。 */
89
+ /**
90
+ * 逐会话投影文件的**进程内缓存**(键含 homeDir + 相对路径,失效判据 = 该文件 mtime)。
91
+ *
92
+ * 为什么需要:导出选择器一次要补上千条会话的标题,而同一批会话在一次交互里会被反复查询
93
+ * (打开选择器 → 勾选 → 会话↔工作区联动补跑)。每次重读 1300 个小文件会把「打开弹窗」拖成秒级。
94
+ * 与 `metaCache` 同一条纪律:**读不到 mtime 就不缓存**(宁可贵一点,也绝不拿陈旧标题骗用户)。
95
+ */
96
+ const projectionCache = new Map<string, { key: string; facts: ProjectedSessionFacts | undefined }>();
97
+
98
+ /** 清空索引与逐会话投影缓存(测试用;宿主在收到「DSH 存储已重载」信号时也可调用)。 */
82
99
  export function clearSessionMetaCache(): void {
83
100
  metaCache.clear();
101
+ projectionCache.clear();
84
102
  }
85
103
 
86
104
  /** 文件 mtime(毫秒;门面未实现 / 读不到 → null = 无法判定新鲜度,调用方不得缓存)。 */
@@ -154,6 +172,102 @@ async function loadSessionMeta(ctx: HostContext): Promise<SessionMetaIndex> {
154
172
  return index;
155
173
  }
156
174
 
175
+ /** 逐会话投影文件里**展示所需**的事实(只取这两项;文件读不到 = undefined)。 */
176
+ interface ProjectedSessionFacts {
177
+ /** 界面标题(`record.rows.title.val`) */
178
+ title?: string;
179
+ /** 空白会话(`record.rows.sessionListMetadata.val.blank`)—— DSH 自己也据此显示「新会话」 */
180
+ blank?: boolean;
181
+ }
182
+
183
+ /**
184
+ * 会话单元 id(`sessions:<项目键>/<目录名>`)或裸目录名 → **归一化裸键**(`sessionIdKey`)。
185
+ *
186
+ * 为什么要有:调用方手上是单元 id / 计划项 id(末段才是目录名),而索引与投影文件都按**裸键**存。
187
+ */
188
+ export function sessionBareKeyOf(ref: string): string {
189
+ const slash = ref.lastIndexOf('/');
190
+ return sessionIdKey(slash >= 0 ? ref.slice(slash + 1) : ref);
191
+ }
192
+
193
+ /** 逐会话投影文件的候选名(DSH 实测两种目录名形态并存:`session-<uuid>.json` 与 `<uuid>.json`)。 */
194
+ function projectionFileNames(bare: string): string[] {
195
+ return ['session-' + bare + '.json', bare + '.json'];
196
+ }
197
+
198
+ /**
199
+ * 读一条会话的**逐会话投影文件**(`storages/session_projcache/sessions/<会话目录名>.json`)。
200
+ *
201
+ * 形状与聚合缓存略有差异:文件里是 `{ version, record: { identity, rows } }`,聚合里是 `{ identity, rows }`
202
+ * —— 两种都认(`record` 缺省即顶层)。**只读、尽力而为、绝不抛错**:读不到 / 形状不认 → undefined。
203
+ */
204
+ async function readProjectedSession(ctx: HostContext, bare: string): Promise<ProjectedSessionFacts | undefined> {
205
+ for (const name of projectionFileNames(bare)) {
206
+ const rel = SESSION_PROJECTION_DIR + '/' + name;
207
+ const cacheId = ctx.homeDir + '\u0000' + rel;
208
+ const mtime = await mtimeOf(ctx, rel);
209
+ if (mtime !== null) {
210
+ const hit = projectionCache.get(cacheId);
211
+ if (hit !== undefined && hit.key === String(mtime)) return hit.facts;
212
+ }
213
+ let facts: ProjectedSessionFacts | undefined;
214
+ const raw = asRecord(await readJsonSafe(ctx, rel));
215
+ if (raw !== null) {
216
+ const rec = asRecord(raw['record']) ?? raw;
217
+ const rows = asRecord(rec['rows']);
218
+ const title = asRecord(rows?.['title'])?.['val'];
219
+ const listMeta = asRecord(asRecord(rows?.['sessionListMetadata'])?.['val']);
220
+ const out: ProjectedSessionFacts = {};
221
+ if (typeof title === 'string' && title.trim() !== '') out.title = title.trim();
222
+ if (listMeta?.['blank'] === true) out.blank = true;
223
+ facts = out;
224
+ }
225
+ if (mtime === null) projectionCache.delete(cacheId);
226
+ else projectionCache.set(cacheId, { key: String(mtime), facts });
227
+ if (facts !== undefined) return facts;
228
+ }
229
+ return undefined;
230
+ }
231
+
232
+ /** 一次并发读多少份投影文件(小文件;32 路实测把 1300 条压到百毫秒级)。 */
233
+ const PROJECTION_CONCURRENCY = 32;
234
+
235
+ /**
236
+ * 「界面标题」的**全量**解析:聚合缓存优先,缺的按需读**逐会话投影文件**。
237
+ *
238
+ * 用户要求(2026-10-12):「历史对话的文本**始终**用对话标题,而不是只取缓存里的标题」。
239
+ * 只读聚合缓存(`session_projcache.json`)时,本机 1347 条会话里有 902 条拿不到标题 → 界面退化成
240
+ * `session-<uuid>`,用户在同屏十几条里认不出哪条是自己的对话。
241
+ *
242
+ * 成本:只读「聚合缓存里没有标题」的那些(本机约 900 条小文件;实测全量顺序读 1349 份 = 361 ms),
243
+ * 32 路并发 + mtime 缓存 —— 同一批会话重复查询(打开选择器 / 勾选 / 联动补跑)不再重复读盘。
244
+ *
245
+ * 语义:**只增不改** —— 已缓存的标题与 blank 一律保留;实在读不到的会话原样返回(界面回落目录名)。
246
+ *
247
+ * @param refs 会话单元 id(`sessions:<项目键>/<目录名>`)或裸目录名,混着传也行
248
+ */
249
+ export async function readSessionMetaWithTitles(ctx: HostContext, refs: readonly string[]): Promise<SessionMetaIndex> {
250
+ const index = await readSessionMeta(ctx);
251
+ const need = [...new Set(refs.map((ref) => sessionBareKeyOf(ref)).filter((bare) => bare !== ''))]
252
+ .filter((bare) => index.bySessionId.get(bare)?.title === undefined);
253
+ if (need.length === 0) return index;
254
+ const bySessionId = new Map(index.bySessionId);
255
+ for (let i = 0; i < need.length; i += PROJECTION_CONCURRENCY) {
256
+ const chunk = need.slice(i, i + PROJECTION_CONCURRENCY);
257
+ const found = await Promise.all(chunk.map(async (bare) => [bare, await readProjectedSession(ctx, bare)] as const));
258
+ for (const [bare, facts] of found) {
259
+ if (facts === undefined) continue;
260
+ const existing = bySessionId.get(bare) ?? {};
261
+ bySessionId.set(bare, {
262
+ ...existing,
263
+ ...(facts.title !== undefined ? { title: facts.title } : {}),
264
+ ...(facts.blank !== undefined ? { blank: facts.blank } : {}),
265
+ });
266
+ }
267
+ }
268
+ return { ...index, bySessionId };
269
+ }
270
+
157
271
  /** 单元 id(`sessions:<项目键>/<会话目录名>`)→ 会话目录名;形状不符 → null。 */
158
272
  function sessionIdOfUnit(sectionId: string, unitId: string): { projectKey: string; sessionId: string } | null {
159
273
  const prefix = sectionId + ':';
@@ -176,7 +290,7 @@ function sessionIdOfUnit(sectionId: string, unitId: string): { projectKey: strin
176
290
  */
177
291
  export function applySessionMetaToPlanItems<
178
292
  T extends { id: string; unitId?: string; adapter: string; label?: string; group?: string },
179
- >(items: readonly T[], index: SessionMetaIndex, sectionId = 'sessions'): T[] {
293
+ >(items: readonly T[], index: SessionMetaIndex, sectionId = 'sessions'): (T & { blank?: boolean })[] {
180
294
  return items.map((item) => {
181
295
  if (item.adapter !== sectionId) return item;
182
296
  const parsed = sessionIdOfUnit(sectionId, item.unitId ?? item.id);
@@ -188,6 +302,8 @@ export function applySessionMetaToPlanItems<
188
302
  return {
189
303
  ...item,
190
304
  ...(meta?.title !== undefined ? { label: meta.title } : {}),
305
+ // 空白会话(从未提问):DSH 自己显示「新会话」而不是 uuid;标记随计划项下发,由界面本地化
306
+ ...(meta?.blank === true ? { blank: true } : {}),
191
307
  group,
192
308
  };
193
309
  });
@@ -234,6 +350,35 @@ export function applySessionParentLinks<T extends { id: string; parentSessionId?
234
350
  });
235
351
  }
236
352
 
353
+ /**
354
+ * 给**会话体检行**补上「界面标题」(行上的 `sessionId` = 会话目录名,索引键是裸键 → 查表前必须归一化)。
355
+ *
356
+ * 为什么需要:体检列表此前只显示目录名(`session-20d2722c-…`),用户在同一屏十几条里认不出哪条是自己的
357
+ * 对话(2026-10-12 用户反馈截图实证)。标题的唯一可靠来源就是 DSH 自己的会话缓存
358
+ * (`storages/session_projcache.json` 的 `rows.title.val`)—— 与导出选择器**同一份索引、同一套
359
+ * `sessionIdKey` 归一化口径**,绝不另写一套查表规则。
360
+ *
361
+ * 只读、纯函数、**绝不编造**:索引里没有这条会话 / 没有标题 → 原样返回(界面回落显示目录名)。
362
+ * 索引本身由 `readSessionMetaWithTitles()` 产出(聚合缓存 + **逐会话投影文件**兜底,见那里)。
363
+ */
364
+ export function applySessionTitles<T extends { sessionId?: string | null }>(
365
+ rows: readonly T[],
366
+ index: SessionMetaIndex,
367
+ ): (T & { title?: string; blank?: boolean })[] {
368
+ return rows.map((row) => {
369
+ // 会话 id 缺失 / 空串的记录**一个字段都不加**(回滚台账里有这类历史记录:绝不猜它属于哪条对话)
370
+ if (typeof row.sessionId !== 'string' || row.sessionId === '') return row;
371
+ const meta = index.bySessionId.get(sessionIdKey(row.sessionId));
372
+ if (meta === undefined) return row;
373
+ return {
374
+ ...row,
375
+ ...(meta.title !== undefined ? { title: meta.title } : {}),
376
+ // 空白会话(从未提问):没有标题,界面据此显示本地化的「新会话」(与 DSH 的 session.new 同口径)
377
+ ...(meta.blank === true ? { blank: true } : {}),
378
+ };
379
+ });
380
+ }
381
+
237
382
  /**
238
383
  * 给 sessions 分区的单元补上「界面标题 + 工作区分组」,并按「工作区 → 最近在前」排序。
239
384
  *
@@ -267,6 +412,8 @@ export function applySessionMeta(
267
412
  label: meta?.title ?? unit.label,
268
413
  ...(detail !== undefined ? { detail } : {}),
269
414
  group: workspaceTitle ?? parsed.projectKey,
415
+ // 空白会话(从未提问):没有标题可显示,标记出来让界面显示本地化的「新会话」(与 DSH 同口径)
416
+ ...(meta?.blank === true ? { blank: true } : {}),
270
417
  };
271
418
  });
272
419
  // 排序:先按工作区(保持首次出现顺序),组内最近活跃的在前;无时间的排在组内末尾。
@@ -0,0 +1,127 @@
1
+ /**
2
+ * 「会话标题」的**日志兜底**单测(2026-10-12 用户要求:缓存里没有这条会话,就自动读这条会话自己的名字)。
3
+ *
4
+ * 钉住的语义:`session/title` 事件 → 标题(多条取最后一条 = 用户改名压过自动标题);读不到一律 null
5
+ * (界面回落显示会话 id,**绝不从首条消息编造**);越界 / 形状不符的 unitId 不拼路径;
6
+ * 缓存按 mtime+size 失效;一次请求的读盘有界(同 unitId 只读一次)。
7
+ */
8
+ import test from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import fs from 'node:fs/promises';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+
14
+ import { encodeZstdFrame } from '../utils/zstd-frame.ts';
15
+ import {
16
+ applySessionLogTitles,
17
+ clearSessionTitleCache,
18
+ readSessionTitleFromLog,
19
+ } from './session-title.ts';
20
+
21
+ async function withTmp<T>(fn: (dir: string) => Promise<T>): Promise<T> {
22
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-cm-session-title-'));
23
+ try {
24
+ return await fn(dir);
25
+ } finally {
26
+ await fs.rm(dir, { recursive: true, force: true });
27
+ }
28
+ }
29
+
30
+ /** 造一个会话日志:第 1 帧 = header 行,其余每行一帧(与 DSH 的多帧容器同形)。 */
31
+ function logBytes(id: string, lines: readonly string[]): Buffer {
32
+ const frames: Buffer[] = [encodeZstdFrame(Buffer.from(JSON.stringify({ version: 3, id, cwd: 'D:/P' }) + '\n', 'utf8'))];
33
+ for (const line of lines) frames.push(encodeZstdFrame(Buffer.from(line, 'utf8')));
34
+ return Buffer.concat(frames);
35
+ }
36
+
37
+ /** `session/title` 事件行(DSH 的形状:data = { title, messageSeqs, source })。 */
38
+ function titleLine(title: string, source = 'auto'): string {
39
+ return JSON.stringify({ seq: 9, type: 'session/title', data: { title, messageSeqs: [1], source } }) + '\n';
40
+ }
41
+
42
+ const PROJECT_KEY = '--D--Projects-personal-demo--';
43
+ const UNIT = PROJECT_KEY + '/session-0b46a72f-d25e-423f-af4b-ef768938292e';
44
+
45
+ async function writeUnit(sessionsDir: string, unitId: string, bytes: Buffer, name = 'session.jsonl'): Promise<string> {
46
+ const dir = path.join(sessionsDir, ...unitId.split('/'));
47
+ await fs.mkdir(dir, { recursive: true });
48
+ const file = path.join(dir, name);
49
+ await fs.writeFile(file, bytes);
50
+ return file;
51
+ }
52
+
53
+ test('session/title 事件 → 标题;多条取最后一条(用户改名压过自动标题)', async () => {
54
+ await withTmp(async (dir) => {
55
+ clearSessionTitleCache();
56
+ await writeUnit(dir, UNIT, logBytes('session-a', [titleLine('自动生成的标题'), titleLine('用户改的名字', 'user')]));
57
+ assert.equal(await readSessionTitleFromLog(dir, UNIT), '用户改的名字');
58
+ });
59
+ });
60
+
61
+ test('没有标题事件 / 空标题 / 形状不符 → null(绝不编造)', async () => {
62
+ await withTmp(async (dir) => {
63
+ clearSessionTitleCache();
64
+ await writeUnit(dir, UNIT, logBytes('session-a', ['{"seq":1,"type":"message"}\n']));
65
+ assert.equal(await readSessionTitleFromLog(dir, UNIT), null, '没有 session/title 事件 → null');
66
+
67
+ const emptyTitle = UNIT + '-empty';
68
+ await writeUnit(dir, emptyTitle, logBytes('session-b', [titleLine(' ')]));
69
+ assert.equal(await readSessionTitleFromLog(dir, emptyTitle), null, '空白标题不算标题');
70
+
71
+ const wrongShape = UNIT + '-shape';
72
+ await writeUnit(dir, wrongShape, logBytes('session-c', ['{"seq":2,"type":"session/title","data":null}\n']));
73
+ assert.equal(await readSessionTitleFromLog(dir, wrongShape), null, 'data 形状不符 → null');
74
+ });
75
+ });
76
+
77
+ test('目录 / 文件缺失、unitId 形状不符(单段 / 越界)→ null(不拼可疑路径)', async () => {
78
+ await withTmp(async (dir) => {
79
+ clearSessionTitleCache();
80
+ assert.equal(await readSessionTitleFromLog(dir, UNIT), null, '目录不存在 → null');
81
+ assert.equal(await readSessionTitleFromLog(dir, 'session-only'), null, '单段 unitId → null');
82
+ assert.equal(await readSessionTitleFromLog(dir, PROJECT_KEY + '/../..'), null, '越界段 → null');
83
+ const dirOnly = PROJECT_KEY + '/session-no-log';
84
+ await fs.mkdir(path.join(dir, ...dirOnly.split('/')), { recursive: true });
85
+ await fs.writeFile(path.join(dir, ...dirOnly.split('/'), 'notes.txt'), 'not a log');
86
+ assert.equal(await readSessionTitleFromLog(dir, dirOnly), null, '目录里没有会话日志 → null');
87
+ });
88
+ });
89
+
90
+ test('缓存按 mtime+size 失效:日志增长后能读到新落盘的标题', async () => {
91
+ await withTmp(async (dir) => {
92
+ clearSessionTitleCache();
93
+ const file = await writeUnit(dir, UNIT, logBytes('session-a', ['{"seq":1}\n']));
94
+ assert.equal(await readSessionTitleFromLog(dir, UNIT), null, '第一次:还没有标题');
95
+ // 追加一帧(保留原字节)⇒ 文件变大 ⇒ 缓存必须失效,下次读得到刚落盘的标题
96
+ await fs.appendFile(file, encodeZstdFrame(Buffer.from(titleLine('刚生成的标题'), 'utf8')));
97
+ assert.equal(await readSessionTitleFromLog(dir, UNIT), '刚生成的标题');
98
+ });
99
+ });
100
+
101
+ test('applySessionLogTitles:已有标题不动、同 unitId 只读一次、读盘次数有上界', async () => {
102
+ await withTmp(async (dir) => {
103
+ clearSessionTitleCache();
104
+ const unitB = PROJECT_KEY + '/session-b';
105
+ await writeUnit(dir, UNIT, logBytes('session-a', [titleLine('A 的标题')]));
106
+ await writeUnit(dir, unitB, logBytes('session-b', [titleLine('B 的标题')]));
107
+
108
+ const rows = await applySessionLogTitles([
109
+ { sessionId: 'a1', unitId: UNIT },
110
+ { sessionId: 'a1', unitId: UNIT },
111
+ { sessionId: 'b1', unitId: unitB, title: '缓存里已有' },
112
+ { sessionId: 'c1', unitId: null },
113
+ ], dir);
114
+ assert.equal(rows[0]?.title, 'A 的标题');
115
+ assert.equal(rows[1]?.title, 'A 的标题', '同 unitId 的其余行复用同一次读取');
116
+ assert.equal(rows[2]?.title, '缓存里已有', '已有标题的行原样保留(不覆盖缓存结论)');
117
+ assert.equal(rows[3]?.title, undefined, '没有 unitId 的行不猜归属');
118
+
119
+ // 上界:maxReads=1 时,第二个**不同**会话不再读盘(保持无标题,界面回落显示 id)
120
+ const capped = await applySessionLogTitles([
121
+ { sessionId: 'a1', unitId: UNIT },
122
+ { sessionId: 'b1', unitId: unitB },
123
+ ], dir, { maxReads: 1 });
124
+ assert.equal(capped[0]?.title, 'A 的标题');
125
+ assert.equal(capped[1]?.title, undefined, '超过上界的会话不再读盘(绝不把盘读穿)');
126
+ });
127
+ });