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,85 @@
1
+ <template>
2
+ <div class="w-72 flex-shrink-0 border-r border-gray-200 overflow-y-auto p-2 bg-gray-50">
3
+ <div class="flex justify-end items-center gap-2 px-1 pb-1 text-xs">
4
+ <span class="text-gray-400">Sort:</span>
5
+ <button
6
+ type="button"
7
+ class="px-2 py-0.5 rounded transition-colors"
8
+ :class="sortMode === 'name' ? 'bg-blue-100 text-blue-700 font-medium' : 'text-gray-500 hover:bg-gray-200'"
9
+ :aria-pressed="sortMode === 'name'"
10
+ title="Sort by name"
11
+ data-testid="file-sort-name"
12
+ @click="emit('update:sortMode', 'name')"
13
+ >
14
+ Name
15
+ </button>
16
+ <button
17
+ type="button"
18
+ class="px-2 py-0.5 rounded transition-colors"
19
+ :class="sortMode === 'recent' ? 'bg-blue-100 text-blue-700 font-medium' : 'text-gray-500 hover:bg-gray-200'"
20
+ :aria-pressed="sortMode === 'recent'"
21
+ title="Sort by modified date (newest first)"
22
+ data-testid="file-sort-recent"
23
+ @click="emit('update:sortMode', 'recent')"
24
+ >
25
+ Recent
26
+ </button>
27
+ </div>
28
+ <div v-if="treeError" class="p-2 text-xs text-red-600">
29
+ {{ treeError }}
30
+ </div>
31
+ <div v-else-if="!rootNode" class="p-2 text-xs text-gray-400">Loading...</div>
32
+ <FileTree
33
+ v-else
34
+ :node="rootNode"
35
+ :selected-path="selectedPath"
36
+ :recent-paths="recentPaths"
37
+ :children-by-path="childrenByPath"
38
+ :sort-mode="sortMode"
39
+ @select="emit('select', $event)"
40
+ @load-children="emit('loadChildren', $event)"
41
+ />
42
+ <template v-if="refRoots.length > 0">
43
+ <div class="mt-2 pt-2 border-t border-gray-200 px-1 mb-1 flex items-center gap-1">
44
+ <span class="text-[10px] font-semibold text-gray-400 uppercase">Reference</span>
45
+ <span class="text-[9px] px-1 py-0.5 rounded bg-blue-100 text-blue-600">RO</span>
46
+ </div>
47
+ <FileTree
48
+ v-for="refNode in refRoots"
49
+ :key="refNode.path"
50
+ :node="refNode"
51
+ :selected-path="selectedPath"
52
+ :recent-paths="emptySet"
53
+ :children-by-path="childrenByPath"
54
+ :sort-mode="sortMode"
55
+ @select="emit('select', $event)"
56
+ @load-children="emit('loadChildren', $event)"
57
+ />
58
+ </template>
59
+ </div>
60
+ </template>
61
+
62
+ <script setup lang="ts">
63
+ import FileTree from "./FileTree.vue";
64
+ import type { TreeNode } from "../types/fileTree";
65
+ import type { FileSortMode } from "../composables/useFileSortMode";
66
+
67
+ defineProps<{
68
+ rootNode: TreeNode | null;
69
+ refRoots: TreeNode[];
70
+ childrenByPath: Map<string, TreeNode[] | null>;
71
+ treeError: string | null;
72
+ selectedPath: string | null;
73
+ recentPaths: Set<string>;
74
+ sortMode: FileSortMode;
75
+ }>();
76
+
77
+ const emit = defineEmits<{
78
+ select: [path: string];
79
+ loadChildren: [path: string];
80
+ "update:sortMode": [mode: FileSortMode];
81
+ }>();
82
+
83
+ // Shared empty set for reference roots (they don't highlight recents).
84
+ const emptySet = new Set<string>();
85
+ </script>
@@ -0,0 +1,206 @@
1
+ <template>
2
+ <div class="h-full flex bg-white">
3
+ <FileTreePane
4
+ :root-node="rootNode"
5
+ :ref-roots="refRoots"
6
+ :children-by-path="childrenByPath"
7
+ :tree-error="treeError"
8
+ :selected-path="selectedPath"
9
+ :recent-paths="recentPaths"
10
+ :sort-mode="sortMode"
11
+ @select="selectFile"
12
+ @load-children="loadDirChildren"
13
+ @update:sort-mode="setSortMode"
14
+ />
15
+ <!-- Content pane -->
16
+ <div class="flex-1 flex flex-col min-w-0 overflow-hidden">
17
+ <FileContentHeader
18
+ :selected-path="selectedPath"
19
+ :size="content?.size ?? null"
20
+ :modified-ms="content?.modifiedMs ?? null"
21
+ :is-markdown="isMarkdown"
22
+ :md-raw-mode="mdRawMode"
23
+ @toggle-md-raw="toggleMdRaw"
24
+ @deselect="deselectFile"
25
+ />
26
+ <FileContentRenderer
27
+ :selected-path="selectedPath"
28
+ :content="content"
29
+ :content-error="contentError"
30
+ :content-loading="contentLoading"
31
+ :scheduler-result="schedulerResult"
32
+ :todo-explorer-result="todoExplorerResult"
33
+ :is-markdown="isMarkdown"
34
+ :is-html="isHtml"
35
+ :is-json="isJson"
36
+ :is-jsonl="isJsonl"
37
+ :md-raw-mode="mdRawMode"
38
+ :sandboxed-html="sandboxedHtml"
39
+ :json-tokens="jsonTokens"
40
+ :jsonl-lines="jsonlLines"
41
+ :md-frontmatter="mdFrontmatter"
42
+ :raw-save-error="rawSaveError"
43
+ @markdown-link-click="handleMarkdownLinkClick"
44
+ @update-source="saveRawMarkdown"
45
+ />
46
+ </div>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { ref, computed, watch, onMounted, onUnmounted } from "vue";
52
+ import { useRoute } from "vue-router";
53
+ import FileTreePane from "./FileTreePane.vue";
54
+ import FileContentHeader from "./FileContentHeader.vue";
55
+ import FileContentRenderer from "./FileContentRenderer.vue";
56
+ import { useFileTree } from "../composables/useFileTree";
57
+ import { useFileSelection, isValidFilePath } from "../composables/useFileSelection";
58
+ import { useMarkdownMode } from "../composables/useMarkdownMode";
59
+ import { useFileSortMode } from "../composables/useFileSortMode";
60
+ import { useContentDisplay } from "../composables/useContentDisplay";
61
+ import { useMarkdownLinkHandler } from "../composables/useMarkdownLinkHandler";
62
+ import { apiPut } from "../utils/api";
63
+ import { API_ROUTES } from "../config/apiRoutes";
64
+ import { toSchedulerResult } from "../utils/filesPreview/schedulerPreview";
65
+ import { toTodoExplorerResult } from "../utils/filesPreview/todoPreview";
66
+
67
+ const RECENT_THRESHOLD_MS = 60 * 1000;
68
+
69
+ const route = useRoute();
70
+
71
+ const props = defineProps<{
72
+ refreshToken?: number;
73
+ }>();
74
+
75
+ const emit = defineEmits<{
76
+ // Emitted when the user clicks a markdown link whose target is
77
+ // a chat session jsonl; App.vue should load that session into
78
+ // the active chat view rather than opening the raw jsonl.
79
+ loadSession: [sessionId: string];
80
+ }>();
81
+
82
+ const { rootNode, refRoots, childrenByPath, treeError, loadDirChildren, ensureAncestorsLoaded, reloadRoot, loadRefRoots } = useFileTree();
83
+
84
+ const { selectedPath, content, contentLoading, contentError, loadContent, selectFile, deselectFile, abortContent } = useFileSelection();
85
+
86
+ const { mdRawMode, toggleMdRaw } = useMarkdownMode();
87
+
88
+ const { sortMode, setSortMode } = useFileSortMode();
89
+
90
+ const { isMarkdown, isHtml, isJson, isJsonl, sandboxedHtml, jsonTokens, jsonlLines, mdFrontmatter } = useContentDisplay(selectedPath, content);
91
+
92
+ // Save-error banner shown above the Rendered-mode markdown editor.
93
+ // Cleared on every new file load and on the next successful save.
94
+ const rawSaveError = ref<string | null>(null);
95
+
96
+ async function saveRawMarkdown(newSource: string): Promise<void> {
97
+ if (!selectedPath.value) return;
98
+ if (content.value?.kind !== "text") return;
99
+ if (newSource === content.value.content) return;
100
+ // Snapshot the target path so a late response from a PUT for file A
101
+ // can't overwrite `content.value` after the user has navigated to
102
+ // file B. Server-side the save still completes — we only suppress
103
+ // the stale UI update.
104
+ const pathAtSave = selectedPath.value;
105
+ rawSaveError.value = null;
106
+ const result = await apiPut<{
107
+ path: string;
108
+ size: number;
109
+ modifiedMs: number;
110
+ }>(API_ROUTES.files.content, {
111
+ path: pathAtSave,
112
+ content: newSource,
113
+ });
114
+ if (selectedPath.value !== pathAtSave) return;
115
+ if (!result.ok) {
116
+ rawSaveError.value = result.error;
117
+ return;
118
+ }
119
+ // Reflect the saved state locally — size/modifiedMs come from the
120
+ // server's post-write stat, and `content` is what we just sent. Avoid
121
+ // a round-trip GET since the server has already confirmed the write.
122
+ content.value = {
123
+ kind: "text",
124
+ path: result.data.path,
125
+ content: newSource,
126
+ size: result.data.size,
127
+ modifiedMs: result.data.modifiedMs,
128
+ };
129
+ }
130
+
131
+ // Clear any stale save error whenever a new file is loaded.
132
+ watch(content, () => {
133
+ rawSaveError.value = null;
134
+ });
135
+
136
+ const schedulerResult = computed(() => toSchedulerResult(selectedPath.value, content.value?.kind === "text" ? content.value.content : null));
137
+
138
+ const todoExplorerResult = computed(() => toTodoExplorerResult(selectedPath.value, content.value?.kind === "text" ? content.value.content : null));
139
+
140
+ const recentPaths = computed(() => {
141
+ const set = new Set<string>();
142
+ const now = Date.now();
143
+ // Walk every loaded directory in the cache — lazy-loaded children
144
+ // may not be rooted under the ref we start from, so iterating the
145
+ // cache directly is both cheaper and more complete.
146
+ for (const children of childrenByPath.value.values()) {
147
+ if (!children) continue;
148
+ for (const node of children) {
149
+ if (node.type === "file" && node.modifiedMs && now - node.modifiedMs < RECENT_THRESHOLD_MS) {
150
+ set.add(node.path);
151
+ }
152
+ }
153
+ }
154
+ return set;
155
+ });
156
+
157
+ // Uses click.capture so we intercept before TextResponseView's own
158
+ // handler (which only knows about absolute URLs) sees the event.
159
+ const { handleMarkdownLinkClick } = useMarkdownLinkHandler(selectedPath, {
160
+ onNavigate: selectFile,
161
+ onLoadSession: (sessionId) => emit("loadSession", sessionId),
162
+ });
163
+
164
+ // External URL changes (back/forward) → update selectedPath.
165
+ watch(
166
+ () => route.query.path,
167
+ (newPath) => {
168
+ if (!isValidFilePath(newPath)) {
169
+ if (selectedPath.value !== null) {
170
+ selectedPath.value = null;
171
+ content.value = null;
172
+ }
173
+ return;
174
+ }
175
+ if (newPath !== selectedPath.value) {
176
+ selectedPath.value = newPath;
177
+ loadContent(newPath);
178
+ }
179
+ },
180
+ );
181
+
182
+ watch(
183
+ () => props.refreshToken,
184
+ () => {
185
+ reloadRoot();
186
+ if (selectedPath.value) loadContent(selectedPath.value);
187
+ },
188
+ );
189
+
190
+ onMounted(async () => {
191
+ await loadDirChildren("");
192
+ await loadRefRoots();
193
+
194
+ // Deep-link: if the URL has a selected path, reveal its ancestors
195
+ // by fetching each dir in sequence so the tree auto-expands to
196
+ // the selection.
197
+ if (selectedPath.value) {
198
+ await ensureAncestorsLoaded(selectedPath.value);
199
+ loadContent(selectedPath.value);
200
+ }
201
+ });
202
+
203
+ onUnmounted(() => {
204
+ abortContent();
205
+ });
206
+ </script>
@@ -0,0 +1,111 @@
1
+ <template>
2
+ <div class="relative">
3
+ <button
4
+ ref="button"
5
+ data-testid="sandbox-lock-button"
6
+ :class="sandboxEnabled ? 'text-gray-400 hover:text-gray-700' : 'text-amber-400 hover:text-amber-500'"
7
+ :title="sandboxEnabled ? 'Sandbox enabled (Docker)' : 'No sandbox (Docker not found)'"
8
+ @click="emit('update:open', !open)"
9
+ >
10
+ <span class="material-icons">{{ sandboxEnabled ? "lock" : "lock_open" }}</span>
11
+ </button>
12
+ <div v-if="open" ref="popup" class="absolute left-0 top-full mt-1 w-64 bg-white border border-gray-200 rounded-lg shadow-lg z-50 p-3 text-xs">
13
+ <p class="mb-2" :class="sandboxEnabled ? 'text-green-800' : 'text-amber-500'">
14
+ <template v-if="sandboxEnabled">
15
+ <span class="material-icons text-xs align-middle mr-1">lock</span>
16
+ <strong>Sandbox enabled:</strong> Docker is running. Filesystem access is isolated.
17
+ </template>
18
+ <template v-else>
19
+ <span class="material-icons text-xs align-middle mr-1">warning</span>
20
+ <strong>No sandbox:</strong> Claude can access all files on your machine. Install
21
+ <a href="https://www.docker.com/products/docker-desktop/" target="_blank" class="underline">Docker Desktop</a>
22
+ to enable filesystem isolation.
23
+ </template>
24
+ </p>
25
+ <div v-if="sandboxEnabled" data-testid="sandbox-credentials-block" class="mb-2 border-t border-gray-100 pt-2">
26
+ <p class="text-gray-400 mb-1">Host credentials attached:</p>
27
+ <p v-if="sandboxStatus === null" class="text-gray-400 italic" data-testid="sandbox-credentials-loading">loading…</p>
28
+ <template v-else>
29
+ <p data-testid="sandbox-credentials-ssh">
30
+ <span class="mr-1">🔑</span>
31
+ <span class="text-gray-500">SSH agent:</span>
32
+ <span :class="sandboxStatus.sshAgent ? 'text-green-700' : 'text-gray-400'" class="ml-1">
33
+ {{ sandboxStatus.sshAgent ? "forwarded" : "not forwarded" }}
34
+ </span>
35
+ </p>
36
+ <p data-testid="sandbox-credentials-mounts">
37
+ <span class="mr-1">📁</span>
38
+ <span class="text-gray-500">Mounted configs:</span>
39
+ <span :class="sandboxStatus.mounts.length > 0 ? 'text-green-700' : 'text-gray-400'" class="ml-1">
40
+ {{ sandboxStatus.mounts.length > 0 ? sandboxStatus.mounts.join(", ") : "none" }}
41
+ </span>
42
+ </p>
43
+ </template>
44
+ </div>
45
+ <p class="text-gray-400 mb-1">Test sandbox isolation:</p>
46
+ <div class="flex flex-col gap-1">
47
+ <button
48
+ v-for="q in SANDBOX_TEST_QUERIES"
49
+ :key="q"
50
+ data-testid="sandbox-test-query"
51
+ class="text-left rounded px-2 py-1 bg-gray-100 hover:bg-gray-200 text-gray-700 transition-colors"
52
+ @click="onTestQuery(q)"
53
+ >
54
+ {{ q }}
55
+ </button>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </template>
60
+
61
+ <script setup lang="ts">
62
+ import { ref, watch } from "vue";
63
+ import { useSandboxStatus } from "../composables/useSandboxStatus";
64
+
65
+ const props = defineProps<{
66
+ sandboxEnabled: boolean;
67
+ open: boolean;
68
+ }>();
69
+
70
+ const emit = defineEmits<{
71
+ "update:open": [value: boolean];
72
+ testQuery: [query: string];
73
+ }>();
74
+
75
+ // Canned queries that demonstrate what the sandbox does / doesn't
76
+ // allow. Kept here (not passed as a prop) because they are specific
77
+ // to this popup and nothing else in the app needs them.
78
+ //
79
+ // The last entry is intentionally broader than the others: after #327
80
+ // shipped opt-in credential forwarding, users need a way to ask what's
81
+ // actually attached to *their* container, not just read the generic
82
+ // docs. Claude answers by reading config/helps/sandbox.md and
83
+ // inspecting the live state via built-in tools.
84
+ const SANDBOX_TEST_QUERIES = [
85
+ "Run `whoami` and show the result",
86
+ "Run `hostname` and show the result",
87
+ "Try to list files in ~/Library",
88
+ "Read config/helps/sandbox.md and explain how the sandbox works",
89
+ "Explain my current sandbox and credential setup",
90
+ ];
91
+
92
+ const button = ref<HTMLButtonElement | null>(null);
93
+ const popup = ref<HTMLDivElement | null>(null);
94
+ defineExpose({ button, popup });
95
+
96
+ // Lazy-load the sandbox credential state the first time the popup
97
+ // opens (and only when the sandbox is actually enabled — /api/sandbox
98
+ // returns `{}` otherwise and the UI block is hidden).
99
+ const { status: sandboxStatus, ensureLoaded } = useSandboxStatus();
100
+ watch(
101
+ () => props.open,
102
+ async (isOpen) => {
103
+ if (isOpen && props.sandboxEnabled) await ensureLoaded();
104
+ },
105
+ );
106
+
107
+ function onTestQuery(q: string): void {
108
+ emit("update:open", false);
109
+ emit("testQuery", q);
110
+ }
111
+ </script>
@@ -0,0 +1,131 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, onMounted, onUnmounted } from "vue";
3
+ import { useNotifications } from "../composables/useNotifications";
4
+ import { formatRelativeTime } from "../utils/format/date";
5
+ import { NOTIFICATION_ICONS, NOTIFICATION_ACTION_TYPES, NOTIFICATION_PRIORITIES } from "../types/notification";
6
+ import type { NotificationPayload } from "../types/notification";
7
+
8
+ const { notifications, unreadCount, markAllRead, dismiss } = useNotifications();
9
+ const open = ref(false);
10
+ const rootRef = ref<HTMLElement | null>(null);
11
+
12
+ function onDocumentClick(event: MouseEvent): void {
13
+ if (!open.value || !rootRef.value) return;
14
+ if (!rootRef.value.contains(event.target as Node)) {
15
+ close();
16
+ }
17
+ }
18
+
19
+ onMounted(() => document.addEventListener("mousedown", onDocumentClick));
20
+ onUnmounted(() => document.removeEventListener("mousedown", onDocumentClick));
21
+
22
+ const props = defineProps<{
23
+ forceClose?: boolean;
24
+ }>();
25
+
26
+ const emit = defineEmits<{
27
+ navigate: [action: NotificationPayload["action"]];
28
+ "update:open": [open: boolean];
29
+ }>();
30
+
31
+ watch(
32
+ () => props.forceClose,
33
+ (shouldClose) => {
34
+ if (shouldClose && open.value) close();
35
+ },
36
+ );
37
+
38
+ function toggle(): void {
39
+ open.value = !open.value;
40
+ if (open.value) markAllRead();
41
+ emit("update:open", open.value);
42
+ }
43
+
44
+ function close(): void {
45
+ open.value = false;
46
+ emit("update:open", false);
47
+ }
48
+
49
+ function iconName(notification: NotificationPayload): string {
50
+ return notification.icon ?? NOTIFICATION_ICONS[notification.kind] ?? "notifications";
51
+ }
52
+
53
+ function formatTime(iso: string): string {
54
+ return formatRelativeTime(iso);
55
+ }
56
+
57
+ function handleClick(notification: NotificationPayload): void {
58
+ if (notification.action.type === NOTIFICATION_ACTION_TYPES.navigate) {
59
+ emit("navigate", notification.action);
60
+ close();
61
+ }
62
+ }
63
+
64
+ function handleDismiss(event: Event, notificationId: string): void {
65
+ event.stopPropagation();
66
+ dismiss(notificationId);
67
+ }
68
+ </script>
69
+
70
+ <template>
71
+ <div ref="rootRef" class="relative">
72
+ <!-- Bell button -->
73
+ <button class="relative text-gray-400 hover:text-gray-700" data-testid="notification-bell" aria-label="Notifications" @click="toggle">
74
+ <span class="material-icons">notifications</span>
75
+ <span
76
+ v-if="unreadCount > 0"
77
+ class="absolute -top-1.5 -right-1.5 min-w-[1rem] h-4 px-0.5 bg-red-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center leading-none"
78
+ data-testid="notification-badge"
79
+ >
80
+ {{ unreadCount > 99 ? "99+" : unreadCount }}
81
+ </span>
82
+ </button>
83
+
84
+ <!-- Dropdown panel -->
85
+ <div
86
+ v-if="open"
87
+ class="absolute left-0 top-full mt-1 w-72 max-h-80 overflow-y-auto rounded-lg shadow-xl border border-gray-200 bg-white z-50"
88
+ data-testid="notification-panel"
89
+ >
90
+ <!-- Header -->
91
+ <div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
92
+ <span class="text-sm font-semibold text-gray-700">Notifications</span>
93
+ <button class="text-xs text-blue-500 hover:text-blue-700" data-testid="notification-mark-all-read" @click="markAllRead">Mark all read</button>
94
+ </div>
95
+
96
+ <!-- Empty state -->
97
+ <div v-if="notifications.length === 0" class="py-8 text-center text-sm text-gray-400">No notifications</div>
98
+
99
+ <!-- Items -->
100
+ <div v-else>
101
+ <div
102
+ v-for="n in notifications"
103
+ :key="n.id"
104
+ role="button"
105
+ tabindex="0"
106
+ class="flex items-start gap-3 px-4 py-3 border-b border-gray-50 hover:bg-gray-50 focus:bg-gray-100 cursor-pointer outline-none"
107
+ :data-testid="`notification-item-${n.id}`"
108
+ :aria-label="n.title"
109
+ @click="handleClick(n)"
110
+ @keydown.enter="handleClick(n)"
111
+ >
112
+ <span class="material-icons text-lg mt-0.5 shrink-0" :class="n.priority === NOTIFICATION_PRIORITIES.high ? 'text-red-500' : 'text-gray-400'">
113
+ {{ iconName(n) }}
114
+ </span>
115
+ <div class="flex-1 min-w-0">
116
+ <p class="text-sm text-gray-800 truncate">{{ n.title }}</p>
117
+ <p v-if="n.body" class="text-xs text-gray-500 truncate mt-0.5">
118
+ {{ n.body }}
119
+ </p>
120
+ <p class="text-xs text-gray-400 mt-0.5">
121
+ {{ formatTime(n.firedAt) }}
122
+ </p>
123
+ </div>
124
+ <button class="text-gray-300 hover:text-gray-500 shrink-0 mt-0.5" aria-label="Dismiss" @click="handleDismiss($event, n.id)">
125
+ <span class="material-icons text-sm">close</span>
126
+ </button>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </template>
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from "vue";
3
+ import { useNotifications } from "../composables/useNotifications";
4
+ import { NOTIFICATION_ICONS } from "../types/notification";
5
+ import type { NotificationPayload } from "../types/notification";
6
+ import { ONE_SECOND_MS } from "../../server/utils/time";
7
+ import { formatSmartTime } from "../utils/format/date";
8
+
9
+ const AUTO_HIDE_MS = 5 * ONE_SECOND_MS;
10
+
11
+ const { latest } = useNotifications();
12
+ const visible = ref<NotificationPayload | null>(null);
13
+ let hideTimer: ReturnType<typeof setTimeout> | null = null;
14
+
15
+ watch(latest, (item) => {
16
+ if (!item) return;
17
+ visible.value = item;
18
+ if (hideTimer !== null) clearTimeout(hideTimer);
19
+ hideTimer = setTimeout(() => {
20
+ visible.value = null;
21
+ hideTimer = null;
22
+ }, AUTO_HIDE_MS);
23
+ });
24
+
25
+ function dismiss(): void {
26
+ if (hideTimer !== null) clearTimeout(hideTimer);
27
+ hideTimer = null;
28
+ visible.value = null;
29
+ }
30
+
31
+ function iconName(n: NotificationPayload): string {
32
+ return n.icon ?? NOTIFICATION_ICONS[n.kind] ?? "notifications";
33
+ }
34
+ </script>
35
+
36
+ <template>
37
+ <Transition name="toast">
38
+ <div
39
+ v-if="visible"
40
+ data-testid="notification-toast"
41
+ class="fixed top-4 right-4 z-50 max-w-sm rounded-lg bg-slate-800 text-white shadow-lg p-4 flex items-start gap-3"
42
+ >
43
+ <span class="material-icons text-sky-300" aria-hidden="true">
44
+ {{ iconName(visible) }}
45
+ </span>
46
+ <div class="flex-1 min-w-0">
47
+ <p class="text-sm font-medium break-words">{{ visible.title }}</p>
48
+ <p v-if="visible.body" class="mt-0.5 text-xs text-slate-300 break-words">
49
+ {{ visible.body }}
50
+ </p>
51
+ <p class="mt-1 text-xs text-slate-400">
52
+ {{ formatSmartTime(visible.firedAt) }}
53
+ </p>
54
+ </div>
55
+ <button type="button" class="text-slate-400 hover:text-white" aria-label="Dismiss" @click="dismiss">
56
+ <span class="material-icons text-base">close</span>
57
+ </button>
58
+ </div>
59
+ </Transition>
60
+ </template>
61
+
62
+ <style scoped>
63
+ .toast-enter-active,
64
+ .toast-leave-active {
65
+ transition: all 200ms ease;
66
+ }
67
+ .toast-enter-from,
68
+ .toast-leave-to {
69
+ opacity: 0;
70
+ transform: translateY(-8px);
71
+ }
72
+ </style>