mulmoclaude 0.1.2 → 0.4.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 (251) hide show
  1. package/bin/mulmoclaude.js +7 -24
  2. package/client/assets/html2canvas-Cx501zZr-Cv5snK9D.js +5 -0
  3. package/client/assets/index-CubzmCVK.css +2 -0
  4. package/client/assets/{index-D8rhwXLq.js → index-DtcyExH9.js} +80 -61
  5. package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-DnizuhIY.js} +5 -5
  6. package/client/index.html +2 -4
  7. package/package.json +13 -13
  8. package/server/agent/attachmentConverter.ts +2 -2
  9. package/server/agent/config.ts +12 -12
  10. package/server/agent/index.ts +9 -3
  11. package/server/agent/mcp-server.ts +19 -19
  12. package/server/agent/mcp-tools/index.ts +6 -6
  13. package/server/agent/mcp-tools/x.ts +7 -6
  14. package/server/agent/prompt.ts +195 -29
  15. package/server/agent/resumeFailover.ts +5 -5
  16. package/server/agent/sandboxMounts.ts +10 -10
  17. package/server/agent/stream.ts +4 -4
  18. package/server/api/auth/bearerAuth.ts +3 -3
  19. package/server/api/auth/token.ts +2 -2
  20. package/server/api/routes/agent.ts +21 -3
  21. package/server/api/routes/config.ts +1 -1
  22. package/server/api/routes/files.ts +22 -21
  23. package/server/api/routes/html.ts +2 -2
  24. package/server/api/routes/image.ts +7 -7
  25. package/server/api/routes/mulmo-script.ts +33 -31
  26. package/server/api/routes/pdf.ts +2 -2
  27. package/server/api/routes/plugins.ts +16 -6
  28. package/server/api/routes/roles.ts +2 -2
  29. package/server/api/routes/scheduler.ts +14 -12
  30. package/server/api/routes/schedulerHandlers.ts +12 -12
  31. package/server/api/routes/schedulerTasks.ts +19 -17
  32. package/server/api/routes/sessions.ts +26 -26
  33. package/server/api/routes/sessionsCursor.ts +4 -4
  34. package/server/api/routes/skills.ts +5 -5
  35. package/server/api/routes/sources.ts +3 -3
  36. package/server/api/routes/todosColumnsHandlers.ts +30 -30
  37. package/server/api/routes/todosHandlers.ts +1 -1
  38. package/server/api/routes/todosItemsHandlers.ts +14 -14
  39. package/server/api/routes/wiki.ts +36 -22
  40. package/server/api/sandboxStatus.ts +1 -1
  41. package/server/events/notifications.ts +6 -6
  42. package/server/events/pub-sub/index.ts +3 -3
  43. package/server/events/relay-client.ts +17 -16
  44. package/server/events/scheduler-adapter.ts +20 -20
  45. package/server/events/session-store/index.ts +10 -10
  46. package/server/events/task-manager/index.ts +7 -7
  47. package/server/index.ts +59 -65
  48. package/server/system/config.ts +5 -5
  49. package/server/system/credentials.ts +7 -5
  50. package/server/system/env.ts +5 -5
  51. package/server/utils/date.ts +18 -18
  52. package/server/utils/files/atomic.ts +16 -16
  53. package/server/utils/files/html-io.ts +5 -5
  54. package/server/utils/files/image-store.ts +19 -8
  55. package/server/utils/files/journal-io.ts +4 -4
  56. package/server/utils/files/json.ts +5 -5
  57. package/server/utils/files/markdown-store.ts +4 -4
  58. package/server/utils/files/naming.ts +2 -2
  59. package/server/utils/files/reference-dirs-io.ts +3 -3
  60. package/server/utils/files/roles-io.ts +12 -12
  61. package/server/utils/files/safe.ts +14 -14
  62. package/server/utils/files/scheduler-io.ts +5 -5
  63. package/server/utils/files/scheduler-overrides-io.ts +2 -2
  64. package/server/utils/files/session-io.ts +35 -35
  65. package/server/utils/files/spreadsheet-store.ts +7 -7
  66. package/server/utils/files/todos-io.ts +9 -9
  67. package/server/utils/files/user-tasks-io.ts +5 -5
  68. package/server/utils/files/workspace-io.ts +12 -12
  69. package/server/utils/gemini.ts +2 -2
  70. package/server/utils/gitignore.ts +9 -9
  71. package/server/utils/json.ts +5 -5
  72. package/server/utils/logBackgroundError.ts +12 -3
  73. package/server/utils/markdown.ts +5 -5
  74. package/server/utils/port.d.mts +6 -0
  75. package/server/utils/port.mjs +48 -0
  76. package/server/utils/request.ts +12 -6
  77. package/server/utils/spawn.ts +1 -1
  78. package/server/utils/types.ts +2 -2
  79. package/server/workspace/chat-index/indexer.ts +15 -15
  80. package/server/workspace/chat-index/summarizer.ts +4 -4
  81. package/server/workspace/custom-dirs.ts +16 -16
  82. package/server/workspace/journal/archivist.ts +35 -35
  83. package/server/workspace/journal/dailyPass.ts +31 -28
  84. package/server/workspace/journal/diff.ts +2 -2
  85. package/server/workspace/journal/index.ts +4 -4
  86. package/server/workspace/journal/indexFile.ts +29 -25
  87. package/server/workspace/journal/optimizationPass.ts +2 -2
  88. package/server/workspace/journal/state.ts +6 -6
  89. package/server/workspace/paths.ts +3 -3
  90. package/server/workspace/reference-dirs.ts +20 -20
  91. package/server/workspace/roles.ts +6 -6
  92. package/server/workspace/skills/discovery.ts +4 -4
  93. package/server/workspace/skills/parser.ts +6 -6
  94. package/server/workspace/skills/scheduler.ts +3 -3
  95. package/server/workspace/skills/user-tasks.ts +34 -34
  96. package/server/workspace/skills/writer.ts +3 -3
  97. package/server/workspace/sources/arxivDiscovery.ts +10 -10
  98. package/server/workspace/sources/classifier.ts +7 -7
  99. package/server/workspace/sources/fetchers/arxiv.ts +7 -7
  100. package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
  101. package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
  102. package/server/workspace/sources/fetchers/rss.ts +5 -5
  103. package/server/workspace/sources/fetchers/rssParser.ts +4 -4
  104. package/server/workspace/sources/interests.ts +12 -12
  105. package/server/workspace/sources/paths.ts +6 -6
  106. package/server/workspace/sources/pipeline/fetch.ts +36 -13
  107. package/server/workspace/sources/pipeline/index.ts +8 -13
  108. package/server/workspace/sources/pipeline/notify.ts +3 -3
  109. package/server/workspace/sources/pipeline/plan.ts +15 -13
  110. package/server/workspace/sources/pipeline/write.ts +5 -5
  111. package/server/workspace/sources/rateLimiter.ts +1 -1
  112. package/server/workspace/sources/registry.ts +16 -16
  113. package/server/workspace/sources/robots.ts +14 -14
  114. package/server/workspace/sources/sourceState.ts +17 -10
  115. package/server/workspace/sources/types.ts +9 -0
  116. package/server/workspace/sources/urls.ts +1 -1
  117. package/server/workspace/tool-trace/classify.ts +4 -4
  118. package/server/workspace/tool-trace/index.ts +1 -1
  119. package/server/workspace/tool-trace/writeSearch.ts +26 -16
  120. package/server/workspace/wiki-backlinks/index.ts +8 -8
  121. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
  122. package/server/workspace/workspace.ts +7 -7
  123. package/src/App.vue +315 -141
  124. package/src/components/CanvasViewToggle.vue +10 -7
  125. package/src/components/ChatInput.vue +67 -33
  126. package/src/components/FileContentHeader.vue +7 -4
  127. package/src/components/FileContentRenderer.vue +20 -6
  128. package/src/components/FileTree.vue +6 -3
  129. package/src/components/FileTreePane.vue +11 -8
  130. package/src/components/FilesView.vue +5 -3
  131. package/src/components/LockStatusPopup.vue +17 -14
  132. package/src/components/NotificationBell.vue +14 -5
  133. package/src/components/NotificationToast.vue +6 -3
  134. package/src/components/PluginLauncher.vue +19 -56
  135. package/src/components/RightSidebar.vue +13 -10
  136. package/src/components/RoleSelector.vue +2 -2
  137. package/src/components/SessionHistoryPanel.vue +38 -34
  138. package/src/components/SessionTabBar.vue +8 -10
  139. package/src/components/SettingsMcpTab.vue +49 -36
  140. package/src/components/SettingsModal.vue +24 -22
  141. package/src/components/SettingsReferenceDirsTab.vue +39 -34
  142. package/src/components/SettingsWorkspaceDirsTab.vue +37 -27
  143. package/src/components/SidebarHeader.vue +25 -4
  144. package/src/components/StackView.vue +4 -1
  145. package/src/components/SuggestionsPanel.vue +7 -4
  146. package/src/components/TodoExplorer.vue +26 -15
  147. package/src/components/ToolResultsPanel.vue +27 -13
  148. package/src/components/todo/TodoAddDialog.vue +19 -14
  149. package/src/components/todo/TodoEditDialog.vue +7 -2
  150. package/src/components/todo/TodoEditPanel.vue +17 -12
  151. package/src/components/todo/TodoKanbanView.vue +10 -5
  152. package/src/components/todo/TodoListView.vue +10 -7
  153. package/src/components/todo/TodoTableView.vue +5 -2
  154. package/src/composables/useAppApi.ts +9 -0
  155. package/src/composables/useClickOutside.ts +2 -2
  156. package/src/composables/useDynamicFavicon.ts +172 -37
  157. package/src/composables/useEventListeners.ts +7 -8
  158. package/src/composables/useFaviconState.ts +13 -2
  159. package/src/composables/useFileSelection.ts +24 -6
  160. package/src/composables/useFreshPluginData.ts +3 -3
  161. package/src/composables/useKeyNavigation.ts +11 -11
  162. package/src/composables/useLayoutMode.ts +32 -0
  163. package/src/composables/useMcpTools.ts +2 -2
  164. package/src/composables/useNotifications.ts +3 -3
  165. package/src/composables/usePdfDownload.ts +4 -4
  166. package/src/composables/usePendingCalls.ts +1 -1
  167. package/src/composables/usePubSub.ts +10 -10
  168. package/src/composables/useRoles.ts +1 -1
  169. package/src/composables/useSandboxStatus.ts +1 -1
  170. package/src/composables/useSessionDerived.ts +3 -3
  171. package/src/composables/useSessionHistory.ts +7 -17
  172. package/src/composables/useSessionSync.ts +8 -8
  173. package/src/composables/useViewLayout.ts +20 -34
  174. package/src/config/roles.ts +2 -2
  175. package/src/lang/de.ts +536 -0
  176. package/src/lang/en.ts +558 -0
  177. package/src/lang/es.ts +543 -0
  178. package/src/lang/fr.ts +536 -0
  179. package/src/lang/ja.ts +536 -0
  180. package/src/lang/ko.ts +540 -0
  181. package/src/lang/pt-BR.ts +534 -0
  182. package/src/lang/zh.ts +537 -0
  183. package/src/lib/vue-i18n.ts +97 -0
  184. package/src/main.ts +2 -0
  185. package/src/plugins/canvas/View.vue +102 -186
  186. package/src/plugins/canvas/definition.ts +0 -8
  187. package/src/plugins/chart/Preview.vue +5 -5
  188. package/src/plugins/chart/View.vue +9 -4
  189. package/src/plugins/manageRoles/Preview.vue +4 -1
  190. package/src/plugins/manageRoles/View.vue +59 -43
  191. package/src/plugins/manageSkills/Preview.vue +8 -3
  192. package/src/plugins/manageSkills/View.vue +29 -25
  193. package/src/plugins/manageSource/Preview.vue +2 -2
  194. package/src/plugins/manageSource/View.vue +73 -52
  195. package/src/plugins/markdown/Preview.vue +1 -1
  196. package/src/plugins/markdown/View.vue +26 -36
  197. package/src/plugins/presentHtml/Preview.vue +1 -1
  198. package/src/plugins/presentHtml/View.vue +7 -4
  199. package/src/plugins/presentHtml/helpers.ts +8 -8
  200. package/src/plugins/presentMulmoScript/Preview.vue +1 -1
  201. package/src/plugins/presentMulmoScript/View.vue +40 -30
  202. package/src/plugins/presentMulmoScript/helpers.ts +1 -1
  203. package/src/plugins/scheduler/Preview.vue +13 -10
  204. package/src/plugins/scheduler/TasksTab.vue +57 -28
  205. package/src/plugins/scheduler/View.vue +28 -19
  206. package/src/plugins/scheduler/formatSchedule.ts +93 -0
  207. package/src/plugins/spreadsheet/Preview.vue +8 -3
  208. package/src/plugins/spreadsheet/View.vue +21 -12
  209. package/src/plugins/textResponse/Preview.vue +15 -58
  210. package/src/plugins/textResponse/View.vue +29 -9
  211. package/src/plugins/todo/Preview.vue +13 -8
  212. package/src/plugins/todo/View.vue +38 -24
  213. package/src/plugins/todo/composables/useTodos.ts +5 -5
  214. package/src/plugins/ui-image/ImagePreview.vue +6 -3
  215. package/src/plugins/ui-image/ImageView.vue +7 -4
  216. package/src/plugins/wiki/Preview.vue +10 -7
  217. package/src/plugins/wiki/View.vue +202 -81
  218. package/src/plugins/wiki/helpers.ts +4 -4
  219. package/src/plugins/wiki/route.ts +112 -0
  220. package/src/router/guards.ts +46 -28
  221. package/src/router/index.ts +41 -26
  222. package/src/types/session.ts +4 -3
  223. package/src/types/vue-i18n.d.ts +20 -0
  224. package/src/utils/agent/request.ts +22 -3
  225. package/src/utils/canvas/layoutMode.ts +26 -0
  226. package/src/utils/dom/scrollable.ts +2 -2
  227. package/src/utils/files/expandedDirs.ts +1 -1
  228. package/src/utils/files/sortChildren.ts +6 -6
  229. package/src/utils/format/frontmatter.ts +6 -6
  230. package/src/utils/image/cacheBust.ts +16 -0
  231. package/src/utils/image/resolve.ts +16 -0
  232. package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
  233. package/src/utils/markdown/extractFirstH1.ts +2 -2
  234. package/src/utils/path/relativeLink.ts +15 -15
  235. package/src/utils/path/workspaceLinkRouter.ts +81 -0
  236. package/src/utils/role/icon.ts +2 -2
  237. package/src/utils/role/merge.ts +2 -2
  238. package/src/utils/role/plugins.ts +1 -1
  239. package/src/utils/session/sessionFactory.ts +2 -2
  240. package/src/utils/session/sessionHelpers.ts +2 -2
  241. package/src/utils/tools/dedup.ts +4 -4
  242. package/src/utils/tools/result.ts +3 -3
  243. package/src/utils/types.ts +2 -2
  244. package/src/vite-env.d.ts +9 -0
  245. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
  246. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
  247. package/client/assets/index-KNLBjwuh.css +0 -1
  248. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
  249. package/src/composables/useCanvasViewMode.ts +0 -121
  250. package/src/utils/canvas/viewMode.ts +0 -46
  251. /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
@@ -1,9 +1,9 @@
1
1
  <template>
2
2
  <div class="h-full flex flex-col overflow-hidden">
3
3
  <div class="px-4 py-2 border-b border-gray-100 shrink-0 flex items-center justify-between gap-2">
4
- <span class="text-sm font-medium text-gray-700 truncate"> Information sources </span>
4
+ <span class="text-sm font-medium text-gray-700 truncate"> {{ t("pluginManageSource.heading") }} </span>
5
5
  <div class="flex items-center gap-2 shrink-0">
6
- <span class="text-xs text-gray-500"> {{ sources.length }} source{{ sources.length === 1 ? "" : "s" }} </span>
6
+ <span class="text-xs text-gray-500"> {{ t("pluginManageSource.sourceCount", sources.length, { named: { count: sources.length } }) }} </span>
7
7
  <button
8
8
  class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:opacity-50"
9
9
  :disabled="adding || busy === 'rebuild'"
@@ -11,7 +11,7 @@
11
11
  @click="startAdd"
12
12
  >
13
13
  <span class="material-icons text-sm align-middle">add</span>
14
- Add
14
+ {{ t("pluginManageSource.addButton") }}
15
15
  </button>
16
16
  <button
17
17
  class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:opacity-50"
@@ -20,7 +20,7 @@
20
20
  @click="rebuild"
21
21
  >
22
22
  <span class="material-icons text-sm align-middle">refresh</span>
23
- {{ busy === "rebuild" ? "Rebuilding…" : "Rebuild now" }}
23
+ {{ busy === "rebuild" ? t("pluginManageSource.rebuilding") : t("pluginManageSource.rebuildNow") }}
24
24
  </button>
25
25
  </div>
26
26
  </div>
@@ -28,12 +28,12 @@
28
28
  <div v-if="adding" class="px-4 py-3 border-b border-blue-200 bg-blue-50/50 shrink-0 space-y-2" data-testid="sources-add-form">
29
29
  <div class="flex flex-wrap items-center gap-2">
30
30
  <label class="text-xs text-gray-700">
31
- Type
31
+ {{ t("pluginManageSource.typeField") }}
32
32
  <select v-model="draft.kind" class="ml-1 text-xs border border-gray-300 rounded px-1 py-0.5" data-testid="sources-draft-kind" @change="onKindChange">
33
- <option value="rss">RSS</option>
34
- <option value="github-releases">GitHub releases</option>
35
- <option value="github-issues">GitHub issues</option>
36
- <option value="arxiv">arXiv</option>
33
+ <option value="rss">{{ t("pluginManageSource.kindRss") }}</option>
34
+ <option value="github-releases">{{ t("pluginManageSource.kindGithubReleases") }}</option>
35
+ <option value="github-issues">{{ t("pluginManageSource.kindGithubIssues") }}</option>
36
+ <option value="arxiv">{{ t("pluginManageSource.kindArxiv") }}</option>
37
37
  </select>
38
38
  </label>
39
39
  <input
@@ -46,7 +46,7 @@
46
46
  <input
47
47
  v-model="draft.title"
48
48
  class="w-40 text-xs border border-gray-300 rounded px-2 py-1"
49
- placeholder="Title (optional)"
49
+ :placeholder="t('pluginManageSource.titlePlaceholder')"
50
50
  data-testid="sources-draft-title"
51
51
  @keydown.enter="commitAdd"
52
52
  />
@@ -57,7 +57,7 @@
57
57
  </span>
58
58
  <div class="flex gap-2">
59
59
  <button class="px-2 py-1 rounded border border-gray-300 text-gray-600 hover:bg-gray-50" data-testid="sources-draft-cancel" @click="cancelAdd">
60
- Cancel
60
+ {{ t("common.cancel") }}
61
61
  </button>
62
62
  <button
63
63
  class="px-2 py-1 rounded bg-blue-500 text-white hover:bg-blue-600 disabled:opacity-50"
@@ -65,7 +65,7 @@
65
65
  data-testid="sources-draft-add"
66
66
  @click="commitAdd"
67
67
  >
68
- {{ busy === "add" ? "Adding…" : "Add + Rebuild" }}
68
+ {{ busy === "add" ? t("pluginManageSource.addingLabel") : t("pluginManageSource.addAndRebuild") }}
69
69
  </button>
70
70
  </div>
71
71
  </div>
@@ -85,10 +85,11 @@
85
85
 
86
86
  <div class="flex-1 overflow-y-auto">
87
87
  <div v-if="sources.length === 0" class="flex flex-col items-center justify-center h-full p-6 gap-4" data-testid="sources-empty">
88
- <p class="text-sm text-gray-500 italic text-center max-w-md">
89
- No sources registered yet. Pick a starter pack below, click
90
- <strong>+ Add</strong> above, or ask Claude to register one.
91
- </p>
88
+ <i18n-t keypath="pluginManageSource.emptyPickPack" tag="p" class="text-sm text-gray-500 italic text-center max-w-md">
89
+ <template #addBold>
90
+ <strong>{{ t("pluginManageSource.emptyAddStrong") }}</strong>
91
+ </template>
92
+ </i18n-t>
92
93
  <div class="w-full max-w-md space-y-2" data-testid="sources-presets">
93
94
  <button
94
95
  v-for="preset in PRESETS"
@@ -102,12 +103,14 @@
102
103
  <span class="text-sm font-medium text-gray-800">
103
104
  {{ preset.label }}
104
105
  </span>
105
- <span class="text-[11px] text-gray-500 shrink-0"> {{ preset.entries.length }} source{{ preset.entries.length === 1 ? "" : "s" }} </span>
106
+ <span class="text-[11px] text-gray-500 shrink-0">
107
+ {{ t("pluginManageSource.sourceCount", preset.entries.length, { named: { count: preset.entries.length } }) }}
108
+ </span>
106
109
  </div>
107
110
  <div class="text-xs text-gray-500 mt-1">
108
111
  {{ preset.description }}
109
112
  </div>
110
- <div v-if="busy === 'preset-' + preset.id" class="text-xs text-blue-600 mt-1 italic">Registering + fetching…</div>
113
+ <div v-if="busy === 'preset-' + preset.id" class="text-xs text-blue-600 mt-1 italic">{{ t("pluginManageSource.registering") }}</div>
111
114
  </button>
112
115
  </div>
113
116
  </div>
@@ -151,7 +154,7 @@
151
154
  :data-testid="`source-remove-${source.slug}`"
152
155
  @click="remove(source.slug)"
153
156
  >
154
- {{ busy === source.slug ? "Removing…" : "Remove" }}
157
+ {{ busy === source.slug ? t("pluginManageSource.removingLabel") : t("pluginManageSource.removeLabel") }}
155
158
  </button>
156
159
  </li>
157
160
  </ul>
@@ -162,14 +165,14 @@
162
165
  <div v-if="sources.length > 0 && (briefLoading || briefHtml || briefError)" class="p-4" data-testid="sources-brief">
163
166
  <div class="flex items-baseline justify-between mb-2">
164
167
  <h3 class="text-sm font-semibold text-gray-800">
165
- Today's brief
166
- <span v-if="briefDate" class="text-xs text-gray-400 font-normal"> ({{ briefDate }}) </span>
168
+ {{ t("pluginManageSource.todaysBrief") }}
169
+ <span v-if="briefDate" class="text-xs text-gray-400 font-normal"> {{ t("pluginManageSource.briefDateLabel", { date: briefDate }) }} </span>
167
170
  </h3>
168
171
  <button v-if="briefFilePath" class="text-[11px] text-gray-500 hover:text-gray-700" :title="briefFilePath">
169
172
  {{ briefFilePath }}
170
173
  </button>
171
174
  </div>
172
- <div v-if="briefLoading" class="text-xs text-gray-500 italic">Loading today's brief…</div>
175
+ <div v-if="briefLoading" class="text-xs text-gray-500 italic">{{ t("pluginManageSource.todaysBriefLoading") }}</div>
173
176
  <div v-else-if="briefError" class="text-xs text-gray-500 italic" data-testid="sources-brief-empty">
174
177
  {{ briefError }}
175
178
  </div>
@@ -179,15 +182,24 @@
179
182
  </div>
180
183
 
181
184
  <div v-if="lastRebuild" class="px-4 py-2 border-t border-gray-100 shrink-0 text-xs text-gray-600" data-testid="sources-rebuild-summary">
182
- Last rebuild ({{ lastRebuild.isoDate }}): <strong>{{ lastRebuild.itemCount }}</strong> items from <strong>{{ lastRebuild.plannedCount }}</strong> sources,
183
- <strong>{{ lastRebuild.duplicateCount }}</strong> duplicates dropped.
184
- <span v-if="lastRebuild.archiveErrors.length > 0" class="text-red-600"> ({{ lastRebuild.archiveErrors.length }} archive errors) </span>
185
+ {{
186
+ t("pluginManageSource.lastRebuildSummary", {
187
+ date: lastRebuild.isoDate,
188
+ itemCount: lastRebuild.itemCount,
189
+ planned: lastRebuild.plannedCount,
190
+ duplicates: lastRebuild.duplicateCount,
191
+ })
192
+ }}
193
+ <span v-if="lastRebuild.archiveErrors.length > 0" class="text-red-600">
194
+ {{ t("pluginManageSource.archiveErrorsSuffix", { count: lastRebuild.archiveErrors.length }) }}
195
+ </span>
185
196
  </div>
186
197
  </div>
187
198
  </template>
188
199
 
189
200
  <script setup lang="ts">
190
201
  import { computed, onMounted, ref, watch } from "vue";
202
+ import { useI18n } from "vue-i18n";
191
203
  import { marked } from "marked";
192
204
  import DOMPurify from "dompurify";
193
205
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
@@ -195,6 +207,8 @@ import type { ManageSourceData, RebuildSummary, Source } from "./index";
195
207
  import { apiGet, apiPost, apiDelete } from "../../utils/api";
196
208
  import { API_ROUTES } from "../../config/apiRoutes";
197
209
 
210
+ const { t } = useI18n();
211
+
198
212
  const props = defineProps<{
199
213
  selectedResult: ToolResultComplete<ManageSourceData>;
200
214
  }>();
@@ -245,12 +259,12 @@ function onKindChange(): void {
245
259
  const primaryPlaceholder = computed(() => {
246
260
  switch (draft.value.kind) {
247
261
  case "rss":
248
- return "https://news.ycombinator.com/rss";
262
+ return t("pluginManageSource.primaryRssPlaceholder");
249
263
  case "github-releases":
250
264
  case "github-issues":
251
- return "https://github.com/owner/repo (or owner/repo)";
265
+ return t("pluginManageSource.primaryGithubPlaceholder");
252
266
  case "arxiv":
253
- return "cat:cs.CL";
267
+ return t("pluginManageSource.primaryArxivPlaceholder");
254
268
  }
255
269
  return "";
256
270
  });
@@ -258,13 +272,13 @@ const primaryPlaceholder = computed(() => {
258
272
  const primaryHint = computed(() => {
259
273
  switch (draft.value.kind) {
260
274
  case "rss":
261
- return "Feed URL (RSS 2.0 / Atom / RDF)";
275
+ return t("pluginManageSource.primaryRssHint");
262
276
  case "github-releases":
263
- return "GitHub repo URL or owner/repo — fetches releases";
277
+ return t("pluginManageSource.primaryGithubRelHint");
264
278
  case "github-issues":
265
- return "GitHub repo URL or owner/repo — fetches issues";
279
+ return t("pluginManageSource.primaryGithubIssHint");
266
280
  case "arxiv":
267
- return "arXiv search query (e.g. cat:cs.CL or au:hinton)";
281
+ return t("pluginManageSource.primaryArxivHint");
268
282
  }
269
283
  return "";
270
284
  });
@@ -351,11 +365,11 @@ async function commitAdd(): Promise<void> {
351
365
  busy.value = "add";
352
366
  const response = await apiPost<unknown>(API_ROUTES.sources.create, payload);
353
367
  if (!response.ok) {
354
- draftError.value = response.error || "Failed to register source";
368
+ draftError.value = response.error || t("pluginManageSource.flashRegisterFailed");
355
369
  busy.value = null;
356
370
  return;
357
371
  }
358
- flash(`Registered. Fetching new items…`);
372
+ flash(t("pluginManageSource.flashRegistered"));
359
373
  adding.value = false;
360
374
  await refreshList();
361
375
  // C: auto-rebuild so the user sees items without an extra click.
@@ -443,7 +457,7 @@ async function installPreset(preset: Preset): Promise<void> {
443
457
  const alreadyHave = new Set(sources.value.map((source) => source.slug));
444
458
  const toRegister = preset.entries.filter((entry) => !alreadyHave.has(entry.slug));
445
459
  if (toRegister.length === 0) {
446
- flash(`All sources in "${preset.label}" are already registered.`);
460
+ flash(t("pluginManageSource.flashPresetAlreadyRegistered", { label: preset.label }));
447
461
  busy.value = null;
448
462
  return;
449
463
  }
@@ -465,9 +479,16 @@ async function installPreset(preset: Preset): Promise<void> {
465
479
  }
466
480
  }
467
481
  if (failures.length > 0) {
468
- flash(`Registered ${toRegister.length - failures.length}/${toRegister.length}. Errors: ${failures.join("; ")}`, true);
482
+ flash(
483
+ t("pluginManageSource.flashPresetPartial", {
484
+ ok: toRegister.length - failures.length,
485
+ total: toRegister.length,
486
+ errors: failures.join("; "),
487
+ }),
488
+ true,
489
+ );
469
490
  } else {
470
- flash(`Registered ${toRegister.length} source${toRegister.length === 1 ? "" : "s"} from "${preset.label}". Fetching…`);
491
+ flash(t("pluginManageSource.flashPresetRegistered", toRegister.length, { named: { count: toRegister.length, label: preset.label } }));
471
492
  }
472
493
  await refreshList();
473
494
  await rebuildInline();
@@ -479,12 +500,12 @@ async function installPreset(preset: Preset): Promise<void> {
479
500
  async function rebuildInline(): Promise<void> {
480
501
  const response = await apiPost<RebuildSummary>(API_ROUTES.sources.rebuild);
481
502
  if (!response.ok) {
482
- flash(`Register succeeded but rebuild failed: ${response.error}`, true);
503
+ flash(t("pluginManageSource.flashRegisterSucceededRebuildFailed", { error: response.error }), true);
483
504
  return;
484
505
  }
485
506
  const summary = response.data;
486
507
  lastRebuild.value = summary;
487
- flash(`Ready: ${summary.itemCount} items from ${summary.plannedCount} source${summary.plannedCount === 1 ? "" : "s"}.`);
508
+ flash(t("pluginManageSource.flashRebuildReady", summary.plannedCount, { named: { itemCount: summary.itemCount, planned: summary.plannedCount } }));
488
509
  await loadBrief(summary.isoDate);
489
510
  }
490
511
 
@@ -525,13 +546,13 @@ watch(
525
546
  function kindLabel(kind: Source["fetcherKind"]): string {
526
547
  switch (kind) {
527
548
  case "rss":
528
- return "RSS";
549
+ return t("pluginManageSource.kindRss");
529
550
  case "github-releases":
530
- return "GitHub rel";
551
+ return t("pluginManageSource.kindGithubRel");
531
552
  case "github-issues":
532
- return "GitHub iss";
553
+ return t("pluginManageSource.kindGithubIss");
533
554
  case "arxiv":
534
- return "arXiv";
555
+ return t("pluginManageSource.kindArxiv");
535
556
  }
536
557
  }
537
558
 
@@ -559,22 +580,22 @@ function flash(message: string, isError = false): void {
559
580
  async function refreshList(): Promise<void> {
560
581
  const response = await apiGet<{ sources: Source[] }>(API_ROUTES.sources.list);
561
582
  if (!response.ok) {
562
- flash(`Failed to refresh sources: ${response.error}`, true);
583
+ flash(t("pluginManageSource.flashRefreshListFailed", { error: response.error }), true);
563
584
  return;
564
585
  }
565
586
  localSources.value = response.data.sources;
566
587
  }
567
588
 
568
589
  async function remove(slug: string): Promise<void> {
569
- if (!confirm(`Remove source "${slug}"?`)) return;
590
+ if (!confirm(t("pluginManageSource.confirmRemove", { slug }))) return;
570
591
  busy.value = slug;
571
592
  const response = await apiDelete<unknown>(API_ROUTES.sources.remove.replace(":slug", encodeURIComponent(slug)));
572
593
  busy.value = null;
573
594
  if (!response.ok) {
574
- flash(`Remove failed: ${response.error}`, true);
595
+ flash(t("pluginManageSource.flashRemoveFailed", { error: response.error }), true);
575
596
  return;
576
597
  }
577
- flash(`Removed "${slug}".`);
598
+ flash(t("pluginManageSource.flashRemoved", { slug }));
578
599
  await refreshList();
579
600
  }
580
601
 
@@ -582,13 +603,13 @@ async function rebuild(): Promise<void> {
582
603
  busy.value = "rebuild";
583
604
  const response = await apiPost<RebuildSummary>(API_ROUTES.sources.rebuild);
584
605
  if (!response.ok) {
585
- flash(`Rebuild failed: ${response.error}`, true);
606
+ flash(t("pluginManageSource.flashRebuildFailed", { error: response.error }), true);
586
607
  busy.value = null;
587
608
  return;
588
609
  }
589
610
  const summary = response.data;
590
611
  lastRebuild.value = summary;
591
- flash(`Rebuild complete: ${summary.itemCount} items from ${summary.plannedCount} sources.`);
612
+ flash(t("pluginManageSource.flashRebuildComplete", { itemCount: summary.itemCount, planned: summary.plannedCount }));
592
613
  await Promise.all([refreshList(), loadBrief(summary.isoDate)]);
593
614
  busy.value = null;
594
615
  }
@@ -638,16 +659,16 @@ async function loadBrief(isoDate: string): Promise<void> {
638
659
  if (!response.ok) {
639
660
  if (response.status === 404) {
640
661
  briefMarkdown.value = "";
641
- briefError.value = "No brief written for this date yet. Click Rebuild now.";
662
+ briefError.value = t("pluginManageSource.briefNone");
642
663
  } else {
643
- briefError.value = response.error || "Failed to load brief";
664
+ briefError.value = response.error || t("pluginManageSource.briefLoadFailed");
644
665
  }
645
666
  briefLoading.value = false;
646
667
  return;
647
668
  }
648
669
  briefMarkdown.value = response.data.content ?? "";
649
670
  if (!briefMarkdown.value.trim()) {
650
- briefError.value = "Today's brief is empty.";
671
+ briefError.value = t("pluginManageSource.briefEmpty");
651
672
  }
652
673
  briefLoading.value = false;
653
674
  }
@@ -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,46 @@
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 justify-end px-4 py-2 border-b border-gray-100 shrink-0">
14
+ <div class="button-group">
15
+ <button class="download-btn download-btn-green" :disabled="pdfDownloading" @click="downloadPdf">
16
+ <span class="material-icons">{{ pdfDownloading ? "hourglass_empty" : "download" }}</span>
17
+ {{ t("pluginMarkdown.pdf") }}
18
+ </button>
19
+ </div>
20
+ <span v-if="pdfError" class="text-xs text-red-500 self-center ml-2" :title="pdfError">{{ t("pluginMarkdown.pdfFailedShort") }}</span>
21
+ </div>
13
22
  <div v-if="loadError" class="load-error-banner" role="alert">
14
- Failed to refresh document: {{ loadError }} — showing last successfully loaded content.
23
+ {{ t("pluginMarkdown.refreshFailed", { error: loadError }) }}
15
24
  </div>
16
25
  <div class="markdown-content-wrapper">
17
26
  <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
27
  <div class="markdown-content prose prose-slate max-w-none" v-html="renderedHtml"></div>
31
28
  </div>
32
29
  </div>
33
30
 
34
31
  <div class="bottom-bar-wrapper">
35
32
  <details ref="sourceDetails" class="markdown-source" @toggle="onDetailsToggle">
36
- <summary>Edit Markdown Source</summary>
33
+ <summary>{{ t("pluginMarkdown.editSource") }}</summary>
37
34
  <textarea v-model="editableMarkdown" class="markdown-editor" spellcheck="false"></textarea>
38
35
  <div class="editor-actions">
39
36
  <button class="apply-btn" :disabled="!hasChanges || saving" @click="applyMarkdown">
40
- {{ saving ? "Saving..." : "Apply Changes" }}
37
+ {{ saving ? t("pluginMarkdown.saving") : t("pluginMarkdown.applyChanges") }}
41
38
  </button>
42
- <button class="cancel-btn" @click="cancelEdit">Cancel</button>
39
+ <button class="cancel-btn" @click="cancelEdit">{{ t("pluginMarkdown.cancel") }}</button>
43
40
  </div>
44
- <p v-if="saveError" class="save-error" role="alert">⚠ {{ saveError }}</p>
41
+ <p v-if="saveError" class="save-error" role="alert">{{ t("pluginMarkdown.saveError", { error: saveError }) }}</p>
45
42
  </details>
46
- <button v-show="!editing" class="copy-btn" :title="copied ? 'Copied!' : 'Copy'" @click="copyText">
43
+ <button v-show="!editing" class="copy-btn" :title="copied ? t('pluginMarkdown.copiedLabel') : t('pluginMarkdown.copyLabel')" @click="copyText">
47
44
  <span class="material-icons">{{ copied ? "check" : "content_copy" }}</span>
48
45
  </button>
49
46
  </div>
@@ -53,7 +50,10 @@
53
50
 
54
51
  <script setup lang="ts">
55
52
  import { computed, ref, watch, nextTick } from "vue";
53
+ import { useI18n } from "vue-i18n";
56
54
  import { marked } from "marked";
55
+
56
+ const { t } = useI18n();
57
57
  import type { ToolResult } from "gui-chat-protocol";
58
58
  import { isFilePath, type MarkdownToolData } from "./definition";
59
59
  import { rewriteMarkdownImageRefs } from "../../utils/image/rewriteMarkdownImageRefs";
@@ -156,8 +156,8 @@ const sourceDetails = ref<HTMLDetailsElement>();
156
156
  const editing = ref(false);
157
157
  const { copied, copy } = useClipboardCopy();
158
158
 
159
- function onDetailsToggle(e: Event) {
160
- const open = (e.target as HTMLDetailsElement).open;
159
+ function onDetailsToggle(event: Event) {
160
+ const open = (event.target as HTMLDetailsElement).open;
161
161
  editing.value = open;
162
162
  if (!open) {
163
163
  editableMarkdown.value = markdownContent.value;
@@ -198,7 +198,9 @@ async function applyMarkdown() {
198
198
  });
199
199
  saving.value = false;
200
200
  if (!result.ok) {
201
- saveError.value = `Save failed: ${result.error}`;
201
+ // Store the raw error; the template formats it via t() so locale
202
+ // switches re-render without double-translating.
203
+ saveError.value = result.error;
202
204
  return;
203
205
  }
204
206
  }
@@ -245,18 +247,6 @@ watch(
245
247
  min-height: 0;
246
248
  }
247
249
 
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
250
  .button-group {
261
251
  display: flex;
262
252
  gap: 0.5em;
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="text-sm">
2
+ <div class="p-2 text-sm">
3
3
  <div class="font-medium text-gray-700 truncate mb-1">
4
4
  {{ title }}
5
5
  </div>
@@ -1,18 +1,18 @@
1
1
  <template>
2
2
  <div class="h-full flex flex-col overflow-hidden">
3
3
  <div class="px-4 py-2 border-b border-gray-100 shrink-0 flex items-center justify-between">
4
- <span class="text-sm font-medium text-gray-700 truncate">{{ title ?? "HTML Page" }}</span>
4
+ <span class="text-sm font-medium text-gray-700 truncate">{{ title ?? t("pluginPresentHtml.untitled") }}</span>
5
5
  <div class="flex items-center gap-2">
6
6
  <button
7
7
  class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 shrink-0"
8
- title="Save as PDF (opens print dialog)"
8
+ :title="t('pluginPresentHtml.saveAsPdf')"
9
9
  @click="printToPdf"
10
10
  >
11
11
  <span class="material-icons text-sm align-middle">picture_as_pdf</span>
12
- PDF
12
+ {{ t("pluginPresentHtml.pdf") }}
13
13
  </button>
14
14
  <button class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 shrink-0" @click="sourceOpen = !sourceOpen">
15
- {{ sourceOpen ? "Hide Source <>" : "Show Source <>" }}
15
+ {{ sourceOpen ? t("pluginPresentHtml.hideSource") : t("pluginPresentHtml.showSource") }}
16
16
  </button>
17
17
  </div>
18
18
  </div>
@@ -25,9 +25,12 @@
25
25
 
26
26
  <script setup lang="ts">
27
27
  import { computed, ref } from "vue";
28
+ import { useI18n } from "vue-i18n";
28
29
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
29
30
  import type { PresentHtmlData } from "./index";
30
31
 
32
+ const { t } = useI18n();
33
+
31
34
  const props = defineProps<{
32
35
  selectedResult: ToolResultComplete<PresentHtmlData>;
33
36
  }>();
@@ -25,8 +25,8 @@ export function stripHtmlToPreview(html: string, maxLength: number): string {
25
25
  };
26
26
  let i = 0;
27
27
  while (i < html.length) {
28
- const c = html[i];
29
- if (c === "<") {
28
+ const char = html[i];
29
+ if (char === "<") {
30
30
  const close = html.indexOf(">", i + 1);
31
31
  if (close !== -1) {
32
32
  // Real tag span `<...>` — skip it, emit a separator.
@@ -36,7 +36,7 @@ export function stripHtmlToPreview(html: string, maxLength: number): string {
36
36
  }
37
37
  // No closing `>` anywhere after — treat as literal.
38
38
  }
39
- emitChar(state, c);
39
+ emitChar(state, char);
40
40
  i++;
41
41
  }
42
42
  trimTrailingSpace(state.out);
@@ -48,12 +48,12 @@ interface WalkerState {
48
48
  lastWasSpace: boolean;
49
49
  }
50
50
 
51
- function emitChar(state: WalkerState, c: string): void {
52
- if (isWhitespace(c)) {
51
+ function emitChar(state: WalkerState, char: string): void {
52
+ if (isWhitespace(char)) {
53
53
  emitSeparator(state);
54
54
  return;
55
55
  }
56
- state.out.push(c);
56
+ state.out.push(char);
57
57
  state.lastWasSpace = false;
58
58
  }
59
59
 
@@ -67,6 +67,6 @@ function trimTrailingSpace(out: string[]): void {
67
67
  if (out.length > 0 && out[out.length - 1] === " ") out.pop();
68
68
  }
69
69
 
70
- function isWhitespace(c: string): boolean {
71
- return c === " " || c === "\t" || c === "\n" || c === "\r" || c === "\v" || c === "\f";
70
+ function isWhitespace(char: string): boolean {
71
+ return char === " " || char === "\t" || char === "\n" || char === "\r" || char === "\v" || char === "\f";
72
72
  }
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="text-sm">
2
+ <div class="p-2 text-sm">
3
3
  <div class="font-medium text-gray-700 truncate mb-1">
4
4
  {{ title }}
5
5
  </div>