@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,1532 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+
3
+ import type { WorkspaceSetupSnapshot, WorkspaceSetupStepId } from "@octogent/core";
4
+ import {
5
+ Check as CheckIcon,
6
+ ChevronDown,
7
+ GitBranch,
8
+ Hexagon,
9
+ Layers,
10
+ ListTodo,
11
+ Maximize,
12
+ Pause,
13
+ Play,
14
+ RefreshCw,
15
+ Sparkles,
16
+ Terminal as TerminalIcon,
17
+ Trash2,
18
+ X,
19
+ } from "lucide-react";
20
+ import type { GraphNode } from "../app/canvas/types";
21
+ import { useAgentRuntimeStates } from "../app/hooks/useAgentRuntimeStates";
22
+ import { useCanvasGraphData } from "../app/hooks/useCanvasGraphData";
23
+ import { useCanvasTransform } from "../app/hooks/useCanvasTransform";
24
+ import { DEFAULT_FORCE_PARAMS, useForceSimulation } from "../app/hooks/useForceSimulation";
25
+ import type { PendingDeleteTerminal } from "../app/hooks/useTerminalMutations";
26
+ import {
27
+ type TerminalRuntimeStateStore,
28
+ createTerminalRuntimeStateStore,
29
+ } from "../app/terminalRuntimeStateStore";
30
+ import type { TerminalView, TerminalWorkspaceMode } from "../app/types";
31
+ import { DeleteTentacleDialog } from "./DeleteTentacleDialog";
32
+ import { CanvasTentaclePanel } from "./canvas/CanvasTentaclePanel";
33
+ import { CanvasTerminalColumn } from "./canvas/CanvasTerminalColumn";
34
+ import { DeleteAllTerminalsDialog } from "./canvas/DeleteAllTerminalsDialog";
35
+ import { OctopusNode } from "./canvas/OctopusNode";
36
+ import { SessionNode } from "./canvas/SessionNode";
37
+ import { WorkspaceSetupCard } from "./deck/WorkspaceSetupCard";
38
+
39
+ type ContextMenuState =
40
+ | { kind: "canvas"; x: number; y: number }
41
+ | { kind: "tentacle"; x: number; y: number; tentacleId: string }
42
+ | { kind: "octoboss"; x: number; y: number }
43
+ | {
44
+ kind: "active-session";
45
+ x: number;
46
+ y: number;
47
+ nodeId: string;
48
+ tentacleId: string;
49
+ sessionId: string;
50
+ label: string;
51
+ workspaceMode?: string;
52
+ };
53
+
54
+ type CanvasPrimaryViewProps = {
55
+ columns: TerminalView;
56
+ runtimeStateStore?: TerminalRuntimeStateStore;
57
+ isUiStateHydrated?: boolean;
58
+ canvasOpenTerminalIds?: string[];
59
+ canvasOpenTentacleIds?: string[];
60
+ canvasTerminalsPanelWidth?: number | null;
61
+ workspaceSetup?: WorkspaceSetupSnapshot | null;
62
+ isWorkspaceSetupLoading?: boolean;
63
+ workspaceSetupError?: string | null;
64
+ runningWorkspaceSetupStepId?: WorkspaceSetupStepId | null;
65
+ onRunWorkspaceSetupStep?: (stepId: WorkspaceSetupStepId) => Promise<void> | void;
66
+ onLaunchWorkspaceSetupPlanner?: () => Promise<string | undefined> | undefined;
67
+ recentlyCreatedTerminal?: TerminalView[number] | null;
68
+ onCanvasOpenTerminalIdsChange?: (ids: string[]) => void;
69
+ onCanvasOpenTentacleIdsChange?: (ids: string[]) => void;
70
+ onCanvasTerminalsPanelWidthChange?: (width: number | null) => void;
71
+ onCreateAgent?: (tentacleId: string) => Promise<string | undefined> | undefined;
72
+ onCreateTerminal?: () => Promise<string | undefined> | undefined;
73
+ onCreateWorktreeTerminal?: () => Promise<string | undefined> | undefined;
74
+ onCreateTentacle?: () => void;
75
+ onSpawnSwarm?: (tentacleId: string, workspaceMode: TerminalWorkspaceMode) => Promise<void>;
76
+ onSolveTodoItem?: (tentacleId: string, itemIndex: number) => Promise<void> | void;
77
+ onOctobossAction?: (action: string) => Promise<string | undefined> | undefined;
78
+ onTentacleAction?: (
79
+ tentacleId: string,
80
+ action: string,
81
+ ) => Promise<string | undefined> | undefined;
82
+ onNavigateToConversation?: (sessionId: string) => void;
83
+ onDeleteActiveSession?: (
84
+ terminalId: string,
85
+ terminalName: string,
86
+ workspaceMode?: string,
87
+ ) => void;
88
+ pendingDeleteTerminal?: PendingDeleteTerminal | null;
89
+ isDeletingTerminalId?: string | null;
90
+ onCancelDelete?: () => void;
91
+ onConfirmDelete?: () => void;
92
+ onTerminalRenamed?: ((terminalId: string, tentacleName: string) => void) | undefined;
93
+ onTerminalActivity?: ((terminalId: string) => void) | undefined;
94
+ onRefreshColumns?: () => Promise<void> | void;
95
+ };
96
+
97
+ const CLICK_THRESHOLD = 5;
98
+ const GRAPH_MIN_WIDTH = 300;
99
+ const TERMINAL_MIN_WIDTH = 370;
100
+ const ACTIVE_SESSION_RADIUS = 12;
101
+ const buildActiveSessionNodeId = (terminalId: string) => `a:${terminalId}`;
102
+ const buildTentacleNodeId = (tentacleId: string) => `t:${tentacleId}`;
103
+
104
+ const buildCanvasEdgePath = (
105
+ source: GraphNode,
106
+ target: GraphNode,
107
+ edgeIndex: number,
108
+ edgeCount: number,
109
+ ): string => {
110
+ const dx = target.x - source.x;
111
+ const dy = target.y - source.y;
112
+ const dist = Math.sqrt(dx * dx + dy * dy);
113
+ if (dist < 1) return "";
114
+
115
+ const shortenSourceBy = source.radius + 6;
116
+ const shortenTargetBy = target.radius + 6;
117
+ const startRatio = Math.min(1, shortenSourceBy / dist);
118
+ const endRatio = Math.max(0, (dist - shortenTargetBy) / dist);
119
+ const sx = source.x + dx * startRatio;
120
+ const sy = source.y + dy * startRatio;
121
+ const tx = source.x + dx * endRatio;
122
+ const ty = source.y + dy * endRatio;
123
+
124
+ const curvature = edgeCount <= 1 ? 0.18 : (edgeIndex / (edgeCount - 1) - 0.5) * 1.2;
125
+ const offsetRatio = edgeCount <= 1 ? 0.16 : 0.18;
126
+ const baseOffset = Math.max(16, Math.min(32, dist * offsetRatio));
127
+ const offsetX = (-dy / dist) * curvature * baseOffset;
128
+ const offsetY = (dx / dist) * curvature * baseOffset;
129
+ const cpx = (sx + tx) / 2 + offsetX;
130
+ const cpy = (sy + ty) / 2 + offsetY;
131
+
132
+ return `M ${sx} ${sy} Q ${cpx} ${cpy} ${tx} ${ty}`;
133
+ };
134
+
135
+ const isEdgeActivityVisible = (target: GraphNode): boolean =>
136
+ target.type === "active-session" &&
137
+ target.hasUserPrompt !== false &&
138
+ target.agentRuntimeState !== undefined &&
139
+ target.agentRuntimeState !== "idle";
140
+
141
+ const renderEdgeActivityDots = (path: string, color: string, keyPrefix: string) =>
142
+ [0, 1, 2].flatMap((index) => [
143
+ <circle
144
+ key={`${keyPrefix}-trail-${index}`}
145
+ className="canvas-edge-activity-dot canvas-edge-activity-dot--trail"
146
+ r={4.6}
147
+ fill={color}
148
+ opacity={Math.max(0.14, 0.28 - index * 0.04)}
149
+ >
150
+ <animateMotion
151
+ path={path}
152
+ begin={`${index * 0.62}s`}
153
+ dur="1.9s"
154
+ repeatCount="indefinite"
155
+ rotate="auto"
156
+ />
157
+ <animate
158
+ attributeName="r"
159
+ values="3.8;5.2;3.8"
160
+ dur="1.9s"
161
+ begin={`${index * 0.62}s`}
162
+ repeatCount="indefinite"
163
+ />
164
+ </circle>,
165
+ <circle
166
+ key={`${keyPrefix}-dot-${index}`}
167
+ className="canvas-edge-activity-dot"
168
+ r={3.2}
169
+ fill="#fff4cc"
170
+ stroke={color}
171
+ strokeWidth={1.2}
172
+ opacity={Math.max(0.7, 1 - index * 0.08)}
173
+ >
174
+ <animateMotion
175
+ path={path}
176
+ begin={`${index * 0.62}s`}
177
+ dur="1.9s"
178
+ repeatCount="indefinite"
179
+ rotate="auto"
180
+ />
181
+ <animate
182
+ attributeName="r"
183
+ values="2.8;3.8;2.8"
184
+ dur="1.9s"
185
+ begin={`${index * 0.62}s`}
186
+ repeatCount="indefinite"
187
+ />
188
+ </circle>,
189
+ ]);
190
+
191
+ export const CanvasPrimaryView = ({
192
+ columns,
193
+ runtimeStateStore: providedRuntimeStateStore,
194
+ isUiStateHydrated,
195
+ canvasOpenTerminalIds,
196
+ canvasOpenTentacleIds,
197
+ canvasTerminalsPanelWidth: persistedTerminalsPanelWidth,
198
+ workspaceSetup = null,
199
+ isWorkspaceSetupLoading = false,
200
+ workspaceSetupError = null,
201
+ runningWorkspaceSetupStepId = null,
202
+ onRunWorkspaceSetupStep,
203
+ onLaunchWorkspaceSetupPlanner,
204
+ recentlyCreatedTerminal,
205
+ onCanvasOpenTerminalIdsChange,
206
+ onCanvasOpenTentacleIdsChange,
207
+ onCanvasTerminalsPanelWidthChange,
208
+ onCreateAgent,
209
+ onCreateTerminal,
210
+ onCreateWorktreeTerminal,
211
+ onCreateTentacle,
212
+ onSpawnSwarm,
213
+ onSolveTodoItem,
214
+ onOctobossAction,
215
+ onTentacleAction,
216
+ onNavigateToConversation,
217
+ onDeleteActiveSession,
218
+ pendingDeleteTerminal,
219
+ isDeletingTerminalId,
220
+ onCancelDelete,
221
+ onConfirmDelete,
222
+ onTerminalRenamed,
223
+ onTerminalActivity,
224
+ onRefreshColumns,
225
+ }: CanvasPrimaryViewProps) => {
226
+ const runtimeStateStoreRef = useRef<TerminalRuntimeStateStore | null>(null);
227
+ if (runtimeStateStoreRef.current === null) {
228
+ runtimeStateStoreRef.current = providedRuntimeStateStore ?? createTerminalRuntimeStateStore();
229
+ }
230
+ const runtimeStateStore = providedRuntimeStateStore ?? runtimeStateStoreRef.current;
231
+ const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
232
+ const [isDeleteAllDialogOpen, setIsDeleteAllDialogOpen] = useState(false);
233
+ const [openTerminals, setOpenTerminals] = useState<Map<string, GraphNode>>(new Map());
234
+ const [openTentacles, setOpenTentacles] = useState<Map<string, GraphNode>>(new Map());
235
+ const [dragNodeId, setDragNodeId] = useState<string | null>(null);
236
+ const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
237
+ const [terminalsPanelWidth, setTerminalsPanelWidth] = useState<number | null>(null);
238
+ const [pendingOpenAgentId, setPendingOpenAgentId] = useState<string | null>(null);
239
+ const [hideIdleTerminals, setHideIdleTerminals] = useState(false);
240
+ const [isLaunchingWorkspaceSetupPlanner, setIsLaunchingWorkspaceSetupPlanner] = useState(false);
241
+ const hasHydratedTerminals = useRef(false);
242
+ const hasHydratedTentacles = useRef(false);
243
+ const lastHandledCreatedTerminalIdRef = useRef<string | null>(null);
244
+ const dragStartRef = useRef<{ x: number; y: number } | null>(null);
245
+ const nodeClickedRef = useRef(false);
246
+ const dividerDragRef = useRef<{ startX: number; startWidth: number } | null>(null);
247
+ const containerRef = useRef<HTMLElement>(null);
248
+ const terminalsPanelRef = useRef<HTMLDivElement>(null);
249
+ const panelRefs = useRef(new Map<string, HTMLElement>());
250
+ const lastFocusedPanelIdRef = useRef<string | null>(null);
251
+ const shouldShowWorkspaceSetupCard = Boolean(workspaceSetup?.shouldShowSetupCard);
252
+
253
+ const agentRuntimeStates = useAgentRuntimeStates(runtimeStateStore, columns);
254
+
255
+ const {
256
+ nodes,
257
+ edges,
258
+ tentacleById,
259
+ sessionsByTentacleId,
260
+ refresh: refreshGraphData,
261
+ refreshDeckTentacles,
262
+ } = useCanvasGraphData({ columns, enabled: true, agentRuntimeStates });
263
+
264
+ const {
265
+ transform,
266
+ isPanning,
267
+ svgRef,
268
+ handleWheel,
269
+ handlePointerDown: handleCanvasPointerDown,
270
+ handlePointerMove: handleCanvasPointerMove,
271
+ handlePointerUp: handleCanvasPointerUp,
272
+ screenToGraph,
273
+ fitAll,
274
+ } = useCanvasTransform();
275
+
276
+ const { simulatedNodes, pinNode, unpinNode, moveNode, reheat } = useForceSimulation({
277
+ nodes,
278
+ edges,
279
+ centerX: 0,
280
+ centerY: 0,
281
+ });
282
+
283
+ const nodesById = useMemo(() => {
284
+ const map = new Map<string, GraphNode>();
285
+ for (const n of simulatedNodes) {
286
+ map.set(n.id, n);
287
+ }
288
+ return map;
289
+ }, [simulatedNodes]);
290
+
291
+ const resolveActiveSessionNode = useCallback(
292
+ (terminalId: string): GraphNode | null => {
293
+ const nodeId = buildActiveSessionNodeId(terminalId);
294
+ const existingNode = nodesById.get(nodeId);
295
+ const terminal = columns.find((entry) => entry.terminalId === terminalId);
296
+ if (!terminal) {
297
+ return existingNode?.type === "active-session" ? existingNode : null;
298
+ }
299
+
300
+ const parentNodeId = terminal.parentTerminalId
301
+ ? buildActiveSessionNodeId(terminal.parentTerminalId)
302
+ : buildTentacleNodeId(terminal.tentacleId);
303
+ const anchorNode =
304
+ existingNode?.type === "active-session"
305
+ ? existingNode
306
+ : (nodesById.get(parentNodeId) ??
307
+ nodesById.get(buildTentacleNodeId(terminal.tentacleId)));
308
+
309
+ return {
310
+ id: nodeId,
311
+ type: "active-session",
312
+ x: anchorNode?.x ?? 0,
313
+ y: anchorNode?.y ?? 0,
314
+ vx: 0,
315
+ vy: 0,
316
+ pinned: false,
317
+ radius: ACTIVE_SESSION_RADIUS,
318
+ tentacleId: terminal.tentacleId,
319
+ label: terminal.tentacleName || terminal.label || terminal.terminalId,
320
+ color: anchorNode?.color ?? "#c0c0c0",
321
+ sessionId: terminal.terminalId,
322
+ agentState: terminal.state,
323
+ hasUserPrompt: terminal.hasUserPrompt ?? false,
324
+ ...(terminal.workspaceMode ? { workspaceMode: terminal.workspaceMode } : {}),
325
+ ...(terminal.parentTerminalId ? { parentTerminalId: terminal.parentTerminalId } : {}),
326
+ };
327
+ },
328
+ [columns, nodesById],
329
+ );
330
+
331
+ // Hydrate open terminals after a settling delay so all async data (columns,
332
+ // graph nodes, simulation) has time to land before we attempt the lookup.
333
+ const [isHydratingTerminals, setIsHydratingTerminals] = useState(false);
334
+
335
+ useEffect(() => {
336
+ if (hasHydratedTerminals.current) return;
337
+ if (!isUiStateHydrated) return;
338
+ if (!canvasOpenTerminalIds || canvasOpenTerminalIds.length === 0) {
339
+ hasHydratedTerminals.current = true;
340
+ return;
341
+ }
342
+
343
+ setIsHydratingTerminals(true);
344
+ const timer = window.setTimeout(() => {
345
+ setIsHydratingTerminals(false);
346
+ hasHydratedTerminals.current = true;
347
+ }, 800);
348
+
349
+ return () => window.clearTimeout(timer);
350
+ }, [isUiStateHydrated, canvasOpenTerminalIds]);
351
+
352
+ // Once the settling timer fires, perform the actual hydration from the
353
+ // simulation graph which should now be fully populated.
354
+ const openTerminalCount = openTerminals.size;
355
+ useEffect(() => {
356
+ if (isHydratingTerminals) return;
357
+ if (!hasHydratedTerminals.current) return;
358
+ if (openTerminalCount > 0) return;
359
+ if (!canvasOpenTerminalIds || canvasOpenTerminalIds.length === 0) return;
360
+
361
+ const restoredMap = new Map<string, GraphNode>();
362
+ for (const nodeId of canvasOpenTerminalIds) {
363
+ const node = nodesById.get(nodeId);
364
+ if (node && node.type === "active-session") {
365
+ restoredMap.set(nodeId, { ...node });
366
+ }
367
+ }
368
+ if (restoredMap.size > 0) {
369
+ setOpenTerminals(restoredMap);
370
+ }
371
+
372
+ if (persistedTerminalsPanelWidth != null && persistedTerminalsPanelWidth > 0) {
373
+ setTerminalsPanelWidth(persistedTerminalsPanelWidth);
374
+ }
375
+ }, [
376
+ isHydratingTerminals,
377
+ openTerminalCount,
378
+ canvasOpenTerminalIds,
379
+ persistedTerminalsPanelWidth,
380
+ nodesById,
381
+ ]);
382
+
383
+ // Persist open terminal IDs when they change
384
+ useEffect(() => {
385
+ if (!hasHydratedTerminals.current) return;
386
+ onCanvasOpenTerminalIdsChange?.(Array.from(openTerminals.keys()));
387
+ }, [openTerminals, onCanvasOpenTerminalIdsChange]);
388
+
389
+ useEffect(() => {
390
+ setOpenTerminals((current) => {
391
+ let didChange = false;
392
+ const next = new Map<string, GraphNode>();
393
+
394
+ for (const [nodeId, node] of current) {
395
+ if (!node.sessionId) {
396
+ next.set(nodeId, node);
397
+ continue;
398
+ }
399
+
400
+ const terminal = columns.find((entry) => entry.terminalId === node.sessionId);
401
+ if (!terminal) {
402
+ didChange = true;
403
+ continue;
404
+ }
405
+
406
+ const nextLabel = terminal.tentacleName || terminal.label || terminal.terminalId;
407
+ const nextNode: GraphNode = {
408
+ ...node,
409
+ tentacleId: terminal.tentacleId,
410
+ label: nextLabel,
411
+ agentState: terminal.state,
412
+ hasUserPrompt: terminal.hasUserPrompt ?? false,
413
+ ...(terminal.workspaceMode ? { workspaceMode: terminal.workspaceMode } : {}),
414
+ ...(terminal.parentTerminalId ? { parentTerminalId: terminal.parentTerminalId } : {}),
415
+ };
416
+
417
+ if (
418
+ node.label !== nextNode.label ||
419
+ node.tentacleId !== nextNode.tentacleId ||
420
+ node.agentState !== nextNode.agentState ||
421
+ node.hasUserPrompt !== nextNode.hasUserPrompt ||
422
+ node.workspaceMode !== nextNode.workspaceMode ||
423
+ node.parentTerminalId !== nextNode.parentTerminalId
424
+ ) {
425
+ didChange = true;
426
+ next.set(nodeId, nextNode);
427
+ continue;
428
+ }
429
+
430
+ next.set(nodeId, node);
431
+ }
432
+
433
+ return didChange ? next : current;
434
+ });
435
+ }, [columns]);
436
+
437
+ // Hydrate open tentacles from persisted IDs.
438
+ // Gate on tentacle-type nodes being present (deck API fetch is async).
439
+ const hasTentacleNodes = simulatedNodes.some((n) => n.type === "tentacle");
440
+ const openTentacleCount = openTentacles.size;
441
+ useEffect(() => {
442
+ if (hasHydratedTentacles.current) return;
443
+ if (!isUiStateHydrated) return;
444
+ if (!hasTentacleNodes) return;
445
+
446
+ if (canvasOpenTentacleIds && canvasOpenTentacleIds.length > 0) {
447
+ const restoredMap = new Map<string, GraphNode>();
448
+ for (const nodeId of canvasOpenTentacleIds) {
449
+ const node = nodesById.get(nodeId);
450
+ if (node && (node.type === "tentacle" || node.type === "octoboss")) {
451
+ restoredMap.set(nodeId, { ...node });
452
+ }
453
+ }
454
+ if (restoredMap.size > 0) {
455
+ setOpenTentacles(restoredMap);
456
+ }
457
+ }
458
+
459
+ hasHydratedTentacles.current = true;
460
+ }, [isUiStateHydrated, canvasOpenTentacleIds, hasTentacleNodes, nodesById]);
461
+
462
+ // Persist open tentacle IDs when they change
463
+ useEffect(() => {
464
+ if (!hasHydratedTentacles.current) return;
465
+ onCanvasOpenTentacleIdsChange?.(Array.from(openTentacles.keys()));
466
+ }, [openTentacles, onCanvasOpenTentacleIdsChange]);
467
+
468
+ // Persist terminals panel width only when user has explicitly dragged the divider
469
+ useEffect(() => {
470
+ if (!hasHydratedTerminals.current) return;
471
+ if (terminalsPanelWidth == null) return;
472
+ onCanvasTerminalsPanelWidthChange?.(terminalsPanelWidth);
473
+ }, [terminalsPanelWidth, onCanvasTerminalsPanelWidthChange]);
474
+
475
+ const handleNodePointerDown = useCallback(
476
+ (e: React.PointerEvent, nodeId: string) => {
477
+ if (e.button !== 0) return;
478
+ dragStartRef.current = { x: e.clientX, y: e.clientY };
479
+ setDragNodeId(nodeId);
480
+ pinNode(nodeId);
481
+ svgRef.current?.setPointerCapture(e.pointerId);
482
+ },
483
+ [pinNode, svgRef],
484
+ );
485
+
486
+ const handleSvgPointerMove = useCallback(
487
+ (e: React.PointerEvent<SVGSVGElement>) => {
488
+ if (dragNodeId) {
489
+ const graphPos = screenToGraph(e.clientX, e.clientY);
490
+ moveNode(dragNodeId, graphPos.x, graphPos.y);
491
+ return;
492
+ }
493
+ handleCanvasPointerMove(e);
494
+ },
495
+ [dragNodeId, screenToGraph, moveNode, handleCanvasPointerMove],
496
+ );
497
+
498
+ const handleNodeClick = useCallback(
499
+ (nodeId: string) => {
500
+ setSelectedNodeId(nodeId);
501
+ const node = nodesById.get(nodeId);
502
+ if (!node) return;
503
+
504
+ if (node.type === "active-session") {
505
+ const resolvedNode = node.sessionId
506
+ ? (resolveActiveSessionNode(node.sessionId) ?? node)
507
+ : node;
508
+ setOpenTerminals((prev) => {
509
+ const next = new Map(prev);
510
+ if (next.has(nodeId)) {
511
+ next.delete(nodeId);
512
+ } else {
513
+ next.set(nodeId, { ...resolvedNode });
514
+ }
515
+ return next;
516
+ });
517
+ } else if (node.type === "tentacle" || node.type === "octoboss") {
518
+ setOpenTentacles((prev) => {
519
+ const next = new Map(prev);
520
+ if (next.has(nodeId)) {
521
+ next.delete(nodeId);
522
+ } else {
523
+ next.set(nodeId, { ...node });
524
+ }
525
+ return next;
526
+ });
527
+ } else if (node.type === "inactive-session" && node.sessionId) {
528
+ onNavigateToConversation?.(node.sessionId);
529
+ }
530
+ },
531
+ [nodesById, onNavigateToConversation, resolveActiveSessionNode],
532
+ );
533
+
534
+ const setPanelRef = useCallback(
535
+ (nodeId: string) => (element: HTMLElement | null) => {
536
+ if (element) {
537
+ panelRefs.current.set(nodeId, element);
538
+ return;
539
+ }
540
+ panelRefs.current.delete(nodeId);
541
+ },
542
+ [],
543
+ );
544
+
545
+ const handleCloseTentacle = useCallback((nodeId: string) => {
546
+ setOpenTentacles((prev) => {
547
+ const next = new Map(prev);
548
+ next.delete(nodeId);
549
+ return next;
550
+ });
551
+ setSelectedNodeId((prev) => (prev === nodeId ? null : prev));
552
+ }, []);
553
+
554
+ const handleCloseTerminal = useCallback((nodeId: string) => {
555
+ setOpenTerminals((prev) => {
556
+ const next = new Map(prev);
557
+ next.delete(nodeId);
558
+ return next;
559
+ });
560
+ setSelectedNodeId((prev) => (prev === nodeId ? null : prev));
561
+ }, []);
562
+
563
+ // Divider drag handlers
564
+ const handleDividerPointerDown = useCallback(
565
+ (e: React.PointerEvent<HTMLDivElement>) => {
566
+ e.preventDefault();
567
+ // Measure the actual rendered width of the terminals panel (works whether CSS- or inline-sized)
568
+ const panelEl = (e.target as HTMLElement).nextElementSibling as HTMLElement | null;
569
+ const currentWidth = panelEl?.clientWidth ?? terminalsPanelWidth ?? 600;
570
+ dividerDragRef.current = { startX: e.clientX, startWidth: currentWidth };
571
+ (e.target as HTMLElement).setPointerCapture(e.pointerId);
572
+ },
573
+ [terminalsPanelWidth],
574
+ );
575
+
576
+ const handleDividerPointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
577
+ const drag = dividerDragRef.current;
578
+ if (!drag) return;
579
+ const containerWidth = containerRef.current?.clientWidth ?? 1200;
580
+ // Dragging left → terminals grow, dragging right → terminals shrink
581
+ const delta = drag.startX - e.clientX;
582
+ const newWidth = Math.max(
583
+ TERMINAL_MIN_WIDTH,
584
+ Math.min(containerWidth - GRAPH_MIN_WIDTH - 6, drag.startWidth + delta),
585
+ );
586
+ setTerminalsPanelWidth(newWidth);
587
+ }, []);
588
+
589
+ const handleDividerPointerUp = useCallback(() => {
590
+ dividerDragRef.current = null;
591
+ }, []);
592
+
593
+ // Convert vertical wheel to horizontal scroll only when hovering terminal headers
594
+ useEffect(() => {
595
+ if (!isHydratingTerminals && openTerminalCount === 0 && openTentacleCount === 0) return;
596
+ const panel = terminalsPanelRef.current;
597
+ if (!panel) return;
598
+ const handler = (e: WheelEvent) => {
599
+ const target = e.target as Element | null;
600
+ if (!target?.closest(".canvas-terminal-column-header")) return;
601
+ if (e.deltaY !== 0 && e.deltaX === 0) {
602
+ e.preventDefault();
603
+ panel.scrollLeft += e.deltaY;
604
+ }
605
+ };
606
+ panel.addEventListener("wheel", handler, { passive: false });
607
+ return () => panel.removeEventListener("wheel", handler);
608
+ }, [isHydratingTerminals, openTerminalCount, openTentacleCount]);
609
+
610
+ const handleSvgPointerUp = useCallback(
611
+ (e: React.PointerEvent<SVGSVGElement>) => {
612
+ if (dragNodeId) {
613
+ const start = dragStartRef.current;
614
+ const dx = start ? e.clientX - start.x : Number.POSITIVE_INFINITY;
615
+ const dy = start ? e.clientY - start.y : Number.POSITIVE_INFINITY;
616
+ const wasClick = Math.abs(dx) < CLICK_THRESHOLD && Math.abs(dy) < CLICK_THRESHOLD;
617
+
618
+ unpinNode(dragNodeId);
619
+ reheat();
620
+
621
+ if (wasClick) {
622
+ nodeClickedRef.current = true;
623
+ handleNodeClick(dragNodeId);
624
+ }
625
+
626
+ setDragNodeId(null);
627
+ dragStartRef.current = null;
628
+ return;
629
+ }
630
+ handleCanvasPointerUp(e);
631
+ },
632
+ [dragNodeId, unpinNode, reheat, handleCanvasPointerUp, handleNodeClick],
633
+ );
634
+
635
+ const handleSvgClick = useCallback((e: React.MouseEvent<SVGSVGElement>) => {
636
+ if (nodeClickedRef.current) {
637
+ nodeClickedRef.current = false;
638
+ return;
639
+ }
640
+ if (e.target === e.currentTarget) {
641
+ setSelectedNodeId(null);
642
+ }
643
+ }, []);
644
+
645
+ // Stable ref for nodesById so native listener always sees latest data
646
+ const nodesByIdRef = useRef(nodesById);
647
+ nodesByIdRef.current = nodesById;
648
+
649
+ // Stable refs so the native listener always sees the latest callbacks
650
+ const onNavigateRef = useRef(onNavigateToConversation);
651
+ onNavigateRef.current = onNavigateToConversation;
652
+
653
+ // Native contextmenu listener — must be native to reliably preventDefault
654
+ useEffect(() => {
655
+ const svg = svgRef.current;
656
+ if (!svg) return;
657
+
658
+ const handler = (e: MouseEvent) => {
659
+ let el = e.target as Element | null;
660
+ let nodeId: string | null = null;
661
+ while (el && el !== svg) {
662
+ const id = el.getAttribute("data-node-id");
663
+ if (id) {
664
+ nodeId = id;
665
+ break;
666
+ }
667
+ el = el.parentElement;
668
+ }
669
+ if (!nodeId) {
670
+ e.preventDefault();
671
+ e.stopPropagation();
672
+ setContextMenu({ kind: "canvas", x: e.clientX, y: e.clientY });
673
+ return;
674
+ }
675
+ const node = nodesByIdRef.current.get(nodeId);
676
+ if (!node) return;
677
+
678
+ if (node.type === "octoboss") {
679
+ e.preventDefault();
680
+ e.stopPropagation();
681
+ setContextMenu({ kind: "octoboss", x: e.clientX, y: e.clientY });
682
+ return;
683
+ }
684
+
685
+ if (node.type === "tentacle") {
686
+ e.preventDefault();
687
+ e.stopPropagation();
688
+ setContextMenu({
689
+ kind: "tentacle",
690
+ x: e.clientX,
691
+ y: e.clientY,
692
+ tentacleId: node.tentacleId,
693
+ });
694
+ return;
695
+ }
696
+
697
+ if (node.type === "inactive-session" && node.sessionId) {
698
+ e.preventDefault();
699
+ e.stopPropagation();
700
+ onNavigateRef.current?.(node.sessionId);
701
+ return;
702
+ }
703
+
704
+ if (node.type === "active-session" && node.sessionId) {
705
+ e.preventDefault();
706
+ e.stopPropagation();
707
+ setContextMenu({
708
+ kind: "active-session",
709
+ x: e.clientX,
710
+ y: e.clientY,
711
+ nodeId: node.id,
712
+ tentacleId: node.tentacleId,
713
+ sessionId: node.sessionId,
714
+ label: node.label,
715
+ ...(node.workspaceMode ? { workspaceMode: node.workspaceMode } : {}),
716
+ });
717
+ }
718
+ };
719
+
720
+ svg.addEventListener("contextmenu", handler);
721
+ return () => svg.removeEventListener("contextmenu", handler);
722
+ }, [svgRef]);
723
+
724
+ const handleCreateAgent = useCallback(
725
+ (tentacleId: string) => {
726
+ if (!onCreateAgent) return;
727
+ setContextMenu(null);
728
+ const result = onCreateAgent(tentacleId);
729
+ if (result && typeof result.then === "function") {
730
+ void result.then((agentId) => {
731
+ if (agentId) setPendingOpenAgentId(agentId);
732
+ });
733
+ }
734
+ },
735
+ [onCreateAgent],
736
+ );
737
+
738
+ const handleSpawnSwarm = useCallback(
739
+ (tentacleId: string, workspaceMode: TerminalWorkspaceMode) => {
740
+ setContextMenu(null);
741
+ void onSpawnSwarm?.(tentacleId, workspaceMode);
742
+ },
743
+ [onSpawnSwarm],
744
+ );
745
+
746
+ const handleOctobossAction = useCallback(
747
+ (action: string) => {
748
+ setContextMenu(null);
749
+ const result = onOctobossAction?.(action);
750
+ if (result && typeof result.then === "function") {
751
+ void result.then((agentId) => {
752
+ if (agentId) setPendingOpenAgentId(agentId);
753
+ });
754
+ }
755
+ },
756
+ [onOctobossAction],
757
+ );
758
+
759
+ const handleTentacleAction = useCallback(
760
+ (tentacleId: string, action: string) => {
761
+ setContextMenu(null);
762
+ const result = onTentacleAction?.(tentacleId, action);
763
+ if (result && typeof result.then === "function") {
764
+ void result.then((agentId) => {
765
+ if (agentId) setPendingOpenAgentId(agentId);
766
+ });
767
+ }
768
+ },
769
+ [onTentacleAction],
770
+ );
771
+
772
+ // Auto-open terminal for newly created agent once it appears in the graph
773
+ useEffect(() => {
774
+ if (!pendingOpenAgentId) return;
775
+ const nodeId = buildActiveSessionNodeId(pendingOpenAgentId);
776
+ const node = resolveActiveSessionNode(pendingOpenAgentId);
777
+ if (!node) return;
778
+ setPendingOpenAgentId(null);
779
+ setSelectedNodeId(nodeId);
780
+ setOpenTerminals((prev) => {
781
+ const next = new Map(prev);
782
+ next.set(nodeId, { ...node });
783
+ return next;
784
+ });
785
+ }, [pendingOpenAgentId, resolveActiveSessionNode]);
786
+
787
+ useEffect(() => {
788
+ if (!isUiStateHydrated || !recentlyCreatedTerminal) {
789
+ return;
790
+ }
791
+ if (lastHandledCreatedTerminalIdRef.current === recentlyCreatedTerminal.terminalId) {
792
+ return;
793
+ }
794
+ if (!recentlyCreatedTerminal.parentTerminalId) {
795
+ lastHandledCreatedTerminalIdRef.current = recentlyCreatedTerminal.terminalId;
796
+ return;
797
+ }
798
+ if (!openTerminals.has(buildActiveSessionNodeId(recentlyCreatedTerminal.parentTerminalId))) {
799
+ lastHandledCreatedTerminalIdRef.current = recentlyCreatedTerminal.terminalId;
800
+ return;
801
+ }
802
+
803
+ const nodeId = buildActiveSessionNodeId(recentlyCreatedTerminal.terminalId);
804
+ const node = resolveActiveSessionNode(recentlyCreatedTerminal.terminalId);
805
+ if (!node) {
806
+ return;
807
+ }
808
+
809
+ lastHandledCreatedTerminalIdRef.current = recentlyCreatedTerminal.terminalId;
810
+ setSelectedNodeId(nodeId);
811
+ setOpenTerminals((prev) => {
812
+ const next = new Map(prev);
813
+ next.set(nodeId, { ...node });
814
+ return next;
815
+ });
816
+ }, [isUiStateHydrated, openTerminals, recentlyCreatedTerminal, resolveActiveSessionNode]);
817
+
818
+ useEffect(() => {
819
+ if (!selectedNodeId) {
820
+ lastFocusedPanelIdRef.current = null;
821
+ return;
822
+ }
823
+ if (!openTerminals.has(selectedNodeId) && !openTentacles.has(selectedNodeId)) {
824
+ if (lastFocusedPanelIdRef.current === selectedNodeId) {
825
+ lastFocusedPanelIdRef.current = null;
826
+ }
827
+ return;
828
+ }
829
+ if (lastFocusedPanelIdRef.current === selectedNodeId) {
830
+ return;
831
+ }
832
+
833
+ const panel = panelRefs.current.get(selectedNodeId);
834
+ if (!panel) {
835
+ return;
836
+ }
837
+
838
+ lastFocusedPanelIdRef.current = selectedNodeId;
839
+ const rafId = window.requestAnimationFrame(() => {
840
+ panel.scrollIntoView({
841
+ behavior: "smooth",
842
+ block: "nearest",
843
+ inline: "nearest",
844
+ });
845
+ panel.focus({ preventScroll: true });
846
+ });
847
+
848
+ return () => {
849
+ window.cancelAnimationFrame(rafId);
850
+ };
851
+ }, [selectedNodeId, openTerminals, openTentacles]);
852
+
853
+ // Separate tentacle and session nodes for render order
854
+ const tentacleNodes = simulatedNodes.filter(
855
+ (n) => n.type === "tentacle" || n.type === "octoboss",
856
+ );
857
+ const sessionNodes = simulatedNodes.filter((n) => {
858
+ if (n.type === "tentacle" || n.type === "octoboss") return false;
859
+ if (hideIdleTerminals && n.type === "inactive-session") return false;
860
+ if (
861
+ hideIdleTerminals &&
862
+ n.type === "active-session" &&
863
+ (n.agentState === "idle" || n.hasUserPrompt === false)
864
+ )
865
+ return false;
866
+ return true;
867
+ });
868
+
869
+ const handleFitView = useCallback(() => {
870
+ fitAll(simulatedNodes);
871
+ }, [fitAll, simulatedNodes]);
872
+
873
+ const handleRefresh = useCallback(() => {
874
+ if (onRefreshColumns) {
875
+ const result = onRefreshColumns();
876
+ if (result && typeof result.then === "function") {
877
+ void result.finally(() => {
878
+ refreshGraphData();
879
+ });
880
+ return;
881
+ }
882
+ }
883
+ refreshGraphData();
884
+ }, [onRefreshColumns, refreshGraphData]);
885
+
886
+ const waitingNodes = simulatedNodes.filter(
887
+ (n) =>
888
+ n.type === "active-session" &&
889
+ (n.agentRuntimeState === "waiting_for_permission" ||
890
+ n.agentRuntimeState === "waiting_for_user"),
891
+ );
892
+
893
+ const sessionEdges = edges
894
+ .map((edge) => {
895
+ const source = nodesById.get(edge.source);
896
+ const target = nodesById.get(edge.target);
897
+ if (!source || !target) {
898
+ return null;
899
+ }
900
+ if (source.type !== "active-session" || target.type !== "active-session") {
901
+ return null;
902
+ }
903
+ if (
904
+ hideIdleTerminals &&
905
+ (source.agentState === "idle" ||
906
+ source.hasUserPrompt === false ||
907
+ target.agentState === "idle" ||
908
+ target.hasUserPrompt === false)
909
+ ) {
910
+ return null;
911
+ }
912
+ return { source, target };
913
+ })
914
+ .filter((edge): edge is { source: GraphNode; target: GraphNode } => edge !== null);
915
+
916
+ const sessionEdgesBySource = new Map<string, { source: GraphNode; target: GraphNode }[]>();
917
+ for (const edge of sessionEdges) {
918
+ const group = sessionEdgesBySource.get(edge.source.id);
919
+ if (group) {
920
+ group.push(edge);
921
+ } else {
922
+ sessionEdgesBySource.set(edge.source.id, [edge]);
923
+ }
924
+ }
925
+
926
+ for (const group of sessionEdgesBySource.values()) {
927
+ group.sort((left, right) => {
928
+ const leftAngle = Math.atan2(left.target.y - left.source.y, left.target.x - left.source.x);
929
+ const rightAngle = Math.atan2(
930
+ right.target.y - right.source.y,
931
+ right.target.x - right.source.x,
932
+ );
933
+ return leftAngle - rightAngle;
934
+ });
935
+ }
936
+
937
+ const hasPanels = isHydratingTerminals || openTerminals.size > 0 || openTentacles.size > 0;
938
+ const terminalLayoutVersion = useMemo(() => {
939
+ const openIds = Array.from(openTerminals.keys()).join("|");
940
+ return `${openIds}::${terminalsPanelWidth ?? "auto"}`;
941
+ }, [openTerminals, terminalsPanelWidth]);
942
+ const handleLaunchWorkspaceSetupPlanner = useCallback(async () => {
943
+ if (!onLaunchWorkspaceSetupPlanner) {
944
+ return;
945
+ }
946
+
947
+ setIsLaunchingWorkspaceSetupPlanner(true);
948
+ try {
949
+ const agentId = await onLaunchWorkspaceSetupPlanner();
950
+ if (agentId) {
951
+ setPendingOpenAgentId(agentId);
952
+ }
953
+ } finally {
954
+ setIsLaunchingWorkspaceSetupPlanner(false);
955
+ }
956
+ }, [onLaunchWorkspaceSetupPlanner]);
957
+
958
+ return (
959
+ <section ref={containerRef} className="canvas-view" aria-label="Canvas graph view">
960
+ <div className={`canvas-graph-panel${hasPanels ? " canvas-graph-panel--split" : ""}`}>
961
+ <svg
962
+ aria-label="Canvas graph"
963
+ ref={svgRef}
964
+ className={`canvas-svg${isPanning || dragNodeId ? " canvas-svg--panning" : ""}`}
965
+ onWheel={handleWheel}
966
+ onPointerDown={handleCanvasPointerDown}
967
+ onPointerMove={handleSvgPointerMove}
968
+ onPointerUp={handleSvgPointerUp}
969
+ onClick={handleSvgClick}
970
+ onKeyDown={(e) => {
971
+ if (e.key === "Escape") {
972
+ e.preventDefault();
973
+ setContextMenu(null);
974
+ setSelectedNodeId(null);
975
+ return;
976
+ }
977
+ if ((e.key === "Enter" || e.key === " ") && e.target === e.currentTarget) {
978
+ e.preventDefault();
979
+ setSelectedNodeId(null);
980
+ }
981
+ }}
982
+ >
983
+ <title>Canvas graph</title>
984
+ <g
985
+ transform={`translate(${transform.translateX}, ${transform.translateY}) scale(${transform.scale})`}
986
+ >
987
+ {Array.from(sessionEdgesBySource.entries()).flatMap(([sourceId, group]) =>
988
+ group.map(({ source, target }, index) => {
989
+ const active = selectedNodeId === source.id || selectedNodeId === target.id;
990
+ const selectedColor = selectedNodeId
991
+ ? (nodesById.get(selectedNodeId)?.color ?? null)
992
+ : null;
993
+ const path = buildCanvasEdgePath(source, target, index, group.length);
994
+
995
+ return (
996
+ <g key={`${sourceId}->${target.id}`}>
997
+ <path
998
+ className="canvas-edge"
999
+ d={path}
1000
+ fill="none"
1001
+ stroke={active ? (selectedColor ?? source.color) : "#C0C0C0"}
1002
+ strokeWidth={active ? 2 : 1.5}
1003
+ strokeOpacity={1}
1004
+ />
1005
+ {isEdgeActivityVisible(target)
1006
+ ? renderEdgeActivityDots(
1007
+ path,
1008
+ active ? (selectedColor ?? source.color) : source.color,
1009
+ `${sourceId}->${target.id}`,
1010
+ )
1011
+ : null}
1012
+ </g>
1013
+ );
1014
+ }),
1015
+ )}
1016
+
1017
+ {/* Render tentacle nodes (with arms) first */}
1018
+ {tentacleNodes.map((node) => {
1019
+ const connected = edges
1020
+ .filter((e) => e.source === node.id)
1021
+ .map((e) => nodesById.get(e.target))
1022
+ .filter((n): n is GraphNode => {
1023
+ if (!n) return false;
1024
+ if (hideIdleTerminals && n.type === "inactive-session") return false;
1025
+ if (
1026
+ hideIdleTerminals &&
1027
+ n.type === "active-session" &&
1028
+ (n.agentState === "idle" || n.hasUserPrompt === false)
1029
+ )
1030
+ return false;
1031
+ return true;
1032
+ });
1033
+
1034
+ const selectedColor = selectedNodeId
1035
+ ? (nodesById.get(selectedNodeId)?.color ?? null)
1036
+ : null;
1037
+
1038
+ return (
1039
+ <OctopusNode
1040
+ key={node.id}
1041
+ node={node}
1042
+ connectedNodes={connected}
1043
+ isSelected={selectedNodeId === node.id}
1044
+ selectedNodeId={selectedNodeId}
1045
+ selectedNodeColor={selectedColor}
1046
+ onPointerDown={handleNodePointerDown}
1047
+ onClick={handleNodeClick}
1048
+ />
1049
+ );
1050
+ })}
1051
+
1052
+ {/* Render session nodes on top */}
1053
+ {sessionNodes.map((node) => (
1054
+ <SessionNode
1055
+ key={node.id}
1056
+ node={node}
1057
+ isSelected={selectedNodeId === node.id}
1058
+ onPointerDown={handleNodePointerDown}
1059
+ onClick={handleNodeClick}
1060
+ />
1061
+ ))}
1062
+ </g>
1063
+ </svg>
1064
+
1065
+ {/* Canvas toolbar — top-left action buttons */}
1066
+ <div className="canvas-toolbar" role="toolbar" aria-label="Canvas actions">
1067
+ <button
1068
+ type="button"
1069
+ className="canvas-toolbar-btn"
1070
+ onClick={() => {
1071
+ const result = onCreateTerminal?.();
1072
+ if (result && typeof result.then === "function") {
1073
+ void result.then((agentId) => {
1074
+ if (agentId) setPendingOpenAgentId(agentId);
1075
+ });
1076
+ }
1077
+ }}
1078
+ >
1079
+ <span className="canvas-toolbar-icon">
1080
+ <TerminalIcon size={14} />
1081
+ </span>
1082
+ <span className="canvas-toolbar-label">Terminal</span>
1083
+ </button>
1084
+ <button
1085
+ type="button"
1086
+ className="canvas-toolbar-btn"
1087
+ onClick={() => {
1088
+ const result = onCreateWorktreeTerminal?.();
1089
+ if (result && typeof result.then === "function") {
1090
+ void result.then((agentId) => {
1091
+ if (agentId) setPendingOpenAgentId(agentId);
1092
+ });
1093
+ }
1094
+ }}
1095
+ >
1096
+ <span className="canvas-toolbar-icon">
1097
+ <GitBranch size={14} />
1098
+ </span>
1099
+ <span className="canvas-toolbar-label">Worktree</span>
1100
+ </button>
1101
+ <button type="button" className="canvas-toolbar-btn" onClick={onCreateTentacle}>
1102
+ <span className="canvas-toolbar-icon">
1103
+ <Hexagon size={14} />
1104
+ </span>
1105
+ <span className="canvas-toolbar-label">Tentacle</span>
1106
+ </button>
1107
+ <div className="canvas-toolbar-separator" />
1108
+ <button type="button" className="canvas-toolbar-btn" onClick={handleFitView}>
1109
+ <span className="canvas-toolbar-icon">
1110
+ <Maximize size={14} />
1111
+ </span>
1112
+ <span className="canvas-toolbar-label">Fit</span>
1113
+ </button>
1114
+ <button type="button" className="canvas-toolbar-btn" onClick={handleRefresh}>
1115
+ <span className="canvas-toolbar-icon">
1116
+ <RefreshCw size={14} />
1117
+ </span>
1118
+ <span className="canvas-toolbar-label">Refresh</span>
1119
+ </button>
1120
+ <div className="canvas-toolbar-separator" />
1121
+ <button
1122
+ type="button"
1123
+ className={`canvas-toolbar-btn${hideIdleTerminals ? " canvas-toolbar-btn--active" : ""}`}
1124
+ onClick={() => setHideIdleTerminals((prev) => !prev)}
1125
+ >
1126
+ <span className="canvas-toolbar-icon">
1127
+ {hideIdleTerminals ? <Play size={14} /> : <Pause size={14} />}
1128
+ </span>
1129
+ <span className="canvas-toolbar-label">
1130
+ {hideIdleTerminals ? "Show Idle" : "Hide Idle"}
1131
+ </span>
1132
+ </button>
1133
+ <div className="canvas-toolbar-separator" />
1134
+ <button
1135
+ type="button"
1136
+ className="canvas-toolbar-btn canvas-toolbar-btn--danger"
1137
+ onClick={() => setIsDeleteAllDialogOpen(true)}
1138
+ >
1139
+ <span className="canvas-toolbar-icon">
1140
+ <Trash2 size={14} />
1141
+ </span>
1142
+ <span className="canvas-toolbar-label">Delete All</span>
1143
+ </button>
1144
+ </div>
1145
+
1146
+ {/* Waiting notifications — compact bars below the toolbar */}
1147
+ {waitingNodes.length > 0 && (
1148
+ <div className="canvas-waiting-list">
1149
+ {waitingNodes.map((node) => {
1150
+ const nameRaw = node.label;
1151
+ const name = nameRaw.length > 20 ? `${nameRaw.slice(0, 20)}…` : nameRaw;
1152
+ const prefix =
1153
+ node.agentRuntimeState === "waiting_for_permission"
1154
+ ? `${node.waitingToolName ?? "Permission"}: `
1155
+ : "Waiting: ";
1156
+ return (
1157
+ <button
1158
+ key={node.id}
1159
+ type="button"
1160
+ className="canvas-waiting-bar"
1161
+ onClick={() => handleNodeClick(node.id)}
1162
+ >
1163
+ <span className="canvas-waiting-bar-name">
1164
+ <span className="canvas-waiting-bar-prefix">{prefix}</span>
1165
+ {name}
1166
+ </span>
1167
+ </button>
1168
+ );
1169
+ })}
1170
+ </div>
1171
+ )}
1172
+
1173
+ {shouldShowWorkspaceSetupCard && (
1174
+ <div className="canvas-setup-overlay">
1175
+ <WorkspaceSetupCard
1176
+ workspaceSetup={workspaceSetup}
1177
+ isLoading={isWorkspaceSetupLoading}
1178
+ error={workspaceSetupError}
1179
+ onRunStep={(stepId) => {
1180
+ void onRunWorkspaceSetupStep?.(stepId);
1181
+ }}
1182
+ onLaunchClaudeCode={() => {
1183
+ void handleLaunchWorkspaceSetupPlanner();
1184
+ }}
1185
+ isLaunchingAgent={isLaunchingWorkspaceSetupPlanner}
1186
+ isRunningStepId={runningWorkspaceSetupStepId}
1187
+ />
1188
+ </div>
1189
+ )}
1190
+ </div>
1191
+
1192
+ {hasPanels && (
1193
+ <>
1194
+ <div
1195
+ className="canvas-panel-divider"
1196
+ role="separator"
1197
+ aria-orientation="vertical"
1198
+ tabIndex={0}
1199
+ onPointerDown={handleDividerPointerDown}
1200
+ onPointerMove={handleDividerPointerMove}
1201
+ onPointerUp={handleDividerPointerUp}
1202
+ />
1203
+ <div
1204
+ ref={terminalsPanelRef}
1205
+ className="canvas-terminals-panel"
1206
+ style={
1207
+ terminalsPanelWidth != null ? { flex: `0 0 ${terminalsPanelWidth}px` } : undefined
1208
+ }
1209
+ >
1210
+ {Array.from(openTentacles.entries()).map(([nodeId, node]) => (
1211
+ <CanvasTentaclePanel
1212
+ key={nodeId}
1213
+ node={node}
1214
+ isFocused={selectedNodeId === nodeId}
1215
+ panelRef={setPanelRef(nodeId)}
1216
+ tentacle={tentacleById.get(node.tentacleId) ?? null}
1217
+ sessions={sessionsByTentacleId.get(node.tentacleId) ?? []}
1218
+ onClose={() => handleCloseTentacle(nodeId)}
1219
+ onFocus={() => setSelectedNodeId(nodeId)}
1220
+ onCreateAgent={(tentacleId) => {
1221
+ handleCreateAgent(tentacleId);
1222
+ }}
1223
+ onSolveTodoItem={(tentacleId, itemIndex) => {
1224
+ void onSolveTodoItem?.(tentacleId, itemIndex);
1225
+ }}
1226
+ onSpawnSwarm={(tentacleId, workspaceMode) => {
1227
+ handleSpawnSwarm(tentacleId, workspaceMode);
1228
+ }}
1229
+ onNavigateToConversation={onNavigateToConversation}
1230
+ onRefreshTentacleData={refreshDeckTentacles}
1231
+ />
1232
+ ))}
1233
+ {isHydratingTerminals && openTerminals.size === 0 && (
1234
+ <div className="canvas-terminal-skeleton">
1235
+ <div className="canvas-terminal-skeleton__header" />
1236
+ <div className="canvas-terminal-skeleton__body">
1237
+ <div className="canvas-terminal-skeleton__line" style={{ width: "60%" }} />
1238
+ <div className="canvas-terminal-skeleton__line" style={{ width: "80%" }} />
1239
+ <div className="canvas-terminal-skeleton__line" style={{ width: "45%" }} />
1240
+ </div>
1241
+ </div>
1242
+ )}
1243
+ {Array.from(openTerminals.entries()).map(([nodeId, node]) => (
1244
+ <CanvasTerminalColumn
1245
+ key={nodeId}
1246
+ node={node}
1247
+ terminals={columns}
1248
+ layoutVersion={terminalLayoutVersion}
1249
+ isFocused={selectedNodeId === nodeId}
1250
+ panelRef={setPanelRef(nodeId)}
1251
+ onClose={() => handleCloseTerminal(nodeId)}
1252
+ onFocus={() => setSelectedNodeId(nodeId)}
1253
+ onTerminalRenamed={onTerminalRenamed}
1254
+ onTerminalActivity={onTerminalActivity}
1255
+ />
1256
+ ))}
1257
+ </div>
1258
+ </>
1259
+ )}
1260
+
1261
+ {/* Context menu */}
1262
+ {contextMenu && (
1263
+ <>
1264
+ <div
1265
+ aria-label="Close canvas context menu"
1266
+ className="canvas-context-menu-backdrop"
1267
+ onClick={() => setContextMenu(null)}
1268
+ onContextMenu={(e) => {
1269
+ e.preventDefault();
1270
+ e.stopPropagation();
1271
+ // Close current menu, then re-derive what's under the cursor on the SVG
1272
+ setContextMenu(null);
1273
+ // Use rAF so the backdrop is removed before we probe elementFromPoint
1274
+ requestAnimationFrame(() => {
1275
+ const under = document.elementFromPoint(e.clientX, e.clientY);
1276
+ if (under) {
1277
+ under.dispatchEvent(
1278
+ new MouseEvent("contextmenu", {
1279
+ bubbles: true,
1280
+ clientX: e.clientX,
1281
+ clientY: e.clientY,
1282
+ }),
1283
+ );
1284
+ }
1285
+ });
1286
+ }}
1287
+ onKeyDown={(e) => {
1288
+ if (e.key !== "Enter" && e.key !== " " && e.key !== "Escape") return;
1289
+ e.preventDefault();
1290
+ setContextMenu(null);
1291
+ }}
1292
+ role="button"
1293
+ tabIndex={0}
1294
+ />
1295
+ <div
1296
+ className="canvas-context-menu"
1297
+ style={{ left: `${contextMenu.x}px`, top: `${contextMenu.y}px` }}
1298
+ onContextMenu={(e) => {
1299
+ e.preventDefault();
1300
+ e.stopPropagation();
1301
+ setContextMenu(null);
1302
+ requestAnimationFrame(() => {
1303
+ const under = document.elementFromPoint(e.clientX, e.clientY);
1304
+ if (under) {
1305
+ under.dispatchEvent(
1306
+ new MouseEvent("contextmenu", {
1307
+ bubbles: true,
1308
+ clientX: e.clientX,
1309
+ clientY: e.clientY,
1310
+ }),
1311
+ );
1312
+ }
1313
+ });
1314
+ }}
1315
+ >
1316
+ {contextMenu.kind === "canvas" && (
1317
+ <>
1318
+ <button
1319
+ type="button"
1320
+ className="canvas-context-menu-item"
1321
+ onClick={() => {
1322
+ setContextMenu(null);
1323
+ onCreateTentacle?.();
1324
+ }}
1325
+ >
1326
+ <span className="canvas-context-menu-icon">
1327
+ <Hexagon size={14} />
1328
+ </span>
1329
+ New Tentacle
1330
+ </button>
1331
+ <button
1332
+ type="button"
1333
+ className="canvas-context-menu-item"
1334
+ onClick={() => {
1335
+ setContextMenu(null);
1336
+ const result = onCreateTerminal?.();
1337
+ if (result && typeof result.then === "function") {
1338
+ void result.then((agentId) => {
1339
+ if (agentId) setPendingOpenAgentId(agentId);
1340
+ });
1341
+ }
1342
+ }}
1343
+ >
1344
+ <span className="canvas-context-menu-icon">
1345
+ <TerminalIcon size={14} />
1346
+ </span>
1347
+ New Terminal
1348
+ </button>
1349
+ <button
1350
+ type="button"
1351
+ className="canvas-context-menu-item"
1352
+ onClick={() => {
1353
+ setContextMenu(null);
1354
+ const result = onCreateWorktreeTerminal?.();
1355
+ if (result && typeof result.then === "function") {
1356
+ void result.then((agentId) => {
1357
+ if (agentId) setPendingOpenAgentId(agentId);
1358
+ });
1359
+ }
1360
+ }}
1361
+ >
1362
+ <span className="canvas-context-menu-icon">
1363
+ <GitBranch size={14} />
1364
+ </span>
1365
+ New Worktree Terminal
1366
+ </button>
1367
+ </>
1368
+ )}
1369
+ {contextMenu.kind === "tentacle" && (
1370
+ <>
1371
+ <button
1372
+ type="button"
1373
+ className="canvas-context-menu-item"
1374
+ onClick={() => handleCreateAgent(contextMenu.tentacleId)}
1375
+ >
1376
+ <span className="canvas-context-menu-icon">
1377
+ <TerminalIcon size={14} />
1378
+ </span>
1379
+ Create new agent
1380
+ </button>
1381
+ <button
1382
+ type="button"
1383
+ className="canvas-context-menu-item"
1384
+ onClick={() => {
1385
+ setContextMenu(null);
1386
+ const result = onCreateWorktreeTerminal?.();
1387
+ if (result && typeof result.then === "function") {
1388
+ void result.then((agentId) => {
1389
+ if (agentId) setPendingOpenAgentId(agentId);
1390
+ });
1391
+ }
1392
+ }}
1393
+ >
1394
+ <span className="canvas-context-menu-icon">
1395
+ <GitBranch size={14} />
1396
+ </span>
1397
+ New Worktree Terminal
1398
+ </button>
1399
+ <button
1400
+ type="button"
1401
+ className="canvas-context-menu-item"
1402
+ onClick={() =>
1403
+ handleTentacleAction(contextMenu.tentacleId, "tentacle-reorganize-todos")
1404
+ }
1405
+ >
1406
+ <span className="canvas-context-menu-icon">
1407
+ <ListTodo size={14} />
1408
+ </span>
1409
+ Update To-Do List
1410
+ </button>
1411
+ <button
1412
+ type="button"
1413
+ className="canvas-context-menu-item"
1414
+ onClick={() =>
1415
+ handleTentacleAction(contextMenu.tentacleId, "tentacle-update-tentacle")
1416
+ }
1417
+ >
1418
+ <span className="canvas-context-menu-icon">
1419
+ <Hexagon size={14} />
1420
+ </span>
1421
+ Update Tentacle
1422
+ </button>
1423
+ <button
1424
+ type="button"
1425
+ className="canvas-context-menu-item"
1426
+ onClick={() => handleSpawnSwarm(contextMenu.tentacleId, "worktree")}
1427
+ >
1428
+ <span className="canvas-context-menu-icon">
1429
+ <Layers size={14} />
1430
+ </span>
1431
+ Spawn Swarm (Worktrees)
1432
+ </button>
1433
+ <button
1434
+ type="button"
1435
+ className="canvas-context-menu-item"
1436
+ onClick={() => handleSpawnSwarm(contextMenu.tentacleId, "shared")}
1437
+ >
1438
+ <span className="canvas-context-menu-icon">
1439
+ <Layers size={14} />
1440
+ </span>
1441
+ Spawn Swarm (Normal)
1442
+ </button>
1443
+ </>
1444
+ )}
1445
+ {contextMenu.kind === "octoboss" && (
1446
+ <>
1447
+ <button
1448
+ type="button"
1449
+ className="canvas-context-menu-item"
1450
+ onClick={() => handleOctobossAction("octoboss-reorganize-todos")}
1451
+ >
1452
+ <span className="canvas-context-menu-icon">
1453
+ <ListTodo size={14} />
1454
+ </span>
1455
+ Reorganize To-Do's
1456
+ </button>
1457
+ <button
1458
+ type="button"
1459
+ className="canvas-context-menu-item"
1460
+ onClick={() => handleOctobossAction("octoboss-reorganize-tentacles")}
1461
+ >
1462
+ <span className="canvas-context-menu-icon">
1463
+ <Hexagon size={14} />
1464
+ </span>
1465
+ Reorganize Tentacles
1466
+ </button>
1467
+ <button
1468
+ type="button"
1469
+ className="canvas-context-menu-item"
1470
+ onClick={() => handleOctobossAction("octoboss-clean-contexts")}
1471
+ >
1472
+ <span className="canvas-context-menu-icon">
1473
+ <Sparkles size={14} />
1474
+ </span>
1475
+ Clean Tentacle Contexts
1476
+ </button>
1477
+ </>
1478
+ )}
1479
+ {contextMenu.kind === "active-session" && (
1480
+ <button
1481
+ type="button"
1482
+ className="canvas-context-menu-item canvas-context-menu-item--danger"
1483
+ onClick={() => {
1484
+ onDeleteActiveSession?.(
1485
+ contextMenu.sessionId,
1486
+ contextMenu.label,
1487
+ contextMenu.workspaceMode,
1488
+ );
1489
+ setContextMenu(null);
1490
+ }}
1491
+ >
1492
+ <span className="canvas-context-menu-icon">
1493
+ <Trash2 size={14} />
1494
+ </span>
1495
+ Delete
1496
+ </button>
1497
+ )}
1498
+ </div>
1499
+ </>
1500
+ )}
1501
+
1502
+ {pendingDeleteTerminal && onCancelDelete && onConfirmDelete && (
1503
+ <div className="canvas-delete-dialog">
1504
+ <DeleteTentacleDialog
1505
+ pendingDeleteTerminal={pendingDeleteTerminal}
1506
+ isDeletingTerminalId={isDeletingTerminalId ?? null}
1507
+ onCancel={onCancelDelete}
1508
+ onConfirmDelete={onConfirmDelete}
1509
+ />
1510
+ </div>
1511
+ )}
1512
+
1513
+ {isDeleteAllDialogOpen && (
1514
+ <div className="canvas-delete-dialog">
1515
+ <DeleteAllTerminalsDialog
1516
+ columns={columns}
1517
+ nodes={nodes}
1518
+ onCancel={() => setIsDeleteAllDialogOpen(false)}
1519
+ onDeleted={({ hadFailures }) => {
1520
+ if (!hadFailures) {
1521
+ setIsDeleteAllDialogOpen(false);
1522
+ }
1523
+ setOpenTerminals(new Map());
1524
+ void onRefreshColumns?.();
1525
+ refreshGraphData();
1526
+ }}
1527
+ />
1528
+ </div>
1529
+ )}
1530
+ </section>
1531
+ );
1532
+ };