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
@@ -1,27 +1,31 @@
1
1
  <template>
2
2
  <div class="spreadsheet-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 spreadsheet...</div>
4
+ <div class="text-gray-500">{{ t("pluginSpreadsheet.loading") }}</div>
5
5
  </div>
6
6
  <div v-else-if="errorMessage" class="min-h-full p-8 flex items-center justify-center">
7
7
  <div class="error">{{ errorMessage }}</div>
8
8
  </div>
9
9
  <div v-else-if="!resolvedSheets || resolvedSheets.length === 0" class="min-h-full p-8 flex items-center justify-center">
10
- <div class="text-gray-500">No spreadsheet data available</div>
10
+ <div class="text-gray-500">{{ t("pluginSpreadsheet.noData") }}</div>
11
11
  </div>
12
12
  <template v-else>
13
13
  <div class="spreadsheet-content-wrapper">
14
14
  <div class="p-4">
15
15
  <div class="header">
16
16
  <h1 class="title">
17
- {{ selectedResult.title || "Spreadsheet" }}
17
+ {{ selectedResult.title || t("pluginSpreadsheet.untitled") }}
18
18
  </h1>
19
- <div class="button-group">
20
- <button class="download-btn excel-btn" @click="downloadExcel">
21
- <span class="material-icons">download</span>
22
- Excel
23
- </button>
24
- </div>
19
+ <!-- `download-btn` class kept as a name hook — referenced by
20
+ e2e/tests/spreadsheet.spec.ts via `button.download-btn`.
21
+ Styling is inline Tailwind; the class has no CSS rules. -->
22
+ <button
23
+ class="download-btn h-8 px-2.5 flex items-center gap-1 rounded bg-[#217346] hover:bg-[#1e6a3f] active:bg-[#1a5c36] text-white text-sm transition-colors"
24
+ @click="downloadExcel"
25
+ >
26
+ <span class="material-icons text-base">download</span>
27
+ {{ t("pluginSpreadsheet.excel") }}
28
+ </button>
25
29
  </div>
26
30
 
27
31
  <!-- Sheet tabs (if multiple sheets) -->
@@ -43,9 +47,9 @@
43
47
 
44
48
  <!-- Collapsible Editor -->
45
49
  <details v-if="!miniEditorOpen" ref="editorDetails" class="spreadsheet-source">
46
- <summary>Edit Spreadsheet Data</summary>
50
+ <summary>{{ t("pluginSpreadsheet.editData") }}</summary>
47
51
  <textarea ref="editorTextarea" v-model="editableData" class="spreadsheet-editor" spellcheck="false" @input="handleDataEdit"></textarea>
48
- <button class="apply-btn" :disabled="!hasChanges" @click="applyChanges">Apply Changes</button>
52
+ <button class="apply-btn" :disabled="!hasChanges" @click="applyChanges">{{ t("pluginSpreadsheet.applyChanges") }}</button>
49
53
  </details>
50
54
 
51
55
  <!-- Mini Editor at Bottom -->
@@ -57,11 +61,11 @@
57
61
  <div class="radio-group">
58
62
  <label class="radio-option">
59
63
  <input v-model="miniEditorType" type="radio" value="string" />
60
- String
64
+ {{ t("pluginSpreadsheet.stringType") }}
61
65
  </label>
62
66
  <label class="radio-option">
63
67
  <input v-model="miniEditorType" type="radio" value="object" />
64
- Formula
68
+ {{ t("pluginSpreadsheet.formulaType") }}
65
69
  </label>
66
70
  </div>
67
71
 
@@ -71,7 +75,7 @@
71
75
  v-model="miniEditorValue"
72
76
  type="text"
73
77
  class="form-input"
74
- placeholder="Value"
78
+ :placeholder="t('pluginSpreadsheet.valuePlaceholder')"
75
79
  @keyup.enter="saveMiniEditor"
76
80
  />
77
81
 
@@ -81,13 +85,19 @@
81
85
  v-model="miniEditorFormula"
82
86
  type="text"
83
87
  class="form-input"
84
- placeholder="Value or Formula (e.g., 100 or SUM(B2:B11))"
88
+ :placeholder="t('pluginSpreadsheet.valueOrFormulaPlaceholder')"
89
+ @keyup.enter="saveMiniEditor"
90
+ />
91
+ <input
92
+ v-model="miniEditorFormat"
93
+ type="text"
94
+ class="form-input"
95
+ :placeholder="t('pluginSpreadsheet.formatPlaceholder')"
85
96
  @keyup.enter="saveMiniEditor"
86
97
  />
87
- <input v-model="miniEditorFormat" type="text" class="form-input" placeholder="Format (e.g., $#,##0.00)" @keyup.enter="saveMiniEditor" />
88
98
  </template>
89
99
 
90
- <button class="save-btn" @click="saveMiniEditor">Update</button>
100
+ <button class="save-btn" @click="saveMiniEditor">{{ t("pluginSpreadsheet.update") }}</button>
91
101
  <button class="cancel-btn" @click="closeMiniEditor">✕</button>
92
102
  </div>
93
103
  </div>
@@ -97,6 +107,7 @@
97
107
 
98
108
  <script setup lang="ts">
99
109
  import { computed, ref, watch, onMounted, onUnmounted } from "vue";
110
+ import { useI18n } from "vue-i18n";
100
111
  import * as XLSX from "xlsx";
101
112
  import type { ToolResult } from "gui-chat-protocol";
102
113
  import type { SpreadsheetToolData, SpreadsheetSheet } from "./definition";
@@ -111,6 +122,7 @@ import {
111
122
  type CellValue,
112
123
  } from "./engine";
113
124
  import { applyCellHighlights, clearCellHighlights } from "./cellHighlights";
125
+ import { errorMessage as formatErrorMessage } from "../../utils/errors";
114
126
 
115
127
  // Import all spreadsheet functions to populate the function registry
116
128
  import "./engine/functions";
@@ -119,6 +131,8 @@ import { API_ROUTES } from "../../config/apiRoutes";
119
131
  import type { FilesContentResponseLike } from "./engine/responseDecoder";
120
132
  import { isObj, isRecord } from "../../utils/types";
121
133
 
134
+ const { t } = useI18n();
135
+
122
136
  /**
123
137
  * Normalize malformed data structures
124
138
  * Some models generate flat arrays instead of 2D arrays - fix them
@@ -181,8 +195,11 @@ const loading = ref(false);
181
195
  const errorMessage = ref("");
182
196
  const resolvedSheets = ref<SpreadsheetSheet[]>([]);
183
197
 
198
+ // Accepts only the canonical prefix. Any legacy `spreadsheets/*.json`
199
+ // references in old session JSONL must be migrated via
200
+ // `scripts/migrate-legacy-artifact-paths.ts` (#773).
184
201
  function isFilePath(value: unknown): value is string {
185
- return typeof value === "string" && (value.startsWith("artifacts/spreadsheets/") || value.startsWith("spreadsheets/")) && value.endsWith(".json");
202
+ return typeof value === "string" && value.startsWith("artifacts/spreadsheets/") && value.endsWith(".json");
186
203
  }
187
204
 
188
205
  async function fetchSheets(): Promise<void> {
@@ -203,7 +220,7 @@ async function fetchSheets(): Promise<void> {
203
220
  loading.value = true;
204
221
  const response = await apiGet<FilesContentResponseLike>(API_ROUTES.files.content, { path: raw });
205
222
  if (!response.ok) {
206
- errorMessage.value = `Failed to load spreadsheet: ${response.error}`;
223
+ errorMessage.value = t("pluginSpreadsheet.loadFailed", { error: response.error });
207
224
  resolvedSheets.value = [];
208
225
  loading.value = false;
209
226
  return;
@@ -230,8 +247,11 @@ fetchSheets().then(() => {
230
247
  async function persistSheets(sheets: SpreadsheetSheet[]): Promise<void> {
231
248
  const raw = props.selectedResult.data?.sheets;
232
249
  if (isFilePath(raw)) {
233
- const filename = raw.replace(/^(artifacts\/spreadsheets|spreadsheets)\//, "");
234
- const result = await apiPut<unknown>(API_ROUTES.plugins.updateSpreadsheet.replace(":filename", filename), {
250
+ // Send the full workspace-relative path so the route doesn't have
251
+ // to reconstruct one from a basename — paths under
252
+ // `artifacts/spreadsheets/` are now sharded by YYYY/MM (#764).
253
+ const result = await apiPut<unknown>(API_ROUTES.plugins.updateSpreadsheet, {
254
+ relativePath: raw,
235
255
  sheets,
236
256
  });
237
257
  if (!result.ok) {
@@ -551,7 +571,7 @@ async function applyChanges() {
551
571
 
552
572
  // Validate it's an array
553
573
  if (!Array.isArray(parsedSheets)) {
554
- throw new Error("Data must be an array of sheets");
574
+ throw new Error(t("pluginSpreadsheet.dataMustBeArray"));
555
575
  }
556
576
 
557
577
  // Persist to disk (if file-backed) and emit update
@@ -560,7 +580,7 @@ async function applyChanges() {
560
580
  // Reset to first sheet after update
561
581
  activeSheetIndex.value = 0;
562
582
  } catch (error) {
563
- alert(`Invalid JSON format: ${error instanceof Error ? error.message : "Unknown error"}`);
583
+ alert(t("pluginSpreadsheet.invalidJsonAlert", { error: formatErrorMessage(error, t("pluginSpreadsheet.unknownError")) }));
564
584
  }
565
585
  }
566
586
 
@@ -694,40 +714,6 @@ onUnmounted(() => {
694
714
  font-weight: bold;
695
715
  }
696
716
 
697
- .button-group {
698
- display: flex;
699
- gap: 0.5em;
700
- }
701
-
702
- .download-btn {
703
- padding: 0.5em 1em;
704
- color: white;
705
- border: none;
706
- border-radius: 4px;
707
- cursor: pointer;
708
- font-size: 0.9em;
709
- display: flex;
710
- align-items: center;
711
- gap: 0.5em;
712
- transition: background-color 0.2s;
713
- }
714
-
715
- .excel-btn {
716
- background-color: #217346;
717
- }
718
-
719
- .excel-btn:hover {
720
- background-color: #1e6a3f;
721
- }
722
-
723
- .excel-btn:active {
724
- background-color: #1a5c36;
725
- }
726
-
727
- .download-btn .material-icons {
728
- font-size: 1.2em;
729
- }
730
-
731
717
  /* Sheet tabs */
732
718
  .sheet-tabs {
733
719
  display: flex;
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import type { SheetData } from "./types.js";
14
+ import { errorMessage } from "../../../utils/errors";
14
15
 
15
16
  /** Shape of the `/api/files/content` response we care about. The
16
17
  * server returns more fields (kind, size, modifiedMs, …) but this
@@ -51,7 +52,7 @@ export function decodeSpreadsheetResponse(body: FilesContentResponseLike): Decod
51
52
  } catch (err) {
52
53
  return {
53
54
  kind: "error",
54
- message: `Spreadsheet JSON is malformed: ${err instanceof Error ? err.message : "parse error"}`,
55
+ message: `Spreadsheet JSON is malformed: ${errorMessage(err, "parse error")}`,
55
56
  };
56
57
  }
57
58
  if (!Array.isArray(parsed)) {
@@ -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 spreadsheetPlugin: ToolPlugin<SpreadsheetToolData> = {
11
12
  toolDefinition,
@@ -15,14 +16,14 @@ const spreadsheetPlugin: ToolPlugin<SpreadsheetToolData> = {
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
 
@@ -1,7 +1,5 @@
1
1
  <template>
2
- <div class="text-sm leading-snug" :class="textColorClass">
3
- <div class="preview-markdown" v-html="renderedMarkdown" />
4
- </div>
2
+ <div class="preview-text p-2 text-sm leading-snug" :class="textColorClass">{{ previewText }}</div>
5
3
  </template>
6
4
 
7
5
  <script setup lang="ts">
@@ -14,7 +12,6 @@ const props = defineProps<{
14
12
  result: ToolResultComplete<TextResponseData>;
15
13
  }>();
16
14
 
17
- const previewText = computed(() => props.result.data?.text ?? "");
18
15
  const messageRole = computed(() => props.result.data?.role ?? "assistant");
19
16
 
20
17
  const textColorClass = computed(() => {
@@ -28,67 +25,27 @@ const textColorClass = computed(() => {
28
25
  }
29
26
  });
30
27
 
31
- const renderedMarkdown = computed(() => marked(previewText.value, { breaks: true, gfm: true }));
28
+ const previewText = computed(() => markdownToPlainText(props.result.data?.text ?? ""));
29
+
30
+ function markdownToPlainText(markdown: string): string {
31
+ const html = marked(markdown, { breaks: true, gfm: true }) as string;
32
+ const spaced = html
33
+ .replace(/<\/(td|th)>/gi, " ")
34
+ .replace(/<\/(p|h[1-6]|li|tr|blockquote|pre|div)>/gi, "$&\n")
35
+ .replace(/<br\s*\/?>/gi, "\n");
36
+ const doc = new DOMParser().parseFromString(spaced, "text/html");
37
+ const text = doc.body.textContent ?? "";
38
+ return text.replace(/\n{3,}/g, "\n\n").trim();
39
+ }
32
40
  </script>
33
41
 
34
42
  <style scoped>
35
- .preview-markdown {
43
+ .preview-text {
36
44
  overflow: hidden;
37
45
  display: -webkit-box;
38
46
  -webkit-line-clamp: 5;
39
47
  -webkit-box-orient: vertical;
40
- /* Links inside v-html would otherwise hijack the row-level
41
- select click and navigate to the article URL. */
42
- pointer-events: none;
43
- }
44
-
45
- .preview-markdown :deep(p) {
46
- margin: 0;
47
- display: inline;
48
- }
49
-
50
- .preview-markdown :deep(h1),
51
- .preview-markdown :deep(h2),
52
- .preview-markdown :deep(h3),
53
- .preview-markdown :deep(h4),
54
- .preview-markdown :deep(h5),
55
- .preview-markdown :deep(h6) {
56
- font-size: inherit;
57
- font-weight: bold;
58
- display: inline;
59
- }
60
-
61
- .preview-markdown :deep(ul),
62
- .preview-markdown :deep(ol) {
63
- display: inline;
64
- margin: 0;
65
- padding: 0;
66
- list-style: none;
67
- }
68
-
69
- .preview-markdown :deep(li) {
70
- display: inline;
71
- }
72
-
73
- .preview-markdown :deep(li)::before {
74
- content: "• ";
75
- }
76
-
77
- .preview-markdown :deep(strong) {
78
- font-weight: bold;
79
- }
80
-
81
- .preview-markdown :deep(em) {
82
- font-style: italic;
83
- }
84
-
85
- .preview-markdown :deep(code) {
86
- font-family: monospace;
87
- font-size: 0.9em;
88
- }
89
-
90
- .preview-markdown :deep(pre) {
91
- display: inline;
92
48
  white-space: pre-wrap;
49
+ word-break: break-word;
93
50
  }
94
51
  </style>
@@ -1,13 +1,15 @@
1
1
  <template>
2
2
  <div class="h-full flex flex-col">
3
- <div v-if="isAssistant" class="flex justify-end px-4 py-2 border-b border-gray-100 shrink-0">
4
- <div class="button-group">
5
- <button class="download-btn download-btn-green" :disabled="pdfDownloading" @click="downloadPdf">
6
- <span class="material-icons">{{ pdfDownloading ? "hourglass_empty" : "download" }}</span>
7
- PDF
8
- </button>
9
- </div>
10
- <span v-if="pdfError" class="text-xs text-red-500 self-center ml-2" :title="pdfError">⚠ PDF failed</span>
3
+ <div v-if="isAssistant" class="flex items-center justify-end gap-2 px-3 py-2 border-b border-gray-100 shrink-0">
4
+ <button
5
+ 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"
6
+ :disabled="pdfDownloading"
7
+ @click="downloadPdf"
8
+ >
9
+ <span class="material-icons text-base">{{ pdfDownloading ? "hourglass_empty" : "download" }}</span>
10
+ {{ t("pluginTextResponse.pdf") }}
11
+ </button>
12
+ <span v-if="pdfError" class="text-xs text-red-500" :title="pdfError">{{ t("pluginTextResponse.pdfFailed") }}</span>
11
13
  </div>
12
14
  <div class="flex-1 overflow-hidden relative" @click.capture="openLinksInNewTab">
13
15
  <div class="text-response-container">
@@ -27,27 +29,37 @@
27
29
 
28
30
  <!-- Collapsible Editor -->
29
31
  <details v-if="editable" ref="detailsEl" class="text-response-source" data-testid="text-response-edit">
30
- <summary data-testid="text-response-edit-summary">Edit Text Content</summary>
32
+ <summary data-testid="text-response-edit-summary">{{ t("pluginTextResponse.editContent") }}</summary>
31
33
  <textarea v-model="editedText" class="text-response-editor" spellcheck="false" data-testid="text-response-edit-textarea"></textarea>
32
- <button class="apply-btn" :disabled="!hasChanges" data-testid="text-response-apply-btn" @click="applyChanges">Apply Changes</button>
34
+ <button class="apply-btn" :disabled="!hasChanges" data-testid="text-response-apply-btn" @click="applyChanges">
35
+ {{ t("pluginTextResponse.applyChanges") }}
36
+ </button>
33
37
  </details>
34
38
  </div>
35
- <button v-show="!editing" class="copy-btn" :title="copied ? 'Copied!' : 'Copy'" @click="copyText">
39
+ <button v-show="!editing" class="copy-btn" :title="copied ? t('pluginTextResponse.copiedLabel') : t('pluginTextResponse.copyLabel')" @click="copyText">
36
40
  <span class="material-icons">{{ copied ? "check" : "content_copy" }}</span>
37
41
  </button>
38
- <button v-show="editing" class="cancel-btn" @click="cancelEdit">Cancel</button>
42
+ <button v-show="editing" class="cancel-btn" @click="cancelEdit">{{ t("pluginTextResponse.cancel") }}</button>
39
43
  </div>
40
44
  </div>
41
45
  </template>
42
46
 
43
47
  <script setup lang="ts">
44
48
  import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
49
+ import { useI18n } from "vue-i18n";
45
50
  import { marked } from "marked";
46
51
  import type { ToolResult, ToolResultComplete } from "gui-chat-protocol/vue";
47
52
  import type { TextResponseData } from "./types";
48
53
  import { handleExternalLinkClick } from "../../utils/dom/externalLink";
54
+ import { classifyWorkspacePath } from "../../utils/path/workspaceLinkRouter";
55
+ import { useAppApi } from "../../composables/useAppApi";
49
56
  import { usePdfDownload } from "../../composables/usePdfDownload";
50
57
  import { useClipboardCopy } from "../../composables/useClipboardCopy";
58
+ import { buildPdfFilename } from "../../utils/files/filename";
59
+ import { extractTextResponseTitle } from "./utils";
60
+
61
+ const { t } = useI18n();
62
+ const appApi = useAppApi();
51
63
 
52
64
  const props = withDefaults(
53
65
  defineProps<{
@@ -160,7 +172,19 @@ function applyChanges() {
160
172
  const isAssistant = computed(() => (props.selectedResult.data?.role ?? "assistant") === "assistant");
161
173
 
162
174
  function openLinksInNewTab(event: MouseEvent): void {
163
- handleExternalLinkClick(event);
175
+ if (handleExternalLinkClick(event)) return;
176
+ // Internal workspace-path links (rendered by marked from agent
177
+ // Markdown): route to the appropriate view instead of letting them
178
+ // navigate the SPA to a non-existent session route.
179
+ const target = event.target as HTMLElement;
180
+ const anchor = target.closest("a");
181
+ if (!anchor) return;
182
+ const href = anchor.getAttribute("href");
183
+ if (!href || href.startsWith("#")) return;
184
+ if (classifyWorkspacePath(href)) {
185
+ event.preventDefault();
186
+ appApi.navigateToWorkspacePath(href);
187
+ }
164
188
  }
165
189
 
166
190
  const { pdfDownloading, pdfError, downloadPdf: rawDownloadPdf } = usePdfDownload();
@@ -196,7 +220,11 @@ async function copyText() {
196
220
 
197
221
  async function downloadPdf() {
198
222
  const text = props.selectedResult.data?.text ?? "";
199
- const filename = `${props.selectedResult.title ?? "response"}.pdf`;
223
+ const filename = buildPdfFilename({
224
+ name: extractTextResponseTitle(text),
225
+ fallback: "chat",
226
+ timestampMs: appApi.getResultTimestamp(props.selectedResult.uuid),
227
+ });
200
228
  await rawDownloadPdf(text, filename);
201
229
  }
202
230
  </script>
@@ -431,37 +459,6 @@ async function downloadPdf() {
431
459
  background: #cccccc;
432
460
  }
433
461
 
434
- /* Toolbar button styles */
435
- .button-group {
436
- display: flex;
437
- gap: 0.5em;
438
- }
439
-
440
- .download-btn {
441
- padding: 0.5em 1em;
442
- color: white;
443
- border: none;
444
- border-radius: 4px;
445
- cursor: pointer;
446
- font-size: 0.9em;
447
- display: flex;
448
- align-items: center;
449
- gap: 0.5em;
450
- }
451
-
452
- .download-btn-green {
453
- background-color: #4caf50;
454
- }
455
-
456
- .download-btn .material-icons {
457
- font-size: 1.2em;
458
- }
459
-
460
- .download-btn:disabled {
461
- opacity: 0.6;
462
- cursor: not-allowed;
463
- }
464
-
465
462
  .copy-btn {
466
463
  position: absolute;
467
464
  bottom: 0.3rem;
@@ -0,0 +1,25 @@
1
+ // Plugin-specific helpers for textResponse. Kept separate from
2
+ // View.vue so the pure logic is easy to unit-test from node:test
3
+ // without needing a Vue runtime.
4
+
5
+ const MAX_TITLE_CHARS = 50;
6
+
7
+ // Pull a short, human-meaningful title out of a chat reply for use as
8
+ // a download filename. Priority:
9
+ // 1. First markdown H1 ("# ...") — the model often opens a long
10
+ // reply with a heading; that's the cleanest signal.
11
+ // 2. First non-empty line, truncated.
12
+ // 3. Empty string when neither is available — caller decides the
13
+ // fallback (the PDF filename builder uses "chat").
14
+ export function extractTextResponseTitle(text: string): string {
15
+ let firstNonEmpty: string | null = null;
16
+ for (const line of text.split("\n")) {
17
+ const trimmed = line.trim();
18
+ if (!trimmed) continue;
19
+ if (trimmed.startsWith("# ")) {
20
+ return trimmed.slice(2).trim().slice(0, MAX_TITLE_CHARS);
21
+ }
22
+ if (firstNonEmpty === null) firstNonEmpty = trimmed;
23
+ }
24
+ return (firstNonEmpty ?? "").slice(0, MAX_TITLE_CHARS);
25
+ }
@@ -1,8 +1,8 @@
1
1
  <template>
2
- <div class="text-sm">
2
+ <div class="p-2 text-sm">
3
3
  <div class="flex items-center gap-1 font-medium text-gray-700 mb-1">
4
- <span>☑</span>
5
- <span>{{ completedCount }}/{{ items.length }} completed</span>
4
+ <span aria-hidden="true">{{ t("todoPreview.headerIcon") }}</span>
5
+ <span>{{ t("todoPreview.completedRatio", { done: completedCount, total: items.length }) }}</span>
6
6
  </div>
7
7
  <div
8
8
  v-for="item in preview"
@@ -10,7 +10,7 @@
10
10
  class="text-xs truncate flex items-center gap-1"
11
11
  :class="item.completed ? 'line-through text-gray-400' : 'text-gray-600'"
12
12
  >
13
- <span class="shrink-0">{{ item.completed ? "" : "" }}</span>
13
+ <span class="shrink-0">{{ item.completed ? t("todoPreview.doneIcon") : t("todoPreview.pendingIcon") }}</span>
14
14
  <span class="truncate">{{ item.text }}</span>
15
15
  <template v-if="(item.labels?.length ?? 0) > 0">
16
16
  <span
@@ -20,16 +20,21 @@
20
20
  :class="colorForLabel(label)"
21
21
  >{{ label }}</span
22
22
  >
23
- <span v-if="(item.labels?.length ?? 0) > 2" class="text-[9px] text-gray-400 shrink-0">+{{ (item.labels?.length ?? 0) - 2 }}</span>
23
+ <span v-if="(item.labels?.length ?? 0) > 2" class="text-[9px] text-gray-400 shrink-0">{{
24
+ t("todoPreview.moreLabels", { count: (item.labels?.length ?? 0) - 2 })
25
+ }}</span>
24
26
  </template>
25
27
  </div>
26
- <div v-if="more > 0" class="text-xs text-gray-400">+ {{ more }} more…</div>
28
+ <div v-if="more > 0" class="text-xs text-gray-400">{{ t("todoPreview.moreItems", { count: more }) }}</div>
27
29
  </div>
28
30
  </template>
29
31
 
30
32
  <script setup lang="ts">
31
33
  import { computed, ref, watch } from "vue";
34
+ import { useI18n } from "vue-i18n";
32
35
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
36
+
37
+ const { t } = useI18n();
33
38
  import type { TodoData, TodoItem } from "./index";
34
39
  import { useFreshPluginData } from "../../composables/useFreshPluginData";
35
40
  import { API_ROUTES } from "../../config/apiRoutes";
@@ -3,16 +3,16 @@
3
3
  <!-- API error banner — surfaces POST /api/todos failures so a
4
4
  silent add/remove/toggle becomes diagnosable. -->
5
5
  <div v-if="todoApiError" class="px-4 py-2 bg-red-50 border-b border-red-200 text-sm text-red-700" role="alert" data-testid="todo-api-error">
6
- Failed to update todos: {{ todoApiError }}
6
+ {{ t("pluginTodo.apiError", { error: todoApiError }) }}
7
7
  </div>
8
8
  <div class="flex items-center justify-between px-6 py-4 border-b border-gray-100">
9
- <h2 class="text-lg font-semibold text-gray-800">Todo List</h2>
10
- <span class="text-sm text-gray-500">{{ completedCount }}/{{ items.length }} completed</span>
9
+ <h2 class="text-lg font-semibold text-gray-800">{{ t("pluginTodo.heading") }}</h2>
10
+ <span class="text-sm text-gray-500">{{ t("pluginTodo.completedRatio", { done: completedCount, total: items.length }) }}</span>
11
11
  </div>
12
12
 
13
13
  <!-- Filter bar: only shown when at least one label is in use. -->
14
14
  <div v-if="labelInventory.length > 0" class="flex flex-wrap items-center gap-1.5 px-6 py-2 border-b border-gray-100 bg-gray-50">
15
- <span class="text-xs text-gray-500 mr-1">Filter:</span>
15
+ <span class="text-xs text-gray-500 mr-1">{{ t("pluginTodo.filter") }}</span>
16
16
  <button
17
17
  v-for="entry in labelInventory"
18
18
  :key="entry.label"
@@ -27,14 +27,21 @@
27
27
  {{ entry.label }}
28
28
  <span class="opacity-60">{{ entry.count }}</span>
29
29
  </button>
30
- <button v-if="activeFilters.size > 0" class="ml-auto text-xs text-gray-500 hover:text-gray-700" title="Clear all filters" @click="clearFilters">
31
- Clear
30
+ <button
31
+ v-if="activeFilters.size > 0"
32
+ class="ml-auto text-xs text-gray-500 hover:text-gray-700"
33
+ :title="t('pluginTodo.clearFilters')"
34
+ @click="clearFilters"
35
+ >
36
+ {{ t("pluginTodo.clearButton") }}
32
37
  </button>
33
38
  </div>
34
39
 
35
- <div v-if="items.length === 0" class="flex-1 flex items-center justify-center text-gray-400">No todo items yet</div>
40
+ <div v-if="items.length === 0" class="flex-1 flex items-center justify-center text-gray-400">{{ t("pluginTodo.noItems") }}</div>
36
41
 
37
- <div v-else-if="filteredItems.length === 0" class="flex-1 flex items-center justify-center text-gray-400 text-sm">No items match the active filter</div>
42
+ <div v-else-if="filteredItems.length === 0" class="flex-1 flex items-center justify-center text-gray-400 text-sm">
43
+ {{ t("pluginTodo.noMatchingFilter") }}
44
+ </div>
38
45
 
39
46
  <ul v-else class="flex-1 overflow-y-auto p-4 space-y-2">
40
47
  <li v-for="item in filteredItems" :key="item.id" class="rounded-lg border" :class="selectedId === item.id ? 'border-blue-400' : 'border-gray-200'">
@@ -62,10 +69,10 @@
62
69
  </div>
63
70
  <button
64
71
  class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 text-xs px-1 shrink-0"
65
- title="Delete item"
72
+ :title="t('pluginTodo.deleteItem')"
66
73
  @click.stop="remove(item)"
67
74
  >
68
-
75
+ {{ t("pluginTodo.deleteSymbol") }}
69
76
  </button>
70
77
  <span class="material-icons text-gray-400 text-sm" :title="selectedId === item.id ? 'Collapse' : 'Expand'">
71
78
  {{ selectedId === item.id ? "expand_less" : "expand_more" }}
@@ -80,20 +87,25 @@
80
87
  spellcheck="false"
81
88
  />
82
89
  <div class="flex items-center gap-2">
83
- <button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="applyItemEdit">Update</button>
84
- <button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="selectedId = null">Cancel</button>
90
+ <button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="applyItemEdit">{{ t("pluginTodo.update") }}</button>
91
+ <button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="selectedId = null">
92
+ {{ t("common.cancel") }}
93
+ </button>
85
94
  <span v-if="yamlError" class="text-xs text-red-500">{{ yamlError }}</span>
86
95
  </div>
87
96
  </div>
88
97
  </li>
89
98
  </ul>
90
99
 
91
- <button v-if="hasCompleted" class="mx-6 mb-2 text-sm text-gray-500 hover:text-gray-700 self-start" @click="clearCompleted">Clear completed</button>
100
+ <button v-if="hasCompleted" class="mx-6 mb-2 text-sm text-gray-500 hover:text-gray-700 self-start" @click="clearCompleted">
101
+ {{ t("pluginTodo.clearCompleted") }}
102
+ </button>
92
103
  </div>
93
104
  </template>
94
105
 
95
106
  <script setup lang="ts">
96
107
  import { computed, ref, watch } from "vue";
108
+ import { useI18n } from "vue-i18n";
97
109
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
98
110
  import type { TodoData, TodoItem } from "./index";
99
111
  import { useFreshPluginData } from "../../composables/useFreshPluginData";
@@ -101,6 +113,8 @@ import { apiPost } from "../../utils/api";
101
113
  import { API_ROUTES } from "../../config/apiRoutes";
102
114
  import { colorForLabel, filterByLabels, listLabelsWithCount, subtractLabels } from "./labels";
103
115
 
116
+ const { t } = useI18n();
117
+
104
118
  const props = defineProps<{
105
119
  selectedResult: ToolResultComplete<TodoData>;
106
120
  }>();