mulmoclaude 0.1.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/README.md +44 -0
- package/bin/mulmoclaude.js +202 -0
- package/bin/prepare-dist.js +93 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
- package/client/assets/index-D8rhwXLq.js +4906 -0
- package/client/assets/index-KNLBjwuh.css +1 -0
- package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
- package/client/assets/material-icons-Dr0goTwe.woff +0 -0
- package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
- package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
- package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
- package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
- package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
- package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
- package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
- package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
- package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
- package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
- package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
- package/client/index.html +28 -0
- package/package.json +66 -0
- package/server/agent/attachmentConverter.ts +270 -0
- package/server/agent/config.ts +414 -0
- package/server/agent/index.ts +260 -0
- package/server/agent/mcp-server.ts +412 -0
- package/server/agent/mcp-tools/index.ts +63 -0
- package/server/agent/mcp-tools/x.ts +188 -0
- package/server/agent/plugin-names.ts +75 -0
- package/server/agent/prompt.ts +349 -0
- package/server/agent/resumeFailover.ts +129 -0
- package/server/agent/sandboxMounts.ts +329 -0
- package/server/agent/stream.ts +194 -0
- package/server/api/auth/bearerAuth.ts +61 -0
- package/server/api/auth/token.ts +98 -0
- package/server/api/csrfGuard.ts +85 -0
- package/server/api/routes/agent.ts +478 -0
- package/server/api/routes/chart.ts +98 -0
- package/server/api/routes/chat-index.ts +46 -0
- package/server/api/routes/config.ts +258 -0
- package/server/api/routes/dispatchResponse.ts +79 -0
- package/server/api/routes/files.ts +812 -0
- package/server/api/routes/html.ts +101 -0
- package/server/api/routes/image.ts +169 -0
- package/server/api/routes/mulmo-script.ts +712 -0
- package/server/api/routes/mulmoScriptValidate.ts +101 -0
- package/server/api/routes/notifications.ts +69 -0
- package/server/api/routes/pdf.ts +163 -0
- package/server/api/routes/plugins.ts +276 -0
- package/server/api/routes/presentHtml.ts +48 -0
- package/server/api/routes/roles.ts +125 -0
- package/server/api/routes/scheduler.ts +153 -0
- package/server/api/routes/schedulerHandlers.ts +151 -0
- package/server/api/routes/schedulerTasks.ts +163 -0
- package/server/api/routes/sessions.ts +294 -0
- package/server/api/routes/sessionsCursor.ts +59 -0
- package/server/api/routes/skills.ts +195 -0
- package/server/api/routes/sources.ts +540 -0
- package/server/api/routes/todos.ts +263 -0
- package/server/api/routes/todosColumnsHandlers.ts +347 -0
- package/server/api/routes/todosHandlers.ts +274 -0
- package/server/api/routes/todosItemsHandlers.ts +386 -0
- package/server/api/routes/wiki/pageIndex.ts +53 -0
- package/server/api/routes/wiki.ts +363 -0
- package/server/api/sandboxStatus.ts +64 -0
- package/server/events/notifications.ts +160 -0
- package/server/events/pub-sub/index.ts +45 -0
- package/server/events/relay-client.ts +288 -0
- package/server/events/scheduler-adapter.ts +302 -0
- package/server/events/session-store/index.ts +492 -0
- package/server/events/task-manager/index.ts +181 -0
- package/server/index.ts +572 -0
- package/server/system/config.ts +243 -0
- package/server/system/credentials.ts +220 -0
- package/server/system/docker.ts +97 -0
- package/server/system/env.ts +109 -0
- package/server/system/logger/config.ts +112 -0
- package/server/system/logger/formatters.ts +40 -0
- package/server/system/logger/index.ts +53 -0
- package/server/system/logger/rotation.ts +37 -0
- package/server/system/logger/sinks.ts +101 -0
- package/server/system/logger/types.ts +29 -0
- package/server/utils/date.ts +57 -0
- package/server/utils/errors.ts +7 -0
- package/server/utils/fetch.ts +27 -0
- package/server/utils/files/atomic.ts +125 -0
- package/server/utils/files/html-io.ts +20 -0
- package/server/utils/files/image-store.ts +66 -0
- package/server/utils/files/index.ts +45 -0
- package/server/utils/files/journal-io.ts +213 -0
- package/server/utils/files/json.ts +69 -0
- package/server/utils/files/markdown-store.ts +33 -0
- package/server/utils/files/naming.ts +50 -0
- package/server/utils/files/reference-dirs-io.ts +45 -0
- package/server/utils/files/roles-io.ts +45 -0
- package/server/utils/files/safe.ts +106 -0
- package/server/utils/files/scheduler-io.ts +20 -0
- package/server/utils/files/scheduler-overrides-io.ts +64 -0
- package/server/utils/files/session-io.ts +136 -0
- package/server/utils/files/spreadsheet-store.ts +63 -0
- package/server/utils/files/todos-io.ts +29 -0
- package/server/utils/files/user-tasks-io.ts +25 -0
- package/server/utils/files/workspace-io.ts +221 -0
- package/server/utils/gemini.ts +59 -0
- package/server/utils/gitignore.ts +69 -0
- package/server/utils/http.ts +15 -0
- package/server/utils/httpError.ts +61 -0
- package/server/utils/id.ts +16 -0
- package/server/utils/json.ts +83 -0
- package/server/utils/logBackgroundError.ts +22 -0
- package/server/utils/markdown.ts +82 -0
- package/server/utils/request.ts +29 -0
- package/server/utils/slug.ts +50 -0
- package/server/utils/spawn.ts +62 -0
- package/server/utils/time.ts +34 -0
- package/server/utils/types.ts +47 -0
- package/server/workspace/chat-index/index.ts +153 -0
- package/server/workspace/chat-index/indexer.ts +209 -0
- package/server/workspace/chat-index/paths.ts +34 -0
- package/server/workspace/chat-index/summarizer.ts +247 -0
- package/server/workspace/chat-index/types.ts +38 -0
- package/server/workspace/custom-dirs.ts +220 -0
- package/server/workspace/helps/business.md +104 -0
- package/server/workspace/helps/github.md +23 -0
- package/server/workspace/helps/index.md +60 -0
- package/server/workspace/helps/mulmoscript.md +249 -0
- package/server/workspace/helps/sandbox.md +90 -0
- package/server/workspace/helps/spreadsheet.md +43 -0
- package/server/workspace/helps/telegram.md +135 -0
- package/server/workspace/helps/wiki.md +131 -0
- package/server/workspace/journal/archivist.ts +386 -0
- package/server/workspace/journal/dailyPass.ts +743 -0
- package/server/workspace/journal/diff.ts +71 -0
- package/server/workspace/journal/index.ts +185 -0
- package/server/workspace/journal/indexFile.ts +136 -0
- package/server/workspace/journal/linkRewrite.ts +4 -0
- package/server/workspace/journal/memoryExtractor.ts +130 -0
- package/server/workspace/journal/optimizationPass.ts +160 -0
- package/server/workspace/journal/paths.ts +76 -0
- package/server/workspace/journal/state.ts +125 -0
- package/server/workspace/paths.ts +158 -0
- package/server/workspace/reference-dirs.ts +252 -0
- package/server/workspace/roles.ts +37 -0
- package/server/workspace/skills/discovery.ts +125 -0
- package/server/workspace/skills/index.ts +10 -0
- package/server/workspace/skills/parser.ts +144 -0
- package/server/workspace/skills/paths.ts +41 -0
- package/server/workspace/skills/scheduler.ts +149 -0
- package/server/workspace/skills/types.ts +30 -0
- package/server/workspace/skills/user-tasks.ts +257 -0
- package/server/workspace/skills/writer.ts +189 -0
- package/server/workspace/sources/arxivDiscovery.ts +182 -0
- package/server/workspace/sources/classifier.ts +268 -0
- package/server/workspace/sources/fetchers/arxiv.ts +170 -0
- package/server/workspace/sources/fetchers/github.ts +106 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
- package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
- package/server/workspace/sources/fetchers/index.ts +71 -0
- package/server/workspace/sources/fetchers/registerAll.ts +15 -0
- package/server/workspace/sources/fetchers/rss.ts +141 -0
- package/server/workspace/sources/fetchers/rssParser.ts +295 -0
- package/server/workspace/sources/httpFetcher.ts +230 -0
- package/server/workspace/sources/interests.ts +120 -0
- package/server/workspace/sources/paths.ts +110 -0
- package/server/workspace/sources/pipeline/dedup.ts +60 -0
- package/server/workspace/sources/pipeline/fetch.ts +136 -0
- package/server/workspace/sources/pipeline/index.ts +249 -0
- package/server/workspace/sources/pipeline/notify.ts +72 -0
- package/server/workspace/sources/pipeline/plan.ts +66 -0
- package/server/workspace/sources/pipeline/summarize.ts +189 -0
- package/server/workspace/sources/pipeline/write.ts +185 -0
- package/server/workspace/sources/rateLimiter.ts +148 -0
- package/server/workspace/sources/registry.ts +326 -0
- package/server/workspace/sources/robots.ts +271 -0
- package/server/workspace/sources/sourceState.ts +135 -0
- package/server/workspace/sources/taxonomy.ts +74 -0
- package/server/workspace/sources/types.ts +144 -0
- package/server/workspace/sources/urls.ts +112 -0
- package/server/workspace/tool-trace/classify.ts +114 -0
- package/server/workspace/tool-trace/index.ts +250 -0
- package/server/workspace/tool-trace/writeSearch.ts +98 -0
- package/server/workspace/wiki-backlinks/index.ts +107 -0
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
- package/server/workspace/workspace.ts +66 -0
- package/src/App.vue +720 -0
- package/src/assets/mulmo_bw.png +0 -0
- package/src/components/CanvasViewToggle.vue +27 -0
- package/src/components/ChatAttachmentPreview.vue +45 -0
- package/src/components/ChatImagePreview.vue +17 -0
- package/src/components/ChatInput.vue +208 -0
- package/src/components/FileContentHeader.vue +49 -0
- package/src/components/FileContentRenderer.vue +162 -0
- package/src/components/FileTree.vue +115 -0
- package/src/components/FileTreePane.vue +85 -0
- package/src/components/FilesView.vue +206 -0
- package/src/components/LockStatusPopup.vue +111 -0
- package/src/components/NotificationBell.vue +131 -0
- package/src/components/NotificationToast.vue +72 -0
- package/src/components/PluginLauncher.vue +138 -0
- package/src/components/RightSidebar.vue +113 -0
- package/src/components/RoleSelector.vue +64 -0
- package/src/components/SessionHistoryPanel.vue +176 -0
- package/src/components/SessionTabBar.vue +81 -0
- package/src/components/SettingsMcpTab.vue +350 -0
- package/src/components/SettingsModal.vue +275 -0
- package/src/components/SettingsReferenceDirsTab.vue +173 -0
- package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
- package/src/components/SidebarHeader.vue +69 -0
- package/src/components/StackView.vue +360 -0
- package/src/components/SuggestionsPanel.vue +65 -0
- package/src/components/TodoExplorer.vue +358 -0
- package/src/components/ToolResultsPanel.vue +77 -0
- package/src/components/todo/TodoAddDialog.vue +131 -0
- package/src/components/todo/TodoEditDialog.vue +47 -0
- package/src/components/todo/TodoEditPanel.vue +113 -0
- package/src/components/todo/TodoKanbanView.vue +249 -0
- package/src/components/todo/TodoListView.vue +79 -0
- package/src/components/todo/TodoTableView.vue +177 -0
- package/src/composables/useActiveSession.ts +40 -0
- package/src/composables/useAppApi.ts +45 -0
- package/src/composables/useCanvasViewMode.ts +121 -0
- package/src/composables/useChatScroll.ts +47 -0
- package/src/composables/useClickOutside.ts +26 -0
- package/src/composables/useClipboardCopy.ts +44 -0
- package/src/composables/useContentDisplay.ts +52 -0
- package/src/composables/useDebugBeat.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +115 -0
- package/src/composables/useEventListeners.ts +42 -0
- package/src/composables/useExpandedDirs.ts +64 -0
- package/src/composables/useFaviconState.ts +30 -0
- package/src/composables/useFileSelection.ts +115 -0
- package/src/composables/useFileSortMode.ts +24 -0
- package/src/composables/useFileTree.ts +85 -0
- package/src/composables/useFreshPluginData.ts +89 -0
- package/src/composables/useHealth.ts +38 -0
- package/src/composables/useImeAwareEnter.ts +57 -0
- package/src/composables/useKeyNavigation.ts +60 -0
- package/src/composables/useMarkdownLinkHandler.ts +46 -0
- package/src/composables/useMarkdownMode.ts +17 -0
- package/src/composables/useMcpTools.ts +71 -0
- package/src/composables/useMergedSessions.ts +27 -0
- package/src/composables/useNotifications.ts +90 -0
- package/src/composables/usePdfDownload.ts +60 -0
- package/src/composables/usePendingCalls.ts +77 -0
- package/src/composables/usePubSub.ts +85 -0
- package/src/composables/useRightSidebar.ts +23 -0
- package/src/composables/useRoles.ts +34 -0
- package/src/composables/useSandboxStatus.ts +67 -0
- package/src/composables/useSelectedResult.ts +49 -0
- package/src/composables/useSessionDerived.ts +51 -0
- package/src/composables/useSessionHistory.ts +81 -0
- package/src/composables/useSessionSync.ts +57 -0
- package/src/composables/useViewLayout.ts +55 -0
- package/src/config/apiRoutes.ts +173 -0
- package/src/config/pubsubChannels.ts +45 -0
- package/src/config/roles.ts +335 -0
- package/src/config/schedulerActions.ts +25 -0
- package/src/config/toolNames.ts +71 -0
- package/src/config/workspacePaths.ts +24 -0
- package/src/index.css +107 -0
- package/src/main.ts +25 -0
- package/src/plugins/canvas/Preview.vue +13 -0
- package/src/plugins/canvas/View.vue +333 -0
- package/src/plugins/canvas/definition.ts +38 -0
- package/src/plugins/canvas/index.ts +36 -0
- package/src/plugins/chart/Preview.vue +49 -0
- package/src/plugins/chart/View.vue +143 -0
- package/src/plugins/chart/definition.ts +58 -0
- package/src/plugins/chart/index.ts +52 -0
- package/src/plugins/editImage/Preview.vue +13 -0
- package/src/plugins/editImage/View.vue +13 -0
- package/src/plugins/editImage/definition.ts +27 -0
- package/src/plugins/editImage/index.ts +36 -0
- package/src/plugins/generateImage/Preview.vue +13 -0
- package/src/plugins/generateImage/View.vue +33 -0
- package/src/plugins/generateImage/definition.ts +32 -0
- package/src/plugins/generateImage/index.ts +56 -0
- package/src/plugins/manageRoles/Preview.vue +49 -0
- package/src/plugins/manageRoles/View.vue +525 -0
- package/src/plugins/manageRoles/definition.ts +43 -0
- package/src/plugins/manageRoles/index.ts +47 -0
- package/src/plugins/manageSkills/Preview.vue +21 -0
- package/src/plugins/manageSkills/View.vue +321 -0
- package/src/plugins/manageSkills/definition.ts +49 -0
- package/src/plugins/manageSkills/index.ts +49 -0
- package/src/plugins/manageSource/Preview.vue +33 -0
- package/src/plugins/manageSource/View.vue +697 -0
- package/src/plugins/manageSource/definition.ts +63 -0
- package/src/plugins/manageSource/index.ts +66 -0
- package/src/plugins/markdown/Preview.vue +77 -0
- package/src/plugins/markdown/View.vue +476 -0
- package/src/plugins/markdown/definition.ts +50 -0
- package/src/plugins/markdown/index.ts +36 -0
- package/src/plugins/presentHtml/Preview.vue +25 -0
- package/src/plugins/presentHtml/View.vue +52 -0
- package/src/plugins/presentHtml/definition.ts +27 -0
- package/src/plugins/presentHtml/helpers.ts +72 -0
- package/src/plugins/presentHtml/index.ts +41 -0
- package/src/plugins/presentMulmoScript/Preview.vue +23 -0
- package/src/plugins/presentMulmoScript/View.vue +1166 -0
- package/src/plugins/presentMulmoScript/definition.ts +95 -0
- package/src/plugins/presentMulmoScript/helpers.ts +162 -0
- package/src/plugins/presentMulmoScript/index.ts +40 -0
- package/src/plugins/scheduler/Preview.vue +67 -0
- package/src/plugins/scheduler/TasksTab.vue +205 -0
- package/src/plugins/scheduler/View.vue +565 -0
- package/src/plugins/scheduler/definition.ts +57 -0
- package/src/plugins/scheduler/index.ts +45 -0
- package/src/plugins/scheduler/viewModes.ts +26 -0
- package/src/plugins/spreadsheet/Preview.vue +29 -0
- package/src/plugins/spreadsheet/View.vue +997 -0
- package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
- package/src/plugins/spreadsheet/definition.ts +121 -0
- package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
- package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
- package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
- package/src/plugins/spreadsheet/engine/engine.ts +176 -0
- package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
- package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
- package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
- package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
- package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
- package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
- package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
- package/src/plugins/spreadsheet/engine/index.ts +27 -0
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
- package/src/plugins/spreadsheet/engine/parser.ts +143 -0
- package/src/plugins/spreadsheet/engine/registry.ts +150 -0
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
- package/src/plugins/spreadsheet/engine/types.ts +64 -0
- package/src/plugins/spreadsheet/index.ts +36 -0
- package/src/plugins/textResponse/Preview.vue +94 -0
- package/src/plugins/textResponse/View.vue +503 -0
- package/src/plugins/textResponse/definition.ts +34 -0
- package/src/plugins/textResponse/index.ts +27 -0
- package/src/plugins/textResponse/plugin.ts +29 -0
- package/src/plugins/textResponse/samples.ts +97 -0
- package/src/plugins/textResponse/types.ts +11 -0
- package/src/plugins/todo/Preview.vue +63 -0
- package/src/plugins/todo/View.vue +364 -0
- package/src/plugins/todo/composables/useTodos.ts +177 -0
- package/src/plugins/todo/definition.ts +45 -0
- package/src/plugins/todo/index.ts +61 -0
- package/src/plugins/todo/labels.ts +163 -0
- package/src/plugins/todo/priority.ts +98 -0
- package/src/plugins/todo/viewModes.ts +19 -0
- package/src/plugins/ui-image/ImagePreview.vue +23 -0
- package/src/plugins/ui-image/ImageView.vue +34 -0
- package/src/plugins/ui-image/index.ts +3 -0
- package/src/plugins/ui-image/types.ts +4 -0
- package/src/plugins/wiki/Preview.vue +65 -0
- package/src/plugins/wiki/View.vue +342 -0
- package/src/plugins/wiki/definition.ts +25 -0
- package/src/plugins/wiki/helpers.ts +59 -0
- package/src/plugins/wiki/index.ts +52 -0
- package/src/router/guards.ts +61 -0
- package/src/router/index.ts +50 -0
- package/src/tools/index.ts +52 -0
- package/src/tools/types.ts +27 -0
- package/src/types/events.ts +16 -0
- package/src/types/fileTree.ts +13 -0
- package/src/types/notification.ts +67 -0
- package/src/types/session.ts +116 -0
- package/src/types/sse.ts +90 -0
- package/src/types/toolCallHistory.ts +13 -0
- package/src/utils/agent/eventDispatch.ts +74 -0
- package/src/utils/agent/request.ts +55 -0
- package/src/utils/agent/toolCalls.ts +62 -0
- package/src/utils/api.ts +218 -0
- package/src/utils/canvas/viewMode.ts +46 -0
- package/src/utils/dom/authTokenMeta.ts +20 -0
- package/src/utils/dom/clickOutside.ts +11 -0
- package/src/utils/dom/externalLink.ts +57 -0
- package/src/utils/dom/scrollable.ts +24 -0
- package/src/utils/errors.ts +11 -0
- package/src/utils/files/expandedDirs.ts +25 -0
- package/src/utils/files/filename.ts +12 -0
- package/src/utils/files/sortChildren.ts +20 -0
- package/src/utils/filesPreview/schedulerPreview.ts +38 -0
- package/src/utils/filesPreview/todoPreview.ts +40 -0
- package/src/utils/format/date.ts +85 -0
- package/src/utils/format/frontmatter.ts +80 -0
- package/src/utils/format/jsonSyntax.ts +109 -0
- package/src/utils/html/previewCsp.ts +65 -0
- package/src/utils/image/resolve.ts +8 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
- package/src/utils/markdown/extractFirstH1.ts +39 -0
- package/src/utils/notification/dispatch.ts +22 -0
- package/src/utils/path/relativeLink.ts +130 -0
- package/src/utils/role/icon.ts +20 -0
- package/src/utils/role/merge.ts +10 -0
- package/src/utils/role/plugins.ts +12 -0
- package/src/utils/session/mergeSessions.ts +103 -0
- package/src/utils/session/seedRoleDefault.ts +35 -0
- package/src/utils/session/sessionEntries.ts +121 -0
- package/src/utils/session/sessionFactory.ts +22 -0
- package/src/utils/session/sessionHelpers.ts +99 -0
- package/src/utils/tools/dedup.ts +17 -0
- package/src/utils/tools/mcp.ts +33 -0
- package/src/utils/tools/pendingCalls.ts +16 -0
- package/src/utils/tools/result.ts +40 -0
- package/src/utils/types.ts +44 -0
package/src/App.vue
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col fixed inset-0 bg-gray-900 text-white">
|
|
3
|
+
<!-- Global top bar — shown in every view mode -->
|
|
4
|
+
<div ref="topBarRef" class="shrink-0 bg-white text-gray-900">
|
|
5
|
+
<!-- Row 1: title + plugin launcher -->
|
|
6
|
+
<div class="flex items-center gap-3 px-3 py-2 border-b border-gray-200">
|
|
7
|
+
<SidebarHeader
|
|
8
|
+
:sandbox-enabled="sandboxEnabled"
|
|
9
|
+
:show-right-sidebar="showRightSidebar"
|
|
10
|
+
:title-style="debugTitleStyle"
|
|
11
|
+
@test-query="(q) => sendMessage(q)"
|
|
12
|
+
@notification-navigate="handleNotificationNavigate"
|
|
13
|
+
@toggle-right-sidebar="toggleRightSidebar"
|
|
14
|
+
@open-settings="showSettings = true"
|
|
15
|
+
/>
|
|
16
|
+
<div class="flex-1 min-w-0">
|
|
17
|
+
<PluginLauncher :active-tool-name="selectedResult?.toolName ?? null" :active-view-mode="canvasViewMode" @navigate="onPluginNavigate" />
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<!-- Row 2: canvas toggle + role selector + session tabs -->
|
|
21
|
+
<div class="flex items-center gap-3 px-3 py-2 border-b border-gray-100">
|
|
22
|
+
<CanvasViewToggle :model-value="canvasViewMode" @update:model-value="setCanvasViewMode" />
|
|
23
|
+
<RoleSelector v-model:current-role-id="currentRoleId" :roles="roles" @change="onRoleChange" />
|
|
24
|
+
<SessionTabBar
|
|
25
|
+
ref="sessionTabBarRef"
|
|
26
|
+
:sessions="tabSessions"
|
|
27
|
+
:current-session-id="displayedCurrentSessionId"
|
|
28
|
+
:roles="roles"
|
|
29
|
+
:active-session-count="activeSessionCount"
|
|
30
|
+
:unread-count="unreadCount"
|
|
31
|
+
:history-open="showHistory"
|
|
32
|
+
@new-session="handleNewSessionClick"
|
|
33
|
+
@load-session="handleSessionSelect"
|
|
34
|
+
@toggle-history="toggleHistory"
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<!-- History popup (all layouts) -->
|
|
40
|
+
<SessionHistoryPanel
|
|
41
|
+
v-if="showHistory"
|
|
42
|
+
ref="historyPanelRef"
|
|
43
|
+
:sessions="mergedSessions"
|
|
44
|
+
:current-session-id="currentSessionId"
|
|
45
|
+
:roles="roles"
|
|
46
|
+
:top-offset="historyTopOffset"
|
|
47
|
+
:error-message="historyError"
|
|
48
|
+
@load-session="handleSessionSelect"
|
|
49
|
+
/>
|
|
50
|
+
|
|
51
|
+
<!-- Body: sidebar (Single only) + canvas column + right sidebar -->
|
|
52
|
+
<div class="flex flex-1 min-h-0">
|
|
53
|
+
<!-- Sidebar (Single layout only) -->
|
|
54
|
+
<div v-if="!isStackLayout" class="w-80 flex-shrink-0 border-r border-gray-200 flex flex-col bg-white text-gray-900 relative">
|
|
55
|
+
<!-- Gemini API key warning -->
|
|
56
|
+
<div
|
|
57
|
+
v-if="!geminiAvailable && needsGeminiForRole(currentRoleId)"
|
|
58
|
+
class="mx-4 mt-3 mb-2 rounded border border-yellow-400 bg-yellow-50 p-3 text-xs text-yellow-700 shrink-0"
|
|
59
|
+
>
|
|
60
|
+
<span class="material-icons text-xs align-middle mr-1">warning</span>
|
|
61
|
+
Image generation requires
|
|
62
|
+
<code class="font-mono">GEMINI_API_KEY</code>. Add it to <code class="font-mono">.env</code> and restart the app.
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Tool result previews -->
|
|
66
|
+
<ToolResultsPanel
|
|
67
|
+
ref="toolResultsPanelRef"
|
|
68
|
+
:results="sidebarResults"
|
|
69
|
+
:selected-uuid="selectedResultUuid"
|
|
70
|
+
:result-timestamps="activeSession?.resultTimestamps ?? new Map()"
|
|
71
|
+
:is-running="isRunning"
|
|
72
|
+
:status-message="statusMessage"
|
|
73
|
+
:pending-calls="pendingCalls"
|
|
74
|
+
@select="onSidebarItemClick"
|
|
75
|
+
@activate="activePane = 'sidebar'"
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
<!-- Sample queries (expandable pane) -->
|
|
79
|
+
<SuggestionsPanel ref="suggestionsPanelRef" :queries="currentRole.queries ?? []" @send="(q) => sendMessage(q)" @edit="onQueryEdit" />
|
|
80
|
+
|
|
81
|
+
<!-- Text input -->
|
|
82
|
+
<ChatInput ref="chatInputRef" v-model="userInput" v-model:pasted-file="pastedFile" :is-running="isRunning" @send="sendMessage()" />
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<!-- Canvas column -->
|
|
86
|
+
<div class="flex-1 flex flex-col bg-white text-gray-900 min-w-0 overflow-hidden relative">
|
|
87
|
+
<!-- Gemini API key warning (Stack layouts — no sidebar to host it) -->
|
|
88
|
+
<div
|
|
89
|
+
v-if="isStackLayout && !geminiAvailable && needsGeminiForRole(currentRoleId)"
|
|
90
|
+
class="mx-3 mt-2 rounded border border-yellow-400 bg-yellow-50 p-2 text-xs text-yellow-700 shrink-0"
|
|
91
|
+
>
|
|
92
|
+
<span class="material-icons text-xs align-middle mr-1">warning</span>
|
|
93
|
+
Image generation requires
|
|
94
|
+
<code class="font-mono">GEMINI_API_KEY</code>. Add it to <code class="font-mono">.env</code> and restart the app.
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div ref="canvasRef" class="flex-1 overflow-hidden outline-none min-h-0" tabindex="0" @mousedown="activePane = 'main'" @keydown="handleCanvasKeydown">
|
|
98
|
+
<!-- Single mode -->
|
|
99
|
+
<template v-if="canvasViewMode === 'single'">
|
|
100
|
+
<component
|
|
101
|
+
:is="getPlugin(selectedResult.toolName)?.viewComponent"
|
|
102
|
+
v-if="selectedResult && getPlugin(selectedResult.toolName)?.viewComponent"
|
|
103
|
+
:selected-result="selectedResult"
|
|
104
|
+
:send-text-message="sendMessage"
|
|
105
|
+
@update-result="handleUpdateResult"
|
|
106
|
+
/>
|
|
107
|
+
<div v-else-if="selectedResult" class="h-full overflow-auto p-6">
|
|
108
|
+
<pre class="text-sm text-gray-700 whitespace-pre-wrap">{{ JSON.stringify(selectedResult, null, 2) }}</pre>
|
|
109
|
+
</div>
|
|
110
|
+
<div v-else class="flex items-center justify-center h-full text-gray-600">
|
|
111
|
+
<p>Start a conversation</p>
|
|
112
|
+
</div>
|
|
113
|
+
</template>
|
|
114
|
+
<!-- Stack mode -->
|
|
115
|
+
<StackView
|
|
116
|
+
v-else-if="canvasViewMode === 'stack'"
|
|
117
|
+
:tool-results="sidebarResults"
|
|
118
|
+
:selected-result-uuid="selectedResultUuid"
|
|
119
|
+
:result-timestamps="activeSession?.resultTimestamps ?? new Map()"
|
|
120
|
+
:send-text-message="sendMessage"
|
|
121
|
+
@select="(uuid) => (selectedResultUuid = uuid)"
|
|
122
|
+
@update-result="handleUpdateResult"
|
|
123
|
+
/>
|
|
124
|
+
<!-- Files mode -->
|
|
125
|
+
<FilesView v-else-if="canvasViewMode === 'files'" :refresh-token="filesRefreshToken" @load-session="handleSessionSelect" />
|
|
126
|
+
<!-- Todos mode -->
|
|
127
|
+
<TodoExplorer v-else-if="canvasViewMode === 'todos'" />
|
|
128
|
+
<!-- Scheduler mode -->
|
|
129
|
+
<SchedulerView v-else-if="canvasViewMode === 'scheduler'" />
|
|
130
|
+
<!-- Wiki mode -->
|
|
131
|
+
<WikiView v-else-if="canvasViewMode === 'wiki'" />
|
|
132
|
+
<!-- Skills mode -->
|
|
133
|
+
<SkillsView v-else-if="canvasViewMode === 'skills'" />
|
|
134
|
+
<!-- Roles mode -->
|
|
135
|
+
<RolesView v-else-if="canvasViewMode === 'roles'" />
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- Bottom bar (Stack chat only — plugin views have no
|
|
139
|
+
session context, so no chat input is shown) -->
|
|
140
|
+
<div v-if="canvasViewMode === 'stack'" class="border-t border-gray-200 bg-white shrink-0">
|
|
141
|
+
<SuggestionsPanel ref="suggestionsPanelRef" :queries="currentRole.queries ?? []" @send="(q) => sendMessage(q)" @edit="onQueryEdit" />
|
|
142
|
+
<ChatInput ref="chatInputRef" v-model="userInput" v-model:pasted-file="pastedFile" :is-running="isRunning" @send="sendMessage()" />
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<!-- Right sidebar: tool call history -->
|
|
147
|
+
<RightSidebar
|
|
148
|
+
v-if="showRightSidebar"
|
|
149
|
+
ref="rightSidebarRef"
|
|
150
|
+
:tool-call-history="toolCallHistory"
|
|
151
|
+
:available-tools="availableTools"
|
|
152
|
+
:role-prompt="currentRole.prompt"
|
|
153
|
+
:tool-descriptions="toolDescriptions"
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<!-- Global settings modal -->
|
|
158
|
+
<SettingsModal :open="showSettings" :docker-mode="sandboxEnabled" :mcp-tools-error="mcpToolsError" @update:open="showSettings = $event" />
|
|
159
|
+
<NotificationToast />
|
|
160
|
+
</div>
|
|
161
|
+
</template>
|
|
162
|
+
|
|
163
|
+
<script setup lang="ts">
|
|
164
|
+
import { ref, computed, watch, nextTick, onMounted, reactive } from "vue";
|
|
165
|
+
import { v4 as uuidv4 } from "uuid";
|
|
166
|
+
import { getPlugin } from "./tools";
|
|
167
|
+
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
168
|
+
import RightSidebar from "./components/RightSidebar.vue";
|
|
169
|
+
import SidebarHeader from "./components/SidebarHeader.vue";
|
|
170
|
+
import RoleSelector from "./components/RoleSelector.vue";
|
|
171
|
+
import SessionTabBar from "./components/SessionTabBar.vue";
|
|
172
|
+
import SuggestionsPanel from "./components/SuggestionsPanel.vue";
|
|
173
|
+
import ChatInput, { type PastedFile } from "./components/ChatInput.vue";
|
|
174
|
+
import SessionHistoryPanel from "./components/SessionHistoryPanel.vue";
|
|
175
|
+
import ToolResultsPanel from "./components/ToolResultsPanel.vue";
|
|
176
|
+
import CanvasViewToggle from "./components/CanvasViewToggle.vue";
|
|
177
|
+
import PluginLauncher from "./components/PluginLauncher.vue";
|
|
178
|
+
import StackView from "./components/StackView.vue";
|
|
179
|
+
import FilesView from "./components/FilesView.vue";
|
|
180
|
+
import TodoExplorer from "./components/TodoExplorer.vue";
|
|
181
|
+
import SchedulerView from "./plugins/scheduler/View.vue";
|
|
182
|
+
import WikiView from "./plugins/wiki/View.vue";
|
|
183
|
+
import SkillsView from "./plugins/manageSkills/View.vue";
|
|
184
|
+
import RolesView from "./plugins/manageRoles/View.vue";
|
|
185
|
+
import SettingsModal from "./components/SettingsModal.vue";
|
|
186
|
+
import NotificationToast from "./components/NotificationToast.vue";
|
|
187
|
+
import type { NotificationAction } from "./types/notification";
|
|
188
|
+
import { CANVAS_VIEW } from "./utils/canvas/viewMode";
|
|
189
|
+
import type { SseEvent } from "./types/sse";
|
|
190
|
+
import { type SessionEntry, type ActiveSession } from "./types/session";
|
|
191
|
+
import { EVENT_TYPES } from "./types/events";
|
|
192
|
+
import { extractImageData } from "./utils/tools/result";
|
|
193
|
+
import { buildAgentRequestBody, postAgentRun } from "./utils/agent/request";
|
|
194
|
+
import { applyAgentEvent, type AgentEventContext } from "./utils/agent/eventDispatch";
|
|
195
|
+
import { pushErrorMessage, beginUserTurn, updateResult } from "./utils/session/sessionHelpers";
|
|
196
|
+
import { maybeSeedRoleDefault } from "./utils/session/seedRoleDefault";
|
|
197
|
+
import { createEmptySession } from "./utils/session/sessionFactory";
|
|
198
|
+
import { buildLoadedSession, parseSessionEntries } from "./utils/session/sessionEntries";
|
|
199
|
+
import { resolveNotificationTarget } from "./utils/notification/dispatch";
|
|
200
|
+
import { usePendingCalls } from "./composables/usePendingCalls";
|
|
201
|
+
import { useClickOutside } from "./composables/useClickOutside";
|
|
202
|
+
import { useKeyNavigation } from "./composables/useKeyNavigation";
|
|
203
|
+
import { useDebugBeat } from "./composables/useDebugBeat";
|
|
204
|
+
import { useChatScroll } from "./composables/useChatScroll";
|
|
205
|
+
import { useViewLayout } from "./composables/useViewLayout";
|
|
206
|
+
import { useSessionSync } from "./composables/useSessionSync";
|
|
207
|
+
import { useSessionDerived } from "./composables/useSessionDerived";
|
|
208
|
+
import { useFaviconState } from "./composables/useFaviconState";
|
|
209
|
+
import { useMergedSessions } from "./composables/useMergedSessions";
|
|
210
|
+
import { useCanvasViewMode } from "./composables/useCanvasViewMode";
|
|
211
|
+
import { useSelectedResult } from "./composables/useSelectedResult";
|
|
212
|
+
import { useMcpTools } from "./composables/useMcpTools";
|
|
213
|
+
import { useRoles } from "./composables/useRoles";
|
|
214
|
+
import { usePubSub } from "./composables/usePubSub";
|
|
215
|
+
import { sessionChannel } from "./config/pubsubChannels";
|
|
216
|
+
import { useHealth } from "./composables/useHealth";
|
|
217
|
+
import { useSessionHistory } from "./composables/useSessionHistory";
|
|
218
|
+
import { useRightSidebar } from "./composables/useRightSidebar";
|
|
219
|
+
import { useEventListeners } from "./composables/useEventListeners";
|
|
220
|
+
import { provideAppApi } from "./composables/useAppApi";
|
|
221
|
+
import { provideActiveSession } from "./composables/useActiveSession";
|
|
222
|
+
import { useRoute, useRouter } from "vue-router";
|
|
223
|
+
import { apiGet } from "./utils/api";
|
|
224
|
+
import { API_ROUTES } from "./config/apiRoutes";
|
|
225
|
+
import { needsGemini } from "./utils/role/plugins";
|
|
226
|
+
|
|
227
|
+
// --- Per-session state ---
|
|
228
|
+
// Declared early so that pub/sub callbacks and function declarations
|
|
229
|
+
// below can reference them without forward-reference ambiguity.
|
|
230
|
+
const sessionMap = reactive(new Map<string, ActiveSession>());
|
|
231
|
+
|
|
232
|
+
// Tracks active pub/sub subscriptions per session. The unsubscribe
|
|
233
|
+
// function is stored so we can clean up when the session is removed
|
|
234
|
+
// from memory. Sessions that are running always have an active
|
|
235
|
+
// subscription so events arrive via WebSocket.
|
|
236
|
+
const sessionSubscriptions = new Map<string, () => void>();
|
|
237
|
+
|
|
238
|
+
// currentSessionId is a plain ref so that synchronous writes (e.g.
|
|
239
|
+
// inside createNewSession, which is called right before sendMessage
|
|
240
|
+
// might run) take effect immediately. The URL is kept in sync via
|
|
241
|
+
// navigateToSession, and external URL changes (back button, typed
|
|
242
|
+
// URL) feed back into the ref via the route watcher below.
|
|
243
|
+
const currentSessionId = ref("");
|
|
244
|
+
|
|
245
|
+
// --- Debug beat (pub/sub) ---
|
|
246
|
+
const { debugTitleStyle } = useDebugBeat();
|
|
247
|
+
|
|
248
|
+
const { subscribe: pubsubSubscribe } = usePubSub();
|
|
249
|
+
|
|
250
|
+
// --- Routing ---
|
|
251
|
+
const route = useRoute();
|
|
252
|
+
const router = useRouter();
|
|
253
|
+
|
|
254
|
+
// Omit ?role= for the default role to keep URLs clean.
|
|
255
|
+
function buildRoleQuery(): Record<string, string> {
|
|
256
|
+
const id = currentRoleId.value;
|
|
257
|
+
if (!id || roles.value.length === 0 || id === roles.value[0]?.id) return {};
|
|
258
|
+
return { role: id };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function navigateToSession(id: string, replace = false): void {
|
|
262
|
+
currentSessionId.value = id;
|
|
263
|
+
const method = replace ? router.replace : router.push;
|
|
264
|
+
method({
|
|
265
|
+
name: "chat",
|
|
266
|
+
params: { sessionId: id },
|
|
267
|
+
query: { ...buildViewQuery(), ...buildRoleQuery() },
|
|
268
|
+
}).catch((err) => {
|
|
269
|
+
if (err?.type !== 16) {
|
|
270
|
+
console.error("[navigateToSession] push failed:", err);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function handleNotificationNavigate(action: NotificationAction): void {
|
|
276
|
+
const target = resolveNotificationTarget(action);
|
|
277
|
+
if (!target) return;
|
|
278
|
+
if (target.kind === "session") {
|
|
279
|
+
navigateToSession(target.sessionId);
|
|
280
|
+
} else {
|
|
281
|
+
setCanvasViewMode(CANVAS_VIEW[target.view]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// External URL changes (back/forward button, typed URL) → update ref.
|
|
286
|
+
// If the session isn't in memory, load it from the server.
|
|
287
|
+
watch(
|
|
288
|
+
() => route.params.sessionId,
|
|
289
|
+
async (newId) => {
|
|
290
|
+
if (typeof newId !== "string" || newId === currentSessionId.value) return;
|
|
291
|
+
currentSessionId.value = newId;
|
|
292
|
+
if (!sessionMap.has(newId)) {
|
|
293
|
+
await loadSession(newId);
|
|
294
|
+
if (!sessionMap.has(newId)) {
|
|
295
|
+
createNewSession();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// External URL changes for ?role= → sync into currentRoleId.
|
|
302
|
+
// This doesn't trigger onRoleChange (which creates a new session) —
|
|
303
|
+
// the user is just navigating back/forward between sessions that
|
|
304
|
+
// were already associated with a role.
|
|
305
|
+
watch(
|
|
306
|
+
() => route.query.role,
|
|
307
|
+
(newRole) => {
|
|
308
|
+
if (typeof newRole !== "string" || newRole === currentRoleId.value) return;
|
|
309
|
+
const roleExists = roles.value.some((role) => role.id === newRole);
|
|
310
|
+
if (roleExists) currentRoleId.value = newRole;
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// --- Global state ---
|
|
315
|
+
const { roles, currentRoleId, currentRole, refreshRoles } = useRoles();
|
|
316
|
+
|
|
317
|
+
const userInput = ref("");
|
|
318
|
+
const pastedFile = ref<PastedFile | null>(null);
|
|
319
|
+
const activePane = ref<"sidebar" | "main">("sidebar");
|
|
320
|
+
|
|
321
|
+
const { sessions, showHistory, historyError, fetchSessions, toggleHistory } = useSessionHistory();
|
|
322
|
+
const { markSessionRead } = useSessionSync({
|
|
323
|
+
sessionMap,
|
|
324
|
+
currentSessionId,
|
|
325
|
+
fetchSessions,
|
|
326
|
+
});
|
|
327
|
+
const { geminiAvailable, sandboxEnabled, fetchHealth } = useHealth();
|
|
328
|
+
|
|
329
|
+
const { activeSession, toolResults, sidebarResults, currentSummary, isRunning, statusMessage, toolCallHistory, activeSessionCount, unreadCount } =
|
|
330
|
+
useSessionDerived({ sessionMap, currentSessionId, sessions });
|
|
331
|
+
|
|
332
|
+
const { selectedResultUuid } = useSelectedResult({
|
|
333
|
+
activeSession,
|
|
334
|
+
sessionMap,
|
|
335
|
+
currentSessionId,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ── Dynamic favicon (#470) ──────────────────────────────────
|
|
339
|
+
useFaviconState({ isRunning, currentSummary, activeSession });
|
|
340
|
+
|
|
341
|
+
const toolResultsPanelRef = ref<{ root: HTMLDivElement | null } | null>(null);
|
|
342
|
+
const canvasRef = ref<HTMLDivElement | null>(null);
|
|
343
|
+
const chatInputRef = ref<{ focus: () => void } | null>(null);
|
|
344
|
+
const topBarRef = ref<HTMLDivElement | null>(null);
|
|
345
|
+
const historyTopOffset = ref<number | undefined>(undefined);
|
|
346
|
+
|
|
347
|
+
const sessionTabBarRef = ref<{
|
|
348
|
+
historyButton: HTMLButtonElement | null;
|
|
349
|
+
} | null>(null);
|
|
350
|
+
const historyButtonRef = computed(() => sessionTabBarRef.value?.historyButton ?? null);
|
|
351
|
+
const historyPanelRef = ref<{ root: HTMLDivElement | null } | null>(null);
|
|
352
|
+
const historyPopupRef = computed(() => historyPanelRef.value?.root ?? null);
|
|
353
|
+
|
|
354
|
+
const { focusChatInput } = useChatScroll({
|
|
355
|
+
toolResultsPanelRef,
|
|
356
|
+
toolResults,
|
|
357
|
+
isRunning,
|
|
358
|
+
chatInputRef,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const { showRightSidebar, toggleRightSidebar } = useRightSidebar();
|
|
362
|
+
const showSettings = ref(false);
|
|
363
|
+
|
|
364
|
+
const { canvasViewMode, setCanvasViewMode, buildViewQuery, filesRefreshToken, handleViewModeShortcut, onPluginNavigate } = useCanvasViewMode({ isRunning });
|
|
365
|
+
|
|
366
|
+
// The no-sidebar "stack-style" layout (top bar + full-width canvas +
|
|
367
|
+
// bottom bar) is used for every view mode except Single. Clicking a
|
|
368
|
+
// plugin launcher button (Todos / Scheduler / Files / ...) swaps the
|
|
369
|
+
// canvas content without collapsing the frame back to the sidebar
|
|
370
|
+
// layout.
|
|
371
|
+
const { isStackLayout, restoreChatViewForSession, displayedCurrentSessionId } = useViewLayout({
|
|
372
|
+
canvasViewMode,
|
|
373
|
+
setCanvasViewMode,
|
|
374
|
+
currentSessionId,
|
|
375
|
+
activePane,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// User-initiated session switches: clicking a session tab, a history
|
|
379
|
+
// row, or a chat link in FilesView. In plugin views (Todos / Files /
|
|
380
|
+
// ...) no chat is active, so the click's purpose is to surface the
|
|
381
|
+
// chat — restore the preferred Single/Stack mode before loading.
|
|
382
|
+
// Not wired into the internal `loadSession` call path because that
|
|
383
|
+
// also fires on initial mount with `?view=plugin` URLs, which must
|
|
384
|
+
// be honoured as-is.
|
|
385
|
+
function handleSessionSelect(id: string): void {
|
|
386
|
+
restoreChatViewForSession();
|
|
387
|
+
loadSession(id);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function handleNewSessionClick(): void {
|
|
391
|
+
restoreChatViewForSession();
|
|
392
|
+
createNewSession();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Measure the top bar's height when the history popup opens.
|
|
396
|
+
watch(showHistory, (open) => {
|
|
397
|
+
if (open) {
|
|
398
|
+
nextTick(() => {
|
|
399
|
+
historyTopOffset.value = topBarRef.value?.offsetHeight;
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
const rightSidebarRef = ref<InstanceType<typeof RightSidebar> | null>(null);
|
|
404
|
+
|
|
405
|
+
const { availableTools, toolDescriptions, mcpToolsError, fetchMcpToolsStatus } = useMcpTools({
|
|
406
|
+
currentRole,
|
|
407
|
+
getDefinition: (name) => getPlugin(name)?.toolDefinition ?? null,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const { pendingCalls, teardown: teardownPendingCalls } = usePendingCalls({
|
|
411
|
+
isRunning,
|
|
412
|
+
toolCallHistory,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const selectedResult = computed(() => toolResults.value.find((result) => result.uuid === selectedResultUuid.value) ?? null);
|
|
416
|
+
|
|
417
|
+
const { mergedSessions, tabSessions } = useMergedSessions({
|
|
418
|
+
sessionMap,
|
|
419
|
+
sessions,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// Centralised session-switch handler: subscribe to the current session's
|
|
423
|
+
// pub/sub channel so we receive real-time events even if the session is
|
|
424
|
+
// idle (another tab may start a run). Unsubscribe from idle sessions
|
|
425
|
+
// when switching away (running sessions keep their subscription so they
|
|
426
|
+
// continue receiving events — session_finished will clean them up).
|
|
427
|
+
let previousSessionId: string | null = null;
|
|
428
|
+
watch(currentSessionId, (id) => {
|
|
429
|
+
const session = sessionMap.get(id);
|
|
430
|
+
// Subscribe to the new session's channel
|
|
431
|
+
if (session) {
|
|
432
|
+
ensureSessionSubscription(session);
|
|
433
|
+
}
|
|
434
|
+
// Unsubscribe from the previous session if it's not running and has
|
|
435
|
+
// no in-flight background generations. Tearing down the subscription
|
|
436
|
+
// while a generation is still running would orphan its completion
|
|
437
|
+
// event, leaving the session's busy indicator stuck on.
|
|
438
|
+
if (previousSessionId && previousSessionId !== id) {
|
|
439
|
+
const prevSession = sessionMap.get(previousSessionId);
|
|
440
|
+
const prevBusy = !!prevSession && (prevSession.isRunning || Object.keys(prevSession.pendingGenerations ?? {}).length > 0);
|
|
441
|
+
if (prevSession && !prevBusy) {
|
|
442
|
+
unsubscribeSession(previousSessionId);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
previousSessionId = id;
|
|
446
|
+
|
|
447
|
+
// Clear unread in both sessionMap and sessions list (for badge count),
|
|
448
|
+
// then tell the server so other tabs see it too.
|
|
449
|
+
const summary = sessions.value.find((entry) => entry.id === id);
|
|
450
|
+
const wasUnread = (session && session.hasUnread) || (summary && summary.hasUnread);
|
|
451
|
+
if (wasUnread) {
|
|
452
|
+
if (session) session.hasUnread = false;
|
|
453
|
+
if (summary) summary.hasUnread = false;
|
|
454
|
+
markSessionRead(id);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const { handleCanvasKeydown, handleKeyNavigation } = useKeyNavigation({
|
|
459
|
+
canvasRef,
|
|
460
|
+
activePane,
|
|
461
|
+
sidebarResults,
|
|
462
|
+
selectedResultUuid,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const suggestionsPanelRef = ref<{ collapse: () => void } | null>(null);
|
|
466
|
+
|
|
467
|
+
function onQueryEdit(query: string): void {
|
|
468
|
+
userInput.value = query;
|
|
469
|
+
nextTick(() => focusChatInput());
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function handleUpdateResult(updatedResult: ToolResultComplete) {
|
|
473
|
+
if (activeSession.value) updateResult(activeSession.value, updatedResult);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function onSidebarItemClick(uuid: string) {
|
|
477
|
+
selectedResultUuid.value = uuid;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const needsGeminiForRole = (roleId: string) => needsGemini(roles.value, roleId);
|
|
481
|
+
|
|
482
|
+
// Remove the current session from sessionMap if it's empty (no messages).
|
|
483
|
+
// Returns true if a session was removed, so the caller can use
|
|
484
|
+
// router.replace instead of router.push to keep the empty session out
|
|
485
|
+
// of browser navigation history.
|
|
486
|
+
function removeCurrentIfEmpty(): boolean {
|
|
487
|
+
const id = currentSessionId.value;
|
|
488
|
+
if (!id) return false;
|
|
489
|
+
const session = sessionMap.get(id);
|
|
490
|
+
if (session && session.toolResults.length === 0) {
|
|
491
|
+
sessionMap.delete(id);
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function createNewSession(roleId?: string): ActiveSession {
|
|
498
|
+
removeCurrentIfEmpty();
|
|
499
|
+
const rId = roleId ?? currentRoleId.value;
|
|
500
|
+
const session = createEmptySession(uuidv4(), rId);
|
|
501
|
+
sessionMap.set(session.id, session);
|
|
502
|
+
currentRoleId.value = rId;
|
|
503
|
+
navigateToSession(session.id, true);
|
|
504
|
+
suggestionsPanelRef.value?.collapse();
|
|
505
|
+
nextTick(() => focusChatInput());
|
|
506
|
+
return sessionMap.get(session.id)!;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function onRoleChange() {
|
|
510
|
+
// Covers both the user dropdown click and the agent-triggered role
|
|
511
|
+
// switch (EVENT_TYPES.switchRole) — either way the user ends up in
|
|
512
|
+
// a fresh chat session, so a plugin view should yield to chat.
|
|
513
|
+
restoreChatViewForSession();
|
|
514
|
+
const session = createNewSession(currentRoleId.value);
|
|
515
|
+
maybeSeedRoleDefault(session);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function activateSession(id: string, roleId: string, replace: boolean): void {
|
|
519
|
+
const reactiveSession = sessionMap.get(id);
|
|
520
|
+
if (reactiveSession) ensureSessionSubscription(reactiveSession);
|
|
521
|
+
// Set role before navigating: buildRoleQuery() reads currentRoleId to
|
|
522
|
+
// build ?role=, and the route.query.role watcher would otherwise fire
|
|
523
|
+
// after navigation and revert currentRoleId to the previous session's role.
|
|
524
|
+
currentRoleId.value = roleId;
|
|
525
|
+
navigateToSession(id, replace);
|
|
526
|
+
showHistory.value = false;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function loadSession(id: string) {
|
|
530
|
+
if (id === currentSessionId.value && sessionMap.has(id)) return;
|
|
531
|
+
const replaced = removeCurrentIfEmpty();
|
|
532
|
+
|
|
533
|
+
const live = sessionMap.get(id);
|
|
534
|
+
if (live) {
|
|
535
|
+
activateSession(id, live.roleId, replaced);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const response = await apiGet<SessionEntry[]>(API_ROUTES.sessions.detail.replace(":id", encodeURIComponent(id)));
|
|
540
|
+
if (!response.ok) return;
|
|
541
|
+
|
|
542
|
+
const newSession = buildLoadedSession({
|
|
543
|
+
id,
|
|
544
|
+
entries: response.data,
|
|
545
|
+
defaultRoleId: currentRoleId.value,
|
|
546
|
+
urlResult: typeof route.query.result === "string" ? route.query.result : null,
|
|
547
|
+
serverSummary: sessions.value.find((s) => s.id === id),
|
|
548
|
+
nowIso: new Date().toISOString(),
|
|
549
|
+
});
|
|
550
|
+
sessionMap.set(id, newSession);
|
|
551
|
+
activateSession(id, newSession.roleId, replaced);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Re-fetch the transcript from the server and patch any entries the
|
|
555
|
+
// client missed (e.g. due to a pub-sub disconnect during a long
|
|
556
|
+
// Docker build). Called on session_finished so the user sees the
|
|
557
|
+
// full response even if mid-run events were lost. See issue #350.
|
|
558
|
+
async function refreshSessionTranscript(sessionId: string): Promise<void> {
|
|
559
|
+
const session = sessionMap.get(sessionId);
|
|
560
|
+
if (!session) return;
|
|
561
|
+
const response = await apiGet<SessionEntry[]>(API_ROUTES.sessions.detail.replace(":id", encodeURIComponent(sessionId)));
|
|
562
|
+
if (!response.ok) return;
|
|
563
|
+
const serverResults = parseSessionEntries(response.data);
|
|
564
|
+
// Only patch if the server knows more than we do — avoids
|
|
565
|
+
// replacing a richer in-flight state with a stale snapshot when
|
|
566
|
+
// session_finished races with the last few events.
|
|
567
|
+
if (serverResults.length > session.toolResults.length) {
|
|
568
|
+
session.toolResults = serverResults;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function buildAgentEventContext(session: ActiveSession): AgentEventContext {
|
|
573
|
+
const sessionId = session.id;
|
|
574
|
+
return {
|
|
575
|
+
get session() {
|
|
576
|
+
return sessionMap.get(sessionId) ?? session;
|
|
577
|
+
},
|
|
578
|
+
setCurrentRoleId: (roleId) => {
|
|
579
|
+
currentRoleId.value = roleId;
|
|
580
|
+
},
|
|
581
|
+
onRoleChange,
|
|
582
|
+
refreshRoles,
|
|
583
|
+
scrollSidebarToBottom: () => rightSidebarRef.value?.scrollToBottom(),
|
|
584
|
+
onGenerationsDrained: () => {
|
|
585
|
+
if (currentSessionId.value === sessionId) {
|
|
586
|
+
markSessionRead(sessionId);
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function hasPendingGenerations(sessionId: string): boolean {
|
|
593
|
+
const live = sessionMap.get(sessionId);
|
|
594
|
+
return !!live && Object.keys(live.pendingGenerations).length > 0;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function handleSessionFinished(sessionId: string): void {
|
|
598
|
+
refreshSessionTranscript(sessionId).catch((err) => {
|
|
599
|
+
console.error("[handleSessionFinished] refresh failed:", err);
|
|
600
|
+
});
|
|
601
|
+
if (currentSessionId.value === sessionId) {
|
|
602
|
+
markSessionRead(sessionId);
|
|
603
|
+
} else if (!hasPendingGenerations(sessionId)) {
|
|
604
|
+
unsubscribeSession(sessionId);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function createSessionEventHandler(session: ActiveSession, ctx: AgentEventContext): (data: unknown) => void {
|
|
609
|
+
return (data: unknown) => {
|
|
610
|
+
const event = data as SseEvent;
|
|
611
|
+
if (!event || typeof event !== "object") return;
|
|
612
|
+
if (event.type === EVENT_TYPES.sessionFinished) {
|
|
613
|
+
handleSessionFinished(session.id);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
applyAgentEvent(event, ctx).catch((err) => {
|
|
617
|
+
console.error("[applyAgentEvent] unhandled:", err);
|
|
618
|
+
});
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function ensureSessionSubscription(session: ActiveSession): void {
|
|
623
|
+
if (sessionSubscriptions.has(session.id)) return;
|
|
624
|
+
const ctx = buildAgentEventContext(session);
|
|
625
|
+
const handler = createSessionEventHandler(session, ctx);
|
|
626
|
+
const unsub = pubsubSubscribe(sessionChannel(session.id), handler);
|
|
627
|
+
sessionSubscriptions.set(session.id, unsub);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function unsubscribeSession(chatSessionId: string): void {
|
|
631
|
+
const unsub = sessionSubscriptions.get(chatSessionId);
|
|
632
|
+
if (unsub) {
|
|
633
|
+
unsub();
|
|
634
|
+
sessionSubscriptions.delete(chatSessionId);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function sendMessage(text?: string) {
|
|
639
|
+
const message = typeof text === "string" ? text : userInput.value.trim();
|
|
640
|
+
if (!message || isRunning.value) return;
|
|
641
|
+
userInput.value = "";
|
|
642
|
+
const fileSnapshot = pastedFile.value;
|
|
643
|
+
pastedFile.value = null;
|
|
644
|
+
|
|
645
|
+
const session = sessionMap.get(currentSessionId.value);
|
|
646
|
+
if (!session) return;
|
|
647
|
+
|
|
648
|
+
beginUserTurn(session, message);
|
|
649
|
+
const sessionRole = roles.value.find((role) => role.id === session.roleId) ?? roles.value[0];
|
|
650
|
+
const selectedRes = session.toolResults.find((result) => result.uuid === session.selectedResultUuid) ?? undefined;
|
|
651
|
+
|
|
652
|
+
ensureSessionSubscription(session);
|
|
653
|
+
|
|
654
|
+
const result = await postAgentRun(
|
|
655
|
+
buildAgentRequestBody({
|
|
656
|
+
message,
|
|
657
|
+
role: sessionRole,
|
|
658
|
+
chatSessionId: session.id,
|
|
659
|
+
selectedImageData: fileSnapshot?.dataUrl ?? extractImageData(selectedRes),
|
|
660
|
+
}),
|
|
661
|
+
);
|
|
662
|
+
if (!result.ok) {
|
|
663
|
+
pushErrorMessage(session, result.error);
|
|
664
|
+
unsubscribeSession(session.id);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const { handler: handleClickOutsideHistory } = useClickOutside({
|
|
669
|
+
isOpen: showHistory,
|
|
670
|
+
buttonRef: historyButtonRef,
|
|
671
|
+
popupRef: historyPopupRef,
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Plugin Views call back into App.vue via provide/inject (#227).
|
|
675
|
+
provideAppApi({
|
|
676
|
+
refreshRoles,
|
|
677
|
+
sendMessage: (message: string) => sendMessage(message),
|
|
678
|
+
});
|
|
679
|
+
// Plugin Views that need to tag background work with the current
|
|
680
|
+
// session (e.g. MulmoScript generations) inject this.
|
|
681
|
+
provideActiveSession(activeSession);
|
|
682
|
+
|
|
683
|
+
useEventListeners({
|
|
684
|
+
onKeyNavigation: handleKeyNavigation,
|
|
685
|
+
onViewModeShortcut: handleViewModeShortcut,
|
|
686
|
+
onClickOutsideHistory: handleClickOutsideHistory,
|
|
687
|
+
onTeardown: teardownPendingCalls,
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
onMounted(async () => {
|
|
691
|
+
// Fire-and-forget side fetches.
|
|
692
|
+
fetchHealth();
|
|
693
|
+
fetchMcpToolsStatus();
|
|
694
|
+
fetchSessions();
|
|
695
|
+
// Roles must be loaded before the first session is created, so
|
|
696
|
+
// createNewSession() picks a roleId that exists in the merged
|
|
697
|
+
// role list (built-in + custom).
|
|
698
|
+
await refreshRoles();
|
|
699
|
+
|
|
700
|
+
// If the URL specifies a role, apply it before session creation.
|
|
701
|
+
const urlRole = typeof route.query.role === "string" ? route.query.role : null;
|
|
702
|
+
if (urlRole && roles.value.some((role) => role.id === urlRole)) {
|
|
703
|
+
currentRoleId.value = urlRole;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// If the URL already names a session (e.g. a bookmarked link or a
|
|
707
|
+
// page reload), try to load it. Otherwise create a fresh one.
|
|
708
|
+
const initialSessionId = currentSessionId.value;
|
|
709
|
+
if (initialSessionId) {
|
|
710
|
+
await loadSession(initialSessionId);
|
|
711
|
+
// loadSession is a no-op when the server returns 404 — in that
|
|
712
|
+
// case sessionMap won't have the id, so fall through to create.
|
|
713
|
+
if (!sessionMap.has(initialSessionId)) {
|
|
714
|
+
createNewSession();
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
createNewSession();
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
</script>
|