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.
Files changed (251) hide show
  1. package/bin/mulmoclaude.js +7 -24
  2. package/client/assets/html2canvas-Cx501zZr-Cv5snK9D.js +5 -0
  3. package/client/assets/index-CubzmCVK.css +2 -0
  4. package/client/assets/{index-D8rhwXLq.js → index-DtcyExH9.js} +80 -61
  5. package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-DnizuhIY.js} +5 -5
  6. package/client/index.html +2 -4
  7. package/package.json +13 -13
  8. package/server/agent/attachmentConverter.ts +2 -2
  9. package/server/agent/config.ts +12 -12
  10. package/server/agent/index.ts +9 -3
  11. package/server/agent/mcp-server.ts +19 -19
  12. package/server/agent/mcp-tools/index.ts +6 -6
  13. package/server/agent/mcp-tools/x.ts +7 -6
  14. package/server/agent/prompt.ts +195 -29
  15. package/server/agent/resumeFailover.ts +5 -5
  16. package/server/agent/sandboxMounts.ts +10 -10
  17. package/server/agent/stream.ts +4 -4
  18. package/server/api/auth/bearerAuth.ts +3 -3
  19. package/server/api/auth/token.ts +2 -2
  20. package/server/api/routes/agent.ts +21 -3
  21. package/server/api/routes/config.ts +1 -1
  22. package/server/api/routes/files.ts +22 -21
  23. package/server/api/routes/html.ts +2 -2
  24. package/server/api/routes/image.ts +7 -7
  25. package/server/api/routes/mulmo-script.ts +33 -31
  26. package/server/api/routes/pdf.ts +2 -2
  27. package/server/api/routes/plugins.ts +16 -6
  28. package/server/api/routes/roles.ts +2 -2
  29. package/server/api/routes/scheduler.ts +14 -12
  30. package/server/api/routes/schedulerHandlers.ts +12 -12
  31. package/server/api/routes/schedulerTasks.ts +19 -17
  32. package/server/api/routes/sessions.ts +26 -26
  33. package/server/api/routes/sessionsCursor.ts +4 -4
  34. package/server/api/routes/skills.ts +5 -5
  35. package/server/api/routes/sources.ts +3 -3
  36. package/server/api/routes/todosColumnsHandlers.ts +30 -30
  37. package/server/api/routes/todosHandlers.ts +1 -1
  38. package/server/api/routes/todosItemsHandlers.ts +14 -14
  39. package/server/api/routes/wiki.ts +36 -22
  40. package/server/api/sandboxStatus.ts +1 -1
  41. package/server/events/notifications.ts +6 -6
  42. package/server/events/pub-sub/index.ts +3 -3
  43. package/server/events/relay-client.ts +17 -16
  44. package/server/events/scheduler-adapter.ts +20 -20
  45. package/server/events/session-store/index.ts +10 -10
  46. package/server/events/task-manager/index.ts +7 -7
  47. package/server/index.ts +59 -65
  48. package/server/system/config.ts +5 -5
  49. package/server/system/credentials.ts +7 -5
  50. package/server/system/env.ts +5 -5
  51. package/server/utils/date.ts +18 -18
  52. package/server/utils/files/atomic.ts +16 -16
  53. package/server/utils/files/html-io.ts +5 -5
  54. package/server/utils/files/image-store.ts +19 -8
  55. package/server/utils/files/journal-io.ts +4 -4
  56. package/server/utils/files/json.ts +5 -5
  57. package/server/utils/files/markdown-store.ts +4 -4
  58. package/server/utils/files/naming.ts +2 -2
  59. package/server/utils/files/reference-dirs-io.ts +3 -3
  60. package/server/utils/files/roles-io.ts +12 -12
  61. package/server/utils/files/safe.ts +14 -14
  62. package/server/utils/files/scheduler-io.ts +5 -5
  63. package/server/utils/files/scheduler-overrides-io.ts +2 -2
  64. package/server/utils/files/session-io.ts +35 -35
  65. package/server/utils/files/spreadsheet-store.ts +7 -7
  66. package/server/utils/files/todos-io.ts +9 -9
  67. package/server/utils/files/user-tasks-io.ts +5 -5
  68. package/server/utils/files/workspace-io.ts +12 -12
  69. package/server/utils/gemini.ts +2 -2
  70. package/server/utils/gitignore.ts +9 -9
  71. package/server/utils/json.ts +5 -5
  72. package/server/utils/logBackgroundError.ts +12 -3
  73. package/server/utils/markdown.ts +5 -5
  74. package/server/utils/port.d.mts +6 -0
  75. package/server/utils/port.mjs +48 -0
  76. package/server/utils/request.ts +12 -6
  77. package/server/utils/spawn.ts +1 -1
  78. package/server/utils/types.ts +2 -2
  79. package/server/workspace/chat-index/indexer.ts +15 -15
  80. package/server/workspace/chat-index/summarizer.ts +4 -4
  81. package/server/workspace/custom-dirs.ts +16 -16
  82. package/server/workspace/journal/archivist.ts +35 -35
  83. package/server/workspace/journal/dailyPass.ts +31 -28
  84. package/server/workspace/journal/diff.ts +2 -2
  85. package/server/workspace/journal/index.ts +4 -4
  86. package/server/workspace/journal/indexFile.ts +29 -25
  87. package/server/workspace/journal/optimizationPass.ts +2 -2
  88. package/server/workspace/journal/state.ts +6 -6
  89. package/server/workspace/paths.ts +3 -3
  90. package/server/workspace/reference-dirs.ts +20 -20
  91. package/server/workspace/roles.ts +6 -6
  92. package/server/workspace/skills/discovery.ts +4 -4
  93. package/server/workspace/skills/parser.ts +6 -6
  94. package/server/workspace/skills/scheduler.ts +3 -3
  95. package/server/workspace/skills/user-tasks.ts +34 -34
  96. package/server/workspace/skills/writer.ts +3 -3
  97. package/server/workspace/sources/arxivDiscovery.ts +10 -10
  98. package/server/workspace/sources/classifier.ts +7 -7
  99. package/server/workspace/sources/fetchers/arxiv.ts +7 -7
  100. package/server/workspace/sources/fetchers/githubIssues.ts +7 -7
  101. package/server/workspace/sources/fetchers/githubReleases.ts +7 -7
  102. package/server/workspace/sources/fetchers/rss.ts +5 -5
  103. package/server/workspace/sources/fetchers/rssParser.ts +4 -4
  104. package/server/workspace/sources/interests.ts +12 -12
  105. package/server/workspace/sources/paths.ts +6 -6
  106. package/server/workspace/sources/pipeline/fetch.ts +36 -13
  107. package/server/workspace/sources/pipeline/index.ts +8 -13
  108. package/server/workspace/sources/pipeline/notify.ts +3 -3
  109. package/server/workspace/sources/pipeline/plan.ts +15 -13
  110. package/server/workspace/sources/pipeline/write.ts +5 -5
  111. package/server/workspace/sources/rateLimiter.ts +1 -1
  112. package/server/workspace/sources/registry.ts +16 -16
  113. package/server/workspace/sources/robots.ts +14 -14
  114. package/server/workspace/sources/sourceState.ts +17 -10
  115. package/server/workspace/sources/types.ts +9 -0
  116. package/server/workspace/sources/urls.ts +1 -1
  117. package/server/workspace/tool-trace/classify.ts +4 -4
  118. package/server/workspace/tool-trace/index.ts +1 -1
  119. package/server/workspace/tool-trace/writeSearch.ts +26 -16
  120. package/server/workspace/wiki-backlinks/index.ts +8 -8
  121. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +15 -15
  122. package/server/workspace/workspace.ts +7 -7
  123. package/src/App.vue +315 -141
  124. package/src/components/CanvasViewToggle.vue +10 -7
  125. package/src/components/ChatInput.vue +67 -33
  126. package/src/components/FileContentHeader.vue +7 -4
  127. package/src/components/FileContentRenderer.vue +20 -6
  128. package/src/components/FileTree.vue +6 -3
  129. package/src/components/FileTreePane.vue +11 -8
  130. package/src/components/FilesView.vue +5 -3
  131. package/src/components/LockStatusPopup.vue +17 -14
  132. package/src/components/NotificationBell.vue +14 -5
  133. package/src/components/NotificationToast.vue +6 -3
  134. package/src/components/PluginLauncher.vue +19 -56
  135. package/src/components/RightSidebar.vue +13 -10
  136. package/src/components/RoleSelector.vue +2 -2
  137. package/src/components/SessionHistoryPanel.vue +38 -34
  138. package/src/components/SessionTabBar.vue +8 -10
  139. package/src/components/SettingsMcpTab.vue +49 -36
  140. package/src/components/SettingsModal.vue +24 -22
  141. package/src/components/SettingsReferenceDirsTab.vue +39 -34
  142. package/src/components/SettingsWorkspaceDirsTab.vue +37 -27
  143. package/src/components/SidebarHeader.vue +25 -4
  144. package/src/components/StackView.vue +4 -1
  145. package/src/components/SuggestionsPanel.vue +7 -4
  146. package/src/components/TodoExplorer.vue +26 -15
  147. package/src/components/ToolResultsPanel.vue +27 -13
  148. package/src/components/todo/TodoAddDialog.vue +19 -14
  149. package/src/components/todo/TodoEditDialog.vue +7 -2
  150. package/src/components/todo/TodoEditPanel.vue +17 -12
  151. package/src/components/todo/TodoKanbanView.vue +10 -5
  152. package/src/components/todo/TodoListView.vue +10 -7
  153. package/src/components/todo/TodoTableView.vue +5 -2
  154. package/src/composables/useAppApi.ts +9 -0
  155. package/src/composables/useClickOutside.ts +2 -2
  156. package/src/composables/useDynamicFavicon.ts +172 -37
  157. package/src/composables/useEventListeners.ts +7 -8
  158. package/src/composables/useFaviconState.ts +13 -2
  159. package/src/composables/useFileSelection.ts +24 -6
  160. package/src/composables/useFreshPluginData.ts +3 -3
  161. package/src/composables/useKeyNavigation.ts +11 -11
  162. package/src/composables/useLayoutMode.ts +32 -0
  163. package/src/composables/useMcpTools.ts +2 -2
  164. package/src/composables/useNotifications.ts +3 -3
  165. package/src/composables/usePdfDownload.ts +4 -4
  166. package/src/composables/usePendingCalls.ts +1 -1
  167. package/src/composables/usePubSub.ts +10 -10
  168. package/src/composables/useRoles.ts +1 -1
  169. package/src/composables/useSandboxStatus.ts +1 -1
  170. package/src/composables/useSessionDerived.ts +3 -3
  171. package/src/composables/useSessionHistory.ts +7 -17
  172. package/src/composables/useSessionSync.ts +8 -8
  173. package/src/composables/useViewLayout.ts +20 -34
  174. package/src/config/roles.ts +2 -2
  175. package/src/lang/de.ts +536 -0
  176. package/src/lang/en.ts +558 -0
  177. package/src/lang/es.ts +543 -0
  178. package/src/lang/fr.ts +536 -0
  179. package/src/lang/ja.ts +536 -0
  180. package/src/lang/ko.ts +540 -0
  181. package/src/lang/pt-BR.ts +534 -0
  182. package/src/lang/zh.ts +537 -0
  183. package/src/lib/vue-i18n.ts +97 -0
  184. package/src/main.ts +2 -0
  185. package/src/plugins/canvas/View.vue +102 -186
  186. package/src/plugins/canvas/definition.ts +0 -8
  187. package/src/plugins/chart/Preview.vue +5 -5
  188. package/src/plugins/chart/View.vue +9 -4
  189. package/src/plugins/manageRoles/Preview.vue +4 -1
  190. package/src/plugins/manageRoles/View.vue +59 -43
  191. package/src/plugins/manageSkills/Preview.vue +8 -3
  192. package/src/plugins/manageSkills/View.vue +29 -25
  193. package/src/plugins/manageSource/Preview.vue +2 -2
  194. package/src/plugins/manageSource/View.vue +73 -52
  195. package/src/plugins/markdown/Preview.vue +1 -1
  196. package/src/plugins/markdown/View.vue +26 -36
  197. package/src/plugins/presentHtml/Preview.vue +1 -1
  198. package/src/plugins/presentHtml/View.vue +7 -4
  199. package/src/plugins/presentHtml/helpers.ts +8 -8
  200. package/src/plugins/presentMulmoScript/Preview.vue +1 -1
  201. package/src/plugins/presentMulmoScript/View.vue +40 -30
  202. package/src/plugins/presentMulmoScript/helpers.ts +1 -1
  203. package/src/plugins/scheduler/Preview.vue +13 -10
  204. package/src/plugins/scheduler/TasksTab.vue +57 -28
  205. package/src/plugins/scheduler/View.vue +28 -19
  206. package/src/plugins/scheduler/formatSchedule.ts +93 -0
  207. package/src/plugins/spreadsheet/Preview.vue +8 -3
  208. package/src/plugins/spreadsheet/View.vue +21 -12
  209. package/src/plugins/textResponse/Preview.vue +15 -58
  210. package/src/plugins/textResponse/View.vue +29 -9
  211. package/src/plugins/todo/Preview.vue +13 -8
  212. package/src/plugins/todo/View.vue +38 -24
  213. package/src/plugins/todo/composables/useTodos.ts +5 -5
  214. package/src/plugins/ui-image/ImagePreview.vue +6 -3
  215. package/src/plugins/ui-image/ImageView.vue +7 -4
  216. package/src/plugins/wiki/Preview.vue +10 -7
  217. package/src/plugins/wiki/View.vue +202 -81
  218. package/src/plugins/wiki/helpers.ts +4 -4
  219. package/src/plugins/wiki/route.ts +112 -0
  220. package/src/router/guards.ts +46 -28
  221. package/src/router/index.ts +41 -26
  222. package/src/types/session.ts +4 -3
  223. package/src/types/vue-i18n.d.ts +20 -0
  224. package/src/utils/agent/request.ts +22 -3
  225. package/src/utils/canvas/layoutMode.ts +26 -0
  226. package/src/utils/dom/scrollable.ts +2 -2
  227. package/src/utils/files/expandedDirs.ts +1 -1
  228. package/src/utils/files/sortChildren.ts +6 -6
  229. package/src/utils/format/frontmatter.ts +6 -6
  230. package/src/utils/image/cacheBust.ts +16 -0
  231. package/src/utils/image/resolve.ts +16 -0
  232. package/src/utils/image/rewriteMarkdownImageRefs.ts +5 -5
  233. package/src/utils/markdown/extractFirstH1.ts +2 -2
  234. package/src/utils/path/relativeLink.ts +15 -15
  235. package/src/utils/path/workspaceLinkRouter.ts +81 -0
  236. package/src/utils/role/icon.ts +2 -2
  237. package/src/utils/role/merge.ts +2 -2
  238. package/src/utils/role/plugins.ts +1 -1
  239. package/src/utils/session/sessionFactory.ts +2 -2
  240. package/src/utils/session/sessionHelpers.ts +2 -2
  241. package/src/utils/tools/dedup.ts +4 -4
  242. package/src/utils/tools/result.ts +3 -3
  243. package/src/utils/types.ts +2 -2
  244. package/src/vite-env.d.ts +9 -0
  245. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
  246. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
  247. package/client/assets/index-KNLBjwuh.css +0 -1
  248. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
  249. package/src/composables/useCanvasViewMode.ts +0 -121
  250. package/src/utils/canvas/viewMode.ts +0 -46
  251. /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 ref="topBarRef" class="shrink-0 bg-white text-gray-900">
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="canvasViewMode" @navigate="onPluginNavigate" />
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="canvasViewMode" @update:model-value="setCanvasViewMode" />
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="showHistory"
32
+ :history-open="currentPage === 'history'"
32
33
  @new-session="handleNewSessionClick"
33
34
  @load-session="handleSessionSelect"
34
- @toggle-history="toggleHistory"
35
+ @toggle-history="handleHistoryClick"
35
36
  />
36
37
  </div>
37
38
  </div>
38
39
 
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
- />
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
- 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.
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
- 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.
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
- <!-- Single mode -->
99
- <template v-if="canvasViewMode === 'single'">
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>Start a conversation</p>
109
+ <p>{{ t("app.startConversation") }}</p>
112
110
  </div>
113
111
  </template>
114
- <!-- Stack mode -->
115
112
  <StackView
116
- v-else-if="canvasViewMode === 'stack'"
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
- <!-- 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'" />
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="canvasViewMode === 'stack'" class="border-t border-gray-200 bg-white shrink-0">
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 { CANVAS_VIEW } from "./utils/canvas/viewMode";
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 { useCanvasViewMode } from "./composables/useCanvasViewMode";
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 id = currentRoleId.value;
257
- if (!id || roles.value.length === 0 || id === roles.value[0]?.id) return {};
258
- return { role: id };
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(id: string, replace = false): void {
262
- currentSessionId.value = id;
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: "chat",
266
- params: { sessionId: id },
267
- query: { ...buildViewQuery(), ...buildRoleQuery() },
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
- setCanvasViewMode(CANVAS_VIEW[target.view]);
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, showHistory, historyError, fetchSessions, toggleHistory } = useSessionHistory();
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
- useFaviconState({ isRunning, currentSummary, activeSession });
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 { canvasViewMode, setCanvasViewMode, buildViewQuery, filesRefreshToken, handleViewModeShortcut, onPluginNavigate } = useCanvasViewMode({ isRunning });
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
- // 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,
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
- // 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);
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
- // 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
- });
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, (id) => {
429
- const session = sessionMap.get(id);
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 !== id) {
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 = id;
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 === 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(id);
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 id = currentSessionId.value;
488
- if (!id) return false;
489
- const session = sessionMap.get(id);
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(id);
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, true);
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
- // 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();
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
- function activateSession(id: string, roleId: string, replace: boolean): void {
519
- const reactiveSession = sessionMap.get(id);
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(id, replace);
526
- showHistory.value = false;
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(id: string) {
530
- if (id === currentSessionId.value && sessionMap.has(id)) return;
531
- const replaced = removeCurrentIfEmpty();
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(id);
630
+ const live = sessionMap.get(sessionId);
534
631
  if (live) {
535
- activateSession(id, live.roleId, replaced);
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(id)));
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((s) => s.id === id),
644
+ serverSummary: sessions.value.find((summary) => summary.id === sessionId),
548
645
  nowIso: new Date().toISOString(),
549
646
  });
550
- sessionMap.set(id, newSession);
551
- activateSession(id, newSession.roleId, replaced);
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
- const { handler: handleClickOutsideHistory } = useClickOutside({
669
- isOpen: showHistory,
670
- buttonRef: historyButtonRef,
671
- popupRef: historyPopupRef,
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
- fetchSessions();
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
- // 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();
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>