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,12 @@
1
+ // Pure helpers for role → plugin queries.
2
+ // Takes the role list as a parameter for testability.
3
+
4
+ import type { Role } from "../../config/roles";
5
+ import { TOOL_NAMES, type ToolName } from "../../config/toolNames";
6
+
7
+ const GEMINI_PLUGINS = new Set<ToolName>([TOOL_NAMES.generateImage, TOOL_NAMES.presentDocument]);
8
+
9
+ /** Whether the given role uses any plugin that requires a Gemini API key. */
10
+ export function needsGemini(roles: Role[], roleId: string): boolean {
11
+ return (roles.find((r) => r.id === roleId)?.availablePlugins ?? []).some((p) => GEMINI_PLUGINS.has(p));
12
+ }
@@ -0,0 +1,103 @@
1
+ // Pure helper: merge the two views of "sessions" that the sidebar
2
+ // history pane shows — live in-memory sessions (from `sessionMap`)
3
+ // and server-persisted summaries (from `/api/sessions`). Extracted
4
+ // from `src/App.vue` as part of the cognitive-complexity refactor
5
+ // tracked in #175.
6
+ //
7
+ // The merge is deterministic given the inputs: the test suite pins
8
+ // every edge case we've hit (live-only, server-only, overlap,
9
+ // server-side AI title vs local first-user-message, sort tie-breaks).
10
+
11
+ import { isUserTextResponse } from "../tools/result";
12
+ import type { SessionSummary, ActiveSession } from "../../types/session";
13
+
14
+ // Build the summary shape the sidebar expects for a single live
15
+ // session. Server-side data (AI-generated title, summary,
16
+ // keywords) takes precedence over the local first-user-message
17
+ // heuristic — otherwise opening an indexed session in a new tab
18
+ // would regress the sidebar row to the raw first message.
19
+ function buildLiveSummary(live: ActiveSession, serverEntry: SessionSummary | undefined): SessionSummary {
20
+ const firstUserMsg = live.toolResults.find(isUserTextResponse);
21
+ const preview = serverEntry?.preview || (firstUserMsg?.message ?? "");
22
+ const base: SessionSummary = {
23
+ id: live.id,
24
+ roleId: live.roleId,
25
+ startedAt: live.startedAt,
26
+ updatedAt: live.updatedAt,
27
+ preview,
28
+ };
29
+ // Fold every in-memory signal into isRunning so the sidebar spinner
30
+ // reacts as fast as the fastest source:
31
+ // - serverEntry.isRunning: authoritative but arrives on a /api/sessions
32
+ // refetch
33
+ // - live.isRunning: mirrored from the server via refreshSessionStates;
34
+ // may be ahead during the refetch window, and covers live-only
35
+ // sessions with no serverEntry yet
36
+ // - live.pendingGenerations: updates on the socket round-trip of a
37
+ // generationStarted event, before any REST refetch
38
+ // OR them so any one is enough. `live.isRunning` is always defined on
39
+ // an ActiveSession, so the summary always carries a boolean here.
40
+ const pending = live.pendingGenerations ?? {};
41
+ const isRunning = !!serverEntry?.isRunning || live.isRunning || Object.keys(pending).length > 0;
42
+ // Carry summary / keywords ONLY if the server already has them.
43
+ // Object-spread with a conditional object keeps us from adding
44
+ // `undefined` values that would otherwise show up as explicit
45
+ // `summary: undefined` in a later shallow-copy.
46
+ return {
47
+ ...base,
48
+ ...(serverEntry?.summary !== undefined && { summary: serverEntry.summary }),
49
+ ...(serverEntry?.keywords !== undefined && {
50
+ keywords: serverEntry.keywords,
51
+ }),
52
+ isRunning,
53
+ ...(serverEntry?.hasUnread !== undefined && {
54
+ hasUnread: serverEntry.hasUnread,
55
+ }),
56
+ ...(serverEntry?.statusMessage !== undefined && {
57
+ statusMessage: serverEntry.statusMessage,
58
+ }),
59
+ };
60
+ }
61
+
62
+ // Compare two summaries for sort order. Newest `updatedAt` wins;
63
+ // if updatedAt ties (same second-granularity mtime on two
64
+ // server-only rows, say), fall back to `startedAt`. Exported so
65
+ // tests can exercise the tie-break directly.
66
+ export function compareSessionsByRecency(left: SessionSummary, right: SessionSummary): number {
67
+ const byUpdated = Date.parse(right.updatedAt) - Date.parse(left.updatedAt);
68
+ if (byUpdated !== 0) return byUpdated;
69
+ return Date.parse(right.startedAt) - Date.parse(left.startedAt);
70
+ }
71
+
72
+ // Merge live sessions (in-memory, still editable) with server
73
+ // summaries (from /api/sessions). Live sessions always win for
74
+ // the same id — we prefer the local state we know is current —
75
+ // but pull over the server's AI title / summary / keywords when
76
+ // present. Pure, returns a new array; does not mutate inputs.
77
+ export function mergeSessionLists(liveSessions: readonly ActiveSession[], serverSessions: readonly SessionSummary[]): SessionSummary[] {
78
+ const liveIds = new Set(liveSessions.map((session) => session.id));
79
+ const serverById = new Map<string, SessionSummary>(serverSessions.map((session) => [session.id, session]));
80
+ const liveSummaries = liveSessions.map((live) => buildLiveSummary(live, serverById.get(live.id)));
81
+ const serverOnly = serverSessions.filter((session) => !liveIds.has(session.id));
82
+ return [...liveSummaries, ...serverOnly].sort(compareSessionsByRecency);
83
+ }
84
+
85
+ // Apply a server-sent diff to the client's cached session list
86
+ // (see issue #205). `diff` holds rows the server says have changed
87
+ // since the client's last cursor — each replaces any existing row
88
+ // with the same id, or is prepended if new. `deletedIds` removes
89
+ // rows the server has forgotten; always empty today (no
90
+ // session-delete code path exists) but the shape is plumbed through
91
+ // so populating it becomes a server-only change later.
92
+ //
93
+ // Pure; returns a new array sorted by the same recency rule
94
+ // `mergeSessionLists` uses, so the two are interchangeable at call
95
+ // sites that don't care which they got.
96
+ export function applySessionDiff(cache: readonly SessionSummary[], diff: readonly SessionSummary[], deletedIds: readonly string[]): SessionSummary[] {
97
+ const deleted = new Set(deletedIds);
98
+ const diffById = new Map<string, SessionSummary>(diff.map((session) => [session.id, session]));
99
+ const kept = cache.filter((session) => !deleted.has(session.id)).map((session) => diffById.get(session.id) ?? session);
100
+ const existingIds = new Set(kept.map((session) => session.id));
101
+ const added = diff.filter((session) => !existingIds.has(session.id));
102
+ return [...kept, ...added].sort(compareSessionsByRecency);
103
+ }
@@ -0,0 +1,35 @@
1
+ // Seed a synthetic tool result when switching to a role that has a
2
+ // "default view". Extracted from App.vue so the component stays lean.
3
+
4
+ import { v4 as uuidv4 } from "uuid";
5
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
6
+ import type { ActiveSession } from "../../types/session";
7
+ import { BUILTIN_ROLE_IDS } from "../../config/roles";
8
+ import { apiGet } from "../api";
9
+ import { API_ROUTES } from "../../config/apiRoutes";
10
+ import { pushResult, pushErrorMessage } from "./sessionHelpers";
11
+
12
+ export async function maybeSeedRoleDefault(session: ActiveSession): Promise<void> {
13
+ if (session.roleId !== BUILTIN_ROLE_IDS.sourceManager) return;
14
+ // Pre-fetch guard: skip the network call entirely if the session
15
+ // already has content (user typed fast, or a previous seed ran).
16
+ if (session.toolResults.length > 0) return;
17
+ const response = await apiGet<{ sources?: unknown[] }>(API_ROUTES.sources.list);
18
+ if (!response.ok) {
19
+ if (session.toolResults.length === 0) {
20
+ const detail = response.status === 0 ? response.error : `HTTP ${response.status}`;
21
+ pushErrorMessage(session, `Could not preload sources (${detail}). Ask Claude to list them, or check the server log.`);
22
+ }
23
+ return;
24
+ }
25
+ const result: ToolResultComplete = {
26
+ uuid: uuidv4(),
27
+ toolName: "manageSource",
28
+ message: "Loaded source registry.",
29
+ title: "Information sources",
30
+ data: { sources: response.data.sources ?? [] },
31
+ };
32
+ if (session.toolResults.length > 0) return;
33
+ pushResult(session, result);
34
+ session.selectedResultUuid = result.uuid;
35
+ }
@@ -0,0 +1,121 @@
1
+ // Pure helpers for reconstructing an `ActiveSession`'s runtime
2
+ // shape from the `/api/sessions/:id` JSONL payload. Extracted from
3
+ // `src/App.vue#loadSession` so the parse / select / timestamp-
4
+ // resolution logic is unit-testable without mocking `fetch`.
5
+ //
6
+ // Tracks #175.
7
+
8
+ import { makeTextResult } from "../tools/result";
9
+ import { isTextEntry, isToolResultEntry, type ActiveSession, type SessionEntry, type SessionSummary } from "../../types/session";
10
+ import { EVENT_TYPES } from "../../types/events";
11
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
12
+
13
+ // Walk the server's session entries and produce the flat
14
+ // `toolResults` array the client keeps in `ActiveSession`. Drops
15
+ // `session_meta` rows (they're metadata, not a result), converts
16
+ // text entries into tool-result-shaped envelopes via
17
+ // `makeTextResult`, and passes tool_result entries through verbatim.
18
+ export function parseSessionEntries(entries: readonly SessionEntry[]): ToolResultComplete[] {
19
+ const out: ToolResultComplete[] = [];
20
+ for (const entry of entries) {
21
+ if (entry.type === EVENT_TYPES.sessionMeta) continue;
22
+ if (isTextEntry(entry)) {
23
+ out.push(makeTextResult(entry.message, entry.source));
24
+ } else if (isToolResultEntry(entry)) {
25
+ out.push(entry.result);
26
+ }
27
+ }
28
+ return out;
29
+ }
30
+
31
+ // Pick the `selectedResultUuid` the session should restore to.
32
+ // Rules:
33
+ // 1. If the URL carries `?result=<uuid>` AND that uuid actually
34
+ // exists in the loaded list, honour it verbatim. This lets
35
+ // bookmarks restore the exact result the user was viewing.
36
+ // 2. Otherwise fall back to the heuristic: the most recent
37
+ // non-text tool result (images, wiki pages, etc. carry more
38
+ // visual information than bare text).
39
+ // 3. If there are no non-text results, use the last result of
40
+ // any kind.
41
+ // 4. If the list is empty, return null.
42
+ export function resolveSelectedUuid(toolResults: readonly ToolResultComplete[], urlResult: string | null): string | null {
43
+ if (urlResult && toolResults.some((result) => result.uuid === urlResult)) {
44
+ return urlResult;
45
+ }
46
+ // Iterate backwards for the "last non-text" lookup so callers
47
+ // don't pay for an intermediate reverse copy.
48
+ for (let i = toolResults.length - 1; i >= 0; i--) {
49
+ if (toolResults[i].toolName !== "text-response") {
50
+ return toolResults[i].uuid;
51
+ }
52
+ }
53
+ const last = toolResults[toolResults.length - 1];
54
+ return last?.uuid ?? null;
55
+ }
56
+
57
+ // Decide the `startedAt` / `updatedAt` to seed the in-memory
58
+ // ActiveSession with. We prefer the server summary's timestamps
59
+ // so the restored session keeps its existing sidebar ordering;
60
+ // we fall through to the current clock only if the server
61
+ // summary is missing (e.g. freshly-created session that hasn't
62
+ // round-tripped through `/api/sessions` yet).
63
+ //
64
+ // Keeping this logic named lets the test suite pin the
65
+ // "updatedAt missing → fall back to startedAt" rule explicitly,
66
+ // which was previously a fragile `??` chain buried in loadSession.
67
+ export function resolveSessionTimestamps(serverSummary: SessionSummary | undefined, nowIso: string): { startedAt: string; updatedAt: string } {
68
+ const startedAt = serverSummary?.startedAt ?? nowIso;
69
+ const updatedAt = serverSummary?.updatedAt ?? startedAt;
70
+ return { startedAt, updatedAt };
71
+ }
72
+
73
+ // Spread toolResults evenly between startedAt and updatedAt to
74
+ // approximate per-entry timestamps for sessions loaded from disk.
75
+ // Real-time results will overwrite with Date.now() via pushResult.
76
+ export function interpolateTimestamps(toolResults: readonly ToolResultComplete[], startedAt: string, updatedAt: string): Map<string, number> {
77
+ const timestamps = new Map<string, number>();
78
+ const startMs = new Date(startedAt).getTime();
79
+ const endMs = new Date(updatedAt).getTime();
80
+ toolResults.forEach((result, i) => {
81
+ const frac = toolResults.length > 1 ? i / (toolResults.length - 1) : 0;
82
+ timestamps.set(result.uuid, startMs + (endMs - startMs) * frac);
83
+ });
84
+ return timestamps;
85
+ }
86
+
87
+ // Build an ActiveSession from server-fetched entries + metadata.
88
+ // Pure — the caller is responsible for inserting into sessionMap
89
+ // and subscribing.
90
+ export function buildLoadedSession(opts: {
91
+ id: string;
92
+ entries: readonly SessionEntry[];
93
+ defaultRoleId: string;
94
+ urlResult: string | null;
95
+ serverSummary: SessionSummary | undefined;
96
+ nowIso: string;
97
+ }): ActiveSession {
98
+ const { id, entries, defaultRoleId, urlResult, serverSummary, nowIso } = opts;
99
+ const meta = entries.find((entry) => entry.type === EVENT_TYPES.sessionMeta);
100
+ const roleId = meta?.roleId ?? defaultRoleId;
101
+ const toolResults = parseSessionEntries(entries);
102
+ const selectedResultUuid = resolveSelectedUuid(toolResults, urlResult);
103
+ const { startedAt, updatedAt } = resolveSessionTimestamps(serverSummary, nowIso);
104
+ const resultTimestamps = interpolateTimestamps(toolResults, startedAt, updatedAt);
105
+
106
+ return {
107
+ id,
108
+ roleId,
109
+ toolResults,
110
+ resultTimestamps,
111
+ isRunning: serverSummary?.isRunning ?? false,
112
+ statusMessage: serverSummary?.statusMessage ?? "",
113
+ toolCallHistory: [],
114
+ selectedResultUuid,
115
+ hasUnread: serverSummary?.hasUnread ?? false,
116
+ startedAt,
117
+ updatedAt,
118
+ runStartIndex: toolResults.length,
119
+ pendingGenerations: {},
120
+ };
121
+ }
@@ -0,0 +1,22 @@
1
+ // Pure factory for creating a blank ActiveSession.
2
+
3
+ import type { ActiveSession } from "../../types/session";
4
+
5
+ export function createEmptySession(id: string, roleId: string): ActiveSession {
6
+ const now = new Date().toISOString();
7
+ return {
8
+ id,
9
+ roleId,
10
+ toolResults: [],
11
+ resultTimestamps: new Map(),
12
+ isRunning: false,
13
+ statusMessage: "",
14
+ toolCallHistory: [],
15
+ selectedResultUuid: null,
16
+ hasUnread: false,
17
+ startedAt: now,
18
+ updatedAt: now,
19
+ runStartIndex: 0,
20
+ pendingGenerations: {},
21
+ };
22
+ }
@@ -0,0 +1,99 @@
1
+ // Pure session-mutation helpers extracted from App.vue.
2
+ // These operate on ActiveSession objects directly — no Vue
3
+ // reactivity, no imports from the component.
4
+
5
+ import { v4 as uuidv4 } from "uuid";
6
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
7
+ import type { ActiveSession } from "../../types/session";
8
+ import { makeTextResult } from "../tools/result";
9
+ import { shouldSelectAssistantText } from "../agent/toolCalls";
10
+
11
+ /** Push a result and record its timestamp in one place. */
12
+ export function pushResult(session: ActiveSession, result: ToolResultComplete): void {
13
+ session.toolResults.push(result);
14
+ session.resultTimestamps.set(result.uuid, Date.now());
15
+ }
16
+
17
+ /** Surface a server/transport error as a visible card in the session. */
18
+ export function pushErrorMessage(session: ActiveSession, message: string): void {
19
+ const text = `[Error] ${message}`;
20
+ const errorResult: ToolResultComplete = {
21
+ uuid: uuidv4(),
22
+ toolName: "text-response",
23
+ message: text,
24
+ title: "Error",
25
+ data: { text, role: "assistant", transportKind: "text-rest" },
26
+ };
27
+ pushResult(session, errorResult);
28
+ session.selectedResultUuid = errorResult.uuid;
29
+ }
30
+
31
+ /** Append the user's message so it renders immediately. */
32
+ export function beginUserTurn(session: ActiveSession, message: string): void {
33
+ session.updatedAt = new Date().toISOString();
34
+ pushResult(session, makeTextResult(message, "user"));
35
+ session.runStartIndex = session.toolResults.length;
36
+ }
37
+
38
+ /** Append text to the last assistant text-response if one exists.
39
+ * Returns true if appended, false if a new card is needed. */
40
+ export function appendToLastAssistantText(session: ActiveSession, text: string): boolean {
41
+ const last = session.toolResults[session.toolResults.length - 1];
42
+ const lastData = last?.data as { role?: string; text?: string } | undefined;
43
+ if (last?.toolName !== "text-response" || lastData?.role !== "assistant") {
44
+ return false;
45
+ }
46
+ lastData.text = (lastData.text ?? "") + text;
47
+ last.message = (last.message ?? "") + text;
48
+ return true;
49
+ }
50
+
51
+ /** Check if an incoming user text event is a duplicate of the last
52
+ * user message (sent by this tab via beginUserTurn). */
53
+ function isDuplicateUserText(session: ActiveSession, message: string): boolean {
54
+ const last = session.toolResults[session.toolResults.length - 1];
55
+ const lastData = last?.data as { role?: string; text?: string } | undefined;
56
+ return last?.toolName === "text-response" && lastData?.role === "user" && lastData?.text === message;
57
+ }
58
+
59
+ /** Handle an incoming text event (user or assistant) from the
60
+ * agent's SSE/pubsub stream. Deduplicates user messages,
61
+ * streams assistant text into the last card, and selects the
62
+ * result when appropriate. */
63
+ export function applyTextEvent(session: ActiveSession, message: string, source: "user" | "assistant"): void {
64
+ if (source === "user") {
65
+ if (!isDuplicateUserText(session, message)) {
66
+ pushResult(session, makeTextResult(message, "user"));
67
+ session.runStartIndex = session.toolResults.length;
68
+ }
69
+ return;
70
+ }
71
+ if (appendToLastAssistantText(session, message)) return;
72
+ const textResult = makeTextResult(message, "assistant");
73
+ pushResult(session, textResult);
74
+ if (shouldSelectAssistantText(session.toolResults, session.runStartIndex)) {
75
+ session.selectedResultUuid = textResult.uuid;
76
+ }
77
+ }
78
+
79
+ /** In-place update a result that was re-emitted by a plugin view
80
+ * (e.g. after the user edits a chart config). */
81
+ export function updateResult(session: ActiveSession, updatedResult: ToolResultComplete): void {
82
+ const index = session.toolResults.findIndex((r) => r.uuid === updatedResult.uuid);
83
+ if (index !== -1) {
84
+ Object.assign(session.toolResults[index], updatedResult);
85
+ }
86
+ }
87
+
88
+ /** Handle an incoming tool_result event: upsert into the session's
89
+ * result list. Selects the result only on insert; in-place updates
90
+ * preserve the user's current selection. */
91
+ export function applyToolResultToSession(session: ActiveSession, result: ToolResultComplete): void {
92
+ const idx = session.toolResults.findIndex((r) => r.uuid === result.uuid);
93
+ if (idx >= 0) {
94
+ session.toolResults[idx] = result;
95
+ } else {
96
+ pushResult(session, result);
97
+ session.selectedResultUuid = result.uuid;
98
+ }
99
+ }
@@ -0,0 +1,17 @@
1
+ // Deduplicate consecutive tool results that represent "full-state
2
+ // refreshes" (e.g. a wiki page re-render after an edit). When two
3
+ // adjacent results have the same toolName and both carry
4
+ // `updating: true`, only the later one is kept. Text-response
5
+ // cards are never collapsed — each is a distinct message.
6
+
7
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
8
+
9
+ export function deduplicateResults(all: ToolResultComplete[]): ToolResultComplete[] {
10
+ return all.filter((r, i) => {
11
+ if (r.toolName === "text-response") return true;
12
+ const next = all[i + 1];
13
+ if (!next) return true;
14
+ if (next.toolName !== r.toolName) return true;
15
+ return !(r.updating === true && next.updating === true);
16
+ });
17
+ }
@@ -0,0 +1,33 @@
1
+ // Pure helpers for the MCP-tool sidebar state. The composable in
2
+ // src/composables/useMcpTools wires these up to a Vue ref + a fetch
3
+ // against /api/mcp-tools; here we keep just the rules so they can
4
+ // be unit-tested without Vue or fetch.
5
+
6
+ export interface ToolDefinitionMetadata {
7
+ description?: string;
8
+ }
9
+
10
+ // Filter a role's plugin list down to the tools that are still
11
+ // enabled — i.e. not in the disabled set.
12
+ export function availableToolsFor(rolePlugins: string[], disabled: Set<string>): string[] {
13
+ return rolePlugins.filter((name) => !disabled.has(name));
14
+ }
15
+
16
+ // Build a name → description map for the tools the current role can
17
+ // see. Prefers the bundled tool definition's `description`, falling
18
+ // back to the MCP tool's prompt when the bundled one is missing.
19
+ // `getDefinition` is injected so tests can stub the local plugin
20
+ // lookup without importing src/tools.
21
+ export function toolDescriptionsFor(
22
+ rolePlugins: string[],
23
+ getDefinition: (name: string) => ToolDefinitionMetadata | null,
24
+ mcpDescriptions: Record<string, string>,
25
+ ): Record<string, string> {
26
+ const map: Record<string, string> = {};
27
+ for (const name of rolePlugins) {
28
+ const def = getDefinition(name);
29
+ const desc = def?.description ?? mcpDescriptions[name];
30
+ if (desc) map[name] = desc;
31
+ }
32
+ return map;
33
+ }
@@ -0,0 +1,16 @@
1
+ // Pure logic for "is this tool call still considered pending right
2
+ // now?" — extracted so it can be unit-tested without spinning up a
3
+ // Vue reactive scope. The composable in src/composables/usePendingCalls
4
+ // pairs this with the timing / interval bookkeeping.
5
+
6
+ import type { ToolCallHistoryItem } from "../../types/toolCallHistory";
7
+
8
+ // A freshly-resolved call is held visible for this many milliseconds
9
+ // after its result lands, so the spinner / loading row does not flash
10
+ // off the screen if the response was very fast.
11
+ export const PENDING_MIN_MS = 500;
12
+
13
+ export function isCallStillPending(call: ToolCallHistoryItem, nowMs: number): boolean {
14
+ if (call.result === undefined && call.error === undefined) return true;
15
+ return nowMs < call.timestamp + PENDING_MIN_MS;
16
+ }
@@ -0,0 +1,40 @@
1
+ // Pure helpers for `ToolResultComplete` shapes used across the
2
+ // frontend. Kept dependency-free of Vue / DOM so they are trivially
3
+ // unit-testable from `node:test`.
4
+
5
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
6
+ import { v4 as uuidv4 } from "uuid";
7
+ import { isRecord } from "../types";
8
+
9
+ // Type guard: a text-response entry whose `data.role` is `"user"`.
10
+ // Used by App.vue to find the first user message in a live session
11
+ // when building the merged history list.
12
+ export function isUserTextResponse(r: ToolResultComplete): boolean {
13
+ if (r.toolName !== "text-response") return false;
14
+ const data = r.data;
15
+ if (!isRecord(data)) return false;
16
+ return data.role === "user";
17
+ }
18
+
19
+ // Pull out the optional base64 image attached to a tool result, if
20
+ // any. Returns `undefined` for results that have no `data.imageData`
21
+ // or where it isn't a string.
22
+ export function extractImageData(result: ToolResultComplete | undefined): string | undefined {
23
+ const data = result?.data;
24
+ if (isRecord(data) && typeof data.imageData === "string") {
25
+ return data.imageData;
26
+ }
27
+ return undefined;
28
+ }
29
+
30
+ // Build a synthetic text-response result for either a user or
31
+ // assistant turn. Used by sendMessage and the chat history UI.
32
+ export function makeTextResult(text: string, role: "user" | "assistant"): ToolResultComplete {
33
+ return {
34
+ uuid: uuidv4(),
35
+ toolName: "text-response",
36
+ message: text,
37
+ title: role === "user" ? "You" : "Assistant",
38
+ data: { text, role, transportKind: "text-rest" },
39
+ };
40
+ }
@@ -0,0 +1,44 @@
1
+ // Shared runtime type guards for the Vue frontend (#520).
2
+ // Same API as server/utils/types.ts — kept in sync manually
3
+ // until a shared packages/types workspace is introduced.
4
+
5
+ /** Narrow `unknown` to a plain object (not null, not array). */
6
+ export function isRecord(value: unknown): value is Record<string, unknown> {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+
10
+ /** Narrow `unknown` to any object (not null, arrays allowed). */
11
+ export function isObj(value: unknown): value is object {
12
+ return typeof value === "object" && value !== null;
13
+ }
14
+
15
+ /** Non-empty string after trimming whitespace. */
16
+ export function isNonEmptyString(value: unknown): value is string {
17
+ return typeof value === "string" && value.trim().length > 0;
18
+ }
19
+
20
+ /** Record whose values are all strings. */
21
+ export function isStringRecord(value: unknown): value is Record<string, string> {
22
+ if (!isRecord(value)) return false;
23
+ return Object.values(value).every((v) => typeof v === "string");
24
+ }
25
+
26
+ /** String array (every element is a string). */
27
+ export function isStringArray(value: unknown): value is string[] {
28
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
29
+ }
30
+
31
+ /** Error-like object with a `code` property. */
32
+ export function isErrorWithCode(value: unknown): value is { code: string; message?: string } {
33
+ return isRecord(value) && typeof value.code === "string";
34
+ }
35
+
36
+ /** Check that a record has a specific key with a string value. */
37
+ export function hasStringProp<K extends string>(value: unknown, key: K): value is Record<K, string> & Record<string, unknown> {
38
+ return isRecord(value) && typeof value[key] === "string";
39
+ }
40
+
41
+ /** Check that a record has a specific key with a number value. */
42
+ export function hasNumberProp<K extends string>(value: unknown, key: K): value is Record<K, number> & Record<string, unknown> {
43
+ return isRecord(value) && typeof value[key] === "number";
44
+ }