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.
- 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-D8rhwXLq.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/config.ts +12 -12
- package/server/agent/index.ts +9 -3
- package/server/agent/mcp-server.ts +19 -19
- package/server/agent/mcp-tools/index.ts +6 -6
- package/server/agent/mcp-tools/x.ts +7 -6
- package/server/agent/prompt.ts +195 -29
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +10 -10
- package/server/agent/stream.ts +4 -4
- 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 +22 -21
- 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 +14 -12
- package/server/api/routes/schedulerHandlers.ts +12 -12
- package/server/api/routes/schedulerTasks.ts +19 -17
- package/server/api/routes/sessions.ts +26 -26
- 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/todosColumnsHandlers.ts +30 -30
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki.ts +36 -22
- 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/events/scheduler-adapter.ts +20 -20
- package/server/events/session-store/index.ts +10 -10
- package/server/events/task-manager/index.ts +7 -7
- package/server/index.ts +59 -65
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +5 -5
- package/server/utils/date.ts +18 -18
- package/server/utils/files/atomic.ts +16 -16
- package/server/utils/files/html-io.ts +5 -5
- package/server/utils/files/image-store.ts +19 -8
- package/server/utils/files/journal-io.ts +4 -4
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-store.ts +4 -4
- package/server/utils/files/naming.ts +2 -2
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +12 -12
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-io.ts +5 -5
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/session-io.ts +35 -35
- package/server/utils/files/spreadsheet-store.ts +7 -7
- package/server/utils/files/todos-io.ts +9 -9
- package/server/utils/files/user-tasks-io.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/indexer.ts +15 -15
- package/server/workspace/chat-index/summarizer.ts +4 -4
- package/server/workspace/custom-dirs.ts +16 -16
- package/server/workspace/journal/archivist.ts +35 -35
- package/server/workspace/journal/dailyPass.ts +31 -28
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +4 -4
- package/server/workspace/journal/indexFile.ts +29 -25
- 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 +20 -20
- package/server/workspace/roles.ts +6 -6
- package/server/workspace/skills/discovery.ts +4 -4
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +3 -3
- package/server/workspace/skills/user-tasks.ts +34 -34
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +10 -10
- package/server/workspace/sources/classifier.ts +7 -7
- package/server/workspace/sources/fetchers/arxiv.ts +7 -7
- package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
- package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
- package/server/workspace/sources/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +12 -12
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +36 -13
- package/server/workspace/sources/pipeline/index.ts +8 -13
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/plan.ts +15 -13
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/registry.ts +16 -16
- package/server/workspace/sources/robots.ts +14 -14
- package/server/workspace/sources/sourceState.ts +17 -10
- 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/tool-trace/index.ts +1 -1
- package/server/workspace/tool-trace/writeSearch.ts +26 -16
- package/server/workspace/wiki-backlinks/index.ts +8 -8
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
- package/server/workspace/workspace.ts +7 -7
- package/src/App.vue +315 -141
- package/src/components/CanvasViewToggle.vue +10 -7
- package/src/components/ChatInput.vue +67 -33
- 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 +17 -14
- package/src/components/NotificationBell.vue +14 -5
- package/src/components/NotificationToast.vue +6 -3
- package/src/components/PluginLauncher.vue +19 -56
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/RoleSelector.vue +2 -2
- package/src/components/SessionHistoryPanel.vue +38 -34
- package/src/components/SessionTabBar.vue +8 -10
- package/src/components/SettingsMcpTab.vue +49 -36
- package/src/components/SettingsModal.vue +24 -22
- package/src/components/SettingsReferenceDirsTab.vue +39 -34
- package/src/components/SettingsWorkspaceDirsTab.vue +37 -27
- package/src/components/SidebarHeader.vue +25 -4
- package/src/components/StackView.vue +4 -1
- package/src/components/SuggestionsPanel.vue +7 -4
- package/src/components/TodoExplorer.vue +26 -15
- package/src/components/ToolResultsPanel.vue +27 -13
- package/src/components/todo/TodoAddDialog.vue +19 -14
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +17 -12
- package/src/components/todo/TodoKanbanView.vue +10 -5
- package/src/components/todo/TodoListView.vue +10 -7
- package/src/components/todo/TodoTableView.vue +5 -2
- package/src/composables/useAppApi.ts +9 -0
- package/src/composables/useClickOutside.ts +2 -2
- 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/useFreshPluginData.ts +3 -3
- package/src/composables/useKeyNavigation.ts +11 -11
- package/src/composables/useLayoutMode.ts +32 -0
- package/src/composables/useMcpTools.ts +2 -2
- package/src/composables/useNotifications.ts +3 -3
- package/src/composables/usePdfDownload.ts +4 -4
- package/src/composables/usePendingCalls.ts +1 -1
- package/src/composables/usePubSub.ts +10 -10
- package/src/composables/useRoles.ts +1 -1
- package/src/composables/useSandboxStatus.ts +1 -1
- package/src/composables/useSessionDerived.ts +3 -3
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useSessionSync.ts +8 -8
- package/src/composables/useViewLayout.ts +20 -34
- package/src/config/roles.ts +2 -2
- 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 +5 -5
- 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 +29 -25
- package/src/plugins/manageSource/Preview.vue +2 -2
- package/src/plugins/manageSource/View.vue +73 -52
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +26 -36
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentHtml/helpers.ts +8 -8
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +40 -30
- package/src/plugins/presentMulmoScript/helpers.ts +1 -1
- package/src/plugins/scheduler/Preview.vue +13 -10
- package/src/plugins/scheduler/TasksTab.vue +57 -28
- 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 +29 -9
- package/src/plugins/todo/Preview.vue +13 -8
- package/src/plugins/todo/View.vue +38 -24
- package/src/plugins/todo/composables/useTodos.ts +5 -5
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +10 -7
- package/src/plugins/wiki/View.vue +202 -81
- package/src/plugins/wiki/helpers.ts +4 -4
- package/src/plugins/wiki/route.ts +112 -0
- package/src/router/guards.ts +46 -28
- package/src/router/index.ts +41 -26
- package/src/types/session.ts +4 -3
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/request.ts +22 -3
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/dom/scrollable.ts +2 -2
- package/src/utils/files/expandedDirs.ts +1 -1
- package/src/utils/files/sortChildren.ts +6 -6
- package/src/utils/format/frontmatter.ts +6 -6
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
- package/src/utils/markdown/extractFirstH1.ts +2 -2
- package/src/utils/path/relativeLink.ts +15 -15
- package/src/utils/path/workspaceLinkRouter.ts +81 -0
- package/src/utils/role/icon.ts +2 -2
- package/src/utils/role/merge.ts +2 -2
- package/src/utils/role/plugins.ts +1 -1
- package/src/utils/session/sessionFactory.ts +2 -2
- package/src/utils/session/sessionHelpers.ts +2 -2
- package/src/utils/tools/dedup.ts +4 -4
- package/src/utils/tools/result.ts +3 -3
- package/src/utils/types.ts +2 -2
- 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-KNLBjwuh.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
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
<button
|
|
3
3
|
class="flex items-center justify-center w-8 h-8 rounded transition-colors hover:bg-gray-100"
|
|
4
4
|
:class="isStack ? 'text-blue-500' : 'text-gray-400 hover:text-gray-700'"
|
|
5
|
-
:title="isStack ? '
|
|
6
|
-
:aria-label="isStack ? '
|
|
5
|
+
:title="isStack ? t('canvasViewToggle.stackViewTooltip') : t('canvasViewToggle.singleViewTooltip')"
|
|
6
|
+
:aria-label="isStack ? t('canvasViewToggle.switchToSingle') : t('canvasViewToggle.switchToStack')"
|
|
7
7
|
:data-testid="`canvas-view-toggle-${modelValue}`"
|
|
8
|
-
@click="emit('update:modelValue', isStack ?
|
|
8
|
+
@click="emit('update:modelValue', isStack ? LAYOUT_MODES.single : LAYOUT_MODES.stack)"
|
|
9
9
|
>
|
|
10
10
|
<span class="material-icons text-lg">view_agenda</span>
|
|
11
11
|
</button>
|
|
@@ -13,15 +13,18 @@
|
|
|
13
13
|
|
|
14
14
|
<script setup lang="ts">
|
|
15
15
|
import { computed } from "vue";
|
|
16
|
-
import {
|
|
16
|
+
import { useI18n } from "vue-i18n";
|
|
17
|
+
import { LAYOUT_MODES, type LayoutMode } from "../utils/canvas/layoutMode";
|
|
18
|
+
|
|
19
|
+
const { t } = useI18n();
|
|
17
20
|
|
|
18
21
|
const props = defineProps<{
|
|
19
|
-
modelValue:
|
|
22
|
+
modelValue: LayoutMode;
|
|
20
23
|
}>();
|
|
21
24
|
|
|
22
25
|
const emit = defineEmits<{
|
|
23
|
-
"update:modelValue": [mode:
|
|
26
|
+
"update:modelValue": [mode: LayoutMode];
|
|
24
27
|
}>();
|
|
25
28
|
|
|
26
|
-
const isStack = computed(() => props.modelValue ===
|
|
29
|
+
const isStack = computed(() => props.modelValue === LAYOUT_MODES.stack);
|
|
27
30
|
</script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="p-
|
|
2
|
+
<div class="p-2 border-t border-gray-200" @dragover.prevent @drop="onDropFile">
|
|
3
3
|
<div v-if="fileError" class="mb-2 text-xs text-red-600 bg-red-50 border border-red-200 rounded px-3 py-1.5" data-testid="file-error">
|
|
4
4
|
{{ fileError }}
|
|
5
5
|
</div>
|
|
@@ -15,43 +15,55 @@
|
|
|
15
15
|
ref="textarea"
|
|
16
16
|
:value="modelValue"
|
|
17
17
|
data-testid="user-input"
|
|
18
|
-
placeholder="
|
|
19
|
-
|
|
20
|
-
class="flex-1 bg-white border border-gray-300 rounded px-3 py-2 text-sm text-gray-900 placeholder-gray-400 disabled:opacity-50 disabled:cursor-not-allowed resize-none
|
|
21
|
-
:class="inputFocused ? 'ring-2 ring-blue-300' : ''"
|
|
18
|
+
:placeholder="t('chatInput.placeholder')"
|
|
19
|
+
rows="2"
|
|
20
|
+
class="flex-1 bg-white border border-gray-300 rounded px-3 py-2 text-sm text-gray-900 placeholder-gray-400 disabled:opacity-50 disabled:cursor-not-allowed resize-none"
|
|
22
21
|
:disabled="isRunning"
|
|
23
22
|
@input="emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
|
|
24
|
-
@focus="inputFocused = true"
|
|
25
23
|
@compositionstart="imeEnter.onCompositionStart"
|
|
26
24
|
@compositionend="imeEnter.onCompositionEnd"
|
|
27
25
|
@keydown="imeEnter.onKeydown"
|
|
28
|
-
@blur="
|
|
26
|
+
@blur="imeEnter.onBlur"
|
|
29
27
|
@paste="onPasteFile"
|
|
30
28
|
/>
|
|
31
29
|
<div class="flex flex-col gap-1">
|
|
32
30
|
<button
|
|
33
31
|
data-testid="send-btn"
|
|
34
|
-
class="bg-blue-600 hover:bg-blue-700 text-white rounded
|
|
32
|
+
class="bg-blue-600 hover:bg-blue-700 text-white rounded w-8 h-8 flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed"
|
|
35
33
|
:disabled="isRunning"
|
|
36
34
|
@click="emit('send')"
|
|
37
35
|
>
|
|
38
|
-
<span class="material-icons text-base">send</span>
|
|
36
|
+
<span class="material-icons text-base leading-none">send</span>
|
|
37
|
+
</button>
|
|
38
|
+
<button
|
|
39
|
+
data-testid="attach-file-btn"
|
|
40
|
+
class="text-gray-400 hover:text-gray-600 rounded w-8 h-8 flex items-center justify-center"
|
|
41
|
+
:title="t('chatInput.attachFile')"
|
|
42
|
+
@click="openFilePicker"
|
|
43
|
+
>
|
|
44
|
+
<span class="material-icons text-base leading-none">attach_file</span>
|
|
39
45
|
</button>
|
|
40
46
|
<button
|
|
41
47
|
data-testid="expand-input-btn"
|
|
42
|
-
class="text-gray-400 hover:text-gray-600 rounded
|
|
43
|
-
title="
|
|
48
|
+
class="text-gray-400 hover:text-gray-600 rounded w-8 h-8 flex items-center justify-center"
|
|
49
|
+
:title="t('chatInput.expandEditor')"
|
|
44
50
|
@click="openExpandedEditor"
|
|
45
51
|
>
|
|
46
|
-
<span class="material-icons text-base">open_in_full</span>
|
|
52
|
+
<span class="material-icons text-base leading-none">open_in_full</span>
|
|
47
53
|
</button>
|
|
48
54
|
</div>
|
|
49
55
|
</div>
|
|
50
56
|
|
|
57
|
+
<!-- Hidden file input driven by the attach button. The `accept`
|
|
58
|
+
filter matches ACCEPTED_MIME_PREFIXES/_EXACT below; the change
|
|
59
|
+
handler routes through the same readAttachmentFile() used by
|
|
60
|
+
drop + paste, so all three paths behave identically. -->
|
|
61
|
+
<input ref="fileInput" type="file" class="hidden" :accept="fileInputAccept" data-testid="file-input" @change="onFilePicked" />
|
|
62
|
+
|
|
51
63
|
<div v-if="expandedEditorOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40" @click.self="closeExpandedEditor">
|
|
52
64
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4 flex flex-col" style="max-height: 80vh">
|
|
53
65
|
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-200">
|
|
54
|
-
<h3 class="text-sm font-semibold text-gray-700">
|
|
66
|
+
<h3 class="text-sm font-semibold text-gray-700">{{ t("chatInput.composeMessage") }}</h3>
|
|
55
67
|
<button class="text-gray-400 hover:text-gray-600" @click="closeExpandedEditor">
|
|
56
68
|
<span class="material-icons text-base">close</span>
|
|
57
69
|
</button>
|
|
@@ -60,7 +72,7 @@
|
|
|
60
72
|
ref="expandedTextarea"
|
|
61
73
|
:value="modelValue"
|
|
62
74
|
data-testid="expanded-input"
|
|
63
|
-
placeholder="
|
|
75
|
+
:placeholder="t('chatInput.placeholder')"
|
|
64
76
|
class="flex-1 px-4 py-3 text-sm text-gray-900 placeholder-gray-400 resize-none focus:outline-none"
|
|
65
77
|
style="min-height: 300px"
|
|
66
78
|
@input="emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
|
|
@@ -68,16 +80,18 @@
|
|
|
68
80
|
@keydown.ctrl.enter="sendFromExpanded"
|
|
69
81
|
></textarea>
|
|
70
82
|
<div class="flex items-center justify-between px-4 py-3 border-t border-gray-200">
|
|
71
|
-
<p class="text-xs text-gray-400">
|
|
83
|
+
<p class="text-xs text-gray-400">{{ t("chatInput.sendHint") }}</p>
|
|
72
84
|
<div class="flex gap-2">
|
|
73
|
-
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="closeExpandedEditor">
|
|
85
|
+
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="closeExpandedEditor">
|
|
86
|
+
{{ t("common.cancel") }}
|
|
87
|
+
</button>
|
|
74
88
|
<button
|
|
75
89
|
class="px-3 py-1.5 text-sm rounded bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-40"
|
|
76
90
|
:disabled="isRunning"
|
|
77
91
|
data-testid="expanded-send-btn"
|
|
78
92
|
@click="sendFromExpanded"
|
|
79
93
|
>
|
|
80
|
-
|
|
94
|
+
{{ t("chatInput.send") }}
|
|
81
95
|
</button>
|
|
82
96
|
</div>
|
|
83
97
|
</div>
|
|
@@ -88,9 +102,12 @@
|
|
|
88
102
|
|
|
89
103
|
<script setup lang="ts">
|
|
90
104
|
import { nextTick, ref } from "vue";
|
|
105
|
+
import { useI18n } from "vue-i18n";
|
|
91
106
|
import ChatAttachmentPreview from "./ChatAttachmentPreview.vue";
|
|
92
107
|
import { useImeAwareEnter } from "../composables/useImeAwareEnter";
|
|
93
108
|
|
|
109
|
+
const { t } = useI18n();
|
|
110
|
+
|
|
94
111
|
export interface PastedFile {
|
|
95
112
|
dataUrl: string;
|
|
96
113
|
name: string;
|
|
@@ -111,9 +128,9 @@ const emit = defineEmits<{
|
|
|
111
128
|
|
|
112
129
|
const textarea = ref<HTMLTextAreaElement | null>(null);
|
|
113
130
|
const expandedTextarea = ref<HTMLTextAreaElement | null>(null);
|
|
114
|
-
const inputFocused = ref(false);
|
|
115
131
|
const expandedEditorOpen = ref(false);
|
|
116
132
|
const fileError = ref<string | null>(null);
|
|
133
|
+
const fileInput = ref<HTMLInputElement | null>(null);
|
|
117
134
|
|
|
118
135
|
const MAX_ATTACH_BYTES = 30 * 1024 * 1024;
|
|
119
136
|
|
|
@@ -129,16 +146,28 @@ const ACCEPTED_MIME_EXACT = new Set([
|
|
|
129
146
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
130
147
|
]);
|
|
131
148
|
|
|
149
|
+
// `accept` attribute for the hidden <input type="file"> that the
|
|
150
|
+
// paperclip button drives. Prefixes like `image/*` and `text/*` are
|
|
151
|
+
// expanded by the browser's native file picker; exact MIME entries
|
|
152
|
+
// are passed through. Drop + paste still accept the same set via the
|
|
153
|
+
// isAcceptedType() check below, so all three entry points stay in sync.
|
|
154
|
+
const fileInputAccept = [...ACCEPTED_MIME_PREFIXES.map((prefix) => `${prefix}*`), ...ACCEPTED_MIME_EXACT].join(",");
|
|
155
|
+
|
|
132
156
|
function isAcceptedType(mime: string): boolean {
|
|
133
|
-
return ACCEPTED_MIME_PREFIXES.some((
|
|
157
|
+
return ACCEPTED_MIME_PREFIXES.some((prefix) => mime.startsWith(prefix)) || ACCEPTED_MIME_EXACT.has(mime);
|
|
134
158
|
}
|
|
135
159
|
|
|
136
160
|
function readAttachmentFile(file: File): void {
|
|
137
161
|
fileError.value = null;
|
|
138
|
-
if (!isAcceptedType(file.type))
|
|
162
|
+
if (!isAcceptedType(file.type)) {
|
|
163
|
+
// Previously returned silently. That left the user wondering whether
|
|
164
|
+
// the drop/paste registered at all — #499.
|
|
165
|
+
fileError.value = t("chatInput.unsupportedFileType");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
139
168
|
if (file.size > MAX_ATTACH_BYTES) {
|
|
140
169
|
const sizeMB = (file.size / 1024 / 1024).toFixed(1);
|
|
141
|
-
fileError.value =
|
|
170
|
+
fileError.value = t("chatInput.fileTooLarge", { sizeMB });
|
|
142
171
|
return;
|
|
143
172
|
}
|
|
144
173
|
const reader = new FileReader();
|
|
@@ -154,14 +183,14 @@ function readAttachmentFile(file: File): void {
|
|
|
154
183
|
reader.readAsDataURL(file);
|
|
155
184
|
}
|
|
156
185
|
|
|
157
|
-
function onPasteFile(
|
|
158
|
-
const items =
|
|
186
|
+
function onPasteFile(event: ClipboardEvent): void {
|
|
187
|
+
const items = event.clipboardData?.items;
|
|
159
188
|
if (!items) return;
|
|
160
189
|
for (const item of items) {
|
|
161
190
|
if (isAcceptedType(item.type)) {
|
|
162
191
|
const file = item.getAsFile();
|
|
163
192
|
if (file) {
|
|
164
|
-
|
|
193
|
+
event.preventDefault();
|
|
165
194
|
readAttachmentFile(file);
|
|
166
195
|
return;
|
|
167
196
|
}
|
|
@@ -169,21 +198,26 @@ function onPasteFile(e: ClipboardEvent): void {
|
|
|
169
198
|
}
|
|
170
199
|
}
|
|
171
200
|
|
|
172
|
-
function onDropFile(
|
|
173
|
-
|
|
174
|
-
const file =
|
|
201
|
+
function onDropFile(event: DragEvent): void {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
const file = event.dataTransfer?.files[0];
|
|
175
204
|
if (file) readAttachmentFile(file);
|
|
176
205
|
}
|
|
177
206
|
|
|
178
|
-
|
|
207
|
+
function openFilePicker(): void {
|
|
208
|
+
fileInput.value?.click();
|
|
209
|
+
}
|
|
179
210
|
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
211
|
+
function onFilePicked(event: Event): void {
|
|
212
|
+
const input = event.target as HTMLInputElement;
|
|
213
|
+
const file = input.files?.[0];
|
|
214
|
+
if (file) readAttachmentFile(file);
|
|
215
|
+
// Reset so selecting the same file twice in a row still fires @change.
|
|
216
|
+
input.value = "";
|
|
185
217
|
}
|
|
186
218
|
|
|
219
|
+
const imeEnter = useImeAwareEnter(() => emit("send"));
|
|
220
|
+
|
|
187
221
|
function openExpandedEditor(): void {
|
|
188
222
|
expandedEditorOpen.value = true;
|
|
189
223
|
nextTick(() => expandedTextarea.value?.focus());
|
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
<button
|
|
7
7
|
v-if="isMarkdown"
|
|
8
8
|
class="ml-auto shrink-0 px-2 py-0.5 rounded border border-gray-200 text-gray-600 hover:bg-gray-100 font-sans"
|
|
9
|
-
:title="mdRawMode ? '
|
|
9
|
+
:title="mdRawMode ? t('fileContentHeader.showRendered') : t('fileContentHeader.showRaw')"
|
|
10
10
|
@click="emit('toggleMdRaw')"
|
|
11
11
|
>
|
|
12
|
-
{{ mdRawMode ? "
|
|
12
|
+
{{ mdRawMode ? t("fileContentHeader.rendered") : t("fileContentHeader.raw") }}
|
|
13
13
|
</button>
|
|
14
14
|
<button
|
|
15
15
|
type="button"
|
|
16
16
|
class="shrink-0 px-1 py-0.5 rounded text-gray-400 hover:text-gray-700 hover:bg-gray-100"
|
|
17
17
|
:class="{ 'ml-auto': !isMarkdown }"
|
|
18
|
-
title="
|
|
19
|
-
aria-label="
|
|
18
|
+
:title="t('fileContentHeader.closeFile')"
|
|
19
|
+
:aria-label="t('fileContentHeader.closeFile')"
|
|
20
20
|
data-testid="close-file-btn"
|
|
21
21
|
@click="emit('deselect')"
|
|
22
22
|
>
|
|
@@ -26,8 +26,11 @@
|
|
|
26
26
|
</template>
|
|
27
27
|
|
|
28
28
|
<script setup lang="ts">
|
|
29
|
+
import { useI18n } from "vue-i18n";
|
|
29
30
|
import { formatDateTime } from "../utils/format/date";
|
|
30
31
|
|
|
32
|
+
const { t } = useI18n();
|
|
33
|
+
|
|
31
34
|
defineProps<{
|
|
32
35
|
selectedPath: string | null;
|
|
33
36
|
size: number | null;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="flex-1 overflow-auto min-h-0">
|
|
3
|
-
<div v-if="!selectedPath" class="h-full flex items-center justify-center text-gray-400 text-sm">
|
|
3
|
+
<div v-if="!selectedPath" class="h-full flex items-center justify-center text-gray-400 text-sm">{{ t("fileContentRenderer.selectFile") }}</div>
|
|
4
4
|
<div v-else-if="contentError" class="p-4 text-sm text-red-600">
|
|
5
5
|
{{ contentError }}
|
|
6
6
|
</div>
|
|
7
|
-
<div v-else-if="contentLoading" class="p-4 text-sm text-gray-400">
|
|
7
|
+
<div v-else-if="contentLoading" class="p-4 text-sm text-gray-400">{{ t("common.loading") }}</div>
|
|
8
8
|
<template v-else-if="content">
|
|
9
9
|
<template v-if="content.kind === 'text'">
|
|
10
10
|
<!-- Scheduler items.json: render with the scheduler plugin's
|
|
@@ -55,7 +55,13 @@
|
|
|
55
55
|
to restrict script loads to a vetted CDN whitelist +
|
|
56
56
|
inline; connect-src is `'none'` so the page can't
|
|
57
57
|
phone home. See src/utils/html/previewCsp.ts. -->
|
|
58
|
-
<iframe
|
|
58
|
+
<iframe
|
|
59
|
+
v-else-if="isHtml"
|
|
60
|
+
:srcdoc="sandboxedHtml"
|
|
61
|
+
class="w-full h-full border-0"
|
|
62
|
+
sandbox="allow-scripts"
|
|
63
|
+
:title="t('fileContentRenderer.htmlPreview')"
|
|
64
|
+
/>
|
|
59
65
|
<!-- JSON: pretty-printed with simple syntax coloring. Fall
|
|
60
66
|
back to raw content if the file is malformed. -->
|
|
61
67
|
<pre v-else-if="isJson" class="p-4 text-xs whitespace-pre-wrap font-mono text-gray-800"><span
|
|
@@ -66,7 +72,7 @@
|
|
|
66
72
|
<!-- JSONL / NDJSON: one pretty-printed + colored record per line -->
|
|
67
73
|
<div v-else-if="isJsonl" class="p-4 space-y-2">
|
|
68
74
|
<div v-for="(line, i) in jsonlLines" :key="i" class="rounded border bg-gray-50 p-3" :class="line.parseError ? 'border-red-300' : 'border-gray-200'">
|
|
69
|
-
<div v-if="line.parseError" class="text-xs text-red-600 mb-1 font-sans">
|
|
75
|
+
<div v-if="line.parseError" class="text-xs text-red-600 mb-1 font-sans">{{ t("fileContentRenderer.parseError") }}</div>
|
|
70
76
|
<pre class="text-xs font-mono text-gray-800 whitespace-pre-wrap"><span
|
|
71
77
|
v-for="(tok, j) in line.tokens"
|
|
72
78
|
:key="j"
|
|
@@ -82,7 +88,12 @@
|
|
|
82
88
|
<img :src="rawUrl(selectedPath)" :alt="selectedPath" class="max-w-full max-h-full object-contain" />
|
|
83
89
|
</div>
|
|
84
90
|
<!-- PDF -->
|
|
85
|
-
<iframe
|
|
91
|
+
<iframe
|
|
92
|
+
v-else-if="content.kind === 'pdf' && selectedPath"
|
|
93
|
+
:src="rawUrl(selectedPath)"
|
|
94
|
+
class="w-full h-full border-0"
|
|
95
|
+
:title="t('fileContentRenderer.pdfPreview')"
|
|
96
|
+
/>
|
|
86
97
|
<!-- Audio -->
|
|
87
98
|
<div v-else-if="content.kind === 'audio' && selectedPath" class="h-full flex items-center justify-center p-4">
|
|
88
99
|
<audio :key="selectedPath" :src="rawUrl(selectedPath)" controls preload="metadata" class="w-full max-w-2xl" />
|
|
@@ -93,13 +104,14 @@
|
|
|
93
104
|
</div>
|
|
94
105
|
<!-- Binary or too-large -->
|
|
95
106
|
<div v-else class="p-4 text-sm text-gray-500">
|
|
96
|
-
|
|
107
|
+
<template v-if="'message' in content">{{ content.message }}</template>
|
|
97
108
|
</div>
|
|
98
109
|
</template>
|
|
99
110
|
</div>
|
|
100
111
|
</template>
|
|
101
112
|
|
|
102
113
|
<script setup lang="ts">
|
|
114
|
+
import { useI18n } from "vue-i18n";
|
|
103
115
|
import TextResponseView from "../plugins/textResponse/View.vue";
|
|
104
116
|
import SchedulerView from "../plugins/scheduler/View.vue";
|
|
105
117
|
import TodoExplorer from "./TodoExplorer.vue";
|
|
@@ -114,6 +126,8 @@ import type { Frontmatter } from "../utils/format/frontmatter";
|
|
|
114
126
|
import { rewriteMarkdownImageRefs } from "../utils/image/rewriteMarkdownImageRefs";
|
|
115
127
|
import { API_ROUTES } from "../config/apiRoutes";
|
|
116
128
|
|
|
129
|
+
const { t } = useI18n();
|
|
130
|
+
|
|
117
131
|
const props = defineProps<{
|
|
118
132
|
selectedPath: string | null;
|
|
119
133
|
content: FileContent | null;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
@click="onToggle"
|
|
8
8
|
>
|
|
9
9
|
<span class="material-icons text-sm text-gray-400 shrink-0">{{ expanded ? "folder_open" : "folder" }}</span>
|
|
10
|
-
<span class="text-gray-700 truncate">{{ node.name || "
|
|
10
|
+
<span class="text-gray-700 truncate">{{ node.name || t("fileTree.workspace") }}</span>
|
|
11
11
|
</button>
|
|
12
12
|
<button
|
|
13
13
|
v-else
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
>
|
|
20
20
|
<span class="material-icons text-sm text-gray-400 shrink-0">description</span>
|
|
21
21
|
<span class="truncate">{{ node.name }}</span>
|
|
22
|
-
<span v-if="isRecent" class="ml-auto w-1.5 h-1.5 rounded-full bg-green-500 shrink-0" title="
|
|
22
|
+
<span v-if="isRecent" class="ml-auto w-1.5 h-1.5 rounded-full bg-green-500 shrink-0" :title="t('fileTree.recentlyChanged')" />
|
|
23
23
|
</button>
|
|
24
24
|
<div v-if="node.type === 'dir' && expanded" class="pl-4">
|
|
25
25
|
<!-- Loading state: children not in the cache yet. Rendered
|
|
26
26
|
once per dir so a slow network shows where the wait is,
|
|
27
27
|
not as a global overlay. -->
|
|
28
|
-
<div v-if="loadingChildren" class="px-2 py-1 text-xs text-gray-400">
|
|
28
|
+
<div v-if="loadingChildren" class="px-2 py-1 text-xs text-gray-400">{{ t("common.loading") }}</div>
|
|
29
29
|
<FileTree
|
|
30
30
|
v-for="child in loadedChildren"
|
|
31
31
|
:key="child.path"
|
|
@@ -43,10 +43,13 @@
|
|
|
43
43
|
|
|
44
44
|
<script setup lang="ts">
|
|
45
45
|
import { computed, watch } from "vue";
|
|
46
|
+
import { useI18n } from "vue-i18n";
|
|
46
47
|
import { useExpandedDirs } from "../composables/useExpandedDirs";
|
|
47
48
|
import { sortChildren } from "../utils/files/sortChildren";
|
|
48
49
|
import type { FileSortMode } from "../composables/useFileSortMode";
|
|
49
50
|
|
|
51
|
+
const { t } = useI18n();
|
|
52
|
+
|
|
50
53
|
// TreeNode lives in src/types/fileTree.ts so .ts composables can
|
|
51
54
|
// import it without depending on a .vue module. Re-export here so
|
|
52
55
|
// existing `import { TreeNode } from "./FileTree.vue"` keeps working.
|
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="w-72 flex-shrink-0 border-r border-gray-200 overflow-y-auto p-2 bg-gray-50">
|
|
3
3
|
<div class="flex justify-end items-center gap-2 px-1 pb-1 text-xs">
|
|
4
|
-
<span class="text-gray-400">
|
|
4
|
+
<span class="text-gray-400">{{ t("fileTreePane.sort") }}</span>
|
|
5
5
|
<button
|
|
6
6
|
type="button"
|
|
7
7
|
class="px-2 py-0.5 rounded transition-colors"
|
|
8
8
|
:class="sortMode === 'name' ? 'bg-blue-100 text-blue-700 font-medium' : 'text-gray-500 hover:bg-gray-200'"
|
|
9
9
|
:aria-pressed="sortMode === 'name'"
|
|
10
|
-
title="
|
|
10
|
+
:title="t('fileTreePane.sortByName')"
|
|
11
11
|
data-testid="file-sort-name"
|
|
12
12
|
@click="emit('update:sortMode', 'name')"
|
|
13
13
|
>
|
|
14
|
-
|
|
14
|
+
{{ t("fileTreePane.name") }}
|
|
15
15
|
</button>
|
|
16
16
|
<button
|
|
17
17
|
type="button"
|
|
18
18
|
class="px-2 py-0.5 rounded transition-colors"
|
|
19
19
|
:class="sortMode === 'recent' ? 'bg-blue-100 text-blue-700 font-medium' : 'text-gray-500 hover:bg-gray-200'"
|
|
20
20
|
:aria-pressed="sortMode === 'recent'"
|
|
21
|
-
title="
|
|
21
|
+
:title="t('fileTreePane.sortByRecent')"
|
|
22
22
|
data-testid="file-sort-recent"
|
|
23
23
|
@click="emit('update:sortMode', 'recent')"
|
|
24
24
|
>
|
|
25
|
-
|
|
25
|
+
{{ t("fileTreePane.recent") }}
|
|
26
26
|
</button>
|
|
27
27
|
</div>
|
|
28
28
|
<div v-if="treeError" class="p-2 text-xs text-red-600">
|
|
29
29
|
{{ treeError }}
|
|
30
30
|
</div>
|
|
31
|
-
<div v-else-if="!rootNode" class="p-2 text-xs text-gray-400">
|
|
31
|
+
<div v-else-if="!rootNode" class="p-2 text-xs text-gray-400">{{ t("common.loading") }}</div>
|
|
32
32
|
<FileTree
|
|
33
33
|
v-else
|
|
34
34
|
:node="rootNode"
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
/>
|
|
42
42
|
<template v-if="refRoots.length > 0">
|
|
43
43
|
<div class="mt-2 pt-2 border-t border-gray-200 px-1 mb-1 flex items-center gap-1">
|
|
44
|
-
<span class="text-[10px] font-semibold text-gray-400 uppercase">
|
|
45
|
-
<span class="text-[9px] px-1 py-0.5 rounded bg-blue-100 text-blue-600">
|
|
44
|
+
<span class="text-[10px] font-semibold text-gray-400 uppercase">{{ t("fileTreePane.reference") }}</span>
|
|
45
|
+
<span class="text-[9px] px-1 py-0.5 rounded bg-blue-100 text-blue-600">{{ t("fileTreePane.readOnlyBadge") }}</span>
|
|
46
46
|
</div>
|
|
47
47
|
<FileTree
|
|
48
48
|
v-for="refNode in refRoots"
|
|
@@ -60,10 +60,13 @@
|
|
|
60
60
|
</template>
|
|
61
61
|
|
|
62
62
|
<script setup lang="ts">
|
|
63
|
+
import { useI18n } from "vue-i18n";
|
|
63
64
|
import FileTree from "./FileTree.vue";
|
|
64
65
|
import type { TreeNode } from "../types/fileTree";
|
|
65
66
|
import type { FileSortMode } from "../composables/useFileSortMode";
|
|
66
67
|
|
|
68
|
+
const { t } = useI18n();
|
|
69
|
+
|
|
67
70
|
defineProps<{
|
|
68
71
|
rootNode: TreeNode | null;
|
|
69
72
|
refRoots: TreeNode[];
|
|
@@ -54,7 +54,7 @@ import FileTreePane from "./FileTreePane.vue";
|
|
|
54
54
|
import FileContentHeader from "./FileContentHeader.vue";
|
|
55
55
|
import FileContentRenderer from "./FileContentRenderer.vue";
|
|
56
56
|
import { useFileTree } from "../composables/useFileTree";
|
|
57
|
-
import { useFileSelection, isValidFilePath } from "../composables/useFileSelection";
|
|
57
|
+
import { useFileSelection, isValidFilePath, readPathMatch } from "../composables/useFileSelection";
|
|
58
58
|
import { useMarkdownMode } from "../composables/useMarkdownMode";
|
|
59
59
|
import { useFileSortMode } from "../composables/useFileSortMode";
|
|
60
60
|
import { useContentDisplay } from "../composables/useContentDisplay";
|
|
@@ -161,9 +161,11 @@ const { handleMarkdownLinkClick } = useMarkdownLinkHandler(selectedPath, {
|
|
|
161
161
|
onLoadSession: (sessionId) => emit("loadSession", sessionId),
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
// External URL changes (back/forward) → update selectedPath.
|
|
164
|
+
// External URL changes (back/forward) → update selectedPath. Reading
|
|
165
|
+
// from `route.params.pathMatch` after the query→params migration;
|
|
166
|
+
// see plans/feat-files-path-url.md.
|
|
165
167
|
watch(
|
|
166
|
-
() => route.
|
|
168
|
+
() => readPathMatch(route.params.pathMatch),
|
|
167
169
|
(newPath) => {
|
|
168
170
|
if (!isValidFilePath(newPath)) {
|
|
169
171
|
if (selectedPath.value !== null) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
ref="button"
|
|
5
5
|
data-testid="sandbox-lock-button"
|
|
6
6
|
:class="sandboxEnabled ? 'text-gray-400 hover:text-gray-700' : 'text-amber-400 hover:text-amber-500'"
|
|
7
|
-
:title="sandboxEnabled ? '
|
|
7
|
+
:title="sandboxEnabled ? t('lockStatusPopup.sandboxEnabledTooltip') : t('lockStatusPopup.noSandboxTooltip')"
|
|
8
8
|
@click="emit('update:open', !open)"
|
|
9
9
|
>
|
|
10
10
|
<span class="material-icons">{{ sandboxEnabled ? "lock" : "lock_open" }}</span>
|
|
@@ -13,36 +13,36 @@
|
|
|
13
13
|
<p class="mb-2" :class="sandboxEnabled ? 'text-green-800' : 'text-amber-500'">
|
|
14
14
|
<template v-if="sandboxEnabled">
|
|
15
15
|
<span class="material-icons text-xs align-middle mr-1">lock</span>
|
|
16
|
-
<strong>
|
|
16
|
+
<strong>{{ t("lockStatusPopup.sandboxEnabledLabel") }}</strong> {{ t("lockStatusPopup.sandboxEnabledBody") }}
|
|
17
17
|
</template>
|
|
18
18
|
<template v-else>
|
|
19
19
|
<span class="material-icons text-xs align-middle mr-1">warning</span>
|
|
20
|
-
<strong>
|
|
21
|
-
<a href="https://www.docker.com/products/docker-desktop/" target="_blank" class="underline">
|
|
22
|
-
|
|
20
|
+
<strong>{{ t("lockStatusPopup.noSandboxLabel") }}</strong> {{ t("lockStatusPopup.noSandboxBodyPrefix") }}
|
|
21
|
+
<a href="https://www.docker.com/products/docker-desktop/" target="_blank" class="underline">{{ t("lockStatusPopup.dockerDesktop") }}</a>
|
|
22
|
+
{{ t("lockStatusPopup.noSandboxBodySuffix") }}
|
|
23
23
|
</template>
|
|
24
24
|
</p>
|
|
25
25
|
<div v-if="sandboxEnabled" data-testid="sandbox-credentials-block" class="mb-2 border-t border-gray-100 pt-2">
|
|
26
|
-
<p class="text-gray-400 mb-1">
|
|
27
|
-
<p v-if="sandboxStatus === null" class="text-gray-400 italic" data-testid="sandbox-credentials-loading">
|
|
26
|
+
<p class="text-gray-400 mb-1">{{ t("lockStatusPopup.hostCredentials") }}</p>
|
|
27
|
+
<p v-if="sandboxStatus === null" class="text-gray-400 italic" data-testid="sandbox-credentials-loading">{{ t("lockStatusPopup.credsLoading") }}</p>
|
|
28
28
|
<template v-else>
|
|
29
29
|
<p data-testid="sandbox-credentials-ssh">
|
|
30
30
|
<span class="mr-1">🔑</span>
|
|
31
|
-
<span class="text-gray-500">
|
|
31
|
+
<span class="text-gray-500">{{ t("lockStatusPopup.sshAgent") }}</span>
|
|
32
32
|
<span :class="sandboxStatus.sshAgent ? 'text-green-700' : 'text-gray-400'" class="ml-1">
|
|
33
|
-
{{ sandboxStatus.sshAgent ? "forwarded" : "
|
|
33
|
+
{{ sandboxStatus.sshAgent ? t("lockStatusPopup.forwarded") : t("lockStatusPopup.notForwarded") }}
|
|
34
34
|
</span>
|
|
35
35
|
</p>
|
|
36
36
|
<p data-testid="sandbox-credentials-mounts">
|
|
37
37
|
<span class="mr-1">📁</span>
|
|
38
|
-
<span class="text-gray-500">
|
|
38
|
+
<span class="text-gray-500">{{ t("lockStatusPopup.mountedConfigs") }}</span>
|
|
39
39
|
<span :class="sandboxStatus.mounts.length > 0 ? 'text-green-700' : 'text-gray-400'" class="ml-1">
|
|
40
|
-
{{ sandboxStatus.mounts.length > 0 ? sandboxStatus.mounts.join(", ") : "none" }}
|
|
40
|
+
{{ sandboxStatus.mounts.length > 0 ? sandboxStatus.mounts.join(", ") : t("lockStatusPopup.none") }}
|
|
41
41
|
</span>
|
|
42
42
|
</p>
|
|
43
43
|
</template>
|
|
44
44
|
</div>
|
|
45
|
-
<p class="text-gray-400 mb-1">
|
|
45
|
+
<p class="text-gray-400 mb-1">{{ t("lockStatusPopup.testIsolation") }}</p>
|
|
46
46
|
<div class="flex flex-col gap-1">
|
|
47
47
|
<button
|
|
48
48
|
v-for="q in SANDBOX_TEST_QUERIES"
|
|
@@ -60,8 +60,11 @@
|
|
|
60
60
|
|
|
61
61
|
<script setup lang="ts">
|
|
62
62
|
import { ref, watch } from "vue";
|
|
63
|
+
import { useI18n } from "vue-i18n";
|
|
63
64
|
import { useSandboxStatus } from "../composables/useSandboxStatus";
|
|
64
65
|
|
|
66
|
+
const { t } = useI18n();
|
|
67
|
+
|
|
65
68
|
const props = defineProps<{
|
|
66
69
|
sandboxEnabled: boolean;
|
|
67
70
|
open: boolean;
|
|
@@ -104,8 +107,8 @@ watch(
|
|
|
104
107
|
},
|
|
105
108
|
);
|
|
106
109
|
|
|
107
|
-
function onTestQuery(
|
|
110
|
+
function onTestQuery(query: string): void {
|
|
108
111
|
emit("update:open", false);
|
|
109
|
-
emit("testQuery",
|
|
112
|
+
emit("testQuery", query);
|
|
110
113
|
}
|
|
111
114
|
</script>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, watch, onMounted, onUnmounted } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
3
4
|
import { useNotifications } from "../composables/useNotifications";
|
|
4
5
|
import { formatRelativeTime } from "../utils/format/date";
|
|
5
6
|
import { NOTIFICATION_ICONS, NOTIFICATION_ACTION_TYPES, NOTIFICATION_PRIORITIES } from "../types/notification";
|
|
6
7
|
import type { NotificationPayload } from "../types/notification";
|
|
7
8
|
|
|
9
|
+
const { t } = useI18n();
|
|
8
10
|
const { notifications, unreadCount, markAllRead, dismiss } = useNotifications();
|
|
9
11
|
const open = ref(false);
|
|
10
12
|
const rootRef = ref<HTMLElement | null>(null);
|
|
@@ -70,7 +72,12 @@ function handleDismiss(event: Event, notificationId: string): void {
|
|
|
70
72
|
<template>
|
|
71
73
|
<div ref="rootRef" class="relative">
|
|
72
74
|
<!-- Bell button -->
|
|
73
|
-
<button
|
|
75
|
+
<button
|
|
76
|
+
class="relative text-gray-400 hover:text-gray-700"
|
|
77
|
+
data-testid="notification-bell"
|
|
78
|
+
:aria-label="t('notificationBell.notifications')"
|
|
79
|
+
@click="toggle"
|
|
80
|
+
>
|
|
74
81
|
<span class="material-icons">notifications</span>
|
|
75
82
|
<span
|
|
76
83
|
v-if="unreadCount > 0"
|
|
@@ -89,12 +96,14 @@ function handleDismiss(event: Event, notificationId: string): void {
|
|
|
89
96
|
>
|
|
90
97
|
<!-- Header -->
|
|
91
98
|
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
|
|
92
|
-
<span class="text-sm font-semibold text-gray-700">
|
|
93
|
-
<button class="text-xs text-blue-500 hover:text-blue-700" data-testid="notification-mark-all-read" @click="markAllRead">
|
|
99
|
+
<span class="text-sm font-semibold text-gray-700">{{ t("notificationBell.notifications") }}</span>
|
|
100
|
+
<button class="text-xs text-blue-500 hover:text-blue-700" data-testid="notification-mark-all-read" @click="markAllRead">
|
|
101
|
+
{{ t("notificationBell.markAllRead") }}
|
|
102
|
+
</button>
|
|
94
103
|
</div>
|
|
95
104
|
|
|
96
105
|
<!-- Empty state -->
|
|
97
|
-
<div v-if="notifications.length === 0" class="py-8 text-center text-sm text-gray-400">
|
|
106
|
+
<div v-if="notifications.length === 0" class="py-8 text-center text-sm text-gray-400">{{ t("notificationBell.noNotifications") }}</div>
|
|
98
107
|
|
|
99
108
|
<!-- Items -->
|
|
100
109
|
<div v-else>
|
|
@@ -121,7 +130,7 @@ function handleDismiss(event: Event, notificationId: string): void {
|
|
|
121
130
|
{{ formatTime(n.firedAt) }}
|
|
122
131
|
</p>
|
|
123
132
|
</div>
|
|
124
|
-
<button class="text-gray-300 hover:text-gray-500 shrink-0 mt-0.5" aria-label="
|
|
133
|
+
<button class="text-gray-300 hover:text-gray-500 shrink-0 mt-0.5" :aria-label="t('notificationBell.dismiss')" @click="handleDismiss($event, n.id)">
|
|
125
134
|
<span class="material-icons text-sm">close</span>
|
|
126
135
|
</button>
|
|
127
136
|
</div>
|