mulmoclaude 0.1.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 (408) hide show
  1. package/README.md +44 -0
  2. package/bin/mulmoclaude.js +202 -0
  3. package/bin/prepare-dist.js +93 -0
  4. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
  5. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
  6. package/client/assets/index-D8rhwXLq.js +4906 -0
  7. package/client/assets/index-KNLBjwuh.css +1 -0
  8. package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
  9. package/client/assets/material-icons-Dr0goTwe.woff +0 -0
  10. package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
  11. package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
  12. package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
  13. package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
  14. package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
  15. package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
  16. package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
  17. package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
  18. package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
  19. package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
  20. package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
  21. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
  22. package/client/index.html +28 -0
  23. package/package.json +66 -0
  24. package/server/agent/attachmentConverter.ts +270 -0
  25. package/server/agent/config.ts +414 -0
  26. package/server/agent/index.ts +260 -0
  27. package/server/agent/mcp-server.ts +412 -0
  28. package/server/agent/mcp-tools/index.ts +63 -0
  29. package/server/agent/mcp-tools/x.ts +188 -0
  30. package/server/agent/plugin-names.ts +75 -0
  31. package/server/agent/prompt.ts +349 -0
  32. package/server/agent/resumeFailover.ts +129 -0
  33. package/server/agent/sandboxMounts.ts +329 -0
  34. package/server/agent/stream.ts +194 -0
  35. package/server/api/auth/bearerAuth.ts +61 -0
  36. package/server/api/auth/token.ts +98 -0
  37. package/server/api/csrfGuard.ts +85 -0
  38. package/server/api/routes/agent.ts +478 -0
  39. package/server/api/routes/chart.ts +98 -0
  40. package/server/api/routes/chat-index.ts +46 -0
  41. package/server/api/routes/config.ts +258 -0
  42. package/server/api/routes/dispatchResponse.ts +79 -0
  43. package/server/api/routes/files.ts +812 -0
  44. package/server/api/routes/html.ts +101 -0
  45. package/server/api/routes/image.ts +169 -0
  46. package/server/api/routes/mulmo-script.ts +712 -0
  47. package/server/api/routes/mulmoScriptValidate.ts +101 -0
  48. package/server/api/routes/notifications.ts +69 -0
  49. package/server/api/routes/pdf.ts +163 -0
  50. package/server/api/routes/plugins.ts +276 -0
  51. package/server/api/routes/presentHtml.ts +48 -0
  52. package/server/api/routes/roles.ts +125 -0
  53. package/server/api/routes/scheduler.ts +153 -0
  54. package/server/api/routes/schedulerHandlers.ts +151 -0
  55. package/server/api/routes/schedulerTasks.ts +163 -0
  56. package/server/api/routes/sessions.ts +294 -0
  57. package/server/api/routes/sessionsCursor.ts +59 -0
  58. package/server/api/routes/skills.ts +195 -0
  59. package/server/api/routes/sources.ts +540 -0
  60. package/server/api/routes/todos.ts +263 -0
  61. package/server/api/routes/todosColumnsHandlers.ts +347 -0
  62. package/server/api/routes/todosHandlers.ts +274 -0
  63. package/server/api/routes/todosItemsHandlers.ts +386 -0
  64. package/server/api/routes/wiki/pageIndex.ts +53 -0
  65. package/server/api/routes/wiki.ts +363 -0
  66. package/server/api/sandboxStatus.ts +64 -0
  67. package/server/events/notifications.ts +160 -0
  68. package/server/events/pub-sub/index.ts +45 -0
  69. package/server/events/relay-client.ts +288 -0
  70. package/server/events/scheduler-adapter.ts +302 -0
  71. package/server/events/session-store/index.ts +492 -0
  72. package/server/events/task-manager/index.ts +181 -0
  73. package/server/index.ts +572 -0
  74. package/server/system/config.ts +243 -0
  75. package/server/system/credentials.ts +220 -0
  76. package/server/system/docker.ts +97 -0
  77. package/server/system/env.ts +109 -0
  78. package/server/system/logger/config.ts +112 -0
  79. package/server/system/logger/formatters.ts +40 -0
  80. package/server/system/logger/index.ts +53 -0
  81. package/server/system/logger/rotation.ts +37 -0
  82. package/server/system/logger/sinks.ts +101 -0
  83. package/server/system/logger/types.ts +29 -0
  84. package/server/utils/date.ts +57 -0
  85. package/server/utils/errors.ts +7 -0
  86. package/server/utils/fetch.ts +27 -0
  87. package/server/utils/files/atomic.ts +125 -0
  88. package/server/utils/files/html-io.ts +20 -0
  89. package/server/utils/files/image-store.ts +66 -0
  90. package/server/utils/files/index.ts +45 -0
  91. package/server/utils/files/journal-io.ts +213 -0
  92. package/server/utils/files/json.ts +69 -0
  93. package/server/utils/files/markdown-store.ts +33 -0
  94. package/server/utils/files/naming.ts +50 -0
  95. package/server/utils/files/reference-dirs-io.ts +45 -0
  96. package/server/utils/files/roles-io.ts +45 -0
  97. package/server/utils/files/safe.ts +106 -0
  98. package/server/utils/files/scheduler-io.ts +20 -0
  99. package/server/utils/files/scheduler-overrides-io.ts +64 -0
  100. package/server/utils/files/session-io.ts +136 -0
  101. package/server/utils/files/spreadsheet-store.ts +63 -0
  102. package/server/utils/files/todos-io.ts +29 -0
  103. package/server/utils/files/user-tasks-io.ts +25 -0
  104. package/server/utils/files/workspace-io.ts +221 -0
  105. package/server/utils/gemini.ts +59 -0
  106. package/server/utils/gitignore.ts +69 -0
  107. package/server/utils/http.ts +15 -0
  108. package/server/utils/httpError.ts +61 -0
  109. package/server/utils/id.ts +16 -0
  110. package/server/utils/json.ts +83 -0
  111. package/server/utils/logBackgroundError.ts +22 -0
  112. package/server/utils/markdown.ts +82 -0
  113. package/server/utils/request.ts +29 -0
  114. package/server/utils/slug.ts +50 -0
  115. package/server/utils/spawn.ts +62 -0
  116. package/server/utils/time.ts +34 -0
  117. package/server/utils/types.ts +47 -0
  118. package/server/workspace/chat-index/index.ts +153 -0
  119. package/server/workspace/chat-index/indexer.ts +209 -0
  120. package/server/workspace/chat-index/paths.ts +34 -0
  121. package/server/workspace/chat-index/summarizer.ts +247 -0
  122. package/server/workspace/chat-index/types.ts +38 -0
  123. package/server/workspace/custom-dirs.ts +220 -0
  124. package/server/workspace/helps/business.md +104 -0
  125. package/server/workspace/helps/github.md +23 -0
  126. package/server/workspace/helps/index.md +60 -0
  127. package/server/workspace/helps/mulmoscript.md +249 -0
  128. package/server/workspace/helps/sandbox.md +90 -0
  129. package/server/workspace/helps/spreadsheet.md +43 -0
  130. package/server/workspace/helps/telegram.md +135 -0
  131. package/server/workspace/helps/wiki.md +131 -0
  132. package/server/workspace/journal/archivist.ts +386 -0
  133. package/server/workspace/journal/dailyPass.ts +743 -0
  134. package/server/workspace/journal/diff.ts +71 -0
  135. package/server/workspace/journal/index.ts +185 -0
  136. package/server/workspace/journal/indexFile.ts +136 -0
  137. package/server/workspace/journal/linkRewrite.ts +4 -0
  138. package/server/workspace/journal/memoryExtractor.ts +130 -0
  139. package/server/workspace/journal/optimizationPass.ts +160 -0
  140. package/server/workspace/journal/paths.ts +76 -0
  141. package/server/workspace/journal/state.ts +125 -0
  142. package/server/workspace/paths.ts +158 -0
  143. package/server/workspace/reference-dirs.ts +252 -0
  144. package/server/workspace/roles.ts +37 -0
  145. package/server/workspace/skills/discovery.ts +125 -0
  146. package/server/workspace/skills/index.ts +10 -0
  147. package/server/workspace/skills/parser.ts +144 -0
  148. package/server/workspace/skills/paths.ts +41 -0
  149. package/server/workspace/skills/scheduler.ts +149 -0
  150. package/server/workspace/skills/types.ts +30 -0
  151. package/server/workspace/skills/user-tasks.ts +257 -0
  152. package/server/workspace/skills/writer.ts +189 -0
  153. package/server/workspace/sources/arxivDiscovery.ts +182 -0
  154. package/server/workspace/sources/classifier.ts +268 -0
  155. package/server/workspace/sources/fetchers/arxiv.ts +170 -0
  156. package/server/workspace/sources/fetchers/github.ts +106 -0
  157. package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
  158. package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
  159. package/server/workspace/sources/fetchers/index.ts +71 -0
  160. package/server/workspace/sources/fetchers/registerAll.ts +15 -0
  161. package/server/workspace/sources/fetchers/rss.ts +141 -0
  162. package/server/workspace/sources/fetchers/rssParser.ts +295 -0
  163. package/server/workspace/sources/httpFetcher.ts +230 -0
  164. package/server/workspace/sources/interests.ts +120 -0
  165. package/server/workspace/sources/paths.ts +110 -0
  166. package/server/workspace/sources/pipeline/dedup.ts +60 -0
  167. package/server/workspace/sources/pipeline/fetch.ts +136 -0
  168. package/server/workspace/sources/pipeline/index.ts +249 -0
  169. package/server/workspace/sources/pipeline/notify.ts +72 -0
  170. package/server/workspace/sources/pipeline/plan.ts +66 -0
  171. package/server/workspace/sources/pipeline/summarize.ts +189 -0
  172. package/server/workspace/sources/pipeline/write.ts +185 -0
  173. package/server/workspace/sources/rateLimiter.ts +148 -0
  174. package/server/workspace/sources/registry.ts +326 -0
  175. package/server/workspace/sources/robots.ts +271 -0
  176. package/server/workspace/sources/sourceState.ts +135 -0
  177. package/server/workspace/sources/taxonomy.ts +74 -0
  178. package/server/workspace/sources/types.ts +144 -0
  179. package/server/workspace/sources/urls.ts +112 -0
  180. package/server/workspace/tool-trace/classify.ts +114 -0
  181. package/server/workspace/tool-trace/index.ts +250 -0
  182. package/server/workspace/tool-trace/writeSearch.ts +98 -0
  183. package/server/workspace/wiki-backlinks/index.ts +107 -0
  184. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
  185. package/server/workspace/workspace.ts +66 -0
  186. package/src/App.vue +720 -0
  187. package/src/assets/mulmo_bw.png +0 -0
  188. package/src/components/CanvasViewToggle.vue +27 -0
  189. package/src/components/ChatAttachmentPreview.vue +45 -0
  190. package/src/components/ChatImagePreview.vue +17 -0
  191. package/src/components/ChatInput.vue +208 -0
  192. package/src/components/FileContentHeader.vue +49 -0
  193. package/src/components/FileContentRenderer.vue +162 -0
  194. package/src/components/FileTree.vue +115 -0
  195. package/src/components/FileTreePane.vue +85 -0
  196. package/src/components/FilesView.vue +206 -0
  197. package/src/components/LockStatusPopup.vue +111 -0
  198. package/src/components/NotificationBell.vue +131 -0
  199. package/src/components/NotificationToast.vue +72 -0
  200. package/src/components/PluginLauncher.vue +138 -0
  201. package/src/components/RightSidebar.vue +113 -0
  202. package/src/components/RoleSelector.vue +64 -0
  203. package/src/components/SessionHistoryPanel.vue +176 -0
  204. package/src/components/SessionTabBar.vue +81 -0
  205. package/src/components/SettingsMcpTab.vue +350 -0
  206. package/src/components/SettingsModal.vue +275 -0
  207. package/src/components/SettingsReferenceDirsTab.vue +173 -0
  208. package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
  209. package/src/components/SidebarHeader.vue +69 -0
  210. package/src/components/StackView.vue +360 -0
  211. package/src/components/SuggestionsPanel.vue +65 -0
  212. package/src/components/TodoExplorer.vue +358 -0
  213. package/src/components/ToolResultsPanel.vue +77 -0
  214. package/src/components/todo/TodoAddDialog.vue +131 -0
  215. package/src/components/todo/TodoEditDialog.vue +47 -0
  216. package/src/components/todo/TodoEditPanel.vue +113 -0
  217. package/src/components/todo/TodoKanbanView.vue +249 -0
  218. package/src/components/todo/TodoListView.vue +79 -0
  219. package/src/components/todo/TodoTableView.vue +177 -0
  220. package/src/composables/useActiveSession.ts +40 -0
  221. package/src/composables/useAppApi.ts +45 -0
  222. package/src/composables/useCanvasViewMode.ts +121 -0
  223. package/src/composables/useChatScroll.ts +47 -0
  224. package/src/composables/useClickOutside.ts +26 -0
  225. package/src/composables/useClipboardCopy.ts +44 -0
  226. package/src/composables/useContentDisplay.ts +52 -0
  227. package/src/composables/useDebugBeat.ts +23 -0
  228. package/src/composables/useDynamicFavicon.ts +115 -0
  229. package/src/composables/useEventListeners.ts +42 -0
  230. package/src/composables/useExpandedDirs.ts +64 -0
  231. package/src/composables/useFaviconState.ts +30 -0
  232. package/src/composables/useFileSelection.ts +115 -0
  233. package/src/composables/useFileSortMode.ts +24 -0
  234. package/src/composables/useFileTree.ts +85 -0
  235. package/src/composables/useFreshPluginData.ts +89 -0
  236. package/src/composables/useHealth.ts +38 -0
  237. package/src/composables/useImeAwareEnter.ts +57 -0
  238. package/src/composables/useKeyNavigation.ts +60 -0
  239. package/src/composables/useMarkdownLinkHandler.ts +46 -0
  240. package/src/composables/useMarkdownMode.ts +17 -0
  241. package/src/composables/useMcpTools.ts +71 -0
  242. package/src/composables/useMergedSessions.ts +27 -0
  243. package/src/composables/useNotifications.ts +90 -0
  244. package/src/composables/usePdfDownload.ts +60 -0
  245. package/src/composables/usePendingCalls.ts +77 -0
  246. package/src/composables/usePubSub.ts +85 -0
  247. package/src/composables/useRightSidebar.ts +23 -0
  248. package/src/composables/useRoles.ts +34 -0
  249. package/src/composables/useSandboxStatus.ts +67 -0
  250. package/src/composables/useSelectedResult.ts +49 -0
  251. package/src/composables/useSessionDerived.ts +51 -0
  252. package/src/composables/useSessionHistory.ts +81 -0
  253. package/src/composables/useSessionSync.ts +57 -0
  254. package/src/composables/useViewLayout.ts +55 -0
  255. package/src/config/apiRoutes.ts +173 -0
  256. package/src/config/pubsubChannels.ts +45 -0
  257. package/src/config/roles.ts +335 -0
  258. package/src/config/schedulerActions.ts +25 -0
  259. package/src/config/toolNames.ts +71 -0
  260. package/src/config/workspacePaths.ts +24 -0
  261. package/src/index.css +107 -0
  262. package/src/main.ts +25 -0
  263. package/src/plugins/canvas/Preview.vue +13 -0
  264. package/src/plugins/canvas/View.vue +333 -0
  265. package/src/plugins/canvas/definition.ts +38 -0
  266. package/src/plugins/canvas/index.ts +36 -0
  267. package/src/plugins/chart/Preview.vue +49 -0
  268. package/src/plugins/chart/View.vue +143 -0
  269. package/src/plugins/chart/definition.ts +58 -0
  270. package/src/plugins/chart/index.ts +52 -0
  271. package/src/plugins/editImage/Preview.vue +13 -0
  272. package/src/plugins/editImage/View.vue +13 -0
  273. package/src/plugins/editImage/definition.ts +27 -0
  274. package/src/plugins/editImage/index.ts +36 -0
  275. package/src/plugins/generateImage/Preview.vue +13 -0
  276. package/src/plugins/generateImage/View.vue +33 -0
  277. package/src/plugins/generateImage/definition.ts +32 -0
  278. package/src/plugins/generateImage/index.ts +56 -0
  279. package/src/plugins/manageRoles/Preview.vue +49 -0
  280. package/src/plugins/manageRoles/View.vue +525 -0
  281. package/src/plugins/manageRoles/definition.ts +43 -0
  282. package/src/plugins/manageRoles/index.ts +47 -0
  283. package/src/plugins/manageSkills/Preview.vue +21 -0
  284. package/src/plugins/manageSkills/View.vue +321 -0
  285. package/src/plugins/manageSkills/definition.ts +49 -0
  286. package/src/plugins/manageSkills/index.ts +49 -0
  287. package/src/plugins/manageSource/Preview.vue +33 -0
  288. package/src/plugins/manageSource/View.vue +697 -0
  289. package/src/plugins/manageSource/definition.ts +63 -0
  290. package/src/plugins/manageSource/index.ts +66 -0
  291. package/src/plugins/markdown/Preview.vue +77 -0
  292. package/src/plugins/markdown/View.vue +476 -0
  293. package/src/plugins/markdown/definition.ts +50 -0
  294. package/src/plugins/markdown/index.ts +36 -0
  295. package/src/plugins/presentHtml/Preview.vue +25 -0
  296. package/src/plugins/presentHtml/View.vue +52 -0
  297. package/src/plugins/presentHtml/definition.ts +27 -0
  298. package/src/plugins/presentHtml/helpers.ts +72 -0
  299. package/src/plugins/presentHtml/index.ts +41 -0
  300. package/src/plugins/presentMulmoScript/Preview.vue +23 -0
  301. package/src/plugins/presentMulmoScript/View.vue +1166 -0
  302. package/src/plugins/presentMulmoScript/definition.ts +95 -0
  303. package/src/plugins/presentMulmoScript/helpers.ts +162 -0
  304. package/src/plugins/presentMulmoScript/index.ts +40 -0
  305. package/src/plugins/scheduler/Preview.vue +67 -0
  306. package/src/plugins/scheduler/TasksTab.vue +205 -0
  307. package/src/plugins/scheduler/View.vue +565 -0
  308. package/src/plugins/scheduler/definition.ts +57 -0
  309. package/src/plugins/scheduler/index.ts +45 -0
  310. package/src/plugins/scheduler/viewModes.ts +26 -0
  311. package/src/plugins/spreadsheet/Preview.vue +29 -0
  312. package/src/plugins/spreadsheet/View.vue +997 -0
  313. package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
  314. package/src/plugins/spreadsheet/definition.ts +121 -0
  315. package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
  316. package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
  317. package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
  318. package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
  319. package/src/plugins/spreadsheet/engine/engine.ts +176 -0
  320. package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
  321. package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
  322. package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
  323. package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
  324. package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
  325. package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
  326. package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
  327. package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
  328. package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
  329. package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
  330. package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
  331. package/src/plugins/spreadsheet/engine/index.ts +27 -0
  332. package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
  333. package/src/plugins/spreadsheet/engine/parser.ts +143 -0
  334. package/src/plugins/spreadsheet/engine/registry.ts +150 -0
  335. package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
  336. package/src/plugins/spreadsheet/engine/types.ts +64 -0
  337. package/src/plugins/spreadsheet/index.ts +36 -0
  338. package/src/plugins/textResponse/Preview.vue +94 -0
  339. package/src/plugins/textResponse/View.vue +503 -0
  340. package/src/plugins/textResponse/definition.ts +34 -0
  341. package/src/plugins/textResponse/index.ts +27 -0
  342. package/src/plugins/textResponse/plugin.ts +29 -0
  343. package/src/plugins/textResponse/samples.ts +97 -0
  344. package/src/plugins/textResponse/types.ts +11 -0
  345. package/src/plugins/todo/Preview.vue +63 -0
  346. package/src/plugins/todo/View.vue +364 -0
  347. package/src/plugins/todo/composables/useTodos.ts +177 -0
  348. package/src/plugins/todo/definition.ts +45 -0
  349. package/src/plugins/todo/index.ts +61 -0
  350. package/src/plugins/todo/labels.ts +163 -0
  351. package/src/plugins/todo/priority.ts +98 -0
  352. package/src/plugins/todo/viewModes.ts +19 -0
  353. package/src/plugins/ui-image/ImagePreview.vue +23 -0
  354. package/src/plugins/ui-image/ImageView.vue +34 -0
  355. package/src/plugins/ui-image/index.ts +3 -0
  356. package/src/plugins/ui-image/types.ts +4 -0
  357. package/src/plugins/wiki/Preview.vue +65 -0
  358. package/src/plugins/wiki/View.vue +342 -0
  359. package/src/plugins/wiki/definition.ts +25 -0
  360. package/src/plugins/wiki/helpers.ts +59 -0
  361. package/src/plugins/wiki/index.ts +52 -0
  362. package/src/router/guards.ts +61 -0
  363. package/src/router/index.ts +50 -0
  364. package/src/tools/index.ts +52 -0
  365. package/src/tools/types.ts +27 -0
  366. package/src/types/events.ts +16 -0
  367. package/src/types/fileTree.ts +13 -0
  368. package/src/types/notification.ts +67 -0
  369. package/src/types/session.ts +116 -0
  370. package/src/types/sse.ts +90 -0
  371. package/src/types/toolCallHistory.ts +13 -0
  372. package/src/utils/agent/eventDispatch.ts +74 -0
  373. package/src/utils/agent/request.ts +55 -0
  374. package/src/utils/agent/toolCalls.ts +62 -0
  375. package/src/utils/api.ts +218 -0
  376. package/src/utils/canvas/viewMode.ts +46 -0
  377. package/src/utils/dom/authTokenMeta.ts +20 -0
  378. package/src/utils/dom/clickOutside.ts +11 -0
  379. package/src/utils/dom/externalLink.ts +57 -0
  380. package/src/utils/dom/scrollable.ts +24 -0
  381. package/src/utils/errors.ts +11 -0
  382. package/src/utils/files/expandedDirs.ts +25 -0
  383. package/src/utils/files/filename.ts +12 -0
  384. package/src/utils/files/sortChildren.ts +20 -0
  385. package/src/utils/filesPreview/schedulerPreview.ts +38 -0
  386. package/src/utils/filesPreview/todoPreview.ts +40 -0
  387. package/src/utils/format/date.ts +85 -0
  388. package/src/utils/format/frontmatter.ts +80 -0
  389. package/src/utils/format/jsonSyntax.ts +109 -0
  390. package/src/utils/html/previewCsp.ts +65 -0
  391. package/src/utils/image/resolve.ts +8 -0
  392. package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
  393. package/src/utils/markdown/extractFirstH1.ts +39 -0
  394. package/src/utils/notification/dispatch.ts +22 -0
  395. package/src/utils/path/relativeLink.ts +130 -0
  396. package/src/utils/role/icon.ts +20 -0
  397. package/src/utils/role/merge.ts +10 -0
  398. package/src/utils/role/plugins.ts +12 -0
  399. package/src/utils/session/mergeSessions.ts +103 -0
  400. package/src/utils/session/seedRoleDefault.ts +35 -0
  401. package/src/utils/session/sessionEntries.ts +121 -0
  402. package/src/utils/session/sessionFactory.ts +22 -0
  403. package/src/utils/session/sessionHelpers.ts +99 -0
  404. package/src/utils/tools/dedup.ts +17 -0
  405. package/src/utils/tools/mcp.ts +33 -0
  406. package/src/utils/tools/pendingCalls.ts +16 -0
  407. package/src/utils/tools/result.ts +40 -0
  408. package/src/utils/types.ts +44 -0
package/src/App.vue ADDED
@@ -0,0 +1,720 @@
1
+ <template>
2
+ <div class="flex flex-col fixed inset-0 bg-gray-900 text-white">
3
+ <!-- Global top bar — shown in every view mode -->
4
+ <div ref="topBarRef" class="shrink-0 bg-white text-gray-900">
5
+ <!-- Row 1: title + plugin launcher -->
6
+ <div class="flex items-center gap-3 px-3 py-2 border-b border-gray-200">
7
+ <SidebarHeader
8
+ :sandbox-enabled="sandboxEnabled"
9
+ :show-right-sidebar="showRightSidebar"
10
+ :title-style="debugTitleStyle"
11
+ @test-query="(q) => sendMessage(q)"
12
+ @notification-navigate="handleNotificationNavigate"
13
+ @toggle-right-sidebar="toggleRightSidebar"
14
+ @open-settings="showSettings = true"
15
+ />
16
+ <div class="flex-1 min-w-0">
17
+ <PluginLauncher :active-tool-name="selectedResult?.toolName ?? null" :active-view-mode="canvasViewMode" @navigate="onPluginNavigate" />
18
+ </div>
19
+ </div>
20
+ <!-- Row 2: canvas toggle + role selector + session tabs -->
21
+ <div class="flex items-center gap-3 px-3 py-2 border-b border-gray-100">
22
+ <CanvasViewToggle :model-value="canvasViewMode" @update:model-value="setCanvasViewMode" />
23
+ <RoleSelector v-model:current-role-id="currentRoleId" :roles="roles" @change="onRoleChange" />
24
+ <SessionTabBar
25
+ ref="sessionTabBarRef"
26
+ :sessions="tabSessions"
27
+ :current-session-id="displayedCurrentSessionId"
28
+ :roles="roles"
29
+ :active-session-count="activeSessionCount"
30
+ :unread-count="unreadCount"
31
+ :history-open="showHistory"
32
+ @new-session="handleNewSessionClick"
33
+ @load-session="handleSessionSelect"
34
+ @toggle-history="toggleHistory"
35
+ />
36
+ </div>
37
+ </div>
38
+
39
+ <!-- History popup (all layouts) -->
40
+ <SessionHistoryPanel
41
+ v-if="showHistory"
42
+ ref="historyPanelRef"
43
+ :sessions="mergedSessions"
44
+ :current-session-id="currentSessionId"
45
+ :roles="roles"
46
+ :top-offset="historyTopOffset"
47
+ :error-message="historyError"
48
+ @load-session="handleSessionSelect"
49
+ />
50
+
51
+ <!-- Body: sidebar (Single only) + canvas column + right sidebar -->
52
+ <div class="flex flex-1 min-h-0">
53
+ <!-- Sidebar (Single layout only) -->
54
+ <div v-if="!isStackLayout" class="w-80 flex-shrink-0 border-r border-gray-200 flex flex-col bg-white text-gray-900 relative">
55
+ <!-- Gemini API key warning -->
56
+ <div
57
+ v-if="!geminiAvailable && needsGeminiForRole(currentRoleId)"
58
+ class="mx-4 mt-3 mb-2 rounded border border-yellow-400 bg-yellow-50 p-3 text-xs text-yellow-700 shrink-0"
59
+ >
60
+ <span class="material-icons text-xs align-middle mr-1">warning</span>
61
+ Image generation requires
62
+ <code class="font-mono">GEMINI_API_KEY</code>. Add it to <code class="font-mono">.env</code> and restart the app.
63
+ </div>
64
+
65
+ <!-- Tool result previews -->
66
+ <ToolResultsPanel
67
+ ref="toolResultsPanelRef"
68
+ :results="sidebarResults"
69
+ :selected-uuid="selectedResultUuid"
70
+ :result-timestamps="activeSession?.resultTimestamps ?? new Map()"
71
+ :is-running="isRunning"
72
+ :status-message="statusMessage"
73
+ :pending-calls="pendingCalls"
74
+ @select="onSidebarItemClick"
75
+ @activate="activePane = 'sidebar'"
76
+ />
77
+
78
+ <!-- Sample queries (expandable pane) -->
79
+ <SuggestionsPanel ref="suggestionsPanelRef" :queries="currentRole.queries ?? []" @send="(q) => sendMessage(q)" @edit="onQueryEdit" />
80
+
81
+ <!-- Text input -->
82
+ <ChatInput ref="chatInputRef" v-model="userInput" v-model:pasted-file="pastedFile" :is-running="isRunning" @send="sendMessage()" />
83
+ </div>
84
+
85
+ <!-- Canvas column -->
86
+ <div class="flex-1 flex flex-col bg-white text-gray-900 min-w-0 overflow-hidden relative">
87
+ <!-- Gemini API key warning (Stack layouts — no sidebar to host it) -->
88
+ <div
89
+ v-if="isStackLayout && !geminiAvailable && needsGeminiForRole(currentRoleId)"
90
+ class="mx-3 mt-2 rounded border border-yellow-400 bg-yellow-50 p-2 text-xs text-yellow-700 shrink-0"
91
+ >
92
+ <span class="material-icons text-xs align-middle mr-1">warning</span>
93
+ Image generation requires
94
+ <code class="font-mono">GEMINI_API_KEY</code>. Add it to <code class="font-mono">.env</code> and restart the app.
95
+ </div>
96
+
97
+ <div ref="canvasRef" class="flex-1 overflow-hidden outline-none min-h-0" tabindex="0" @mousedown="activePane = 'main'" @keydown="handleCanvasKeydown">
98
+ <!-- Single mode -->
99
+ <template v-if="canvasViewMode === 'single'">
100
+ <component
101
+ :is="getPlugin(selectedResult.toolName)?.viewComponent"
102
+ v-if="selectedResult && getPlugin(selectedResult.toolName)?.viewComponent"
103
+ :selected-result="selectedResult"
104
+ :send-text-message="sendMessage"
105
+ @update-result="handleUpdateResult"
106
+ />
107
+ <div v-else-if="selectedResult" class="h-full overflow-auto p-6">
108
+ <pre class="text-sm text-gray-700 whitespace-pre-wrap">{{ JSON.stringify(selectedResult, null, 2) }}</pre>
109
+ </div>
110
+ <div v-else class="flex items-center justify-center h-full text-gray-600">
111
+ <p>Start a conversation</p>
112
+ </div>
113
+ </template>
114
+ <!-- Stack mode -->
115
+ <StackView
116
+ v-else-if="canvasViewMode === 'stack'"
117
+ :tool-results="sidebarResults"
118
+ :selected-result-uuid="selectedResultUuid"
119
+ :result-timestamps="activeSession?.resultTimestamps ?? new Map()"
120
+ :send-text-message="sendMessage"
121
+ @select="(uuid) => (selectedResultUuid = uuid)"
122
+ @update-result="handleUpdateResult"
123
+ />
124
+ <!-- Files mode -->
125
+ <FilesView v-else-if="canvasViewMode === 'files'" :refresh-token="filesRefreshToken" @load-session="handleSessionSelect" />
126
+ <!-- Todos mode -->
127
+ <TodoExplorer v-else-if="canvasViewMode === 'todos'" />
128
+ <!-- Scheduler mode -->
129
+ <SchedulerView v-else-if="canvasViewMode === 'scheduler'" />
130
+ <!-- Wiki mode -->
131
+ <WikiView v-else-if="canvasViewMode === 'wiki'" />
132
+ <!-- Skills mode -->
133
+ <SkillsView v-else-if="canvasViewMode === 'skills'" />
134
+ <!-- Roles mode -->
135
+ <RolesView v-else-if="canvasViewMode === 'roles'" />
136
+ </div>
137
+
138
+ <!-- Bottom bar (Stack chat only — plugin views have no
139
+ session context, so no chat input is shown) -->
140
+ <div v-if="canvasViewMode === 'stack'" class="border-t border-gray-200 bg-white shrink-0">
141
+ <SuggestionsPanel ref="suggestionsPanelRef" :queries="currentRole.queries ?? []" @send="(q) => sendMessage(q)" @edit="onQueryEdit" />
142
+ <ChatInput ref="chatInputRef" v-model="userInput" v-model:pasted-file="pastedFile" :is-running="isRunning" @send="sendMessage()" />
143
+ </div>
144
+ </div>
145
+
146
+ <!-- Right sidebar: tool call history -->
147
+ <RightSidebar
148
+ v-if="showRightSidebar"
149
+ ref="rightSidebarRef"
150
+ :tool-call-history="toolCallHistory"
151
+ :available-tools="availableTools"
152
+ :role-prompt="currentRole.prompt"
153
+ :tool-descriptions="toolDescriptions"
154
+ />
155
+ </div>
156
+
157
+ <!-- Global settings modal -->
158
+ <SettingsModal :open="showSettings" :docker-mode="sandboxEnabled" :mcp-tools-error="mcpToolsError" @update:open="showSettings = $event" />
159
+ <NotificationToast />
160
+ </div>
161
+ </template>
162
+
163
+ <script setup lang="ts">
164
+ import { ref, computed, watch, nextTick, onMounted, reactive } from "vue";
165
+ import { v4 as uuidv4 } from "uuid";
166
+ import { getPlugin } from "./tools";
167
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
168
+ import RightSidebar from "./components/RightSidebar.vue";
169
+ import SidebarHeader from "./components/SidebarHeader.vue";
170
+ import RoleSelector from "./components/RoleSelector.vue";
171
+ import SessionTabBar from "./components/SessionTabBar.vue";
172
+ import SuggestionsPanel from "./components/SuggestionsPanel.vue";
173
+ import ChatInput, { type PastedFile } from "./components/ChatInput.vue";
174
+ import SessionHistoryPanel from "./components/SessionHistoryPanel.vue";
175
+ import ToolResultsPanel from "./components/ToolResultsPanel.vue";
176
+ import CanvasViewToggle from "./components/CanvasViewToggle.vue";
177
+ import PluginLauncher from "./components/PluginLauncher.vue";
178
+ import StackView from "./components/StackView.vue";
179
+ import FilesView from "./components/FilesView.vue";
180
+ import TodoExplorer from "./components/TodoExplorer.vue";
181
+ import SchedulerView from "./plugins/scheduler/View.vue";
182
+ import WikiView from "./plugins/wiki/View.vue";
183
+ import SkillsView from "./plugins/manageSkills/View.vue";
184
+ import RolesView from "./plugins/manageRoles/View.vue";
185
+ import SettingsModal from "./components/SettingsModal.vue";
186
+ import NotificationToast from "./components/NotificationToast.vue";
187
+ import type { NotificationAction } from "./types/notification";
188
+ import { CANVAS_VIEW } from "./utils/canvas/viewMode";
189
+ import type { SseEvent } from "./types/sse";
190
+ import { type SessionEntry, type ActiveSession } from "./types/session";
191
+ import { EVENT_TYPES } from "./types/events";
192
+ import { extractImageData } from "./utils/tools/result";
193
+ import { buildAgentRequestBody, postAgentRun } from "./utils/agent/request";
194
+ import { applyAgentEvent, type AgentEventContext } from "./utils/agent/eventDispatch";
195
+ import { pushErrorMessage, beginUserTurn, updateResult } from "./utils/session/sessionHelpers";
196
+ import { maybeSeedRoleDefault } from "./utils/session/seedRoleDefault";
197
+ import { createEmptySession } from "./utils/session/sessionFactory";
198
+ import { buildLoadedSession, parseSessionEntries } from "./utils/session/sessionEntries";
199
+ import { resolveNotificationTarget } from "./utils/notification/dispatch";
200
+ import { usePendingCalls } from "./composables/usePendingCalls";
201
+ import { useClickOutside } from "./composables/useClickOutside";
202
+ import { useKeyNavigation } from "./composables/useKeyNavigation";
203
+ import { useDebugBeat } from "./composables/useDebugBeat";
204
+ import { useChatScroll } from "./composables/useChatScroll";
205
+ import { useViewLayout } from "./composables/useViewLayout";
206
+ import { useSessionSync } from "./composables/useSessionSync";
207
+ import { useSessionDerived } from "./composables/useSessionDerived";
208
+ import { useFaviconState } from "./composables/useFaviconState";
209
+ import { useMergedSessions } from "./composables/useMergedSessions";
210
+ import { useCanvasViewMode } from "./composables/useCanvasViewMode";
211
+ import { useSelectedResult } from "./composables/useSelectedResult";
212
+ import { useMcpTools } from "./composables/useMcpTools";
213
+ import { useRoles } from "./composables/useRoles";
214
+ import { usePubSub } from "./composables/usePubSub";
215
+ import { sessionChannel } from "./config/pubsubChannels";
216
+ import { useHealth } from "./composables/useHealth";
217
+ import { useSessionHistory } from "./composables/useSessionHistory";
218
+ import { useRightSidebar } from "./composables/useRightSidebar";
219
+ import { useEventListeners } from "./composables/useEventListeners";
220
+ import { provideAppApi } from "./composables/useAppApi";
221
+ import { provideActiveSession } from "./composables/useActiveSession";
222
+ import { useRoute, useRouter } from "vue-router";
223
+ import { apiGet } from "./utils/api";
224
+ import { API_ROUTES } from "./config/apiRoutes";
225
+ import { needsGemini } from "./utils/role/plugins";
226
+
227
+ // --- Per-session state ---
228
+ // Declared early so that pub/sub callbacks and function declarations
229
+ // below can reference them without forward-reference ambiguity.
230
+ const sessionMap = reactive(new Map<string, ActiveSession>());
231
+
232
+ // Tracks active pub/sub subscriptions per session. The unsubscribe
233
+ // function is stored so we can clean up when the session is removed
234
+ // from memory. Sessions that are running always have an active
235
+ // subscription so events arrive via WebSocket.
236
+ const sessionSubscriptions = new Map<string, () => void>();
237
+
238
+ // currentSessionId is a plain ref so that synchronous writes (e.g.
239
+ // inside createNewSession, which is called right before sendMessage
240
+ // might run) take effect immediately. The URL is kept in sync via
241
+ // navigateToSession, and external URL changes (back button, typed
242
+ // URL) feed back into the ref via the route watcher below.
243
+ const currentSessionId = ref("");
244
+
245
+ // --- Debug beat (pub/sub) ---
246
+ const { debugTitleStyle } = useDebugBeat();
247
+
248
+ const { subscribe: pubsubSubscribe } = usePubSub();
249
+
250
+ // --- Routing ---
251
+ const route = useRoute();
252
+ const router = useRouter();
253
+
254
+ // Omit ?role= for the default role to keep URLs clean.
255
+ function buildRoleQuery(): Record<string, string> {
256
+ const id = currentRoleId.value;
257
+ if (!id || roles.value.length === 0 || id === roles.value[0]?.id) return {};
258
+ return { role: id };
259
+ }
260
+
261
+ function navigateToSession(id: string, replace = false): void {
262
+ currentSessionId.value = id;
263
+ const method = replace ? router.replace : router.push;
264
+ method({
265
+ name: "chat",
266
+ params: { sessionId: id },
267
+ query: { ...buildViewQuery(), ...buildRoleQuery() },
268
+ }).catch((err) => {
269
+ if (err?.type !== 16) {
270
+ console.error("[navigateToSession] push failed:", err);
271
+ }
272
+ });
273
+ }
274
+
275
+ function handleNotificationNavigate(action: NotificationAction): void {
276
+ const target = resolveNotificationTarget(action);
277
+ if (!target) return;
278
+ if (target.kind === "session") {
279
+ navigateToSession(target.sessionId);
280
+ } else {
281
+ setCanvasViewMode(CANVAS_VIEW[target.view]);
282
+ }
283
+ }
284
+
285
+ // External URL changes (back/forward button, typed URL) → update ref.
286
+ // If the session isn't in memory, load it from the server.
287
+ watch(
288
+ () => route.params.sessionId,
289
+ async (newId) => {
290
+ if (typeof newId !== "string" || newId === currentSessionId.value) return;
291
+ currentSessionId.value = newId;
292
+ if (!sessionMap.has(newId)) {
293
+ await loadSession(newId);
294
+ if (!sessionMap.has(newId)) {
295
+ createNewSession();
296
+ }
297
+ }
298
+ },
299
+ );
300
+
301
+ // External URL changes for ?role= → sync into currentRoleId.
302
+ // This doesn't trigger onRoleChange (which creates a new session) —
303
+ // the user is just navigating back/forward between sessions that
304
+ // were already associated with a role.
305
+ watch(
306
+ () => route.query.role,
307
+ (newRole) => {
308
+ if (typeof newRole !== "string" || newRole === currentRoleId.value) return;
309
+ const roleExists = roles.value.some((role) => role.id === newRole);
310
+ if (roleExists) currentRoleId.value = newRole;
311
+ },
312
+ );
313
+
314
+ // --- Global state ---
315
+ const { roles, currentRoleId, currentRole, refreshRoles } = useRoles();
316
+
317
+ const userInput = ref("");
318
+ const pastedFile = ref<PastedFile | null>(null);
319
+ const activePane = ref<"sidebar" | "main">("sidebar");
320
+
321
+ const { sessions, showHistory, historyError, fetchSessions, toggleHistory } = useSessionHistory();
322
+ const { markSessionRead } = useSessionSync({
323
+ sessionMap,
324
+ currentSessionId,
325
+ fetchSessions,
326
+ });
327
+ const { geminiAvailable, sandboxEnabled, fetchHealth } = useHealth();
328
+
329
+ const { activeSession, toolResults, sidebarResults, currentSummary, isRunning, statusMessage, toolCallHistory, activeSessionCount, unreadCount } =
330
+ useSessionDerived({ sessionMap, currentSessionId, sessions });
331
+
332
+ const { selectedResultUuid } = useSelectedResult({
333
+ activeSession,
334
+ sessionMap,
335
+ currentSessionId,
336
+ });
337
+
338
+ // ── Dynamic favicon (#470) ──────────────────────────────────
339
+ useFaviconState({ isRunning, currentSummary, activeSession });
340
+
341
+ const toolResultsPanelRef = ref<{ root: HTMLDivElement | null } | null>(null);
342
+ const canvasRef = ref<HTMLDivElement | null>(null);
343
+ const chatInputRef = ref<{ focus: () => void } | null>(null);
344
+ const topBarRef = ref<HTMLDivElement | null>(null);
345
+ const historyTopOffset = ref<number | undefined>(undefined);
346
+
347
+ const sessionTabBarRef = ref<{
348
+ historyButton: HTMLButtonElement | null;
349
+ } | null>(null);
350
+ const historyButtonRef = computed(() => sessionTabBarRef.value?.historyButton ?? null);
351
+ const historyPanelRef = ref<{ root: HTMLDivElement | null } | null>(null);
352
+ const historyPopupRef = computed(() => historyPanelRef.value?.root ?? null);
353
+
354
+ const { focusChatInput } = useChatScroll({
355
+ toolResultsPanelRef,
356
+ toolResults,
357
+ isRunning,
358
+ chatInputRef,
359
+ });
360
+
361
+ const { showRightSidebar, toggleRightSidebar } = useRightSidebar();
362
+ const showSettings = ref(false);
363
+
364
+ const { canvasViewMode, setCanvasViewMode, buildViewQuery, filesRefreshToken, handleViewModeShortcut, onPluginNavigate } = useCanvasViewMode({ isRunning });
365
+
366
+ // The no-sidebar "stack-style" layout (top bar + full-width canvas +
367
+ // bottom bar) is used for every view mode except Single. Clicking a
368
+ // plugin launcher button (Todos / Scheduler / Files / ...) swaps the
369
+ // canvas content without collapsing the frame back to the sidebar
370
+ // layout.
371
+ const { isStackLayout, restoreChatViewForSession, displayedCurrentSessionId } = useViewLayout({
372
+ canvasViewMode,
373
+ setCanvasViewMode,
374
+ currentSessionId,
375
+ activePane,
376
+ });
377
+
378
+ // User-initiated session switches: clicking a session tab, a history
379
+ // row, or a chat link in FilesView. In plugin views (Todos / Files /
380
+ // ...) no chat is active, so the click's purpose is to surface the
381
+ // chat — restore the preferred Single/Stack mode before loading.
382
+ // Not wired into the internal `loadSession` call path because that
383
+ // also fires on initial mount with `?view=plugin` URLs, which must
384
+ // be honoured as-is.
385
+ function handleSessionSelect(id: string): void {
386
+ restoreChatViewForSession();
387
+ loadSession(id);
388
+ }
389
+
390
+ function handleNewSessionClick(): void {
391
+ restoreChatViewForSession();
392
+ createNewSession();
393
+ }
394
+
395
+ // Measure the top bar's height when the history popup opens.
396
+ watch(showHistory, (open) => {
397
+ if (open) {
398
+ nextTick(() => {
399
+ historyTopOffset.value = topBarRef.value?.offsetHeight;
400
+ });
401
+ }
402
+ });
403
+ const rightSidebarRef = ref<InstanceType<typeof RightSidebar> | null>(null);
404
+
405
+ const { availableTools, toolDescriptions, mcpToolsError, fetchMcpToolsStatus } = useMcpTools({
406
+ currentRole,
407
+ getDefinition: (name) => getPlugin(name)?.toolDefinition ?? null,
408
+ });
409
+
410
+ const { pendingCalls, teardown: teardownPendingCalls } = usePendingCalls({
411
+ isRunning,
412
+ toolCallHistory,
413
+ });
414
+
415
+ const selectedResult = computed(() => toolResults.value.find((result) => result.uuid === selectedResultUuid.value) ?? null);
416
+
417
+ const { mergedSessions, tabSessions } = useMergedSessions({
418
+ sessionMap,
419
+ sessions,
420
+ });
421
+
422
+ // Centralised session-switch handler: subscribe to the current session's
423
+ // pub/sub channel so we receive real-time events even if the session is
424
+ // idle (another tab may start a run). Unsubscribe from idle sessions
425
+ // when switching away (running sessions keep their subscription so they
426
+ // continue receiving events — session_finished will clean them up).
427
+ let previousSessionId: string | null = null;
428
+ watch(currentSessionId, (id) => {
429
+ const session = sessionMap.get(id);
430
+ // Subscribe to the new session's channel
431
+ if (session) {
432
+ ensureSessionSubscription(session);
433
+ }
434
+ // Unsubscribe from the previous session if it's not running and has
435
+ // no in-flight background generations. Tearing down the subscription
436
+ // while a generation is still running would orphan its completion
437
+ // event, leaving the session's busy indicator stuck on.
438
+ if (previousSessionId && previousSessionId !== id) {
439
+ const prevSession = sessionMap.get(previousSessionId);
440
+ const prevBusy = !!prevSession && (prevSession.isRunning || Object.keys(prevSession.pendingGenerations ?? {}).length > 0);
441
+ if (prevSession && !prevBusy) {
442
+ unsubscribeSession(previousSessionId);
443
+ }
444
+ }
445
+ previousSessionId = id;
446
+
447
+ // Clear unread in both sessionMap and sessions list (for badge count),
448
+ // then tell the server so other tabs see it too.
449
+ const summary = sessions.value.find((entry) => entry.id === id);
450
+ const wasUnread = (session && session.hasUnread) || (summary && summary.hasUnread);
451
+ if (wasUnread) {
452
+ if (session) session.hasUnread = false;
453
+ if (summary) summary.hasUnread = false;
454
+ markSessionRead(id);
455
+ }
456
+ });
457
+
458
+ const { handleCanvasKeydown, handleKeyNavigation } = useKeyNavigation({
459
+ canvasRef,
460
+ activePane,
461
+ sidebarResults,
462
+ selectedResultUuid,
463
+ });
464
+
465
+ const suggestionsPanelRef = ref<{ collapse: () => void } | null>(null);
466
+
467
+ function onQueryEdit(query: string): void {
468
+ userInput.value = query;
469
+ nextTick(() => focusChatInput());
470
+ }
471
+
472
+ function handleUpdateResult(updatedResult: ToolResultComplete) {
473
+ if (activeSession.value) updateResult(activeSession.value, updatedResult);
474
+ }
475
+
476
+ function onSidebarItemClick(uuid: string) {
477
+ selectedResultUuid.value = uuid;
478
+ }
479
+
480
+ const needsGeminiForRole = (roleId: string) => needsGemini(roles.value, roleId);
481
+
482
+ // Remove the current session from sessionMap if it's empty (no messages).
483
+ // Returns true if a session was removed, so the caller can use
484
+ // router.replace instead of router.push to keep the empty session out
485
+ // of browser navigation history.
486
+ function removeCurrentIfEmpty(): boolean {
487
+ const id = currentSessionId.value;
488
+ if (!id) return false;
489
+ const session = sessionMap.get(id);
490
+ if (session && session.toolResults.length === 0) {
491
+ sessionMap.delete(id);
492
+ return true;
493
+ }
494
+ return false;
495
+ }
496
+
497
+ function createNewSession(roleId?: string): ActiveSession {
498
+ removeCurrentIfEmpty();
499
+ const rId = roleId ?? currentRoleId.value;
500
+ const session = createEmptySession(uuidv4(), rId);
501
+ sessionMap.set(session.id, session);
502
+ currentRoleId.value = rId;
503
+ navigateToSession(session.id, true);
504
+ suggestionsPanelRef.value?.collapse();
505
+ nextTick(() => focusChatInput());
506
+ return sessionMap.get(session.id)!;
507
+ }
508
+
509
+ function onRoleChange() {
510
+ // Covers both the user dropdown click and the agent-triggered role
511
+ // switch (EVENT_TYPES.switchRole) — either way the user ends up in
512
+ // a fresh chat session, so a plugin view should yield to chat.
513
+ restoreChatViewForSession();
514
+ const session = createNewSession(currentRoleId.value);
515
+ maybeSeedRoleDefault(session);
516
+ }
517
+
518
+ function activateSession(id: string, roleId: string, replace: boolean): void {
519
+ const reactiveSession = sessionMap.get(id);
520
+ if (reactiveSession) ensureSessionSubscription(reactiveSession);
521
+ // Set role before navigating: buildRoleQuery() reads currentRoleId to
522
+ // build ?role=, and the route.query.role watcher would otherwise fire
523
+ // after navigation and revert currentRoleId to the previous session's role.
524
+ currentRoleId.value = roleId;
525
+ navigateToSession(id, replace);
526
+ showHistory.value = false;
527
+ }
528
+
529
+ async function loadSession(id: string) {
530
+ if (id === currentSessionId.value && sessionMap.has(id)) return;
531
+ const replaced = removeCurrentIfEmpty();
532
+
533
+ const live = sessionMap.get(id);
534
+ if (live) {
535
+ activateSession(id, live.roleId, replaced);
536
+ return;
537
+ }
538
+
539
+ const response = await apiGet<SessionEntry[]>(API_ROUTES.sessions.detail.replace(":id", encodeURIComponent(id)));
540
+ if (!response.ok) return;
541
+
542
+ const newSession = buildLoadedSession({
543
+ id,
544
+ entries: response.data,
545
+ defaultRoleId: currentRoleId.value,
546
+ urlResult: typeof route.query.result === "string" ? route.query.result : null,
547
+ serverSummary: sessions.value.find((s) => s.id === id),
548
+ nowIso: new Date().toISOString(),
549
+ });
550
+ sessionMap.set(id, newSession);
551
+ activateSession(id, newSession.roleId, replaced);
552
+ }
553
+
554
+ // Re-fetch the transcript from the server and patch any entries the
555
+ // client missed (e.g. due to a pub-sub disconnect during a long
556
+ // Docker build). Called on session_finished so the user sees the
557
+ // full response even if mid-run events were lost. See issue #350.
558
+ async function refreshSessionTranscript(sessionId: string): Promise<void> {
559
+ const session = sessionMap.get(sessionId);
560
+ if (!session) return;
561
+ const response = await apiGet<SessionEntry[]>(API_ROUTES.sessions.detail.replace(":id", encodeURIComponent(sessionId)));
562
+ if (!response.ok) return;
563
+ const serverResults = parseSessionEntries(response.data);
564
+ // Only patch if the server knows more than we do — avoids
565
+ // replacing a richer in-flight state with a stale snapshot when
566
+ // session_finished races with the last few events.
567
+ if (serverResults.length > session.toolResults.length) {
568
+ session.toolResults = serverResults;
569
+ }
570
+ }
571
+
572
+ function buildAgentEventContext(session: ActiveSession): AgentEventContext {
573
+ const sessionId = session.id;
574
+ return {
575
+ get session() {
576
+ return sessionMap.get(sessionId) ?? session;
577
+ },
578
+ setCurrentRoleId: (roleId) => {
579
+ currentRoleId.value = roleId;
580
+ },
581
+ onRoleChange,
582
+ refreshRoles,
583
+ scrollSidebarToBottom: () => rightSidebarRef.value?.scrollToBottom(),
584
+ onGenerationsDrained: () => {
585
+ if (currentSessionId.value === sessionId) {
586
+ markSessionRead(sessionId);
587
+ }
588
+ },
589
+ };
590
+ }
591
+
592
+ function hasPendingGenerations(sessionId: string): boolean {
593
+ const live = sessionMap.get(sessionId);
594
+ return !!live && Object.keys(live.pendingGenerations).length > 0;
595
+ }
596
+
597
+ function handleSessionFinished(sessionId: string): void {
598
+ refreshSessionTranscript(sessionId).catch((err) => {
599
+ console.error("[handleSessionFinished] refresh failed:", err);
600
+ });
601
+ if (currentSessionId.value === sessionId) {
602
+ markSessionRead(sessionId);
603
+ } else if (!hasPendingGenerations(sessionId)) {
604
+ unsubscribeSession(sessionId);
605
+ }
606
+ }
607
+
608
+ function createSessionEventHandler(session: ActiveSession, ctx: AgentEventContext): (data: unknown) => void {
609
+ return (data: unknown) => {
610
+ const event = data as SseEvent;
611
+ if (!event || typeof event !== "object") return;
612
+ if (event.type === EVENT_TYPES.sessionFinished) {
613
+ handleSessionFinished(session.id);
614
+ return;
615
+ }
616
+ applyAgentEvent(event, ctx).catch((err) => {
617
+ console.error("[applyAgentEvent] unhandled:", err);
618
+ });
619
+ };
620
+ }
621
+
622
+ function ensureSessionSubscription(session: ActiveSession): void {
623
+ if (sessionSubscriptions.has(session.id)) return;
624
+ const ctx = buildAgentEventContext(session);
625
+ const handler = createSessionEventHandler(session, ctx);
626
+ const unsub = pubsubSubscribe(sessionChannel(session.id), handler);
627
+ sessionSubscriptions.set(session.id, unsub);
628
+ }
629
+
630
+ function unsubscribeSession(chatSessionId: string): void {
631
+ const unsub = sessionSubscriptions.get(chatSessionId);
632
+ if (unsub) {
633
+ unsub();
634
+ sessionSubscriptions.delete(chatSessionId);
635
+ }
636
+ }
637
+
638
+ async function sendMessage(text?: string) {
639
+ const message = typeof text === "string" ? text : userInput.value.trim();
640
+ if (!message || isRunning.value) return;
641
+ userInput.value = "";
642
+ const fileSnapshot = pastedFile.value;
643
+ pastedFile.value = null;
644
+
645
+ const session = sessionMap.get(currentSessionId.value);
646
+ if (!session) return;
647
+
648
+ beginUserTurn(session, message);
649
+ const sessionRole = roles.value.find((role) => role.id === session.roleId) ?? roles.value[0];
650
+ const selectedRes = session.toolResults.find((result) => result.uuid === session.selectedResultUuid) ?? undefined;
651
+
652
+ ensureSessionSubscription(session);
653
+
654
+ const result = await postAgentRun(
655
+ buildAgentRequestBody({
656
+ message,
657
+ role: sessionRole,
658
+ chatSessionId: session.id,
659
+ selectedImageData: fileSnapshot?.dataUrl ?? extractImageData(selectedRes),
660
+ }),
661
+ );
662
+ if (!result.ok) {
663
+ pushErrorMessage(session, result.error);
664
+ unsubscribeSession(session.id);
665
+ }
666
+ }
667
+
668
+ const { handler: handleClickOutsideHistory } = useClickOutside({
669
+ isOpen: showHistory,
670
+ buttonRef: historyButtonRef,
671
+ popupRef: historyPopupRef,
672
+ });
673
+
674
+ // Plugin Views call back into App.vue via provide/inject (#227).
675
+ provideAppApi({
676
+ refreshRoles,
677
+ sendMessage: (message: string) => sendMessage(message),
678
+ });
679
+ // Plugin Views that need to tag background work with the current
680
+ // session (e.g. MulmoScript generations) inject this.
681
+ provideActiveSession(activeSession);
682
+
683
+ useEventListeners({
684
+ onKeyNavigation: handleKeyNavigation,
685
+ onViewModeShortcut: handleViewModeShortcut,
686
+ onClickOutsideHistory: handleClickOutsideHistory,
687
+ onTeardown: teardownPendingCalls,
688
+ });
689
+
690
+ onMounted(async () => {
691
+ // Fire-and-forget side fetches.
692
+ fetchHealth();
693
+ fetchMcpToolsStatus();
694
+ fetchSessions();
695
+ // Roles must be loaded before the first session is created, so
696
+ // createNewSession() picks a roleId that exists in the merged
697
+ // role list (built-in + custom).
698
+ await refreshRoles();
699
+
700
+ // If the URL specifies a role, apply it before session creation.
701
+ const urlRole = typeof route.query.role === "string" ? route.query.role : null;
702
+ if (urlRole && roles.value.some((role) => role.id === urlRole)) {
703
+ currentRoleId.value = urlRole;
704
+ }
705
+
706
+ // If the URL already names a session (e.g. a bookmarked link or a
707
+ // page reload), try to load it. Otherwise create a fresh one.
708
+ const initialSessionId = currentSessionId.value;
709
+ if (initialSessionId) {
710
+ await loadSession(initialSessionId);
711
+ // loadSession is a no-op when the server returns 404 — in that
712
+ // case sessionMap won't have the id, so fall through to create.
713
+ if (!sessionMap.has(initialSessionId)) {
714
+ createNewSession();
715
+ }
716
+ } else {
717
+ createNewSession();
718
+ }
719
+ });
720
+ </script>