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,350 @@
1
+ <template>
2
+ <div class="space-y-3">
3
+ <p class="text-xs text-gray-600 leading-relaxed">
4
+ Add external MCP servers. HTTP servers work in every mode. Stdio servers use the sandbox image's
5
+ <code class="bg-gray-100 px-1 rounded">npx</code> / <code class="bg-gray-100 px-1 rounded">node</code> /
6
+ <code class="bg-gray-100 px-1 rounded">tsx</code>; paths must live under the workspace when Docker is enabled.
7
+ </p>
8
+
9
+ <div v-if="servers.length === 0" class="text-xs text-gray-500 italic" data-testid="mcp-empty">No MCP servers configured yet.</div>
10
+
11
+ <ul v-else class="space-y-2" data-testid="mcp-server-list">
12
+ <li
13
+ v-for="(entry, idx) in servers"
14
+ :key="entry.id + ':' + idx"
15
+ class="border border-gray-200 rounded p-3 space-y-2"
16
+ :data-testid="'mcp-server-' + entry.id"
17
+ >
18
+ <div class="flex items-center justify-between">
19
+ <div class="flex items-center gap-2">
20
+ <span class="text-sm font-semibold text-gray-800">{{ entry.id }}</span>
21
+ <span
22
+ class="text-[10px] uppercase tracking-wide rounded px-1.5 py-0.5"
23
+ :class="entry.spec.type === 'http' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'"
24
+ >{{ entry.spec.type }}</span
25
+ >
26
+ <label class="flex items-center gap-1 text-xs text-gray-600 ml-2">
27
+ <input type="checkbox" :checked="entry.spec.enabled !== false" :data-testid="'mcp-enabled-' + entry.id" @change="onToggleEnabled(idx, $event)" />
28
+ enabled
29
+ </label>
30
+ </div>
31
+ <button class="text-xs text-red-600 hover:text-red-800" :data-testid="'mcp-remove-' + entry.id" @click="emit('remove', idx)">Remove</button>
32
+ </div>
33
+ <div v-if="entry.spec.type === 'http'" class="text-xs space-y-1">
34
+ <div>
35
+ <span class="text-gray-500">URL:</span>
36
+ <code class="ml-1">{{ entry.spec.url }}</code>
37
+ </div>
38
+ <div v-if="dockerMode && wouldRewriteLocalhost((entry.spec as HttpSpec).url)" class="text-amber-700">
39
+ In Docker mode <code>localhost</code> is rewritten to <code>host.docker.internal</code>.
40
+ </div>
41
+ </div>
42
+ <div v-else-if="entry.spec.type === 'stdio'" class="text-xs space-y-1">
43
+ <div>
44
+ <span class="text-gray-500">Command:</span>
45
+ <code class="ml-1">{{ entry.spec.command }}</code>
46
+ <code v-if="(entry.spec as StdioSpec).args?.length" class="ml-1">
47
+ {{ ((entry.spec as StdioSpec).args ?? []).join(" ") }}
48
+ </code>
49
+ </div>
50
+ <div
51
+ v-if="dockerMode && stdioHasNonWorkspaceArg((entry.spec as StdioSpec).args)"
52
+ class="text-red-600"
53
+ :data-testid="'mcp-docker-warning-' + entry.id"
54
+ >
55
+ ⚠ Contains paths outside the workspace — will not resolve inside Docker.
56
+ </div>
57
+ </div>
58
+ </li>
59
+ </ul>
60
+
61
+ <button v-if="!adding" class="text-xs px-2 py-1 rounded border border-gray-300 text-gray-700 hover:bg-gray-50" data-testid="mcp-add-btn" @click="startAdd">
62
+ + Add MCP Server
63
+ </button>
64
+
65
+ <div v-else class="border border-blue-300 rounded p-3 space-y-2" data-testid="mcp-add-form">
66
+ <label class="block text-xs font-semibold text-gray-700">
67
+ Name
68
+ <input
69
+ v-model="draft.id"
70
+ type="text"
71
+ placeholder="my-server"
72
+ class="mt-1 w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
73
+ data-testid="mcp-draft-id"
74
+ @keydown.stop
75
+ />
76
+ </label>
77
+ <div class="flex gap-3 text-xs">
78
+ <label class="flex items-center gap-1">
79
+ <input v-model="draft.type" type="radio" value="http" data-testid="mcp-draft-type-http" />
80
+ HTTP
81
+ </label>
82
+ <label class="flex items-center gap-1">
83
+ <input v-model="draft.type" type="radio" value="stdio" data-testid="mcp-draft-type-stdio" />
84
+ Stdio (command)
85
+ </label>
86
+ </div>
87
+ <div v-if="draft.type === 'http'" class="space-y-2">
88
+ <label class="block text-xs font-semibold text-gray-700">
89
+ URL
90
+ <input
91
+ v-model="draft.url"
92
+ type="text"
93
+ placeholder="https://example.com/mcp"
94
+ class="mt-1 w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
95
+ data-testid="mcp-draft-url"
96
+ @keydown.stop
97
+ />
98
+ </label>
99
+ </div>
100
+ <div v-else class="space-y-2">
101
+ <label class="block text-xs font-semibold text-gray-700">
102
+ Command
103
+ <select
104
+ v-model="draft.command"
105
+ class="mt-1 w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
106
+ data-testid="mcp-draft-command"
107
+ >
108
+ <option value="npx">npx</option>
109
+ <option value="node">node</option>
110
+ <option value="tsx">tsx</option>
111
+ </select>
112
+ </label>
113
+ <label class="block text-xs font-semibold text-gray-700">
114
+ Arguments (one per line)
115
+ <textarea
116
+ v-model="draft.argsText"
117
+ class="mt-1 w-full h-20 px-2 py-1 text-sm font-mono border border-gray-300 rounded focus:outline-none focus:border-blue-400"
118
+ placeholder="-y&#10;@modelcontextprotocol/server-filesystem&#10;/workspace/path"
119
+ data-testid="mcp-draft-args"
120
+ @keydown.stop
121
+ ></textarea>
122
+ </label>
123
+ </div>
124
+ <div v-if="draftError" class="text-xs text-red-600" data-testid="mcp-draft-error">
125
+ {{ draftError }}
126
+ </div>
127
+ <div class="flex justify-end gap-2">
128
+ <button class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-50" data-testid="mcp-draft-cancel" @click="cancelAdd">
129
+ Cancel
130
+ </button>
131
+ <button class="px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600" data-testid="mcp-draft-add" @click="commitAdd">Add</button>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </template>
136
+
137
+ <script setup lang="ts">
138
+ import { ref } from "vue";
139
+
140
+ // UI-local representation of a configured server. Matches
141
+ // server/config.ts#McpServerEntry. Re-declared here to avoid a
142
+ // cross-module type import from the server package.
143
+ export interface HttpSpec {
144
+ type: "http";
145
+ url: string;
146
+ headers?: Record<string, string>;
147
+ enabled?: boolean;
148
+ }
149
+ export interface StdioSpec {
150
+ type: "stdio";
151
+ command: string;
152
+ args?: string[];
153
+ env?: Record<string, string>;
154
+ enabled?: boolean;
155
+ }
156
+ export type ServerSpec = HttpSpec | StdioSpec;
157
+ export interface McpServerEntry {
158
+ id: string;
159
+ spec: ServerSpec;
160
+ }
161
+
162
+ interface Props {
163
+ servers: McpServerEntry[];
164
+ dockerMode: boolean;
165
+ }
166
+ const props = defineProps<Props>();
167
+
168
+ const emit = defineEmits<{
169
+ add: [entry: McpServerEntry];
170
+ update: [index: number, entry: McpServerEntry];
171
+ remove: [index: number];
172
+ }>();
173
+
174
+ interface DraftState {
175
+ id: string;
176
+ type: "http" | "stdio";
177
+ url: string;
178
+ command: string;
179
+ argsText: string;
180
+ }
181
+
182
+ const adding = ref(false);
183
+ const draft = ref<DraftState>(emptyDraft());
184
+ const draftError = ref("");
185
+
186
+ function emptyDraft(): DraftState {
187
+ return { id: "", type: "http", url: "", command: "npx", argsText: "" };
188
+ }
189
+
190
+ function startAdd(): void {
191
+ draft.value = emptyDraft();
192
+ draftError.value = "";
193
+ adding.value = true;
194
+ }
195
+
196
+ function cancelAdd(): void {
197
+ adding.value = false;
198
+ draftError.value = "";
199
+ }
200
+
201
+ const ID_RE = /^[a-z][a-z0-9_-]{0,63}$/;
202
+
203
+ // Derive an id from user input when the Name field is left blank.
204
+ // Covers the common shapes: a scoped npm package in stdio args
205
+ // (`@modelcontextprotocol/server-everything` → `everything`), or a
206
+ // hostname for an HTTP url (`mcp.deepwiki.com` → `deepwiki`).
207
+ function suggestIdFromDraft(state: DraftState): string {
208
+ if (state.type === "http") {
209
+ return suggestIdFromUrl(state.url.trim());
210
+ }
211
+ const args = state.argsText
212
+ .split("\n")
213
+ .map((line) => line.trim())
214
+ .filter((line) => line.length > 0);
215
+ return suggestIdFromStdioArgs(args);
216
+ }
217
+
218
+ function suggestIdFromUrl(rawUrl: string): string {
219
+ try {
220
+ const host = new URL(rawUrl).hostname;
221
+ const parts = host.split(".").filter((part) => part.length > 0);
222
+ // Drop generic subdomain / TLD noise so `mcp.deepwiki.com` → `deepwiki`.
223
+ const filtered = parts.filter(
224
+ (part, i) => !(i === 0 && (part === "mcp" || part === "www" || part === "api")) && !(i === parts.length - 1 && /^[a-z]{2,4}$/.test(part)),
225
+ );
226
+ const candidate = filtered[0] ?? parts[0] ?? "";
227
+ return slugifyToId(candidate);
228
+ } catch {
229
+ return "";
230
+ }
231
+ }
232
+
233
+ function suggestIdFromStdioArgs(args: string[]): string {
234
+ // First arg that isn't a flag is typically the package/script name.
235
+ const payload = args.find((arg) => !arg.startsWith("-"));
236
+ if (!payload) return "";
237
+ // For scoped packages / paths, keep only the last segment.
238
+ const lastSegment = payload.split("/").pop() ?? payload;
239
+ // Strip common MCP naming prefixes so `server-everything` → `everything`.
240
+ const stripped = lastSegment.replace(/^(mcp-server-|server-|mcp-)/, "").replace(/\.(?:[jt]s|mjs|cjs)$/, "");
241
+ return slugifyToId(stripped);
242
+ }
243
+
244
+ function slugifyToId(raw: string): string {
245
+ let slug = raw.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
246
+ // Strip leading/trailing hyphens with explicit while-loops so the
247
+ // regex engine can't be lured into catastrophic backtracking on a
248
+ // crafted input.
249
+ while (slug.startsWith("-")) slug = slug.slice(1);
250
+ while (slug.endsWith("-")) slug = slug.slice(0, -1);
251
+ slug = slug.slice(0, 64);
252
+ // Must start with a lowercase letter.
253
+ if (!/^[a-z]/.test(slug)) return "";
254
+ return slug;
255
+ }
256
+
257
+ function ensureUniqueId(base: string): string {
258
+ if (!base) return "";
259
+ if (!props.servers.some((server) => server.id === base)) return base;
260
+ for (let i = 2; i < 1000; i += 1) {
261
+ const candidate = `${base}-${i}`;
262
+ if (!props.servers.some((server) => server.id === candidate)) return candidate;
263
+ }
264
+ return "";
265
+ }
266
+
267
+ function commitAdd(): void {
268
+ let id = draft.value.id.trim();
269
+ if (!id) {
270
+ const suggested = ensureUniqueId(suggestIdFromDraft(draft.value));
271
+ if (!suggested) {
272
+ draftError.value = "Please provide a Name, or enter a URL / args we can derive one from.";
273
+ return;
274
+ }
275
+ id = suggested;
276
+ }
277
+ if (!ID_RE.test(id)) {
278
+ draftError.value = "Name must start with a lowercase letter and contain only [a-z0-9_-].";
279
+ return;
280
+ }
281
+ if (props.servers.some((server) => server.id === id)) {
282
+ draftError.value = `Server id "${id}" already exists.`;
283
+ return;
284
+ }
285
+ let spec: ServerSpec;
286
+ if (draft.value.type === "http") {
287
+ const url = draft.value.url.trim();
288
+ if (!/^https?:\/\//.test(url)) {
289
+ draftError.value = "HTTP URL must start with http:// or https://";
290
+ return;
291
+ }
292
+ spec = { type: "http", url, enabled: true };
293
+ } else {
294
+ const args = draft.value.argsText
295
+ .split("\n")
296
+ .map((line) => line.trim())
297
+ .filter((line) => line.length > 0);
298
+ spec = {
299
+ type: "stdio",
300
+ command: draft.value.command,
301
+ args,
302
+ enabled: true,
303
+ };
304
+ }
305
+ emit("add", { id, spec });
306
+ adding.value = false;
307
+ draftError.value = "";
308
+ }
309
+
310
+ // Called by the parent right before Save. If the draft form is open
311
+ // and has any input, commit it (auto-generating a Name if blank). If
312
+ // the draft is empty, silently close the form. Returns false only
313
+ // when validation fails so the parent can surface an error and abort
314
+ // the save — this is what spares users the pre-PR footgun of clicking
315
+ // Save without first clicking the inner Add button.
316
+ function flushDraft(): boolean {
317
+ if (!adding.value) return true;
318
+ const hasInput =
319
+ draft.value.id.trim().length > 0 ||
320
+ (draft.value.type === "http" && draft.value.url.trim().length > 0) ||
321
+ (draft.value.type === "stdio" && draft.value.argsText.trim().length > 0);
322
+ if (!hasInput) {
323
+ cancelAdd();
324
+ return true;
325
+ }
326
+ commitAdd();
327
+ return !adding.value;
328
+ }
329
+
330
+ defineExpose({ flushDraft });
331
+
332
+ function onToggleEnabled(index: number, event: Event): void {
333
+ const target = event.target as HTMLInputElement;
334
+ const entry = props.servers[index];
335
+ if (!entry) return;
336
+ emit("update", index, {
337
+ ...entry,
338
+ spec: { ...entry.spec, enabled: target.checked },
339
+ });
340
+ }
341
+
342
+ function wouldRewriteLocalhost(url: string): boolean {
343
+ return /^https?:\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/.test(url);
344
+ }
345
+
346
+ function stdioHasNonWorkspaceArg(args?: string[]): boolean {
347
+ if (!args) return false;
348
+ return args.some((arg) => /^\//.test(arg) && arg !== "/workspace" && !arg.startsWith("/workspace/"));
349
+ }
350
+ </script>
@@ -0,0 +1,275 @@
1
+ <template>
2
+ <div v-if="open" class="fixed inset-0 z-50 bg-black/40 flex items-center justify-center" data-testid="settings-modal-backdrop" @click="close">
3
+ <div
4
+ class="bg-white rounded-lg shadow-xl w-[36rem] max-h-[85vh] flex flex-col"
5
+ role="dialog"
6
+ aria-modal="true"
7
+ aria-labelledby="settings-modal-title"
8
+ data-testid="settings-modal"
9
+ @click.stop
10
+ >
11
+ <div class="px-5 py-4 border-b border-gray-200 flex items-center justify-between">
12
+ <h2 id="settings-modal-title" class="text-base font-semibold text-gray-900">Settings</h2>
13
+ <button class="text-gray-400 hover:text-gray-700" title="Close" data-testid="settings-close-btn" @click="close">
14
+ <span class="material-icons">close</span>
15
+ </button>
16
+ </div>
17
+
18
+ <div class="flex border-b border-gray-200 px-5">
19
+ <button
20
+ class="px-3 py-2 text-sm border-b-2"
21
+ :class="activeTab === 'tools' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-800'"
22
+ data-testid="settings-tab-tools"
23
+ @click="activeTab = 'tools'"
24
+ >
25
+ Allowed Tools
26
+ </button>
27
+ <button
28
+ class="px-3 py-2 text-sm border-b-2"
29
+ :class="activeTab === 'mcp' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-800'"
30
+ data-testid="settings-tab-mcp"
31
+ @click="activeTab = 'mcp'"
32
+ >
33
+ MCP Servers
34
+ </button>
35
+ <button
36
+ class="px-3 py-2 text-sm border-b-2"
37
+ :class="activeTab === 'dirs' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-800'"
38
+ data-testid="settings-tab-dirs"
39
+ @click="activeTab = 'dirs'"
40
+ >
41
+ Directories
42
+ </button>
43
+ <button
44
+ class="px-3 py-2 text-sm border-b-2"
45
+ :class="activeTab === 'refs' ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-800'"
46
+ data-testid="settings-tab-refs"
47
+ @click="activeTab = 'refs'"
48
+ >
49
+ Reference Dirs
50
+ </button>
51
+ </div>
52
+
53
+ <div class="px-5 py-4 overflow-y-auto flex-1 space-y-4 text-gray-900">
54
+ <div v-if="loadError" class="text-sm text-red-700 bg-red-50 border border-red-200 rounded px-3 py-2" role="alert" data-testid="settings-load-error">
55
+ ⚠ {{ loadError }}
56
+ </div>
57
+
58
+ <div v-if="activeTab === 'tools'" class="space-y-3">
59
+ <p class="text-xs text-gray-600 leading-relaxed">
60
+ Extra tool names to pass to Claude via
61
+ <code class="bg-gray-100 px-1 rounded">--allowedTools</code>. One per line. Useful for built-in Claude Code MCP servers like Gmail / Google Calendar
62
+ after you have authenticated via <code class="bg-gray-100 px-1 rounded">claude mcp</code>.
63
+ </p>
64
+ <label class="block">
65
+ <span class="text-xs font-semibold text-gray-700">Tool names</span>
66
+ <textarea
67
+ v-model="toolsText"
68
+ class="mt-1 w-full h-48 px-2 py-1.5 text-sm font-mono border border-gray-300 rounded focus:outline-none focus:border-blue-400"
69
+ placeholder="mcp__claude_ai_Gmail&#10;mcp__claude_ai_Google_Calendar"
70
+ data-testid="settings-tools-textarea"
71
+ @keydown.stop
72
+ ></textarea>
73
+ </label>
74
+ <p v-if="invalidToolNames.length > 0" class="text-xs text-amber-700">
75
+ These look non-standard (expected prefix
76
+ <code class="bg-gray-100 px-1 rounded">mcp__</code>):
77
+ {{ invalidToolNames.join(", ") }}
78
+ </p>
79
+ </div>
80
+
81
+ <div v-else-if="activeTab === 'mcp'" class="space-y-3">
82
+ <div
83
+ v-if="mcpToolsError"
84
+ class="text-xs text-amber-800 bg-amber-50 border border-amber-200 rounded px-2 py-1"
85
+ role="alert"
86
+ data-testid="mcp-tools-error"
87
+ >
88
+ ⚠ Could not fetch MCP tool status: {{ mcpToolsError }}. Showing all tools regardless of enablement.
89
+ </div>
90
+ <SettingsMcpTab
91
+ ref="mcpTabRef"
92
+ :servers="mcpServers"
93
+ :docker-mode="dockerMode"
94
+ @add="addMcpServer"
95
+ @update="updateMcpServer"
96
+ @remove="removeMcpServer"
97
+ />
98
+ </div>
99
+
100
+ <SettingsWorkspaceDirsTab v-else-if="activeTab === 'dirs'" />
101
+
102
+ <SettingsReferenceDirsTab v-else-if="activeTab === 'refs'" />
103
+ </div>
104
+
105
+ <div class="px-5 py-3 border-t border-gray-200 flex items-center justify-between gap-3">
106
+ <span v-if="statusMessage" class="text-xs" :class="statusError ? 'text-red-600' : 'text-green-600'" data-testid="settings-status">
107
+ {{ statusMessage }}
108
+ </span>
109
+ <span v-else class="text-xs text-gray-500"> Changes apply on the next message. No restart needed. </span>
110
+ <div class="flex gap-2">
111
+ <button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" data-testid="settings-cancel-btn" @click="close">
112
+ Cancel
113
+ </button>
114
+ <button
115
+ class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
116
+ :disabled="saving || loading || !!loadError"
117
+ :title="loadError ? 'Cannot save until settings load successfully' : undefined"
118
+ data-testid="settings-save-btn"
119
+ @click="save"
120
+ >
121
+ {{ saving ? "Saving…" : loading ? "Loading…" : "Save" }}
122
+ </button>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </template>
128
+
129
+ <script setup lang="ts">
130
+ import { computed, ref, watch } from "vue";
131
+ import SettingsMcpTab from "./SettingsMcpTab.vue";
132
+ import SettingsWorkspaceDirsTab from "./SettingsWorkspaceDirsTab.vue";
133
+ import SettingsReferenceDirsTab from "./SettingsReferenceDirsTab.vue";
134
+ import type { McpServerEntry } from "./SettingsMcpTab.vue";
135
+ import { apiGet, apiPut } from "../utils/api";
136
+ import { API_ROUTES } from "../config/apiRoutes";
137
+
138
+ interface Props {
139
+ open: boolean;
140
+ dockerMode?: boolean;
141
+ // Forwarded from useMcpTools — if non-null, the MCP tab shows a
142
+ // small warning strip so the user knows "all tools visible" is a
143
+ // fallback rather than an accurate listing.
144
+ mcpToolsError?: string | null;
145
+ }
146
+
147
+ const props = withDefaults(defineProps<Props>(), {
148
+ dockerMode: false,
149
+ mcpToolsError: null,
150
+ });
151
+ const emit = defineEmits<{
152
+ "update:open": [value: boolean];
153
+ saved: [];
154
+ }>();
155
+
156
+ // Typed ref to the SettingsMcpTab so save() can flush a pending draft
157
+ // before PUTing (eliminates the "user typed but forgot the inner Add
158
+ // button" footgun). Null when the MCP tab isn't the active one.
159
+ const mcpTabRef = ref<{ flushDraft: () => boolean } | null>(null);
160
+
161
+ const activeTab = ref<"tools" | "mcp" | "dirs" | "refs">("tools");
162
+ const toolsText = ref("");
163
+ const mcpServers = ref<McpServerEntry[]>([]);
164
+ const loadError = ref("");
165
+ const statusMessage = ref("");
166
+ const statusError = ref(false);
167
+ const saving = ref(false);
168
+ // `true` from the moment the modal opens until the first loadConfig()
169
+ // call resolves. Prevents the Save button from submitting the initial
170
+ // empty arrays before the real config arrives, and prevents stale
171
+ // responses (from a previous open) from overwriting fresh input.
172
+ const loading = ref(false);
173
+ // Monotonically increasing token so an in-flight loadConfig() whose
174
+ // modal has been reopened can notice it's stale and discard its result.
175
+ let loadToken = 0;
176
+
177
+ const parsedToolNames = computed(() =>
178
+ toolsText.value
179
+ .split("\n")
180
+ .map((s) => s.trim())
181
+ .filter((s) => s.length > 0),
182
+ );
183
+
184
+ const invalidToolNames = computed(() => parsedToolNames.value.filter((n) => !n.startsWith("mcp__") && !isBuiltIn(n)));
185
+
186
+ function isBuiltIn(name: string): boolean {
187
+ return ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "WebFetch", "WebSearch"].includes(name);
188
+ }
189
+
190
+ async function loadConfig(): Promise<void> {
191
+ const token = ++loadToken;
192
+ loading.value = true;
193
+ loadError.value = "";
194
+ statusMessage.value = "";
195
+ const response = await apiGet<{
196
+ settings: { extraAllowedTools: string[] };
197
+ mcp?: { servers: McpServerEntry[] };
198
+ }>(API_ROUTES.config.base);
199
+ // A newer open() has already started another load — drop this one.
200
+ if (token !== loadToken) return;
201
+ if (!response.ok) {
202
+ loadError.value = response.status === 0 ? response.error || "Network error" : `Failed to load settings (HTTP ${response.status})`;
203
+ } else {
204
+ toolsText.value = response.data.settings.extraAllowedTools.join("\n");
205
+ mcpServers.value = response.data.mcp?.servers ?? [];
206
+ }
207
+ if (token === loadToken) loading.value = false;
208
+ }
209
+
210
+ async function save(): Promise<void> {
211
+ // Extra safety: the button is already disabled while loading, but
212
+ // guard the function body too so any programmatic caller can't
213
+ // submit a half-loaded form.
214
+ if (loading.value) return;
215
+ // Auto-commit any half-entered draft on the MCP tab. If the draft
216
+ // is invalid the tab sets its own inline error — abort the save so
217
+ // the user can fix it.
218
+ if (mcpTabRef.value && !mcpTabRef.value.flushDraft()) {
219
+ statusError.value = true;
220
+ statusMessage.value = "Finish or cancel the pending MCP server entry first.";
221
+ return;
222
+ }
223
+ saving.value = true;
224
+ statusMessage.value = "";
225
+ statusError.value = false;
226
+ // Single atomic endpoint — avoids the partial-save state where
227
+ // extraAllowedTools is persisted but MCP config write fails.
228
+ const response = await apiPut<unknown>(API_ROUTES.config.base, {
229
+ settings: { extraAllowedTools: parsedToolNames.value },
230
+ mcp: { servers: mcpServers.value },
231
+ });
232
+ if (!response.ok) {
233
+ statusError.value = true;
234
+ statusMessage.value = response.error || "Save failed";
235
+ } else {
236
+ emit("saved");
237
+ // Close on success. Changes take effect on the next message, so
238
+ // the user has no reason to stay in the modal after a good save.
239
+ close();
240
+ }
241
+ saving.value = false;
242
+ }
243
+
244
+ function close(): void {
245
+ emit("update:open", false);
246
+ }
247
+
248
+ function addMcpServer(entry: McpServerEntry): void {
249
+ mcpServers.value = [...mcpServers.value, entry];
250
+ }
251
+
252
+ function updateMcpServer(index: number, entry: McpServerEntry): void {
253
+ const next = [...mcpServers.value];
254
+ next[index] = entry;
255
+ mcpServers.value = next;
256
+ }
257
+
258
+ function removeMcpServer(index: number): void {
259
+ const next = [...mcpServers.value];
260
+ next.splice(index, 1);
261
+ mcpServers.value = next;
262
+ }
263
+
264
+ watch(
265
+ () => props.open,
266
+ (isOpen) => {
267
+ if (isOpen) {
268
+ loadConfig();
269
+ statusMessage.value = "";
270
+ statusError.value = false;
271
+ }
272
+ },
273
+ { immediate: true },
274
+ );
275
+ </script>