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,113 @@
1
+ <template>
2
+ <div class="border-t border-blue-100 bg-blue-50 p-4 space-y-3 rounded-b-lg">
3
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
4
+ <label class="block text-xs text-gray-600 sm:col-span-2">
5
+ Text
6
+ <input
7
+ v-model="text"
8
+ type="text"
9
+ class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500"
10
+ />
11
+ </label>
12
+ <label class="block text-xs text-gray-600 sm:col-span-2">
13
+ Note
14
+ <textarea
15
+ v-model="note"
16
+ rows="2"
17
+ class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded resize-y focus:outline-none focus:border-blue-500"
18
+ />
19
+ </label>
20
+ <label class="block text-xs text-gray-600">
21
+ Status
22
+ <select v-model="status" class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500">
23
+ <option v-for="col in columns" :key="col.id" :value="col.id">
24
+ {{ col.label }}
25
+ </option>
26
+ </select>
27
+ </label>
28
+ <label class="block text-xs text-gray-600">
29
+ Priority
30
+ <select v-model="priority" class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500">
31
+ <option value="">— None —</option>
32
+ <option v-for="p in PRIORITIES" :key="p" :value="p">
33
+ {{ PRIORITY_LABELS[p] }}
34
+ </option>
35
+ </select>
36
+ </label>
37
+ <label class="block text-xs text-gray-600">
38
+ Due date
39
+ <input
40
+ v-model="dueDate"
41
+ type="date"
42
+ class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500"
43
+ />
44
+ </label>
45
+ <label class="block text-xs text-gray-600">
46
+ Labels (comma-separated)
47
+ <input
48
+ v-model="labelsText"
49
+ type="text"
50
+ placeholder="work, urgent"
51
+ class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500"
52
+ />
53
+ </label>
54
+ </div>
55
+ <div class="flex items-center gap-2 pt-1">
56
+ <button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="save">Save</button>
57
+ <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>
58
+ </div>
59
+ </div>
60
+ </template>
61
+
62
+ <script setup lang="ts">
63
+ import { ref } from "vue";
64
+ import type { StatusColumn, TodoItem, TodoPriority } from "../../plugins/todo/index";
65
+ import { PRIORITIES, PRIORITY_LABELS } from "../../plugins/todo/priority";
66
+ import type { PatchItemInput } from "../../plugins/todo/composables/useTodos";
67
+
68
+ const props = defineProps<{
69
+ item: TodoItem;
70
+ columns: StatusColumn[];
71
+ }>();
72
+
73
+ const emit = defineEmits<{
74
+ save: [input: PatchItemInput];
75
+ cancel: [];
76
+ }>();
77
+
78
+ const text = ref(props.item.text);
79
+ const note = ref(props.item.note ?? "");
80
+ const status = ref<string>(props.item.status ?? props.columns[0]?.id ?? "");
81
+ const priority = ref<string>(props.item.priority ?? "");
82
+ const dueDate = ref(props.item.dueDate ?? "");
83
+ const labelsText = ref((props.item.labels ?? []).join(", "));
84
+
85
+ function parseLabels(raw: string): string[] {
86
+ return raw
87
+ .split(",")
88
+ .map((s) => s.trim())
89
+ .filter((s) => s.length > 0);
90
+ }
91
+
92
+ function save(): void {
93
+ const input: PatchItemInput = {
94
+ text: text.value,
95
+ note: note.value === "" ? null : note.value,
96
+ status: status.value,
97
+ labels: parseLabels(labelsText.value),
98
+ };
99
+ // Priority: empty string clears, valid priority sets, anything else
100
+ // is silently ignored (the dropdown ensures we never send garbage).
101
+ if (priority.value === "") {
102
+ input.priority = null;
103
+ } else {
104
+ input.priority = priority.value as TodoPriority;
105
+ }
106
+ if (dueDate.value === "") {
107
+ input.dueDate = null;
108
+ } else {
109
+ input.dueDate = dueDate.value;
110
+ }
111
+ emit("save", input);
112
+ }
113
+ </script>
@@ -0,0 +1,249 @@
1
+ <template>
2
+ <div class="h-full overflow-x-auto overflow-y-hidden">
3
+ <draggable
4
+ :list="columnsLocal"
5
+ item-key="id"
6
+ group="todo-columns"
7
+ handle=".col-handle"
8
+ :animation="150"
9
+ class="flex gap-3 h-full p-3 min-w-max"
10
+ @end="onColumnDragEnd"
11
+ >
12
+ <template #item="{ element: col }: { element: StatusColumn }">
13
+ <div :data-testid="`todo-column-${col.id}`" class="w-72 shrink-0 flex flex-col bg-gray-100 rounded-lg">
14
+ <!-- Column header. The whole header is the drag handle —
15
+ clicking the menu button still works because the menu
16
+ button has its own @click handler that doesn't kick off
17
+ a drag, but pressing-and-holding anywhere on the header
18
+ starts a column drag. -->
19
+ <div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 col-handle cursor-grab active:cursor-grabbing">
20
+ <div class="flex items-center gap-2 min-w-0">
21
+ <span class="w-2 h-2 rounded-full shrink-0" :class="col.isDone ? 'bg-green-500' : 'bg-gray-400'" />
22
+ <span v-if="renamingId !== col.id" class="font-semibold text-sm text-gray-700 truncate" :title="col.label">{{ col.label }}</span>
23
+ <input
24
+ v-else
25
+ ref="renameInput"
26
+ v-model="renameDraft"
27
+ class="px-1 py-0.5 text-sm bg-white border border-blue-400 rounded w-32"
28
+ @keydown.enter="commitRename(col.id)"
29
+ @keydown.escape="renamingId = null"
30
+ @blur="commitRename(col.id)"
31
+ />
32
+ <span class="text-xs text-gray-500 shrink-0">{{ itemsByColumn(col.id).length }}</span>
33
+ </div>
34
+ <div class="relative">
35
+ <button class="text-gray-400 hover:text-gray-600 px-1" title="Column actions" @click="toggleMenu(col.id)">
36
+ <span class="material-icons text-base">more_horiz</span>
37
+ </button>
38
+ <div
39
+ v-if="menuOpenId === col.id"
40
+ class="absolute right-0 top-6 z-20 bg-white border border-gray-200 rounded shadow-md text-xs w-40 py-1"
41
+ @click.stop
42
+ >
43
+ <button class="w-full text-left px-3 py-1.5 hover:bg-gray-50" @click="startRename(col)">Rename</button>
44
+ <button class="w-full text-left px-3 py-1.5 hover:bg-gray-50" @click="markAsDone(col.id)">
45
+ {{ col.isDone ? "Already done column" : "Mark as done column" }}
46
+ </button>
47
+ <button class="w-full text-left px-3 py-1.5 text-red-600 hover:bg-red-50" @click="deleteColumn(col.id)">Delete column</button>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <!-- Cards -->
53
+ <draggable
54
+ :model-value="itemsByColumn(col.id)"
55
+ item-key="id"
56
+ group="todos"
57
+ class="flex-1 overflow-y-auto p-2 space-y-2 min-h-[2rem]"
58
+ :animation="150"
59
+ @change="(e: DragChangeEvent) => onDragChange(col.id, e)"
60
+ >
61
+ <template #item="{ element }: { element: TodoItem }">
62
+ <div
63
+ :data-testid="`todo-card-${element.id}`"
64
+ class="bg-white border border-l-4 border-gray-200 rounded shadow-sm p-2 cursor-grab hover:shadow active:cursor-grabbing"
65
+ :class="element.priority ? PRIORITY_BORDER[element.priority] : 'border-l-gray-200'"
66
+ @click="emit('open', element)"
67
+ >
68
+ <div class="flex items-start gap-2">
69
+ <input
70
+ type="checkbox"
71
+ :checked="element.completed"
72
+ class="mt-0.5 cursor-pointer shrink-0"
73
+ @click.stop
74
+ @change="emit('toggleComplete', element)"
75
+ />
76
+ <div class="flex-1 min-w-0">
77
+ <div class="text-sm" :class="element.completed ? 'line-through text-gray-400' : 'text-gray-800'">
78
+ {{ element.text }}
79
+ </div>
80
+ <div v-if="element.note" class="text-[11px] text-gray-400 mt-0.5 line-clamp-2">
81
+ {{ element.note }}
82
+ </div>
83
+ <div v-if="(element.labels && element.labels.length > 0) || element.priority || element.dueDate" class="flex flex-wrap gap-1 mt-1.5">
84
+ <span v-if="element.priority" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="PRIORITY_CLASSES[element.priority]">{{
85
+ PRIORITY_LABELS[element.priority]
86
+ }}</span>
87
+ <span v-if="element.dueDate" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="dueDateClasses(element.dueDate)">{{
88
+ formatDueLabel(element.dueDate)
89
+ }}</span>
90
+ <span
91
+ v-for="label in element.labels ?? []"
92
+ :key="label"
93
+ class="px-1.5 py-0.5 rounded-full text-[10px] font-medium"
94
+ :class="colorForLabel(label)"
95
+ >{{ label }}</span
96
+ >
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </template>
102
+ </draggable>
103
+
104
+ <!-- Add card stub -->
105
+ <button class="m-2 text-xs text-gray-500 hover:text-gray-800 hover:bg-gray-200 rounded py-1.5 transition-colors" @click="emit('quickAdd', col.id)">
106
+ + Add card
107
+ </button>
108
+ </div>
109
+ </template>
110
+ </draggable>
111
+ </div>
112
+ </template>
113
+
114
+ <script setup lang="ts">
115
+ import { computed, nextTick, ref, watch } from "vue";
116
+ import draggable from "vuedraggable";
117
+ import type { StatusColumn, TodoItem } from "../../plugins/todo/index";
118
+ import { colorForLabel } from "../../plugins/todo/labels";
119
+ import { PRIORITY_BORDER, PRIORITY_CLASSES, PRIORITY_LABELS, dueDateClasses, formatDueLabel } from "../../plugins/todo/priority";
120
+
121
+ // vuedraggable @change event shape. The library emits one of these
122
+ // three keys depending on whether the move was within the same list,
123
+ // added from another list, or removed from the current list. We only
124
+ // react to "added" (the destination column) and "moved" (reorder
125
+ // within a single column) — "removed" is the source side and is
126
+ // always paired with an "added" on the destination, so handling it
127
+ // would double the API calls.
128
+ interface DragChangeEvent {
129
+ added?: { newIndex: number; element: TodoItem };
130
+ moved?: { newIndex: number; oldIndex: number; element: TodoItem };
131
+ removed?: { oldIndex: number; element: TodoItem };
132
+ }
133
+
134
+ const props = defineProps<{
135
+ filteredItems: TodoItem[];
136
+ columns: StatusColumn[];
137
+ }>();
138
+
139
+ const emit = defineEmits<{
140
+ move: [id: string, statusId: string, position: number];
141
+ open: [item: TodoItem];
142
+ toggleComplete: [item: TodoItem];
143
+ quickAdd: [statusId: string];
144
+ renameColumn: [id: string, label: string];
145
+ deleteColumn: [id: string];
146
+ markDone: [id: string];
147
+ reorderColumns: [ids: string[]];
148
+ }>();
149
+
150
+ // Local mirror of props.columns so vuedraggable can reorder it in
151
+ // place (`:list` mode mutates the bound array). When the parent
152
+ // updates props.columns — either after we successfully persist a
153
+ // reorder, or because some other action changed the column set —
154
+ // we copy the new array in. This also rolls the kanban back if the
155
+ // API call fails: the parent's columns ref stays at the old order,
156
+ // the watch fires, and columnsLocal snaps back.
157
+ const columnsLocal = ref<StatusColumn[]>([...props.columns]);
158
+ watch(
159
+ () => props.columns,
160
+ (next) => {
161
+ columnsLocal.value = [...next];
162
+ },
163
+ );
164
+
165
+ function onColumnDragEnd(): void {
166
+ const before = props.columns.map((column) => column.id);
167
+ const after = columnsLocal.value.map((column) => column.id);
168
+ // No-op drops: avoid an unnecessary network round-trip when the
169
+ // drop position equals the original.
170
+ if (before.length === after.length && before.every((columnId, i) => columnId === after[i])) {
171
+ return;
172
+ }
173
+ emit("reorderColumns", after);
174
+ }
175
+
176
+ // Group filtered items by status, sorted by `order`. Computed so the
177
+ // kanban re-derives whenever items / filter changes.
178
+ const itemsByStatus = computed(() => {
179
+ const map = new Map<string, TodoItem[]>();
180
+ for (const col of props.columns) map.set(col.id, []);
181
+ for (const item of props.filteredItems) {
182
+ const columnId = item.status ?? props.columns[0]?.id;
183
+ if (!columnId) continue;
184
+ if (!map.has(columnId)) map.set(columnId, []);
185
+ map.get(columnId)!.push(item);
186
+ }
187
+ for (const list of map.values()) {
188
+ list.sort((left, right) => (left.order ?? 0) - (right.order ?? 0));
189
+ }
190
+ return map;
191
+ });
192
+
193
+ function itemsByColumn(columnId: string): TodoItem[] {
194
+ return itemsByStatus.value.get(columnId) ?? [];
195
+ }
196
+
197
+ function onDragChange(columnId: string, event: DragChangeEvent): void {
198
+ if (event.added) {
199
+ emit("move", event.added.element.id, columnId, event.added.newIndex);
200
+ return;
201
+ }
202
+ if (event.moved) {
203
+ emit("move", event.moved.element.id, columnId, event.moved.newIndex);
204
+ }
205
+ }
206
+
207
+ // ── Column menu / rename ─────────────────────────────────────────
208
+
209
+ const menuOpenId = ref<string | null>(null);
210
+ const renamingId = ref<string | null>(null);
211
+ const renameDraft = ref("");
212
+ const renameInput = ref<HTMLInputElement[] | HTMLInputElement | null>(null);
213
+
214
+ function toggleMenu(columnId: string): void {
215
+ menuOpenId.value = menuOpenId.value === columnId ? null : columnId;
216
+ }
217
+
218
+ function startRename(col: StatusColumn): void {
219
+ menuOpenId.value = null;
220
+ renamingId.value = col.id;
221
+ renameDraft.value = col.label;
222
+ void nextTick(() => {
223
+ const inputRef = renameInput.value;
224
+ const input = Array.isArray(inputRef) ? inputRef[0] : inputRef;
225
+ input?.focus();
226
+ input?.select();
227
+ });
228
+ }
229
+
230
+ function commitRename(columnId: string): void {
231
+ if (renamingId.value !== columnId) return;
232
+ const next = renameDraft.value.trim();
233
+ renamingId.value = null;
234
+ if (next.length === 0) return;
235
+ const current = props.columns.find((column) => column.id === columnId);
236
+ if (!current || current.label === next) return;
237
+ emit("renameColumn", columnId, next);
238
+ }
239
+
240
+ function deleteColumn(columnId: string): void {
241
+ menuOpenId.value = null;
242
+ emit("deleteColumn", columnId);
243
+ }
244
+
245
+ function markAsDone(columnId: string): void {
246
+ menuOpenId.value = null;
247
+ emit("markDone", columnId);
248
+ }
249
+ </script>
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <div class="h-full overflow-y-auto p-4">
3
+ <div v-if="filteredItems.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">No items match the current filter</div>
4
+ <ul v-else class="space-y-2 max-w-3xl mx-auto">
5
+ <li v-for="item in filteredItems" :key="item.id" class="rounded-lg border border-gray-200 hover:border-gray-300 transition-colors">
6
+ <div class="flex items-center gap-3 p-3 cursor-pointer group" @click="toggleExpand(item.id)">
7
+ <input type="checkbox" :checked="item.completed" class="cursor-pointer shrink-0" @click.stop @change="toggleComplete(item)" />
8
+ <div class="flex-1 min-w-0">
9
+ <div class="flex items-center gap-2 flex-wrap">
10
+ <span class="text-sm" :class="item.completed ? 'line-through text-gray-400' : 'text-gray-800'">{{ item.text }}</span>
11
+ <span v-if="statusLabel(item)" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium bg-gray-100 text-gray-600">{{ statusLabel(item) }}</span>
12
+ <span v-if="item.priority" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="PRIORITY_CLASSES[item.priority]">{{
13
+ PRIORITY_LABELS[item.priority]
14
+ }}</span>
15
+ <span v-if="item.dueDate" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="dueDateClasses(item.dueDate)">{{
16
+ formatDueLabel(item.dueDate)
17
+ }}</span>
18
+ <span v-for="label in item.labels ?? []" :key="label" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="colorForLabel(label)">{{
19
+ label
20
+ }}</span>
21
+ </div>
22
+ <div v-if="item.note" class="text-xs text-gray-400 mt-0.5">
23
+ {{ item.note }}
24
+ </div>
25
+ </div>
26
+ <button
27
+ class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 text-xs px-1 shrink-0"
28
+ title="Delete item"
29
+ @click.stop="emit('delete', item.id)"
30
+ >
31
+
32
+ </button>
33
+ </div>
34
+ <TodoEditPanel v-if="expandedId === item.id" :item="item" :columns="columns" @save="(input) => onSave(item.id, input)" @cancel="expandedId = null" />
35
+ </li>
36
+ </ul>
37
+ </div>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import { ref } from "vue";
42
+ import type { StatusColumn, TodoItem } from "../../plugins/todo/index";
43
+ import { colorForLabel } from "../../plugins/todo/labels";
44
+ import { PRIORITY_CLASSES, PRIORITY_LABELS, dueDateClasses, formatDueLabel } from "../../plugins/todo/priority";
45
+ import type { PatchItemInput } from "../../plugins/todo/composables/useTodos";
46
+ import TodoEditPanel from "./TodoEditPanel.vue";
47
+
48
+ const props = defineProps<{
49
+ filteredItems: TodoItem[];
50
+ columns: StatusColumn[];
51
+ }>();
52
+
53
+ const emit = defineEmits<{
54
+ patch: [id: string, input: PatchItemInput];
55
+ delete: [id: string];
56
+ toggleComplete: [item: TodoItem];
57
+ }>();
58
+
59
+ const expandedId = ref<string | null>(null);
60
+
61
+ function toggleExpand(id: string): void {
62
+ expandedId.value = expandedId.value === id ? null : id;
63
+ }
64
+
65
+ function toggleComplete(item: TodoItem): void {
66
+ emit("toggleComplete", item);
67
+ }
68
+
69
+ function onSave(id: string, input: PatchItemInput): void {
70
+ emit("patch", id, input);
71
+ expandedId.value = null;
72
+ }
73
+
74
+ function statusLabel(item: TodoItem): string {
75
+ if (!item.status) return "";
76
+ const col = props.columns.find((c) => c.id === item.status);
77
+ return col?.label ?? "";
78
+ }
79
+ </script>
@@ -0,0 +1,177 @@
1
+ <template>
2
+ <div class="h-full overflow-auto">
3
+ <div v-if="filteredItems.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">No items match the current filter</div>
4
+ <table v-else class="min-w-full text-sm">
5
+ <thead class="bg-gray-50 sticky top-0 z-10">
6
+ <tr class="text-left text-xs font-medium text-gray-500 uppercase">
7
+ <th v-for="col in COLUMNS" :key="col.key" class="px-3 py-2 cursor-pointer hover:bg-gray-100 select-none" @click="setSort(col.key)">
8
+ {{ col.label }}
9
+ <span v-if="sortKey === col.key" class="material-icons text-xs align-middle">{{ sortDir === "asc" ? "arrow_upward" : "arrow_downward" }}</span>
10
+ </th>
11
+ <th class="px-3 py-2"></th>
12
+ </tr>
13
+ </thead>
14
+ <tbody class="bg-white divide-y divide-gray-100">
15
+ <template v-for="item in sortedItems" :key="item.id">
16
+ <tr class="hover:bg-gray-50">
17
+ <td class="px-3 py-2">
18
+ <input type="checkbox" :checked="item.completed" @change="emit('toggleComplete', item)" />
19
+ </td>
20
+ <td class="px-3 py-2 max-w-md cursor-pointer" @click="toggleExpand(item.id)">
21
+ <div :class="item.completed ? 'line-through text-gray-400' : 'text-gray-800'">
22
+ {{ item.text }}
23
+ </div>
24
+ <div v-if="item.note" class="text-xs text-gray-400 truncate max-w-xs">
25
+ {{ item.note }}
26
+ </div>
27
+ </td>
28
+ <td class="px-3 py-2 text-xs text-gray-600">
29
+ {{ statusLabel(item) }}
30
+ </td>
31
+ <td class="px-3 py-2">
32
+ <span v-if="item.priority" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="PRIORITY_CLASSES[item.priority]">{{
33
+ PRIORITY_LABELS[item.priority]
34
+ }}</span>
35
+ </td>
36
+ <td class="px-3 py-2">
37
+ <div class="flex flex-wrap gap-1">
38
+ <span
39
+ v-for="label in item.labels ?? []"
40
+ :key="label"
41
+ class="px-1.5 py-0.5 rounded-full text-[10px] font-medium"
42
+ :class="colorForLabel(label)"
43
+ >{{ label }}</span
44
+ >
45
+ </div>
46
+ </td>
47
+ <td class="px-3 py-2 text-xs">
48
+ <span v-if="item.dueDate" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="dueDateClasses(item.dueDate)">{{
49
+ formatDueLabel(item.dueDate)
50
+ }}</span>
51
+ </td>
52
+ <td class="px-3 py-2 text-xs text-gray-400">
53
+ {{ formatShortDate(item.createdAt) }}
54
+ </td>
55
+ <td class="px-3 py-2 text-right">
56
+ <button class="text-gray-300 hover:text-red-500 text-xs" title="Delete item" @click="emit('delete', item.id)">✕</button>
57
+ </td>
58
+ </tr>
59
+ <tr v-if="expandedId === item.id">
60
+ <td colspan="8" class="bg-blue-50 p-0">
61
+ <TodoEditPanel :item="item" :columns="columns" @save="(input) => onSave(item.id, input)" @cancel="expandedId = null" />
62
+ </td>
63
+ </tr>
64
+ </template>
65
+ </tbody>
66
+ </table>
67
+ </div>
68
+ </template>
69
+
70
+ <script setup lang="ts">
71
+ import { computed, ref } from "vue";
72
+ import type { StatusColumn, TodoItem } from "../../plugins/todo/index";
73
+ import { colorForLabel } from "../../plugins/todo/labels";
74
+ import { PRIORITY_CLASSES, PRIORITY_LABELS, PRIORITY_ORDER, dueDateClasses, formatDueLabel } from "../../plugins/todo/priority";
75
+ import type { PatchItemInput } from "../../plugins/todo/composables/useTodos";
76
+ import TodoEditPanel from "./TodoEditPanel.vue";
77
+ import { formatShortDate } from "../../utils/format/date";
78
+
79
+ type SortKey = "completed" | "text" | "status" | "priority" | "labels" | "dueDate" | "createdAt";
80
+ type SortDir = "asc" | "desc";
81
+
82
+ interface ColumnDef {
83
+ key: SortKey;
84
+ label: string;
85
+ }
86
+
87
+ const COLUMNS: ColumnDef[] = [
88
+ { key: "completed", label: "" },
89
+ { key: "text", label: "Text" },
90
+ { key: "status", label: "Status" },
91
+ { key: "priority", label: "Priority" },
92
+ { key: "labels", label: "Labels" },
93
+ { key: "dueDate", label: "Due" },
94
+ { key: "createdAt", label: "Created" },
95
+ ];
96
+
97
+ const props = defineProps<{
98
+ filteredItems: TodoItem[];
99
+ columns: StatusColumn[];
100
+ }>();
101
+
102
+ const emit = defineEmits<{
103
+ patch: [id: string, input: PatchItemInput];
104
+ delete: [id: string];
105
+ toggleComplete: [item: TodoItem];
106
+ }>();
107
+
108
+ const sortKey = ref<SortKey>("createdAt");
109
+ const sortDir = ref<SortDir>("desc");
110
+ const expandedId = ref<string | null>(null);
111
+
112
+ function setSort(key: SortKey): void {
113
+ if (sortKey.value === key) {
114
+ sortDir.value = sortDir.value === "asc" ? "desc" : "asc";
115
+ } else {
116
+ sortKey.value = key;
117
+ sortDir.value = key === "createdAt" ? "desc" : "asc";
118
+ }
119
+ }
120
+
121
+ function toggleExpand(itemId: string): void {
122
+ expandedId.value = expandedId.value === itemId ? null : itemId;
123
+ }
124
+
125
+ function onSave(itemId: string, input: PatchItemInput): void {
126
+ emit("patch", itemId, input);
127
+ expandedId.value = null;
128
+ }
129
+
130
+ function statusLabel(item: TodoItem): string {
131
+ if (!item.status) return "";
132
+ return props.columns.find((col) => col.id === item.status)?.label ?? "";
133
+ }
134
+
135
+ function compareValues(left: unknown, right: unknown): number {
136
+ // Undefined sorts last regardless of direction so empty cells stay
137
+ // at the bottom of the list (ascending) or top (descending — flipped
138
+ // by the caller). This matches GitHub's "issues with no due date"
139
+ // ordering, which I find the least surprising.
140
+ if (left === undefined && right === undefined) return 0;
141
+ if (left === undefined) return 1;
142
+ if (right === undefined) return -1;
143
+ if (typeof left === "number" && typeof right === "number") return left - right;
144
+ if (typeof left === "boolean" && typeof right === "boolean") {
145
+ return left === right ? 0 : left ? 1 : -1;
146
+ }
147
+ return String(left).localeCompare(String(right));
148
+ }
149
+
150
+ function sortValueOf(item: TodoItem, key: SortKey): unknown {
151
+ switch (key) {
152
+ case "completed":
153
+ return item.completed;
154
+ case "text":
155
+ return item.text.toLowerCase();
156
+ case "status":
157
+ return statusLabel(item).toLowerCase();
158
+ case "priority":
159
+ return item.priority ? PRIORITY_ORDER[item.priority] : undefined;
160
+ case "labels":
161
+ return (item.labels ?? []).join(",").toLowerCase();
162
+ case "dueDate":
163
+ return item.dueDate;
164
+ case "createdAt":
165
+ return item.createdAt;
166
+ }
167
+ }
168
+
169
+ const sortedItems = computed(() => {
170
+ const list = [...props.filteredItems];
171
+ list.sort((left, right) => {
172
+ const result = compareValues(sortValueOf(left, sortKey.value), sortValueOf(right, sortKey.value));
173
+ return sortDir.value === "asc" ? result : -result;
174
+ });
175
+ return list;
176
+ });
177
+ </script>
@@ -0,0 +1,40 @@
1
+ // Provide/inject contract for the currently-active chat session.
2
+ //
3
+ // Plugin Views (e.g. MulmoScript) need two things from the app-level
4
+ // session state:
5
+ //
6
+ // 1. The `chatSessionId` so they can tag long-running work (image /
7
+ // audio / movie generation) on the right session channel.
8
+ // 2. A reactive view of `pendingGenerations` so they can derive
9
+ // per-beat / per-character "rendering" spinners from the same
10
+ // map the sidebar busy-indicator reads. This lets the spinner
11
+ // survive View unmount/remount across session switches.
12
+ //
13
+ // Rather than thread two new props through every plugin's
14
+ // `<component :is="...">` mount point, we expose the active session
15
+ // via provide/inject — same pattern as useAppApi.
16
+
17
+ import { inject, provide, type Ref } from "vue";
18
+ import type { ActiveSession } from "../types/session";
19
+
20
+ /**
21
+ * Ref to the currently-active session. May be `undefined` during the
22
+ * brief window before the first session loads.
23
+ */
24
+ export type ActiveSessionRef = Ref<ActiveSession | undefined>;
25
+
26
+ const ACTIVE_SESSION_KEY = Symbol("activeSession");
27
+
28
+ /** Called once in App.vue setup to expose the ref to descendants. */
29
+ export function provideActiveSession(ref: ActiveSessionRef): void {
30
+ provide(ACTIVE_SESSION_KEY, ref);
31
+ }
32
+
33
+ /**
34
+ * Plugin Views call this to observe the active session. Returns
35
+ * `undefined` when used outside an App.vue subtree (e.g. in a unit
36
+ * test) so plugins can render standalone without the provider.
37
+ */
38
+ export function useActiveSession(): ActiveSessionRef | undefined {
39
+ return inject<ActiveSessionRef>(ACTIVE_SESSION_KEY);
40
+ }