mulmoclaude 0.1.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mulmoclaude.js +7 -24
- package/client/assets/html2canvas-Cx501zZr-Cv5snK9D.js +5 -0
- package/client/assets/index-CubzmCVK.css +2 -0
- package/client/assets/{index-D8rhwXLq.js → index-DtcyExH9.js} +80 -61
- package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-DnizuhIY.js} +5 -5
- package/client/index.html +2 -4
- package/package.json +13 -13
- package/server/agent/attachmentConverter.ts +2 -2
- package/server/agent/config.ts +12 -12
- package/server/agent/index.ts +9 -3
- package/server/agent/mcp-server.ts +19 -19
- package/server/agent/mcp-tools/index.ts +6 -6
- package/server/agent/mcp-tools/x.ts +7 -6
- package/server/agent/prompt.ts +195 -29
- package/server/agent/resumeFailover.ts +5 -5
- package/server/agent/sandboxMounts.ts +10 -10
- package/server/agent/stream.ts +4 -4
- package/server/api/auth/bearerAuth.ts +3 -3
- package/server/api/auth/token.ts +2 -2
- package/server/api/routes/agent.ts +21 -3
- package/server/api/routes/config.ts +1 -1
- package/server/api/routes/files.ts +22 -21
- package/server/api/routes/html.ts +2 -2
- package/server/api/routes/image.ts +7 -7
- package/server/api/routes/mulmo-script.ts +33 -31
- package/server/api/routes/pdf.ts +2 -2
- package/server/api/routes/plugins.ts +16 -6
- package/server/api/routes/roles.ts +2 -2
- package/server/api/routes/scheduler.ts +14 -12
- package/server/api/routes/schedulerHandlers.ts +12 -12
- package/server/api/routes/schedulerTasks.ts +19 -17
- package/server/api/routes/sessions.ts +26 -26
- package/server/api/routes/sessionsCursor.ts +4 -4
- package/server/api/routes/skills.ts +5 -5
- package/server/api/routes/sources.ts +3 -3
- package/server/api/routes/todosColumnsHandlers.ts +30 -30
- package/server/api/routes/todosHandlers.ts +1 -1
- package/server/api/routes/todosItemsHandlers.ts +14 -14
- package/server/api/routes/wiki.ts +36 -22
- package/server/api/sandboxStatus.ts +1 -1
- package/server/events/notifications.ts +6 -6
- package/server/events/pub-sub/index.ts +3 -3
- package/server/events/relay-client.ts +17 -16
- package/server/events/scheduler-adapter.ts +20 -20
- package/server/events/session-store/index.ts +10 -10
- package/server/events/task-manager/index.ts +7 -7
- package/server/index.ts +59 -65
- package/server/system/config.ts +5 -5
- package/server/system/credentials.ts +7 -5
- package/server/system/env.ts +5 -5
- package/server/utils/date.ts +18 -18
- package/server/utils/files/atomic.ts +16 -16
- package/server/utils/files/html-io.ts +5 -5
- package/server/utils/files/image-store.ts +19 -8
- package/server/utils/files/journal-io.ts +4 -4
- package/server/utils/files/json.ts +5 -5
- package/server/utils/files/markdown-store.ts +4 -4
- package/server/utils/files/naming.ts +2 -2
- package/server/utils/files/reference-dirs-io.ts +3 -3
- package/server/utils/files/roles-io.ts +12 -12
- package/server/utils/files/safe.ts +14 -14
- package/server/utils/files/scheduler-io.ts +5 -5
- package/server/utils/files/scheduler-overrides-io.ts +2 -2
- package/server/utils/files/session-io.ts +35 -35
- package/server/utils/files/spreadsheet-store.ts +7 -7
- package/server/utils/files/todos-io.ts +9 -9
- package/server/utils/files/user-tasks-io.ts +5 -5
- package/server/utils/files/workspace-io.ts +12 -12
- package/server/utils/gemini.ts +2 -2
- package/server/utils/gitignore.ts +9 -9
- package/server/utils/json.ts +5 -5
- package/server/utils/logBackgroundError.ts +12 -3
- package/server/utils/markdown.ts +5 -5
- package/server/utils/port.d.mts +6 -0
- package/server/utils/port.mjs +48 -0
- package/server/utils/request.ts +12 -6
- package/server/utils/spawn.ts +1 -1
- package/server/utils/types.ts +2 -2
- package/server/workspace/chat-index/indexer.ts +15 -15
- package/server/workspace/chat-index/summarizer.ts +4 -4
- package/server/workspace/custom-dirs.ts +16 -16
- package/server/workspace/journal/archivist.ts +35 -35
- package/server/workspace/journal/dailyPass.ts +31 -28
- package/server/workspace/journal/diff.ts +2 -2
- package/server/workspace/journal/index.ts +4 -4
- package/server/workspace/journal/indexFile.ts +29 -25
- package/server/workspace/journal/optimizationPass.ts +2 -2
- package/server/workspace/journal/state.ts +6 -6
- package/server/workspace/paths.ts +3 -3
- package/server/workspace/reference-dirs.ts +20 -20
- package/server/workspace/roles.ts +6 -6
- package/server/workspace/skills/discovery.ts +4 -4
- package/server/workspace/skills/parser.ts +6 -6
- package/server/workspace/skills/scheduler.ts +3 -3
- package/server/workspace/skills/user-tasks.ts +34 -34
- package/server/workspace/skills/writer.ts +3 -3
- package/server/workspace/sources/arxivDiscovery.ts +10 -10
- package/server/workspace/sources/classifier.ts +7 -7
- package/server/workspace/sources/fetchers/arxiv.ts +7 -7
- package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
- package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
- package/server/workspace/sources/fetchers/rss.ts +5 -5
- package/server/workspace/sources/fetchers/rssParser.ts +4 -4
- package/server/workspace/sources/interests.ts +12 -12
- package/server/workspace/sources/paths.ts +6 -6
- package/server/workspace/sources/pipeline/fetch.ts +36 -13
- package/server/workspace/sources/pipeline/index.ts +8 -13
- package/server/workspace/sources/pipeline/notify.ts +3 -3
- package/server/workspace/sources/pipeline/plan.ts +15 -13
- package/server/workspace/sources/pipeline/write.ts +5 -5
- package/server/workspace/sources/rateLimiter.ts +1 -1
- package/server/workspace/sources/registry.ts +16 -16
- package/server/workspace/sources/robots.ts +14 -14
- package/server/workspace/sources/sourceState.ts +17 -10
- package/server/workspace/sources/types.ts +9 -0
- package/server/workspace/sources/urls.ts +1 -1
- package/server/workspace/tool-trace/classify.ts +4 -4
- package/server/workspace/tool-trace/index.ts +1 -1
- package/server/workspace/tool-trace/writeSearch.ts +26 -16
- package/server/workspace/wiki-backlinks/index.ts +8 -8
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
- package/server/workspace/workspace.ts +7 -7
- package/src/App.vue +315 -141
- package/src/components/CanvasViewToggle.vue +10 -7
- package/src/components/ChatInput.vue +67 -33
- package/src/components/FileContentHeader.vue +7 -4
- package/src/components/FileContentRenderer.vue +20 -6
- package/src/components/FileTree.vue +6 -3
- package/src/components/FileTreePane.vue +11 -8
- package/src/components/FilesView.vue +5 -3
- package/src/components/LockStatusPopup.vue +17 -14
- package/src/components/NotificationBell.vue +14 -5
- package/src/components/NotificationToast.vue +6 -3
- package/src/components/PluginLauncher.vue +19 -56
- package/src/components/RightSidebar.vue +13 -10
- package/src/components/RoleSelector.vue +2 -2
- package/src/components/SessionHistoryPanel.vue +38 -34
- package/src/components/SessionTabBar.vue +8 -10
- package/src/components/SettingsMcpTab.vue +49 -36
- package/src/components/SettingsModal.vue +24 -22
- package/src/components/SettingsReferenceDirsTab.vue +39 -34
- package/src/components/SettingsWorkspaceDirsTab.vue +37 -27
- package/src/components/SidebarHeader.vue +25 -4
- package/src/components/StackView.vue +4 -1
- package/src/components/SuggestionsPanel.vue +7 -4
- package/src/components/TodoExplorer.vue +26 -15
- package/src/components/ToolResultsPanel.vue +27 -13
- package/src/components/todo/TodoAddDialog.vue +19 -14
- package/src/components/todo/TodoEditDialog.vue +7 -2
- package/src/components/todo/TodoEditPanel.vue +17 -12
- package/src/components/todo/TodoKanbanView.vue +10 -5
- package/src/components/todo/TodoListView.vue +10 -7
- package/src/components/todo/TodoTableView.vue +5 -2
- package/src/composables/useAppApi.ts +9 -0
- package/src/composables/useClickOutside.ts +2 -2
- package/src/composables/useDynamicFavicon.ts +172 -37
- package/src/composables/useEventListeners.ts +7 -8
- package/src/composables/useFaviconState.ts +13 -2
- package/src/composables/useFileSelection.ts +24 -6
- package/src/composables/useFreshPluginData.ts +3 -3
- package/src/composables/useKeyNavigation.ts +11 -11
- package/src/composables/useLayoutMode.ts +32 -0
- package/src/composables/useMcpTools.ts +2 -2
- package/src/composables/useNotifications.ts +3 -3
- package/src/composables/usePdfDownload.ts +4 -4
- package/src/composables/usePendingCalls.ts +1 -1
- package/src/composables/usePubSub.ts +10 -10
- package/src/composables/useRoles.ts +1 -1
- package/src/composables/useSandboxStatus.ts +1 -1
- package/src/composables/useSessionDerived.ts +3 -3
- package/src/composables/useSessionHistory.ts +7 -17
- package/src/composables/useSessionSync.ts +8 -8
- package/src/composables/useViewLayout.ts +20 -34
- package/src/config/roles.ts +2 -2
- package/src/lang/de.ts +536 -0
- package/src/lang/en.ts +558 -0
- package/src/lang/es.ts +543 -0
- package/src/lang/fr.ts +536 -0
- package/src/lang/ja.ts +536 -0
- package/src/lang/ko.ts +540 -0
- package/src/lang/pt-BR.ts +534 -0
- package/src/lang/zh.ts +537 -0
- package/src/lib/vue-i18n.ts +97 -0
- package/src/main.ts +2 -0
- package/src/plugins/canvas/View.vue +102 -186
- package/src/plugins/canvas/definition.ts +0 -8
- package/src/plugins/chart/Preview.vue +5 -5
- package/src/plugins/chart/View.vue +9 -4
- package/src/plugins/manageRoles/Preview.vue +4 -1
- package/src/plugins/manageRoles/View.vue +59 -43
- package/src/plugins/manageSkills/Preview.vue +8 -3
- package/src/plugins/manageSkills/View.vue +29 -25
- package/src/plugins/manageSource/Preview.vue +2 -2
- package/src/plugins/manageSource/View.vue +73 -52
- package/src/plugins/markdown/Preview.vue +1 -1
- package/src/plugins/markdown/View.vue +26 -36
- package/src/plugins/presentHtml/Preview.vue +1 -1
- package/src/plugins/presentHtml/View.vue +7 -4
- package/src/plugins/presentHtml/helpers.ts +8 -8
- package/src/plugins/presentMulmoScript/Preview.vue +1 -1
- package/src/plugins/presentMulmoScript/View.vue +40 -30
- package/src/plugins/presentMulmoScript/helpers.ts +1 -1
- package/src/plugins/scheduler/Preview.vue +13 -10
- package/src/plugins/scheduler/TasksTab.vue +57 -28
- package/src/plugins/scheduler/View.vue +28 -19
- package/src/plugins/scheduler/formatSchedule.ts +93 -0
- package/src/plugins/spreadsheet/Preview.vue +8 -3
- package/src/plugins/spreadsheet/View.vue +21 -12
- package/src/plugins/textResponse/Preview.vue +15 -58
- package/src/plugins/textResponse/View.vue +29 -9
- package/src/plugins/todo/Preview.vue +13 -8
- package/src/plugins/todo/View.vue +38 -24
- package/src/plugins/todo/composables/useTodos.ts +5 -5
- package/src/plugins/ui-image/ImagePreview.vue +6 -3
- package/src/plugins/ui-image/ImageView.vue +7 -4
- package/src/plugins/wiki/Preview.vue +10 -7
- package/src/plugins/wiki/View.vue +202 -81
- package/src/plugins/wiki/helpers.ts +4 -4
- package/src/plugins/wiki/route.ts +112 -0
- package/src/router/guards.ts +46 -28
- package/src/router/index.ts +41 -26
- package/src/types/session.ts +4 -3
- package/src/types/vue-i18n.d.ts +20 -0
- package/src/utils/agent/request.ts +22 -3
- package/src/utils/canvas/layoutMode.ts +26 -0
- package/src/utils/dom/scrollable.ts +2 -2
- package/src/utils/files/expandedDirs.ts +1 -1
- package/src/utils/files/sortChildren.ts +6 -6
- package/src/utils/format/frontmatter.ts +6 -6
- package/src/utils/image/cacheBust.ts +16 -0
- package/src/utils/image/resolve.ts +16 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
- package/src/utils/markdown/extractFirstH1.ts +2 -2
- package/src/utils/path/relativeLink.ts +15 -15
- package/src/utils/path/workspaceLinkRouter.ts +81 -0
- package/src/utils/role/icon.ts +2 -2
- package/src/utils/role/merge.ts +2 -2
- package/src/utils/role/plugins.ts +1 -1
- package/src/utils/session/sessionFactory.ts +2 -2
- package/src/utils/session/sessionHelpers.ts +2 -2
- package/src/utils/tools/dedup.ts +4 -4
- package/src/utils/tools/result.ts +3 -3
- package/src/utils/types.ts +2 -2
- package/src/vite-env.d.ts +9 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
- package/client/assets/index-KNLBjwuh.css +0 -1
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
- package/src/composables/useCanvasViewMode.ts +0 -121
- package/src/utils/canvas/viewMode.ts +0 -46
- /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
package/src/App.vue
CHANGED
|
@@ -1,52 +1,46 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="flex flex-col fixed inset-0 bg-gray-900 text-white">
|
|
3
3
|
<!-- Global top bar — shown in every view mode -->
|
|
4
|
-
<div
|
|
4
|
+
<div class="shrink-0 bg-white text-gray-900">
|
|
5
5
|
<!-- Row 1: title + plugin launcher -->
|
|
6
6
|
<div class="flex items-center gap-3 px-3 py-2 border-b border-gray-200">
|
|
7
7
|
<SidebarHeader
|
|
8
8
|
:sandbox-enabled="sandboxEnabled"
|
|
9
9
|
:show-right-sidebar="showRightSidebar"
|
|
10
|
+
:is-chat-page="isChatPage"
|
|
10
11
|
:title-style="debugTitleStyle"
|
|
11
12
|
@test-query="(q) => sendMessage(q)"
|
|
12
13
|
@notification-navigate="handleNotificationNavigate"
|
|
13
14
|
@toggle-right-sidebar="toggleRightSidebar"
|
|
14
15
|
@open-settings="showSettings = true"
|
|
16
|
+
@home="handleHomeClick"
|
|
15
17
|
/>
|
|
16
18
|
<div class="flex-1 min-w-0">
|
|
17
|
-
<PluginLauncher :active-tool-name="selectedResult?.toolName ?? null" :active-view-mode="
|
|
19
|
+
<PluginLauncher :active-tool-name="selectedResult?.toolName ?? null" :active-view-mode="currentPage" @navigate="onPluginNavigate" />
|
|
18
20
|
</div>
|
|
19
21
|
</div>
|
|
20
22
|
<!-- Row 2: canvas toggle + role selector + session tabs -->
|
|
21
23
|
<div class="flex items-center gap-3 px-3 py-2 border-b border-gray-100">
|
|
22
|
-
<CanvasViewToggle :model-value="
|
|
24
|
+
<CanvasViewToggle v-if="isChatPage" :model-value="layoutMode" @update:model-value="setLayoutMode" />
|
|
23
25
|
<RoleSelector v-model:current-role-id="currentRoleId" :roles="roles" @change="onRoleChange" />
|
|
24
26
|
<SessionTabBar
|
|
25
|
-
ref="sessionTabBarRef"
|
|
26
27
|
:sessions="tabSessions"
|
|
27
28
|
:current-session-id="displayedCurrentSessionId"
|
|
28
29
|
:roles="roles"
|
|
29
30
|
:active-session-count="activeSessionCount"
|
|
30
31
|
:unread-count="unreadCount"
|
|
31
|
-
:history-open="
|
|
32
|
+
:history-open="currentPage === 'history'"
|
|
32
33
|
@new-session="handleNewSessionClick"
|
|
33
34
|
@load-session="handleSessionSelect"
|
|
34
|
-
@toggle-history="
|
|
35
|
+
@toggle-history="handleHistoryClick"
|
|
35
36
|
/>
|
|
36
37
|
</div>
|
|
37
38
|
</div>
|
|
38
39
|
|
|
39
|
-
<!--
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
:sessions="mergedSessions"
|
|
44
|
-
:current-session-id="currentSessionId"
|
|
45
|
-
:roles="roles"
|
|
46
|
-
:top-offset="historyTopOffset"
|
|
47
|
-
:error-message="historyError"
|
|
48
|
-
@load-session="handleSessionSelect"
|
|
49
|
-
/>
|
|
40
|
+
<!-- Session history is now a canvas-column view rendered under
|
|
41
|
+
the `/history` route (see plans/feat-history-url-route.md).
|
|
42
|
+
The old absolute-positioned overlay is gone — browser
|
|
43
|
+
back/forward drives open/close instead. -->
|
|
50
44
|
|
|
51
45
|
<!-- Body: sidebar (Single only) + canvas column + right sidebar -->
|
|
52
46
|
<div class="flex flex-1 min-h-0">
|
|
@@ -58,8 +52,10 @@
|
|
|
58
52
|
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
53
|
>
|
|
60
54
|
<span class="material-icons text-xs align-middle mr-1">warning</span>
|
|
61
|
-
|
|
62
|
-
|
|
55
|
+
<i18n-t keypath="app.geminiRequired" tag="span">
|
|
56
|
+
<template #envKey><code class="font-mono">GEMINI_API_KEY</code></template>
|
|
57
|
+
<template #envFile><code class="font-mono">.env</code></template>
|
|
58
|
+
</i18n-t>
|
|
63
59
|
</div>
|
|
64
60
|
|
|
65
61
|
<!-- Tool result previews -->
|
|
@@ -90,13 +86,15 @@
|
|
|
90
86
|
class="mx-3 mt-2 rounded border border-yellow-400 bg-yellow-50 p-2 text-xs text-yellow-700 shrink-0"
|
|
91
87
|
>
|
|
92
88
|
<span class="material-icons text-xs align-middle mr-1">warning</span>
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
<i18n-t keypath="app.geminiRequired" tag="span">
|
|
90
|
+
<template #envKey><code class="font-mono">GEMINI_API_KEY</code></template>
|
|
91
|
+
<template #envFile><code class="font-mono">.env</code></template>
|
|
92
|
+
</i18n-t>
|
|
95
93
|
</div>
|
|
96
94
|
|
|
97
95
|
<div ref="canvasRef" class="flex-1 overflow-hidden outline-none min-h-0" tabindex="0" @mousedown="activePane = 'main'" @keydown="handleCanvasKeydown">
|
|
98
|
-
<!--
|
|
99
|
-
<template v-if="
|
|
96
|
+
<!-- Chat page: single or stack layout -->
|
|
97
|
+
<template v-if="isChatPage && layoutMode === 'single'">
|
|
100
98
|
<component
|
|
101
99
|
:is="getPlugin(selectedResult.toolName)?.viewComponent"
|
|
102
100
|
v-if="selectedResult && getPlugin(selectedResult.toolName)?.viewComponent"
|
|
@@ -108,12 +106,11 @@
|
|
|
108
106
|
<pre class="text-sm text-gray-700 whitespace-pre-wrap">{{ JSON.stringify(selectedResult, null, 2) }}</pre>
|
|
109
107
|
</div>
|
|
110
108
|
<div v-else class="flex items-center justify-center h-full text-gray-600">
|
|
111
|
-
<p>
|
|
109
|
+
<p>{{ t("app.startConversation") }}</p>
|
|
112
110
|
</div>
|
|
113
111
|
</template>
|
|
114
|
-
<!-- Stack mode -->
|
|
115
112
|
<StackView
|
|
116
|
-
v-else-if="
|
|
113
|
+
v-else-if="isChatPage && layoutMode === 'stack'"
|
|
117
114
|
:tool-results="sidebarResults"
|
|
118
115
|
:selected-result-uuid="selectedResultUuid"
|
|
119
116
|
:result-timestamps="activeSession?.resultTimestamps ?? new Map()"
|
|
@@ -121,31 +118,36 @@
|
|
|
121
118
|
@select="(uuid) => (selectedResultUuid = uuid)"
|
|
122
119
|
@update-result="handleUpdateResult"
|
|
123
120
|
/>
|
|
124
|
-
<!--
|
|
125
|
-
<FilesView v-else-if="
|
|
126
|
-
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
121
|
+
<!-- Distinct pages -->
|
|
122
|
+
<FilesView v-else-if="currentPage === 'files'" :refresh-token="filesRefreshToken" @load-session="handleSessionSelect" />
|
|
123
|
+
<TodoExplorer v-else-if="currentPage === 'todos'" />
|
|
124
|
+
<SchedulerView v-else-if="currentPage === 'scheduler'" />
|
|
125
|
+
<WikiView v-else-if="currentPage === 'wiki'" />
|
|
126
|
+
<SkillsView v-else-if="currentPage === 'skills'" />
|
|
127
|
+
<RolesView v-else-if="currentPage === 'roles'" />
|
|
128
|
+
<SessionHistoryPanel
|
|
129
|
+
v-else-if="currentPage === 'history'"
|
|
130
|
+
:sessions="mergedSessions"
|
|
131
|
+
:current-session-id="currentSessionId"
|
|
132
|
+
:roles="roles"
|
|
133
|
+
:error-message="historyError"
|
|
134
|
+
@load-session="handleSessionSelect"
|
|
135
|
+
/>
|
|
136
136
|
</div>
|
|
137
137
|
|
|
138
138
|
<!-- Bottom bar (Stack chat only — plugin views have no
|
|
139
139
|
session context, so no chat input is shown) -->
|
|
140
|
-
<div v-if="
|
|
140
|
+
<div v-if="isChatPage && layoutMode === 'stack'" class="border-t border-gray-200 bg-white shrink-0">
|
|
141
141
|
<SuggestionsPanel ref="suggestionsPanelRef" :queries="currentRole.queries ?? []" @send="(q) => sendMessage(q)" @edit="onQueryEdit" />
|
|
142
142
|
<ChatInput ref="chatInputRef" v-model="userInput" v-model:pasted-file="pastedFile" :is-running="isRunning" @send="sendMessage()" />
|
|
143
143
|
</div>
|
|
144
144
|
</div>
|
|
145
145
|
|
|
146
|
-
<!-- Right sidebar: tool call history
|
|
146
|
+
<!-- Right sidebar: tool call history. Only shown on the chat
|
|
147
|
+
page — system prompt / tools / tool-call history are all
|
|
148
|
+
agent-context and have no meaning on plugin views. -->
|
|
147
149
|
<RightSidebar
|
|
148
|
-
v-if="showRightSidebar"
|
|
150
|
+
v-if="showRightSidebar && isChatPage"
|
|
149
151
|
ref="rightSidebarRef"
|
|
150
152
|
:tool-call-history="toolCallHistory"
|
|
151
153
|
:available-tools="availableTools"
|
|
@@ -162,7 +164,10 @@
|
|
|
162
164
|
|
|
163
165
|
<script setup lang="ts">
|
|
164
166
|
import { ref, computed, watch, nextTick, onMounted, reactive } from "vue";
|
|
167
|
+
import { useI18n } from "vue-i18n";
|
|
165
168
|
import { v4 as uuidv4 } from "uuid";
|
|
169
|
+
|
|
170
|
+
const { t } = useI18n();
|
|
166
171
|
import { getPlugin } from "./tools";
|
|
167
172
|
import type { ToolResultComplete } from "gui-chat-protocol/vue";
|
|
168
173
|
import RightSidebar from "./components/RightSidebar.vue";
|
|
@@ -180,12 +185,13 @@ import FilesView from "./components/FilesView.vue";
|
|
|
180
185
|
import TodoExplorer from "./components/TodoExplorer.vue";
|
|
181
186
|
import SchedulerView from "./plugins/scheduler/View.vue";
|
|
182
187
|
import WikiView from "./plugins/wiki/View.vue";
|
|
188
|
+
import { buildWikiRouteParams } from "./plugins/wiki/route";
|
|
183
189
|
import SkillsView from "./plugins/manageSkills/View.vue";
|
|
184
190
|
import RolesView from "./plugins/manageRoles/View.vue";
|
|
185
191
|
import SettingsModal from "./components/SettingsModal.vue";
|
|
186
192
|
import NotificationToast from "./components/NotificationToast.vue";
|
|
187
193
|
import type { NotificationAction } from "./types/notification";
|
|
188
|
-
import {
|
|
194
|
+
import { PAGE_ROUTES, type PageRouteName } from "./router";
|
|
189
195
|
import type { SseEvent } from "./types/sse";
|
|
190
196
|
import { type SessionEntry, type ActiveSession } from "./types/session";
|
|
191
197
|
import { EVENT_TYPES } from "./types/events";
|
|
@@ -198,7 +204,6 @@ import { createEmptySession } from "./utils/session/sessionFactory";
|
|
|
198
204
|
import { buildLoadedSession, parseSessionEntries } from "./utils/session/sessionEntries";
|
|
199
205
|
import { resolveNotificationTarget } from "./utils/notification/dispatch";
|
|
200
206
|
import { usePendingCalls } from "./composables/usePendingCalls";
|
|
201
|
-
import { useClickOutside } from "./composables/useClickOutside";
|
|
202
207
|
import { useKeyNavigation } from "./composables/useKeyNavigation";
|
|
203
208
|
import { useDebugBeat } from "./composables/useDebugBeat";
|
|
204
209
|
import { useChatScroll } from "./composables/useChatScroll";
|
|
@@ -207,7 +212,7 @@ import { useSessionSync } from "./composables/useSessionSync";
|
|
|
207
212
|
import { useSessionDerived } from "./composables/useSessionDerived";
|
|
208
213
|
import { useFaviconState } from "./composables/useFaviconState";
|
|
209
214
|
import { useMergedSessions } from "./composables/useMergedSessions";
|
|
210
|
-
import {
|
|
215
|
+
import { useLayoutMode } from "./composables/useLayoutMode";
|
|
211
216
|
import { useSelectedResult } from "./composables/useSelectedResult";
|
|
212
217
|
import { useMcpTools } from "./composables/useMcpTools";
|
|
213
218
|
import { useRoles } from "./composables/useRoles";
|
|
@@ -223,6 +228,7 @@ import { useRoute, useRouter } from "vue-router";
|
|
|
223
228
|
import { apiGet } from "./utils/api";
|
|
224
229
|
import { API_ROUTES } from "./config/apiRoutes";
|
|
225
230
|
import { needsGemini } from "./utils/role/plugins";
|
|
231
|
+
import { classifyWorkspacePath } from "./utils/path/workspaceLinkRouter";
|
|
226
232
|
|
|
227
233
|
// --- Per-session state ---
|
|
228
234
|
// Declared early so that pub/sub callbacks and function declarations
|
|
@@ -253,18 +259,18 @@ const router = useRouter();
|
|
|
253
259
|
|
|
254
260
|
// Omit ?role= for the default role to keep URLs clean.
|
|
255
261
|
function buildRoleQuery(): Record<string, string> {
|
|
256
|
-
const
|
|
257
|
-
if (!
|
|
258
|
-
return { role:
|
|
262
|
+
const roleId = currentRoleId.value;
|
|
263
|
+
if (!roleId || roles.value.length === 0 || roleId === roles.value[0]?.id) return {};
|
|
264
|
+
return { role: roleId };
|
|
259
265
|
}
|
|
260
266
|
|
|
261
|
-
function navigateToSession(
|
|
262
|
-
currentSessionId.value =
|
|
267
|
+
function navigateToSession(sessionId: string, replace = false): void {
|
|
268
|
+
currentSessionId.value = sessionId;
|
|
263
269
|
const method = replace ? router.replace : router.push;
|
|
264
270
|
method({
|
|
265
|
-
name:
|
|
266
|
-
params: { sessionId
|
|
267
|
-
query:
|
|
271
|
+
name: PAGE_ROUTES.chat,
|
|
272
|
+
params: { sessionId },
|
|
273
|
+
query: buildRoleQuery(),
|
|
268
274
|
}).catch((err) => {
|
|
269
275
|
if (err?.type !== 16) {
|
|
270
276
|
console.error("[navigateToSession] push failed:", err);
|
|
@@ -278,7 +284,7 @@ function handleNotificationNavigate(action: NotificationAction): void {
|
|
|
278
284
|
if (target.kind === "session") {
|
|
279
285
|
navigateToSession(target.sessionId);
|
|
280
286
|
} else {
|
|
281
|
-
|
|
287
|
+
router.push({ name: target.view }).catch(() => {});
|
|
282
288
|
}
|
|
283
289
|
}
|
|
284
290
|
|
|
@@ -318,7 +324,7 @@ const userInput = ref("");
|
|
|
318
324
|
const pastedFile = ref<PastedFile | null>(null);
|
|
319
325
|
const activePane = ref<"sidebar" | "main">("sidebar");
|
|
320
326
|
|
|
321
|
-
const { sessions,
|
|
327
|
+
const { sessions, historyError, fetchSessions } = useSessionHistory();
|
|
322
328
|
const { markSessionRead } = useSessionSync({
|
|
323
329
|
sessionMap,
|
|
324
330
|
currentSessionId,
|
|
@@ -336,21 +342,14 @@ const { selectedResultUuid } = useSelectedResult({
|
|
|
336
342
|
});
|
|
337
343
|
|
|
338
344
|
// ── Dynamic favicon (#470) ──────────────────────────────────
|
|
339
|
-
|
|
345
|
+
// `unreadCount` covers every session (not just the active tab), so
|
|
346
|
+
// the favicon badge lights up when a background session gets a new
|
|
347
|
+
// reply even though the user is looking at a different session.
|
|
348
|
+
useFaviconState({ isRunning, currentSummary, activeSession, sessionsUnreadCount: unreadCount });
|
|
340
349
|
|
|
341
350
|
const toolResultsPanelRef = ref<{ root: HTMLDivElement | null } | null>(null);
|
|
342
351
|
const canvasRef = ref<HTMLDivElement | null>(null);
|
|
343
352
|
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
353
|
const { focusChatInput } = useChatScroll({
|
|
355
354
|
toolResultsPanelRef,
|
|
356
355
|
toolResults,
|
|
@@ -361,45 +360,87 @@ const { focusChatInput } = useChatScroll({
|
|
|
361
360
|
const { showRightSidebar, toggleRightSidebar } = useRightSidebar();
|
|
362
361
|
const showSettings = ref(false);
|
|
363
362
|
|
|
364
|
-
const {
|
|
363
|
+
const { layoutMode, setLayoutMode, toggleLayoutMode } = useLayoutMode();
|
|
364
|
+
|
|
365
|
+
// Current page derives from the route. The chat page has a layout
|
|
366
|
+
// preference on top (single vs. stack); other pages are distinct
|
|
367
|
+
// full-width views.
|
|
368
|
+
const isChatPage = computed(() => route.name === PAGE_ROUTES.chat);
|
|
369
|
+
const currentPage = computed<PageRouteName | null>(() => {
|
|
370
|
+
const name = route.name;
|
|
371
|
+
return typeof name === "string" && isPageRouteName(name) ? name : null;
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Refresh the files tree after each agent run so newly written files
|
|
375
|
+
// appear without a manual reload.
|
|
376
|
+
const filesRefreshToken = ref(0);
|
|
377
|
+
watch(isRunning, (running, prev) => {
|
|
378
|
+
if (prev && !running) filesRefreshToken.value++;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Cmd/Ctrl + 1 toggles layout when on /chat; on any other page it
|
|
382
|
+
// navigates to /chat (layout flip requires a second press). Cmd+2–7
|
|
383
|
+
// navigate directly to the matching page.
|
|
384
|
+
const PAGE_SHORTCUT_KEYS: Record<string, PageRouteName> = {
|
|
385
|
+
"2": PAGE_ROUTES.files,
|
|
386
|
+
"3": PAGE_ROUTES.todos,
|
|
387
|
+
"4": PAGE_ROUTES.scheduler,
|
|
388
|
+
"5": PAGE_ROUTES.wiki,
|
|
389
|
+
"6": PAGE_ROUTES.skills,
|
|
390
|
+
"7": PAGE_ROUTES.roles,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
function handleViewModeShortcut(event: KeyboardEvent): void {
|
|
394
|
+
if (!(event.metaKey || event.ctrlKey)) return;
|
|
395
|
+
if (event.altKey || event.shiftKey) return;
|
|
396
|
+
|
|
397
|
+
if (event.key === "1") {
|
|
398
|
+
event.preventDefault();
|
|
399
|
+
if (route.name === PAGE_ROUTES.chat) {
|
|
400
|
+
toggleLayoutMode();
|
|
401
|
+
} else {
|
|
402
|
+
resumeOrCreateChatSession().catch((err) => console.error("[Cmd+1] resume failed:", err));
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const page = PAGE_SHORTCUT_KEYS[event.key];
|
|
408
|
+
if (page) {
|
|
409
|
+
event.preventDefault();
|
|
410
|
+
router.push({ name: page }).catch(() => {});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function onPluginNavigate(target: { key: string }): void {
|
|
415
|
+
if (isPageRouteName(target.key)) {
|
|
416
|
+
router.push({ name: target.key }).catch(() => {});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isPageRouteName(value: string): value is PageRouteName {
|
|
421
|
+
return Object.values(PAGE_ROUTES).includes(value as PageRouteName);
|
|
422
|
+
}
|
|
365
423
|
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// layout.
|
|
371
|
-
const { isStackLayout, restoreChatViewForSession, displayedCurrentSessionId } = useViewLayout({
|
|
372
|
-
canvasViewMode,
|
|
373
|
-
setCanvasViewMode,
|
|
424
|
+
// Layout only matters on /chat; other pages are full-width by design.
|
|
425
|
+
const { isStackLayout, displayedCurrentSessionId } = useViewLayout({
|
|
426
|
+
layoutMode,
|
|
427
|
+
isChatPage,
|
|
374
428
|
currentSessionId,
|
|
375
429
|
activePane,
|
|
376
430
|
});
|
|
377
431
|
|
|
378
|
-
|
|
379
|
-
|
|
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);
|
|
432
|
+
function handleSessionSelect(sessionId: string): void {
|
|
433
|
+
loadSession(sessionId);
|
|
388
434
|
}
|
|
389
435
|
|
|
390
436
|
function handleNewSessionClick(): void {
|
|
391
|
-
restoreChatViewForSession();
|
|
392
437
|
createNewSession();
|
|
393
438
|
}
|
|
394
439
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
historyTopOffset.value = topBarRef.value?.offsetHeight;
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
});
|
|
440
|
+
function handleHomeClick(): void {
|
|
441
|
+
resumeOrCreateChatSession().catch((err) => console.error("[home] resume failed:", err));
|
|
442
|
+
}
|
|
443
|
+
|
|
403
444
|
const rightSidebarRef = ref<InstanceType<typeof RightSidebar> | null>(null);
|
|
404
445
|
|
|
405
446
|
const { availableTools, toolDescriptions, mcpToolsError, fetchMcpToolsStatus } = useMcpTools({
|
|
@@ -425,8 +466,8 @@ const { mergedSessions, tabSessions } = useMergedSessions({
|
|
|
425
466
|
// when switching away (running sessions keep their subscription so they
|
|
426
467
|
// continue receiving events — session_finished will clean them up).
|
|
427
468
|
let previousSessionId: string | null = null;
|
|
428
|
-
watch(currentSessionId, (
|
|
429
|
-
const session = sessionMap.get(
|
|
469
|
+
watch(currentSessionId, (sessionId) => {
|
|
470
|
+
const session = sessionMap.get(sessionId);
|
|
430
471
|
// Subscribe to the new session's channel
|
|
431
472
|
if (session) {
|
|
432
473
|
ensureSessionSubscription(session);
|
|
@@ -435,23 +476,23 @@ watch(currentSessionId, (id) => {
|
|
|
435
476
|
// no in-flight background generations. Tearing down the subscription
|
|
436
477
|
// while a generation is still running would orphan its completion
|
|
437
478
|
// event, leaving the session's busy indicator stuck on.
|
|
438
|
-
if (previousSessionId && previousSessionId !==
|
|
479
|
+
if (previousSessionId && previousSessionId !== sessionId) {
|
|
439
480
|
const prevSession = sessionMap.get(previousSessionId);
|
|
440
481
|
const prevBusy = !!prevSession && (prevSession.isRunning || Object.keys(prevSession.pendingGenerations ?? {}).length > 0);
|
|
441
482
|
if (prevSession && !prevBusy) {
|
|
442
483
|
unsubscribeSession(previousSessionId);
|
|
443
484
|
}
|
|
444
485
|
}
|
|
445
|
-
previousSessionId =
|
|
486
|
+
previousSessionId = sessionId;
|
|
446
487
|
|
|
447
488
|
// Clear unread in both sessionMap and sessions list (for badge count),
|
|
448
489
|
// then tell the server so other tabs see it too.
|
|
449
|
-
const summary = sessions.value.find((entry) => entry.id ===
|
|
490
|
+
const summary = sessions.value.find((entry) => entry.id === sessionId);
|
|
450
491
|
const wasUnread = (session && session.hasUnread) || (summary && summary.hasUnread);
|
|
451
492
|
if (wasUnread) {
|
|
452
493
|
if (session) session.hasUnread = false;
|
|
453
494
|
if (summary) summary.hasUnread = false;
|
|
454
|
-
markSessionRead(
|
|
495
|
+
markSessionRead(sessionId);
|
|
455
496
|
}
|
|
456
497
|
});
|
|
457
498
|
|
|
@@ -484,71 +525,127 @@ const needsGeminiForRole = (roleId: string) => needsGemini(roles.value, roleId);
|
|
|
484
525
|
// router.replace instead of router.push to keep the empty session out
|
|
485
526
|
// of browser navigation history.
|
|
486
527
|
function removeCurrentIfEmpty(): boolean {
|
|
487
|
-
const
|
|
488
|
-
if (!
|
|
489
|
-
const session = sessionMap.get(
|
|
528
|
+
const sessionId = currentSessionId.value;
|
|
529
|
+
if (!sessionId) return false;
|
|
530
|
+
const session = sessionMap.get(sessionId);
|
|
490
531
|
if (session && session.toolResults.length === 0) {
|
|
491
|
-
sessionMap.delete(
|
|
532
|
+
sessionMap.delete(sessionId);
|
|
492
533
|
return true;
|
|
493
534
|
}
|
|
494
535
|
return false;
|
|
495
536
|
}
|
|
496
537
|
|
|
538
|
+
// Replace vs push is derived from state, not chosen by the caller:
|
|
539
|
+
// replace only when we just discarded an empty session AND we're
|
|
540
|
+
// currently on that same /chat/:emptyId URL — otherwise there's
|
|
541
|
+
// something worth keeping in history (a real chat transcript, or
|
|
542
|
+
// a non-chat page like /wiki the user came from).
|
|
497
543
|
function createNewSession(roleId?: string): ActiveSession {
|
|
498
|
-
removeCurrentIfEmpty();
|
|
544
|
+
const removedEmpty = removeCurrentIfEmpty();
|
|
545
|
+
const replace = removedEmpty && isChatPage.value;
|
|
499
546
|
const rId = roleId ?? currentRoleId.value;
|
|
500
547
|
const session = createEmptySession(uuidv4(), rId);
|
|
501
548
|
sessionMap.set(session.id, session);
|
|
502
549
|
currentRoleId.value = rId;
|
|
503
|
-
navigateToSession(session.id,
|
|
550
|
+
navigateToSession(session.id, replace);
|
|
504
551
|
suggestionsPanelRef.value?.collapse();
|
|
505
552
|
nextTick(() => focusChatInput());
|
|
506
553
|
return sessionMap.get(session.id)!;
|
|
507
554
|
}
|
|
508
555
|
|
|
509
556
|
function onRoleChange() {
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
|
|
557
|
+
// On non-chat pages (wiki, files, etc.) the user is just picking
|
|
558
|
+
// the role that future new-chat actions should use — don't yank
|
|
559
|
+
// them onto /chat by creating a session here. currentRoleId is
|
|
560
|
+
// already updated by RoleSelector's v-model, so future "+" clicks
|
|
561
|
+
// or composer sends will pick it up. Agent-triggered role switches
|
|
562
|
+
// (EVENT_TYPES.switchRole) always fire during an active run on
|
|
563
|
+
// /chat, so the guard doesn't affect them.
|
|
564
|
+
if (!isChatPage.value) return;
|
|
514
565
|
const session = createNewSession(currentRoleId.value);
|
|
515
566
|
maybeSeedRoleDefault(session);
|
|
516
567
|
}
|
|
517
568
|
|
|
518
|
-
|
|
519
|
-
|
|
569
|
+
// Land on /chat with no specific session in mind (initial load, Cmd+1
|
|
570
|
+
// from another page). Prefer the most-recent session so the user
|
|
571
|
+
// resumes where they left off; only create a fresh session when they
|
|
572
|
+
// have no chat history at all. Explicit "+" clicks and role switches
|
|
573
|
+
// still create a new session via createNewSession() directly.
|
|
574
|
+
async function resumeOrCreateChatSession(): Promise<void> {
|
|
575
|
+
const topId = mergedSessions.value[0]?.id;
|
|
576
|
+
if (!topId) {
|
|
577
|
+
const currentSession = sessionMap.get(currentSessionId.value);
|
|
578
|
+
if (currentSession && currentSession.toolResults.length === 0) {
|
|
579
|
+
navigateToSession(currentSession.id);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
createNewSession();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (sessionMap.has(topId)) {
|
|
586
|
+
// Already in memory — navigate explicitly. loadSession would
|
|
587
|
+
// early-return here if topId === currentSessionId, skipping the
|
|
588
|
+
// URL push we need when arriving from a non-chat page.
|
|
589
|
+
navigateToSession(topId);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
await loadSession(topId);
|
|
593
|
+
// loadSession silently returns on fetch failure (stale summary,
|
|
594
|
+
// transient API error). Without a fallback, /chat is left with no
|
|
595
|
+
// active session and sendMessage becomes a no-op.
|
|
596
|
+
if (!sessionMap.has(topId)) {
|
|
597
|
+
createNewSession();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function activateSession(sessionId: string, roleId: string, replace: boolean): void {
|
|
602
|
+
const reactiveSession = sessionMap.get(sessionId);
|
|
520
603
|
if (reactiveSession) ensureSessionSubscription(reactiveSession);
|
|
521
604
|
// Set role before navigating: buildRoleQuery() reads currentRoleId to
|
|
522
605
|
// build ?role=, and the route.query.role watcher would otherwise fire
|
|
523
606
|
// after navigation and revert currentRoleId to the previous session's role.
|
|
524
607
|
currentRoleId.value = roleId;
|
|
525
|
-
navigateToSession(
|
|
526
|
-
|
|
608
|
+
navigateToSession(sessionId, replace);
|
|
609
|
+
// Closing the history popup is no longer explicit — navigating to
|
|
610
|
+
// /chat/:id via navigateToSession changes the route, and the
|
|
611
|
+
// canvas-column branches away from SessionHistoryPanel naturally.
|
|
527
612
|
}
|
|
528
613
|
|
|
529
|
-
async function loadSession(
|
|
530
|
-
|
|
531
|
-
|
|
614
|
+
async function loadSession(sessionId: string) {
|
|
615
|
+
// currentSessionId tracks "last active chat session" and is NOT
|
|
616
|
+
// reset when the user navigates to a non-chat page (/wiki, /files,
|
|
617
|
+
// …). Also checking the URL ensures that clicking the same session
|
|
618
|
+
// in the tab bar from /wiki still triggers a /chat navigation
|
|
619
|
+
// instead of silently no-opping.
|
|
620
|
+
const alreadyOnThatChat = sessionId === currentSessionId.value && sessionMap.has(sessionId) && route.params.sessionId === sessionId;
|
|
621
|
+
if (alreadyOnThatChat) return;
|
|
622
|
+
// Mirror createNewSession: only replace when we just discarded an
|
|
623
|
+
// empty session AND we're on that /chat/:emptyId URL. On /history
|
|
624
|
+
// (or any non-chat page) selecting a session must push, otherwise
|
|
625
|
+
// the /history entry would be skipped when the last chat happened
|
|
626
|
+
// to be empty.
|
|
627
|
+
const removedEmpty = removeCurrentIfEmpty();
|
|
628
|
+
const replaced = removedEmpty && isChatPage.value;
|
|
532
629
|
|
|
533
|
-
const live = sessionMap.get(
|
|
630
|
+
const live = sessionMap.get(sessionId);
|
|
534
631
|
if (live) {
|
|
535
|
-
activateSession(
|
|
632
|
+
activateSession(sessionId, live.roleId, replaced);
|
|
536
633
|
return;
|
|
537
634
|
}
|
|
538
635
|
|
|
539
|
-
const response = await apiGet<SessionEntry[]>(API_ROUTES.sessions.detail.replace(":id", encodeURIComponent(
|
|
636
|
+
const response = await apiGet<SessionEntry[]>(API_ROUTES.sessions.detail.replace(":id", encodeURIComponent(sessionId)));
|
|
540
637
|
if (!response.ok) return;
|
|
541
638
|
|
|
542
639
|
const newSession = buildLoadedSession({
|
|
543
|
-
id,
|
|
640
|
+
id: sessionId,
|
|
544
641
|
entries: response.data,
|
|
545
642
|
defaultRoleId: currentRoleId.value,
|
|
546
643
|
urlResult: typeof route.query.result === "string" ? route.query.result : null,
|
|
547
|
-
serverSummary: sessions.value.find((
|
|
644
|
+
serverSummary: sessions.value.find((summary) => summary.id === sessionId),
|
|
548
645
|
nowIso: new Date().toISOString(),
|
|
549
646
|
});
|
|
550
|
-
sessionMap.set(
|
|
551
|
-
activateSession(
|
|
647
|
+
sessionMap.set(sessionId, newSession);
|
|
648
|
+
activateSession(sessionId, newSession.roleId, replaced);
|
|
552
649
|
}
|
|
553
650
|
|
|
554
651
|
// Re-fetch the transcript from the server and patch any entries the
|
|
@@ -665,16 +762,79 @@ async function sendMessage(text?: string) {
|
|
|
665
762
|
}
|
|
666
763
|
}
|
|
667
764
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
765
|
+
// History is a page route (/history) now — no click-outside handling
|
|
766
|
+
// needed. Clicking the history button toggles between /history and the
|
|
767
|
+
// previous page via router.back / router.push.
|
|
768
|
+
function handleHistoryClick(): void {
|
|
769
|
+
if (currentPage.value !== PAGE_ROUTES.history) {
|
|
770
|
+
router.push({ name: PAGE_ROUTES.history }).catch(() => {});
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
// Direct-link to /history has no prior entry to go back to.
|
|
774
|
+
// vue-router exposes the previous entry on window.history.state.back;
|
|
775
|
+
// when null, fall back to /chat so the button still closes the panel.
|
|
776
|
+
const hasBack = typeof window !== "undefined" && window.history.state?.back != null;
|
|
777
|
+
if (hasBack) {
|
|
778
|
+
router.back();
|
|
779
|
+
} else {
|
|
780
|
+
router.push({ name: PAGE_ROUTES.chat }).catch(() => {});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Fetch the session list when entering /history. Not `immediate` on
|
|
785
|
+
// purpose: onMounted() already fires fetchSessions() unconditionally,
|
|
786
|
+
// so the direct-link /history case is already covered — adding an
|
|
787
|
+
// immediate watcher would race two initial fetches against each other
|
|
788
|
+
// (fetchSessions picks snapshot-vs-diff from the mutable cursor at
|
|
789
|
+
// response time, and a late-arriving full snapshot could be misread
|
|
790
|
+
// as a diff, leaving stale deleted sessions in the list).
|
|
791
|
+
watch(
|
|
792
|
+
() => currentPage.value,
|
|
793
|
+
(page) => {
|
|
794
|
+
if (page === PAGE_ROUTES.history) {
|
|
795
|
+
fetchSessions().catch((err) => console.error("[history] fetch failed:", err));
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
// Route workspace-internal links (wiki pages, files, sessions) to the
|
|
801
|
+
// appropriate page. Called from plugin Views via AppApi.
|
|
802
|
+
function navigateToWorkspacePath(href: string): void {
|
|
803
|
+
const target = classifyWorkspacePath(href);
|
|
804
|
+
if (!target) return;
|
|
805
|
+
|
|
806
|
+
switch (target.kind) {
|
|
807
|
+
case "wiki":
|
|
808
|
+
router.push({ name: PAGE_ROUTES.wiki, params: buildWikiRouteParams({ kind: "page", slug: target.slug }) }).catch(() => {});
|
|
809
|
+
break;
|
|
810
|
+
case "file":
|
|
811
|
+
// Path-based files URL (see plans/feat-files-path-url.md) — pass
|
|
812
|
+
// segments as an array so each piece is url-encoded independently
|
|
813
|
+
// and slashes stay as path separators.
|
|
814
|
+
router.push({ name: PAGE_ROUTES.files, params: { pathMatch: target.path.split("/") } }).catch(() => {});
|
|
815
|
+
break;
|
|
816
|
+
case "session":
|
|
817
|
+
handleSessionSelect(target.sessionId);
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function startNewChat(message: string): void {
|
|
823
|
+
// createNewSession sets currentSessionId synchronously (see the
|
|
824
|
+
// comment on its declaration), so the follow-up sendMessage lands
|
|
825
|
+
// in the new session rather than whatever was previously active.
|
|
826
|
+
// Cross-route push behaviour (so browser Back returns to /wiki)
|
|
827
|
+
// is now handled inside createNewSession via the isChatPage check.
|
|
828
|
+
createNewSession(currentRoleId.value);
|
|
829
|
+
void sendMessage(message);
|
|
830
|
+
}
|
|
673
831
|
|
|
674
832
|
// Plugin Views call back into App.vue via provide/inject (#227).
|
|
675
833
|
provideAppApi({
|
|
676
834
|
refreshRoles,
|
|
677
835
|
sendMessage: (message: string) => sendMessage(message),
|
|
836
|
+
startNewChat: (message: string) => startNewChat(message),
|
|
837
|
+
navigateToWorkspacePath: (href: string) => navigateToWorkspacePath(href),
|
|
678
838
|
});
|
|
679
839
|
// Plugin Views that need to tag background work with the current
|
|
680
840
|
// session (e.g. MulmoScript generations) inject this.
|
|
@@ -683,7 +843,6 @@ provideActiveSession(activeSession);
|
|
|
683
843
|
useEventListeners({
|
|
684
844
|
onKeyNavigation: handleKeyNavigation,
|
|
685
845
|
onViewModeShortcut: handleViewModeShortcut,
|
|
686
|
-
onClickOutsideHistory: handleClickOutsideHistory,
|
|
687
846
|
onTeardown: teardownPendingCalls,
|
|
688
847
|
});
|
|
689
848
|
|
|
@@ -691,7 +850,9 @@ onMounted(async () => {
|
|
|
691
850
|
// Fire-and-forget side fetches.
|
|
692
851
|
fetchHealth();
|
|
693
852
|
fetchMcpToolsStatus();
|
|
694
|
-
|
|
853
|
+
// Awaited below before resuming the top session, so we know the
|
|
854
|
+
// sessions list is populated when we pick which one to land on.
|
|
855
|
+
const sessionsReady = fetchSessions();
|
|
695
856
|
// Roles must be loaded before the first session is created, so
|
|
696
857
|
// createNewSession() picks a roleId that exists in the merged
|
|
697
858
|
// role list (built-in + custom).
|
|
@@ -703,18 +864,31 @@ onMounted(async () => {
|
|
|
703
864
|
currentRoleId.value = urlRole;
|
|
704
865
|
}
|
|
705
866
|
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
867
|
+
// Session bootstrap only applies on /chat. On /files, /todos, /wiki,
|
|
868
|
+
// etc. we must not create or load a chat session — doing so would
|
|
869
|
+
// replace the URL with /chat/<new-id> and pull the user off the page
|
|
870
|
+
// they actually loaded.
|
|
871
|
+
//
|
|
872
|
+
// Read the URL's sessionId directly rather than through
|
|
873
|
+
// currentSessionId.value — the route-param watcher isn't `immediate`,
|
|
874
|
+
// so on a hard load of /chat/<id> the ref may still be "" when we
|
|
875
|
+
// reach this code and we'd mistakenly resume the top session.
|
|
876
|
+
if (route.name === PAGE_ROUTES.chat) {
|
|
877
|
+
const urlSessionId = typeof route.params.sessionId === "string" ? route.params.sessionId : "";
|
|
878
|
+
if (urlSessionId) {
|
|
879
|
+
if (currentSessionId.value !== urlSessionId) {
|
|
880
|
+
currentSessionId.value = urlSessionId;
|
|
881
|
+
}
|
|
882
|
+
await loadSession(urlSessionId);
|
|
883
|
+
// loadSession is a no-op when the server returns 404 — in that
|
|
884
|
+
// case sessionMap won't have the id, so fall through to create.
|
|
885
|
+
if (!sessionMap.has(urlSessionId)) {
|
|
886
|
+
createNewSession();
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
await sessionsReady;
|
|
890
|
+
await resumeOrCreateChatSession();
|
|
715
891
|
}
|
|
716
|
-
} else {
|
|
717
|
-
createNewSession();
|
|
718
892
|
}
|
|
719
893
|
});
|
|
720
894
|
</script>
|