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
@@ -10,7 +10,7 @@
10
10
  {{ script.description }}
11
11
  </p>
12
12
  <div class="flex items-center gap-3 mt-1 text-xs text-gray-400">
13
- <span>{{ beats.length }} beat{{ beats.length !== 1 ? "s" : "" }}</span>
13
+ <span>{{ t("pluginMulmoScript.beatCount", beats.length, { named: { count: beats.length } }) }}</span>
14
14
  <span v-if="script.lang">{{ script.lang }}</span>
15
15
  <span v-if="filePath" class="truncate">{{ filePath }}</span>
16
16
  </div>
@@ -24,7 +24,7 @@
24
24
  class="px-3 py-1 text-xs rounded-full border transition-colors border-gray-200 text-gray-500 hover:bg-gray-50 flex items-center justify-center gap-1"
25
25
  >
26
26
  <span class="material-icons text-sm leading-none">download</span>
27
- <span>Movie</span>
27
+ <span>{{ t("pluginMulmoScript.movie") }}</span>
28
28
  </a>
29
29
  <!-- Generate / Regenerate Movie -->
30
30
  <button
@@ -36,10 +36,10 @@
36
36
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
37
37
  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
38
38
  </svg>
39
- <span v-if="movieGenerating">Generating…</span>
39
+ <span v-if="movieGenerating">{{ t("pluginMulmoScript.generating") }}</span>
40
40
  <template v-else>
41
41
  <span class="material-icons text-sm leading-none">refresh</span>
42
- <span>Movie</span>
42
+ <span>{{ t("pluginMulmoScript.movie") }}</span>
43
43
  </template>
44
44
  </button>
45
45
  </div>
@@ -48,13 +48,13 @@
48
48
  <!-- Characters section -->
49
49
  <div v-if="characterKeys.length > 0" class="border-b border-gray-100 shrink-0 px-4 py-3">
50
50
  <div class="flex items-center justify-between mb-2">
51
- <span class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Characters</span>
51
+ <span class="text-xs font-semibold text-gray-500 uppercase tracking-wide">{{ t("pluginMulmoScript.characters") }}</span>
52
52
  <button
53
53
  class="px-2 py-0.5 text-xs rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50"
54
54
  :disabled="movieGenerating || anyBeatRendering || characterKeys.every((key) => charRenderState[key] === 'rendering')"
55
55
  @click="generateAllCharacters"
56
56
  >
57
- Generate All
57
+ {{ t("pluginMulmoScript.generateAll") }}
58
58
  </button>
59
59
  </div>
60
60
  <div class="flex gap-3 flex-wrap">
@@ -88,11 +88,11 @@
88
88
  </template>
89
89
  <!-- Permanent drop hint -->
90
90
  <div v-if="!charDragOver[key]" class="absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none">
91
- or drop image
91
+ {{ t("pluginMulmoScript.orDropImage") }}
92
92
  </div>
93
93
  <!-- Drop overlay -->
94
94
  <div v-if="charDragOver[key]" class="absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none">
95
- <span class="text-xs text-blue-500 font-medium">Drop</span>
95
+ <span class="text-xs text-blue-500 font-medium">{{ t("pluginMulmoScript.drop") }}</span>
96
96
  </div>
97
97
  <!-- Regenerate button -->
98
98
  <button
@@ -121,7 +121,7 @@
121
121
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
122
122
  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
123
123
  </svg>
124
- <span v-else>Gen</span>
124
+ <span v-else>{{ t("pluginMulmoScript.gen") }}</span>
125
125
  </button>
126
126
  </div>
127
127
  <span class="text-xs text-gray-600 text-center truncate w-full">{{ key }}</span>
@@ -163,7 +163,7 @@
163
163
  <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
164
164
  <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
165
165
  </svg>
166
- <span class="text-xs text-green-500">Rendering…</span>
166
+ <span class="text-xs text-green-500">{{ t("pluginMulmoScript.rendering") }}</span>
167
167
  </template>
168
168
  <template v-else-if="renderState[index] === 'error'">
169
169
  <span class="text-xs text-red-400 text-center">{{ renderErrors[index] }}</span>
@@ -177,13 +177,13 @@
177
177
  </div>
178
178
  <!-- Beat drop hint / overlay -->
179
179
  <div v-if="beatDragOver[index]" class="absolute inset-0 flex items-center justify-center bg-blue-50/80 pointer-events-none">
180
- <span class="text-xs text-blue-500 font-medium">Drop</span>
180
+ <span class="text-xs text-blue-500 font-medium">{{ t("pluginMulmoScript.drop") }}</span>
181
181
  </div>
182
182
  <div
183
183
  v-else-if="!renderedImages[index] && renderState[index] !== 'rendering'"
184
184
  class="absolute bottom-0 inset-x-0 text-center text-xs text-gray-400 bg-white/70 py-0.5 pointer-events-none"
185
185
  >
186
- or drop image
186
+ {{ t("pluginMulmoScript.orDropImage") }}
187
187
  </div>
188
188
  <!-- Generate button for imagePrompt beats -->
189
189
  <button
@@ -191,7 +191,7 @@
191
191
  class="absolute top-1.5 right-1.5 flex items-center gap-1 px-2 py-0.5 text-xs rounded border border-blue-400 text-blue-600 bg-white hover:bg-blue-50"
192
192
  @click="renderBeat(index)"
193
193
  >
194
- Generate
194
+ {{ t("pluginMulmoScript.generate") }}
195
195
  </button>
196
196
  </div>
197
197
 
@@ -213,10 +213,10 @@
213
213
  :class="playingAudio?.index === index ? 'border-red-400 text-red-600 hover:bg-red-50' : 'border-green-400 text-green-600 hover:bg-green-50'"
214
214
  @click="playAudio(index)"
215
215
  >
216
- {{ playingAudio?.index === index ? "■ Stop" : "▶ Play" }}
216
+ {{ playingAudio?.index === index ? t("pluginMulmoScript.stop") : t("pluginMulmoScript.play") }}
217
217
  </button>
218
218
  <template v-else-if="audioErrors[index]">
219
- <span class="text-xs text-red-400" :title="audioErrors[index]">⚠ Error</span>
219
+ <span class="text-xs text-red-400" :title="audioErrors[index]">{{ t("pluginMulmoScript.errPrefix") }}</span>
220
220
  <button
221
221
  v-if="effectiveBeat(index).text"
222
222
  class="text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50 disabled:opacity-50"
@@ -231,7 +231,7 @@
231
231
  class="text-xs px-2 py-0.5 rounded border border-gray-300 text-gray-500 hover:bg-gray-50"
232
232
  @click="generateAudio(index)"
233
233
  >
234
- Generate
234
+ {{ t("pluginMulmoScript.generateAudio") }}
235
235
  </button>
236
236
  </div>
237
237
  <button class="text-gray-400 hover:text-gray-600" :title="sourceOpen[index] ? 'Hide source' : 'Show source'" @click="toggleSource(index)">
@@ -263,7 +263,11 @@
263
263
  spellcheck="false"
264
264
  />
265
265
  <div class="flex items-center justify-end gap-2 px-2 pb-2">
266
- <span v-if="beatSaveErrors[index]" class="text-xs text-red-600" role="alert">⚠ {{ beatSaveErrors[index] }}</span>
266
+ <span v-if="beatSaveErrors[index]" class="text-xs text-red-600" role="alert">{{
267
+ t(beatSaveErrors[index].kind === "invalidJson" ? "pluginMulmoScript.saveErrorInvalidJson" : "pluginMulmoScript.saveErrorSaveFailed", {
268
+ error: beatSaveErrors[index].error,
269
+ })
270
+ }}</span>
267
271
  <button
268
272
  class="px-2 py-1 text-xs rounded border"
269
273
  :class="
@@ -274,19 +278,19 @@
274
278
  :disabled="!isValidBeat(index) || !!beatSaving[index]"
275
279
  @click="updateBeat(index)"
276
280
  >
277
- {{ beatSaving[index] ? "Saving…" : "Update" }}
281
+ {{ beatSaving[index] ? t("pluginMulmoScript.saving") : t("pluginMulmoScript.update") }}
278
282
  </button>
279
283
  </div>
280
284
  </div>
281
285
  </div>
282
286
 
283
- <div v-if="beats.length === 0" class="flex items-center justify-center h-32 text-gray-400 text-sm">No beats found in script</div>
287
+ <div v-if="beats.length === 0" class="flex items-center justify-center h-32 text-gray-400 text-sm">{{ t("pluginMulmoScript.noBeats") }}</div>
284
288
  </div>
285
289
 
286
290
  <!-- Bottom bar: Edit Script Source + Copy -->
287
291
  <div class="bottom-bar-wrapper">
288
292
  <details ref="sourceDetails" class="script-source" @toggle="onSourceToggle(($event.target as HTMLDetailsElement).open)">
289
- <summary>Edit Script Source</summary>
293
+ <summary>{{ t("pluginMulmoScript.editSource") }}</summary>
290
294
  <textarea
291
295
  v-model="editableSource"
292
296
  class="script-editor"
@@ -294,8 +298,8 @@
294
298
  spellcheck="false"
295
299
  ></textarea>
296
300
  <div class="editor-actions">
297
- <button class="apply-btn" :disabled="!sourceChanged || !sourceValid" @click="applySource">Apply Changes</button>
298
- <button class="cancel-btn" @click="cancelSourceEdit">Cancel</button>
301
+ <button class="apply-btn" :disabled="!sourceChanged || !sourceValid" @click="applySource">{{ t("pluginMulmoScript.applyChanges") }}</button>
302
+ <button class="cancel-btn" @click="cancelSourceEdit">{{ t("common.cancel") }}</button>
299
303
  </div>
300
304
  </details>
301
305
  <button v-show="!editing" class="copy-btn" :title="copied ? 'Copied!' : 'Copy'" @click="copyText">
@@ -328,7 +332,7 @@
328
332
  "
329
333
  @click="playAudio(lightbox.index)"
330
334
  >
331
- {{ playingAudio?.index === lightbox.index ? "■ Stop" : "▶ Play" }}
335
+ {{ playingAudio?.index === lightbox.index ? t("pluginMulmoScript.stop") : t("pluginMulmoScript.play") }}
332
336
  </button>
333
337
  </div>
334
338
  </div>
@@ -347,7 +351,10 @@
347
351
 
348
352
  <script setup lang="ts">
349
353
  import { computed, onMounted, reactive, ref, watch } from "vue";
354
+ import { useI18n } from "vue-i18n";
350
355
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
356
+
357
+ const { t } = useI18n();
351
358
  import type { MulmoScriptData } from "./index";
352
359
  import { mulmoBeatSchema, mulmoScriptSchema } from "@mulmocast/types";
353
360
  import { extractErrorMessage, getMissingCharacterKeys, shouldAutoRenderBeat, streamMovieEvents, validateBeatJSON } from "./helpers";
@@ -407,7 +414,10 @@ const sourceOpen = reactive<Record<number, boolean>>({});
407
414
  const sourceText = reactive<Record<number, string>>({});
408
415
  // Surface POST /api/mulmo-script/update-beat failures inline next to
409
416
  // the Update button. Cleared on next successful save or editor close.
410
- const beatSaveErrors = reactive<Record<number, string>>({});
417
+ // Store raw error + kind tag so the template picks a localized key,
418
+ // instead of pre-composing an English-prefixed string here.
419
+ type BeatSaveError = { kind: "invalidJson" | "saveFailed"; error: string };
420
+ const beatSaveErrors = reactive<Record<number, BeatSaveError>>({});
411
421
  const beatSaving = reactive<Record<number, boolean>>({});
412
422
  const localOverrides = reactive<Record<number, Beat>>({});
413
423
  const movieGenerating = ref(false);
@@ -431,7 +441,7 @@ const charErrors = reactive<Record<string, string>>({});
431
441
  const charDragOver = reactive<Record<string, boolean>>({});
432
442
  const beatDragOver = reactive<Record<number, boolean>>({});
433
443
 
434
- const anyBeatRendering = computed(() => Object.values(renderState).some((s) => s === "rendering"));
444
+ const anyBeatRendering = computed(() => Object.values(renderState).some((state) => state === "rendering"));
435
445
 
436
446
  const characterKeys = computed(() => {
437
447
  const imgs = script.value.imageParams?.images ?? {};
@@ -447,10 +457,10 @@ const chatSessionId = computed(() => activeSessionRef?.value?.id);
447
457
  const pendingForThisScript = computed(() => {
448
458
  const out: Record<string, PendingGeneration> = {};
449
459
  const pending = activeSessionRef?.value?.pendingGenerations ?? {};
450
- const fp = filePath.value;
451
- if (!fp) return out;
460
+ const currentPath = filePath.value;
461
+ if (!currentPath) return out;
452
462
  for (const [mapKey, entry] of Object.entries(pending)) {
453
- if (entry.filePath === fp) out[mapKey] = entry;
463
+ if (entry.filePath === currentPath) out[mapKey] = entry;
454
464
  }
455
465
  return out;
456
466
  });
@@ -606,7 +616,7 @@ async function updateBeat(index: number) {
606
616
  try {
607
617
  beat = JSON.parse(sourceText[index]);
608
618
  } catch (err) {
609
- beatSaveErrors[index] = `Invalid JSON: ${errorMessage(err)}`;
619
+ beatSaveErrors[index] = { kind: "invalidJson", error: errorMessage(err) };
610
620
  return;
611
621
  }
612
622
  const prevImage = JSON.stringify(effectiveBeat(index).image);
@@ -620,7 +630,7 @@ async function updateBeat(index: number) {
620
630
  });
621
631
  delete beatSaving[index];
622
632
  if (!response.ok) {
623
- beatSaveErrors[index] = `Save failed: ${response.error}`;
633
+ beatSaveErrors[index] = { kind: "saveFailed", error: response.error };
624
634
  return;
625
635
  }
626
636
 
@@ -63,7 +63,7 @@ export function shouldAutoRenderBeat(beat: { image?: { type?: string } | undefin
63
63
  * what's missing after a movie-generation event arrives.
64
64
  */
65
65
  export function getMissingCharacterKeys(keys: readonly string[], images: Record<string, unknown>, renderState: Record<string, string | undefined>): string[] {
66
- return keys.filter((k) => !images[k] && renderState[k] !== "rendering");
66
+ return keys.filter((charKey) => !images[charKey] && renderState[charKey] !== "rendering");
67
67
  }
68
68
 
69
69
  /**
@@ -1,24 +1,27 @@
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>{{ upcomingItems.length }} upcoming</span>
4
+ <span aria-hidden="true">{{ t("pluginScheduler.previewIcon") }}</span>
5
+ <span>{{ t("pluginScheduler.previewUpcoming", { count: upcomingItems.length }) }}</span>
6
6
  </div>
7
7
  <div v-for="item in preview" :key="item.id" class="text-xs truncate text-gray-600">
8
8
  <span v-if="item.props.date" class="text-gray-400 mr-1">{{ item.props.date }}</span>
9
9
  {{ item.title }}
10
10
  </div>
11
- <div v-if="more > 0" class="text-xs text-gray-400">+ {{ more }} more…</div>
11
+ <div v-if="more > 0" class="text-xs text-gray-400">{{ t("pluginScheduler.previewMore", { count: more }) }}</div>
12
12
  </div>
13
13
  </template>
14
14
 
15
15
  <script setup lang="ts">
16
16
  import { computed, ref, watch } from "vue";
17
+ import { useI18n } from "vue-i18n";
17
18
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
18
19
  import type { SchedulerData, ScheduledItem } from "./index";
19
20
  import { useFreshPluginData } from "../../composables/useFreshPluginData";
20
21
  import { API_ROUTES } from "../../config/apiRoutes";
21
22
 
23
+ const { t } = useI18n();
24
+
22
25
  const props = defineProps<{ result: ToolResultComplete<SchedulerData> }>();
23
26
 
24
27
  const items = ref<ScheduledItem[]>(props.result.data?.items ?? []);
@@ -26,8 +29,8 @@ const items = ref<ScheduledItem[]>(props.result.data?.items ?? []);
26
29
  const { refresh } = useFreshPluginData<ScheduledItem[]>({
27
30
  endpoint: () => API_ROUTES.scheduler.base,
28
31
  extract: (json) => {
29
- const v = (json as { data?: { items?: ScheduledItem[] } }).data?.items;
30
- return Array.isArray(v) ? v : null;
32
+ const extracted = (json as { data?: { items?: ScheduledItem[] } }).data?.items;
33
+ return Array.isArray(extracted) ? extracted : null;
31
34
  },
32
35
  apply: (data) => {
33
36
  items.value = data;
@@ -49,15 +52,15 @@ const upcomingItems = computed(() => {
49
52
  const noDate: ScheduledItem[] = [];
50
53
 
51
54
  for (const item of items.value) {
52
- const d = item.props.date;
53
- if (typeof d === "string") {
54
- if (d >= today) withDate.push(item);
55
+ const dateVal = item.props.date;
56
+ if (typeof dateVal === "string") {
57
+ if (dateVal >= today) withDate.push(item);
55
58
  } else {
56
59
  noDate.push(item);
57
60
  }
58
61
  }
59
62
 
60
- withDate.sort((a, b) => (String(a.props.date) < String(b.props.date) ? -1 : 1));
63
+ withDate.sort((itemA, itemB) => (String(itemA.props.date) < String(itemB.props.date) ? -1 : 1));
61
64
 
62
65
  return [...withDate, ...noDate];
63
66
  });
@@ -6,16 +6,37 @@
6
6
  </div>
7
7
 
8
8
  <!-- Loading -->
9
- <div v-if="loading" class="flex items-center justify-center h-32 text-gray-400">Loading...</div>
9
+ <div v-if="loading" class="flex items-center justify-center h-32 text-gray-400">{{ t("common.loading") }}</div>
10
10
 
11
11
  <!-- Error -->
12
12
  <div v-else-if="error" class="px-4 py-2 bg-red-50 text-red-700 rounded text-sm">
13
13
  {{ error }}
14
14
  </div>
15
15
 
16
- <!-- Task list -->
16
+ <!-- Task list + frequency hints -->
17
17
  <div v-else>
18
- <div v-if="tasks.length === 0" class="flex items-center justify-center h-32 text-gray-400">No scheduled tasks</div>
18
+ <!-- Frequency hints reference -->
19
+ <details class="mb-4 border border-gray-200 rounded-lg text-sm" data-testid="scheduler-frequency-hints">
20
+ <summary class="px-3 py-2 cursor-pointer text-gray-600 font-medium select-none hover:bg-gray-50 rounded-lg">
21
+ {{ t("pluginSchedulerTasks.recommendedFrequencies") }}
22
+ </summary>
23
+ <table class="w-full mt-1 mb-2 text-xs text-gray-500">
24
+ <thead>
25
+ <tr class="border-b border-gray-100">
26
+ <th class="px-3 py-1 text-left font-medium text-gray-600">{{ t("pluginSchedulerTasks.tableTaskType") }}</th>
27
+ <th class="px-3 py-1 text-left font-medium text-gray-600">{{ t("pluginSchedulerTasks.tableSuggestedSchedule") }}</th>
28
+ </tr>
29
+ </thead>
30
+ <tbody>
31
+ <tr v-for="hint in FREQUENCY_HINTS" :key="hint.label" class="border-b border-gray-50 last:border-0">
32
+ <td class="px-3 py-1">{{ hint.label }}</td>
33
+ <td class="px-3 py-1 font-mono text-gray-700">{{ formatSchedule(hint.schedule) }}</td>
34
+ </tr>
35
+ </tbody>
36
+ </table>
37
+ </details>
38
+
39
+ <div v-if="tasks.length === 0" class="flex items-center justify-center h-32 text-gray-400">{{ t("pluginSchedulerTasks.noTasks") }}</div>
19
40
 
20
41
  <div v-else class="space-y-2">
21
42
  <div
@@ -40,8 +61,8 @@
40
61
  <button
41
62
  v-if="task.origin === 'user'"
42
63
  class="px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 rounded"
43
- title="Run now"
44
- aria-label="Run now"
64
+ :title="t('pluginSchedulerTasks.runNow')"
65
+ :aria-label="t('pluginSchedulerTasks.runNow')"
45
66
  data-testid="scheduler-task-run"
46
67
  @click="runTask(task.id)"
47
68
  >
@@ -52,7 +73,7 @@
52
73
  v-if="task.origin === 'user'"
53
74
  class="px-2 py-1 text-xs rounded"
54
75
  :class="task.enabled !== false ? 'text-green-600 hover:bg-green-50' : 'text-gray-400 hover:bg-gray-100'"
55
- :title="task.enabled !== false ? 'Disable' : 'Enable'"
76
+ :title="task.enabled !== false ? t('pluginSchedulerTasks.disable') : t('pluginSchedulerTasks.enable')"
56
77
  @click="toggleEnabled(task)"
57
78
  >
58
79
  <span class="material-icons text-sm">
@@ -63,8 +84,8 @@
63
84
  <button
64
85
  v-if="task.origin === 'user'"
65
86
  class="px-2 py-1 text-xs text-red-500 hover:bg-red-50 rounded"
66
- title="Delete"
67
- aria-label="Delete"
87
+ :title="t('pluginSchedulerTasks.delete')"
88
+ :aria-label="t('pluginSchedulerTasks.delete')"
68
89
  data-testid="scheduler-task-delete"
69
90
  @click="deleteTask(task.id)"
70
91
  >
@@ -80,7 +101,7 @@
80
101
  <span class="inline-block w-2 h-2 rounded-full" :class="resultDotClass(task.state.lastRunResult)"></span>
81
102
  {{ task.state.lastRunResult }}
82
103
  </span>
83
- <span v-if="task.state?.nextScheduledAt"> Next: {{ formatShortTime(task.state.nextScheduledAt) }} </span>
104
+ <span v-if="task.state?.nextScheduledAt">{{ t("pluginSchedulerTasks.nextRun", { time: formatShortTime(task.state.nextScheduledAt) }) }}</span>
84
105
  </div>
85
106
 
86
107
  <!-- Description -->
@@ -95,9 +116,13 @@
95
116
 
96
117
  <script setup lang="ts">
97
118
  import { ref, onMounted } from "vue";
119
+ import { useI18n } from "vue-i18n";
98
120
  import { apiGet, apiPost, apiPut, apiDelete } from "../../utils/api";
99
121
  import { API_ROUTES } from "../../config/apiRoutes";
100
122
  import { formatShortTime } from "../../utils/format/date";
123
+ import { formatSchedule as formatTaskSchedule, type TaskSchedule as FormatterTaskSchedule } from "./formatSchedule";
124
+
125
+ const { t } = useI18n();
101
126
 
102
127
  interface TaskSchedule {
103
128
  type: string;
@@ -121,6 +146,18 @@ interface SchedulerTask {
121
146
  state?: TaskState;
122
147
  }
123
148
 
149
+ // Hints showing common task cadences. Stored as structured schedules
150
+ // (not pre-rendered strings) so the display routes through
151
+ // formatTaskSchedule() and picks up the viewer's local timezone for
152
+ // daily rows — the same conversion applied to real tasks below.
153
+ const FREQUENCY_HINTS: Array<{ label: string; schedule: FormatterTaskSchedule }> = [
154
+ { label: "News / RSS fetch", schedule: { type: "interval", intervalMs: 3_600_000 } },
155
+ { label: "Journal daily pass", schedule: { type: "daily", time: "23:00" } },
156
+ { label: "Wiki maintenance", schedule: { type: "daily", time: "02:00" } },
157
+ { label: "Memory extraction", schedule: { type: "daily", time: "00:00" } },
158
+ { label: "Calendar / contact sync", schedule: { type: "interval", intervalMs: 14_400_000 } },
159
+ ];
160
+
124
161
  const tasks = ref<SchedulerTask[]>([]);
125
162
  const loading = ref(true);
126
163
  const error = ref("");
@@ -139,9 +176,9 @@ async function fetchTasks(): Promise<void> {
139
176
  }
140
177
 
141
178
  function originLabel(origin: string): string {
142
- if (origin === "system") return "System";
143
- if (origin === "user") return "User";
144
- return "Skill";
179
+ if (origin === "system") return t("pluginSchedulerTasks.originSystem");
180
+ if (origin === "user") return t("pluginSchedulerTasks.originUser");
181
+ return t("pluginSchedulerTasks.originSkill");
145
182
  }
146
183
 
147
184
  function originClass(origin: string): string {
@@ -157,23 +194,15 @@ function resultDotClass(result: string): string {
157
194
  }
158
195
 
159
196
  function formatSchedule(schedule: TaskSchedule): string {
160
- if (schedule.type === "interval" && schedule.intervalMs) {
161
- const mins = Math.round(schedule.intervalMs / 60000);
162
- if (mins >= 60) return `Every ${Math.round(mins / 60)}h`;
163
- return `Every ${mins}m`;
164
- }
165
- if (schedule.type === "daily" && schedule.time) {
166
- return `Daily ${schedule.time} UTC`;
167
- }
168
- return JSON.stringify(schedule);
197
+ return formatTaskSchedule(schedule as FormatterTaskSchedule);
169
198
  }
170
199
 
171
- async function runTask(id: string): Promise<void> {
200
+ async function runTask(taskId: string): Promise<void> {
172
201
  mutationError.value = "";
173
- const url = API_ROUTES.scheduler.taskRun.replace(":id", id);
202
+ const url = API_ROUTES.scheduler.taskRun.replace(":id", taskId);
174
203
  const result = await apiPost(url, {});
175
204
  if (!result.ok) {
176
- mutationError.value = `Run failed: ${result.error}`;
205
+ mutationError.value = t("pluginSchedulerTasks.runFailed", { error: result.error });
177
206
  return;
178
207
  }
179
208
  await fetchTasks();
@@ -184,18 +213,18 @@ async function toggleEnabled(task: SchedulerTask): Promise<void> {
184
213
  const url = API_ROUTES.scheduler.task.replace(":id", task.id);
185
214
  const result = await apiPut(url, { enabled: task.enabled === false });
186
215
  if (!result.ok) {
187
- mutationError.value = `Toggle failed: ${result.error}`;
216
+ mutationError.value = t("pluginSchedulerTasks.toggleFailed", { error: result.error });
188
217
  return;
189
218
  }
190
219
  await fetchTasks();
191
220
  }
192
221
 
193
- async function deleteTask(id: string): Promise<void> {
222
+ async function deleteTask(taskId: string): Promise<void> {
194
223
  mutationError.value = "";
195
- const url = API_ROUTES.scheduler.task.replace(":id", id);
224
+ const url = API_ROUTES.scheduler.task.replace(":id", taskId);
196
225
  const result = await apiDelete(url);
197
226
  if (!result.ok) {
198
- mutationError.value = `Delete failed: ${result.error}`;
227
+ mutationError.value = t("pluginSchedulerTasks.deleteFailed", { error: result.error });
199
228
  return;
200
229
  }
201
230
  await fetchTasks();
@@ -3,7 +3,7 @@
3
3
  <!-- API error banner — surfaces POST /api/scheduler failures so a
4
4
  delete/add/replace that silently no-ops becomes diagnosable. -->
5
5
  <div v-if="apiError" class="px-4 py-2 bg-red-50 border-b border-red-200 text-sm text-red-700" role="alert" data-testid="scheduler-api-error">
6
- Failed to update scheduler: {{ apiError }}
6
+ {{ t("pluginScheduler.apiError", { error: apiError }) }}
7
7
  </div>
8
8
  <!-- Top-level tab bar: Calendar / Tasks -->
9
9
  <div class="flex border-b border-gray-200 px-6">
@@ -13,7 +13,7 @@
13
13
  data-testid="scheduler-tab-calendar"
14
14
  @click="activeTab = SCHEDULER_TAB.calendar"
15
15
  >
16
- Calendar
16
+ {{ t("pluginScheduler.tabCalendar") }}
17
17
  </button>
18
18
  <button
19
19
  class="px-4 py-2 text-sm font-medium border-b-2 -mb-px"
@@ -21,7 +21,7 @@
21
21
  data-testid="scheduler-tab-tasks"
22
22
  @click="activeTab = SCHEDULER_TAB.tasks"
23
23
  >
24
- Tasks
24
+ {{ t("pluginScheduler.tabTasks") }}
25
25
  </button>
26
26
  </div>
27
27
 
@@ -33,17 +33,19 @@
33
33
  <!-- Header -->
34
34
  <div class="flex items-center justify-between px-6 py-3 border-b border-gray-100">
35
35
  <div class="flex items-center gap-3">
36
- <h2 class="text-lg font-semibold text-gray-800">Scheduler</h2>
37
- <span class="text-sm text-gray-500">{{ items.length }} item{{ items.length !== 1 ? "s" : "" }}</span>
36
+ <h2 class="text-lg font-semibold text-gray-800">{{ t("pluginScheduler.heading") }}</h2>
37
+ <span class="text-sm text-gray-500">{{ t("pluginScheduler.itemCount", items.length, { named: { count: items.length } }) }}</span>
38
38
  </div>
39
39
  <div class="flex items-center gap-2">
40
40
  <!-- Navigation (calendar modes only) -->
41
41
  <template v-if="viewMode !== SCHEDULER_VIEW.list">
42
- <button class="px-2 py-1 text-sm text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded" title="Previous" @click="goPrev">
42
+ <button class="px-2 py-1 text-sm text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded" :title="t('pluginScheduler.prev')" @click="goPrev">
43
43
  <span class="material-icons text-sm">chevron_left</span>
44
44
  </button>
45
- <button class="px-2 py-1 text-xs text-gray-600 hover:bg-gray-100 rounded" title="Go to today" @click="goToday">Today</button>
46
- <button class="px-2 py-1 text-sm text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded" title="Next" @click="goNext">
45
+ <button class="px-2 py-1 text-xs text-gray-600 hover:bg-gray-100 rounded" :title="t('pluginScheduler.goToday')" @click="goToday">
46
+ {{ t("pluginScheduler.today") }}
47
+ </button>
48
+ <button class="px-2 py-1 text-sm text-gray-500 hover:text-gray-800 hover:bg-gray-100 rounded" :title="t('pluginScheduler.next')" @click="goNext">
47
49
  <span class="material-icons text-sm">chevron_right</span>
48
50
  </button>
49
51
  <span class="text-sm text-gray-600 min-w-[140px] text-center">{{ headerLabel }}</span>
@@ -66,7 +68,7 @@
66
68
 
67
69
  <!-- List view -->
68
70
  <div v-if="viewMode === SCHEDULER_VIEW.list" class="flex-1 overflow-y-auto min-h-0">
69
- <div v-if="items.length === 0" class="flex items-center justify-center h-full text-gray-400">No scheduled items</div>
71
+ <div v-if="items.length === 0" class="flex items-center justify-center h-full text-gray-400">{{ t("pluginScheduler.noScheduled") }}</div>
70
72
 
71
73
  <ul v-else class="p-4 space-y-2">
72
74
  <li
@@ -86,14 +88,14 @@
86
88
  :key="key"
87
89
  class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-gray-100 text-xs text-gray-600"
88
90
  >
89
- <span class="text-gray-400">{{ key }}:</span>
91
+ <span class="text-gray-400">{{ t("pluginScheduler.propLabel", { key }) }}</span>
90
92
  <span>{{ val }}</span>
91
93
  </span>
92
94
  </div>
93
95
  </div>
94
96
  <button
95
97
  class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 text-xs px-1 mt-0.5 shrink-0"
96
- title="Delete item"
98
+ :title="t('pluginScheduler.deleteItem')"
97
99
  @click.stop="remove(item)"
98
100
  >
99
101
 
@@ -133,7 +135,7 @@
133
135
  </div>
134
136
  <!-- Unscheduled -->
135
137
  <div v-if="unscheduledItems.length > 0" class="p-3 border-t border-gray-200">
136
- <div class="text-xs text-gray-400 mb-1.5">Unscheduled</div>
138
+ <div class="text-xs text-gray-400 mb-1.5">{{ t("pluginScheduler.unscheduled") }}</div>
137
139
  <div class="flex flex-wrap gap-1">
138
140
  <div
139
141
  v-for="item in unscheduledItems"
@@ -179,14 +181,14 @@
179
181
  {{ item.title }}
180
182
  </div>
181
183
  <div v-if="itemsForDay(day).length > MAX_MONTH_ITEMS" class="text-[10px] text-gray-400 px-1">
182
- +{{ itemsForDay(day).length - MAX_MONTH_ITEMS }} more
184
+ {{ t("pluginScheduler.moreCount", { count: itemsForDay(day).length - MAX_MONTH_ITEMS }) }}
183
185
  </div>
184
186
  </div>
185
187
  </div>
186
188
  </div>
187
189
  <!-- Unscheduled -->
188
190
  <div v-if="unscheduledItems.length > 0" class="p-3 border-t border-gray-200">
189
- <div class="text-xs text-gray-400 mb-1.5">Unscheduled</div>
191
+ <div class="text-xs text-gray-400 mb-1.5">{{ t("pluginScheduler.unscheduled") }}</div>
190
192
  <div class="flex flex-wrap gap-1">
191
193
  <div
192
194
  v-for="item in unscheduledItems"
@@ -204,8 +206,8 @@
204
206
  <!-- Item YAML editor -->
205
207
  <div v-if="selectedId" class="border-t border-blue-200 bg-blue-50 shrink-0">
206
208
  <div class="flex items-center justify-between px-4 py-2 text-sm font-medium text-blue-700">
207
- <span>Edit item</span>
208
- <button class="text-blue-400 hover:text-blue-600 text-xs" title="Close editor" @click="selectedId = null">✕</button>
209
+ <span>{{ t("pluginScheduler.editItem") }}</span>
210
+ <button class="text-blue-400 hover:text-blue-600 text-xs" :title="t('pluginScheduler.closeEditor')" @click="selectedId = null">✕</button>
209
211
  </div>
210
212
  <div class="px-3 pb-3">
211
213
  <textarea
@@ -214,7 +216,9 @@
214
216
  spellcheck="false"
215
217
  />
216
218
  <div class="flex items-center gap-2 mt-2">
217
- <button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="applyItemEdit">Update</button>
219
+ <button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="applyItemEdit">
220
+ {{ t("pluginScheduler.update") }}
221
+ </button>
218
222
  <span v-if="yamlError" class="text-xs text-red-500">{{ yamlError }}</span>
219
223
  </div>
220
224
  </div>
@@ -222,7 +226,9 @@
222
226
 
223
227
  <!-- JSON source editor -->
224
228
  <details class="border-t border-gray-200 bg-gray-50 shrink-0">
225
- <summary class="cursor-pointer select-none px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100">Edit Source</summary>
229
+ <summary class="cursor-pointer select-none px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100">
230
+ {{ t("pluginScheduler.editSource") }}
231
+ </summary>
226
232
  <div class="p-3">
227
233
  <textarea
228
234
  v-model="editorText"
@@ -235,7 +241,7 @@
235
241
  class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600 disabled:bg-gray-200 disabled:text-gray-400 disabled:cursor-not-allowed"
236
242
  @click="applyChanges"
237
243
  >
238
- Apply Changes
244
+ {{ t("pluginScheduler.applyChanges") }}
239
245
  </button>
240
246
  <span v-if="parseError" class="text-xs text-red-500">{{ parseError }}</span>
241
247
  </div>
@@ -247,6 +253,7 @@
247
253
 
248
254
  <script setup lang="ts">
249
255
  import { computed, ref, watch } from "vue";
256
+ import { useI18n } from "vue-i18n";
250
257
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
251
258
  import type { SchedulerData, ScheduledItem } from "./index";
252
259
  import { useFreshPluginData } from "../../composables/useFreshPluginData";
@@ -255,6 +262,8 @@ import { API_ROUTES } from "../../config/apiRoutes";
255
262
  import TasksTab from "./TasksTab.vue";
256
263
  import { isToday } from "../../utils/format/date";
257
264
 
265
+ const { t } = useI18n();
266
+
258
267
  type YamlScalar = string | number | boolean | null;
259
268
 
260
269
  const props = defineProps<{