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
Binary file
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <button
3
+ class="flex items-center justify-center w-8 h-8 rounded transition-colors hover:bg-gray-100"
4
+ :class="isStack ? 'text-blue-500' : 'text-gray-400 hover:text-gray-700'"
5
+ :title="isStack ? 'Stack view · click to switch to Single (⌘1)' : 'Single view · click to switch to Stack (⌘2)'"
6
+ :aria-label="isStack ? 'Switch to Single view' : 'Switch to Stack view'"
7
+ :data-testid="`canvas-view-toggle-${modelValue}`"
8
+ @click="emit('update:modelValue', isStack ? CANVAS_VIEW.single : CANVAS_VIEW.stack)"
9
+ >
10
+ <span class="material-icons text-lg">view_agenda</span>
11
+ </button>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { computed } from "vue";
16
+ import { CANVAS_VIEW, type CanvasViewMode } from "../utils/canvas/viewMode";
17
+
18
+ const props = defineProps<{
19
+ modelValue: CanvasViewMode;
20
+ }>();
21
+
22
+ const emit = defineEmits<{
23
+ "update:modelValue": [mode: CanvasViewMode];
24
+ }>();
25
+
26
+ const isStack = computed(() => props.modelValue === CANVAS_VIEW.stack);
27
+ </script>
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <div data-testid="chat-attachment-preview" class="relative inline-flex items-center gap-2 border border-gray-300 rounded overflow-hidden px-2 py-1">
3
+ <img v-if="isImage" :src="dataUrl" alt="Attached image" class="max-h-20 max-w-40 object-contain" />
4
+ <div v-else class="flex items-center gap-1.5 text-xs text-gray-700">
5
+ <span class="material-icons text-base" :class="iconColor">{{ icon }}</span>
6
+ <span class="max-w-40 truncate">{{ filename || "attachment" }}</span>
7
+ </div>
8
+ <button
9
+ data-testid="chat-attachment-remove"
10
+ class="absolute top-0 right-0 bg-black/60 text-white rounded-bl px-1 text-xs leading-tight hover:bg-black/80"
11
+ @click="emit('remove')"
12
+ >
13
+
14
+ </button>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { computed } from "vue";
20
+
21
+ const props = defineProps<{
22
+ dataUrl: string;
23
+ filename: string;
24
+ mime: string;
25
+ }>();
26
+ const emit = defineEmits<{ remove: [] }>();
27
+
28
+ const isImage = computed(() => props.mime.startsWith("image/"));
29
+
30
+ const icon = computed(() => {
31
+ if (props.mime === "application/pdf") return "picture_as_pdf";
32
+ if (props.mime.includes("wordprocessingml")) return "description";
33
+ if (props.mime.includes("spreadsheetml")) return "table_chart";
34
+ if (props.mime.includes("presentationml")) return "slideshow";
35
+ return "insert_drive_file";
36
+ });
37
+
38
+ const iconColor = computed(() => {
39
+ if (props.mime === "application/pdf") return "text-red-500";
40
+ if (props.mime.includes("wordprocessingml")) return "text-blue-500";
41
+ if (props.mime.includes("spreadsheetml")) return "text-green-600";
42
+ if (props.mime.includes("presentationml")) return "text-orange-500";
43
+ return "text-gray-500";
44
+ });
45
+ </script>
@@ -0,0 +1,17 @@
1
+ <template>
2
+ <div data-testid="chat-image-preview" class="relative inline-block border border-gray-300 rounded overflow-hidden">
3
+ <img :src="src" alt="Attached image" class="max-h-20 max-w-40 object-contain" />
4
+ <button
5
+ data-testid="chat-image-remove"
6
+ class="absolute top-0 right-0 bg-black/60 text-white rounded-bl px-1 text-xs leading-tight hover:bg-black/80"
7
+ @click="emit('remove')"
8
+ >
9
+
10
+ </button>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ defineProps<{ src: string }>();
16
+ const emit = defineEmits<{ remove: [] }>();
17
+ </script>
@@ -0,0 +1,208 @@
1
+ <template>
2
+ <div class="p-4 border-t border-gray-200" @dragover.prevent @drop="onDropFile">
3
+ <div v-if="fileError" class="mb-2 text-xs text-red-600 bg-red-50 border border-red-200 rounded px-3 py-1.5" data-testid="file-error">
4
+ {{ fileError }}
5
+ </div>
6
+ <ChatAttachmentPreview
7
+ v-if="pastedFile"
8
+ :data-url="pastedFile.dataUrl"
9
+ :filename="pastedFile.name"
10
+ :mime="pastedFile.mime"
11
+ @remove="emit('update:pastedFile', null)"
12
+ />
13
+ <div class="flex gap-2" :class="{ 'mt-2': pastedFile }">
14
+ <textarea
15
+ ref="textarea"
16
+ :value="modelValue"
17
+ data-testid="user-input"
18
+ placeholder="Type a task..."
19
+ :rows="inputFocused ? 8 : 2"
20
+ class="flex-1 bg-white border border-gray-300 rounded px-3 py-2 text-sm text-gray-900 placeholder-gray-400 disabled:opacity-50 disabled:cursor-not-allowed resize-none transition-all duration-200"
21
+ :class="inputFocused ? 'ring-2 ring-blue-300' : ''"
22
+ :disabled="isRunning"
23
+ @input="emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
24
+ @focus="inputFocused = true"
25
+ @compositionstart="imeEnter.onCompositionStart"
26
+ @compositionend="imeEnter.onCompositionEnd"
27
+ @keydown="imeEnter.onKeydown"
28
+ @blur="onInputBlur"
29
+ @paste="onPasteFile"
30
+ />
31
+ <div class="flex flex-col gap-1">
32
+ <button
33
+ data-testid="send-btn"
34
+ class="bg-blue-600 hover:bg-blue-700 text-white rounded px-3 py-2 text-sm disabled:opacity-50 disabled:cursor-not-allowed"
35
+ :disabled="isRunning"
36
+ @click="emit('send')"
37
+ >
38
+ <span class="material-icons text-base">send</span>
39
+ </button>
40
+ <button
41
+ data-testid="expand-input-btn"
42
+ class="text-gray-400 hover:text-gray-600 rounded px-3 py-1 text-sm"
43
+ title="Expand editor"
44
+ @click="openExpandedEditor"
45
+ >
46
+ <span class="material-icons text-base">open_in_full</span>
47
+ </button>
48
+ </div>
49
+ </div>
50
+
51
+ <div v-if="expandedEditorOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40" @click.self="closeExpandedEditor">
52
+ <div class="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4 flex flex-col" style="max-height: 80vh">
53
+ <div class="flex items-center justify-between px-4 py-3 border-b border-gray-200">
54
+ <h3 class="text-sm font-semibold text-gray-700">Compose message</h3>
55
+ <button class="text-gray-400 hover:text-gray-600" @click="closeExpandedEditor">
56
+ <span class="material-icons text-base">close</span>
57
+ </button>
58
+ </div>
59
+ <textarea
60
+ ref="expandedTextarea"
61
+ :value="modelValue"
62
+ data-testid="expanded-input"
63
+ placeholder="Type a task..."
64
+ class="flex-1 px-4 py-3 text-sm text-gray-900 placeholder-gray-400 resize-none focus:outline-none"
65
+ style="min-height: 300px"
66
+ @input="emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
67
+ @keydown.meta.enter="sendFromExpanded"
68
+ @keydown.ctrl.enter="sendFromExpanded"
69
+ ></textarea>
70
+ <div class="flex items-center justify-between px-4 py-3 border-t border-gray-200">
71
+ <p class="text-xs text-gray-400">Cmd+Enter to send</p>
72
+ <div class="flex gap-2">
73
+ <button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="closeExpandedEditor">Cancel</button>
74
+ <button
75
+ class="px-3 py-1.5 text-sm rounded bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-40"
76
+ :disabled="isRunning"
77
+ data-testid="expanded-send-btn"
78
+ @click="sendFromExpanded"
79
+ >
80
+ Send
81
+ </button>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </template>
88
+
89
+ <script setup lang="ts">
90
+ import { nextTick, ref } from "vue";
91
+ import ChatAttachmentPreview from "./ChatAttachmentPreview.vue";
92
+ import { useImeAwareEnter } from "../composables/useImeAwareEnter";
93
+
94
+ export interface PastedFile {
95
+ dataUrl: string;
96
+ name: string;
97
+ mime: string;
98
+ }
99
+
100
+ const props = defineProps<{
101
+ modelValue: string;
102
+ pastedFile: PastedFile | null;
103
+ isRunning: boolean;
104
+ }>();
105
+
106
+ const emit = defineEmits<{
107
+ "update:modelValue": [value: string];
108
+ "update:pastedFile": [file: PastedFile | null];
109
+ send: [];
110
+ }>();
111
+
112
+ const textarea = ref<HTMLTextAreaElement | null>(null);
113
+ const expandedTextarea = ref<HTMLTextAreaElement | null>(null);
114
+ const inputFocused = ref(false);
115
+ const expandedEditorOpen = ref(false);
116
+ const fileError = ref<string | null>(null);
117
+
118
+ const MAX_ATTACH_BYTES = 30 * 1024 * 1024;
119
+
120
+ const ACCEPTED_MIME_PREFIXES = ["image/", "text/"];
121
+ const ACCEPTED_MIME_EXACT = new Set([
122
+ "application/pdf",
123
+ "application/json",
124
+ "application/xml",
125
+ "application/x-yaml",
126
+ "application/toml",
127
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
128
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
129
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
130
+ ]);
131
+
132
+ function isAcceptedType(mime: string): boolean {
133
+ return ACCEPTED_MIME_PREFIXES.some((p) => mime.startsWith(p)) || ACCEPTED_MIME_EXACT.has(mime);
134
+ }
135
+
136
+ function readAttachmentFile(file: File): void {
137
+ fileError.value = null;
138
+ if (!isAcceptedType(file.type)) return;
139
+ if (file.size > MAX_ATTACH_BYTES) {
140
+ const sizeMB = (file.size / 1024 / 1024).toFixed(1);
141
+ fileError.value = `File too large (${sizeMB} MB). Maximum is 30 MB.`;
142
+ return;
143
+ }
144
+ const reader = new FileReader();
145
+ reader.onload = () => {
146
+ if (typeof reader.result === "string") {
147
+ emit("update:pastedFile", {
148
+ dataUrl: reader.result,
149
+ name: file.name,
150
+ mime: file.type,
151
+ });
152
+ }
153
+ };
154
+ reader.readAsDataURL(file);
155
+ }
156
+
157
+ function onPasteFile(e: ClipboardEvent): void {
158
+ const items = e.clipboardData?.items;
159
+ if (!items) return;
160
+ for (const item of items) {
161
+ if (isAcceptedType(item.type)) {
162
+ const file = item.getAsFile();
163
+ if (file) {
164
+ e.preventDefault();
165
+ readAttachmentFile(file);
166
+ return;
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ function onDropFile(e: DragEvent): void {
173
+ e.preventDefault();
174
+ const file = e.dataTransfer?.files[0];
175
+ if (file) readAttachmentFile(file);
176
+ }
177
+
178
+ const imeEnter = useImeAwareEnter(() => emit("send"));
179
+
180
+ function onInputBlur(): void {
181
+ imeEnter.onBlur();
182
+ setTimeout(() => {
183
+ inputFocused.value = false;
184
+ }, 150);
185
+ }
186
+
187
+ function openExpandedEditor(): void {
188
+ expandedEditorOpen.value = true;
189
+ nextTick(() => expandedTextarea.value?.focus());
190
+ }
191
+
192
+ function closeExpandedEditor(): void {
193
+ expandedEditorOpen.value = false;
194
+ nextTick(() => textarea.value?.focus());
195
+ }
196
+
197
+ function sendFromExpanded(): void {
198
+ if (props.isRunning) return;
199
+ closeExpandedEditor();
200
+ emit("send");
201
+ }
202
+
203
+ function focus(): void {
204
+ textarea.value?.focus();
205
+ }
206
+
207
+ defineExpose({ focus });
208
+ </script>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <div v-if="selectedPath" class="px-4 py-2 border-b border-gray-200 text-xs text-gray-500 font-mono shrink-0 flex items-center gap-2">
3
+ <span class="truncate min-w-0">{{ selectedPath }}</span>
4
+ <span v-if="size !== null" class="text-gray-400 shrink-0">· {{ formatBytes(size) }}</span>
5
+ <span v-if="modifiedMs !== null" class="text-gray-400 shrink-0">· {{ formatDateTime(modifiedMs) }}</span>
6
+ <button
7
+ v-if="isMarkdown"
8
+ class="ml-auto shrink-0 px-2 py-0.5 rounded border border-gray-200 text-gray-600 hover:bg-gray-100 font-sans"
9
+ :title="mdRawMode ? 'Show rendered Markdown' : 'Show raw source'"
10
+ @click="emit('toggleMdRaw')"
11
+ >
12
+ {{ mdRawMode ? "Rendered" : "Raw" }}
13
+ </button>
14
+ <button
15
+ type="button"
16
+ class="shrink-0 px-1 py-0.5 rounded text-gray-400 hover:text-gray-700 hover:bg-gray-100"
17
+ :class="{ 'ml-auto': !isMarkdown }"
18
+ title="Close file"
19
+ aria-label="Close file"
20
+ data-testid="close-file-btn"
21
+ @click="emit('deselect')"
22
+ >
23
+ <span class="material-icons text-base" aria-hidden="true">close</span>
24
+ </button>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { formatDateTime } from "../utils/format/date";
30
+
31
+ defineProps<{
32
+ selectedPath: string | null;
33
+ size: number | null;
34
+ modifiedMs: number | null;
35
+ isMarkdown: boolean;
36
+ mdRawMode: boolean;
37
+ }>();
38
+
39
+ const emit = defineEmits<{
40
+ toggleMdRaw: [];
41
+ deselect: [];
42
+ }>();
43
+
44
+ function formatBytes(bytes: number): string {
45
+ if (bytes < 1024) return `${bytes} B`;
46
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
47
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
48
+ }
49
+ </script>
@@ -0,0 +1,162 @@
1
+ <template>
2
+ <div class="flex-1 overflow-auto min-h-0">
3
+ <div v-if="!selectedPath" class="h-full flex items-center justify-center text-gray-400 text-sm">Select a file</div>
4
+ <div v-else-if="contentError" class="p-4 text-sm text-red-600">
5
+ {{ contentError }}
6
+ </div>
7
+ <div v-else-if="contentLoading" class="p-4 text-sm text-gray-400">Loading...</div>
8
+ <template v-else-if="content">
9
+ <template v-if="content.kind === 'text'">
10
+ <!-- Scheduler items.json: render with the scheduler plugin's
11
+ calendar/list view by synthesizing a fake tool result. -->
12
+ <div v-if="schedulerResult" class="h-full">
13
+ <SchedulerView :selected-result="schedulerResult" />
14
+ </div>
15
+ <!-- Todos todos.json: full kanban / table / list explorer. -->
16
+ <div v-else-if="todoExplorerResult" class="h-full">
17
+ <TodoExplorer :selected-result="todoExplorerResult" />
18
+ </div>
19
+ <!-- Markdown rendered: frontmatter panel + body -->
20
+ <div v-else-if="isMarkdown && !mdRawMode" class="h-full flex flex-col overflow-auto">
21
+ <div v-if="mdFrontmatter && mdFrontmatter.fields.length > 0" class="shrink-0 m-4 mb-0 rounded border border-gray-200 bg-gray-50 p-3 text-xs">
22
+ <div v-for="field in mdFrontmatter.fields" :key="field.key" class="flex items-baseline gap-2 py-0.5">
23
+ <span class="font-semibold text-gray-600 shrink-0">{{ field.key }}:</span>
24
+ <template v-if="Array.isArray(field.value)">
25
+ <span class="flex flex-wrap gap-1">
26
+ <span v-for="item in field.value" :key="item" class="rounded-full bg-white border border-gray-300 px-2 py-0.5 text-gray-700">
27
+ {{ item }}
28
+ </span>
29
+ </span>
30
+ </template>
31
+ <span v-else class="text-gray-800 break-words">{{ field.value }}</span>
32
+ </div>
33
+ </div>
34
+ <div class="flex-1 min-h-0" @click.capture="(e: MouseEvent) => emit('markdownLinkClick', e)">
35
+ <TextResponseView
36
+ :selected-result="markdownResult(mdFrontmatter ? mdFrontmatter.body : content.content)"
37
+ :editable-source="content.content"
38
+ @update-source="(src: string) => emit('updateSource', src)"
39
+ />
40
+ </div>
41
+ <div v-if="rawSaveError" class="shrink-0 m-4 mt-0 rounded border border-red-300 bg-red-50 p-2 text-xs text-red-700" role="alert">
42
+ ⚠ {{ rawSaveError }}
43
+ </div>
44
+ </div>
45
+ <!-- Markdown raw source (includes frontmatter) -->
46
+ <pre v-else-if="isMarkdown && mdRawMode" class="p-4 text-xs whitespace-pre-wrap font-mono text-gray-800">{{ content.content }}</pre>
47
+ <!-- HTML: sandboxed iframe preview.
48
+ `allow-scripts` lets Chart.js / canvas drawing / other
49
+ JS-driven HTML (the common case for LLM-generated
50
+ results) run. We deliberately DO NOT grant
51
+ `allow-same-origin`, so the iframe keeps a null
52
+ origin — it can't read MulmoClaude's cookies,
53
+ localStorage, or the parent window's DOM.
54
+ A CSP meta tag is injected via wrapHtmlWithPreviewCsp
55
+ to restrict script loads to a vetted CDN whitelist +
56
+ inline; connect-src is `'none'` so the page can't
57
+ phone home. See src/utils/html/previewCsp.ts. -->
58
+ <iframe v-else-if="isHtml" :srcdoc="sandboxedHtml" class="w-full h-full border-0" sandbox="allow-scripts" title="HTML preview" />
59
+ <!-- JSON: pretty-printed with simple syntax coloring. Fall
60
+ back to raw content if the file is malformed. -->
61
+ <pre v-else-if="isJson" class="p-4 text-xs whitespace-pre-wrap font-mono text-gray-800"><span
62
+ v-for="(tok, i) in jsonTokens"
63
+ :key="i"
64
+ :class="JSON_TOKEN_CLASS[tok.type]"
65
+ >{{ tok.value }}</span></pre>
66
+ <!-- JSONL / NDJSON: one pretty-printed + colored record per line -->
67
+ <div v-else-if="isJsonl" class="p-4 space-y-2">
68
+ <div v-for="(line, i) in jsonlLines" :key="i" class="rounded border bg-gray-50 p-3" :class="line.parseError ? 'border-red-300' : 'border-gray-200'">
69
+ <div v-if="line.parseError" class="text-xs text-red-600 mb-1 font-sans">parse error</div>
70
+ <pre class="text-xs font-mono text-gray-800 whitespace-pre-wrap"><span
71
+ v-for="(tok, j) in line.tokens"
72
+ :key="j"
73
+ :class="JSON_TOKEN_CLASS[tok.type]"
74
+ >{{ tok.value }}</span></pre>
75
+ </div>
76
+ </div>
77
+ <!-- Plain text fallback -->
78
+ <pre v-else class="p-4 text-xs whitespace-pre-wrap font-mono text-gray-800">{{ content.content }}</pre>
79
+ </template>
80
+ <!-- Image -->
81
+ <div v-else-if="content.kind === 'image' && selectedPath" class="h-full flex items-center justify-center p-4">
82
+ <img :src="rawUrl(selectedPath)" :alt="selectedPath" class="max-w-full max-h-full object-contain" />
83
+ </div>
84
+ <!-- PDF -->
85
+ <iframe v-else-if="content.kind === 'pdf' && selectedPath" :src="rawUrl(selectedPath)" class="w-full h-full border-0" title="PDF preview" />
86
+ <!-- Audio -->
87
+ <div v-else-if="content.kind === 'audio' && selectedPath" class="h-full flex items-center justify-center p-4">
88
+ <audio :key="selectedPath" :src="rawUrl(selectedPath)" controls preload="metadata" class="w-full max-w-2xl" />
89
+ </div>
90
+ <!-- Video -->
91
+ <div v-else-if="content.kind === 'video' && selectedPath" class="h-full flex items-center justify-center p-4 bg-black">
92
+ <video :key="selectedPath" :src="rawUrl(selectedPath)" controls preload="metadata" class="max-w-full max-h-full" />
93
+ </div>
94
+ <!-- Binary or too-large -->
95
+ <div v-else class="p-4 text-sm text-gray-500">
96
+ {{ "message" in content ? content.message : "" }}
97
+ </div>
98
+ </template>
99
+ </div>
100
+ </template>
101
+
102
+ <script setup lang="ts">
103
+ import TextResponseView from "../plugins/textResponse/View.vue";
104
+ import SchedulerView from "../plugins/scheduler/View.vue";
105
+ import TodoExplorer from "./TodoExplorer.vue";
106
+ import type { FileContent } from "../composables/useFileSelection";
107
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
108
+ import type { TextResponseData } from "../plugins/textResponse/types";
109
+ import type { SchedulerData } from "../plugins/scheduler/index";
110
+ import type { TodoData } from "../plugins/todo/index";
111
+ import { JSON_TOKEN_CLASS } from "../utils/format/jsonSyntax";
112
+ import type { JsonToken, JsonlLine } from "../utils/format/jsonSyntax";
113
+ import type { Frontmatter } from "../utils/format/frontmatter";
114
+ import { rewriteMarkdownImageRefs } from "../utils/image/rewriteMarkdownImageRefs";
115
+ import { API_ROUTES } from "../config/apiRoutes";
116
+
117
+ const props = defineProps<{
118
+ selectedPath: string | null;
119
+ content: FileContent | null;
120
+ contentError: string | null;
121
+ contentLoading: boolean;
122
+ schedulerResult: ToolResultComplete<SchedulerData> | null;
123
+ todoExplorerResult: ToolResultComplete<TodoData> | null;
124
+ isMarkdown: boolean;
125
+ isHtml: boolean;
126
+ isJson: boolean;
127
+ isJsonl: boolean;
128
+ mdRawMode: boolean;
129
+ sandboxedHtml: string;
130
+ jsonTokens: JsonToken[];
131
+ jsonlLines: JsonlLine[];
132
+ mdFrontmatter: Frontmatter | null;
133
+ rawSaveError: string | null;
134
+ }>();
135
+
136
+ const emit = defineEmits<{
137
+ markdownLinkClick: [event: MouseEvent];
138
+ updateSource: [newSource: string];
139
+ }>();
140
+
141
+ function rawUrl(filePath: string): string {
142
+ return `${API_ROUTES.files.raw}?path=${encodeURIComponent(filePath)}`;
143
+ }
144
+
145
+ function markdownResult(text: string): ToolResultComplete<TextResponseData> {
146
+ // Rewrite `![alt](path)` refs BEFORE handing the markdown to
147
+ // TextResponseView so workspace-relative image paths resolve via
148
+ // /api/files/raw instead of 404-ing against the SPA page URL.
149
+ const current = props.selectedPath ?? "";
150
+ const slash = current.lastIndexOf("/");
151
+ const basePath = slash >= 0 ? current.slice(0, slash) : "";
152
+ const rewritten = rewriteMarkdownImageRefs(text, basePath);
153
+ return {
154
+ uuid: "files-preview",
155
+ toolName: "text-response",
156
+ message: rewritten,
157
+ title: props.selectedPath ?? "",
158
+ // role: "user" hides the PDF download button in TextResponseView
159
+ data: { text: rewritten, role: "user", transportKind: "text-rest" },
160
+ };
161
+ }
162
+ </script>
@@ -0,0 +1,115 @@
1
+ <template>
2
+ <div>
3
+ <button
4
+ v-if="node.type === 'dir'"
5
+ class="w-full flex items-center gap-1 px-2 py-1 text-left text-sm hover:bg-gray-100 rounded"
6
+ :data-testid="`file-tree-dir-${node.name || 'root'}`"
7
+ @click="onToggle"
8
+ >
9
+ <span class="material-icons text-sm text-gray-400 shrink-0">{{ expanded ? "folder_open" : "folder" }}</span>
10
+ <span class="text-gray-700 truncate">{{ node.name || "(workspace)" }}</span>
11
+ </button>
12
+ <button
13
+ v-else
14
+ class="w-full flex items-center gap-1 px-2 py-1 text-left text-sm rounded transition-colors"
15
+ :class="selectedPath === node.path ? 'bg-blue-100 text-blue-700' : 'text-gray-700 hover:bg-gray-100'"
16
+ :data-testid="`file-tree-file-${node.name}`"
17
+ :title="node.path"
18
+ @click="emit('select', node.path)"
19
+ >
20
+ <span class="material-icons text-sm text-gray-400 shrink-0">description</span>
21
+ <span class="truncate">{{ node.name }}</span>
22
+ <span v-if="isRecent" class="ml-auto w-1.5 h-1.5 rounded-full bg-green-500 shrink-0" title="Recently changed" />
23
+ </button>
24
+ <div v-if="node.type === 'dir' && expanded" class="pl-4">
25
+ <!-- Loading state: children not in the cache yet. Rendered
26
+ once per dir so a slow network shows where the wait is,
27
+ not as a global overlay. -->
28
+ <div v-if="loadingChildren" class="px-2 py-1 text-xs text-gray-400">Loading...</div>
29
+ <FileTree
30
+ v-for="child in loadedChildren"
31
+ :key="child.path"
32
+ :node="child"
33
+ :selected-path="selectedPath"
34
+ :recent-paths="recentPaths"
35
+ :children-by-path="childrenByPath"
36
+ :sort-mode="sortMode"
37
+ @select="(p) => emit('select', p)"
38
+ @load-children="(p) => emit('loadChildren', p)"
39
+ />
40
+ </div>
41
+ </div>
42
+ </template>
43
+
44
+ <script setup lang="ts">
45
+ import { computed, watch } from "vue";
46
+ import { useExpandedDirs } from "../composables/useExpandedDirs";
47
+ import { sortChildren } from "../utils/files/sortChildren";
48
+ import type { FileSortMode } from "../composables/useFileSortMode";
49
+
50
+ // TreeNode lives in src/types/fileTree.ts so .ts composables can
51
+ // import it without depending on a .vue module. Re-export here so
52
+ // existing `import { TreeNode } from "./FileTree.vue"` keeps working.
53
+ export type { TreeNode } from "../types/fileTree";
54
+ import type { TreeNode } from "../types/fileTree";
55
+
56
+ const props = defineProps<{
57
+ node: TreeNode;
58
+ selectedPath: string | null;
59
+ recentPaths: Set<string>;
60
+ // Lazy-expand cache managed by the parent (FilesView). `undefined`
61
+ // entry (not in the map) = not loaded yet → we emit `loadChildren`
62
+ // so the parent kicks off the fetch. `null` = load in flight →
63
+ // show spinner. Array = loaded.
64
+ childrenByPath: Map<string, TreeNode[] | null>;
65
+ sortMode: FileSortMode;
66
+ }>();
67
+
68
+ const emit = defineEmits<{
69
+ select: [path: string];
70
+ loadChildren: [path: string];
71
+ }>();
72
+
73
+ // Expand/collapse state lives in a module-level singleton so every
74
+ // recursive FileTree instance shares it, and survives remounts (e.g.
75
+ // the agent-run refresh that bumps filesRefreshToken in FilesView).
76
+ // Default on first run: only the workspace root ("") is expanded.
77
+ const { isExpanded, toggle } = useExpandedDirs();
78
+ const expanded = computed(() => isExpanded(props.node.path));
79
+
80
+ const cached = computed(() => props.childrenByPath.get(props.node.path));
81
+ // `cached === null` = load in flight. `undefined` = never requested.
82
+ // Array = loaded.
83
+ const loadingChildren = computed(() => cached.value === null);
84
+ const loadedChildren = computed(() => (Array.isArray(cached.value) ? sortChildren(cached.value, props.sortMode) : []));
85
+
86
+ // Kick off a fetch if the dir is expanded but its children haven't
87
+ // been requested yet. Covers two scenarios:
88
+ // 1. User just toggled open → onToggle already emits, but watching
89
+ // here makes the flow idempotent when the parent re-mounts the
90
+ // component with expand state restored from localStorage.
91
+ // 2. Deep link: FilesView calls `expand(ancestor)` before children
92
+ // arrive; this watcher catches that case too.
93
+ watch(
94
+ [expanded, cached],
95
+ ([isOpen, current]) => {
96
+ if (!isOpen) return;
97
+ if (props.node.type !== "dir") return;
98
+ if (current !== undefined) return;
99
+ emit("loadChildren", props.node.path);
100
+ },
101
+ { immediate: true },
102
+ );
103
+
104
+ function onToggle(): void {
105
+ toggle(props.node.path);
106
+ // When newly-opened, request children if cache miss. The watcher
107
+ // above covers the reactive path but we also fire here so the
108
+ // request is visibly tied to the click for network inspection.
109
+ if (!isExpanded(props.node.path)) return;
110
+ if (cached.value !== undefined) return;
111
+ emit("loadChildren", props.node.path);
112
+ }
113
+
114
+ const isRecent = computed(() => props.recentPaths.has(props.node.path));
115
+ </script>