mulmoclaude 0.3.0 → 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 (185) 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-eHWB79u5.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/index.ts +9 -3
  10. package/server/agent/mcp-tools/index.ts +6 -6
  11. package/server/agent/mcp-tools/x.ts +2 -1
  12. package/server/agent/prompt.ts +187 -26
  13. package/server/agent/resumeFailover.ts +5 -5
  14. package/server/agent/sandboxMounts.ts +3 -3
  15. package/server/api/auth/bearerAuth.ts +3 -3
  16. package/server/api/auth/token.ts +2 -2
  17. package/server/api/routes/agent.ts +21 -3
  18. package/server/api/routes/config.ts +1 -1
  19. package/server/api/routes/files.ts +13 -12
  20. package/server/api/routes/html.ts +2 -2
  21. package/server/api/routes/image.ts +7 -7
  22. package/server/api/routes/mulmo-script.ts +33 -31
  23. package/server/api/routes/pdf.ts +2 -2
  24. package/server/api/routes/plugins.ts +16 -6
  25. package/server/api/routes/roles.ts +2 -2
  26. package/server/api/routes/scheduler.ts +8 -6
  27. package/server/api/routes/schedulerTasks.ts +5 -3
  28. package/server/api/routes/sessions.ts +2 -2
  29. package/server/api/routes/sessionsCursor.ts +4 -4
  30. package/server/api/routes/skills.ts +5 -5
  31. package/server/api/routes/sources.ts +3 -3
  32. package/server/api/routes/todosHandlers.ts +1 -1
  33. package/server/api/routes/todosItemsHandlers.ts +14 -14
  34. package/server/api/routes/wiki.ts +22 -8
  35. package/server/api/sandboxStatus.ts +1 -1
  36. package/server/events/notifications.ts +6 -6
  37. package/server/events/pub-sub/index.ts +3 -3
  38. package/server/events/relay-client.ts +17 -16
  39. package/server/index.ts +40 -46
  40. package/server/system/config.ts +5 -5
  41. package/server/system/credentials.ts +7 -5
  42. package/server/system/env.ts +5 -5
  43. package/server/utils/files/atomic.ts +11 -11
  44. package/server/utils/files/image-store.ts +17 -6
  45. package/server/utils/files/journal-io.ts +2 -2
  46. package/server/utils/files/json.ts +5 -5
  47. package/server/utils/files/markdown-store.ts +4 -4
  48. package/server/utils/files/reference-dirs-io.ts +3 -3
  49. package/server/utils/files/roles-io.ts +4 -4
  50. package/server/utils/files/safe.ts +14 -14
  51. package/server/utils/files/scheduler-overrides-io.ts +2 -2
  52. package/server/utils/files/spreadsheet-store.ts +5 -5
  53. package/server/utils/files/workspace-io.ts +12 -12
  54. package/server/utils/gemini.ts +2 -2
  55. package/server/utils/gitignore.ts +9 -9
  56. package/server/utils/json.ts +5 -5
  57. package/server/utils/logBackgroundError.ts +12 -3
  58. package/server/utils/markdown.ts +5 -5
  59. package/server/utils/port.d.mts +6 -0
  60. package/server/utils/port.mjs +48 -0
  61. package/server/utils/request.ts +12 -6
  62. package/server/utils/spawn.ts +1 -1
  63. package/server/utils/types.ts +2 -2
  64. package/server/workspace/chat-index/summarizer.ts +4 -4
  65. package/server/workspace/custom-dirs.ts +5 -5
  66. package/server/workspace/journal/diff.ts +2 -2
  67. package/server/workspace/journal/index.ts +4 -4
  68. package/server/workspace/journal/optimizationPass.ts +2 -2
  69. package/server/workspace/journal/state.ts +6 -6
  70. package/server/workspace/paths.ts +3 -3
  71. package/server/workspace/reference-dirs.ts +3 -3
  72. package/server/workspace/skills/parser.ts +6 -6
  73. package/server/workspace/skills/scheduler.ts +3 -3
  74. package/server/workspace/skills/writer.ts +3 -3
  75. package/server/workspace/sources/arxivDiscovery.ts +2 -2
  76. package/server/workspace/sources/fetchers/rss.ts +5 -5
  77. package/server/workspace/sources/fetchers/rssParser.ts +4 -4
  78. package/server/workspace/sources/interests.ts +3 -3
  79. package/server/workspace/sources/paths.ts +6 -6
  80. package/server/workspace/sources/pipeline/fetch.ts +36 -13
  81. package/server/workspace/sources/pipeline/index.ts +2 -7
  82. package/server/workspace/sources/pipeline/notify.ts +3 -3
  83. package/server/workspace/sources/pipeline/plan.ts +11 -9
  84. package/server/workspace/sources/pipeline/write.ts +5 -5
  85. package/server/workspace/sources/rateLimiter.ts +1 -1
  86. package/server/workspace/sources/sourceState.ts +9 -4
  87. package/server/workspace/sources/types.ts +9 -0
  88. package/server/workspace/sources/urls.ts +1 -1
  89. package/server/workspace/tool-trace/classify.ts +4 -4
  90. package/server/workspace/workspace.ts +7 -7
  91. package/src/App.vue +286 -112
  92. package/src/components/CanvasViewToggle.vue +10 -7
  93. package/src/components/ChatInput.vue +60 -26
  94. package/src/components/FileContentHeader.vue +7 -4
  95. package/src/components/FileContentRenderer.vue +20 -6
  96. package/src/components/FileTree.vue +6 -3
  97. package/src/components/FileTreePane.vue +11 -8
  98. package/src/components/FilesView.vue +5 -3
  99. package/src/components/LockStatusPopup.vue +15 -12
  100. package/src/components/NotificationBell.vue +14 -5
  101. package/src/components/NotificationToast.vue +4 -1
  102. package/src/components/PluginLauncher.vue +19 -56
  103. package/src/components/RightSidebar.vue +13 -10
  104. package/src/components/SessionHistoryPanel.vue +33 -29
  105. package/src/components/SessionTabBar.vue +8 -10
  106. package/src/components/SettingsMcpTab.vue +43 -30
  107. package/src/components/SettingsModal.vue +21 -19
  108. package/src/components/SettingsReferenceDirsTab.vue +29 -24
  109. package/src/components/SettingsWorkspaceDirsTab.vue +32 -22
  110. package/src/components/SidebarHeader.vue +25 -4
  111. package/src/components/StackView.vue +4 -1
  112. package/src/components/SuggestionsPanel.vue +5 -2
  113. package/src/components/TodoExplorer.vue +26 -15
  114. package/src/components/ToolResultsPanel.vue +27 -13
  115. package/src/components/todo/TodoAddDialog.vue +17 -12
  116. package/src/components/todo/TodoEditDialog.vue +7 -2
  117. package/src/components/todo/TodoEditPanel.vue +15 -10
  118. package/src/components/todo/TodoKanbanView.vue +10 -5
  119. package/src/components/todo/TodoListView.vue +5 -2
  120. package/src/components/todo/TodoTableView.vue +5 -2
  121. package/src/composables/useAppApi.ts +9 -0
  122. package/src/composables/useDynamicFavicon.ts +172 -37
  123. package/src/composables/useEventListeners.ts +7 -8
  124. package/src/composables/useFaviconState.ts +13 -2
  125. package/src/composables/useFileSelection.ts +24 -6
  126. package/src/composables/useLayoutMode.ts +32 -0
  127. package/src/composables/useSessionHistory.ts +7 -17
  128. package/src/composables/useViewLayout.ts +20 -34
  129. package/src/lang/de.ts +536 -0
  130. package/src/lang/en.ts +558 -0
  131. package/src/lang/es.ts +543 -0
  132. package/src/lang/fr.ts +536 -0
  133. package/src/lang/ja.ts +536 -0
  134. package/src/lang/ko.ts +540 -0
  135. package/src/lang/pt-BR.ts +534 -0
  136. package/src/lang/zh.ts +537 -0
  137. package/src/lib/vue-i18n.ts +97 -0
  138. package/src/main.ts +2 -0
  139. package/src/plugins/canvas/View.vue +102 -186
  140. package/src/plugins/canvas/definition.ts +0 -8
  141. package/src/plugins/chart/Preview.vue +1 -1
  142. package/src/plugins/chart/View.vue +9 -4
  143. package/src/plugins/manageRoles/Preview.vue +4 -1
  144. package/src/plugins/manageRoles/View.vue +59 -43
  145. package/src/plugins/manageSkills/Preview.vue +8 -3
  146. package/src/plugins/manageSkills/View.vue +26 -22
  147. package/src/plugins/manageSource/Preview.vue +1 -1
  148. package/src/plugins/manageSource/View.vue +73 -52
  149. package/src/plugins/markdown/Preview.vue +1 -1
  150. package/src/plugins/markdown/View.vue +24 -34
  151. package/src/plugins/presentHtml/Preview.vue +1 -1
  152. package/src/plugins/presentHtml/View.vue +7 -4
  153. package/src/plugins/presentMulmoScript/Preview.vue +1 -1
  154. package/src/plugins/presentMulmoScript/View.vue +36 -26
  155. package/src/plugins/scheduler/Preview.vue +7 -4
  156. package/src/plugins/scheduler/TasksTab.vue +53 -24
  157. package/src/plugins/scheduler/View.vue +28 -19
  158. package/src/plugins/scheduler/formatSchedule.ts +93 -0
  159. package/src/plugins/spreadsheet/Preview.vue +8 -3
  160. package/src/plugins/spreadsheet/View.vue +21 -12
  161. package/src/plugins/textResponse/Preview.vue +15 -58
  162. package/src/plugins/textResponse/View.vue +27 -7
  163. package/src/plugins/todo/Preview.vue +11 -6
  164. package/src/plugins/todo/View.vue +27 -13
  165. package/src/plugins/ui-image/ImagePreview.vue +6 -3
  166. package/src/plugins/ui-image/ImageView.vue +7 -4
  167. package/src/plugins/wiki/Preview.vue +5 -2
  168. package/src/plugins/wiki/View.vue +202 -81
  169. package/src/plugins/wiki/route.ts +112 -0
  170. package/src/router/guards.ts +42 -24
  171. package/src/router/index.ts +41 -26
  172. package/src/types/vue-i18n.d.ts +20 -0
  173. package/src/utils/agent/request.ts +19 -0
  174. package/src/utils/canvas/layoutMode.ts +26 -0
  175. package/src/utils/image/cacheBust.ts +16 -0
  176. package/src/utils/image/resolve.ts +16 -0
  177. package/src/utils/path/workspaceLinkRouter.ts +81 -0
  178. package/src/vite-env.d.ts +9 -0
  179. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
  180. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
  181. package/client/assets/index-Bm70FDU2.css +0 -1
  182. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
  183. package/src/composables/useCanvasViewMode.ts +0 -121
  184. package/src/utils/canvas/viewMode.ts +0 -46
  185. /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
@@ -8,12 +8,12 @@
8
8
  'px-2.5 py-1 flex items-center gap-1 border-r border-gray-200 last:border-r-0 transition-colors',
9
9
  isActive(target) ? 'bg-blue-50 text-blue-600 font-medium' : 'bg-white text-gray-600 hover:bg-gray-50',
10
10
  ]"
11
- :title="target.title"
11
+ :title="t(`pluginLauncher.${target.key}.title`)"
12
12
  :data-testid="`plugin-launcher-${target.key}`"
13
13
  @click="emit('navigate', target)"
14
14
  >
15
15
  <span class="material-icons text-sm">{{ target.icon }}</span>
16
- <span v-if="!compact">{{ target.label }}</span>
16
+ <span v-if="!compact">{{ t(`pluginLauncher.${target.key}.label`) }}</span>
17
17
  </button>
18
18
  </template>
19
19
  </div>
@@ -21,80 +21,43 @@
21
21
 
22
22
  <script setup lang="ts">
23
23
  import { onBeforeUnmount, onMounted, ref } from "vue";
24
+ import { useI18n } from "vue-i18n";
25
+
26
+ const { t } = useI18n();
24
27
 
25
28
  // Quick-access toolbar sitting above the canvas. Each button
26
- // switches the canvas to a dedicated view mode via URL
27
- // (?view=todos, ?view=wiki, etc.). The "invoke" kind is kept in
28
- // the union for future use but currently all targets use "view".
29
- //
30
- // First slice of issue #253. The list of targets is declared here so
31
- // the launcher can be swapped for a customisable per-role palette
32
- // later without touching the App.vue wiring.
29
+ // navigates to a dedicated page (/todos, /wiki, etc.). The "invoke"
30
+ // kind is kept in the union for future use but currently all targets
31
+ // use "view".
33
32
 
34
33
  const props = defineProps<{
35
- /** Current canvas view mode — the matching button lights up. */
34
+ /** Current page route name — the matching button lights up. */
36
35
  activeViewMode?: string | null;
37
36
  }>();
38
37
 
39
38
  export type PluginLauncherKind = "view"; // Switch the canvas to a dedicated view mode
40
39
 
40
+ // The `key` is also the i18n lookup prefix (see pluginLauncher.*
41
+ // in src/lang/en.ts). Templates resolve the label / tooltip via
42
+ // `t(\`pluginLauncher.\${target.key}.label\`)` — keeping label/title
43
+ // strings out of this file avoids duplication across locales.
41
44
  export interface PluginLauncherTarget {
42
45
  /** Stable key for testid + dispatch in App.vue. */
43
46
  key: "todos" | "scheduler" | "skills" | "wiki" | "roles" | "files";
44
47
  kind: PluginLauncherKind;
45
48
  /** Material-icons glyph. */
46
49
  icon: string;
47
- /** Visible label next to the icon. */
48
- label: string;
49
- /** Tooltip on hover. */
50
- title: string;
51
50
  }
52
51
 
53
52
  const TARGETS: PluginLauncherTarget[] = [
54
53
  // ─── Data plugins ───
55
- {
56
- key: "todos",
57
- kind: "view",
58
- icon: "checklist",
59
- label: "Todos",
60
- title: "Open todos (⌘4)",
61
- },
62
- {
63
- key: "scheduler",
64
- kind: "view",
65
- icon: "event",
66
- label: "Schedule",
67
- title: "Open schedule (⌘5)",
68
- },
69
- {
70
- key: "wiki",
71
- kind: "view",
72
- icon: "menu_book",
73
- label: "Wiki",
74
- title: "Open wiki (⌘6)",
75
- },
54
+ { key: "todos", kind: "view", icon: "checklist" },
55
+ { key: "scheduler", kind: "view", icon: "event" },
56
+ { key: "wiki", kind: "view", icon: "menu_book" },
76
57
  // ─── Management / navigation ───
77
- {
78
- key: "skills",
79
- kind: "view",
80
- icon: "psychology",
81
- label: "Skills",
82
- title: "Open skills (⌘7)",
83
- },
84
- {
85
- key: "roles",
86
- kind: "view",
87
- icon: "manage_accounts",
88
- label: "Roles",
89
- title: "Open roles (⌘8)",
90
- },
91
- {
92
- key: "files",
93
- kind: "view",
94
- icon: "folder",
95
- label: "Files",
96
- title: "Open workspace files (⌘3)",
97
- },
58
+ { key: "skills", kind: "view", icon: "psychology" },
59
+ { key: "roles", kind: "view", icon: "manage_accounts" },
60
+ { key: "files", kind: "view", icon: "folder" },
98
61
  ];
99
62
 
100
63
  // Index AFTER which the visual separator is inserted (between wiki
@@ -4,10 +4,10 @@
4
4
  <div class="bg-white border-b border-gray-200">
5
5
  <button
6
6
  class="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50"
7
- title="Toggle system prompt"
7
+ :title="t('rightSidebar.toggleSystemPrompt')"
8
8
  @click="showSystemPrompt = !showSystemPrompt"
9
9
  >
10
- <span class="text-xs font-semibold text-gray-500 uppercase tracking-wide">System Prompt</span>
10
+ <span class="text-xs font-semibold text-gray-500 uppercase tracking-wide">{{ t("rightSidebar.systemPrompt") }}</span>
11
11
  <span class="text-gray-400 text-xs">{{ showSystemPrompt ? "▲" : "▼" }}</span>
12
12
  </button>
13
13
  <div v-if="showSystemPrompt" class="px-4 pb-4">
@@ -19,11 +19,11 @@
19
19
 
20
20
  <div class="bg-white border-b border-gray-200">
21
21
  <div class="p-4 pb-0">
22
- <span class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Available Tools</span>
22
+ <span class="text-xs font-semibold text-gray-500 uppercase tracking-wide">{{ t("rightSidebar.availableTools") }}</span>
23
23
  </div>
24
24
  <div class="px-4 py-3 space-y-1">
25
25
  <div v-for="tool in availableTools" :key="tool" class="text-xs">
26
- <button class="flex items-center gap-1 w-full text-left" title="Toggle tool description" @click="toggleTool(tool)">
26
+ <button class="flex items-center gap-1 w-full text-left" :title="t('rightSidebar.toggleToolDescription')" @click="toggleTool(tool)">
27
27
  <span class="bg-gray-100 text-gray-700 rounded px-2 py-0.5 border border-gray-200 font-mono">{{ tool }}</span>
28
28
  <span v-if="toolDescriptions[tool]" class="text-gray-400">{{ expandedTools.has(tool) ? "▲" : "▼" }}</span>
29
29
  </button>
@@ -35,32 +35,32 @@
35
35
  </div>
36
36
 
37
37
  <div class="p-4 border-b border-gray-200 bg-white">
38
- <h2 class="text-lg font-semibold">Tool Call History</h2>
38
+ <h2 class="text-lg font-semibold">{{ t("rightSidebar.toolCallHistory") }}</h2>
39
39
  </div>
40
40
 
41
41
  <div class="p-2 space-y-2 bg-gray-100">
42
- <div v-if="toolCallHistory.length === 0" class="text-gray-400 text-sm text-center py-4">No tool calls yet</div>
42
+ <div v-if="toolCallHistory.length === 0" class="text-gray-400 text-sm text-center py-4">{{ t("rightSidebar.noToolCalls") }}</div>
43
43
  <div v-for="(call, index) in toolCallHistory" :key="index" class="border border-gray-300 rounded p-3 bg-white text-xs space-y-1">
44
44
  <div class="flex justify-between items-start gap-2">
45
45
  <span class="font-semibold text-blue-600 break-all">{{ call.toolName }}</span>
46
46
  <span class="text-gray-400 flex-shrink-0">{{ formatTime(call.timestamp) }}</span>
47
47
  </div>
48
48
  <div>
49
- <div class="font-medium text-gray-500 mb-1">Arguments</div>
49
+ <div class="font-medium text-gray-500 mb-1">{{ t("rightSidebar.arguments") }}</div>
50
50
  <pre class="bg-gray-50 p-2 rounded overflow-x-auto text-gray-700">{{ formatJson(call.args) }}</pre>
51
51
  </div>
52
52
  <div v-if="call.error">
53
- <div class="font-medium text-gray-500 mb-1">Error</div>
53
+ <div class="font-medium text-gray-500 mb-1">{{ t("rightSidebar.error") }}</div>
54
54
  <div class="bg-red-50 p-2 rounded text-red-700">
55
55
  {{ call.error }}
56
56
  </div>
57
57
  </div>
58
58
  <div v-else-if="call.result !== undefined">
59
- <div class="font-medium text-gray-500 mb-1">Result</div>
59
+ <div class="font-medium text-gray-500 mb-1">{{ t("rightSidebar.result") }}</div>
60
60
  <pre class="bg-green-50 p-2 rounded overflow-x-auto text-gray-700">{{ call.result }}</pre>
61
61
  </div>
62
62
  <div v-else>
63
- <div class="text-gray-400 italic">Running...</div>
63
+ <div class="text-gray-400 italic">{{ t("rightSidebar.running") }}</div>
64
64
  </div>
65
65
  </div>
66
66
  </div>
@@ -70,9 +70,12 @@
70
70
 
71
71
  <script setup lang="ts">
72
72
  import { ref, nextTick } from "vue";
73
+ import { useI18n } from "vue-i18n";
73
74
  import type { ToolCallHistoryItem } from "../types/toolCallHistory";
74
75
  import { formatTime } from "../utils/format/date";
75
76
 
77
+ const { t } = useI18n();
78
+
76
79
  defineProps<{
77
80
  toolCallHistory: ToolCallHistoryItem[];
78
81
  availableTools: string[];
@@ -1,22 +1,22 @@
1
1
  <template>
2
- <div
3
- ref="root"
4
- class="absolute left-0 right-0 bottom-0 bg-white border-b border-gray-200 shadow-lg z-50 overflow-y-auto"
5
- :style="{ top: topOffset != null ? topOffset + 'px' : '4rem' }"
6
- >
2
+ <!-- Rendered as the canvas-column content for the /history route
3
+ (see plans/feat-history-url-route.md). Previously this was an
4
+ absolute-positioned overlay; the `h-full overflow-y-auto` root
5
+ plus inline flow replaces the z-index + topOffset plumbing. -->
6
+ <div ref="root" class="h-full overflow-y-auto bg-white">
7
7
  <div class="p-2 space-y-1">
8
8
  <!-- Origin filter bar -->
9
9
  <div class="flex gap-1 mb-1 flex-wrap" data-testid="session-filter-bar">
10
10
  <button
11
11
  v-for="f in FILTERS"
12
- :key="f.value"
12
+ :key="f"
13
13
  class="px-2 py-0.5 text-[10px] rounded-full border transition-colors"
14
- :class="activeFilter === f.value ? 'bg-blue-500 text-white border-blue-500' : 'bg-white text-gray-500 border-gray-300 hover:bg-gray-50'"
15
- :data-testid="`session-filter-${f.value}`"
16
- @click="activeFilter = f.value"
14
+ :class="activeFilter === f ? 'bg-blue-500 text-white border-blue-500' : 'bg-white text-gray-500 border-gray-300 hover:bg-gray-50'"
15
+ :data-testid="`session-filter-${f}`"
16
+ @click="activeFilter = f"
17
17
  >
18
- {{ f.label }}
19
- <span v-if="f.value !== 'all'" class="ml-0.5 opacity-70">{{ countByOrigin(f.value) }}</span>
18
+ {{ t(`sessionHistoryPanel.filters.${f}`) }}
19
+ <span v-if="f !== 'all'" class="ml-0.5 opacity-70">{{ countByOrigin(f) }}</span>
20
20
  </button>
21
21
  </div>
22
22
 
@@ -26,11 +26,11 @@
26
26
  role="alert"
27
27
  data-testid="session-history-error"
28
28
  >
29
- Failed to refresh: {{ errorMessage }}
30
- <span v-if="sessions.length > 0"> showing last known list.</span>
29
+ {{ t("sessionHistoryPanel.failedToRefresh", { error: errorMessage }) }}
30
+ <span v-if="sessions.length > 0">{{ t("sessionHistoryPanel.showingLastKnown") }}</span>
31
31
  </div>
32
32
  <p v-if="filteredSessions.length === 0" class="text-xs text-gray-400 p-2">
33
- {{ activeFilter === "all" ? "No sessions yet." : "No matching sessions." }}
33
+ {{ activeFilter === "all" ? t("sessionHistoryPanel.noSessions") : t("sessionHistoryPanel.noMatching") }}
34
34
  </p>
35
35
  <div
36
36
  v-for="session in filteredSessions"
@@ -53,14 +53,14 @@
53
53
  <span class="ml-auto flex items-center gap-1.5">
54
54
  <span v-if="isSessionRunning(session)" class="flex items-center gap-0.5 text-yellow-600 font-medium">
55
55
  <span class="w-1.5 h-1.5 rounded-full bg-yellow-400 animate-pulse" />
56
- Running
56
+ {{ t("sessionHistoryPanel.running") }}
57
57
  </span>
58
- <span v-else-if="isSessionUnread(session)" class="flex items-center gap-0.5 text-gray-900 font-bold"> Unread </span>
58
+ <span v-else-if="isSessionUnread(session)" class="flex items-center gap-0.5 text-gray-900 font-bold">{{ t("sessionHistoryPanel.unread") }}</span>
59
59
  <span v-else>{{ formatDate(session.updatedAt) }}</span>
60
60
  </span>
61
61
  </div>
62
62
  <p class="truncate" :class="previewClasses(session)">
63
- {{ session.preview || "(no messages)" }}
63
+ {{ session.preview || t("sessionHistoryPanel.noMessages") }}
64
64
  </p>
65
65
  <!-- Optional second line: AI-generated summary of the
66
66
  session, populated by the chat indexer (#123). -->
@@ -74,19 +74,22 @@
74
74
 
75
75
  <script setup lang="ts">
76
76
  import { ref, computed } from "vue";
77
+ import { useI18n } from "vue-i18n";
77
78
  import type { Role } from "../config/roles";
78
79
  import type { SessionSummary, SessionOrigin } from "../types/session";
79
80
  import { SESSION_ORIGINS } from "../types/session";
80
81
  import { formatDate } from "../utils/format/date";
81
82
  import { roleIcon, roleName } from "../utils/role/icon";
82
83
 
83
- const FILTERS = [
84
- { value: "all" as const, label: "All" },
85
- { value: SESSION_ORIGINS.human, label: "Human" },
86
- { value: SESSION_ORIGINS.scheduler, label: "Scheduler" },
87
- { value: SESSION_ORIGINS.skill, label: "Skill" },
88
- { value: SESSION_ORIGINS.bridge, label: "Bridge" },
89
- ];
84
+ const { t } = useI18n();
85
+
86
+ // `unread` is mutually exclusive with origin pills — selecting it
87
+ // shows every unread-flagged session regardless of origin, matching
88
+ // the user expectation that "unread" is the primary question ("what
89
+ // needs my attention?") rather than an origin sub-filter.
90
+ const UNREAD_FILTER = "unread" as const;
91
+ const FILTERS = ["all" as const, UNREAD_FILTER, SESSION_ORIGINS.human, SESSION_ORIGINS.scheduler, SESSION_ORIGINS.skill, SESSION_ORIGINS.bridge];
92
+ type FilterKey = (typeof FILTERS)[number];
90
93
 
91
94
  const ORIGIN_ICONS: Record<string, string> = {
92
95
  human: "person",
@@ -106,7 +109,6 @@ const props = defineProps<{
106
109
  sessions: SessionSummary[];
107
110
  currentSessionId: string;
108
111
  roles: Role[];
109
- topOffset?: number;
110
112
  // Latest fetch error from useSessionHistory, or null when healthy.
111
113
  errorMessage?: string | null;
112
114
  }>();
@@ -120,7 +122,7 @@ defineExpose({ root });
120
122
 
121
123
  // ── Filter ──────────────────────────────────────────────────
122
124
 
123
- const activeFilter = ref<SessionOrigin | "all">("all");
125
+ const activeFilter = ref<FilterKey>("all");
124
126
 
125
127
  function originOf(session: SessionSummary): SessionOrigin {
126
128
  return session.origin ?? SESSION_ORIGINS.human;
@@ -128,12 +130,14 @@ function originOf(session: SessionSummary): SessionOrigin {
128
130
 
129
131
  const filteredSessions = computed(() => {
130
132
  if (activeFilter.value === "all") return props.sessions;
133
+ if (activeFilter.value === UNREAD_FILTER) return props.sessions.filter((session) => session.hasUnread === true);
131
134
  return props.sessions.filter((session) => originOf(session) === activeFilter.value);
132
135
  });
133
136
 
134
- function countByOrigin(origin: string): number {
135
- if (origin === "all") return props.sessions.length;
136
- return props.sessions.filter((session) => originOf(session) === origin).length;
137
+ function countByOrigin(filterKey: FilterKey): number {
138
+ if (filterKey === "all") return props.sessions.length;
139
+ if (filterKey === UNREAD_FILTER) return props.sessions.filter((session) => session.hasUnread === true).length;
140
+ return props.sessions.filter((session) => originOf(session) === filterKey).length;
137
141
  }
138
142
 
139
143
  function originIcon(origin: string): string {
@@ -3,8 +3,8 @@
3
3
  <button
4
4
  class="flex-shrink-0 flex items-center justify-center w-7 py-1 rounded border border-dashed border-gray-300 text-gray-400 hover:border-blue-400 hover:text-blue-500 hover:bg-blue-50 transition-colors"
5
5
  data-testid="new-session-btn"
6
- title="New session"
7
- aria-label="New session"
6
+ :title="t('sessionTabBar.newSession')"
7
+ :aria-label="t('sessionTabBar.newSession')"
8
8
  @click="emit('newSession')"
9
9
  >
10
10
  <span class="material-icons text-sm">add</span>
@@ -25,24 +25,23 @@
25
25
  <div v-else class="flex-1" />
26
26
  </template>
27
27
  <button
28
- ref="historyButton"
29
28
  data-testid="history-btn"
30
29
  class="relative flex-shrink-0 flex items-center justify-center w-7 py-1 rounded text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors"
31
30
  :class="{ 'text-blue-500': historyOpen }"
32
- title="Session history"
31
+ :title="t('sessionTabBar.sessionHistory')"
33
32
  @click="emit('toggleHistory')"
34
33
  >
35
34
  <span class="material-icons text-base">expand_more</span>
36
35
  <span
37
36
  v-if="activeSessionCount > 0"
38
37
  class="absolute -top-0.5 -left-0.5 min-w-[1rem] h-4 px-0.5 bg-yellow-400 text-white text-[10px] font-bold rounded-full flex items-center justify-center leading-none cursor-help"
39
- :title="`${activeSessionCount} active session${activeSessionCount > 1 ? 's' : ''} (agent running)`"
38
+ :title="t('sessionTabBar.activeSessions', activeSessionCount, { named: { count: activeSessionCount } })"
40
39
  >{{ activeSessionCount }}</span
41
40
  >
42
41
  <span
43
42
  v-if="unreadCount > 0"
44
43
  class="absolute -top-0.5 -right-0.5 min-w-[1rem] h-4 px-0.5 bg-red-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center leading-none cursor-help"
45
- :title="`${unreadCount} unread repl${unreadCount > 1 ? 'ies' : 'y'}`"
44
+ :title="t('sessionTabBar.unreadReplies', unreadCount, { named: { count: unreadCount } })"
46
45
  >{{ unreadCount }}</span
47
46
  >
48
47
  </button>
@@ -50,11 +49,13 @@
50
49
  </template>
51
50
 
52
51
  <script setup lang="ts">
53
- import { ref } from "vue";
52
+ import { useI18n } from "vue-i18n";
54
53
  import type { Role } from "../config/roles";
55
54
  import type { SessionSummary } from "../types/session";
56
55
  import { roleIcon, roleName } from "../utils/role/icon";
57
56
 
57
+ const { t } = useI18n();
58
+
58
59
  defineProps<{
59
60
  sessions: SessionSummary[];
60
61
  currentSessionId: string;
@@ -70,9 +71,6 @@ const emit = defineEmits<{
70
71
  toggleHistory: [];
71
72
  }>();
72
73
 
73
- const historyButton = ref<HTMLButtonElement | null>(null);
74
- defineExpose({ historyButton });
75
-
76
74
  function tabColor(session: SessionSummary): string {
77
75
  if (session.isRunning) return "text-yellow-400";
78
76
  if (session.hasUnread) return "text-gray-900";
@@ -1,12 +1,12 @@
1
1
  <template>
2
2
  <div class="space-y-3">
3
- <p class="text-xs text-gray-600 leading-relaxed">
4
- Add external MCP servers. HTTP servers work in every mode. Stdio servers use the sandbox image's
5
- <code class="bg-gray-100 px-1 rounded">npx</code> / <code class="bg-gray-100 px-1 rounded">node</code> /
6
- <code class="bg-gray-100 px-1 rounded">tsx</code>; paths must live under the workspace when Docker is enabled.
7
- </p>
3
+ <i18n-t keypath="settingsMcpTab.explanation" tag="p" class="text-xs text-gray-600 leading-relaxed">
4
+ <template #npx><code class="bg-gray-100 px-1 rounded">npx</code></template>
5
+ <template #node><code class="bg-gray-100 px-1 rounded">node</code></template>
6
+ <template #tsx><code class="bg-gray-100 px-1 rounded">tsx</code></template>
7
+ </i18n-t>
8
8
 
9
- <div v-if="servers.length === 0" class="text-xs text-gray-500 italic" data-testid="mcp-empty">No MCP servers configured yet.</div>
9
+ <div v-if="servers.length === 0" class="text-xs text-gray-500 italic" data-testid="mcp-empty">{{ t("settingsMcpTab.noServers") }}</div>
10
10
 
11
11
  <ul v-else class="space-y-2" data-testid="mcp-server-list">
12
12
  <li
@@ -25,23 +25,31 @@
25
25
  >
26
26
  <label class="flex items-center gap-1 text-xs text-gray-600 ml-2">
27
27
  <input type="checkbox" :checked="entry.spec.enabled !== false" :data-testid="'mcp-enabled-' + entry.id" @change="onToggleEnabled(idx, $event)" />
28
- enabled
28
+ {{ t("settingsMcpTab.enabled") }}
29
29
  </label>
30
30
  </div>
31
- <button class="text-xs text-red-600 hover:text-red-800" :data-testid="'mcp-remove-' + entry.id" @click="emit('remove', idx)">Remove</button>
31
+ <button class="text-xs text-red-600 hover:text-red-800" :data-testid="'mcp-remove-' + entry.id" @click="emit('remove', idx)">
32
+ {{ t("common.remove") }}
33
+ </button>
32
34
  </div>
33
35
  <div v-if="entry.spec.type === 'http'" class="text-xs space-y-1">
34
36
  <div>
35
- <span class="text-gray-500">URL:</span>
37
+ <span class="text-gray-500">{{ t("settingsMcpTab.urlLabel") }}</span>
36
38
  <code class="ml-1">{{ entry.spec.url }}</code>
37
39
  </div>
38
- <div v-if="dockerMode && wouldRewriteLocalhost((entry.spec as HttpSpec).url)" class="text-amber-700">
39
- In Docker mode <code>localhost</code> is rewritten to <code>host.docker.internal</code>.
40
- </div>
40
+ <i18n-t
41
+ v-if="dockerMode && wouldRewriteLocalhost((entry.spec as HttpSpec).url)"
42
+ keypath="settingsMcpTab.localhostRewrite"
43
+ tag="div"
44
+ class="text-amber-700"
45
+ >
46
+ <template #localhost><code>localhost</code></template>
47
+ <template #hostDockerInternal><code>host.docker.internal</code></template>
48
+ </i18n-t>
41
49
  </div>
42
50
  <div v-else-if="entry.spec.type === 'stdio'" class="text-xs space-y-1">
43
51
  <div>
44
- <span class="text-gray-500">Command:</span>
52
+ <span class="text-gray-500">{{ t("settingsMcpTab.commandLabel") }}</span>
45
53
  <code class="ml-1">{{ entry.spec.command }}</code>
46
54
  <code v-if="(entry.spec as StdioSpec).args?.length" class="ml-1">
47
55
  {{ ((entry.spec as StdioSpec).args ?? []).join(" ") }}
@@ -52,23 +60,23 @@
52
60
  class="text-red-600"
53
61
  :data-testid="'mcp-docker-warning-' + entry.id"
54
62
  >
55
- Contains paths outside the workspace — will not resolve inside Docker.
63
+ {{ t("settingsMcpTab.dockerNonWorkspaceWarning") }}
56
64
  </div>
57
65
  </div>
58
66
  </li>
59
67
  </ul>
60
68
 
61
69
  <button v-if="!adding" class="text-xs px-2 py-1 rounded border border-gray-300 text-gray-700 hover:bg-gray-50" data-testid="mcp-add-btn" @click="startAdd">
62
- + Add MCP Server
70
+ {{ t("settingsMcpTab.addServerButton") }}
63
71
  </button>
64
72
 
65
73
  <div v-else class="border border-blue-300 rounded p-3 space-y-2" data-testid="mcp-add-form">
66
74
  <label class="block text-xs font-semibold text-gray-700">
67
- Name
75
+ {{ t("settingsMcpTab.nameLabel") }}
68
76
  <input
69
77
  v-model="draft.id"
70
78
  type="text"
71
- placeholder="my-server"
79
+ :placeholder="t('settingsMcpTab.namePlaceholder')"
72
80
  class="mt-1 w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
73
81
  data-testid="mcp-draft-id"
74
82
  @keydown.stop
@@ -77,20 +85,20 @@
77
85
  <div class="flex gap-3 text-xs">
78
86
  <label class="flex items-center gap-1">
79
87
  <input v-model="draft.type" type="radio" value="http" data-testid="mcp-draft-type-http" />
80
- HTTP
88
+ {{ t("settingsMcpTab.typeHttp") }}
81
89
  </label>
82
90
  <label class="flex items-center gap-1">
83
91
  <input v-model="draft.type" type="radio" value="stdio" data-testid="mcp-draft-type-stdio" />
84
- Stdio (command)
92
+ {{ t("settingsMcpTab.typeStdio") }}
85
93
  </label>
86
94
  </div>
87
95
  <div v-if="draft.type === 'http'" class="space-y-2">
88
96
  <label class="block text-xs font-semibold text-gray-700">
89
- URL
97
+ {{ t("settingsMcpTab.urlFieldLabel") }}
90
98
  <input
91
99
  v-model="draft.url"
92
100
  type="text"
93
- placeholder="https://example.com/mcp"
101
+ :placeholder="t('settingsMcpTab.urlPlaceholder')"
94
102
  class="mt-1 w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
95
103
  data-testid="mcp-draft-url"
96
104
  @keydown.stop
@@ -99,7 +107,7 @@
99
107
  </div>
100
108
  <div v-else class="space-y-2">
101
109
  <label class="block text-xs font-semibold text-gray-700">
102
- Command
110
+ {{ t("settingsMcpTab.commandFieldLabel") }}
103
111
  <select
104
112
  v-model="draft.command"
105
113
  class="mt-1 w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
@@ -111,11 +119,11 @@
111
119
  </select>
112
120
  </label>
113
121
  <label class="block text-xs font-semibold text-gray-700">
114
- Arguments (one per line)
122
+ {{ t("settingsMcpTab.argsLabel") }}
115
123
  <textarea
116
124
  v-model="draft.argsText"
117
125
  class="mt-1 w-full h-20 px-2 py-1 text-sm font-mono border border-gray-300 rounded focus:outline-none focus:border-blue-400"
118
- placeholder="-y&#10;@modelcontextprotocol/server-filesystem&#10;/workspace/path"
126
+ :placeholder="t('settingsMcpTab.argsPlaceholder')"
119
127
  data-testid="mcp-draft-args"
120
128
  @keydown.stop
121
129
  ></textarea>
@@ -126,9 +134,11 @@
126
134
  </div>
127
135
  <div class="flex justify-end gap-2">
128
136
  <button class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-50" data-testid="mcp-draft-cancel" @click="cancelAdd">
129
- Cancel
137
+ {{ t("common.cancel") }}
138
+ </button>
139
+ <button class="px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600" data-testid="mcp-draft-add" @click="commitAdd">
140
+ {{ t("common.add") }}
130
141
  </button>
131
- <button class="px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600" data-testid="mcp-draft-add" @click="commitAdd">Add</button>
132
142
  </div>
133
143
  </div>
134
144
  </div>
@@ -136,6 +146,9 @@
136
146
 
137
147
  <script setup lang="ts">
138
148
  import { ref } from "vue";
149
+ import { useI18n } from "vue-i18n";
150
+
151
+ const { t } = useI18n();
139
152
 
140
153
  // UI-local representation of a configured server. Matches
141
154
  // server/config.ts#McpServerEntry. Re-declared here to avoid a
@@ -269,24 +282,24 @@ function commitAdd(): void {
269
282
  if (!mcpId) {
270
283
  const suggested = ensureUniqueId(suggestIdFromDraft(draft.value));
271
284
  if (!suggested) {
272
- draftError.value = "Please provide a Name, or enter a URL / args we can derive one from.";
285
+ draftError.value = t("settingsMcpTab.errNoName");
273
286
  return;
274
287
  }
275
288
  mcpId = suggested;
276
289
  }
277
290
  if (!ID_RE.test(mcpId)) {
278
- draftError.value = "Name must start with a lowercase letter and contain only [a-z0-9_-].";
291
+ draftError.value = t("settingsMcpTab.errBadName");
279
292
  return;
280
293
  }
281
294
  if (props.servers.some((server) => server.id === mcpId)) {
282
- draftError.value = `Server id "${mcpId}" already exists.`;
295
+ draftError.value = t("settingsMcpTab.errIdExists", { id: mcpId });
283
296
  return;
284
297
  }
285
298
  let spec: ServerSpec;
286
299
  if (draft.value.type === "http") {
287
300
  const url = draft.value.url.trim();
288
301
  if (!/^https?:\/\//.test(url)) {
289
- draftError.value = "HTTP URL must start with http:// or https://";
302
+ draftError.value = t("settingsMcpTab.errBadHttpUrl");
290
303
  return;
291
304
  }
292
305
  spec = { type: "http", url, enabled: true };