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
@@ -9,8 +9,8 @@
9
9
  @click.stop
10
10
  >
11
11
  <div class="px-5 py-4 border-b border-gray-200 flex items-center justify-between">
12
- <h2 id="settings-modal-title" class="text-base font-semibold text-gray-900">Settings</h2>
13
- <button class="text-gray-400 hover:text-gray-700" title="Close" data-testid="settings-close-btn" @click="close">
12
+ <h2 id="settings-modal-title" class="text-base font-semibold text-gray-900">{{ t("settingsModal.title") }}</h2>
13
+ <button class="text-gray-400 hover:text-gray-700" :title="t('common.close')" data-testid="settings-close-btn" @click="close">
14
14
  <span class="material-icons">close</span>
15
15
  </button>
16
16
  </div>
@@ -22,7 +22,7 @@
22
22
  data-testid="settings-tab-tools"
23
23
  @click="activeTab = 'tools'"
24
24
  >
25
- Allowed Tools
25
+ {{ t("settingsModal.tabs.tools") }}
26
26
  </button>
27
27
  <button
28
28
  class="px-3 py-2 text-sm border-b-2"
@@ -30,7 +30,7 @@
30
30
  data-testid="settings-tab-mcp"
31
31
  @click="activeTab = 'mcp'"
32
32
  >
33
- MCP Servers
33
+ {{ t("settingsModal.tabs.mcp") }}
34
34
  </button>
35
35
  <button
36
36
  class="px-3 py-2 text-sm border-b-2"
@@ -38,7 +38,7 @@
38
38
  data-testid="settings-tab-dirs"
39
39
  @click="activeTab = 'dirs'"
40
40
  >
41
- Directories
41
+ {{ t("settingsModal.tabs.dirs") }}
42
42
  </button>
43
43
  <button
44
44
  class="px-3 py-2 text-sm border-b-2"
@@ -46,7 +46,7 @@
46
46
  data-testid="settings-tab-refs"
47
47
  @click="activeTab = 'refs'"
48
48
  >
49
- Reference Dirs
49
+ {{ t("settingsModal.tabs.refs") }}
50
50
  </button>
51
51
  </div>
52
52
 
@@ -56,13 +56,12 @@
56
56
  </div>
57
57
 
58
58
  <div v-if="activeTab === 'tools'" class="space-y-3">
59
- <p class="text-xs text-gray-600 leading-relaxed">
60
- Extra tool names to pass to Claude via
61
- <code class="bg-gray-100 px-1 rounded">--allowedTools</code>. One per line. Useful for built-in Claude Code MCP servers like Gmail / Google Calendar
62
- after you have authenticated via <code class="bg-gray-100 px-1 rounded">claude mcp</code>.
63
- </p>
59
+ <i18n-t keypath="settingsToolsTab.explanation" tag="p" class="text-xs text-gray-600 leading-relaxed">
60
+ <template #allowedTools><code class="bg-gray-100 px-1 rounded">--allowedTools</code></template>
61
+ <template #claudeMcp><code class="bg-gray-100 px-1 rounded">claude mcp</code></template>
62
+ </i18n-t>
64
63
  <label class="block">
65
- <span class="text-xs font-semibold text-gray-700">Tool names</span>
64
+ <span class="text-xs font-semibold text-gray-700">{{ t("settingsModal.toolNamesLabel") }}</span>
66
65
  <textarea
67
66
  v-model="toolsText"
68
67
  class="mt-1 w-full h-48 px-2 py-1.5 text-sm font-mono border border-gray-300 rounded focus:outline-none focus:border-blue-400"
@@ -72,8 +71,8 @@
72
71
  ></textarea>
73
72
  </label>
74
73
  <p v-if="invalidToolNames.length > 0" class="text-xs text-amber-700">
75
- These look non-standard (expected prefix
76
- <code class="bg-gray-100 px-1 rounded">mcp__</code>):
74
+ {{ t("settingsModal.invalidToolNamesPrefix") }}
75
+ <code class="bg-gray-100 px-1 rounded">mcp__</code>{{ t("settingsModal.invalidToolNamesSuffix") }}
77
76
  {{ invalidToolNames.join(", ") }}
78
77
  </p>
79
78
  </div>
@@ -85,7 +84,7 @@
85
84
  role="alert"
86
85
  data-testid="mcp-tools-error"
87
86
  >
88
- Could not fetch MCP tool status: {{ mcpToolsError }}. Showing all tools regardless of enablement.
87
+ {{ t("settingsModal.mcpToolsError", { error: mcpToolsError }) }}
89
88
  </div>
90
89
  <SettingsMcpTab
91
90
  ref="mcpTabRef"
@@ -106,19 +105,19 @@
106
105
  <span v-if="statusMessage" class="text-xs" :class="statusError ? 'text-red-600' : 'text-green-600'" data-testid="settings-status">
107
106
  {{ statusMessage }}
108
107
  </span>
109
- <span v-else class="text-xs text-gray-500"> Changes apply on the next message. No restart needed. </span>
108
+ <span v-else class="text-xs text-gray-500"> {{ t("settingsModal.changesHint") }} </span>
110
109
  <div class="flex gap-2">
111
110
  <button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" data-testid="settings-cancel-btn" @click="close">
112
- Cancel
111
+ {{ t("common.cancel") }}
113
112
  </button>
114
113
  <button
115
114
  class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
116
115
  :disabled="saving || loading || !!loadError"
117
- :title="loadError ? 'Cannot save until settings load successfully' : undefined"
116
+ :title="loadError ? t('settingsModal.cannotSaveTooltip') : undefined"
118
117
  data-testid="settings-save-btn"
119
118
  @click="save"
120
119
  >
121
- {{ saving ? "Saving…" : loading ? "Loading…" : "Save" }}
120
+ {{ saving ? t("settingsModal.saving") : loading ? t("settingsModal.loadingLabel") : t("common.save") }}
122
121
  </button>
123
122
  </div>
124
123
  </div>
@@ -128,6 +127,7 @@
128
127
 
129
128
  <script setup lang="ts">
130
129
  import { computed, ref, watch } from "vue";
130
+ import { useI18n } from "vue-i18n";
131
131
  import SettingsMcpTab from "./SettingsMcpTab.vue";
132
132
  import SettingsWorkspaceDirsTab from "./SettingsWorkspaceDirsTab.vue";
133
133
  import SettingsReferenceDirsTab from "./SettingsReferenceDirsTab.vue";
@@ -135,6 +135,8 @@ import type { McpServerEntry } from "./SettingsMcpTab.vue";
135
135
  import { apiGet, apiPut } from "../utils/api";
136
136
  import { API_ROUTES } from "../config/apiRoutes";
137
137
 
138
+ const { t } = useI18n();
139
+
138
140
  interface Props {
139
141
  open: boolean;
140
142
  dockerMode?: boolean;
@@ -177,11 +179,11 @@ let loadToken = 0;
177
179
  const parsedToolNames = computed(() =>
178
180
  toolsText.value
179
181
  .split("\n")
180
- .map((s) => s.trim())
181
- .filter((s) => s.length > 0),
182
+ .map((line) => line.trim())
183
+ .filter((line) => line.length > 0),
182
184
  );
183
185
 
184
- const invalidToolNames = computed(() => parsedToolNames.value.filter((n) => !n.startsWith("mcp__") && !isBuiltIn(n)));
186
+ const invalidToolNames = computed(() => parsedToolNames.value.filter((name) => !name.startsWith("mcp__") && !isBuiltIn(name)));
185
187
 
186
188
  function isBuiltIn(name: string): boolean {
187
189
  return ["Bash", "Read", "Write", "Edit", "Glob", "Grep", "WebFetch", "WebSearch"].includes(name);
@@ -1,18 +1,24 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, onMounted } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import { apiGet, apiPut } from "../utils/api";
4
5
  import { API_ROUTES } from "../config/apiRoutes";
5
6
 
7
+ const { t } = useI18n();
8
+
6
9
  interface RefDirEntry {
7
10
  hostPath: string;
8
11
  label: string;
9
12
  }
10
13
 
14
+ // See note in SettingsWorkspaceDirsTab — mirrored here.
15
+ type SaveStatus = { kind: "ok" } | { kind: "error"; message: string };
16
+
11
17
  const dirs = ref<RefDirEntry[]>([]);
12
18
  const loading = ref(true);
13
19
  const error = ref("");
14
20
  const saving = ref(false);
15
- const saveStatus = ref("");
21
+ const saveStatus = ref<SaveStatus | null>(null);
16
22
 
17
23
  const draftPath = ref("");
18
24
  const draftLabel = ref("");
@@ -32,50 +38,50 @@ async function load(): Promise<void> {
32
38
 
33
39
  async function save(): Promise<void> {
34
40
  saving.value = true;
35
- saveStatus.value = "";
41
+ saveStatus.value = null;
36
42
  const result = await apiPut<{ dirs: RefDirEntry[] }>(API_ROUTES.config.referenceDirs, { dirs: dirs.value });
37
43
  saving.value = false;
38
44
  if (!result.ok) {
39
- saveStatus.value = result.error;
45
+ saveStatus.value = { kind: "error", message: result.error };
40
46
  return;
41
47
  }
42
48
  dirs.value = result.data.dirs;
43
- saveStatus.value = "Saved";
49
+ saveStatus.value = { kind: "ok" };
44
50
  setTimeout(() => {
45
- saveStatus.value = "";
51
+ saveStatus.value = null;
46
52
  }, 2000);
47
53
  }
48
54
 
49
55
  function addEntry(): void {
50
56
  draftError.value = "";
51
- const p = draftPath.value.trim();
52
- if (!p) {
53
- draftError.value = "Path required";
57
+ const path = draftPath.value.trim();
58
+ if (!path) {
59
+ draftError.value = t("settingsReferenceDirs.errPathRequired");
54
60
  return;
55
61
  }
56
- if (!p.startsWith("/") && !p.startsWith("~/")) {
57
- draftError.value = "Must be an absolute path or start with ~/";
62
+ if (!path.startsWith("/") && !path.startsWith("~/")) {
63
+ draftError.value = t("settingsReferenceDirs.errMustBeAbsolute");
58
64
  return;
59
65
  }
60
66
  // Normalize: trim trailing slashes for consistent comparison
61
- let normalized = p;
67
+ let normalized = path;
62
68
  while (normalized.length > 1 && normalized.endsWith("/")) {
63
69
  normalized = normalized.slice(0, -1);
64
70
  }
65
- const stripSlash = (s: string): string => {
66
- let r = s;
67
- while (r.length > 1 && r.endsWith("/")) r = r.slice(0, -1);
68
- return r;
71
+ const stripSlash = (str: string): string => {
72
+ let cleaned = str;
73
+ while (cleaned.length > 1 && cleaned.endsWith("/")) cleaned = cleaned.slice(0, -1);
74
+ return cleaned;
69
75
  };
70
- if (dirs.value.some((d) => stripSlash(d.hostPath) === normalized)) {
71
- draftError.value = "Already exists";
76
+ if (dirs.value.some((dir) => stripSlash(dir.hostPath) === normalized)) {
77
+ draftError.value = t("settingsReferenceDirs.errAlreadyExists");
72
78
  return;
73
79
  }
74
80
  const lastSeg = normalized.split("/").pop();
75
81
  const label = draftLabel.value.trim() || lastSeg || normalized;
76
82
  // Reject duplicate labels — @ref/<label> routing requires uniqueness
77
- if (dirs.value.some((d) => d.label === label)) {
78
- draftError.value = `Label "${label}" already exists`;
83
+ if (dirs.value.some((dir) => dir.label === label)) {
84
+ draftError.value = t("settingsReferenceDirs.errLabelConflict", { label });
79
85
  return;
80
86
  }
81
87
  dirs.value.push({ hostPath: normalized, label });
@@ -92,20 +98,17 @@ onMounted(load);
92
98
 
93
99
  <template>
94
100
  <div class="space-y-3">
95
- <p class="text-xs text-gray-600 leading-relaxed">
96
- External directories Claude can read but not modify. In Docker mode, these are mounted read-only. Useful for referencing Obsidian vaults, project code, or
97
- document folders.
98
- </p>
101
+ <p class="text-xs text-gray-600 leading-relaxed">{{ t("settingsReferenceDirs.explanation") }}</p>
99
102
 
100
103
  <!-- Loading -->
101
- <div v-if="loading" class="text-sm text-gray-400">Loading...</div>
104
+ <div v-if="loading" class="text-sm text-gray-400">{{ t("common.loading") }}</div>
102
105
  <div v-else-if="error" class="text-sm text-red-600 bg-red-50 rounded px-3 py-2">
103
106
  {{ error }}
104
107
  </div>
105
108
 
106
109
  <template v-else>
107
110
  <!-- Existing entries -->
108
- <div v-if="dirs.length === 0" class="text-sm text-gray-400">No reference directories configured.</div>
111
+ <div v-if="dirs.length === 0" class="text-sm text-gray-400">{{ t("settingsReferenceDirs.noEntries") }}</div>
109
112
  <div v-else class="space-y-1.5">
110
113
  <div
111
114
  v-for="(dir, i) in dirs"
@@ -122,8 +125,8 @@ onMounted(load);
122
125
  {{ dir.label }}
123
126
  </div>
124
127
  </div>
125
- <span class="text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-600 shrink-0"> read-only </span>
126
- <button class="text-gray-300 hover:text-red-500 shrink-0" title="Remove" data-testid="reference-dir-remove-btn" @click="removeEntry(i)">
128
+ <span class="text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-600 shrink-0">{{ t("settingsReferenceDirs.readOnlyBadge") }}</span>
129
+ <button class="text-gray-300 hover:text-red-500 shrink-0" :title="t('common.remove')" data-testid="reference-dir-remove-btn" @click="removeEntry(i)">
127
130
  <span class="material-icons text-sm">close</span>
128
131
  </button>
129
132
  </div>
@@ -131,11 +134,11 @@ onMounted(load);
131
134
 
132
135
  <!-- Add new -->
133
136
  <div class="border border-gray-200 rounded p-2 space-y-2">
134
- <div class="text-xs font-semibold text-gray-600">Add reference directory</div>
137
+ <div class="text-xs font-semibold text-gray-600">{{ t("settingsReferenceDirs.addDirTitle") }}</div>
135
138
  <input
136
139
  v-model="draftPath"
137
140
  class="w-full px-2 py-1 text-xs font-mono border border-gray-300 rounded focus:outline-none focus:border-blue-400"
138
- placeholder="/Users/me/ObsidianVault or ~/Documents/notes"
141
+ :placeholder="t('settingsReferenceDirs.pathPlaceholder')"
139
142
  data-testid="reference-dir-path-input"
140
143
  @keydown.enter="addEntry"
141
144
  @keydown.stop
@@ -143,13 +146,15 @@ onMounted(load);
143
146
  <input
144
147
  v-model="draftLabel"
145
148
  class="w-full px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:border-blue-400"
146
- placeholder="Label (optional — defaults to folder name)"
149
+ :placeholder="t('settingsReferenceDirs.labelPlaceholder')"
147
150
  data-testid="reference-dir-label-input"
148
151
  @keydown.enter="addEntry"
149
152
  @keydown.stop
150
153
  />
151
154
  <div class="flex items-center gap-2">
152
- <button class="px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600" data-testid="reference-dir-add-btn" @click="addEntry">Add</button>
155
+ <button class="px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600" data-testid="reference-dir-add-btn" @click="addEntry">
156
+ {{ t("common.add") }}
157
+ </button>
153
158
  <span v-if="draftError" class="text-xs text-red-500">{{ draftError }}</span>
154
159
  </div>
155
160
  </div>
@@ -162,10 +167,10 @@ onMounted(load);
162
167
  data-testid="reference-dirs-save-btn"
163
168
  @click="save"
164
169
  >
165
- {{ saving ? "Saving..." : "Save" }}
170
+ {{ saving ? t("common.saving") : t("common.save") }}
166
171
  </button>
167
- <span v-if="saveStatus" class="text-xs" :class="saveStatus === 'Saved' ? 'text-green-600' : 'text-red-600'">
168
- {{ saveStatus }}
172
+ <span v-if="saveStatus" class="text-xs" :class="saveStatus.kind === 'ok' ? 'text-green-600' : 'text-red-600'">
173
+ {{ saveStatus.kind === "ok" ? t("common.saved") : saveStatus.message }}
169
174
  </span>
170
175
  </div>
171
176
  </template>
@@ -1,19 +1,27 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, onMounted } from "vue";
3
+ import { useI18n } from "vue-i18n";
3
4
  import { apiGet, apiPut } from "../utils/api";
4
5
  import { API_ROUTES } from "../config/apiRoutes";
5
6
 
7
+ const { t } = useI18n();
8
+
6
9
  interface DirEntry {
7
10
  path: string;
8
11
  description: string;
9
12
  structure: "flat" | "by-name" | "by-date";
10
13
  }
11
14
 
15
+ // Typed save status so the template can drive colour off `kind`
16
+ // instead of string-comparing a localised "Saved" — previously the
17
+ // green-on-success styling was coupled to the English literal.
18
+ type SaveStatus = { kind: "ok" } | { kind: "error"; message: string };
19
+
12
20
  const dirs = ref<DirEntry[]>([]);
13
21
  const loading = ref(true);
14
22
  const error = ref("");
15
23
  const saving = ref(false);
16
- const saveStatus = ref("");
24
+ const saveStatus = ref<SaveStatus | null>(null);
17
25
 
18
26
  // Draft for new entry
19
27
  const draftPath = ref("");
@@ -35,37 +43,37 @@ async function load(): Promise<void> {
35
43
 
36
44
  async function save(): Promise<void> {
37
45
  saving.value = true;
38
- saveStatus.value = "";
46
+ saveStatus.value = null;
39
47
  const result = await apiPut<{ dirs: DirEntry[] }>(API_ROUTES.config.workspaceDirs, { dirs: dirs.value });
40
48
  saving.value = false;
41
49
  if (!result.ok) {
42
- saveStatus.value = result.error;
50
+ saveStatus.value = { kind: "error", message: result.error };
43
51
  return;
44
52
  }
45
53
  dirs.value = result.data.dirs;
46
- saveStatus.value = "Saved";
54
+ saveStatus.value = { kind: "ok" };
47
55
  setTimeout(() => {
48
- saveStatus.value = "";
56
+ saveStatus.value = null;
49
57
  }, 2000);
50
58
  }
51
59
 
52
60
  function addEntry(): void {
53
61
  draftError.value = "";
54
- const p = draftPath.value.trim();
55
- if (!p) {
56
- draftError.value = "Path required";
62
+ const path = draftPath.value.trim();
63
+ if (!path) {
64
+ draftError.value = t("settingsWorkspaceDirs.errPathRequired");
57
65
  return;
58
66
  }
59
- if (!p.startsWith("data/") && !p.startsWith("artifacts/")) {
60
- draftError.value = "Must start with data/ or artifacts/";
67
+ if (!path.startsWith("data/") && !path.startsWith("artifacts/")) {
68
+ draftError.value = t("settingsWorkspaceDirs.errMustStartWith");
61
69
  return;
62
70
  }
63
- if (dirs.value.some((d) => d.path === p)) {
64
- draftError.value = "Already exists";
71
+ if (dirs.value.some((dir) => dir.path === path)) {
72
+ draftError.value = t("settingsWorkspaceDirs.errAlreadyExists");
65
73
  return;
66
74
  }
67
75
  dirs.value.push({
68
- path: p,
76
+ path,
69
77
  description: draftDescription.value.trim(),
70
78
  structure: draftStructure.value,
71
79
  });
@@ -83,20 +91,20 @@ onMounted(load);
83
91
 
84
92
  <template>
85
93
  <div class="space-y-3">
86
- <p class="text-xs text-gray-600 leading-relaxed">
87
- Custom directories for organizing files under
88
- <code class="bg-gray-100 px-1 rounded">data/</code> and <code class="bg-gray-100 px-1 rounded">artifacts/</code>. Claude uses these to route file saves.
89
- </p>
94
+ <i18n-t keypath="settingsWorkspaceDirs.explanation" tag="p" class="text-xs text-gray-600 leading-relaxed">
95
+ <template #dataDir><code class="bg-gray-100 px-1 rounded">data/</code></template>
96
+ <template #artifactsDir><code class="bg-gray-100 px-1 rounded">artifacts/</code></template>
97
+ </i18n-t>
90
98
 
91
99
  <!-- Loading -->
92
- <div v-if="loading" class="text-sm text-gray-400">Loading...</div>
100
+ <div v-if="loading" class="text-sm text-gray-400">{{ t("common.loading") }}</div>
93
101
  <div v-else-if="error" class="text-sm text-red-600 bg-red-50 rounded px-3 py-2">
94
102
  {{ error }}
95
103
  </div>
96
104
 
97
105
  <template v-else>
98
106
  <!-- Existing entries -->
99
- <div v-if="dirs.length === 0" class="text-sm text-gray-400">No custom directories configured.</div>
107
+ <div v-if="dirs.length === 0" class="text-sm text-gray-400">{{ t("settingsWorkspaceDirs.noEntries") }}</div>
100
108
  <div v-else class="space-y-1.5">
101
109
  <div
102
110
  v-for="(dir, i) in dirs"
@@ -113,7 +121,7 @@ onMounted(load);
113
121
  <span class="text-[10px] px-1.5 py-0.5 rounded bg-gray-200 text-gray-600 shrink-0">
114
122
  {{ dir.structure }}
115
123
  </span>
116
- <button class="text-gray-300 hover:text-red-500 shrink-0" title="Remove" @click="removeEntry(i)">
124
+ <button class="text-gray-300 hover:text-red-500 shrink-0" :title="t('common.remove')" @click="removeEntry(i)">
117
125
  <span class="material-icons text-sm">close</span>
118
126
  </button>
119
127
  </div>
@@ -121,12 +129,12 @@ onMounted(load);
121
129
 
122
130
  <!-- Add new -->
123
131
  <div class="border border-gray-200 rounded p-2 space-y-2">
124
- <div class="text-xs font-semibold text-gray-600">Add directory</div>
132
+ <div class="text-xs font-semibold text-gray-600">{{ t("settingsWorkspaceDirs.addDirTitle") }}</div>
125
133
  <div class="flex gap-2">
126
134
  <input
127
135
  v-model="draftPath"
128
136
  class="flex-1 px-2 py-1 text-xs font-mono border border-gray-300 rounded focus:outline-none focus:border-blue-400"
129
- placeholder="data/clients or artifacts/reports"
137
+ :placeholder="t('settingsWorkspaceDirs.pathPlaceholder')"
130
138
  data-testid="workspace-dir-path-input"
131
139
  @keydown.enter="addEntry"
132
140
  @keydown.stop
@@ -144,13 +152,15 @@ onMounted(load);
144
152
  <input
145
153
  v-model="draftDescription"
146
154
  class="w-full px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:border-blue-400"
147
- placeholder="Description (what goes in this folder)"
155
+ :placeholder="t('settingsWorkspaceDirs.descPlaceholder')"
148
156
  data-testid="workspace-dir-desc-input"
149
157
  @keydown.enter="addEntry"
150
158
  @keydown.stop
151
159
  />
152
160
  <div class="flex items-center gap-2">
153
- <button class="px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600" data-testid="workspace-dir-add-btn" @click="addEntry">Add</button>
161
+ <button class="px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600" data-testid="workspace-dir-add-btn" @click="addEntry">
162
+ {{ t("common.add") }}
163
+ </button>
154
164
  <span v-if="draftError" class="text-xs text-red-500">{{ draftError }}</span>
155
165
  </div>
156
166
  </div>
@@ -163,10 +173,10 @@ onMounted(load);
163
173
  data-testid="workspace-dirs-save-btn"
164
174
  @click="save"
165
175
  >
166
- {{ saving ? "Saving..." : "Save" }}
176
+ {{ saving ? t("common.saving") : t("common.save") }}
167
177
  </button>
168
- <span v-if="saveStatus" class="text-xs" :class="saveStatus === 'Saved' ? 'text-green-600' : 'text-red-600'">
169
- {{ saveStatus }}
178
+ <span v-if="saveStatus" class="text-xs" :class="saveStatus.kind === 'ok' ? 'text-green-600' : 'text-red-600'">
179
+ {{ saveStatus.kind === "ok" ? t("common.saved") : saveStatus.message }}
170
180
  </span>
171
181
  </div>
172
182
  </template>
@@ -1,7 +1,16 @@
1
1
  <template>
2
2
  <div class="flex items-center gap-2">
3
- <img :src="logoUrl" alt="" class="h-[50px] w-auto -my-3.5 -ml-3 rounded object-contain shrink-0" />
4
- <h1 data-testid="app-title" class="text-sm font-semibold text-gray-800 mr-1" :style="titleStyle">MulmoClaude</h1>
3
+ <button
4
+ type="button"
5
+ class="flex items-center gap-2 -my-1 -ml-1 py-1 pl-1 pr-2 rounded hover:bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400"
6
+ data-testid="app-home-btn"
7
+ :title="t('sidebarHeader.home')"
8
+ :aria-label="t('sidebarHeader.home')"
9
+ @click="emit('home')"
10
+ >
11
+ <img :src="logoUrl" alt="" class="h-[50px] w-auto -my-3.5 -ml-3 rounded object-contain shrink-0" />
12
+ <h1 data-testid="app-title" class="text-sm font-semibold text-gray-800 mr-1" :style="titleStyle">MulmoClaude</h1>
13
+ </button>
5
14
  <div class="flex gap-2">
6
15
  <LockStatusPopup
7
16
  ref="lockPopup"
@@ -12,14 +21,21 @@
12
21
  />
13
22
  <NotificationBell :force-close="lockPopupOpen" @navigate="(action) => emit('notificationNavigate', action)" @update:open="onNotificationOpen" />
14
23
  <button
24
+ v-if="isChatPage"
15
25
  class="text-gray-400 hover:text-gray-700"
16
26
  :class="{ 'text-blue-500': showRightSidebar }"
17
- title="Tool call history"
27
+ :title="t('sidebarHeader.toolCallHistory')"
18
28
  @click="emit('toggleRightSidebar')"
19
29
  >
20
30
  <span class="material-icons">build</span>
21
31
  </button>
22
- <button class="text-gray-400 hover:text-gray-700" data-testid="settings-btn" title="Settings" aria-label="Settings" @click="emit('openSettings')">
32
+ <button
33
+ class="text-gray-400 hover:text-gray-700"
34
+ data-testid="settings-btn"
35
+ :title="t('sidebarHeader.settings')"
36
+ :aria-label="t('sidebarHeader.settings')"
37
+ @click="emit('openSettings')"
38
+ >
23
39
  <span class="material-icons">settings</span>
24
40
  </button>
25
41
  </div>
@@ -28,15 +44,19 @@
28
44
 
29
45
  <script setup lang="ts">
30
46
  import { computed, onBeforeUnmount, onMounted, ref, type CSSProperties } from "vue";
47
+ import { useI18n } from "vue-i18n";
31
48
  import LockStatusPopup from "./LockStatusPopup.vue";
32
49
  import NotificationBell from "./NotificationBell.vue";
33
50
  import { useClickOutside } from "../composables/useClickOutside";
34
51
  import type { NotificationPayload } from "../types/notification";
35
52
  import logoUrl from "../assets/mulmo_bw.png";
36
53
 
54
+ const { t } = useI18n();
55
+
37
56
  defineProps<{
38
57
  sandboxEnabled: boolean;
39
58
  showRightSidebar: boolean;
59
+ isChatPage: boolean;
40
60
  titleStyle?: CSSProperties;
41
61
  }>();
42
62
 
@@ -45,6 +65,7 @@ const emit = defineEmits<{
45
65
  notificationNavigate: [action: NotificationPayload["action"]];
46
66
  toggleRightSidebar: [];
47
67
  openSettings: [];
68
+ home: [];
48
69
  }>();
49
70
 
50
71
  const lockPopupOpen = ref(false);
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div ref="containerRef" class="h-full overflow-y-auto bg-gray-50 p-4 space-y-3" data-testid="stack-scroll">
3
- <div v-if="toolResults.length === 0" class="flex items-center justify-center h-full text-gray-400 text-sm">No results yet</div>
3
+ <div v-if="toolResults.length === 0" class="flex items-center justify-center h-full text-gray-400 text-sm">{{ t("common.noResultsYet") }}</div>
4
4
  <div
5
5
  v-for="result in toolResults"
6
6
  :key="result.uuid"
@@ -70,7 +70,10 @@
70
70
 
71
71
  <script setup lang="ts">
72
72
  import { ref, computed, watch, nextTick, onMounted, onUnmounted } from "vue";
73
+ import { useI18n } from "vue-i18n";
73
74
  import { getPlugin } from "../tools";
75
+
76
+ const { t } = useI18n();
74
77
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
75
78
  import { View as TextResponseOriginalView } from "../plugins/textResponse/index";
76
79
  import { handleExternalLinkClick } from "../utils/dom/externalLink";
@@ -9,7 +9,7 @@
9
9
  >
10
10
  {{ query }}
11
11
  </button>
12
- <p class="text-center text-[10px] text-gray-400 py-0.5">click to send · shift+click to edit</p>
12
+ <p class="text-center text-[10px] text-gray-400 py-0.5">{{ t("suggestionsPanel.sendEditHint") }}</p>
13
13
  </div>
14
14
  <button
15
15
  class="w-full flex items-center justify-between px-4 py-1 text-xs text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors"
@@ -17,7 +17,7 @@
17
17
  >
18
18
  <span class="flex items-center gap-1">
19
19
  <span class="material-icons text-sm">lightbulb</span>
20
- Suggestions
20
+ {{ t("suggestionsPanel.suggestions") }}
21
21
  </span>
22
22
  <span class="material-icons text-sm transition-transform" :class="{ 'rotate-180': !expanded }">expand_less</span>
23
23
  </button>
@@ -26,6 +26,9 @@
26
26
 
27
27
  <script setup lang="ts">
28
28
  import { nextTick, ref, watch } from "vue";
29
+ import { useI18n } from "vue-i18n";
30
+
31
+ const { t } = useI18n();
29
32
 
30
33
  defineProps<{
31
34
  queries: string[];
@@ -48,9 +51,9 @@ watch(expanded, (isExpanded) => {
48
51
  });
49
52
  });
50
53
 
51
- function onClick(e: MouseEvent, query: string): void {
54
+ function onClick(event: MouseEvent, query: string): void {
52
55
  expanded.value = false;
53
- if (e.shiftKey) {
56
+ if (event.shiftKey) {
54
57
  emit("edit", query);
55
58
  return;
56
59
  }