agent-relay 2.0.21 → 2.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/relay-pty-linux-arm64 +0 -0
- package/dist/src/cli/index.d.ts +3 -3
- package/dist/src/cli/index.js +31 -100
- package/package.json +22 -29
- package/packages/api-types/package.json +1 -1
- package/packages/bridge/package.json +8 -8
- package/packages/cli-tester/package.json +1 -1
- package/packages/cloud/dist/server.js +25 -4
- package/packages/cloud/package.json +6 -6
- package/packages/config/package.json +2 -2
- package/packages/continuity/package.json +1 -1
- package/packages/daemon/dist/orchestrator.js +21 -1
- package/packages/daemon/dist/router.d.ts +5 -0
- package/packages/daemon/dist/router.js +31 -0
- package/packages/daemon/dist/server.d.ts +5 -0
- package/packages/daemon/dist/server.js +131 -1
- package/packages/daemon/package.json +12 -12
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/dist/client.d.ts +15 -0
- package/packages/mcp/dist/client.js +9 -0
- package/packages/mcp/dist/server.js +13 -1
- package/packages/mcp/dist/tools/index.d.ts +2 -0
- package/packages/mcp/dist/tools/index.js +2 -0
- package/packages/mcp/dist/tools/relay-connected.d.ts +17 -0
- package/packages/mcp/dist/tools/relay-connected.js +40 -0
- package/packages/mcp/dist/tools/relay-remove-agent.d.ts +20 -0
- package/packages/mcp/dist/tools/relay-remove-agent.js +50 -0
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/dist/types.d.ts +46 -1
- package/packages/protocol/package.json +1 -1
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/dist/client.d.ts +22 -1
- package/packages/sdk/dist/client.js +31 -0
- package/packages/sdk/dist/protocol/index.d.ts +1 -1
- package/packages/sdk/dist/protocol/types.d.ts +35 -1
- package/packages/sdk/package.json +2 -2
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/dist/adapter.d.ts +4 -0
- package/packages/storage/dist/sqlite-adapter.d.ts +10 -0
- package/packages/storage/dist/sqlite-adapter.js +26 -0
- package/packages/storage/package.json +2 -2
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/dist/update-checker.js +4 -0
- package/packages/utils/package.json +1 -1
- package/packages/wrapper/package.json +6 -6
- package/deploy/workspace/codex.config.toml +0 -20
- package/deploy/workspace/entrypoint-browser.sh +0 -118
- package/deploy/workspace/entrypoint.sh +0 -612
- package/deploy/workspace/gh-credential-relay +0 -90
- package/deploy/workspace/gh-relay +0 -156
- package/deploy/workspace/git-credential-relay +0 -330
- package/deploy/workspace/git-credential-relay.test.sh +0 -230
- package/dist/dashboard/out/404.html +0 -1
- package/dist/dashboard/out/_next/static/7MZPqYkVGw3EGzVBkVmY9/_buildManifest.js +0 -1
- package/dist/dashboard/out/_next/static/7MZPqYkVGw3EGzVBkVmY9/_ssgManifest.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/116-a883fca163f3a5bc.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/117-c8afed19e821a35d.js +0 -2
- package/dist/dashboard/out/_next/static/chunks/282-980c2eb8fff20123.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/320-a6304232cd0ee2ce.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/532-bace199897eeab37.js +0 -9
- package/dist/dashboard/out/_next/static/chunks/631-16b905e5920f9b59.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/648-acb2ff9f77cbfbd3.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/766-2aea80818f7eb0d8.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/83-26d2bde54616ee90.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/847-f1f467060f32afff.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/891-5cb1513eeb97a891.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/_not-found/page-60501fddbafba9dc.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/onboarding/page-9914652442f7e4fb.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/app/page-366fb7c078d4e9e0.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/cloud/link/page-fa1d5842aa90e8a6.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/complete-profile/page-dd64bbdf66b639cd.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/connect-repos/page-113060009ef35bc2.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/history/page-9965d2483011b846.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/layout-6b91e33784c20610.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/login/page-435eceb0073be027.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/metrics/page-1e37ef8e73940b40.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/page-8119d4246743574e.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/pricing/page-9db3ebdfa567a7c9.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/page-ecb16ffd3b36262b.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/providers/setup/[provider]/page-4dbe33f0f7691b7c.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/app/signup/page-c7a0a28341365ae0.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/e868780c-48e5f147c90a3a41.js +0 -18
- package/dist/dashboard/out/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/framework-f66176bb897dc684.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/main-311c3db74dcfadb7.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/main-app-fdbeb09028f57c9f.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/pages/_app-72b849fbd24ac258.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/pages/_error-7ba65e1336b92748.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/dist/dashboard/out/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +0 -1
- package/dist/dashboard/out/_next/static/css/4034f236dd1a3178.css +0 -1
- package/dist/dashboard/out/_next/static/css/6892f8422896ef7a.css +0 -1
- package/dist/dashboard/out/alt-logos/agent-relay-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/agent-relay-logo.svg +0 -45
- package/dist/dashboard/out/alt-logos/logo.svg +0 -38
- package/dist/dashboard/out/alt-logos/monogram-logo-128.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-256.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-32.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-512.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo-64.png +0 -0
- package/dist/dashboard/out/alt-logos/monogram-logo.svg +0 -38
- package/dist/dashboard/out/app/onboarding.html +0 -1
- package/dist/dashboard/out/app/onboarding.txt +0 -7
- package/dist/dashboard/out/app.html +0 -1
- package/dist/dashboard/out/app.txt +0 -7
- package/dist/dashboard/out/apple-icon.png +0 -0
- package/dist/dashboard/out/cloud/link.html +0 -1
- package/dist/dashboard/out/cloud/link.txt +0 -7
- package/dist/dashboard/out/complete-profile.html +0 -5
- package/dist/dashboard/out/complete-profile.txt +0 -7
- package/dist/dashboard/out/connect-repos.html +0 -1
- package/dist/dashboard/out/connect-repos.txt +0 -7
- package/dist/dashboard/out/history.html +0 -1
- package/dist/dashboard/out/history.txt +0 -7
- package/dist/dashboard/out/index.html +0 -1
- package/dist/dashboard/out/index.txt +0 -7
- package/dist/dashboard/out/login.html +0 -5
- package/dist/dashboard/out/login.txt +0 -7
- package/dist/dashboard/out/metrics.html +0 -1
- package/dist/dashboard/out/metrics.txt +0 -7
- package/dist/dashboard/out/pricing.html +0 -13
- package/dist/dashboard/out/pricing.txt +0 -7
- package/dist/dashboard/out/providers/setup/claude.html +0 -1
- package/dist/dashboard/out/providers/setup/claude.txt +0 -8
- package/dist/dashboard/out/providers/setup/codex.html +0 -1
- package/dist/dashboard/out/providers/setup/codex.txt +0 -8
- package/dist/dashboard/out/providers/setup/cursor.html +0 -1
- package/dist/dashboard/out/providers/setup/cursor.txt +0 -8
- package/dist/dashboard/out/providers.html +0 -1
- package/dist/dashboard/out/providers.txt +0 -7
- package/dist/dashboard/out/signup.html +0 -6
- package/dist/dashboard/out/signup.txt +0 -7
- package/dist/src/dashboard-server/index.d.ts +0 -8
- package/dist/src/dashboard-server/index.js +0 -8
- package/packages/dashboard/README.md +0 -48
- package/packages/dashboard/dist/health-worker-manager.d.ts +0 -62
- package/packages/dashboard/dist/health-worker-manager.js +0 -144
- package/packages/dashboard/dist/health-worker.d.ts +0 -9
- package/packages/dashboard/dist/health-worker.js +0 -79
- package/packages/dashboard/dist/index.d.ts +0 -20
- package/packages/dashboard/dist/index.js +0 -19
- package/packages/dashboard/dist/metrics.d.ts +0 -105
- package/packages/dashboard/dist/metrics.js +0 -193
- package/packages/dashboard/dist/needs-attention.d.ts +0 -24
- package/packages/dashboard/dist/needs-attention.js +0 -78
- package/packages/dashboard/dist/server.d.ts +0 -25
- package/packages/dashboard/dist/server.js +0 -5270
- package/packages/dashboard/dist/start.d.ts +0 -6
- package/packages/dashboard/dist/start.js +0 -13
- package/packages/dashboard/dist/types/threading.d.ts +0 -8
- package/packages/dashboard/dist/types/threading.js +0 -2
- package/packages/dashboard/dist/user-bridge.d.ts +0 -154
- package/packages/dashboard/dist/user-bridge.js +0 -372
- package/packages/dashboard/package.json +0 -65
- package/packages/dashboard/ui/app/app/onboarding/page.tsx +0 -394
- package/packages/dashboard/ui/app/app/page.tsx +0 -667
- package/packages/dashboard/ui/app/apple-icon.png +0 -0
- package/packages/dashboard/ui/app/cloud/link/page.tsx +0 -464
- package/packages/dashboard/ui/app/complete-profile/page.tsx +0 -204
- package/packages/dashboard/ui/app/connect-repos/page.tsx +0 -410
- package/packages/dashboard/ui/app/favicon.png +0 -0
- package/packages/dashboard/ui/app/globals.css +0 -59
- package/packages/dashboard/ui/app/history/page.tsx +0 -658
- package/packages/dashboard/ui/app/layout.tsx +0 -25
- package/packages/dashboard/ui/app/login/page.tsx +0 -424
- package/packages/dashboard/ui/app/metrics/page.tsx +0 -751
- package/packages/dashboard/ui/app/page.tsx +0 -59
- package/packages/dashboard/ui/app/pricing/page.tsx +0 -7
- package/packages/dashboard/ui/app/providers/page.tsx +0 -193
- package/packages/dashboard/ui/app/providers/setup/[provider]/ProviderSetupClient.tsx +0 -148
- package/packages/dashboard/ui/app/providers/setup/[provider]/constants.ts +0 -35
- package/packages/dashboard/ui/app/providers/setup/[provider]/page.tsx +0 -42
- package/packages/dashboard/ui/app/signup/page.tsx +0 -533
- package/packages/dashboard/ui/index.ts +0 -49
- package/packages/dashboard/ui/landing/LandingPage.tsx +0 -713
- package/packages/dashboard/ui/landing/PricingPage.tsx +0 -559
- package/packages/dashboard/ui/landing/index.ts +0 -6
- package/packages/dashboard/ui/landing/styles.css +0 -2850
- package/packages/dashboard/ui/lib/agent-merge.ts +0 -35
- package/packages/dashboard/ui/lib/api.ts +0 -1155
- package/packages/dashboard/ui/lib/cloudApi.ts +0 -877
- package/packages/dashboard/ui/lib/colors.ts +0 -218
- package/packages/dashboard/ui/lib/hierarchy.ts +0 -242
- package/packages/dashboard/ui/lib/stuckDetection.ts +0 -142
- package/packages/dashboard/ui/next-env.d.ts +0 -5
- package/packages/dashboard/ui/next.config.js +0 -41
- package/packages/dashboard/ui/package-lock.json +0 -2882
- package/packages/dashboard/ui/package.json +0 -33
- package/packages/dashboard/ui/postcss.config.js +0 -5
- package/packages/dashboard/ui/react-components/ActivityFeed.tsx +0 -216
- package/packages/dashboard/ui/react-components/AddWorkspaceModal.tsx +0 -170
- package/packages/dashboard/ui/react-components/AgentCard.tsx +0 -587
- package/packages/dashboard/ui/react-components/AgentList.tsx +0 -411
- package/packages/dashboard/ui/react-components/AgentProfilePanel.tsx +0 -564
- package/packages/dashboard/ui/react-components/App.tsx +0 -3033
- package/packages/dashboard/ui/react-components/BillingPanel.tsx +0 -922
- package/packages/dashboard/ui/react-components/BillingResult.tsx +0 -447
- package/packages/dashboard/ui/react-components/BroadcastComposer.tsx +0 -690
- package/packages/dashboard/ui/react-components/ChannelAdminPanel.tsx +0 -773
- package/packages/dashboard/ui/react-components/ChannelBrowser.tsx +0 -385
- package/packages/dashboard/ui/react-components/ChannelChat.tsx +0 -261
- package/packages/dashboard/ui/react-components/ChannelSidebar.tsx +0 -399
- package/packages/dashboard/ui/react-components/CloudSessionProvider.tsx +0 -130
- package/packages/dashboard/ui/react-components/CommandPalette.tsx +0 -815
- package/packages/dashboard/ui/react-components/ConfirmationDialog.tsx +0 -133
- package/packages/dashboard/ui/react-components/ConversationHistory.tsx +0 -518
- package/packages/dashboard/ui/react-components/CoordinatorPanel.tsx +0 -944
- package/packages/dashboard/ui/react-components/DecisionQueue.tsx +0 -717
- package/packages/dashboard/ui/react-components/DirectMessageView.tsx +0 -164
- package/packages/dashboard/ui/react-components/FileAutocomplete.tsx +0 -368
- package/packages/dashboard/ui/react-components/FleetOverview.tsx +0 -278
- package/packages/dashboard/ui/react-components/LogViewer.tsx +0 -310
- package/packages/dashboard/ui/react-components/LogViewerPanel.tsx +0 -482
- package/packages/dashboard/ui/react-components/Logo.tsx +0 -284
- package/packages/dashboard/ui/react-components/MentionAutocomplete.tsx +0 -384
- package/packages/dashboard/ui/react-components/MessageComposer.tsx +0 -457
- package/packages/dashboard/ui/react-components/MessageList.tsx +0 -649
- package/packages/dashboard/ui/react-components/MessageSenderName.tsx +0 -91
- package/packages/dashboard/ui/react-components/MessageStatusIndicator.tsx +0 -142
- package/packages/dashboard/ui/react-components/NewConversationModal.tsx +0 -400
- package/packages/dashboard/ui/react-components/NotificationToast.tsx +0 -488
- package/packages/dashboard/ui/react-components/OnlineUsersIndicator.tsx +0 -164
- package/packages/dashboard/ui/react-components/Pagination.tsx +0 -124
- package/packages/dashboard/ui/react-components/PricingPlans.tsx +0 -386
- package/packages/dashboard/ui/react-components/ProjectList.tsx +0 -625
- package/packages/dashboard/ui/react-components/ProviderAuthFlow.tsx +0 -853
- package/packages/dashboard/ui/react-components/ProviderConnectionList.tsx +0 -378
- package/packages/dashboard/ui/react-components/ProvisioningProgress.tsx +0 -730
- package/packages/dashboard/ui/react-components/RepoAccessPanel.tsx +0 -549
- package/packages/dashboard/ui/react-components/ServerCard.tsx +0 -202
- package/packages/dashboard/ui/react-components/SessionExpiredModal.tsx +0 -128
- package/packages/dashboard/ui/react-components/SpawnModal.tsx +0 -804
- package/packages/dashboard/ui/react-components/TaskAssignmentUI.tsx +0 -375
- package/packages/dashboard/ui/react-components/TerminalProviderSetup.tsx +0 -608
- package/packages/dashboard/ui/react-components/ThemeProvider.tsx +0 -325
- package/packages/dashboard/ui/react-components/ThinkingIndicator.tsx +0 -231
- package/packages/dashboard/ui/react-components/ThreadList.tsx +0 -198
- package/packages/dashboard/ui/react-components/ThreadPanel.tsx +0 -346
- package/packages/dashboard/ui/react-components/TrajectoryViewer.tsx +0 -698
- package/packages/dashboard/ui/react-components/TypingIndicator.tsx +0 -69
- package/packages/dashboard/ui/react-components/UsageBanner.tsx +0 -231
- package/packages/dashboard/ui/react-components/UserProfilePanel.tsx +0 -233
- package/packages/dashboard/ui/react-components/WorkspaceContext.tsx +0 -107
- package/packages/dashboard/ui/react-components/WorkspaceSelector.tsx +0 -234
- package/packages/dashboard/ui/react-components/WorkspaceStatusIndicator.tsx +0 -370
- package/packages/dashboard/ui/react-components/XTermInteractive.tsx +0 -510
- package/packages/dashboard/ui/react-components/XTermLogViewer.tsx +0 -719
- package/packages/dashboard/ui/react-components/channels/ChannelDialogs.tsx +0 -1411
- package/packages/dashboard/ui/react-components/channels/ChannelHeader.tsx +0 -317
- package/packages/dashboard/ui/react-components/channels/ChannelMessageList.tsx +0 -463
- package/packages/dashboard/ui/react-components/channels/ChannelViewV1.tsx +0 -146
- package/packages/dashboard/ui/react-components/channels/MessageInput.tsx +0 -288
- package/packages/dashboard/ui/react-components/channels/SearchInput.tsx +0 -172
- package/packages/dashboard/ui/react-components/channels/SearchResults.tsx +0 -336
- package/packages/dashboard/ui/react-components/channels/api.ts +0 -697
- package/packages/dashboard/ui/react-components/channels/index.ts +0 -76
- package/packages/dashboard/ui/react-components/channels/mockApi.ts +0 -344
- package/packages/dashboard/ui/react-components/channels/types.ts +0 -566
- package/packages/dashboard/ui/react-components/hooks/index.ts +0 -57
- package/packages/dashboard/ui/react-components/hooks/useAgentLogs.ts +0 -394
- package/packages/dashboard/ui/react-components/hooks/useAgents.ts +0 -127
- package/packages/dashboard/ui/react-components/hooks/useBroadcastDedup.ts +0 -86
- package/packages/dashboard/ui/react-components/hooks/useChannelAdmin.ts +0 -329
- package/packages/dashboard/ui/react-components/hooks/useChannelBrowser.ts +0 -239
- package/packages/dashboard/ui/react-components/hooks/useChannelCommands.ts +0 -138
- package/packages/dashboard/ui/react-components/hooks/useChannels.ts +0 -328
- package/packages/dashboard/ui/react-components/hooks/useDebounce.ts +0 -29
- package/packages/dashboard/ui/react-components/hooks/useDirectMessage.ts +0 -141
- package/packages/dashboard/ui/react-components/hooks/useMessages.ts +0 -309
- package/packages/dashboard/ui/react-components/hooks/useOrchestrator.ts +0 -364
- package/packages/dashboard/ui/react-components/hooks/usePinnedAgents.ts +0 -140
- package/packages/dashboard/ui/react-components/hooks/usePresence.ts +0 -340
- package/packages/dashboard/ui/react-components/hooks/useRecentRepos.ts +0 -130
- package/packages/dashboard/ui/react-components/hooks/useSession.ts +0 -209
- package/packages/dashboard/ui/react-components/hooks/useTrajectory.ts +0 -265
- package/packages/dashboard/ui/react-components/hooks/useWebSocket.ts +0 -169
- package/packages/dashboard/ui/react-components/hooks/useWorkspaceMembers.ts +0 -120
- package/packages/dashboard/ui/react-components/hooks/useWorkspaceRepos.ts +0 -73
- package/packages/dashboard/ui/react-components/hooks/useWorkspaceStatus.ts +0 -237
- package/packages/dashboard/ui/react-components/index.ts +0 -81
- package/packages/dashboard/ui/react-components/layout/Header.tsx +0 -355
- package/packages/dashboard/ui/react-components/layout/RepoContextHeader.tsx +0 -361
- package/packages/dashboard/ui/react-components/layout/Sidebar.archive.test.tsx +0 -126
- package/packages/dashboard/ui/react-components/layout/Sidebar.test.tsx +0 -691
- package/packages/dashboard/ui/react-components/layout/Sidebar.tsx +0 -930
- package/packages/dashboard/ui/react-components/layout/index.ts +0 -7
- package/packages/dashboard/ui/react-components/settings/BillingSettingsPanel.tsx +0 -564
- package/packages/dashboard/ui/react-components/settings/SettingsPage.tsx +0 -544
- package/packages/dashboard/ui/react-components/settings/TeamSettingsPanel.tsx +0 -560
- package/packages/dashboard/ui/react-components/settings/WorkspaceSettingsPanel.tsx +0 -1386
- package/packages/dashboard/ui/react-components/settings/index.ts +0 -11
- package/packages/dashboard/ui/react-components/settings/types.ts +0 -53
- package/packages/dashboard/ui/react-components/utils/messageFormatting.tsx +0 -370
- package/packages/dashboard/ui/tailwind.config.js +0 -148
- package/packages/dashboard/ui/types/index.ts +0 -304
- package/packages/dashboard/ui/types/threading.ts +0 -7
- package/packages/dashboard/ui-dist/404.html +0 -1
- package/packages/dashboard/ui-dist/_next/static/7MZPqYkVGw3EGzVBkVmY9/_buildManifest.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/7MZPqYkVGw3EGzVBkVmY9/_ssgManifest.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/116-a883fca163f3a5bc.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/117-c8afed19e821a35d.js +0 -2
- package/packages/dashboard/ui-dist/_next/static/chunks/282-980c2eb8fff20123.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/320-a6304232cd0ee2ce.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/532-bace199897eeab37.js +0 -9
- package/packages/dashboard/ui-dist/_next/static/chunks/631-16b905e5920f9b59.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/648-acb2ff9f77cbfbd3.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/766-2aea80818f7eb0d8.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/83-26d2bde54616ee90.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/847-f1f467060f32afff.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/891-5cb1513eeb97a891.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/_not-found/page-60501fddbafba9dc.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/app/onboarding/page-9914652442f7e4fb.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/app/page-366fb7c078d4e9e0.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/cloud/link/page-fa1d5842aa90e8a6.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/complete-profile/page-dd64bbdf66b639cd.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/connect-repos/page-113060009ef35bc2.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/history/page-9965d2483011b846.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/layout-6b91e33784c20610.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-435eceb0073be027.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/metrics/page-1e37ef8e73940b40.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/page-8119d4246743574e.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/pricing/page-9db3ebdfa567a7c9.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/page-ecb16ffd3b36262b.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/providers/setup/[provider]/page-4dbe33f0f7691b7c.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-c7a0a28341365ae0.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/e868780c-48e5f147c90a3a41.js +0 -18
- package/packages/dashboard/ui-dist/_next/static/chunks/fd9d1056-609918ca7b6280bb.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/framework-f66176bb897dc684.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/main-311c3db74dcfadb7.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/main-app-fdbeb09028f57c9f.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/pages/_app-72b849fbd24ac258.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/pages/_error-7ba65e1336b92748.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/chunks/webpack-1cdd8ed57114d5e1.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/css/4034f236dd1a3178.css +0 -1
- package/packages/dashboard/ui-dist/_next/static/css/6892f8422896ef7a.css +0 -1
- package/packages/dashboard/ui-dist/_next/static/iJ3Uiz3IrqUJL7IxKZHiV/_buildManifest.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/iJ3Uiz3IrqUJL7IxKZHiV/_ssgManifest.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/l-jd878zUJ_IlraqEWMZc/_buildManifest.js +0 -1
- package/packages/dashboard/ui-dist/_next/static/l-jd878zUJ_IlraqEWMZc/_ssgManifest.js +0 -1
- package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-128.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-256.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-32.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-512.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo-64.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/agent-relay-logo.svg +0 -45
- package/packages/dashboard/ui-dist/alt-logos/logo.svg +0 -38
- package/packages/dashboard/ui-dist/alt-logos/monogram-logo-128.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/monogram-logo-256.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/monogram-logo-32.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/monogram-logo-512.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/monogram-logo-64.png +0 -0
- package/packages/dashboard/ui-dist/alt-logos/monogram-logo.svg +0 -38
- package/packages/dashboard/ui-dist/app/onboarding.html +0 -1
- package/packages/dashboard/ui-dist/app/onboarding.txt +0 -7
- package/packages/dashboard/ui-dist/app.html +0 -1
- package/packages/dashboard/ui-dist/app.txt +0 -7
- package/packages/dashboard/ui-dist/apple-icon.png +0 -0
- package/packages/dashboard/ui-dist/cloud/link.html +0 -1
- package/packages/dashboard/ui-dist/cloud/link.txt +0 -7
- package/packages/dashboard/ui-dist/complete-profile.html +0 -5
- package/packages/dashboard/ui-dist/complete-profile.txt +0 -7
- package/packages/dashboard/ui-dist/connect-repos.html +0 -1
- package/packages/dashboard/ui-dist/connect-repos.txt +0 -7
- package/packages/dashboard/ui-dist/history.html +0 -1
- package/packages/dashboard/ui-dist/history.txt +0 -7
- package/packages/dashboard/ui-dist/index.html +0 -1
- package/packages/dashboard/ui-dist/index.txt +0 -7
- package/packages/dashboard/ui-dist/login.html +0 -5
- package/packages/dashboard/ui-dist/login.txt +0 -7
- package/packages/dashboard/ui-dist/metrics.html +0 -1
- package/packages/dashboard/ui-dist/metrics.txt +0 -7
- package/packages/dashboard/ui-dist/pricing.html +0 -13
- package/packages/dashboard/ui-dist/pricing.txt +0 -7
- package/packages/dashboard/ui-dist/providers/setup/claude.html +0 -1
- package/packages/dashboard/ui-dist/providers/setup/claude.txt +0 -8
- package/packages/dashboard/ui-dist/providers/setup/codex.html +0 -1
- package/packages/dashboard/ui-dist/providers/setup/codex.txt +0 -8
- package/packages/dashboard/ui-dist/providers/setup/cursor.html +0 -1
- package/packages/dashboard/ui-dist/providers/setup/cursor.txt +0 -8
- package/packages/dashboard/ui-dist/providers.html +0 -1
- package/packages/dashboard/ui-dist/providers.txt +0 -7
- package/packages/dashboard/ui-dist/signup.html +0 -6
- package/packages/dashboard/ui-dist/signup.txt +0 -7
- package/packages/dashboard-server/dist/health-worker-manager.d.ts +0 -62
- package/packages/dashboard-server/dist/health-worker-manager.js +0 -144
- package/packages/dashboard-server/dist/health-worker.d.ts +0 -9
- package/packages/dashboard-server/dist/health-worker.js +0 -79
- package/packages/dashboard-server/dist/index.d.ts +0 -18
- package/packages/dashboard-server/dist/index.js +0 -17
- package/packages/dashboard-server/dist/metrics.d.ts +0 -105
- package/packages/dashboard-server/dist/metrics.js +0 -193
- package/packages/dashboard-server/dist/needs-attention.d.ts +0 -24
- package/packages/dashboard-server/dist/needs-attention.js +0 -78
- package/packages/dashboard-server/dist/server.d.ts +0 -25
- package/packages/dashboard-server/dist/server.js +0 -5158
- package/packages/dashboard-server/dist/start.d.ts +0 -6
- package/packages/dashboard-server/dist/start.js +0 -13
- package/packages/dashboard-server/dist/types/threading.d.ts +0 -8
- package/packages/dashboard-server/dist/types/threading.js +0 -2
- package/packages/dashboard-server/dist/user-bridge.d.ts +0 -158
- package/packages/dashboard-server/dist/user-bridge.js +0 -390
- package/packages/dashboard-server/package.json +0 -55
|
@@ -1,3033 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dashboard V2 - Main Application Component
|
|
3
|
-
*
|
|
4
|
-
* Root component that combines sidebar, header, and main content area.
|
|
5
|
-
* Manages global state via hooks and provides context to child components.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
|
9
|
-
import type { Agent, Project, Message, AgentSummary, ActivityEvent } from '../types';
|
|
10
|
-
import { ActivityFeed } from './ActivityFeed';
|
|
11
|
-
import { Sidebar } from './layout/Sidebar';
|
|
12
|
-
import { Header } from './layout/Header';
|
|
13
|
-
import { MessageList } from './MessageList';
|
|
14
|
-
import { ThreadPanel } from './ThreadPanel';
|
|
15
|
-
import { CommandPalette, type TaskCreateRequest, PRIORITY_CONFIG } from './CommandPalette';
|
|
16
|
-
import { SpawnModal, type SpawnConfig } from './SpawnModal';
|
|
17
|
-
import { NewConversationModal } from './NewConversationModal';
|
|
18
|
-
import { SettingsPage, defaultSettings, type Settings } from './settings';
|
|
19
|
-
import { ConversationHistory } from './ConversationHistory';
|
|
20
|
-
import type { HumanUser } from './MentionAutocomplete';
|
|
21
|
-
import { NotificationToast, useToasts } from './NotificationToast';
|
|
22
|
-
import { WorkspaceSelector, type Workspace } from './WorkspaceSelector';
|
|
23
|
-
import { AddWorkspaceModal } from './AddWorkspaceModal';
|
|
24
|
-
import { LogViewerPanel } from './LogViewerPanel';
|
|
25
|
-
import { TrajectoryViewer } from './TrajectoryViewer';
|
|
26
|
-
import { DecisionQueue, type Decision } from './DecisionQueue';
|
|
27
|
-
import { FleetOverview } from './FleetOverview';
|
|
28
|
-
import type { ServerInfo } from './ServerCard';
|
|
29
|
-
import { TypingIndicator } from './TypingIndicator';
|
|
30
|
-
import { MessageComposer } from './MessageComposer';
|
|
31
|
-
import { OnlineUsersIndicator } from './OnlineUsersIndicator';
|
|
32
|
-
import { UserProfilePanel } from './UserProfilePanel';
|
|
33
|
-
import { AgentProfilePanel } from './AgentProfilePanel';
|
|
34
|
-
import { useDirectMessage } from './hooks/useDirectMessage';
|
|
35
|
-
import { CoordinatorPanel } from './CoordinatorPanel';
|
|
36
|
-
import { BillingResult } from './BillingResult';
|
|
37
|
-
import { UsageBanner } from './UsageBanner';
|
|
38
|
-
import { useWebSocket } from './hooks/useWebSocket';
|
|
39
|
-
import { useAgents } from './hooks/useAgents';
|
|
40
|
-
import { useMessages } from './hooks/useMessages';
|
|
41
|
-
import { useOrchestrator } from './hooks/useOrchestrator';
|
|
42
|
-
import { useTrajectory } from './hooks/useTrajectory';
|
|
43
|
-
import { useRecentRepos } from './hooks/useRecentRepos';
|
|
44
|
-
import { useWorkspaceRepos } from './hooks/useWorkspaceRepos';
|
|
45
|
-
import { usePresence, type UserPresence } from './hooks/usePresence';
|
|
46
|
-
import {
|
|
47
|
-
ChannelViewV1,
|
|
48
|
-
SearchInput,
|
|
49
|
-
CreateChannelModal,
|
|
50
|
-
InviteToChannelModal,
|
|
51
|
-
MemberManagementPanel,
|
|
52
|
-
listChannels,
|
|
53
|
-
getMessages,
|
|
54
|
-
getChannelMembers,
|
|
55
|
-
removeMember as removeChannelMember,
|
|
56
|
-
sendMessage as sendChannelApiMessage,
|
|
57
|
-
markRead,
|
|
58
|
-
createChannel,
|
|
59
|
-
type Channel,
|
|
60
|
-
type ChannelMember,
|
|
61
|
-
type ChannelMessage as ChannelApiMessage,
|
|
62
|
-
type UnreadState,
|
|
63
|
-
type CreateChannelRequest,
|
|
64
|
-
} from './channels';
|
|
65
|
-
import { useWorkspaceMembers, filterOnlineUsersByWorkspace } from './hooks/useWorkspaceMembers';
|
|
66
|
-
import { useCloudSessionOptional } from './CloudSessionProvider';
|
|
67
|
-
import { WorkspaceProvider } from './WorkspaceContext';
|
|
68
|
-
import { api, convertApiDecision, setActiveWorkspaceId as setApiWorkspaceId, getActiveWorkspaceId, getCsrfToken } from '../lib/api';
|
|
69
|
-
import { cloudApi } from '../lib/cloudApi';
|
|
70
|
-
import { mergeAgentsForDashboard } from '../lib/agent-merge';
|
|
71
|
-
import type { CurrentUser } from './MessageList';
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Check if a sender is a human user (not an agent or system name)
|
|
75
|
-
* Extracts the logic for identifying human users to avoid duplication
|
|
76
|
-
*/
|
|
77
|
-
function isHumanSender(sender: string, agentNames: Set<string>): boolean {
|
|
78
|
-
return sender !== 'Dashboard' &&
|
|
79
|
-
sender !== '*' &&
|
|
80
|
-
!agentNames.has(sender.toLowerCase());
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const SETTINGS_STORAGE_KEY = 'dashboard-settings';
|
|
84
|
-
|
|
85
|
-
/** Special ID for the Activity feed (broadcasts) */
|
|
86
|
-
export const ACTIVITY_FEED_ID = '__activity__';
|
|
87
|
-
|
|
88
|
-
type LegacyDashboardSettings = {
|
|
89
|
-
theme?: 'dark' | 'light' | 'system';
|
|
90
|
-
compactMode?: boolean;
|
|
91
|
-
showTimestamps?: boolean;
|
|
92
|
-
soundEnabled?: boolean;
|
|
93
|
-
notificationsEnabled?: boolean;
|
|
94
|
-
autoScrollMessages?: boolean;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
function mergeSettings(base: Settings, partial: Partial<Settings>): Settings {
|
|
98
|
-
return {
|
|
99
|
-
...base,
|
|
100
|
-
...partial,
|
|
101
|
-
notifications: { ...base.notifications, ...partial.notifications },
|
|
102
|
-
display: { ...base.display, ...partial.display },
|
|
103
|
-
messages: { ...base.messages, ...partial.messages },
|
|
104
|
-
connection: { ...base.connection, ...partial.connection },
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function migrateLegacySettings(raw: LegacyDashboardSettings): Settings {
|
|
109
|
-
const theme = raw.theme && ['dark', 'light', 'system'].includes(raw.theme)
|
|
110
|
-
? raw.theme
|
|
111
|
-
: defaultSettings.theme;
|
|
112
|
-
const sound = raw.soundEnabled ?? defaultSettings.notifications.sound;
|
|
113
|
-
const desktop = raw.notificationsEnabled ?? defaultSettings.notifications.desktop;
|
|
114
|
-
return {
|
|
115
|
-
...defaultSettings,
|
|
116
|
-
theme,
|
|
117
|
-
display: {
|
|
118
|
-
...defaultSettings.display,
|
|
119
|
-
compactMode: raw.compactMode ?? defaultSettings.display.compactMode,
|
|
120
|
-
showTimestamps: raw.showTimestamps ?? defaultSettings.display.showTimestamps,
|
|
121
|
-
},
|
|
122
|
-
notifications: {
|
|
123
|
-
...defaultSettings.notifications,
|
|
124
|
-
sound,
|
|
125
|
-
desktop,
|
|
126
|
-
enabled: sound || desktop || defaultSettings.notifications.mentionsOnly,
|
|
127
|
-
},
|
|
128
|
-
messages: {
|
|
129
|
-
...defaultSettings.messages,
|
|
130
|
-
autoScroll: raw.autoScrollMessages ?? defaultSettings.messages.autoScroll,
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function loadSettingsFromStorage(): Settings {
|
|
136
|
-
if (typeof window === 'undefined') return defaultSettings;
|
|
137
|
-
try {
|
|
138
|
-
const saved = localStorage.getItem(SETTINGS_STORAGE_KEY);
|
|
139
|
-
if (!saved) return defaultSettings;
|
|
140
|
-
const parsed = JSON.parse(saved);
|
|
141
|
-
if (!parsed || typeof parsed !== 'object') return defaultSettings;
|
|
142
|
-
if ('notifications' in parsed && 'display' in parsed) {
|
|
143
|
-
const merged = mergeSettings(defaultSettings, parsed as Partial<Settings>);
|
|
144
|
-
merged.notifications.enabled = merged.notifications.sound ||
|
|
145
|
-
merged.notifications.desktop ||
|
|
146
|
-
merged.notifications.mentionsOnly;
|
|
147
|
-
return merged;
|
|
148
|
-
}
|
|
149
|
-
if ('notificationsEnabled' in parsed || 'soundEnabled' in parsed || 'autoScrollMessages' in parsed) {
|
|
150
|
-
return migrateLegacySettings(parsed as LegacyDashboardSettings);
|
|
151
|
-
}
|
|
152
|
-
} catch {
|
|
153
|
-
// Fall back to defaults
|
|
154
|
-
}
|
|
155
|
-
return defaultSettings;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function saveSettingsToStorage(settings: Settings) {
|
|
159
|
-
if (typeof window === 'undefined') return;
|
|
160
|
-
try {
|
|
161
|
-
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
|
|
162
|
-
} catch {
|
|
163
|
-
// Ignore localStorage failures
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function playNotificationSound() {
|
|
168
|
-
if (typeof window === 'undefined') return;
|
|
169
|
-
const AudioContextConstructor =
|
|
170
|
-
window.AudioContext ||
|
|
171
|
-
(window as Window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
|
|
172
|
-
if (!AudioContextConstructor) return;
|
|
173
|
-
try {
|
|
174
|
-
const context = new AudioContextConstructor();
|
|
175
|
-
const oscillator = context.createOscillator();
|
|
176
|
-
const gain = context.createGain();
|
|
177
|
-
oscillator.type = 'sine';
|
|
178
|
-
oscillator.frequency.value = 880;
|
|
179
|
-
gain.gain.value = 0.03;
|
|
180
|
-
oscillator.connect(gain);
|
|
181
|
-
gain.connect(context.destination);
|
|
182
|
-
oscillator.start();
|
|
183
|
-
oscillator.stop(context.currentTime + 0.12);
|
|
184
|
-
oscillator.onended = () => {
|
|
185
|
-
context.close().catch(() => undefined);
|
|
186
|
-
};
|
|
187
|
-
} catch {
|
|
188
|
-
// Audio might be blocked by browser autoplay policies
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export interface AppProps {
|
|
193
|
-
/** Initial WebSocket URL (optional, defaults to current host) */
|
|
194
|
-
wsUrl?: string;
|
|
195
|
-
/** Orchestrator API URL (optional, defaults to localhost:3456) */
|
|
196
|
-
orchestratorUrl?: string;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export function App({ wsUrl, orchestratorUrl }: AppProps) {
|
|
200
|
-
// WebSocket connection for real-time data (per-project daemon)
|
|
201
|
-
const { data, isConnected, error: wsError } = useWebSocket({ url: wsUrl });
|
|
202
|
-
|
|
203
|
-
// Orchestrator for multi-workspace management
|
|
204
|
-
const {
|
|
205
|
-
workspaces,
|
|
206
|
-
activeWorkspaceId,
|
|
207
|
-
agents: orchestratorAgents,
|
|
208
|
-
isConnected: isOrchestratorConnected,
|
|
209
|
-
isLoading: isOrchestratorLoading,
|
|
210
|
-
error: orchestratorError,
|
|
211
|
-
switchWorkspace,
|
|
212
|
-
addWorkspace,
|
|
213
|
-
removeWorkspace,
|
|
214
|
-
spawnAgent: orchestratorSpawnAgent,
|
|
215
|
-
stopAgent: orchestratorStopAgent,
|
|
216
|
-
} = useOrchestrator({ apiUrl: orchestratorUrl });
|
|
217
|
-
|
|
218
|
-
// Cloud session for user info (GitHub avatar/username)
|
|
219
|
-
const cloudSession = useCloudSessionOptional();
|
|
220
|
-
|
|
221
|
-
// Derive current user from cloud session (falls back to undefined in non-cloud mode)
|
|
222
|
-
const currentUser: CurrentUser | undefined = cloudSession?.user
|
|
223
|
-
? {
|
|
224
|
-
displayName: cloudSession.user.githubUsername || cloudSession.user.displayName || '',
|
|
225
|
-
avatarUrl: cloudSession.user.avatarUrl,
|
|
226
|
-
}
|
|
227
|
-
: undefined;
|
|
228
|
-
|
|
229
|
-
// Cloud workspaces state (for cloud mode)
|
|
230
|
-
// Includes owned, member, and contributor workspaces (via GitHub repo access)
|
|
231
|
-
const [cloudWorkspaces, setCloudWorkspaces] = useState<Array<{
|
|
232
|
-
id: string;
|
|
233
|
-
name: string;
|
|
234
|
-
status: string;
|
|
235
|
-
publicUrl?: string;
|
|
236
|
-
accessType?: 'owner' | 'member' | 'contributor';
|
|
237
|
-
permission?: 'admin' | 'write' | 'read';
|
|
238
|
-
}>>([]);
|
|
239
|
-
// Initialize from API module if already set (e.g., by DashboardPage when connecting to workspace)
|
|
240
|
-
const [activeCloudWorkspaceId, setActiveCloudWorkspaceId] = useState<string | null>(() => getActiveWorkspaceId());
|
|
241
|
-
const [isLoadingCloudWorkspaces, setIsLoadingCloudWorkspaces] = useState(false);
|
|
242
|
-
|
|
243
|
-
// Local agents from linked daemons
|
|
244
|
-
const [localAgents, setLocalAgents] = useState<Agent[]>([]);
|
|
245
|
-
|
|
246
|
-
// Fetch cloud workspaces when in cloud mode
|
|
247
|
-
// Uses getAccessibleWorkspaces to include contributor workspaces (via GitHub repos)
|
|
248
|
-
useEffect(() => {
|
|
249
|
-
if (!cloudSession?.user) return;
|
|
250
|
-
|
|
251
|
-
const fetchCloudWorkspaces = async () => {
|
|
252
|
-
setIsLoadingCloudWorkspaces(true);
|
|
253
|
-
try {
|
|
254
|
-
const result = await cloudApi.getAccessibleWorkspaces();
|
|
255
|
-
if (result.success && result.data.workspaces) {
|
|
256
|
-
setCloudWorkspaces(result.data.workspaces);
|
|
257
|
-
const workspaceIds = new Set(result.data.workspaces.map(w => w.id));
|
|
258
|
-
// Validate current selection exists, or auto-select first workspace
|
|
259
|
-
if (activeCloudWorkspaceId && !workspaceIds.has(activeCloudWorkspaceId)) {
|
|
260
|
-
// Current workspace no longer exists, clear selection to trigger auto-select
|
|
261
|
-
if (result.data.workspaces.length > 0) {
|
|
262
|
-
const firstWorkspaceId = result.data.workspaces[0].id;
|
|
263
|
-
setActiveCloudWorkspaceId(firstWorkspaceId);
|
|
264
|
-
setApiWorkspaceId(firstWorkspaceId);
|
|
265
|
-
} else {
|
|
266
|
-
setActiveCloudWorkspaceId(null);
|
|
267
|
-
setApiWorkspaceId(null);
|
|
268
|
-
}
|
|
269
|
-
} else if (!activeCloudWorkspaceId && result.data.workspaces.length > 0) {
|
|
270
|
-
// No selection yet, auto-select first workspace
|
|
271
|
-
const firstWorkspaceId = result.data.workspaces[0].id;
|
|
272
|
-
setActiveCloudWorkspaceId(firstWorkspaceId);
|
|
273
|
-
// Sync immediately with api module to avoid race conditions
|
|
274
|
-
setApiWorkspaceId(firstWorkspaceId);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
} catch (err) {
|
|
278
|
-
console.error('Failed to fetch cloud workspaces:', err);
|
|
279
|
-
} finally {
|
|
280
|
-
setIsLoadingCloudWorkspaces(false);
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
fetchCloudWorkspaces();
|
|
285
|
-
// Poll for updates every 30 seconds
|
|
286
|
-
const interval = setInterval(fetchCloudWorkspaces, 30000);
|
|
287
|
-
return () => clearInterval(interval);
|
|
288
|
-
}, [cloudSession?.user, activeCloudWorkspaceId]);
|
|
289
|
-
|
|
290
|
-
// Fetch local agents for the active workspace
|
|
291
|
-
useEffect(() => {
|
|
292
|
-
if (!cloudSession?.user || !activeCloudWorkspaceId) {
|
|
293
|
-
setLocalAgents([]);
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const fetchLocalAgents = async () => {
|
|
298
|
-
try {
|
|
299
|
-
const result = await api.get<{
|
|
300
|
-
agents: Array<{
|
|
301
|
-
name: string;
|
|
302
|
-
status: string;
|
|
303
|
-
isLocal: boolean;
|
|
304
|
-
isHuman?: boolean;
|
|
305
|
-
avatarUrl?: string;
|
|
306
|
-
daemonId: string;
|
|
307
|
-
daemonName: string;
|
|
308
|
-
daemonStatus: string;
|
|
309
|
-
machineId: string;
|
|
310
|
-
lastSeenAt: string | null;
|
|
311
|
-
}>;
|
|
312
|
-
}>(`/api/daemons/workspace/${activeCloudWorkspaceId}/agents`);
|
|
313
|
-
|
|
314
|
-
if (result.agents) {
|
|
315
|
-
// Convert API response to Agent format
|
|
316
|
-
// Agent status is 'online' when daemon is online (agent is connected to daemon)
|
|
317
|
-
const agents: Agent[] = result.agents.map((a) => ({
|
|
318
|
-
name: a.name,
|
|
319
|
-
status: a.daemonStatus === 'online' ? 'online' : 'offline',
|
|
320
|
-
// Only mark AI agents as "local" (from linked daemon), not human users
|
|
321
|
-
isLocal: !a.isHuman,
|
|
322
|
-
isHuman: a.isHuman,
|
|
323
|
-
avatarUrl: a.avatarUrl,
|
|
324
|
-
// Don't include daemon info for human users
|
|
325
|
-
daemonName: a.isHuman ? undefined : a.daemonName,
|
|
326
|
-
machineId: a.isHuman ? undefined : a.machineId,
|
|
327
|
-
lastSeen: a.lastSeenAt || undefined,
|
|
328
|
-
}));
|
|
329
|
-
setLocalAgents(agents);
|
|
330
|
-
}
|
|
331
|
-
} catch (err) {
|
|
332
|
-
console.error('Failed to fetch local agents:', err);
|
|
333
|
-
setLocalAgents([]);
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
fetchLocalAgents();
|
|
338
|
-
// Poll for updates every 15 seconds
|
|
339
|
-
const interval = setInterval(fetchLocalAgents, 15000);
|
|
340
|
-
return () => clearInterval(interval);
|
|
341
|
-
}, [cloudSession?.user, activeCloudWorkspaceId]);
|
|
342
|
-
|
|
343
|
-
// Determine which workspaces to use (cloud mode or orchestrator)
|
|
344
|
-
const isCloudMode = Boolean(cloudSession?.user);
|
|
345
|
-
const effectiveWorkspaces = useMemo(() => {
|
|
346
|
-
if (isCloudMode && cloudWorkspaces.length > 0) {
|
|
347
|
-
// Convert cloud workspaces to the format expected by WorkspaceSelector
|
|
348
|
-
// Includes owned, member, and contributor workspaces
|
|
349
|
-
return cloudWorkspaces.map(ws => ({
|
|
350
|
-
id: ws.id,
|
|
351
|
-
name: ws.name,
|
|
352
|
-
path: ws.publicUrl || `/workspace/${ws.name}`,
|
|
353
|
-
status: ws.status === 'running' ? 'active' as const : 'inactive' as const,
|
|
354
|
-
provider: 'claude' as const,
|
|
355
|
-
lastActiveAt: new Date(),
|
|
356
|
-
}));
|
|
357
|
-
}
|
|
358
|
-
return workspaces;
|
|
359
|
-
}, [isCloudMode, cloudWorkspaces, workspaces]);
|
|
360
|
-
|
|
361
|
-
const effectiveActiveWorkspaceId = isCloudMode ? activeCloudWorkspaceId : activeWorkspaceId;
|
|
362
|
-
const effectiveIsLoading = isCloudMode ? isLoadingCloudWorkspaces : isOrchestratorLoading;
|
|
363
|
-
|
|
364
|
-
// Sync the active workspace ID with the api module for cloud mode proxying
|
|
365
|
-
// This useEffect serves as a safeguard and handles initial load/edge cases
|
|
366
|
-
// The immediate sync in handleEffectiveWorkspaceSelect handles user-initiated changes
|
|
367
|
-
useEffect(() => {
|
|
368
|
-
if (isCloudMode && activeCloudWorkspaceId) {
|
|
369
|
-
setApiWorkspaceId(activeCloudWorkspaceId);
|
|
370
|
-
} else if (isCloudMode && !activeCloudWorkspaceId) {
|
|
371
|
-
// In cloud mode but no workspace selected - clear the proxy
|
|
372
|
-
setApiWorkspaceId(null);
|
|
373
|
-
} else if (!isCloudMode) {
|
|
374
|
-
// Clear the workspace ID when not in cloud mode
|
|
375
|
-
setApiWorkspaceId(null);
|
|
376
|
-
}
|
|
377
|
-
}, [isCloudMode, activeCloudWorkspaceId]);
|
|
378
|
-
|
|
379
|
-
// Handle workspace selection (works for both cloud and orchestrator)
|
|
380
|
-
const handleEffectiveWorkspaceSelect = useCallback(async (workspace: { id: string; name: string }) => {
|
|
381
|
-
if (isCloudMode) {
|
|
382
|
-
setActiveCloudWorkspaceId(workspace.id);
|
|
383
|
-
// Sync immediately with api module to avoid race conditions
|
|
384
|
-
// This ensures spawn/release calls use the correct workspace before the useEffect runs
|
|
385
|
-
setApiWorkspaceId(workspace.id);
|
|
386
|
-
} else {
|
|
387
|
-
await switchWorkspace(workspace.id);
|
|
388
|
-
}
|
|
389
|
-
}, [isCloudMode, switchWorkspace]);
|
|
390
|
-
|
|
391
|
-
// Presence tracking for online users and typing indicators
|
|
392
|
-
// Memoize the user object to prevent reconnection on every render
|
|
393
|
-
const presenceUser = useMemo(() =>
|
|
394
|
-
currentUser
|
|
395
|
-
? { username: currentUser.displayName, avatarUrl: currentUser.avatarUrl }
|
|
396
|
-
: undefined,
|
|
397
|
-
[currentUser?.displayName, currentUser?.avatarUrl]
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
// Channel state: selectedChannelId must be declared before callbacks that use it
|
|
401
|
-
// Default to Activity feed on load
|
|
402
|
-
const [selectedChannelId, setSelectedChannelId] = useState<string | undefined>(ACTIVITY_FEED_ID);
|
|
403
|
-
|
|
404
|
-
// Activity feed state - unified timeline of workspace events
|
|
405
|
-
const [activityEvents, setActivityEvents] = useState<ActivityEvent[]>([]);
|
|
406
|
-
|
|
407
|
-
// Helper to add activity events
|
|
408
|
-
const addActivityEvent = useCallback((event: Omit<ActivityEvent, 'id' | 'timestamp'>) => {
|
|
409
|
-
const newEvent: ActivityEvent = {
|
|
410
|
-
...event,
|
|
411
|
-
id: `activity-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
412
|
-
timestamp: new Date().toISOString(),
|
|
413
|
-
};
|
|
414
|
-
setActivityEvents(prev => [newEvent, ...prev].slice(0, 200)); // Keep last 200 events
|
|
415
|
-
}, []);
|
|
416
|
-
|
|
417
|
-
// Member management state
|
|
418
|
-
const [showMemberPanel, setShowMemberPanel] = useState(false);
|
|
419
|
-
const [channelMembers, setChannelMembers] = useState<ChannelMember[]>([]);
|
|
420
|
-
|
|
421
|
-
const appendChannelMessage = useCallback((channelId: string, message: ChannelApiMessage, options?: { incrementUnread?: boolean }) => {
|
|
422
|
-
const incrementUnread = options?.incrementUnread ?? true;
|
|
423
|
-
|
|
424
|
-
setChannelMessageMap(prev => {
|
|
425
|
-
const list = prev[channelId] ?? [];
|
|
426
|
-
const isDuplicate = list.some((m) => {
|
|
427
|
-
if (m.id === message.id) return true;
|
|
428
|
-
if (m.from !== message.from) return false;
|
|
429
|
-
if (m.content !== message.content) return false;
|
|
430
|
-
if (m.threadId !== message.threadId) return false;
|
|
431
|
-
const timeDiff = Math.abs(new Date(m.timestamp).getTime() - new Date(message.timestamp).getTime());
|
|
432
|
-
return timeDiff < 2000;
|
|
433
|
-
});
|
|
434
|
-
if (isDuplicate) return prev;
|
|
435
|
-
return { ...prev, [channelId]: [...list, message] };
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
if (selectedChannelId === channelId) {
|
|
439
|
-
setChannelMessages(prev => [...prev, message]);
|
|
440
|
-
setChannelUnreadState(undefined);
|
|
441
|
-
} else if (incrementUnread) {
|
|
442
|
-
setChannelsList(prev => {
|
|
443
|
-
const existing = prev.find(c => c.id === channelId);
|
|
444
|
-
if (existing) {
|
|
445
|
-
return prev.map(c =>
|
|
446
|
-
c.id === channelId
|
|
447
|
-
? { ...c, unreadCount: (c.unreadCount ?? 0) + 1 }
|
|
448
|
-
: c
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const newChannel: Channel = {
|
|
453
|
-
id: channelId,
|
|
454
|
-
name: channelId.startsWith('#') ? channelId.slice(1) : channelId,
|
|
455
|
-
visibility: 'public',
|
|
456
|
-
status: 'active',
|
|
457
|
-
createdAt: new Date().toISOString(),
|
|
458
|
-
createdBy: currentUser?.displayName || 'Dashboard',
|
|
459
|
-
memberCount: 1,
|
|
460
|
-
unreadCount: 1,
|
|
461
|
-
hasMentions: false,
|
|
462
|
-
isDm: channelId.startsWith('dm:'),
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
return [...prev, newChannel];
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
}, [currentUser?.displayName, selectedChannelId]);
|
|
469
|
-
|
|
470
|
-
const handlePresenceEvent = useCallback((event: any) => {
|
|
471
|
-
// Activity feed: capture presence join/leave events
|
|
472
|
-
if (event?.type === 'presence_join' && event.user) {
|
|
473
|
-
const user = event.user;
|
|
474
|
-
// Skip self
|
|
475
|
-
if (user.username !== currentUser?.displayName) {
|
|
476
|
-
addActivityEvent({
|
|
477
|
-
type: 'user_joined',
|
|
478
|
-
actor: user.username,
|
|
479
|
-
actorAvatarUrl: user.avatarUrl,
|
|
480
|
-
actorType: 'user',
|
|
481
|
-
title: 'came online',
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
} else if (event?.type === 'presence_leave' && event.username) {
|
|
485
|
-
// Skip self
|
|
486
|
-
if (event.username !== currentUser?.displayName) {
|
|
487
|
-
addActivityEvent({
|
|
488
|
-
type: 'user_left',
|
|
489
|
-
actor: event.username,
|
|
490
|
-
actorType: 'user',
|
|
491
|
-
title: 'went offline',
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
} else if (event?.type === 'agent_spawned' && event.agent) {
|
|
495
|
-
// Agent spawned event from backend
|
|
496
|
-
addActivityEvent({
|
|
497
|
-
type: 'agent_spawned',
|
|
498
|
-
actor: event.agent.name || event.agent,
|
|
499
|
-
actorType: 'agent',
|
|
500
|
-
title: 'was spawned',
|
|
501
|
-
description: event.task,
|
|
502
|
-
metadata: { cli: event.cli, task: event.task, spawnedBy: event.spawnedBy },
|
|
503
|
-
});
|
|
504
|
-
} else if (event?.type === 'agent_released' && event.agent) {
|
|
505
|
-
// Agent released event from backend
|
|
506
|
-
addActivityEvent({
|
|
507
|
-
type: 'agent_released',
|
|
508
|
-
actor: event.agent.name || event.agent,
|
|
509
|
-
actorType: 'agent',
|
|
510
|
-
title: 'was released',
|
|
511
|
-
metadata: { releasedBy: event.releasedBy },
|
|
512
|
-
});
|
|
513
|
-
} else if (event?.type === 'channel_created') {
|
|
514
|
-
// Another user created a channel - add it to the list
|
|
515
|
-
const newChannel = event.channel;
|
|
516
|
-
if (!newChannel || !newChannel.id) return;
|
|
517
|
-
|
|
518
|
-
setChannelsList(prev => {
|
|
519
|
-
// Don't add if already exists
|
|
520
|
-
if (prev.some(c => c.id === newChannel.id)) return prev;
|
|
521
|
-
|
|
522
|
-
const channel: Channel = {
|
|
523
|
-
id: newChannel.id,
|
|
524
|
-
name: newChannel.name || newChannel.id,
|
|
525
|
-
description: newChannel.description,
|
|
526
|
-
visibility: newChannel.visibility || 'public',
|
|
527
|
-
status: newChannel.status || 'active',
|
|
528
|
-
createdAt: newChannel.createdAt || new Date().toISOString(),
|
|
529
|
-
createdBy: newChannel.createdBy || 'unknown',
|
|
530
|
-
memberCount: newChannel.memberCount || 1,
|
|
531
|
-
unreadCount: newChannel.unreadCount || 0,
|
|
532
|
-
hasMentions: newChannel.hasMentions || false,
|
|
533
|
-
isDm: newChannel.isDm || false,
|
|
534
|
-
};
|
|
535
|
-
console.log('[App] Channel created via WebSocket:', channel.id);
|
|
536
|
-
return [...prev, channel];
|
|
537
|
-
});
|
|
538
|
-
} else if (event?.type === 'channel_message') {
|
|
539
|
-
const channelId = event.channel as string | undefined;
|
|
540
|
-
if (!channelId) return;
|
|
541
|
-
const sender = event.from || 'unknown';
|
|
542
|
-
// Use server-provided entity type if available, otherwise derive locally
|
|
543
|
-
const fromEntityType = event.fromEntityType || (currentUser?.displayName && sender === currentUser.displayName ? 'user' : 'agent');
|
|
544
|
-
const msg: ChannelApiMessage = {
|
|
545
|
-
id: event.id ?? `ws-${Date.now()}`,
|
|
546
|
-
channelId,
|
|
547
|
-
from: sender,
|
|
548
|
-
fromEntityType,
|
|
549
|
-
fromAvatarUrl: event.fromAvatarUrl,
|
|
550
|
-
content: event.body ?? '',
|
|
551
|
-
timestamp: event.timestamp || new Date().toISOString(),
|
|
552
|
-
threadId: event.thread,
|
|
553
|
-
isRead: selectedChannelId === channelId,
|
|
554
|
-
};
|
|
555
|
-
appendChannelMessage(channelId, msg, { incrementUnread: selectedChannelId !== channelId });
|
|
556
|
-
} else if (event?.type === 'direct_message') {
|
|
557
|
-
// Handle direct messages sent to the user's GitHub username
|
|
558
|
-
const sender = event.from || 'unknown';
|
|
559
|
-
const recipient = currentUser?.displayName;
|
|
560
|
-
if (!recipient) return;
|
|
561
|
-
|
|
562
|
-
// Create DM channel ID with sorted participants for consistency
|
|
563
|
-
const participants = [sender, recipient].sort();
|
|
564
|
-
const dmChannelId = `dm:${participants.join(':')}`;
|
|
565
|
-
|
|
566
|
-
// Use server-provided entity type if available
|
|
567
|
-
const fromEntityType = event.fromEntityType || 'agent';
|
|
568
|
-
const msg: ChannelApiMessage = {
|
|
569
|
-
id: event.id ?? `dm-${Date.now()}`,
|
|
570
|
-
channelId: dmChannelId,
|
|
571
|
-
from: sender,
|
|
572
|
-
fromEntityType,
|
|
573
|
-
fromAvatarUrl: event.fromAvatarUrl,
|
|
574
|
-
content: event.body ?? '',
|
|
575
|
-
timestamp: event.timestamp || new Date().toISOString(),
|
|
576
|
-
threadId: event.thread,
|
|
577
|
-
isRead: selectedChannelId === dmChannelId,
|
|
578
|
-
};
|
|
579
|
-
appendChannelMessage(dmChannelId, msg, { incrementUnread: selectedChannelId !== dmChannelId });
|
|
580
|
-
}
|
|
581
|
-
}, [addActivityEvent, appendChannelMessage, currentUser?.displayName, selectedChannelId]);
|
|
582
|
-
|
|
583
|
-
const { onlineUsers: allOnlineUsers, typingUsers, sendTyping, isConnected: isPresenceConnected } = usePresence({
|
|
584
|
-
currentUser: presenceUser,
|
|
585
|
-
onEvent: handlePresenceEvent,
|
|
586
|
-
workspaceId: effectiveActiveWorkspaceId ?? undefined,
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
// Keep local username for channel API calls
|
|
590
|
-
useEffect(() => {
|
|
591
|
-
if (typeof window !== 'undefined' && currentUser?.displayName) {
|
|
592
|
-
localStorage.setItem('relay_username', currentUser.displayName);
|
|
593
|
-
}
|
|
594
|
-
}, [currentUser?.displayName]);
|
|
595
|
-
|
|
596
|
-
// Filter online users by workspace membership (cloud mode only)
|
|
597
|
-
const { memberUsernames } = useWorkspaceMembers({
|
|
598
|
-
workspaceId: effectiveActiveWorkspaceId ?? undefined,
|
|
599
|
-
enabled: isCloudMode && !!effectiveActiveWorkspaceId,
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
// Filter online users to only show those with access to current workspace
|
|
603
|
-
const onlineUsers = useMemo(
|
|
604
|
-
() => filterOnlineUsersByWorkspace(allOnlineUsers, memberUsernames),
|
|
605
|
-
[allOnlineUsers, memberUsernames]
|
|
606
|
-
);
|
|
607
|
-
|
|
608
|
-
// User profile panel state
|
|
609
|
-
const [selectedUserProfile, setSelectedUserProfile] = useState<UserPresence | null>(null);
|
|
610
|
-
const [pendingMention, setPendingMention] = useState<string | undefined>();
|
|
611
|
-
|
|
612
|
-
// Agent profile panel state
|
|
613
|
-
const [selectedAgentProfile, setSelectedAgentProfile] = useState<Agent | null>(null);
|
|
614
|
-
|
|
615
|
-
// Agent summaries lookup
|
|
616
|
-
const agentSummariesMap = useMemo(() => {
|
|
617
|
-
const map = new Map<string, AgentSummary>();
|
|
618
|
-
for (const summary of data?.summaries ?? []) {
|
|
619
|
-
map.set(summary.agentName.toLowerCase(), summary);
|
|
620
|
-
}
|
|
621
|
-
return map;
|
|
622
|
-
}, [data?.summaries]);
|
|
623
|
-
|
|
624
|
-
// View mode state: 'local' (agents), 'fleet' (multi-server), 'channels' (channel messaging)
|
|
625
|
-
const [viewMode, setViewMode] = useState<'local' | 'fleet' | 'channels'>('local');
|
|
626
|
-
|
|
627
|
-
// Channel state for V1 channels UI
|
|
628
|
-
const [channelsList, setChannelsList] = useState<Channel[]>([]);
|
|
629
|
-
const [archivedChannelsList, setArchivedChannelsList] = useState<Channel[]>([]);
|
|
630
|
-
const [channelMessages, setChannelMessages] = useState<ChannelApiMessage[]>([]);
|
|
631
|
-
const [channelMessageMap, setChannelMessageMap] = useState<Record<string, ChannelApiMessage[]>>({});
|
|
632
|
-
const fetchedChannelsRef = useRef<Set<string>>(new Set()); // Track channels already fetched to prevent loops
|
|
633
|
-
const [isChannelsLoading, setIsChannelsLoading] = useState(false);
|
|
634
|
-
const [hasMoreMessages, setHasMoreMessages] = useState(false);
|
|
635
|
-
const [channelUnreadState, setChannelUnreadState] = useState<UnreadState | undefined>();
|
|
636
|
-
|
|
637
|
-
// Default channel IDs that should always be visible
|
|
638
|
-
const DEFAULT_CHANNEL_IDS = ['#general', '#engineering'];
|
|
639
|
-
|
|
640
|
-
const setChannelListsFromResponse = useCallback((response: { channels: Channel[]; archivedChannels?: Channel[] }) => {
|
|
641
|
-
const archived = [
|
|
642
|
-
...(response.archivedChannels || []),
|
|
643
|
-
...response.channels.filter(c => c.status === 'archived'),
|
|
644
|
-
];
|
|
645
|
-
const apiActive = response.channels.filter(c => c.status !== 'archived');
|
|
646
|
-
|
|
647
|
-
// Merge with default channels to ensure #general is always visible
|
|
648
|
-
// Default channels are added if not present in API response
|
|
649
|
-
const apiChannelIds = new Set(apiActive.map(c => c.id));
|
|
650
|
-
const defaultChannelsToAdd: Channel[] = DEFAULT_CHANNEL_IDS
|
|
651
|
-
.filter(id => !apiChannelIds.has(id))
|
|
652
|
-
.map(id => ({
|
|
653
|
-
id,
|
|
654
|
-
name: id.replace('#', ''),
|
|
655
|
-
description: id === '#general' ? 'General discussion for all agents' : 'Engineering discussion',
|
|
656
|
-
visibility: 'public' as const,
|
|
657
|
-
memberCount: 0,
|
|
658
|
-
unreadCount: 0,
|
|
659
|
-
hasMentions: false,
|
|
660
|
-
createdAt: new Date().toISOString(),
|
|
661
|
-
status: 'active' as const,
|
|
662
|
-
createdBy: 'system',
|
|
663
|
-
isDm: false,
|
|
664
|
-
}));
|
|
665
|
-
|
|
666
|
-
setChannelsList([...defaultChannelsToAdd, ...apiActive]);
|
|
667
|
-
setArchivedChannelsList(archived);
|
|
668
|
-
}, []);
|
|
669
|
-
|
|
670
|
-
// Find selected channel object
|
|
671
|
-
const selectedChannel = useMemo(() => {
|
|
672
|
-
if (!selectedChannelId) return undefined;
|
|
673
|
-
return channelsList.find(c => c.id === selectedChannelId) ||
|
|
674
|
-
archivedChannelsList.find(c => c.id === selectedChannelId);
|
|
675
|
-
}, [selectedChannelId, channelsList, archivedChannelsList]);
|
|
676
|
-
|
|
677
|
-
// Project state for unified navigation (converted from workspaces)
|
|
678
|
-
const [projects, setProjects] = useState<Project[]>([]);
|
|
679
|
-
const [currentProject, setCurrentProject] = useState<string | undefined>();
|
|
680
|
-
|
|
681
|
-
// Spawn modal state
|
|
682
|
-
const [isSpawnModalOpen, setIsSpawnModalOpen] = useState(false);
|
|
683
|
-
const [isSpawning, setIsSpawning] = useState(false);
|
|
684
|
-
const [spawnError, setSpawnError] = useState<string | null>(null);
|
|
685
|
-
|
|
686
|
-
// Add workspace modal state
|
|
687
|
-
const [isAddWorkspaceOpen, setIsAddWorkspaceOpen] = useState(false);
|
|
688
|
-
const [isAddingWorkspace, setIsAddingWorkspace] = useState(false);
|
|
689
|
-
const [addWorkspaceError, setAddWorkspaceError] = useState<string | null>(null);
|
|
690
|
-
|
|
691
|
-
// Create channel modal state
|
|
692
|
-
const [isCreateChannelOpen, setIsCreateChannelOpen] = useState(false);
|
|
693
|
-
const [isCreatingChannel, setIsCreatingChannel] = useState(false);
|
|
694
|
-
|
|
695
|
-
// Invite to channel modal state
|
|
696
|
-
const [isInviteChannelOpen, setIsInviteChannelOpen] = useState(false);
|
|
697
|
-
const [inviteChannelTarget, setInviteChannelTarget] = useState<Channel | null>(null);
|
|
698
|
-
const [isInvitingToChannel, setIsInvitingToChannel] = useState(false);
|
|
699
|
-
|
|
700
|
-
// Command palette state
|
|
701
|
-
const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
|
|
702
|
-
|
|
703
|
-
// Settings state (theme, display, notifications)
|
|
704
|
-
const [settings, setSettings] = useState<Settings>(() => loadSettingsFromStorage());
|
|
705
|
-
const updateSettings = useCallback((updater: (prev: Settings) => Settings) => {
|
|
706
|
-
setSettings((prev) => updater(prev));
|
|
707
|
-
}, []);
|
|
708
|
-
|
|
709
|
-
// Full settings page state
|
|
710
|
-
const [isFullSettingsOpen, setIsFullSettingsOpen] = useState(false);
|
|
711
|
-
const [settingsInitialTab, setSettingsInitialTab] = useState<'dashboard' | 'workspace' | 'team' | 'billing'>('dashboard');
|
|
712
|
-
|
|
713
|
-
// Conversation history panel state
|
|
714
|
-
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
|
715
|
-
|
|
716
|
-
// New conversation modal state
|
|
717
|
-
const [isNewConversationOpen, setIsNewConversationOpen] = useState(false);
|
|
718
|
-
|
|
719
|
-
// DM participant selections (human -> invited agents) and removals
|
|
720
|
-
const [dmSelectedAgentsByHuman, setDmSelectedAgentsByHuman] = useState<Record<string, string[]>>({});
|
|
721
|
-
const [dmRemovedAgentsByHuman, setDmRemovedAgentsByHuman] = useState<Record<string, string[]>>({});
|
|
722
|
-
|
|
723
|
-
// Log viewer panel state
|
|
724
|
-
const [logViewerAgent, setLogViewerAgent] = useState<Agent | null>(null);
|
|
725
|
-
|
|
726
|
-
// Trajectory panel state
|
|
727
|
-
const [isTrajectoryOpen, setIsTrajectoryOpen] = useState(false);
|
|
728
|
-
const {
|
|
729
|
-
steps: trajectorySteps,
|
|
730
|
-
status: trajectoryStatus,
|
|
731
|
-
history: trajectoryHistory,
|
|
732
|
-
isLoading: isTrajectoryLoading,
|
|
733
|
-
selectTrajectory,
|
|
734
|
-
selectedTrajectoryId,
|
|
735
|
-
} = useTrajectory({
|
|
736
|
-
autoPoll: isTrajectoryOpen, // Only poll when panel is open
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
// Get the title of the selected trajectory from history
|
|
740
|
-
const selectedTrajectoryTitle = useMemo(() => {
|
|
741
|
-
if (!selectedTrajectoryId) return null;
|
|
742
|
-
return trajectoryHistory.find(t => t.id === selectedTrajectoryId)?.title ?? null;
|
|
743
|
-
}, [selectedTrajectoryId, trajectoryHistory]);
|
|
744
|
-
|
|
745
|
-
// Recent repos tracking
|
|
746
|
-
const { recentRepos, addRecentRepo, getRecentProjects } = useRecentRepos();
|
|
747
|
-
|
|
748
|
-
// Workspace repos for multi-repo workspaces
|
|
749
|
-
const { repos: workspaceRepos, refetch: refetchWorkspaceRepos } = useWorkspaceRepos({
|
|
750
|
-
workspaceId: effectiveActiveWorkspaceId ?? undefined,
|
|
751
|
-
apiBaseUrl: '/api',
|
|
752
|
-
enabled: isCloudMode && !!effectiveActiveWorkspaceId,
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
// Reset channel state when switching workspaces
|
|
756
|
-
useEffect(() => {
|
|
757
|
-
setChannelMessageMap({});
|
|
758
|
-
setChannelMessages([]);
|
|
759
|
-
setSelectedChannelId(undefined);
|
|
760
|
-
}, [effectiveActiveWorkspaceId]);
|
|
761
|
-
|
|
762
|
-
// Coordinator panel state
|
|
763
|
-
const [isCoordinatorOpen, setIsCoordinatorOpen] = useState(false);
|
|
764
|
-
|
|
765
|
-
// Decision queue state
|
|
766
|
-
const [isDecisionQueueOpen, setIsDecisionQueueOpen] = useState(false);
|
|
767
|
-
const [decisions, setDecisions] = useState<Decision[]>([]);
|
|
768
|
-
const [decisionProcessing, setDecisionProcessing] = useState<Record<string, boolean>>({});
|
|
769
|
-
|
|
770
|
-
// Fleet overview state
|
|
771
|
-
const [isFleetViewActive, setIsFleetViewActive] = useState(false);
|
|
772
|
-
const [fleetServers, setFleetServers] = useState<ServerInfo[]>([]);
|
|
773
|
-
|
|
774
|
-
// Auth revocation notification state
|
|
775
|
-
const { toasts, addToast, dismissToast } = useToasts();
|
|
776
|
-
const [authRevokedAgents, setAuthRevokedAgents] = useState<Set<string>>(new Set());
|
|
777
|
-
const [selectedServerId, setSelectedServerId] = useState<string | undefined>();
|
|
778
|
-
|
|
779
|
-
// Task creation state (tasks are stored in beads, not local state)
|
|
780
|
-
const [isCreatingTask, setIsCreatingTask] = useState(false);
|
|
781
|
-
|
|
782
|
-
// Mobile sidebar state
|
|
783
|
-
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
784
|
-
|
|
785
|
-
// Unread message notification state for mobile
|
|
786
|
-
const [hasUnreadMessages, setHasUnreadMessages] = useState(false);
|
|
787
|
-
const lastSeenMessageCountRef = useRef<number>(0);
|
|
788
|
-
const sidebarClosedRef = useRef<boolean>(true); // Track if sidebar is currently closed
|
|
789
|
-
const [dmSeenAt, setDmSeenAt] = useState<Map<string, number>>(new Map());
|
|
790
|
-
const lastNotifiedMessageIdRef = useRef<string | null>(null);
|
|
791
|
-
|
|
792
|
-
// Close sidebar when selecting an agent or project on mobile
|
|
793
|
-
const closeSidebarOnMobile = useCallback(() => {
|
|
794
|
-
if (window.innerWidth <= 768) {
|
|
795
|
-
setIsSidebarOpen(false);
|
|
796
|
-
}
|
|
797
|
-
}, []);
|
|
798
|
-
|
|
799
|
-
// Merge AI agents, human users, and local agents from linked daemons
|
|
800
|
-
const combinedAgents = useMemo(() => {
|
|
801
|
-
return mergeAgentsForDashboard({
|
|
802
|
-
agents: data?.agents,
|
|
803
|
-
users: data?.users,
|
|
804
|
-
localAgents,
|
|
805
|
-
});
|
|
806
|
-
}, [data?.agents, data?.users, localAgents]);
|
|
807
|
-
|
|
808
|
-
// Mark a DM conversation as seen (used for unread badges)
|
|
809
|
-
const markDmSeen = useCallback((username: string) => {
|
|
810
|
-
setDmSeenAt((prev) => {
|
|
811
|
-
const next = new Map(prev);
|
|
812
|
-
next.set(username.toLowerCase(), Date.now());
|
|
813
|
-
return next;
|
|
814
|
-
});
|
|
815
|
-
}, []);
|
|
816
|
-
|
|
817
|
-
// Agent state management
|
|
818
|
-
const {
|
|
819
|
-
agents,
|
|
820
|
-
groups,
|
|
821
|
-
selectedAgent,
|
|
822
|
-
selectAgent,
|
|
823
|
-
searchQuery,
|
|
824
|
-
setSearchQuery,
|
|
825
|
-
totalCount,
|
|
826
|
-
onlineCount,
|
|
827
|
-
needsAttentionCount,
|
|
828
|
-
} = useAgents({
|
|
829
|
-
agents: combinedAgents,
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
// Message state management
|
|
833
|
-
const {
|
|
834
|
-
messages,
|
|
835
|
-
threadMessages,
|
|
836
|
-
currentChannel,
|
|
837
|
-
setCurrentChannel,
|
|
838
|
-
currentThread,
|
|
839
|
-
setCurrentThread,
|
|
840
|
-
activeThreads,
|
|
841
|
-
totalUnreadThreadCount,
|
|
842
|
-
sendMessage,
|
|
843
|
-
isSending,
|
|
844
|
-
sendError,
|
|
845
|
-
} = useMessages({
|
|
846
|
-
messages: data?.messages ?? [],
|
|
847
|
-
senderName: currentUser?.displayName,
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
// Human context (DM inline view)
|
|
851
|
-
const currentHuman = useMemo(() => {
|
|
852
|
-
if (!currentChannel) return null;
|
|
853
|
-
return combinedAgents.find(
|
|
854
|
-
(a) => a.isHuman && a.name.toLowerCase() === currentChannel.toLowerCase()
|
|
855
|
-
) || null;
|
|
856
|
-
}, [combinedAgents, currentChannel]);
|
|
857
|
-
|
|
858
|
-
const selectedDmAgents = useMemo(
|
|
859
|
-
() => (currentHuman ? dmSelectedAgentsByHuman[currentHuman.name] ?? [] : []),
|
|
860
|
-
[currentHuman, dmSelectedAgentsByHuman]
|
|
861
|
-
);
|
|
862
|
-
const removedDmAgents = useMemo(
|
|
863
|
-
() => (currentHuman ? dmRemovedAgentsByHuman[currentHuman.name] ?? [] : []),
|
|
864
|
-
[currentHuman, dmRemovedAgentsByHuman]
|
|
865
|
-
);
|
|
866
|
-
|
|
867
|
-
// Use DM hook for message filtering and deduplication
|
|
868
|
-
const { visibleMessages: dedupedVisibleMessages, participantAgents: dmParticipantAgents } = useDirectMessage({
|
|
869
|
-
currentHuman,
|
|
870
|
-
currentUserName: currentUser?.displayName ?? null,
|
|
871
|
-
messages,
|
|
872
|
-
agents,
|
|
873
|
-
selectedDmAgents,
|
|
874
|
-
removedDmAgents,
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
// For local mode: convert relay messages to channel message format
|
|
878
|
-
// Filter messages by channel (checking multiple fields for compatibility)
|
|
879
|
-
const localChannelMessages = useMemo((): ChannelApiMessage[] => {
|
|
880
|
-
if (effectiveActiveWorkspaceId || !selectedChannelId) return [];
|
|
881
|
-
|
|
882
|
-
// Filter messages that belong to this channel
|
|
883
|
-
const filtered = messages.filter(m => {
|
|
884
|
-
// Activity feed shows broadcasts (to='*')
|
|
885
|
-
if (selectedChannelId === ACTIVITY_FEED_ID) {
|
|
886
|
-
return m.to === '*' || m.isBroadcast;
|
|
887
|
-
}
|
|
888
|
-
// Check if message is explicitly for this channel (CHANNEL_MESSAGE format)
|
|
889
|
-
if (m.to === selectedChannelId) return true;
|
|
890
|
-
// Check channel property for channel messages
|
|
891
|
-
if (m.channel === selectedChannelId) return true;
|
|
892
|
-
// Legacy: messages with this channel as thread
|
|
893
|
-
if (m.thread === selectedChannelId) return true;
|
|
894
|
-
return false;
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
// Convert to ChannelMessage format
|
|
898
|
-
return filtered.map(m => ({
|
|
899
|
-
id: m.id,
|
|
900
|
-
channelId: selectedChannelId,
|
|
901
|
-
from: m.from,
|
|
902
|
-
fromEntityType: (m.from === 'Dashboard' || m.from === currentUser?.displayName) ? 'user' : 'agent' as const,
|
|
903
|
-
content: m.content,
|
|
904
|
-
timestamp: m.timestamp,
|
|
905
|
-
isRead: m.isRead ?? true,
|
|
906
|
-
threadId: m.thread !== selectedChannelId ? m.thread : undefined,
|
|
907
|
-
}));
|
|
908
|
-
}, [messages, selectedChannelId, effectiveActiveWorkspaceId, currentUser?.displayName]);
|
|
909
|
-
|
|
910
|
-
// Use local or cloud messages depending on mode
|
|
911
|
-
const effectiveChannelMessages = effectiveActiveWorkspaceId ? channelMessages : localChannelMessages;
|
|
912
|
-
|
|
913
|
-
// Extract human users from messages (users who are not agents)
|
|
914
|
-
// This enables @ mentioning other human users in cloud mode
|
|
915
|
-
const humanUsers = useMemo((): HumanUser[] => {
|
|
916
|
-
const agentNames = new Set(agents.map((a) => a.name.toLowerCase()));
|
|
917
|
-
const seenUsers = new Map<string, HumanUser>();
|
|
918
|
-
|
|
919
|
-
// Include current user if in cloud mode
|
|
920
|
-
if (currentUser) {
|
|
921
|
-
seenUsers.set(currentUser.displayName.toLowerCase(), {
|
|
922
|
-
username: currentUser.displayName,
|
|
923
|
-
avatarUrl: currentUser.avatarUrl,
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
// Extract unique human users from message senders
|
|
928
|
-
for (const msg of data?.messages ?? []) {
|
|
929
|
-
const sender = msg.from;
|
|
930
|
-
if (sender && isHumanSender(sender, agentNames) && !seenUsers.has(sender.toLowerCase())) {
|
|
931
|
-
seenUsers.set(sender.toLowerCase(), {
|
|
932
|
-
username: sender,
|
|
933
|
-
// Note: We don't have avatar URLs for users from messages
|
|
934
|
-
// unless we fetch them separately
|
|
935
|
-
});
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
return Array.from(seenUsers.values());
|
|
940
|
-
}, [data?.messages, agents, currentUser]);
|
|
941
|
-
|
|
942
|
-
// Unread counts for human conversations (DMs)
|
|
943
|
-
const humanUnreadCounts = useMemo(() => {
|
|
944
|
-
if (!currentUser) return {};
|
|
945
|
-
|
|
946
|
-
const counts: Record<string, number> = {};
|
|
947
|
-
const humanNameSet = new Set(
|
|
948
|
-
combinedAgents.filter((a) => a.isHuman).map((a) => a.name.toLowerCase())
|
|
949
|
-
);
|
|
950
|
-
|
|
951
|
-
for (const msg of data?.messages ?? []) {
|
|
952
|
-
const sender = msg.from;
|
|
953
|
-
const recipient = msg.to;
|
|
954
|
-
if (!sender || !recipient) continue;
|
|
955
|
-
|
|
956
|
-
const isToCurrentUser = recipient === currentUser.displayName;
|
|
957
|
-
const senderIsHuman = humanNameSet.has(sender.toLowerCase());
|
|
958
|
-
if (!isToCurrentUser || !senderIsHuman) continue;
|
|
959
|
-
|
|
960
|
-
const seenAt = dmSeenAt.get(sender.toLowerCase()) ?? 0;
|
|
961
|
-
const ts = new Date(msg.timestamp).getTime();
|
|
962
|
-
if (ts > seenAt) {
|
|
963
|
-
counts[sender] = (counts[sender] || 0) + 1;
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
return counts;
|
|
968
|
-
}, [combinedAgents, currentUser, data?.messages, dmSeenAt]);
|
|
969
|
-
|
|
970
|
-
// Mark DM as seen when actively viewing a human channel
|
|
971
|
-
useEffect(() => {
|
|
972
|
-
if (!currentUser || !currentChannel) return;
|
|
973
|
-
const humanNameSet = new Set(
|
|
974
|
-
combinedAgents.filter((a) => a.isHuman).map((a) => a.name.toLowerCase())
|
|
975
|
-
);
|
|
976
|
-
if (humanNameSet.has(currentChannel.toLowerCase())) {
|
|
977
|
-
markDmSeen(currentChannel);
|
|
978
|
-
}
|
|
979
|
-
}, [combinedAgents, currentChannel, currentUser, markDmSeen]);
|
|
980
|
-
|
|
981
|
-
// Track unread messages when sidebar is closed on mobile
|
|
982
|
-
useEffect(() => {
|
|
983
|
-
// Only track on mobile viewport
|
|
984
|
-
const isMobile = window.innerWidth <= 768;
|
|
985
|
-
if (!isMobile) {
|
|
986
|
-
setHasUnreadMessages(false);
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
const messageCount = messages.length;
|
|
991
|
-
|
|
992
|
-
// If sidebar is closed and we have new messages since last seen
|
|
993
|
-
if (!isSidebarOpen && messageCount > lastSeenMessageCountRef.current) {
|
|
994
|
-
setHasUnreadMessages(true);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// Update the ref based on current sidebar state
|
|
998
|
-
sidebarClosedRef.current = !isSidebarOpen;
|
|
999
|
-
}, [messages.length, isSidebarOpen]);
|
|
1000
|
-
|
|
1001
|
-
// Clear unread state and update last seen count when sidebar opens
|
|
1002
|
-
useEffect(() => {
|
|
1003
|
-
if (isSidebarOpen) {
|
|
1004
|
-
setHasUnreadMessages(false);
|
|
1005
|
-
lastSeenMessageCountRef.current = messages.length;
|
|
1006
|
-
}
|
|
1007
|
-
}, [isSidebarOpen, messages.length]);
|
|
1008
|
-
|
|
1009
|
-
// Initialize last seen message count on mount
|
|
1010
|
-
useEffect(() => {
|
|
1011
|
-
lastSeenMessageCountRef.current = messages.length;
|
|
1012
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1013
|
-
}, []);
|
|
1014
|
-
|
|
1015
|
-
// Detect auth revocation messages and show notification
|
|
1016
|
-
useEffect(() => {
|
|
1017
|
-
if (!data?.messages) return;
|
|
1018
|
-
|
|
1019
|
-
for (const msg of data.messages) {
|
|
1020
|
-
// Check for auth_revoked control messages
|
|
1021
|
-
if (msg.content?.includes('auth_revoked') || msg.content?.includes('authentication_error')) {
|
|
1022
|
-
try {
|
|
1023
|
-
const parsed = JSON.parse(msg.content);
|
|
1024
|
-
if (parsed.type === 'auth_revoked' && parsed.agent) {
|
|
1025
|
-
const agentName = parsed.agent;
|
|
1026
|
-
if (!authRevokedAgents.has(agentName)) {
|
|
1027
|
-
setAuthRevokedAgents(prev => new Set([...prev, agentName]));
|
|
1028
|
-
addToast({
|
|
1029
|
-
type: 'error',
|
|
1030
|
-
title: 'Authentication Expired',
|
|
1031
|
-
message: `${agentName}'s API credentials have expired. Please reconnect.`,
|
|
1032
|
-
agentName,
|
|
1033
|
-
duration: 0, // Don't auto-dismiss
|
|
1034
|
-
action: {
|
|
1035
|
-
label: 'Reconnect',
|
|
1036
|
-
onClick: () => {
|
|
1037
|
-
window.location.href = '/providers';
|
|
1038
|
-
},
|
|
1039
|
-
},
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
} catch {
|
|
1044
|
-
// Not JSON, check for plain text auth error patterns
|
|
1045
|
-
if (msg.content?.includes('OAuth token') && msg.content?.includes('expired')) {
|
|
1046
|
-
const agentName = msg.from;
|
|
1047
|
-
if (agentName && !authRevokedAgents.has(agentName)) {
|
|
1048
|
-
setAuthRevokedAgents(prev => new Set([...prev, agentName]));
|
|
1049
|
-
addToast({
|
|
1050
|
-
type: 'error',
|
|
1051
|
-
title: 'Authentication Expired',
|
|
1052
|
-
message: `${agentName}'s API credentials have expired. Please reconnect.`,
|
|
1053
|
-
agentName,
|
|
1054
|
-
duration: 0,
|
|
1055
|
-
action: {
|
|
1056
|
-
label: 'Reconnect',
|
|
1057
|
-
onClick: () => {
|
|
1058
|
-
window.location.href = '/providers';
|
|
1059
|
-
},
|
|
1060
|
-
},
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
}, [data?.messages, authRevokedAgents, addToast]);
|
|
1068
|
-
|
|
1069
|
-
// Check if fleet view is available
|
|
1070
|
-
const isFleetAvailable = Boolean(data?.fleet?.servers?.length) || workspaces.length > 0;
|
|
1071
|
-
|
|
1072
|
-
// Convert workspaces/repos to projects for unified navigation
|
|
1073
|
-
useEffect(() => {
|
|
1074
|
-
if (workspaces.length > 0) {
|
|
1075
|
-
// If we have repos for the active workspace, show each repo as a project folder
|
|
1076
|
-
if (workspaceRepos.length > 1 && effectiveActiveWorkspaceId) {
|
|
1077
|
-
const projectList: Project[] = workspaceRepos.map((repo) => ({
|
|
1078
|
-
id: repo.id,
|
|
1079
|
-
path: repo.githubFullName,
|
|
1080
|
-
name: repo.githubFullName.split('/').pop() || repo.githubFullName,
|
|
1081
|
-
agents: orchestratorAgents
|
|
1082
|
-
.filter((a) => a.workspaceId === effectiveActiveWorkspaceId)
|
|
1083
|
-
.map((a) => ({
|
|
1084
|
-
name: a.name,
|
|
1085
|
-
status: a.status === 'running' ? 'online' : 'offline',
|
|
1086
|
-
isSpawned: true,
|
|
1087
|
-
cli: a.provider,
|
|
1088
|
-
})) as Agent[],
|
|
1089
|
-
lead: undefined,
|
|
1090
|
-
}));
|
|
1091
|
-
setProjects(projectList);
|
|
1092
|
-
// Set first repo as current if none selected
|
|
1093
|
-
if (!currentProject || !projectList.find(p => p.id === currentProject)) {
|
|
1094
|
-
setCurrentProject(projectList[0]?.id);
|
|
1095
|
-
}
|
|
1096
|
-
} else {
|
|
1097
|
-
// Single repo or no repos fetched yet - show workspace as single project
|
|
1098
|
-
const projectList: Project[] = workspaces.map((workspace) => ({
|
|
1099
|
-
id: workspace.id,
|
|
1100
|
-
path: workspace.path,
|
|
1101
|
-
name: workspace.name,
|
|
1102
|
-
agents: orchestratorAgents
|
|
1103
|
-
.filter((a) => a.workspaceId === workspace.id)
|
|
1104
|
-
.map((a) => ({
|
|
1105
|
-
name: a.name,
|
|
1106
|
-
status: a.status === 'running' ? 'online' : 'offline',
|
|
1107
|
-
isSpawned: true,
|
|
1108
|
-
cli: a.provider,
|
|
1109
|
-
})) as Agent[],
|
|
1110
|
-
lead: undefined,
|
|
1111
|
-
}));
|
|
1112
|
-
setProjects(projectList);
|
|
1113
|
-
setCurrentProject(activeWorkspaceId);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
}, [workspaces, orchestratorAgents, activeWorkspaceId, workspaceRepos, effectiveActiveWorkspaceId, currentProject]);
|
|
1117
|
-
|
|
1118
|
-
// Fetch bridge/project data for multi-project mode
|
|
1119
|
-
useEffect(() => {
|
|
1120
|
-
if (workspaces.length > 0) return; // Skip if using orchestrator
|
|
1121
|
-
|
|
1122
|
-
const fetchProjects = async () => {
|
|
1123
|
-
const result = await api.getBridgeData();
|
|
1124
|
-
if (result.success && result.data) {
|
|
1125
|
-
// Bridge data returns { projects, messages, connected }
|
|
1126
|
-
const bridgeData = result.data as {
|
|
1127
|
-
projects?: Array<{
|
|
1128
|
-
id: string;
|
|
1129
|
-
name?: string;
|
|
1130
|
-
path: string;
|
|
1131
|
-
connected?: boolean;
|
|
1132
|
-
agents?: Array<{ name: string; status: string; task?: string; cli?: string }>;
|
|
1133
|
-
lead?: { name: string; connected: boolean };
|
|
1134
|
-
}>;
|
|
1135
|
-
connected?: boolean;
|
|
1136
|
-
currentProjectPath?: string;
|
|
1137
|
-
};
|
|
1138
|
-
|
|
1139
|
-
if (bridgeData.projects && bridgeData.projects.length > 0) {
|
|
1140
|
-
const projectList: Project[] = bridgeData.projects.map((p) => ({
|
|
1141
|
-
id: p.id,
|
|
1142
|
-
path: p.path,
|
|
1143
|
-
name: p.name || p.path.split('/').pop(),
|
|
1144
|
-
agents: (p.agents || []).map((a) => ({
|
|
1145
|
-
name: a.name,
|
|
1146
|
-
status: a.status === 'online' || a.status === 'active' ? 'online' : 'offline',
|
|
1147
|
-
currentTask: a.task,
|
|
1148
|
-
cli: a.cli,
|
|
1149
|
-
})) as Agent[],
|
|
1150
|
-
lead: p.lead,
|
|
1151
|
-
}));
|
|
1152
|
-
setProjects(projectList);
|
|
1153
|
-
// Set first project as current if none selected
|
|
1154
|
-
if (!currentProject && projectList.length > 0) {
|
|
1155
|
-
setCurrentProject(projectList[0].id);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
};
|
|
1160
|
-
|
|
1161
|
-
// Fetch immediately on mount
|
|
1162
|
-
fetchProjects();
|
|
1163
|
-
// Poll for updates
|
|
1164
|
-
const interval = setInterval(fetchProjects, 5000);
|
|
1165
|
-
return () => clearInterval(interval);
|
|
1166
|
-
}, [workspaces.length, currentProject]);
|
|
1167
|
-
|
|
1168
|
-
// Bridge-level agents (like Architect) that should be shown separately
|
|
1169
|
-
const BRIDGE_AGENT_NAMES = ['architect'];
|
|
1170
|
-
|
|
1171
|
-
// Separate bridge-level agents from regular project agents
|
|
1172
|
-
const { bridgeAgents, projectAgents } = useMemo(() => {
|
|
1173
|
-
const bridge: Agent[] = [];
|
|
1174
|
-
const project: Agent[] = [];
|
|
1175
|
-
|
|
1176
|
-
for (const agent of agents) {
|
|
1177
|
-
if (BRIDGE_AGENT_NAMES.includes(agent.name.toLowerCase())) {
|
|
1178
|
-
bridge.push(agent);
|
|
1179
|
-
} else {
|
|
1180
|
-
project.push(agent);
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
return { bridgeAgents: bridge, projectAgents: project };
|
|
1185
|
-
}, [agents]);
|
|
1186
|
-
|
|
1187
|
-
// Merge local daemon agents into their project when we have bridge projects
|
|
1188
|
-
// This prevents agents from appearing under "Local" instead of their project folder
|
|
1189
|
-
const mergedProjects = useMemo(() => {
|
|
1190
|
-
if (projects.length === 0) return projects;
|
|
1191
|
-
|
|
1192
|
-
// Get local agent names (excluding bridge agents)
|
|
1193
|
-
const localAgentNames = new Set(projectAgents.map((a) => a.name.toLowerCase()));
|
|
1194
|
-
if (localAgentNames.size === 0) return projects;
|
|
1195
|
-
|
|
1196
|
-
// Find the current project (the one whose daemon we're connected to)
|
|
1197
|
-
// This is typically the first project or the one marked as current
|
|
1198
|
-
return projects.map((project, index) => {
|
|
1199
|
-
// Merge local agents into the current/first project
|
|
1200
|
-
// Local agents should appear in their actual project, not "Local"
|
|
1201
|
-
const isCurrentDaemonProject = index === 0 || project.id === currentProject;
|
|
1202
|
-
|
|
1203
|
-
if (isCurrentDaemonProject) {
|
|
1204
|
-
// Merge local agents with project agents, avoiding duplicates
|
|
1205
|
-
const existingNames = new Set(project.agents.map((a) => a.name.toLowerCase()));
|
|
1206
|
-
const newAgents = projectAgents.filter((a) => !existingNames.has(a.name.toLowerCase()));
|
|
1207
|
-
|
|
1208
|
-
return {
|
|
1209
|
-
...project,
|
|
1210
|
-
agents: [...project.agents, ...newAgents],
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
return project;
|
|
1215
|
-
});
|
|
1216
|
-
}, [projects, projectAgents, currentProject]);
|
|
1217
|
-
|
|
1218
|
-
// Determine if local agents should be shown separately
|
|
1219
|
-
// Only show "Local" folder if we don't have bridge projects to merge them into
|
|
1220
|
-
// But always include human users so they appear in the sidebar for DM
|
|
1221
|
-
const localAgentsForSidebar = useMemo(() => {
|
|
1222
|
-
// Human users should always be shown in sidebar for DM access
|
|
1223
|
-
const humanUsers = projectAgents.filter(a => a.isHuman);
|
|
1224
|
-
|
|
1225
|
-
if (mergedProjects.length > 0) {
|
|
1226
|
-
// Don't show AI agents separately - they're merged into projects
|
|
1227
|
-
// But keep human users visible for DM conversations
|
|
1228
|
-
return humanUsers;
|
|
1229
|
-
}
|
|
1230
|
-
return projectAgents;
|
|
1231
|
-
}, [mergedProjects, projectAgents]);
|
|
1232
|
-
|
|
1233
|
-
// Handle workspace selection
|
|
1234
|
-
const handleWorkspaceSelect = useCallback(async (workspace: Workspace) => {
|
|
1235
|
-
try {
|
|
1236
|
-
await switchWorkspace(workspace.id);
|
|
1237
|
-
} catch (err) {
|
|
1238
|
-
console.error('Failed to switch workspace:', err);
|
|
1239
|
-
}
|
|
1240
|
-
}, [switchWorkspace]);
|
|
1241
|
-
|
|
1242
|
-
// Handle add workspace
|
|
1243
|
-
const handleAddWorkspace = useCallback(async (path: string, name?: string) => {
|
|
1244
|
-
setIsAddingWorkspace(true);
|
|
1245
|
-
setAddWorkspaceError(null);
|
|
1246
|
-
try {
|
|
1247
|
-
await addWorkspace(path, name);
|
|
1248
|
-
setIsAddWorkspaceOpen(false);
|
|
1249
|
-
} catch (err) {
|
|
1250
|
-
setAddWorkspaceError(err instanceof Error ? err.message : 'Failed to add workspace');
|
|
1251
|
-
throw err;
|
|
1252
|
-
} finally {
|
|
1253
|
-
setIsAddingWorkspace(false);
|
|
1254
|
-
}
|
|
1255
|
-
}, [addWorkspace]);
|
|
1256
|
-
|
|
1257
|
-
// Handle project selection (also switches workspace if using orchestrator)
|
|
1258
|
-
const handleProjectSelect = useCallback((project: Project) => {
|
|
1259
|
-
setCurrentProject(project.id);
|
|
1260
|
-
// Switch to DM view mode and clear channel selection
|
|
1261
|
-
setViewMode('local');
|
|
1262
|
-
setSelectedChannelId(undefined);
|
|
1263
|
-
|
|
1264
|
-
// Track as recently accessed
|
|
1265
|
-
addRecentRepo(project);
|
|
1266
|
-
|
|
1267
|
-
// Switch workspace if using orchestrator
|
|
1268
|
-
if (workspaces.length > 0) {
|
|
1269
|
-
switchWorkspace(project.id).catch((err) => {
|
|
1270
|
-
console.error('Failed to switch workspace:', err);
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
if (project.agents.length > 0) {
|
|
1275
|
-
selectAgent(project.agents[0].name);
|
|
1276
|
-
setCurrentChannel(project.agents[0].name);
|
|
1277
|
-
}
|
|
1278
|
-
closeSidebarOnMobile();
|
|
1279
|
-
}, [selectAgent, setCurrentChannel, closeSidebarOnMobile, workspaces.length, switchWorkspace, addRecentRepo]);
|
|
1280
|
-
|
|
1281
|
-
// Handle agent selection
|
|
1282
|
-
const handleAgentSelect = useCallback((agent: Agent) => {
|
|
1283
|
-
// Switch to DM view mode and clear channel selection
|
|
1284
|
-
setViewMode('local');
|
|
1285
|
-
setSelectedChannelId(undefined);
|
|
1286
|
-
selectAgent(agent.name);
|
|
1287
|
-
setCurrentChannel(agent.name);
|
|
1288
|
-
closeSidebarOnMobile();
|
|
1289
|
-
}, [selectAgent, setCurrentChannel, closeSidebarOnMobile]);
|
|
1290
|
-
|
|
1291
|
-
// Handle spawn button click
|
|
1292
|
-
const handleSpawnClick = useCallback(() => {
|
|
1293
|
-
setSpawnError(null);
|
|
1294
|
-
setIsSpawnModalOpen(true);
|
|
1295
|
-
}, []);
|
|
1296
|
-
|
|
1297
|
-
// Handle settings click - opens full settings page
|
|
1298
|
-
const handleSettingsClick = useCallback(() => {
|
|
1299
|
-
setSettingsInitialTab('dashboard');
|
|
1300
|
-
setIsFullSettingsOpen(true);
|
|
1301
|
-
}, []);
|
|
1302
|
-
|
|
1303
|
-
// Handle workspace settings click - opens full settings page with workspace tab
|
|
1304
|
-
const handleWorkspaceSettingsClick = useCallback(() => {
|
|
1305
|
-
setSettingsInitialTab('workspace');
|
|
1306
|
-
setIsFullSettingsOpen(true);
|
|
1307
|
-
}, []);
|
|
1308
|
-
|
|
1309
|
-
// Handle billing click - opens full settings page with billing tab
|
|
1310
|
-
const handleBillingClick = useCallback(() => {
|
|
1311
|
-
setSettingsInitialTab('billing');
|
|
1312
|
-
setIsFullSettingsOpen(true);
|
|
1313
|
-
}, []);
|
|
1314
|
-
|
|
1315
|
-
// Handle history click
|
|
1316
|
-
const handleHistoryClick = useCallback(() => {
|
|
1317
|
-
setIsHistoryOpen(true);
|
|
1318
|
-
}, []);
|
|
1319
|
-
|
|
1320
|
-
// Handle new conversation click
|
|
1321
|
-
const handleNewConversationClick = useCallback(() => {
|
|
1322
|
-
setIsNewConversationOpen(true);
|
|
1323
|
-
}, []);
|
|
1324
|
-
|
|
1325
|
-
// Handle coordinator click
|
|
1326
|
-
const handleCoordinatorClick = useCallback(() => {
|
|
1327
|
-
setIsCoordinatorOpen(true);
|
|
1328
|
-
}, []);
|
|
1329
|
-
|
|
1330
|
-
// Open a DM with a human user from the sidebar
|
|
1331
|
-
const handleHumanSelect = useCallback((human: Agent) => {
|
|
1332
|
-
// Switch to DM view mode and clear channel selection
|
|
1333
|
-
setViewMode('local');
|
|
1334
|
-
setSelectedChannelId(undefined);
|
|
1335
|
-
setCurrentChannel(human.name);
|
|
1336
|
-
markDmSeen(human.name);
|
|
1337
|
-
closeSidebarOnMobile();
|
|
1338
|
-
}, [closeSidebarOnMobile, markDmSeen, setCurrentChannel]);
|
|
1339
|
-
|
|
1340
|
-
// Handle channel member click - switch to DM with that member
|
|
1341
|
-
const handleChannelMemberClick = useCallback((memberId: string, entityType: 'user' | 'agent') => {
|
|
1342
|
-
// Don't navigate to self
|
|
1343
|
-
if (memberId === currentUser?.displayName) return;
|
|
1344
|
-
|
|
1345
|
-
// Switch from channel view to local (DM) view
|
|
1346
|
-
setViewMode('local');
|
|
1347
|
-
setSelectedChannelId(undefined);
|
|
1348
|
-
|
|
1349
|
-
// Select the agent or user
|
|
1350
|
-
if (entityType === 'agent') {
|
|
1351
|
-
selectAgent(memberId);
|
|
1352
|
-
setCurrentChannel(memberId);
|
|
1353
|
-
} else {
|
|
1354
|
-
// For users, just set the channel
|
|
1355
|
-
setCurrentChannel(memberId);
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
closeSidebarOnMobile();
|
|
1359
|
-
}, [currentUser?.displayName, selectAgent, setCurrentChannel, closeSidebarOnMobile]);
|
|
1360
|
-
|
|
1361
|
-
// =============================================================================
|
|
1362
|
-
// Channel V1 Handlers
|
|
1363
|
-
// =============================================================================
|
|
1364
|
-
|
|
1365
|
-
// Default channels that should always be visible - stable reference
|
|
1366
|
-
const defaultChannels = useMemo<Channel[]>(() => [
|
|
1367
|
-
{
|
|
1368
|
-
id: '#general',
|
|
1369
|
-
name: 'general',
|
|
1370
|
-
description: 'General discussion for all agents',
|
|
1371
|
-
visibility: 'public',
|
|
1372
|
-
memberCount: 0,
|
|
1373
|
-
unreadCount: 0,
|
|
1374
|
-
hasMentions: false,
|
|
1375
|
-
createdAt: '2024-01-01T00:00:00.000Z', // Static date for stability
|
|
1376
|
-
status: 'active',
|
|
1377
|
-
createdBy: 'system',
|
|
1378
|
-
isDm: false,
|
|
1379
|
-
},
|
|
1380
|
-
{
|
|
1381
|
-
id: '#engineering',
|
|
1382
|
-
name: 'engineering',
|
|
1383
|
-
description: 'Engineering discussion',
|
|
1384
|
-
visibility: 'public',
|
|
1385
|
-
memberCount: 0,
|
|
1386
|
-
unreadCount: 0,
|
|
1387
|
-
hasMentions: false,
|
|
1388
|
-
createdAt: '2024-01-01T00:00:00.000Z', // Static date for stability
|
|
1389
|
-
status: 'active',
|
|
1390
|
-
createdBy: 'system',
|
|
1391
|
-
isDm: false,
|
|
1392
|
-
},
|
|
1393
|
-
], []);
|
|
1394
|
-
|
|
1395
|
-
// Load channels on mount (they're always visible in sidebar, collapsed by default)
|
|
1396
|
-
useEffect(() => {
|
|
1397
|
-
// Not in cloud mode or no workspace - show default channels only
|
|
1398
|
-
if (!isCloudMode || !effectiveActiveWorkspaceId) {
|
|
1399
|
-
setChannelsList(defaultChannels);
|
|
1400
|
-
setArchivedChannelsList([]);
|
|
1401
|
-
return;
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// Cloud mode with workspace - fetch from API and merge with defaults
|
|
1405
|
-
setChannelsList(defaultChannels);
|
|
1406
|
-
setArchivedChannelsList([]);
|
|
1407
|
-
setIsChannelsLoading(true);
|
|
1408
|
-
|
|
1409
|
-
const fetchChannels = async () => {
|
|
1410
|
-
try {
|
|
1411
|
-
const response = await listChannels(effectiveActiveWorkspaceId);
|
|
1412
|
-
setChannelListsFromResponse(response);
|
|
1413
|
-
} catch (err) {
|
|
1414
|
-
console.error('Failed to fetch channels:', err);
|
|
1415
|
-
} finally {
|
|
1416
|
-
setIsChannelsLoading(false);
|
|
1417
|
-
}
|
|
1418
|
-
};
|
|
1419
|
-
|
|
1420
|
-
fetchChannels();
|
|
1421
|
-
}, [effectiveActiveWorkspaceId, isCloudMode, defaultChannels, setChannelListsFromResponse]);
|
|
1422
|
-
|
|
1423
|
-
// Load messages when a channel is selected (persisted + live)
|
|
1424
|
-
useEffect(() => {
|
|
1425
|
-
if (!selectedChannelId || viewMode !== 'channels') return;
|
|
1426
|
-
|
|
1427
|
-
// Check if we already have messages cached
|
|
1428
|
-
const existing = channelMessageMap[selectedChannelId] ?? [];
|
|
1429
|
-
if (existing.length > 0) {
|
|
1430
|
-
setChannelMessages(existing);
|
|
1431
|
-
setHasMoreMessages(false);
|
|
1432
|
-
} else if (!fetchedChannelsRef.current.has(selectedChannelId)) {
|
|
1433
|
-
// Only fetch if we haven't already fetched this channel (prevents infinite loop)
|
|
1434
|
-
fetchedChannelsRef.current.add(selectedChannelId);
|
|
1435
|
-
(async () => {
|
|
1436
|
-
try {
|
|
1437
|
-
const response = await getMessages(effectiveActiveWorkspaceId || 'local', selectedChannelId, { limit: 200 });
|
|
1438
|
-
setChannelMessageMap(prev => ({ ...prev, [selectedChannelId]: response.messages }));
|
|
1439
|
-
setChannelMessages(response.messages);
|
|
1440
|
-
setHasMoreMessages(response.hasMore);
|
|
1441
|
-
} catch (err) {
|
|
1442
|
-
console.error('Failed to fetch channel messages:', err);
|
|
1443
|
-
setChannelMessages([]);
|
|
1444
|
-
setHasMoreMessages(false);
|
|
1445
|
-
}
|
|
1446
|
-
})();
|
|
1447
|
-
} else {
|
|
1448
|
-
// Already fetched but no messages - show empty state
|
|
1449
|
-
setChannelMessages([]);
|
|
1450
|
-
setHasMoreMessages(false);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
setChannelUnreadState(undefined);
|
|
1454
|
-
setChannelsList(prev =>
|
|
1455
|
-
prev.map(c =>
|
|
1456
|
-
c.id === selectedChannelId ? { ...c, unreadCount: 0, hasMentions: false } : c
|
|
1457
|
-
)
|
|
1458
|
-
);
|
|
1459
|
-
}, [selectedChannelId, viewMode, effectiveActiveWorkspaceId]); // Removed channelMessageMap to prevent infinite loop
|
|
1460
|
-
|
|
1461
|
-
// Channel selection handler - also joins the channel in local mode
|
|
1462
|
-
const handleSelectChannel = useCallback(async (channel: Channel) => {
|
|
1463
|
-
setSelectedChannelId(channel.id);
|
|
1464
|
-
closeSidebarOnMobile();
|
|
1465
|
-
|
|
1466
|
-
// Join the channel via the daemon (needed for local mode)
|
|
1467
|
-
// This ensures the user is a member before sending messages
|
|
1468
|
-
try {
|
|
1469
|
-
const { joinChannel: joinChannelApi } = await import('./channels');
|
|
1470
|
-
await joinChannelApi(effectiveActiveWorkspaceId || 'local', channel.id);
|
|
1471
|
-
} catch (err) {
|
|
1472
|
-
console.error('Failed to join channel:', err);
|
|
1473
|
-
}
|
|
1474
|
-
}, [closeSidebarOnMobile, effectiveActiveWorkspaceId]);
|
|
1475
|
-
|
|
1476
|
-
// Create channel handler - opens the create channel modal
|
|
1477
|
-
const handleCreateChannel = useCallback(() => {
|
|
1478
|
-
setIsCreateChannelOpen(true);
|
|
1479
|
-
}, []);
|
|
1480
|
-
|
|
1481
|
-
// Handler for creating a new channel via API
|
|
1482
|
-
const handleCreateChannelSubmit = useCallback(async (request: CreateChannelRequest) => {
|
|
1483
|
-
if (!effectiveActiveWorkspaceId) return;
|
|
1484
|
-
setIsCreatingChannel(true);
|
|
1485
|
-
try {
|
|
1486
|
-
const result = await createChannel(effectiveActiveWorkspaceId, request);
|
|
1487
|
-
// Refresh channels list after successful creation
|
|
1488
|
-
const response = await listChannels(effectiveActiveWorkspaceId);
|
|
1489
|
-
setChannelListsFromResponse(response);
|
|
1490
|
-
if (result.channel?.id) {
|
|
1491
|
-
setSelectedChannelId(result.channel.id);
|
|
1492
|
-
}
|
|
1493
|
-
setIsCreateChannelOpen(false);
|
|
1494
|
-
} catch (err) {
|
|
1495
|
-
console.error('Failed to create channel:', err);
|
|
1496
|
-
// Keep modal open on error so user can retry
|
|
1497
|
-
} finally {
|
|
1498
|
-
setIsCreatingChannel(false);
|
|
1499
|
-
}
|
|
1500
|
-
}, [effectiveActiveWorkspaceId]);
|
|
1501
|
-
|
|
1502
|
-
// Handler for opening the invite to channel modal
|
|
1503
|
-
const handleInviteToChannel = useCallback((channel: Channel) => {
|
|
1504
|
-
setInviteChannelTarget(channel);
|
|
1505
|
-
setIsInviteChannelOpen(true);
|
|
1506
|
-
}, []);
|
|
1507
|
-
|
|
1508
|
-
// Handler for inviting members to a channel
|
|
1509
|
-
// Note: InviteToChannelModal is given agents as availableMembers, so all invitees are agents
|
|
1510
|
-
const handleInviteSubmit = useCallback(async (members: string[]) => {
|
|
1511
|
-
if (!inviteChannelTarget) return;
|
|
1512
|
-
setIsInvitingToChannel(true);
|
|
1513
|
-
try {
|
|
1514
|
-
// Call the invite API endpoint with CSRF token
|
|
1515
|
-
const csrfToken = getCsrfToken();
|
|
1516
|
-
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
1517
|
-
if (csrfToken) {
|
|
1518
|
-
headers['X-CSRF-Token'] = csrfToken;
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
// Send invites with type info - all members from invite modal are agents
|
|
1522
|
-
const invites = members.map(name => ({ id: name, type: 'agent' as const }));
|
|
1523
|
-
|
|
1524
|
-
const response = await fetch('/api/channels/invite', {
|
|
1525
|
-
method: 'POST',
|
|
1526
|
-
headers,
|
|
1527
|
-
credentials: 'include',
|
|
1528
|
-
body: JSON.stringify({
|
|
1529
|
-
channel: inviteChannelTarget.name,
|
|
1530
|
-
invites,
|
|
1531
|
-
workspaceId: effectiveActiveWorkspaceId,
|
|
1532
|
-
}),
|
|
1533
|
-
});
|
|
1534
|
-
if (!response.ok) {
|
|
1535
|
-
throw new Error('Failed to invite members');
|
|
1536
|
-
}
|
|
1537
|
-
setIsInviteChannelOpen(false);
|
|
1538
|
-
setInviteChannelTarget(null);
|
|
1539
|
-
} catch (err) {
|
|
1540
|
-
console.error('Failed to invite to channel:', err);
|
|
1541
|
-
} finally {
|
|
1542
|
-
setIsInvitingToChannel(false);
|
|
1543
|
-
}
|
|
1544
|
-
}, [inviteChannelTarget, effectiveActiveWorkspaceId]);
|
|
1545
|
-
|
|
1546
|
-
// Join channel handler
|
|
1547
|
-
const handleJoinChannel = useCallback(async (channelId: string) => {
|
|
1548
|
-
if (!effectiveActiveWorkspaceId) return;
|
|
1549
|
-
try {
|
|
1550
|
-
const { joinChannel } = await import('./channels');
|
|
1551
|
-
await joinChannel(effectiveActiveWorkspaceId, channelId);
|
|
1552
|
-
// Refresh channels list
|
|
1553
|
-
const response = await listChannels(effectiveActiveWorkspaceId);
|
|
1554
|
-
setChannelListsFromResponse(response);
|
|
1555
|
-
} catch (err) {
|
|
1556
|
-
console.error('Failed to join channel:', err);
|
|
1557
|
-
}
|
|
1558
|
-
}, [effectiveActiveWorkspaceId, setChannelListsFromResponse]);
|
|
1559
|
-
|
|
1560
|
-
// Leave channel handler
|
|
1561
|
-
const handleLeaveChannel = useCallback(async (channel: Channel) => {
|
|
1562
|
-
if (!effectiveActiveWorkspaceId) return;
|
|
1563
|
-
try {
|
|
1564
|
-
const { leaveChannel } = await import('./channels');
|
|
1565
|
-
await leaveChannel(effectiveActiveWorkspaceId, channel.id);
|
|
1566
|
-
// Clear selection if leaving current channel
|
|
1567
|
-
if (selectedChannelId === channel.id) {
|
|
1568
|
-
setSelectedChannelId(undefined);
|
|
1569
|
-
}
|
|
1570
|
-
// Refresh channels list
|
|
1571
|
-
const response = await listChannels(effectiveActiveWorkspaceId);
|
|
1572
|
-
setChannelListsFromResponse(response);
|
|
1573
|
-
} catch (err) {
|
|
1574
|
-
console.error('Failed to leave channel:', err);
|
|
1575
|
-
}
|
|
1576
|
-
}, [effectiveActiveWorkspaceId, selectedChannelId, setChannelListsFromResponse]);
|
|
1577
|
-
|
|
1578
|
-
// Show members panel handler
|
|
1579
|
-
const handleShowMembers = useCallback(async () => {
|
|
1580
|
-
if (!selectedChannel || !effectiveActiveWorkspaceId) return;
|
|
1581
|
-
try {
|
|
1582
|
-
const members = await getChannelMembers(effectiveActiveWorkspaceId, selectedChannel.id);
|
|
1583
|
-
setChannelMembers(members);
|
|
1584
|
-
setShowMemberPanel(true);
|
|
1585
|
-
} catch (err) {
|
|
1586
|
-
console.error('Failed to load channel members:', err);
|
|
1587
|
-
}
|
|
1588
|
-
}, [selectedChannel, effectiveActiveWorkspaceId]);
|
|
1589
|
-
|
|
1590
|
-
// Remove member handler
|
|
1591
|
-
const handleRemoveMember = useCallback(async (memberId: string, memberType: 'user' | 'agent') => {
|
|
1592
|
-
if (!selectedChannel || !effectiveActiveWorkspaceId) return;
|
|
1593
|
-
try {
|
|
1594
|
-
await removeChannelMember(effectiveActiveWorkspaceId, selectedChannel.id, memberId, memberType);
|
|
1595
|
-
// Refresh members list
|
|
1596
|
-
const members = await getChannelMembers(effectiveActiveWorkspaceId, selectedChannel.id);
|
|
1597
|
-
setChannelMembers(members);
|
|
1598
|
-
} catch (err) {
|
|
1599
|
-
console.error('Failed to remove member:', err);
|
|
1600
|
-
}
|
|
1601
|
-
}, [selectedChannel, effectiveActiveWorkspaceId]);
|
|
1602
|
-
|
|
1603
|
-
// Add member handler (for MemberManagementPanel)
|
|
1604
|
-
const handleAddMember = useCallback(async (memberId: string, memberType: 'user' | 'agent', _role: 'admin' | 'member' | 'read_only') => {
|
|
1605
|
-
if (!selectedChannel || !effectiveActiveWorkspaceId) return;
|
|
1606
|
-
try {
|
|
1607
|
-
const csrfToken = getCsrfToken();
|
|
1608
|
-
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
1609
|
-
if (csrfToken) {
|
|
1610
|
-
headers['X-CSRF-Token'] = csrfToken;
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
const response = await fetch('/api/channels/invite', {
|
|
1614
|
-
method: 'POST',
|
|
1615
|
-
headers,
|
|
1616
|
-
credentials: 'include',
|
|
1617
|
-
body: JSON.stringify({
|
|
1618
|
-
channel: selectedChannel.name,
|
|
1619
|
-
invites: [{ id: memberId, type: memberType }],
|
|
1620
|
-
workspaceId: effectiveActiveWorkspaceId,
|
|
1621
|
-
}),
|
|
1622
|
-
});
|
|
1623
|
-
|
|
1624
|
-
if (!response.ok) {
|
|
1625
|
-
throw new Error('Failed to add member');
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
// Refresh members list
|
|
1629
|
-
const members = await getChannelMembers(effectiveActiveWorkspaceId, selectedChannel.id);
|
|
1630
|
-
setChannelMembers(members);
|
|
1631
|
-
} catch (err) {
|
|
1632
|
-
console.error('Failed to add member:', err);
|
|
1633
|
-
}
|
|
1634
|
-
}, [selectedChannel, effectiveActiveWorkspaceId]);
|
|
1635
|
-
|
|
1636
|
-
// Archive channel handler
|
|
1637
|
-
const handleArchiveChannel = useCallback(async (channel: Channel) => {
|
|
1638
|
-
if (!effectiveActiveWorkspaceId) return;
|
|
1639
|
-
try {
|
|
1640
|
-
const { archiveChannel } = await import('./channels');
|
|
1641
|
-
await archiveChannel(effectiveActiveWorkspaceId, channel.id);
|
|
1642
|
-
// Clear selection if archiving current channel
|
|
1643
|
-
if (selectedChannelId === channel.id) {
|
|
1644
|
-
setSelectedChannelId(undefined);
|
|
1645
|
-
}
|
|
1646
|
-
// Refresh channels list
|
|
1647
|
-
const response = await listChannels(effectiveActiveWorkspaceId);
|
|
1648
|
-
setChannelListsFromResponse(response);
|
|
1649
|
-
} catch (err) {
|
|
1650
|
-
console.error('Failed to archive channel:', err);
|
|
1651
|
-
}
|
|
1652
|
-
}, [effectiveActiveWorkspaceId, selectedChannelId, setChannelListsFromResponse]);
|
|
1653
|
-
|
|
1654
|
-
// Unarchive channel handler
|
|
1655
|
-
const handleUnarchiveChannel = useCallback(async (channel: Channel) => {
|
|
1656
|
-
if (!effectiveActiveWorkspaceId) return;
|
|
1657
|
-
try {
|
|
1658
|
-
const { unarchiveChannel } = await import('./channels');
|
|
1659
|
-
await unarchiveChannel(effectiveActiveWorkspaceId, channel.id);
|
|
1660
|
-
// Refresh channels list
|
|
1661
|
-
const response = await listChannels(effectiveActiveWorkspaceId);
|
|
1662
|
-
setChannelListsFromResponse(response);
|
|
1663
|
-
} catch (err) {
|
|
1664
|
-
console.error('Failed to unarchive channel:', err);
|
|
1665
|
-
}
|
|
1666
|
-
}, [effectiveActiveWorkspaceId, setChannelListsFromResponse]);
|
|
1667
|
-
|
|
1668
|
-
// Send message to channel handler
|
|
1669
|
-
const handleSendChannelMessage = useCallback(async (content: string, threadId?: string) => {
|
|
1670
|
-
if (!selectedChannelId) return;
|
|
1671
|
-
|
|
1672
|
-
const senderName = currentUser?.displayName || 'Dashboard';
|
|
1673
|
-
const optimisticMessage: ChannelApiMessage = {
|
|
1674
|
-
id: `local-${Date.now()}`,
|
|
1675
|
-
channelId: selectedChannelId,
|
|
1676
|
-
from: senderName,
|
|
1677
|
-
fromEntityType: 'user',
|
|
1678
|
-
content,
|
|
1679
|
-
timestamp: new Date().toISOString(),
|
|
1680
|
-
threadId,
|
|
1681
|
-
isRead: true,
|
|
1682
|
-
};
|
|
1683
|
-
|
|
1684
|
-
// Optimistic append; daemon will echo back via WS
|
|
1685
|
-
appendChannelMessage(selectedChannelId, optimisticMessage, { incrementUnread: false });
|
|
1686
|
-
|
|
1687
|
-
try {
|
|
1688
|
-
await sendChannelApiMessage(
|
|
1689
|
-
effectiveActiveWorkspaceId || 'local',
|
|
1690
|
-
selectedChannelId,
|
|
1691
|
-
{ content, threadId }
|
|
1692
|
-
);
|
|
1693
|
-
} catch (err) {
|
|
1694
|
-
console.error('Failed to send channel message:', err);
|
|
1695
|
-
}
|
|
1696
|
-
}, [effectiveActiveWorkspaceId, selectedChannelId, currentUser?.displayName, appendChannelMessage]);
|
|
1697
|
-
|
|
1698
|
-
// Load more messages (pagination) handler
|
|
1699
|
-
const handleLoadMoreMessages = useCallback(async () => {
|
|
1700
|
-
// Pagination not yet supported for daemon channels
|
|
1701
|
-
return;
|
|
1702
|
-
}, []);
|
|
1703
|
-
|
|
1704
|
-
// Mark channel as read handler (with debouncing via useRef)
|
|
1705
|
-
const markReadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
1706
|
-
const handleMarkChannelRead = useCallback((channelId: string) => {
|
|
1707
|
-
if (!effectiveActiveWorkspaceId) return;
|
|
1708
|
-
|
|
1709
|
-
// Clear existing timeout to debounce
|
|
1710
|
-
if (markReadTimeoutRef.current) {
|
|
1711
|
-
clearTimeout(markReadTimeoutRef.current);
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
// Debounce the markRead call (500ms delay)
|
|
1715
|
-
markReadTimeoutRef.current = setTimeout(async () => {
|
|
1716
|
-
try {
|
|
1717
|
-
await markRead(effectiveActiveWorkspaceId, channelId);
|
|
1718
|
-
// Update local unread state
|
|
1719
|
-
setChannelUnreadState(undefined);
|
|
1720
|
-
// Update channel list unread counts
|
|
1721
|
-
setChannelsList(prev => prev.map(c =>
|
|
1722
|
-
c.id === channelId ? { ...c, unreadCount: 0, hasMentions: false } : c
|
|
1723
|
-
));
|
|
1724
|
-
} catch (err) {
|
|
1725
|
-
console.error('Failed to mark channel as read:', err);
|
|
1726
|
-
}
|
|
1727
|
-
}, 500);
|
|
1728
|
-
}, [effectiveActiveWorkspaceId]);
|
|
1729
|
-
|
|
1730
|
-
// Auto-mark channel as read when viewing it
|
|
1731
|
-
useEffect(() => {
|
|
1732
|
-
if (!selectedChannelId || !channelUnreadState || channelUnreadState.count === 0) return;
|
|
1733
|
-
if (viewMode !== 'channels') return;
|
|
1734
|
-
|
|
1735
|
-
// Mark as read when channel is viewed and has unread messages
|
|
1736
|
-
handleMarkChannelRead(selectedChannelId);
|
|
1737
|
-
}, [selectedChannelId, channelUnreadState, viewMode, handleMarkChannelRead]);
|
|
1738
|
-
|
|
1739
|
-
// Cleanup markRead timeout on unmount
|
|
1740
|
-
useEffect(() => {
|
|
1741
|
-
return () => {
|
|
1742
|
-
if (markReadTimeoutRef.current) {
|
|
1743
|
-
clearTimeout(markReadTimeoutRef.current);
|
|
1744
|
-
}
|
|
1745
|
-
};
|
|
1746
|
-
}, []);
|
|
1747
|
-
|
|
1748
|
-
const handleDmAgentToggle = useCallback((agentName: string) => {
|
|
1749
|
-
if (!currentHuman) return;
|
|
1750
|
-
const humanName = currentHuman.name;
|
|
1751
|
-
const isSelected = (dmSelectedAgentsByHuman[humanName] ?? []).includes(agentName);
|
|
1752
|
-
|
|
1753
|
-
setDmSelectedAgentsByHuman((prev) => {
|
|
1754
|
-
const currentList = prev[humanName] ?? [];
|
|
1755
|
-
const nextList = isSelected
|
|
1756
|
-
? currentList.filter((a) => a !== agentName)
|
|
1757
|
-
: [...currentList, agentName];
|
|
1758
|
-
return { ...prev, [humanName]: nextList };
|
|
1759
|
-
});
|
|
1760
|
-
|
|
1761
|
-
setDmRemovedAgentsByHuman((prev) => {
|
|
1762
|
-
const currentList = prev[humanName] ?? [];
|
|
1763
|
-
if (isSelected) {
|
|
1764
|
-
// Mark as removed so derived participants don't auto-readd
|
|
1765
|
-
return currentList.includes(agentName)
|
|
1766
|
-
? prev
|
|
1767
|
-
: { ...prev, [humanName]: [...currentList, agentName] };
|
|
1768
|
-
}
|
|
1769
|
-
// Re-adding clears removal
|
|
1770
|
-
return { ...prev, [humanName]: currentList.filter((a) => a !== agentName) };
|
|
1771
|
-
});
|
|
1772
|
-
}, [currentHuman, dmSelectedAgentsByHuman]);
|
|
1773
|
-
|
|
1774
|
-
const handleDmSend = useCallback(async (content: string, attachmentIds?: string[]): Promise<boolean> => {
|
|
1775
|
-
if (!currentHuman) return false;
|
|
1776
|
-
const humanName = currentHuman.name;
|
|
1777
|
-
|
|
1778
|
-
// Always send to the human
|
|
1779
|
-
await sendMessage(humanName, content, undefined, attachmentIds);
|
|
1780
|
-
|
|
1781
|
-
// Only send to agents if they were explicitly selected for this conversation
|
|
1782
|
-
// Don't send to agents in pure 1:1 human conversations
|
|
1783
|
-
if (selectedDmAgents.length > 0) {
|
|
1784
|
-
for (const agent of selectedDmAgents) {
|
|
1785
|
-
await sendMessage(agent, content, undefined, attachmentIds);
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
return true;
|
|
1790
|
-
}, [currentHuman, selectedDmAgents, sendMessage]);
|
|
1791
|
-
|
|
1792
|
-
const handleMainComposerSend = useCallback(
|
|
1793
|
-
async (content: string, attachmentIds?: string[]) => {
|
|
1794
|
-
const recipient = currentChannel === 'general' ? '*' : currentChannel;
|
|
1795
|
-
|
|
1796
|
-
if (currentHuman) {
|
|
1797
|
-
return handleDmSend(content, attachmentIds);
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
return sendMessage(recipient, content, undefined, attachmentIds);
|
|
1801
|
-
},
|
|
1802
|
-
[currentChannel, currentHuman, handleDmSend, sendMessage]
|
|
1803
|
-
);
|
|
1804
|
-
|
|
1805
|
-
const dmInviteCommands = useMemo(() => {
|
|
1806
|
-
if (!currentHuman) return [];
|
|
1807
|
-
return agents
|
|
1808
|
-
.filter((a) => !a.isHuman)
|
|
1809
|
-
.map((agent) => {
|
|
1810
|
-
const isSelected = (dmSelectedAgentsByHuman[currentHuman.name] ?? []).includes(agent.name);
|
|
1811
|
-
return {
|
|
1812
|
-
id: `dm-toggle-${currentHuman.name}-${agent.name}`,
|
|
1813
|
-
label: `${isSelected ? 'Remove' : 'Invite'} ${agent.name} in DM`,
|
|
1814
|
-
description: `DM with ${currentHuman.name}`,
|
|
1815
|
-
category: 'actions' as const,
|
|
1816
|
-
action: () => handleDmAgentToggle(agent.name),
|
|
1817
|
-
};
|
|
1818
|
-
});
|
|
1819
|
-
}, [agents, currentHuman, dmSelectedAgentsByHuman, handleDmAgentToggle]);
|
|
1820
|
-
|
|
1821
|
-
// Channel commands for command palette
|
|
1822
|
-
const channelCommands = useMemo(() => {
|
|
1823
|
-
const commands: Array<{
|
|
1824
|
-
id: string;
|
|
1825
|
-
label: string;
|
|
1826
|
-
description?: string;
|
|
1827
|
-
category: 'channels';
|
|
1828
|
-
shortcut?: string;
|
|
1829
|
-
action: () => void;
|
|
1830
|
-
}> = [];
|
|
1831
|
-
|
|
1832
|
-
// Switch to channels view
|
|
1833
|
-
commands.push({
|
|
1834
|
-
id: 'channels-view',
|
|
1835
|
-
label: 'Go to Channels',
|
|
1836
|
-
description: 'Switch to channel messaging view',
|
|
1837
|
-
category: 'channels',
|
|
1838
|
-
shortcut: '⌘⇧C',
|
|
1839
|
-
action: () => {
|
|
1840
|
-
setViewMode('channels');
|
|
1841
|
-
},
|
|
1842
|
-
});
|
|
1843
|
-
|
|
1844
|
-
// Create new channel
|
|
1845
|
-
commands.push({
|
|
1846
|
-
id: 'channels-create',
|
|
1847
|
-
label: 'Create Channel',
|
|
1848
|
-
description: 'Create a new messaging channel',
|
|
1849
|
-
category: 'channels',
|
|
1850
|
-
action: () => {
|
|
1851
|
-
setViewMode('channels');
|
|
1852
|
-
handleCreateChannel();
|
|
1853
|
-
},
|
|
1854
|
-
});
|
|
1855
|
-
|
|
1856
|
-
// Add each channel as a quick-switch command
|
|
1857
|
-
channelsList.forEach((channel) => {
|
|
1858
|
-
const unreadBadge = channel.unreadCount > 0 ? ` (${channel.unreadCount} unread)` : '';
|
|
1859
|
-
commands.push({
|
|
1860
|
-
id: `channel-switch-${channel.id}`,
|
|
1861
|
-
label: channel.isDm ? `@${channel.name}` : `#${channel.name}`,
|
|
1862
|
-
description: channel.description || `Switch to ${channel.isDm ? 'DM' : 'channel'}${unreadBadge}`,
|
|
1863
|
-
category: 'channels',
|
|
1864
|
-
action: () => {
|
|
1865
|
-
setViewMode('channels');
|
|
1866
|
-
setSelectedChannelId(channel.id);
|
|
1867
|
-
},
|
|
1868
|
-
});
|
|
1869
|
-
});
|
|
1870
|
-
|
|
1871
|
-
return commands;
|
|
1872
|
-
}, [channelsList, handleCreateChannel]);
|
|
1873
|
-
|
|
1874
|
-
// Handle send from new conversation modal - select the channel after sending
|
|
1875
|
-
const handleNewConversationSend = useCallback(async (to: string, content: string): Promise<boolean> => {
|
|
1876
|
-
const success = await sendMessage(to, content);
|
|
1877
|
-
if (success) {
|
|
1878
|
-
// Switch to the channel we just messaged
|
|
1879
|
-
if (to === '*') {
|
|
1880
|
-
selectAgent(null);
|
|
1881
|
-
setSelectedChannelId(ACTIVITY_FEED_ID);
|
|
1882
|
-
setViewMode('channels');
|
|
1883
|
-
} else {
|
|
1884
|
-
const targetAgent = agents.find((a) => a.name === to);
|
|
1885
|
-
if (targetAgent) {
|
|
1886
|
-
selectAgent(targetAgent.name);
|
|
1887
|
-
setCurrentChannel(targetAgent.name);
|
|
1888
|
-
} else {
|
|
1889
|
-
setCurrentChannel(to);
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
return success;
|
|
1894
|
-
}, [sendMessage, selectAgent, setCurrentChannel, agents]);
|
|
1895
|
-
|
|
1896
|
-
// Handle server reconnect (restart workspace)
|
|
1897
|
-
const handleServerReconnect = useCallback(async (serverId: string) => {
|
|
1898
|
-
if (isCloudMode) {
|
|
1899
|
-
try {
|
|
1900
|
-
const result = await cloudApi.restartWorkspace(serverId);
|
|
1901
|
-
if (result.success) {
|
|
1902
|
-
// Update the fleet servers state to show the server is restarting
|
|
1903
|
-
setFleetServers(prev => prev.map(s =>
|
|
1904
|
-
s.id === serverId ? { ...s, status: 'connecting' as const } : s
|
|
1905
|
-
));
|
|
1906
|
-
// Refresh cloud workspaces after a short delay to get updated status
|
|
1907
|
-
setTimeout(async () => {
|
|
1908
|
-
try {
|
|
1909
|
-
const workspacesResult = await cloudApi.getWorkspaceSummary();
|
|
1910
|
-
if (workspacesResult.success && workspacesResult.data.workspaces) {
|
|
1911
|
-
setCloudWorkspaces(workspacesResult.data.workspaces);
|
|
1912
|
-
}
|
|
1913
|
-
} catch (err) {
|
|
1914
|
-
console.error('Failed to refresh workspaces after reconnect:', err);
|
|
1915
|
-
}
|
|
1916
|
-
}, 2000);
|
|
1917
|
-
} else {
|
|
1918
|
-
console.error('Failed to restart workspace:', result.error);
|
|
1919
|
-
}
|
|
1920
|
-
} catch (err) {
|
|
1921
|
-
console.error('Failed to reconnect to server:', err);
|
|
1922
|
-
}
|
|
1923
|
-
} else {
|
|
1924
|
-
// For orchestrator mode, attempt to reconnect by removing and re-adding the workspace
|
|
1925
|
-
console.warn('Server reconnect not fully supported in orchestrator mode');
|
|
1926
|
-
// Refresh the workspace list as a fallback
|
|
1927
|
-
// The orchestrator's WebSocket will handle reconnection automatically
|
|
1928
|
-
}
|
|
1929
|
-
}, [isCloudMode]);
|
|
1930
|
-
|
|
1931
|
-
// Handle spawn agent
|
|
1932
|
-
const handleSpawn = useCallback(async (config: SpawnConfig): Promise<boolean> => {
|
|
1933
|
-
setIsSpawning(true);
|
|
1934
|
-
setSpawnError(null);
|
|
1935
|
-
try {
|
|
1936
|
-
// Use orchestrator if workspaces are available
|
|
1937
|
-
if (workspaces.length > 0 && activeWorkspaceId) {
|
|
1938
|
-
await orchestratorSpawnAgent(config.name, undefined, config.command);
|
|
1939
|
-
return true;
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
// Fallback to legacy API
|
|
1943
|
-
const result = await api.spawnAgent({
|
|
1944
|
-
name: config.name,
|
|
1945
|
-
cli: config.command,
|
|
1946
|
-
team: config.team,
|
|
1947
|
-
shadowMode: config.shadowMode,
|
|
1948
|
-
shadowOf: config.shadowOf,
|
|
1949
|
-
shadowAgent: config.shadowAgent,
|
|
1950
|
-
shadowTriggers: config.shadowTriggers,
|
|
1951
|
-
shadowSpeakOn: config.shadowSpeakOn,
|
|
1952
|
-
});
|
|
1953
|
-
if (!result.success) {
|
|
1954
|
-
setSpawnError(result.error || 'Failed to spawn agent');
|
|
1955
|
-
return false;
|
|
1956
|
-
}
|
|
1957
|
-
return true;
|
|
1958
|
-
} catch (err) {
|
|
1959
|
-
setSpawnError(err instanceof Error ? err.message : 'Failed to spawn agent');
|
|
1960
|
-
return false;
|
|
1961
|
-
} finally {
|
|
1962
|
-
setIsSpawning(false);
|
|
1963
|
-
}
|
|
1964
|
-
}, [workspaces.length, activeWorkspaceId, orchestratorSpawnAgent]);
|
|
1965
|
-
|
|
1966
|
-
// Handle release/kill agent
|
|
1967
|
-
const handleReleaseAgent = useCallback(async (agent: Agent) => {
|
|
1968
|
-
if (!agent.isSpawned) return;
|
|
1969
|
-
|
|
1970
|
-
const confirmed = window.confirm(`Are you sure you want to release agent "${agent.name}"?`);
|
|
1971
|
-
if (!confirmed) return;
|
|
1972
|
-
|
|
1973
|
-
try {
|
|
1974
|
-
// Use orchestrator if workspaces are available
|
|
1975
|
-
if (workspaces.length > 0 && activeWorkspaceId) {
|
|
1976
|
-
await orchestratorStopAgent(agent.name);
|
|
1977
|
-
return;
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
// Fallback to legacy API
|
|
1981
|
-
const result = await api.releaseAgent(agent.name);
|
|
1982
|
-
if (!result.success) {
|
|
1983
|
-
console.error('Failed to release agent:', result.error);
|
|
1984
|
-
}
|
|
1985
|
-
} catch (err) {
|
|
1986
|
-
console.error('Failed to release agent:', err);
|
|
1987
|
-
}
|
|
1988
|
-
}, [workspaces.length, activeWorkspaceId, orchestratorStopAgent]);
|
|
1989
|
-
|
|
1990
|
-
// Handle logs click - open log viewer panel
|
|
1991
|
-
const handleLogsClick = useCallback((agent: Agent) => {
|
|
1992
|
-
setLogViewerAgent(agent);
|
|
1993
|
-
}, []);
|
|
1994
|
-
|
|
1995
|
-
// Fetch fleet servers periodically when fleet view is active
|
|
1996
|
-
useEffect(() => {
|
|
1997
|
-
if (!isFleetViewActive) return;
|
|
1998
|
-
|
|
1999
|
-
const fetchFleetServers = async () => {
|
|
2000
|
-
const result = await api.getFleetServers();
|
|
2001
|
-
if (result.success && result.data) {
|
|
2002
|
-
// Convert FleetServer to ServerInfo format
|
|
2003
|
-
const servers: ServerInfo[] = result.data.servers.map((s) => ({
|
|
2004
|
-
id: s.id,
|
|
2005
|
-
name: s.name,
|
|
2006
|
-
url: s.id === 'local' ? window.location.origin : `http://${s.id}`,
|
|
2007
|
-
status: s.status === 'healthy' ? 'online' : s.status === 'degraded' ? 'degraded' : 'offline',
|
|
2008
|
-
agentCount: s.agents.length,
|
|
2009
|
-
uptime: s.uptime,
|
|
2010
|
-
lastSeen: s.lastHeartbeat,
|
|
2011
|
-
}));
|
|
2012
|
-
setFleetServers(servers);
|
|
2013
|
-
}
|
|
2014
|
-
};
|
|
2015
|
-
|
|
2016
|
-
fetchFleetServers();
|
|
2017
|
-
const interval = setInterval(fetchFleetServers, 5000);
|
|
2018
|
-
return () => clearInterval(interval);
|
|
2019
|
-
}, [isFleetViewActive]);
|
|
2020
|
-
|
|
2021
|
-
// Fetch decisions periodically when queue is open
|
|
2022
|
-
useEffect(() => {
|
|
2023
|
-
if (!isDecisionQueueOpen) return;
|
|
2024
|
-
|
|
2025
|
-
const fetchDecisions = async () => {
|
|
2026
|
-
const result = await api.getDecisions();
|
|
2027
|
-
if (result.success && result.data) {
|
|
2028
|
-
setDecisions(result.data.decisions.map(convertApiDecision));
|
|
2029
|
-
}
|
|
2030
|
-
};
|
|
2031
|
-
|
|
2032
|
-
fetchDecisions();
|
|
2033
|
-
const interval = setInterval(fetchDecisions, 5000);
|
|
2034
|
-
return () => clearInterval(interval);
|
|
2035
|
-
}, [isDecisionQueueOpen]);
|
|
2036
|
-
|
|
2037
|
-
// Decision queue handlers
|
|
2038
|
-
const handleDecisionApprove = useCallback(async (decisionId: string, optionId?: string) => {
|
|
2039
|
-
setDecisionProcessing((prev) => ({ ...prev, [decisionId]: true }));
|
|
2040
|
-
try {
|
|
2041
|
-
const result = await api.approveDecision(decisionId, optionId);
|
|
2042
|
-
if (result.success) {
|
|
2043
|
-
setDecisions((prev) => prev.filter((d) => d.id !== decisionId));
|
|
2044
|
-
} else {
|
|
2045
|
-
console.error('Failed to approve decision:', result.error);
|
|
2046
|
-
}
|
|
2047
|
-
} catch (err) {
|
|
2048
|
-
console.error('Failed to approve decision:', err);
|
|
2049
|
-
} finally {
|
|
2050
|
-
setDecisionProcessing((prev) => ({ ...prev, [decisionId]: false }));
|
|
2051
|
-
}
|
|
2052
|
-
}, []);
|
|
2053
|
-
|
|
2054
|
-
const handleDecisionReject = useCallback(async (decisionId: string, reason?: string) => {
|
|
2055
|
-
setDecisionProcessing((prev) => ({ ...prev, [decisionId]: true }));
|
|
2056
|
-
try {
|
|
2057
|
-
const result = await api.rejectDecision(decisionId, reason);
|
|
2058
|
-
if (result.success) {
|
|
2059
|
-
setDecisions((prev) => prev.filter((d) => d.id !== decisionId));
|
|
2060
|
-
} else {
|
|
2061
|
-
console.error('Failed to reject decision:', result.error);
|
|
2062
|
-
}
|
|
2063
|
-
} catch (err) {
|
|
2064
|
-
console.error('Failed to reject decision:', err);
|
|
2065
|
-
} finally {
|
|
2066
|
-
setDecisionProcessing((prev) => ({ ...prev, [decisionId]: false }));
|
|
2067
|
-
}
|
|
2068
|
-
}, []);
|
|
2069
|
-
|
|
2070
|
-
const handleDecisionDismiss = useCallback(async (decisionId: string) => {
|
|
2071
|
-
const result = await api.dismissDecision(decisionId);
|
|
2072
|
-
if (result.success) {
|
|
2073
|
-
setDecisions((prev) => prev.filter((d) => d.id !== decisionId));
|
|
2074
|
-
}
|
|
2075
|
-
}, []);
|
|
2076
|
-
|
|
2077
|
-
// Task creation handler - creates bead and sends relay notification
|
|
2078
|
-
const handleTaskCreate = useCallback(async (task: TaskCreateRequest) => {
|
|
2079
|
-
setIsCreatingTask(true);
|
|
2080
|
-
try {
|
|
2081
|
-
// Map UI priority to beads priority number
|
|
2082
|
-
const beadsPriority = PRIORITY_CONFIG[task.priority].beadsPriority;
|
|
2083
|
-
|
|
2084
|
-
// Create bead via API
|
|
2085
|
-
const result = await api.createBead({
|
|
2086
|
-
title: task.title,
|
|
2087
|
-
assignee: task.agentName,
|
|
2088
|
-
priority: beadsPriority,
|
|
2089
|
-
type: 'task',
|
|
2090
|
-
});
|
|
2091
|
-
|
|
2092
|
-
if (result.success && result.data?.bead) {
|
|
2093
|
-
// Send relay notification to agent (non-interrupting)
|
|
2094
|
-
await api.sendRelayMessage({
|
|
2095
|
-
to: task.agentName,
|
|
2096
|
-
content: `📋 New task assigned: "${task.title}" (P${beadsPriority})\nCheck \`bd ready\` for details.`,
|
|
2097
|
-
});
|
|
2098
|
-
console.log('Task created:', result.data.bead.id);
|
|
2099
|
-
} else {
|
|
2100
|
-
console.error('Failed to create task bead:', result.error);
|
|
2101
|
-
throw new Error(result.error || 'Failed to create task');
|
|
2102
|
-
}
|
|
2103
|
-
} catch (err) {
|
|
2104
|
-
console.error('Failed to create task:', err);
|
|
2105
|
-
throw err;
|
|
2106
|
-
} finally {
|
|
2107
|
-
setIsCreatingTask(false);
|
|
2108
|
-
}
|
|
2109
|
-
}, []);
|
|
2110
|
-
|
|
2111
|
-
// Handle command palette
|
|
2112
|
-
const handleCommandPaletteOpen = useCallback(() => {
|
|
2113
|
-
setIsCommandPaletteOpen(true);
|
|
2114
|
-
}, []);
|
|
2115
|
-
|
|
2116
|
-
const handleCommandPaletteClose = useCallback(() => {
|
|
2117
|
-
setIsCommandPaletteOpen(false);
|
|
2118
|
-
}, []);
|
|
2119
|
-
|
|
2120
|
-
// Persist settings changes
|
|
2121
|
-
useEffect(() => {
|
|
2122
|
-
saveSettingsToStorage(settings);
|
|
2123
|
-
}, [settings]);
|
|
2124
|
-
|
|
2125
|
-
// Apply theme to document
|
|
2126
|
-
React.useEffect(() => {
|
|
2127
|
-
const applyTheme = (theme: 'light' | 'dark' | 'system') => {
|
|
2128
|
-
let effectiveTheme: 'light' | 'dark';
|
|
2129
|
-
|
|
2130
|
-
if (theme === 'system') {
|
|
2131
|
-
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
2132
|
-
effectiveTheme = prefersDark ? 'dark' : 'light';
|
|
2133
|
-
} else {
|
|
2134
|
-
effectiveTheme = theme;
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
document.documentElement.setAttribute('data-theme', effectiveTheme);
|
|
2138
|
-
};
|
|
2139
|
-
|
|
2140
|
-
applyTheme(settings.theme);
|
|
2141
|
-
|
|
2142
|
-
if (settings.theme === 'system') {
|
|
2143
|
-
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
2144
|
-
const handleChange = () => applyTheme('system');
|
|
2145
|
-
mediaQuery.addEventListener('change', handleChange);
|
|
2146
|
-
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
2147
|
-
}
|
|
2148
|
-
}, [settings.theme]);
|
|
2149
|
-
|
|
2150
|
-
// Request browser notification permissions when enabled
|
|
2151
|
-
useEffect(() => {
|
|
2152
|
-
if (!settings.notifications.desktop) return;
|
|
2153
|
-
if (typeof window === 'undefined' || !('Notification' in window)) return;
|
|
2154
|
-
|
|
2155
|
-
if (Notification.permission === 'granted') return;
|
|
2156
|
-
|
|
2157
|
-
if (Notification.permission === 'denied') {
|
|
2158
|
-
updateSettings((prev) => ({
|
|
2159
|
-
...prev,
|
|
2160
|
-
notifications: {
|
|
2161
|
-
...prev.notifications,
|
|
2162
|
-
desktop: false,
|
|
2163
|
-
enabled: prev.notifications.sound || prev.notifications.mentionsOnly,
|
|
2164
|
-
},
|
|
2165
|
-
}));
|
|
2166
|
-
return;
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
Notification.requestPermission().then((permission) => {
|
|
2170
|
-
if (permission !== 'granted') {
|
|
2171
|
-
updateSettings((prev) => ({
|
|
2172
|
-
...prev,
|
|
2173
|
-
notifications: {
|
|
2174
|
-
...prev.notifications,
|
|
2175
|
-
desktop: false,
|
|
2176
|
-
enabled: prev.notifications.sound || prev.notifications.mentionsOnly,
|
|
2177
|
-
},
|
|
2178
|
-
}));
|
|
2179
|
-
}
|
|
2180
|
-
}).catch(() => undefined);
|
|
2181
|
-
}, [settings.notifications.desktop, settings.notifications.sound, settings.notifications.mentionsOnly, updateSettings]);
|
|
2182
|
-
|
|
2183
|
-
// Browser notifications and sounds for new messages
|
|
2184
|
-
useEffect(() => {
|
|
2185
|
-
const messages = data?.messages;
|
|
2186
|
-
if (!messages || messages.length === 0) {
|
|
2187
|
-
lastNotifiedMessageIdRef.current = null;
|
|
2188
|
-
return;
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
const latestMessage = messages[messages.length - 1];
|
|
2192
|
-
|
|
2193
|
-
if (!settings.notifications.enabled) {
|
|
2194
|
-
lastNotifiedMessageIdRef.current = latestMessage?.id ?? null;
|
|
2195
|
-
return;
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
if (!lastNotifiedMessageIdRef.current) {
|
|
2199
|
-
lastNotifiedMessageIdRef.current = latestMessage.id;
|
|
2200
|
-
return;
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
const lastNotifiedIndex = messages.findIndex((message) => (
|
|
2204
|
-
message.id === lastNotifiedMessageIdRef.current
|
|
2205
|
-
));
|
|
2206
|
-
|
|
2207
|
-
if (lastNotifiedIndex === -1) {
|
|
2208
|
-
lastNotifiedMessageIdRef.current = latestMessage.id;
|
|
2209
|
-
return;
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
const newMessages = messages.slice(lastNotifiedIndex + 1);
|
|
2213
|
-
if (newMessages.length === 0) {
|
|
2214
|
-
return;
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
lastNotifiedMessageIdRef.current = latestMessage.id;
|
|
2218
|
-
|
|
2219
|
-
const isFromCurrentUser = (message: Message) =>
|
|
2220
|
-
message.from === 'Dashboard' ||
|
|
2221
|
-
(currentUser && message.from === currentUser.displayName);
|
|
2222
|
-
|
|
2223
|
-
const isMessageInCurrentChannel = (message: Message) => {
|
|
2224
|
-
if (currentChannel === 'general') {
|
|
2225
|
-
return message.to === '*' || message.isBroadcast || message.channel === 'general';
|
|
2226
|
-
}
|
|
2227
|
-
return message.from === currentChannel || message.to === currentChannel;
|
|
2228
|
-
};
|
|
2229
|
-
|
|
2230
|
-
const shouldNotifyForMessage = (message: Message) => {
|
|
2231
|
-
if (isFromCurrentUser(message)) return false;
|
|
2232
|
-
if (settings.notifications.mentionsOnly && currentUser?.displayName) {
|
|
2233
|
-
if (!message.content.includes(`@${currentUser.displayName}`)) {
|
|
2234
|
-
return false;
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
const isActive = typeof document !== 'undefined' ? !document.hidden : false;
|
|
2238
|
-
if (isActive && isMessageInCurrentChannel(message)) return false;
|
|
2239
|
-
return true;
|
|
2240
|
-
};
|
|
2241
|
-
|
|
2242
|
-
let shouldPlaySound = false;
|
|
2243
|
-
|
|
2244
|
-
for (const message of newMessages) {
|
|
2245
|
-
if (!shouldNotifyForMessage(message)) continue;
|
|
2246
|
-
|
|
2247
|
-
if (settings.notifications.desktop && typeof window !== 'undefined' && 'Notification' in window) {
|
|
2248
|
-
if (Notification.permission === 'granted') {
|
|
2249
|
-
const channelLabel = message.to === '*' ? 'Activity' : message.to;
|
|
2250
|
-
const body = message.content.split('\n')[0].slice(0, 160);
|
|
2251
|
-
const notification = new Notification(`${message.from} → ${channelLabel}`, { body });
|
|
2252
|
-
notification.onclick = () => {
|
|
2253
|
-
window.focus();
|
|
2254
|
-
if (message.to === '*') {
|
|
2255
|
-
setSelectedChannelId(ACTIVITY_FEED_ID);
|
|
2256
|
-
setViewMode('channels');
|
|
2257
|
-
} else {
|
|
2258
|
-
setCurrentChannel(message.from);
|
|
2259
|
-
}
|
|
2260
|
-
notification.close();
|
|
2261
|
-
};
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
if (settings.notifications.sound) {
|
|
2266
|
-
shouldPlaySound = true;
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
if (shouldPlaySound) {
|
|
2271
|
-
playNotificationSound();
|
|
2272
|
-
}
|
|
2273
|
-
}, [data?.messages, settings.notifications, currentChannel, currentUser, setCurrentChannel]);
|
|
2274
|
-
|
|
2275
|
-
// Keyboard shortcuts
|
|
2276
|
-
React.useEffect(() => {
|
|
2277
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
2278
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
2279
|
-
e.preventDefault();
|
|
2280
|
-
setIsCommandPaletteOpen(true);
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 's') {
|
|
2284
|
-
e.preventDefault();
|
|
2285
|
-
handleSpawnClick();
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'c') {
|
|
2289
|
-
e.preventDefault();
|
|
2290
|
-
setViewMode('channels');
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
|
|
2294
|
-
e.preventDefault();
|
|
2295
|
-
handleNewConversationClick();
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
if (e.key === 'Escape') {
|
|
2299
|
-
setIsCommandPaletteOpen(false);
|
|
2300
|
-
setIsSpawnModalOpen(false);
|
|
2301
|
-
setIsNewConversationOpen(false);
|
|
2302
|
-
setIsTrajectoryOpen(false);
|
|
2303
|
-
setIsFullSettingsOpen(false);
|
|
2304
|
-
}
|
|
2305
|
-
};
|
|
2306
|
-
|
|
2307
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
2308
|
-
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
2309
|
-
}, [handleSpawnClick, handleNewConversationClick]);
|
|
2310
|
-
|
|
2311
|
-
// Handle billing result routes (success/cancel after Stripe checkout)
|
|
2312
|
-
const pathname = typeof window !== 'undefined' ? window.location.pathname : '';
|
|
2313
|
-
const searchParams = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : new URLSearchParams();
|
|
2314
|
-
|
|
2315
|
-
if (pathname === '/billing/success') {
|
|
2316
|
-
return (
|
|
2317
|
-
<BillingResult
|
|
2318
|
-
type="success"
|
|
2319
|
-
sessionId={searchParams.get('session_id') || undefined}
|
|
2320
|
-
onClose={() => {
|
|
2321
|
-
window.location.href = '/';
|
|
2322
|
-
}}
|
|
2323
|
-
/>
|
|
2324
|
-
);
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
if (pathname === '/billing/canceled') {
|
|
2328
|
-
return (
|
|
2329
|
-
<BillingResult
|
|
2330
|
-
type="canceled"
|
|
2331
|
-
onClose={() => {
|
|
2332
|
-
window.location.href = '/';
|
|
2333
|
-
}}
|
|
2334
|
-
/>
|
|
2335
|
-
);
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
return (
|
|
2339
|
-
<WorkspaceProvider wsUrl={wsUrl}>
|
|
2340
|
-
<div className="flex h-screen bg-bg-deep font-sans text-text-primary">
|
|
2341
|
-
{/* Mobile Sidebar Overlay */}
|
|
2342
|
-
<div
|
|
2343
|
-
className={`
|
|
2344
|
-
fixed inset-0 bg-black/60 backdrop-blur-sm z-[999] transition-opacity duration-200
|
|
2345
|
-
md:hidden
|
|
2346
|
-
${isSidebarOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}
|
|
2347
|
-
`}
|
|
2348
|
-
onClick={() => setIsSidebarOpen(false)}
|
|
2349
|
-
/>
|
|
2350
|
-
|
|
2351
|
-
{/* Sidebar with Workspace Selector */}
|
|
2352
|
-
<div className={`
|
|
2353
|
-
flex flex-col w-[280px] max-md:w-[85vw] max-md:max-w-[280px] h-screen bg-bg-primary border-r border-border-subtle
|
|
2354
|
-
fixed left-0 top-0 z-[1000] transition-transform duration-200
|
|
2355
|
-
md:relative md:translate-x-0 md:flex-shrink-0
|
|
2356
|
-
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
|
|
2357
|
-
`}>
|
|
2358
|
-
{/* Workspace Selector */}
|
|
2359
|
-
<div className="p-3 border-b border-sidebar-border">
|
|
2360
|
-
<WorkspaceSelector
|
|
2361
|
-
workspaces={effectiveWorkspaces}
|
|
2362
|
-
activeWorkspaceId={effectiveActiveWorkspaceId ?? undefined}
|
|
2363
|
-
onSelect={handleEffectiveWorkspaceSelect}
|
|
2364
|
-
onAddWorkspace={() => setIsAddWorkspaceOpen(true)}
|
|
2365
|
-
onWorkspaceSettings={handleWorkspaceSettingsClick}
|
|
2366
|
-
isLoading={effectiveIsLoading}
|
|
2367
|
-
/>
|
|
2368
|
-
</div>
|
|
2369
|
-
|
|
2370
|
-
{/* Unified Sidebar - Channels collapsed by default, Agents always visible */}
|
|
2371
|
-
<Sidebar
|
|
2372
|
-
agents={localAgentsForSidebar}
|
|
2373
|
-
bridgeAgents={bridgeAgents}
|
|
2374
|
-
projects={mergedProjects}
|
|
2375
|
-
currentUserName={currentUser?.displayName}
|
|
2376
|
-
humanUnreadCounts={humanUnreadCounts}
|
|
2377
|
-
currentProject={currentProject}
|
|
2378
|
-
selectedAgent={selectedAgent?.name}
|
|
2379
|
-
viewMode={viewMode}
|
|
2380
|
-
isFleetAvailable={isFleetAvailable}
|
|
2381
|
-
isConnected={isConnected || isOrchestratorConnected}
|
|
2382
|
-
isOpen={isSidebarOpen}
|
|
2383
|
-
activeThreads={activeThreads}
|
|
2384
|
-
currentThread={currentThread}
|
|
2385
|
-
totalUnreadThreadCount={totalUnreadThreadCount}
|
|
2386
|
-
channels={channelsList
|
|
2387
|
-
.filter(c => !c.isDm && !c.id.startsWith('dm:'))
|
|
2388
|
-
.map(c => ({
|
|
2389
|
-
id: c.id,
|
|
2390
|
-
name: c.name,
|
|
2391
|
-
unreadCount: c.unreadCount,
|
|
2392
|
-
hasMentions: c.hasMentions,
|
|
2393
|
-
}))}
|
|
2394
|
-
archivedChannels={archivedChannelsList
|
|
2395
|
-
.filter(c => !c.isDm && !c.id.startsWith('dm:'))
|
|
2396
|
-
.map((c) => ({
|
|
2397
|
-
id: c.id,
|
|
2398
|
-
name: c.name,
|
|
2399
|
-
unreadCount: c.unreadCount ?? 0,
|
|
2400
|
-
hasMentions: c.hasMentions,
|
|
2401
|
-
}))}
|
|
2402
|
-
selectedChannelId={selectedChannelId}
|
|
2403
|
-
isActivitySelected={selectedChannelId === ACTIVITY_FEED_ID}
|
|
2404
|
-
activityUnreadCount={0}
|
|
2405
|
-
onActivitySelect={() => {
|
|
2406
|
-
setSelectedChannelId(ACTIVITY_FEED_ID);
|
|
2407
|
-
selectAgent(null);
|
|
2408
|
-
setViewMode('channels');
|
|
2409
|
-
}}
|
|
2410
|
-
onChannelSelect={(channel) => {
|
|
2411
|
-
const fullChannel =
|
|
2412
|
-
channelsList.find(c => c.id === channel.id) ||
|
|
2413
|
-
archivedChannelsList.find(c => c.id === channel.id);
|
|
2414
|
-
if (fullChannel) {
|
|
2415
|
-
handleSelectChannel(fullChannel);
|
|
2416
|
-
setViewMode('channels');
|
|
2417
|
-
}
|
|
2418
|
-
}}
|
|
2419
|
-
onCreateChannel={handleCreateChannel}
|
|
2420
|
-
onInviteToChannel={(channel) => {
|
|
2421
|
-
const fullChannel = channelsList.find(c => c.id === channel.id);
|
|
2422
|
-
if (fullChannel) {
|
|
2423
|
-
handleInviteToChannel(fullChannel);
|
|
2424
|
-
}
|
|
2425
|
-
}}
|
|
2426
|
-
onArchiveChannel={(channel) => {
|
|
2427
|
-
const fullChannel = channelsList.find((c) => c.id === channel.id);
|
|
2428
|
-
if (fullChannel) {
|
|
2429
|
-
handleArchiveChannel(fullChannel);
|
|
2430
|
-
}
|
|
2431
|
-
}}
|
|
2432
|
-
onUnarchiveChannel={(channel) => {
|
|
2433
|
-
const fullChannel =
|
|
2434
|
-
archivedChannelsList.find((c) => c.id === channel.id) ||
|
|
2435
|
-
channelsList.find((c) => c.id === channel.id);
|
|
2436
|
-
if (fullChannel) {
|
|
2437
|
-
handleUnarchiveChannel(fullChannel);
|
|
2438
|
-
}
|
|
2439
|
-
}}
|
|
2440
|
-
onAgentSelect={handleAgentSelect}
|
|
2441
|
-
onHumanSelect={handleHumanSelect}
|
|
2442
|
-
onProjectSelect={handleProjectSelect}
|
|
2443
|
-
onViewModeChange={setViewMode}
|
|
2444
|
-
onSpawnClick={handleSpawnClick}
|
|
2445
|
-
onReleaseClick={handleReleaseAgent}
|
|
2446
|
-
onLogsClick={handleLogsClick}
|
|
2447
|
-
onProfileClick={setSelectedAgentProfile}
|
|
2448
|
-
onThreadSelect={setCurrentThread}
|
|
2449
|
-
onClose={() => setIsSidebarOpen(false)}
|
|
2450
|
-
onSettingsClick={handleSettingsClick}
|
|
2451
|
-
onTrajectoryClick={() => setIsTrajectoryOpen(true)}
|
|
2452
|
-
hasActiveTrajectory={trajectoryStatus?.active}
|
|
2453
|
-
onFleetClick={() => setIsFleetViewActive(!isFleetViewActive)}
|
|
2454
|
-
isFleetViewActive={isFleetViewActive}
|
|
2455
|
-
onCoordinatorClick={handleCoordinatorClick}
|
|
2456
|
-
hasMultipleProjects={mergedProjects.length > 1}
|
|
2457
|
-
/>
|
|
2458
|
-
</div>
|
|
2459
|
-
|
|
2460
|
-
{/* Main Content */}
|
|
2461
|
-
<main className="flex-1 flex flex-col min-w-0 bg-bg-secondary/50 overflow-hidden">
|
|
2462
|
-
{/* Header - fixed on mobile for keyboard-safe positioning, sticky on desktop */}
|
|
2463
|
-
<div className="fixed top-0 left-0 right-0 z-50 md:sticky md:top-0 md:left-auto md:right-auto bg-bg-secondary">
|
|
2464
|
-
<Header
|
|
2465
|
-
currentChannel={currentChannel}
|
|
2466
|
-
selectedAgent={selectedAgent}
|
|
2467
|
-
projects={mergedProjects}
|
|
2468
|
-
currentProject={mergedProjects.find(p => p.id === currentProject) || null}
|
|
2469
|
-
recentProjects={getRecentProjects(mergedProjects)}
|
|
2470
|
-
viewMode={viewMode}
|
|
2471
|
-
selectedChannelName={selectedChannel?.name}
|
|
2472
|
-
onProjectChange={handleProjectSelect}
|
|
2473
|
-
onCommandPaletteOpen={handleCommandPaletteOpen}
|
|
2474
|
-
onSettingsClick={handleSettingsClick}
|
|
2475
|
-
onHistoryClick={handleHistoryClick}
|
|
2476
|
-
onNewConversationClick={handleNewConversationClick}
|
|
2477
|
-
onCoordinatorClick={handleCoordinatorClick}
|
|
2478
|
-
onFleetClick={() => setIsFleetViewActive(!isFleetViewActive)}
|
|
2479
|
-
isFleetViewActive={isFleetViewActive}
|
|
2480
|
-
onTrajectoryClick={() => setIsTrajectoryOpen(true)}
|
|
2481
|
-
hasActiveTrajectory={trajectoryStatus?.active}
|
|
2482
|
-
onMenuClick={() => setIsSidebarOpen(true)}
|
|
2483
|
-
hasUnreadNotifications={hasUnreadMessages}
|
|
2484
|
-
/>
|
|
2485
|
-
{/* Usage banner for free tier users */}
|
|
2486
|
-
<UsageBanner onUpgradeClick={handleBillingClick} />
|
|
2487
|
-
</div>
|
|
2488
|
-
{/* Spacer for fixed header on mobile - matches header height (52px) */}
|
|
2489
|
-
<div className="h-[52px] flex-shrink-0 md:hidden" />
|
|
2490
|
-
{/* Online users indicator - outside fixed header so it scrolls with content on mobile */}
|
|
2491
|
-
{currentUser && onlineUsers.length > 0 && (
|
|
2492
|
-
<div className="flex items-center justify-end px-4 py-1 bg-bg-tertiary/80 border-b border-border-subtle flex-shrink-0">
|
|
2493
|
-
<OnlineUsersIndicator
|
|
2494
|
-
onlineUsers={onlineUsers}
|
|
2495
|
-
onUserClick={setSelectedUserProfile}
|
|
2496
|
-
/>
|
|
2497
|
-
</div>
|
|
2498
|
-
)}
|
|
2499
|
-
|
|
2500
|
-
{/* Content Area */}
|
|
2501
|
-
<div className="flex-1 flex overflow-hidden min-h-0">
|
|
2502
|
-
{/* Message List */}
|
|
2503
|
-
<div className={`flex-1 min-h-0 overflow-y-auto ${currentThread ? 'hidden md:block md:flex-[2]' : ''}`}>
|
|
2504
|
-
{currentHuman && (
|
|
2505
|
-
<div className="px-4 py-2 border-b border-border-subtle bg-bg-secondary flex flex-col gap-2 sticky top-0 z-10">
|
|
2506
|
-
<div className="text-xs text-text-muted">
|
|
2507
|
-
DM with <span className="font-semibold text-text-primary">{currentHuman.name}</span>. Invite agents:
|
|
2508
|
-
</div>
|
|
2509
|
-
<div className="flex flex-wrap gap-2">
|
|
2510
|
-
{agents
|
|
2511
|
-
.filter((a) => !a.isHuman)
|
|
2512
|
-
.map((agent) => {
|
|
2513
|
-
const isSelected = (dmSelectedAgentsByHuman[currentHuman.name] ?? []).includes(agent.name);
|
|
2514
|
-
return (
|
|
2515
|
-
<button
|
|
2516
|
-
key={agent.name}
|
|
2517
|
-
onClick={() => handleDmAgentToggle(agent.name)}
|
|
2518
|
-
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors ${
|
|
2519
|
-
isSelected
|
|
2520
|
-
? 'bg-accent-cyan text-bg-deep'
|
|
2521
|
-
: 'bg-bg-tertiary text-text-secondary hover:bg-bg-tertiary/80'
|
|
2522
|
-
}`}
|
|
2523
|
-
title={agent.name}
|
|
2524
|
-
>
|
|
2525
|
-
{isSelected ? '✓ ' : ''}{agent.name}
|
|
2526
|
-
</button>
|
|
2527
|
-
);
|
|
2528
|
-
})}
|
|
2529
|
-
{agents.filter((a) => !a.isHuman).length === 0 && (
|
|
2530
|
-
<span className="text-xs text-text-muted">No agents available</span>
|
|
2531
|
-
)}
|
|
2532
|
-
</div>
|
|
2533
|
-
</div>
|
|
2534
|
-
)}
|
|
2535
|
-
{wsError ? (
|
|
2536
|
-
<div className="flex flex-col items-center justify-center h-full text-text-muted text-center px-4">
|
|
2537
|
-
<ErrorIcon />
|
|
2538
|
-
<h2 className="m-0 mb-2 font-display text-text-primary">Connection Error</h2>
|
|
2539
|
-
<p className="text-text-secondary">{wsError.message}</p>
|
|
2540
|
-
<button
|
|
2541
|
-
className="mt-6 py-3 px-6 bg-gradient-to-r from-accent-cyan to-[#00b8d9] text-bg-deep font-semibold border-none rounded-xl cursor-pointer transition-all duration-150 hover:shadow-glow-cyan hover:-translate-y-0.5"
|
|
2542
|
-
onClick={() => window.location.reload()}
|
|
2543
|
-
>
|
|
2544
|
-
Retry Connection
|
|
2545
|
-
</button>
|
|
2546
|
-
</div>
|
|
2547
|
-
) : !data ? (
|
|
2548
|
-
<div className="flex flex-col items-center justify-center h-full text-text-muted text-center">
|
|
2549
|
-
<LoadingSpinner />
|
|
2550
|
-
<p className="font-display text-text-secondary">Connecting to dashboard...</p>
|
|
2551
|
-
</div>
|
|
2552
|
-
) : isFleetViewActive ? (
|
|
2553
|
-
<div className="p-4 h-full overflow-y-auto">
|
|
2554
|
-
<FleetOverview
|
|
2555
|
-
servers={fleetServers}
|
|
2556
|
-
agents={agents}
|
|
2557
|
-
selectedServerId={selectedServerId}
|
|
2558
|
-
onServerSelect={setSelectedServerId}
|
|
2559
|
-
onServerReconnect={handleServerReconnect}
|
|
2560
|
-
isLoading={!data}
|
|
2561
|
-
/>
|
|
2562
|
-
</div>
|
|
2563
|
-
) : selectedChannelId === ACTIVITY_FEED_ID ? (
|
|
2564
|
-
<ActivityFeed
|
|
2565
|
-
events={activityEvents}
|
|
2566
|
-
maxEvents={100}
|
|
2567
|
-
/>
|
|
2568
|
-
) : viewMode === 'channels' && selectedChannel ? (
|
|
2569
|
-
<ChannelViewV1
|
|
2570
|
-
channel={selectedChannel}
|
|
2571
|
-
messages={effectiveChannelMessages}
|
|
2572
|
-
currentUser={currentUser?.displayName || 'Anonymous'}
|
|
2573
|
-
isLoadingMore={false}
|
|
2574
|
-
hasMoreMessages={hasMoreMessages && !!effectiveActiveWorkspaceId}
|
|
2575
|
-
mentionSuggestions={agents.map(a => a.name)}
|
|
2576
|
-
unreadState={channelUnreadState}
|
|
2577
|
-
onSendMessage={handleSendChannelMessage}
|
|
2578
|
-
onLoadMore={handleLoadMoreMessages}
|
|
2579
|
-
onThreadClick={(messageId) => setCurrentThread(messageId)}
|
|
2580
|
-
onShowMembers={handleShowMembers}
|
|
2581
|
-
onMemberClick={handleChannelMemberClick}
|
|
2582
|
-
/>
|
|
2583
|
-
) : viewMode === 'channels' ? (
|
|
2584
|
-
<div className="flex flex-col items-center justify-center h-full text-text-muted text-center px-4">
|
|
2585
|
-
<HashIconLarge />
|
|
2586
|
-
<h2 className="m-0 mb-2 font-display text-text-primary">Select a channel</h2>
|
|
2587
|
-
<p className="text-text-secondary">Choose a channel from the sidebar to start messaging</p>
|
|
2588
|
-
</div>
|
|
2589
|
-
) : (
|
|
2590
|
-
<MessageList
|
|
2591
|
-
messages={dedupedVisibleMessages}
|
|
2592
|
-
currentChannel={currentChannel}
|
|
2593
|
-
currentThread={currentThread}
|
|
2594
|
-
onThreadClick={(messageId) => setCurrentThread(messageId)}
|
|
2595
|
-
highlightedMessageId={currentThread ?? undefined}
|
|
2596
|
-
agents={combinedAgents}
|
|
2597
|
-
currentUser={currentUser}
|
|
2598
|
-
skipChannelFilter={currentHuman !== null}
|
|
2599
|
-
showTimestamps={settings.display.showTimestamps}
|
|
2600
|
-
autoScrollDefault={settings.messages.autoScroll}
|
|
2601
|
-
compactMode={settings.display.compactMode}
|
|
2602
|
-
onAgentClick={setSelectedAgentProfile}
|
|
2603
|
-
onUserClick={setSelectedUserProfile}
|
|
2604
|
-
onlineUsers={onlineUsers}
|
|
2605
|
-
/>
|
|
2606
|
-
)}
|
|
2607
|
-
</div>
|
|
2608
|
-
|
|
2609
|
-
{/* Thread Panel */}
|
|
2610
|
-
{currentThread && (() => {
|
|
2611
|
-
// Determine which message list to search based on view mode
|
|
2612
|
-
const isChannelView = viewMode === 'channels';
|
|
2613
|
-
|
|
2614
|
-
// Helper to convert ChannelMessage to Message format for ThreadPanel
|
|
2615
|
-
const convertChannelMessage = (cm: ChannelApiMessage): Message => ({
|
|
2616
|
-
id: cm.id,
|
|
2617
|
-
from: cm.from,
|
|
2618
|
-
to: cm.channelId,
|
|
2619
|
-
content: cm.content,
|
|
2620
|
-
timestamp: cm.timestamp,
|
|
2621
|
-
thread: cm.threadId,
|
|
2622
|
-
isRead: cm.isRead,
|
|
2623
|
-
replyCount: cm.threadSummary?.replyCount,
|
|
2624
|
-
threadSummary: cm.threadSummary,
|
|
2625
|
-
});
|
|
2626
|
-
|
|
2627
|
-
let originalMessage: Message | null = null;
|
|
2628
|
-
let isTopicThread = false;
|
|
2629
|
-
|
|
2630
|
-
if (isChannelView) {
|
|
2631
|
-
const channelMsg = effectiveChannelMessages.find((m) => m.id === currentThread);
|
|
2632
|
-
if (channelMsg) {
|
|
2633
|
-
originalMessage = convertChannelMessage(channelMsg);
|
|
2634
|
-
} else {
|
|
2635
|
-
isTopicThread = true;
|
|
2636
|
-
const threadMsgs = effectiveChannelMessages
|
|
2637
|
-
.filter((m) => m.threadId === currentThread)
|
|
2638
|
-
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
2639
|
-
if (threadMsgs[0]) {
|
|
2640
|
-
originalMessage = convertChannelMessage(threadMsgs[0]);
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
} else {
|
|
2644
|
-
originalMessage = messages.find((m) => m.id === currentThread) ?? null;
|
|
2645
|
-
isTopicThread = !originalMessage;
|
|
2646
|
-
if (!originalMessage) {
|
|
2647
|
-
const threadMsgs = messages
|
|
2648
|
-
.filter((m) => m.thread === currentThread)
|
|
2649
|
-
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
2650
|
-
originalMessage = threadMsgs[0] ?? null;
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
|
|
2654
|
-
// Get thread replies based on view mode
|
|
2655
|
-
const replies: Message[] = isChannelView
|
|
2656
|
-
? effectiveChannelMessages
|
|
2657
|
-
.filter((m) => m.threadId === currentThread)
|
|
2658
|
-
.map(convertChannelMessage)
|
|
2659
|
-
: threadMessages(currentThread);
|
|
2660
|
-
|
|
2661
|
-
return (
|
|
2662
|
-
<div className="w-full md:w-[400px] md:min-w-[320px] md:max-w-[500px] flex-shrink-0">
|
|
2663
|
-
<ThreadPanel
|
|
2664
|
-
originalMessage={originalMessage}
|
|
2665
|
-
replies={replies}
|
|
2666
|
-
onClose={() => setCurrentThread(null)}
|
|
2667
|
-
showTimestamps={settings.display.showTimestamps}
|
|
2668
|
-
onReply={async (content) => {
|
|
2669
|
-
if (isChannelView && selectedChannel) {
|
|
2670
|
-
// For channels, send threaded message
|
|
2671
|
-
await handleSendChannelMessage(content, currentThread);
|
|
2672
|
-
return true;
|
|
2673
|
-
}
|
|
2674
|
-
// For topic threads, broadcast to all; for reply chains, reply to the other participant
|
|
2675
|
-
let recipient = '*';
|
|
2676
|
-
if (!isTopicThread && originalMessage) {
|
|
2677
|
-
// If current user sent the original message, reply to the recipient
|
|
2678
|
-
// If someone else sent it, reply to the sender
|
|
2679
|
-
const isFromCurrentUser = originalMessage.from === 'Dashboard' ||
|
|
2680
|
-
(currentUser && originalMessage.from === currentUser.displayName);
|
|
2681
|
-
recipient = isFromCurrentUser
|
|
2682
|
-
? originalMessage.to
|
|
2683
|
-
: originalMessage.from;
|
|
2684
|
-
}
|
|
2685
|
-
return sendMessage(recipient, content, currentThread);
|
|
2686
|
-
}}
|
|
2687
|
-
isSending={isSending}
|
|
2688
|
-
currentUser={currentUser}
|
|
2689
|
-
/>
|
|
2690
|
-
</div>
|
|
2691
|
-
);
|
|
2692
|
-
})()}
|
|
2693
|
-
</div>
|
|
2694
|
-
|
|
2695
|
-
{/* Typing Indicator */}
|
|
2696
|
-
{typingUsers.length > 0 && (
|
|
2697
|
-
<div className="px-4 bg-bg-tertiary border-t border-border-subtle">
|
|
2698
|
-
<TypingIndicator typingUsers={typingUsers} />
|
|
2699
|
-
</div>
|
|
2700
|
-
)}
|
|
2701
|
-
|
|
2702
|
-
{/* Message Composer - hide in channels mode (ChannelViewV1 has its own input) */}
|
|
2703
|
-
{viewMode !== 'channels' && (
|
|
2704
|
-
<div className="p-2 sm:p-4 bg-bg-tertiary border-t border-border-subtle">
|
|
2705
|
-
<MessageComposer
|
|
2706
|
-
agents={agents}
|
|
2707
|
-
humanUsers={humanUsers}
|
|
2708
|
-
onSend={handleMainComposerSend}
|
|
2709
|
-
onTyping={sendTyping}
|
|
2710
|
-
isSending={isSending}
|
|
2711
|
-
error={sendError}
|
|
2712
|
-
insertMention={pendingMention}
|
|
2713
|
-
onMentionInserted={() => setPendingMention(undefined)}
|
|
2714
|
-
enableFileAutocomplete
|
|
2715
|
-
placeholder={`Message ${currentChannel === 'general' ? 'everyone' : '@' + currentChannel}...`}
|
|
2716
|
-
/>
|
|
2717
|
-
</div>
|
|
2718
|
-
)}
|
|
2719
|
-
</main>
|
|
2720
|
-
|
|
2721
|
-
{/* Command Palette */}
|
|
2722
|
-
<CommandPalette
|
|
2723
|
-
isOpen={isCommandPaletteOpen}
|
|
2724
|
-
onClose={handleCommandPaletteClose}
|
|
2725
|
-
agents={agents}
|
|
2726
|
-
projects={projects}
|
|
2727
|
-
currentProject={currentProject}
|
|
2728
|
-
onAgentSelect={handleAgentSelect}
|
|
2729
|
-
onProjectSelect={handleProjectSelect}
|
|
2730
|
-
onSpawnClick={handleSpawnClick}
|
|
2731
|
-
onTaskCreate={handleTaskCreate}
|
|
2732
|
-
onGeneralClick={() => {
|
|
2733
|
-
selectAgent(null);
|
|
2734
|
-
setCurrentChannel('general');
|
|
2735
|
-
}}
|
|
2736
|
-
customCommands={[...dmInviteCommands, ...channelCommands]}
|
|
2737
|
-
/>
|
|
2738
|
-
|
|
2739
|
-
{/* Spawn Modal */}
|
|
2740
|
-
<SpawnModal
|
|
2741
|
-
isOpen={isSpawnModalOpen}
|
|
2742
|
-
onClose={() => setIsSpawnModalOpen(false)}
|
|
2743
|
-
onSpawn={handleSpawn}
|
|
2744
|
-
existingAgents={agents.map((a) => a.name)}
|
|
2745
|
-
isSpawning={isSpawning}
|
|
2746
|
-
error={spawnError}
|
|
2747
|
-
isCloudMode={isCloudMode}
|
|
2748
|
-
workspaceId={effectiveActiveWorkspaceId ?? undefined}
|
|
2749
|
-
/>
|
|
2750
|
-
|
|
2751
|
-
{/* Add Workspace Modal */}
|
|
2752
|
-
<AddWorkspaceModal
|
|
2753
|
-
isOpen={isAddWorkspaceOpen}
|
|
2754
|
-
onClose={() => {
|
|
2755
|
-
setIsAddWorkspaceOpen(false);
|
|
2756
|
-
setAddWorkspaceError(null);
|
|
2757
|
-
}}
|
|
2758
|
-
onAdd={handleAddWorkspace}
|
|
2759
|
-
isAdding={isAddingWorkspace}
|
|
2760
|
-
error={addWorkspaceError}
|
|
2761
|
-
/>
|
|
2762
|
-
|
|
2763
|
-
{/* Create Channel Modal */}
|
|
2764
|
-
<CreateChannelModal
|
|
2765
|
-
isOpen={isCreateChannelOpen}
|
|
2766
|
-
onClose={() => setIsCreateChannelOpen(false)}
|
|
2767
|
-
onCreate={handleCreateChannelSubmit}
|
|
2768
|
-
isLoading={isCreatingChannel}
|
|
2769
|
-
existingChannels={channelsList.map(c => c.name)}
|
|
2770
|
-
availableMembers={agents.map(a => a.name)}
|
|
2771
|
-
/>
|
|
2772
|
-
|
|
2773
|
-
{/* Invite to Channel Modal */}
|
|
2774
|
-
<InviteToChannelModal
|
|
2775
|
-
isOpen={isInviteChannelOpen}
|
|
2776
|
-
channelName={inviteChannelTarget?.name || ''}
|
|
2777
|
-
onClose={() => {
|
|
2778
|
-
setIsInviteChannelOpen(false);
|
|
2779
|
-
setInviteChannelTarget(null);
|
|
2780
|
-
}}
|
|
2781
|
-
onInvite={handleInviteSubmit}
|
|
2782
|
-
isLoading={isInvitingToChannel}
|
|
2783
|
-
availableMembers={agents.map(a => a.name)}
|
|
2784
|
-
/>
|
|
2785
|
-
|
|
2786
|
-
{/* Member Management Panel */}
|
|
2787
|
-
{selectedChannel && (
|
|
2788
|
-
<MemberManagementPanel
|
|
2789
|
-
channel={selectedChannel}
|
|
2790
|
-
members={channelMembers}
|
|
2791
|
-
isOpen={showMemberPanel}
|
|
2792
|
-
onClose={() => setShowMemberPanel(false)}
|
|
2793
|
-
onAddMember={handleAddMember}
|
|
2794
|
-
onRemoveMember={handleRemoveMember}
|
|
2795
|
-
onUpdateRole={() => {}}
|
|
2796
|
-
currentUserId={currentUser?.displayName}
|
|
2797
|
-
availableAgents={agents.map(a => ({ name: a.name }))}
|
|
2798
|
-
workspaceId={effectiveActiveWorkspaceId ?? undefined}
|
|
2799
|
-
/>
|
|
2800
|
-
)}
|
|
2801
|
-
|
|
2802
|
-
{/* Conversation History */}
|
|
2803
|
-
<ConversationHistory
|
|
2804
|
-
isOpen={isHistoryOpen}
|
|
2805
|
-
onClose={() => setIsHistoryOpen(false)}
|
|
2806
|
-
/>
|
|
2807
|
-
|
|
2808
|
-
{/* New Conversation Modal */}
|
|
2809
|
-
<NewConversationModal
|
|
2810
|
-
isOpen={isNewConversationOpen}
|
|
2811
|
-
onClose={() => setIsNewConversationOpen(false)}
|
|
2812
|
-
onSend={handleNewConversationSend}
|
|
2813
|
-
agents={agents}
|
|
2814
|
-
isSending={isSending}
|
|
2815
|
-
error={sendError}
|
|
2816
|
-
/>
|
|
2817
|
-
|
|
2818
|
-
{/* Log Viewer Panel */}
|
|
2819
|
-
{logViewerAgent && (
|
|
2820
|
-
<LogViewerPanel
|
|
2821
|
-
agent={logViewerAgent}
|
|
2822
|
-
isOpen={true}
|
|
2823
|
-
onClose={() => setLogViewerAgent(null)}
|
|
2824
|
-
availableAgents={agents}
|
|
2825
|
-
onAgentChange={setLogViewerAgent}
|
|
2826
|
-
/>
|
|
2827
|
-
)}
|
|
2828
|
-
|
|
2829
|
-
{/* Trajectory Panel - Fullscreen slide-over */}
|
|
2830
|
-
{isTrajectoryOpen && (
|
|
2831
|
-
<div
|
|
2832
|
-
className="fixed inset-0 z-50 flex bg-black/50 backdrop-blur-sm"
|
|
2833
|
-
onClick={() => setIsTrajectoryOpen(false)}
|
|
2834
|
-
>
|
|
2835
|
-
<div
|
|
2836
|
-
className="ml-auto w-full max-w-3xl h-full bg-bg-primary shadow-2xl animate-in slide-in-from-right duration-300 flex flex-col"
|
|
2837
|
-
onClick={(e) => e.stopPropagation()}
|
|
2838
|
-
>
|
|
2839
|
-
{/* Header */}
|
|
2840
|
-
<div className="flex items-center justify-between px-6 py-4 border-b border-border-subtle bg-bg-secondary">
|
|
2841
|
-
<div className="flex items-center gap-3">
|
|
2842
|
-
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500/20 to-accent-cyan/20 flex items-center justify-center border border-blue-500/30">
|
|
2843
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-blue-500">
|
|
2844
|
-
<path d="M3 12h4l3 9 4-18 3 9h4" strokeLinecap="round" strokeLinejoin="round" />
|
|
2845
|
-
</svg>
|
|
2846
|
-
</div>
|
|
2847
|
-
<div>
|
|
2848
|
-
<h2 className="text-lg font-semibold text-text-primary m-0">Trajectory Viewer</h2>
|
|
2849
|
-
<p className="text-xs text-text-muted m-0">
|
|
2850
|
-
{trajectoryStatus?.active ? `Active: ${trajectoryStatus.task || 'Working...'}` : 'Browse past trajectories'}
|
|
2851
|
-
</p>
|
|
2852
|
-
</div>
|
|
2853
|
-
</div>
|
|
2854
|
-
<button
|
|
2855
|
-
onClick={() => setIsTrajectoryOpen(false)}
|
|
2856
|
-
className="w-10 h-10 rounded-lg bg-bg-tertiary border border-border-subtle flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-bg-hover hover:border-blue-500/50 transition-all"
|
|
2857
|
-
title="Close (Esc)"
|
|
2858
|
-
>
|
|
2859
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2860
|
-
<path d="M18 6L6 18M6 6l12 12" />
|
|
2861
|
-
</svg>
|
|
2862
|
-
</button>
|
|
2863
|
-
</div>
|
|
2864
|
-
|
|
2865
|
-
{/* Content */}
|
|
2866
|
-
<div className="flex-1 overflow-hidden p-6">
|
|
2867
|
-
<TrajectoryViewer
|
|
2868
|
-
agentName={selectedTrajectoryTitle?.slice(0, 30) || trajectoryStatus?.task?.slice(0, 30) || 'Trajectories'}
|
|
2869
|
-
steps={trajectorySteps}
|
|
2870
|
-
history={trajectoryHistory}
|
|
2871
|
-
selectedTrajectoryId={selectedTrajectoryId}
|
|
2872
|
-
onSelectTrajectory={selectTrajectory}
|
|
2873
|
-
isLoading={isTrajectoryLoading}
|
|
2874
|
-
/>
|
|
2875
|
-
</div>
|
|
2876
|
-
</div>
|
|
2877
|
-
</div>
|
|
2878
|
-
)}
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
{/* Decision Queue Panel */}
|
|
2882
|
-
{isDecisionQueueOpen && (
|
|
2883
|
-
<div className="fixed left-4 bottom-4 w-[400px] max-h-[500px] z-50 shadow-modal">
|
|
2884
|
-
<div className="relative">
|
|
2885
|
-
<button
|
|
2886
|
-
onClick={() => setIsDecisionQueueOpen(false)}
|
|
2887
|
-
className="absolute -top-2 -right-2 w-6 h-6 bg-bg-elevated border border-border rounded-full flex items-center justify-center text-text-muted hover:text-text-primary hover:bg-bg-hover z-10"
|
|
2888
|
-
title="Close decisions"
|
|
2889
|
-
>
|
|
2890
|
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2891
|
-
<path d="M18 6L6 18M6 6l12 12" />
|
|
2892
|
-
</svg>
|
|
2893
|
-
</button>
|
|
2894
|
-
<DecisionQueue
|
|
2895
|
-
decisions={decisions}
|
|
2896
|
-
onApprove={handleDecisionApprove}
|
|
2897
|
-
onReject={handleDecisionReject}
|
|
2898
|
-
onDismiss={handleDecisionDismiss}
|
|
2899
|
-
isProcessing={decisionProcessing}
|
|
2900
|
-
/>
|
|
2901
|
-
</div>
|
|
2902
|
-
</div>
|
|
2903
|
-
)}
|
|
2904
|
-
|
|
2905
|
-
{/* Decision Queue Toggle Button (bottom-left when panel is closed) */}
|
|
2906
|
-
{!isDecisionQueueOpen && decisions.length > 0 && (
|
|
2907
|
-
<button
|
|
2908
|
-
onClick={() => setIsDecisionQueueOpen(true)}
|
|
2909
|
-
className="fixed left-4 bottom-4 w-12 h-12 bg-warning text-bg-deep rounded-full shadow-[0_0_20px_rgba(255,107,53,0.4)] flex items-center justify-center hover:scale-105 transition-transform z-50"
|
|
2910
|
-
title={`${decisions.length} pending decision${decisions.length > 1 ? 's' : ''}`}
|
|
2911
|
-
>
|
|
2912
|
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
2913
|
-
<circle cx="12" cy="12" r="10" />
|
|
2914
|
-
<line x1="12" y1="8" x2="12" y2="12" />
|
|
2915
|
-
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
2916
|
-
</svg>
|
|
2917
|
-
{decisions.length > 0 && (
|
|
2918
|
-
<span className="absolute -top-1 -right-1 w-5 h-5 bg-error text-white text-[10px] font-bold rounded-full flex items-center justify-center">
|
|
2919
|
-
{decisions.length}
|
|
2920
|
-
</span>
|
|
2921
|
-
)}
|
|
2922
|
-
</button>
|
|
2923
|
-
)}
|
|
2924
|
-
|
|
2925
|
-
{/* User Profile Panel */}
|
|
2926
|
-
<UserProfilePanel
|
|
2927
|
-
user={selectedUserProfile}
|
|
2928
|
-
onClose={() => setSelectedUserProfile(null)}
|
|
2929
|
-
onMention={(username) => {
|
|
2930
|
-
// Set pending mention to trigger insertion in MessageComposer
|
|
2931
|
-
setPendingMention(username);
|
|
2932
|
-
setSelectedUserProfile(null);
|
|
2933
|
-
}}
|
|
2934
|
-
onSendMessage={(user) => {
|
|
2935
|
-
setCurrentChannel(user.username);
|
|
2936
|
-
markDmSeen(user.username);
|
|
2937
|
-
setSelectedUserProfile(null);
|
|
2938
|
-
}}
|
|
2939
|
-
/>
|
|
2940
|
-
|
|
2941
|
-
{/* Agent Profile Panel */}
|
|
2942
|
-
<AgentProfilePanel
|
|
2943
|
-
agent={selectedAgentProfile}
|
|
2944
|
-
onClose={() => setSelectedAgentProfile(null)}
|
|
2945
|
-
onMessage={(agent) => {
|
|
2946
|
-
selectAgent(agent.name);
|
|
2947
|
-
setCurrentChannel(agent.name);
|
|
2948
|
-
setSelectedAgentProfile(null);
|
|
2949
|
-
}}
|
|
2950
|
-
onLogs={handleLogsClick}
|
|
2951
|
-
onRelease={handleReleaseAgent}
|
|
2952
|
-
summary={selectedAgentProfile ? agentSummariesMap.get(selectedAgentProfile.name.toLowerCase()) : null}
|
|
2953
|
-
/>
|
|
2954
|
-
|
|
2955
|
-
{/* Coordinator Panel */}
|
|
2956
|
-
<CoordinatorPanel
|
|
2957
|
-
isOpen={isCoordinatorOpen}
|
|
2958
|
-
onClose={() => setIsCoordinatorOpen(false)}
|
|
2959
|
-
projects={mergedProjects}
|
|
2960
|
-
isCloudMode={!!currentUser}
|
|
2961
|
-
hasArchitect={bridgeAgents.some(a => a.name.toLowerCase() === 'architect')}
|
|
2962
|
-
onArchitectSpawned={() => {
|
|
2963
|
-
// Architect will appear via WebSocket update
|
|
2964
|
-
setIsCoordinatorOpen(false);
|
|
2965
|
-
}}
|
|
2966
|
-
/>
|
|
2967
|
-
|
|
2968
|
-
{/* Full Settings Page */}
|
|
2969
|
-
{isFullSettingsOpen && (
|
|
2970
|
-
<SettingsPage
|
|
2971
|
-
currentUserId={cloudSession?.user?.id}
|
|
2972
|
-
initialTab={settingsInitialTab}
|
|
2973
|
-
onClose={() => setIsFullSettingsOpen(false)}
|
|
2974
|
-
settings={settings}
|
|
2975
|
-
onUpdateSettings={updateSettings}
|
|
2976
|
-
activeWorkspaceId={effectiveActiveWorkspaceId}
|
|
2977
|
-
/>
|
|
2978
|
-
)}
|
|
2979
|
-
|
|
2980
|
-
{/* Toast Notifications */}
|
|
2981
|
-
<NotificationToast
|
|
2982
|
-
toasts={toasts}
|
|
2983
|
-
onDismiss={dismissToast}
|
|
2984
|
-
position="top-right"
|
|
2985
|
-
/>
|
|
2986
|
-
</div>
|
|
2987
|
-
</WorkspaceProvider>
|
|
2988
|
-
);
|
|
2989
|
-
}
|
|
2990
|
-
|
|
2991
|
-
function LoadingSpinner() {
|
|
2992
|
-
return (
|
|
2993
|
-
<svg className="animate-spin mb-4 text-accent-cyan" width="28" height="28" viewBox="0 0 24 24">
|
|
2994
|
-
<circle
|
|
2995
|
-
cx="12"
|
|
2996
|
-
cy="12"
|
|
2997
|
-
r="10"
|
|
2998
|
-
stroke="currentColor"
|
|
2999
|
-
strokeWidth="2"
|
|
3000
|
-
fill="none"
|
|
3001
|
-
strokeDasharray="32"
|
|
3002
|
-
strokeLinecap="round"
|
|
3003
|
-
/>
|
|
3004
|
-
</svg>
|
|
3005
|
-
);
|
|
3006
|
-
}
|
|
3007
|
-
|
|
3008
|
-
function ErrorIcon() {
|
|
3009
|
-
return (
|
|
3010
|
-
<svg className="text-error mb-4" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
3011
|
-
<circle cx="12" cy="12" r="10" />
|
|
3012
|
-
<line x1="12" y1="8" x2="12" y2="12" />
|
|
3013
|
-
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
3014
|
-
</svg>
|
|
3015
|
-
);
|
|
3016
|
-
}
|
|
3017
|
-
|
|
3018
|
-
function HashIconLarge() {
|
|
3019
|
-
return (
|
|
3020
|
-
<svg className="text-text-muted mb-4" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
3021
|
-
<line x1="4" y1="9" x2="20" y2="9" />
|
|
3022
|
-
<line x1="4" y1="15" x2="20" y2="15" />
|
|
3023
|
-
<line x1="10" y1="3" x2="8" y2="21" />
|
|
3024
|
-
<line x1="16" y1="3" x2="14" y2="21" />
|
|
3025
|
-
</svg>
|
|
3026
|
-
);
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
/**
|
|
3030
|
-
* Legacy CSS styles export - kept for backwards compatibility
|
|
3031
|
-
* @deprecated Use Tailwind classes directly instead
|
|
3032
|
-
*/
|
|
3033
|
-
export const appStyles = '';
|