dsh-config-manager 0.1.69 → 0.1.71

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 (719) hide show
  1. package/README.md +59 -18
  2. package/README.zh-CN.md +29 -9
  3. package/lib/adapters/credentials.js +6 -3
  4. package/lib/adapters/file-collection.d.ts +21 -1
  5. package/lib/adapters/file-collection.js +32 -8
  6. package/lib/adapters/mcp.d.ts +12 -2
  7. package/lib/adapters/mcp.js +30 -13
  8. package/lib/adapters/plugin-files.d.ts +8 -0
  9. package/lib/adapters/plugin-files.js +36 -4
  10. package/lib/adapters/plugins.d.ts +28 -2
  11. package/lib/adapters/plugins.js +204 -55
  12. package/lib/adapters/prompts.d.ts +12 -2
  13. package/lib/adapters/prompts.js +34 -19
  14. package/lib/adapters/providers.js +5 -4
  15. package/lib/adapters/self.d.ts +20 -0
  16. package/lib/adapters/self.js +73 -0
  17. package/lib/adapters/sessions.d.ts +8 -0
  18. package/lib/adapters/sessions.js +47 -1
  19. package/lib/adapters/settings.d.ts +21 -3
  20. package/lib/adapters/settings.js +44 -7
  21. package/lib/adapters/skills.d.ts +36 -4
  22. package/lib/adapters/skills.js +222 -3
  23. package/lib/adapters/test-helpers.d.ts +31 -2
  24. package/lib/adapters/test-helpers.js +42 -5
  25. package/lib/adapters/workspaces.js +97 -5
  26. package/lib/cli/actions.d.ts +50 -3
  27. package/lib/cli/actions.js +103 -11
  28. package/lib/cli/help.d.ts +65 -0
  29. package/lib/cli/help.js +517 -0
  30. package/lib/cli/import-source.d.ts +32 -0
  31. package/lib/cli/import-source.js +178 -0
  32. package/lib/cli/index.d.ts +18 -2
  33. package/lib/cli/index.js +118 -90
  34. package/lib/cli/web/client-script.d.ts +8 -0
  35. package/lib/cli/web/client-script.js +11 -0
  36. package/lib/cli/web/http.d.ts +5 -0
  37. package/lib/cli/web/http.js +2 -1
  38. package/lib/cli/web/page.d.ts +16 -6
  39. package/lib/cli/web/page.js +102 -25
  40. package/lib/cli/web/routes.js +24 -5
  41. package/lib/cli/web/server.js +23 -2
  42. package/lib/client.d.ts +1019 -36
  43. package/lib/client.js +5522 -1564
  44. package/lib/core/analyzer.d.ts +15 -1
  45. package/lib/core/analyzer.js +114 -13
  46. package/lib/core/backup-plan.js +17 -1
  47. package/lib/core/backup.d.ts +8 -0
  48. package/lib/core/backup.js +28 -6
  49. package/lib/core/cache-cleaner.d.ts +10 -0
  50. package/lib/core/cache-cleaner.js +63 -22
  51. package/lib/core/checkpoint.d.ts +362 -0
  52. package/lib/core/checkpoint.js +1309 -0
  53. package/lib/core/journal.d.ts +37 -0
  54. package/lib/core/journal.js +69 -0
  55. package/lib/core/messages.d.ts +13 -0
  56. package/lib/core/messages.js +26 -0
  57. package/lib/core/model-tools.js +3 -0
  58. package/lib/core/patch-layers.d.ts +83 -0
  59. package/lib/core/patch-layers.js +110 -0
  60. package/lib/core/restore.d.ts +21 -0
  61. package/lib/core/restore.js +37 -3
  62. package/lib/core/rollback.js +6 -3
  63. package/lib/core/self-update.d.ts +52 -0
  64. package/lib/core/self-update.js +70 -0
  65. package/lib/core/session-health.d.ts +29 -5
  66. package/lib/core/session-health.js +28 -3
  67. package/lib/core/types.d.ts +126 -0
  68. package/lib/foreign/antigravity.d.ts +28 -0
  69. package/lib/foreign/antigravity.js +157 -0
  70. package/lib/foreign/bundle.d.ts +25 -0
  71. package/lib/foreign/bundle.js +79 -0
  72. package/lib/foreign/chatgpt.d.ts +6 -0
  73. package/lib/foreign/chatgpt.js +25 -0
  74. package/lib/foreign/claude-code.d.ts +3 -0
  75. package/lib/foreign/claude-code.js +85 -0
  76. package/lib/foreign/claude-sessions.d.ts +108 -0
  77. package/lib/foreign/claude-sessions.js +299 -0
  78. package/lib/foreign/cline.d.ts +6 -0
  79. package/lib/foreign/cline.js +27 -0
  80. package/lib/foreign/codex.d.ts +83 -0
  81. package/lib/foreign/codex.js +348 -0
  82. package/lib/foreign/continue.d.ts +9 -0
  83. package/lib/foreign/continue.js +65 -0
  84. package/lib/foreign/copilot.d.ts +39 -0
  85. package/lib/foreign/copilot.js +95 -0
  86. package/lib/foreign/crush.d.ts +11 -0
  87. package/lib/foreign/crush.js +57 -0
  88. package/lib/foreign/cursor.d.ts +87 -0
  89. package/lib/foreign/cursor.js +171 -0
  90. package/lib/foreign/dsh.d.ts +30 -0
  91. package/lib/foreign/dsh.js +101 -0
  92. package/lib/foreign/dsh4.d.ts +3 -0
  93. package/lib/foreign/dsh4.js +13 -0
  94. package/lib/foreign/gemini.d.ts +7 -0
  95. package/lib/foreign/gemini.js +44 -0
  96. package/lib/foreign/goose.d.ts +11 -0
  97. package/lib/foreign/goose.js +53 -0
  98. package/lib/foreign/grokbuild.d.ts +8 -0
  99. package/lib/foreign/grokbuild.js +25 -0
  100. package/lib/foreign/hermes.d.ts +2 -0
  101. package/lib/foreign/hermes.js +78 -0
  102. package/lib/foreign/kernel.d.ts +132 -0
  103. package/lib/foreign/kernel.js +569 -0
  104. package/lib/foreign/kilocode.d.ts +11 -0
  105. package/lib/foreign/kilocode.js +51 -0
  106. package/lib/foreign/kimi.d.ts +7 -0
  107. package/lib/foreign/kimi.js +47 -0
  108. package/lib/foreign/mimocode.d.ts +11 -0
  109. package/lib/foreign/mimocode.js +51 -0
  110. package/lib/foreign/openclaw.d.ts +7 -0
  111. package/lib/foreign/openclaw.js +46 -0
  112. package/lib/foreign/opencode.d.ts +11 -0
  113. package/lib/foreign/opencode.js +52 -0
  114. package/lib/foreign/pi.d.ts +7 -0
  115. package/lib/foreign/pi.js +48 -0
  116. package/lib/foreign/platform-paths.d.ts +66 -0
  117. package/lib/foreign/platform-paths.js +141 -0
  118. package/lib/foreign/qoder.d.ts +10 -0
  119. package/lib/foreign/qoder.js +49 -0
  120. package/lib/foreign/qwen.d.ts +7 -0
  121. package/lib/foreign/qwen.js +45 -0
  122. package/lib/foreign/read-antigravity.d.ts +57 -0
  123. package/lib/foreign/read-antigravity.js +504 -0
  124. package/lib/foreign/read-chatgpt.d.ts +20 -0
  125. package/lib/foreign/read-chatgpt.js +509 -0
  126. package/lib/foreign/read-claude-code.d.ts +24 -0
  127. package/lib/foreign/read-claude-code.js +234 -0
  128. package/lib/foreign/read-cline.d.ts +53 -0
  129. package/lib/foreign/read-cline.js +479 -0
  130. package/lib/foreign/read-codex.d.ts +75 -0
  131. package/lib/foreign/read-codex.js +842 -0
  132. package/lib/foreign/read-continue.d.ts +87 -0
  133. package/lib/foreign/read-continue.js +489 -0
  134. package/lib/foreign/read-copilot.d.ts +42 -0
  135. package/lib/foreign/read-copilot.js +267 -0
  136. package/lib/foreign/read-crush.d.ts +54 -0
  137. package/lib/foreign/read-crush.js +478 -0
  138. package/lib/foreign/read-cursor.d.ts +141 -0
  139. package/lib/foreign/read-cursor.js +705 -0
  140. package/lib/foreign/read-dsh.d.ts +43 -0
  141. package/lib/foreign/read-dsh.js +205 -0
  142. package/lib/foreign/read-dsh4.d.ts +14 -0
  143. package/lib/foreign/read-dsh4.js +27 -0
  144. package/lib/foreign/read-gemini.d.ts +55 -0
  145. package/lib/foreign/read-gemini.js +273 -0
  146. package/lib/foreign/read-goose.d.ts +25 -0
  147. package/lib/foreign/read-goose.js +322 -0
  148. package/lib/foreign/read-grokbuild.d.ts +34 -0
  149. package/lib/foreign/read-grokbuild.js +455 -0
  150. package/lib/foreign/read-hermes.d.ts +74 -0
  151. package/lib/foreign/read-hermes.js +690 -0
  152. package/lib/foreign/read-kilocode.d.ts +17 -0
  153. package/lib/foreign/read-kilocode.js +46 -0
  154. package/lib/foreign/read-kimi.d.ts +63 -0
  155. package/lib/foreign/read-kimi.js +719 -0
  156. package/lib/foreign/read-mimocode.d.ts +12 -0
  157. package/lib/foreign/read-mimocode.js +56 -0
  158. package/lib/foreign/read-openclaw.d.ts +51 -0
  159. package/lib/foreign/read-openclaw.js +445 -0
  160. package/lib/foreign/read-opencode.d.ts +150 -0
  161. package/lib/foreign/read-opencode.js +896 -0
  162. package/lib/foreign/read-pi.d.ts +56 -0
  163. package/lib/foreign/read-pi.js +411 -0
  164. package/lib/foreign/read-qoder.d.ts +54 -0
  165. package/lib/foreign/read-qoder.js +317 -0
  166. package/lib/foreign/read-qwen.d.ts +40 -0
  167. package/lib/foreign/read-qwen.js +326 -0
  168. package/lib/foreign/read-reasonix.d.ts +66 -0
  169. package/lib/foreign/read-reasonix.js +537 -0
  170. package/lib/foreign/read-teleagent.d.ts +15 -0
  171. package/lib/foreign/read-teleagent.js +68 -0
  172. package/lib/foreign/read-trae.d.ts +67 -0
  173. package/lib/foreign/read-trae.js +492 -0
  174. package/lib/foreign/read-vibe.d.ts +28 -0
  175. package/lib/foreign/read-vibe.js +364 -0
  176. package/lib/foreign/read-workbuddy.d.ts +49 -0
  177. package/lib/foreign/read-workbuddy.js +346 -0
  178. package/lib/foreign/read-zcode.d.ts +12 -0
  179. package/lib/foreign/read-zcode.js +38 -0
  180. package/lib/foreign/read-zed.d.ts +30 -0
  181. package/lib/foreign/read-zed.js +473 -0
  182. package/lib/foreign/reasonix.d.ts +6 -0
  183. package/lib/foreign/reasonix.js +20 -0
  184. package/lib/foreign/registry.d.ts +196 -0
  185. package/lib/foreign/registry.js +573 -0
  186. package/lib/foreign/session-ir.d.ts +186 -0
  187. package/lib/foreign/session-ir.js +427 -0
  188. package/lib/foreign/session-read.d.ts +102 -0
  189. package/lib/foreign/session-read.js +254 -0
  190. package/lib/foreign/session-source.d.ts +216 -0
  191. package/lib/foreign/session-source.js +453 -0
  192. package/lib/foreign/source-modules.d.ts +45 -0
  193. package/lib/foreign/source-modules.js +52 -0
  194. package/lib/foreign/sqlite.d.ts +61 -0
  195. package/lib/foreign/sqlite.js +404 -0
  196. package/lib/foreign/teleagent.d.ts +11 -0
  197. package/lib/foreign/teleagent.js +53 -0
  198. package/lib/foreign/trae.d.ts +6 -0
  199. package/lib/foreign/trae.js +20 -0
  200. package/lib/foreign/truth-table.d.ts +62 -0
  201. package/lib/foreign/truth-table.js +333 -0
  202. package/lib/foreign/types.d.ts +272 -0
  203. package/lib/foreign/types.js +2 -0
  204. package/lib/foreign/vibe.d.ts +6 -0
  205. package/lib/foreign/vibe.js +23 -0
  206. package/lib/foreign/workbuddy.d.ts +7 -0
  207. package/lib/foreign/workbuddy.js +45 -0
  208. package/lib/foreign/zcode.d.ts +11 -0
  209. package/lib/foreign/zcode.js +51 -0
  210. package/lib/foreign/zed.d.ts +11 -0
  211. package/lib/foreign/zed.js +53 -0
  212. package/lib/index.d.ts +154 -13
  213. package/lib/index.js +661 -84
  214. package/lib/market/github-repos.d.ts +28 -0
  215. package/lib/market/github-repos.js +43 -1
  216. package/lib/market/types.d.ts +1 -3
  217. package/lib/profiles/dsh-profile-manager.d.ts +54 -2
  218. package/lib/profiles/dsh-profile-manager.js +182 -11
  219. package/lib/profiles/dsh-profile-shared.d.ts +47 -0
  220. package/lib/profiles/dsh-profile-shared.js +15 -1
  221. package/lib/profiles/index.d.ts +1 -1
  222. package/lib/profiles/index.js +1 -1
  223. package/lib/routes/consult.js +51 -5
  224. package/lib/routes/foreign-context.d.ts +43 -0
  225. package/lib/routes/foreign-context.js +65 -0
  226. package/lib/routes/foreign.d.ts +11 -0
  227. package/lib/routes/foreign.js +208 -0
  228. package/lib/routes/import.d.ts +8 -0
  229. package/lib/routes/import.js +22 -1
  230. package/lib/routes/index.d.ts +2 -2
  231. package/lib/routes/index.js +4 -0
  232. package/lib/routes/kit.d.ts +4 -3
  233. package/lib/routes/market.js +0 -1
  234. package/lib/routes/prefs.d.ts +2 -1
  235. package/lib/routes/prefs.js +14 -2
  236. package/lib/routes/recovery.js +363 -3
  237. package/lib/routes/session-export.d.ts +117 -0
  238. package/lib/routes/session-export.js +270 -0
  239. package/lib/routes/snapshots.js +12 -2
  240. package/lib/routes/sync.js +82 -10
  241. package/lib/security/redaction.d.ts +14 -0
  242. package/lib/security/redaction.js +24 -9
  243. package/lib/security/secret-scanner.d.ts +27 -0
  244. package/lib/security/secret-scanner.js +54 -1
  245. package/lib/sync/autosync-scheduler.d.ts +13 -1
  246. package/lib/sync/autosync-scheduler.js +49 -26
  247. package/lib/sync/backup-files.js +6 -0
  248. package/lib/sync/gist/gist-transport.d.ts +91 -0
  249. package/lib/sync/gist/gist-transport.js +406 -0
  250. package/lib/sync/gist/messages.d.ts +22 -0
  251. package/lib/sync/gist/messages.js +46 -0
  252. package/lib/sync/s3/messages.d.ts +53 -0
  253. package/lib/sync/s3/messages.js +83 -0
  254. package/lib/sync/s3/s3-providers.d.ts +78 -0
  255. package/lib/sync/s3/s3-providers.js +156 -0
  256. package/lib/sync/s3/s3-transport.d.ts +126 -0
  257. package/lib/sync/s3/s3-transport.js +551 -0
  258. package/lib/sync/s3/sigv4.d.ts +107 -0
  259. package/lib/sync/s3/sigv4.js +196 -0
  260. package/lib/sync/sync-config.d.ts +250 -4
  261. package/lib/sync/sync-config.js +658 -57
  262. package/lib/sync/sync-engine.d.ts +1 -0
  263. package/lib/sync/sync-engine.js +4 -2
  264. package/lib/sync/sync-history.d.ts +1 -1
  265. package/lib/sync/webdav/webdav-transport.d.ts +31 -3
  266. package/lib/sync/webdav/webdav-transport.js +102 -18
  267. package/lib/ui/commands.d.ts +4 -3
  268. package/lib/ui/commands.js +41 -9
  269. package/lib/ui/disk-usage-view.d.ts +44 -0
  270. package/lib/ui/disk-usage-view.js +55 -1
  271. package/lib/ui/dsh-profiles-view.d.ts +53 -0
  272. package/lib/ui/dsh-profiles-view.js +56 -0
  273. package/lib/ui/foreign-view.d.ts +162 -0
  274. package/lib/ui/foreign-view.js +225 -0
  275. package/lib/ui/i18n.d.ts +77 -0
  276. package/lib/ui/i18n.js +164 -0
  277. package/lib/ui/session-inventory-view.d.ts +106 -0
  278. package/lib/ui/session-inventory-view.js +58 -0
  279. package/lib/ui/session-layout-view.d.ts +265 -0
  280. package/lib/ui/session-layout-view.js +271 -0
  281. package/lib/ui/sync-repository-picker.d.ts +98 -0
  282. package/lib/ui/sync-repository-picker.js +145 -0
  283. package/lib/ui/sync-settings-view.d.ts +143 -4
  284. package/lib/ui/sync-settings-view.js +231 -7
  285. package/lib/ui/types.d.ts +47 -0
  286. package/lib/utils/asar-read.d.ts +113 -0
  287. package/lib/utils/asar-read.js +523 -0
  288. package/lib/utils/env-lock.js +7 -2
  289. package/lib/utils/patch-yaml.d.ts +25 -0
  290. package/lib/utils/patch-yaml.js +62 -0
  291. package/lib/utils/paths.d.ts +10 -1
  292. package/lib/utils/paths.js +24 -3
  293. package/lib/utils/recursive-walk.js +12 -13
  294. package/lib/utils/session-format.d.ts +35 -2
  295. package/lib/utils/session-format.js +131 -5
  296. package/lib/utils/session-health-scan.d.ts +7 -1
  297. package/lib/utils/session-health-scan.js +369 -12
  298. package/lib/utils/session-layout-repair-service.d.ts +98 -0
  299. package/lib/utils/session-layout-repair-service.js +457 -0
  300. package/lib/utils/session-log-repair.d.ts +27 -0
  301. package/lib/utils/session-log-repair.js +233 -62
  302. package/lib/utils/session-repair-service.d.ts +32 -3
  303. package/lib/utils/session-repair-service.js +148 -9
  304. package/lib/utils/session-row-facts.d.ts +73 -0
  305. package/lib/utils/session-row-facts.js +182 -0
  306. package/lib/utils/session-verify.d.ts +118 -0
  307. package/lib/utils/session-verify.js +705 -0
  308. package/lib/utils/shared-constants.d.ts +59 -0
  309. package/lib/utils/shared-constants.js +71 -0
  310. package/package.json +1 -1
  311. package/src/adapters/credentials.test.ts +27 -0
  312. package/src/adapters/credentials.ts +6 -3
  313. package/src/adapters/file-collection.ts +33 -8
  314. package/src/adapters/files.test.ts +222 -0
  315. package/src/adapters/mcp.test.ts +43 -0
  316. package/src/adapters/mcp.ts +37 -14
  317. package/src/adapters/plugin-files.ts +34 -4
  318. package/src/adapters/plugins.test.ts +241 -0
  319. package/src/adapters/plugins.ts +201 -53
  320. package/src/adapters/prompts.test.ts +49 -0
  321. package/src/adapters/prompts.ts +41 -20
  322. package/src/adapters/providers.test.ts +16 -0
  323. package/src/adapters/providers.ts +4 -3
  324. package/src/adapters/self.test.ts +77 -0
  325. package/src/adapters/self.ts +75 -0
  326. package/src/adapters/sessions.test.ts +77 -0
  327. package/src/adapters/sessions.ts +47 -1
  328. package/src/adapters/settings.test.ts +78 -0
  329. package/src/adapters/settings.ts +42 -6
  330. package/src/adapters/skills.ts +221 -3
  331. package/src/adapters/test-helpers.ts +43 -5
  332. package/src/adapters/workspaces-dup-id.test.ts +144 -0
  333. package/src/adapters/workspaces.test.ts +10 -0
  334. package/src/adapters/workspaces.ts +97 -5
  335. package/src/cli/actions.test.ts +117 -2
  336. package/src/cli/actions.ts +132 -12
  337. package/src/cli/help.ts +570 -0
  338. package/src/cli/import-source.test.ts +193 -0
  339. package/src/cli/import-source.ts +229 -0
  340. package/src/cli/index.ts +129 -94
  341. package/src/cli/web/client-script.ts +12 -0
  342. package/src/cli/web/console-fixes.test.ts +36 -0
  343. package/src/cli/web/http.ts +7 -1
  344. package/src/cli/web/page.ts +108 -25
  345. package/src/cli/web/routes.ts +23 -4
  346. package/src/cli/web/script-anchor.test.ts +33 -0
  347. package/src/cli/web/server.ts +28 -2
  348. package/src/cli/web/web.test.ts +447 -4
  349. package/src/client/ConfigManagerSection.tsx +118 -63
  350. package/src/client/about/AboutPanel.tsx +42 -3
  351. package/src/client/about/PluginUpdateDialog.tsx +185 -0
  352. package/src/client/about/ReleaseBody.tsx +144 -0
  353. package/src/client/about/ReleaseNotesDialog.tsx +3 -130
  354. package/src/client/about/about-view.ts +7 -5
  355. package/src/client/about/plugin-update-view.test.ts +25 -0
  356. package/src/client/about/plugin-update-view.ts +26 -0
  357. package/src/client/about/release-notes-view.test.ts +33 -0
  358. package/src/client/about/release-notes-view.ts +34 -0
  359. package/src/client/api.ts +145 -1
  360. package/src/client/common/Icon.tsx +3 -0
  361. package/src/client/common/RunsCenter.tsx +8 -2
  362. package/src/client/common/Select.tsx +9 -2
  363. package/src/client/common/info-hint-guard.test.ts +63 -18
  364. package/src/client/common/plan-text-redaction.test.ts +193 -8
  365. package/src/client/common/routes.ts +15 -0
  366. package/src/client/config-manager.module.css +506 -354
  367. package/src/client/css-scale-guard.test.ts +122 -0
  368. package/src/client/css-token-guard.test.ts +82 -0
  369. package/src/client/environment/EnvironmentPanel.tsx +45 -16
  370. package/src/client/{snapshots → environment/maintenance}/DiskUsageCard.tsx +21 -17
  371. package/src/client/environment/profiles-incomplete-ui.test.ts +103 -0
  372. package/src/client/export/ExportView.tsx +9 -2
  373. package/src/client/{snapshots → home}/BackupScheduleCard.tsx +13 -4
  374. package/src/client/home/HomePanel.tsx +69 -157
  375. package/src/client/icon-layer-guard.test.ts +1 -1
  376. package/src/client/import/ForeignImportView.tsx +318 -0
  377. package/src/client/import/ImportWizardView.tsx +3 -1
  378. package/src/client/import/foreign-import-view-guard.test.ts +195 -0
  379. package/src/client/import/import-file-select.test.ts +6 -6
  380. package/src/client/import/import-wizard-steps.tsx +17 -1
  381. package/src/client/import/use-import-wizard-controller.ts +3 -3
  382. package/src/client/index.ts +1 -1
  383. package/src/client/library/LibraryActions.tsx +31 -34
  384. package/src/client/library/LibraryPanel.tsx +4 -1
  385. package/src/client/{snapshots → library}/RestorePlanView.tsx +1 -1
  386. package/src/client/{snapshots → library}/files-layout.test.ts +2 -2
  387. package/src/client/{snapshots → library}/report-dismiss.test.ts +26 -3
  388. package/src/client/library/task-panel-visibility.test.ts +25 -17
  389. package/src/client/locales.ts +278 -36
  390. package/src/client/lucide-icons.d.ts +1 -0
  391. package/src/client/market/MarketPanel.tsx +11 -2
  392. package/src/client/recovery/IncompleteCopiesSection.tsx +132 -0
  393. package/src/client/recovery/RecoveryPanel.tsx +486 -59
  394. package/src/client/recovery/recovery-api.ts +30 -0
  395. package/src/client/recovery/recovery-incomplete-copies.test.ts +151 -0
  396. package/src/client/recovery/recovery-locales.test.ts +88 -0
  397. package/src/client/recovery/recovery-locales.ts +321 -6
  398. package/src/client/recovery/recovery-panel-layout.test.ts +52 -0
  399. package/src/client/recovery/recovery-view.ts +92 -2
  400. package/src/client/run-store.test.ts +201 -89
  401. package/src/client/run-store.ts +114 -143
  402. package/src/client/session-export/session-export-view.test.ts +103 -0
  403. package/src/client/session-export/session-export-view.ts +118 -0
  404. package/src/client/sync/ChannelConfigDialog.tsx +219 -14
  405. package/src/client/sync/SyncChannelEntryCard.tsx +52 -4
  406. package/src/client/sync/SyncLogList.tsx +1 -1
  407. package/src/client/sync/SyncPanel.tsx +166 -102
  408. package/src/client/sync/SyncRepositoryPicker.tsx +216 -0
  409. package/src/client/sync/SyncSettingsView.test.ts +6 -1
  410. package/src/client/sync/history-model.test.ts +94 -57
  411. package/src/client/sync/history-model.ts +44 -25
  412. package/src/client/sync/sync-api.test.ts +56 -0
  413. package/src/client/sync/sync-api.ts +92 -0
  414. package/src/client/sync/sync-locales.ts +162 -2
  415. package/src/client/sync/sync-view.test.ts +3 -8
  416. package/src/client/sync/sync-view.ts +111 -21
  417. package/src/core/analyzer-cache.test.ts +47 -0
  418. package/src/core/analyzer.ts +115 -14
  419. package/src/core/backup-plan.test.ts +35 -0
  420. package/src/core/backup-plan.ts +19 -1
  421. package/src/core/backup.test.ts +37 -1
  422. package/src/core/backup.ts +27 -6
  423. package/src/core/cache-cleaner.test.ts +120 -1
  424. package/src/core/cache-cleaner.ts +59 -22
  425. package/src/core/checkpoint.test.ts +665 -0
  426. package/src/core/checkpoint.ts +1614 -0
  427. package/src/core/credential-plan-consistency.test.ts +93 -0
  428. package/src/core/incident-wiring.test.ts +6 -2
  429. package/src/core/journal.test.ts +70 -0
  430. package/src/core/journal.ts +68 -0
  431. package/src/core/messages.ts +26 -0
  432. package/src/core/model-tools.ts +3 -0
  433. package/src/core/patch-layers.test.ts +94 -0
  434. package/src/core/patch-layers.ts +150 -0
  435. package/src/core/restore.ts +59 -3
  436. package/src/core/rollback.ts +6 -3
  437. package/src/core/section-content-consistency.test.ts +185 -0
  438. package/src/core/self-update.test.ts +110 -0
  439. package/src/core/self-update.ts +118 -0
  440. package/src/core/session-health.test.ts +91 -1
  441. package/src/core/session-health.ts +56 -9
  442. package/src/core/types.ts +127 -1
  443. package/src/foreign/antigravity.test.ts +271 -0
  444. package/src/foreign/antigravity.ts +188 -0
  445. package/src/foreign/bundle.ts +108 -0
  446. package/src/foreign/chatgpt.test.ts +279 -0
  447. package/src/foreign/chatgpt.ts +30 -0
  448. package/src/foreign/claude-code.test.ts +234 -0
  449. package/src/foreign/claude-code.ts +106 -0
  450. package/src/foreign/claude-sessions-bytes.test.ts +227 -0
  451. package/src/foreign/claude-sessions.test.ts +180 -0
  452. package/src/foreign/claude-sessions.ts +461 -0
  453. package/src/foreign/cline.test.ts +274 -0
  454. package/src/foreign/cline.ts +32 -0
  455. package/src/foreign/codex.test.ts +585 -0
  456. package/src/foreign/codex.ts +409 -0
  457. package/src/foreign/continue.test.ts +365 -0
  458. package/src/foreign/continue.ts +73 -0
  459. package/src/foreign/copilot.test.ts +273 -0
  460. package/src/foreign/copilot.ts +133 -0
  461. package/src/foreign/crush.test.ts +234 -0
  462. package/src/foreign/crush.ts +66 -0
  463. package/src/foreign/cursor.test.ts +509 -0
  464. package/src/foreign/cursor.ts +259 -0
  465. package/src/foreign/dsh.test.ts +198 -0
  466. package/src/foreign/dsh.ts +117 -0
  467. package/src/foreign/dsh4.test.ts +127 -0
  468. package/src/foreign/dsh4.ts +15 -0
  469. package/src/foreign/file-budget.test.ts +256 -0
  470. package/src/foreign/fixtures/antigravity/README.md +18 -0
  471. package/src/foreign/fixtures/antigravity/basic/gemini/antigravity/mcp_config.json +13 -0
  472. package/src/foreign/fixtures/antigravity/basic/gemini/config/mcp_config.json +24 -0
  473. package/src/foreign/fixtures/antigravity/empty/gemini/antigravity/mcp_config.json +0 -0
  474. package/src/foreign/fixtures/antigravity/empty/gemini/antigravity/mcp_oauth_tokens.json +0 -0
  475. package/src/foreign/fixtures/antigravity/empty/gemini/config/mcp_config.json +0 -0
  476. package/src/foreign/fixtures/codex/README.md +31 -0
  477. package/src/foreign/fixtures/codex/basic/.agents/skills/hello/SKILL.md +6 -0
  478. package/src/foreign/fixtures/codex/basic/.agents/skills/wrapper/nested/SKILL.md +6 -0
  479. package/src/foreign/fixtures/codex/basic/.codex/AGENTS.md +4 -0
  480. package/src/foreign/fixtures/codex/basic/.codex/config.toml +28 -0
  481. package/src/foreign/fixtures/codex/malformed/.codex/config.toml +3 -0
  482. package/src/foreign/fixtures/codex/override/.codex/AGENTS.md +1 -0
  483. package/src/foreign/fixtures/codex/override/.codex/AGENTS.override.md +3 -0
  484. package/src/foreign/fixtures/codex/sessions/.codex/archived_sessions/rollout-2026-10-05T09-00-00-22222222-2222-4222-8222-222222222222.jsonl +2 -0
  485. package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T10-00-00-11111111-1111-4111-8111-111111111111.jsonl +15 -0
  486. package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T11-00-00-33333333-3333-4333-8333-333333333333.jsonl +2 -0
  487. package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T11-30-00-55555555-5555-4555-8555-555555555555.jsonl +2 -0
  488. package/src/foreign/fixtures/codex/sessions/.codex/sessions/2026/10/06/rollout-2026-10-06T12-00-00-44444444-4444-4444-8444-444444444444.jsonl +2 -0
  489. package/src/foreign/fixtures/codex/sessions/.codex/sessions/notes.jsonl +1 -0
  490. package/src/foreign/fixtures/copilot/README.md +22 -0
  491. package/src/foreign/fixtures/copilot/basic/copilot-instructions.md +3 -0
  492. package/src/foreign/fixtures/copilot/basic/instructions/empty.instructions.md +1 -0
  493. package/src/foreign/fixtures/copilot/basic/instructions/repo.instructions.md +5 -0
  494. package/src/foreign/fixtures/copilot/basic/mcp-config.json +24 -0
  495. package/src/foreign/fixtures/copilot/basic/skills/hello/SKILL.md +6 -0
  496. package/src/foreign/fixtures/copilot/overridden/mcp-config.json +10 -0
  497. package/src/foreign/fixtures/cursor/README.md +17 -0
  498. package/src/foreign/fixtures/cursor/alt/.cursor/mcp.json +5 -0
  499. package/src/foreign/fixtures/cursor/basic/.cursor/mcp.json +18 -0
  500. package/src/foreign/fixtures/cursor/basic/.cursor/rules/agent.mdc +5 -0
  501. package/src/foreign/fixtures/cursor/basic/.cursor/rules/always.mdc +6 -0
  502. package/src/foreign/fixtures/cursor/basic/.cursor/rules/auto.mdc +7 -0
  503. package/src/foreign/fixtures/cursor/basic/.cursor/rules/manual.mdc +3 -0
  504. package/src/foreign/fixtures/cursor/basic/.cursor/skills/.hidden/SKILL.md +6 -0
  505. package/src/foreign/fixtures/cursor/basic/.cursor/skills/hello/SKILL.md +6 -0
  506. package/src/foreign/fixtures/cursor/basic/.cursor/skills/wrapper/nested/SKILL.md +6 -0
  507. package/src/foreign/fixtures/cursor/basic/.cursor/skills/wrapper/nested/notes.md +1 -0
  508. package/src/foreign/fixtures/cursor/basic/.cursorrules +1 -0
  509. package/src/foreign/fixtures/cursor/basic/project/.cursor/mcp.json +9 -0
  510. package/src/foreign/fixtures/cursor/basic/project/.cursor/rules/project-always.mdc +5 -0
  511. package/src/foreign/fixtures/cursor/basic/project/.cursor/skills/hello/SKILL.md +6 -0
  512. package/src/foreign/fixtures/cursor/basic/project/.cursorrules +1 -0
  513. package/src/foreign/fixtures/cursor/empty/.cursor/mcp.json +0 -0
  514. package/src/foreign/fixtures/cursor/malformed/.cursor/mcp.json +3 -0
  515. package/src/foreign/fixtures/cursor/malformed/.cursor/rules/broken.mdc +6 -0
  516. package/src/foreign/fixtures/cursor/shape/.cursor/mcp.json +3 -0
  517. package/src/foreign/fixtures/hermes/README.md +14 -0
  518. package/src/foreign/fixtures/hermes/basic/SOUL.md +3 -0
  519. package/src/foreign/fixtures/hermes/basic/config.yaml +48 -0
  520. package/src/foreign/fixtures/hermes/basic/dotenv.fixture +1 -0
  521. package/src/foreign/fixtures/hermes/basic/memories/MEMORY.md +1 -0
  522. package/src/foreign/fixtures/hermes/basic/memories/USER.md +1 -0
  523. package/src/foreign/fixtures/hermes/basic/skills/se-team-design/SKILL.md +6 -0
  524. package/src/foreign/fixtures/hermes/basic/skills/se-team-design/examples/usage.md +1 -0
  525. package/src/foreign/fixtures/hermes/basic/skills/software-development/dogfood/SKILL.md +6 -0
  526. package/src/foreign/fixtures/hermes/basic/skills/software-development/dogfood/notes.md +1 -0
  527. package/src/foreign/fixtures/hermes/basic/state.db +1 -0
  528. package/src/foreign/fixtures/hermes/empty/config.yaml +0 -0
  529. package/src/foreign/gemini.test.ts +291 -0
  530. package/src/foreign/gemini.ts +52 -0
  531. package/src/foreign/goose.test.ts +196 -0
  532. package/src/foreign/goose.ts +62 -0
  533. package/src/foreign/grokbuild.test.ts +246 -0
  534. package/src/foreign/grokbuild.ts +30 -0
  535. package/src/foreign/hermes.test.ts +545 -0
  536. package/src/foreign/hermes.ts +86 -0
  537. package/src/foreign/kernel-credentials.test.ts +88 -0
  538. package/src/foreign/kernel-workspaces.test.ts +88 -0
  539. package/src/foreign/kernel.ts +584 -0
  540. package/src/foreign/kilocode.test.ts +186 -0
  541. package/src/foreign/kilocode.ts +60 -0
  542. package/src/foreign/kimi.test.ts +259 -0
  543. package/src/foreign/kimi.ts +55 -0
  544. package/src/foreign/mcp-value-shape.test.ts +286 -0
  545. package/src/foreign/mimocode.test.ts +178 -0
  546. package/src/foreign/mimocode.ts +60 -0
  547. package/src/foreign/openclaw.test.ts +248 -0
  548. package/src/foreign/openclaw.ts +54 -0
  549. package/src/foreign/opencode.test.ts +376 -0
  550. package/src/foreign/opencode.ts +61 -0
  551. package/src/foreign/pi.test.ts +275 -0
  552. package/src/foreign/pi.ts +56 -0
  553. package/src/foreign/platform-paths.ts +156 -0
  554. package/src/foreign/qoder.test.ts +266 -0
  555. package/src/foreign/qoder.ts +57 -0
  556. package/src/foreign/qwen.test.ts +203 -0
  557. package/src/foreign/qwen.ts +53 -0
  558. package/src/foreign/read-antigravity.ts +569 -0
  559. package/src/foreign/read-chatgpt.ts +506 -0
  560. package/src/foreign/read-claude-code.ts +264 -0
  561. package/src/foreign/read-cline.ts +517 -0
  562. package/src/foreign/read-codex.ts +823 -0
  563. package/src/foreign/read-continue.ts +557 -0
  564. package/src/foreign/read-copilot.ts +304 -0
  565. package/src/foreign/read-crush.ts +503 -0
  566. package/src/foreign/read-cursor.ts +778 -0
  567. package/src/foreign/read-dsh.ts +215 -0
  568. package/src/foreign/read-dsh4.ts +35 -0
  569. package/src/foreign/read-gemini.ts +306 -0
  570. package/src/foreign/read-goose.ts +333 -0
  571. package/src/foreign/read-grokbuild.ts +475 -0
  572. package/src/foreign/read-hermes.ts +724 -0
  573. package/src/foreign/read-kilocode.ts +55 -0
  574. package/src/foreign/read-kimi.ts +768 -0
  575. package/src/foreign/read-mimocode.ts +66 -0
  576. package/src/foreign/read-openclaw.ts +505 -0
  577. package/src/foreign/read-opencode.ts +994 -0
  578. package/src/foreign/read-pi.ts +459 -0
  579. package/src/foreign/read-qoder.ts +351 -0
  580. package/src/foreign/read-qwen.ts +342 -0
  581. package/src/foreign/read-reasonix.ts +590 -0
  582. package/src/foreign/read-teleagent.ts +82 -0
  583. package/src/foreign/read-trae.ts +488 -0
  584. package/src/foreign/read-vibe.ts +353 -0
  585. package/src/foreign/read-workbuddy.ts +374 -0
  586. package/src/foreign/read-zcode.ts +48 -0
  587. package/src/foreign/read-zed.ts +479 -0
  588. package/src/foreign/reasonix.test.ts +336 -0
  589. package/src/foreign/reasonix.ts +25 -0
  590. package/src/foreign/registry.test.ts +377 -0
  591. package/src/foreign/registry.ts +750 -0
  592. package/src/foreign/session-ir.test.ts +189 -0
  593. package/src/foreign/session-ir.ts +614 -0
  594. package/src/foreign/session-read.ts +295 -0
  595. package/src/foreign/session-source.ts +625 -0
  596. package/src/foreign/skill-file-caps.test.ts +171 -0
  597. package/src/foreign/source-modules.ts +86 -0
  598. package/src/foreign/source-registry.test.ts +190 -0
  599. package/src/foreign/sqlite.test.ts +313 -0
  600. package/src/foreign/sqlite.ts +446 -0
  601. package/src/foreign/teleagent.test.ts +147 -0
  602. package/src/foreign/teleagent.ts +62 -0
  603. package/src/foreign/trae.test.ts +316 -0
  604. package/src/foreign/trae.ts +25 -0
  605. package/src/foreign/truncation-report.test.ts +238 -0
  606. package/src/foreign/truth-table.ts +453 -0
  607. package/src/foreign/types.ts +312 -0
  608. package/src/foreign/vibe.test.ts +209 -0
  609. package/src/foreign/vibe.ts +28 -0
  610. package/src/foreign/workbuddy.test.ts +207 -0
  611. package/src/foreign/workbuddy.ts +53 -0
  612. package/src/foreign/zcode.test.ts +224 -0
  613. package/src/foreign/zcode.ts +60 -0
  614. package/src/foreign/zed.test.ts +240 -0
  615. package/src/foreign/zed.ts +62 -0
  616. package/src/index.facade.test.ts +254 -16
  617. package/src/index.ts +704 -82
  618. package/src/market/github-repos.test.ts +98 -1
  619. package/src/market/github-repos.ts +63 -1
  620. package/src/market/types.ts +3 -3
  621. package/src/profiles/dsh-profile-incomplete-copy.test.ts +177 -0
  622. package/src/profiles/dsh-profile-manager.test.ts +37 -0
  623. package/src/profiles/dsh-profile-manager.ts +188 -13
  624. package/src/profiles/dsh-profile-shared.ts +55 -1
  625. package/src/profiles/index.ts +2 -1
  626. package/src/routes/consult.ts +53 -4
  627. package/src/routes/foreign-context.ts +89 -0
  628. package/src/routes/foreign.test.ts +443 -0
  629. package/src/routes/foreign.ts +226 -0
  630. package/src/routes/import.ts +23 -1
  631. package/src/routes/index.ts +6 -2
  632. package/src/routes/kit.ts +4 -3
  633. package/src/routes/market.ts +0 -1
  634. package/src/routes/prefs.ts +15 -1
  635. package/src/routes/recovery.test.ts +167 -0
  636. package/src/routes/recovery.ts +331 -1
  637. package/src/routes/session-export.ts +308 -0
  638. package/src/routes/snapshots.ts +12 -2
  639. package/src/routes/sync.ts +91 -7
  640. package/src/security/redaction.test.ts +149 -0
  641. package/src/security/redaction.ts +25 -9
  642. package/src/security/secret-scanner.test.ts +186 -0
  643. package/src/security/secret-scanner.ts +57 -1
  644. package/src/sync/autosync-risk-tier.test.ts +61 -0
  645. package/src/sync/autosync-scheduler.test.ts +48 -1
  646. package/src/sync/autosync-scheduler.ts +50 -26
  647. package/src/sync/backup-files.ts +5 -0
  648. package/src/sync/gist/gist-msgkeys-catalog.test.ts +33 -0
  649. package/src/sync/gist/gist-msgkeys.test.ts +71 -0
  650. package/src/sync/gist/gist-transport.test.ts +308 -0
  651. package/src/sync/gist/gist-transport.ts +550 -0
  652. package/src/sync/gist/messages.ts +49 -0
  653. package/src/sync/retention-prune-all-off.test.ts +50 -0
  654. package/src/sync/s3/cloud-config.test.ts +283 -0
  655. package/src/sync/s3/messages.ts +102 -0
  656. package/src/sync/s3/s3-delete-index-404.test.ts +170 -0
  657. package/src/sync/s3/s3-delete-index-unreadable.test.ts +141 -0
  658. package/src/sync/s3/s3-msgkeys.test.ts +101 -0
  659. package/src/sync/s3/s3-providers.ts +213 -0
  660. package/src/sync/s3/s3-transport.test.ts +394 -0
  661. package/src/sync/s3/s3-transport.ts +651 -0
  662. package/src/sync/s3/s3-upload-index-404.test.ts +171 -0
  663. package/src/sync/s3/sigv4.test.ts +219 -0
  664. package/src/sync/s3/sigv4.ts +283 -0
  665. package/src/sync/sync-config.test.ts +237 -5
  666. package/src/sync/sync-config.ts +841 -52
  667. package/src/sync/sync-engine-download-tombstone.test.ts +73 -0
  668. package/src/sync/sync-engine.ts +5 -3
  669. package/src/sync/sync-history.ts +1 -1
  670. package/src/sync/webdav/webdav-delete-index-404.test.ts +178 -0
  671. package/src/sync/webdav/webdav-delete-index-unreadable.test.ts +143 -0
  672. package/src/sync/webdav/webdav-msgkeys.test.ts +53 -0
  673. package/src/sync/webdav/webdav-transport.test.ts +9 -5
  674. package/src/sync/webdav/webdav-transport.ts +99 -17
  675. package/src/sync/webdav/webdav-upload-index-404.test.ts +177 -0
  676. package/src/ui/commands.test.ts +4 -1
  677. package/src/ui/commands.ts +46 -12
  678. package/src/ui/disk-usage-view.test.ts +63 -1
  679. package/src/ui/disk-usage-view.ts +93 -1
  680. package/src/ui/dsh-profiles-incomplete-view.test.ts +102 -0
  681. package/src/ui/dsh-profiles-view.ts +94 -0
  682. package/src/ui/foreign-view.test.ts +241 -0
  683. package/src/ui/foreign-view.ts +362 -0
  684. package/src/ui/i18n.ts +164 -0
  685. package/src/ui/mid-ellipsis.test.ts +5 -10
  686. package/src/ui/session-inventory-view.test.ts +142 -2
  687. package/src/ui/session-inventory-view.ts +127 -0
  688. package/src/ui/session-layout-view.test.ts +361 -0
  689. package/src/ui/session-layout-view.ts +444 -0
  690. package/src/ui/sync-repository-picker.test.ts +130 -0
  691. package/src/ui/sync-repository-picker.ts +180 -0
  692. package/src/ui/sync-settings-view.test.ts +166 -0
  693. package/src/ui/sync-settings-view.ts +319 -9
  694. package/src/ui/types.ts +42 -0
  695. package/src/utils/asar-read.test.ts +330 -0
  696. package/src/utils/asar-read.ts +606 -0
  697. package/src/utils/env-lock.test.ts +55 -0
  698. package/src/utils/env-lock.ts +6 -1
  699. package/src/utils/patch-yaml.test.ts +112 -0
  700. package/src/utils/patch-yaml.ts +72 -0
  701. package/src/utils/paths.test.ts +63 -0
  702. package/src/utils/paths.ts +25 -3
  703. package/src/utils/recursive-walk.test.ts +20 -0
  704. package/src/utils/recursive-walk.ts +12 -13
  705. package/src/utils/session-format.test.ts +113 -2
  706. package/src/utils/session-format.ts +127 -5
  707. package/src/utils/session-health-scan.test.ts +364 -1
  708. package/src/utils/session-health-scan.ts +434 -15
  709. package/src/utils/session-layout-repair-service.test.ts +362 -0
  710. package/src/utils/session-layout-repair-service.ts +568 -0
  711. package/src/utils/session-log-repair.test.ts +260 -1
  712. package/src/utils/session-log-repair.ts +294 -55
  713. package/src/utils/session-repair-service.test.ts +148 -1
  714. package/src/utils/session-repair-service.ts +165 -10
  715. package/src/utils/session-row-facts.test.ts +159 -0
  716. package/src/utils/session-row-facts.ts +187 -0
  717. package/src/utils/session-verify.test.ts +762 -0
  718. package/src/utils/session-verify.ts +784 -0
  719. package/src/utils/shared-constants.ts +82 -0
@@ -22,7 +22,7 @@ import type { RecoveryPort } from '../../ui/types.ts'
22
22
  import type { RecoveryPreview, RecoveryStatus, RecoveryVerifyResult } from '../../ui/types.ts'
23
23
  import type { TranslateNS } from '../client-types.ts'
24
24
  import { Badge, Banner, Button, Card, Empty, SectionTitle, Spinner } from '../common/ui.tsx'
25
- import { DiskUsageCard } from '../snapshots/DiskUsageCard.tsx'
25
+ import { DiskUsageCard } from '../environment/maintenance/DiskUsageCard.tsx'
26
26
  import { SkeletonList } from '../common/Skeleton.tsx'
27
27
  import { ConfirmDialog } from '../common/ConfirmDialog.tsx'
28
28
  import { Modal } from '../common/Modal.tsx'
@@ -36,15 +36,38 @@ import {
36
36
  sessionHealthRows,
37
37
  sessionHealthSummaryView,
38
38
  sessionHealthTruncated,
39
+ sessionRepairBatchEntry,
39
40
  sessionRepairCommands,
40
41
  sessionRepairEntries,
41
42
  sessionRepairReasonKey,
43
+ sessionRepairVerifyKey,
42
44
  type SessionHealthResponse,
45
+ type SessionRepairBatchEntryView,
43
46
  type SessionRepairEntryView,
44
47
  type SessionRepairResult,
45
48
  } from '../../ui/session-inventory-view.ts'
49
+ import {
50
+ sessionLayoutApplyItems,
51
+ sessionLayoutCandidates,
52
+ sessionLayoutKeepState,
53
+ sessionLayoutOverallReasonKey,
54
+ sessionLayoutPlanDisplayRows,
55
+ sessionLayoutPlanSkippedCount,
56
+ sessionLayoutPlanSubmittable,
57
+ sessionLayoutResultCounts,
58
+ sessionLayoutSectionState,
59
+ type SessionLayoutApplyView,
60
+ type SessionLayoutPlanView,
61
+ type SessionLayoutPort,
62
+ } from '../../ui/session-layout-view.ts'
63
+ import {
64
+ sessionExportEntryState,
65
+ sessionExportReasonText,
66
+ type SessionExportProbe,
67
+ } from '../session-export/session-export-view.ts'
46
68
  import { runStore, type RecoveryStoreSlice } from '../run-store.ts'
47
69
  import type { CrashReport, IncidentApi, RescueStatus } from './incident-api.ts'
70
+ import { IncompleteCopiesSection } from './IncompleteCopiesSection.tsx'
48
71
  import {
49
72
  isSnapshotTrusted, isVerdictAttention, isVerdictSuccess, toRecoveryPreviewView,
50
73
  toRecoveryView,
@@ -53,12 +76,29 @@ import {
53
76
  formatRecoveryTime,
54
77
  rescueHintKey,
55
78
  } from './recovery-view.ts'
79
+ import { redact } from '../../security/redaction.ts'
56
80
  import css from '../config-manager.module.css'
57
81
 
82
+ /**
83
+ * 宿主/错误文本渲染前统一过 redact(AGENTS.md §UI 硬性规则 7;client-F4)。
84
+ * 单列成一行:plan-text-redaction.test.ts 的「按渲染点」登记表需要一个唯一锚点。
85
+ */
86
+ const redactErrorText = (err: unknown): string => redact(err instanceof Error ? err.message : String(err))
87
+
88
+ /**
89
+ * E2:布局归位端口 = `RecoveryPort` 的**可选**扩展(面板只用 `layoutRepairSessions`)。
90
+ *
91
+ * 为什么是 Partial:`src/ui/types.ts` 的 RecoveryPort 契约文件不在本次改动范围内,而注入的实现
92
+ * (`src/client/recovery/recovery-api.ts` 的 `RecoveryApi`)一定带这个方法。可选形态让所有既有
93
+ * 调用点(按 RecoveryPort 传值)原样可编译;运行时若真缺失,面板走 `sessions.layout.apiMissing`
94
+ * 如实提示并且**一条也不执行**,绝不假装有能力。
95
+ */
96
+ type LayoutRecoveryApi = RecoveryPort & Partial<SessionLayoutPort>
97
+
58
98
  export interface RecoveryPanelProps {
59
99
  /** 事故处置(崩溃归因 + 救援模式)API;与 recoveryApi 同属「事故恢复」子 tab */
60
100
  incidentApi: IncidentApi
61
- recoveryApi: RecoveryPort
101
+ recoveryApi: LayoutRecoveryApi
62
102
  t: TranslateNS<'config-manager-recovery'>
63
103
  /**
64
104
  * **主字典**(config-manager)翻译器 —— 仅供 CopyButton 这类「共用原语」使用:
@@ -216,7 +256,7 @@ function incidentStateLabel(t: TranslateNS<'config-manager-recovery'>, state: st
216
256
  */
217
257
  const EMPTY_COPY_T: TranslateNS<'config-manager'> = ((key: string) => key) as TranslateNS<'config-manager'>
218
258
 
219
- function SessionHealthCard({ recoveryApi, t, copyT }: Pick<RecoveryPanelProps, 'recoveryApi' | 't' | 'copyT'>) {
259
+ function SessionHealthCard({ recoveryApi, t, copyT, diskApi }: Pick<RecoveryPanelProps, 'recoveryApi' | 't' | 'copyT' | 'diskApi'>) {
220
260
  /** 剪贴板反馈的翻译器(config-manager 命名空间;缺省退到 zhUiT 兜底)。 */
221
261
  const clipboardT = copyT ?? EMPTY_COPY_T
222
262
  /** 体检结果住**卡片**里:弹窗每次打开重新扫一遍(scanVersion 变化即重扫),关掉不留一屏陈旧数据。 */
@@ -226,6 +266,25 @@ function SessionHealthCard({ recoveryApi, t, copyT }: Pick<RecoveryPanelProps, '
226
266
  const [open, setOpen] = useState(false)
227
267
  const [scanVersion, setScanVersion] = useState(0)
228
268
  const scan = (): void => { setScanVersion((v) => v + 1) }
269
+ /**
270
+ * F-2:官方 session.export 端的可用性探测(一次;打开弹窗时顺带探)。
271
+ *
272
+ * 为什么**只在打开弹窗时**探:探测要走一次宿主自请求,用户没进这个界面时毫无意义;
273
+ * 而进程内缓存(setState 在卡片上)保证同一次对话里只探一次 —— 后端能力不会中途变化。
274
+ * 探测失败(网络/宿主异常)**不写 error**:它是「下载入口的可用性未知」,不是体检失败。
275
+ */
276
+ const [zipProbe, setZipProbe] = useState<SessionExportProbe | null>(null)
277
+ const zipChecked = useRef(false)
278
+ useEffect(() => {
279
+ if (!open || zipChecked.current) return
280
+ zipChecked.current = true
281
+ let alive = true
282
+ diskApi.sessionExportProbe()
283
+ .then((probe) => { if (alive) setZipProbe(probe) })
284
+ // 探测本身失败(宿主不可达等)→ 落到保守三态:**绝不**改判成不可用而把入口藏掉。
285
+ .catch(() => { if (alive) setZipProbe({ availability: 'unknown', reason: 'network-error', status: 0, path: '', checkedAt: new Date().toISOString() }) })
286
+ return () => { alive = false }
287
+ }, [open, diskApi])
229
288
 
230
289
  useEffect(() => {
231
290
  if (!open) return
@@ -279,6 +338,8 @@ function SessionHealthCard({ recoveryApi, t, copyT }: Pick<RecoveryPanelProps, '
279
338
  loading={loading}
280
339
  error={error}
281
340
  onScan={scan}
341
+ zipProbe={zipProbe}
342
+ api={diskApi}
282
343
  />
283
344
  )}
284
345
  </Card>
@@ -292,35 +353,261 @@ function SessionHealthCard({ recoveryApi, t, copyT }: Pick<RecoveryPanelProps, '
292
353
  * 交互约定:打开即扫(scanVersion 变化重扫);修复一律先 dry-run 预览(零写入),有损计划必须再点
293
354
  * 「截断并修复」才执行;回滚走 ConfirmDialog(danger)。
294
355
  */
295
- /** 批量修复里的一条结果(成功或被拒;原因是机器可读码,界面映射成文案)。 */
296
- interface BatchRepairResult {
297
- unitId: string
298
- sessionId: string
299
- ok: boolean
300
- /** 被拒原因(机器可读;未知一律走 unknown 文案,绝不渲染裸枚举) */
301
- reason?: string
302
- /** 被拒是「需要逐个确认的有损修复」 */
303
- lossy?: boolean
304
- /** 成功时丢弃的行数 */
305
- droppedRows?: number
306
- /** 传输层失败(HTTP/网络)时的原文(已由 ErrorBanner/http 层脱敏) */
307
- transportError?: string
308
- }
309
-
310
356
  interface SessionHealthDialogProps {
311
357
  open: boolean
312
358
  onClose: () => void
313
- recoveryApi: RecoveryPort
359
+ recoveryApi: LayoutRecoveryApi
314
360
  t: TranslateNS<'config-manager-recovery'>
315
361
  clipboardT: TranslateNS<'config-manager'>
316
362
  response: SessionHealthResponse | null
317
363
  loading: boolean
318
364
  error: string | null
319
365
  onScan: () => void
366
+ /** F-2:官方 session.export 的可用性(null = 还没探出来 → 入口显示加载态,不误判禁用)。 */
367
+ zipProbe: SessionExportProbe | null
368
+ /** 供「下载原始日志 (ZIP)」拼下载地址(同一份 api 实例,别新起一个)。 */
369
+ api: import('../api.ts').ConfigManagerApi
370
+ }
371
+
372
+ /**
373
+ * E2:面板「会话布局归位 / 重复 id 隔离」写入口(宿主 E1:`POST /recovery/sessions/layout`)。
374
+ *
375
+ * 与字节级修复(/repair)的分工:那条改日志**字节**,这条改**位置** —— 把会话目录搬回首帧 cwd
376
+ * 对应的 projectKey 段,并把重复 id 的其它副本移进隔离目录(可人工找回)。
377
+ *
378
+ * 安全口径(键 `sessions.layout.runtimeNote`,必须让用户看见):本入口在 **DSH 运行时**执行,
379
+ * 与离线 CLI `dcm sessions repair --fix` 的「须先停 DSH」不同 —— 靠门前置(SAFE MODE / mutation lock /
380
+ * 逐目标无 session.lock + 不在静止期)+ 索引刷新必须成功 + 失败逐条回滚兜底;宿主没给索引刷新端口时
381
+ * **一条也不执行**(`reindex-unavailable`,绝不显示成成功)。
382
+ *
383
+ * 硬约束:**本组件不做任何业务判断** —— 候选筛选 / keep 完整性 / 可否提交 / 逐条状态与计数全在
384
+ * `ui/session-layout-view.ts`(含「未执行 / 已回滚不得算成功」的判定),组件只装配与渲染。
385
+ */
386
+ interface SessionLayoutSectionProps {
387
+ recoveryApi: LayoutRecoveryApi
388
+ t: TranslateNS<'config-manager-recovery'>
389
+ response: SessionHealthResponse | null
390
+ /** 执行后重扫列表(结果已落盘,列表必须跟着刷新) */
391
+ onScan: () => void
392
+ }
393
+
394
+ function SessionLayoutSection({ recoveryApi, t, response, onScan }: SessionLayoutSectionProps) {
395
+ const [plan, setPlan] = useState<SessionLayoutPlanView | null>(null)
396
+ const [applied, setApplied] = useState<SessionLayoutApplyView | null>(null)
397
+ /** sessionId → 用户选定保留的副本 unitId(提交判定在 sessionLayoutKeepState) */
398
+ const [keep, setKeep] = useState<Record<string, string>>({})
399
+ const [busy, setBusy] = useState<'preview' | 'apply' | null>(null)
400
+ /** 客户端/传输层失败(宿主的拒绝走结果里的机器码,不进这里) */
401
+ const [failure, setFailure] = useState<string | null>(null)
402
+
403
+ const candidates = sessionLayoutCandidates(response)
404
+ const keepState = sessionLayoutKeepState(plan, keep)
405
+ const submittable = sessionLayoutPlanSubmittable(plan, keepState)
406
+ const planRows = sessionLayoutPlanDisplayRows(plan)
407
+ const skippedCount = sessionLayoutPlanSkippedCount(plan)
408
+ const planReasonKey = sessionLayoutOverallReasonKey(plan?.reason)
409
+ const appliedReasonKey = sessionLayoutOverallReasonKey(applied?.reason)
410
+ const appliedItems = sessionLayoutApplyItems(applied)
411
+ const counts = sessionLayoutResultCounts(appliedItems)
412
+
413
+ const preview = (): void => {
414
+ if (busy !== null) return
415
+ setBusy('preview')
416
+ setFailure(null)
417
+ const request = recoveryApi.layoutRepairSessions?.(false)
418
+ if (request === undefined) {
419
+ setFailure(t('sessions.layout.apiMissing'))
420
+ setBusy(null)
421
+ return
422
+ }
423
+ request
424
+ .then((next) => {
425
+ if ('results' in next) { setApplied(next); return }
426
+ setPlan(next)
427
+ setKeep({})
428
+ setApplied(null)
429
+ })
430
+ .catch((err) => { setFailure(redactErrorText(err)) })
431
+ .finally(() => { setBusy(null) })
432
+ }
433
+
434
+ const apply = (): void => {
435
+ if (busy !== null || !submittable) return
436
+ setBusy('apply')
437
+ setFailure(null)
438
+ const request = recoveryApi.layoutRepairSessions?.(true, keep)
439
+ if (request === undefined) {
440
+ setFailure(t('sessions.layout.apiMissing'))
441
+ setBusy(null)
442
+ return
443
+ }
444
+ request
445
+ .then((next) => {
446
+ if ('results' in next) {
447
+ setApplied(next)
448
+ // 结果已落盘(含逐条回滚):重扫列表是唯一权威,结果视图保留到用户关闭
449
+ onScan()
450
+ return
451
+ }
452
+ setPlan(next)
453
+ setKeep({})
454
+ setApplied(null)
455
+ })
456
+ .catch((err) => { setFailure(redactErrorText(err)) })
457
+ .finally(() => { setBusy(null) })
458
+ }
459
+
460
+ // 扫过之后入口**始终可见**(否则本机没有这一档的用户根本找不到它);只有「还没扫过且手上没计划/结果」才隐藏。
461
+ const sectionState = sessionLayoutSectionState({ response, plan, applied })
462
+ if (sectionState === 'hidden') return null
463
+
464
+ return (
465
+ <div className={css.card}>
466
+ <div className={css.groupLabel}>{t('sessions.layout.title')}</div>
467
+ <div className={css.hint}>{t('sessions.layout.desc')}</div>
468
+ {/* 口径:本入口跑在 DSH 运行时 —— 与离线 CLI 的「须先停 DSH」不同,必须让用户看见 */}
469
+ <div className={css.hint}>{t('sessions.layout.runtimeNote')}</div>
470
+ <div className={css.hint}>{t('sessions.layout.quarantineNote')}</div>
471
+ {sectionState === 'empty' && <div className={css.hint}>{t('sessions.layout.noCandidates')}</div>}
472
+ {candidates.length > 0 && (
473
+ <div className={css.snapshotList + ' ' + css.reportScroll}>
474
+ {candidates.map((candidate) => (
475
+ <div key={candidate.unitId} className={css.snapshotRow}>
476
+ <span className={css.snapshotRowMain} title={candidate.unitId}>{candidate.sessionId}</span>
477
+ <span className={css.snapshotRowFacts}>
478
+ {candidate.issueCodes.join(' · ')} · {candidate.projectKey}
479
+ </span>
480
+ </div>
481
+ ))}
482
+ </div>
483
+ )}
484
+ <div className={css.actionRow}>
485
+ <Button variant="ghost" loading={busy === 'preview'} disabled={busy !== null} onClick={preview}>
486
+ {busy === 'preview' ? t('sessions.layout.previewing') : t('sessions.layout.preview')}
487
+ </Button>
488
+ </div>
489
+ {failure !== null && <Banner kind="error">{failure}</Banner>}
490
+ {applied === null && plan !== null && (
491
+ <>
492
+ <div className={css.groupLabel}>{t('sessions.layout.planTitle')}</div>
493
+ {/* 计划只读:确认前零写入 */}
494
+ <div className={css.hint}>{t('sessions.layout.planReadOnly')}</div>
495
+ {planReasonKey !== null && <Banner kind="warn">{t(planReasonKey)}</Banner>}
496
+ <div className={css.hint}>
497
+ {t('sessions.layout.summary', { move: String(plan.summary.move), quarantine: String(plan.summary.quarantine) })}
498
+ </div>
499
+ {planRows.length === 0 && <div className={css.hint}>{t('sessions.layout.planEmpty')}</div>}
500
+ {planRows.length > 0 && (
501
+ <div className={css.snapshotList + ' ' + css.reportScroll}>
502
+ {planRows.map((row) => (
503
+ <div key={row.unitId} className={css.snapshotRow}>
504
+ <span className={css.snapshotRowBadges}>
505
+ <Badge kind="info">{t(row.kindKey)}</Badge>
506
+ {row.kind === 'skip' && <Badge kind="warn">{t('sessions.layout.onlyReported')}</Badge>}
507
+ </span>
508
+ <span className={css.snapshotRowIssues} title={row.unitId}>{row.sessionId}</span>
509
+ <span className={css.snapshotRowFacts}>
510
+ {row.toProjectKey !== null ? row.fromProjectKey + ' → ' + row.toProjectKey : row.fromProjectKey}
511
+ {' · '}
512
+ {t(row.reasonKey)}
513
+ </span>
514
+ </div>
515
+ ))}
516
+ </div>
517
+ )}
518
+ {skippedCount > 0 && (
519
+ <div className={css.hint}>{t('sessions.layout.planSkipped', { count: String(skippedCount) })}</div>
520
+ )}
521
+ {/* 重复 id:必须选定保留哪一份才可提交(判定在 ui 纯函数层) */}
522
+ {plan.needsKeep.length > 0 && (
523
+ <>
524
+ <div className={css.groupLabel}>{t('sessions.layout.keepTitle')}</div>
525
+ <div className={css.hint}>{t('sessions.layout.keepHint')}</div>
526
+ {plan.needsKeep.map((sessionId) => (
527
+ <div key={sessionId} className={css.conflictChoices} role="radiogroup" aria-label={sessionId}>
528
+ {(keepState.candidates[sessionId] ?? []).map((unitId) => {
529
+ const selected = keep[sessionId] === unitId
530
+ return (
531
+ <label key={unitId} className={css.choiceCard} data-selected={selected ? '' : undefined}>
532
+ <input
533
+ type="radio"
534
+ name={'layout-keep-' + sessionId}
535
+ checked={selected}
536
+ onChange={() => { setKeep((prev) => ({ ...prev, [sessionId]: unitId })) }}
537
+ />
538
+ <span className={css.choiceTitle}>{unitId}</span>
539
+ </label>
540
+ )
541
+ })}
542
+ </div>
543
+ ))}
544
+ {!keepState.complete && (
545
+ <div className={css.hint}>{t('sessions.layout.keepMissing', { count: String(keepState.missing.length) })}</div>
546
+ )}
547
+ </>
548
+ )}
549
+ <div className={css.actionRow}>
550
+ {/* 未选定 keep / 计划不可用 ⇒ 不可提交(判定在 ui 纯函数层) */}
551
+ <Button variant="primary" loading={busy === 'apply'} disabled={busy !== null || !submittable} onClick={apply}>
552
+ {busy === 'apply' ? t('sessions.layout.applying') : t('sessions.layout.apply')}
553
+ </Button>
554
+ </div>
555
+ </>
556
+ )}
557
+ {applied !== null && (
558
+ <>
559
+ <div className={css.groupLabel}>{t('sessions.layout.resultTitle')}</div>
560
+ {/* 整体未执行(如 reindex-unavailable)**不是**成功:单独说明,且下面不会有任何成功条目 */}
561
+ {appliedReasonKey !== null && <Banner kind="warn">{t(appliedReasonKey)}</Banner>}
562
+ {appliedItems.length > 0 && (
563
+ <div className={css.hint}>
564
+ {t('sessions.layout.done', {
565
+ moved: String(counts.moved),
566
+ quarantined: String(counts.quarantined),
567
+ failed: String(counts.failedTotal),
568
+ skipped: String(counts.skipped),
569
+ })}
570
+ </div>
571
+ )}
572
+ {appliedItems.length > 0 && (
573
+ <div className={css.snapshotList + ' ' + css.reportScroll}>
574
+ {appliedItems.map((item, index) => (
575
+ <div key={item.unitId + '#' + String(index)} className={css.snapshotRow}>
576
+ <span className={css.snapshotRowBadges}>
577
+ <Badge kind={item.badgeKind}>{t(item.statusKey)}</Badge>
578
+ </span>
579
+ <span className={css.snapshotRowIssues} title={item.unitId}>{item.sessionId}</span>
580
+ <span className={css.snapshotRowFacts}>
581
+ {item.reasonKey !== null ? t(item.reasonKey) : ''}
582
+ {item.movedUnitId !== null ? ' → ' + item.movedUnitId : ''}
583
+ {item.quarantineDir !== null ? ' → ' + item.quarantineDir : ''}
584
+ </span>
585
+ </div>
586
+ ))}
587
+ </div>
588
+ )}
589
+ <div className={css.hint}>{t('sessions.layout.refreshHint')}</div>
590
+ <div className={css.actionRow}>
591
+ <Button variant="ghost" onClick={() => { setApplied(null); setPlan(null); setKeep({}) }}>
592
+ {t('sessions.layout.close')}
593
+ </Button>
594
+ </div>
595
+ </>
596
+ )}
597
+ </div>
598
+ )
320
599
  }
321
600
 
322
601
  function SessionHealthDialog(props: SessionHealthDialogProps) {
323
- const { open, onClose, recoveryApi, t, clipboardT, response, loading, error, onScan } = props
602
+ const { open, onClose, recoveryApi, t, clipboardT, response, loading, error, onScan, zipProbe, api } = props
603
+ /**
604
+ * F-2:「下载原始日志 (ZIP)」入口的三态判定(**唯一**判定点在 ui/ 纯函数里,组件只映射)。
605
+ *
606
+ * 为什么不在组件里判 `status !== 501`(竞品做法):那条判据在本机 DSH 上**已过时** ——
607
+ * 服务缺失返的是 500 而不是 501,照抄会让入口几乎恒显示。判定与理由见 session-export-view.ts。
608
+ */
609
+ const zipState = sessionExportEntryState(zipProbe)
610
+ const zipHint = sessionExportReasonText(zipState, t)
324
611
  const phase = sessionHealthPhase({ loading, error, response })
325
612
  /**
326
613
  * 本地的「已修复」行集合:批量/单条修复成功后**就地更新界面**,不再整屏重扫。
@@ -341,8 +628,38 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
341
628
  const repairEntries = sessionRepairEntries(response)
342
629
  /** 正在预览 / 应用 / 回滚的 unitId(进行中态住组件内;弹窗不跨页签) */
343
630
  const [busy, setBusy] = useState<string | null>(null)
631
+ /**
632
+ * F-2:正在下载原始日志的会话 id(进行中态住组件内 —— 弹窗不跨页签,无需进 runStore)。
633
+ *
634
+ * 为什么必须有:ZIP 可达数百 MB,点下去到浏览器开始落盘之间可能好几秒,
635
+ * 没有任何反馈会被当成「点了没反应」而反复点击。
636
+ */
637
+ const [zipBusy, setZipBusy] = useState<string | null>(null)
638
+ /** 触发一次 ZIP 下载;**失败必须显式报告**(绝不静默)。 */
639
+ const downloadZip = (sessionId: string): void => {
640
+ if (zipBusy !== null) return
641
+ setZipBusy(sessionId)
642
+ api.downloadSessionExport(sessionId)
643
+ .then((result) => { toast.ok(t('sessions.zip.done', { name: result.filename })) })
644
+ // 错误文本走 toast(与本节其它动作同一口径;http 层已脱敏)。
645
+ .catch((err) => { toast.error(t('sessions.zip.failed') + ': ' + redactErrorText(err)) })
646
+ .finally(() => { setZipBusy(null) })
647
+ }
344
648
  /** 预览得到的修复计划(应用前必须显式确认;ok=false 时也在这里显示拒绝原因) */
345
649
  const [plan, setPlan] = useState<{ unitId: string; result: SessionRepairResult } | null>(null)
650
+ /**
651
+ * 应用成功后的结果(含写后真 codec 复验结论)。
652
+ *
653
+ * 为什么必须有:修复「写完了」与「DSH 现在能读它」是两件事 —— 没有复验结论时(如 unavailable)
654
+ * 把这次修复当成普通成功呈现,正是本轮要消灭的谎报。行已从列表移出,所以结果留在这里。
655
+ */
656
+ const [applied, setApplied] = useState<{ unitId: string; result: SessionRepairResult } | null>(null)
657
+ /**
658
+ * 复验三态 / 危险态的字典键(**判定全部在 ui 纯函数层**,组件只装配 —— 见 sessionRepairVerifyKey)。
659
+ * null = 没有复验结论(预览路径)⇒ 不渲染这一行,绝不臆造「已验证」。
660
+ */
661
+ const planVerifyKey = plan !== null ? sessionRepairVerifyKey(plan.result.verify, plan.result.rolledBack) : null
662
+ const appliedVerifyKey = applied !== null ? sessionRepairVerifyKey(applied.result.verify, applied.result.rolledBack) : null
346
663
  /** 待确认的回滚目标(ConfirmDialog,danger) */
347
664
  const [rollbackTarget, setRollbackTarget] = useState<SessionRepairEntryView | null>(null)
348
665
  /**
@@ -352,14 +669,15 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
352
669
  * 「哪几条成了、哪几条为什么没成」,绝不只给一个计数。
353
670
  */
354
671
  const [batchBusy, setBatchBusy] = useState(false)
355
- const [batch, setBatch] = useState<{ ok: number; failed: number; results: BatchRepairResult[] } | null>(null)
672
+ const [batch, setBatch] = useState<{ ok: number; failed: number; results: SessionRepairBatchEntryView[] } | null>(null)
356
673
 
357
674
  const startRepair = (unitId: string): void => {
358
675
  setBusy(unitId)
359
676
  setPlan(null)
677
+ setApplied(null)
360
678
  recoveryApi.repairSession(unitId, false)
361
679
  .then((result) => { setPlan({ unitId, result }); setBusy(null) })
362
- .catch((err) => { toast.error(err instanceof Error ? err.message : String(err)); setBusy(null) })
680
+ .catch((err) => { toast.error(redactErrorText(err)); setBusy(null) })
363
681
  }
364
682
 
365
683
  /**
@@ -376,7 +694,7 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
376
694
  setBatch(null)
377
695
  let ok = 0
378
696
  let failed = 0
379
- const results: BatchRepairResult[] = []
697
+ const results: SessionRepairBatchEntryView[] = []
380
698
  const done: string[] = []
381
699
  for (const unitId of targets) {
382
700
  setBusy(unitId)
@@ -384,27 +702,53 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
384
702
  const plan = await recoveryApi.repairSession(unitId, false)
385
703
  if (!plan.ok) {
386
704
  failed += 1
387
- results.push({ unitId, sessionId: plan.sessionId ?? unitId, ok: false, reason: plan.reason })
705
+ results.push(sessionRepairBatchEntry({
706
+ unitId,
707
+ sessionId: plan.sessionId ?? unitId,
708
+ ok: false,
709
+ ...(plan.reason !== undefined ? { reason: plan.reason } : {}),
710
+ }))
388
711
  continue
389
712
  }
390
713
  if (plan.lossy === true) {
391
714
  // 有损(截断)绝不批量代做 —— 逐条确认是硬约束,这里如实说明「需要你逐条确认」
392
715
  failed += 1
393
- results.push({ unitId, sessionId: plan.sessionId ?? unitId, ok: false, reason: 'lossy-required', lossy: true })
716
+ results.push(sessionRepairBatchEntry({ unitId, sessionId: plan.sessionId ?? unitId, ok: false, reason: 'lossy-required', lossy: true }))
394
717
  continue
395
718
  }
396
719
  const applied = await recoveryApi.repairSession(unitId, true, plan.expect)
397
720
  if (applied.ok) {
398
721
  ok += 1
399
722
  done.push(unitId)
400
- results.push({ unitId, sessionId: applied.sessionId ?? unitId, ok: true, droppedRows: applied.droppedRows ?? 0 })
723
+ results.push(sessionRepairBatchEntry({
724
+ unitId,
725
+ sessionId: applied.sessionId ?? unitId,
726
+ ok: true,
727
+ droppedRows: applied.droppedRows ?? 0,
728
+ ...(applied.verify !== undefined ? { verify: applied.verify } : {}),
729
+ ...(applied.rolledBack !== undefined ? { rolledBack: applied.rolledBack } : {}),
730
+ }))
401
731
  } else {
402
732
  failed += 1
403
- results.push({ unitId, sessionId: applied.sessionId ?? unitId, ok: false, reason: applied.reason })
733
+ // A2-F1:失败分支与成功分支**同源**搬运 —— verify-failed 必带 verify,
734
+ // rolledBack===false(自动回滚未成功)是危险态,必须在批量视图里可见。
735
+ results.push(sessionRepairBatchEntry({
736
+ unitId,
737
+ sessionId: applied.sessionId ?? unitId,
738
+ ok: false,
739
+ ...(applied.reason !== undefined ? { reason: applied.reason } : {}),
740
+ ...(applied.verify !== undefined ? { verify: applied.verify } : {}),
741
+ ...(applied.rolledBack !== undefined ? { rolledBack: applied.rolledBack } : {}),
742
+ }))
404
743
  }
405
744
  } catch (err) {
406
745
  failed += 1
407
- results.push({ unitId, sessionId: unitId, ok: false, transportError: err instanceof Error ? err.message : String(err) })
746
+ results.push(sessionRepairBatchEntry({
747
+ unitId,
748
+ sessionId: unitId,
749
+ ok: false,
750
+ transportError: err instanceof Error ? err.message : String(err),
751
+ }))
408
752
  }
409
753
  }
410
754
  setBusy(null)
@@ -425,6 +769,8 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
425
769
  if (next.ok) {
426
770
  toast.ok(t('sessions.repair.applied', { rows: String(next.droppedRows ?? 0) }))
427
771
  setPlan(null)
772
+ // 结果(含复验三态)留在面板上:绝不把「写完了」说成「已可加载」
773
+ setApplied({ unitId, result: next })
428
774
  setRepairedUnits((prev) => new Set([...prev, unitId]))
429
775
  } else {
430
776
  // 应用期被拒(指纹变化 / 校验不过 / 写失败):**就地给出真实原因**,不假装成功
@@ -432,7 +778,7 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
432
778
  }
433
779
  setBusy(null)
434
780
  })
435
- .catch((err) => { toast.error(err instanceof Error ? err.message : String(err)); setBusy(null) })
781
+ .catch((err) => { toast.error(redactErrorText(err)); setBusy(null) })
436
782
  }
437
783
 
438
784
  const doRollback = async (): Promise<void> => {
@@ -450,7 +796,7 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
450
796
  setRollbackTarget(null)
451
797
  }
452
798
  } catch (err) {
453
- toast.error(err instanceof Error ? err.message : String(err))
799
+ toast.error(redactErrorText(err))
454
800
  setRollbackTarget(null)
455
801
  } finally {
456
802
  setBusy(null)
@@ -468,6 +814,15 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
468
814
  </Button>
469
815
  </div>
470
816
 
817
+ {/*
818
+ F-2:官方 session.export 通道的**可用性说明**(逐行入口见下方列表)。
819
+ ZIP 由 DSH 自己打包(含子会话与附件);体检/修复动的是本机文件,两者不是一回事。
820
+ 不可用时界面给出**可读原因**,绝不静默消失(消失会让用户以为插件根本没做这个功能)。
821
+ */}
822
+ {zipHint !== null && (
823
+ <Banner kind="warn">{zipHint}</Banner>
824
+ )}
825
+
471
826
  {phase === 'error' && <Banner kind="error">{error ?? t('common.unknownError')}</Banner>}
472
827
 
473
828
  {phase === 'ready' && response !== null && (
@@ -496,6 +851,14 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
496
851
  <Badge kind="info">{t('sessions.targetFormat', { version: String(summary.targetFormatVersion) })}</Badge>
497
852
  )}
498
853
  </div>
854
+ {/* E2:本档(blocksStartup 的 location-mismatch / duplicate-id)的**应用内写入口**。
855
+ 只在有候选行或手上有计划/结果时渲染;判定与键映射全在 ui/session-layout-view.ts。 */}
856
+ <SessionLayoutSection
857
+ recoveryApi={recoveryApi}
858
+ t={t}
859
+ response={response}
860
+ onScan={onScan}
861
+ />
499
862
  <div className={css.hint}>
500
863
  {summary.repairable > 0
501
864
  ? t('sessions.repair.available', { count: String(summary.repairable) })
@@ -524,25 +887,31 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
524
887
  {t('sessions.repair.allDone', { ok: String(batch.ok), failed: String(batch.failed) })}
525
888
  </span>
526
889
  {/* 逐条留痕:修好了几条、剩下几条为什么没修成 —— 绝不只给一个计数 */}
527
- {batch.results.map((item) => (
528
- <div key={item.unitId} className={css.snapshotRowMeta}>
529
- <span className={css.snapshotRowBadges}>
530
- <Badge kind={item.ok ? 'ok' : 'warn'}>
531
- {item.ok ? t('sessions.repair.batch.ok') : t('sessions.repair.batch.failed')}
532
- </Badge>
533
- </span>
534
- <span className={css.snapshotRowIssues} title={item.unitId}>{item.sessionId}</span>
535
- <span className={css.snapshotRowFacts}>
536
- {item.ok
537
- ? t('sessions.repair.batch.dropped', { rows: String(item.droppedRows ?? 0) })
538
- : item.transportError !== undefined
539
- ? t('sessions.repair.batch.transportError', { message: item.transportError })
540
- : item.lossy === true
541
- ? t('sessions.repair.reason.lossyRequired')
542
- : t(sessionRepairReasonKey(item.reason))}
543
- </span>
544
- </div>
545
- ))}
890
+ {batch.results.map((item) => {
891
+ // 一键修复的成功项同样要说出复验结论(三态判定在 ui 纯函数层,组件只查字典)
892
+ // 与单条修复路径同一个纯函数、同一套 locale 键(失败项也必须给出复验结论)
893
+ const verifyKey = sessionRepairVerifyKey(item.verify, item.rolledBack)
894
+ return (
895
+ <div key={item.unitId} className={css.snapshotRowMeta}>
896
+ <span className={css.snapshotRowBadges}>
897
+ <Badge kind={item.ok ? 'ok' : 'warn'}>
898
+ {item.ok ? t('sessions.repair.batch.ok') : t('sessions.repair.batch.failed')}
899
+ </Badge>
900
+ </span>
901
+ <span className={css.snapshotRowIssues} title={item.unitId}>{item.sessionId}</span>
902
+ <span className={css.snapshotRowFacts}>
903
+ {item.ok
904
+ ? t('sessions.repair.batch.dropped', { rows: String(item.droppedRows ?? 0) })
905
+ : item.transportError !== undefined
906
+ ? t('sessions.repair.batch.transportError', { message: item.transportError })
907
+ : item.lossy === true
908
+ ? t('sessions.repair.reason.lossyRequired')
909
+ : t(sessionRepairReasonKey(item.reason))}
910
+ {verifyKey !== null && <> · {t(verifyKey)}</>}
911
+ </span>
912
+ </div>
913
+ )
914
+ })}
546
915
  </div>
547
916
  </div>
548
917
  )}
@@ -591,6 +960,20 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
591
960
  </Button>
592
961
  </div>
593
962
  )}
963
+ {/* F-2:下载该会话的原始日志 ZIP(**只读**;由 DSH 自己打包,含子会话与附件)。
964
+ 不可用时**禁用并给出原因**(不静默消失)—— 三态判定在 session-export-view.ts。 */}
965
+ <div className={css.actionRow}>
966
+ <Button
967
+ variant="ghost"
968
+ size="sm"
969
+ disabled={!zipState.enabled || zipBusy !== null}
970
+ loading={zipBusy === row.sessionId}
971
+ title={zipHint ?? t('sessions.zip.hint')}
972
+ onClick={() => { downloadZip(row.sessionId) }}
973
+ >
974
+ {t('sessions.zip.action')}
975
+ </Button>
976
+ </div>
594
977
  </div>
595
978
  ))}
596
979
  </div>
@@ -630,6 +1013,9 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
630
1013
  <div className={css.hint}>{t('sessions.repair.rejectedNext')}</div>
631
1014
  </>
632
1015
  )}
1016
+ {/* 写后真 codec 复验结论(现役读盘可读 / 迁移链可还原 / 未验证 / 回滚失败)——
1017
+ key 由 ui 纯函数判定,组件不写三态逻辑 */}
1018
+ {planVerifyKey !== null && <div className={css.hint}>{t(planVerifyKey)}</div>}
633
1019
  <div className={css.actionRow}>
634
1020
  {plan.result.ok && (
635
1021
  <Button variant="primary" loading={busy !== null} onClick={applyRepair}>
@@ -642,6 +1028,23 @@ function SessionHealthDialog(props: SessionHealthDialogProps) {
642
1028
  </div>
643
1029
  </div>
644
1030
  )}
1031
+ {/* 应用成功后的结果(含真 codec 复验结论):绝不把「写完了」说成「已可加载」 */}
1032
+ {applied !== null && (
1033
+ <div className={css.snapshotRow}>
1034
+ <div className={css.snapshotRowMain}>
1035
+ <span title={applied.unitId}>{applied.result.sessionId ?? applied.unitId}</span>
1036
+ </div>
1037
+ <div className={css.hint}>
1038
+ {t('sessions.repair.applied', { rows: String(applied.result.droppedRows ?? 0) })}
1039
+ </div>
1040
+ {appliedVerifyKey !== null && <div className={css.hint}>{t(appliedVerifyKey)}</div>}
1041
+ <div className={css.actionRow}>
1042
+ <Button variant="ghost" onClick={() => { setApplied(null) }}>
1043
+ {t('common.close')}
1044
+ </Button>
1045
+ </div>
1046
+ </div>
1047
+ )}
645
1048
  {/* 应用内不动的问题类别:离线出路 */}
646
1049
  <div className={css.groupLabel}>{t('sessions.repair.title')}</div>
647
1050
  <div className={css.hint}>{t('sessions.repair.desc')}</div>
@@ -781,13 +1184,13 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
781
1184
  incidentApi.rescueOn().then(
782
1185
  (res) => {
783
1186
  setRescueBusy(false)
784
- if (!res.ok) { toast.error(res.message ?? t('common.unknownError')); return }
1187
+ if (!res.ok) { toast.error(redact(res.message ?? t('common.unknownError'))); return }
785
1188
  toast.ok(t('recovery.rescue.on'))
786
1189
  loadIncident()
787
1190
  },
788
1191
  (err) => {
789
1192
  setRescueBusy(false)
790
- toast.error(err instanceof Error ? err.message : String(err))
1193
+ toast.error(redactErrorText(err))
791
1194
  },
792
1195
  )
793
1196
  }
@@ -798,13 +1201,13 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
798
1201
  incidentApi.rescueOff().then(
799
1202
  (res) => {
800
1203
  setRescueBusy(false)
801
- if (!res.ok) { toast.error(res.message ?? t('common.unknownError')); return }
1204
+ if (!res.ok) { toast.error(redact(res.message ?? t('common.unknownError'))); return }
802
1205
  toast.ok(t('recovery.rescue.off'))
803
1206
  loadIncident()
804
1207
  },
805
1208
  (err) => {
806
1209
  setRescueBusy(false)
807
- toast.error(err instanceof Error ? err.message : String(err))
1210
+ toast.error(redactErrorText(err))
808
1211
  },
809
1212
  )
810
1213
  }
@@ -845,7 +1248,7 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
845
1248
  patch({ running: false })
846
1249
  runStore.stopRunWatch('recovery')
847
1250
  // 确认弹窗已关闭 → 用 Toast 送达(写 panel state 将无渲染点)
848
- toast.error(err instanceof Error ? err.message : String(err))
1251
+ toast.error(redactErrorText(err))
849
1252
  return null
850
1253
  },
851
1254
  ).then((verifyResult) => {
@@ -872,7 +1275,7 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
872
1275
  (err) => {
873
1276
  patch({ running: false })
874
1277
  runStore.stopRunWatch('recovery')
875
- toast.error(err instanceof Error ? err.message : String(err))
1278
+ toast.error(redactErrorText(err))
876
1279
  return null
877
1280
  },
878
1281
  ).then((verifyResult) => {
@@ -899,7 +1302,7 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
899
1302
  },
900
1303
  (err) => {
901
1304
  patch({ running: false })
902
- toast.error(err instanceof Error ? err.message : String(err))
1305
+ toast.error(redactErrorText(err))
903
1306
  },
904
1307
  )
905
1308
  }
@@ -919,7 +1322,7 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
919
1322
  },
920
1323
  (err) => {
921
1324
  setLockBusy(false)
922
- toast.error(err instanceof Error ? err.message : String(err))
1325
+ toast.error(redactErrorText(err))
923
1326
  },
924
1327
  )
925
1328
  }
@@ -942,7 +1345,7 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
942
1345
  },
943
1346
  (err) => {
944
1347
  setSafeModeBusy(false)
945
- toast.error(err instanceof Error ? err.message : String(err))
1348
+ toast.error(redactErrorText(err))
946
1349
  },
947
1350
  )
948
1351
  }
@@ -998,13 +1401,37 @@ export function RecoveryPanel(props: RecoveryPanelProps) {
998
1401
  </Card>
999
1402
  )}
1000
1403
 
1404
+ {/* t54:中断的档案复制残留(cross-F3)—— 独立形态 + 只给删除;删除走既有 POST /profiles/delete,
1405
+ 被 SAFE MODE 拦下(423 mutation-locked)时给原因与本页出口(safe-mode/clear)。 */}
1406
+ {view !== null && view.incompleteCopies.length > 0 && (
1407
+ <IncompleteCopiesSection
1408
+ rows={view.incompleteCopies}
1409
+ t={t}
1410
+ diskApi={props.diskApi}
1411
+ recoveryApi={props.recoveryApi}
1412
+ onChanged={load}
1413
+ />
1414
+ )}
1415
+
1416
+ {/* t89:残留**枚举失败** —— 与上面「有残留」和「暂无残留」都不同形:显式告知「读不到 ≠ 没有」,
1417
+ 并给唯一的可行动作(重试 = 重拉 /recovery/status)。判定在 recovery-view.ts 的纯函数里,这里只装配。 */}
1418
+ {view !== null && view.incompleteCopiesNotice === 'unreadable' && (
1419
+ <Card>
1420
+ <SectionTitle title={t('recovery.incomplete.unreadable.title')} />
1421
+ <Banner kind="warn">{t('recovery.incomplete.unreadable.hint')}</Banner>
1422
+ <div className={css.actionRow}>
1423
+ <Button onClick={load}>{t('common.retry')}</Button>
1424
+ </div>
1425
+ </Card>
1426
+ )}
1427
+
1001
1428
  {/* 磁盘占用与清理(m-disk-usage):§9 固定的第 2 块。
1002
1429
  三条硬边界一字不动(候选集只有可重建区 + 显式勾选的过期导出;snapshots/sync 永不在候选集;
1003
1430
  界面数字由渲染行现算)—— 实现全在 DiskUsageCard 与 ui/disk-usage-view.ts 里。 */}
1004
1431
  <DiskUsageCard api={props.diskApi} infoHintLabel={props.infoHintLabel ?? props.t('common.infoHint')} />
1005
1432
 
1006
1433
  {/* T5:会话体检(只读)—— 与「我的对话去哪了」是同一件事,故与事故恢复同屏 */}
1007
- <SessionHealthCard recoveryApi={recoveryApi} t={t} copyT={props.copyT} />
1434
+ <SessionHealthCard recoveryApi={recoveryApi} t={t} copyT={props.copyT} diskApi={props.diskApi} />
1008
1435
 
1009
1436
  {/* SAFE MODE / recovery-required 状态提示(正常态不渲染任何横幅——无事项即静默) */}
1010
1437
  {view?.recoveryRequired === true && (