@gokulvenkatareddy/cortex 0.1.7

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 (299) hide show
  1. package/README.md +1295 -0
  2. package/apps/octogent/.github/workflows/ci.yml +40 -0
  3. package/apps/octogent/.shims/claude +4 -0
  4. package/apps/octogent/AGENTS.md +71 -0
  5. package/apps/octogent/CONTRIBUTING.md +72 -0
  6. package/apps/octogent/LICENSE +21 -0
  7. package/apps/octogent/README.md +184 -0
  8. package/apps/octogent/apps/api/AGENTS.md +32 -0
  9. package/apps/octogent/apps/api/package.json +19 -0
  10. package/apps/octogent/apps/api/src/agentStateDetection.ts +181 -0
  11. package/apps/octogent/apps/api/src/claudeSessionScanner.ts +235 -0
  12. package/apps/octogent/apps/api/src/claudeSkills.ts +182 -0
  13. package/apps/octogent/apps/api/src/claudeUsage.ts +922 -0
  14. package/apps/octogent/apps/api/src/cli.ts +595 -0
  15. package/apps/octogent/apps/api/src/codeIntelStore.ts +46 -0
  16. package/apps/octogent/apps/api/src/codexUsage.ts +278 -0
  17. package/apps/octogent/apps/api/src/createApiServer/codeIntelRoutes.ts +60 -0
  18. package/apps/octogent/apps/api/src/createApiServer/conversationRoutes.ts +128 -0
  19. package/apps/octogent/apps/api/src/createApiServer/deckRoutes.ts +873 -0
  20. package/apps/octogent/apps/api/src/createApiServer/gitParsers.ts +140 -0
  21. package/apps/octogent/apps/api/src/createApiServer/gitRoutes.ts +214 -0
  22. package/apps/octogent/apps/api/src/createApiServer/miscRoutes.ts +316 -0
  23. package/apps/octogent/apps/api/src/createApiServer/monitorParsers.ts +137 -0
  24. package/apps/octogent/apps/api/src/createApiServer/monitorRoutes.ts +95 -0
  25. package/apps/octogent/apps/api/src/createApiServer/requestHandler.ts +311 -0
  26. package/apps/octogent/apps/api/src/createApiServer/requestParsers.ts +25 -0
  27. package/apps/octogent/apps/api/src/createApiServer/routeHelpers.ts +97 -0
  28. package/apps/octogent/apps/api/src/createApiServer/security.ts +70 -0
  29. package/apps/octogent/apps/api/src/createApiServer/terminalParsers.ts +167 -0
  30. package/apps/octogent/apps/api/src/createApiServer/terminalRoutes.ts +315 -0
  31. package/apps/octogent/apps/api/src/createApiServer/types.ts +24 -0
  32. package/apps/octogent/apps/api/src/createApiServer/uiStateParsers.ts +255 -0
  33. package/apps/octogent/apps/api/src/createApiServer/upgradeHandler.ts +38 -0
  34. package/apps/octogent/apps/api/src/createApiServer/usageRoutes.ts +84 -0
  35. package/apps/octogent/apps/api/src/createApiServer.ts +176 -0
  36. package/apps/octogent/apps/api/src/deck/readDeckTentacles.ts +595 -0
  37. package/apps/octogent/apps/api/src/githubRepoSummary.ts +397 -0
  38. package/apps/octogent/apps/api/src/logging.ts +9 -0
  39. package/apps/octogent/apps/api/src/monitor/defaults.ts +3 -0
  40. package/apps/octogent/apps/api/src/monitor/index.ts +8 -0
  41. package/apps/octogent/apps/api/src/monitor/repository.ts +303 -0
  42. package/apps/octogent/apps/api/src/monitor/service.ts +349 -0
  43. package/apps/octogent/apps/api/src/monitor/types.ts +120 -0
  44. package/apps/octogent/apps/api/src/monitor/xProvider.ts +587 -0
  45. package/apps/octogent/apps/api/src/projectPersistence.ts +377 -0
  46. package/apps/octogent/apps/api/src/prompts/index.ts +10 -0
  47. package/apps/octogent/apps/api/src/prompts/promptResolver.ts +145 -0
  48. package/apps/octogent/apps/api/src/runtimeMetadata.ts +69 -0
  49. package/apps/octogent/apps/api/src/server.ts +80 -0
  50. package/apps/octogent/apps/api/src/setupState.ts +80 -0
  51. package/apps/octogent/apps/api/src/setupStatus.ts +174 -0
  52. package/apps/octogent/apps/api/src/startupPrerequisites.ts +146 -0
  53. package/apps/octogent/apps/api/src/terminalRuntime/channelMessaging.ts +87 -0
  54. package/apps/octogent/apps/api/src/terminalRuntime/claudeTranscript.ts +279 -0
  55. package/apps/octogent/apps/api/src/terminalRuntime/constants.ts +15 -0
  56. package/apps/octogent/apps/api/src/terminalRuntime/conversations.ts +492 -0
  57. package/apps/octogent/apps/api/src/terminalRuntime/gitOperations.ts +341 -0
  58. package/apps/octogent/apps/api/src/terminalRuntime/hookProcessor.ts +405 -0
  59. package/apps/octogent/apps/api/src/terminalRuntime/protocol.ts +46 -0
  60. package/apps/octogent/apps/api/src/terminalRuntime/ptyEnvironment.ts +50 -0
  61. package/apps/octogent/apps/api/src/terminalRuntime/registry.ts +423 -0
  62. package/apps/octogent/apps/api/src/terminalRuntime/sessionRuntime.ts +671 -0
  63. package/apps/octogent/apps/api/src/terminalRuntime/systemClients.ts +432 -0
  64. package/apps/octogent/apps/api/src/terminalRuntime/types.ts +157 -0
  65. package/apps/octogent/apps/api/src/terminalRuntime/worktreeManager.ts +135 -0
  66. package/apps/octogent/apps/api/src/terminalRuntime.ts +567 -0
  67. package/apps/octogent/apps/api/src/usageUtils.ts +16 -0
  68. package/apps/octogent/apps/api/src/ws-shim.d.ts +28 -0
  69. package/apps/octogent/apps/api/tests/agentStateDetection.test.ts +67 -0
  70. package/apps/octogent/apps/api/tests/claudeUsage.test.ts +583 -0
  71. package/apps/octogent/apps/api/tests/codexUsage.test.ts +107 -0
  72. package/apps/octogent/apps/api/tests/createApiServer.test.ts +3207 -0
  73. package/apps/octogent/apps/api/tests/githubRepoSummary.test.ts +100 -0
  74. package/apps/octogent/apps/api/tests/logging.test.ts +33 -0
  75. package/apps/octogent/apps/api/tests/monitorApi.test.ts +467 -0
  76. package/apps/octogent/apps/api/tests/monitorCore.test.ts +104 -0
  77. package/apps/octogent/apps/api/tests/promptResolver.test.ts +109 -0
  78. package/apps/octogent/apps/api/tests/protocol.test.ts +14 -0
  79. package/apps/octogent/apps/api/tests/sessionRuntime.test.ts +608 -0
  80. package/apps/octogent/apps/api/tests/startupPrerequisites.test.ts +70 -0
  81. package/apps/octogent/apps/api/tests/upgradeHandler.test.ts +40 -0
  82. package/apps/octogent/apps/api/tests/xMonitorProvider.test.ts +109 -0
  83. package/apps/octogent/apps/api/tsconfig.json +7 -0
  84. package/apps/octogent/apps/api/vitest.config.ts +7 -0
  85. package/apps/octogent/apps/web/AGENTS.md +38 -0
  86. package/apps/octogent/apps/web/index.html +13 -0
  87. package/apps/octogent/apps/web/package.json +32 -0
  88. package/apps/octogent/apps/web/public/octopus-favicon.svg +26 -0
  89. package/apps/octogent/apps/web/src/App.tsx +646 -0
  90. package/apps/octogent/apps/web/src/app/canvas/types.ts +34 -0
  91. package/apps/octogent/apps/web/src/app/codeIntelAggregation.ts +278 -0
  92. package/apps/octogent/apps/web/src/app/constants.ts +28 -0
  93. package/apps/octogent/apps/web/src/app/conversationNormalizers.ts +135 -0
  94. package/apps/octogent/apps/web/src/app/formatTimestamp.ts +18 -0
  95. package/apps/octogent/apps/web/src/app/githubMetrics.ts +76 -0
  96. package/apps/octogent/apps/web/src/app/githubNormalizers.ts +91 -0
  97. package/apps/octogent/apps/web/src/app/hooks/useAgentRuntimeStates.ts +18 -0
  98. package/apps/octogent/apps/web/src/app/hooks/useBackendLivenessPolling.ts +53 -0
  99. package/apps/octogent/apps/web/src/app/hooks/useCanvasGraphData.ts +449 -0
  100. package/apps/octogent/apps/web/src/app/hooks/useCanvasTransform.ts +260 -0
  101. package/apps/octogent/apps/web/src/app/hooks/useClaudeUsagePolling.ts +40 -0
  102. package/apps/octogent/apps/web/src/app/hooks/useClickOutside.ts +30 -0
  103. package/apps/octogent/apps/web/src/app/hooks/useCodeIntelRuntime.ts +83 -0
  104. package/apps/octogent/apps/web/src/app/hooks/useCodexUsagePolling.ts +35 -0
  105. package/apps/octogent/apps/web/src/app/hooks/useConsoleKeyboardShortcuts.ts +31 -0
  106. package/apps/octogent/apps/web/src/app/hooks/useConversationsRuntime.ts +377 -0
  107. package/apps/octogent/apps/web/src/app/hooks/useForceSimulation.ts +319 -0
  108. package/apps/octogent/apps/web/src/app/hooks/useGitHubPrimaryViewModel.ts +143 -0
  109. package/apps/octogent/apps/web/src/app/hooks/useGithubSummaryPolling.ts +28 -0
  110. package/apps/octogent/apps/web/src/app/hooks/useInitialColumnsHydration.ts +64 -0
  111. package/apps/octogent/apps/web/src/app/hooks/useMonitorRuntime.ts +220 -0
  112. package/apps/octogent/apps/web/src/app/hooks/usePersistedUiState.ts +536 -0
  113. package/apps/octogent/apps/web/src/app/hooks/usePollingData.ts +79 -0
  114. package/apps/octogent/apps/web/src/app/hooks/usePromptLibrary.ts +185 -0
  115. package/apps/octogent/apps/web/src/app/hooks/useTentacleGitLifecycle.ts +530 -0
  116. package/apps/octogent/apps/web/src/app/hooks/useTerminalCompletionNotification.ts +94 -0
  117. package/apps/octogent/apps/web/src/app/hooks/useTerminalMutations.ts +266 -0
  118. package/apps/octogent/apps/web/src/app/hooks/useTerminalStateReconciliation.ts +23 -0
  119. package/apps/octogent/apps/web/src/app/hooks/useUsageHeatmapPolling.ts +43 -0
  120. package/apps/octogent/apps/web/src/app/hooks/useWorkspaceSetup.ts +80 -0
  121. package/apps/octogent/apps/web/src/app/hotkeys.ts +31 -0
  122. package/apps/octogent/apps/web/src/app/monitorNormalizers.ts +145 -0
  123. package/apps/octogent/apps/web/src/app/notificationSounds.ts +164 -0
  124. package/apps/octogent/apps/web/src/app/terminalRuntimeStateStore.ts +261 -0
  125. package/apps/octogent/apps/web/src/app/terminalState.ts +21 -0
  126. package/apps/octogent/apps/web/src/app/types.ts +42 -0
  127. package/apps/octogent/apps/web/src/app/uiStateNormalizers.ts +113 -0
  128. package/apps/octogent/apps/web/src/app/usageNormalizers.ts +58 -0
  129. package/apps/octogent/apps/web/src/components/ActiveAgentsSidebar.tsx +60 -0
  130. package/apps/octogent/apps/web/src/components/ActivityPrimaryView.tsx +21 -0
  131. package/apps/octogent/apps/web/src/components/AgentStateBadge.tsx +47 -0
  132. package/apps/octogent/apps/web/src/components/CanvasPrimaryView.tsx +1532 -0
  133. package/apps/octogent/apps/web/src/components/ClearAllConversationsDialog.tsx +33 -0
  134. package/apps/octogent/apps/web/src/components/CodeIntelArcDiagram.tsx +245 -0
  135. package/apps/octogent/apps/web/src/components/CodeIntelPrimaryView.tsx +104 -0
  136. package/apps/octogent/apps/web/src/components/CodeIntelTreemap.tsx +138 -0
  137. package/apps/octogent/apps/web/src/components/ConsolePrimaryNav.tsx +31 -0
  138. package/apps/octogent/apps/web/src/components/ConversationsPrimaryView.tsx +243 -0
  139. package/apps/octogent/apps/web/src/components/DeckPrimaryView.tsx +613 -0
  140. package/apps/octogent/apps/web/src/components/DeleteTentacleDialog.tsx +91 -0
  141. package/apps/octogent/apps/web/src/components/EmptyOctopus.tsx +715 -0
  142. package/apps/octogent/apps/web/src/components/GitHubPrimaryView.tsx +494 -0
  143. package/apps/octogent/apps/web/src/components/MonitorPrimaryView.tsx +475 -0
  144. package/apps/octogent/apps/web/src/components/PrimaryViewRouter.tsx +99 -0
  145. package/apps/octogent/apps/web/src/components/PromptsPrimaryView.tsx +243 -0
  146. package/apps/octogent/apps/web/src/components/RuntimeStatusStrip.tsx +273 -0
  147. package/apps/octogent/apps/web/src/components/SettingsPrimaryView.tsx +92 -0
  148. package/apps/octogent/apps/web/src/components/SidebarActionPanel.tsx +124 -0
  149. package/apps/octogent/apps/web/src/components/SidebarConversationsList.tsx +279 -0
  150. package/apps/octogent/apps/web/src/components/SidebarPromptsList.tsx +116 -0
  151. package/apps/octogent/apps/web/src/components/TelemetryTape.tsx +106 -0
  152. package/apps/octogent/apps/web/src/components/TentacleGitActionsDialog.tsx +341 -0
  153. package/apps/octogent/apps/web/src/components/Terminal.tsx +524 -0
  154. package/apps/octogent/apps/web/src/components/TerminalPromptPicker.tsx +140 -0
  155. package/apps/octogent/apps/web/src/components/UsageHeatmap.tsx +702 -0
  156. package/apps/octogent/apps/web/src/components/canvas/CanvasTentaclePanel.tsx +485 -0
  157. package/apps/octogent/apps/web/src/components/canvas/CanvasTerminalColumn.tsx +89 -0
  158. package/apps/octogent/apps/web/src/components/canvas/DeleteAllTerminalsDialog.tsx +221 -0
  159. package/apps/octogent/apps/web/src/components/canvas/OctopusNode.tsx +307 -0
  160. package/apps/octogent/apps/web/src/components/canvas/SessionNode.tsx +185 -0
  161. package/apps/octogent/apps/web/src/components/deck/ActionCards.tsx +118 -0
  162. package/apps/octogent/apps/web/src/components/deck/AddTentacleForm.tsx +269 -0
  163. package/apps/octogent/apps/web/src/components/deck/DeckBottomActions.tsx +56 -0
  164. package/apps/octogent/apps/web/src/components/deck/TentaclePod.tsx +334 -0
  165. package/apps/octogent/apps/web/src/components/deck/WorkspaceSetupCard.tsx +105 -0
  166. package/apps/octogent/apps/web/src/components/deck/octopusVisuals.ts +72 -0
  167. package/apps/octogent/apps/web/src/components/terminalReplay.ts +62 -0
  168. package/apps/octogent/apps/web/src/components/terminalWheel.ts +54 -0
  169. package/apps/octogent/apps/web/src/components/ui/ActionButton.tsx +34 -0
  170. package/apps/octogent/apps/web/src/components/ui/ConfirmationDialog.tsx +86 -0
  171. package/apps/octogent/apps/web/src/components/ui/MarkdownContent.tsx +43 -0
  172. package/apps/octogent/apps/web/src/components/ui/SettingsToggle.tsx +34 -0
  173. package/apps/octogent/apps/web/src/components/ui/StatusBadge.tsx +24 -0
  174. package/apps/octogent/apps/web/src/main.tsx +17 -0
  175. package/apps/octogent/apps/web/src/runtime/HttpTerminalSnapshotReader.ts +87 -0
  176. package/apps/octogent/apps/web/src/runtime/runtimeEndpoints.ts +412 -0
  177. package/apps/octogent/apps/web/src/styles/chrome-and-buttons.css +272 -0
  178. package/apps/octogent/apps/web/src/styles/console-canvas-activity.css +358 -0
  179. package/apps/octogent/apps/web/src/styles/console-canvas-canvas.css +1843 -0
  180. package/apps/octogent/apps/web/src/styles/console-canvas-code-intel.css +227 -0
  181. package/apps/octogent/apps/web/src/styles/console-canvas-conversations.css +705 -0
  182. package/apps/octogent/apps/web/src/styles/console-canvas-deck.css +1524 -0
  183. package/apps/octogent/apps/web/src/styles/console-canvas-github.css +541 -0
  184. package/apps/octogent/apps/web/src/styles/console-canvas-monitor.css +595 -0
  185. package/apps/octogent/apps/web/src/styles/console-canvas-pixpack.css +81 -0
  186. package/apps/octogent/apps/web/src/styles/console-canvas-prompts.css +474 -0
  187. package/apps/octogent/apps/web/src/styles/console-canvas-settings.css +207 -0
  188. package/apps/octogent/apps/web/src/styles/console-chrome-status-nav.css +441 -0
  189. package/apps/octogent/apps/web/src/styles/console-overrides-telemetry.css +320 -0
  190. package/apps/octogent/apps/web/src/styles/console-theme-tokens.css +25 -0
  191. package/apps/octogent/apps/web/src/styles/cortex-theme.css +412 -0
  192. package/apps/octogent/apps/web/src/styles/foundation.css +100 -0
  193. package/apps/octogent/apps/web/src/styles/sidebar-and-scrollbars.css +447 -0
  194. package/apps/octogent/apps/web/src/styles/terminal-and-status.css +356 -0
  195. package/apps/octogent/apps/web/src/styles.css +25 -0
  196. package/apps/octogent/apps/web/src/types/ws.d.ts +23 -0
  197. package/apps/octogent/apps/web/tests/CanvasPrimaryView.test.tsx +347 -0
  198. package/apps/octogent/apps/web/tests/HttpTerminalSnapshotReader.test.tsx +54 -0
  199. package/apps/octogent/apps/web/tests/RuntimeStatusStrip.test.tsx +70 -0
  200. package/apps/octogent/apps/web/tests/Terminal.test.tsx +87 -0
  201. package/apps/octogent/apps/web/tests/add-tentacle-form.test.tsx +48 -0
  202. package/apps/octogent/apps/web/tests/app-github-runtime.test.tsx +162 -0
  203. package/apps/octogent/apps/web/tests/app-monitor-runtime.test.tsx +657 -0
  204. package/apps/octogent/apps/web/tests/app-shell-navigation.test.tsx +109 -0
  205. package/apps/octogent/apps/web/tests/app-swarm-refresh.test.tsx +268 -0
  206. package/apps/octogent/apps/web/tests/app-ui-state-persistence.test.tsx +116 -0
  207. package/apps/octogent/apps/web/tests/app-workspace-setup.test.tsx +217 -0
  208. package/apps/octogent/apps/web/tests/canvas-tentacle-panel.test.tsx +195 -0
  209. package/apps/octogent/apps/web/tests/delete-all-terminals-dialog.test.tsx +76 -0
  210. package/apps/octogent/apps/web/tests/githubMetrics.test.tsx +52 -0
  211. package/apps/octogent/apps/web/tests/hotkeys.test.tsx +44 -0
  212. package/apps/octogent/apps/web/tests/runtimeEndpoints.test.tsx +240 -0
  213. package/apps/octogent/apps/web/tests/setup.ts +39 -0
  214. package/apps/octogent/apps/web/tests/tentacle-pod.test.tsx +62 -0
  215. package/apps/octogent/apps/web/tests/terminalReplay.test.ts +71 -0
  216. package/apps/octogent/apps/web/tests/terminalState.test.tsx +49 -0
  217. package/apps/octogent/apps/web/tests/terminalWheel.test.tsx +51 -0
  218. package/apps/octogent/apps/web/tests/test-utils/appTestHarness.ts +48 -0
  219. package/apps/octogent/apps/web/tests/uiPrimitives.test.tsx +31 -0
  220. package/apps/octogent/apps/web/tests/useAgentRuntimeStates.test.tsx +47 -0
  221. package/apps/octogent/apps/web/tsconfig.json +8 -0
  222. package/apps/octogent/apps/web/vite.api.bundle.config.mts +32 -0
  223. package/apps/octogent/apps/web/vite.config.ts +22 -0
  224. package/apps/octogent/bin/octogent +3 -0
  225. package/apps/octogent/biome.json +21 -0
  226. package/apps/octogent/docs/concepts/mental-model.md +79 -0
  227. package/apps/octogent/docs/concepts/runtime-and-api.md +60 -0
  228. package/apps/octogent/docs/concepts/tentacles.md +85 -0
  229. package/apps/octogent/docs/getting-started/installation.md +54 -0
  230. package/apps/octogent/docs/getting-started/quickstart.md +79 -0
  231. package/apps/octogent/docs/guides/inter-agent-messaging.md +43 -0
  232. package/apps/octogent/docs/guides/orchestrating-child-agents.md +49 -0
  233. package/apps/octogent/docs/guides/working-with-todos.md +56 -0
  234. package/apps/octogent/docs/index.md +40 -0
  235. package/apps/octogent/docs/reference/api.md +103 -0
  236. package/apps/octogent/docs/reference/cli.md +71 -0
  237. package/apps/octogent/docs/reference/experimental-features.md +28 -0
  238. package/apps/octogent/docs/reference/filesystem-layout.md +62 -0
  239. package/apps/octogent/docs/reference/troubleshooting.md +49 -0
  240. package/apps/octogent/package.json +35 -0
  241. package/apps/octogent/packages/core/AGENTS.md +31 -0
  242. package/apps/octogent/packages/core/package.json +12 -0
  243. package/apps/octogent/packages/core/src/adapters/InMemoryTerminalSnapshotReader.ts +10 -0
  244. package/apps/octogent/packages/core/src/application/buildTerminalList.ts +13 -0
  245. package/apps/octogent/packages/core/src/domain/agentRuntime.ts +18 -0
  246. package/apps/octogent/packages/core/src/domain/channel.ts +8 -0
  247. package/apps/octogent/packages/core/src/domain/completionSound.ts +14 -0
  248. package/apps/octogent/packages/core/src/domain/conversation.ts +48 -0
  249. package/apps/octogent/packages/core/src/domain/deck.ts +33 -0
  250. package/apps/octogent/packages/core/src/domain/git.ts +32 -0
  251. package/apps/octogent/packages/core/src/domain/monitor.ts +62 -0
  252. package/apps/octogent/packages/core/src/domain/setup.ts +27 -0
  253. package/apps/octogent/packages/core/src/domain/terminal.ts +17 -0
  254. package/apps/octogent/packages/core/src/domain/uiState.ts +22 -0
  255. package/apps/octogent/packages/core/src/domain/usage.ts +60 -0
  256. package/apps/octogent/packages/core/src/index.ts +15 -0
  257. package/apps/octogent/packages/core/src/ports/TerminalSnapshotReader.ts +5 -0
  258. package/apps/octogent/packages/core/src/util/typeCoercion.ts +20 -0
  259. package/apps/octogent/packages/core/tests/buildTerminalList.test.ts +75 -0
  260. package/apps/octogent/packages/core/tsconfig.json +7 -0
  261. package/apps/octogent/packages/core/tsconfig.tsbuildinfo +1 -0
  262. package/apps/octogent/packages/core/vitest.config.ts +7 -0
  263. package/apps/octogent/pnpm-lock.yaml +3212 -0
  264. package/apps/octogent/pnpm-workspace.yaml +3 -0
  265. package/apps/octogent/prompts/meta-prompt-generator.md +223 -0
  266. package/apps/octogent/prompts/octoboss-clean-contexts.md +30 -0
  267. package/apps/octogent/prompts/octoboss-reorganize-tentacles.md +29 -0
  268. package/apps/octogent/prompts/octoboss-reorganize-todos.md +27 -0
  269. package/apps/octogent/prompts/sandbox-init.md +3 -0
  270. package/apps/octogent/prompts/swarm-parent.md +83 -0
  271. package/apps/octogent/prompts/swarm-worker.md +50 -0
  272. package/apps/octogent/prompts/tentacle-context-init.md +1 -0
  273. package/apps/octogent/prompts/tentacle-planner.md +110 -0
  274. package/apps/octogent/prompts/tentacle-reorganize-todos.md +20 -0
  275. package/apps/octogent/prompts/tentacle-update-tentacle.md +18 -0
  276. package/apps/octogent/scripts/build-package.mjs +23 -0
  277. package/apps/octogent/scripts/dev.mjs +158 -0
  278. package/apps/octogent/scripts/smoke-public-install.mjs +271 -0
  279. package/apps/octogent/static/images/octogent-header.png +0 -0
  280. package/apps/octogent/static/images/preview_1.jpg +0 -0
  281. package/apps/octogent/static/images/preview_2.jpg +0 -0
  282. package/apps/octogent/static/images/preview_3.jpg +0 -0
  283. package/apps/octogent/static/images/preview_4.jpg +0 -0
  284. package/apps/octogent/static/images/preview_5.jpg +0 -0
  285. package/apps/octogent/static/images/preview_6.jpg +0 -0
  286. package/apps/octogent/tsconfig.base.json +16 -0
  287. package/bin/AGI +3 -0
  288. package/bin/AGI-install-app +71 -0
  289. package/bin/AGI-ui +16 -0
  290. package/bin/AGI-voice +15 -0
  291. package/bin/AGI-web +16 -0
  292. package/bin/cortex +109 -0
  293. package/bin/cortex-octogent +99 -0
  294. package/bin/import-specifier.mjs +13 -0
  295. package/bin/import-specifier.test.mjs +13 -0
  296. package/bin/octo +150 -0
  297. package/dist/cli.mjs +555650 -0
  298. package/package.json +157 -0
  299. package/scripts/setup-wizard.ts +390 -0
@@ -0,0 +1,449 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ import type { DeckTentacleSummary } from "@octogent/core";
4
+ import { buildConversationsUrl, buildDeckTentaclesUrl } from "../../runtime/runtimeEndpoints";
5
+ import type { GraphEdge, GraphNode } from "../canvas/types";
6
+ import { normalizeConversationSessionSummary } from "../conversationNormalizers";
7
+ import type { ConversationSessionSummary, TerminalView } from "../types";
8
+ import type { AgentRuntimeStateInfo } from "./useAgentRuntimeStates";
9
+
10
+ const TENTACLE_RADIUS = 40;
11
+ const ACTIVE_SESSION_RADIUS = 12;
12
+ const INACTIVE_SESSION_RADIUS = 10;
13
+
14
+ const OCTOBOSS_RADIUS = 52;
15
+ export const OCTOBOSS_ID = "__octoboss__";
16
+ const OCTOBOSS_NODE_ID = `t:${OCTOBOSS_ID}`;
17
+
18
+ const getAccentPrimary = (): string =>
19
+ (typeof document !== "undefined"
20
+ ? getComputedStyle(document.documentElement).getPropertyValue("--accent-primary").trim()
21
+ : "") || "#d4a017";
22
+
23
+ // Must match the Deck tab's OCTOPUS_COLORS for consistent tentacle colors
24
+ const OCTOPUS_COLORS = [
25
+ "#ff6b2b",
26
+ "#ff2d6b",
27
+ "#00ffaa",
28
+ "#bf5fff",
29
+ "#00c8ff",
30
+ "#ffee00",
31
+ "#39ff14",
32
+ "#ff4df0",
33
+ "#00fff7",
34
+ "#ff9500",
35
+ ];
36
+
37
+ function hashString(str: string): number {
38
+ let h = 0;
39
+ for (let i = 0; i < str.length; i++) {
40
+ h = ((h << 5) - h + str.charCodeAt(i)) | 0;
41
+ }
42
+ return Math.abs(h);
43
+ }
44
+
45
+ const tentacleColor = (tentacleId: string, deckColor: string | null | undefined) =>
46
+ deckColor && deckColor.length > 0
47
+ ? deckColor
48
+ : (OCTOPUS_COLORS[hashString(tentacleId) % OCTOPUS_COLORS.length] as string);
49
+
50
+ type UseCanvasGraphDataOptions = {
51
+ columns: TerminalView;
52
+ enabled: boolean;
53
+ agentRuntimeStates?: Map<string, AgentRuntimeStateInfo>;
54
+ };
55
+
56
+ type UseCanvasGraphDataResult = {
57
+ nodes: GraphNode[];
58
+ edges: GraphEdge[];
59
+ tentacleById: ReadonlyMap<string, DeckTentacleSummary>;
60
+ sessionsByTentacleId: ReadonlyMap<string, ConversationSessionSummary[]>;
61
+ refresh: () => Promise<void>;
62
+ refreshDeckTentacles: () => Promise<void>;
63
+ };
64
+
65
+ const buildTentacleNodeId = (tentacleId: string) => `t:${tentacleId}`;
66
+ const buildActiveSessionNodeId = (agentId: string) => `a:${agentId}`;
67
+ const buildInactiveSessionNodeId = (sessionId: string) => `i:${sessionId}`;
68
+
69
+ const normalizeDeckTentacleSummary = (value: unknown): DeckTentacleSummary | null => {
70
+ if (value === null || typeof value !== "object") {
71
+ return null;
72
+ }
73
+
74
+ const record = value as Record<string, unknown>;
75
+ if (typeof record.tentacleId !== "string") {
76
+ return null;
77
+ }
78
+
79
+ const todoItems = Array.isArray(record.todoItems)
80
+ ? record.todoItems
81
+ .map((item) => {
82
+ if (item === null || typeof item !== "object") {
83
+ return null;
84
+ }
85
+
86
+ const todoRecord = item as Record<string, unknown>;
87
+ if (typeof todoRecord.text !== "string") {
88
+ return null;
89
+ }
90
+
91
+ return {
92
+ text: todoRecord.text,
93
+ done: todoRecord.done === true,
94
+ };
95
+ })
96
+ .filter((item): item is { text: string; done: boolean } => item !== null)
97
+ : [];
98
+
99
+ const scopeRecord =
100
+ record.scope !== null && typeof record.scope === "object"
101
+ ? (record.scope as Record<string, unknown>)
102
+ : null;
103
+ const octopusRecord =
104
+ record.octopus !== null && typeof record.octopus === "object"
105
+ ? (record.octopus as Record<string, unknown>)
106
+ : null;
107
+
108
+ const status =
109
+ record.status === "idle" ||
110
+ record.status === "active" ||
111
+ record.status === "blocked" ||
112
+ record.status === "needs-review"
113
+ ? record.status
114
+ : "idle";
115
+
116
+ return {
117
+ tentacleId: record.tentacleId,
118
+ displayName: typeof record.displayName === "string" ? record.displayName : record.tentacleId,
119
+ description: typeof record.description === "string" ? record.description : "",
120
+ status,
121
+ color: typeof record.color === "string" ? record.color : null,
122
+ octopus: {
123
+ animation: typeof octopusRecord?.animation === "string" ? octopusRecord.animation : null,
124
+ expression: typeof octopusRecord?.expression === "string" ? octopusRecord.expression : null,
125
+ accessory: typeof octopusRecord?.accessory === "string" ? octopusRecord.accessory : null,
126
+ hairColor: typeof octopusRecord?.hairColor === "string" ? octopusRecord.hairColor : null,
127
+ },
128
+ scope: {
129
+ paths: Array.isArray(scopeRecord?.paths)
130
+ ? scopeRecord.paths.filter((path): path is string => typeof path === "string")
131
+ : [],
132
+ tags: Array.isArray(scopeRecord?.tags)
133
+ ? scopeRecord.tags.filter((tag): tag is string => typeof tag === "string")
134
+ : [],
135
+ },
136
+ vaultFiles: Array.isArray(record.vaultFiles)
137
+ ? record.vaultFiles.filter((file): file is string => typeof file === "string")
138
+ : [],
139
+ todoTotal:
140
+ typeof record.todoTotal === "number" && Number.isFinite(record.todoTotal)
141
+ ? record.todoTotal
142
+ : todoItems.length,
143
+ todoDone:
144
+ typeof record.todoDone === "number" && Number.isFinite(record.todoDone)
145
+ ? record.todoDone
146
+ : todoItems.filter((item) => item.done).length,
147
+ todoItems,
148
+ suggestedSkills: Array.isArray(record.suggestedSkills)
149
+ ? record.suggestedSkills.filter((skill): skill is string => typeof skill === "string")
150
+ : [],
151
+ };
152
+ };
153
+
154
+ export const useCanvasGraphData = ({
155
+ columns,
156
+ enabled,
157
+ agentRuntimeStates,
158
+ }: UseCanvasGraphDataOptions): UseCanvasGraphDataResult => {
159
+ const [deckTentacles, setDeckTentacles] = useState<DeckTentacleSummary[]>([]);
160
+ const [inactiveSessions, setInactiveSessions] = useState<ConversationSessionSummary[]>([]);
161
+ const prevNodesRef = useRef<Map<string, GraphNode>>(new Map());
162
+
163
+ const fetchDeckTentacles = useCallback(async () => {
164
+ try {
165
+ const response = await fetch(buildDeckTentaclesUrl(), {
166
+ method: "GET",
167
+ headers: { Accept: "application/json" },
168
+ });
169
+ if (!response.ok) return;
170
+ const payload = (await response.json()) as unknown;
171
+ if (!Array.isArray(payload)) return;
172
+ const items = payload
173
+ .map((entry) => normalizeDeckTentacleSummary(entry))
174
+ .filter((entry): entry is DeckTentacleSummary => entry !== null);
175
+ setDeckTentacles(items);
176
+ } catch {
177
+ // silent
178
+ }
179
+ }, []);
180
+
181
+ const fetchInactiveSessions = useCallback(async () => {
182
+ try {
183
+ const response = await fetch(buildConversationsUrl(), {
184
+ method: "GET",
185
+ headers: { Accept: "application/json" },
186
+ });
187
+ if (!response.ok) return;
188
+ const payload = (await response.json()) as unknown;
189
+ const normalized = Array.isArray(payload)
190
+ ? payload
191
+ .map((entry) => normalizeConversationSessionSummary(entry))
192
+ .filter((entry): entry is ConversationSessionSummary => entry !== null)
193
+ : [];
194
+ setInactiveSessions(normalized);
195
+ } catch {
196
+ // silent
197
+ }
198
+ }, []);
199
+
200
+ useEffect(() => {
201
+ if (!enabled) {
202
+ setDeckTentacles([]);
203
+ setInactiveSessions([]);
204
+ return;
205
+ }
206
+ void fetchDeckTentacles();
207
+ void fetchInactiveSessions();
208
+ }, [enabled, fetchDeckTentacles, fetchInactiveSessions]);
209
+
210
+ const refresh = useCallback(async () => {
211
+ await Promise.all([fetchDeckTentacles(), fetchInactiveSessions()]);
212
+ }, [fetchDeckTentacles, fetchInactiveSessions]);
213
+ const refreshDeckTentacles = useCallback(async () => {
214
+ await fetchDeckTentacles();
215
+ }, [fetchDeckTentacles]);
216
+
217
+ const activeTerminalIds = new Set(columns.map((terminal) => terminal.terminalId));
218
+
219
+ // Build a map of deck tentacles for color/label lookup
220
+ const deckMap = new Map<string, DeckTentacleSummary>();
221
+ for (const dt of deckTentacles) {
222
+ deckMap.set(dt.tentacleId, dt);
223
+ }
224
+
225
+ const sessionsByTentacleId = new Map<string, ConversationSessionSummary[]>();
226
+ for (const session of inactiveSessions) {
227
+ if (!session.tentacleId) {
228
+ continue;
229
+ }
230
+ const tentacleSessions = sessionsByTentacleId.get(session.tentacleId);
231
+ if (tentacleSessions) {
232
+ tentacleSessions.push(session);
233
+ } else {
234
+ sessionsByTentacleId.set(session.tentacleId, [session]);
235
+ }
236
+ }
237
+
238
+ const nodes: GraphNode[] = [];
239
+ const edges: GraphEdge[] = [];
240
+ const prevNodes = prevNodesRef.current;
241
+ const currentNodesById = new Map<string, GraphNode>();
242
+ const seenTentacleIds = new Set<string>();
243
+
244
+ // Build a map of active terminals by tentacleId (multiple terminals can share a tentacle)
245
+ const activeTerminalsByTentacle = new Map<string, TerminalView>();
246
+ for (const terminal of columns) {
247
+ const group = activeTerminalsByTentacle.get(terminal.tentacleId);
248
+ if (group) {
249
+ group.push(terminal);
250
+ } else {
251
+ activeTerminalsByTentacle.set(terminal.tentacleId, [terminal]);
252
+ }
253
+ }
254
+
255
+ // Build tentacle list: only deck tentacles (sandbox and other non-deck
256
+ // terminals are excluded from the graph).
257
+ const allTentacleIds: string[] = [];
258
+ for (const dt of deckTentacles) {
259
+ allTentacleIds.push(dt.tentacleId);
260
+ seenTentacleIds.add(dt.tentacleId);
261
+ }
262
+
263
+ const totalTentacles = allTentacleIds.length;
264
+
265
+ for (let i = 0; i < allTentacleIds.length; i++) {
266
+ const tentacleId = allTentacleIds[i];
267
+ if (!tentacleId) continue;
268
+ const tentacleNodeId = buildTentacleNodeId(tentacleId);
269
+ const prev = prevNodes.get(tentacleNodeId);
270
+ const deck = deckMap.get(tentacleId);
271
+ const activeTerminals = activeTerminalsByTentacle.get(tentacleId);
272
+ const firstActiveTerminal = activeTerminals?.[0];
273
+ const color = tentacleColor(tentacleId, deck?.color);
274
+ const label = deck?.displayName ?? firstActiveTerminal?.tentacleName ?? tentacleId;
275
+
276
+ const angle = (2 * Math.PI * i) / Math.max(totalTentacles, 1);
277
+ const spread = 300;
278
+
279
+ const node: GraphNode = {
280
+ id: tentacleNodeId,
281
+ type: "tentacle",
282
+ x: prev?.x ?? Math.cos(angle) * spread,
283
+ y: prev?.y ?? Math.sin(angle) * spread,
284
+ vx: prev?.vx ?? 0,
285
+ vy: prev?.vy ?? 0,
286
+ pinned: prev?.pinned ?? false,
287
+ radius: TENTACLE_RADIUS,
288
+ tentacleId,
289
+ label,
290
+ color,
291
+ ...(firstActiveTerminal ? { workspaceMode: firstActiveTerminal.workspaceMode } : {}),
292
+ ...(deck?.octopus ? { octopus: deck.octopus } : {}),
293
+ };
294
+ nodes.push(node);
295
+ currentNodesById.set(tentacleNodeId, node);
296
+
297
+ // Active terminal session nodes — one per terminal in this tentacle
298
+ if (activeTerminals) {
299
+ for (const activeTerminal of activeTerminals) {
300
+ const sessionNodeId = buildActiveSessionNodeId(activeTerminal.terminalId);
301
+ const prevSession = prevNodes.get(sessionNodeId);
302
+ const parentNodeId = activeTerminal.parentTerminalId
303
+ ? buildActiveSessionNodeId(activeTerminal.parentTerminalId)
304
+ : tentacleNodeId;
305
+ const parentNode = currentNodesById.get(parentNodeId) ?? node;
306
+ const jitter = () => (Math.random() - 0.5) * 60;
307
+
308
+ const runtimeInfo = agentRuntimeStates?.get(activeTerminal.terminalId);
309
+ const sessionNode: GraphNode = {
310
+ id: sessionNodeId,
311
+ type: "active-session",
312
+ x: prevSession?.x ?? parentNode.x + jitter(),
313
+ y: prevSession?.y ?? parentNode.y + jitter(),
314
+ vx: prevSession?.vx ?? 0,
315
+ vy: prevSession?.vy ?? 0,
316
+ pinned: prevSession?.pinned ?? false,
317
+ radius: ACTIVE_SESSION_RADIUS,
318
+ tentacleId,
319
+ label: activeTerminal.tentacleName || activeTerminal.terminalId,
320
+ color,
321
+ sessionId: activeTerminal.terminalId,
322
+ agentState: activeTerminal.state,
323
+ hasUserPrompt: activeTerminal.hasUserPrompt ?? false,
324
+ ...(activeTerminal.workspaceMode ? { workspaceMode: activeTerminal.workspaceMode } : {}),
325
+ ...(activeTerminal.parentTerminalId
326
+ ? { parentTerminalId: activeTerminal.parentTerminalId }
327
+ : {}),
328
+ ...(runtimeInfo ? { agentRuntimeState: runtimeInfo.state } : {}),
329
+ ...(runtimeInfo?.toolName ? { waitingToolName: runtimeInfo.toolName } : {}),
330
+ };
331
+ nodes.push(sessionNode);
332
+ currentNodesById.set(sessionNodeId, sessionNode);
333
+ edges.push({ source: parentNodeId, target: sessionNodeId });
334
+ }
335
+ }
336
+ }
337
+
338
+ // Octoboss — synthetic always-present node
339
+ const prevBoss = prevNodes.get(OCTOBOSS_NODE_ID);
340
+ const octobossColor = getAccentPrimary();
341
+ const octobossNode: GraphNode = {
342
+ id: OCTOBOSS_NODE_ID,
343
+ type: "octoboss",
344
+ x: prevBoss?.x ?? 0,
345
+ y: prevBoss?.y ?? 0,
346
+ vx: prevBoss?.vx ?? 0,
347
+ vy: prevBoss?.vy ?? 0,
348
+ pinned: prevBoss?.pinned ?? false,
349
+ radius: OCTOBOSS_RADIUS,
350
+ tentacleId: OCTOBOSS_ID,
351
+ label: "Octoboss",
352
+ color: octobossColor,
353
+ };
354
+ nodes.push(octobossNode);
355
+ currentNodesById.set(OCTOBOSS_NODE_ID, octobossNode);
356
+
357
+ // Connect octoboss to every tentacle node
358
+ for (const tentacleId of allTentacleIds) {
359
+ edges.push({ source: OCTOBOSS_NODE_ID, target: buildTentacleNodeId(tentacleId) });
360
+ }
361
+
362
+ // Link active terminals belonging to octoboss
363
+ for (const terminal of columns) {
364
+ if (terminal.tentacleId !== OCTOBOSS_ID) continue;
365
+ const sessionNodeId = buildActiveSessionNodeId(terminal.terminalId);
366
+ const prevSession = prevNodes.get(sessionNodeId);
367
+ const jitter = () => (Math.random() - 0.5) * 60;
368
+
369
+ const bossRuntimeInfo = agentRuntimeStates?.get(terminal.terminalId);
370
+ const sessionNode: GraphNode = {
371
+ id: sessionNodeId,
372
+ type: "active-session",
373
+ x: prevSession?.x ?? octobossNode.x + jitter(),
374
+ y: prevSession?.y ?? octobossNode.y + jitter(),
375
+ vx: prevSession?.vx ?? 0,
376
+ vy: prevSession?.vy ?? 0,
377
+ pinned: prevSession?.pinned ?? false,
378
+ radius: ACTIVE_SESSION_RADIUS,
379
+ tentacleId: OCTOBOSS_ID,
380
+ label: terminal.tentacleName || terminal.terminalId,
381
+ color: octobossColor,
382
+ sessionId: terminal.terminalId,
383
+ agentState: terminal.state,
384
+ hasUserPrompt: terminal.hasUserPrompt ?? false,
385
+ ...(terminal.workspaceMode ? { workspaceMode: terminal.workspaceMode } : {}),
386
+ ...(terminal.parentTerminalId ? { parentTerminalId: terminal.parentTerminalId } : {}),
387
+ ...(bossRuntimeInfo ? { agentRuntimeState: bossRuntimeInfo.state } : {}),
388
+ ...(bossRuntimeInfo?.toolName ? { waitingToolName: bossRuntimeInfo.toolName } : {}),
389
+ };
390
+ nodes.push(sessionNode);
391
+ currentNodesById.set(sessionNodeId, sessionNode);
392
+ edges.push({ source: OCTOBOSS_NODE_ID, target: sessionNodeId });
393
+ }
394
+
395
+ // Inactive sessions from conversations
396
+ for (const session of inactiveSessions) {
397
+ if (!session.tentacleId || !seenTentacleIds.has(session.tentacleId)) continue;
398
+ if (activeTerminalIds.has(session.sessionId)) continue;
399
+
400
+ const tentacleNodeId = buildTentacleNodeId(session.tentacleId);
401
+ const sessionNodeId = buildInactiveSessionNodeId(session.sessionId);
402
+ const prevSession = prevNodes.get(sessionNodeId);
403
+
404
+ const parentNode = nodes.find((n) => n.id === tentacleNodeId);
405
+ const parentX = parentNode?.x ?? 0;
406
+ const parentY = parentNode?.y ?? 0;
407
+ const color = tentacleColor(session.tentacleId, deckMap.get(session.tentacleId)?.color);
408
+ const jitter = () => (Math.random() - 0.5) * 60;
409
+
410
+ const sessionNode: GraphNode = {
411
+ id: sessionNodeId,
412
+ type: "inactive-session",
413
+ x: prevSession?.x ?? parentX + jitter(),
414
+ y: prevSession?.y ?? parentY + jitter(),
415
+ vx: prevSession?.vx ?? 0,
416
+ vy: prevSession?.vy ?? 0,
417
+ pinned: prevSession?.pinned ?? false,
418
+ radius: INACTIVE_SESSION_RADIUS,
419
+ tentacleId: session.tentacleId,
420
+ label: session.firstUserTurnPreview
421
+ ? session.firstUserTurnPreview.slice(0, 40)
422
+ : session.sessionId.slice(0, 12),
423
+ color,
424
+ sessionId: session.sessionId,
425
+ ...(session.firstUserTurnPreview !== null
426
+ ? { firstPromptPreview: session.firstUserTurnPreview }
427
+ : {}),
428
+ };
429
+ nodes.push(sessionNode);
430
+ currentNodesById.set(sessionNodeId, sessionNode);
431
+ edges.push({ source: tentacleNodeId, target: sessionNodeId });
432
+ }
433
+
434
+ // Update position cache
435
+ const nextMap = new Map<string, GraphNode>();
436
+ for (const n of nodes) {
437
+ nextMap.set(n.id, n);
438
+ }
439
+ prevNodesRef.current = nextMap;
440
+
441
+ return {
442
+ nodes,
443
+ edges,
444
+ tentacleById: deckMap,
445
+ sessionsByTentacleId,
446
+ refresh,
447
+ refreshDeckTentacles,
448
+ };
449
+ };
@@ -0,0 +1,260 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import { WORLD_H, WORLD_W } from "./useForceSimulation";
3
+
4
+ type CanvasTransform = {
5
+ translateX: number;
6
+ translateY: number;
7
+ scale: number;
8
+ };
9
+
10
+ const MIN_SCALE = 0.1;
11
+ const MAX_SCALE = 3.0;
12
+ const ZOOM_FACTOR = 0.1;
13
+
14
+ type UseCanvasTransformResult = {
15
+ transform: CanvasTransform;
16
+ isPanning: boolean;
17
+ svgRef: React.RefObject<SVGSVGElement | null>;
18
+ handleWheel: (e: React.WheelEvent<SVGSVGElement>) => void;
19
+ handlePointerDown: (e: React.PointerEvent<SVGSVGElement>) => void;
20
+ handlePointerMove: (e: React.PointerEvent<SVGSVGElement>) => void;
21
+ handlePointerUp: (e: React.PointerEvent<SVGSVGElement>) => void;
22
+ screenToGraph: (screenX: number, screenY: number) => { x: number; y: number };
23
+ graphToScreen: (graphX: number, graphY: number) => { x: number; y: number };
24
+ zoomIn: () => void;
25
+ zoomOut: () => void;
26
+ fitAll: (nodes: { x: number; y: number }[]) => void;
27
+ };
28
+
29
+ export const useCanvasTransform = (): UseCanvasTransformResult => {
30
+ const [transform, setTransform] = useState<CanvasTransform>({
31
+ translateX: 0,
32
+ translateY: 0,
33
+ scale: 1,
34
+ });
35
+ const svgRef = useRef<SVGSVGElement | null>(null);
36
+ const centeredRef = useRef(false);
37
+
38
+ // Fit the fixed world bounds into the viewport once on mount
39
+ useEffect(() => {
40
+ if (centeredRef.current) return;
41
+ const svg = svgRef.current;
42
+ if (!svg) return;
43
+ const rect = svg.getBoundingClientRect();
44
+ if (rect.width === 0 || rect.height === 0) return;
45
+ centeredRef.current = true;
46
+
47
+ const padding = 40;
48
+ const scaleX = (rect.width - padding * 2) / WORLD_W;
49
+ const scaleY = (rect.height - padding * 2) / WORLD_H;
50
+ const scale = Math.min(scaleX, scaleY);
51
+
52
+ setTransform({
53
+ scale,
54
+ translateX: rect.width / 2,
55
+ translateY: rect.height / 2,
56
+ });
57
+ });
58
+
59
+ // Re-center the graph when the SVG container resizes (e.g. split panel opens)
60
+ const prevSizeRef = useRef<{ width: number; height: number } | null>(null);
61
+ useEffect(() => {
62
+ const svg = svgRef.current;
63
+ if (!svg) return;
64
+
65
+ const ro = new ResizeObserver((entries) => {
66
+ const entry = entries[0];
67
+ if (!entry) return;
68
+ const { width, height } = entry.contentRect;
69
+ if (width === 0 || height === 0) return;
70
+
71
+ const prev = prevSizeRef.current;
72
+ if (prev && (Math.abs(prev.width - width) > 1 || Math.abs(prev.height - height) > 1)) {
73
+ // Adjust translate so the graph center stays in the center of the new viewport
74
+ setTransform((t) => ({
75
+ ...t,
76
+ translateX: t.translateX + (width - prev.width) / 2,
77
+ translateY: t.translateY + (height - prev.height) / 2,
78
+ }));
79
+ }
80
+ prevSizeRef.current = { width, height };
81
+ });
82
+ ro.observe(svg);
83
+ return () => ro.disconnect();
84
+ }, []);
85
+
86
+ const [isPanning, setIsPanning] = useState(false);
87
+ const panState = useRef<{
88
+ startX: number;
89
+ startY: number;
90
+ startTx: number;
91
+ startTy: number;
92
+ } | null>(null);
93
+
94
+ const screenToGraph = useCallback(
95
+ (screenX: number, screenY: number) => {
96
+ const svg = svgRef.current;
97
+ if (!svg) return { x: screenX, y: screenY };
98
+ const rect = svg.getBoundingClientRect();
99
+ const svgX = screenX - rect.left;
100
+ const svgY = screenY - rect.top;
101
+ return {
102
+ x: (svgX - transform.translateX) / transform.scale,
103
+ y: (svgY - transform.translateY) / transform.scale,
104
+ };
105
+ },
106
+ [transform],
107
+ );
108
+
109
+ const graphToScreen = useCallback(
110
+ (graphX: number, graphY: number) => {
111
+ const svg = svgRef.current;
112
+ if (!svg) return { x: graphX, y: graphY };
113
+ const rect = svg.getBoundingClientRect();
114
+ return {
115
+ x: graphX * transform.scale + transform.translateX + rect.left,
116
+ y: graphY * transform.scale + transform.translateY + rect.top,
117
+ };
118
+ },
119
+ [transform],
120
+ );
121
+
122
+ const handleWheel = useCallback((e: React.WheelEvent<SVGSVGElement>) => {
123
+ e.preventDefault();
124
+ const svg = svgRef.current;
125
+ if (!svg) return;
126
+
127
+ const rect = svg.getBoundingClientRect();
128
+ const cursorX = e.clientX - rect.left;
129
+ const cursorY = e.clientY - rect.top;
130
+
131
+ setTransform((prev) => {
132
+ const direction = e.deltaY < 0 ? 1 : -1;
133
+ const factor = 1 + direction * ZOOM_FACTOR;
134
+ const nextScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, prev.scale * factor));
135
+ const scaleRatio = nextScale / prev.scale;
136
+
137
+ return {
138
+ scale: nextScale,
139
+ translateX: cursorX - (cursorX - prev.translateX) * scaleRatio,
140
+ translateY: cursorY - (cursorY - prev.translateY) * scaleRatio,
141
+ };
142
+ });
143
+ }, []);
144
+
145
+ const handlePointerDown = useCallback(
146
+ (e: React.PointerEvent<SVGSVGElement>) => {
147
+ if (e.button !== 0) return;
148
+ if (e.target !== svgRef.current && (e.target as SVGElement).closest?.(".canvas-node")) {
149
+ return;
150
+ }
151
+ panState.current = {
152
+ startX: e.clientX,
153
+ startY: e.clientY,
154
+ startTx: transform.translateX,
155
+ startTy: transform.translateY,
156
+ };
157
+ setIsPanning(true);
158
+ (e.target as SVGSVGElement).setPointerCapture?.(e.pointerId);
159
+ },
160
+ [transform.translateX, transform.translateY],
161
+ );
162
+
163
+ const handlePointerMove = useCallback((e: React.PointerEvent<SVGSVGElement>) => {
164
+ const pan = panState.current;
165
+ if (!pan) return;
166
+
167
+ setTransform((prev) => ({
168
+ ...prev,
169
+ translateX: pan.startTx + (e.clientX - pan.startX),
170
+ translateY: pan.startTy + (e.clientY - pan.startY),
171
+ }));
172
+ }, []);
173
+
174
+ const handlePointerUp = useCallback(() => {
175
+ panState.current = null;
176
+ setIsPanning(false);
177
+ }, []);
178
+
179
+ const zoomIn = useCallback(() => {
180
+ const svg = svgRef.current;
181
+ if (!svg) return;
182
+ const rect = svg.getBoundingClientRect();
183
+ const cx = rect.width / 2;
184
+ const cy = rect.height / 2;
185
+ setTransform((prev) => {
186
+ const nextScale = Math.min(MAX_SCALE, prev.scale * (1 + ZOOM_FACTOR));
187
+ const ratio = nextScale / prev.scale;
188
+ return {
189
+ scale: nextScale,
190
+ translateX: cx - (cx - prev.translateX) * ratio,
191
+ translateY: cy - (cy - prev.translateY) * ratio,
192
+ };
193
+ });
194
+ }, []);
195
+
196
+ const zoomOut = useCallback(() => {
197
+ const svg = svgRef.current;
198
+ if (!svg) return;
199
+ const rect = svg.getBoundingClientRect();
200
+ const cx = rect.width / 2;
201
+ const cy = rect.height / 2;
202
+ setTransform((prev) => {
203
+ const nextScale = Math.max(MIN_SCALE, prev.scale * (1 - ZOOM_FACTOR));
204
+ const ratio = nextScale / prev.scale;
205
+ return {
206
+ scale: nextScale,
207
+ translateX: cx - (cx - prev.translateX) * ratio,
208
+ translateY: cy - (cy - prev.translateY) * ratio,
209
+ };
210
+ });
211
+ }, []);
212
+
213
+ const fitAll = useCallback((nodes: { x: number; y: number }[]) => {
214
+ const svg = svgRef.current;
215
+ if (!svg || nodes.length === 0) return;
216
+ const rect = svg.getBoundingClientRect();
217
+ if (rect.width === 0 || rect.height === 0) return;
218
+
219
+ let minX = Number.POSITIVE_INFINITY;
220
+ let minY = Number.POSITIVE_INFINITY;
221
+ let maxX = Number.NEGATIVE_INFINITY;
222
+ let maxY = Number.NEGATIVE_INFINITY;
223
+ for (const n of nodes) {
224
+ if (n.x < minX) minX = n.x;
225
+ if (n.y < minY) minY = n.y;
226
+ if (n.x > maxX) maxX = n.x;
227
+ if (n.y > maxY) maxY = n.y;
228
+ }
229
+
230
+ const graphW = maxX - minX || 1;
231
+ const graphH = maxY - minY || 1;
232
+ const padding = 80;
233
+ const scaleX = (rect.width - padding * 2) / graphW;
234
+ const scaleY = (rect.height - padding * 2) / graphH;
235
+ const scale = Math.min(Math.max(Math.min(scaleX, scaleY), MIN_SCALE), MAX_SCALE);
236
+ const centerGx = (minX + maxX) / 2;
237
+ const centerGy = (minY + maxY) / 2;
238
+
239
+ setTransform({
240
+ scale,
241
+ translateX: rect.width / 2 - centerGx * scale,
242
+ translateY: rect.height / 2 - centerGy * scale,
243
+ });
244
+ }, []);
245
+
246
+ return {
247
+ transform,
248
+ isPanning,
249
+ svgRef,
250
+ handleWheel,
251
+ handlePointerDown,
252
+ handlePointerMove,
253
+ handlePointerUp,
254
+ screenToGraph,
255
+ graphToScreen,
256
+ zoomIn,
257
+ zoomOut,
258
+ fitAll,
259
+ };
260
+ };