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
@@ -0,0 +1,45 @@
1
+ // Vue provide/inject contract that lets plugin Views call back into
2
+ // App.vue without going through window-level CustomEvents.
3
+ //
4
+ // Background: plugins like manageRoles and manageSkills used to
5
+ // dispatch `roles-updated` / `skill-run` on `window` and App.vue
6
+ // listened with `addEventListener`. That worked but routed
7
+ // component-to-component communication through a global side channel,
8
+ // which got hard to follow as more plugins came online (#227).
9
+ //
10
+ // `provide` / `inject` is the Vue-native equivalent: App.vue provides
11
+ // a small typed API surface, plugins inject and call methods directly.
12
+ // No string event names, full type-checking, no chance of a typo
13
+ // silently failing.
14
+
15
+ import { inject, provide } from "vue";
16
+
17
+ /** API surface that plugin Views can call on App.vue. */
18
+ export interface AppApi {
19
+ /** Refresh the role dropdown — call after the roles list changes. */
20
+ refreshRoles: () => void | Promise<void>;
21
+ /** Send a chat message through App.vue's normal sendMessage pipeline. */
22
+ sendMessage: (message: string) => void;
23
+ }
24
+
25
+ const APP_API_KEY = Symbol("appApi");
26
+
27
+ /** Called once in App.vue setup to expose the API to descendants. */
28
+ export function provideAppApi(api: AppApi): void {
29
+ provide(APP_API_KEY, api);
30
+ }
31
+
32
+ /**
33
+ * Called by plugin Views (any descendant of App.vue) to access the API.
34
+ *
35
+ * Throws if used outside an App.vue subtree — if you need a no-op
36
+ * fallback (e.g. a plugin rendered in isolation in a test), pass a
37
+ * default-returning callback to `inject` directly instead.
38
+ */
39
+ export function useAppApi(): AppApi {
40
+ const api = inject<AppApi>(APP_API_KEY);
41
+ if (!api) {
42
+ throw new Error("useAppApi() called outside an App.vue subtree — provideAppApi must run first.");
43
+ }
44
+ return api;
45
+ }
@@ -0,0 +1,121 @@
1
+ // Composable for the canvas view mode values defined in
2
+ // src/utils/canvas/viewMode.ts:
3
+ // owns the reactive ref, syncs to the URL via vue-router, persists
4
+ // to localStorage as fallback, hooks the "refresh files tree after
5
+ // each agent run" side effect, and exposes the Cmd/Ctrl+digit
6
+ // keydown handler. The pure parsing helpers live in
7
+ // src/utils/canvas/viewMode.ts so the rules are unit-testable.
8
+
9
+ import { ref, watch, type ComputedRef, type Ref } from "vue";
10
+ import { useRoute, useRouter, isNavigationFailure } from "vue-router";
11
+ import {
12
+ type CanvasViewMode,
13
+ CANVAS_VIEW,
14
+ VIEW_MODE_STORAGE_KEY,
15
+ parseStoredViewMode,
16
+ viewModeForShortcutKey,
17
+ isCanvasViewMode,
18
+ } from "../utils/canvas/viewMode";
19
+ import type { LocationQuery } from "vue-router";
20
+
21
+ interface UseCanvasViewModeOptions {
22
+ // Watched so the file tree can be refreshed when the agent run
23
+ // ends — newly written files appear without a manual reload.
24
+ isRunning: ComputedRef<boolean> | Ref<boolean>;
25
+ }
26
+
27
+ /**
28
+ * Build a query object that reflects the given view mode.
29
+ * "single" (the default) omits `?view=` for cleaner URLs;
30
+ * other modes set it explicitly.
31
+ */
32
+ function applyViewToQuery(currentQuery: LocationQuery, mode: CanvasViewMode): LocationQuery {
33
+ const rest: LocationQuery = { ...currentQuery };
34
+ delete rest.view;
35
+ // Remove ?path= when leaving the files view — it's only meaningful
36
+ // in files mode and would cause a stale file selection on reload.
37
+ if (mode !== CANVAS_VIEW.files) delete rest.path;
38
+ if (mode === CANVAS_VIEW.single) return rest;
39
+ return { ...rest, view: mode };
40
+ }
41
+
42
+ export function useCanvasViewMode(opts: UseCanvasViewModeOptions): {
43
+ canvasViewMode: Ref<CanvasViewMode>;
44
+ setCanvasViewMode: (mode: CanvasViewMode) => void;
45
+ buildViewQuery: () => LocationQuery;
46
+ filesRefreshToken: Ref<number>;
47
+ handleViewModeShortcut: (e: KeyboardEvent) => void;
48
+ onPluginNavigate: (target: { key: string }) => void;
49
+ } {
50
+ const route = useRoute();
51
+ const router = useRouter();
52
+
53
+ // Initialise from URL if ?view= is present, otherwise fall back to
54
+ // localStorage (the user's last-chosen mode), then to "single".
55
+ const urlView = typeof route.query.view === "string" ? route.query.view : null;
56
+ const canvasViewMode = ref<CanvasViewMode>(parseStoredViewMode(urlView ?? localStorage.getItem(VIEW_MODE_STORAGE_KEY)));
57
+ const filesRefreshToken = ref(0);
58
+
59
+ function setCanvasViewMode(mode: CanvasViewMode): void {
60
+ canvasViewMode.value = mode;
61
+ localStorage.setItem(VIEW_MODE_STORAGE_KEY, mode);
62
+ router.push({ query: applyViewToQuery(route.query, mode) }).catch((err: unknown) => {
63
+ if (!isNavigationFailure(err)) {
64
+ console.error("[setCanvasViewMode] navigation failed:", err);
65
+ }
66
+ });
67
+ }
68
+
69
+ /** Return a query object with the current view mode applied.
70
+ * Used by App.vue's navigateToSession so the URL always reflects
71
+ * the latest canvasViewMode.value (which may be more recent than
72
+ * route.query.view when setCanvasViewMode was just called). */
73
+ function buildViewQuery(): LocationQuery {
74
+ return applyViewToQuery(route.query, canvasViewMode.value);
75
+ }
76
+
77
+ // External URL changes (back/forward button, typed URL) → update ref.
78
+ watch(
79
+ () => route.query.view,
80
+ (newView) => {
81
+ const parsed = parseStoredViewMode(typeof newView === "string" ? newView : null);
82
+ if (parsed !== canvasViewMode.value) {
83
+ canvasViewMode.value = parsed;
84
+ localStorage.setItem(VIEW_MODE_STORAGE_KEY, parsed);
85
+ }
86
+ },
87
+ );
88
+
89
+ // After each run completes, bump filesRefreshToken so any open
90
+ // FilesView re-fetches the workspace tree.
91
+ watch(opts.isRunning, (running, prev) => {
92
+ if (prev && !running) {
93
+ filesRefreshToken.value++;
94
+ }
95
+ });
96
+
97
+ function handleViewModeShortcut(e: KeyboardEvent): void {
98
+ if (!(e.metaKey || e.ctrlKey)) return;
99
+ if (e.altKey || e.shiftKey) return;
100
+ const target = viewModeForShortcutKey(e.key);
101
+ if (target === null) return;
102
+ setCanvasViewMode(target);
103
+ e.preventDefault();
104
+ }
105
+
106
+ /** Plugin-launcher click: switch canvas to the matching view mode. */
107
+ function onPluginNavigate(target: { key: string }): void {
108
+ if (isCanvasViewMode(target.key)) {
109
+ setCanvasViewMode(target.key);
110
+ }
111
+ }
112
+
113
+ return {
114
+ canvasViewMode,
115
+ setCanvasViewMode,
116
+ buildViewQuery,
117
+ filesRefreshToken,
118
+ handleViewModeShortcut,
119
+ onPluginNavigate,
120
+ };
121
+ }
@@ -0,0 +1,47 @@
1
+ // Auto-scroll the sidebar chat list to the bottom when new results
2
+ // arrive or a run starts. Also re-focuses the chat input when a run
3
+ // finishes.
4
+
5
+ import { computed, nextTick, watch, type ComputedRef, type Ref } from "vue";
6
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
7
+
8
+ export function useChatScroll(opts: {
9
+ toolResultsPanelRef: Ref<{ root: HTMLDivElement | null } | null>;
10
+ toolResults: ComputedRef<ToolResultComplete[]>;
11
+ isRunning: ComputedRef<boolean>;
12
+ chatInputRef: Ref<{ focus: () => void } | null>;
13
+ }) {
14
+ const { toolResultsPanelRef, toolResults, isRunning, chatInputRef } = opts;
15
+
16
+ const chatListRef = computed(() => toolResultsPanelRef.value?.root ?? null);
17
+ // Key that changes both on new results AND on streaming updates to
18
+ // the last text card (which appends in place, leaving length stable).
19
+ const latestResultScrollKey = computed(() => {
20
+ const list = toolResults.value;
21
+ const last = list[list.length - 1];
22
+ return `${list.length}:${last?.uuid ?? ""}:${last?.message?.length ?? 0}`;
23
+ });
24
+
25
+ function scrollChatToBottom(): void {
26
+ nextTick(() => {
27
+ if (chatListRef.value) {
28
+ chatListRef.value.scrollTop = chatListRef.value.scrollHeight;
29
+ }
30
+ });
31
+ }
32
+
33
+ function focusChatInput(): void {
34
+ chatInputRef.value?.focus();
35
+ }
36
+
37
+ watch(latestResultScrollKey, scrollChatToBottom);
38
+ watch(isRunning, (running) => {
39
+ if (running) {
40
+ scrollChatToBottom();
41
+ } else {
42
+ nextTick(() => focusChatInput());
43
+ }
44
+ });
45
+
46
+ return { scrollChatToBottom, focusChatInput };
47
+ }
@@ -0,0 +1,26 @@
1
+ // Composable that builds a `mousedown` handler which closes a
2
+ // boolean ref (`isOpen`) when the click happened outside both the
3
+ // trigger button and the popup body. Three popups in App.vue
4
+ // (history, sandbox lock, role dropdown) all share this exact
5
+ // pattern.
6
+
7
+ import type { Ref } from "vue";
8
+ import { isClickOutside } from "../utils/dom/clickOutside";
9
+
10
+ interface UseClickOutsideOptions {
11
+ isOpen: Ref<boolean>;
12
+ buttonRef: Ref<HTMLElement | null>;
13
+ popupRef: Ref<HTMLElement | null>;
14
+ }
15
+
16
+ export function useClickOutside(opts: UseClickOutsideOptions): {
17
+ handler: (e: MouseEvent) => void;
18
+ } {
19
+ function handler(e: MouseEvent): void {
20
+ if (!opts.isOpen.value) return;
21
+ if (isClickOutside(e.target as Node | null, opts.buttonRef.value, opts.popupRef.value)) {
22
+ opts.isOpen.value = false;
23
+ }
24
+ }
25
+ return { handler };
26
+ }
@@ -0,0 +1,44 @@
1
+ // "Copy to clipboard with a transient confirmation flag" — the
2
+ // same 9-line pattern was copied into 3 plugin views
3
+ // (markdown / presentMulmoScript / textResponse) before this
4
+ // composable existed.
5
+ //
6
+ // Usage:
7
+ //
8
+ // const { copied, copy } = useClipboardCopy();
9
+ //
10
+ // async function copyText() {
11
+ // await copy(textToCopy.value);
12
+ // }
13
+ //
14
+ // `copied` flips to true on success and back to false after
15
+ // `resetMs` (default 2000ms) so the UI can show a ✓ / "Copied!"
16
+ // hint. Clipboard failures (permissions, insecure context) are
17
+ // swallowed on purpose — there's no useful UI action beyond letting
18
+ // the hint stay off, which is exactly what the ref signals.
19
+
20
+ import { ref, type Ref } from "vue";
21
+
22
+ export interface UseClipboardCopyHandle {
23
+ copied: Ref<boolean>;
24
+ copy: (text: string) => Promise<void>;
25
+ }
26
+
27
+ export function useClipboardCopy(resetMs = 2000): UseClipboardCopyHandle {
28
+ const copied = ref(false);
29
+
30
+ async function copy(text: string): Promise<void> {
31
+ try {
32
+ await navigator.clipboard.writeText(text);
33
+ copied.value = true;
34
+ setTimeout(() => {
35
+ copied.value = false;
36
+ }, resetMs);
37
+ } catch {
38
+ // Clipboard API may be blocked in some contexts (e.g. iframe
39
+ // without permissions, non-HTTPS origin). Leave `copied` false.
40
+ }
41
+ }
42
+
43
+ return { copied, copy };
44
+ }
@@ -0,0 +1,52 @@
1
+ // Composable: derive content-type flags and formatted views from the
2
+ // current selection. Extracted from FilesView.vue (#507 step 5).
3
+
4
+ import { computed, type Ref } from "vue";
5
+ import type { FileContent } from "./useFileSelection";
6
+ import { wrapHtmlWithPreviewCsp } from "../utils/html/previewCsp";
7
+ import { tokenizeJson, tokenizeJsonl, prettyJson } from "../utils/format/jsonSyntax";
8
+ import { extractFrontmatter } from "../utils/format/frontmatter";
9
+
10
+ function hasExt(filePath: string | null, exts: string[]): boolean {
11
+ if (!filePath) return false;
12
+ const lower = filePath.toLowerCase();
13
+ return exts.some((ext) => lower.endsWith(ext));
14
+ }
15
+
16
+ export function useContentDisplay(selectedPath: Ref<string | null>, content: Ref<FileContent | null>) {
17
+ const isMarkdown = computed(() => hasExt(selectedPath.value, [".md", ".markdown"]));
18
+ const isHtml = computed(() => hasExt(selectedPath.value, [".html", ".htm"]));
19
+ const isJson = computed(() => hasExt(selectedPath.value, [".json"]));
20
+ const isJsonl = computed(() => hasExt(selectedPath.value, [".jsonl", ".ndjson"]));
21
+
22
+ const sandboxedHtml = computed(() => (content.value?.kind === "text" && isHtml.value ? wrapHtmlWithPreviewCsp(content.value.content) : ""));
23
+
24
+ const jsonTokens = computed(() => {
25
+ if (!content.value || content.value.kind !== "text") return [];
26
+ if (!isJson.value) return [];
27
+ return tokenizeJson(prettyJson(content.value.content));
28
+ });
29
+
30
+ const jsonlLines = computed(() => {
31
+ if (!content.value || content.value.kind !== "text") return [];
32
+ if (!isJsonl.value) return [];
33
+ return tokenizeJsonl(content.value.content);
34
+ });
35
+
36
+ const mdFrontmatter = computed(() => {
37
+ if (!content.value || content.value.kind !== "text") return null;
38
+ if (!isMarkdown.value) return null;
39
+ return extractFrontmatter(content.value.content);
40
+ });
41
+
42
+ return {
43
+ isMarkdown,
44
+ isHtml,
45
+ isJson,
46
+ isJsonl,
47
+ sandboxedHtml,
48
+ jsonTokens,
49
+ jsonlLines,
50
+ mdFrontmatter,
51
+ };
52
+ }
@@ -0,0 +1,23 @@
1
+ // Debug beat indicator — toggles the app title color when the server
2
+ // emits debug-beat events via pub/sub. Only active in --debug mode.
3
+
4
+ import { ref, computed, type CSSProperties } from "vue";
5
+ import { usePubSub } from "./usePubSub";
6
+ import { PUBSUB_CHANNELS } from "../config/pubsubChannels";
7
+
8
+ export function useDebugBeat() {
9
+ const debugBeatColor = ref<string | null>(null);
10
+ const debugTitleStyle = computed<CSSProperties>(() => (debugBeatColor.value ? { color: debugBeatColor.value } : {}));
11
+
12
+ const { subscribe } = usePubSub();
13
+ subscribe(PUBSUB_CHANNELS.debugBeat, (data) => {
14
+ const msg = data as { count: number; last?: boolean };
15
+ if (msg.last) {
16
+ debugBeatColor.value = null;
17
+ } else {
18
+ debugBeatColor.value = msg.count % 2 === 0 ? "#3b82f6" : "#ef4444";
19
+ }
20
+ });
21
+
22
+ return { debugTitleStyle };
23
+ }
@@ -0,0 +1,115 @@
1
+ // Dynamic favicon that changes color based on agent state (#470).
2
+ //
3
+ // Uses Canvas API to draw a rounded-square icon with the letter "M"
4
+ // in the center. Color reflects the current state:
5
+ // idle (gray) → running (blue, pulse) → done (green) → error (red)
6
+ // notification badge (orange dot) overlaid when unread count > 0.
7
+
8
+ import { watch, type Ref, type ComputedRef } from "vue";
9
+
10
+ export const FAVICON_STATES = {
11
+ idle: "idle",
12
+ running: "running",
13
+ done: "done",
14
+ error: "error",
15
+ } as const;
16
+
17
+ export type FaviconState = (typeof FAVICON_STATES)[keyof typeof FAVICON_STATES];
18
+
19
+ const STATE_COLORS: Record<FaviconState, string> = {
20
+ idle: "#6B7280", // gray-500
21
+ running: "#3B82F6", // blue-500
22
+ done: "#22C55E", // green-500
23
+ error: "#EF4444", // red-500
24
+ };
25
+
26
+ const NOTIFICATION_DOT_COLOR = "#F97316"; // orange-500
27
+ const SIZE = 32;
28
+ const RADIUS = 6;
29
+
30
+ function drawRoundedRect(ctx: CanvasRenderingContext2D, posX: number, posY: number, width: number, height: number, radius: number): void {
31
+ ctx.beginPath();
32
+ ctx.moveTo(posX + radius, posY);
33
+ ctx.lineTo(posX + width - radius, posY);
34
+ ctx.quadraticCurveTo(posX + width, posY, posX + width, posY + radius);
35
+ ctx.lineTo(posX + width, posY + height - radius);
36
+ ctx.quadraticCurveTo(posX + width, posY + height, posX + width - radius, posY + height);
37
+ ctx.lineTo(posX + radius, posY + height);
38
+ ctx.quadraticCurveTo(posX, posY + height, posX, posY + height - radius);
39
+ ctx.lineTo(posX, posY + radius);
40
+ ctx.quadraticCurveTo(posX, posY, posX + radius, posY);
41
+ ctx.closePath();
42
+ }
43
+
44
+ function renderFavicon(state: FaviconState, hasNotification: boolean): string {
45
+ const canvas = document.createElement("canvas");
46
+ canvas.width = SIZE;
47
+ canvas.height = SIZE;
48
+ const ctx = canvas.getContext("2d");
49
+ if (!ctx) return "";
50
+
51
+ // Background: rounded square
52
+ const color = STATE_COLORS[state];
53
+ drawRoundedRect(ctx, 1, 1, SIZE - 2, SIZE - 2, RADIUS);
54
+ ctx.fillStyle = color;
55
+ ctx.fill();
56
+
57
+ // Subtle shadow/depth
58
+ ctx.strokeStyle = "rgba(0,0,0,0.15)";
59
+ ctx.lineWidth = 1;
60
+ drawRoundedRect(ctx, 1, 1, SIZE - 2, SIZE - 2, RADIUS);
61
+ ctx.stroke();
62
+
63
+ // "M" letter
64
+ ctx.fillStyle = "white";
65
+ ctx.font = "bold 20px -apple-system, BlinkMacSystemFont, sans-serif";
66
+ ctx.textAlign = "center";
67
+ ctx.textBaseline = "middle";
68
+ ctx.fillText("M", SIZE / 2, SIZE / 2 + 1);
69
+
70
+ // Running state: subtle glow ring
71
+ if (state === FAVICON_STATES.running) {
72
+ ctx.strokeStyle = "rgba(255,255,255,0.4)";
73
+ ctx.lineWidth = 2;
74
+ drawRoundedRect(ctx, 3, 3, SIZE - 6, SIZE - 6, RADIUS - 1);
75
+ ctx.stroke();
76
+ }
77
+
78
+ // Notification badge (orange dot, top-right)
79
+ if (hasNotification) {
80
+ const dotR = 5;
81
+ const dotX = SIZE - dotR - 1;
82
+ const dotY = dotR + 1;
83
+ ctx.beginPath();
84
+ ctx.arc(dotX, dotY, dotR, 0, Math.PI * 2);
85
+ ctx.fillStyle = NOTIFICATION_DOT_COLOR;
86
+ ctx.fill();
87
+ ctx.strokeStyle = "white";
88
+ ctx.lineWidth = 1.5;
89
+ ctx.stroke();
90
+ }
91
+
92
+ return canvas.toDataURL("image/png");
93
+ }
94
+
95
+ function applyFavicon(dataUrl: string): void {
96
+ if (!dataUrl) return;
97
+ let link = document.querySelector("link[rel='icon']") as HTMLLinkElement | null;
98
+ if (!link) {
99
+ link = document.createElement("link");
100
+ link.rel = "icon";
101
+ link.type = "image/png";
102
+ document.head.appendChild(link);
103
+ }
104
+ link.type = "image/png";
105
+ link.href = dataUrl;
106
+ }
107
+
108
+ export function useDynamicFavicon(opts: { state: Ref<FaviconState> | ComputedRef<FaviconState>; hasNotification: Ref<boolean> | ComputedRef<boolean> }): void {
109
+ function update(): void {
110
+ const dataUrl = renderFavicon(opts.state.value, opts.hasNotification.value);
111
+ applyFavicon(dataUrl);
112
+ }
113
+
114
+ watch([opts.state, opts.hasNotification], update, { immediate: true });
115
+ }
@@ -0,0 +1,42 @@
1
+ // Composable that wires the window-level event listeners used by
2
+ // App.vue (click-outside handlers for 3 popups + global keydown for
3
+ // navigation + view-mode shortcuts) and tears them down on unmount.
4
+ //
5
+ // Plugin → App.vue communication used to live here too via
6
+ // `roles-updated` / `skill-run` CustomEvents on `window`. That now
7
+ // flows through `useAppApi` (provide/inject) — see #227. Anything
8
+ // remaining in this composable is genuinely a window-level concern
9
+ // (keyboard / mouse events that don't have a single "owning"
10
+ // component).
11
+ //
12
+ // Each listener is supplied as an option so the composable stays
13
+ // independent of App.vue's local state; the caller passes the
14
+ // already-bound handlers.
15
+
16
+ import { onMounted, onUnmounted } from "vue";
17
+
18
+ export interface EventListenerHandlers {
19
+ /** Global keydown for arrow-key navigation / Esc handling. */
20
+ onKeyNavigation: (e: KeyboardEvent) => void;
21
+ /** Global keydown for Cmd/Ctrl+1/2/3 view-mode shortcut. */
22
+ onViewModeShortcut: (e: KeyboardEvent) => void;
23
+ /** mousedown click-outside handlers for each popup. */
24
+ onClickOutsideHistory: (e: MouseEvent) => void;
25
+ /** Called in onUnmounted after all window listeners are removed. */
26
+ onTeardown?: () => void;
27
+ }
28
+
29
+ export function useEventListeners(handlers: EventListenerHandlers): void {
30
+ onMounted(() => {
31
+ window.addEventListener("keydown", handlers.onKeyNavigation);
32
+ window.addEventListener("keydown", handlers.onViewModeShortcut);
33
+ window.addEventListener("mousedown", handlers.onClickOutsideHistory);
34
+ });
35
+
36
+ onUnmounted(() => {
37
+ window.removeEventListener("keydown", handlers.onKeyNavigation);
38
+ window.removeEventListener("keydown", handlers.onViewModeShortcut);
39
+ window.removeEventListener("mousedown", handlers.onClickOutsideHistory);
40
+ handlers.onTeardown?.();
41
+ });
42
+ }
@@ -0,0 +1,64 @@
1
+ // Composable for the FileTree expand/collapse state. Owns a
2
+ // module-level reactive Set so every recursive FileTree instance
3
+ // shares the same state, and persists changes to localStorage.
4
+ //
5
+ // The pure parsing helpers live in src/utils/files/expandedDirs.ts
6
+ // so the rules are unit-testable without a Vue runtime.
7
+
8
+ import { ref, watch, type Ref } from "vue";
9
+ import { EXPANDED_DIRS_STORAGE_KEY, parseStoredExpandedDirs, serializeExpandedDirs } from "../utils/files/expandedDirs";
10
+
11
+ function loadInitial(): Set<string> {
12
+ if (typeof localStorage === "undefined") {
13
+ return parseStoredExpandedDirs(null);
14
+ }
15
+ try {
16
+ return parseStoredExpandedDirs(localStorage.getItem(EXPANDED_DIRS_STORAGE_KEY));
17
+ } catch {
18
+ return parseStoredExpandedDirs(null);
19
+ }
20
+ }
21
+
22
+ // Module-level singleton: every FileTree instance imports this
23
+ // composable and reads/writes the same Set.
24
+ const expandedDirs: Ref<Set<string>> = ref(loadInitial());
25
+
26
+ watch(expandedDirs, (val) => {
27
+ try {
28
+ localStorage.setItem(EXPANDED_DIRS_STORAGE_KEY, serializeExpandedDirs(val));
29
+ } catch {
30
+ // localStorage may be disabled (private mode) or full; ignore
31
+ // and keep the in-memory state working for this session.
32
+ }
33
+ });
34
+
35
+ export function useExpandedDirs(): {
36
+ isExpanded: (path: string) => boolean;
37
+ toggle: (path: string) => void;
38
+ expand: (path: string) => void;
39
+ expandedPaths: () => string[];
40
+ } {
41
+ function isExpanded(path: string): boolean {
42
+ return expandedDirs.value.has(path);
43
+ }
44
+ function toggle(path: string): void {
45
+ // Replace the Set so the watch fires — Set mutations aren't
46
+ // tracked by Vue's reactivity.
47
+ const next = new Set(expandedDirs.value);
48
+ if (next.has(path)) next.delete(path);
49
+ else next.add(path);
50
+ expandedDirs.value = next;
51
+ }
52
+ // Idempotently mark a path as expanded (used by deep-link auto-
53
+ // expand where we want to reveal ancestors without toggling).
54
+ function expand(path: string): void {
55
+ if (expandedDirs.value.has(path)) return;
56
+ const next = new Set(expandedDirs.value);
57
+ next.add(path);
58
+ expandedDirs.value = next;
59
+ }
60
+ function expandedPaths(): string[] {
61
+ return Array.from(expandedDirs.value);
62
+ }
63
+ return { isExpanded, toggle, expand, expandedPaths };
64
+ }
@@ -0,0 +1,30 @@
1
+ // Dynamic favicon state: running → done (unread) → idle.
2
+ // Also drives the notification badge dot on the favicon.
3
+
4
+ import { computed, type ComputedRef } from "vue";
5
+ import { FAVICON_STATES, type FaviconState, useDynamicFavicon } from "./useDynamicFavicon";
6
+ import { useNotifications } from "./useNotifications";
7
+ import type { ActiveSession, SessionSummary } from "../types/session";
8
+
9
+ export function useFaviconState(opts: {
10
+ isRunning: ComputedRef<boolean>;
11
+ currentSummary: ComputedRef<SessionSummary | undefined>;
12
+ activeSession: ComputedRef<ActiveSession | undefined>;
13
+ }) {
14
+ const { isRunning, currentSummary, activeSession } = opts;
15
+
16
+ const faviconState = computed<FaviconState>(() => {
17
+ if (isRunning.value) return FAVICON_STATES.running;
18
+ const hasUnread = currentSummary.value?.hasUnread ?? activeSession.value?.hasUnread ?? false;
19
+ if (hasUnread) return FAVICON_STATES.done;
20
+ return FAVICON_STATES.idle;
21
+ });
22
+
23
+ const { unreadCount: notificationUnreadCount } = useNotifications();
24
+ const hasNotificationBadge = computed(() => notificationUnreadCount.value > 0);
25
+
26
+ useDynamicFavicon({
27
+ state: faviconState,
28
+ hasNotification: hasNotificationBadge,
29
+ });
30
+ }