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.
- package/bin/mulmoclaude.js +7 -24
- package/client/assets/html2canvas-Cx501zZr-Cv5snK9D.js +5 -0
- package/client/assets/index-CubzmCVK.css +2 -0
- package/client/assets/{index-eHWB79u5.js → index-DtcyExH9.js} +80 -61
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-DnizuhIY.js} +5 -5
- package/client/index.html +2 -4
- package/package.json +13 -13
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/index.ts +9 -3
- package/server/agent/mcp-tools/index.ts +6 -6
- package/server/agent/mcp-tools/x.ts +2 -1
- package/server/agent/prompt.ts +187 -26
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +3 -3
- package/server/api/auth/bearerAuth.ts +3 -3
- package/server/api/auth/token.ts +2 -2
- package/server/api/routes/agent.ts +21 -3
- package/server/api/routes/config.ts +1 -1
- package/server/api/routes/files.ts +13 -12
- package/server/api/routes/html.ts +2 -2
- package/server/api/routes/image.ts +7 -7
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +16 -6
- package/server/api/routes/roles.ts +2 -2
- package/server/api/routes/scheduler.ts +8 -6
- package/server/api/routes/schedulerTasks.ts +5 -3
- package/server/api/routes/sessions.ts +2 -2
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +5 -5
- package/server/api/routes/sources.ts +3 -3
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki.ts +22 -8
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +6 -6
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +17 -16
- package/server/index.ts +40 -46
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +5 -5
- package/server/utils/files/atomic.ts +11 -11
- package/server/utils/files/image-store.ts +17 -6
- package/server/utils/files/journal-io.ts +2 -2
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-store.ts +4 -4
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +4 -4
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/spreadsheet-store.ts +5 -5
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +2 -2
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- package/server/utils/markdown.ts +5 -5
- package/server/utils/port.d.mts +6 -0
- package/server/utils/port.mjs +48 -0
- package/server/utils/request.ts +12 -6
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/summarizer.ts +4 -4
- package/server/workspace/custom-dirs.ts +5 -5
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +4 -4
- package/server/workspace/journal/optimizationPass.ts +2 -2
- package/server/workspace/journal/state.ts +6 -6
- package/server/workspace/paths.ts +3 -3
- package/server/workspace/reference-dirs.ts +3 -3
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +3 -3
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +2 -2
- package/server/workspace/sources/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +3 -3
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +36 -13
- package/server/workspace/sources/pipeline/index.ts +2 -7
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/plan.ts +11 -9
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/sourceState.ts +9 -4
- package/server/workspace/sources/types.ts +9 -0
- package/server/workspace/sources/urls.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +4 -4
- package/server/workspace/workspace.ts +7 -7
- package/src/App.vue +286 -112
- package/src/components/CanvasViewToggle.vue +10 -7
- package/src/components/ChatInput.vue +60 -26
- package/src/components/FileContentHeader.vue +7 -4
- package/src/components/FileContentRenderer.vue +20 -6
- package/src/components/FileTree.vue +6 -3
- package/src/components/FileTreePane.vue +11 -8
- package/src/components/FilesView.vue +5 -3
- package/src/components/LockStatusPopup.vue +15 -12
- package/src/components/NotificationBell.vue +14 -5
- package/src/components/NotificationToast.vue +4 -1
- package/src/components/PluginLauncher.vue +19 -56
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/SessionHistoryPanel.vue +33 -29
- package/src/components/SessionTabBar.vue +8 -10
- package/src/components/SettingsMcpTab.vue +43 -30
- package/src/components/SettingsModal.vue +21 -19
- package/src/components/SettingsReferenceDirsTab.vue +29 -24
- package/src/components/SettingsWorkspaceDirsTab.vue +32 -22
- package/src/components/SidebarHeader.vue +25 -4
- package/src/components/StackView.vue +4 -1
- package/src/components/SuggestionsPanel.vue +5 -2
- package/src/components/TodoExplorer.vue +26 -15
- package/src/components/ToolResultsPanel.vue +27 -13
- package/src/components/todo/TodoAddDialog.vue +17 -12
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +15 -10
- package/src/components/todo/TodoKanbanView.vue +10 -5
- package/src/components/todo/TodoListView.vue +5 -2
- package/src/components/todo/TodoTableView.vue +5 -2
- package/src/composables/useAppApi.ts +9 -0
- package/src/composables/useDynamicFavicon.ts +172 -37
- package/src/composables/useEventListeners.ts +7 -8
- package/src/composables/useFaviconState.ts +13 -2
- package/src/composables/useFileSelection.ts +24 -6
- package/src/composables/useLayoutMode.ts +32 -0
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useViewLayout.ts +20 -34
- package/src/lang/de.ts +536 -0
- package/src/lang/en.ts +558 -0
- package/src/lang/es.ts +543 -0
- package/src/lang/fr.ts +536 -0
- package/src/lang/ja.ts +536 -0
- package/src/lang/ko.ts +540 -0
- package/src/lang/pt-BR.ts +534 -0
- package/src/lang/zh.ts +537 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +2 -0
- package/src/plugins/canvas/View.vue +102 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +59 -43
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +26 -22
- package/src/plugins/manageSource/Preview.vue +1 -1
- package/src/plugins/manageSource/View.vue +73 -52
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +24 -34
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +36 -26
- package/src/plugins/scheduler/Preview.vue +7 -4
- package/src/plugins/scheduler/TasksTab.vue +53 -24
- package/src/plugins/scheduler/View.vue +28 -19
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/spreadsheet/Preview.vue +8 -3
- package/src/plugins/spreadsheet/View.vue +21 -12
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +27 -7
- package/src/plugins/todo/Preview.vue +11 -6
- package/src/plugins/todo/View.vue +27 -13
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +5 -2
- package/src/plugins/wiki/View.vue +202 -81
- package/src/plugins/wiki/route.ts +112 -0
- package/src/router/guards.ts +42 -24
- package/src/router/index.ts +41 -26
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/request.ts +19 -0
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/path/workspaceLinkRouter.ts +81 -0
- package/src/vite-env.d.ts +9 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
- package/client/assets/index-Bm70FDU2.css +0 -1
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/utils/canvas/viewMode.ts +0 -46
- /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
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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="
|
|
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">
|
|
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">
|
|
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="
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
12
|
+
:key="f"
|
|
13
13
|
class="px-2 py-0.5 text-[10px] rounded-full border transition-colors"
|
|
14
|
-
:class="activeFilter === f
|
|
15
|
-
:data-testid="`session-filter-${f
|
|
16
|
-
@click="activeFilter = f
|
|
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
|
|
19
|
-
<span v-if="f
|
|
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
|
-
|
|
30
|
-
<span v-if="sessions.length > 0">
|
|
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" ? "
|
|
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
|
-
|
|
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">
|
|
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 || "
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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<
|
|
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(
|
|
135
|
-
if (
|
|
136
|
-
return props.sessions.filter((session) =>
|
|
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="
|
|
7
|
-
aria-label="
|
|
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="
|
|
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="
|
|
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="
|
|
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 {
|
|
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
|
-
|
|
5
|
-
<
|
|
6
|
-
<code class="bg-gray-100 px-1 rounded">tsx</code
|
|
7
|
-
</
|
|
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">
|
|
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)">
|
|
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">
|
|
37
|
+
<span class="text-gray-500">{{ t("settingsMcpTab.urlLabel") }}</span>
|
|
36
38
|
<code class="ml-1">{{ entry.spec.url }}</code>
|
|
37
39
|
</div>
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
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">
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
+
{{ t("settingsMcpTab.nameLabel") }}
|
|
68
76
|
<input
|
|
69
77
|
v-model="draft.id"
|
|
70
78
|
type="text"
|
|
71
|
-
placeholder="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
+
{{ t("settingsMcpTab.urlFieldLabel") }}
|
|
90
98
|
<input
|
|
91
99
|
v-model="draft.url"
|
|
92
100
|
type="text"
|
|
93
|
-
placeholder="
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
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
|
-
|
|
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 = "
|
|
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 = "
|
|
291
|
+
draftError.value = t("settingsMcpTab.errBadName");
|
|
279
292
|
return;
|
|
280
293
|
}
|
|
281
294
|
if (props.servers.some((server) => server.id === mcpId)) {
|
|
282
|
-
draftError.value =
|
|
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 = "
|
|
302
|
+
draftError.value = t("settingsMcpTab.errBadHttpUrl");
|
|
290
303
|
return;
|
|
291
304
|
}
|
|
292
305
|
spec = { type: "http", url, enabled: true };
|