mulmoclaude 0.3.0 → 0.5.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-DiKaqnKs.js +5 -0
- package/client/assets/{index-eHWB79u5.js → index-C94GcmNa.js} +203 -198
- package/client/assets/index-CY-WpQUm.css +2 -0
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-5ipqh8Pe.js} +5 -5
- package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
- package/client/index.html +2 -4
- package/package.json +17 -15
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/backend/claude-code.ts +170 -0
- package/server/agent/backend/index.ts +14 -0
- package/server/agent/backend/types.ts +65 -0
- package/server/agent/index.ts +31 -159
- package/server/agent/mcp-server.ts +88 -10
- package/server/agent/mcp-tools/index.ts +8 -7
- package/server/agent/mcp-tools/notify.ts +76 -0
- package/server/agent/mcp-tools/x.ts +12 -2
- package/server/agent/plugin-names.ts +10 -4
- 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 +99 -4
- package/server/api/routes/chart.ts +13 -0
- package/server/api/routes/chat-index.ts +2 -1
- package/server/api/routes/config.ts +35 -8
- package/server/api/routes/files.ts +75 -24
- package/server/api/routes/html.ts +15 -2
- package/server/api/routes/image.ts +75 -20
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/news.ts +146 -0
- package/server/api/routes/notifications.ts +58 -2
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +73 -91
- package/server/api/routes/presentHtml.ts +9 -0
- package/server/api/routes/roles.ts +12 -2
- package/server/api/routes/scheduler.ts +20 -11
- package/server/api/routes/schedulerTasks.ts +58 -21
- package/server/api/routes/sessions.ts +15 -4
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +26 -5
- package/server/api/routes/sources.ts +8 -7
- package/server/api/routes/todos.ts +30 -0
- package/server/api/routes/todosColumnsHandlers.ts +13 -27
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki/frontmatter.ts +86 -0
- package/server/api/routes/wiki.ts +335 -75
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +32 -8
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +26 -16
- package/server/events/resolveRelayBridgeOptions.ts +125 -0
- package/server/index.ts +72 -49
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +15 -5
- package/server/system/macosNotify.ts +152 -0
- package/server/utils/errors.ts +11 -2
- package/server/utils/fetch.ts +54 -0
- package/server/utils/files/atomic.ts +18 -17
- package/server/utils/files/image-store.ts +19 -13
- package/server/utils/files/journal-io.ts +2 -2
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-image-fill.ts +131 -0
- package/server/utils/files/markdown-store.ts +22 -6
- package/server/utils/files/naming.ts +20 -10
- 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 +15 -10
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +30 -4
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/id.ts +40 -8
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- package/server/utils/logPreview.ts +24 -0
- 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/promptMeta.ts +32 -0
- package/server/utils/request.ts +12 -6
- package/server/utils/slug.ts +65 -4
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/index.ts +1 -1
- package/server/workspace/chat-index/summarizer.ts +5 -5
- package/server/workspace/custom-dirs.ts +5 -5
- package/server/workspace/helps/gemini.md +57 -0
- package/server/workspace/helps/index.md +2 -1
- package/server/workspace/helps/sources.md +42 -0
- package/server/workspace/helps/wiki.md +40 -5
- package/server/workspace/journal/archivist-cli.ts +121 -0
- package/server/workspace/journal/{archivist.ts → archivist-schemas.ts} +12 -120
- package/server/workspace/journal/dailyPass.ts +78 -38
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +56 -5
- package/server/workspace/journal/memoryExtractor.ts +1 -1
- package/server/workspace/journal/optimizationPass.ts +4 -5
- package/server/workspace/journal/paths.ts +8 -24
- package/server/workspace/journal/state.ts +18 -8
- package/server/workspace/news/reader.ts +248 -0
- package/server/workspace/paths.ts +4 -3
- package/server/workspace/reference-dirs.ts +3 -3
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +5 -4
- package/server/workspace/skills/user-tasks.ts +3 -2
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +2 -2
- package/server/workspace/sources/classifier.ts +1 -1
- 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 +59 -13
- package/server/workspace/sources/pipeline/index.ts +59 -7
- package/server/workspace/sources/pipeline/notify.ts +13 -5
- package/server/workspace/sources/pipeline/plan.ts +11 -9
- package/server/workspace/sources/pipeline/summarize.ts +1 -1
- 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 +477 -251
- package/src/components/CanvasViewToggle.vue +12 -10
- package/src/components/ChatInput.vue +112 -105
- package/src/components/FileContentHeader.vue +10 -7
- package/src/components/FileContentRenderer.vue +37 -10
- package/src/components/FileTree.vue +34 -4
- package/src/components/FileTreePane.vue +32 -27
- package/src/components/FilesView.vue +5 -3
- package/src/components/FilterChip.vue +22 -0
- package/src/components/LockStatusPopup.vue +19 -13
- package/src/components/NewsView.vue +252 -0
- package/src/components/NotificationBell.vue +35 -9
- package/src/components/NotificationToast.vue +4 -1
- package/src/components/PageChatComposer.vue +101 -0
- package/src/components/PluginLauncher.vue +36 -62
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/RoleSelector.vue +3 -2
- package/src/components/SessionHeaderControls.vue +63 -0
- package/src/components/SessionHistoryExpandButton.vue +30 -0
- package/src/components/SessionHistoryPanel.vue +64 -93
- package/src/components/SessionHistoryToggleButton.vue +40 -0
- package/src/components/SessionRoleIcon.vue +72 -0
- package/src/components/SessionSidebar.vue +96 -0
- package/src/components/SessionTabBar.vue +44 -51
- package/src/components/SettingsMcpTab.vue +361 -52
- package/src/components/SettingsModal.vue +203 -72
- package/src/components/SettingsReferenceDirsTab.vue +72 -51
- package/src/components/SettingsWorkspaceDirsTab.vue +74 -51
- package/src/components/SidebarHeader.vue +50 -16
- package/src/components/SourcesManager.vue +900 -0
- package/src/components/SourcesView.vue +45 -0
- package/src/components/StackView.vue +84 -48
- package/src/components/SuggestionsPanel.vue +25 -36
- package/src/components/SystemFileBanner.vue +106 -0
- package/src/components/ThinkingIndicator.vue +41 -0
- package/src/components/TodoExplorer.vue +72 -22
- 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 +16 -6
- package/src/components/todo/TodoListView.vue +14 -3
- package/src/components/todo/TodoTableView.vue +36 -5
- package/src/composables/favicon/conditions.ts +76 -0
- package/src/composables/favicon/resolveColor.ts +93 -0
- package/src/composables/favicon/types.ts +61 -0
- package/src/composables/useAppApi.ts +23 -0
- package/src/composables/useChatScroll.ts +5 -5
- package/src/composables/useCurrentRole.ts +32 -0
- package/src/composables/useDynamicFavicon.ts +174 -58
- package/src/composables/useEventListeners.ts +7 -12
- package/src/composables/useFaviconState.ts +93 -12
- package/src/composables/useFileSelection.ts +25 -6
- package/src/composables/useHealth.ts +76 -7
- package/src/composables/useLayoutMode.ts +27 -0
- package/src/composables/useNewsItems.ts +38 -0
- package/src/composables/useNewsReadState.ts +75 -0
- package/src/composables/useNotifications.ts +76 -13
- package/src/composables/usePendingCalls.ts +11 -1
- package/src/composables/useRoles.ts +6 -10
- package/src/composables/useRunElapsed.ts +80 -0
- package/src/composables/useSessionDerived.ts +21 -5
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useSidePanelVisible.ts +25 -0
- package/src/composables/useViewLayout.ts +16 -37
- package/src/config/apiRoutes.ts +19 -6
- package/src/config/historyFilters.ts +30 -0
- package/src/config/mcpCatalog.ts +285 -0
- package/src/config/mcpTypes.ts +26 -0
- package/src/config/roles.ts +19 -51
- package/src/config/systemFileDescriptors.ts +170 -0
- package/src/config/toolNames.ts +6 -1
- package/src/config/workspacePaths.ts +1 -0
- package/src/index.css +14 -0
- package/src/lang/de.ts +706 -0
- package/src/lang/en.ts +726 -0
- package/src/lang/es.ts +712 -0
- package/src/lang/fr.ts +704 -0
- package/src/lang/ja.ts +707 -0
- package/src/lang/ko.ts +709 -0
- package/src/lang/pt-BR.ts +702 -0
- package/src/lang/zh.ts +705 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +3 -0
- package/src/plugins/canvas/View.vue +104 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/canvas/index.ts +3 -2
- package/src/plugins/chart/Preview.vue +1 -1
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/chart/index.ts +3 -2
- package/src/plugins/editImage/index.ts +3 -2
- package/src/plugins/generateImage/index.ts +3 -2
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +67 -46
- package/src/plugins/manageRoles/index.ts +3 -2
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +39 -34
- package/src/plugins/manageSkills/index.ts +3 -2
- package/src/plugins/manageSource/Preview.vue +1 -1
- package/src/plugins/manageSource/View.vue +3 -687
- package/src/plugins/manageSource/index.ts +3 -2
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +164 -73
- package/src/plugins/markdown/definition.ts +6 -4
- package/src/plugins/markdown/index.ts +3 -2
- package/src/plugins/presentForm/Preview.vue +99 -0
- package/src/plugins/presentForm/View.vue +675 -0
- package/src/plugins/presentForm/definition.ts +127 -0
- package/src/plugins/presentForm/index.ts +18 -0
- package/src/plugins/presentForm/plugin.ts +94 -0
- package/src/plugins/presentForm/types.ts +109 -0
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentHtml/index.ts +3 -2
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +36 -26
- package/src/plugins/presentMulmoScript/index.ts +3 -2
- package/src/plugins/scheduler/AutomationsPreview.vue +37 -0
- package/src/plugins/scheduler/AutomationsView.vue +23 -0
- package/src/plugins/scheduler/CalendarView.vue +23 -0
- package/src/plugins/scheduler/LegacySchedulerView.vue +32 -0
- package/src/plugins/scheduler/Preview.vue +7 -4
- package/src/plugins/scheduler/TasksTab.vue +119 -28
- package/src/plugins/scheduler/View.vue +75 -32
- package/src/plugins/scheduler/automationsDefinition.ts +58 -0
- package/src/plugins/scheduler/calendarDefinition.ts +46 -0
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/scheduler/index.ts +68 -14
- package/src/plugins/scheduler/legacyShape.ts +34 -0
- package/src/plugins/spreadsheet/Preview.vue +9 -5
- package/src/plugins/spreadsheet/View.vue +43 -57
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +2 -1
- package/src/plugins/spreadsheet/index.ts +3 -2
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +42 -45
- package/src/plugins/textResponse/utils.ts +25 -0
- package/src/plugins/todo/Preview.vue +11 -6
- package/src/plugins/todo/View.vue +27 -13
- package/src/plugins/todo/composables/useTodos.ts +3 -1
- package/src/plugins/todo/index.ts +3 -2
- 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 +539 -92
- package/src/plugins/wiki/index.ts +5 -2
- package/src/plugins/wiki/route.ts +121 -0
- package/src/router/guards.ts +43 -24
- package/src/router/index.ts +53 -26
- package/src/router/pageRoutes.ts +23 -0
- package/src/tools/index.ts +12 -5
- package/src/tools/legacyPluginNames.ts +13 -0
- package/src/types/notification.ts +31 -6
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/eventDispatch.ts +3 -6
- package/src/utils/agent/formatElapsed.ts +37 -0
- package/src/utils/agent/request.ts +22 -1
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/canvas/sidePanelVisible.ts +19 -0
- package/src/utils/dom/scrollIntoViewByTestId.ts +38 -0
- package/src/utils/errors.ts +9 -2
- package/src/utils/files/filename.ts +24 -0
- package/src/utils/filesPreview/schedulerPreview.ts +9 -3
- package/src/utils/id.ts +18 -0
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/markdown/taskList.ts +175 -0
- package/src/utils/mcp/interpolateSpec.ts +97 -0
- package/src/utils/notification/dispatch.ts +51 -15
- package/src/utils/path/workspaceLinkRouter.ts +99 -0
- package/src/utils/session/mergeSessions.ts +5 -0
- package/src/utils/sources/filter.ts +69 -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/server/workspace/journal/linkRewrite.ts +0 -4
- package/src/components/ToolResultsPanel.vue +0 -77
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/plugins/scheduler/definition.ts +0 -57
- package/src/utils/canvas/viewMode.ts +0 -46
- package/src/utils/role/plugins.ts +0 -12
- package/src/utils/session/seedRoleDefault.ts +0 -35
- /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="spreadsheet-container">
|
|
3
3
|
<div v-if="loading" class="min-h-full p-8 flex items-center justify-center">
|
|
4
|
-
<div class="text-gray-500">
|
|
4
|
+
<div class="text-gray-500">{{ t("pluginSpreadsheet.loading") }}</div>
|
|
5
5
|
</div>
|
|
6
6
|
<div v-else-if="errorMessage" class="min-h-full p-8 flex items-center justify-center">
|
|
7
7
|
<div class="error">{{ errorMessage }}</div>
|
|
8
8
|
</div>
|
|
9
9
|
<div v-else-if="!resolvedSheets || resolvedSheets.length === 0" class="min-h-full p-8 flex items-center justify-center">
|
|
10
|
-
<div class="text-gray-500">
|
|
10
|
+
<div class="text-gray-500">{{ t("pluginSpreadsheet.noData") }}</div>
|
|
11
11
|
</div>
|
|
12
12
|
<template v-else>
|
|
13
13
|
<div class="spreadsheet-content-wrapper">
|
|
14
14
|
<div class="p-4">
|
|
15
15
|
<div class="header">
|
|
16
16
|
<h1 class="title">
|
|
17
|
-
{{ selectedResult.title || "
|
|
17
|
+
{{ selectedResult.title || t("pluginSpreadsheet.untitled") }}
|
|
18
18
|
</h1>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
<!-- `download-btn` class kept as a name hook — referenced by
|
|
20
|
+
e2e/tests/spreadsheet.spec.ts via `button.download-btn`.
|
|
21
|
+
Styling is inline Tailwind; the class has no CSS rules. -->
|
|
22
|
+
<button
|
|
23
|
+
class="download-btn h-8 px-2.5 flex items-center gap-1 rounded bg-[#217346] hover:bg-[#1e6a3f] active:bg-[#1a5c36] text-white text-sm transition-colors"
|
|
24
|
+
@click="downloadExcel"
|
|
25
|
+
>
|
|
26
|
+
<span class="material-icons text-base">download</span>
|
|
27
|
+
{{ t("pluginSpreadsheet.excel") }}
|
|
28
|
+
</button>
|
|
25
29
|
</div>
|
|
26
30
|
|
|
27
31
|
<!-- Sheet tabs (if multiple sheets) -->
|
|
@@ -43,9 +47,9 @@
|
|
|
43
47
|
|
|
44
48
|
<!-- Collapsible Editor -->
|
|
45
49
|
<details v-if="!miniEditorOpen" ref="editorDetails" class="spreadsheet-source">
|
|
46
|
-
<summary>
|
|
50
|
+
<summary>{{ t("pluginSpreadsheet.editData") }}</summary>
|
|
47
51
|
<textarea ref="editorTextarea" v-model="editableData" class="spreadsheet-editor" spellcheck="false" @input="handleDataEdit"></textarea>
|
|
48
|
-
<button class="apply-btn" :disabled="!hasChanges" @click="applyChanges">
|
|
52
|
+
<button class="apply-btn" :disabled="!hasChanges" @click="applyChanges">{{ t("pluginSpreadsheet.applyChanges") }}</button>
|
|
49
53
|
</details>
|
|
50
54
|
|
|
51
55
|
<!-- Mini Editor at Bottom -->
|
|
@@ -57,11 +61,11 @@
|
|
|
57
61
|
<div class="radio-group">
|
|
58
62
|
<label class="radio-option">
|
|
59
63
|
<input v-model="miniEditorType" type="radio" value="string" />
|
|
60
|
-
|
|
64
|
+
{{ t("pluginSpreadsheet.stringType") }}
|
|
61
65
|
</label>
|
|
62
66
|
<label class="radio-option">
|
|
63
67
|
<input v-model="miniEditorType" type="radio" value="object" />
|
|
64
|
-
|
|
68
|
+
{{ t("pluginSpreadsheet.formulaType") }}
|
|
65
69
|
</label>
|
|
66
70
|
</div>
|
|
67
71
|
|
|
@@ -71,7 +75,7 @@
|
|
|
71
75
|
v-model="miniEditorValue"
|
|
72
76
|
type="text"
|
|
73
77
|
class="form-input"
|
|
74
|
-
placeholder="
|
|
78
|
+
:placeholder="t('pluginSpreadsheet.valuePlaceholder')"
|
|
75
79
|
@keyup.enter="saveMiniEditor"
|
|
76
80
|
/>
|
|
77
81
|
|
|
@@ -81,13 +85,19 @@
|
|
|
81
85
|
v-model="miniEditorFormula"
|
|
82
86
|
type="text"
|
|
83
87
|
class="form-input"
|
|
84
|
-
placeholder="
|
|
88
|
+
:placeholder="t('pluginSpreadsheet.valueOrFormulaPlaceholder')"
|
|
89
|
+
@keyup.enter="saveMiniEditor"
|
|
90
|
+
/>
|
|
91
|
+
<input
|
|
92
|
+
v-model="miniEditorFormat"
|
|
93
|
+
type="text"
|
|
94
|
+
class="form-input"
|
|
95
|
+
:placeholder="t('pluginSpreadsheet.formatPlaceholder')"
|
|
85
96
|
@keyup.enter="saveMiniEditor"
|
|
86
97
|
/>
|
|
87
|
-
<input v-model="miniEditorFormat" type="text" class="form-input" placeholder="Format (e.g., $#,##0.00)" @keyup.enter="saveMiniEditor" />
|
|
88
98
|
</template>
|
|
89
99
|
|
|
90
|
-
<button class="save-btn" @click="saveMiniEditor">
|
|
100
|
+
<button class="save-btn" @click="saveMiniEditor">{{ t("pluginSpreadsheet.update") }}</button>
|
|
91
101
|
<button class="cancel-btn" @click="closeMiniEditor">✕</button>
|
|
92
102
|
</div>
|
|
93
103
|
</div>
|
|
@@ -97,6 +107,7 @@
|
|
|
97
107
|
|
|
98
108
|
<script setup lang="ts">
|
|
99
109
|
import { computed, ref, watch, onMounted, onUnmounted } from "vue";
|
|
110
|
+
import { useI18n } from "vue-i18n";
|
|
100
111
|
import * as XLSX from "xlsx";
|
|
101
112
|
import type { ToolResult } from "gui-chat-protocol";
|
|
102
113
|
import type { SpreadsheetToolData, SpreadsheetSheet } from "./definition";
|
|
@@ -111,6 +122,7 @@ import {
|
|
|
111
122
|
type CellValue,
|
|
112
123
|
} from "./engine";
|
|
113
124
|
import { applyCellHighlights, clearCellHighlights } from "./cellHighlights";
|
|
125
|
+
import { errorMessage as formatErrorMessage } from "../../utils/errors";
|
|
114
126
|
|
|
115
127
|
// Import all spreadsheet functions to populate the function registry
|
|
116
128
|
import "./engine/functions";
|
|
@@ -119,6 +131,8 @@ import { API_ROUTES } from "../../config/apiRoutes";
|
|
|
119
131
|
import type { FilesContentResponseLike } from "./engine/responseDecoder";
|
|
120
132
|
import { isObj, isRecord } from "../../utils/types";
|
|
121
133
|
|
|
134
|
+
const { t } = useI18n();
|
|
135
|
+
|
|
122
136
|
/**
|
|
123
137
|
* Normalize malformed data structures
|
|
124
138
|
* Some models generate flat arrays instead of 2D arrays - fix them
|
|
@@ -181,8 +195,11 @@ const loading = ref(false);
|
|
|
181
195
|
const errorMessage = ref("");
|
|
182
196
|
const resolvedSheets = ref<SpreadsheetSheet[]>([]);
|
|
183
197
|
|
|
198
|
+
// Accepts only the canonical prefix. Any legacy `spreadsheets/*.json`
|
|
199
|
+
// references in old session JSONL must be migrated via
|
|
200
|
+
// `scripts/migrate-legacy-artifact-paths.ts` (#773).
|
|
184
201
|
function isFilePath(value: unknown): value is string {
|
|
185
|
-
return typeof value === "string" &&
|
|
202
|
+
return typeof value === "string" && value.startsWith("artifacts/spreadsheets/") && value.endsWith(".json");
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
async function fetchSheets(): Promise<void> {
|
|
@@ -203,7 +220,7 @@ async function fetchSheets(): Promise<void> {
|
|
|
203
220
|
loading.value = true;
|
|
204
221
|
const response = await apiGet<FilesContentResponseLike>(API_ROUTES.files.content, { path: raw });
|
|
205
222
|
if (!response.ok) {
|
|
206
|
-
errorMessage.value =
|
|
223
|
+
errorMessage.value = t("pluginSpreadsheet.loadFailed", { error: response.error });
|
|
207
224
|
resolvedSheets.value = [];
|
|
208
225
|
loading.value = false;
|
|
209
226
|
return;
|
|
@@ -230,8 +247,11 @@ fetchSheets().then(() => {
|
|
|
230
247
|
async function persistSheets(sheets: SpreadsheetSheet[]): Promise<void> {
|
|
231
248
|
const raw = props.selectedResult.data?.sheets;
|
|
232
249
|
if (isFilePath(raw)) {
|
|
233
|
-
|
|
234
|
-
|
|
250
|
+
// Send the full workspace-relative path so the route doesn't have
|
|
251
|
+
// to reconstruct one from a basename — paths under
|
|
252
|
+
// `artifacts/spreadsheets/` are now sharded by YYYY/MM (#764).
|
|
253
|
+
const result = await apiPut<unknown>(API_ROUTES.plugins.updateSpreadsheet, {
|
|
254
|
+
relativePath: raw,
|
|
235
255
|
sheets,
|
|
236
256
|
});
|
|
237
257
|
if (!result.ok) {
|
|
@@ -551,7 +571,7 @@ async function applyChanges() {
|
|
|
551
571
|
|
|
552
572
|
// Validate it's an array
|
|
553
573
|
if (!Array.isArray(parsedSheets)) {
|
|
554
|
-
throw new Error("
|
|
574
|
+
throw new Error(t("pluginSpreadsheet.dataMustBeArray"));
|
|
555
575
|
}
|
|
556
576
|
|
|
557
577
|
// Persist to disk (if file-backed) and emit update
|
|
@@ -560,7 +580,7 @@ async function applyChanges() {
|
|
|
560
580
|
// Reset to first sheet after update
|
|
561
581
|
activeSheetIndex.value = 0;
|
|
562
582
|
} catch (error) {
|
|
563
|
-
alert(
|
|
583
|
+
alert(t("pluginSpreadsheet.invalidJsonAlert", { error: formatErrorMessage(error, t("pluginSpreadsheet.unknownError")) }));
|
|
564
584
|
}
|
|
565
585
|
}
|
|
566
586
|
|
|
@@ -694,40 +714,6 @@ onUnmounted(() => {
|
|
|
694
714
|
font-weight: bold;
|
|
695
715
|
}
|
|
696
716
|
|
|
697
|
-
.button-group {
|
|
698
|
-
display: flex;
|
|
699
|
-
gap: 0.5em;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
.download-btn {
|
|
703
|
-
padding: 0.5em 1em;
|
|
704
|
-
color: white;
|
|
705
|
-
border: none;
|
|
706
|
-
border-radius: 4px;
|
|
707
|
-
cursor: pointer;
|
|
708
|
-
font-size: 0.9em;
|
|
709
|
-
display: flex;
|
|
710
|
-
align-items: center;
|
|
711
|
-
gap: 0.5em;
|
|
712
|
-
transition: background-color 0.2s;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
.excel-btn {
|
|
716
|
-
background-color: #217346;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
.excel-btn:hover {
|
|
720
|
-
background-color: #1e6a3f;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
.excel-btn:active {
|
|
724
|
-
background-color: #1a5c36;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
.download-btn .material-icons {
|
|
728
|
-
font-size: 1.2em;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
717
|
/* Sheet tabs */
|
|
732
718
|
.sheet-tabs {
|
|
733
719
|
display: flex;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { SheetData } from "./types.js";
|
|
14
|
+
import { errorMessage } from "../../../utils/errors";
|
|
14
15
|
|
|
15
16
|
/** Shape of the `/api/files/content` response we care about. The
|
|
16
17
|
* server returns more fields (kind, size, modifiedMs, …) but this
|
|
@@ -51,7 +52,7 @@ export function decodeSpreadsheetResponse(body: FilesContentResponseLike): Decod
|
|
|
51
52
|
} catch (err) {
|
|
52
53
|
return {
|
|
53
54
|
kind: "error",
|
|
54
|
-
message: `Spreadsheet JSON is malformed: ${err
|
|
55
|
+
message: `Spreadsheet JSON is malformed: ${errorMessage(err, "parse error")}`,
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
if (!Array.isArray(parsed)) {
|
|
@@ -6,6 +6,7 @@ import View from "./View.vue";
|
|
|
6
6
|
import Preview from "./Preview.vue";
|
|
7
7
|
import { apiPost } from "../../utils/api";
|
|
8
8
|
import { API_ROUTES } from "../../config/apiRoutes";
|
|
9
|
+
import { makeUuid } from "../../utils/id";
|
|
9
10
|
|
|
10
11
|
const spreadsheetPlugin: ToolPlugin<SpreadsheetToolData> = {
|
|
11
12
|
toolDefinition,
|
|
@@ -15,14 +16,14 @@ const spreadsheetPlugin: ToolPlugin<SpreadsheetToolData> = {
|
|
|
15
16
|
if (!result.ok) {
|
|
16
17
|
return {
|
|
17
18
|
toolName: TOOL_NAME,
|
|
18
|
-
uuid:
|
|
19
|
+
uuid: makeUuid(),
|
|
19
20
|
message: result.error,
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
23
|
return {
|
|
23
24
|
...result.data,
|
|
24
25
|
toolName: TOOL_NAME,
|
|
25
|
-
uuid:
|
|
26
|
+
uuid: makeUuid(),
|
|
26
27
|
};
|
|
27
28
|
},
|
|
28
29
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="text-sm leading-snug" :class="textColorClass">
|
|
3
|
-
<div class="preview-markdown" v-html="renderedMarkdown" />
|
|
4
|
-
</div>
|
|
2
|
+
<div class="preview-text p-2 text-sm leading-snug" :class="textColorClass">{{ previewText }}</div>
|
|
5
3
|
</template>
|
|
6
4
|
|
|
7
5
|
<script setup lang="ts">
|
|
@@ -14,7 +12,6 @@ const props = defineProps<{
|
|
|
14
12
|
result: ToolResultComplete<TextResponseData>;
|
|
15
13
|
}>();
|
|
16
14
|
|
|
17
|
-
const previewText = computed(() => props.result.data?.text ?? "");
|
|
18
15
|
const messageRole = computed(() => props.result.data?.role ?? "assistant");
|
|
19
16
|
|
|
20
17
|
const textColorClass = computed(() => {
|
|
@@ -28,67 +25,27 @@ const textColorClass = computed(() => {
|
|
|
28
25
|
}
|
|
29
26
|
});
|
|
30
27
|
|
|
31
|
-
const
|
|
28
|
+
const previewText = computed(() => markdownToPlainText(props.result.data?.text ?? ""));
|
|
29
|
+
|
|
30
|
+
function markdownToPlainText(markdown: string): string {
|
|
31
|
+
const html = marked(markdown, { breaks: true, gfm: true }) as string;
|
|
32
|
+
const spaced = html
|
|
33
|
+
.replace(/<\/(td|th)>/gi, " ")
|
|
34
|
+
.replace(/<\/(p|h[1-6]|li|tr|blockquote|pre|div)>/gi, "$&\n")
|
|
35
|
+
.replace(/<br\s*\/?>/gi, "\n");
|
|
36
|
+
const doc = new DOMParser().parseFromString(spaced, "text/html");
|
|
37
|
+
const text = doc.body.textContent ?? "";
|
|
38
|
+
return text.replace(/\n{3,}/g, "\n\n").trim();
|
|
39
|
+
}
|
|
32
40
|
</script>
|
|
33
41
|
|
|
34
42
|
<style scoped>
|
|
35
|
-
.preview-
|
|
43
|
+
.preview-text {
|
|
36
44
|
overflow: hidden;
|
|
37
45
|
display: -webkit-box;
|
|
38
46
|
-webkit-line-clamp: 5;
|
|
39
47
|
-webkit-box-orient: vertical;
|
|
40
|
-
/* Links inside v-html would otherwise hijack the row-level
|
|
41
|
-
select click and navigate to the article URL. */
|
|
42
|
-
pointer-events: none;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.preview-markdown :deep(p) {
|
|
46
|
-
margin: 0;
|
|
47
|
-
display: inline;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.preview-markdown :deep(h1),
|
|
51
|
-
.preview-markdown :deep(h2),
|
|
52
|
-
.preview-markdown :deep(h3),
|
|
53
|
-
.preview-markdown :deep(h4),
|
|
54
|
-
.preview-markdown :deep(h5),
|
|
55
|
-
.preview-markdown :deep(h6) {
|
|
56
|
-
font-size: inherit;
|
|
57
|
-
font-weight: bold;
|
|
58
|
-
display: inline;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.preview-markdown :deep(ul),
|
|
62
|
-
.preview-markdown :deep(ol) {
|
|
63
|
-
display: inline;
|
|
64
|
-
margin: 0;
|
|
65
|
-
padding: 0;
|
|
66
|
-
list-style: none;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.preview-markdown :deep(li) {
|
|
70
|
-
display: inline;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
.preview-markdown :deep(li)::before {
|
|
74
|
-
content: "• ";
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.preview-markdown :deep(strong) {
|
|
78
|
-
font-weight: bold;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.preview-markdown :deep(em) {
|
|
82
|
-
font-style: italic;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.preview-markdown :deep(code) {
|
|
86
|
-
font-family: monospace;
|
|
87
|
-
font-size: 0.9em;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.preview-markdown :deep(pre) {
|
|
91
|
-
display: inline;
|
|
92
48
|
white-space: pre-wrap;
|
|
49
|
+
word-break: break-word;
|
|
93
50
|
}
|
|
94
51
|
</style>
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="h-full flex flex-col">
|
|
3
|
-
<div v-if="isAssistant" class="flex justify-end px-
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
<div v-if="isAssistant" class="flex items-center justify-end gap-2 px-3 py-2 border-b border-gray-100 shrink-0">
|
|
4
|
+
<button
|
|
5
|
+
class="h-8 px-2.5 flex items-center gap-1 rounded bg-green-600 hover:bg-green-700 text-white text-sm disabled:opacity-60 disabled:cursor-not-allowed transition-colors"
|
|
6
|
+
:disabled="pdfDownloading"
|
|
7
|
+
@click="downloadPdf"
|
|
8
|
+
>
|
|
9
|
+
<span class="material-icons text-base">{{ pdfDownloading ? "hourglass_empty" : "download" }}</span>
|
|
10
|
+
{{ t("pluginTextResponse.pdf") }}
|
|
11
|
+
</button>
|
|
12
|
+
<span v-if="pdfError" class="text-xs text-red-500" :title="pdfError">{{ t("pluginTextResponse.pdfFailed") }}</span>
|
|
11
13
|
</div>
|
|
12
14
|
<div class="flex-1 overflow-hidden relative" @click.capture="openLinksInNewTab">
|
|
13
15
|
<div class="text-response-container">
|
|
@@ -27,27 +29,37 @@
|
|
|
27
29
|
|
|
28
30
|
<!-- Collapsible Editor -->
|
|
29
31
|
<details v-if="editable" ref="detailsEl" class="text-response-source" data-testid="text-response-edit">
|
|
30
|
-
<summary data-testid="text-response-edit-summary">
|
|
32
|
+
<summary data-testid="text-response-edit-summary">{{ t("pluginTextResponse.editContent") }}</summary>
|
|
31
33
|
<textarea v-model="editedText" class="text-response-editor" spellcheck="false" data-testid="text-response-edit-textarea"></textarea>
|
|
32
|
-
<button class="apply-btn" :disabled="!hasChanges" data-testid="text-response-apply-btn" @click="applyChanges">
|
|
34
|
+
<button class="apply-btn" :disabled="!hasChanges" data-testid="text-response-apply-btn" @click="applyChanges">
|
|
35
|
+
{{ t("pluginTextResponse.applyChanges") }}
|
|
36
|
+
</button>
|
|
33
37
|
</details>
|
|
34
38
|
</div>
|
|
35
|
-
<button v-show="!editing" class="copy-btn" :title="copied ? '
|
|
39
|
+
<button v-show="!editing" class="copy-btn" :title="copied ? t('pluginTextResponse.copiedLabel') : t('pluginTextResponse.copyLabel')" @click="copyText">
|
|
36
40
|
<span class="material-icons">{{ copied ? "check" : "content_copy" }}</span>
|
|
37
41
|
</button>
|
|
38
|
-
<button v-show="editing" class="cancel-btn" @click="cancelEdit">
|
|
42
|
+
<button v-show="editing" class="cancel-btn" @click="cancelEdit">{{ t("pluginTextResponse.cancel") }}</button>
|
|
39
43
|
</div>
|
|
40
44
|
</div>
|
|
41
45
|
</template>
|
|
42
46
|
|
|
43
47
|
<script setup lang="ts">
|
|
44
48
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
|
|
49
|
+
import { useI18n } from "vue-i18n";
|
|
45
50
|
import { marked } from "marked";
|
|
46
51
|
import type { ToolResult, ToolResultComplete } from "gui-chat-protocol/vue";
|
|
47
52
|
import type { TextResponseData } from "./types";
|
|
48
53
|
import { handleExternalLinkClick } from "../../utils/dom/externalLink";
|
|
54
|
+
import { classifyWorkspacePath } from "../../utils/path/workspaceLinkRouter";
|
|
55
|
+
import { useAppApi } from "../../composables/useAppApi";
|
|
49
56
|
import { usePdfDownload } from "../../composables/usePdfDownload";
|
|
50
57
|
import { useClipboardCopy } from "../../composables/useClipboardCopy";
|
|
58
|
+
import { buildPdfFilename } from "../../utils/files/filename";
|
|
59
|
+
import { extractTextResponseTitle } from "./utils";
|
|
60
|
+
|
|
61
|
+
const { t } = useI18n();
|
|
62
|
+
const appApi = useAppApi();
|
|
51
63
|
|
|
52
64
|
const props = withDefaults(
|
|
53
65
|
defineProps<{
|
|
@@ -160,7 +172,19 @@ function applyChanges() {
|
|
|
160
172
|
const isAssistant = computed(() => (props.selectedResult.data?.role ?? "assistant") === "assistant");
|
|
161
173
|
|
|
162
174
|
function openLinksInNewTab(event: MouseEvent): void {
|
|
163
|
-
handleExternalLinkClick(event);
|
|
175
|
+
if (handleExternalLinkClick(event)) return;
|
|
176
|
+
// Internal workspace-path links (rendered by marked from agent
|
|
177
|
+
// Markdown): route to the appropriate view instead of letting them
|
|
178
|
+
// navigate the SPA to a non-existent session route.
|
|
179
|
+
const target = event.target as HTMLElement;
|
|
180
|
+
const anchor = target.closest("a");
|
|
181
|
+
if (!anchor) return;
|
|
182
|
+
const href = anchor.getAttribute("href");
|
|
183
|
+
if (!href || href.startsWith("#")) return;
|
|
184
|
+
if (classifyWorkspacePath(href)) {
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
appApi.navigateToWorkspacePath(href);
|
|
187
|
+
}
|
|
164
188
|
}
|
|
165
189
|
|
|
166
190
|
const { pdfDownloading, pdfError, downloadPdf: rawDownloadPdf } = usePdfDownload();
|
|
@@ -196,7 +220,11 @@ async function copyText() {
|
|
|
196
220
|
|
|
197
221
|
async function downloadPdf() {
|
|
198
222
|
const text = props.selectedResult.data?.text ?? "";
|
|
199
|
-
const filename =
|
|
223
|
+
const filename = buildPdfFilename({
|
|
224
|
+
name: extractTextResponseTitle(text),
|
|
225
|
+
fallback: "chat",
|
|
226
|
+
timestampMs: appApi.getResultTimestamp(props.selectedResult.uuid),
|
|
227
|
+
});
|
|
200
228
|
await rawDownloadPdf(text, filename);
|
|
201
229
|
}
|
|
202
230
|
</script>
|
|
@@ -431,37 +459,6 @@ async function downloadPdf() {
|
|
|
431
459
|
background: #cccccc;
|
|
432
460
|
}
|
|
433
461
|
|
|
434
|
-
/* Toolbar button styles */
|
|
435
|
-
.button-group {
|
|
436
|
-
display: flex;
|
|
437
|
-
gap: 0.5em;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
.download-btn {
|
|
441
|
-
padding: 0.5em 1em;
|
|
442
|
-
color: white;
|
|
443
|
-
border: none;
|
|
444
|
-
border-radius: 4px;
|
|
445
|
-
cursor: pointer;
|
|
446
|
-
font-size: 0.9em;
|
|
447
|
-
display: flex;
|
|
448
|
-
align-items: center;
|
|
449
|
-
gap: 0.5em;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
.download-btn-green {
|
|
453
|
-
background-color: #4caf50;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
.download-btn .material-icons {
|
|
457
|
-
font-size: 1.2em;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
.download-btn:disabled {
|
|
461
|
-
opacity: 0.6;
|
|
462
|
-
cursor: not-allowed;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
462
|
.copy-btn {
|
|
466
463
|
position: absolute;
|
|
467
464
|
bottom: 0.3rem;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Plugin-specific helpers for textResponse. Kept separate from
|
|
2
|
+
// View.vue so the pure logic is easy to unit-test from node:test
|
|
3
|
+
// without needing a Vue runtime.
|
|
4
|
+
|
|
5
|
+
const MAX_TITLE_CHARS = 50;
|
|
6
|
+
|
|
7
|
+
// Pull a short, human-meaningful title out of a chat reply for use as
|
|
8
|
+
// a download filename. Priority:
|
|
9
|
+
// 1. First markdown H1 ("# ...") — the model often opens a long
|
|
10
|
+
// reply with a heading; that's the cleanest signal.
|
|
11
|
+
// 2. First non-empty line, truncated.
|
|
12
|
+
// 3. Empty string when neither is available — caller decides the
|
|
13
|
+
// fallback (the PDF filename builder uses "chat").
|
|
14
|
+
export function extractTextResponseTitle(text: string): string {
|
|
15
|
+
let firstNonEmpty: string | null = null;
|
|
16
|
+
for (const line of text.split("\n")) {
|
|
17
|
+
const trimmed = line.trim();
|
|
18
|
+
if (!trimmed) continue;
|
|
19
|
+
if (trimmed.startsWith("# ")) {
|
|
20
|
+
return trimmed.slice(2).trim().slice(0, MAX_TITLE_CHARS);
|
|
21
|
+
}
|
|
22
|
+
if (firstNonEmpty === null) firstNonEmpty = trimmed;
|
|
23
|
+
}
|
|
24
|
+
return (firstNonEmpty ?? "").slice(0, MAX_TITLE_CHARS);
|
|
25
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="text-sm">
|
|
2
|
+
<div class="p-2 text-sm">
|
|
3
3
|
<div class="flex items-center gap-1 font-medium text-gray-700 mb-1">
|
|
4
|
-
<span
|
|
5
|
-
<span>{{
|
|
4
|
+
<span aria-hidden="true">{{ t("todoPreview.headerIcon") }}</span>
|
|
5
|
+
<span>{{ t("todoPreview.completedRatio", { done: completedCount, total: items.length }) }}</span>
|
|
6
6
|
</div>
|
|
7
7
|
<div
|
|
8
8
|
v-for="item in preview"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
class="text-xs truncate flex items-center gap-1"
|
|
11
11
|
:class="item.completed ? 'line-through text-gray-400' : 'text-gray-600'"
|
|
12
12
|
>
|
|
13
|
-
<span class="shrink-0">{{ item.completed ? "
|
|
13
|
+
<span class="shrink-0">{{ item.completed ? t("todoPreview.doneIcon") : t("todoPreview.pendingIcon") }}</span>
|
|
14
14
|
<span class="truncate">{{ item.text }}</span>
|
|
15
15
|
<template v-if="(item.labels?.length ?? 0) > 0">
|
|
16
16
|
<span
|
|
@@ -20,16 +20,21 @@
|
|
|
20
20
|
:class="colorForLabel(label)"
|
|
21
21
|
>{{ label }}</span
|
|
22
22
|
>
|
|
23
|
-
<span v-if="(item.labels?.length ?? 0) > 2" class="text-[9px] text-gray-400 shrink-0"
|
|
23
|
+
<span v-if="(item.labels?.length ?? 0) > 2" class="text-[9px] text-gray-400 shrink-0">{{
|
|
24
|
+
t("todoPreview.moreLabels", { count: (item.labels?.length ?? 0) - 2 })
|
|
25
|
+
}}</span>
|
|
24
26
|
</template>
|
|
25
27
|
</div>
|
|
26
|
-
<div v-if="more > 0" class="text-xs text-gray-400"
|
|
28
|
+
<div v-if="more > 0" class="text-xs text-gray-400">{{ t("todoPreview.moreItems", { count: more }) }}</div>
|
|
27
29
|
</div>
|
|
28
30
|
</template>
|
|
29
31
|
|
|
30
32
|
<script setup lang="ts">
|
|
31
33
|
import { computed, ref, watch } from "vue";
|
|
34
|
+
import { useI18n } from "vue-i18n";
|
|
32
35
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
36
|
+
|
|
37
|
+
const { t } = useI18n();
|
|
33
38
|
import type { TodoData, TodoItem } from "./index";
|
|
34
39
|
import { useFreshPluginData } from "../../composables/useFreshPluginData";
|
|
35
40
|
import { API_ROUTES } from "../../config/apiRoutes";
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
<!-- API error banner — surfaces POST /api/todos failures so a
|
|
4
4
|
silent add/remove/toggle becomes diagnosable. -->
|
|
5
5
|
<div v-if="todoApiError" class="px-4 py-2 bg-red-50 border-b border-red-200 text-sm text-red-700" role="alert" data-testid="todo-api-error">
|
|
6
|
-
|
|
6
|
+
{{ t("pluginTodo.apiError", { error: todoApiError }) }}
|
|
7
7
|
</div>
|
|
8
8
|
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-100">
|
|
9
|
-
<h2 class="text-lg font-semibold text-gray-800">
|
|
10
|
-
<span class="text-sm text-gray-500">{{
|
|
9
|
+
<h2 class="text-lg font-semibold text-gray-800">{{ t("pluginTodo.heading") }}</h2>
|
|
10
|
+
<span class="text-sm text-gray-500">{{ t("pluginTodo.completedRatio", { done: completedCount, total: items.length }) }}</span>
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
13
|
<!-- Filter bar: only shown when at least one label is in use. -->
|
|
14
14
|
<div v-if="labelInventory.length > 0" class="flex flex-wrap items-center gap-1.5 px-6 py-2 border-b border-gray-100 bg-gray-50">
|
|
15
|
-
<span class="text-xs text-gray-500 mr-1">
|
|
15
|
+
<span class="text-xs text-gray-500 mr-1">{{ t("pluginTodo.filter") }}</span>
|
|
16
16
|
<button
|
|
17
17
|
v-for="entry in labelInventory"
|
|
18
18
|
:key="entry.label"
|
|
@@ -27,14 +27,21 @@
|
|
|
27
27
|
{{ entry.label }}
|
|
28
28
|
<span class="opacity-60">{{ entry.count }}</span>
|
|
29
29
|
</button>
|
|
30
|
-
<button
|
|
31
|
-
|
|
30
|
+
<button
|
|
31
|
+
v-if="activeFilters.size > 0"
|
|
32
|
+
class="ml-auto text-xs text-gray-500 hover:text-gray-700"
|
|
33
|
+
:title="t('pluginTodo.clearFilters')"
|
|
34
|
+
@click="clearFilters"
|
|
35
|
+
>
|
|
36
|
+
{{ t("pluginTodo.clearButton") }}
|
|
32
37
|
</button>
|
|
33
38
|
</div>
|
|
34
39
|
|
|
35
|
-
<div v-if="items.length === 0" class="flex-1 flex items-center justify-center text-gray-400">
|
|
40
|
+
<div v-if="items.length === 0" class="flex-1 flex items-center justify-center text-gray-400">{{ t("pluginTodo.noItems") }}</div>
|
|
36
41
|
|
|
37
|
-
<div v-else-if="filteredItems.length === 0" class="flex-1 flex items-center justify-center text-gray-400 text-sm">
|
|
42
|
+
<div v-else-if="filteredItems.length === 0" class="flex-1 flex items-center justify-center text-gray-400 text-sm">
|
|
43
|
+
{{ t("pluginTodo.noMatchingFilter") }}
|
|
44
|
+
</div>
|
|
38
45
|
|
|
39
46
|
<ul v-else class="flex-1 overflow-y-auto p-4 space-y-2">
|
|
40
47
|
<li v-for="item in filteredItems" :key="item.id" class="rounded-lg border" :class="selectedId === item.id ? 'border-blue-400' : 'border-gray-200'">
|
|
@@ -62,10 +69,10 @@
|
|
|
62
69
|
</div>
|
|
63
70
|
<button
|
|
64
71
|
class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 text-xs px-1 shrink-0"
|
|
65
|
-
title="
|
|
72
|
+
:title="t('pluginTodo.deleteItem')"
|
|
66
73
|
@click.stop="remove(item)"
|
|
67
74
|
>
|
|
68
|
-
|
|
75
|
+
{{ t("pluginTodo.deleteSymbol") }}
|
|
69
76
|
</button>
|
|
70
77
|
<span class="material-icons text-gray-400 text-sm" :title="selectedId === item.id ? 'Collapse' : 'Expand'">
|
|
71
78
|
{{ selectedId === item.id ? "expand_less" : "expand_more" }}
|
|
@@ -80,20 +87,25 @@
|
|
|
80
87
|
spellcheck="false"
|
|
81
88
|
/>
|
|
82
89
|
<div class="flex items-center gap-2">
|
|
83
|
-
<button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="applyItemEdit">
|
|
84
|
-
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="selectedId = null">
|
|
90
|
+
<button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="applyItemEdit">{{ t("pluginTodo.update") }}</button>
|
|
91
|
+
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="selectedId = null">
|
|
92
|
+
{{ t("common.cancel") }}
|
|
93
|
+
</button>
|
|
85
94
|
<span v-if="yamlError" class="text-xs text-red-500">{{ yamlError }}</span>
|
|
86
95
|
</div>
|
|
87
96
|
</div>
|
|
88
97
|
</li>
|
|
89
98
|
</ul>
|
|
90
99
|
|
|
91
|
-
<button v-if="hasCompleted" class="mx-6 mb-2 text-sm text-gray-500 hover:text-gray-700 self-start" @click="clearCompleted">
|
|
100
|
+
<button v-if="hasCompleted" class="mx-6 mb-2 text-sm text-gray-500 hover:text-gray-700 self-start" @click="clearCompleted">
|
|
101
|
+
{{ t("pluginTodo.clearCompleted") }}
|
|
102
|
+
</button>
|
|
92
103
|
</div>
|
|
93
104
|
</template>
|
|
94
105
|
|
|
95
106
|
<script setup lang="ts">
|
|
96
107
|
import { computed, ref, watch } from "vue";
|
|
108
|
+
import { useI18n } from "vue-i18n";
|
|
97
109
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
98
110
|
import type { TodoData, TodoItem } from "./index";
|
|
99
111
|
import { useFreshPluginData } from "../../composables/useFreshPluginData";
|
|
@@ -101,6 +113,8 @@ import { apiPost } from "../../utils/api";
|
|
|
101
113
|
import { API_ROUTES } from "../../config/apiRoutes";
|
|
102
114
|
import { colorForLabel, filterByLabels, listLabelsWithCount, subtractLabels } from "./labels";
|
|
103
115
|
|
|
116
|
+
const { t } = useI18n();
|
|
117
|
+
|
|
104
118
|
const props = defineProps<{
|
|
105
119
|
selectedResult: ToolResultComplete<TodoData>;
|
|
106
120
|
}>();
|