mulmoclaude 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/bin/mulmoclaude.js +7 -24
  2. package/client/assets/html2canvas-Cx501zZr-DiKaqnKs.js +5 -0
  3. package/client/assets/{index-eHWB79u5.js → index-C94GcmNa.js} +203 -198
  4. package/client/assets/index-CY-WpQUm.css +2 -0
  5. package/client/assets/{index.es-D4YyL_Dg-BfRHLTZV.js → index.es-D4YyL_Dg-5ipqh8Pe.js} +5 -5
  6. package/client/assets/material-symbols-outlined-NzYEeyps.woff2 +0 -0
  7. package/client/index.html +2 -4
  8. package/package.json +17 -15
  9. package/server/agent/attachmentConverter.ts +2 -2
  10. package/server/agent/backend/claude-code.ts +170 -0
  11. package/server/agent/backend/index.ts +14 -0
  12. package/server/agent/backend/types.ts +65 -0
  13. package/server/agent/index.ts +31 -159
  14. package/server/agent/mcp-server.ts +88 -10
  15. package/server/agent/mcp-tools/index.ts +8 -7
  16. package/server/agent/mcp-tools/notify.ts +76 -0
  17. package/server/agent/mcp-tools/x.ts +12 -2
  18. package/server/agent/plugin-names.ts +10 -4
  19. package/server/agent/prompt.ts +187 -26
  20. package/server/agent/resumeFailover.ts +5 -5
  21. package/server/agent/sandboxMounts.ts +3 -3
  22. package/server/api/auth/bearerAuth.ts +3 -3
  23. package/server/api/auth/token.ts +2 -2
  24. package/server/api/routes/agent.ts +99 -4
  25. package/server/api/routes/chart.ts +13 -0
  26. package/server/api/routes/chat-index.ts +2 -1
  27. package/server/api/routes/config.ts +35 -8
  28. package/server/api/routes/files.ts +75 -24
  29. package/server/api/routes/html.ts +15 -2
  30. package/server/api/routes/image.ts +75 -20
  31. package/server/api/routes/mulmo-script.ts +33 -31
  32. package/server/api/routes/news.ts +146 -0
  33. package/server/api/routes/notifications.ts +58 -2
  34. package/server/api/routes/pdf.ts +2 -2
  35. package/server/api/routes/plugins.ts +73 -91
  36. package/server/api/routes/presentHtml.ts +9 -0
  37. package/server/api/routes/roles.ts +12 -2
  38. package/server/api/routes/scheduler.ts +20 -11
  39. package/server/api/routes/schedulerTasks.ts +58 -21
  40. package/server/api/routes/sessions.ts +15 -4
  41. package/server/api/routes/sessionsCursor.ts +4 -4
  42. package/server/api/routes/skills.ts +26 -5
  43. package/server/api/routes/sources.ts +8 -7
  44. package/server/api/routes/todos.ts +30 -0
  45. package/server/api/routes/todosColumnsHandlers.ts +13 -27
  46. package/server/api/routes/todosHandlers.ts +1 -1
  47. package/server/api/routes/todosItemsHandlers.ts +14 -14
  48. package/server/api/routes/wiki/frontmatter.ts +86 -0
  49. package/server/api/routes/wiki.ts +335 -75
  50. package/server/api/sandboxStatus.ts +1 -1
  51. package/server/events/notifications.ts +32 -8
  52. package/server/events/pub-sub/index.ts +3 -3
  53. package/server/events/relay-client.ts +26 -16
  54. package/server/events/resolveRelayBridgeOptions.ts +125 -0
  55. package/server/index.ts +72 -49
  56. package/server/system/config.ts +5 -5
  57. package/server/system/credentials.ts +7 -5
  58. package/server/system/env.ts +15 -5
  59. package/server/system/macosNotify.ts +152 -0
  60. package/server/utils/errors.ts +11 -2
  61. package/server/utils/fetch.ts +54 -0
  62. package/server/utils/files/atomic.ts +18 -17
  63. package/server/utils/files/image-store.ts +19 -13
  64. package/server/utils/files/journal-io.ts +2 -2
  65. package/server/utils/files/json.ts +5 -5
  66. package/server/utils/files/markdown-image-fill.ts +131 -0
  67. package/server/utils/files/markdown-store.ts +22 -6
  68. package/server/utils/files/naming.ts +20 -10
  69. package/server/utils/files/reference-dirs-io.ts +3 -3
  70. package/server/utils/files/roles-io.ts +4 -4
  71. package/server/utils/files/safe.ts +14 -14
  72. package/server/utils/files/scheduler-overrides-io.ts +2 -2
  73. package/server/utils/files/spreadsheet-store.ts +15 -10
  74. package/server/utils/files/workspace-io.ts +12 -12
  75. package/server/utils/gemini.ts +30 -4
  76. package/server/utils/gitignore.ts +9 -9
  77. package/server/utils/id.ts +40 -8
  78. package/server/utils/json.ts +5 -5
  79. package/server/utils/logBackgroundError.ts +12 -3
  80. package/server/utils/logPreview.ts +24 -0
  81. package/server/utils/markdown.ts +5 -5
  82. package/server/utils/port.d.mts +6 -0
  83. package/server/utils/port.mjs +48 -0
  84. package/server/utils/promptMeta.ts +32 -0
  85. package/server/utils/request.ts +12 -6
  86. package/server/utils/slug.ts +65 -4
  87. package/server/utils/spawn.ts +1 -1
  88. package/server/utils/types.ts +2 -2
  89. package/server/workspace/chat-index/index.ts +1 -1
  90. package/server/workspace/chat-index/summarizer.ts +5 -5
  91. package/server/workspace/custom-dirs.ts +5 -5
  92. package/server/workspace/helps/gemini.md +57 -0
  93. package/server/workspace/helps/index.md +2 -1
  94. package/server/workspace/helps/sources.md +42 -0
  95. package/server/workspace/helps/wiki.md +40 -5
  96. package/server/workspace/journal/archivist-cli.ts +121 -0
  97. package/server/workspace/journal/{archivist.ts → archivist-schemas.ts} +12 -120
  98. package/server/workspace/journal/dailyPass.ts +78 -38
  99. package/server/workspace/journal/diff.ts +2 -2
  100. package/server/workspace/journal/index.ts +56 -5
  101. package/server/workspace/journal/memoryExtractor.ts +1 -1
  102. package/server/workspace/journal/optimizationPass.ts +4 -5
  103. package/server/workspace/journal/paths.ts +8 -24
  104. package/server/workspace/journal/state.ts +18 -8
  105. package/server/workspace/news/reader.ts +248 -0
  106. package/server/workspace/paths.ts +4 -3
  107. package/server/workspace/reference-dirs.ts +3 -3
  108. package/server/workspace/skills/parser.ts +6 -6
  109. package/server/workspace/skills/scheduler.ts +5 -4
  110. package/server/workspace/skills/user-tasks.ts +3 -2
  111. package/server/workspace/skills/writer.ts +3 -3
  112. package/server/workspace/sources/arxivDiscovery.ts +2 -2
  113. package/server/workspace/sources/classifier.ts +1 -1
  114. package/server/workspace/sources/fetchers/rss.ts +5 -5
  115. package/server/workspace/sources/fetchers/rssParser.ts +4 -4
  116. package/server/workspace/sources/interests.ts +3 -3
  117. package/server/workspace/sources/paths.ts +6 -6
  118. package/server/workspace/sources/pipeline/fetch.ts +59 -13
  119. package/server/workspace/sources/pipeline/index.ts +59 -7
  120. package/server/workspace/sources/pipeline/notify.ts +13 -5
  121. package/server/workspace/sources/pipeline/plan.ts +11 -9
  122. package/server/workspace/sources/pipeline/summarize.ts +1 -1
  123. package/server/workspace/sources/pipeline/write.ts +5 -5
  124. package/server/workspace/sources/rateLimiter.ts +1 -1
  125. package/server/workspace/sources/sourceState.ts +9 -4
  126. package/server/workspace/sources/types.ts +9 -0
  127. package/server/workspace/sources/urls.ts +1 -1
  128. package/server/workspace/tool-trace/classify.ts +4 -4
  129. package/server/workspace/workspace.ts +7 -7
  130. package/src/App.vue +477 -251
  131. package/src/components/CanvasViewToggle.vue +12 -10
  132. package/src/components/ChatInput.vue +112 -105
  133. package/src/components/FileContentHeader.vue +10 -7
  134. package/src/components/FileContentRenderer.vue +37 -10
  135. package/src/components/FileTree.vue +34 -4
  136. package/src/components/FileTreePane.vue +32 -27
  137. package/src/components/FilesView.vue +5 -3
  138. package/src/components/FilterChip.vue +22 -0
  139. package/src/components/LockStatusPopup.vue +19 -13
  140. package/src/components/NewsView.vue +252 -0
  141. package/src/components/NotificationBell.vue +35 -9
  142. package/src/components/NotificationToast.vue +4 -1
  143. package/src/components/PageChatComposer.vue +101 -0
  144. package/src/components/PluginLauncher.vue +36 -62
  145. package/src/components/RightSidebar.vue +13 -10
  146. package/src/components/RoleSelector.vue +3 -2
  147. package/src/components/SessionHeaderControls.vue +63 -0
  148. package/src/components/SessionHistoryExpandButton.vue +30 -0
  149. package/src/components/SessionHistoryPanel.vue +64 -93
  150. package/src/components/SessionHistoryToggleButton.vue +40 -0
  151. package/src/components/SessionRoleIcon.vue +72 -0
  152. package/src/components/SessionSidebar.vue +96 -0
  153. package/src/components/SessionTabBar.vue +44 -51
  154. package/src/components/SettingsMcpTab.vue +361 -52
  155. package/src/components/SettingsModal.vue +203 -72
  156. package/src/components/SettingsReferenceDirsTab.vue +72 -51
  157. package/src/components/SettingsWorkspaceDirsTab.vue +74 -51
  158. package/src/components/SidebarHeader.vue +50 -16
  159. package/src/components/SourcesManager.vue +900 -0
  160. package/src/components/SourcesView.vue +45 -0
  161. package/src/components/StackView.vue +84 -48
  162. package/src/components/SuggestionsPanel.vue +25 -36
  163. package/src/components/SystemFileBanner.vue +106 -0
  164. package/src/components/ThinkingIndicator.vue +41 -0
  165. package/src/components/TodoExplorer.vue +72 -22
  166. package/src/components/todo/TodoAddDialog.vue +17 -12
  167. package/src/components/todo/TodoEditDialog.vue +7 -2
  168. package/src/components/todo/TodoEditPanel.vue +15 -10
  169. package/src/components/todo/TodoKanbanView.vue +16 -6
  170. package/src/components/todo/TodoListView.vue +14 -3
  171. package/src/components/todo/TodoTableView.vue +36 -5
  172. package/src/composables/favicon/conditions.ts +76 -0
  173. package/src/composables/favicon/resolveColor.ts +93 -0
  174. package/src/composables/favicon/types.ts +61 -0
  175. package/src/composables/useAppApi.ts +23 -0
  176. package/src/composables/useChatScroll.ts +5 -5
  177. package/src/composables/useCurrentRole.ts +32 -0
  178. package/src/composables/useDynamicFavicon.ts +174 -58
  179. package/src/composables/useEventListeners.ts +7 -12
  180. package/src/composables/useFaviconState.ts +93 -12
  181. package/src/composables/useFileSelection.ts +25 -6
  182. package/src/composables/useHealth.ts +76 -7
  183. package/src/composables/useLayoutMode.ts +27 -0
  184. package/src/composables/useNewsItems.ts +38 -0
  185. package/src/composables/useNewsReadState.ts +75 -0
  186. package/src/composables/useNotifications.ts +76 -13
  187. package/src/composables/usePendingCalls.ts +11 -1
  188. package/src/composables/useRoles.ts +6 -10
  189. package/src/composables/useRunElapsed.ts +80 -0
  190. package/src/composables/useSessionDerived.ts +21 -5
  191. package/src/composables/useSessionHistory.ts +7 -17
  192. package/src/composables/useSidePanelVisible.ts +25 -0
  193. package/src/composables/useViewLayout.ts +16 -37
  194. package/src/config/apiRoutes.ts +19 -6
  195. package/src/config/historyFilters.ts +30 -0
  196. package/src/config/mcpCatalog.ts +285 -0
  197. package/src/config/mcpTypes.ts +26 -0
  198. package/src/config/roles.ts +19 -51
  199. package/src/config/systemFileDescriptors.ts +170 -0
  200. package/src/config/toolNames.ts +6 -1
  201. package/src/config/workspacePaths.ts +1 -0
  202. package/src/index.css +14 -0
  203. package/src/lang/de.ts +706 -0
  204. package/src/lang/en.ts +726 -0
  205. package/src/lang/es.ts +712 -0
  206. package/src/lang/fr.ts +704 -0
  207. package/src/lang/ja.ts +707 -0
  208. package/src/lang/ko.ts +709 -0
  209. package/src/lang/pt-BR.ts +702 -0
  210. package/src/lang/zh.ts +705 -0
  211. package/src/lib/vue-i18n.ts +97 -0
  212. package/src/main.ts +3 -0
  213. package/src/plugins/canvas/View.vue +104 -186
  214. package/src/plugins/canvas/definition.ts +0 -8
  215. package/src/plugins/canvas/index.ts +3 -2
  216. package/src/plugins/chart/Preview.vue +1 -1
  217. package/src/plugins/chart/View.vue +9 -4
  218. package/src/plugins/chart/index.ts +3 -2
  219. package/src/plugins/editImage/index.ts +3 -2
  220. package/src/plugins/generateImage/index.ts +3 -2
  221. package/src/plugins/manageRoles/Preview.vue +4 -1
  222. package/src/plugins/manageRoles/View.vue +67 -46
  223. package/src/plugins/manageRoles/index.ts +3 -2
  224. package/src/plugins/manageSkills/Preview.vue +8 -3
  225. package/src/plugins/manageSkills/View.vue +39 -34
  226. package/src/plugins/manageSkills/index.ts +3 -2
  227. package/src/plugins/manageSource/Preview.vue +1 -1
  228. package/src/plugins/manageSource/View.vue +3 -687
  229. package/src/plugins/manageSource/index.ts +3 -2
  230. package/src/plugins/markdown/Preview.vue +1 -1
  231. package/src/plugins/markdown/View.vue +164 -73
  232. package/src/plugins/markdown/definition.ts +6 -4
  233. package/src/plugins/markdown/index.ts +3 -2
  234. package/src/plugins/presentForm/Preview.vue +99 -0
  235. package/src/plugins/presentForm/View.vue +675 -0
  236. package/src/plugins/presentForm/definition.ts +127 -0
  237. package/src/plugins/presentForm/index.ts +18 -0
  238. package/src/plugins/presentForm/plugin.ts +94 -0
  239. package/src/plugins/presentForm/types.ts +109 -0
  240. package/src/plugins/presentHtml/Preview.vue +1 -1
  241. package/src/plugins/presentHtml/View.vue +7 -4
  242. package/src/plugins/presentHtml/index.ts +3 -2
  243. package/src/plugins/presentMulmoScript/Preview.vue +1 -1
  244. package/src/plugins/presentMulmoScript/View.vue +36 -26
  245. package/src/plugins/presentMulmoScript/index.ts +3 -2
  246. package/src/plugins/scheduler/AutomationsPreview.vue +37 -0
  247. package/src/plugins/scheduler/AutomationsView.vue +23 -0
  248. package/src/plugins/scheduler/CalendarView.vue +23 -0
  249. package/src/plugins/scheduler/LegacySchedulerView.vue +32 -0
  250. package/src/plugins/scheduler/Preview.vue +7 -4
  251. package/src/plugins/scheduler/TasksTab.vue +119 -28
  252. package/src/plugins/scheduler/View.vue +75 -32
  253. package/src/plugins/scheduler/automationsDefinition.ts +58 -0
  254. package/src/plugins/scheduler/calendarDefinition.ts +46 -0
  255. package/src/plugins/scheduler/formatSchedule.ts +93 -0
  256. package/src/plugins/scheduler/index.ts +68 -14
  257. package/src/plugins/scheduler/legacyShape.ts +34 -0
  258. package/src/plugins/spreadsheet/Preview.vue +9 -5
  259. package/src/plugins/spreadsheet/View.vue +43 -57
  260. package/src/plugins/spreadsheet/engine/responseDecoder.ts +2 -1
  261. package/src/plugins/spreadsheet/index.ts +3 -2
  262. package/src/plugins/textResponse/Preview.vue +15 -58
  263. package/src/plugins/textResponse/View.vue +42 -45
  264. package/src/plugins/textResponse/utils.ts +25 -0
  265. package/src/plugins/todo/Preview.vue +11 -6
  266. package/src/plugins/todo/View.vue +27 -13
  267. package/src/plugins/todo/composables/useTodos.ts +3 -1
  268. package/src/plugins/todo/index.ts +3 -2
  269. package/src/plugins/ui-image/ImagePreview.vue +6 -3
  270. package/src/plugins/ui-image/ImageView.vue +7 -4
  271. package/src/plugins/wiki/Preview.vue +5 -2
  272. package/src/plugins/wiki/View.vue +539 -92
  273. package/src/plugins/wiki/index.ts +5 -2
  274. package/src/plugins/wiki/route.ts +121 -0
  275. package/src/router/guards.ts +43 -24
  276. package/src/router/index.ts +53 -26
  277. package/src/router/pageRoutes.ts +23 -0
  278. package/src/tools/index.ts +12 -5
  279. package/src/tools/legacyPluginNames.ts +13 -0
  280. package/src/types/notification.ts +31 -6
  281. package/src/types/vue-i18n.d.ts +20 -0
  282. package/src/utils/agent/eventDispatch.ts +3 -6
  283. package/src/utils/agent/formatElapsed.ts +37 -0
  284. package/src/utils/agent/request.ts +22 -1
  285. package/src/utils/canvas/layoutMode.ts +26 -0
  286. package/src/utils/canvas/sidePanelVisible.ts +19 -0
  287. package/src/utils/dom/scrollIntoViewByTestId.ts +38 -0
  288. package/src/utils/errors.ts +9 -2
  289. package/src/utils/files/filename.ts +24 -0
  290. package/src/utils/filesPreview/schedulerPreview.ts +9 -3
  291. package/src/utils/id.ts +18 -0
  292. package/src/utils/image/cacheBust.ts +16 -0
  293. package/src/utils/image/resolve.ts +16 -0
  294. package/src/utils/markdown/taskList.ts +175 -0
  295. package/src/utils/mcp/interpolateSpec.ts +97 -0
  296. package/src/utils/notification/dispatch.ts +51 -15
  297. package/src/utils/path/workspaceLinkRouter.ts +99 -0
  298. package/src/utils/session/mergeSessions.ts +5 -0
  299. package/src/utils/sources/filter.ts +69 -0
  300. package/src/vite-env.d.ts +9 -0
  301. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +0 -1
  302. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +0 -5
  303. package/client/assets/index-Bm70FDU2.css +0 -1
  304. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +0 -1
  305. package/server/workspace/journal/linkRewrite.ts +0 -4
  306. package/src/components/ToolResultsPanel.vue +0 -77
  307. package/src/composables/useCanvasViewMode.ts +0 -121
  308. package/src/plugins/scheduler/definition.ts +0 -57
  309. package/src/utils/canvas/viewMode.ts +0 -46
  310. package/src/utils/role/plugins.ts +0 -12
  311. package/src/utils/session/seedRoleDefault.ts +0 -35
  312. /package/client/assets/{purify.es-Fx1Nqyry-PeS5RUhs.js → purify.es-Fx1Nqyry-BwJECkqS.js} +0 -0
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <SourcesManager mode="page" />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { nextTick, onMounted, watch } from "vue";
7
+ import { useRoute } from "vue-router";
8
+ import SourcesManager from "./SourcesManager.vue";
9
+ import { scrollIntoViewByTestId } from "../utils/dom/scrollIntoViewByTestId";
10
+
11
+ // Permalink support (#762): arrivals on /sources/:slug scroll and
12
+ // flash the matching source row. SourcesManager fetches the list
13
+ // asynchronously and renders `source-row-<slug>` testids per row,
14
+ // so we retry a couple of times while the API settles before
15
+ // giving up — avoids a race on first mount when the fetch hasn't
16
+ // resolved yet.
17
+ const MAX_RETRIES = 10;
18
+ const RETRY_INTERVAL_MS = 150;
19
+
20
+ const route = useRoute();
21
+
22
+ async function focusUrlSlug(slug: string): Promise<void> {
23
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
24
+ await nextTick();
25
+ if (scrollIntoViewByTestId(`source-row-${slug}`)) return;
26
+ await new Promise((resolve) => window.setTimeout(resolve, RETRY_INTERVAL_MS));
27
+ }
28
+ }
29
+
30
+ onMounted(() => {
31
+ const slug = route.params.slug;
32
+ if (typeof slug === "string" && slug) {
33
+ void focusUrlSlug(slug);
34
+ }
35
+ });
36
+
37
+ watch(
38
+ () => route.params.slug,
39
+ (slug) => {
40
+ if (typeof slug === "string" && slug) {
41
+ void focusUrlSlug(slug);
42
+ }
43
+ },
44
+ );
45
+ </script>
@@ -1,26 +1,50 @@
1
1
  <template>
2
- <div ref="containerRef" class="h-full overflow-y-auto bg-gray-50 p-4 space-y-3" data-testid="stack-scroll">
3
- <div v-if="toolResults.length === 0" class="flex items-center justify-center h-full text-gray-400 text-sm">No results yet</div>
4
- <div
5
- v-for="result in toolResults"
6
- :key="result.uuid"
7
- :ref="(element) => setItemRef(result.uuid, element as HTMLElement | null)"
8
- class="bg-white rounded-lg border transition-colors"
9
- :class="result.uuid === selectedResultUuid ? 'border-blue-400 ring-2 ring-blue-200' : 'border-gray-200'"
10
- >
11
- <button
12
- class="w-full flex items-center gap-2 px-3 py-2 border-b border-gray-100 text-left hover:bg-gray-50"
13
- :title="result.title || result.toolName"
14
- @click="emit('select', result.uuid)"
2
+ <div class="h-full flex flex-col bg-gray-50">
3
+ <div class="shrink-0 flex items-center gap-1 text-xs text-gray-400 px-4 pt-3 pb-2" data-testid="stack-role-header">
4
+ <span v-if="sessionRoleIcon" class="material-icons text-xs leading-none">{{ sessionRoleIcon }}</span>
5
+ <span v-if="sessionRoleName">{{ sessionRoleName }}</span>
6
+ <div class="ml-auto flex items-center gap-1">
7
+ <button
8
+ type="button"
9
+ class="text-gray-400 hover:text-gray-700"
10
+ :class="{ 'text-blue-500': showRightSidebar }"
11
+ :title="t('sidebarHeader.toolCallHistory')"
12
+ :aria-label="t('sidebarHeader.toolCallHistory')"
13
+ :aria-pressed="showRightSidebar"
14
+ @click="emit('toggle-right-sidebar')"
15
+ >
16
+ <span class="material-icons text-lg" aria-hidden="true">build</span>
17
+ </button>
18
+ <CanvasViewToggle :model-value="layoutMode" @update:model-value="(mode) => emit('update:layoutMode', mode)" />
19
+ </div>
20
+ </div>
21
+ <!-- Empty state pulled out of the scroll container so `h-full` +
22
+ the container's `pb-4` padding can't combine into a stray
23
+ scrollbar. A sibling `flex-1` slot centers cleanly. -->
24
+ <div v-if="toolResults.length === 0" class="flex-1 flex items-center justify-center text-gray-400 text-sm" data-testid="stack-empty">
25
+ {{ t("common.noResultsYet") }}
26
+ </div>
27
+ <div v-else ref="containerRef" class="flex-1 min-h-0 overflow-y-auto px-4 pb-4 space-y-3" data-testid="stack-scroll">
28
+ <div
29
+ v-for="result in toolResults"
30
+ :key="result.uuid"
31
+ :ref="(element) => setItemRef(result.uuid, element as HTMLElement | null)"
32
+ class="bg-white rounded-lg border transition-colors"
33
+ :class="result.uuid === selectedResultUuid ? 'border-blue-400 ring-2 ring-blue-200' : 'border-gray-200'"
15
34
  >
16
- <span class="material-icons text-sm text-gray-400">{{ iconFor(result.toolName) }}</span>
17
- <span class="text-sm font-medium text-gray-800 truncate">{{ result.title || result.toolName }}</span>
18
- <span v-if="resultTimestamps.get(result.uuid)" class="text-[10px] text-gray-400 shrink-0">{{
19
- formatSmartTime(resultTimestamps.get(result.uuid)!)
20
- }}</span>
21
- <span class="font-mono text-xs text-gray-400 shrink-0">{{ result.toolName }}</span>
22
- </button>
23
- <!-- text-response: render the message as Markdown via the
35
+ <button
36
+ class="w-full flex items-center gap-2 px-3 py-2 border-b border-gray-100 text-left hover:bg-gray-50"
37
+ :title="result.title || result.toolName"
38
+ @click="emit('select', result.uuid)"
39
+ >
40
+ <span class="material-icons text-sm text-gray-400">{{ iconFor(result.toolName) }}</span>
41
+ <span class="text-sm font-medium text-gray-800 truncate">{{ result.title || result.toolName }}</span>
42
+ <span v-if="resultTimestamps.get(result.uuid)" class="text-[10px] text-gray-400 shrink-0">{{
43
+ formatSmartTime(resultTimestamps.get(result.uuid)!)
44
+ }}</span>
45
+ <span class="font-mono text-xs text-gray-400 shrink-0">{{ result.toolName }}</span>
46
+ </button>
47
+ <!-- text-response: render the message as Markdown via the
24
48
  underlying plugin View. The .stack-text-response class below
25
49
  collapses the plugin's own card chrome (outer p-6, inner
26
50
  rounded/border/shadow box, role header) so only the stack
@@ -31,38 +55,39 @@
31
55
  "open external links in a new tab" click handler. Attach
32
56
  the same handler here via @click.capture so cross-origin
33
57
  links in assistant Markdown don't navigate the SPA away. -->
34
- <div v-if="isTextResponse(result)" class="stack-text-response" @click.capture="handleExternalLinkClick">
35
- <TextResponseOriginalView :selected-result="result" />
36
- </div>
37
- <!-- Document-like plugins: let the content flow at its natural
58
+ <div v-if="isTextResponse(result)" class="stack-text-response" @click.capture="handleExternalLinkClick">
59
+ <TextResponseOriginalView :selected-result="result" />
60
+ </div>
61
+ <!-- Document-like plugins: let the content flow at its natural
38
62
  height by overriding the plugin's internal h-full / overflow
39
63
  / flex-1 via the .stack-natural scoped styles below. For
40
64
  plugins that embed iframes (e.g. presentHtml) we also size
41
65
  each iframe to its content after load. -->
42
- <div
43
- v-else-if="isStackNatural(result.toolName)"
44
- :ref="(element) => setNaturalWrapperRef(result.uuid, element as HTMLElement | null)"
45
- class="stack-natural"
46
- >
47
- <component
48
- :is="getPlugin(result.toolName)?.viewComponent"
49
- v-if="getPlugin(result.toolName)?.viewComponent"
50
- :selected-result="result"
51
- :send-text-message="sendTextMessage"
52
- @update-result="(r: ToolResultComplete) => emit('updateResult', r)"
53
- />
54
- </div>
55
- <!-- Other plugins: fixed height wrapper so plugins that rely on
66
+ <div
67
+ v-else-if="isStackNatural(result.toolName)"
68
+ :ref="(element) => setNaturalWrapperRef(result.uuid, element as HTMLElement | null)"
69
+ class="stack-natural"
70
+ >
71
+ <component
72
+ :is="getPlugin(result.toolName)?.viewComponent"
73
+ v-if="getPlugin(result.toolName)?.viewComponent"
74
+ :selected-result="result"
75
+ :send-text-message="sendTextMessage"
76
+ @update-result="(r: ToolResultComplete) => emit('updateResult', r)"
77
+ />
78
+ </div>
79
+ <!-- Other plugins: fixed height wrapper so plugins that rely on
56
80
  h-full continue to render properly. -->
57
- <div v-else :style="{ height: PLUGIN_HEIGHT }">
58
- <component
59
- :is="getPlugin(result.toolName)?.viewComponent"
60
- v-if="getPlugin(result.toolName)?.viewComponent"
61
- :selected-result="result"
62
- :send-text-message="sendTextMessage"
63
- @update-result="(r: ToolResultComplete) => emit('updateResult', r)"
64
- />
65
- <pre v-else class="h-full overflow-auto p-4 text-xs text-gray-500 whitespace-pre-wrap">{{ JSON.stringify(result, null, 2) }}</pre>
81
+ <div v-else :style="{ height: PLUGIN_HEIGHT }">
82
+ <component
83
+ :is="getPlugin(result.toolName)?.viewComponent"
84
+ v-if="getPlugin(result.toolName)?.viewComponent"
85
+ :selected-result="result"
86
+ :send-text-message="sendTextMessage"
87
+ @update-result="(r: ToolResultComplete) => emit('updateResult', r)"
88
+ />
89
+ <pre v-else class="h-full overflow-auto p-4 text-xs text-gray-500 whitespace-pre-wrap">{{ JSON.stringify(result, null, 2) }}</pre>
90
+ </div>
66
91
  </div>
67
92
  </div>
68
93
  </div>
@@ -70,13 +95,18 @@
70
95
 
71
96
  <script setup lang="ts">
72
97
  import { ref, computed, watch, nextTick, onMounted, onUnmounted } from "vue";
98
+ import { useI18n } from "vue-i18n";
73
99
  import { getPlugin } from "../tools";
100
+
101
+ const { t } = useI18n();
74
102
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
75
103
  import { View as TextResponseOriginalView } from "../plugins/textResponse/index";
76
104
  import { handleExternalLinkClick } from "../utils/dom/externalLink";
77
105
  import type { TextResponseData } from "../plugins/textResponse/types";
78
106
  import { formatSmartTime } from "../utils/format/date";
79
107
  import { isRecord } from "../utils/types";
108
+ import CanvasViewToggle from "./CanvasViewToggle.vue";
109
+ import type { LayoutMode } from "../utils/canvas/layoutMode";
80
110
 
81
111
  // Most plugin viewComponents use h-full internally, so a defined parent
82
112
  // height is required for them to render. text-response and the
@@ -110,11 +140,17 @@ const props = defineProps<{
110
140
  selectedResultUuid: string | null;
111
141
  resultTimestamps: Map<string, number>;
112
142
  sendTextMessage?: (text: string) => void;
143
+ sessionRoleName?: string;
144
+ sessionRoleIcon?: string;
145
+ layoutMode: LayoutMode;
146
+ showRightSidebar: boolean;
113
147
  }>();
114
148
 
115
149
  const emit = defineEmits<{
116
150
  select: [uuid: string];
117
151
  updateResult: [result: ToolResultComplete];
152
+ "update:layoutMode": [mode: LayoutMode];
153
+ "toggle-right-sidebar": [];
118
154
  }>();
119
155
 
120
156
  const containerRef = ref<HTMLDivElement | null>(null);
@@ -1,65 +1,54 @@
1
1
  <template>
2
- <div v-if="queries.length > 0" class="border-t border-gray-200">
3
- <div v-if="expanded" ref="listRef" class="px-4 pt-2 max-h-64 overflow-y-auto flex flex-col gap-1">
4
- <button
5
- v-for="query in queries"
6
- :key="query"
7
- class="text-left text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 rounded px-3 py-1.5 border border-gray-300 transition-colors"
8
- @click="onClick($event, query)"
9
- >
10
- {{ query }}
11
- </button>
12
- <p class="text-center text-[10px] text-gray-400 py-0.5">click to send · shift+click to edit</p>
13
- </div>
2
+ <div v-if="expanded && queries.length > 0" ref="listRef" class="border-t border-gray-200 px-4 pt-2 pb-2 max-h-64 overflow-y-auto flex flex-col gap-1">
14
3
  <button
15
- class="w-full flex items-center justify-between px-4 py-1 text-xs text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors"
16
- @click="expanded = !expanded"
4
+ v-for="query in queries"
5
+ :key="query"
6
+ class="text-left text-xs bg-gray-100 hover:bg-gray-200 text-gray-700 rounded px-3 py-1.5 border border-gray-300 transition-colors"
7
+ @click="onClick($event, query)"
17
8
  >
18
- <span class="flex items-center gap-1">
19
- <span class="material-icons text-sm">lightbulb</span>
20
- Suggestions
21
- </span>
22
- <span class="material-icons text-sm transition-transform" :class="{ 'rotate-180': !expanded }">expand_less</span>
9
+ {{ query }}
23
10
  </button>
11
+ <p class="text-center text-[10px] text-gray-400 py-0.5">{{ t("suggestionsPanel.sendEditHint") }}</p>
24
12
  </div>
25
13
  </template>
26
14
 
27
15
  <script setup lang="ts">
28
16
  import { nextTick, ref, watch } from "vue";
17
+ import { useI18n } from "vue-i18n";
29
18
 
30
- defineProps<{
19
+ const { t } = useI18n();
20
+
21
+ const props = defineProps<{
31
22
  queries: string[];
23
+ expanded: boolean;
32
24
  }>();
33
25
 
34
26
  const emit = defineEmits<{
27
+ "update:expanded": [value: boolean];
35
28
  send: [query: string];
36
29
  edit: [query: string];
37
30
  }>();
38
31
 
39
- const expanded = ref(false);
40
32
  const listRef = ref<HTMLDivElement | null>(null);
41
33
 
42
- watch(expanded, (isExpanded) => {
43
- if (!isExpanded) return;
44
- nextTick(() => {
45
- if (listRef.value) {
46
- listRef.value.scrollTop = listRef.value.scrollHeight;
47
- }
48
- });
49
- });
34
+ watch(
35
+ () => props.expanded,
36
+ (isExpanded) => {
37
+ if (!isExpanded) return;
38
+ nextTick(() => {
39
+ if (listRef.value) {
40
+ listRef.value.scrollTop = listRef.value.scrollHeight;
41
+ }
42
+ });
43
+ },
44
+ );
50
45
 
51
46
  function onClick(event: MouseEvent, query: string): void {
52
- expanded.value = false;
47
+ emit("update:expanded", false);
53
48
  if (event.shiftKey) {
54
49
  emit("edit", query);
55
50
  return;
56
51
  }
57
52
  emit("send", query);
58
53
  }
59
-
60
- function collapse(): void {
61
- expanded.value = false;
62
- }
63
-
64
- defineExpose({ collapse });
65
54
  </script>
@@ -0,0 +1,106 @@
1
+ <template>
2
+ <div class="m-4 mb-0 rounded border border-blue-200 bg-blue-50 text-sm" data-testid="system-file-banner">
3
+ <div class="flex items-start gap-2 px-3 py-2">
4
+ <span class="text-blue-600 shrink-0" aria-hidden="true">{{ INFO_ICON }}</span>
5
+ <div class="flex-1 min-w-0">
6
+ <div class="flex items-center gap-2 flex-wrap">
7
+ <span class="font-semibold text-blue-900">{{ t(`systemFiles.${descriptor.id}.title`) }}</span>
8
+ <span :class="['text-xs px-1.5 py-0.5 rounded shrink-0', policyChipClass]">
9
+ {{ t(`systemFiles.editPolicy.${descriptor.editPolicy}`) }}
10
+ </span>
11
+ </div>
12
+ <p v-if="!collapsed" class="mt-1 text-gray-700 leading-snug whitespace-pre-line">
13
+ {{ t(`systemFiles.${descriptor.id}.summary`) }}
14
+ </p>
15
+ <p v-if="!collapsed && descriptor.schemaRef" class="mt-1 text-xs text-gray-500">
16
+ {{ t("systemFiles.schemaLabel") }}:
17
+ <a :href="schemaUrl" target="_blank" rel="noopener noreferrer" class="font-mono text-blue-700 hover:underline break-all">
18
+ {{ descriptor.schemaRef }}
19
+ </a>
20
+ </p>
21
+ </div>
22
+ <button
23
+ type="button"
24
+ class="shrink-0 text-xs text-blue-700 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 rounded px-1"
25
+ :aria-label="collapsed ? t('systemFiles.showDetails') : t('systemFiles.hideDetails')"
26
+ :data-testid="`system-file-banner-toggle`"
27
+ @click="toggle"
28
+ >
29
+ {{ collapsed ? t("systemFiles.showDetails") : t("systemFiles.hideDetails") }}
30
+ </button>
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import { computed, ref, watch } from "vue";
37
+ import { useI18n } from "vue-i18n";
38
+ import type { SystemFileDescriptor, EditPolicy } from "../config/systemFileDescriptors";
39
+
40
+ const { t } = useI18n();
41
+
42
+ const props = defineProps<{
43
+ descriptor: SystemFileDescriptor;
44
+ path: string;
45
+ }>();
46
+
47
+ // Per-path collapse state. Persisted to localStorage so a user who
48
+ // dismissed the banner once doesn't see it expanded again on reload.
49
+ // Key includes the descriptor id (not the path) so all roles share
50
+ // one collapse decision — we want "I read the roles banner, hide it
51
+ // for every role file" rather than asking per-file.
52
+ const STORAGE_PREFIX = "systemFileBanner.collapsed.";
53
+
54
+ const INFO_ICON = "ℹ️";
55
+
56
+ function storageKey(descriptorId: string): string {
57
+ return STORAGE_PREFIX + descriptorId;
58
+ }
59
+
60
+ function loadCollapsed(descriptorId: string): boolean {
61
+ try {
62
+ return localStorage.getItem(storageKey(descriptorId)) === "1";
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ function saveCollapsed(descriptorId: string, value: boolean): void {
69
+ try {
70
+ if (value) localStorage.setItem(storageKey(descriptorId), "1");
71
+ else localStorage.removeItem(storageKey(descriptorId));
72
+ } catch {
73
+ // localStorage may throw in private browsing / quota-exceeded;
74
+ // banner state degrades to in-memory only, no user-visible error.
75
+ }
76
+ }
77
+
78
+ const collapsed = ref(loadCollapsed(props.descriptor.id));
79
+
80
+ // Re-load when the descriptor changes (different file selected).
81
+ watch(
82
+ () => props.descriptor.id,
83
+ (descriptorId) => {
84
+ collapsed.value = loadCollapsed(descriptorId);
85
+ },
86
+ );
87
+
88
+ function toggle(): void {
89
+ collapsed.value = !collapsed.value;
90
+ saveCollapsed(props.descriptor.id, collapsed.value);
91
+ }
92
+
93
+ const POLICY_CHIP_CLASSES: Record<EditPolicy, string> = {
94
+ "agent-managed-but-hand-editable": "bg-emerald-100 text-emerald-800",
95
+ "user-editable": "bg-blue-100 text-blue-800",
96
+ "agent-managed": "bg-amber-100 text-amber-800",
97
+ "fragile-format": "bg-orange-100 text-orange-800",
98
+ ephemeral: "bg-gray-200 text-gray-700",
99
+ };
100
+
101
+ const policyChipClass = computed(() => POLICY_CHIP_CLASSES[props.descriptor.editPolicy]);
102
+
103
+ const GITHUB_REPO_URL = "https://github.com/receptron/mulmoclaude/blob/main/";
104
+
105
+ const schemaUrl = computed(() => (props.descriptor.schemaRef ? GITHUB_REPO_URL + props.descriptor.schemaRef : ""));
106
+ </script>
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <div class="px-2 py-1 text-sm" role="status" aria-live="polite" data-testid="thinking-indicator">
3
+ <div class="flex items-center gap-2 text-gray-500">
4
+ <span class="text-xs">{{ statusMessage }}</span>
5
+ <span class="flex gap-1" aria-hidden="true">
6
+ <span class="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" style="animation-delay: 0ms" />
7
+ <span class="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" style="animation-delay: 150ms" />
8
+ <span class="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce" style="animation-delay: 300ms" />
9
+ </span>
10
+ <span v-if="runElapsedMs !== null && runElapsedMs >= 1000" class="text-xs text-gray-400 tabular-nums" data-testid="run-elapsed">
11
+ · {{ formatElapsed(runElapsedMs) }}
12
+ </span>
13
+ </div>
14
+ <div v-if="pendingCalls && pendingCalls.length > 0" class="mt-1 space-y-0.5">
15
+ <div v-for="call in pendingCalls" :key="call.toolUseId" class="flex items-center gap-1.5 text-xs text-gray-400">
16
+ <span class="w-1.5 h-1.5 rounded-full bg-blue-300 shrink-0 animate-pulse" aria-hidden="true" />
17
+ <span class="font-mono truncate">{{ call.toolName }}</span>
18
+ <span class="text-xs text-gray-300 tabular-nums shrink-0">· {{ formatElapsed(call.elapsedMs) }}</span>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import { formatElapsed } from "../utils/agent/formatElapsed";
26
+
27
+ interface PendingCall {
28
+ toolUseId: string;
29
+ toolName: string;
30
+ elapsedMs: number;
31
+ }
32
+
33
+ withDefaults(
34
+ defineProps<{
35
+ statusMessage: string;
36
+ runElapsedMs?: number | null;
37
+ pendingCalls?: PendingCall[];
38
+ }>(),
39
+ { runElapsedMs: null, pendingCalls: () => [] },
40
+ );
41
+ </script>
@@ -1,36 +1,42 @@
1
1
  <template>
2
2
  <div class="h-full bg-white flex flex-col">
3
3
  <!-- Header -->
4
- <div class="flex items-center justify-between px-4 py-2 border-b border-gray-100 shrink-0 gap-3">
5
- <div class="flex items-center gap-3 min-w-0">
6
- <h2 class="text-base font-semibold text-gray-800 shrink-0">Todo</h2>
7
- <span class="text-xs text-gray-500 shrink-0">{{ completedCount }}/{{ items.length }} done</span>
4
+ <div class="flex items-center justify-between gap-2 px-3 py-2 border-b border-gray-100 shrink-0">
5
+ <div class="flex items-center gap-2 min-w-0">
6
+ <h2 class="text-base font-semibold text-gray-800 shrink-0">{{ t("todoExplorer.heading") }}</h2>
7
+ <span class="text-xs text-gray-500 shrink-0">{{ t("todoExplorer.doneRatio", { done: completedCount, total: items.length }) }}</span>
8
8
  <input
9
9
  v-model="search"
10
10
  data-testid="todo-search"
11
11
  type="text"
12
- placeholder="Search..."
13
- class="px-2 py-1 text-xs border border-gray-200 rounded w-44 focus:outline-none focus:border-blue-400"
12
+ :placeholder="t('todoExplorer.searchPlaceholder')"
13
+ class="h-8 px-2.5 text-sm border border-gray-200 rounded w-44 focus:outline-none focus:border-blue-400"
14
14
  />
15
15
  </div>
16
16
  <div class="flex items-center gap-2">
17
17
  <!-- Add button -->
18
- <button data-testid="todo-add-btn" class="px-2 py-1 text-xs rounded bg-blue-500 text-white hover:bg-blue-600" @click="addOpen = true">+ Add</button>
18
+ <button
19
+ data-testid="todo-add-btn"
20
+ class="h-8 px-2.5 flex items-center gap-1 text-sm rounded bg-blue-500 text-white hover:bg-blue-600"
21
+ @click="addOpen = true"
22
+ >
23
+ {{ t("todoExplorer.addButton") }}
24
+ </button>
19
25
  <!-- Add column button (kanban only) -->
20
26
  <button
21
27
  v-if="viewMode === TODO_VIEW.kanban"
22
28
  data-testid="todo-column-add-btn"
23
- class="px-2 py-1 text-xs rounded border border-gray-300 text-gray-600 hover:bg-gray-50"
29
+ class="h-8 px-2.5 flex items-center gap-1 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50"
24
30
  @click="addColumnOpen = true"
25
31
  >
26
- + Column
32
+ {{ t("todoExplorer.addColumnButton") }}
27
33
  </button>
28
34
  <!-- View mode toggle -->
29
- <div class="flex border border-gray-300 rounded overflow-hidden text-xs">
35
+ <div class="flex border border-gray-300 rounded overflow-hidden">
30
36
  <button
31
37
  v-for="mode in VIEW_MODES"
32
38
  :key="mode.key"
33
- class="px-2.5 py-1"
39
+ class="h-8 w-8 flex items-center justify-center"
34
40
  :class="viewMode === mode.key ? 'bg-blue-500 text-white' : 'bg-white text-gray-600 hover:bg-gray-50'"
35
41
  :data-testid="`todo-view-${mode.key}`"
36
42
  :title="mode.label"
@@ -44,7 +50,7 @@
44
50
 
45
51
  <!-- Label filter chips -->
46
52
  <div v-if="labelInventory.length > 0" class="flex flex-wrap items-center gap-1.5 px-4 py-1.5 border-b border-gray-100 bg-gray-50 shrink-0">
47
- <span class="text-[11px] text-gray-500 mr-1">Labels:</span>
53
+ <span class="text-[11px] text-gray-500 mr-1">{{ t("todoExplorer.labels") }}</span>
48
54
  <button
49
55
  v-for="entry in labelInventory"
50
56
  :key="entry.label"
@@ -59,8 +65,13 @@
59
65
  {{ entry.label }}
60
66
  <span class="opacity-60">{{ entry.count }}</span>
61
67
  </button>
62
- <button v-if="activeFilters.size > 0" class="ml-auto text-[11px] text-gray-500 hover:text-gray-700" title="Clear label filters" @click="clearFilters">
63
- Clear
68
+ <button
69
+ v-if="activeFilters.size > 0"
70
+ class="ml-auto text-[11px] text-gray-500 hover:text-gray-700"
71
+ :title="t('todoExplorer.clearFiltersTitle')"
72
+ @click="clearFilters"
73
+ >
74
+ {{ t("todoExplorer.clearButton") }}
64
75
  </button>
65
76
  </div>
66
77
 
@@ -71,7 +82,7 @@
71
82
 
72
83
  <!-- Body -->
73
84
  <div class="flex-1 min-h-0">
74
- <div v-if="items.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">No todo items yet. Click "+ Add" to create one.</div>
85
+ <div v-if="items.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">{{ t("todoExplorer.emptyHint") }}</div>
75
86
  <template v-else>
76
87
  <TodoKanbanView
77
88
  v-if="viewMode === TODO_VIEW.kanban"
@@ -122,20 +133,22 @@
122
133
  <!-- Add column dialog -->
123
134
  <div v-if="addColumnOpen" class="fixed inset-0 z-50 bg-black/30 flex items-center justify-center" @click="addColumnOpen = false">
124
135
  <div class="bg-white rounded-lg shadow-xl w-80 p-5 space-y-3" role="dialog" aria-modal="true" aria-labelledby="todo-add-column-title" @click.stop>
125
- <h3 id="todo-add-column-title" class="text-base font-semibold text-gray-800">Add Column</h3>
136
+ <h3 id="todo-add-column-title" class="text-base font-semibold text-gray-800">{{ t("todoExplorer.addColumn") }}</h3>
126
137
  <label class="block text-xs text-gray-600">
127
- Label
138
+ {{ t("todoExplorer.newColumnLabelField") }}
128
139
  <input
129
140
  v-model="newColumnLabel"
130
141
  type="text"
131
- placeholder="Review"
142
+ :placeholder="t('todoExplorer.newColumnPlaceholder')"
132
143
  class="mt-1 w-full px-2 py-1.5 text-sm border border-gray-300 rounded focus:outline-none focus:border-blue-400"
133
144
  @keydown.enter="commitNewColumn"
134
145
  />
135
146
  </label>
136
147
  <div class="flex justify-end gap-2 pt-1">
137
- <button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="addColumnOpen = false">Cancel</button>
138
- <button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="commitNewColumn">Add</button>
148
+ <button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="addColumnOpen = false">
149
+ {{ t("common.cancel") }}
150
+ </button>
151
+ <button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="commitNewColumn">{{ t("common.add") }}</button>
139
152
  </div>
140
153
  </div>
141
154
  </div>
@@ -143,7 +156,10 @@
143
156
  </template>
144
157
 
145
158
  <script setup lang="ts">
146
- import { computed, onMounted, onUnmounted, ref, watch } from "vue";
159
+ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
160
+ import { useI18n } from "vue-i18n";
161
+ import { useRoute } from "vue-router";
162
+ import { scrollIntoViewByTestId } from "../utils/dom/scrollIntoViewByTestId";
147
163
  import type { ToolResultComplete } from "gui-chat-protocol/vue";
148
164
  import type { TodoData, TodoItem } from "../plugins/todo/index";
149
165
  import { colorForLabel, filterByLabels, listLabelsWithCount } from "../plugins/todo/labels";
@@ -153,9 +169,10 @@ import TodoTableView from "./todo/TodoTableView.vue";
153
169
  import TodoListView from "./todo/TodoListView.vue";
154
170
  import TodoAddDialog from "./todo/TodoAddDialog.vue";
155
171
  import TodoEditDialog from "./todo/TodoEditDialog.vue";
156
-
157
172
  import { TODO_VIEW, TODO_VIEW_MODES as VIEW_MODES, type TodoViewMode as ViewMode } from "../plugins/todo/viewModes";
158
173
 
174
+ const { t } = useI18n();
175
+
159
176
  const VIEW_MODE_KEY = "todo_explorer_view_mode";
160
177
 
161
178
  const props = defineProps<{
@@ -275,6 +292,39 @@ function onExplorerKeydown(event: KeyboardEvent): void {
275
292
  onMounted(() => document.addEventListener("keydown", onExplorerKeydown));
276
293
  onUnmounted(() => document.removeEventListener("keydown", onExplorerKeydown));
277
294
 
295
+ // Permalink support (#762): arrivals on /todos/:itemId scroll and
296
+ // flash the matching card. Safe to run unconditionally — when the
297
+ // explorer is embedded in FilesView the URL has no :itemId and
298
+ // scrollIntoViewByTestId is a no-op. Retry a handful of times to
299
+ // cover the window between mount and the first items fetch landing.
300
+ const TODO_FOCUS_MAX_RETRIES = 10;
301
+ const TODO_FOCUS_RETRY_MS = 150;
302
+ const route = useRoute();
303
+
304
+ async function focusUrlItem(itemId: string): Promise<void> {
305
+ for (let attempt = 0; attempt < TODO_FOCUS_MAX_RETRIES; attempt++) {
306
+ await nextTick();
307
+ if (scrollIntoViewByTestId(`todo-card-${itemId}`)) return;
308
+ await new Promise((resolve) => window.setTimeout(resolve, TODO_FOCUS_RETRY_MS));
309
+ }
310
+ }
311
+
312
+ onMounted(() => {
313
+ const itemId = route.params.itemId;
314
+ if (typeof itemId === "string" && itemId) {
315
+ void focusUrlItem(itemId);
316
+ }
317
+ });
318
+
319
+ watch(
320
+ () => route.params.itemId,
321
+ (itemId) => {
322
+ if (typeof itemId === "string" && itemId) {
323
+ void focusUrlItem(itemId);
324
+ }
325
+ },
326
+ );
327
+
278
328
  // ── Item handlers ──────────────────────────────────────────────
279
329
 
280
330
  function onPatchItem(itemId: string, input: PatchItemInput): void {