mulmoclaude 0.3.0 → 0.5.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 (312) hide show
  1. package/bin/mulmoclaude.js +7 -24
  2. package/client/assets/html2canvas-Cx501zZr-DiKaqnKs.js +5 -0
  3. package/client/assets/{index-eHWB79u5.js → index-C94GcmNa.js} +203 -198
  4. package/client/assets/index-CY-WpQUm.css +2 -0
  5. package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-5ipqh8Pe.js} +5 -5
  6. package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
  7. package/client/index.html +2 -4
  8. package/package.json +17 -15
  9. package/server/agent/attachmentConverter.ts +2 -2
  10. package/server/agent/backend/claude-code.ts +170 -0
  11. package/server/agent/backend/index.ts +14 -0
  12. package/server/agent/backend/types.ts +65 -0
  13. package/server/agent/index.ts +31 -159
  14. package/server/agent/mcp-server.ts +88 -10
  15. package/server/agent/mcp-tools/index.ts +8 -7
  16. package/server/agent/mcp-tools/notify.ts +76 -0
  17. package/server/agent/mcp-tools/x.ts +12 -2
  18. package/server/agent/plugin-names.ts +10 -4
  19. package/server/agent/prompt.ts +187 -26
  20. package/server/agent/resumeFailover.ts +5 -5
  21. package/server/agent/sandboxMounts.ts +3 -3
  22. package/server/api/auth/bearerAuth.ts +3 -3
  23. package/server/api/auth/token.ts +2 -2
  24. package/server/api/routes/agent.ts +99 -4
  25. package/server/api/routes/chart.ts +13 -0
  26. package/server/api/routes/chat-index.ts +2 -1
  27. package/server/api/routes/config.ts +35 -8
  28. package/server/api/routes/files.ts +75 -24
  29. package/server/api/routes/html.ts +15 -2
  30. package/server/api/routes/image.ts +75 -20
  31. package/server/api/routes/mulmo-script.ts +33 -31
  32. package/server/api/routes/news.ts +146 -0
  33. package/server/api/routes/notifications.ts +58 -2
  34. package/server/api/routes/pdf.ts +2 -2
  35. package/server/api/routes/plugins.ts +73 -91
  36. package/server/api/routes/presentHtml.ts +9 -0
  37. package/server/api/routes/roles.ts +12 -2
  38. package/server/api/routes/scheduler.ts +20 -11
  39. package/server/api/routes/schedulerTasks.ts +58 -21
  40. package/server/api/routes/sessions.ts +15 -4
  41. package/server/api/routes/sessionsCursor.ts +4 -4
  42. package/server/api/routes/skills.ts +26 -5
  43. package/server/api/routes/sources.ts +8 -7
  44. package/server/api/routes/todos.ts +30 -0
  45. package/server/api/routes/todosColumnsHandlers.ts +13 -27
  46. package/server/api/routes/todosHandlers.ts +1 -1
  47. package/server/api/routes/todosItemsHandlers.ts +14 -14
  48. package/server/api/routes/wiki/frontmatter.ts +86 -0
  49. package/server/api/routes/wiki.ts +335 -75
  50. package/server/api/sandboxStatus.ts +1 -1
  51. package/server/events/notifications.ts +32 -8
  52. package/server/events/pub-sub/index.ts +3 -3
  53. package/server/events/relay-client.ts +26 -16
  54. package/server/events/resolveRelayBridgeOptions.ts +125 -0
  55. package/server/index.ts +72 -49
  56. package/server/system/config.ts +5 -5
  57. package/server/system/credentials.ts +7 -5
  58. package/server/system/env.ts +15 -5
  59. package/server/system/macosNotify.ts +152 -0
  60. package/server/utils/errors.ts +11 -2
  61. package/server/utils/fetch.ts +54 -0
  62. package/server/utils/files/atomic.ts +18 -17
  63. package/server/utils/files/image-store.ts +19 -13
  64. package/server/utils/files/journal-io.ts +2 -2
  65. package/server/utils/files/json.ts +5 -5
  66. package/server/utils/files/markdown-image-fill.ts +131 -0
  67. package/server/utils/files/markdown-store.ts +22 -6
  68. package/server/utils/files/naming.ts +20 -10
  69. package/server/utils/files/reference-dirs-io.ts +3 -3
  70. package/server/utils/files/roles-io.ts +4 -4
  71. package/server/utils/files/safe.ts +14 -14
  72. package/server/utils/files/scheduler-overrides-io.ts +2 -2
  73. package/server/utils/files/spreadsheet-store.ts +15 -10
  74. package/server/utils/files/workspace-io.ts +12 -12
  75. package/server/utils/gemini.ts +30 -4
  76. package/server/utils/gitignore.ts +9 -9
  77. package/server/utils/id.ts +40 -8
  78. package/server/utils/json.ts +5 -5
  79. package/server/utils/logBackgroundError.ts +12 -3
  80. package/server/utils/logPreview.ts +24 -0
  81. package/server/utils/markdown.ts +5 -5
  82. package/server/utils/port.d.mts +6 -0
  83. package/server/utils/port.mjs +48 -0
  84. package/server/utils/promptMeta.ts +32 -0
  85. package/server/utils/request.ts +12 -6
  86. package/server/utils/slug.ts +65 -4
  87. package/server/utils/spawn.ts +1 -1
  88. package/server/utils/types.ts +2 -2
  89. package/server/workspace/chat-index/index.ts +1 -1
  90. package/server/workspace/chat-index/summarizer.ts +5 -5
  91. package/server/workspace/custom-dirs.ts +5 -5
  92. package/server/workspace/helps/gemini.md +57 -0
  93. package/server/workspace/helps/index.md +2 -1
  94. package/server/workspace/helps/sources.md +42 -0
  95. package/server/workspace/helps/wiki.md +40 -5
  96. package/server/workspace/journal/archivist-cli.ts +121 -0
  97. package/server/workspace/journal/{archivist.ts → archivist-schemas.ts} +12 -120
  98. package/server/workspace/journal/dailyPass.ts +78 -38
  99. package/server/workspace/journal/diff.ts +2 -2
  100. package/server/workspace/journal/index.ts +56 -5
  101. package/server/workspace/journal/memoryExtractor.ts +1 -1
  102. package/server/workspace/journal/optimizationPass.ts +4 -5
  103. package/server/workspace/journal/paths.ts +8 -24
  104. package/server/workspace/journal/state.ts +18 -8
  105. package/server/workspace/news/reader.ts +248 -0
  106. package/server/workspace/paths.ts +4 -3
  107. package/server/workspace/reference-dirs.ts +3 -3
  108. package/server/workspace/skills/parser.ts +6 -6
  109. package/server/workspace/skills/scheduler.ts +5 -4
  110. package/server/workspace/skills/user-tasks.ts +3 -2
  111. package/server/workspace/skills/writer.ts +3 -3
  112. package/server/workspace/sources/arxivDiscovery.ts +2 -2
  113. package/server/workspace/sources/classifier.ts +1 -1
  114. package/server/workspace/sources/fetchers/rss.ts +5 -5
  115. package/server/workspace/sources/fetchers/rssParser.ts +4 -4
  116. package/server/workspace/sources/interests.ts +3 -3
  117. package/server/workspace/sources/paths.ts +6 -6
  118. package/server/workspace/sources/pipeline/fetch.ts +59 -13
  119. package/server/workspace/sources/pipeline/index.ts +59 -7
  120. package/server/workspace/sources/pipeline/notify.ts +13 -5
  121. package/server/workspace/sources/pipeline/plan.ts +11 -9
  122. package/server/workspace/sources/pipeline/summarize.ts +1 -1
  123. package/server/workspace/sources/pipeline/write.ts +5 -5
  124. package/server/workspace/sources/rateLimiter.ts +1 -1
  125. package/server/workspace/sources/sourceState.ts +9 -4
  126. package/server/workspace/sources/types.ts +9 -0
  127. package/server/workspace/sources/urls.ts +1 -1
  128. package/server/workspace/tool-trace/classify.ts +4 -4
  129. package/server/workspace/workspace.ts +7 -7
  130. package/src/App.vue +477 -251
  131. package/src/components/CanvasViewToggle.vue +12 -10
  132. package/src/components/ChatInput.vue +112 -105
  133. package/src/components/FileContentHeader.vue +10 -7
  134. package/src/components/FileContentRenderer.vue +37 -10
  135. package/src/components/FileTree.vue +34 -4
  136. package/src/components/FileTreePane.vue +32 -27
  137. package/src/components/FilesView.vue +5 -3
  138. package/src/components/FilterChip.vue +22 -0
  139. package/src/components/LockStatusPopup.vue +19 -13
  140. package/src/components/NewsView.vue +252 -0
  141. package/src/components/NotificationBell.vue +35 -9
  142. package/src/components/NotificationToast.vue +4 -1
  143. package/src/components/PageChatComposer.vue +101 -0
  144. package/src/components/PluginLauncher.vue +36 -62
  145. package/src/components/RightSidebar.vue +13 -10
  146. package/src/components/RoleSelector.vue +3 -2
  147. package/src/components/SessionHeaderControls.vue +63 -0
  148. package/src/components/SessionHistoryExpandButton.vue +30 -0
  149. package/src/components/SessionHistoryPanel.vue +64 -93
  150. package/src/components/SessionHistoryToggleButton.vue +40 -0
  151. package/src/components/SessionRoleIcon.vue +72 -0
  152. package/src/components/SessionSidebar.vue +96 -0
  153. package/src/components/SessionTabBar.vue +44 -51
  154. package/src/components/SettingsMcpTab.vue +361 -52
  155. package/src/components/SettingsModal.vue +203 -72
  156. package/src/components/SettingsReferenceDirsTab.vue +72 -51
  157. package/src/components/SettingsWorkspaceDirsTab.vue +74 -51
  158. package/src/components/SidebarHeader.vue +50 -16
  159. package/src/components/SourcesManager.vue +900 -0
  160. package/src/components/SourcesView.vue +45 -0
  161. package/src/components/StackView.vue +84 -48
  162. package/src/components/SuggestionsPanel.vue +25 -36
  163. package/src/components/SystemFileBanner.vue +106 -0
  164. package/src/components/ThinkingIndicator.vue +41 -0
  165. package/src/components/TodoExplorer.vue +72 -22
  166. package/src/components/todo/TodoAddDialog.vue +17 -12
  167. package/src/components/todo/TodoEditDialog.vue +7 -2
  168. package/src/components/todo/TodoEditPanel.vue +15 -10
  169. package/src/components/todo/TodoKanbanView.vue +16 -6
  170. package/src/components/todo/TodoListView.vue +14 -3
  171. package/src/components/todo/TodoTableView.vue +36 -5
  172. package/src/composables/favicon/conditions.ts +76 -0
  173. package/src/composables/favicon/resolveColor.ts +93 -0
  174. package/src/composables/favicon/types.ts +61 -0
  175. package/src/composables/useAppApi.ts +23 -0
  176. package/src/composables/useChatScroll.ts +5 -5
  177. package/src/composables/useCurrentRole.ts +32 -0
  178. package/src/composables/useDynamicFavicon.ts +174 -58
  179. package/src/composables/useEventListeners.ts +7 -12
  180. package/src/composables/useFaviconState.ts +93 -12
  181. package/src/composables/useFileSelection.ts +25 -6
  182. package/src/composables/useHealth.ts +76 -7
  183. package/src/composables/useLayoutMode.ts +27 -0
  184. package/src/composables/useNewsItems.ts +38 -0
  185. package/src/composables/useNewsReadState.ts +75 -0
  186. package/src/composables/useNotifications.ts +76 -13
  187. package/src/composables/usePendingCalls.ts +11 -1
  188. package/src/composables/useRoles.ts +6 -10
  189. package/src/composables/useRunElapsed.ts +80 -0
  190. package/src/composables/useSessionDerived.ts +21 -5
  191. package/src/composables/useSessionHistory.ts +7 -17
  192. package/src/composables/useSidePanelVisible.ts +25 -0
  193. package/src/composables/useViewLayout.ts +16 -37
  194. package/src/config/apiRoutes.ts +19 -6
  195. package/src/config/historyFilters.ts +30 -0
  196. package/src/config/mcpCatalog.ts +285 -0
  197. package/src/config/mcpTypes.ts +26 -0
  198. package/src/config/roles.ts +19 -51
  199. package/src/config/systemFileDescriptors.ts +170 -0
  200. package/src/config/toolNames.ts +6 -1
  201. package/src/config/workspacePaths.ts +1 -0
  202. package/src/index.css +14 -0
  203. package/src/lang/de.ts +706 -0
  204. package/src/lang/en.ts +726 -0
  205. package/src/lang/es.ts +712 -0
  206. package/src/lang/fr.ts +704 -0
  207. package/src/lang/ja.ts +707 -0
  208. package/src/lang/ko.ts +709 -0
  209. package/src/lang/pt-BR.ts +702 -0
  210. package/src/lang/zh.ts +705 -0
  211. package/src/lib/vue-i18n.ts +97 -0
  212. package/src/main.ts +3 -0
  213. package/src/plugins/canvas/View.vue +104 -186
  214. package/src/plugins/canvas/definition.ts +0 -8
  215. package/src/plugins/canvas/index.ts +3 -2
  216. package/src/plugins/chart/Preview.vue +1 -1
  217. package/src/plugins/chart/View.vue +9 -4
  218. package/src/plugins/chart/index.ts +3 -2
  219. package/src/plugins/editImage/index.ts +3 -2
  220. package/src/plugins/generateImage/index.ts +3 -2
  221. package/src/plugins/manageRoles/Preview.vue +4 -1
  222. package/src/plugins/manageRoles/View.vue +67 -46
  223. package/src/plugins/manageRoles/index.ts +3 -2
  224. package/src/plugins/manageSkills/Preview.vue +8 -3
  225. package/src/plugins/manageSkills/View.vue +39 -34
  226. package/src/plugins/manageSkills/index.ts +3 -2
  227. package/src/plugins/manageSource/Preview.vue +1 -1
  228. package/src/plugins/manageSource/View.vue +3 -687
  229. package/src/plugins/manageSource/index.ts +3 -2
  230. package/src/plugins/markdown/Preview.vue +1 -1
  231. package/src/plugins/markdown/View.vue +164 -73
  232. package/src/plugins/markdown/definition.ts +6 -4
  233. package/src/plugins/markdown/index.ts +3 -2
  234. package/src/plugins/presentForm/Preview.vue +99 -0
  235. package/src/plugins/presentForm/View.vue +675 -0
  236. package/src/plugins/presentForm/definition.ts +127 -0
  237. package/src/plugins/presentForm/index.ts +18 -0
  238. package/src/plugins/presentForm/plugin.ts +94 -0
  239. package/src/plugins/presentForm/types.ts +109 -0
  240. package/src/plugins/presentHtml/Preview.vue +1 -1
  241. package/src/plugins/presentHtml/View.vue +7 -4
  242. package/src/plugins/presentHtml/index.ts +3 -2
  243. package/src/plugins/presentMulmoScript/Preview.vue +1 -1
  244. package/src/plugins/presentMulmoScript/View.vue +36 -26
  245. package/src/plugins/presentMulmoScript/index.ts +3 -2
  246. package/src/plugins/scheduler/AutomationsPreview.vue +37 -0
  247. package/src/plugins/scheduler/AutomationsView.vue +23 -0
  248. package/src/plugins/scheduler/CalendarView.vue +23 -0
  249. package/src/plugins/scheduler/LegacySchedulerView.vue +32 -0
  250. package/src/plugins/scheduler/Preview.vue +7 -4
  251. package/src/plugins/scheduler/TasksTab.vue +119 -28
  252. package/src/plugins/scheduler/View.vue +75 -32
  253. package/src/plugins/scheduler/automationsDefinition.ts +58 -0
  254. package/src/plugins/scheduler/calendarDefinition.ts +46 -0
  255. package/src/plugins/scheduler/formatSchedule.ts +93 -0
  256. package/src/plugins/scheduler/index.ts +68 -14
  257. package/src/plugins/scheduler/legacyShape.ts +34 -0
  258. package/src/plugins/spreadsheet/Preview.vue +9 -5
  259. package/src/plugins/spreadsheet/View.vue +43 -57
  260. package/src/plugins/spreadsheet/engine/responseDecoder.ts +2 -1
  261. package/src/plugins/spreadsheet/index.ts +3 -2
  262. package/src/plugins/textResponse/Preview.vue +15 -58
  263. package/src/plugins/textResponse/View.vue +42 -45
  264. package/src/plugins/textResponse/utils.ts +25 -0
  265. package/src/plugins/todo/Preview.vue +11 -6
  266. package/src/plugins/todo/View.vue +27 -13
  267. package/src/plugins/todo/composables/useTodos.ts +3 -1
  268. package/src/plugins/todo/index.ts +3 -2
  269. package/src/plugins/ui-image/ImagePreview.vue +6 -3
  270. package/src/plugins/ui-image/ImageView.vue +7 -4
  271. package/src/plugins/wiki/Preview.vue +5 -2
  272. package/src/plugins/wiki/View.vue +539 -92
  273. package/src/plugins/wiki/index.ts +5 -2
  274. package/src/plugins/wiki/route.ts +121 -0
  275. package/src/router/guards.ts +43 -24
  276. package/src/router/index.ts +53 -26
  277. package/src/router/pageRoutes.ts +23 -0
  278. package/src/tools/index.ts +12 -5
  279. package/src/tools/legacyPluginNames.ts +13 -0
  280. package/src/types/notification.ts +31 -6
  281. package/src/types/vue-i18n.d.ts +20 -0
  282. package/src/utils/agent/eventDispatch.ts +3 -6
  283. package/src/utils/agent/formatElapsed.ts +37 -0
  284. package/src/utils/agent/request.ts +22 -1
  285. package/src/utils/canvas/layoutMode.ts +26 -0
  286. package/src/utils/canvas/sidePanelVisible.ts +19 -0
  287. package/src/utils/dom/scrollIntoViewByTestId.ts +38 -0
  288. package/src/utils/errors.ts +9 -2
  289. package/src/utils/files/filename.ts +24 -0
  290. package/src/utils/filesPreview/schedulerPreview.ts +9 -3
  291. package/src/utils/id.ts +18 -0
  292. package/src/utils/image/cacheBust.ts +16 -0
  293. package/src/utils/image/resolve.ts +16 -0
  294. package/src/utils/markdown/taskList.ts +175 -0
  295. package/src/utils/mcp/interpolateSpec.ts +97 -0
  296. package/src/utils/notification/dispatch.ts +51 -15
  297. package/src/utils/path/workspaceLinkRouter.ts +99 -0
  298. package/src/utils/session/mergeSessions.ts +5 -0
  299. package/src/utils/sources/filter.ts +69 -0
  300. package/src/vite-env.d.ts +9 -0
  301. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
  302. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
  303. package/client/assets/index-Bm70FDU2.css +0 -1
  304. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
  305. package/server/workspace/journal/linkRewrite.ts +0 -4
  306. package/src/components/ToolResultsPanel.vue +0 -77
  307. package/src/composables/useCanvasViewMode.ts +0 -121
  308. package/src/plugins/scheduler/definition.ts +0 -57
  309. package/src/utils/canvas/viewMode.ts +0 -46
  310. package/src/utils/role/plugins.ts +0 -12
  311. package/src/utils/session/seedRoleDefault.ts +0 -35
  312. /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
@@ -5,6 +5,7 @@ import View from "./View.vue";
5
5
  import Preview from "./Preview.vue";
6
6
  import { apiPost } from "../../utils/api";
7
7
  import { API_ROUTES } from "../../config/apiRoutes";
8
+ import { makeUuid } from "../../utils/id";
8
9
 
9
10
  // Mirrors server/sources/types.ts#Source. Re-declared here so the
10
11
  // frontend doesn't have to import a server package.
@@ -46,14 +47,14 @@ const manageSourcePlugin: ToolPlugin<ManageSourceData> = {
46
47
  if (!result.ok) {
47
48
  return {
48
49
  toolName: TOOL_NAME,
49
- uuid: crypto.randomUUID(),
50
+ uuid: makeUuid(),
50
51
  message: result.error,
51
52
  };
52
53
  }
53
54
  return {
54
55
  ...result.data,
55
56
  toolName: TOOL_NAME,
56
- uuid: crypto.randomUUID(),
57
+ uuid: makeUuid(),
57
58
  };
58
59
  },
59
60
  isEnabled: () => true,
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="p-3 bg-purple-100 rounded overflow-hidden">
2
+ <div class="p-2 bg-purple-100 rounded overflow-hidden">
3
3
  <div class="text-sm text-gray-800 font-medium truncate">
4
4
  {{ displayTitle }}
5
5
  </div>
@@ -1,49 +1,52 @@
1
1
  <template>
2
2
  <div class="markdown-container">
3
3
  <div v-if="loading" class="min-h-full p-8 flex items-center justify-center">
4
- <div class="text-gray-500">Loading document...</div>
4
+ <div class="text-gray-500">{{ t("pluginMarkdown.loading") }}</div>
5
5
  </div>
6
6
  <div v-else-if="loadError && !markdownContent" class="min-h-full p-8 flex items-center justify-center">
7
- <div class="load-error-banner" role="alert">⚠ Failed to load document: {{ loadError }}</div>
7
+ <div class="load-error-banner" role="alert">{{ t("pluginMarkdown.loadFailed", { error: loadError }) }}</div>
8
8
  </div>
9
9
  <div v-else-if="!markdownContent" class="min-h-full p-8 flex items-center justify-center">
10
- <div class="text-gray-500">No markdown content available</div>
10
+ <div class="text-gray-500">{{ t("pluginMarkdown.noContent") }}</div>
11
11
  </div>
12
12
  <template v-else>
13
+ <div class="flex items-center justify-end gap-2 px-3 py-2 border-b border-gray-100 shrink-0">
14
+ <button
15
+ class="h-8 px-2.5 flex items-center gap-1 rounded bg-green-600 hover:bg-green-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
16
+ :disabled="pdfDownloading"
17
+ @click="downloadPdf"
18
+ >
19
+ <span class="material-icons text-base">{{ pdfDownloading ? "hourglass_empty" : "download" }}</span>
20
+ {{ t("pluginMarkdown.pdf") }}
21
+ </button>
22
+ <span v-if="pdfError" class="text-xs text-red-500" :title="pdfError">{{ t("pluginMarkdown.pdfFailedShort") }}</span>
23
+ </div>
13
24
  <div v-if="loadError" class="load-error-banner" role="alert">
14
- Failed to refresh document: {{ loadError }} — showing last successfully loaded content.
25
+ {{ t("pluginMarkdown.refreshFailed", { error: loadError }) }}
15
26
  </div>
16
27
  <div class="markdown-content-wrapper">
17
28
  <div class="p-4">
18
- <div class="header-row">
19
- <h1 class="document-title">
20
- {{ selectedResult.title || "Document" }}
21
- </h1>
22
- <div class="button-group">
23
- <button class="download-btn download-btn-green" :disabled="pdfDownloading" @click="downloadPdf">
24
- <span class="material-icons">{{ pdfDownloading ? "hourglass_empty" : "download" }}</span>
25
- PDF
26
- </button>
27
- </div>
28
- <span v-if="pdfError" class="text-xs text-red-500 self-center ml-2" :title="pdfError">⚠ PDF failed</span>
29
- </div>
30
- <div class="markdown-content prose prose-slate max-w-none" v-html="renderedHtml"></div>
29
+ <!-- Click delegation: a single listener on the wrapper picks
30
+ up every interactive checkbox inserted by v-html. We
31
+ cannot bind @click directly on each `<input>` because
32
+ v-html bypasses Vue's template compiler. -->
33
+ <div class="markdown-content prose prose-slate max-w-none" @click="onMarkdownClick" v-html="renderedHtml"></div>
31
34
  </div>
32
35
  </div>
33
36
 
34
37
  <div class="bottom-bar-wrapper">
35
38
  <details ref="sourceDetails" class="markdown-source" @toggle="onDetailsToggle">
36
- <summary>Edit Markdown Source</summary>
39
+ <summary>{{ t("pluginMarkdown.editSource") }}</summary>
37
40
  <textarea v-model="editableMarkdown" class="markdown-editor" spellcheck="false"></textarea>
38
41
  <div class="editor-actions">
39
42
  <button class="apply-btn" :disabled="!hasChanges || saving" @click="applyMarkdown">
40
- {{ saving ? "Saving..." : "Apply Changes" }}
43
+ {{ saving ? t("pluginMarkdown.saving") : t("pluginMarkdown.applyChanges") }}
41
44
  </button>
42
- <button class="cancel-btn" @click="cancelEdit">Cancel</button>
45
+ <button class="cancel-btn" @click="cancelEdit">{{ t("pluginMarkdown.cancel") }}</button>
43
46
  </div>
44
- <p v-if="saveError" class="save-error" role="alert">⚠ {{ saveError }}</p>
47
+ <p v-if="saveError" class="save-error" role="alert">{{ t("pluginMarkdown.saveError", { error: saveError }) }}</p>
45
48
  </details>
46
- <button v-show="!editing" class="copy-btn" :title="copied ? 'Copied!' : 'Copy'" @click="copyText">
49
+ <button v-show="!editing" class="copy-btn" :title="copied ? t('pluginMarkdown.copiedLabel') : t('pluginMarkdown.copyLabel')" @click="copyText">
47
50
  <span class="material-icons">{{ copied ? "check" : "content_copy" }}</span>
48
51
  </button>
49
52
  </div>
@@ -53,15 +56,20 @@
53
56
 
54
57
  <script setup lang="ts">
55
58
  import { computed, ref, watch, nextTick } from "vue";
59
+ import { useI18n } from "vue-i18n";
56
60
  import { marked } from "marked";
61
+
62
+ const { t } = useI18n();
57
63
  import type { ToolResult } from "gui-chat-protocol";
58
64
  import { isFilePath, type MarkdownToolData } from "./definition";
59
65
  import { rewriteMarkdownImageRefs } from "../../utils/image/rewriteMarkdownImageRefs";
66
+ import { findTaskLines, makeTasksInteractive, toggleTaskAt } from "../../utils/markdown/taskList";
60
67
  import { usePdfDownload } from "../../composables/usePdfDownload";
61
68
  import { apiGet, apiPut } from "../../utils/api";
62
69
  import { API_ROUTES } from "../../config/apiRoutes";
63
70
  import { useClipboardCopy } from "../../composables/useClipboardCopy";
64
- import { toSafeFilename } from "../../utils/files/filename";
71
+ import { buildPdfFilename } from "../../utils/files/filename";
72
+ import { useAppApi } from "../../composables/useAppApi";
65
73
 
66
74
  const props = defineProps<{
67
75
  selectedResult: ToolResult<MarkdownToolData>;
@@ -71,6 +79,8 @@ const emit = defineEmits<{
71
79
  updateResult: [result: ToolResult<MarkdownToolData>];
72
80
  }>();
73
81
 
82
+ const appApi = useAppApi();
83
+
74
84
  const loading = ref(false);
75
85
  const saving = ref(false);
76
86
  // Human-readable message shown next to the Save button when a PUT
@@ -133,7 +143,12 @@ const renderedHtml = computed(() => {
133
143
  const raw = props.selectedResult.data?.markdown;
134
144
  const basePath = typeof raw === "string" && isFilePath(raw) ? raw.slice(0, raw.lastIndexOf("/")) : "";
135
145
  const withImages = rewriteMarkdownImageRefs(markdownContent.value, basePath);
136
- return marked(withImages) as string;
146
+ // Strip the `disabled=""` attribute marked puts on GFM task
147
+ // checkboxes and tag them so `onMarkdownClick` can find them
148
+ // (#775). Inline content (no file backing) gets the same
149
+ // treatment so non-file-backed sessions still feel responsive,
150
+ // even though clicks there only update local state.
151
+ return makeTasksInteractive(marked(withImages) as string);
137
152
  });
138
153
 
139
154
  // Watch for scroll requests from viewState
@@ -178,9 +193,14 @@ const { pdfDownloading, pdfError, downloadPdf: rawDownloadPdf } = usePdfDownload
178
193
  async function downloadPdf() {
179
194
  if (!markdownContent.value) return;
180
195
  const prefix = props.selectedResult.data?.filenamePrefix;
181
- const rawName = prefix || props.selectedResult.title || "document";
182
- const title = toSafeFilename(rawName, "document");
183
- await rawDownloadPdf(markdownContent.value, `${title}.pdf`);
196
+ const rawName = prefix || props.selectedResult.title || "";
197
+ const uuid = props.selectedResult.uuid;
198
+ const filename = buildPdfFilename({
199
+ name: rawName,
200
+ fallback: "document",
201
+ timestampMs: uuid ? appApi.getResultTimestamp(uuid) : undefined,
202
+ });
203
+ await rawDownloadPdf(markdownContent.value, filename);
184
204
  }
185
205
 
186
206
  async function applyMarkdown() {
@@ -189,16 +209,21 @@ async function applyMarkdown() {
189
209
 
190
210
  saveError.value = null;
191
211
 
192
- // If file-based, save to server
212
+ // If file-based, save to server. The `raw` value is the
213
+ // workspace-relative path returned by the server, so we send it
214
+ // verbatim — the route accepts any depth under `artifacts/documents/`
215
+ // (e.g. the YYYY/MM partitions added in #764).
193
216
  if (isFilePath(raw)) {
194
217
  saving.value = true;
195
- const filename = raw.replace(/^(artifacts\/documents|markdowns)\//, "");
196
- const result = await apiPut<unknown>(API_ROUTES.plugins.updateMarkdown.replace(":filename", filename), {
218
+ const result = await apiPut<unknown>(API_ROUTES.plugins.updateMarkdown, {
219
+ relativePath: raw,
197
220
  markdown: editableMarkdown.value,
198
221
  });
199
222
  saving.value = false;
200
223
  if (!result.ok) {
201
- saveError.value = `Save failed: ${result.error}`;
224
+ // Store the raw error; the template formats it via t() so locale
225
+ // switches re-render without double-translating.
226
+ saveError.value = result.error;
202
227
  return;
203
228
  }
204
229
  }
@@ -221,6 +246,114 @@ async function applyMarkdown() {
221
246
  if (sourceDetails.value) sourceDetails.value.open = false;
222
247
  }
223
248
 
249
+ // ── Inline task-list checkbox toggle (#775) ──────────────────────
250
+ //
251
+ // Click delegation handler bound to the rendered viewer. When the
252
+ // user clicks a GFM task checkbox we:
253
+ // 1. compute the new source via `toggleTaskAt`
254
+ // 2. update local state optimistically (v-html re-renders to match)
255
+ // 3. for file-backed docs, queue a PUT through the existing
256
+ // `/api/markdowns/update` route
257
+ //
258
+ // Skipped while the source editor is open — the textarea has its own
259
+ // edit/apply flow and a checkbox click would race with whatever the
260
+ // user is typing.
261
+
262
+ let taskPersistChain: Promise<unknown> = Promise.resolve();
263
+
264
+ async function persistTaskMarkdown(relativePath: string, markdown: string): Promise<void> {
265
+ // Bail if the user navigated to a different result while this PUT
266
+ // was queued — the snapshot belongs to a document that's no longer
267
+ // on screen, and persisting it would clobber unrelated state.
268
+ if (props.selectedResult.data?.markdown !== relativePath) return;
269
+
270
+ const result = await apiPut<unknown>(API_ROUTES.plugins.updateMarkdown, {
271
+ relativePath,
272
+ markdown,
273
+ });
274
+
275
+ // The user may have switched results during the round-trip. Skip
276
+ // every state mutation past this point — the watcher on
277
+ // `selectedResult.data?.markdown` already loads the new document,
278
+ // and writing `saveError` / triggering a refetch here would touch
279
+ // unrelated state (or refetch the *new* doc, masking edits the
280
+ // user just made there).
281
+ if (props.selectedResult.data?.markdown !== relativePath) return;
282
+
283
+ if (!result.ok) {
284
+ saveError.value = result.error;
285
+ // Refetch synchronously inside the chain so subsequent queued
286
+ // clicks observe the canonical (server-side) markdown before
287
+ // computing their own toggle. Detaching this with `void` could
288
+ // let the refetch land after a newer click already wrote.
289
+ await fetchMarkdownContent();
290
+ return;
291
+ }
292
+ // Clear any stale error from a prior failed click.
293
+ saveError.value = null;
294
+ }
295
+
296
+ function onMarkdownClick(event: MouseEvent): void {
297
+ const target = event.target;
298
+ if (!(target instanceof HTMLInputElement)) return;
299
+ if (target.type !== "checkbox") return;
300
+ if (!target.classList.contains("md-task")) return;
301
+ if (editing.value) {
302
+ // Edit panel open — let the textarea own the source. Reverting
303
+ // here keeps the visual in sync with the still-untouched markdown.
304
+ target.checked = !target.checked;
305
+ return;
306
+ }
307
+
308
+ const root = event.currentTarget as HTMLElement;
309
+ const taskInputs = root.querySelectorAll<HTMLInputElement>("input.md-task");
310
+ const taskIndex = Array.from(taskInputs).indexOf(target);
311
+ if (taskIndex < 0) return;
312
+
313
+ // Cross-check: if the source-side walker sees a different number
314
+ // of tasks than `marked` rendered into the DOM, the index map
315
+ // can't be trusted. The most common cause is a `- [ ]`-shaped line
316
+ // inside a 4-space indented code block (the source walker treats
317
+ // it as a task; marked treats it as code) — toggling source by
318
+ // index would corrupt the file. Refuse all clicks when this
319
+ // happens.
320
+ const sourceTasks = findTaskLines(markdownContent.value);
321
+ if (sourceTasks.length !== taskInputs.length) {
322
+ target.checked = !target.checked;
323
+ saveError.value = t("pluginMarkdown.taskCountMismatch");
324
+ return;
325
+ }
326
+
327
+ const updated = toggleTaskAt(markdownContent.value, taskIndex);
328
+ if (updated === null) {
329
+ // Source/DOM drift — refuse to write something we can't trace.
330
+ target.checked = !target.checked;
331
+ return;
332
+ }
333
+
334
+ // Optimistic local update — v-html will re-render and the
335
+ // textarea (if anyone opens it next) sees the same content.
336
+ markdownContent.value = updated;
337
+ editableMarkdown.value = updated;
338
+
339
+ const raw = props.selectedResult.data?.markdown;
340
+ if (typeof raw === "string" && isFilePath(raw)) {
341
+ // Serialize PUTs so quick successive clicks don't race each
342
+ // other on the wire — the chain captures `updated` per click.
343
+ taskPersistChain = taskPersistChain.then(() => persistTaskMarkdown(raw, updated));
344
+ } else {
345
+ // Inline content — emit so the parent stores the edit.
346
+ emit("updateResult", {
347
+ ...props.selectedResult,
348
+ data: {
349
+ ...props.selectedResult.data,
350
+ markdown: updated,
351
+ pdfPath: undefined,
352
+ },
353
+ });
354
+ }
355
+ }
356
+
224
357
  // Watch for external changes to selectedResult (when user clicks different result)
225
358
  watch(
226
359
  () => props.selectedResult.data?.markdown,
@@ -245,48 +378,6 @@ watch(
245
378
  min-height: 0;
246
379
  }
247
380
 
248
- .header-row {
249
- display: flex;
250
- align-items: center;
251
- justify-content: space-between;
252
- margin-bottom: 1em;
253
- }
254
-
255
- .document-title {
256
- font-size: 2em;
257
- margin: 0;
258
- }
259
-
260
- .button-group {
261
- display: flex;
262
- gap: 0.5em;
263
- }
264
-
265
- .download-btn {
266
- padding: 0.5em 1em;
267
- color: white;
268
- border: none;
269
- border-radius: 4px;
270
- cursor: pointer;
271
- font-size: 0.9em;
272
- display: flex;
273
- align-items: center;
274
- gap: 0.5em;
275
- }
276
-
277
- .download-btn-green {
278
- background-color: #4caf50;
279
- }
280
-
281
- .download-btn .material-icons {
282
- font-size: 1.2em;
283
- }
284
-
285
- .download-btn:disabled {
286
- opacity: 0.6;
287
- cursor: not-allowed;
288
- }
289
-
290
381
  .markdown-content :deep(h1) {
291
382
  font-size: 2rem;
292
383
  font-weight: bold;
@@ -9,12 +9,14 @@ export interface MarkdownToolData {
9
9
  }
10
10
 
11
11
  /** True when the `markdown` field is a workspace-relative file path
12
- * rather than inline content. Covers both the post-#284 canonical
13
- * path (`artifacts/documents/*.md`) and the legacy `markdowns/*.md`
14
- * prefix for sessions whose jsonl hasn't been migrated yet. */
12
+ * rather than inline content. Accepts only the canonical
13
+ * `artifacts/documents/*.md` prefix now that server-side
14
+ * `isMarkdownPath` agrees. Any legacy `markdowns/*.md` references
15
+ * in old session JSONL must be migrated via
16
+ * `scripts/migrate-legacy-artifact-paths.ts` (#773). */
15
17
  export function isFilePath(value: string): boolean {
16
18
  if (!value.endsWith(".md")) return false;
17
- return value.startsWith("artifacts/documents/") || value.startsWith("markdowns/");
19
+ return value.startsWith("artifacts/documents/");
18
20
  }
19
21
 
20
22
  const toolDefinition: ToolDefinition = {
@@ -6,6 +6,7 @@ import View from "./View.vue";
6
6
  import Preview from "./Preview.vue";
7
7
  import { apiPost } from "../../utils/api";
8
8
  import { API_ROUTES } from "../../config/apiRoutes";
9
+ import { makeUuid } from "../../utils/id";
9
10
 
10
11
  const markdownPlugin: ToolPlugin<MarkdownToolData> = {
11
12
  toolDefinition,
@@ -15,14 +16,14 @@ const markdownPlugin: ToolPlugin<MarkdownToolData> = {
15
16
  if (!result.ok) {
16
17
  return {
17
18
  toolName: TOOL_NAME,
18
- uuid: crypto.randomUUID(),
19
+ uuid: makeUuid(),
19
20
  message: result.error,
20
21
  };
21
22
  }
22
23
  return {
23
24
  ...result.data,
24
25
  toolName: TOOL_NAME,
25
- uuid: crypto.randomUUID(),
26
+ uuid: makeUuid(),
26
27
  };
27
28
  },
28
29
 
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <div class="w-full h-full flex flex-col items-center justify-center p-4 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg border-2 border-gray-200">
3
+ <div class="text-center">
4
+ <!-- Form Icon -->
5
+ <svg class="w-12 h-12 mx-auto mb-3 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6
+ <path
7
+ stroke-linecap="round"
8
+ stroke-linejoin="round"
9
+ stroke-width="2"
10
+ d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
11
+ />
12
+ </svg>
13
+
14
+ <!-- Form Title or Default -->
15
+ <h3 class="text-gray-900 font-bold text-lg mb-1 line-clamp-2">
16
+ {{ formData?.title || t("pluginPresentForm.fallbackTitle") }}
17
+ </h3>
18
+
19
+ <!-- Field Count -->
20
+ <p class="text-gray-600 text-sm mb-2">{{ t("pluginPresentForm.fieldCount", { count: fieldCount }, fieldCount) }}</p>
21
+
22
+ <!-- Completion Status -->
23
+ <div v-if="!isSubmitted" class="flex items-center justify-center gap-2">
24
+ <div class="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
25
+ <div class="h-full bg-blue-600 transition-all duration-300" :style="{ width: `${completionPercentage}%` }" />
26
+ </div>
27
+ <span class="text-xs text-gray-500">{{ `${completionPercentage}%` }}</span>
28
+ </div>
29
+
30
+ <!-- Submitted Badge -->
31
+ <div v-else class="inline-flex items-center gap-1 px-3 py-1 bg-green-100 text-green-700 rounded-full text-xs font-semibold">
32
+ <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
33
+ <path
34
+ fill-rule="evenodd"
35
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
36
+ clip-rule="evenodd"
37
+ />
38
+ </svg>
39
+ {{ t("pluginPresentForm.submitted") }}
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ import { computed } from "vue";
47
+ import { useI18n } from "vue-i18n";
48
+ import type { ToolResult } from "gui-chat-protocol";
49
+ import type { FormData } from "./types";
50
+
51
+ const { t } = useI18n();
52
+
53
+ interface FormViewState {
54
+ userResponses: Record<string, unknown>;
55
+ touched: string[];
56
+ submitted?: boolean;
57
+ }
58
+
59
+ const props = defineProps<{
60
+ result: ToolResult;
61
+ }>();
62
+
63
+ const formData = computed<FormData | null>(() => {
64
+ if (props.result?.toolName === "presentForm") {
65
+ return props.result.jsonData as FormData;
66
+ }
67
+ return null;
68
+ });
69
+
70
+ const viewState = computed<FormViewState | null>(() => {
71
+ return (props.result?.viewState as unknown as FormViewState) || null;
72
+ });
73
+
74
+ const fieldCount = computed(() => {
75
+ return formData.value?.fields.length || 0;
76
+ });
77
+
78
+ const isSubmitted = computed(() => {
79
+ return viewState.value?.submitted || false;
80
+ });
81
+
82
+ const completionPercentage = computed(() => {
83
+ if (!formData.value || isSubmitted.value) return 100;
84
+
85
+ const requiredFields = formData.value.fields.filter((field) => field.required);
86
+ if (requiredFields.length === 0) return 0;
87
+
88
+ const responses = viewState.value?.userResponses || {};
89
+ const filledRequired = requiredFields.filter((field) => {
90
+ const value = responses[field.id];
91
+ if (value === null || value === undefined) return false;
92
+ if (typeof value === "string") return value.trim() !== "";
93
+ if (Array.isArray(value)) return value.length > 0;
94
+ return true;
95
+ }).length;
96
+
97
+ return Math.round((filledRequired / requiredFields.length) * 100);
98
+ });
99
+ </script>