p-spore 0.1.4 → 0.1.43

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 (394) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +242 -67
  3. package/dist/agents/agent-manager.d.ts +3 -3
  4. package/dist/agents/agent-manager.d.ts.map +1 -1
  5. package/dist/agents/agent-manager.js +3 -2
  6. package/dist/agents/agent-manager.js.map +1 -1
  7. package/dist/application/agent-application.d.ts +29 -5
  8. package/dist/application/agent-application.d.ts.map +1 -1
  9. package/dist/application/agent-application.js +444 -14
  10. package/dist/application/agent-application.js.map +1 -1
  11. package/dist/articles/diff.d.ts +70 -0
  12. package/dist/articles/diff.d.ts.map +1 -0
  13. package/dist/articles/diff.js +330 -0
  14. package/dist/articles/diff.js.map +1 -0
  15. package/dist/articles/docx.d.ts +10 -0
  16. package/dist/articles/docx.d.ts.map +1 -0
  17. package/dist/articles/docx.js +219 -0
  18. package/dist/articles/docx.js.map +1 -0
  19. package/dist/articles/draft.d.ts +18 -0
  20. package/dist/articles/draft.d.ts.map +1 -0
  21. package/dist/articles/draft.js +88 -0
  22. package/dist/articles/draft.js.map +1 -0
  23. package/dist/articles/edits.d.ts +24 -0
  24. package/dist/articles/edits.d.ts.map +1 -0
  25. package/dist/articles/edits.js +54 -0
  26. package/dist/articles/edits.js.map +1 -0
  27. package/dist/articles/html.d.ts +10 -0
  28. package/dist/articles/html.d.ts.map +1 -0
  29. package/dist/articles/html.js +122 -0
  30. package/dist/articles/html.js.map +1 -0
  31. package/dist/articles/plain.d.ts +8 -0
  32. package/dist/articles/plain.d.ts.map +1 -0
  33. package/dist/articles/plain.js +69 -0
  34. package/dist/articles/plain.js.map +1 -0
  35. package/dist/articles/preview.d.ts +43 -0
  36. package/dist/articles/preview.d.ts.map +1 -0
  37. package/dist/articles/preview.js +90 -0
  38. package/dist/articles/preview.js.map +1 -0
  39. package/dist/articles/publish.d.ts +10 -0
  40. package/dist/articles/publish.d.ts.map +1 -0
  41. package/dist/articles/publish.js +54 -0
  42. package/dist/articles/publish.js.map +1 -0
  43. package/dist/articles/receipt.d.ts +16 -0
  44. package/dist/articles/receipt.d.ts.map +1 -0
  45. package/dist/articles/receipt.js +27 -0
  46. package/dist/articles/receipt.js.map +1 -0
  47. package/dist/articles/store.d.ts +78 -0
  48. package/dist/articles/store.d.ts.map +1 -0
  49. package/dist/articles/store.js +308 -0
  50. package/dist/articles/store.js.map +1 -0
  51. package/dist/articles/tool.d.ts +14 -0
  52. package/dist/articles/tool.d.ts.map +1 -0
  53. package/dist/articles/tool.js +175 -0
  54. package/dist/articles/tool.js.map +1 -0
  55. package/dist/articles/writer-context.d.ts +5 -0
  56. package/dist/articles/writer-context.d.ts.map +1 -0
  57. package/dist/articles/writer-context.js +56 -0
  58. package/dist/articles/writer-context.js.map +1 -0
  59. package/dist/articles/writer.d.ts +18 -0
  60. package/dist/articles/writer.d.ts.map +1 -0
  61. package/dist/articles/writer.js +79 -0
  62. package/dist/articles/writer.js.map +1 -0
  63. package/dist/articles/zip-write.d.ts +11 -0
  64. package/dist/articles/zip-write.d.ts.map +1 -0
  65. package/dist/articles/zip-write.js +63 -0
  66. package/dist/articles/zip-write.js.map +1 -0
  67. package/dist/bundled-skills/README.md +17 -0
  68. package/dist/bundled-skills/aegis-search/SKILL.md +59 -0
  69. package/dist/bundled-skills/aegis-search/bin/.gitkeep +0 -0
  70. package/dist/bundled-skills/article-canvas/SKILL.md +28 -0
  71. package/dist/bundled-skills/binaries.json +23 -0
  72. package/dist/bundled-skills/doc-extract/SKILL.md +56 -0
  73. package/dist/cli/main.d.ts.map +1 -1
  74. package/dist/cli/main.js +407 -54
  75. package/dist/cli/main.js.map +1 -1
  76. package/dist/config/config.d.ts +43 -0
  77. package/dist/config/config.d.ts.map +1 -1
  78. package/dist/config/config.js +150 -6
  79. package/dist/config/config.js.map +1 -1
  80. package/dist/daemon/daemon.d.ts.map +1 -1
  81. package/dist/daemon/daemon.js +80 -11
  82. package/dist/daemon/daemon.js.map +1 -1
  83. package/dist/desktop/application.d.ts +4 -0
  84. package/dist/desktop/application.d.ts.map +1 -0
  85. package/dist/desktop/application.js +1688 -0
  86. package/dist/desktop/application.js.map +1 -0
  87. package/dist/desktop/article-export.d.ts +12 -0
  88. package/dist/desktop/article-export.d.ts.map +1 -0
  89. package/dist/desktop/article-export.js +87 -0
  90. package/dist/desktop/article-export.js.map +1 -0
  91. package/dist/desktop/assets/pspore.png +0 -0
  92. package/dist/desktop/assets/pspore.svg +24 -0
  93. package/dist/desktop/conversation-smoke.d.ts +5 -0
  94. package/dist/desktop/conversation-smoke.d.ts.map +1 -0
  95. package/dist/desktop/conversation-smoke.js +107 -0
  96. package/dist/desktop/conversation-smoke.js.map +1 -0
  97. package/dist/desktop/daemon-bridge.d.ts.map +1 -1
  98. package/dist/desktop/daemon-bridge.js +62 -17
  99. package/dist/desktop/daemon-bridge.js.map +1 -1
  100. package/dist/desktop/ipc-contract.d.ts +124 -0
  101. package/dist/desktop/ipc-contract.d.ts.map +1 -1
  102. package/dist/desktop/ipc-contract.js +191 -12
  103. package/dist/desktop/ipc-contract.js.map +1 -1
  104. package/dist/desktop/local-path.d.ts +15 -0
  105. package/dist/desktop/local-path.d.ts.map +1 -0
  106. package/dist/desktop/local-path.js +59 -0
  107. package/dist/desktop/local-path.js.map +1 -0
  108. package/dist/desktop/main.d.ts +10 -1
  109. package/dist/desktop/main.d.ts.map +1 -1
  110. package/dist/desktop/main.js +122 -530
  111. package/dist/desktop/main.js.map +1 -1
  112. package/dist/desktop/native-startup.d.ts +4 -0
  113. package/dist/desktop/native-startup.d.ts.map +1 -0
  114. package/dist/desktop/native-startup.js +57 -0
  115. package/dist/desktop/native-startup.js.map +1 -0
  116. package/dist/desktop/preload.cjs +28 -1
  117. package/dist/desktop/renderer/article-panel.d.ts +45 -0
  118. package/dist/desktop/renderer/article-panel.d.ts.map +1 -0
  119. package/dist/desktop/renderer/article-panel.js +1126 -0
  120. package/dist/desktop/renderer/article-panel.js.map +1 -0
  121. package/dist/desktop/renderer/article-split.d.ts +32 -0
  122. package/dist/desktop/renderer/article-split.d.ts.map +1 -0
  123. package/dist/desktop/renderer/article-split.js +48 -0
  124. package/dist/desktop/renderer/article-split.js.map +1 -0
  125. package/dist/desktop/renderer/composer-file-references.d.ts +23 -0
  126. package/dist/desktop/renderer/composer-file-references.d.ts.map +1 -0
  127. package/dist/desktop/renderer/composer-file-references.js +51 -0
  128. package/dist/desktop/renderer/composer-file-references.js.map +1 -0
  129. package/dist/desktop/renderer/conversation.d.ts +23 -0
  130. package/dist/desktop/renderer/conversation.d.ts.map +1 -0
  131. package/dist/desktop/renderer/conversation.js +66 -0
  132. package/dist/desktop/renderer/conversation.js.map +1 -0
  133. package/dist/desktop/renderer/dom-reconcile.d.ts +15 -0
  134. package/dist/desktop/renderer/dom-reconcile.d.ts.map +1 -0
  135. package/dist/desktop/renderer/dom-reconcile.js +33 -0
  136. package/dist/desktop/renderer/dom-reconcile.js.map +1 -0
  137. package/dist/desktop/renderer/index.html +211 -34
  138. package/dist/desktop/renderer/indexing-view.d.ts +22 -0
  139. package/dist/desktop/renderer/indexing-view.d.ts.map +1 -0
  140. package/dist/desktop/renderer/indexing-view.js +115 -0
  141. package/dist/desktop/renderer/indexing-view.js.map +1 -0
  142. package/dist/desktop/renderer/markdown-view.d.ts +23 -0
  143. package/dist/desktop/renderer/markdown-view.d.ts.map +1 -0
  144. package/dist/desktop/renderer/markdown-view.js +436 -0
  145. package/dist/desktop/renderer/markdown-view.js.map +1 -0
  146. package/dist/desktop/renderer/motion.d.ts +67 -0
  147. package/dist/desktop/renderer/motion.d.ts.map +1 -0
  148. package/dist/desktop/renderer/motion.js +296 -0
  149. package/dist/desktop/renderer/motion.js.map +1 -0
  150. package/dist/desktop/renderer/renderer.js +1799 -152
  151. package/dist/desktop/renderer/renderer.js.map +1 -1
  152. package/dist/desktop/renderer/session-find.d.ts +18 -0
  153. package/dist/desktop/renderer/session-find.d.ts.map +1 -0
  154. package/dist/desktop/renderer/session-find.js +29 -0
  155. package/dist/desktop/renderer/session-find.js.map +1 -0
  156. package/dist/desktop/renderer/session-search.d.ts +30 -0
  157. package/dist/desktop/renderer/session-search.d.ts.map +1 -0
  158. package/dist/desktop/renderer/session-search.js +296 -0
  159. package/dist/desktop/renderer/session-search.js.map +1 -0
  160. package/dist/desktop/renderer/store.d.ts +33 -0
  161. package/dist/desktop/renderer/store.d.ts.map +1 -1
  162. package/dist/desktop/renderer/store.js +92 -19
  163. package/dist/desktop/renderer/store.js.map +1 -1
  164. package/dist/desktop/renderer/styles.css +928 -268
  165. package/dist/desktop/renderer/vendor/gsap.min.js +11 -0
  166. package/dist/desktop/renderer-diagnostics.d.ts +5 -0
  167. package/dist/desktop/renderer-diagnostics.d.ts.map +1 -0
  168. package/dist/desktop/renderer-diagnostics.js +47 -0
  169. package/dist/desktop/renderer-diagnostics.js.map +1 -0
  170. package/dist/desktop/security.d.ts +26 -1
  171. package/dist/desktop/security.d.ts.map +1 -1
  172. package/dist/desktop/security.js +26 -6
  173. package/dist/desktop/security.js.map +1 -1
  174. package/dist/desktop/smoke-fixture.d.ts.map +1 -1
  175. package/dist/desktop/smoke-fixture.js +77 -3
  176. package/dist/desktop/smoke-fixture.js.map +1 -1
  177. package/dist/desktop/splash/index.html +25 -0
  178. package/dist/desktop/splash/splash.d.ts +2 -0
  179. package/dist/desktop/splash/splash.d.ts.map +1 -0
  180. package/dist/desktop/splash/splash.js +8 -0
  181. package/dist/desktop/splash/splash.js.map +1 -0
  182. package/dist/desktop/splash/styles.css +24 -0
  183. package/dist/desktop/system-open.d.ts +16 -0
  184. package/dist/desktop/system-open.d.ts.map +1 -0
  185. package/dist/desktop/system-open.js +90 -0
  186. package/dist/desktop/system-open.js.map +1 -0
  187. package/dist/desktop/validation.d.ts.map +1 -1
  188. package/dist/desktop/validation.js +99 -21
  189. package/dist/desktop/validation.js.map +1 -1
  190. package/dist/desktop/window-security.d.ts +4 -0
  191. package/dist/desktop/window-security.d.ts.map +1 -0
  192. package/dist/desktop/window-security.js +26 -0
  193. package/dist/desktop/window-security.js.map +1 -0
  194. package/dist/documents/errors.d.ts +12 -0
  195. package/dist/documents/errors.d.ts.map +1 -0
  196. package/dist/documents/errors.js +12 -0
  197. package/dist/documents/errors.js.map +1 -0
  198. package/dist/documents/extract.d.ts +37 -0
  199. package/dist/documents/extract.d.ts.map +1 -0
  200. package/dist/documents/extract.js +90 -0
  201. package/dist/documents/extract.js.map +1 -0
  202. package/dist/documents/legacy-office.d.ts +23 -0
  203. package/dist/documents/legacy-office.d.ts.map +1 -0
  204. package/dist/documents/legacy-office.js +439 -0
  205. package/dist/documents/legacy-office.js.map +1 -0
  206. package/dist/documents/ole.d.ts +20 -0
  207. package/dist/documents/ole.d.ts.map +1 -0
  208. package/dist/documents/ole.js +189 -0
  209. package/dist/documents/ole.js.map +1 -0
  210. package/dist/documents/ooxml.d.ts +23 -0
  211. package/dist/documents/ooxml.d.ts.map +1 -0
  212. package/dist/documents/ooxml.js +197 -0
  213. package/dist/documents/ooxml.js.map +1 -0
  214. package/dist/documents/pdf.d.ts +20 -0
  215. package/dist/documents/pdf.d.ts.map +1 -0
  216. package/dist/documents/pdf.js +95 -0
  217. package/dist/documents/pdf.js.map +1 -0
  218. package/dist/documents/plain-text.d.ts +22 -0
  219. package/dist/documents/plain-text.d.ts.map +1 -0
  220. package/dist/documents/plain-text.js +57 -0
  221. package/dist/documents/plain-text.js.map +1 -0
  222. package/dist/documents/zip.d.ts +15 -0
  223. package/dist/documents/zip.d.ts.map +1 -0
  224. package/dist/documents/zip.js +137 -0
  225. package/dist/documents/zip.js.map +1 -0
  226. package/dist/domain/indexing.d.ts +60 -0
  227. package/dist/domain/indexing.d.ts.map +1 -0
  228. package/dist/domain/indexing.js +13 -0
  229. package/dist/domain/indexing.js.map +1 -0
  230. package/dist/domain/runtime.d.ts +133 -5
  231. package/dist/domain/runtime.d.ts.map +1 -1
  232. package/dist/gateway/local-gateway.d.ts +8 -0
  233. package/dist/gateway/local-gateway.d.ts.map +1 -1
  234. package/dist/gateway/local-gateway.js +105 -21
  235. package/dist/gateway/local-gateway.js.map +1 -1
  236. package/dist/indexing/index-service.d.ts +56 -0
  237. package/dist/indexing/index-service.d.ts.map +1 -0
  238. package/dist/indexing/index-service.js +262 -0
  239. package/dist/indexing/index-service.js.map +1 -0
  240. package/dist/kernel/loop.d.ts +10 -0
  241. package/dist/kernel/loop.d.ts.map +1 -1
  242. package/dist/kernel/loop.js +6 -2
  243. package/dist/kernel/loop.js.map +1 -1
  244. package/dist/kernel/reducer.d.ts.map +1 -1
  245. package/dist/kernel/reducer.js +17 -1
  246. package/dist/kernel/reducer.js.map +1 -1
  247. package/dist/kernel/state.d.ts +17 -4
  248. package/dist/kernel/state.d.ts.map +1 -1
  249. package/dist/kernel/state.js +2 -0
  250. package/dist/kernel/state.js.map +1 -1
  251. package/dist/pi/context-budget.d.ts +8 -0
  252. package/dist/pi/context-budget.d.ts.map +1 -0
  253. package/dist/pi/context-budget.js +62 -0
  254. package/dist/pi/context-budget.js.map +1 -0
  255. package/dist/pi/pi-runtime-adapter.d.ts +38 -3
  256. package/dist/pi/pi-runtime-adapter.d.ts.map +1 -1
  257. package/dist/pi/pi-runtime-adapter.js +551 -56
  258. package/dist/pi/pi-runtime-adapter.js.map +1 -1
  259. package/dist/plugins/bundled-skills.d.ts +56 -0
  260. package/dist/plugins/bundled-skills.d.ts.map +1 -0
  261. package/dist/plugins/bundled-skills.js +243 -0
  262. package/dist/plugins/bundled-skills.js.map +1 -0
  263. package/dist/plugins/extension-manager.d.ts +23 -1
  264. package/dist/plugins/extension-manager.d.ts.map +1 -1
  265. package/dist/plugins/extension-manager.js +117 -0
  266. package/dist/plugins/extension-manager.js.map +1 -1
  267. package/dist/plugins/types.d.ts +29 -0
  268. package/dist/plugins/types.d.ts.map +1 -1
  269. package/dist/plugins/workspace-skills.d.ts +36 -0
  270. package/dist/plugins/workspace-skills.d.ts.map +1 -0
  271. package/dist/plugins/workspace-skills.js +309 -0
  272. package/dist/plugins/workspace-skills.js.map +1 -0
  273. package/dist/policy/policy.d.ts.map +1 -1
  274. package/dist/policy/policy.js +22 -3
  275. package/dist/policy/policy.js.map +1 -1
  276. package/dist/protocol/client.d.ts +1 -0
  277. package/dist/protocol/client.d.ts.map +1 -1
  278. package/dist/protocol/client.js +49 -24
  279. package/dist/protocol/client.js.map +1 -1
  280. package/dist/protocol/limits.d.ts +69 -0
  281. package/dist/protocol/limits.d.ts.map +1 -0
  282. package/dist/protocol/limits.js +166 -0
  283. package/dist/protocol/limits.js.map +1 -0
  284. package/dist/protocol/schema.d.ts +6 -0
  285. package/dist/protocol/schema.d.ts.map +1 -1
  286. package/dist/protocol/schema.js.map +1 -1
  287. package/dist/protocol/server-frame-codec.d.ts +18 -0
  288. package/dist/protocol/server-frame-codec.d.ts.map +1 -0
  289. package/dist/protocol/server-frame-codec.js +211 -0
  290. package/dist/protocol/server-frame-codec.js.map +1 -0
  291. package/dist/service/legacy-daemon.d.ts +6 -0
  292. package/dist/service/legacy-daemon.d.ts.map +1 -0
  293. package/dist/service/legacy-daemon.js +42 -0
  294. package/dist/service/legacy-daemon.js.map +1 -0
  295. package/dist/service/service-launch.d.ts +10 -0
  296. package/dist/service/service-launch.d.ts.map +1 -0
  297. package/dist/service/service-launch.js +56 -0
  298. package/dist/service/service-launch.js.map +1 -0
  299. package/dist/service/system-service.d.ts +57 -0
  300. package/dist/service/system-service.d.ts.map +1 -0
  301. package/dist/service/system-service.js +263 -0
  302. package/dist/service/system-service.js.map +1 -0
  303. package/dist/sessions/session-catalog.d.ts +45 -0
  304. package/dist/sessions/session-catalog.d.ts.map +1 -0
  305. package/dist/sessions/session-catalog.js +270 -0
  306. package/dist/sessions/session-catalog.js.map +1 -0
  307. package/dist/sessions/session-file.d.ts +12 -0
  308. package/dist/sessions/session-file.d.ts.map +1 -0
  309. package/dist/sessions/session-file.js +47 -0
  310. package/dist/sessions/session-file.js.map +1 -0
  311. package/dist/settings/compaction-settings.d.ts +10 -0
  312. package/dist/settings/compaction-settings.d.ts.map +1 -0
  313. package/dist/settings/compaction-settings.js +8 -0
  314. package/dist/settings/compaction-settings.js.map +1 -0
  315. package/dist/settings/indexing-settings-service.d.ts +16 -0
  316. package/dist/settings/indexing-settings-service.d.ts.map +1 -0
  317. package/dist/settings/indexing-settings-service.js +57 -0
  318. package/dist/settings/indexing-settings-service.js.map +1 -0
  319. package/dist/settings/indexing-settings.d.ts +30 -0
  320. package/dist/settings/indexing-settings.d.ts.map +1 -0
  321. package/dist/settings/indexing-settings.js +58 -0
  322. package/dist/settings/indexing-settings.js.map +1 -0
  323. package/dist/settings/llm-settings-service.d.ts +6 -0
  324. package/dist/settings/llm-settings-service.d.ts.map +1 -1
  325. package/dist/settings/llm-settings-service.js +137 -12
  326. package/dist/settings/llm-settings-service.js.map +1 -1
  327. package/dist/settings/llm-settings.d.ts +68 -2
  328. package/dist/settings/llm-settings.d.ts.map +1 -1
  329. package/dist/settings/llm-settings.js +119 -17
  330. package/dist/settings/llm-settings.js.map +1 -1
  331. package/dist/settings/model-api.d.ts +10 -0
  332. package/dist/settings/model-api.d.ts.map +1 -0
  333. package/dist/settings/model-api.js +10 -0
  334. package/dist/settings/model-api.js.map +1 -0
  335. package/dist/storage/home.d.ts +1 -0
  336. package/dist/storage/home.d.ts.map +1 -1
  337. package/dist/storage/home.js +37 -2
  338. package/dist/storage/home.js.map +1 -1
  339. package/dist/tools/core-tools.d.ts +2 -0
  340. package/dist/tools/core-tools.d.ts.map +1 -1
  341. package/dist/tools/core-tools.js +91 -1
  342. package/dist/tools/core-tools.js.map +1 -1
  343. package/dist/ui/shared/activity.d.ts +2 -0
  344. package/dist/ui/shared/activity.d.ts.map +1 -1
  345. package/dist/ui/shared/activity.js +18 -4
  346. package/dist/ui/shared/activity.js.map +1 -1
  347. package/dist/ui/shared/full-message.d.ts +22 -0
  348. package/dist/ui/shared/full-message.d.ts.map +1 -0
  349. package/dist/ui/shared/full-message.js +40 -0
  350. package/dist/ui/shared/full-message.js.map +1 -0
  351. package/dist/ui/shared/local-links.d.ts +7 -0
  352. package/dist/ui/shared/local-links.d.ts.map +1 -0
  353. package/dist/ui/shared/local-links.js +24 -0
  354. package/dist/ui/shared/local-links.js.map +1 -0
  355. package/dist/ui/shared/markdown-stream.d.ts +25 -0
  356. package/dist/ui/shared/markdown-stream.d.ts.map +1 -0
  357. package/dist/ui/shared/markdown-stream.js +35 -0
  358. package/dist/ui/shared/markdown-stream.js.map +1 -0
  359. package/dist/ui/shared/markdown.d.ts +70 -0
  360. package/dist/ui/shared/markdown.d.ts.map +1 -0
  361. package/dist/ui/shared/markdown.js +434 -0
  362. package/dist/ui/shared/markdown.js.map +1 -0
  363. package/dist/ui/shared/presentation.d.ts +10 -2
  364. package/dist/ui/shared/presentation.d.ts.map +1 -1
  365. package/dist/ui/shared/presentation.js +83 -22
  366. package/dist/ui/shared/presentation.js.map +1 -1
  367. package/dist/ui/shared/protocol-state.d.ts +19 -0
  368. package/dist/ui/shared/protocol-state.d.ts.map +1 -1
  369. package/dist/ui/shared/protocol-state.js +188 -26
  370. package/dist/ui/shared/protocol-state.js.map +1 -1
  371. package/dist/ui/shared/virtual-list.d.ts +34 -0
  372. package/dist/ui/shared/virtual-list.d.ts.map +1 -0
  373. package/dist/ui/shared/virtual-list.js +143 -0
  374. package/dist/ui/shared/virtual-list.js.map +1 -0
  375. package/dist/ui/tui/commands.d.ts +1 -1
  376. package/dist/ui/tui/commands.d.ts.map +1 -1
  377. package/dist/ui/tui/commands.js +8 -3
  378. package/dist/ui/tui/commands.js.map +1 -1
  379. package/dist/ui/tui/store.d.ts +22 -1
  380. package/dist/ui/tui/store.d.ts.map +1 -1
  381. package/dist/ui/tui/store.js +50 -3
  382. package/dist/ui/tui/store.js.map +1 -1
  383. package/dist/ui/tui/tui-app.d.ts.map +1 -1
  384. package/dist/ui/tui/tui-app.js +231 -31
  385. package/dist/ui/tui/tui-app.js.map +1 -1
  386. package/dist/ui/tui/view.d.ts +7 -0
  387. package/dist/ui/tui/view.d.ts.map +1 -1
  388. package/dist/ui/tui/view.js +161 -57
  389. package/dist/ui/tui/view.js.map +1 -1
  390. package/dist/version.d.ts +1 -1
  391. package/dist/version.d.ts.map +1 -1
  392. package/dist/version.js +1 -1
  393. package/dist/version.js.map +1 -1
  394. package/package.json +13 -2
@@ -0,0 +1,1126 @@
1
+ /// <reference lib="DOM" />
2
+ import { alignArticleDraft, articleChangeCounts, articleLineChangeCounts, diffArticleBlocks, diffArticleLines, } from "../../articles/diff.js";
3
+ import { parseArticleOpenReceipt, parseArticleReceipt } from "../../articles/receipt.js";
4
+ import { readFullMessage } from "../../ui/shared/full-message.js";
5
+ import { articleShareForWidth, articleWidthForShare, clampArticleWidth } from "./article-split.js";
6
+ import { reconcileChildren } from "./dom-reconcile.js";
7
+ import { appendMarkdown } from "./markdown-view.js";
8
+ import { animateArticleWidth, collapseArticleRemovals, pulseArticleChanges } from "./motion.js";
9
+ /** New blocks that pulse in at once. Past this the page flashes rather than draws attention. */
10
+ const MAX_PULSED_BLOCKS = 16;
11
+ /**
12
+ * Looking up the session's articles can start before the daemon answers. Frames
13
+ * stop arriving once the session is idle, so a failed lookup has to come back on
14
+ * its own rather than wait for one that may never come.
15
+ */
16
+ const REOPEN_RETRY_MS = 300;
17
+ const REOPEN_RETRY_LIMIT = 8;
18
+ /** Recover a lost transient notification or failed read while a call is streaming. */
19
+ const PREVIEW_RETRY_MS = 300;
20
+ /** Blocks kept for reuse, on screen or not. Bounded so a long revision history cannot grow it forever. */
21
+ const BLOCK_POOL_SIZE = 512;
22
+ /**
23
+ * The reader's split, as the document's share of the space it divides with the
24
+ * conversation. The bounds live in `article-split.ts`.
25
+ */
26
+ const ARTICLE_SHARE_KEY = "pspore.desktop.articleShare";
27
+ /** Keyboard steps for the separator, so the width is reachable without a pointer. */
28
+ const ARTICLE_WIDTH_STEP = 24;
29
+ /**
30
+ * Makes a signature unique among identical siblings.
31
+ *
32
+ * Two identical paragraphs render the same markup, so the key has to say which
33
+ * of them this is; otherwise the second one reuses the first one's node and the
34
+ * list collapses.
35
+ */
36
+ function keyOf(seen, signature) {
37
+ const occurrence = (seen.get(signature) ?? 0) + 1;
38
+ seen.set(signature, occurrence);
39
+ return `${occurrence}\u0000${signature}`;
40
+ }
41
+ /**
42
+ * Rendered markdown blocks, keyed by what they draw so an unchanged one keeps
43
+ * its element. Whether a block is a change is carried separately: it is an
44
+ * attribute on the node, not part of its identity, so a block that was new in
45
+ * one version and settled in the next is not rebuilt to say so.
46
+ */
47
+ function blockEntries(blocks) {
48
+ const seen = new Map();
49
+ return blocks.map((block) => ({
50
+ key: keyOf(seen, `block\u0000${block.source}`),
51
+ changed: block.changed,
52
+ build: () => {
53
+ const wrap = document.createElement("div");
54
+ wrap.className = "article-block";
55
+ appendMarkdown(wrap, block.source);
56
+ return wrap;
57
+ },
58
+ }));
59
+ }
60
+ /** One line of a revision: a gutter mark and the text, with the rewrite marked inside it. */
61
+ function diffLineNode(line) {
62
+ const row = document.createElement("div");
63
+ row.className = "article-diff-line";
64
+ row.dataset["change"] = line.kind;
65
+ const gutter = document.createElement("span");
66
+ gutter.className = "article-diff-gutter";
67
+ gutter.setAttribute("aria-hidden", "true");
68
+ gutter.textContent = String(line.number);
69
+ const mark = document.createElement("span");
70
+ mark.className = "article-diff-mark";
71
+ mark.textContent = line.kind === "add" ? "+" : line.kind === "remove" ? "−" : " ";
72
+ const text = document.createElement("span");
73
+ text.className = "article-diff-text";
74
+ const segments = line.segments;
75
+ if (segments && segments.length > 0) {
76
+ for (const segment of segments) {
77
+ if (segment.kind === "same") {
78
+ text.append(document.createTextNode(segment.text));
79
+ continue;
80
+ }
81
+ const marked = document.createElement(segment.kind === "add" ? "ins" : "del");
82
+ marked.textContent = segment.text;
83
+ text.append(marked);
84
+ }
85
+ }
86
+ else {
87
+ text.textContent = line.text;
88
+ }
89
+ row.append(gutter, mark, text);
90
+ return row;
91
+ }
92
+ /**
93
+ * The document panel. Chat keeps a one-line receipt; this is where the article
94
+ * is rendered, revised, and exported.
95
+ *
96
+ * While the model is still streaming the tool arguments the panel renders that
97
+ * draft. When the tool commits a version, the new blocks pulse against the
98
+ * previous saved text. "对照上一版" leaves the removed blocks on screen.
99
+ */
100
+ export class ArticlePanel {
101
+ #sessionId;
102
+ #dispatch;
103
+ #exportArticle;
104
+ #onLayout;
105
+ #panel;
106
+ #title;
107
+ #version;
108
+ #versions;
109
+ #summary;
110
+ #stage;
111
+ #render;
112
+ #status;
113
+ #viewButtons;
114
+ #reopen;
115
+ #exportButtons;
116
+ #resize;
117
+ /** The column the document divides the space with. Its left edge never moves with the split. */
118
+ #conversation;
119
+ #open = false;
120
+ #view = "read";
121
+ /**
122
+ * Blocks keyed by what they render, including ones not on screen right now.
123
+ *
124
+ * A draft passes through partial forms of almost every block on its way to the
125
+ * finished version. Dropping an element the moment it stops matching would
126
+ * mean rebuilding it a second later when the block is whole again, so recently
127
+ * detached blocks stay here until the pool is full.
128
+ */
129
+ #nodes = new Map();
130
+ #paintSequence = 0;
131
+ /** The view the reader chose, restored once a draft becomes a version again. */
132
+ #preferredView = "read";
133
+ #writing = false;
134
+ #waitingForDraft = false;
135
+ #writingKey = "";
136
+ /** Preview events seen for the call being written, and how many of them are already drawn. */
137
+ #previewAsked = 0;
138
+ #previewShown = 0;
139
+ /** Token of the read loop that is running, or -1. */
140
+ #previewPump = -1;
141
+ #previewRetry;
142
+ #lastDraft;
143
+ /** Keep the block being written in view, until the reader scrolls on their own. */
144
+ #followPen = true;
145
+ /** A call the reader closed the panel on. Its drafts stay out of the way; its version still lands. */
146
+ #dismissedKey = "";
147
+ #token = 0;
148
+ #session = "";
149
+ #articleId = "";
150
+ #loadedVersion = 0;
151
+ #titleText = "";
152
+ /** Text of the version this one replaced, when we watched that hand-off. */
153
+ #baseline = "";
154
+ #committed = "";
155
+ #versionsKnown = [];
156
+ #reopenId = "";
157
+ #reopenTitle = "";
158
+ /** Session whose article list has been read. "" means the lookup still owes us an answer. */
159
+ #reopenSession = "";
160
+ #reopenInFlight = false;
161
+ #reopenRetries = 0;
162
+ /** A session change that arrived while a lookup was already running. */
163
+ #reopenPending = false;
164
+ #exporting = false;
165
+ /** True between asking to close and the width reaching zero. */
166
+ #closing = false;
167
+ /** Width the grid is laid out at right now, including mid-animation. */
168
+ #shownWidth = 0;
169
+ /** The open or close in progress. Only one may write the width at a time. */
170
+ #widthAnimation;
171
+ /** Share of the space the reader dragged the document to, kept across opens; undefined until they choose one. */
172
+ #storedShare;
173
+ constructor(options) {
174
+ this.#sessionId = options.sessionId;
175
+ this.#dispatch = options.dispatch;
176
+ this.#exportArticle = options.exportArticle;
177
+ this.#onLayout = options.onLayout;
178
+ this.#panel = requiredElement("article-panel");
179
+ this.#title = requiredElement("article-title");
180
+ this.#version = requiredElement("article-version");
181
+ this.#versions = requiredElement("article-versions");
182
+ this.#summary = requiredElement("article-diff-summary");
183
+ this.#stage = requiredElement("article-stage");
184
+ this.#render = requiredElement("article-render");
185
+ this.#status = requiredElement("article-status");
186
+ this.#viewButtons = Array.from(requiredElement("article-views").querySelectorAll("[data-view]"));
187
+ this.#reopen = requiredButton("open-article-button");
188
+ this.#syncViews(false);
189
+ requiredButton("article-close").addEventListener("click", () => this.close());
190
+ for (const button of this.#viewButtons) {
191
+ button.addEventListener("click", () => {
192
+ const view = button.dataset["view"];
193
+ if (view !== "read" && view !== "token" && view !== "line")
194
+ return;
195
+ if (view === this.#view)
196
+ return;
197
+ this.#view = view;
198
+ this.#preferredView = view;
199
+ this.#syncViews(this.#baseline !== "");
200
+ this.#syncSummary(this.#baseline, this.#loadedVersion);
201
+ if (this.#articleId)
202
+ this.#paint(this.#baseline, false, "top");
203
+ });
204
+ }
205
+ this.#reopen.addEventListener("click", () => {
206
+ if (this.#reopenId)
207
+ void this.open(this.#reopenId);
208
+ });
209
+ // Scrolling or clicking into the document is the reader taking over from the pen.
210
+ for (const type of ["wheel", "touchstart", "pointerdown", "keydown"]) {
211
+ this.#stage.addEventListener(type, () => {
212
+ this.#followPen = false;
213
+ }, { passive: true });
214
+ }
215
+ this.#resize = requiredElement("article-resize");
216
+ const conversation = document.querySelector(".conversation");
217
+ if (!conversation)
218
+ throw new Error("Missing .conversation");
219
+ this.#conversation = conversation;
220
+ this.#storedShare = readStoredShare();
221
+ this.#applyWidth(0);
222
+ this.#bindResize();
223
+ this.#exportButtons = Array.from(this.#panel.querySelectorAll("[data-export]"));
224
+ for (const button of this.#exportButtons) {
225
+ button.addEventListener("click", () => {
226
+ const format = button.dataset["export"];
227
+ if (format === "txt" || format === "md" || format === "docx" || format === "pdf")
228
+ void this.#export(format);
229
+ });
230
+ }
231
+ }
232
+ isOpen() {
233
+ return this.#open;
234
+ }
235
+ isWriting() {
236
+ return this.#writing;
237
+ }
238
+ writingKey() {
239
+ return this.#writingKey;
240
+ }
241
+ /**
242
+ * Called whenever the selected session may have changed, including on the
243
+ * frames that follow a reconnect. The session is often selected for the user
244
+ * — restored at startup, or picked once the daemon answers — so waiting for a
245
+ * click would leave the panel unaware of the articles that session owns. A
246
+ * lookup that could not run, because the daemon was not connected yet, is
247
+ * retried on the next frame instead of leaving the panel silent for good.
248
+ */
249
+ onSessionChanged() {
250
+ const sessionId = this.#sessionId();
251
+ if (sessionId !== this.#session) {
252
+ this.#session = sessionId;
253
+ this.#token += 1;
254
+ this.#writing = false;
255
+ this.#waitingForDraft = false;
256
+ this.#dismissedKey = "";
257
+ this.#reopenSession = "";
258
+ this.#reopenRetries = 0;
259
+ this.#forget();
260
+ this.close();
261
+ }
262
+ if (!sessionId || this.#reopenSession === sessionId)
263
+ return;
264
+ if (this.#reopenInFlight) {
265
+ // The lookup in flight is for the session we have just left. Remembering
266
+ // this is what keeps the change from being dropped: the frames that carry
267
+ // it are the same ones that stop arriving once the session settles.
268
+ this.#reopenPending = true;
269
+ return;
270
+ }
271
+ void this.#refreshReopen(sessionId);
272
+ }
273
+ /**
274
+ * Drops every trace of the document that was open.
275
+ *
276
+ * The panel keeps the current text to diff the next version against, and a
277
+ * pool of rendered blocks to reuse. Both belong to one article in one session:
278
+ * carried across, they turn into the previous session's article appearing
279
+ * underneath a new one being written.
280
+ */
281
+ #forget() {
282
+ this.#paintSequence += 1;
283
+ this.#articleId = "";
284
+ this.#loadedVersion = 0;
285
+ this.#titleText = "";
286
+ this.#baseline = "";
287
+ this.#committed = "";
288
+ this.#versionsKnown = [];
289
+ this.#reopenId = "";
290
+ this.#reopenTitle = "";
291
+ this.#nodes = new Map();
292
+ this.#render.replaceChildren();
293
+ this.#versions.replaceChildren();
294
+ this.#summary.hidden = true;
295
+ this.#status.textContent = "";
296
+ this.#version.textContent = "";
297
+ this.#syncExports();
298
+ this.#title.textContent = "文稿";
299
+ this.#view = "read";
300
+ this.#syncViews(false);
301
+ }
302
+ onEvent(event) {
303
+ const payload = asRecord(event.payload);
304
+ if (stringValue(payload["sessionId"]) !== this.#sessionId())
305
+ return;
306
+ if (event.type === "run.settled" && this.#waitingForDraft) {
307
+ this.#waitingForDraft = false;
308
+ this.#status.textContent = "尚未写入新内容";
309
+ }
310
+ if ((event.type === "run.settled" || event.type === "runtime.error") && this.#writing)
311
+ this.#onRejected();
312
+ if (event.type === "article.preview") {
313
+ const toolCallId = stringValue(payload["toolCallId"]);
314
+ if (!toolCallId)
315
+ return;
316
+ this.#onPreview(toolCallId, stringValue(payload["title"]));
317
+ return;
318
+ }
319
+ if (event.type !== "tool.completed" || stringValue(payload["toolName"]) !== "article" || payload["isError"] === true) {
320
+ if (event.type === "tool.completed" && stringValue(payload["toolName"]) === "article" && payload["isError"] === true) {
321
+ this.#onRejected();
322
+ }
323
+ return;
324
+ }
325
+ const text = toolResultText(payload["result"]);
326
+ const opened = parseArticleOpenReceipt(text);
327
+ if (opened) {
328
+ void this.#onOpened(opened);
329
+ return;
330
+ }
331
+ const receipt = parseArticleReceipt(text);
332
+ if (!receipt)
333
+ return;
334
+ void this.#onCommitted(receipt.id, receipt.version, receipt.title);
335
+ }
336
+ /** The short preparation call completes before the next, possibly buffered, LLM response. */
337
+ async #onOpened(receipt) {
338
+ const token = ++this.#token;
339
+ this.#writing = false;
340
+ this.#waitingForDraft = true;
341
+ if (!receipt.id || receipt.id !== this.#articleId)
342
+ this.#forget();
343
+ this.#titleText = receipt.title;
344
+ this.#show(receipt.title);
345
+ this.#status.textContent = "文稿已打开,等待正文…";
346
+ if (receipt.id) {
347
+ const loaded = await this.#load(receipt.id, receipt.version);
348
+ if (token !== this.#token)
349
+ return;
350
+ if (loaded)
351
+ this.#applyLoaded(loaded, false, "top");
352
+ this.#status.textContent = this.#waitingForDraft ? "文稿已打开,等待修改…" : "尚未写入新内容";
353
+ }
354
+ }
355
+ async open(id, version) {
356
+ const token = ++this.#token;
357
+ this.#show(this.#titleText || "文稿");
358
+ this.#status.textContent = "正在载入文稿…";
359
+ const loaded = await this.#load(id, version);
360
+ if (token !== this.#token || !loaded) {
361
+ if (token === this.#token)
362
+ this.#status.textContent = "没有找到文稿";
363
+ return;
364
+ }
365
+ // Opening a document, or stepping to another version, is a request to read
366
+ // it — so it starts at the title, not at whatever the last line happens to be.
367
+ this.#applyLoaded(loaded, true, "top");
368
+ }
369
+ close() {
370
+ this.#waitingForDraft = false;
371
+ if ((!this.#open && this.#panel.hidden) || this.#closing) {
372
+ this.#syncReopen();
373
+ return;
374
+ }
375
+ // Closing while a draft streams is a choice about that draft: the next
376
+ // preview must not throw the panel straight back open.
377
+ if (this.#writing) {
378
+ this.#dismissedKey = this.#writingKey;
379
+ this.#token += 1;
380
+ }
381
+ this.#open = false;
382
+ this.#writing = false;
383
+ this.#closing = true;
384
+ // `data-article` stays set until the animation ends: it is what makes the
385
+ // grid read the width being animated away. The width is left at zero, so
386
+ // the next open — a reopen, or a document in another session — arrives the
387
+ // same way the first one did instead of appearing at full size.
388
+ this.#animateWidth(0, () => {
389
+ this.#closing = false;
390
+ this.#panel.hidden = true;
391
+ delete document.documentElement.dataset["article"];
392
+ this.#onLayout();
393
+ });
394
+ this.#onLayout();
395
+ this.#syncReopen();
396
+ }
397
+ /**
398
+ * A draft moved on. The panel opens on the first one — before any text has
399
+ * been read — and then keeps exactly one read in flight, always for the
400
+ * newest text.
401
+ *
402
+ * Every event used to start its own read and void the one before it. When a
403
+ * read and its repaint take longer than the gap between events, as on a slow
404
+ * machine, every read was voided by the next event and nothing was drawn
405
+ * until the model stopped writing.
406
+ */
407
+ #onPreview(toolCallId, title) {
408
+ if (toolCallId === this.#dismissedKey)
409
+ return;
410
+ this.#waitingForDraft = false;
411
+ if (!this.#writing || toolCallId !== this.#writingKey) {
412
+ // A new call supersedes whatever was being loaded or written before it.
413
+ this.#token += 1;
414
+ this.#writing = true;
415
+ this.#writingKey = toolCallId;
416
+ this.#previewAsked = 0;
417
+ this.#previewShown = 0;
418
+ this.#lastDraft = undefined;
419
+ this.#followPen = true;
420
+ // A draft has no saved version behind it, so there is nothing to compare it
421
+ // against until it lands. The reader's choice comes back with the version.
422
+ this.#view = "read";
423
+ this.#syncViews(false);
424
+ this.#status.textContent = "正在撰写…";
425
+ }
426
+ this.#previewAsked += 1;
427
+ this.#show(title || this.#titleText || "正在撰写");
428
+ this.#pumpPreview();
429
+ }
430
+ #pumpPreview() {
431
+ const token = this.#token;
432
+ if (this.#previewPump === token)
433
+ return;
434
+ window.clearTimeout(this.#previewRetry);
435
+ this.#previewPump = token;
436
+ void this.#drainPreview(token).finally(() => {
437
+ if (this.#previewPump === token)
438
+ this.#previewPump = -1;
439
+ if (token !== this.#token || !this.#writing)
440
+ return;
441
+ // Preview events are transient and may be shed under backpressure. A
442
+ // single delayed read also recovers a failure after the last notification.
443
+ this.#previewRetry = window.setTimeout(() => {
444
+ if (token !== this.#token || !this.#writing)
445
+ return;
446
+ this.#previewAsked += 1;
447
+ this.#pumpPreview();
448
+ }, PREVIEW_RETRY_MS);
449
+ });
450
+ }
451
+ async #drainPreview(token) {
452
+ while (token === this.#token && this.#previewShown < this.#previewAsked) {
453
+ const asked = this.#previewAsked;
454
+ const draft = await this.#readDraft(this.#writingKey);
455
+ if (token !== this.#token)
456
+ return;
457
+ this.#previewShown = asked;
458
+ if (!draft)
459
+ continue;
460
+ const previous = this.#lastDraft;
461
+ if (previous && previous.text === draft.text && previous.title === draft.title
462
+ && previous.articleId === draft.articleId && previous.baseVersion === draft.baseVersion
463
+ && previous.whole === draft.whole && previous.edits === draft.edits)
464
+ continue;
465
+ // An update is drawn over the version it revises. If the panel is showing
466
+ // another article, or another version, that version goes up first.
467
+ if (draft.articleId !== undefined
468
+ && draft.baseVersion !== undefined
469
+ && !(this.#articleId === draft.articleId && this.#loadedVersion === draft.baseVersion)) {
470
+ const base = await this.#load(draft.articleId, draft.baseVersion);
471
+ if (token !== this.#token)
472
+ return;
473
+ if (base)
474
+ this.#adoptBase(base);
475
+ }
476
+ this.#renderDraft(draft);
477
+ this.#lastDraft = draft;
478
+ }
479
+ }
480
+ async #readDraft(toolCallId) {
481
+ let head = {};
482
+ let text;
483
+ try {
484
+ text = await this.#readSlices(async (offset, limit) => {
485
+ const slice = await this.#dispatch({
486
+ action: "article.preview",
487
+ sessionId: this.#sessionId(),
488
+ toolCallId,
489
+ offset,
490
+ limit,
491
+ });
492
+ if (offset === 0)
493
+ head = asRecord(slice);
494
+ return slice;
495
+ });
496
+ }
497
+ catch {
498
+ // The next event reads again; the version itself arrives with the result.
499
+ return undefined;
500
+ }
501
+ if (text === undefined)
502
+ return undefined;
503
+ const title = stringValue(head["title"]);
504
+ const articleId = stringValue(head["articleId"]);
505
+ return {
506
+ text,
507
+ whole: head["whole"] === true,
508
+ edits: typeof head["edits"] === "number" ? head["edits"] : 0,
509
+ ...(title ? { title } : {}),
510
+ ...(articleId ? { articleId } : {}),
511
+ ...(typeof head["baseVersion"] === "number" ? { baseVersion: head["baseVersion"] } : {}),
512
+ };
513
+ }
514
+ /** Puts the version an update revises on screen, without the hand-off animation. */
515
+ #adoptBase(base) {
516
+ if (this.#articleId !== base.id)
517
+ this.#forget();
518
+ this.#articleId = base.id;
519
+ this.#loadedVersion = base.version;
520
+ this.#titleText = base.title;
521
+ this.#versionsKnown = [...base.versions];
522
+ this.#reopenId = base.id;
523
+ this.#reopenTitle = base.title;
524
+ this.#baseline = "";
525
+ this.#committed = base.text;
526
+ this.#version.textContent = `v${base.version}`;
527
+ this.#paintVersions();
528
+ }
529
+ #renderDraft(draft) {
530
+ // Only a draft of the article already on screen can be shown against it.
531
+ // A draft with no id is a brand new document, and one with a different id
532
+ // belongs to another document — aligning either against what is displayed
533
+ // would leave the wrong article underneath it.
534
+ const baseline = draft.articleId !== undefined && draft.articleId === this.#articleId ? this.#committed : "";
535
+ if (baseline === "" && this.#articleId !== "")
536
+ this.#forget();
537
+ if (draft.articleId)
538
+ this.#articleId = draft.articleId;
539
+ if (draft.title)
540
+ this.#titleText = draft.title;
541
+ this.#title.textContent = this.#titleText || "正在撰写";
542
+ this.#renderLive(draft.text, baseline, draft.whole);
543
+ this.#status.textContent = !draft.whole
544
+ ? `${draft.text.length} 字 · 撰写中`
545
+ : draft.edits > 0 ? `修改中 · 已改 ${draft.edits} 处` : "修改中…";
546
+ }
547
+ /**
548
+ * The call failed, so what the panel drew from its arguments is a version
549
+ * that will never exist; the saved one goes back up.
550
+ */
551
+ #onRejected() {
552
+ this.#waitingForDraft = false;
553
+ if (this.#writing) {
554
+ this.#token += 1;
555
+ this.#writing = false;
556
+ if (this.#loadedVersion > 0) {
557
+ this.#view = this.#preferredView;
558
+ this.#syncViews(this.#baseline !== "");
559
+ this.#paint(this.#baseline, false, "keep");
560
+ }
561
+ }
562
+ this.#status.textContent = "这一版没有写入";
563
+ }
564
+ async #onCommitted(id, version, title) {
565
+ const token = ++this.#token;
566
+ this.#writing = false;
567
+ this.#waitingForDraft = false;
568
+ this.#show(title || this.#titleText || "文稿");
569
+ // Streaming only kept the draft, so an update that lands without the prior
570
+ // version on screen still diffs against that saved text.
571
+ if (!(this.#articleId === id && this.#loadedVersion === version - 1) && version > 1) {
572
+ const previous = await this.#load(id, version - 1);
573
+ if (token !== this.#token)
574
+ return;
575
+ if (previous) {
576
+ this.#articleId = previous.id;
577
+ this.#loadedVersion = previous.version;
578
+ this.#committed = previous.text;
579
+ }
580
+ }
581
+ const loaded = await this.#load(id, version);
582
+ if (token !== this.#token || !loaded)
583
+ return;
584
+ this.#applyLoaded(loaded, true, "keep");
585
+ }
586
+ #applyLoaded(loaded, animate, anchor) {
587
+ const sequential = this.#articleId === loaded.id && this.#loadedVersion > 0 && loaded.version === this.#loadedVersion + 1;
588
+ const firstVersion = this.#loadedVersion === 0 && loaded.version === 1;
589
+ const baseline = sequential ? this.#committed : "";
590
+ this.#articleId = loaded.id;
591
+ this.#loadedVersion = loaded.version;
592
+ this.#titleText = loaded.title;
593
+ this.#versionsKnown = [...loaded.versions];
594
+ this.#reopenId = loaded.id;
595
+ this.#reopenTitle = loaded.title;
596
+ this.#baseline = baseline;
597
+ this.#committed = loaded.text;
598
+ this.#title.textContent = loaded.title;
599
+ this.#version.textContent = `v${loaded.version}`;
600
+ this.#paintVersions();
601
+ this.#paint(baseline, animate && (sequential || firstVersion), anchor);
602
+ this.#view = this.#preferredView;
603
+ this.#syncViews(baseline !== "");
604
+ this.#syncSummary(baseline, loaded.version);
605
+ this.#status.textContent = `${loaded.text.length} 字 · 第 ${loaded.version} 版`;
606
+ this.#syncReopen();
607
+ // Reopening a document, or stepping back through the version chips, skips
608
+ // the hand-off the panel would otherwise have watched. Fetching the version
609
+ // it replaced is what makes "对照上一版" mean something there too.
610
+ if (baseline === "" && loaded.version > 1)
611
+ void this.#loadBaseline(loaded.id, loaded.version);
612
+ }
613
+ async #loadBaseline(id, version) {
614
+ const token = this.#token;
615
+ const previous = await this.#load(id, version - 1);
616
+ if (token !== this.#token || !previous)
617
+ return;
618
+ if (this.#articleId !== id || this.#loadedVersion !== version)
619
+ return;
620
+ this.#baseline = previous.text;
621
+ this.#syncSummary(previous.text, version);
622
+ this.#syncViews(true);
623
+ }
624
+ /**
625
+ * The counts are a caption for the view, not the view itself: paragraphs while
626
+ * reading the article, lines while reading the revision.
627
+ */
628
+ #syncSummary(baseline, version) {
629
+ if (baseline === "") {
630
+ this.#summary.hidden = true;
631
+ return;
632
+ }
633
+ if (this.#view === "read") {
634
+ const counts = articleChangeCounts(diffArticleBlocks(baseline, this.#committed));
635
+ this.#summary.hidden = counts.added === 0 && counts.removed === 0;
636
+ this.#summary.textContent = `相对第 ${version - 1} 版 +${counts.added} 段 −${counts.removed} 段`;
637
+ return;
638
+ }
639
+ const counts = articleLineChangeCounts(diffArticleLines(baseline, this.#committed, "line"));
640
+ this.#summary.hidden = counts.added === 0 && counts.removed === 0;
641
+ this.#summary.textContent = `相对第 ${version - 1} 版 +${counts.added} 行 −${counts.removed} 行`;
642
+ }
643
+ /**
644
+ * Draws the current view, keeping whatever the previous version already put on
645
+ * screen.
646
+ *
647
+ * A new version is mostly the old one. Clearing the stage and rebuilding it
648
+ * would throw away the reading position, any selection, and the very blocks
649
+ * the reader is being asked to compare — so blocks are keyed by what they
650
+ * render, unchanged ones keep their nodes, and only the difference moves:
651
+ * dropped blocks fold away first, then new ones pulse in where they belong.
652
+ */
653
+ #paint(previous, animate, anchor) {
654
+ const sequence = ++this.#paintSequence;
655
+ const entries = this.#view === "read"
656
+ ? this.#readEntries(previous)
657
+ : this.#diffEntries(previous, this.#view === "line" ? "line" : "token");
658
+ const wanted = new Set(entries.map((entry) => entry.key));
659
+ const leaving = [...this.#nodes]
660
+ .filter(([key, node]) => !wanted.has(key) && node.isConnected)
661
+ .map(([, node]) => node);
662
+ // A folded block is reused later, so the collapse must not outlive the exit.
663
+ for (const node of leaving)
664
+ node.dataset["leaving"] = "true";
665
+ const commit = () => {
666
+ if (sequence !== this.#paintSequence)
667
+ return;
668
+ this.#commit(entries, animate, anchor);
669
+ };
670
+ if (animate && leaving.length > 0 && leaving.length <= MAX_PULSED_BLOCKS)
671
+ collapseArticleRemovals(leaving, commit);
672
+ else
673
+ commit();
674
+ }
675
+ /** `pen` is the entry being written, kept in view while a draft streams. */
676
+ #commit(entries, animate, anchor, pen = -1) {
677
+ const pinned = anchor === "keep" && this.#pinnedToEnd();
678
+ const children = [];
679
+ const appeared = [];
680
+ for (const entry of entries) {
681
+ const reused = this.#nodes.get(entry.key);
682
+ const node = reused ?? entry.build();
683
+ if (node.dataset["leaving"] !== undefined) {
684
+ // It was folded away on its way out; coming back has to undo that.
685
+ delete node.dataset["leaving"];
686
+ node.removeAttribute("style");
687
+ }
688
+ // What the reader sees appear is what was not on screen a moment ago,
689
+ // whether it was just built or restored from the pool.
690
+ if (!node.isConnected)
691
+ appeared.push(node);
692
+ if (entry.changed === true)
693
+ node.dataset["change"] = "add";
694
+ else if (entry.changed === false)
695
+ delete node.dataset["change"];
696
+ // Re-inserting refreshes the entry's place in the eviction order.
697
+ this.#nodes.delete(entry.key);
698
+ this.#nodes.set(entry.key, node);
699
+ children.push(node);
700
+ }
701
+ // A draft with no text yet is not an empty version; the status line says it is being written.
702
+ if (children.length === 0 && !this.#writing) {
703
+ const empty = document.createElement("p");
704
+ empty.className = "article-empty";
705
+ empty.textContent = "这一版还没有正文";
706
+ children.push(empty);
707
+ }
708
+ while (this.#nodes.size > BLOCK_POOL_SIZE) {
709
+ const oldest = this.#nodes.keys().next().value;
710
+ if (oldest === undefined)
711
+ break;
712
+ this.#nodes.delete(oldest);
713
+ }
714
+ reconcileChildren(this.#render, children);
715
+ const penNode = pen >= 0 ? children[pen] : undefined;
716
+ if (anchor === "top")
717
+ this.#stage.scrollTop = 0;
718
+ else if (penNode && this.#followPen)
719
+ this.#reveal(penNode);
720
+ else if (pinned)
721
+ this.#stage.scrollTop = this.#stage.scrollHeight;
722
+ if (animate)
723
+ pulseArticleChanges(appeared.slice(0, MAX_PULSED_BLOCKS));
724
+ }
725
+ /**
726
+ * The article itself. Keys carry only what a block renders, not whether the
727
+ * diff called it new, so a paragraph that was new in one version and settled
728
+ * in the next keeps the node it already had.
729
+ */
730
+ #readEntries(previous) {
731
+ return blockEntries(diffArticleBlocks(previous, this.#committed)
732
+ .filter((block) => block.kind !== "remove")
733
+ .map((block) => ({ source: block.source, changed: block.kind === "add" })));
734
+ }
735
+ #diffEntries(previous, granularity) {
736
+ const seen = new Map();
737
+ return diffArticleLines(previous, this.#committed, granularity).map((line) => {
738
+ const marks = (line.segments ?? []).map((segment) => `${segment.kind}:${segment.text}`).join("\u0001");
739
+ const signature = `diff\u0000${granularity}\u0000${line.kind}\u0000${line.number}\u0000${line.text}\u0000${marks}`;
740
+ // A revision row carries its own add/remove/same mark; leaving `changed`
741
+ // unset keeps the commit from overwriting it with the block-level one.
742
+ return { key: keyOf(seen, signature), build: () => diffLineNode(line) };
743
+ });
744
+ }
745
+ /**
746
+ * The draft, drawn through the same keyed blocks as a saved version.
747
+ *
748
+ * A revision is mostly the version before it, and the draft that becomes it is
749
+ * too. Rendering it as its own thing would clear the stage the moment the
750
+ * model starts writing — so by the time the version landed there was nothing
751
+ * left to patch, and the whole document was drawn again.
752
+ */
753
+ #renderLive(text, baseline, whole) {
754
+ let blocks;
755
+ let pen;
756
+ if (whole) {
757
+ // Edits leave the rest of the document where it stood: this is the next
758
+ // version entire, and the pen is in the last block it changes.
759
+ blocks = diffArticleBlocks(baseline, text)
760
+ .filter((block) => block.kind !== "remove")
761
+ .map((block) => ({ source: block.source, changed: block.kind === "add" }));
762
+ pen = blocks.findLastIndex((block) => block.changed);
763
+ }
764
+ else {
765
+ const aligned = alignArticleDraft(baseline, text);
766
+ blocks = [
767
+ ...aligned.written,
768
+ // Not reached yet, so it is still what the document says.
769
+ ...aligned.pending.map((source) => ({ source, changed: false })),
770
+ ];
771
+ pen = aligned.written.length - 1;
772
+ }
773
+ this.#paintSequence += 1;
774
+ this.#commit(blockEntries(blocks), false, "keep", pen);
775
+ }
776
+ /**
777
+ * Scrolls just far enough to keep the block being written on screen, newest
778
+ * line last. The stage is scrolled directly: `scrollIntoView` would also move
779
+ * the clipped panels around it.
780
+ */
781
+ #reveal(node) {
782
+ const margin = 24;
783
+ const stage = this.#stage.getBoundingClientRect();
784
+ const block = node.getBoundingClientRect();
785
+ if (block.bottom > stage.bottom - margin)
786
+ this.#stage.scrollTop += block.bottom - (stage.bottom - margin);
787
+ else if (block.bottom < stage.top + margin)
788
+ this.#stage.scrollTop -= stage.top + margin - block.top;
789
+ }
790
+ #paintVersions() {
791
+ this.#syncExports();
792
+ this.#versions.replaceChildren();
793
+ for (const version of this.#versionsKnown) {
794
+ const button = document.createElement("button");
795
+ button.type = "button";
796
+ button.className = "article-version";
797
+ button.textContent = `v${version.version}`;
798
+ button.setAttribute("aria-pressed", String(version.version === this.#loadedVersion));
799
+ button.addEventListener("click", () => {
800
+ if (version.version !== this.#loadedVersion)
801
+ void this.open(this.#articleId, version.version);
802
+ });
803
+ this.#versions.append(button);
804
+ }
805
+ }
806
+ /**
807
+ * One export at a time. The save dialog is modal to the window but the panel
808
+ * is not, and a second click while the first dialog is open would queue
809
+ * another one behind it and write the file twice.
810
+ */
811
+ async #export(format) {
812
+ if (!this.#articleId || this.#loadedVersion < 1 || this.#exporting)
813
+ return;
814
+ this.#exporting = true;
815
+ this.#syncExports();
816
+ this.#status.textContent = "正在导出…";
817
+ try {
818
+ const result = await this.#exportArticle({ id: this.#articleId, version: this.#loadedVersion, format });
819
+ this.#status.textContent = result.message;
820
+ }
821
+ catch (error) {
822
+ this.#status.textContent = error instanceof Error ? error.message : "导出失败";
823
+ }
824
+ finally {
825
+ this.#exporting = false;
826
+ this.#syncExports();
827
+ }
828
+ }
829
+ #syncExports() {
830
+ for (const button of this.#exportButtons)
831
+ button.disabled = this.#exporting || this.#loadedVersion < 1;
832
+ }
833
+ async #refreshReopen(sessionId) {
834
+ const token = this.#token;
835
+ this.#reopenInFlight = true;
836
+ let listed;
837
+ try {
838
+ listed = await this.#dispatch({ action: "article.list", sessionId });
839
+ }
840
+ catch {
841
+ // Usually "daemon is not connected" during startup.
842
+ this.#reopenInFlight = false;
843
+ if (this.#reopenRetries < REOPEN_RETRY_LIMIT && sessionId === this.#sessionId()) {
844
+ this.#reopenRetries += 1;
845
+ window.setTimeout(() => this.onSessionChanged(), REOPEN_RETRY_MS);
846
+ }
847
+ this.#drainReopenPending();
848
+ return;
849
+ }
850
+ this.#reopenInFlight = false;
851
+ this.#reopenRetries = 0;
852
+ if (token !== this.#token || sessionId !== this.#sessionId()) {
853
+ this.#drainReopenPending();
854
+ return;
855
+ }
856
+ this.#reopenSession = sessionId;
857
+ const articles = asRecord(listed)["articles"];
858
+ const first = Array.isArray(articles) ? asRecord(articles[0]) : undefined;
859
+ const id = stringValue(first?.["id"]);
860
+ if (!id) {
861
+ this.#reopenId = "";
862
+ this.#reopenTitle = "";
863
+ }
864
+ else {
865
+ this.#reopenId = id;
866
+ this.#reopenTitle = stringValue(first?.["title"]) ?? "文稿";
867
+ }
868
+ this.#syncReopen();
869
+ this.#drainReopenPending();
870
+ }
871
+ #drainReopenPending() {
872
+ if (!this.#reopenPending)
873
+ return;
874
+ this.#reopenPending = false;
875
+ this.onSessionChanged();
876
+ }
877
+ async #load(id, version) {
878
+ const text = await this.#readSlices((offset, limit) => this.#dispatch({
879
+ action: "article.get",
880
+ sessionId: this.#sessionId(),
881
+ id,
882
+ ...(version === undefined ? {} : { version }),
883
+ offset,
884
+ limit,
885
+ }));
886
+ if (text === undefined)
887
+ return undefined;
888
+ const head = await this.#dispatch({
889
+ action: "article.get",
890
+ sessionId: this.#sessionId(),
891
+ id,
892
+ ...(version === undefined ? {} : { version }),
893
+ offset: 0,
894
+ limit: 1,
895
+ });
896
+ const record = asRecord(head);
897
+ if (typeof record["title"] !== "string" || typeof record["version"] !== "number")
898
+ return undefined;
899
+ const versions = Array.isArray(record["versions"])
900
+ ? record["versions"].flatMap((entry) => {
901
+ const versionRecord = asRecord(entry);
902
+ return typeof versionRecord["version"] === "number"
903
+ ? [{ version: versionRecord["version"], chars: typeof versionRecord["chars"] === "number" ? versionRecord["chars"] : 0 }]
904
+ : [];
905
+ })
906
+ : [{ version: record["version"], chars: text.length }];
907
+ return { id, title: record["title"], version: record["version"], versions, text };
908
+ }
909
+ async #readSlices(fetchSlice) {
910
+ return readFullMessage(fetchSlice);
911
+ }
912
+ #show(title) {
913
+ const wasOpen = this.#open;
914
+ this.#open = true;
915
+ this.#closing = false;
916
+ this.#panel.hidden = false;
917
+ document.documentElement.dataset["article"] = "open";
918
+ if (title)
919
+ this.#title.textContent = title;
920
+ if (!wasOpen) {
921
+ // Opening over a close still in flight takes the width from wherever that
922
+ // close had got to; the close's own finish is cancelled with it.
923
+ this.#animateWidth(this.#preferredWidth(), () => this.#onLayout());
924
+ this.#onLayout();
925
+ }
926
+ this.#syncReopen();
927
+ }
928
+ /**
929
+ * Follows the space the split divides: a resized window, or the session rail
930
+ * opening or closing. The reader's share is kept, so both halves scale; an open
931
+ * still animating lands on the new width at once rather than on the stale one.
932
+ */
933
+ relayout() {
934
+ if (!this.#open)
935
+ return;
936
+ if (this.#widthAnimation) {
937
+ this.#widthAnimation.kill();
938
+ this.#widthAnimation = undefined;
939
+ }
940
+ const width = this.#preferredWidth();
941
+ if (Math.abs(width - this.#shownWidth) < 1)
942
+ return;
943
+ this.#applyWidth(width);
944
+ this.#onLayout();
945
+ }
946
+ #animateWidth(to, finish) {
947
+ this.#widthAnimation?.kill();
948
+ this.#widthAnimation = undefined;
949
+ // Without the engine, or with reduced motion, the animation finishes before
950
+ // it returns; only one that is still running is kept as the one in flight.
951
+ const run = { finished: false };
952
+ const animation = animateArticleWidth(this.#shownWidth, to, (width) => this.#applyWidth(width), () => {
953
+ run.finished = true;
954
+ if (this.#widthAnimation === run.animation)
955
+ this.#widthAnimation = undefined;
956
+ finish();
957
+ });
958
+ if (!run.finished) {
959
+ run.animation = animation;
960
+ this.#widthAnimation = animation;
961
+ }
962
+ }
963
+ /**
964
+ * The width the two columns divide between them.
965
+ *
966
+ * Measured rather than derived from the viewport: the session rail, the gaps
967
+ * and the page padding all come out of it, and the rail can be open, closed or
968
+ * a drawer that takes no column at all. The conversation's left edge does not
969
+ * move with the split, so this is stable while the boundary is dragged.
970
+ */
971
+ #availableWidth() {
972
+ const grid = this.#panel.parentElement;
973
+ if (!grid)
974
+ return window.innerWidth;
975
+ const gap = Number.parseFloat(getComputedStyle(grid).columnGap) || 0;
976
+ return Math.max(0, grid.getBoundingClientRect().right - this.#conversation.getBoundingClientRect().left - gap);
977
+ }
978
+ /** The reader's share of the space, applied to the space there is now. */
979
+ #preferredWidth() {
980
+ return articleWidthForShare(this.#storedShare, this.#availableWidth());
981
+ }
982
+ #applyWidth(width) {
983
+ this.#shownWidth = width;
984
+ document.documentElement.style.setProperty("--article-width", `${Math.max(0, Math.round(width))}px`);
985
+ }
986
+ /**
987
+ * The boundary between the conversation and the document.
988
+ *
989
+ * Dragging writes the width straight through — an animated drag would lag the
990
+ * pointer — and the chosen share is remembered for the next time the panel
991
+ * opens. The drag is measured from where it started, so grabbing the handle
992
+ * does not make the column jump to the pointer.
993
+ */
994
+ #bindResize() {
995
+ const handle = this.#resize;
996
+ let dragStartX = 0;
997
+ let dragStartWidth = 0;
998
+ const commit = (width) => {
999
+ // A close in flight owns the width until the column is gone.
1000
+ if (!this.#open)
1001
+ return;
1002
+ if (this.#widthAnimation) {
1003
+ this.#widthAnimation.kill();
1004
+ this.#widthAnimation = undefined;
1005
+ }
1006
+ const available = this.#availableWidth();
1007
+ const next = clampArticleWidth(width, available);
1008
+ this.#storedShare = articleShareForWidth(next, available) ?? this.#storedShare;
1009
+ this.#applyWidth(next);
1010
+ this.#onLayout();
1011
+ try {
1012
+ if (this.#storedShare !== undefined)
1013
+ window.localStorage.setItem(ARTICLE_SHARE_KEY, this.#storedShare.toFixed(4));
1014
+ }
1015
+ catch {
1016
+ // Private windows and blocked site data: the split still applies for this run.
1017
+ }
1018
+ };
1019
+ handle.addEventListener("pointerdown", (event) => {
1020
+ if (event.button !== 0 || !this.#open)
1021
+ return;
1022
+ event.preventDefault();
1023
+ handle.setPointerCapture(event.pointerId);
1024
+ dragStartX = event.clientX;
1025
+ dragStartWidth = this.#shownWidth;
1026
+ handle.dataset["dragging"] = "true";
1027
+ document.documentElement.dataset["articleResizing"] = "true";
1028
+ });
1029
+ handle.addEventListener("pointermove", (event) => {
1030
+ if (handle.dataset["dragging"] !== "true")
1031
+ return;
1032
+ // The panel is flush with the workspace's right edge, so moving the
1033
+ // boundary left by a pixel widens the document by one.
1034
+ commit(dragStartWidth + dragStartX - event.clientX);
1035
+ });
1036
+ const release = (event) => {
1037
+ if (handle.dataset["dragging"] !== "true")
1038
+ return;
1039
+ delete handle.dataset["dragging"];
1040
+ delete document.documentElement.dataset["articleResizing"];
1041
+ if (handle.hasPointerCapture(event.pointerId))
1042
+ handle.releasePointerCapture(event.pointerId);
1043
+ };
1044
+ handle.addEventListener("pointerup", release);
1045
+ handle.addEventListener("pointercancel", release);
1046
+ handle.addEventListener("lostpointercapture", release);
1047
+ handle.addEventListener("keydown", (event) => {
1048
+ if (!this.#open)
1049
+ return;
1050
+ const step = event.key === "ArrowLeft" ? ARTICLE_WIDTH_STEP : event.key === "ArrowRight" ? -ARTICLE_WIDTH_STEP : 0;
1051
+ if (step === 0)
1052
+ return;
1053
+ event.preventDefault();
1054
+ commit(this.#shownWidth + step);
1055
+ });
1056
+ window.addEventListener("resize", () => this.relayout());
1057
+ // The session rail is the other thing that changes the space: toggled from
1058
+ // the top bar, or by the renderer as the window crosses a breakpoint.
1059
+ new MutationObserver(() => this.relayout()).observe(document.documentElement, {
1060
+ attributes: true,
1061
+ attributeFilter: ["data-navigation"],
1062
+ });
1063
+ }
1064
+ /**
1065
+ * The first version has nothing to compare against, so the revision views stay
1066
+ * out of reach until there is a baseline to compare with.
1067
+ */
1068
+ #syncViews(comparable) {
1069
+ if (!comparable && this.#view !== "read")
1070
+ this.#view = "read";
1071
+ this.#panel.dataset["view"] = this.#view;
1072
+ for (const button of this.#viewButtons) {
1073
+ const view = button.dataset["view"];
1074
+ button.disabled = !comparable && view !== "read";
1075
+ button.setAttribute("aria-pressed", String(view === this.#view));
1076
+ }
1077
+ }
1078
+ #syncReopen() {
1079
+ const visible = !this.#open && this.#reopenId !== "";
1080
+ this.#reopen.hidden = !visible;
1081
+ this.#reopen.textContent = this.#reopenTitle || "文稿";
1082
+ }
1083
+ #pinnedToEnd() {
1084
+ return this.#stage.scrollHeight - this.#stage.scrollTop - this.#stage.clientHeight < 48;
1085
+ }
1086
+ }
1087
+ function readStoredShare() {
1088
+ try {
1089
+ const raw = window.localStorage.getItem(ARTICLE_SHARE_KEY);
1090
+ if (raw === null)
1091
+ return undefined;
1092
+ const stored = Number(raw);
1093
+ return Number.isFinite(stored) && stored > 0 && stored < 1 ? stored : undefined;
1094
+ }
1095
+ catch {
1096
+ return undefined;
1097
+ }
1098
+ }
1099
+ function requiredElement(id) {
1100
+ const element = document.getElementById(id);
1101
+ if (!element)
1102
+ throw new Error(`Missing #${id}`);
1103
+ return element;
1104
+ }
1105
+ function requiredButton(id) {
1106
+ const element = requiredElement(id);
1107
+ if (!(element instanceof HTMLButtonElement))
1108
+ throw new Error(`#${id} is not a button`);
1109
+ return element;
1110
+ }
1111
+ function toolResultText(result) {
1112
+ const content = asRecord(result)["content"];
1113
+ if (!Array.isArray(content))
1114
+ return "";
1115
+ return content.map((block) => {
1116
+ const text = asRecord(block)["text"];
1117
+ return typeof text === "string" ? text : "";
1118
+ }).filter(Boolean).join("\n");
1119
+ }
1120
+ function asRecord(value) {
1121
+ return typeof value === "object" && value !== null ? value : {};
1122
+ }
1123
+ function stringValue(value) {
1124
+ return typeof value === "string" && value.trim() ? value : undefined;
1125
+ }
1126
+ //# sourceMappingURL=article-panel.js.map