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,14 +1,11 @@
1
1
  <template>
2
2
  <div class="h-full bg-white flex flex-col">
3
3
  <div class="flex items-center justify-between px-6 py-4 border-b border-gray-100">
4
- <h2 class="text-lg font-semibold text-gray-800">Custom Roles</h2>
4
+ <h2 class="text-lg font-semibold text-gray-800">{{ t("pluginManageRoles.heading") }}</h2>
5
5
  <div class="flex items-center gap-3">
6
- <span class="text-sm text-gray-500">
7
- {{ customRoles.length }}
8
- role{{ customRoles.length !== 1 ? "s" : "" }}
9
- </span>
6
+ <span class="text-sm text-gray-500">{{ t("pluginManageRoles.roleCount", customRoles.length, { named: { count: customRoles.length } }) }}</span>
10
7
  <button v-if="!creating" data-testid="role-add-btn" class="px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600" @click="startCreate">
11
- + Add
8
+ {{ t("pluginManageRoles.addButton") }}
12
9
  </button>
13
10
  </div>
14
11
  </div>
@@ -16,12 +13,12 @@
16
13
  <div class="flex-1 overflow-y-auto">
17
14
  <!-- New role creation panel -->
18
15
  <div v-if="creating" class="m-4 border border-blue-300 bg-blue-50 rounded-lg p-4 space-y-3">
19
- <div class="text-sm font-semibold text-gray-700">Create new role</div>
16
+ <div class="text-sm font-semibold text-gray-700">{{ t("pluginManageRoles.createPanel") }}</div>
20
17
 
21
18
  <!-- ID + Name + Icon row -->
22
19
  <div class="flex gap-3">
23
20
  <div class="w-40">
24
- <label class="block text-xs font-medium text-gray-600 mb-1">ID</label>
21
+ <label class="block text-xs font-medium text-gray-600 mb-1">{{ t("pluginManageRoles.fieldId") }}</label>
25
22
  <input
26
23
  v-model="newForm.id"
27
24
  type="text"
@@ -30,7 +27,7 @@
30
27
  />
31
28
  </div>
32
29
  <div class="flex-1">
33
- <label class="block text-xs font-medium text-gray-600 mb-1">Name</label>
30
+ <label class="block text-xs font-medium text-gray-600 mb-1">{{ t("pluginManageRoles.fieldName") }}</label>
34
31
  <input
35
32
  v-model="newForm.name"
36
33
  type="text"
@@ -39,8 +36,10 @@
39
36
  </div>
40
37
  <div class="w-32">
41
38
  <label class="block text-xs font-medium text-gray-600 mb-1">
42
- Icon
43
- <a class="text-blue-400 font-normal ml-1" href="https://fonts.google.com/icons" target="_blank" rel="noopener">?</a>
39
+ {{ t("pluginManageRoles.fieldIcon") }}
40
+ <a class="text-blue-400 font-normal ml-1" href="https://fonts.google.com/icons" target="_blank" rel="noopener">{{
41
+ t("pluginManageRoles.helpLink")
42
+ }}</a>
44
43
  </label>
45
44
  <input
46
45
  v-model="newForm.icon"
@@ -52,7 +51,7 @@
52
51
 
53
52
  <!-- Prompt -->
54
53
  <div>
55
- <label class="block text-xs font-medium text-gray-600 mb-1">Prompt</label>
54
+ <label class="block text-xs font-medium text-gray-600 mb-1">{{ t("pluginManageRoles.fieldPrompt") }}</label>
56
55
  <textarea
57
56
  v-model="newForm.prompt"
58
57
  rows="6"
@@ -63,14 +62,14 @@
63
62
 
64
63
  <!-- Plugins -->
65
64
  <div>
66
- <label class="block text-xs font-medium text-gray-600 mb-2">Plugins</label>
65
+ <label class="block text-xs font-medium text-gray-600 mb-2">{{ t("pluginManageRoles.fieldPlugins") }}</label>
67
66
  <div class="grid gap-x-4 gap-y-1 [grid-template-columns:repeat(auto-fit,minmax(180px,1fr))]">
68
67
  <label
69
68
  v-for="plugin in availablePlugins"
70
69
  :key="plugin.name"
71
70
  class="flex items-center gap-2 text-sm cursor-pointer"
72
71
  :class="plugin.enabled ? 'text-gray-700' : 'text-gray-400 cursor-not-allowed'"
73
- :title="plugin.enabled ? '' : `Requires ${plugin.requiredEnv.join(', ')} in .env`"
72
+ :title="plugin.enabled ? '' : t('pluginManageRoles.requiresEnv', { env: plugin.requiredEnv.join(', ') })"
74
73
  >
75
74
  <input
76
75
  v-model="newForm.selectedPlugins"
@@ -80,7 +79,7 @@
80
79
  class="cursor-pointer disabled:cursor-not-allowed"
81
80
  />
82
81
  {{ plugin.name }}
83
- <span v-if="!plugin.enabled" class="text-xs text-gray-400">(missing {{ plugin.requiredEnv.join(", ") }})</span>
82
+ <span v-if="!plugin.enabled" class="text-xs text-gray-400">{{ t("pluginManageRoles.missingEnv", { env: plugin.requiredEnv.join(", ") }) }}</span>
84
83
  </label>
85
84
  </div>
86
85
  </div>
@@ -88,8 +87,8 @@
88
87
  <!-- Starter queries -->
89
88
  <div>
90
89
  <label class="block text-xs font-medium text-gray-600 mb-1">
91
- Starter queries
92
- <span class="text-gray-400 font-normal">(one per line)</span>
90
+ {{ t("pluginManageRoles.fieldStarterQueries") }}
91
+ <span class="text-gray-400 font-normal">{{ t("pluginManageRoles.onePerLine") }}</span>
93
92
  </label>
94
93
  <textarea
95
94
  v-model="newForm.queriesText"
@@ -106,9 +105,11 @@
106
105
  :title="newFormError ?? ''"
107
106
  @click="saveNew"
108
107
  >
109
- {{ saving ? "Creating…" : "Create" }}
108
+ {{ saving ? t("pluginManageRoles.creating") : t("pluginManageRoles.create") }}
109
+ </button>
110
+ <button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="cancelCreate">
111
+ {{ t("common.cancel") }}
110
112
  </button>
111
- <button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="cancelCreate">Cancel</button>
112
113
  </div>
113
114
  <div v-if="newFormError" class="text-xs text-gray-500" data-testid="role-form-hint">
114
115
  {{ newFormError }}
@@ -119,7 +120,7 @@
119
120
  </div>
120
121
 
121
122
  <div v-if="!creating && customRoles.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">
122
- No custom roles yet. Click "+ Add" or ask Claude to create one.
123
+ {{ t("pluginManageRoles.emptyHint") }}
123
124
  </div>
124
125
 
125
126
  <ul v-if="customRoles.length > 0" class="p-4 space-y-2">
@@ -134,13 +135,16 @@
134
135
  <div class="flex-1 min-w-0">
135
136
  <div class="font-medium text-sm text-gray-800">
136
137
  {{ role.name }}
137
- <span class="ml-1 text-xs font-mono text-gray-400">({{ role.id }})</span>
138
+ <span class="ml-1 text-xs font-mono text-gray-400">{{ t("pluginManageRoles.idFormatted", { id: role.id }) }}</span>
138
139
  </div>
139
140
  <div class="text-xs text-gray-400 truncate">
140
141
  {{ role.availablePlugins.join(", ") }}
141
142
  </div>
142
143
  </div>
143
- <span class="material-icons text-gray-400 text-sm" :title="selectedId === role.id ? 'Collapse' : 'Expand'">
144
+ <span
145
+ class="material-icons text-gray-400 text-sm"
146
+ :title="selectedId === role.id ? t('pluginManageRoles.collapse') : t('pluginManageRoles.expand')"
147
+ >
144
148
  {{ selectedId === role.id ? "expand_less" : "expand_more" }}
145
149
  </span>
146
150
  </div>
@@ -150,7 +154,7 @@
150
154
  <!-- ID + Name + Icon row -->
151
155
  <div class="flex gap-3">
152
156
  <div class="w-40">
153
- <label class="block text-xs font-medium text-gray-600 mb-1">ID</label>
157
+ <label class="block text-xs font-medium text-gray-600 mb-1">{{ t("pluginManageRoles.fieldId") }}</label>
154
158
  <input
155
159
  v-model="editForm.id"
156
160
  type="text"
@@ -158,7 +162,7 @@
158
162
  />
159
163
  </div>
160
164
  <div class="flex-1">
161
- <label class="block text-xs font-medium text-gray-600 mb-1">Name</label>
165
+ <label class="block text-xs font-medium text-gray-600 mb-1">{{ t("pluginManageRoles.fieldName") }}</label>
162
166
  <input
163
167
  v-model="editForm.name"
164
168
  type="text"
@@ -168,8 +172,10 @@
168
172
  </div>
169
173
  <div class="w-32">
170
174
  <label class="block text-xs font-medium text-gray-600 mb-1">
171
- Icon
172
- <a class="text-blue-400 font-normal ml-1" href="https://fonts.google.com/icons" target="_blank" rel="noopener">?</a>
175
+ {{ t("pluginManageRoles.fieldIcon") }}
176
+ <a class="text-blue-400 font-normal ml-1" href="https://fonts.google.com/icons" target="_blank" rel="noopener">{{
177
+ t("pluginManageRoles.helpLink")
178
+ }}</a>
173
179
  </label>
174
180
  <input
175
181
  v-model="editForm.icon"
@@ -181,7 +187,7 @@
181
187
 
182
188
  <!-- Prompt -->
183
189
  <div>
184
- <label class="block text-xs font-medium text-gray-600 mb-1">Prompt</label>
190
+ <label class="block text-xs font-medium text-gray-600 mb-1">{{ t("pluginManageRoles.fieldPrompt") }}</label>
185
191
  <textarea
186
192
  v-model="editForm.prompt"
187
193
  rows="6"
@@ -192,14 +198,14 @@
192
198
 
193
199
  <!-- Plugins -->
194
200
  <div>
195
- <label class="block text-xs font-medium text-gray-600 mb-2">Plugins</label>
201
+ <label class="block text-xs font-medium text-gray-600 mb-2">{{ t("pluginManageRoles.fieldPlugins") }}</label>
196
202
  <div class="grid gap-x-4 gap-y-1 [grid-template-columns:repeat(auto-fit,minmax(180px,1fr))]">
197
203
  <label
198
204
  v-for="plugin in availablePlugins"
199
205
  :key="plugin.name"
200
206
  class="flex items-center gap-2 text-sm cursor-pointer"
201
207
  :class="plugin.enabled ? 'text-gray-700' : 'text-gray-400 cursor-not-allowed'"
202
- :title="plugin.enabled ? '' : `Requires ${plugin.requiredEnv.join(', ')} in .env`"
208
+ :title="plugin.enabled ? '' : t('pluginManageRoles.requiresEnv', { env: plugin.requiredEnv.join(', ') })"
203
209
  >
204
210
  <input
205
211
  v-model="editForm.selectedPlugins"
@@ -209,7 +215,9 @@
209
215
  class="cursor-pointer disabled:cursor-not-allowed"
210
216
  />
211
217
  {{ plugin.name }}
212
- <span v-if="!plugin.enabled" class="text-xs text-gray-400">(missing {{ plugin.requiredEnv.join(", ") }})</span>
218
+ <span v-if="!plugin.enabled" class="text-xs text-gray-400">{{
219
+ t("pluginManageRoles.missingEnv", { env: plugin.requiredEnv.join(", ") })
220
+ }}</span>
213
221
  </label>
214
222
  </div>
215
223
  </div>
@@ -217,8 +225,8 @@
217
225
  <!-- Starter queries -->
218
226
  <div>
219
227
  <label class="block text-xs font-medium text-gray-600 mb-1">
220
- Starter queries
221
- <span class="text-gray-400 font-normal">(one per line)</span>
228
+ {{ t("pluginManageRoles.fieldStarterQueries") }}
229
+ <span class="text-gray-400 font-normal">{{ t("pluginManageRoles.onePerLine") }}</span>
222
230
  </label>
223
231
  <textarea
224
232
  v-model="editForm.queriesText"
@@ -236,16 +244,18 @@
236
244
  :title="editFormError ?? ''"
237
245
  @click="saveEdit(role.id)"
238
246
  >
239
- {{ saving ? "Updating…" : "Update" }}
247
+ {{ saving ? t("pluginManageRoles.updating") : t("pluginManageRoles.update") }}
248
+ </button>
249
+ <button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="selectedId = null">
250
+ {{ t("common.cancel") }}
240
251
  </button>
241
- <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>
242
252
  </div>
243
253
  <button
244
254
  class="px-3 py-1.5 text-sm rounded border border-red-200 text-red-500 hover:bg-red-50 disabled:opacity-50"
245
255
  :disabled="saving"
246
256
  @click="deleteRole(role.id)"
247
257
  >
248
- Delete
258
+ {{ t("pluginManageRoles.delete") }}
249
259
  </button>
250
260
  </div>
251
261
  <div v-if="editFormError" class="text-xs text-gray-500">
@@ -263,6 +273,7 @@
263
273
 
264
274
  <script setup lang="ts">
265
275
  import { ref, computed, watch, onMounted } from "vue";
276
+ import { useI18n } from "vue-i18n";
266
277
  import { useFreshPluginData } from "../../composables/useFreshPluginData";
267
278
  import { useAppApi } from "../../composables/useAppApi";
268
279
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
@@ -271,6 +282,8 @@ import { getAllPluginNames } from "../../tools/index";
271
282
  import { apiGet, apiPost } from "../../utils/api";
272
283
  import { API_ROUTES } from "../../config/apiRoutes";
273
284
 
285
+ const { t } = useI18n();
286
+
274
287
  interface PluginEntry {
275
288
  name: string;
276
289
  enabled: boolean;
@@ -406,7 +419,10 @@ async function callManage(body: Record<string, unknown>): Promise<ManageResult>
406
419
  // give us anything useful.
407
420
  return {
408
421
  success: false,
409
- error: result.status === 0 ? result.error || "Network error" : result.error || `Server error: ${result.status}`,
422
+ error:
423
+ result.status === 0
424
+ ? result.error || t("pluginManageRoles.errNetworkError")
425
+ : result.error || t("pluginManageRoles.errServerError", { status: result.status }),
410
426
  };
411
427
  }
412
428
  return result.data;
@@ -432,13 +448,13 @@ async function refreshList() {
432
448
  function validateRoleForm(form: EditForm, excludeId: string | null): string | null {
433
449
  const trimmedId = form.id.trim();
434
450
  const trimmedName = form.name.trim();
435
- if (!trimmedId) return "ID is required.";
451
+ if (!trimmedId) return t("pluginManageRoles.errIdRequired");
436
452
  if (!/^[a-zA-Z0-9_-]+$/.test(trimmedId)) {
437
- return "ID may only contain letters, numbers, '-' and '_'.";
453
+ return t("pluginManageRoles.errIdInvalid");
438
454
  }
439
- if (!trimmedName) return "Name is required.";
455
+ if (!trimmedName) return t("pluginManageRoles.errNameRequired");
440
456
  if (customRoles.value.some((existing) => existing.id === trimmedId && existing.id !== excludeId)) {
441
- return `A role with ID '${trimmedId}' already exists.`;
457
+ return t("pluginManageRoles.errIdDuplicate", { id: trimmedId });
442
458
  }
443
459
  return null;
444
460
  }
@@ -473,7 +489,7 @@ async function saveNew() {
473
489
  creating.value = false;
474
490
  await refreshList();
475
491
  } else {
476
- createError.value = result.error ?? "Create failed";
492
+ createError.value = result.error ?? t("pluginManageRoles.errCreateFailed");
477
493
  }
478
494
  saving.value = false;
479
495
  }
@@ -505,7 +521,7 @@ async function saveEdit(originalId: string) {
505
521
  selectedId.value = null;
506
522
  await refreshList();
507
523
  } else {
508
- saveError.value = result.error ?? "Save failed";
524
+ saveError.value = result.error ?? t("pluginManageRoles.errSaveFailed");
509
525
  }
510
526
  saving.value = false;
511
527
  }
@@ -518,7 +534,7 @@ async function deleteRole(roleId: string) {
518
534
  selectedId.value = null;
519
535
  await refreshList();
520
536
  } else {
521
- saveError.value = result.error ?? "Delete failed";
537
+ saveError.value = result.error ?? t("pluginManageRoles.errDeleteFailed");
522
538
  }
523
539
  saving.value = false;
524
540
  }
@@ -1,21 +1,26 @@
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
4
  <span class="material-icons" style="font-size: 14px">auto_awesome</span>
5
- <span>{{ skills.length }} skill{{ skills.length !== 1 ? "s" : "" }}</span>
5
+ <span>{{ t("pluginManageSkills.previewCount", skills.length, { named: { count: skills.length } }) }}</span>
6
6
  </div>
7
7
  <div v-for="skill in skills.slice(0, 6)" :key="skill.name" class="text-xs text-gray-600 truncate">
8
8
  {{ skill.name }}
9
9
  </div>
10
- <div v-if="skills.length > 6" class="text-xs text-gray-400 italic">+{{ skills.length - 6 }} more</div>
10
+ <div v-if="skills.length > 6" class="text-xs text-gray-400 italic">
11
+ {{ t("pluginManageSkills.previewMore", { count: skills.length - 6 }) }}
12
+ </div>
11
13
  </div>
12
14
  </template>
13
15
 
14
16
  <script setup lang="ts">
15
17
  import { computed } from "vue";
18
+ import { useI18n } from "vue-i18n";
16
19
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
17
20
  import type { ManageSkillsData } from "./index";
18
21
 
22
+ const { t } = useI18n();
23
+
19
24
  const props = defineProps<{ result: ToolResultComplete<ManageSkillsData> }>();
20
25
  const skills = computed(() => props.result.data?.skills ?? []);
21
26
  </script>
@@ -3,8 +3,8 @@
3
3
  <!-- Header -->
4
4
  <div class="flex items-center justify-between px-6 py-4 border-b border-gray-100 shrink-0">
5
5
  <div>
6
- <h2 class="text-lg font-semibold text-gray-800">Skills</h2>
7
- <p class="text-xs text-gray-400 mt-0.5">{{ skills.length }} available · click one to view · "Run" invokes it as /&lt;name&gt;</p>
6
+ <h2 class="text-lg font-semibold text-gray-800">{{ t("pluginManageSkills.heading") }}</h2>
7
+ <p class="text-xs text-gray-400 mt-0.5">{{ t("pluginManageSkills.subheading", { count: skills.length }) }}</p>
8
8
  </div>
9
9
  </div>
10
10
 
@@ -32,15 +32,16 @@
32
32
  {{ skill.source }}
33
33
  </div>
34
34
  </div>
35
- <p v-if="skills.length === 0" class="p-4 text-sm text-gray-400 italic">
36
- No skills found. Add skill folders under
37
- <code class="text-[11px]">~/.claude/skills/</code>.
38
- </p>
35
+ <i18n-t v-if="skills.length === 0" keypath="pluginManageSkills.emptyWithPath" tag="p" class="p-4 text-sm text-gray-400 italic">
36
+ <template #path>
37
+ <code class="text-[11px]">{{ t("pluginManageSkills.emptySkillPath") }}</code>
38
+ </template>
39
+ </i18n-t>
39
40
  </div>
40
41
 
41
42
  <!-- Right: detail pane -->
42
43
  <div class="flex-1 min-w-0 overflow-y-auto">
43
- <div v-if="!selected" class="p-6 text-sm text-gray-400 italic">Select a skill on the left to view its SKILL.md.</div>
44
+ <div v-if="!selected" class="p-6 text-sm text-gray-400 italic">{{ t("pluginManageSkills.selectHint") }}</div>
44
45
  <div v-else class="p-6">
45
46
  <div class="flex items-start justify-between gap-4 mb-4">
46
47
  <div class="min-w-0">
@@ -58,7 +59,7 @@
58
59
  data-testid="skill-cancel-btn"
59
60
  @click="cancelEdit"
60
61
  >
61
- Cancel
62
+ {{ t("common.cancel") }}
62
63
  </button>
63
64
  <button
64
65
  class="px-3 py-1.5 text-sm rounded bg-green-600 hover:bg-green-700 text-white disabled:opacity-40 flex items-center gap-1"
@@ -67,7 +68,7 @@
67
68
  @click="saveEdit"
68
69
  >
69
70
  <span class="material-icons text-base">save</span>
70
- Save
71
+ {{ t("common.save") }}
71
72
  </button>
72
73
  </template>
73
74
  <template v-else>
@@ -79,18 +80,18 @@
79
80
  @click="startEdit"
80
81
  >
81
82
  <span class="material-icons text-base">edit</span>
82
- Edit
83
+ {{ t("pluginManageSkills.btnEdit") }}
83
84
  </button>
84
85
  <button
85
86
  v-if="detail && detail.source === 'project'"
86
87
  class="px-3 py-1.5 text-sm rounded border border-red-300 text-red-600 hover:bg-red-50 disabled:opacity-40 flex items-center gap-1"
87
88
  :disabled="detailLoading || deleting"
88
89
  data-testid="skill-delete-btn"
89
- title="Delete this project-scope skill"
90
+ :title="t('pluginManageSkills.deleteProjectSkill')"
90
91
  @click="deleteSkill"
91
92
  >
92
93
  <span class="material-icons text-base">delete</span>
93
- Delete
94
+ {{ t("pluginManageSkills.btnDelete") }}
94
95
  </button>
95
96
  <button
96
97
  class="px-3 py-1.5 text-sm rounded bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-40 flex items-center gap-1"
@@ -99,19 +100,19 @@
99
100
  @click="runSkill"
100
101
  >
101
102
  <span class="material-icons text-base">play_arrow</span>
102
- Run
103
+ {{ t("pluginManageSkills.btnRun") }}
103
104
  </button>
104
105
  </template>
105
106
  </div>
106
107
  </div>
107
- <div v-if="detailLoading" class="text-sm text-gray-400 italic">Loading…</div>
108
+ <div v-if="detailLoading" class="text-sm text-gray-400 italic">{{ t("pluginManageSkills.loading") }}</div>
108
109
  <div v-else-if="detailError" class="text-sm text-red-600">
109
110
  {{ detailError }}
110
111
  </div>
111
112
  <!-- Edit mode -->
112
113
  <div v-else-if="editing && detail" class="space-y-4">
113
114
  <div>
114
- <label class="block text-xs font-medium text-gray-500 mb-1"> Description </label>
115
+ <label class="block text-xs font-medium text-gray-500 mb-1"> {{ t("pluginManageSkills.fieldDescription") }} </label>
115
116
  <input
116
117
  v-model="editDescription"
117
118
  data-testid="skill-edit-description"
@@ -119,7 +120,7 @@
119
120
  />
120
121
  </div>
121
122
  <div class="flex-1">
122
- <label class="block text-xs font-medium text-gray-500 mb-1"> Body (Markdown) </label>
123
+ <label class="block text-xs font-medium text-gray-500 mb-1"> {{ t("pluginManageSkills.fieldBody") }} </label>
123
124
  <textarea
124
125
  v-model="editBody"
125
126
  data-testid="skill-edit-body"
@@ -130,7 +131,7 @@
130
131
  <!-- View mode -->
131
132
  <!-- eslint-disable-next-line vue/no-v-html -- sanitized via DOMPurify -->
132
133
  <div v-else-if="detail && renderedBody" class="markdown-content text-gray-700" data-testid="skill-body-rendered" v-html="renderedBody"></div>
133
- <p v-else-if="detail" class="text-sm text-gray-400 italic">(empty body)</p>
134
+ <p v-else-if="detail" class="text-sm text-gray-400 italic">{{ t("pluginManageSkills.emptyBody") }}</p>
134
135
  </div>
135
136
  </div>
136
137
  </div>
@@ -139,6 +140,7 @@
139
140
 
140
141
  <script setup lang="ts">
141
142
  import { computed, onMounted, ref, watch } from "vue";
143
+ import { useI18n } from "vue-i18n";
142
144
  import { marked } from "marked";
143
145
  import DOMPurify from "dompurify";
144
146
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
@@ -147,6 +149,8 @@ import { useAppApi } from "../../composables/useAppApi";
147
149
  import { apiGet, apiPut, apiDelete } from "../../utils/api";
148
150
  import { API_ROUTES } from "../../config/apiRoutes";
149
151
 
152
+ const { t } = useI18n();
153
+
150
154
  interface SkillDetail {
151
155
  name: string;
152
156
  description: string;
@@ -173,7 +177,7 @@ const saving = ref(false);
173
177
  const editDescription = ref("");
174
178
  const editBody = ref("");
175
179
 
176
- const selected = computed(() => skills.value.find((s) => s.name === selectedName.value) ?? null);
180
+ const selected = computed(() => skills.value.find((skill) => skill.name === selectedName.value) ?? null);
177
181
 
178
182
  const renderedBody = computed(() => {
179
183
  const body = detail.value?.body;
@@ -199,7 +203,7 @@ onMounted(async () => {
199
203
  if (props.selectedResult || skills.value.length > 0) return;
200
204
  const response = await apiGet<{ skills: SkillSummary[] }>(API_ROUTES.skills.list);
201
205
  if (!response.ok) {
202
- listError.value = `Failed to load skills: ${response.error}`;
206
+ listError.value = t("pluginManageSkills.errListFailed", { error: response.error });
203
207
  return;
204
208
  }
205
209
  if (Array.isArray(response.data.skills)) {
@@ -231,7 +235,7 @@ watch(
231
235
  return;
232
236
  }
233
237
  if (!response.ok) {
234
- detailError.value = `Failed to load skill: ${response.error}`;
238
+ detailError.value = t("pluginManageSkills.errDetailFailed", { error: response.error });
235
239
  detail.value = null;
236
240
  } else {
237
241
  detail.value = response.data.skill;
@@ -263,7 +267,7 @@ async function saveEdit(): Promise<void> {
263
267
  });
264
268
  saving.value = false;
265
269
  if (!result.ok) {
266
- detailError.value = `Save failed: ${result.error}`;
270
+ detailError.value = t("pluginManageSkills.errSaveFailed", { error: result.error });
267
271
  return;
268
272
  }
269
273
  detail.value = {
@@ -272,7 +276,7 @@ async function saveEdit(): Promise<void> {
272
276
  body: editBody.value,
273
277
  };
274
278
  // Update the sidebar summary too.
275
- const idx = skills.value.findIndex((s) => s.name === name);
279
+ const idx = skills.value.findIndex((skill) => skill.name === name);
276
280
  if (idx >= 0) {
277
281
  skills.value[idx] = {
278
282
  ...skills.value[idx],
@@ -300,18 +304,18 @@ function runSkill(): void {
300
304
  async function deleteSkill(): Promise<void> {
301
305
  if (!detail.value || detail.value.source !== "project") return;
302
306
  const name = detail.value.name;
303
- if (!window.confirm(`Delete skill "${name}"? This removes ~/mulmoclaude/.claude/skills/${name}/SKILL.md.`)) {
307
+ if (!window.confirm(t("pluginManageSkills.confirmDelete", { name }))) {
304
308
  return;
305
309
  }
306
310
  deleting.value = true;
307
311
  const result = await apiDelete<unknown>(API_ROUTES.skills.remove.replace(":name", encodeURIComponent(name)));
308
312
  deleting.value = false;
309
313
  if (!result.ok) {
310
- detailError.value = result.error || "Failed to delete";
314
+ detailError.value = result.error || t("pluginManageSkills.errDeleteFailed");
311
315
  return;
312
316
  }
313
317
  // Remove from the local list, advance selection, clear detail.
314
- const idx = skills.value.findIndex((s) => s.name === name);
318
+ const idx = skills.value.findIndex((skill) => skill.name === name);
315
319
  if (idx >= 0) {
316
320
  skills.value.splice(idx, 1);
317
321
  }
@@ -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>
@@ -24,7 +24,7 @@ const hint = computed(() => {
24
24
  if (sources.length === 0) return "No sources registered yet.";
25
25
  const names = sources
26
26
  .slice(0, 3)
27
- .map((s: Source) => s.slug)
27
+ .map((source: Source) => source.slug)
28
28
  .join(", ");
29
29
  const tail = sources.length > 3 ? ", …" : "";
30
30
  const plural = sources.length === 1 ? "" : "s";