mulmoclaude 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/README.md +44 -0
  2. package/bin/mulmoclaude.js +202 -0
  3. package/bin/prepare-dist.js +93 -0
  4. package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
  5. package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
  6. package/client/assets/index-D8rhwXLq.js +4906 -0
  7. package/client/assets/index-KNLBjwuh.css +1 -0
  8. package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
  9. package/client/assets/material-icons-Dr0goTwe.woff +0 -0
  10. package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
  11. package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
  12. package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
  13. package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
  14. package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
  15. package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
  16. package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
  17. package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
  18. package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
  19. package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
  20. package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
  21. package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
  22. package/client/index.html +28 -0
  23. package/package.json +66 -0
  24. package/server/agent/attachmentConverter.ts +270 -0
  25. package/server/agent/config.ts +414 -0
  26. package/server/agent/index.ts +260 -0
  27. package/server/agent/mcp-server.ts +412 -0
  28. package/server/agent/mcp-tools/index.ts +63 -0
  29. package/server/agent/mcp-tools/x.ts +188 -0
  30. package/server/agent/plugin-names.ts +75 -0
  31. package/server/agent/prompt.ts +349 -0
  32. package/server/agent/resumeFailover.ts +129 -0
  33. package/server/agent/sandboxMounts.ts +329 -0
  34. package/server/agent/stream.ts +194 -0
  35. package/server/api/auth/bearerAuth.ts +61 -0
  36. package/server/api/auth/token.ts +98 -0
  37. package/server/api/csrfGuard.ts +85 -0
  38. package/server/api/routes/agent.ts +478 -0
  39. package/server/api/routes/chart.ts +98 -0
  40. package/server/api/routes/chat-index.ts +46 -0
  41. package/server/api/routes/config.ts +258 -0
  42. package/server/api/routes/dispatchResponse.ts +79 -0
  43. package/server/api/routes/files.ts +812 -0
  44. package/server/api/routes/html.ts +101 -0
  45. package/server/api/routes/image.ts +169 -0
  46. package/server/api/routes/mulmo-script.ts +712 -0
  47. package/server/api/routes/mulmoScriptValidate.ts +101 -0
  48. package/server/api/routes/notifications.ts +69 -0
  49. package/server/api/routes/pdf.ts +163 -0
  50. package/server/api/routes/plugins.ts +276 -0
  51. package/server/api/routes/presentHtml.ts +48 -0
  52. package/server/api/routes/roles.ts +125 -0
  53. package/server/api/routes/scheduler.ts +153 -0
  54. package/server/api/routes/schedulerHandlers.ts +151 -0
  55. package/server/api/routes/schedulerTasks.ts +163 -0
  56. package/server/api/routes/sessions.ts +294 -0
  57. package/server/api/routes/sessionsCursor.ts +59 -0
  58. package/server/api/routes/skills.ts +195 -0
  59. package/server/api/routes/sources.ts +540 -0
  60. package/server/api/routes/todos.ts +263 -0
  61. package/server/api/routes/todosColumnsHandlers.ts +347 -0
  62. package/server/api/routes/todosHandlers.ts +274 -0
  63. package/server/api/routes/todosItemsHandlers.ts +386 -0
  64. package/server/api/routes/wiki/pageIndex.ts +53 -0
  65. package/server/api/routes/wiki.ts +363 -0
  66. package/server/api/sandboxStatus.ts +64 -0
  67. package/server/events/notifications.ts +160 -0
  68. package/server/events/pub-sub/index.ts +45 -0
  69. package/server/events/relay-client.ts +288 -0
  70. package/server/events/scheduler-adapter.ts +302 -0
  71. package/server/events/session-store/index.ts +492 -0
  72. package/server/events/task-manager/index.ts +181 -0
  73. package/server/index.ts +572 -0
  74. package/server/system/config.ts +243 -0
  75. package/server/system/credentials.ts +220 -0
  76. package/server/system/docker.ts +97 -0
  77. package/server/system/env.ts +109 -0
  78. package/server/system/logger/config.ts +112 -0
  79. package/server/system/logger/formatters.ts +40 -0
  80. package/server/system/logger/index.ts +53 -0
  81. package/server/system/logger/rotation.ts +37 -0
  82. package/server/system/logger/sinks.ts +101 -0
  83. package/server/system/logger/types.ts +29 -0
  84. package/server/utils/date.ts +57 -0
  85. package/server/utils/errors.ts +7 -0
  86. package/server/utils/fetch.ts +27 -0
  87. package/server/utils/files/atomic.ts +125 -0
  88. package/server/utils/files/html-io.ts +20 -0
  89. package/server/utils/files/image-store.ts +66 -0
  90. package/server/utils/files/index.ts +45 -0
  91. package/server/utils/files/journal-io.ts +213 -0
  92. package/server/utils/files/json.ts +69 -0
  93. package/server/utils/files/markdown-store.ts +33 -0
  94. package/server/utils/files/naming.ts +50 -0
  95. package/server/utils/files/reference-dirs-io.ts +45 -0
  96. package/server/utils/files/roles-io.ts +45 -0
  97. package/server/utils/files/safe.ts +106 -0
  98. package/server/utils/files/scheduler-io.ts +20 -0
  99. package/server/utils/files/scheduler-overrides-io.ts +64 -0
  100. package/server/utils/files/session-io.ts +136 -0
  101. package/server/utils/files/spreadsheet-store.ts +63 -0
  102. package/server/utils/files/todos-io.ts +29 -0
  103. package/server/utils/files/user-tasks-io.ts +25 -0
  104. package/server/utils/files/workspace-io.ts +221 -0
  105. package/server/utils/gemini.ts +59 -0
  106. package/server/utils/gitignore.ts +69 -0
  107. package/server/utils/http.ts +15 -0
  108. package/server/utils/httpError.ts +61 -0
  109. package/server/utils/id.ts +16 -0
  110. package/server/utils/json.ts +83 -0
  111. package/server/utils/logBackgroundError.ts +22 -0
  112. package/server/utils/markdown.ts +82 -0
  113. package/server/utils/request.ts +29 -0
  114. package/server/utils/slug.ts +50 -0
  115. package/server/utils/spawn.ts +62 -0
  116. package/server/utils/time.ts +34 -0
  117. package/server/utils/types.ts +47 -0
  118. package/server/workspace/chat-index/index.ts +153 -0
  119. package/server/workspace/chat-index/indexer.ts +209 -0
  120. package/server/workspace/chat-index/paths.ts +34 -0
  121. package/server/workspace/chat-index/summarizer.ts +247 -0
  122. package/server/workspace/chat-index/types.ts +38 -0
  123. package/server/workspace/custom-dirs.ts +220 -0
  124. package/server/workspace/helps/business.md +104 -0
  125. package/server/workspace/helps/github.md +23 -0
  126. package/server/workspace/helps/index.md +60 -0
  127. package/server/workspace/helps/mulmoscript.md +249 -0
  128. package/server/workspace/helps/sandbox.md +90 -0
  129. package/server/workspace/helps/spreadsheet.md +43 -0
  130. package/server/workspace/helps/telegram.md +135 -0
  131. package/server/workspace/helps/wiki.md +131 -0
  132. package/server/workspace/journal/archivist.ts +386 -0
  133. package/server/workspace/journal/dailyPass.ts +743 -0
  134. package/server/workspace/journal/diff.ts +71 -0
  135. package/server/workspace/journal/index.ts +185 -0
  136. package/server/workspace/journal/indexFile.ts +136 -0
  137. package/server/workspace/journal/linkRewrite.ts +4 -0
  138. package/server/workspace/journal/memoryExtractor.ts +130 -0
  139. package/server/workspace/journal/optimizationPass.ts +160 -0
  140. package/server/workspace/journal/paths.ts +76 -0
  141. package/server/workspace/journal/state.ts +125 -0
  142. package/server/workspace/paths.ts +158 -0
  143. package/server/workspace/reference-dirs.ts +252 -0
  144. package/server/workspace/roles.ts +37 -0
  145. package/server/workspace/skills/discovery.ts +125 -0
  146. package/server/workspace/skills/index.ts +10 -0
  147. package/server/workspace/skills/parser.ts +144 -0
  148. package/server/workspace/skills/paths.ts +41 -0
  149. package/server/workspace/skills/scheduler.ts +149 -0
  150. package/server/workspace/skills/types.ts +30 -0
  151. package/server/workspace/skills/user-tasks.ts +257 -0
  152. package/server/workspace/skills/writer.ts +189 -0
  153. package/server/workspace/sources/arxivDiscovery.ts +182 -0
  154. package/server/workspace/sources/classifier.ts +268 -0
  155. package/server/workspace/sources/fetchers/arxiv.ts +170 -0
  156. package/server/workspace/sources/fetchers/github.ts +106 -0
  157. package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
  158. package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
  159. package/server/workspace/sources/fetchers/index.ts +71 -0
  160. package/server/workspace/sources/fetchers/registerAll.ts +15 -0
  161. package/server/workspace/sources/fetchers/rss.ts +141 -0
  162. package/server/workspace/sources/fetchers/rssParser.ts +295 -0
  163. package/server/workspace/sources/httpFetcher.ts +230 -0
  164. package/server/workspace/sources/interests.ts +120 -0
  165. package/server/workspace/sources/paths.ts +110 -0
  166. package/server/workspace/sources/pipeline/dedup.ts +60 -0
  167. package/server/workspace/sources/pipeline/fetch.ts +136 -0
  168. package/server/workspace/sources/pipeline/index.ts +249 -0
  169. package/server/workspace/sources/pipeline/notify.ts +72 -0
  170. package/server/workspace/sources/pipeline/plan.ts +66 -0
  171. package/server/workspace/sources/pipeline/summarize.ts +189 -0
  172. package/server/workspace/sources/pipeline/write.ts +185 -0
  173. package/server/workspace/sources/rateLimiter.ts +148 -0
  174. package/server/workspace/sources/registry.ts +326 -0
  175. package/server/workspace/sources/robots.ts +271 -0
  176. package/server/workspace/sources/sourceState.ts +135 -0
  177. package/server/workspace/sources/taxonomy.ts +74 -0
  178. package/server/workspace/sources/types.ts +144 -0
  179. package/server/workspace/sources/urls.ts +112 -0
  180. package/server/workspace/tool-trace/classify.ts +114 -0
  181. package/server/workspace/tool-trace/index.ts +250 -0
  182. package/server/workspace/tool-trace/writeSearch.ts +98 -0
  183. package/server/workspace/wiki-backlinks/index.ts +107 -0
  184. package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
  185. package/server/workspace/workspace.ts +66 -0
  186. package/src/App.vue +720 -0
  187. package/src/assets/mulmo_bw.png +0 -0
  188. package/src/components/CanvasViewToggle.vue +27 -0
  189. package/src/components/ChatAttachmentPreview.vue +45 -0
  190. package/src/components/ChatImagePreview.vue +17 -0
  191. package/src/components/ChatInput.vue +208 -0
  192. package/src/components/FileContentHeader.vue +49 -0
  193. package/src/components/FileContentRenderer.vue +162 -0
  194. package/src/components/FileTree.vue +115 -0
  195. package/src/components/FileTreePane.vue +85 -0
  196. package/src/components/FilesView.vue +206 -0
  197. package/src/components/LockStatusPopup.vue +111 -0
  198. package/src/components/NotificationBell.vue +131 -0
  199. package/src/components/NotificationToast.vue +72 -0
  200. package/src/components/PluginLauncher.vue +138 -0
  201. package/src/components/RightSidebar.vue +113 -0
  202. package/src/components/RoleSelector.vue +64 -0
  203. package/src/components/SessionHistoryPanel.vue +176 -0
  204. package/src/components/SessionTabBar.vue +81 -0
  205. package/src/components/SettingsMcpTab.vue +350 -0
  206. package/src/components/SettingsModal.vue +275 -0
  207. package/src/components/SettingsReferenceDirsTab.vue +173 -0
  208. package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
  209. package/src/components/SidebarHeader.vue +69 -0
  210. package/src/components/StackView.vue +360 -0
  211. package/src/components/SuggestionsPanel.vue +65 -0
  212. package/src/components/TodoExplorer.vue +358 -0
  213. package/src/components/ToolResultsPanel.vue +77 -0
  214. package/src/components/todo/TodoAddDialog.vue +131 -0
  215. package/src/components/todo/TodoEditDialog.vue +47 -0
  216. package/src/components/todo/TodoEditPanel.vue +113 -0
  217. package/src/components/todo/TodoKanbanView.vue +249 -0
  218. package/src/components/todo/TodoListView.vue +79 -0
  219. package/src/components/todo/TodoTableView.vue +177 -0
  220. package/src/composables/useActiveSession.ts +40 -0
  221. package/src/composables/useAppApi.ts +45 -0
  222. package/src/composables/useCanvasViewMode.ts +121 -0
  223. package/src/composables/useChatScroll.ts +47 -0
  224. package/src/composables/useClickOutside.ts +26 -0
  225. package/src/composables/useClipboardCopy.ts +44 -0
  226. package/src/composables/useContentDisplay.ts +52 -0
  227. package/src/composables/useDebugBeat.ts +23 -0
  228. package/src/composables/useDynamicFavicon.ts +115 -0
  229. package/src/composables/useEventListeners.ts +42 -0
  230. package/src/composables/useExpandedDirs.ts +64 -0
  231. package/src/composables/useFaviconState.ts +30 -0
  232. package/src/composables/useFileSelection.ts +115 -0
  233. package/src/composables/useFileSortMode.ts +24 -0
  234. package/src/composables/useFileTree.ts +85 -0
  235. package/src/composables/useFreshPluginData.ts +89 -0
  236. package/src/composables/useHealth.ts +38 -0
  237. package/src/composables/useImeAwareEnter.ts +57 -0
  238. package/src/composables/useKeyNavigation.ts +60 -0
  239. package/src/composables/useMarkdownLinkHandler.ts +46 -0
  240. package/src/composables/useMarkdownMode.ts +17 -0
  241. package/src/composables/useMcpTools.ts +71 -0
  242. package/src/composables/useMergedSessions.ts +27 -0
  243. package/src/composables/useNotifications.ts +90 -0
  244. package/src/composables/usePdfDownload.ts +60 -0
  245. package/src/composables/usePendingCalls.ts +77 -0
  246. package/src/composables/usePubSub.ts +85 -0
  247. package/src/composables/useRightSidebar.ts +23 -0
  248. package/src/composables/useRoles.ts +34 -0
  249. package/src/composables/useSandboxStatus.ts +67 -0
  250. package/src/composables/useSelectedResult.ts +49 -0
  251. package/src/composables/useSessionDerived.ts +51 -0
  252. package/src/composables/useSessionHistory.ts +81 -0
  253. package/src/composables/useSessionSync.ts +57 -0
  254. package/src/composables/useViewLayout.ts +55 -0
  255. package/src/config/apiRoutes.ts +173 -0
  256. package/src/config/pubsubChannels.ts +45 -0
  257. package/src/config/roles.ts +335 -0
  258. package/src/config/schedulerActions.ts +25 -0
  259. package/src/config/toolNames.ts +71 -0
  260. package/src/config/workspacePaths.ts +24 -0
  261. package/src/index.css +107 -0
  262. package/src/main.ts +25 -0
  263. package/src/plugins/canvas/Preview.vue +13 -0
  264. package/src/plugins/canvas/View.vue +333 -0
  265. package/src/plugins/canvas/definition.ts +38 -0
  266. package/src/plugins/canvas/index.ts +36 -0
  267. package/src/plugins/chart/Preview.vue +49 -0
  268. package/src/plugins/chart/View.vue +143 -0
  269. package/src/plugins/chart/definition.ts +58 -0
  270. package/src/plugins/chart/index.ts +52 -0
  271. package/src/plugins/editImage/Preview.vue +13 -0
  272. package/src/plugins/editImage/View.vue +13 -0
  273. package/src/plugins/editImage/definition.ts +27 -0
  274. package/src/plugins/editImage/index.ts +36 -0
  275. package/src/plugins/generateImage/Preview.vue +13 -0
  276. package/src/plugins/generateImage/View.vue +33 -0
  277. package/src/plugins/generateImage/definition.ts +32 -0
  278. package/src/plugins/generateImage/index.ts +56 -0
  279. package/src/plugins/manageRoles/Preview.vue +49 -0
  280. package/src/plugins/manageRoles/View.vue +525 -0
  281. package/src/plugins/manageRoles/definition.ts +43 -0
  282. package/src/plugins/manageRoles/index.ts +47 -0
  283. package/src/plugins/manageSkills/Preview.vue +21 -0
  284. package/src/plugins/manageSkills/View.vue +321 -0
  285. package/src/plugins/manageSkills/definition.ts +49 -0
  286. package/src/plugins/manageSkills/index.ts +49 -0
  287. package/src/plugins/manageSource/Preview.vue +33 -0
  288. package/src/plugins/manageSource/View.vue +697 -0
  289. package/src/plugins/manageSource/definition.ts +63 -0
  290. package/src/plugins/manageSource/index.ts +66 -0
  291. package/src/plugins/markdown/Preview.vue +77 -0
  292. package/src/plugins/markdown/View.vue +476 -0
  293. package/src/plugins/markdown/definition.ts +50 -0
  294. package/src/plugins/markdown/index.ts +36 -0
  295. package/src/plugins/presentHtml/Preview.vue +25 -0
  296. package/src/plugins/presentHtml/View.vue +52 -0
  297. package/src/plugins/presentHtml/definition.ts +27 -0
  298. package/src/plugins/presentHtml/helpers.ts +72 -0
  299. package/src/plugins/presentHtml/index.ts +41 -0
  300. package/src/plugins/presentMulmoScript/Preview.vue +23 -0
  301. package/src/plugins/presentMulmoScript/View.vue +1166 -0
  302. package/src/plugins/presentMulmoScript/definition.ts +95 -0
  303. package/src/plugins/presentMulmoScript/helpers.ts +162 -0
  304. package/src/plugins/presentMulmoScript/index.ts +40 -0
  305. package/src/plugins/scheduler/Preview.vue +67 -0
  306. package/src/plugins/scheduler/TasksTab.vue +205 -0
  307. package/src/plugins/scheduler/View.vue +565 -0
  308. package/src/plugins/scheduler/definition.ts +57 -0
  309. package/src/plugins/scheduler/index.ts +45 -0
  310. package/src/plugins/scheduler/viewModes.ts +26 -0
  311. package/src/plugins/spreadsheet/Preview.vue +29 -0
  312. package/src/plugins/spreadsheet/View.vue +997 -0
  313. package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
  314. package/src/plugins/spreadsheet/definition.ts +121 -0
  315. package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
  316. package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
  317. package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
  318. package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
  319. package/src/plugins/spreadsheet/engine/engine.ts +176 -0
  320. package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
  321. package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
  322. package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
  323. package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
  324. package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
  325. package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
  326. package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
  327. package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
  328. package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
  329. package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
  330. package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
  331. package/src/plugins/spreadsheet/engine/index.ts +27 -0
  332. package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
  333. package/src/plugins/spreadsheet/engine/parser.ts +143 -0
  334. package/src/plugins/spreadsheet/engine/registry.ts +150 -0
  335. package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
  336. package/src/plugins/spreadsheet/engine/types.ts +64 -0
  337. package/src/plugins/spreadsheet/index.ts +36 -0
  338. package/src/plugins/textResponse/Preview.vue +94 -0
  339. package/src/plugins/textResponse/View.vue +503 -0
  340. package/src/plugins/textResponse/definition.ts +34 -0
  341. package/src/plugins/textResponse/index.ts +27 -0
  342. package/src/plugins/textResponse/plugin.ts +29 -0
  343. package/src/plugins/textResponse/samples.ts +97 -0
  344. package/src/plugins/textResponse/types.ts +11 -0
  345. package/src/plugins/todo/Preview.vue +63 -0
  346. package/src/plugins/todo/View.vue +364 -0
  347. package/src/plugins/todo/composables/useTodos.ts +177 -0
  348. package/src/plugins/todo/definition.ts +45 -0
  349. package/src/plugins/todo/index.ts +61 -0
  350. package/src/plugins/todo/labels.ts +163 -0
  351. package/src/plugins/todo/priority.ts +98 -0
  352. package/src/plugins/todo/viewModes.ts +19 -0
  353. package/src/plugins/ui-image/ImagePreview.vue +23 -0
  354. package/src/plugins/ui-image/ImageView.vue +34 -0
  355. package/src/plugins/ui-image/index.ts +3 -0
  356. package/src/plugins/ui-image/types.ts +4 -0
  357. package/src/plugins/wiki/Preview.vue +65 -0
  358. package/src/plugins/wiki/View.vue +342 -0
  359. package/src/plugins/wiki/definition.ts +25 -0
  360. package/src/plugins/wiki/helpers.ts +59 -0
  361. package/src/plugins/wiki/index.ts +52 -0
  362. package/src/router/guards.ts +61 -0
  363. package/src/router/index.ts +50 -0
  364. package/src/tools/index.ts +52 -0
  365. package/src/tools/types.ts +27 -0
  366. package/src/types/events.ts +16 -0
  367. package/src/types/fileTree.ts +13 -0
  368. package/src/types/notification.ts +67 -0
  369. package/src/types/session.ts +116 -0
  370. package/src/types/sse.ts +90 -0
  371. package/src/types/toolCallHistory.ts +13 -0
  372. package/src/utils/agent/eventDispatch.ts +74 -0
  373. package/src/utils/agent/request.ts +55 -0
  374. package/src/utils/agent/toolCalls.ts +62 -0
  375. package/src/utils/api.ts +218 -0
  376. package/src/utils/canvas/viewMode.ts +46 -0
  377. package/src/utils/dom/authTokenMeta.ts +20 -0
  378. package/src/utils/dom/clickOutside.ts +11 -0
  379. package/src/utils/dom/externalLink.ts +57 -0
  380. package/src/utils/dom/scrollable.ts +24 -0
  381. package/src/utils/errors.ts +11 -0
  382. package/src/utils/files/expandedDirs.ts +25 -0
  383. package/src/utils/files/filename.ts +12 -0
  384. package/src/utils/files/sortChildren.ts +20 -0
  385. package/src/utils/filesPreview/schedulerPreview.ts +38 -0
  386. package/src/utils/filesPreview/todoPreview.ts +40 -0
  387. package/src/utils/format/date.ts +85 -0
  388. package/src/utils/format/frontmatter.ts +80 -0
  389. package/src/utils/format/jsonSyntax.ts +109 -0
  390. package/src/utils/html/previewCsp.ts +65 -0
  391. package/src/utils/image/resolve.ts +8 -0
  392. package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
  393. package/src/utils/markdown/extractFirstH1.ts +39 -0
  394. package/src/utils/notification/dispatch.ts +22 -0
  395. package/src/utils/path/relativeLink.ts +130 -0
  396. package/src/utils/role/icon.ts +20 -0
  397. package/src/utils/role/merge.ts +10 -0
  398. package/src/utils/role/plugins.ts +12 -0
  399. package/src/utils/session/mergeSessions.ts +103 -0
  400. package/src/utils/session/seedRoleDefault.ts +35 -0
  401. package/src/utils/session/sessionEntries.ts +121 -0
  402. package/src/utils/session/sessionFactory.ts +22 -0
  403. package/src/utils/session/sessionHelpers.ts +99 -0
  404. package/src/utils/tools/dedup.ts +17 -0
  405. package/src/utils/tools/mcp.ts +33 -0
  406. package/src/utils/tools/pendingCalls.ts +16 -0
  407. package/src/utils/tools/result.ts +40 -0
  408. package/src/utils/types.ts +44 -0
@@ -0,0 +1,997 @@
1
+ <template>
2
+ <div class="spreadsheet-container">
3
+ <div v-if="loading" class="min-h-full p-8 flex items-center justify-center">
4
+ <div class="text-gray-500">Loading spreadsheet...</div>
5
+ </div>
6
+ <div v-else-if="errorMessage" class="min-h-full p-8 flex items-center justify-center">
7
+ <div class="error">{{ errorMessage }}</div>
8
+ </div>
9
+ <div v-else-if="!resolvedSheets || resolvedSheets.length === 0" class="min-h-full p-8 flex items-center justify-center">
10
+ <div class="text-gray-500">No spreadsheet data available</div>
11
+ </div>
12
+ <template v-else>
13
+ <div class="spreadsheet-content-wrapper">
14
+ <div class="p-4">
15
+ <div class="header">
16
+ <h1 class="title">
17
+ {{ selectedResult.title || "Spreadsheet" }}
18
+ </h1>
19
+ <div class="button-group">
20
+ <button class="download-btn excel-btn" @click="downloadExcel">
21
+ <span class="material-icons">download</span>
22
+ Excel
23
+ </button>
24
+ </div>
25
+ </div>
26
+
27
+ <!-- Sheet tabs (if multiple sheets) -->
28
+ <div v-if="resolvedSheets.length > 1" class="sheet-tabs">
29
+ <button
30
+ v-for="(sheet, index) in resolvedSheets"
31
+ :key="index"
32
+ :class="['sheet-tab', { active: activeSheetIndex === index }]"
33
+ @click="activeSheetIndex = index"
34
+ >
35
+ {{ sheet.name }}
36
+ </button>
37
+ </div>
38
+
39
+ <!-- Spreadsheet table -->
40
+ <div ref="tableContainer" class="table-container" @click="handleTableClick" v-html="renderedHtml"></div>
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Collapsible Editor -->
45
+ <details v-if="!miniEditorOpen" ref="editorDetails" class="spreadsheet-source">
46
+ <summary>Edit Spreadsheet Data</summary>
47
+ <textarea ref="editorTextarea" v-model="editableData" class="spreadsheet-editor" spellcheck="false" @input="handleDataEdit"></textarea>
48
+ <button class="apply-btn" :disabled="!hasChanges" @click="applyChanges">Apply Changes</button>
49
+ </details>
50
+
51
+ <!-- Mini Editor at Bottom -->
52
+ <div v-if="miniEditorOpen" class="mini-editor-panel">
53
+ <div class="mini-editor-content">
54
+ <span v-if="miniEditorCell" class="cell-ref"> {{ indexToCol(miniEditorCell.col) }}{{ miniEditorCell.row + 1 }} </span>
55
+
56
+ <!-- Type Selector -->
57
+ <div class="radio-group">
58
+ <label class="radio-option">
59
+ <input v-model="miniEditorType" type="radio" value="string" />
60
+ String
61
+ </label>
62
+ <label class="radio-option">
63
+ <input v-model="miniEditorType" type="radio" value="object" />
64
+ Formula
65
+ </label>
66
+ </div>
67
+
68
+ <!-- String input -->
69
+ <input
70
+ v-if="miniEditorType === 'string'"
71
+ v-model="miniEditorValue"
72
+ type="text"
73
+ class="form-input"
74
+ placeholder="Value"
75
+ @keyup.enter="saveMiniEditor"
76
+ />
77
+
78
+ <!-- Formula inputs -->
79
+ <template v-if="miniEditorType === 'object'">
80
+ <input
81
+ v-model="miniEditorFormula"
82
+ type="text"
83
+ class="form-input"
84
+ placeholder="Value or Formula (e.g., 100 or SUM(B2:B11))"
85
+ @keyup.enter="saveMiniEditor"
86
+ />
87
+ <input v-model="miniEditorFormat" type="text" class="form-input" placeholder="Format (e.g., $#,##0.00)" @keyup.enter="saveMiniEditor" />
88
+ </template>
89
+
90
+ <button class="save-btn" @click="saveMiniEditor">Update</button>
91
+ <button class="cancel-btn" @click="closeMiniEditor">✕</button>
92
+ </div>
93
+ </div>
94
+ </template>
95
+ </div>
96
+ </template>
97
+
98
+ <script setup lang="ts">
99
+ import { computed, ref, watch, onMounted, onUnmounted } from "vue";
100
+ import * as XLSX from "xlsx";
101
+ import type { ToolResult } from "gui-chat-protocol";
102
+ import type { SpreadsheetToolData, SpreadsheetSheet } from "./definition";
103
+ import {
104
+ SpreadsheetEngine,
105
+ indexToColumn,
106
+ extractCellReferences,
107
+ buildCellFromInput,
108
+ decodeSpreadsheetResponse,
109
+ findCellJsonPosition,
110
+ type SpreadsheetCell,
111
+ type CellValue,
112
+ } from "./engine";
113
+ import { applyCellHighlights, clearCellHighlights } from "./cellHighlights";
114
+
115
+ // Import all spreadsheet functions to populate the function registry
116
+ import "./engine/functions";
117
+ import { apiGet, apiPut } from "../../utils/api";
118
+ import { API_ROUTES } from "../../config/apiRoutes";
119
+ import type { FilesContentResponseLike } from "./engine/responseDecoder";
120
+ import { isObj, isRecord } from "../../utils/types";
121
+
122
+ /**
123
+ * Normalize malformed data structures
124
+ * Some models generate flat arrays instead of 2D arrays - fix them
125
+ */
126
+
127
+ // Cells can be raw LLM output of arbitrary shape; tightening here would cascade through the engine API.
128
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
+ function normalizeSheetData(data: any): any[][] {
130
+ // Handle null/undefined
131
+ if (!data) {
132
+ return [];
133
+ }
134
+
135
+ // If not an array
136
+ if (!Array.isArray(data)) {
137
+ return [];
138
+ }
139
+
140
+ // Empty array
141
+ if (data.length === 0) {
142
+ return [];
143
+ }
144
+
145
+ // If data is already a 2D array, return as-is
146
+ if (Array.isArray(data[0])) {
147
+ return data;
148
+ }
149
+
150
+ // If data is a flat array of cell objects, convert to 2D by pairing cells
151
+ // Pattern: [cell1, cell2, cell3, cell4] -> [[cell1, cell2], [cell3, cell4]]
152
+ if (isObj(data[0])) {
153
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
+ const rows: any[][] = [];
155
+ for (let i = 0; i < data.length; i += 2) {
156
+ const row = [data[i]];
157
+ if (i + 1 < data.length) {
158
+ row.push(data[i + 1]);
159
+ }
160
+ rows.push(row);
161
+ }
162
+ return rows;
163
+ }
164
+
165
+ // Unknown structure - return empty
166
+ return [];
167
+ }
168
+
169
+ const props = defineProps<{
170
+ selectedResult: ToolResult<SpreadsheetToolData>;
171
+ }>();
172
+
173
+ const emit = defineEmits<{
174
+ updateResult: [result: ToolResult];
175
+ }>();
176
+
177
+ // Create spreadsheet engine instance
178
+ const engine = new SpreadsheetEngine();
179
+
180
+ const loading = ref(false);
181
+ const errorMessage = ref("");
182
+ const resolvedSheets = ref<SpreadsheetSheet[]>([]);
183
+
184
+ function isFilePath(value: unknown): value is string {
185
+ return typeof value === "string" && (value.startsWith("artifacts/spreadsheets/") || value.startsWith("spreadsheets/")) && value.endsWith(".json");
186
+ }
187
+
188
+ async function fetchSheets(): Promise<void> {
189
+ const raw = props.selectedResult.data?.sheets;
190
+ // Clear any stale error from a previous result BEFORE the early
191
+ // returns, otherwise switching from a failed file-backed load to
192
+ // a new inline-data result leaves the error panel on screen.
193
+ errorMessage.value = "";
194
+ if (!raw) {
195
+ resolvedSheets.value = [];
196
+ return;
197
+ }
198
+ if (!isFilePath(raw)) {
199
+ // Legacy inline data
200
+ resolvedSheets.value = raw as SpreadsheetSheet[];
201
+ return;
202
+ }
203
+ loading.value = true;
204
+ const response = await apiGet<FilesContentResponseLike>(API_ROUTES.files.content, { path: raw });
205
+ if (!response.ok) {
206
+ errorMessage.value = `Failed to load spreadsheet: ${response.error}`;
207
+ resolvedSheets.value = [];
208
+ loading.value = false;
209
+ return;
210
+ }
211
+ // The /files/content endpoint returns { kind, content?, message? }.
212
+ // Delegate the shape/validation decision to decodeSpreadsheetResponse
213
+ // so the async wrapper stays simple.
214
+ const result = decodeSpreadsheetResponse(response.data);
215
+ if (result.kind === "error") {
216
+ errorMessage.value = result.message;
217
+ resolvedSheets.value = [];
218
+ } else {
219
+ resolvedSheets.value = result.sheets as SpreadsheetSheet[];
220
+ }
221
+ loading.value = false;
222
+ }
223
+
224
+ // Fetch on mount and sync editableData
225
+ fetchSheets().then(() => {
226
+ editableData.value = JSON.stringify(resolvedSheets.value || [], null, 2);
227
+ });
228
+
229
+ /** Persist edited sheets to disk when file-backed, and emit updateResult. */
230
+ async function persistSheets(sheets: SpreadsheetSheet[]): Promise<void> {
231
+ const raw = props.selectedResult.data?.sheets;
232
+ if (isFilePath(raw)) {
233
+ const filename = raw.replace(/^(artifacts\/spreadsheets|spreadsheets)\//, "");
234
+ const result = await apiPut<unknown>(API_ROUTES.plugins.updateSpreadsheet.replace(":filename", filename), {
235
+ sheets,
236
+ });
237
+ if (!result.ok) {
238
+ errorMessage.value = `Failed to save spreadsheet: ${result.error}`;
239
+ return;
240
+ }
241
+ }
242
+
243
+ resolvedSheets.value = sheets;
244
+
245
+ const updatedResult: ToolResult<SpreadsheetToolData> = {
246
+ ...props.selectedResult,
247
+ data: {
248
+ ...props.selectedResult.data,
249
+ sheets: isFilePath(raw) ? raw : sheets,
250
+ },
251
+ };
252
+ emit("updateResult", updatedResult);
253
+ }
254
+
255
+ const activeSheetIndex = ref(0);
256
+ const editableData = ref(JSON.stringify(resolvedSheets.value || [], null, 2));
257
+ const editorTextarea = ref<HTMLTextAreaElement | null>(null);
258
+ const editorDetails = ref<HTMLDetailsElement | null>(null);
259
+ const tableContainer = ref<HTMLDivElement | null>(null);
260
+
261
+ // Mini editor state
262
+ const miniEditorOpen = ref(false);
263
+ const miniEditorCell = ref<{ row: number; col: number } | null>(null);
264
+
265
+ const miniEditorValue = ref<unknown>(null);
266
+ const miniEditorType = ref<"number" | "string" | "object">("string");
267
+ const miniEditorFormula = ref("");
268
+ const miniEditorFormat = ref("");
269
+
270
+ // Referenced cells state (for formula highlighting)
271
+ const referencedCells = ref<Array<{ row: number; col: number }>>([]);
272
+
273
+ // Check if spreadsheet data has been modified
274
+ const hasChanges = computed(() => {
275
+ try {
276
+ const currentData = JSON.stringify(resolvedSheets.value || [], null, 2);
277
+ return editableData.value !== currentData;
278
+ } catch {
279
+ return false;
280
+ }
281
+ });
282
+
283
+ // Short alias used in the template column header.
284
+ const indexToCol = indexToColumn;
285
+
286
+ // Calculate formulas in the data using the spreadsheet engine
287
+ const calculateFormulas = (data: SpreadsheetCell[][], sheetName?: string): CellValue[][] => {
288
+ // If we have a sheet name, we need to find all sheets for cross-sheet references
289
+ const allSheets = resolvedSheets.value;
290
+
291
+ // Create a SheetData object for the engine
292
+ const sheet = {
293
+ name: sheetName || "Sheet1",
294
+ data,
295
+ };
296
+
297
+ // Calculate using the engine
298
+ const result = engine.calculate(sheet, allSheets);
299
+
300
+ // Return the calculated data
301
+
302
+ return result.data;
303
+ };
304
+
305
+ // Render the active sheet as HTML table
306
+ const renderedHtml = computed(() => {
307
+ if (!resolvedSheets.value || resolvedSheets.value.length === 0) {
308
+ return "";
309
+ }
310
+
311
+ const sheet = resolvedSheets.value[activeSheetIndex.value];
312
+ if (!sheet || !sheet.data) {
313
+ return "";
314
+ }
315
+
316
+ try {
317
+ // Calculate formulas first with sheet name for cross-sheet references
318
+ const calculatedData = calculateFormulas(sheet.data, sheet.name);
319
+
320
+ // Convert data array to worksheet
321
+ const worksheet = XLSX.utils.aoa_to_sheet(calculatedData);
322
+
323
+ // Generate HTML table
324
+ const html = XLSX.utils.sheet_to_html(worksheet, {
325
+ id: "spreadsheet-table",
326
+ editable: false,
327
+ });
328
+
329
+ return html;
330
+ } catch (error) {
331
+ console.error("Failed to render spreadsheet:", error);
332
+ return `<div class="error">Failed to render spreadsheet: ${error instanceof Error ? error.message : "Unknown error"}</div>`;
333
+ }
334
+ });
335
+
336
+ // Download as Excel file
337
+ const downloadExcel = () => {
338
+ if (!resolvedSheets.value || resolvedSheets.value.length === 0) return;
339
+
340
+ try {
341
+ const workbook = XLSX.utils.book_new();
342
+
343
+ // Add all sheets to workbook
344
+ resolvedSheets.value.forEach((sheet) => {
345
+ const worksheet = XLSX.utils.aoa_to_sheet(sheet.data);
346
+ XLSX.utils.book_append_sheet(workbook, worksheet, sheet.name);
347
+ });
348
+
349
+ // Generate filename
350
+ const filename = props.selectedResult.title ? `${props.selectedResult.title.replace(/[^a-z0-9]/gi, "_").toLowerCase()}.xlsx` : "spreadsheet.xlsx";
351
+
352
+ // Write file
353
+ XLSX.writeFile(workbook, filename);
354
+ } catch (error) {
355
+ console.error("Failed to download Excel:", error);
356
+ alert(`Failed to download Excel file: ${error instanceof Error ? error.message : "Unknown error"}`);
357
+ }
358
+ };
359
+
360
+ function handleDataEdit() {
361
+ // Just update the local state, don't apply yet
362
+ // User needs to click "Apply Changes" button
363
+ }
364
+
365
+ // `extractCellReferences` now lives in `./engine/formulaRefs.ts`
366
+ // (imported at the top of this file). Extracted to bring this
367
+ // file's cognitive complexity back under the sonarjs threshold
368
+ // and to make the formula-reference scanner unit-testable.
369
+ // See `test/plugins/spreadsheet/engine/test_formulaRefs.ts`.
370
+
371
+ function openMiniEditor(rowIndex: number, colIndex: number) {
372
+ try {
373
+ const sheets = JSON.parse(editableData.value);
374
+ const currentSheet = sheets[activeSheetIndex.value];
375
+
376
+ if (!currentSheet || !currentSheet.data) {
377
+ return;
378
+ }
379
+
380
+ // Normalize the data in case it's malformed
381
+ const normalizedData = normalizeSheetData(currentSheet.data);
382
+
383
+ if (!normalizedData[rowIndex] || normalizedData[rowIndex][colIndex] === undefined) {
384
+ return;
385
+ }
386
+
387
+ const cellValue = normalizedData[rowIndex][colIndex];
388
+
389
+ // Determine cell type and extract values (new format: {v, f})
390
+ if (isRecord(cellValue) && "v" in cellValue) {
391
+ const value = cellValue.v;
392
+ const format = typeof cellValue.f === "string" ? cellValue.f : "";
393
+
394
+ // Check if it's a formula (value starts with "=")
395
+ if (typeof value === "string" && value.startsWith("=")) {
396
+ miniEditorType.value = "object";
397
+ miniEditorValue.value = "";
398
+ miniEditorFormula.value = value.substring(1); // Remove "=" prefix
399
+ miniEditorFormat.value = format;
400
+ // Extract and store referenced cells for highlighting
401
+ referencedCells.value = extractCellReferences(value);
402
+ } else if (typeof value === "number") {
403
+ miniEditorType.value = "object";
404
+ miniEditorValue.value = "";
405
+ miniEditorFormula.value = String(value);
406
+ miniEditorFormat.value = format;
407
+ referencedCells.value = [];
408
+ } else {
409
+ miniEditorType.value = "string";
410
+ miniEditorValue.value = String(value);
411
+ miniEditorFormula.value = "";
412
+ miniEditorFormat.value = "";
413
+ referencedCells.value = [];
414
+ }
415
+ } else {
416
+ // Legacy format or plain value
417
+ miniEditorType.value = "string";
418
+ miniEditorValue.value = String(cellValue ?? "");
419
+ miniEditorFormula.value = "";
420
+ miniEditorFormat.value = "";
421
+ referencedCells.value = [];
422
+ }
423
+
424
+ miniEditorCell.value = { row: rowIndex, col: colIndex };
425
+ miniEditorOpen.value = true;
426
+ } catch (error) {
427
+ console.error("Failed to open mini editor:", error);
428
+ }
429
+ }
430
+
431
+ function closeMiniEditor() {
432
+ miniEditorOpen.value = false;
433
+ miniEditorCell.value = null;
434
+ miniEditorValue.value = null;
435
+ miniEditorFormula.value = "";
436
+ miniEditorFormat.value = "";
437
+ referencedCells.value = [];
438
+ }
439
+
440
+ function saveMiniEditor() {
441
+ if (!miniEditorCell.value) return;
442
+
443
+ try {
444
+ const sheets = JSON.parse(editableData.value);
445
+ const currentSheet = sheets[activeSheetIndex.value];
446
+
447
+ if (!currentSheet || !currentSheet.data) return;
448
+
449
+ const { row, col } = miniEditorCell.value;
450
+
451
+ // Normalize the data in case it's malformed
452
+ const normalizedData = normalizeSheetData(currentSheet.data);
453
+
454
+ // Ensure the row exists
455
+ while (normalizedData.length <= row) {
456
+ normalizedData.push([]);
457
+ }
458
+
459
+ // Ensure the row is an array
460
+ if (!Array.isArray(normalizedData[row])) {
461
+ normalizedData[row] = [];
462
+ }
463
+
464
+ // Build the new cell value (delegates formula/number detection
465
+ // and format attachment to the pure helper).
466
+ const newCellValue: SpreadsheetCell = buildCellFromInput({
467
+ type: miniEditorType.value,
468
+ value: miniEditorValue.value,
469
+ formula: miniEditorFormula.value,
470
+ format: miniEditorFormat.value,
471
+ });
472
+
473
+ // Update the cell in normalized data
474
+ normalizedData[row][col] = newCellValue;
475
+
476
+ // Update the sheet with normalized data
477
+ currentSheet.data = normalizedData;
478
+
479
+ // Update editableData
480
+ editableData.value = JSON.stringify(sheets, null, 2);
481
+
482
+ // Persist to disk (if file-backed) and emit update
483
+ persistSheets(sheets);
484
+
485
+ // Update referenced cells if the saved cell contains a formula
486
+ if (typeof newCellValue.v === "string" && newCellValue.v.startsWith("=")) {
487
+ referencedCells.value = extractCellReferences(newCellValue.v);
488
+ } else {
489
+ referencedCells.value = [];
490
+ }
491
+
492
+ // Don't close the mini editor - keep it open so user can see the updated references
493
+ // closeMiniEditor();
494
+ } catch (error) {
495
+ alert(`Failed to save cell: ${error instanceof Error ? error.message : "Unknown error"}`);
496
+ }
497
+ }
498
+
499
+ function handleTableClick(event: MouseEvent) {
500
+ const target = event.target as HTMLElement;
501
+
502
+ // Check if clicked element is a table cell
503
+ if (target.tagName !== "TD") return;
504
+
505
+ // Get the row and column indices
506
+ const cell = target as HTMLTableCellElement;
507
+ const row = cell.parentElement as HTMLTableRowElement;
508
+
509
+ const colIndex = cell.cellIndex;
510
+ const rowIndex = row.rowIndex;
511
+
512
+ // Check if the main editor details is open
513
+ const isEditorOpen = editorDetails.value?.open ?? false;
514
+
515
+ // If editor is closed, open mini editor
516
+ if (!isEditorOpen) {
517
+ openMiniEditor(rowIndex, colIndex);
518
+ return;
519
+ }
520
+
521
+ // If editor is open, try to find and select this cell in the editor.
522
+ if (!editorTextarea.value) return;
523
+ try {
524
+ const sheets = JSON.parse(editableData.value);
525
+ const currentSheet = sheets[activeSheetIndex.value];
526
+ if (!currentSheet || !currentSheet.data) return;
527
+ const normalizedData = normalizeSheetData(currentSheet.data);
528
+ if (!normalizedData[rowIndex] || normalizedData[rowIndex][colIndex] === undefined) {
529
+ return;
530
+ }
531
+ const cellStr = JSON.stringify(normalizedData[rowIndex][colIndex]);
532
+ const cellStart = findCellJsonPosition(editableData.value, currentSheet.name, rowIndex, colIndex);
533
+ if (cellStart < 0) return;
534
+ editorTextarea.value.focus();
535
+ editorTextarea.value.setSelectionRange(cellStart, cellStart + cellStr.length);
536
+ // Scroll the textarea to make the selection visible.
537
+ const textBeforeSelection = editableData.value.substring(0, cellStart);
538
+ const lineNumber = textBeforeSelection.split("\n").length;
539
+ const lineHeight = 22;
540
+ const textarea = editorTextarea.value;
541
+ textarea.scrollTop = Math.max(0, lineNumber * lineHeight - textarea.clientHeight / 2);
542
+ } catch (error) {
543
+ console.error("Failed to select cell in editor:", error);
544
+ }
545
+ }
546
+
547
+ async function applyChanges() {
548
+ try {
549
+ // Parse the edited JSON
550
+ const parsedSheets = JSON.parse(editableData.value);
551
+
552
+ // Validate it's an array
553
+ if (!Array.isArray(parsedSheets)) {
554
+ throw new Error("Data must be an array of sheets");
555
+ }
556
+
557
+ // Persist to disk (if file-backed) and emit update
558
+ await persistSheets(parsedSheets);
559
+
560
+ // Reset to first sheet after update
561
+ activeSheetIndex.value = 0;
562
+ } catch (error) {
563
+ alert(`Invalid JSON format: ${error instanceof Error ? error.message : "Unknown error"}`);
564
+ }
565
+ }
566
+
567
+ // Watch for external changes to selectedResult
568
+ watch(
569
+ () => props.selectedResult.data?.sheets,
570
+ () => {
571
+ fetchSheets().then(() => {
572
+ editableData.value = JSON.stringify(resolvedSheets.value || [], null, 2);
573
+ // Reset to first sheet when result changes
574
+ activeSheetIndex.value = 0;
575
+ });
576
+ },
577
+ );
578
+
579
+ // Reset active sheet if it's out of bounds
580
+ watch(
581
+ () => resolvedSheets.value?.length,
582
+ (length) => {
583
+ if (length && activeSheetIndex.value >= length) {
584
+ activeSheetIndex.value = 0;
585
+ }
586
+ },
587
+ );
588
+
589
+ // Highlight selected cell and referenced cells when mini editor is
590
+ // open. The per-step DOM work lives in cellHighlights.ts so this
591
+ // callback stays trivial and the complexity lands on the helpers,
592
+ // each of which is linear.
593
+ watch(
594
+ [miniEditorOpen, miniEditorCell, referencedCells, renderedHtml],
595
+ () => {
596
+ clearCellHighlights(tableContainer.value);
597
+ if (!miniEditorOpen.value) return;
598
+ applyCellHighlights(tableContainer.value, miniEditorCell.value, referencedCells.value);
599
+ },
600
+ { flush: "post" },
601
+ );
602
+
603
+ // Keyboard navigation handler
604
+ function handleKeyboardNavigation(event: KeyboardEvent) {
605
+ // Only handle arrow keys when mini editor is open and not focused on input
606
+ if (!miniEditorOpen.value || !miniEditorCell.value) return;
607
+
608
+ // Don't interfere if user is typing in an input field
609
+ const target = event.target as HTMLElement;
610
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
611
+ return;
612
+ }
613
+
614
+ const { row, col } = miniEditorCell.value;
615
+ let newRow = row;
616
+ let newCol = col;
617
+
618
+ // Determine new position based on arrow key
619
+ switch (event.key) {
620
+ case "ArrowUp":
621
+ newRow = Math.max(0, row - 1);
622
+ break;
623
+ case "ArrowDown":
624
+ newRow = row + 1;
625
+ break;
626
+ case "ArrowLeft":
627
+ newCol = Math.max(0, col - 1);
628
+ break;
629
+ case "ArrowRight":
630
+ newCol = col + 1;
631
+ break;
632
+ default:
633
+ return; // Not an arrow key, ignore
634
+ }
635
+
636
+ // Get current sheet data to validate bounds
637
+ try {
638
+ const sheets = JSON.parse(editableData.value);
639
+ const currentSheet = sheets[activeSheetIndex.value];
640
+
641
+ if (!currentSheet || !currentSheet.data) return;
642
+
643
+ // Validate new position is within bounds
644
+ if (newRow < 0 || newRow >= currentSheet.data.length || newCol < 0 || !currentSheet.data[newRow] || newCol >= currentSheet.data[newRow].length) {
645
+ return; // Out of bounds, ignore
646
+ }
647
+
648
+ // Prevent default scrolling behavior
649
+ event.preventDefault();
650
+
651
+ // Move to new cell
652
+ openMiniEditor(newRow, newCol);
653
+ } catch (error) {
654
+ console.error("Failed to navigate cells:", error);
655
+ }
656
+ }
657
+
658
+ // Add keyboard event listener on mount
659
+ onMounted(() => {
660
+ document.addEventListener("keydown", handleKeyboardNavigation);
661
+ });
662
+
663
+ // Remove keyboard event listener on unmount
664
+ onUnmounted(() => {
665
+ document.removeEventListener("keydown", handleKeyboardNavigation);
666
+ });
667
+ </script>
668
+
669
+ <style scoped>
670
+ .spreadsheet-container {
671
+ width: 100%;
672
+ height: 100%;
673
+ display: flex;
674
+ flex-direction: column;
675
+ background: white;
676
+ }
677
+
678
+ .spreadsheet-content-wrapper {
679
+ flex: 1;
680
+ overflow-y: auto;
681
+ min-height: 0;
682
+ }
683
+
684
+ .header {
685
+ display: flex;
686
+ align-items: center;
687
+ justify-content: space-between;
688
+ margin-bottom: 1em;
689
+ }
690
+
691
+ .title {
692
+ font-size: 2em;
693
+ margin: 0;
694
+ font-weight: bold;
695
+ }
696
+
697
+ .button-group {
698
+ display: flex;
699
+ gap: 0.5em;
700
+ }
701
+
702
+ .download-btn {
703
+ padding: 0.5em 1em;
704
+ color: white;
705
+ border: none;
706
+ border-radius: 4px;
707
+ cursor: pointer;
708
+ font-size: 0.9em;
709
+ display: flex;
710
+ align-items: center;
711
+ gap: 0.5em;
712
+ transition: background-color 0.2s;
713
+ }
714
+
715
+ .excel-btn {
716
+ background-color: #217346;
717
+ }
718
+
719
+ .excel-btn:hover {
720
+ background-color: #1e6a3f;
721
+ }
722
+
723
+ .excel-btn:active {
724
+ background-color: #1a5c36;
725
+ }
726
+
727
+ .download-btn .material-icons {
728
+ font-size: 1.2em;
729
+ }
730
+
731
+ /* Sheet tabs */
732
+ .sheet-tabs {
733
+ display: flex;
734
+ gap: 0.25em;
735
+ margin-bottom: 1em;
736
+ border-bottom: 2px solid #e0e0e0;
737
+ }
738
+
739
+ .sheet-tab {
740
+ padding: 0.5em 1em;
741
+ background: #f5f5f5;
742
+ border: none;
743
+ border-top-left-radius: 4px;
744
+ border-top-right-radius: 4px;
745
+ cursor: pointer;
746
+ font-size: 0.9em;
747
+ color: #666;
748
+ transition: background-color 0.2s;
749
+ }
750
+
751
+ .sheet-tab:hover {
752
+ background: #e8e8e8;
753
+ }
754
+
755
+ .sheet-tab.active {
756
+ background: white;
757
+ color: #333;
758
+ font-weight: 500;
759
+ border-bottom: 2px solid white;
760
+ margin-bottom: -2px;
761
+ }
762
+
763
+ /* Table container */
764
+ .table-container {
765
+ overflow-x: auto;
766
+ background: white;
767
+ }
768
+
769
+ /* Style the generated table */
770
+ .table-container :deep(table) {
771
+ border-collapse: collapse;
772
+ width: 100%;
773
+ font-family: "Segoe UI", Arial, sans-serif;
774
+ font-size: 0.9em;
775
+ }
776
+
777
+ .table-container :deep(td),
778
+ .table-container :deep(th) {
779
+ border: 1px solid #d0d0d0;
780
+ padding: 0.5em 0.75em;
781
+ text-align: left;
782
+ }
783
+
784
+ .table-container :deep(th) {
785
+ background-color: #f5f5f5;
786
+ font-weight: 600;
787
+ color: #333;
788
+ }
789
+
790
+ .table-container :deep(tr:nth-child(even)) {
791
+ background-color: #fafafa;
792
+ }
793
+
794
+ .table-container :deep(tr:hover) {
795
+ background-color: #f0f0f0;
796
+ }
797
+
798
+ .table-container :deep(.cell-editing) {
799
+ background-color: #e8f5e9 !important;
800
+ outline: 2px solid #217346 !important;
801
+ outline-offset: -2px;
802
+ }
803
+
804
+ .table-container :deep(.cell-referenced) {
805
+ background-color: #fff3e0 !important;
806
+ outline: 2px solid #ff9800 !important;
807
+ outline-offset: -2px;
808
+ }
809
+
810
+ /* Error message */
811
+ .error {
812
+ padding: 1em;
813
+ background: #ffebee;
814
+ color: #c62828;
815
+ border-radius: 4px;
816
+ margin: 1em 0;
817
+ }
818
+
819
+ /* Editor section */
820
+ .spreadsheet-source {
821
+ padding: 0.5rem;
822
+ background: #f5f5f5;
823
+ border-top: 1px solid #e0e0e0;
824
+ font-family: monospace;
825
+ font-size: 0.85rem;
826
+ flex-shrink: 0;
827
+ }
828
+
829
+ .spreadsheet-source summary {
830
+ cursor: pointer;
831
+ user-select: none;
832
+ padding: 0.5rem;
833
+ background: #e8e8e8;
834
+ border-radius: 4px;
835
+ font-weight: 500;
836
+ color: #333;
837
+ }
838
+
839
+ .spreadsheet-source[open] summary {
840
+ margin-bottom: 0.5rem;
841
+ }
842
+
843
+ .spreadsheet-source summary:hover {
844
+ background: #d8d8d8;
845
+ }
846
+
847
+ .spreadsheet-editor {
848
+ width: 100%;
849
+ height: 40vh;
850
+ padding: 1rem;
851
+ background: #ffffff;
852
+ border: 1px solid #ccc;
853
+ border-radius: 4px;
854
+ color: #333;
855
+ font-family: "Courier New", monospace;
856
+ font-size: 0.9rem;
857
+ resize: vertical;
858
+ margin-bottom: 0.5rem;
859
+ line-height: 1.5;
860
+ }
861
+
862
+ .spreadsheet-editor:focus {
863
+ outline: none;
864
+ border-color: #217346;
865
+ box-shadow: 0 0 0 2px rgba(33, 115, 70, 0.1);
866
+ }
867
+
868
+ .apply-btn {
869
+ padding: 0.5rem 1rem;
870
+ background: #217346;
871
+ color: white;
872
+ border: none;
873
+ border-radius: 4px;
874
+ cursor: pointer;
875
+ font-size: 0.9rem;
876
+ transition: background 0.2s;
877
+ font-weight: 500;
878
+ }
879
+
880
+ .apply-btn:hover {
881
+ background: #1e6a3f;
882
+ }
883
+
884
+ .apply-btn:active {
885
+ background: #1a5c36;
886
+ }
887
+
888
+ .apply-btn:disabled {
889
+ background: #cccccc;
890
+ color: #666666;
891
+ cursor: not-allowed;
892
+ opacity: 0.6;
893
+ }
894
+
895
+ .apply-btn:disabled:hover {
896
+ background: #cccccc;
897
+ }
898
+
899
+ /* Mini Editor Panel */
900
+ .mini-editor-panel {
901
+ background: #f8f8f8;
902
+ border-top: 1px solid #d0d0d0;
903
+ flex-shrink: 0;
904
+ }
905
+
906
+ .mini-editor-content {
907
+ display: flex;
908
+ align-items: center;
909
+ gap: 0.5rem;
910
+ padding: 0.5rem 1rem;
911
+ }
912
+
913
+ .cell-ref {
914
+ font-family: monospace;
915
+ font-weight: 600;
916
+ color: #217346;
917
+ font-size: 0.85rem;
918
+ min-width: 2rem;
919
+ }
920
+
921
+ .radio-group {
922
+ display: flex;
923
+ gap: 0.75rem;
924
+ border-right: 1px solid #d0d0d0;
925
+ padding-right: 0.75rem;
926
+ }
927
+
928
+ .radio-option {
929
+ display: flex;
930
+ align-items: center;
931
+ gap: 0.25rem;
932
+ cursor: pointer;
933
+ font-size: 0.85rem;
934
+ color: #555;
935
+ }
936
+
937
+ .radio-option input[type="radio"] {
938
+ cursor: pointer;
939
+ width: 0.9rem;
940
+ height: 0.9rem;
941
+ }
942
+
943
+ .form-input {
944
+ flex: 1;
945
+ padding: 0.4rem 0.6rem;
946
+ border: 1px solid #ccc;
947
+ border-radius: 3px;
948
+ font-size: 0.85rem;
949
+ font-family: inherit;
950
+ transition: border-color 0.2s;
951
+ min-width: 120px;
952
+ }
953
+
954
+ .form-input:focus {
955
+ outline: none;
956
+ border-color: #217346;
957
+ box-shadow: 0 0 0 2px rgba(33, 115, 70, 0.1);
958
+ }
959
+
960
+ .form-input::placeholder {
961
+ color: #999;
962
+ font-size: 0.8rem;
963
+ }
964
+
965
+ .save-btn,
966
+ .cancel-btn {
967
+ padding: 0.4rem 0.8rem;
968
+ border: none;
969
+ border-radius: 3px;
970
+ cursor: pointer;
971
+ font-size: 0.85rem;
972
+ font-weight: 500;
973
+ transition: background 0.2s;
974
+ }
975
+
976
+ .save-btn {
977
+ background: #217346;
978
+ color: white;
979
+ }
980
+
981
+ .save-btn:hover {
982
+ background: #1e6a3f;
983
+ }
984
+
985
+ .cancel-btn {
986
+ background: transparent;
987
+ color: #666;
988
+ padding: 0.4rem;
989
+ font-size: 1.2rem;
990
+ line-height: 1;
991
+ }
992
+
993
+ .cancel-btn:hover {
994
+ color: #333;
995
+ background: #e0e0e0;
996
+ }
997
+ </style>