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,358 @@
1
+ <template>
2
+ <div class="h-full bg-white flex flex-col">
3
+ <!-- Header -->
4
+ <div class="flex items-center justify-between px-4 py-2 border-b border-gray-100 shrink-0 gap-3">
5
+ <div class="flex items-center gap-3 min-w-0">
6
+ <h2 class="text-base font-semibold text-gray-800 shrink-0">Todo</h2>
7
+ <span class="text-xs text-gray-500 shrink-0">{{ completedCount }}/{{ items.length }} done</span>
8
+ <input
9
+ v-model="search"
10
+ data-testid="todo-search"
11
+ type="text"
12
+ placeholder="Search..."
13
+ class="px-2 py-1 text-xs border border-gray-200 rounded w-44 focus:outline-none focus:border-blue-400"
14
+ />
15
+ </div>
16
+ <div class="flex items-center gap-2">
17
+ <!-- Add button -->
18
+ <button data-testid="todo-add-btn" class="px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600" @click="addOpen = true">+ Add</button>
19
+ <!-- Add column button (kanban only) -->
20
+ <button
21
+ v-if="viewMode === TODO_VIEW.kanban"
22
+ data-testid="todo-column-add-btn"
23
+ class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-50"
24
+ @click="addColumnOpen = true"
25
+ >
26
+ + Column
27
+ </button>
28
+ <!-- View mode toggle -->
29
+ <div class="flex border border-gray-300 rounded overflow-hidden text-xs">
30
+ <button
31
+ v-for="mode in VIEW_MODES"
32
+ :key="mode.key"
33
+ class="px-2.5 py-1"
34
+ :class="viewMode === mode.key ? 'bg-blue-500 text-white' : 'bg-white text-gray-600 hover:bg-gray-50'"
35
+ :data-testid="`todo-view-${mode.key}`"
36
+ :title="mode.label"
37
+ @click="setViewMode(mode.key)"
38
+ >
39
+ <span class="material-icons text-sm">{{ mode.icon }}</span>
40
+ </button>
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <!-- Label filter chips -->
46
+ <div v-if="labelInventory.length > 0" class="flex flex-wrap items-center gap-1.5 px-4 py-1.5 border-b border-gray-100 bg-gray-50 shrink-0">
47
+ <span class="text-[11px] text-gray-500 mr-1">Labels:</span>
48
+ <button
49
+ v-for="entry in labelInventory"
50
+ :key="entry.label"
51
+ class="px-2 py-0.5 rounded-full text-[10px] font-medium transition-all"
52
+ :class="
53
+ activeFilters.has(entry.label.toLowerCase())
54
+ ? 'ring-2 ring-blue-400 ' + colorForLabel(entry.label)
55
+ : colorForLabel(entry.label) + ' opacity-70 hover:opacity-100'
56
+ "
57
+ @click="toggleFilter(entry.label)"
58
+ >
59
+ {{ entry.label }}
60
+ <span class="opacity-60">{{ entry.count }}</span>
61
+ </button>
62
+ <button v-if="activeFilters.size > 0" class="ml-auto text-[11px] text-gray-500 hover:text-gray-700" title="Clear label filters" @click="clearFilters">
63
+ Clear ✕
64
+ </button>
65
+ </div>
66
+
67
+ <!-- Error banner -->
68
+ <div v-if="error" class="px-4 py-2 text-xs text-red-600 bg-red-50 border-b border-red-100 shrink-0">
69
+ {{ error }}
70
+ </div>
71
+
72
+ <!-- Body -->
73
+ <div class="flex-1 min-h-0">
74
+ <div v-if="items.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">No todo items yet. Click "+ Add" to create one.</div>
75
+ <template v-else>
76
+ <TodoKanbanView
77
+ v-if="viewMode === TODO_VIEW.kanban"
78
+ :filtered-items="filteredItems"
79
+ :columns="columns"
80
+ @move="onMove"
81
+ @open="onOpenItem"
82
+ @toggle-complete="onToggleComplete"
83
+ @quick-add="quickAddInColumn"
84
+ @rename-column="onRenameColumn"
85
+ @delete-column="onDeleteColumn"
86
+ @mark-done="onMarkDone"
87
+ @reorder-columns="onReorderColumns"
88
+ />
89
+ <TodoTableView
90
+ v-else-if="viewMode === TODO_VIEW.table"
91
+ :filtered-items="filteredItems"
92
+ :columns="columns"
93
+ @patch="onPatchItem"
94
+ @delete="onDeleteItem"
95
+ @toggle-complete="onToggleComplete"
96
+ />
97
+ <TodoListView
98
+ v-else
99
+ :filtered-items="filteredItems"
100
+ :columns="columns"
101
+ @patch="onPatchItem"
102
+ @delete="onDeleteItem"
103
+ @toggle-complete="onToggleComplete"
104
+ />
105
+ </template>
106
+ </div>
107
+
108
+ <!-- Add item dialog -->
109
+ <TodoAddDialog v-if="addOpen" :columns="columns" :default-status="addDefaultStatus" @cancel="addOpen = false" @create="onCreateItem" />
110
+
111
+ <!-- Edit item dialog (used by kanban click; list/table use the
112
+ inline edit panel and don't need to open this) -->
113
+ <TodoEditDialog
114
+ v-if="editingItem"
115
+ :item="editingItem"
116
+ :columns="columns"
117
+ @cancel="editingItem = null"
118
+ @save="onEditDialogSave"
119
+ @delete="onEditDialogDelete"
120
+ />
121
+
122
+ <!-- Add column dialog -->
123
+ <div v-if="addColumnOpen" class="fixed inset-0 z-50 bg-black/30 flex items-center justify-center" @click="addColumnOpen = false">
124
+ <div class="bg-white rounded-lg shadow-xl w-80 p-5 space-y-3" role="dialog" aria-modal="true" aria-labelledby="todo-add-column-title" @click.stop>
125
+ <h3 id="todo-add-column-title" class="text-base font-semibold text-gray-800">Add Column</h3>
126
+ <label class="block text-xs text-gray-600">
127
+ Label
128
+ <input
129
+ v-model="newColumnLabel"
130
+ type="text"
131
+ placeholder="Review"
132
+ class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
133
+ @keydown.enter="commitNewColumn"
134
+ />
135
+ </label>
136
+ <div class="flex justify-end gap-2 pt-1">
137
+ <button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="addColumnOpen = false">Cancel</button>
138
+ <button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="commitNewColumn">Add</button>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </template>
144
+
145
+ <script setup lang="ts">
146
+ import { computed, onMounted, onUnmounted, ref, watch } from "vue";
147
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
148
+ import type { TodoData, TodoItem } from "../plugins/todo/index";
149
+ import { colorForLabel, filterByLabels, listLabelsWithCount } from "../plugins/todo/labels";
150
+ import { useTodos, type CreateItemInput, type PatchItemInput } from "../plugins/todo/composables/useTodos";
151
+ import TodoKanbanView from "./todo/TodoKanbanView.vue";
152
+ import TodoTableView from "./todo/TodoTableView.vue";
153
+ import TodoListView from "./todo/TodoListView.vue";
154
+ import TodoAddDialog from "./todo/TodoAddDialog.vue";
155
+ import TodoEditDialog from "./todo/TodoEditDialog.vue";
156
+
157
+ import { TODO_VIEW, TODO_VIEW_MODES as VIEW_MODES, type TodoViewMode as ViewMode } from "../plugins/todo/viewModes";
158
+
159
+ const VIEW_MODE_KEY = "todo_explorer_view_mode";
160
+
161
+ const props = defineProps<{
162
+ selectedResult?: ToolResultComplete<TodoData>;
163
+ }>();
164
+
165
+ const { items, columns, error, refresh, createItem, patchItem, moveItem, deleteItem, addColumn, patchColumn, deleteColumn, reorderColumns } = useTodos(
166
+ props.selectedResult?.data?.items ?? [],
167
+ props.selectedResult?.data?.columns ?? [],
168
+ );
169
+
170
+ // When the parent swaps in a different tool result, reseed the local
171
+ // state and re-fetch from the server. Watching the uuid (not items)
172
+ // so empty-result swaps still trigger.
173
+ watch(
174
+ () => props.selectedResult?.uuid,
175
+ () => {
176
+ items.value = props.selectedResult?.data?.items ?? [];
177
+ columns.value = props.selectedResult?.data?.columns ?? [];
178
+ void refresh();
179
+ },
180
+ );
181
+
182
+ // ── View mode (persisted in localStorage) ───────────────────────
183
+
184
+ const VALID_VIEW_MODES: ReadonlySet<string> = new Set(Object.values(TODO_VIEW));
185
+
186
+ function loadViewMode(): ViewMode {
187
+ const stored = localStorage.getItem(VIEW_MODE_KEY);
188
+ if (stored && VALID_VIEW_MODES.has(stored)) {
189
+ return stored as ViewMode;
190
+ }
191
+ return TODO_VIEW.kanban;
192
+ }
193
+
194
+ const viewMode = ref<ViewMode>(loadViewMode());
195
+
196
+ function setViewMode(next: ViewMode): void {
197
+ viewMode.value = next;
198
+ localStorage.setItem(VIEW_MODE_KEY, next);
199
+ }
200
+
201
+ // ── Filtering ──────────────────────────────────────────────────
202
+
203
+ const search = ref("");
204
+ const activeFilters = ref<Set<string>>(new Set());
205
+
206
+ const labelInventory = computed(() => listLabelsWithCount(items.value));
207
+
208
+ function toggleFilter(label: string): void {
209
+ const key = label.toLowerCase();
210
+ const next = new Set(activeFilters.value);
211
+ if (next.has(key)) next.delete(key);
212
+ else next.add(key);
213
+ activeFilters.value = next;
214
+ }
215
+
216
+ function clearFilters(): void {
217
+ activeFilters.value = new Set();
218
+ }
219
+
220
+ const filteredItems = computed(() => {
221
+ const byLabels = filterByLabels(items.value, [...activeFilters.value]);
222
+ const query = search.value.trim().toLowerCase();
223
+ if (query.length === 0) return byLabels;
224
+ return byLabels.filter((item) => {
225
+ if (item.text.toLowerCase().includes(query)) return true;
226
+ if (item.note?.toLowerCase().includes(query)) return true;
227
+ return false;
228
+ });
229
+ });
230
+
231
+ const completedCount = computed(() => items.value.filter((i) => i.completed).length);
232
+
233
+ // ── Add dialog ─────────────────────────────────────────────────
234
+
235
+ const addOpen = ref(false);
236
+ const addDefaultStatus = ref<string | undefined>(undefined);
237
+
238
+ function quickAddInColumn(statusId: string): void {
239
+ addDefaultStatus.value = statusId;
240
+ addOpen.value = true;
241
+ }
242
+
243
+ async function onCreateItem(input: CreateItemInput): Promise<void> {
244
+ const created = await createItem(input);
245
+ if (created) {
246
+ addOpen.value = false;
247
+ addDefaultStatus.value = undefined;
248
+ }
249
+ }
250
+
251
+ // ── Add column dialog ──────────────────────────────────────────
252
+
253
+ const addColumnOpen = ref(false);
254
+ const newColumnLabel = ref("");
255
+
256
+ async function commitNewColumn(): Promise<void> {
257
+ const label = newColumnLabel.value.trim();
258
+ if (label.length === 0) return;
259
+ const added = await addColumn({ label });
260
+ if (added) {
261
+ addColumnOpen.value = false;
262
+ newColumnLabel.value = "";
263
+ }
264
+ }
265
+
266
+ // Escape closes the inline add-column dialog. The Add and Edit
267
+ // dialogs handle their own Escape via document listeners; this one
268
+ // is owned by the explorer template directly so it lives here.
269
+ function onExplorerKeydown(event: KeyboardEvent): void {
270
+ if (event.key !== "Escape") return;
271
+ if (addColumnOpen.value) {
272
+ addColumnOpen.value = false;
273
+ }
274
+ }
275
+ onMounted(() => document.addEventListener("keydown", onExplorerKeydown));
276
+ onUnmounted(() => document.removeEventListener("keydown", onExplorerKeydown));
277
+
278
+ // ── Item handlers ──────────────────────────────────────────────
279
+
280
+ function onPatchItem(itemId: string, input: PatchItemInput): void {
281
+ void patchItem(itemId, input);
282
+ }
283
+
284
+ // Single confirm gate for every item deletion path: row "✕" buttons
285
+ // in list/table, the kanban edit dialog's delete button, anything
286
+ // else that wants to remove an item. Centralised so we never
287
+ // accidentally bypass the confirm in a future caller.
288
+ function confirmAndDelete(itemId: string): boolean {
289
+ const item = items.value.find((i) => i.id === itemId);
290
+ if (!item) return false;
291
+ const confirmed = window.confirm(`Delete "${item.text}"?`);
292
+ if (!confirmed) return false;
293
+ void deleteItem(itemId);
294
+ return true;
295
+ }
296
+
297
+ function onDeleteItem(itemId: string): void {
298
+ confirmAndDelete(itemId);
299
+ }
300
+
301
+ function onToggleComplete(item: TodoItem): void {
302
+ void patchItem(item.id, { completed: !item.completed });
303
+ }
304
+
305
+ function onMove(itemId: string, statusId: string, position: number): void {
306
+ void moveItem(itemId, { status: statusId, position });
307
+ }
308
+
309
+ // ── Edit dialog (kanban click) ─────────────────────────────────
310
+
311
+ const editingItem = ref<TodoItem | null>(null);
312
+
313
+ function onOpenItem(item: TodoItem): void {
314
+ // Kanban cards open the modal edit dialog. List and Table views
315
+ // have their own inline edit panels and don't go through here.
316
+ editingItem.value = item;
317
+ }
318
+
319
+ async function onEditDialogSave(input: PatchItemInput): Promise<void> {
320
+ const target = editingItem.value;
321
+ if (!target) return;
322
+ const saved = await patchItem(target.id, input);
323
+ if (saved) editingItem.value = null;
324
+ }
325
+
326
+ function onEditDialogDelete(itemId: string): void {
327
+ // Funnel through the same confirm gate as the inline ✕ buttons.
328
+ // The dialog only closes if the user confirmed; if they cancelled
329
+ // the confirm, the dialog stays open so they can keep editing.
330
+ if (confirmAndDelete(itemId)) editingItem.value = null;
331
+ }
332
+
333
+ // ── Column handlers ────────────────────────────────────────────
334
+
335
+ function onRenameColumn(columnId: string, label: string): void {
336
+ void patchColumn(columnId, { label });
337
+ }
338
+
339
+ function onDeleteColumn(columnId: string): void {
340
+ // Use a native confirm dialog: deleting a column reassigns its
341
+ // items, which is reversible but worth a beat. The other column
342
+ // operations (rename, mark-done) are inexpensive enough not to need
343
+ // confirmation.
344
+ const col = columns.value.find((column) => column.id === columnId);
345
+ if (!col) return;
346
+ const confirmed = window.confirm(`Delete column "${col.label}"? Items in this column will be moved to another column.`);
347
+ if (!confirmed) return;
348
+ void deleteColumn(columnId);
349
+ }
350
+
351
+ function onMarkDone(columnId: string): void {
352
+ void patchColumn(columnId, { isDone: true });
353
+ }
354
+
355
+ function onReorderColumns(ids: string[]): void {
356
+ void reorderColumns(ids);
357
+ }
358
+ </script>
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <div
3
+ ref="root"
4
+ class="flex-1 min-h-0 overflow-y-auto p-2 space-y-2 bg-gray-100 outline-none"
5
+ tabindex="0"
6
+ data-testid="tool-results-scroll"
7
+ @mousedown="emit('activate')"
8
+ >
9
+ <div
10
+ v-for="result in results"
11
+ :key="result.uuid"
12
+ class="cursor-pointer rounded border border-gray-300 p-2 text-sm text-gray-900 hover:opacity-75 transition-opacity"
13
+ :class="result.uuid === selectedUuid ? 'ring-2 ring-blue-500' : ''"
14
+ @click="emit('select', result.uuid)"
15
+ >
16
+ <div class="flex items-center gap-1">
17
+ <component
18
+ :is="getPlugin(result.toolName)?.previewComponent"
19
+ v-if="getPlugin(result.toolName)?.previewComponent"
20
+ :result="result"
21
+ class="flex-1 min-w-0"
22
+ />
23
+ <span v-else class="flex-1 min-w-0 truncate">{{ result.title || result.toolName }}</span>
24
+ <span v-if="resultTimestamps.get(result.uuid)" class="text-[10px] text-gray-400 shrink-0">{{
25
+ formatSmartTime(resultTimestamps.get(result.uuid)!)
26
+ }}</span>
27
+ </div>
28
+ </div>
29
+
30
+ <!-- Thinking indicator -->
31
+ <div v-if="isRunning" class="px-2 py-1 text-sm">
32
+ <div class="flex items-center gap-2 text-gray-500">
33
+ <span class="text-xs">{{ statusMessage }}</span>
34
+ <span class="flex gap-1">
35
+ <span class="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" style="animation-delay: 0ms" />
36
+ <span class="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" style="animation-delay: 150ms" />
37
+ <span class="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" style="animation-delay: 300ms" />
38
+ </span>
39
+ </div>
40
+ <div v-if="pendingCalls.length > 0" class="mt-1 space-y-0.5">
41
+ <div v-for="call in pendingCalls" :key="call.toolUseId" class="flex items-center gap-1.5 text-xs text-gray-400">
42
+ <span class="w-1.5 h-1.5 rounded-full bg-blue-300 shrink-0 animate-pulse" />
43
+ <span class="font-mono truncate">{{ call.toolName }}</span>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { ref } from "vue";
52
+ import type { ToolResultComplete } from "gui-chat-protocol/vue";
53
+ import { getPlugin } from "../tools";
54
+ import { formatSmartTime } from "../utils/format/date";
55
+
56
+ interface PendingCall {
57
+ toolUseId: string;
58
+ toolName: string;
59
+ }
60
+
61
+ defineProps<{
62
+ results: ToolResultComplete[];
63
+ selectedUuid: string | null;
64
+ resultTimestamps: Map<string, number>;
65
+ isRunning: boolean;
66
+ statusMessage: string;
67
+ pendingCalls: PendingCall[];
68
+ }>();
69
+
70
+ const emit = defineEmits<{
71
+ select: [uuid: string];
72
+ activate: [];
73
+ }>();
74
+
75
+ const root = ref<HTMLDivElement | null>(null);
76
+ defineExpose({ root });
77
+ </script>
@@ -0,0 +1,131 @@
1
+ <template>
2
+ <div class="fixed inset-0 z-50 bg-black/30 flex items-center justify-center" @click="emit('cancel')">
3
+ <div
4
+ class="bg-white rounded-lg shadow-xl w-96 max-w-[90vw] p-5 space-y-3"
5
+ role="dialog"
6
+ aria-modal="true"
7
+ aria-labelledby="todo-add-dialog-title"
8
+ @click.stop
9
+ >
10
+ <h3 id="todo-add-dialog-title" class="text-base font-semibold text-gray-800">Add Todo</h3>
11
+ <label class="block text-xs text-gray-600">
12
+ Text
13
+ <input
14
+ ref="textInput"
15
+ v-model="text"
16
+ type="text"
17
+ placeholder="What needs doing?"
18
+ class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
19
+ @keydown.enter="submit"
20
+ />
21
+ </label>
22
+ <label class="block text-xs text-gray-600">
23
+ Note
24
+ <textarea
25
+ v-model="note"
26
+ rows="2"
27
+ class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded resize-y focus:outline-none focus:border-blue-400"
28
+ />
29
+ </label>
30
+ <div class="grid grid-cols-2 gap-3">
31
+ <label class="block text-xs text-gray-600">
32
+ Status
33
+ <select v-model="status" class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400">
34
+ <option v-for="col in columns" :key="col.id" :value="col.id">
35
+ {{ col.label }}
36
+ </option>
37
+ </select>
38
+ </label>
39
+ <label class="block text-xs text-gray-600">
40
+ Priority
41
+ <select v-model="priority" class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400">
42
+ <option value="">— None —</option>
43
+ <option v-for="p in PRIORITIES" :key="p" :value="p">
44
+ {{ PRIORITY_LABELS[p] }}
45
+ </option>
46
+ </select>
47
+ </label>
48
+ <label class="block text-xs text-gray-600">
49
+ Due date
50
+ <input
51
+ v-model="dueDate"
52
+ type="date"
53
+ class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
54
+ />
55
+ </label>
56
+ <label class="block text-xs text-gray-600">
57
+ Labels
58
+ <input
59
+ v-model="labelsText"
60
+ type="text"
61
+ placeholder="work, urgent"
62
+ class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
63
+ />
64
+ </label>
65
+ </div>
66
+ <div class="flex justify-end gap-2 pt-1">
67
+ <button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="emit('cancel')">Cancel</button>
68
+ <button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="submit">Add</button>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </template>
73
+
74
+ <script setup lang="ts">
75
+ import { onMounted, onUnmounted, ref } from "vue";
76
+ import type { StatusColumn } from "../../plugins/todo/index";
77
+ import { PRIORITIES, PRIORITY_LABELS } from "../../plugins/todo/priority";
78
+ import type { CreateItemInput } from "../../plugins/todo/composables/useTodos";
79
+
80
+ const props = defineProps<{
81
+ columns: StatusColumn[];
82
+ defaultStatus?: string;
83
+ }>();
84
+
85
+ const emit = defineEmits<{
86
+ cancel: [];
87
+ create: [input: CreateItemInput];
88
+ }>();
89
+
90
+ const text = ref("");
91
+ const note = ref("");
92
+ const status = ref<string>(props.defaultStatus ?? props.columns[0]?.id ?? "");
93
+ const priority = ref<string>("");
94
+ const dueDate = ref("");
95
+ const labelsText = ref("");
96
+
97
+ const textInput = ref<HTMLInputElement | null>(null);
98
+
99
+ // Escape closes the dialog. Bound at the document level rather than
100
+ // on the modal div so it works no matter where focus is — Vue's
101
+ // `@keydown.esc` only fires when the modal owns focus, which it
102
+ // loses as soon as the user tabs into one of the form inputs.
103
+ function onKeydown(event: KeyboardEvent): void {
104
+ if (event.key === "Escape") emit("cancel");
105
+ }
106
+
107
+ onMounted(() => {
108
+ textInput.value?.focus();
109
+ document.addEventListener("keydown", onKeydown);
110
+ });
111
+
112
+ onUnmounted(() => {
113
+ document.removeEventListener("keydown", onKeydown);
114
+ });
115
+
116
+ function submit(): void {
117
+ const trimmed = text.value.trim();
118
+ if (trimmed.length === 0) return;
119
+ const input: CreateItemInput = { text: trimmed };
120
+ if (note.value !== "") input.note = note.value;
121
+ if (status.value !== "") input.status = status.value;
122
+ if (priority.value !== "") input.priority = priority.value;
123
+ if (dueDate.value !== "") input.dueDate = dueDate.value;
124
+ const labels = labelsText.value
125
+ .split(",")
126
+ .map((s) => s.trim())
127
+ .filter((s) => s.length > 0);
128
+ if (labels.length > 0) input.labels = labels;
129
+ emit("create", input);
130
+ }
131
+ </script>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <div class="fixed inset-0 z-50 bg-black/30 flex items-center justify-center" @click="emit('cancel')">
3
+ <div
4
+ class="bg-white rounded-lg shadow-xl w-[28rem] max-w-[92vw] overflow-hidden"
5
+ role="dialog"
6
+ aria-modal="true"
7
+ aria-labelledby="todo-edit-dialog-title"
8
+ @click.stop
9
+ >
10
+ <div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
11
+ <h3 id="todo-edit-dialog-title" class="text-base font-semibold text-gray-800">Edit Todo</h3>
12
+ <button class="text-gray-400 hover:text-red-500 text-xs px-2 py-0.5" title="Delete this item" @click="emit('delete', item.id)">Delete</button>
13
+ </div>
14
+ <TodoEditPanel :item="item" :columns="columns" @save="(input) => emit('save', input)" @cancel="emit('cancel')" />
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { onMounted, onUnmounted } from "vue";
21
+ import type { StatusColumn, TodoItem } from "../../plugins/todo/index";
22
+ import type { PatchItemInput } from "../../plugins/todo/composables/useTodos";
23
+ import TodoEditPanel from "./TodoEditPanel.vue";
24
+
25
+ defineProps<{
26
+ item: TodoItem;
27
+ columns: StatusColumn[];
28
+ }>();
29
+
30
+ const emit = defineEmits<{
31
+ save: [input: PatchItemInput];
32
+ cancel: [];
33
+ delete: [id: string];
34
+ }>();
35
+ // The parent (TodoExplorer) gates deletion behind a single confirm
36
+ // helper, so this dialog just emits the delete intent and lets the
37
+ // caller decide whether to actually remove the item.
38
+
39
+ // Escape closes the dialog. Document-level listener so it works even
40
+ // when focus is inside the form (Vue's @keydown.esc only fires on
41
+ // the element that owns focus).
42
+ function onKeydown(event: KeyboardEvent): void {
43
+ if (event.key === "Escape") emit("cancel");
44
+ }
45
+ onMounted(() => document.addEventListener("keydown", onKeydown));
46
+ onUnmounted(() => document.removeEventListener("keydown", onKeydown));
47
+ </script>