mulmoclaude 0.5.2 → 0.6.0

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 (486) hide show
  1. package/Dockerfile.sandbox +100 -0
  2. package/README.md +17 -4
  3. package/bin/mulmoclaude.js +46 -15
  4. package/bin/prepare-dist.js +18 -2
  5. package/client/assets/chunk-CernVdwh.js +1 -0
  6. package/client/assets/chunk-D8eiyYIV-C1eAZMzz.js +1 -0
  7. package/client/assets/html2canvas-CDGcmOD3-BbPeutDg.js +5 -0
  8. package/client/assets/index-BbgSjFQ8.js +4968 -0
  9. package/client/assets/index-ECD0lgIv.css +2 -0
  10. package/client/assets/{index.es-D4YyL_Dg-BgT6a3Nd.js → index.es-DqtpmBm8-DJdTPdnc.js} +5 -5
  11. package/client/assets/material-symbols-outlined-BLDfUw-_.woff2 +0 -0
  12. package/client/assets/runtime-protocol-vue-6WYa8hAs.js +1 -0
  13. package/client/assets/runtime-vue-BVUzgYGA.js +1 -0
  14. package/client/assets/typeof-DBp4T-Ny-C2xoZtcz.js +1 -0
  15. package/client/assets/vue-1e_vz2LW.js +1 -0
  16. package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +4 -0
  17. package/client/index.html +33 -2
  18. package/package.json +20 -18
  19. package/sandbox-entrypoint.sh +106 -0
  20. package/server/accounting/accountNormalize.ts +32 -0
  21. package/server/accounting/defaultAccounts.ts +87 -0
  22. package/server/accounting/eventPublisher.ts +51 -0
  23. package/server/accounting/journal.ts +252 -0
  24. package/server/accounting/openingBalances.ts +114 -0
  25. package/server/accounting/report.ts +237 -0
  26. package/server/accounting/service.ts +718 -0
  27. package/server/accounting/snapshotCache.ts +333 -0
  28. package/server/accounting/timeSeries.ts +265 -0
  29. package/server/accounting/types.ts +148 -0
  30. package/server/agent/activeTools.ts +128 -0
  31. package/server/agent/attachmentConverter.ts +10 -5
  32. package/server/agent/backend/claude-code.ts +8 -2
  33. package/server/agent/backend/types.ts +1 -1
  34. package/server/agent/config.ts +101 -31
  35. package/server/agent/index.ts +45 -33
  36. package/server/agent/mcp-server.ts +146 -69
  37. package/server/agent/mcp-tools/index.ts +1 -5
  38. package/server/agent/mcp-tools/notify.ts +2 -22
  39. package/server/agent/mcp-tools/x.ts +0 -4
  40. package/server/agent/mcpHealth.ts +168 -0
  41. package/server/agent/plugin-names.ts +20 -77
  42. package/server/agent/prompt.ts +259 -51
  43. package/server/agent/resumeFailover.ts +1 -1
  44. package/server/agent/stream.ts +0 -1
  45. package/server/api/auth/bearerAuth.ts +5 -5
  46. package/server/api/csrfGuard.ts +1 -1
  47. package/server/api/routes/accounting.ts +366 -0
  48. package/server/api/routes/agent.ts +509 -46
  49. package/server/api/routes/attachment.ts +104 -0
  50. package/server/api/routes/chart.ts +2 -1
  51. package/server/api/routes/config.ts +12 -12
  52. package/server/api/routes/files.ts +105 -48
  53. package/server/api/routes/image.ts +70 -25
  54. package/server/api/routes/journal.ts +35 -0
  55. package/server/api/routes/mulmo-script.ts +358 -118
  56. package/server/api/routes/mulmoScriptValidate.ts +1 -1
  57. package/server/api/routes/news.ts +1 -1
  58. package/server/api/routes/notifications.ts +92 -22
  59. package/server/api/routes/notifier.ts +98 -0
  60. package/server/api/routes/pdf.ts +188 -48
  61. package/server/api/routes/plugins.ts +34 -14
  62. package/server/api/routes/presentHtml.ts +58 -3
  63. package/server/api/routes/roles.ts +1 -8
  64. package/server/api/routes/runtime-plugin.ts +224 -0
  65. package/server/api/routes/scheduler.ts +7 -5
  66. package/server/api/routes/schedulerHandlers.ts +1 -1
  67. package/server/api/routes/schedulerTasks.ts +8 -7
  68. package/server/api/routes/sessions.ts +234 -121
  69. package/server/api/routes/skills.ts +56 -51
  70. package/server/api/routes/sources.ts +52 -45
  71. package/server/api/routes/translation.ts +44 -0
  72. package/server/api/routes/wiki/frontmatter.ts +13 -65
  73. package/server/api/routes/wiki/history.ts +261 -0
  74. package/server/api/routes/wiki/pageIndex.ts +1 -1
  75. package/server/api/routes/wiki.ts +50 -26
  76. package/server/events/file-change.ts +83 -0
  77. package/server/events/notifications.ts +247 -91
  78. package/server/events/pub-sub/index.ts +1 -1
  79. package/server/events/relay-client.ts +5 -5
  80. package/server/events/scheduler-adapter.ts +2 -2
  81. package/server/events/session-store/index.ts +110 -22
  82. package/server/events/task-manager/index.ts +10 -9
  83. package/server/index.ts +509 -33
  84. package/server/notifier/engine.ts +419 -0
  85. package/server/notifier/legacy-adapters.ts +76 -0
  86. package/server/notifier/runtime-api.ts +74 -0
  87. package/server/notifier/store.ts +70 -0
  88. package/server/notifier/types.ts +121 -0
  89. package/server/plugins/dev-loader.ts +171 -0
  90. package/server/plugins/dev-watcher.ts +150 -0
  91. package/server/plugins/diagnostics.ts +188 -0
  92. package/server/plugins/preset-list.ts +52 -0
  93. package/server/plugins/preset-loader.ts +112 -0
  94. package/server/plugins/runtime-chat-api.ts +38 -0
  95. package/server/plugins/runtime-loader.ts +430 -0
  96. package/server/plugins/runtime-registry.ts +112 -0
  97. package/server/plugins/runtime-tasks-api.ts +50 -0
  98. package/server/plugins/runtime.ts +378 -0
  99. package/server/services/translation/cache.ts +72 -0
  100. package/server/services/translation/index.ts +106 -0
  101. package/server/services/translation/llm.ts +140 -0
  102. package/server/services/translation/types.ts +35 -0
  103. package/server/system/credentials.ts +13 -2
  104. package/server/system/env.ts +6 -1
  105. package/server/system/logger/formatters.ts +46 -4
  106. package/server/system/logger/index.ts +4 -4
  107. package/server/system/logger/sinks.ts +26 -5
  108. package/server/system/logger/types.ts +2 -2
  109. package/server/utils/dev-plugin-args.d.mts +11 -0
  110. package/server/utils/dev-plugin-args.mjs +43 -0
  111. package/server/utils/errors.ts +13 -4
  112. package/server/utils/files/accounting-io.ts +295 -0
  113. package/server/utils/files/atomic.ts +17 -49
  114. package/server/utils/files/attachment-store.ts +182 -0
  115. package/server/utils/files/html-io.ts +1 -7
  116. package/server/utils/files/html-store.ts +19 -0
  117. package/server/utils/files/image-store.ts +20 -22
  118. package/server/utils/files/index.ts +5 -15
  119. package/server/utils/files/journal-io.ts +7 -35
  120. package/server/utils/files/json.ts +2 -29
  121. package/server/utils/files/markdown-image-fill.ts +6 -37
  122. package/server/utils/files/markdown-store.ts +6 -21
  123. package/server/utils/files/naming.ts +3 -39
  124. package/server/utils/files/plugins-io.ts +100 -0
  125. package/server/utils/files/reference-dirs-io.ts +1 -9
  126. package/server/utils/files/roles-io.ts +2 -10
  127. package/server/utils/files/safe.ts +17 -19
  128. package/server/utils/files/scheduler-io.ts +1 -7
  129. package/server/utils/files/scheduler-overrides-io.ts +3 -12
  130. package/server/utils/files/session-io.ts +21 -30
  131. package/server/utils/files/spreadsheet-store.ts +9 -22
  132. package/server/utils/files/translation-io.ts +46 -0
  133. package/server/utils/files/user-tasks-io.ts +1 -7
  134. package/server/utils/files/workspace-io.ts +3 -79
  135. package/server/utils/gemini.ts +33 -11
  136. package/server/utils/html/htmlArtifactSplicer.ts +41 -0
  137. package/server/utils/markdown/frontmatter.ts +112 -0
  138. package/server/utils/regex.ts +56 -0
  139. package/server/utils/router.ts +41 -0
  140. package/server/utils/slug.ts +5 -3
  141. package/server/utils/time.ts +12 -0
  142. package/server/workspace/chat-index/indexer.ts +15 -2
  143. package/server/workspace/chat-index/summarizer.ts +1 -1
  144. package/server/workspace/custom-dirs.ts +1 -1
  145. package/server/workspace/helps/gemini.md +1 -1
  146. package/server/workspace/helps/guide.md +61 -0
  147. package/server/workspace/helps/index.md +4 -0
  148. package/server/workspace/helps/presenthtml.md +80 -0
  149. package/server/workspace/helps/sandbox.md +7 -0
  150. package/server/workspace/helps/storyteller.md +101 -0
  151. package/server/workspace/helps/telegram.md +1 -0
  152. package/server/workspace/helps/wiki.md +9 -7
  153. package/server/workspace/journal/archivist-cli.ts +7 -33
  154. package/server/workspace/journal/archivist-schemas.ts +5 -43
  155. package/server/workspace/journal/dailyPass.ts +34 -187
  156. package/server/workspace/journal/diff.ts +3 -28
  157. package/server/workspace/journal/index.ts +10 -81
  158. package/server/workspace/journal/indexFile.ts +3 -24
  159. package/server/workspace/journal/latestDaily.ts +51 -0
  160. package/server/workspace/journal/memoryExtractor.ts +4 -20
  161. package/server/workspace/journal/optimizationPass.ts +4 -21
  162. package/server/workspace/journal/paths.ts +4 -23
  163. package/server/workspace/journal/state.ts +6 -29
  164. package/server/workspace/memory/io.ts +213 -0
  165. package/server/workspace/memory/llm-classifier.ts +158 -0
  166. package/server/workspace/memory/migrate.ts +263 -0
  167. package/server/workspace/memory/run.ts +84 -0
  168. package/server/workspace/memory/topic-cluster.ts +218 -0
  169. package/server/workspace/memory/topic-detect.ts +67 -0
  170. package/server/workspace/memory/topic-index-hook.ts +128 -0
  171. package/server/workspace/memory/topic-io.ts +180 -0
  172. package/server/workspace/memory/topic-migrate.ts +248 -0
  173. package/server/workspace/memory/topic-run.ts +172 -0
  174. package/server/workspace/memory/topic-swap.ts +135 -0
  175. package/server/workspace/memory/topic-types.ts +142 -0
  176. package/server/workspace/memory/types.ts +83 -0
  177. package/server/workspace/news/reader.ts +4 -5
  178. package/server/workspace/paths.ts +124 -47
  179. package/server/workspace/roles.ts +2 -11
  180. package/server/workspace/skills/parser.ts +38 -55
  181. package/server/workspace/skills/user-tasks.ts +1 -2
  182. package/server/workspace/skills-preset/mc-library/SKILL.md +188 -0
  183. package/server/workspace/skills-preset.ts +196 -0
  184. package/server/workspace/sources/fetchers/githubIssues.ts +13 -11
  185. package/server/workspace/sources/fetchers/index.ts +1 -1
  186. package/server/workspace/sources/fetchers/rssParser.ts +1 -1
  187. package/server/workspace/sources/pipeline/index.ts +2 -2
  188. package/server/workspace/sources/pipeline/notify.ts +3 -3
  189. package/server/workspace/sources/pipeline/write.ts +2 -2
  190. package/server/workspace/sources/registry.ts +39 -61
  191. package/server/workspace/sources/robots.ts +1 -1
  192. package/server/workspace/tool-trace/classify.ts +2 -1
  193. package/server/workspace/tool-trace/index.ts +1 -1
  194. package/server/workspace/tool-trace/writeSearch.ts +6 -1
  195. package/server/workspace/wiki-backlinks/index.ts +19 -7
  196. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +1 -0
  197. package/server/workspace/wiki-history/hook/snapshot.mjs +98 -0
  198. package/server/workspace/wiki-history/hook/snapshot.ts +135 -0
  199. package/server/workspace/wiki-history/provision.ts +181 -0
  200. package/server/workspace/wiki-pages/io.ts +217 -0
  201. package/server/workspace/wiki-pages/snapshot.ts +380 -0
  202. package/server/workspace/workspace.ts +75 -13
  203. package/src/App.vue +115 -40
  204. package/src/_runtime/protocol-vue.ts +21 -0
  205. package/src/_runtime/vue.ts +22 -0
  206. package/src/components/ChatInput.vue +14 -10
  207. package/src/components/CopyChatButton.vue +76 -0
  208. package/src/components/FileContentRenderer.vue +67 -14
  209. package/src/components/FileTree.vue +2 -2
  210. package/src/components/FilesView.vue +17 -1
  211. package/src/components/NewsView.vue +16 -2
  212. package/src/components/NotificationBell.vue +320 -93
  213. package/src/components/PageChatComposer.vue +5 -4
  214. package/src/components/PluginLauncher.vue +42 -6
  215. package/src/components/PluginScopedRoot.vue +87 -0
  216. package/src/components/RoleSelector.vue +12 -1
  217. package/src/components/RolesView.vue +562 -0
  218. package/src/components/SentAttachmentChip.vue +102 -0
  219. package/src/components/SessionHistoryPanel.vue +109 -20
  220. package/src/components/SessionRoleIcon.vue +7 -4
  221. package/src/components/SessionSidebar.vue +20 -7
  222. package/src/components/SessionTabBar.vue +1 -1
  223. package/src/components/SettingsMcpTab.vue +4 -4
  224. package/src/components/SettingsModal.vue +2 -0
  225. package/src/components/SidebarHeader.vue +16 -5
  226. package/src/components/SourcesManager.vue +23 -9
  227. package/src/components/SourcesView.vue +1 -1
  228. package/src/components/StackView.vue +102 -6
  229. package/src/components/SuggestionsPanel.vue +105 -16
  230. package/src/components/SystemFileBanner.vue +1 -1
  231. package/src/components/TodoExplorer.vue +4 -5
  232. package/src/components/todo/TodoAddDialog.vue +2 -3
  233. package/src/components/todo/TodoEditDialog.vue +1 -2
  234. package/src/components/todo/TodoEditPanel.vue +2 -3
  235. package/src/components/todo/TodoKanbanView.vue +8 -5
  236. package/src/components/todo/TodoListView.vue +3 -5
  237. package/src/components/todo/TodoTableView.vue +7 -5
  238. package/src/composables/useAccountingChannel.ts +58 -0
  239. package/src/composables/useActiveSession.ts +4 -25
  240. package/src/composables/useAppApi.ts +6 -44
  241. package/src/composables/useClipboardCopy.ts +3 -20
  242. package/src/composables/useContentDisplay.ts +33 -2
  243. package/src/composables/useDevPluginReload.ts +23 -0
  244. package/src/composables/useDynamicFavicon.ts +5 -31
  245. package/src/composables/useEventListeners.ts +0 -20
  246. package/src/composables/useExpandedDirs.ts +4 -15
  247. package/src/composables/useFaviconState.ts +12 -46
  248. package/src/composables/useFileChange.ts +53 -0
  249. package/src/composables/useFreshPluginData.ts +6 -43
  250. package/src/composables/useHealth.ts +14 -43
  251. package/src/composables/useImageErrorRepair.ts +104 -0
  252. package/src/composables/useLatestDaily.ts +40 -0
  253. package/src/composables/useMarkdownDoc.ts +39 -0
  254. package/src/composables/useMarkdownLinkHandler.ts +1 -1
  255. package/src/composables/useMcpTools.ts +3 -16
  256. package/src/composables/useNotifications.ts +138 -112
  257. package/src/composables/usePdfDownload.ts +17 -3
  258. package/src/composables/usePendingCalls.ts +8 -26
  259. package/src/composables/usePluginErrorBoundary.ts +68 -0
  260. package/src/composables/usePubSub.ts +9 -17
  261. package/src/composables/useRunElapsed.ts +5 -22
  262. package/src/composables/useSandboxStatus.ts +4 -20
  263. package/src/composables/useSessionDerived.ts +7 -15
  264. package/src/composables/useSessionHistory.ts +70 -29
  265. package/src/composables/useSessionSync.ts +25 -3
  266. package/src/composables/useSkillsList.ts +59 -0
  267. package/src/composables/useTranslatedQueries.ts +109 -0
  268. package/src/config/apiRoutes.ts +181 -80
  269. package/src/config/historyFilters.ts +5 -3
  270. package/src/config/hostEvents.ts +17 -0
  271. package/src/config/mcpCatalog.ts +277 -5
  272. package/src/config/pubsubChannels.ts +134 -12
  273. package/src/config/roles.ts +212 -147
  274. package/src/config/systemFileDescriptors.ts +5 -5
  275. package/src/config/toolNames.ts +52 -30
  276. package/src/config/workspacePaths.ts +26 -2
  277. package/src/lang/de.ts +483 -27
  278. package/src/lang/en.ts +448 -27
  279. package/src/lang/es.ts +474 -27
  280. package/src/lang/fr.ts +476 -27
  281. package/src/lang/ja.ts +465 -27
  282. package/src/lang/ko.ts +466 -27
  283. package/src/lang/pt-BR.ts +473 -27
  284. package/src/lang/zh.ts +463 -27
  285. package/src/lib/vue-i18n.ts +1 -1
  286. package/src/lib/wiki-page/slug.ts +66 -0
  287. package/src/main.ts +85 -0
  288. package/src/plugins/_extras.ts +58 -0
  289. package/src/plugins/_generated/metas.ts +42 -0
  290. package/src/plugins/_generated/registrations.ts +44 -0
  291. package/src/plugins/_generated/server-bindings.ts +47 -0
  292. package/src/plugins/accounting/Preview.vue +106 -0
  293. package/src/plugins/accounting/View.vue +632 -0
  294. package/src/plugins/accounting/actions.ts +34 -0
  295. package/src/plugins/accounting/api.ts +301 -0
  296. package/src/plugins/accounting/components/AccountEditor.vue +250 -0
  297. package/src/plugins/accounting/components/AccountRow.vue +50 -0
  298. package/src/plugins/accounting/components/AccountsList.vue +102 -0
  299. package/src/plugins/accounting/components/AccountsModal.vue +300 -0
  300. package/src/plugins/accounting/components/BalanceSheet.vue +186 -0
  301. package/src/plugins/accounting/components/BookSettings.vue +284 -0
  302. package/src/plugins/accounting/components/BookSwitcher.vue +78 -0
  303. package/src/plugins/accounting/components/DateRangePicker.vue +140 -0
  304. package/src/plugins/accounting/components/JournalEntryForm.vue +504 -0
  305. package/src/plugins/accounting/components/JournalList.vue +553 -0
  306. package/src/plugins/accounting/components/Ledger.vue +206 -0
  307. package/src/plugins/accounting/components/NewBookForm.vue +211 -0
  308. package/src/plugins/accounting/components/OpeningBalancesForm.vue +271 -0
  309. package/src/plugins/accounting/components/ProfitLoss.vue +160 -0
  310. package/src/plugins/accounting/components/accountDraft.ts +13 -0
  311. package/src/plugins/accounting/components/accountNumbering.ts +103 -0
  312. package/src/plugins/accounting/components/accountValidation.ts +75 -0
  313. package/src/plugins/accounting/components/useLatestRequest.ts +44 -0
  314. package/src/plugins/accounting/countries.ts +158 -0
  315. package/src/plugins/accounting/currencies.ts +64 -0
  316. package/src/plugins/accounting/dates.ts +51 -0
  317. package/src/plugins/accounting/definition.ts +199 -0
  318. package/src/plugins/accounting/fiscalYear.ts +136 -0
  319. package/src/plugins/accounting/index.ts +49 -0
  320. package/src/plugins/accounting/meta.ts +91 -0
  321. package/src/plugins/accounting/timeSeriesEnums.ts +16 -0
  322. package/src/plugins/api.ts +125 -0
  323. package/src/plugins/canvas/View.vue +38 -28
  324. package/src/plugins/canvas/definition.ts +10 -8
  325. package/src/plugins/canvas/index.ts +15 -8
  326. package/src/plugins/canvas/meta.ts +12 -0
  327. package/src/plugins/chart/Preview.vue +1 -1
  328. package/src/plugins/chart/View.vue +2 -2
  329. package/src/plugins/chart/definition.ts +12 -2
  330. package/src/plugins/chart/index.ts +15 -7
  331. package/src/plugins/chart/meta.ts +18 -0
  332. package/src/plugins/editImages/definition.ts +44 -0
  333. package/src/plugins/editImages/index.ts +43 -0
  334. package/src/plugins/editImages/meta.ts +5 -0
  335. package/src/plugins/generateImage/View.vue +3 -1
  336. package/src/plugins/generateImage/definition.ts +2 -0
  337. package/src/plugins/generateImage/index.ts +13 -5
  338. package/src/plugins/generateImage/meta.ts +5 -0
  339. package/src/plugins/index.ts +35 -0
  340. package/src/plugins/manageRoles/Preview.vue +7 -4
  341. package/src/plugins/manageRoles/View.vue +12 -8
  342. package/src/plugins/manageRoles/definition.ts +6 -0
  343. package/src/plugins/manageRoles/index.ts +7 -6
  344. package/src/plugins/manageSkills/View.vue +11 -7
  345. package/src/plugins/manageSkills/definition.ts +4 -1
  346. package/src/plugins/manageSkills/index.ts +14 -7
  347. package/src/plugins/manageSkills/meta.ts +21 -0
  348. package/src/plugins/manageSource/definition.ts +4 -1
  349. package/src/plugins/manageSource/index.ts +15 -7
  350. package/src/plugins/manageSource/meta.ts +21 -0
  351. package/src/plugins/markdown/Preview.vue +10 -8
  352. package/src/plugins/markdown/View.vue +84 -17
  353. package/src/plugins/markdown/definition.ts +7 -1
  354. package/src/plugins/markdown/index.ts +15 -8
  355. package/src/plugins/markdown/meta.ts +16 -0
  356. package/src/plugins/meta-types.ts +97 -0
  357. package/src/plugins/metas.ts +224 -0
  358. package/src/plugins/presentForm/Preview.vue +4 -15
  359. package/src/plugins/presentForm/View.vue +35 -78
  360. package/src/plugins/presentForm/definition.ts +7 -6
  361. package/src/plugins/presentForm/index.ts +12 -5
  362. package/src/plugins/presentForm/meta.ts +11 -0
  363. package/src/plugins/presentForm/plugin.ts +8 -9
  364. package/src/plugins/presentForm/types.ts +0 -24
  365. package/src/plugins/presentHtml/Preview.vue +1 -8
  366. package/src/plugins/presentHtml/View.vue +401 -30
  367. package/src/plugins/presentHtml/definition.ts +8 -5
  368. package/src/plugins/presentHtml/index.ts +15 -8
  369. package/src/plugins/presentHtml/meta.ts +14 -0
  370. package/src/plugins/presentMulmoScript/View.vue +327 -107
  371. package/src/plugins/presentMulmoScript/definition.ts +34 -7
  372. package/src/plugins/presentMulmoScript/helpers.ts +4 -5
  373. package/src/plugins/presentMulmoScript/index.ts +20 -7
  374. package/src/plugins/presentMulmoScript/meta.ts +52 -0
  375. package/src/plugins/scheduler/AutomationsPreview.vue +2 -8
  376. package/src/plugins/scheduler/Preview.vue +5 -2
  377. package/src/plugins/scheduler/TasksTab.vue +16 -36
  378. package/src/plugins/scheduler/View.vue +22 -54
  379. package/src/plugins/scheduler/automationsDefinition.ts +14 -9
  380. package/src/plugins/scheduler/automationsMeta.ts +5 -0
  381. package/src/plugins/scheduler/calendarDefinition.ts +4 -7
  382. package/src/plugins/scheduler/calendarMeta.ts +28 -0
  383. package/src/plugins/scheduler/formatSchedule.ts +6 -24
  384. package/src/plugins/scheduler/index.ts +26 -52
  385. package/src/plugins/scope.ts +57 -0
  386. package/src/plugins/server-bindings-types.ts +38 -0
  387. package/src/plugins/server.ts +32 -0
  388. package/src/plugins/skill/Preview.vue +25 -0
  389. package/src/plugins/skill/View.vue +125 -0
  390. package/src/plugins/skill/definition.ts +23 -0
  391. package/src/plugins/skill/index.ts +36 -0
  392. package/src/plugins/skill/plugin.ts +31 -0
  393. package/src/plugins/skill/types.ts +21 -0
  394. package/src/plugins/spreadsheet/Preview.vue +1 -3
  395. package/src/plugins/spreadsheet/View.vue +29 -49
  396. package/src/plugins/spreadsheet/cellHighlights.ts +2 -3
  397. package/src/plugins/spreadsheet/definition.ts +5 -2
  398. package/src/plugins/spreadsheet/index.ts +15 -8
  399. package/src/plugins/spreadsheet/keyboardNav.ts +38 -0
  400. package/src/plugins/spreadsheet/meta.ts +14 -0
  401. package/src/plugins/textResponse/Preview.vue +9 -1
  402. package/src/plugins/textResponse/View.vue +59 -8
  403. package/src/plugins/textResponse/index.ts +11 -3
  404. package/src/plugins/textResponse/plugin.ts +8 -10
  405. package/src/plugins/textResponse/types.ts +28 -0
  406. package/src/plugins/wiki/Preview.vue +6 -4
  407. package/src/plugins/wiki/View.vue +463 -254
  408. package/src/plugins/wiki/components/WikiPageBody.vue +159 -0
  409. package/src/plugins/wiki/helpers.ts +17 -0
  410. package/src/plugins/wiki/history/HistoryDetail.vue +325 -0
  411. package/src/plugins/wiki/history/HistoryTab.vue +167 -0
  412. package/src/plugins/wiki/history/RestoreConfirm.vue +63 -0
  413. package/src/plugins/wiki/history/api.ts +52 -0
  414. package/src/plugins/wiki/history/diff.ts +145 -0
  415. package/src/plugins/wiki/index.ts +42 -32
  416. package/src/plugins/wiki/meta.ts +10 -0
  417. package/src/plugins/wiki/pageEditLoader.ts +53 -0
  418. package/src/plugins/wiki/route.ts +8 -0
  419. package/src/router/guards.ts +2 -1
  420. package/src/router/index.ts +19 -0
  421. package/src/router/pageRoutes.ts +1 -0
  422. package/src/tools/index.ts +50 -51
  423. package/src/tools/runtimeLoader.ts +141 -0
  424. package/src/tools/types.ts +44 -1
  425. package/src/types/notification.ts +23 -0
  426. package/src/types/pastedFile.ts +10 -0
  427. package/src/types/session.ts +61 -3
  428. package/src/types/sse.ts +21 -6
  429. package/src/utils/agent/eventDispatch.ts +12 -9
  430. package/src/utils/agent/pastedAttachment.ts +35 -0
  431. package/src/utils/agent/request.ts +32 -3
  432. package/src/utils/agent/toolCalls.ts +7 -1
  433. package/src/utils/api.ts +1 -1
  434. package/src/utils/chat/exportMarkdown.ts +243 -0
  435. package/src/utils/errors.ts +10 -2
  436. package/src/utils/files/expandedDirs.ts +1 -1
  437. package/src/utils/filesPreview/todoPreview.ts +13 -2
  438. package/src/utils/format/date.ts +1 -3
  439. package/src/utils/format/jsonSyntax.ts +5 -0
  440. package/src/utils/html/iframeHeightReporterScript.ts +62 -0
  441. package/src/utils/html/previewCsp.ts +29 -2
  442. package/src/utils/image/htmlSrcAttrs.ts +122 -0
  443. package/src/utils/image/imageRepairInlineScript.ts +115 -0
  444. package/src/utils/image/resolve.ts +17 -3
  445. package/src/utils/image/rewriteMarkdownImageRefs.ts +62 -9
  446. package/src/utils/markdown/frontmatter.ts +125 -0
  447. package/src/utils/markdown/taskList.ts +7 -2
  448. package/src/utils/plugin/runtime.ts +132 -0
  449. package/src/utils/session/mergeSessions.ts +40 -37
  450. package/src/utils/session/sessionEntries.ts +74 -18
  451. package/src/utils/session/sessionHelpers.ts +54 -10
  452. package/src/utils/tools/result.ts +76 -14
  453. package/src/vite-env.d.ts +6 -0
  454. package/client/assets/html2canvas-Cx501zZr-Bug0qRNv.js +0 -5
  455. package/client/assets/index-CY-WpQUm.css +0 -2
  456. package/client/assets/index-DbTz2Mfs.js +0 -4911
  457. package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
  458. package/server/api/routes/html.ts +0 -114
  459. package/server/api/routes/todos.ts +0 -293
  460. package/server/api/routes/todosColumnsHandlers.ts +0 -333
  461. package/server/api/routes/todosHandlers.ts +0 -274
  462. package/server/api/routes/todosItemsHandlers.ts +0 -386
  463. package/server/utils/files/todos-io.ts +0 -29
  464. package/src/components/NotificationToast.vue +0 -75
  465. package/src/plugins/editImage/definition.ts +0 -27
  466. package/src/plugins/editImage/index.ts +0 -37
  467. package/src/plugins/presentHtml/helpers.ts +0 -72
  468. package/src/plugins/scheduler/LegacySchedulerView.vue +0 -32
  469. package/src/plugins/scheduler/legacyShape.ts +0 -34
  470. package/src/plugins/todo/Preview.vue +0 -68
  471. package/src/plugins/todo/View.vue +0 -378
  472. package/src/plugins/todo/composables/useTodos.ts +0 -179
  473. package/src/plugins/todo/definition.ts +0 -45
  474. package/src/plugins/todo/index.ts +0 -62
  475. package/src/plugins/todo/labels.ts +0 -163
  476. package/src/plugins/todo/priority.ts +0 -98
  477. package/src/plugins/todo/viewModes.ts +0 -19
  478. package/src/plugins/wiki/definition.ts +0 -25
  479. package/src/tools/legacyPluginNames.ts +0 -13
  480. package/src/utils/format/frontmatter.ts +0 -80
  481. package/src/utils/image/rewriteHtmlImageRefs.ts +0 -50
  482. package/src/utils/notification/dispatch.ts +0 -58
  483. /package/client/assets/{purify.es-Fx1Nqyry-BwJECkqS.js → purify.es-Fx1Nqyry-BSVNht6S.js} +0 -0
  484. /package/src/plugins/{editImage → editImages}/Preview.vue +0 -0
  485. /package/src/plugins/{editImage → editImages}/View.vue +0 -0
  486. /package/src/{config/schedulerActions.ts → plugins/scheduler/actions.ts} +0 -0
@@ -13,17 +13,31 @@ import { API_ROUTES } from "../config/apiRoutes";
13
13
  import { apiFetchRaw } from "../utils/api";
14
14
  import { errorMessage } from "../utils/errors";
15
15
 
16
+ export interface DownloadPdfOptions {
17
+ /** Workspace-relative source directory of the markdown (e.g.
18
+ * `"data/wiki/pages"` for Wiki pages). The server uses it to
19
+ * resolve relative `<img>` references to the right base path
20
+ * before inlining. Omit for the legacy `markdowns/` default. */
21
+ baseDir?: string;
22
+ /** When true, the server strips a leading YAML frontmatter envelope
23
+ * (`---\n…\n---\n`) before rendering, so the YAML header doesn't
24
+ * appear as plain text on page 1 of the PDF. Wiki pages set this.
25
+ * Other callers leave it false so a chat-generated document that
26
+ * literally starts with `---` is preserved. */
27
+ stripFrontmatter?: boolean;
28
+ }
29
+
16
30
  export interface UsePdfDownloadHandle {
17
31
  pdfDownloading: Ref<boolean>;
18
32
  pdfError: Ref<string | null>;
19
- downloadPdf: (markdown: string, filename: string) => Promise<void>;
33
+ downloadPdf: (markdown: string, filename: string, options?: DownloadPdfOptions) => Promise<void>;
20
34
  }
21
35
 
22
36
  export function usePdfDownload(): UsePdfDownloadHandle {
23
37
  const pdfDownloading = ref(false);
24
38
  const pdfError = ref<string | null>(null);
25
39
 
26
- async function downloadPdf(markdown: string, filename: string): Promise<void> {
40
+ async function downloadPdf(markdown: string, filename: string, options: DownloadPdfOptions = {}): Promise<void> {
27
41
  pdfError.value = null;
28
42
  pdfDownloading.value = true;
29
43
  let url: string | null = null;
@@ -33,7 +47,7 @@ export function usePdfDownload(): UsePdfDownloadHandle {
33
47
  const response = await apiFetchRaw(API_ROUTES.pdf.markdown, {
34
48
  method: "POST",
35
49
  headers: { "Content-Type": "application/json" },
36
- body: JSON.stringify({ markdown, filename }),
50
+ body: JSON.stringify({ markdown, filename, baseDir: options.baseDir, stripFrontmatter: options.stripFrontmatter }),
37
51
  });
38
52
  if (!response.ok) {
39
53
  const errText = await response.text().catch(() => "");
@@ -1,9 +1,5 @@
1
- // Composable that bundles the "minimum visible duration" trick for
2
- // pending tool call rows: while the agent is running, tick a counter
3
- // every 50ms so the `pendingCalls` computed re-evaluates and any
4
- // freshly-resolved call stays visible for at least PENDING_MIN_MS
5
- // before disappearing. After the run ends, schedule one final tick
6
- // so the computed clears the lingering rows.
1
+ // "Minimum visible duration" trick: tick every 50ms while running so a freshly-resolved call stays on-screen for
2
+ // PENDING_MIN_MS before clearing. One final tick after the run ends sweeps lingering rows out of the computed.
7
3
 
8
4
  import { computed, ref, watch, type ComputedRef, type Ref } from "vue";
9
5
  import type { ToolCallHistoryItem } from "../types/toolCallHistory";
@@ -17,17 +13,14 @@ interface UsePendingCallsOptions {
17
13
  export function usePendingCalls(opts: UsePendingCallsOptions) {
18
14
  const displayTick = ref(0);
19
15
  let tickInterval: ReturnType<typeof setInterval> | null = null;
20
- // Tracked so teardown can cancel the lingering "final tick" and we
21
- // never mutate displayTick after the composable's owner unmounts.
16
+ // Tracked so teardown can cancel the trailing tick never mutate displayTick after the owner unmounts.
22
17
  let delayedTickTimeout: ReturnType<typeof setTimeout> | null = null;
23
18
 
24
19
  watch(
25
20
  opts.isRunning,
26
21
  (running) => {
27
22
  if (running) {
28
- // Guard against double-start: if the watcher fires twice with
29
- // running=true (e.g. immediate + a synchronous flip), don't
30
- // stack a second interval.
23
+ // Guard against double-start (immediate + a synchronous flip would otherwise stack intervals).
31
24
  if (tickInterval !== null) return;
32
25
  tickInterval = setInterval(() => {
33
26
  displayTick.value++;
@@ -35,9 +28,7 @@ export function usePendingCalls(opts: UsePendingCallsOptions) {
35
28
  } else if (tickInterval !== null) {
36
29
  clearInterval(tickInterval);
37
30
  tickInterval = null;
38
- // One final tick so the computed clears after the minimum
39
- // duration has elapsed. Cancel any previous pending one first
40
- // so back-to-back start/stop runs do not stack timeouts.
31
+ // Cancel any previous trailing tick so back-to-back start/stop runs don't stack timeouts.
41
32
  if (delayedTickTimeout !== null) clearTimeout(delayedTickTimeout);
42
33
  delayedTickTimeout = setTimeout(() => {
43
34
  displayTick.value++;
@@ -45,24 +36,15 @@ export function usePendingCalls(opts: UsePendingCallsOptions) {
45
36
  }, PENDING_MIN_MS);
46
37
  }
47
38
  },
48
- // immediate so a composable created while a run is already in
49
- // flight (e.g. mounted mid-stream) starts ticking right away
50
- // instead of waiting for the next isRunning flip.
39
+ // Immediate so a composable mounted mid-stream starts ticking right away instead of waiting for the next flip.
51
40
  { immediate: true },
52
41
  );
53
42
 
54
43
  const pendingCalls = computed(() => {
55
- // Read displayTick to register the computed as a reactive
56
- // dependency on it — that is how a freshly-resolved row stays
57
- // visible for the minimum window. The `__` prefix tells ESLint
58
- // (varsIgnorePattern: "^__") that the variable is intentionally
59
- // unused.
44
+ // Reads displayTick purely to register a reactive dep that's how rows linger for the minimum window.
60
45
  const __tickDep = displayTick.value;
61
46
  const now = Date.now();
62
- // Project to a narrower shape that carries `elapsedMs` so the
63
- // consumer doesn't need its own ticker for the per-tool badge —
64
- // the 50ms re-evaluation here already drives the display
65
- // (#731 PR2).
47
+ // #731 PR2: project to elapsedMs so the per-tool badge piggybacks on this 50ms ticker (no second ticker downstream).
66
48
  return opts.toolCallHistory.value
67
49
  .filter((entry) => __tickDep >= 0 && isCallStillPending(entry, now))
68
50
  .map((entry) => ({
@@ -0,0 +1,68 @@
1
+ // State and reset logic for the plugin error boundary mounted in
2
+ // `<PluginScopedRoot>`. Extracted from the SFC so the behaviour
3
+ // (error capture, details toggle, retry-remount key bump, error
4
+ // detail composition) can be unit-tested without a DOM — Codex
5
+ // review iter-1 #1147 flagged the absence of automated coverage
6
+ // for the boundary's wiring.
7
+ //
8
+ // The host component still owns `onErrorCaptured` registration —
9
+ // that hook must be called inside `setup()` and is not portable —
10
+ // but every observable side-effect of "an error was captured" lives
11
+ // here and is fully testable in isolation.
12
+
13
+ import { computed, ref, type ComputedRef, type Ref } from "vue";
14
+
15
+ export interface PluginErrorBoundary {
16
+ /** Captured error object, or `null` while the plugin renders
17
+ * normally. The host SFC reads this to switch to the fallback
18
+ * panel. */
19
+ readonly error: Readonly<Ref<Error | null>>;
20
+ /** Toggle for the "Show details" / "Hide details" disclosure. */
21
+ readonly showDetails: Ref<boolean>;
22
+ /** Bumped on every Retry. The SFC binds it as `<slot :key>` so
23
+ * Vue treats the slotted subtree as a brand-new component on
24
+ * each retry — transient bugs (stale refs, momentary endpoint
25
+ * outages) clear without a full page reload. */
26
+ readonly mountKey: Readonly<Ref<number>>;
27
+ /** Composed `<message>\n\n<stack>` text shown inside the
28
+ * details `<pre>`. Empty string when there's no error. */
29
+ readonly errorDetails: ComputedRef<string>;
30
+ /** Forward an unknown thrown value into the boundary's error
31
+ * state. Coerces non-Error throws to `new Error(String(err))`
32
+ * so `.stack` access stays type-safe in the template. Logs to
33
+ * the console with a `[plugin/<pkgName>]` prefix so devs can
34
+ * trace which plugin owned the crash. */
35
+ readonly captureError: (err: unknown) => void;
36
+ /** Clear the error and bump `mountKey`. */
37
+ readonly retry: () => void;
38
+ }
39
+
40
+ /** @param pkgName plugin package name; used as the console log
41
+ * prefix so the owning plugin is obvious in dev
42
+ * tools. */
43
+ export function usePluginErrorBoundary(pkgName: string): PluginErrorBoundary {
44
+ const error = ref<Error | null>(null);
45
+ const showDetails = ref(false);
46
+ const mountKey = ref(0);
47
+
48
+ const errorDetails = computed((): string => {
49
+ if (!error.value) return "";
50
+ const message = error.value.message || String(error.value);
51
+ const stack = error.value.stack ?? "";
52
+ return stack ? `${message}\n\n${stack}` : message;
53
+ });
54
+
55
+ function captureError(err: unknown): void {
56
+ const captured = err instanceof Error ? err : new Error(String(err));
57
+ console.error(`[plugin/${pkgName}] uncaught error`, captured);
58
+ error.value = captured;
59
+ }
60
+
61
+ function retry(): void {
62
+ error.value = null;
63
+ showDetails.value = false;
64
+ mountKey.value += 1;
65
+ }
66
+
67
+ return { error, showDetails, mountKey, errorDetails, captureError, retry };
68
+ }
@@ -8,15 +8,7 @@ interface PubSubMessage {
8
8
  type Callback = (data: unknown) => void;
9
9
  type Unsubscribe = () => void;
10
10
 
11
- // Socket.IO replaces the raw WebSocket + hand-rolled reconnect
12
- // state machine. One multiplexed connection; channels map to
13
- // socket.io rooms via `subscribe` / `unsubscribe` events.
14
- //
15
- // Reconnect / backoff / heartbeat are all handled by socket.io,
16
- // so there's no reconnectTimer / reconnectDelay here anymore. On
17
- // reconnect, `connect` fires again and we re-send every
18
- // subscription the client still cares about.
19
-
11
+ // On reconnect we re-emit every live subscription so the rooms list survives the bounce.
20
12
  let socket: Socket | null = null;
21
13
 
22
14
  const listeners = new Map<string, Set<Callback>>();
@@ -32,9 +24,7 @@ function connect(): Socket {
32
24
 
33
25
  const sock = io({
34
26
  path: "/ws/pubsub",
35
- // Match the server. Long-polling is fine as a fallback but
36
- // the server refuses it, so don't negotiate it here either —
37
- // fail fast if the WS upgrade doesn't go through.
27
+ // Server refuses long-polling fallback, so fail fast here too if the WS upgrade doesn't go through.
38
28
  transports: ["websocket"],
39
29
  });
40
30
 
@@ -60,14 +50,16 @@ function maybeDisconnect(): void {
60
50
 
61
51
  export function usePubSub() {
62
52
  function subscribe(channel: string, callback: Callback): Unsubscribe {
63
- if (!listeners.has(channel)) listeners.set(channel, new Set());
64
- listeners.get(channel)!.add(callback);
53
+ let entry = listeners.get(channel);
54
+ if (!entry) {
55
+ entry = new Set();
56
+ listeners.set(channel, entry);
57
+ }
58
+ entry.add(callback);
65
59
 
66
60
  const sock = connect();
67
61
  if (sock.connected) sock.emit("subscribe", channel);
68
- // If not yet connected, the "connect" handler replays every
69
- // listener's subscription, so newly-added channels are
70
- // covered without extra bookkeeping.
62
+ // If not yet connected, the "connect" handler replays every subscription — no extra bookkeeping needed.
71
63
 
72
64
  return () => {
73
65
  const cbs = listeners.get(channel);
@@ -1,18 +1,5 @@
1
- // Tracks how long the active agent run has been going. While
2
- // `isRunning` is true, `elapsedMs` updates once per second so the
3
- // rendered string ("12s" / "1m 23s") moves visibly. When the run
4
- // ends, `elapsedMs` flips back to null and the timer is cleared.
5
- //
6
- // Separated from `usePendingCalls` (which ticks every 50ms for the
7
- // minimum-visible-duration trick) — the run-elapsed display only
8
- // needs second-granularity, and the consumer renders one badge per
9
- // run rather than one per pending row, so a tighter tick would just
10
- // burn re-renders.
11
- //
12
- // Why a watcher + setInterval rather than a `requestAnimationFrame`
13
- // driven computed: tab-throttled rAF freezes when the tab is in the
14
- // background, and the user expects the elapsed counter to keep
15
- // running across tab switches.
1
+ // setInterval, not rAF: tab-throttled rAF freezes in background tabs and the user expects elapsed to keep ticking
2
+ // across tab switches. Second-granularity is enough the consumer renders one badge per run, not per pending row.
16
3
 
17
4
  import { computed, ref, watch, type ComputedRef, type Ref, type WatchStopHandle } from "vue";
18
5
 
@@ -35,9 +22,7 @@ export function useRunElapsed(opts: UseRunElapsedOptions): {
35
22
  opts.isRunning,
36
23
  (running) => {
37
24
  if (running) {
38
- // Guard against double-start: if the watcher fires twice with
39
- // running=true (e.g. immediate + a synchronous flip), don't
40
- // stack a second interval.
25
+ // Guard against double-start (immediate + synchronous flip would otherwise stack intervals).
41
26
  if (interval !== null) return;
42
27
  startedAt.value = Date.now();
43
28
  now.value = startedAt.value;
@@ -52,8 +37,7 @@ export function useRunElapsed(opts: UseRunElapsedOptions): {
52
37
  }
53
38
  startedAt.value = null;
54
39
  },
55
- // immediate so a composable created while a run is already in
56
- // flight (mounted mid-stream) starts ticking right away.
40
+ // Immediate: a composable mounted mid-stream starts ticking right away.
57
41
  { immediate: true },
58
42
  );
59
43
 
@@ -63,8 +47,7 @@ export function useRunElapsed(opts: UseRunElapsedOptions): {
63
47
  });
64
48
 
65
49
  function teardown(): void {
66
- // Stop the watcher first — otherwise an isRunning flip after
67
- // teardown would recreate the interval (Codex iter-1 #798).
50
+ // Stop the watcher first — otherwise an isRunning flip after teardown recreates the interval (#798 Codex iter-1).
68
51
  if (stopWatch !== null) {
69
52
  stopWatch();
70
53
  stopWatch = null;
@@ -1,13 +1,4 @@
1
- // Lazy fetcher for `GET /api/sandbox` (#329).
2
- //
3
- // The lock popup consumes this to show which host credentials are
4
- // attached to the Docker sandbox. Deliberately lazy: the popup is
5
- // hidden most of the time, and env-var changes only take effect on
6
- // server restart anyway, so a page-lifetime cache populated on first
7
- // open is enough.
8
- //
9
- // Paired with `useHealth` (which loads `sandboxEnabled` at bootstrap)
10
- // — when the sandbox is disabled this composable is never called.
1
+ // #329. Lazy because env-var changes only take effect on server restart, so a page-lifetime cache is enough.
11
2
 
12
3
  import { ref, type Ref } from "vue";
13
4
  import { API_ROUTES } from "../config/apiRoutes";
@@ -33,12 +24,8 @@ function isSandboxStatus(raw: RawResponse): raw is {
33
24
  }
34
25
 
35
26
  export interface UseSandboxStatusHandle {
36
- /** Parsed status, or null while not yet loaded / sandbox disabled
37
- * / fetch failed. UI renders a placeholder in all three cases. */
27
+ // null = not yet loaded / sandbox disabled / fetch failed (UI renders a placeholder for all three).
38
28
  status: Ref<SandboxStatus | null>;
39
- /** One-shot loader. Safe to call repeatedly — short-circuits once a
40
- * non-null value is cached. Designed to be triggered from a
41
- * `watch(() => props.open, …)` on the popup. */
42
29
  ensureLoaded: () => Promise<void>;
43
30
  }
44
31
 
@@ -51,14 +38,11 @@ export function useSandboxStatus(): UseSandboxStatusHandle {
51
38
  loaded = true;
52
39
  const result = await apiGet<RawResponse>(API_ROUTES.sandbox);
53
40
  if (!result.ok) {
54
- // Leave `status` null popup shows a neutral "state unavailable"
55
- // line. Allow a retry on next open by flipping `loaded` back.
41
+ // Allow retry on next open — `status` stays null so the popup shows "state unavailable".
56
42
  loaded = false;
57
43
  return;
58
44
  }
59
- // Server returns `{}` (empty object) when the sandbox is disabled.
60
- // The popup shouldn't call us in that case, but double-guard here
61
- // so a stale render doesn't blow up on shape validation.
45
+ // Server returns `{}` when the sandbox is disabled — popup shouldn't call us then, but double-guard.
62
46
  if (!isSandboxStatus(result.data)) return;
63
47
  status.value = result.data;
64
48
  }
@@ -1,6 +1,3 @@
1
- // Computed properties derived from sessionMap + sessions list.
2
- // Extracted from App.vue to reduce the component's reactive surface.
3
-
4
1
  import { computed, type Ref } from "vue";
5
2
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
6
3
  import type { ActiveSession, SessionSummary } from "../types/session";
@@ -14,17 +11,16 @@ export function useSessionDerived(opts: { sessionMap: Map<string, ActiveSession>
14
11
 
15
12
  const toolResults = computed<ToolResultComplete[]>(() => activeSession.value?.toolResults ?? []);
16
13
 
14
+ // `sidebarResults` is the canonical "what the user sees and can
15
+ // navigate through" list — keyboard nav (useKeyNavigation), the
16
+ // sidebar render, and StackView all consume it.
17
17
  const sidebarResults = computed(() => deduplicateResults(toolResults.value));
18
18
 
19
19
  const currentSummary = computed(() => sessions.value.find((summary) => summary.id === currentSessionId.value));
20
20
 
21
- // Global "is anything running" across every known session in-memory
22
- // map (which reflects pub/sub events faster than server refetch) and
23
- // server-side summaries (for sessions not yet hydrated into the map).
24
- // Used for consumers that must stay true across page navigation:
25
- // favicon spinner and the FilesView refresh watcher (which would
26
- // otherwise fire before a background run actually finishes, because
27
- // leaving /chat drops activeSession to undefined).
21
+ // OR of in-memory map (pub/sub-fast) + server summaries (covers un-hydrated sessions). Must stay true across page
22
+ // nav so favicon + FilesView refresh-watcher don't fire before a background run finishes (leaving /chat drops
23
+ // activeSession to undefined).
28
24
  const isRunning = computed(() => {
29
25
  for (const session of sessionMap.values()) {
30
26
  if (session.isRunning) return true;
@@ -33,11 +29,7 @@ export function useSessionDerived(opts: { sessionMap: Map<string, ActiveSession>
33
29
  return sessions.value.some((summary) => summary.isRunning);
34
30
  });
35
31
 
36
- // True only when the session on screen has a run in flight. Drives
37
- // UX touchpoints that should react per-session — ChatInput disable,
38
- // sendMessage guard, chat-list auto-scroll, pending-call row tick —
39
- // so a background run in session B doesn't disable the composer
40
- // while the user is actively chatting in session A.
32
+ // Per-session: a background run in session B must not disable session A's composer or block its auto-scroll.
41
33
  const activeSessionRunning = computed(() => {
42
34
  const active = activeSession.value;
43
35
  const pending = active ? Object.keys(active.pendingGenerations).length > 0 : false;
@@ -1,22 +1,12 @@
1
- // Composable for the session-history view at `/history`.
2
- //
3
- // Owns the `sessions` list (what the server knows about) plus the
4
- // fetch helper. The view's open/closed state is now URL-backed (see
5
- // plans/done/feat-history-url-route.md) — callers watch `route.name` and
6
- // invoke `fetchSessions()` on route enter rather than going through
7
- // an in-memory toggle flag.
8
- //
9
- // Since #205, `fetchSessions()` sends the server's last-issued
10
- // cursor back as `?since=<cursor>` so the server can reply with
11
- // only the rows that changed. The first call has no cursor (full
12
- // fetch); subsequent calls receive a diff that we merge into the
13
- // existing cache via `applySessionDiff`.
1
+ // #205: send the server's last cursor as ?since=<cursor> so the server replies with a diff. First call has no cursor.
14
2
 
15
- import { ref, type Ref } from "vue";
3
+ import { getCurrentScope, onScopeDispose, ref, type Ref } from "vue";
16
4
  import { API_ROUTES } from "../config/apiRoutes";
5
+ import { PUBSUB_CHANNELS, type SessionsChannelPayload } from "../config/pubsubChannels";
17
6
  import type { SessionSummary } from "../types/session";
18
- import { apiGet } from "../utils/api";
7
+ import { apiDelete, apiGet, apiPost } from "../utils/api";
19
8
  import { applySessionDiff } from "../utils/session/mergeSessions";
9
+ import { usePubSub } from "./usePubSub";
20
10
 
21
11
  interface SessionsResponse {
22
12
  sessions: SessionSummary[];
@@ -24,20 +14,25 @@ interface SessionsResponse {
24
14
  deletedIds: string[];
25
15
  }
26
16
 
27
- export function useSessionHistory(): {
17
+ function readDeletedIds(payload: unknown): string[] {
18
+ if (!payload || typeof payload !== "object") return [];
19
+ const ids = (payload as SessionsChannelPayload).deletedIds;
20
+ return Array.isArray(ids) ? ids.filter((entry): entry is string => typeof entry === "string") : [];
21
+ }
22
+
23
+ export interface UseSessionHistory {
28
24
  sessions: Ref<SessionSummary[]>;
29
25
  historyError: Ref<string | null>;
30
26
  fetchSessions: () => Promise<SessionSummary[]>;
31
- } {
27
+ setBookmark: (sessionId: string, bookmarked: boolean) => Promise<boolean>;
28
+ deleteSession: (sessionId: string) => Promise<boolean>;
29
+ }
30
+
31
+ export function useSessionHistory(): UseSessionHistory {
32
32
  const sessions = ref<SessionSummary[]>([]);
33
- // Surfaces the most recent fetch failure. Kept alongside the (stale)
34
- // sessions list rather than wiping it — a panel that goes blank
35
- // the moment the network hiccups is worse UX than one that shows
36
- // "⚠ using cached list" with the last-known good entries.
33
+ // Held alongside the stale list, not in place of it — a blank panel on a network blip is worse UX than "⚠ cached".
37
34
  const historyError = ref<string | null>(null);
38
- // Opaque cursor the server hands back on every successful call.
39
- // Tab-scoped — issue #205 calls out cross-tab sharing via
40
- // localStorage as out of scope.
35
+ // Tab-scoped; #205 explicitly leaves cross-tab sharing via localStorage out of scope.
41
36
  let cursor: string | null = null;
42
37
 
43
38
  async function fetchSessions(): Promise<SessionSummary[]> {
@@ -46,26 +41,72 @@ export function useSessionHistory(): {
46
41
  const result = await apiGet<SessionsResponse>(API_ROUTES.sessions.list, query);
47
42
  if (!result.ok) {
48
43
  historyError.value = result.error;
49
- // Intentionally preserve `sessions.value` callers keep showing
50
- // whatever list was last known to work.
44
+ // Preserve sessions.value so callers keep showing the last-known-good list.
51
45
  return sessions.value;
52
46
  }
53
47
  historyError.value = null;
54
48
  const body = result.data;
55
49
  if (cursor === null) {
56
- // First call in this composable instance — server returned the
57
- // full list; seed the cache directly.
58
50
  sessions.value = body.sessions;
59
51
  } else {
60
52
  sessions.value = applySessionDiff(sessions.value, body.sessions, body.deletedIds);
61
53
  }
62
- cursor = body.cursor;
54
+ ({ cursor } = body);
63
55
  return sessions.value;
64
56
  }
65
57
 
58
+ async function setBookmark(sessionId: string, bookmarked: boolean): Promise<boolean> {
59
+ const path = API_ROUTES.sessions.bookmark.replace(":id", encodeURIComponent(sessionId));
60
+ const result = await apiPost<{ ok: boolean }>(path, { bookmarked });
61
+ if (!result.ok) {
62
+ historyError.value = result.error;
63
+ return false;
64
+ }
65
+ // Optimistic local update so the green-icon flip is immediate;
66
+ // the pub/sub round-trip will reaffirm via the cursor diff (meta
67
+ // mtime feeds into changeMs) and also reach other tabs.
68
+ sessions.value = sessions.value.map((session) => (session.id === sessionId ? { ...session, isBookmarked: bookmarked } : session));
69
+ return true;
70
+ }
71
+
72
+ async function deleteSession(sessionId: string): Promise<boolean> {
73
+ const path = API_ROUTES.sessions.detail.replace(":id", encodeURIComponent(sessionId));
74
+ const result = await apiDelete<{ ok: boolean }>(path);
75
+ if (!result.ok) {
76
+ historyError.value = result.error;
77
+ return false;
78
+ }
79
+ // Don't update locally — the server publishes `deletedIds` on the
80
+ // sessions channel and the subscriber below removes the row in
81
+ // every tab (including this one) the same way. One code path, no
82
+ // race between the optimistic write and the broadcast.
83
+ return true;
84
+ }
85
+
86
+ // Cross-tab cache pruning: cursor diffs don't carry deletions
87
+ // (deletedIds is always [] in the REST response — see #205 comments
88
+ // in routes/sessions.ts), so we rely on the channel payload.
89
+ //
90
+ // Gated on getCurrentScope() so unit tests that instantiate the
91
+ // composable outside a Vue setup don't open a real socket.io
92
+ // connection (which would keep node's event loop alive and hang
93
+ // the test process).
94
+ if (getCurrentScope()) {
95
+ const { subscribe } = usePubSub();
96
+ const unsubscribe = subscribe(PUBSUB_CHANNELS.sessions, (data) => {
97
+ const ids = readDeletedIds(data);
98
+ if (ids.length === 0) return;
99
+ const drop = new Set(ids);
100
+ sessions.value = sessions.value.filter((session) => !drop.has(session.id));
101
+ });
102
+ if (typeof unsubscribe === "function") onScopeDispose(unsubscribe);
103
+ }
104
+
66
105
  return {
67
106
  sessions,
68
107
  historyError,
69
108
  fetchSessions,
109
+ setBookmark,
110
+ deleteSession,
70
111
  };
71
112
  }
@@ -7,16 +7,27 @@ import { onScopeDispose } from "vue";
7
7
  import type { Ref } from "vue";
8
8
  import type { ActiveSession, SessionSummary } from "../types/session";
9
9
  import { usePubSub } from "./usePubSub";
10
- import { PUBSUB_CHANNELS } from "../config/pubsubChannels";
10
+ import { PUBSUB_CHANNELS, type SessionsChannelPayload } from "../config/pubsubChannels";
11
11
  import { apiPost } from "../utils/api";
12
12
  import { API_ROUTES } from "../config/apiRoutes";
13
13
 
14
+ function readDeletedIds(payload: unknown): string[] {
15
+ if (!payload || typeof payload !== "object") return [];
16
+ const ids = (payload as SessionsChannelPayload).deletedIds;
17
+ return Array.isArray(ids) ? ids.filter((entry): entry is string => typeof entry === "string") : [];
18
+ }
19
+
14
20
  export function useSessionSync(opts: {
15
21
  sessionMap: Map<string, ActiveSession>;
16
22
  currentSessionId: Ref<string>;
17
23
  fetchSessions: () => Promise<SessionSummary[]>;
24
+ /** Called when the session the user is currently viewing has been
25
+ * hard-deleted (typically from another tab). The host owns the
26
+ * recovery action — usually navigate to a fresh session so the
27
+ * blank chat view doesn't linger on a dead URL. */
28
+ onCurrentSessionDeleted?: () => void;
18
29
  }) {
19
- const { sessionMap, currentSessionId, fetchSessions } = opts;
30
+ const { sessionMap, currentSessionId, fetchSessions, onCurrentSessionDeleted } = opts;
20
31
  const { subscribe } = usePubSub();
21
32
 
22
33
  async function refreshSessionStates(): Promise<void> {
@@ -48,7 +59,18 @@ export function useSessionSync(opts: {
48
59
  }
49
60
  }
50
61
 
51
- const unsub = subscribe(PUBSUB_CHANNELS.sessions, () => {
62
+ const unsub = subscribe(PUBSUB_CHANNELS.sessions, (data) => {
63
+ // Hard-deleted sessions need to leave sessionMap immediately —
64
+ // refreshSessionStates only updates entries it still finds in the
65
+ // server response, so a deleted live session would otherwise
66
+ // linger in mergedSessions until the tab reloads.
67
+ const deletedIds = readDeletedIds(data);
68
+ let currentWasDeleted = false;
69
+ for (const deletedId of deletedIds) {
70
+ sessionMap.delete(deletedId);
71
+ if (deletedId === currentSessionId.value) currentWasDeleted = true;
72
+ }
73
+ if (currentWasDeleted) onCurrentSessionDeleted?.();
52
74
  void refreshSessionStates();
53
75
  });
54
76
  if (typeof unsub === "function") onScopeDispose(unsub);
@@ -0,0 +1,59 @@
1
+ import { readonly, ref, type Ref, type DeepReadonly } from "vue";
2
+ import { apiGet } from "../utils/api";
3
+ import { API_ROUTES } from "../config/apiRoutes";
4
+
5
+ export interface SkillSummary {
6
+ name: string;
7
+ description: string;
8
+ source: "user" | "project";
9
+ }
10
+
11
+ // Module-level shared state across consumers. Failed fetch keeps the previous list (no visual wipe on a blip) and
12
+ // surfaces the message via `error` — the Skills tab renders that as a banner so stale vs current is distinguishable.
13
+ const skills = ref<SkillSummary[]>([]);
14
+ const error = ref<string | null>(null);
15
+ let bootstrapped = false;
16
+ let inflight: Promise<void> | null = null;
17
+
18
+ async function refresh(): Promise<void> {
19
+ if (inflight) return inflight;
20
+ inflight = (async () => {
21
+ try {
22
+ const result = await apiGet<{ skills: SkillSummary[] }>(API_ROUTES.skills.list.url);
23
+ if (result.ok && Array.isArray(result.data.skills)) {
24
+ skills.value = result.data.skills;
25
+ error.value = null;
26
+ return;
27
+ }
28
+ // Leave `skills` untouched (stale > wiped); surface on `error` AND console.warn — silent failures here had
29
+ // historically made "Skills tab won't refresh" hard to diagnose without breakpoints.
30
+ const message = !result.ok ? result.error || "Failed to load skills" : "Skills response missing `skills` array";
31
+ error.value = message;
32
+ console.warn("[useSkillsList] refresh failed:", message);
33
+ } catch (err) {
34
+ // Runtime throw must not become an unhandled rejection — the bootstrap call site is `void refresh()`.
35
+ const message = err instanceof Error ? err.message : String(err);
36
+ error.value = message;
37
+ console.warn("[useSkillsList] refresh threw:", err);
38
+ } finally {
39
+ inflight = null;
40
+ }
41
+ })();
42
+ return inflight;
43
+ }
44
+
45
+ export function useSkillsList(): {
46
+ skills: DeepReadonly<Ref<SkillSummary[]>>;
47
+ error: DeepReadonly<Ref<string | null>>;
48
+ refresh: () => Promise<void>;
49
+ } {
50
+ if (!bootstrapped) {
51
+ bootstrapped = true;
52
+ void refresh();
53
+ }
54
+ return {
55
+ skills: readonly(skills),
56
+ error: readonly(error),
57
+ refresh,
58
+ };
59
+ }