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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<SourcesManager mode="page" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { nextTick, onMounted, watch } from "vue";
|
|
7
|
+
import { useRoute } from "vue-router";
|
|
8
|
+
import SourcesManager from "./SourcesManager.vue";
|
|
9
|
+
import { scrollIntoViewByTestId } from "../utils/dom/scrollIntoViewByTestId";
|
|
10
|
+
|
|
11
|
+
// Permalink support (#762): arrivals on /sources/:slug scroll and
|
|
12
|
+
// flash the matching source row. SourcesManager fetches the list
|
|
13
|
+
// asynchronously and renders `source-row-<slug>` testids per row,
|
|
14
|
+
// so we retry a couple of times while the API settles before
|
|
15
|
+
// giving up — avoids a race on first mount when the fetch hasn't
|
|
16
|
+
// resolved yet.
|
|
17
|
+
const MAX_RETRIES = 10;
|
|
18
|
+
const RETRY_INTERVAL_MS = 150;
|
|
19
|
+
|
|
20
|
+
const route = useRoute();
|
|
21
|
+
|
|
22
|
+
async function focusUrlSlug(slug: string): Promise<void> {
|
|
23
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
24
|
+
await nextTick();
|
|
25
|
+
if (scrollIntoViewByTestId(`source-row-${slug}`)) return;
|
|
26
|
+
await new Promise((resolve) => window.setTimeout(resolve, RETRY_INTERVAL_MS));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
onMounted(() => {
|
|
31
|
+
const slug = route.params.slug;
|
|
32
|
+
if (typeof slug === "string" && slug) {
|
|
33
|
+
void focusUrlSlug(slug);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
watch(
|
|
38
|
+
() => route.params.slug,
|
|
39
|
+
(slug) => {
|
|
40
|
+
if (typeof slug === "string" && slug) {
|
|
41
|
+
void focusUrlSlug(slug);
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
</script>
|
|
@@ -1,26 +1,50 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
<div
|
|
4
|
-
|
|
5
|
-
v-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
<div class="h-full flex flex-col bg-gray-50">
|
|
3
|
+
<div class="shrink-0 flex items-center gap-1 text-xs text-gray-400 px-4 pt-3 pb-2" data-testid="stack-role-header">
|
|
4
|
+
<span v-if="sessionRoleIcon" class="material-icons text-xs leading-none">{{ sessionRoleIcon }}</span>
|
|
5
|
+
<span v-if="sessionRoleName">{{ sessionRoleName }}</span>
|
|
6
|
+
<div class="ml-auto flex items-center gap-1">
|
|
7
|
+
<button
|
|
8
|
+
type="button"
|
|
9
|
+
class="text-gray-400 hover:text-gray-700"
|
|
10
|
+
:class="{ 'text-blue-500': showRightSidebar }"
|
|
11
|
+
:title="t('sidebarHeader.toolCallHistory')"
|
|
12
|
+
:aria-label="t('sidebarHeader.toolCallHistory')"
|
|
13
|
+
:aria-pressed="showRightSidebar"
|
|
14
|
+
@click="emit('toggle-right-sidebar')"
|
|
15
|
+
>
|
|
16
|
+
<span class="material-icons text-lg" aria-hidden="true">build</span>
|
|
17
|
+
</button>
|
|
18
|
+
<CanvasViewToggle :model-value="layoutMode" @update:model-value="(mode) => emit('update:layoutMode', mode)" />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<!-- Empty state pulled out of the scroll container so `h-full` +
|
|
22
|
+
the container's `pb-4` padding can't combine into a stray
|
|
23
|
+
scrollbar. A sibling `flex-1` slot centers cleanly. -->
|
|
24
|
+
<div v-if="toolResults.length === 0" class="flex-1 flex items-center justify-center text-gray-400 text-sm" data-testid="stack-empty">
|
|
25
|
+
{{ t("common.noResultsYet") }}
|
|
26
|
+
</div>
|
|
27
|
+
<div v-else ref="containerRef" class="flex-1 min-h-0 overflow-y-auto px-4 pb-4 space-y-3" data-testid="stack-scroll">
|
|
28
|
+
<div
|
|
29
|
+
v-for="result in toolResults"
|
|
30
|
+
:key="result.uuid"
|
|
31
|
+
:ref="(element) => setItemRef(result.uuid, element as HTMLElement | null)"
|
|
32
|
+
class="bg-white rounded-lg border transition-colors"
|
|
33
|
+
:class="result.uuid === selectedResultUuid ? 'border-blue-400 ring-2 ring-blue-200' : 'border-gray-200'"
|
|
15
34
|
>
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
<button
|
|
36
|
+
class="w-full flex items-center gap-2 px-3 py-2 border-b border-gray-100 text-left hover:bg-gray-50"
|
|
37
|
+
:title="result.title || result.toolName"
|
|
38
|
+
@click="emit('select', result.uuid)"
|
|
39
|
+
>
|
|
40
|
+
<span class="material-icons text-sm text-gray-400">{{ iconFor(result.toolName) }}</span>
|
|
41
|
+
<span class="text-sm font-medium text-gray-800 truncate">{{ result.title || result.toolName }}</span>
|
|
42
|
+
<span v-if="resultTimestamps.get(result.uuid)" class="text-[10px] text-gray-400 shrink-0">{{
|
|
43
|
+
formatSmartTime(resultTimestamps.get(result.uuid)!)
|
|
44
|
+
}}</span>
|
|
45
|
+
<span class="font-mono text-xs text-gray-400 shrink-0">{{ result.toolName }}</span>
|
|
46
|
+
</button>
|
|
47
|
+
<!-- text-response: render the message as Markdown via the
|
|
24
48
|
underlying plugin View. The .stack-text-response class below
|
|
25
49
|
collapses the plugin's own card chrome (outer p-6, inner
|
|
26
50
|
rounded/border/shadow box, role header) so only the stack
|
|
@@ -31,38 +55,39 @@
|
|
|
31
55
|
"open external links in a new tab" click handler. Attach
|
|
32
56
|
the same handler here via @click.capture so cross-origin
|
|
33
57
|
links in assistant Markdown don't navigate the SPA away. -->
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
58
|
+
<div v-if="isTextResponse(result)" class="stack-text-response" @click.capture="handleExternalLinkClick">
|
|
59
|
+
<TextResponseOriginalView :selected-result="result" />
|
|
60
|
+
</div>
|
|
61
|
+
<!-- Document-like plugins: let the content flow at its natural
|
|
38
62
|
height by overriding the plugin's internal h-full / overflow
|
|
39
63
|
/ flex-1 via the .stack-natural scoped styles below. For
|
|
40
64
|
plugins that embed iframes (e.g. presentHtml) we also size
|
|
41
65
|
each iframe to its content after load. -->
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
<div
|
|
67
|
+
v-else-if="isStackNatural(result.toolName)"
|
|
68
|
+
:ref="(element) => setNaturalWrapperRef(result.uuid, element as HTMLElement | null)"
|
|
69
|
+
class="stack-natural"
|
|
70
|
+
>
|
|
71
|
+
<component
|
|
72
|
+
:is="getPlugin(result.toolName)?.viewComponent"
|
|
73
|
+
v-if="getPlugin(result.toolName)?.viewComponent"
|
|
74
|
+
:selected-result="result"
|
|
75
|
+
:send-text-message="sendTextMessage"
|
|
76
|
+
@update-result="(r: ToolResultComplete) => emit('updateResult', r)"
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
<!-- Other plugins: fixed height wrapper so plugins that rely on
|
|
56
80
|
h-full continue to render properly. -->
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
<div v-else :style="{ height: PLUGIN_HEIGHT }">
|
|
82
|
+
<component
|
|
83
|
+
:is="getPlugin(result.toolName)?.viewComponent"
|
|
84
|
+
v-if="getPlugin(result.toolName)?.viewComponent"
|
|
85
|
+
:selected-result="result"
|
|
86
|
+
:send-text-message="sendTextMessage"
|
|
87
|
+
@update-result="(r: ToolResultComplete) => emit('updateResult', r)"
|
|
88
|
+
/>
|
|
89
|
+
<pre v-else class="h-full overflow-auto p-4 text-xs text-gray-500 whitespace-pre-wrap">{{ JSON.stringify(result, null, 2) }}</pre>
|
|
90
|
+
</div>
|
|
66
91
|
</div>
|
|
67
92
|
</div>
|
|
68
93
|
</div>
|
|
@@ -70,13 +95,18 @@
|
|
|
70
95
|
|
|
71
96
|
<script setup lang="ts">
|
|
72
97
|
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from "vue";
|
|
98
|
+
import { useI18n } from "vue-i18n";
|
|
73
99
|
import { getPlugin } from "../tools";
|
|
100
|
+
|
|
101
|
+
const { t } = useI18n();
|
|
74
102
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
75
103
|
import { View as TextResponseOriginalView } from "../plugins/textResponse/index";
|
|
76
104
|
import { handleExternalLinkClick } from "../utils/dom/externalLink";
|
|
77
105
|
import type { TextResponseData } from "../plugins/textResponse/types";
|
|
78
106
|
import { formatSmartTime } from "../utils/format/date";
|
|
79
107
|
import { isRecord } from "../utils/types";
|
|
108
|
+
import CanvasViewToggle from "./CanvasViewToggle.vue";
|
|
109
|
+
import type { LayoutMode } from "../utils/canvas/layoutMode";
|
|
80
110
|
|
|
81
111
|
// Most plugin viewComponents use h-full internally, so a defined parent
|
|
82
112
|
// height is required for them to render. text-response and the
|
|
@@ -110,11 +140,17 @@ const props = defineProps<{
|
|
|
110
140
|
selectedResultUuid: string | null;
|
|
111
141
|
resultTimestamps: Map<string, number>;
|
|
112
142
|
sendTextMessage?: (text: string) => void;
|
|
143
|
+
sessionRoleName?: string;
|
|
144
|
+
sessionRoleIcon?: string;
|
|
145
|
+
layoutMode: LayoutMode;
|
|
146
|
+
showRightSidebar: boolean;
|
|
113
147
|
}>();
|
|
114
148
|
|
|
115
149
|
const emit = defineEmits<{
|
|
116
150
|
select: [uuid: string];
|
|
117
151
|
updateResult: [result: ToolResultComplete];
|
|
152
|
+
"update:layoutMode": [mode: LayoutMode];
|
|
153
|
+
"toggle-right-sidebar": [];
|
|
118
154
|
}>();
|
|
119
155
|
|
|
120
156
|
const containerRef = ref<HTMLDivElement | null>(null);
|
|
@@ -1,65 +1,54 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="queries.length > 0" class="border-t border-gray-200">
|
|
3
|
-
<div v-if="expanded" ref="listRef" class="px-4 pt-2 max-h-64 overflow-y-auto flex flex-col gap-1">
|
|
4
|
-
<button
|
|
5
|
-
v-for="query in queries"
|
|
6
|
-
:key="query"
|
|
7
|
-
class="text-left text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 rounded px-3 py-1.5 border border-gray-300 transition-colors"
|
|
8
|
-
@click="onClick($event, query)"
|
|
9
|
-
>
|
|
10
|
-
{{ query }}
|
|
11
|
-
</button>
|
|
12
|
-
<p class="text-center text-[10px] text-gray-400 py-0.5">click to send · shift+click to edit</p>
|
|
13
|
-
</div>
|
|
2
|
+
<div v-if="expanded && queries.length > 0" ref="listRef" class="border-t border-gray-200 px-4 pt-2 pb-2 max-h-64 overflow-y-auto flex flex-col gap-1">
|
|
14
3
|
<button
|
|
15
|
-
|
|
16
|
-
|
|
4
|
+
v-for="query in queries"
|
|
5
|
+
:key="query"
|
|
6
|
+
class="text-left text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 rounded px-3 py-1.5 border border-gray-300 transition-colors"
|
|
7
|
+
@click="onClick($event, query)"
|
|
17
8
|
>
|
|
18
|
-
|
|
19
|
-
<span class="material-icons text-sm">lightbulb</span>
|
|
20
|
-
Suggestions
|
|
21
|
-
</span>
|
|
22
|
-
<span class="material-icons text-sm transition-transform" :class="{ 'rotate-180': !expanded }">expand_less</span>
|
|
9
|
+
{{ query }}
|
|
23
10
|
</button>
|
|
11
|
+
<p class="text-center text-[10px] text-gray-400 py-0.5">{{ t("suggestionsPanel.sendEditHint") }}</p>
|
|
24
12
|
</div>
|
|
25
13
|
</template>
|
|
26
14
|
|
|
27
15
|
<script setup lang="ts">
|
|
28
16
|
import { nextTick, ref, watch } from "vue";
|
|
17
|
+
import { useI18n } from "vue-i18n";
|
|
29
18
|
|
|
30
|
-
|
|
19
|
+
const { t } = useI18n();
|
|
20
|
+
|
|
21
|
+
const props = defineProps<{
|
|
31
22
|
queries: string[];
|
|
23
|
+
expanded: boolean;
|
|
32
24
|
}>();
|
|
33
25
|
|
|
34
26
|
const emit = defineEmits<{
|
|
27
|
+
"update:expanded": [value: boolean];
|
|
35
28
|
send: [query: string];
|
|
36
29
|
edit: [query: string];
|
|
37
30
|
}>();
|
|
38
31
|
|
|
39
|
-
const expanded = ref(false);
|
|
40
32
|
const listRef = ref<HTMLDivElement | null>(null);
|
|
41
33
|
|
|
42
|
-
watch(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
34
|
+
watch(
|
|
35
|
+
() => props.expanded,
|
|
36
|
+
(isExpanded) => {
|
|
37
|
+
if (!isExpanded) return;
|
|
38
|
+
nextTick(() => {
|
|
39
|
+
if (listRef.value) {
|
|
40
|
+
listRef.value.scrollTop = listRef.value.scrollHeight;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
);
|
|
50
45
|
|
|
51
46
|
function onClick(event: MouseEvent, query: string): void {
|
|
52
|
-
expanded
|
|
47
|
+
emit("update:expanded", false);
|
|
53
48
|
if (event.shiftKey) {
|
|
54
49
|
emit("edit", query);
|
|
55
50
|
return;
|
|
56
51
|
}
|
|
57
52
|
emit("send", query);
|
|
58
53
|
}
|
|
59
|
-
|
|
60
|
-
function collapse(): void {
|
|
61
|
-
expanded.value = false;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
defineExpose({ collapse });
|
|
65
54
|
</script>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="m-4 mb-0 rounded border border-blue-200 bg-blue-50 text-sm" data-testid="system-file-banner">
|
|
3
|
+
<div class="flex items-start gap-2 px-3 py-2">
|
|
4
|
+
<span class="text-blue-600 shrink-0" aria-hidden="true">{{ INFO_ICON }}</span>
|
|
5
|
+
<div class="flex-1 min-w-0">
|
|
6
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
7
|
+
<span class="font-semibold text-blue-900">{{ t(`systemFiles.${descriptor.id}.title`) }}</span>
|
|
8
|
+
<span :class="['text-xs px-1.5 py-0.5 rounded shrink-0', policyChipClass]">
|
|
9
|
+
{{ t(`systemFiles.editPolicy.${descriptor.editPolicy}`) }}
|
|
10
|
+
</span>
|
|
11
|
+
</div>
|
|
12
|
+
<p v-if="!collapsed" class="mt-1 text-gray-700 leading-snug whitespace-pre-line">
|
|
13
|
+
{{ t(`systemFiles.${descriptor.id}.summary`) }}
|
|
14
|
+
</p>
|
|
15
|
+
<p v-if="!collapsed && descriptor.schemaRef" class="mt-1 text-xs text-gray-500">
|
|
16
|
+
{{ t("systemFiles.schemaLabel") }}:
|
|
17
|
+
<a :href="schemaUrl" target="_blank" rel="noopener noreferrer" class="font-mono text-blue-700 hover:underline break-all">
|
|
18
|
+
{{ descriptor.schemaRef }}
|
|
19
|
+
</a>
|
|
20
|
+
</p>
|
|
21
|
+
</div>
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
class="shrink-0 text-xs text-blue-700 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 rounded px-1"
|
|
25
|
+
:aria-label="collapsed ? t('systemFiles.showDetails') : t('systemFiles.hideDetails')"
|
|
26
|
+
:data-testid="`system-file-banner-toggle`"
|
|
27
|
+
@click="toggle"
|
|
28
|
+
>
|
|
29
|
+
{{ collapsed ? t("systemFiles.showDetails") : t("systemFiles.hideDetails") }}
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup lang="ts">
|
|
36
|
+
import { computed, ref, watch } from "vue";
|
|
37
|
+
import { useI18n } from "vue-i18n";
|
|
38
|
+
import type { SystemFileDescriptor, EditPolicy } from "../config/systemFileDescriptors";
|
|
39
|
+
|
|
40
|
+
const { t } = useI18n();
|
|
41
|
+
|
|
42
|
+
const props = defineProps<{
|
|
43
|
+
descriptor: SystemFileDescriptor;
|
|
44
|
+
path: string;
|
|
45
|
+
}>();
|
|
46
|
+
|
|
47
|
+
// Per-path collapse state. Persisted to localStorage so a user who
|
|
48
|
+
// dismissed the banner once doesn't see it expanded again on reload.
|
|
49
|
+
// Key includes the descriptor id (not the path) so all roles share
|
|
50
|
+
// one collapse decision — we want "I read the roles banner, hide it
|
|
51
|
+
// for every role file" rather than asking per-file.
|
|
52
|
+
const STORAGE_PREFIX = "systemFileBanner.collapsed.";
|
|
53
|
+
|
|
54
|
+
const INFO_ICON = "ℹ️";
|
|
55
|
+
|
|
56
|
+
function storageKey(descriptorId: string): string {
|
|
57
|
+
return STORAGE_PREFIX + descriptorId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadCollapsed(descriptorId: string): boolean {
|
|
61
|
+
try {
|
|
62
|
+
return localStorage.getItem(storageKey(descriptorId)) === "1";
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function saveCollapsed(descriptorId: string, value: boolean): void {
|
|
69
|
+
try {
|
|
70
|
+
if (value) localStorage.setItem(storageKey(descriptorId), "1");
|
|
71
|
+
else localStorage.removeItem(storageKey(descriptorId));
|
|
72
|
+
} catch {
|
|
73
|
+
// localStorage may throw in private browsing / quota-exceeded;
|
|
74
|
+
// banner state degrades to in-memory only, no user-visible error.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const collapsed = ref(loadCollapsed(props.descriptor.id));
|
|
79
|
+
|
|
80
|
+
// Re-load when the descriptor changes (different file selected).
|
|
81
|
+
watch(
|
|
82
|
+
() => props.descriptor.id,
|
|
83
|
+
(descriptorId) => {
|
|
84
|
+
collapsed.value = loadCollapsed(descriptorId);
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
function toggle(): void {
|
|
89
|
+
collapsed.value = !collapsed.value;
|
|
90
|
+
saveCollapsed(props.descriptor.id, collapsed.value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const POLICY_CHIP_CLASSES: Record<EditPolicy, string> = {
|
|
94
|
+
"agent-managed-but-hand-editable": "bg-emerald-100 text-emerald-800",
|
|
95
|
+
"user-editable": "bg-blue-100 text-blue-800",
|
|
96
|
+
"agent-managed": "bg-amber-100 text-amber-800",
|
|
97
|
+
"fragile-format": "bg-orange-100 text-orange-800",
|
|
98
|
+
ephemeral: "bg-gray-200 text-gray-700",
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const policyChipClass = computed(() => POLICY_CHIP_CLASSES[props.descriptor.editPolicy]);
|
|
102
|
+
|
|
103
|
+
const GITHUB_REPO_URL = "https://github.com/receptron/mulmoclaude/blob/main/";
|
|
104
|
+
|
|
105
|
+
const schemaUrl = computed(() => (props.descriptor.schemaRef ? GITHUB_REPO_URL + props.descriptor.schemaRef : ""));
|
|
106
|
+
</script>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="px-2 py-1 text-sm" role="status" aria-live="polite" data-testid="thinking-indicator">
|
|
3
|
+
<div class="flex items-center gap-2 text-gray-500">
|
|
4
|
+
<span class="text-xs">{{ statusMessage }}</span>
|
|
5
|
+
<span class="flex gap-1" aria-hidden="true">
|
|
6
|
+
<span class="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" style="animation-delay: 0ms" />
|
|
7
|
+
<span class="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" style="animation-delay: 150ms" />
|
|
8
|
+
<span class="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" style="animation-delay: 300ms" />
|
|
9
|
+
</span>
|
|
10
|
+
<span v-if="runElapsedMs !== null && runElapsedMs >= 1000" class="text-xs text-gray-400 tabular-nums" data-testid="run-elapsed">
|
|
11
|
+
· {{ formatElapsed(runElapsedMs) }}
|
|
12
|
+
</span>
|
|
13
|
+
</div>
|
|
14
|
+
<div v-if="pendingCalls && pendingCalls.length > 0" class="mt-1 space-y-0.5">
|
|
15
|
+
<div v-for="call in pendingCalls" :key="call.toolUseId" class="flex items-center gap-1.5 text-xs text-gray-400">
|
|
16
|
+
<span class="w-1.5 h-1.5 rounded-full bg-blue-300 shrink-0 animate-pulse" aria-hidden="true" />
|
|
17
|
+
<span class="font-mono truncate">{{ call.toolName }}</span>
|
|
18
|
+
<span class="text-xs text-gray-300 tabular-nums shrink-0">· {{ formatElapsed(call.elapsedMs) }}</span>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import { formatElapsed } from "../utils/agent/formatElapsed";
|
|
26
|
+
|
|
27
|
+
interface PendingCall {
|
|
28
|
+
toolUseId: string;
|
|
29
|
+
toolName: string;
|
|
30
|
+
elapsedMs: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
withDefaults(
|
|
34
|
+
defineProps<{
|
|
35
|
+
statusMessage: string;
|
|
36
|
+
runElapsedMs?: number | null;
|
|
37
|
+
pendingCalls?: PendingCall[];
|
|
38
|
+
}>(),
|
|
39
|
+
{ runElapsedMs: null, pendingCalls: () => [] },
|
|
40
|
+
);
|
|
41
|
+
</script>
|
|
@@ -1,36 +1,42 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="h-full bg-white flex flex-col">
|
|
3
3
|
<!-- Header -->
|
|
4
|
-
<div class="flex items-center justify-between px-
|
|
5
|
-
<div class="flex items-center gap-
|
|
6
|
-
<h2 class="text-base font-semibold text-gray-800 shrink-0">
|
|
7
|
-
<span class="text-xs text-gray-500 shrink-0">{{
|
|
4
|
+
<div class="flex items-center justify-between gap-2 px-3 py-2 border-b border-gray-100 shrink-0">
|
|
5
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
6
|
+
<h2 class="text-base font-semibold text-gray-800 shrink-0">{{ t("todoExplorer.heading") }}</h2>
|
|
7
|
+
<span class="text-xs text-gray-500 shrink-0">{{ t("todoExplorer.doneRatio", { done: completedCount, total: items.length }) }}</span>
|
|
8
8
|
<input
|
|
9
9
|
v-model="search"
|
|
10
10
|
data-testid="todo-search"
|
|
11
11
|
type="text"
|
|
12
|
-
placeholder="
|
|
13
|
-
class="px-2
|
|
12
|
+
:placeholder="t('todoExplorer.searchPlaceholder')"
|
|
13
|
+
class="h-8 px-2.5 text-sm border border-gray-200 rounded w-44 focus:outline-none focus:border-blue-400"
|
|
14
14
|
/>
|
|
15
15
|
</div>
|
|
16
16
|
<div class="flex items-center gap-2">
|
|
17
17
|
<!-- Add button -->
|
|
18
|
-
<button
|
|
18
|
+
<button
|
|
19
|
+
data-testid="todo-add-btn"
|
|
20
|
+
class="h-8 px-2.5 flex items-center gap-1 text-sm rounded bg-blue-500 text-white hover:bg-blue-600"
|
|
21
|
+
@click="addOpen = true"
|
|
22
|
+
>
|
|
23
|
+
{{ t("todoExplorer.addButton") }}
|
|
24
|
+
</button>
|
|
19
25
|
<!-- Add column button (kanban only) -->
|
|
20
26
|
<button
|
|
21
27
|
v-if="viewMode === TODO_VIEW.kanban"
|
|
22
28
|
data-testid="todo-column-add-btn"
|
|
23
|
-
class="px-2
|
|
29
|
+
class="h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50"
|
|
24
30
|
@click="addColumnOpen = true"
|
|
25
31
|
>
|
|
26
|
-
|
|
32
|
+
{{ t("todoExplorer.addColumnButton") }}
|
|
27
33
|
</button>
|
|
28
34
|
<!-- View mode toggle -->
|
|
29
|
-
<div class="flex border border-gray-300 rounded overflow-hidden
|
|
35
|
+
<div class="flex border border-gray-300 rounded overflow-hidden">
|
|
30
36
|
<button
|
|
31
37
|
v-for="mode in VIEW_MODES"
|
|
32
38
|
:key="mode.key"
|
|
33
|
-
class="
|
|
39
|
+
class="h-8 w-8 flex items-center justify-center"
|
|
34
40
|
:class="viewMode === mode.key ? 'bg-blue-500 text-white' : 'bg-white text-gray-600 hover:bg-gray-50'"
|
|
35
41
|
:data-testid="`todo-view-${mode.key}`"
|
|
36
42
|
:title="mode.label"
|
|
@@ -44,7 +50,7 @@
|
|
|
44
50
|
|
|
45
51
|
<!-- Label filter chips -->
|
|
46
52
|
<div v-if="labelInventory.length > 0" class="flex flex-wrap items-center gap-1.5 px-4 py-1.5 border-b border-gray-100 bg-gray-50 shrink-0">
|
|
47
|
-
<span class="text-[11px] text-gray-500 mr-1">
|
|
53
|
+
<span class="text-[11px] text-gray-500 mr-1">{{ t("todoExplorer.labels") }}</span>
|
|
48
54
|
<button
|
|
49
55
|
v-for="entry in labelInventory"
|
|
50
56
|
:key="entry.label"
|
|
@@ -59,8 +65,13 @@
|
|
|
59
65
|
{{ entry.label }}
|
|
60
66
|
<span class="opacity-60">{{ entry.count }}</span>
|
|
61
67
|
</button>
|
|
62
|
-
<button
|
|
63
|
-
|
|
68
|
+
<button
|
|
69
|
+
v-if="activeFilters.size > 0"
|
|
70
|
+
class="ml-auto text-[11px] text-gray-500 hover:text-gray-700"
|
|
71
|
+
:title="t('todoExplorer.clearFiltersTitle')"
|
|
72
|
+
@click="clearFilters"
|
|
73
|
+
>
|
|
74
|
+
{{ t("todoExplorer.clearButton") }}
|
|
64
75
|
</button>
|
|
65
76
|
</div>
|
|
66
77
|
|
|
@@ -71,7 +82,7 @@
|
|
|
71
82
|
|
|
72
83
|
<!-- Body -->
|
|
73
84
|
<div class="flex-1 min-h-0">
|
|
74
|
-
<div v-if="items.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">
|
|
85
|
+
<div v-if="items.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">{{ t("todoExplorer.emptyHint") }}</div>
|
|
75
86
|
<template v-else>
|
|
76
87
|
<TodoKanbanView
|
|
77
88
|
v-if="viewMode === TODO_VIEW.kanban"
|
|
@@ -122,20 +133,22 @@
|
|
|
122
133
|
<!-- Add column dialog -->
|
|
123
134
|
<div v-if="addColumnOpen" class="fixed inset-0 z-50 bg-black/30 flex items-center justify-center" @click="addColumnOpen = false">
|
|
124
135
|
<div class="bg-white rounded-lg shadow-xl w-80 p-5 space-y-3" role="dialog" aria-modal="true" aria-labelledby="todo-add-column-title" @click.stop>
|
|
125
|
-
<h3 id="todo-add-column-title" class="text-base font-semibold text-gray-800">
|
|
136
|
+
<h3 id="todo-add-column-title" class="text-base font-semibold text-gray-800">{{ t("todoExplorer.addColumn") }}</h3>
|
|
126
137
|
<label class="block text-xs text-gray-600">
|
|
127
|
-
|
|
138
|
+
{{ t("todoExplorer.newColumnLabelField") }}
|
|
128
139
|
<input
|
|
129
140
|
v-model="newColumnLabel"
|
|
130
141
|
type="text"
|
|
131
|
-
placeholder="
|
|
142
|
+
:placeholder="t('todoExplorer.newColumnPlaceholder')"
|
|
132
143
|
class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
|
|
133
144
|
@keydown.enter="commitNewColumn"
|
|
134
145
|
/>
|
|
135
146
|
</label>
|
|
136
147
|
<div class="flex justify-end gap-2 pt-1">
|
|
137
|
-
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="addColumnOpen = false">
|
|
138
|
-
|
|
148
|
+
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="addColumnOpen = false">
|
|
149
|
+
{{ t("common.cancel") }}
|
|
150
|
+
</button>
|
|
151
|
+
<button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="commitNewColumn">{{ t("common.add") }}</button>
|
|
139
152
|
</div>
|
|
140
153
|
</div>
|
|
141
154
|
</div>
|
|
@@ -143,7 +156,10 @@
|
|
|
143
156
|
</template>
|
|
144
157
|
|
|
145
158
|
<script setup lang="ts">
|
|
146
|
-
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
|
159
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
|
160
|
+
import { useI18n } from "vue-i18n";
|
|
161
|
+
import { useRoute } from "vue-router";
|
|
162
|
+
import { scrollIntoViewByTestId } from "../utils/dom/scrollIntoViewByTestId";
|
|
147
163
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
148
164
|
import type { TodoData, TodoItem } from "../plugins/todo/index";
|
|
149
165
|
import { colorForLabel, filterByLabels, listLabelsWithCount } from "../plugins/todo/labels";
|
|
@@ -153,9 +169,10 @@ import TodoTableView from "./todo/TodoTableView.vue";
|
|
|
153
169
|
import TodoListView from "./todo/TodoListView.vue";
|
|
154
170
|
import TodoAddDialog from "./todo/TodoAddDialog.vue";
|
|
155
171
|
import TodoEditDialog from "./todo/TodoEditDialog.vue";
|
|
156
|
-
|
|
157
172
|
import { TODO_VIEW, TODO_VIEW_MODES as VIEW_MODES, type TodoViewMode as ViewMode } from "../plugins/todo/viewModes";
|
|
158
173
|
|
|
174
|
+
const { t } = useI18n();
|
|
175
|
+
|
|
159
176
|
const VIEW_MODE_KEY = "todo_explorer_view_mode";
|
|
160
177
|
|
|
161
178
|
const props = defineProps<{
|
|
@@ -275,6 +292,39 @@ function onExplorerKeydown(event: KeyboardEvent): void {
|
|
|
275
292
|
onMounted(() => document.addEventListener("keydown", onExplorerKeydown));
|
|
276
293
|
onUnmounted(() => document.removeEventListener("keydown", onExplorerKeydown));
|
|
277
294
|
|
|
295
|
+
// Permalink support (#762): arrivals on /todos/:itemId scroll and
|
|
296
|
+
// flash the matching card. Safe to run unconditionally — when the
|
|
297
|
+
// explorer is embedded in FilesView the URL has no :itemId and
|
|
298
|
+
// scrollIntoViewByTestId is a no-op. Retry a handful of times to
|
|
299
|
+
// cover the window between mount and the first items fetch landing.
|
|
300
|
+
const TODO_FOCUS_MAX_RETRIES = 10;
|
|
301
|
+
const TODO_FOCUS_RETRY_MS = 150;
|
|
302
|
+
const route = useRoute();
|
|
303
|
+
|
|
304
|
+
async function focusUrlItem(itemId: string): Promise<void> {
|
|
305
|
+
for (let attempt = 0; attempt < TODO_FOCUS_MAX_RETRIES; attempt++) {
|
|
306
|
+
await nextTick();
|
|
307
|
+
if (scrollIntoViewByTestId(`todo-card-${itemId}`)) return;
|
|
308
|
+
await new Promise((resolve) => window.setTimeout(resolve, TODO_FOCUS_RETRY_MS));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
onMounted(() => {
|
|
313
|
+
const itemId = route.params.itemId;
|
|
314
|
+
if (typeof itemId === "string" && itemId) {
|
|
315
|
+
void focusUrlItem(itemId);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
watch(
|
|
320
|
+
() => route.params.itemId,
|
|
321
|
+
(itemId) => {
|
|
322
|
+
if (typeof itemId === "string" && itemId) {
|
|
323
|
+
void focusUrlItem(itemId);
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
|
|
278
328
|
// ── Item handlers ──────────────────────────────────────────────
|
|
279
329
|
|
|
280
330
|
function onPatchItem(itemId: string, input: PatchItemInput): void {
|