mulmoclaude 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/bin/mulmoclaude.js +202 -0
- package/bin/prepare-dist.js +93 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
- package/client/assets/index-D8rhwXLq.js +4906 -0
- package/client/assets/index-KNLBjwuh.css +1 -0
- package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
- package/client/assets/material-icons-Dr0goTwe.woff +0 -0
- package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
- package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
- package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
- package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
- package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
- package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
- package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
- package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
- package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
- package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
- package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
- package/client/index.html +28 -0
- package/package.json +66 -0
- package/server/agent/attachmentConverter.ts +270 -0
- package/server/agent/config.ts +414 -0
- package/server/agent/index.ts +260 -0
- package/server/agent/mcp-server.ts +412 -0
- package/server/agent/mcp-tools/index.ts +63 -0
- package/server/agent/mcp-tools/x.ts +188 -0
- package/server/agent/plugin-names.ts +75 -0
- package/server/agent/prompt.ts +349 -0
- package/server/agent/resumeFailover.ts +129 -0
- package/server/agent/sandboxMounts.ts +329 -0
- package/server/agent/stream.ts +194 -0
- package/server/api/auth/bearerAuth.ts +61 -0
- package/server/api/auth/token.ts +98 -0
- package/server/api/csrfGuard.ts +85 -0
- package/server/api/routes/agent.ts +478 -0
- package/server/api/routes/chart.ts +98 -0
- package/server/api/routes/chat-index.ts +46 -0
- package/server/api/routes/config.ts +258 -0
- package/server/api/routes/dispatchResponse.ts +79 -0
- package/server/api/routes/files.ts +812 -0
- package/server/api/routes/html.ts +101 -0
- package/server/api/routes/image.ts +169 -0
- package/server/api/routes/mulmo-script.ts +712 -0
- package/server/api/routes/mulmoScriptValidate.ts +101 -0
- package/server/api/routes/notifications.ts +69 -0
- package/server/api/routes/pdf.ts +163 -0
- package/server/api/routes/plugins.ts +276 -0
- package/server/api/routes/presentHtml.ts +48 -0
- package/server/api/routes/roles.ts +125 -0
- package/server/api/routes/scheduler.ts +153 -0
- package/server/api/routes/schedulerHandlers.ts +151 -0
- package/server/api/routes/schedulerTasks.ts +163 -0
- package/server/api/routes/sessions.ts +294 -0
- package/server/api/routes/sessionsCursor.ts +59 -0
- package/server/api/routes/skills.ts +195 -0
- package/server/api/routes/sources.ts +540 -0
- package/server/api/routes/todos.ts +263 -0
- package/server/api/routes/todosColumnsHandlers.ts +347 -0
- package/server/api/routes/todosHandlers.ts +274 -0
- package/server/api/routes/todosItemsHandlers.ts +386 -0
- package/server/api/routes/wiki/pageIndex.ts +53 -0
- package/server/api/routes/wiki.ts +363 -0
- package/server/api/sandboxStatus.ts +64 -0
- package/server/events/notifications.ts +160 -0
- package/server/events/pub-sub/index.ts +45 -0
- package/server/events/relay-client.ts +288 -0
- package/server/events/scheduler-adapter.ts +302 -0
- package/server/events/session-store/index.ts +492 -0
- package/server/events/task-manager/index.ts +181 -0
- package/server/index.ts +572 -0
- package/server/system/config.ts +243 -0
- package/server/system/credentials.ts +220 -0
- package/server/system/docker.ts +97 -0
- package/server/system/env.ts +109 -0
- package/server/system/logger/config.ts +112 -0
- package/server/system/logger/formatters.ts +40 -0
- package/server/system/logger/index.ts +53 -0
- package/server/system/logger/rotation.ts +37 -0
- package/server/system/logger/sinks.ts +101 -0
- package/server/system/logger/types.ts +29 -0
- package/server/utils/date.ts +57 -0
- package/server/utils/errors.ts +7 -0
- package/server/utils/fetch.ts +27 -0
- package/server/utils/files/atomic.ts +125 -0
- package/server/utils/files/html-io.ts +20 -0
- package/server/utils/files/image-store.ts +66 -0
- package/server/utils/files/index.ts +45 -0
- package/server/utils/files/journal-io.ts +213 -0
- package/server/utils/files/json.ts +69 -0
- package/server/utils/files/markdown-store.ts +33 -0
- package/server/utils/files/naming.ts +50 -0
- package/server/utils/files/reference-dirs-io.ts +45 -0
- package/server/utils/files/roles-io.ts +45 -0
- package/server/utils/files/safe.ts +106 -0
- package/server/utils/files/scheduler-io.ts +20 -0
- package/server/utils/files/scheduler-overrides-io.ts +64 -0
- package/server/utils/files/session-io.ts +136 -0
- package/server/utils/files/spreadsheet-store.ts +63 -0
- package/server/utils/files/todos-io.ts +29 -0
- package/server/utils/files/user-tasks-io.ts +25 -0
- package/server/utils/files/workspace-io.ts +221 -0
- package/server/utils/gemini.ts +59 -0
- package/server/utils/gitignore.ts +69 -0
- package/server/utils/http.ts +15 -0
- package/server/utils/httpError.ts +61 -0
- package/server/utils/id.ts +16 -0
- package/server/utils/json.ts +83 -0
- package/server/utils/logBackgroundError.ts +22 -0
- package/server/utils/markdown.ts +82 -0
- package/server/utils/request.ts +29 -0
- package/server/utils/slug.ts +50 -0
- package/server/utils/spawn.ts +62 -0
- package/server/utils/time.ts +34 -0
- package/server/utils/types.ts +47 -0
- package/server/workspace/chat-index/index.ts +153 -0
- package/server/workspace/chat-index/indexer.ts +209 -0
- package/server/workspace/chat-index/paths.ts +34 -0
- package/server/workspace/chat-index/summarizer.ts +247 -0
- package/server/workspace/chat-index/types.ts +38 -0
- package/server/workspace/custom-dirs.ts +220 -0
- package/server/workspace/helps/business.md +104 -0
- package/server/workspace/helps/github.md +23 -0
- package/server/workspace/helps/index.md +60 -0
- package/server/workspace/helps/mulmoscript.md +249 -0
- package/server/workspace/helps/sandbox.md +90 -0
- package/server/workspace/helps/spreadsheet.md +43 -0
- package/server/workspace/helps/telegram.md +135 -0
- package/server/workspace/helps/wiki.md +131 -0
- package/server/workspace/journal/archivist.ts +386 -0
- package/server/workspace/journal/dailyPass.ts +743 -0
- package/server/workspace/journal/diff.ts +71 -0
- package/server/workspace/journal/index.ts +185 -0
- package/server/workspace/journal/indexFile.ts +136 -0
- package/server/workspace/journal/linkRewrite.ts +4 -0
- package/server/workspace/journal/memoryExtractor.ts +130 -0
- package/server/workspace/journal/optimizationPass.ts +160 -0
- package/server/workspace/journal/paths.ts +76 -0
- package/server/workspace/journal/state.ts +125 -0
- package/server/workspace/paths.ts +158 -0
- package/server/workspace/reference-dirs.ts +252 -0
- package/server/workspace/roles.ts +37 -0
- package/server/workspace/skills/discovery.ts +125 -0
- package/server/workspace/skills/index.ts +10 -0
- package/server/workspace/skills/parser.ts +144 -0
- package/server/workspace/skills/paths.ts +41 -0
- package/server/workspace/skills/scheduler.ts +149 -0
- package/server/workspace/skills/types.ts +30 -0
- package/server/workspace/skills/user-tasks.ts +257 -0
- package/server/workspace/skills/writer.ts +189 -0
- package/server/workspace/sources/arxivDiscovery.ts +182 -0
- package/server/workspace/sources/classifier.ts +268 -0
- package/server/workspace/sources/fetchers/arxiv.ts +170 -0
- package/server/workspace/sources/fetchers/github.ts +106 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
- package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
- package/server/workspace/sources/fetchers/index.ts +71 -0
- package/server/workspace/sources/fetchers/registerAll.ts +15 -0
- package/server/workspace/sources/fetchers/rss.ts +141 -0
- package/server/workspace/sources/fetchers/rssParser.ts +295 -0
- package/server/workspace/sources/httpFetcher.ts +230 -0
- package/server/workspace/sources/interests.ts +120 -0
- package/server/workspace/sources/paths.ts +110 -0
- package/server/workspace/sources/pipeline/dedup.ts +60 -0
- package/server/workspace/sources/pipeline/fetch.ts +136 -0
- package/server/workspace/sources/pipeline/index.ts +249 -0
- package/server/workspace/sources/pipeline/notify.ts +72 -0
- package/server/workspace/sources/pipeline/plan.ts +66 -0
- package/server/workspace/sources/pipeline/summarize.ts +189 -0
- package/server/workspace/sources/pipeline/write.ts +185 -0
- package/server/workspace/sources/rateLimiter.ts +148 -0
- package/server/workspace/sources/registry.ts +326 -0
- package/server/workspace/sources/robots.ts +271 -0
- package/server/workspace/sources/sourceState.ts +135 -0
- package/server/workspace/sources/taxonomy.ts +74 -0
- package/server/workspace/sources/types.ts +144 -0
- package/server/workspace/sources/urls.ts +112 -0
- package/server/workspace/tool-trace/classify.ts +114 -0
- package/server/workspace/tool-trace/index.ts +250 -0
- package/server/workspace/tool-trace/writeSearch.ts +98 -0
- package/server/workspace/wiki-backlinks/index.ts +107 -0
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
- package/server/workspace/workspace.ts +66 -0
- package/src/App.vue +720 -0
- package/src/assets/mulmo_bw.png +0 -0
- package/src/components/CanvasViewToggle.vue +27 -0
- package/src/components/ChatAttachmentPreview.vue +45 -0
- package/src/components/ChatImagePreview.vue +17 -0
- package/src/components/ChatInput.vue +208 -0
- package/src/components/FileContentHeader.vue +49 -0
- package/src/components/FileContentRenderer.vue +162 -0
- package/src/components/FileTree.vue +115 -0
- package/src/components/FileTreePane.vue +85 -0
- package/src/components/FilesView.vue +206 -0
- package/src/components/LockStatusPopup.vue +111 -0
- package/src/components/NotificationBell.vue +131 -0
- package/src/components/NotificationToast.vue +72 -0
- package/src/components/PluginLauncher.vue +138 -0
- package/src/components/RightSidebar.vue +113 -0
- package/src/components/RoleSelector.vue +64 -0
- package/src/components/SessionHistoryPanel.vue +176 -0
- package/src/components/SessionTabBar.vue +81 -0
- package/src/components/SettingsMcpTab.vue +350 -0
- package/src/components/SettingsModal.vue +275 -0
- package/src/components/SettingsReferenceDirsTab.vue +173 -0
- package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
- package/src/components/SidebarHeader.vue +69 -0
- package/src/components/StackView.vue +360 -0
- package/src/components/SuggestionsPanel.vue +65 -0
- package/src/components/TodoExplorer.vue +358 -0
- package/src/components/ToolResultsPanel.vue +77 -0
- package/src/components/todo/TodoAddDialog.vue +131 -0
- package/src/components/todo/TodoEditDialog.vue +47 -0
- package/src/components/todo/TodoEditPanel.vue +113 -0
- package/src/components/todo/TodoKanbanView.vue +249 -0
- package/src/components/todo/TodoListView.vue +79 -0
- package/src/components/todo/TodoTableView.vue +177 -0
- package/src/composables/useActiveSession.ts +40 -0
- package/src/composables/useAppApi.ts +45 -0
- package/src/composables/useCanvasViewMode.ts +121 -0
- package/src/composables/useChatScroll.ts +47 -0
- package/src/composables/useClickOutside.ts +26 -0
- package/src/composables/useClipboardCopy.ts +44 -0
- package/src/composables/useContentDisplay.ts +52 -0
- package/src/composables/useDebugBeat.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +115 -0
- package/src/composables/useEventListeners.ts +42 -0
- package/src/composables/useExpandedDirs.ts +64 -0
- package/src/composables/useFaviconState.ts +30 -0
- package/src/composables/useFileSelection.ts +115 -0
- package/src/composables/useFileSortMode.ts +24 -0
- package/src/composables/useFileTree.ts +85 -0
- package/src/composables/useFreshPluginData.ts +89 -0
- package/src/composables/useHealth.ts +38 -0
- package/src/composables/useImeAwareEnter.ts +57 -0
- package/src/composables/useKeyNavigation.ts +60 -0
- package/src/composables/useMarkdownLinkHandler.ts +46 -0
- package/src/composables/useMarkdownMode.ts +17 -0
- package/src/composables/useMcpTools.ts +71 -0
- package/src/composables/useMergedSessions.ts +27 -0
- package/src/composables/useNotifications.ts +90 -0
- package/src/composables/usePdfDownload.ts +60 -0
- package/src/composables/usePendingCalls.ts +77 -0
- package/src/composables/usePubSub.ts +85 -0
- package/src/composables/useRightSidebar.ts +23 -0
- package/src/composables/useRoles.ts +34 -0
- package/src/composables/useSandboxStatus.ts +67 -0
- package/src/composables/useSelectedResult.ts +49 -0
- package/src/composables/useSessionDerived.ts +51 -0
- package/src/composables/useSessionHistory.ts +81 -0
- package/src/composables/useSessionSync.ts +57 -0
- package/src/composables/useViewLayout.ts +55 -0
- package/src/config/apiRoutes.ts +173 -0
- package/src/config/pubsubChannels.ts +45 -0
- package/src/config/roles.ts +335 -0
- package/src/config/schedulerActions.ts +25 -0
- package/src/config/toolNames.ts +71 -0
- package/src/config/workspacePaths.ts +24 -0
- package/src/index.css +107 -0
- package/src/main.ts +25 -0
- package/src/plugins/canvas/Preview.vue +13 -0
- package/src/plugins/canvas/View.vue +333 -0
- package/src/plugins/canvas/definition.ts +38 -0
- package/src/plugins/canvas/index.ts +36 -0
- package/src/plugins/chart/Preview.vue +49 -0
- package/src/plugins/chart/View.vue +143 -0
- package/src/plugins/chart/definition.ts +58 -0
- package/src/plugins/chart/index.ts +52 -0
- package/src/plugins/editImage/Preview.vue +13 -0
- package/src/plugins/editImage/View.vue +13 -0
- package/src/plugins/editImage/definition.ts +27 -0
- package/src/plugins/editImage/index.ts +36 -0
- package/src/plugins/generateImage/Preview.vue +13 -0
- package/src/plugins/generateImage/View.vue +33 -0
- package/src/plugins/generateImage/definition.ts +32 -0
- package/src/plugins/generateImage/index.ts +56 -0
- package/src/plugins/manageRoles/Preview.vue +49 -0
- package/src/plugins/manageRoles/View.vue +525 -0
- package/src/plugins/manageRoles/definition.ts +43 -0
- package/src/plugins/manageRoles/index.ts +47 -0
- package/src/plugins/manageSkills/Preview.vue +21 -0
- package/src/plugins/manageSkills/View.vue +321 -0
- package/src/plugins/manageSkills/definition.ts +49 -0
- package/src/plugins/manageSkills/index.ts +49 -0
- package/src/plugins/manageSource/Preview.vue +33 -0
- package/src/plugins/manageSource/View.vue +697 -0
- package/src/plugins/manageSource/definition.ts +63 -0
- package/src/plugins/manageSource/index.ts +66 -0
- package/src/plugins/markdown/Preview.vue +77 -0
- package/src/plugins/markdown/View.vue +476 -0
- package/src/plugins/markdown/definition.ts +50 -0
- package/src/plugins/markdown/index.ts +36 -0
- package/src/plugins/presentHtml/Preview.vue +25 -0
- package/src/plugins/presentHtml/View.vue +52 -0
- package/src/plugins/presentHtml/definition.ts +27 -0
- package/src/plugins/presentHtml/helpers.ts +72 -0
- package/src/plugins/presentHtml/index.ts +41 -0
- package/src/plugins/presentMulmoScript/Preview.vue +23 -0
- package/src/plugins/presentMulmoScript/View.vue +1166 -0
- package/src/plugins/presentMulmoScript/definition.ts +95 -0
- package/src/plugins/presentMulmoScript/helpers.ts +162 -0
- package/src/plugins/presentMulmoScript/index.ts +40 -0
- package/src/plugins/scheduler/Preview.vue +67 -0
- package/src/plugins/scheduler/TasksTab.vue +205 -0
- package/src/plugins/scheduler/View.vue +565 -0
- package/src/plugins/scheduler/definition.ts +57 -0
- package/src/plugins/scheduler/index.ts +45 -0
- package/src/plugins/scheduler/viewModes.ts +26 -0
- package/src/plugins/spreadsheet/Preview.vue +29 -0
- package/src/plugins/spreadsheet/View.vue +997 -0
- package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
- package/src/plugins/spreadsheet/definition.ts +121 -0
- package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
- package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
- package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
- package/src/plugins/spreadsheet/engine/engine.ts +176 -0
- package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
- package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
- package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
- package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
- package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
- package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
- package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
- package/src/plugins/spreadsheet/engine/index.ts +27 -0
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
- package/src/plugins/spreadsheet/engine/parser.ts +143 -0
- package/src/plugins/spreadsheet/engine/registry.ts +150 -0
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
- package/src/plugins/spreadsheet/engine/types.ts +64 -0
- package/src/plugins/spreadsheet/index.ts +36 -0
- package/src/plugins/textResponse/Preview.vue +94 -0
- package/src/plugins/textResponse/View.vue +503 -0
- package/src/plugins/textResponse/definition.ts +34 -0
- package/src/plugins/textResponse/index.ts +27 -0
- package/src/plugins/textResponse/plugin.ts +29 -0
- package/src/plugins/textResponse/samples.ts +97 -0
- package/src/plugins/textResponse/types.ts +11 -0
- package/src/plugins/todo/Preview.vue +63 -0
- package/src/plugins/todo/View.vue +364 -0
- package/src/plugins/todo/composables/useTodos.ts +177 -0
- package/src/plugins/todo/definition.ts +45 -0
- package/src/plugins/todo/index.ts +61 -0
- package/src/plugins/todo/labels.ts +163 -0
- package/src/plugins/todo/priority.ts +98 -0
- package/src/plugins/todo/viewModes.ts +19 -0
- package/src/plugins/ui-image/ImagePreview.vue +23 -0
- package/src/plugins/ui-image/ImageView.vue +34 -0
- package/src/plugins/ui-image/index.ts +3 -0
- package/src/plugins/ui-image/types.ts +4 -0
- package/src/plugins/wiki/Preview.vue +65 -0
- package/src/plugins/wiki/View.vue +342 -0
- package/src/plugins/wiki/definition.ts +25 -0
- package/src/plugins/wiki/helpers.ts +59 -0
- package/src/plugins/wiki/index.ts +52 -0
- package/src/router/guards.ts +61 -0
- package/src/router/index.ts +50 -0
- package/src/tools/index.ts +52 -0
- package/src/tools/types.ts +27 -0
- package/src/types/events.ts +16 -0
- package/src/types/fileTree.ts +13 -0
- package/src/types/notification.ts +67 -0
- package/src/types/session.ts +116 -0
- package/src/types/sse.ts +90 -0
- package/src/types/toolCallHistory.ts +13 -0
- package/src/utils/agent/eventDispatch.ts +74 -0
- package/src/utils/agent/request.ts +55 -0
- package/src/utils/agent/toolCalls.ts +62 -0
- package/src/utils/api.ts +218 -0
- package/src/utils/canvas/viewMode.ts +46 -0
- package/src/utils/dom/authTokenMeta.ts +20 -0
- package/src/utils/dom/clickOutside.ts +11 -0
- package/src/utils/dom/externalLink.ts +57 -0
- package/src/utils/dom/scrollable.ts +24 -0
- package/src/utils/errors.ts +11 -0
- package/src/utils/files/expandedDirs.ts +25 -0
- package/src/utils/files/filename.ts +12 -0
- package/src/utils/files/sortChildren.ts +20 -0
- package/src/utils/filesPreview/schedulerPreview.ts +38 -0
- package/src/utils/filesPreview/todoPreview.ts +40 -0
- package/src/utils/format/date.ts +85 -0
- package/src/utils/format/frontmatter.ts +80 -0
- package/src/utils/format/jsonSyntax.ts +109 -0
- package/src/utils/html/previewCsp.ts +65 -0
- package/src/utils/image/resolve.ts +8 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
- package/src/utils/markdown/extractFirstH1.ts +39 -0
- package/src/utils/notification/dispatch.ts +22 -0
- package/src/utils/path/relativeLink.ts +130 -0
- package/src/utils/role/icon.ts +20 -0
- package/src/utils/role/merge.ts +10 -0
- package/src/utils/role/plugins.ts +12 -0
- package/src/utils/session/mergeSessions.ts +103 -0
- package/src/utils/session/seedRoleDefault.ts +35 -0
- package/src/utils/session/sessionEntries.ts +121 -0
- package/src/utils/session/sessionFactory.ts +22 -0
- package/src/utils/session/sessionHelpers.ts +99 -0
- package/src/utils/tools/dedup.ts +17 -0
- package/src/utils/tools/mcp.ts +33 -0
- package/src/utils/tools/pendingCalls.ts +16 -0
- package/src/utils/tools/result.ts +40 -0
- package/src/utils/types.ts +44 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM helpers for the mini-editor cell-highlight pass. Extracted from
|
|
3
|
+
* the post-flush watch in `src/plugins/spreadsheet/View.vue`, which
|
|
4
|
+
* had a cognitive complexity of 30 driven by four levels of nested
|
|
5
|
+
* optional chaining + loops.
|
|
6
|
+
*
|
|
7
|
+
* These helpers are side-effectful by nature (they add/remove CSS
|
|
8
|
+
* classes on DOM nodes) but each one is small enough that its
|
|
9
|
+
* behaviour is obvious. Unit-testable with a minimal mock DOM; see
|
|
10
|
+
* `test/plugins/spreadsheet/test_cellHighlights.ts`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Minimal DOM surface the helpers need. Defined here so tests can
|
|
14
|
+
* pass plain objects without pulling in jsdom. */
|
|
15
|
+
export interface HighlightableElement {
|
|
16
|
+
classList: { add: (cls: string) => void; remove: (cls: string) => void };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HighlightableRow {
|
|
20
|
+
querySelectorAll: (selector: string) => ArrayLike<HighlightableElement>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HighlightableTable {
|
|
24
|
+
querySelectorAll: (selector: string) => ArrayLike<HighlightableRow>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface HighlightableContainer {
|
|
28
|
+
// Overload: the spreadsheet root container is known to return a
|
|
29
|
+
// table when asked for the table id, so callers can keep the
|
|
30
|
+
// result strongly typed without casting.
|
|
31
|
+
querySelector(selector: "#spreadsheet-table"): HighlightableTable | null;
|
|
32
|
+
querySelector(selector: string): HighlightableElement | null;
|
|
33
|
+
querySelectorAll(selector: string): ArrayLike<HighlightableElement> & Iterable<HighlightableElement>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CellCoord {
|
|
37
|
+
row: number;
|
|
38
|
+
col: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CELL_EDITING = "cell-editing";
|
|
42
|
+
const CELL_REFERENCED = "cell-referenced";
|
|
43
|
+
|
|
44
|
+
/** Remove both kinds of highlight classes from the container. */
|
|
45
|
+
export function clearCellHighlights(container: HighlightableContainer | null | undefined): void {
|
|
46
|
+
if (!container) return;
|
|
47
|
+
container.querySelector(`.${CELL_EDITING}`)?.classList.remove(CELL_EDITING);
|
|
48
|
+
for (const cell of container.querySelectorAll(`.${CELL_REFERENCED}`)) {
|
|
49
|
+
cell.classList.remove(CELL_REFERENCED);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Add `className` to the <td> at (row, col) of the given table.
|
|
54
|
+
* No-op if the row or cell doesn't exist. */
|
|
55
|
+
export function highlightCell(table: HighlightableTable | null | undefined, coord: CellCoord, className: string): void {
|
|
56
|
+
if (!table) return;
|
|
57
|
+
const rows = table.querySelectorAll("tr");
|
|
58
|
+
const row = rows[coord.row];
|
|
59
|
+
if (!row) return;
|
|
60
|
+
const cells = row.querySelectorAll("td");
|
|
61
|
+
const cell = cells[coord.col];
|
|
62
|
+
if (!cell) return;
|
|
63
|
+
cell.classList.add(className);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Apply the editing cell + referenced cells highlights. Looks up
|
|
67
|
+
* the #spreadsheet-table inside the container and no-ops if the
|
|
68
|
+
* table hasn't rendered yet. */
|
|
69
|
+
export function applyCellHighlights(
|
|
70
|
+
container: HighlightableContainer | null | undefined,
|
|
71
|
+
editingCell: CellCoord | null,
|
|
72
|
+
references: readonly CellCoord[],
|
|
73
|
+
): void {
|
|
74
|
+
if (!container) return;
|
|
75
|
+
const table = container.querySelector("#spreadsheet-table");
|
|
76
|
+
if (!table) return;
|
|
77
|
+
if (editingCell) highlightCell(table, editingCell, CELL_EDITING);
|
|
78
|
+
for (const ref of references) highlightCell(table, ref, CELL_REFERENCED);
|
|
79
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ToolDefinition } from "gui-chat-protocol";
|
|
2
|
+
|
|
3
|
+
export const TOOL_NAME = "presentSpreadsheet";
|
|
4
|
+
|
|
5
|
+
export interface SpreadsheetCell {
|
|
6
|
+
v: string | number;
|
|
7
|
+
f?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SpreadsheetSheet {
|
|
11
|
+
name: string;
|
|
12
|
+
data: Array<Array<SpreadsheetCell>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SpreadsheetToolData {
|
|
16
|
+
sheets: SpreadsheetSheet[] | string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SpreadsheetArgs {
|
|
20
|
+
title: string;
|
|
21
|
+
sheets: SpreadsheetSheet[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const toolDefinition: ToolDefinition = {
|
|
25
|
+
type: "function",
|
|
26
|
+
name: TOOL_NAME,
|
|
27
|
+
description: "Display an Excel-like spreadsheet with formulas and calculations.",
|
|
28
|
+
prompt: `Use ${TOOL_NAME} when the user asks for a spreadsheet, table with calculations, or what-if analysis. Use formulas and cell references instead of pre-calculated values so the spreadsheet stays interactive. For cell format details and available functions, read \`helps/spreadsheet.md\` in the workspace.`,
|
|
29
|
+
parameters: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
title: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Title for the spreadsheet",
|
|
35
|
+
},
|
|
36
|
+
sheets: {
|
|
37
|
+
type: "array",
|
|
38
|
+
description: "Sheets to render as spreadsheet tabs. Each sheet includes a name and 2D array of cells (rows x columns).",
|
|
39
|
+
items: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
name: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "Sheet name (e.g., 'Sales Q1', 'Summary')",
|
|
45
|
+
},
|
|
46
|
+
data: {
|
|
47
|
+
type: "array",
|
|
48
|
+
description:
|
|
49
|
+
'Rows of cells. Each cell is an object with \'v\' (value) and \'f\' (format). Use Excel-style A1 notation in formulas: columns are letters (A, B, C...), rows are 1-based numbers (1, 2, 3...). Values can be text, numbers, dates, or formulas. Examples: [{"v": "Product"}, {"v": 2024, "f": "#,##0"}, {"v": "01/15/2025", "f": "MM/DD/YYYY"}, {"v": "=B2*1.05", "f": "$#,##0.00"}]. Format codes: \'$#,##0.00\' (currency), \'#,##0\' (integer), \'0.00%\' (percent), \'0.00\' (decimal), \'MM/DD/YYYY\' (date), \'DD-MMM-YYYY\' (date), \'YYYY-MM-DD\' (ISO date).',
|
|
50
|
+
items: {
|
|
51
|
+
type: "array",
|
|
52
|
+
description: "Row of cells. Each cell is an object with value and format.",
|
|
53
|
+
items: {
|
|
54
|
+
type: "object",
|
|
55
|
+
description: "Cell object with value and optional format. If value is a string starting with '=', it's treated as a formula.",
|
|
56
|
+
properties: {
|
|
57
|
+
v: {
|
|
58
|
+
oneOf: [{ type: "string" }, { type: "number" }],
|
|
59
|
+
description:
|
|
60
|
+
"Cell value. Can be text, number, date, or formula (string starting with '='). Examples: 'Revenue', 1500000, '01/15/2025', '=SUM(A1:A10)', '=B2-TODAY()'. Date strings like '01/15/2025' are automatically parsed to date serial numbers.",
|
|
61
|
+
},
|
|
62
|
+
f: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description:
|
|
65
|
+
"Optional format code for displaying the value. Common formats: '$#,##0.00' (currency), '#,##0' (integer), '0.00%' (percent), '0.00' (decimal), 'MM/DD/YYYY' (date), 'DD-MMM-YYYY' (date), 'YYYY-MM-DD' (ISO date)",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
required: ["v"],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: ["name", "data"],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ["title", "sheets"],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default toolDefinition;
|
|
82
|
+
|
|
83
|
+
export const executeSpreadsheet = async (
|
|
84
|
+
args: SpreadsheetArgs,
|
|
85
|
+
): Promise<{
|
|
86
|
+
message: string;
|
|
87
|
+
title: string;
|
|
88
|
+
data: SpreadsheetToolData;
|
|
89
|
+
instructions: string;
|
|
90
|
+
}> => {
|
|
91
|
+
const { title } = args;
|
|
92
|
+
let { sheets } = args;
|
|
93
|
+
|
|
94
|
+
// Handle case where LLM accidentally stringifies the sheets array
|
|
95
|
+
if (typeof sheets === "string") {
|
|
96
|
+
try {
|
|
97
|
+
sheets = JSON.parse(sheets);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw new Error(`Invalid sheets format: sheets must be an array, not a string. Parse error: ${error instanceof Error ? error.message : String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Validate that sheets are provided
|
|
104
|
+
if (!Array.isArray(sheets) || sheets.length === 0) {
|
|
105
|
+
throw new Error("At least one sheet is required. Sheets must be an array of sheet objects.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Validate each sheet has data
|
|
109
|
+
for (const sheet of sheets) {
|
|
110
|
+
if (!sheet.name || !sheet.data || sheet.data.length === 0) {
|
|
111
|
+
throw new Error(`Invalid sheet: ${sheet.name || "unnamed"}. Each sheet must have a name and data array.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
message: `Created spreadsheet: ${title}`,
|
|
117
|
+
title,
|
|
118
|
+
data: { sheets },
|
|
119
|
+
instructions: "Acknowledge that the spreadsheet has been created and is displayed to the user.",
|
|
120
|
+
};
|
|
121
|
+
};
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spreadsheet Calculator
|
|
3
|
+
*
|
|
4
|
+
* Core calculation engine with circular reference detection and cross-sheet support
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { formatNumber } from "./formatter";
|
|
8
|
+
import { columnToIndex } from "./parser";
|
|
9
|
+
import { evaluateFormula as evaluateFormulaFn } from "./evaluator";
|
|
10
|
+
import { parseDate, getDefaultDateFormat } from "./date-parser";
|
|
11
|
+
import type { SheetData, CellValue, CalculatedSheet, CalculationError, FormulaInfo, SpreadsheetCell } from "./types";
|
|
12
|
+
import { isObj } from "../../../utils/types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Normalize malformed data structures
|
|
16
|
+
* Some models generate flat arrays instead of 2D arrays - fix them
|
|
17
|
+
*
|
|
18
|
+
* @param data - Potentially malformed sheet data
|
|
19
|
+
* @returns Normalized 2D array
|
|
20
|
+
*/
|
|
21
|
+
function normalizeData(data: any): SpreadsheetCell[][] {
|
|
22
|
+
// Handle null/undefined
|
|
23
|
+
if (!data) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// If not an array, wrap in array
|
|
28
|
+
if (!Array.isArray(data)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Empty array
|
|
33
|
+
if (data.length === 0) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// If data is already a 2D array, return as-is
|
|
38
|
+
if (Array.isArray(data[0])) {
|
|
39
|
+
return data as SpreadsheetCell[][];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If data is a flat array of cell objects, convert to 2D by pairing cells
|
|
43
|
+
// Pattern: [cell1, cell2, cell3, cell4] -> [[cell1, cell2], [cell3, cell4]]
|
|
44
|
+
// This handles the case where models output flat arrays instead of rows
|
|
45
|
+
if (isObj(data[0])) {
|
|
46
|
+
const rows: SpreadsheetCell[][] = [];
|
|
47
|
+
for (let i = 0; i < data.length; i += 2) {
|
|
48
|
+
const row = [data[i]];
|
|
49
|
+
if (i + 1 < data.length) {
|
|
50
|
+
row.push(data[i + 1]);
|
|
51
|
+
}
|
|
52
|
+
rows.push(row);
|
|
53
|
+
}
|
|
54
|
+
return rows;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Unknown structure - return empty
|
|
58
|
+
console.warn("Unknown data structure in spreadsheet, returning empty:", data);
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pre-process sheet data to parse date strings into serial numbers
|
|
64
|
+
*
|
|
65
|
+
* @param data - Raw sheet data
|
|
66
|
+
* @returns Processed data with dates converted to serial numbers
|
|
67
|
+
*/
|
|
68
|
+
function preprocessDates(data: SpreadsheetCell[][]): SpreadsheetCell[][] {
|
|
69
|
+
return data.map((row) =>
|
|
70
|
+
row.map((cell) => {
|
|
71
|
+
// Skip if not a cell object or if it has a formula
|
|
72
|
+
if (!isObj(cell) || !("v" in cell)) {
|
|
73
|
+
return cell;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const value = cell.v;
|
|
77
|
+
|
|
78
|
+
// Only parse strings that aren't formulas
|
|
79
|
+
if (typeof value === "string" && !value.startsWith("=")) {
|
|
80
|
+
const dateSerial = parseDate(value);
|
|
81
|
+
|
|
82
|
+
if (dateSerial !== null) {
|
|
83
|
+
// It's a date! Convert to serial number
|
|
84
|
+
return {
|
|
85
|
+
v: dateSerial,
|
|
86
|
+
f: cell.f || getDefaultDateFormat(value), // Use existing format or detect from input
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Not a date, return as-is
|
|
92
|
+
return cell;
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Calculate formulas in a single sheet
|
|
99
|
+
*
|
|
100
|
+
* @param sheet - Sheet data to calculate
|
|
101
|
+
* @param allSheets - All sheets for cross-sheet references
|
|
102
|
+
* @returns Calculated sheet with formulas evaluated
|
|
103
|
+
*/
|
|
104
|
+
export function calculateSheet(sheet: SheetData, allSheets?: SheetData[]): CalculatedSheet {
|
|
105
|
+
// Normalize malformed data structures first
|
|
106
|
+
const normalizedData = normalizeData(sheet.data);
|
|
107
|
+
|
|
108
|
+
// Pre-process dates before calculation
|
|
109
|
+
const processedData = preprocessDates(normalizedData);
|
|
110
|
+
|
|
111
|
+
// Also preprocess all sheets if provided
|
|
112
|
+
const processedAllSheets = allSheets?.map((s) => ({
|
|
113
|
+
...s,
|
|
114
|
+
data: preprocessDates(normalizeData(s.data)),
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
const data = processedData;
|
|
118
|
+
const sheetName = sheet.name;
|
|
119
|
+
// Cache stores either SpreadsheetCell[][] (before calculation) or CellValue[][] (after)
|
|
120
|
+
const sheetsCache = new Map<string, (SpreadsheetCell | CellValue)[][]>();
|
|
121
|
+
const errors: CalculationError[] = [];
|
|
122
|
+
const formulas: FormulaInfo[] = [];
|
|
123
|
+
|
|
124
|
+
// Create a copy of the data with calculated values
|
|
125
|
+
const calculated: any[][] = data.map((row) => [...row]);
|
|
126
|
+
|
|
127
|
+
// Add current sheet to cache to prevent infinite loops
|
|
128
|
+
sheetsCache.set(sheetName, calculated);
|
|
129
|
+
|
|
130
|
+
// Track cells being calculated to detect circular references
|
|
131
|
+
const calculating = new Set<string>();
|
|
132
|
+
|
|
133
|
+
// Helper to extract raw value from cell with recursive formula evaluation
|
|
134
|
+
const getRawValue = (cell: any, row?: number, col?: number): CellValue => {
|
|
135
|
+
// Handle null/undefined cells - treat as 0
|
|
136
|
+
if (cell === null || cell === undefined) return 0;
|
|
137
|
+
|
|
138
|
+
if (typeof cell === "number") return cell;
|
|
139
|
+
|
|
140
|
+
// Handle string values (for legacy or calculated cells)
|
|
141
|
+
if (typeof cell === "string") {
|
|
142
|
+
// Handle empty strings as 0
|
|
143
|
+
if (cell.trim() === "") return 0;
|
|
144
|
+
|
|
145
|
+
// Handle percentage strings like "5%" or "0.4167%"
|
|
146
|
+
if (cell.includes("%")) {
|
|
147
|
+
const numericPart = cell.replace("%", "").trim();
|
|
148
|
+
const value = parseFloat(numericPart);
|
|
149
|
+
return isNaN(value) ? 0 : value / 100;
|
|
150
|
+
}
|
|
151
|
+
// Handle currency strings like "$1,000" or "$1,000.00"
|
|
152
|
+
if (cell.includes("$")) {
|
|
153
|
+
const numericPart = cell.replace(/[$,]/g, "").trim();
|
|
154
|
+
const value = parseFloat(numericPart);
|
|
155
|
+
return isNaN(value) ? 0 : value;
|
|
156
|
+
}
|
|
157
|
+
// Handle comma-separated numbers like "1,000"
|
|
158
|
+
if (cell.includes(",")) {
|
|
159
|
+
const numericPart = cell.replace(/,/g, "").trim();
|
|
160
|
+
const value = parseFloat(numericPart);
|
|
161
|
+
return isNaN(value) ? 0 : value;
|
|
162
|
+
}
|
|
163
|
+
// Handle regular numeric strings, but preserve non-numeric strings
|
|
164
|
+
const num = parseFloat(cell);
|
|
165
|
+
return isNaN(num) ? cell : num;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Handle new cell format {v, f}
|
|
169
|
+
if (isObj(cell) && "v" in cell) {
|
|
170
|
+
const value = cell.v;
|
|
171
|
+
// If value is a string starting with "=", it's a formula
|
|
172
|
+
if (typeof value === "string" && value.startsWith("=")) {
|
|
173
|
+
// Check if we have row/col info to evaluate recursively
|
|
174
|
+
if (row !== undefined && col !== undefined) {
|
|
175
|
+
const cellKey = `${row},${col}`;
|
|
176
|
+
|
|
177
|
+
// Check for circular reference
|
|
178
|
+
if (calculating.has(cellKey)) {
|
|
179
|
+
console.warn(`Circular reference detected at row ${row}, col ${col}`);
|
|
180
|
+
errors.push({
|
|
181
|
+
cell: { row, col },
|
|
182
|
+
formula: value,
|
|
183
|
+
error: "Circular reference detected",
|
|
184
|
+
type: "circular",
|
|
185
|
+
});
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check if already calculated (result is cached as a number)
|
|
190
|
+
const calculatedCell = calculated[row][col];
|
|
191
|
+
if (typeof calculatedCell === "number") {
|
|
192
|
+
return calculatedCell;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Recursively evaluate the formula
|
|
196
|
+
calculating.add(cellKey);
|
|
197
|
+
try {
|
|
198
|
+
const formula = value.substring(1); // Remove "=" prefix
|
|
199
|
+
const result = evaluateFormula(formula);
|
|
200
|
+
calculating.delete(cellKey);
|
|
201
|
+
|
|
202
|
+
// Cache the calculated result (preserve strings and numbers)
|
|
203
|
+
calculated[row][col] = result;
|
|
204
|
+
|
|
205
|
+
return result;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
calculating.delete(cellKey);
|
|
208
|
+
console.error(`Error evaluating formula at row ${row}, col ${col}:`, error);
|
|
209
|
+
errors.push({
|
|
210
|
+
cell: { row, col },
|
|
211
|
+
formula: value,
|
|
212
|
+
error: error instanceof Error ? error.message : String(error),
|
|
213
|
+
type: "unknown",
|
|
214
|
+
});
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return 0; // No position info, can't evaluate
|
|
219
|
+
}
|
|
220
|
+
// Try to parse as number, but preserve original type on failure
|
|
221
|
+
if (typeof value === "number") return value;
|
|
222
|
+
if (typeof value === "boolean") return value;
|
|
223
|
+
if (typeof value === "string") {
|
|
224
|
+
const num = parseFloat(value);
|
|
225
|
+
return isNaN(num) ? value : num;
|
|
226
|
+
}
|
|
227
|
+
return String(value);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Try to parse cell as number, but preserve strings
|
|
231
|
+
const num = parseFloat(cell);
|
|
232
|
+
return isNaN(num) ? cell : num;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// Helper to get cell value by reference (e.g., "B2", "$B$2", or "'Sheet1'!B2")
|
|
236
|
+
const getCellValue = (ref: string): CellValue => {
|
|
237
|
+
let sheetData: any[][] = calculated;
|
|
238
|
+
let cellRef = ref;
|
|
239
|
+
let isCurrentSheet = true;
|
|
240
|
+
|
|
241
|
+
// Check for cross-sheet reference (e.g., 'Sheet Name'!B2 or Sheet1!B2)
|
|
242
|
+
const sheetMatch = ref.match(/^(?:'([^']+)'|([^!]+))!(.+)$/);
|
|
243
|
+
if (sheetMatch) {
|
|
244
|
+
const targetSheetName = sheetMatch[1] || sheetMatch[2]; // Quoted or unquoted sheet name
|
|
245
|
+
cellRef = sheetMatch[3]; // Cell reference part
|
|
246
|
+
isCurrentSheet = false;
|
|
247
|
+
|
|
248
|
+
// Check cache first to prevent infinite loops
|
|
249
|
+
if (sheetsCache.has(targetSheetName)) {
|
|
250
|
+
sheetData = sheetsCache.get(targetSheetName)!;
|
|
251
|
+
} else {
|
|
252
|
+
// Find the sheet in all sheets
|
|
253
|
+
const targetSheet = processedAllSheets?.find((s) => s.name === targetSheetName);
|
|
254
|
+
if (targetSheet && targetSheet.data) {
|
|
255
|
+
// Calculate formulas for the target sheet with cache
|
|
256
|
+
const targetCalculated = targetSheet.data.map((row) => [...row]);
|
|
257
|
+
sheetsCache.set(targetSheetName, targetCalculated);
|
|
258
|
+
|
|
259
|
+
// Recursively calculate the target sheet
|
|
260
|
+
const targetResult = calculateSheet(targetSheet, processedAllSheets);
|
|
261
|
+
sheetsCache.set(targetSheetName, targetResult.data);
|
|
262
|
+
sheetData = targetResult.data as any[][];
|
|
263
|
+
} else {
|
|
264
|
+
return 0; // Sheet not found
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Remove $ symbols for absolute references
|
|
270
|
+
const cleanRef = cellRef.replace(/\$/g, "");
|
|
271
|
+
const match = cleanRef.match(/^([A-Z]+)(\d+)$/);
|
|
272
|
+
if (!match) return 0;
|
|
273
|
+
|
|
274
|
+
const col = columnToIndex(match[1]); // A=0, B=1, ..., Z=25, AA=26, etc.
|
|
275
|
+
const row = parseInt(match[2]) - 1; // 1-indexed to 0-indexed
|
|
276
|
+
|
|
277
|
+
if (row < 0 || row >= sheetData.length || col < 0 || col >= sheetData[row].length) {
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const cell = sheetData[row][col];
|
|
282
|
+
// Pass row/col only if this is the current sheet (for recursive evaluation)
|
|
283
|
+
return getRawValue(cell, isCurrentSheet ? row : undefined, isCurrentSheet ? col : undefined);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const collectRangeValues = (range: string, options: { numericOnly: boolean }): CellValue[] => {
|
|
287
|
+
let sheetData: any[][] = calculated;
|
|
288
|
+
let rangeRef = range;
|
|
289
|
+
let isCurrentSheet = true;
|
|
290
|
+
|
|
291
|
+
// Check for cross-sheet reference
|
|
292
|
+
const sheetMatch = range.match(/^(?:'([^']+)'|([^!]+))!(.+)$/);
|
|
293
|
+
if (sheetMatch) {
|
|
294
|
+
const targetSheetName = sheetMatch[1] || sheetMatch[2];
|
|
295
|
+
rangeRef = sheetMatch[3];
|
|
296
|
+
isCurrentSheet = false;
|
|
297
|
+
|
|
298
|
+
// Check cache first
|
|
299
|
+
if (sheetsCache.has(targetSheetName)) {
|
|
300
|
+
sheetData = sheetsCache.get(targetSheetName)!;
|
|
301
|
+
} else {
|
|
302
|
+
// Find and calculate the target sheet
|
|
303
|
+
const targetSheet = processedAllSheets?.find((s) => s.name === targetSheetName);
|
|
304
|
+
if (targetSheet && targetSheet.data) {
|
|
305
|
+
const targetCalculated = targetSheet.data.map((row) => [...row]);
|
|
306
|
+
sheetsCache.set(targetSheetName, targetCalculated);
|
|
307
|
+
|
|
308
|
+
// Recursively calculate the target sheet
|
|
309
|
+
const targetResult = calculateSheet(targetSheet, processedAllSheets);
|
|
310
|
+
sheetsCache.set(targetSheetName, targetResult.data);
|
|
311
|
+
sheetData = targetResult.data as any[][];
|
|
312
|
+
} else {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const match = rangeRef.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
|
|
319
|
+
if (!match) return [];
|
|
320
|
+
|
|
321
|
+
const startCol = columnToIndex(match[1]);
|
|
322
|
+
const startRow = parseInt(match[2]) - 1;
|
|
323
|
+
const endCol = columnToIndex(match[3]);
|
|
324
|
+
const endRow = parseInt(match[4]) - 1;
|
|
325
|
+
|
|
326
|
+
const values: CellValue[] = [];
|
|
327
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
328
|
+
for (let col = startCol; col <= endCol; col++) {
|
|
329
|
+
if (row >= 0 && row < sheetData.length && col >= 0 && col < sheetData[row].length) {
|
|
330
|
+
const cell = sheetData[row][col];
|
|
331
|
+
// Pass row/col only if current sheet (for recursive evaluation)
|
|
332
|
+
const rawValue = getRawValue(cell, isCurrentSheet ? row : undefined, isCurrentSheet ? col : undefined);
|
|
333
|
+
|
|
334
|
+
if (options.numericOnly) {
|
|
335
|
+
if (!isNaN(rawValue as number)) {
|
|
336
|
+
values.push(rawValue);
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
values.push(rawValue);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return values;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Helper to get numeric-only range values (legacy behavior)
|
|
348
|
+
const getRangeValues = (range: string): CellValue[] => collectRangeValues(range, { numericOnly: true });
|
|
349
|
+
|
|
350
|
+
// Helper to get raw range values including text
|
|
351
|
+
const getRangeValuesRaw = (range: string): CellValue[] => collectRangeValues(range, { numericOnly: false });
|
|
352
|
+
|
|
353
|
+
// Evaluate a formula with context
|
|
354
|
+
const evaluateFormula = (formula: string): CellValue => {
|
|
355
|
+
return evaluateFormulaFn(formula, {
|
|
356
|
+
getCellValue,
|
|
357
|
+
getRangeValues,
|
|
358
|
+
getRangeValuesRaw,
|
|
359
|
+
evaluateFormula,
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// Process all cells and calculate formulas
|
|
364
|
+
for (let rowIdx = 0; rowIdx < data.length; rowIdx++) {
|
|
365
|
+
for (let colIdx = 0; colIdx < data[rowIdx].length; colIdx++) {
|
|
366
|
+
const originalCell = data[rowIdx][colIdx];
|
|
367
|
+
const calculatedCell = calculated[rowIdx][colIdx];
|
|
368
|
+
|
|
369
|
+
// Skip if cell was already calculated recursively
|
|
370
|
+
if (typeof calculatedCell === "number" && isObj(originalCell) && "f" in originalCell) {
|
|
371
|
+
// Cell was already evaluated - keep it as number for now
|
|
372
|
+
// Formatting will be applied at the end
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Handle cell format {v, f}
|
|
377
|
+
if (isObj(originalCell) && "v" in originalCell) {
|
|
378
|
+
const value = originalCell.v;
|
|
379
|
+
|
|
380
|
+
// Check if value is a formula (string starting with "=")
|
|
381
|
+
if (typeof value === "string" && value.startsWith("=")) {
|
|
382
|
+
// Remove the "=" prefix and evaluate the formula
|
|
383
|
+
const formula = value.substring(1);
|
|
384
|
+
|
|
385
|
+
// Track formula info
|
|
386
|
+
formulas.push({
|
|
387
|
+
cell: { row: rowIdx, col: colIdx },
|
|
388
|
+
formula: value,
|
|
389
|
+
dependencies: [], // TODO: Extract dependencies from formula
|
|
390
|
+
result: 0, // Will be updated below
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const result = evaluateFormula(formula);
|
|
394
|
+
|
|
395
|
+
// Update formula result
|
|
396
|
+
formulas[formulas.length - 1].result = result;
|
|
397
|
+
|
|
398
|
+
// Store result as-is (formatting will be applied at the end)
|
|
399
|
+
calculated[rowIdx][colIdx] = result;
|
|
400
|
+
} else {
|
|
401
|
+
// Regular value cell (not a formula)
|
|
402
|
+
// Convert to plain value (important for range evaluation)
|
|
403
|
+
calculated[rowIdx][colIdx] = value;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// If cell is not in {v, f} format, leave it as-is (already a plain value)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Final formatting pass: apply formatting to all cells with format codes
|
|
411
|
+
for (let rowIdx = 0; rowIdx < data.length; rowIdx++) {
|
|
412
|
+
for (let colIdx = 0; colIdx < data[rowIdx].length; colIdx++) {
|
|
413
|
+
const originalCell = data[rowIdx][colIdx];
|
|
414
|
+
const calculatedValue = calculated[rowIdx][colIdx];
|
|
415
|
+
|
|
416
|
+
if (isObj(originalCell) && "v" in originalCell) {
|
|
417
|
+
const isFormula = typeof originalCell.v === "string" && originalCell.v.startsWith("=");
|
|
418
|
+
|
|
419
|
+
// Apply formatting if cell has a format code and calculated value is a number
|
|
420
|
+
if ("f" in originalCell && originalCell.f && typeof calculatedValue === "number") {
|
|
421
|
+
calculated[rowIdx][colIdx] = formatNumber(calculatedValue, originalCell.f);
|
|
422
|
+
}
|
|
423
|
+
// Auto-format date serial numbers from formulas without explicit format
|
|
424
|
+
else if (
|
|
425
|
+
isFormula &&
|
|
426
|
+
typeof calculatedValue === "number" &&
|
|
427
|
+
calculatedValue >= 36000 &&
|
|
428
|
+
calculatedValue <= 63499 &&
|
|
429
|
+
Number.isInteger(calculatedValue) &&
|
|
430
|
+
(!("f" in originalCell) || !originalCell.f)
|
|
431
|
+
) {
|
|
432
|
+
// Check if this looks like a date serial number
|
|
433
|
+
// 36000 = Jul 1998, 63499 = Dec 2073
|
|
434
|
+
// Must be integer (dates without time component)
|
|
435
|
+
// Avoids formatting calculated averages/sums as dates
|
|
436
|
+
// Apply default date format
|
|
437
|
+
calculated[rowIdx][colIdx] = formatNumber(calculatedValue, "MM/DD/YYYY");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
name: sheetName,
|
|
445
|
+
data: calculated,
|
|
446
|
+
formulas,
|
|
447
|
+
errors,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Calculate all sheets in a workbook
|
|
453
|
+
*
|
|
454
|
+
* @param sheets - Array of sheets to calculate
|
|
455
|
+
* @returns Array of calculated sheets
|
|
456
|
+
*/
|
|
457
|
+
export function calculateWorkbook(sheets: SheetData[]): CalculatedSheet[] {
|
|
458
|
+
return sheets.map((sheet) => calculateSheet(sheet, sheets));
|
|
459
|
+
}
|