From 8afb6c1a907c7a67c267d2d403ecded9b069f6d6 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 5 Jan 2026 20:52:20 +0100 Subject: [PATCH 1/4] trajectories --- .beads/issues.jsonl | 1 + .trajectories/active/traj_22bhyulruouw.json | 46 +++++++++ .../2026-01}/traj_7ludwvz45veh.json | 13 ++- .../completed/2026-01/traj_7ludwvz45veh.md | 97 +++++++++++++++++++ .trajectories/index.json | 13 ++- 5 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 .trajectories/active/traj_22bhyulruouw.json rename .trajectories/{active => completed/2026-01}/traj_7ludwvz45veh.json (97%) create mode 100644 .trajectories/completed/2026-01/traj_7ludwvz45veh.md diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 1176df9f9..000b655a4 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -119,6 +119,7 @@ {"id":"agent-relay-454","title":"OpenCode headless mode integration","description":"Integrate OpenCode's headless mode (opencode run) with Agent Relay. Options: 1) Create MCP server adapter for agent-relay that OpenCode can use, 2) Document OpenCode config to work with relay. See: https://github.com/anomalyco/opencode/issues/953","status":"open","priority":3,"issue_type":"feature","created_at":"2026-01-04T01:01:55.715466+01:00","updated_at":"2026-01-04T01:01:55.715466+01:00"} {"id":"agent-relay-455","title":"Create shared types package between backend and frontend","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-04T21:03:08.485997+01:00","updated_at":"2026-01-04T21:03:08.485997+01:00"} {"id":"agent-relay-456","title":"Create dedicated /app/onboarding route for workspace deletion flow","description":"Currently when a workspace is deleted, we redirect to /app which handles multiple states (workspace selection, no-workspaces, etc). This works but could be more robust with a dedicated onboarding route.\n\n**Current behavior:**\n- Workspace deleted → redirect to /app\n- /app re-initializes and shows appropriate state based on remaining workspaces/repos\n\n**Proposed improvements:**\n1. Create /app/onboarding route with explicit onboarding UI\n2. Different UX for first-time users vs users who deleted their workspace\n3. Better analytics tracking for onboarding funnel\n4. Cleaner separation of concerns between workspace selection and onboarding\n\n**Files involved:**\n- src/dashboard/app/app/page.tsx (current state machine)\n- src/dashboard/react-components/settings/WorkspaceSettingsPanel.tsx (deletion redirect)","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-05T17:10:00.072986+01:00","updated_at":"2026-01-05T17:10:10.425185+01:00"} +{"id":"agent-relay-457","title":"Responsive: metrics/trajectories missing from hamburger menu","description":"On mobile/responsive views, the navigation options for metrics and trajectories disappear entirely instead of being added to the hamburger menu. Users cannot access these features on smaller screens. Expected: Nav items should collapse into hamburger menu at mobile breakpoints.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"Frontend","created_at":"2026-01-05T20:45:18.983136+01:00","updated_at":"2026-01-05T20:45:28.504+01:00"} {"id":"agent-relay-47z","title":"Express 5 may have breaking changes from Express 4 patterns","description":"package.json uses express@5.2.1 which is a major version with breaking changes from Express 4. Verify: (1) Error handling middleware patterns, (2) Router behavior, (3) Body parsing (express.json vs body-parser).","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-20T00:18:49.269841+01:00","updated_at":"2025-12-20T00:18:49.269841+01:00"} {"id":"agent-relay-4e0","title":"Fix message truncation - messages cut off at source","description":"Root cause found: parser.ts:40 inline regex only captures single line. Multi-line messages are split by parsePassThrough() at line 206. Fix options: (1) Allow continuation lines in inline format, (2) Use block format for multi-line, (3) Add heuristic to join lines until next @relay pattern.","status":"closed","priority":2,"issue_type":"bug","assignee":"MistyShelter","created_at":"2025-12-19T23:40:35.082717+01:00","updated_at":"2025-12-20T00:03:54.806087+01:00","closed_at":"2025-12-20T00:03:54.806087+01:00"} {"id":"agent-relay-4ft","title":"Merge project info into status command","status":"closed","priority":2,"issue_type":"task","assignee":"Pruner","created_at":"2025-12-19T21:59:52.685495+01:00","updated_at":"2025-12-19T22:06:44.276187+01:00","closed_at":"2025-12-19T22:06:44.276187+01:00"} diff --git a/.trajectories/active/traj_22bhyulruouw.json b/.trajectories/active/traj_22bhyulruouw.json new file mode 100644 index 000000000..26a8dd080 --- /dev/null +++ b/.trajectories/active/traj_22bhyulruouw.json @@ -0,0 +1,46 @@ +{ + "id": "traj_22bhyulruouw", + "version": 1, + "task": { + "title": "Lead agent coordination - responsive nav fix and team assignments", + "source": { + "system": "plain", + "id": "agent-relay-457" + } + }, + "status": "active", + "startedAt": "2026-01-05T19:45:50.807Z", + "agents": [ + { + "name": "Lead", + "role": "lead", + "joinedAt": "2026-01-05T19:45:50.808Z" + } + ], + "chapters": [ + { + "id": "chap_aqd40zd682kf", + "title": "Work", + "agentName": "default", + "startedAt": "2026-01-05T19:52:06.440Z", + "events": [ + { + "ts": 1767642726441, + "type": "decision", + "content": "Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis: Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis", + "raw": { + "question": "Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis", + "chosen": "Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis", + "alternatives": [], + "reasoning": "P1 bug blocking mobile users - provided line numbers and fix approach for Header.tsx" + }, + "significance": "high" + } + ] + } + ], + "commits": [], + "filesChanged": [], + "projectId": "/Users/khaliqgant/Projects/agent-workforce/relay", + "tags": [] +} \ No newline at end of file diff --git a/.trajectories/active/traj_7ludwvz45veh.json b/.trajectories/completed/2026-01/traj_7ludwvz45veh.json similarity index 97% rename from .trajectories/active/traj_7ludwvz45veh.json rename to .trajectories/completed/2026-01/traj_7ludwvz45veh.json index e6557bcfb..4a2c315db 100644 --- a/.trajectories/active/traj_7ludwvz45veh.json +++ b/.trajectories/completed/2026-01/traj_7ludwvz45veh.json @@ -8,7 +8,7 @@ "id": "pre-launch-fixes" } }, - "status": "active", + "status": "completed", "startedAt": "2026-01-04T00:05:43.304Z", "agents": [ { @@ -192,11 +192,18 @@ }, "significance": "high" } - ] + ], + "endedAt": "2026-01-05T19:45:45.762Z" } ], "commits": [], "filesChanged": [], "projectId": "/Users/khaliqgant/Projects/agent-workforce/relay", - "tags": [] + "tags": [], + "completedAt": "2026-01-05T19:45:45.762Z", + "retrospective": { + "summary": "Previous session work on provider CLI auth flow", + "approach": "Standard approach", + "confidence": 0.7 + } } \ No newline at end of file diff --git a/.trajectories/completed/2026-01/traj_7ludwvz45veh.md b/.trajectories/completed/2026-01/traj_7ludwvz45veh.md new file mode 100644 index 000000000..6494fde46 --- /dev/null +++ b/.trajectories/completed/2026-01/traj_7ludwvz45veh.md @@ -0,0 +1,97 @@ +# Trajectory: Provider CLI auth flow for cloud workspaces + +> **Status:** ✅ Completed +> **Task:** pre-launch-fixes +> **Confidence:** 70% +> **Started:** January 4, 2026 at 01:05 AM +> **Completed:** January 5, 2026 at 08:45 PM + +--- + +## Summary + +Previous session work on provider CLI auth flow + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Pre-seed Claude CLI config to skip interactive setup +- **Chose:** Pre-seed Claude CLI config to skip interactive setup +- **Reasoning:** Claude CLI has interactive first-run (theme selection, etc). Alternative was web terminal (xterm.js) which is more flexible but complex. Pre-seeding config is simpler for MVP. May revisit for web terminal if other CLIs have similar issues. + +### Add settings page for CLI provider management +- **Chose:** Add settings page for CLI provider management +- **Reasoning:** Users should be able to connect additional AI providers after initial setup. Settings page in workspace dashboard will allow connecting Claude, Codex, OpenCode, Droid at any time, not just during initial workspace setup. + +### Default trajectories to opt-out (user-level storage) +- **Chose:** Default trajectories to opt-out (user-level storage) +- **Reasoning:** Most repos won't want trajectory files in source control. Users must explicitly opt-in to store in repo via .relay/config.json + +### Store user-level trajectories in ~/.config/agent-relay/trajectories// +- **Chose:** Store user-level trajectories in ~/.config/agent-relay/trajectories// +- **Reasoning:** XDG-compliant path, project-isolated via hash to prevent collisions, survives repo deletion + +### Trajectory settings configurable after GitHub app setup +- **Chose:** Trajectory settings configurable after GitHub app setup +- **Reasoning:** Users should configure .relay/config.json after connecting repo to cloud workspace. This happens in the workspace onboarding flow. + +### Add dashboard API for trajectory preferences +- **Chose:** Add dashboard API for trajectory preferences +- **Reasoning:** Users configure via dashboard after GitHub app setup, during workspace onboarding + +### Add comprehensive settings with trajectory explanations +- **Chose:** Add comprehensive settings with trajectory explanations +- **Reasoning:** Users need to understand what trajectories are (PDERO paradigm), why they'd opt-in, and link to pdero.com for more info + +### Investigate Claude OAuth login flow +- **Chose:** Investigate Claude OAuth login flow +- **Reasoning:** Current provider setup uses API keys but Claude uses OAuth. Need to bypass interactive prompts and get login URL for popup-based auth. + +### Cloud provider auth strategy for Claude +- **Chose:** Cloud provider auth strategy for Claude +- **Reasoning:** Claude uses OAuth in cloud environments. For users connecting accounts: 1) API key works (already supported), 2) CLI setup-token is interactive, 3) Need proper OAuth device flow from Anthropic. Recommend API key for now with improved UX. + +### Use node-pty for CLI OAuth flow +- **Chose:** Use node-pty for CLI OAuth flow +- **Reasoning:** Regular spawn with pipes doesn't properly emulate TTY, causing CLIs to behave differently. PTY ensures auth URLs are output correctly and allows sending responses to interactive prompts. + +### Auto-respond to Claude interactive setup prompts +- **Chose:** Auto-respond to Claude interactive setup prompts +- **Reasoning:** Claude has multi-step setup: dark mode -> auth method -> login URL. We detect prompts and send enter key to progress through them automatically. + +### Fixed cloud server body limit for screenshot uploads +- **Chose:** Fixed cloud server body limit for screenshot uploads +- **Reasoning:** Default express.json limit of 100kb was too small for base64 encoded images + +### Added GH_TOKEN env var for gh CLI compatibility +- **Chose:** Added GH_TOKEN env var for gh CLI compatibility +- **Reasoning:** gh CLI uses GH_TOKEN not GITHUB_TOKEN; added to all provisioners (Fly.io, Railway, Docker) + +### Fixed mobile usability issues in dashboard +- **Chose:** Fixed mobile usability issues in dashboard +- **Reasoning:** Hamburger menu visibility, logs button always visible on mobile, responsive padding throughout + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Pre-seed Claude CLI config to skip interactive setup: Pre-seed Claude CLI config to skip interactive setup +- Add settings page for CLI provider management: Add settings page for CLI provider management +- Default trajectories to opt-out (user-level storage): Default trajectories to opt-out (user-level storage) +- Store user-level trajectories in ~/.config/agent-relay/trajectories//: Store user-level trajectories in ~/.config/agent-relay/trajectories// +- Trajectory settings configurable after GitHub app setup: Trajectory settings configurable after GitHub app setup +- Add dashboard API for trajectory preferences: Add dashboard API for trajectory preferences +- Add comprehensive settings with trajectory explanations: Add comprehensive settings with trajectory explanations +- Investigate Claude OAuth login flow: Investigate Claude OAuth login flow +- Cloud provider auth strategy for Claude: Cloud provider auth strategy for Claude +- Use node-pty for CLI OAuth flow: Use node-pty for CLI OAuth flow +- Auto-respond to Claude interactive setup prompts: Auto-respond to Claude interactive setup prompts +- Fixed cloud server body limit for screenshot uploads: Fixed cloud server body limit for screenshot uploads +- Added GH_TOKEN env var for gh CLI compatibility: Added GH_TOKEN env var for gh CLI compatibility +- Fixed mobile usability issues in dashboard: Fixed mobile usability issues in dashboard diff --git a/.trajectories/index.json b/.trajectories/index.json index 4382779eb..2bceb4577 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-01-05T17:59:19.223Z", + "lastUpdated": "2026-01-05T19:52:06.442Z", "trajectories": { "traj_ozd98si6a7ns": { "title": "Fix thinking indicator showing on all messages", @@ -277,9 +277,16 @@ }, "traj_7ludwvz45veh": { "title": "Provider CLI auth flow for cloud workspaces", - "status": "active", + "status": "completed", "startedAt": "2026-01-04T00:05:43.304Z", - "path": "/workspace/relay/.trajectories/active/traj_7ludwvz45veh.json" + "completedAt": "2026-01-05T19:45:45.762Z", + "path": "/Users/khaliqgant/Projects/agent-workforce/relay/.trajectories/completed/2026-01/traj_7ludwvz45veh.json" + }, + "traj_22bhyulruouw": { + "title": "Lead agent coordination - responsive nav fix and team assignments", + "status": "active", + "startedAt": "2026-01-05T19:45:50.807Z", + "path": "/Users/khaliqgant/Projects/agent-workforce/relay/.trajectories/active/traj_22bhyulruouw.json" } } } \ No newline at end of file From 06dd338a02b4fcd87035dc127d94ebcf2be8d940 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 5 Jan 2026 21:56:02 +0100 Subject: [PATCH 2/4] billing and styling --- .beads/issues.jsonl | 6 +- .gitignore | 1 + .trajectories/active/traj_22bhyulruouw.json | 46 -- .../completed/2026-01/traj_22bhyulruouw.json | 113 +++++ .../completed/2026-01/traj_22bhyulruouw.md | 57 +++ .../completed/2026-01/traj_hhxte7w4gjjx.json | 22 + .../completed/2026-01/traj_hhxte7w4gjjx.md | 5 + .trajectories/index.json | 14 +- src/cli/index.ts | 8 +- src/cloud/api/billing.ts | 43 +- src/cloud/api/workspaces.ts | 89 +++- src/cloud/billing/plans.ts | 38 +- src/cloud/provisioner/index.ts | 165 ++++++- src/cloud/services/planLimits.ts | 74 ++- src/dashboard/app/app/onboarding/page.tsx | 394 ++++++++++++++++ src/dashboard/landing/LandingPage.tsx | 14 +- src/dashboard/landing/PricingPage.tsx | 68 +-- src/dashboard/react-components/App.tsx | 34 ++ .../react-components/BillingResult.tsx | 440 ++++++++++++++++++ .../WorkspaceStatusIndicator.tsx | 5 +- .../react-components/layout/Header.tsx | 13 +- .../react-components/layout/Sidebar.tsx | 125 +++++ .../settings/WorkspaceSettingsPanel.tsx | 4 +- 23 files changed, 1623 insertions(+), 155 deletions(-) delete mode 100644 .trajectories/active/traj_22bhyulruouw.json create mode 100644 .trajectories/completed/2026-01/traj_22bhyulruouw.json create mode 100644 .trajectories/completed/2026-01/traj_22bhyulruouw.md create mode 100644 .trajectories/completed/2026-01/traj_hhxte7w4gjjx.json create mode 100644 .trajectories/completed/2026-01/traj_hhxte7w4gjjx.md create mode 100644 src/dashboard/app/app/onboarding/page.tsx create mode 100644 src/dashboard/react-components/BillingResult.tsx diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 000b655a4..1f3a675e3 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -118,8 +118,10 @@ {"id":"agent-relay-453","title":"BUG: Spawn command fails silently when CLI not specified","description":"Users can send `-\u003erelay:spawn WorkerName` without a CLI type, but the parser silently ignores it because it requires both name AND cli. \n\nParse code at pty-wrapper.ts:931 checks `parts.length \u003e= 2` which fails for commands like:\n- `-\u003erelay:spawn Investigator`\n\nShould either:\n1. Make CLI optional with sensible default (claude)\n2. Provide error feedback when CLI is missing\n\nThis blocks relay spawn/release functionality entirely.","status":"closed","priority":0,"issue_type":"bug","assignee":"Backend","created_at":"2026-01-03T16:43:37.927258+01:00","updated_at":"2026-01-03T16:50:11.02666+01:00","closed_at":"2026-01-03T16:50:11.02666+01:00"} {"id":"agent-relay-454","title":"OpenCode headless mode integration","description":"Integrate OpenCode's headless mode (opencode run) with Agent Relay. Options: 1) Create MCP server adapter for agent-relay that OpenCode can use, 2) Document OpenCode config to work with relay. See: https://github.com/anomalyco/opencode/issues/953","status":"open","priority":3,"issue_type":"feature","created_at":"2026-01-04T01:01:55.715466+01:00","updated_at":"2026-01-04T01:01:55.715466+01:00"} {"id":"agent-relay-455","title":"Create shared types package between backend and frontend","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-04T21:03:08.485997+01:00","updated_at":"2026-01-04T21:03:08.485997+01:00"} -{"id":"agent-relay-456","title":"Create dedicated /app/onboarding route for workspace deletion flow","description":"Currently when a workspace is deleted, we redirect to /app which handles multiple states (workspace selection, no-workspaces, etc). This works but could be more robust with a dedicated onboarding route.\n\n**Current behavior:**\n- Workspace deleted → redirect to /app\n- /app re-initializes and shows appropriate state based on remaining workspaces/repos\n\n**Proposed improvements:**\n1. Create /app/onboarding route with explicit onboarding UI\n2. Different UX for first-time users vs users who deleted their workspace\n3. Better analytics tracking for onboarding funnel\n4. Cleaner separation of concerns between workspace selection and onboarding\n\n**Files involved:**\n- src/dashboard/app/app/page.tsx (current state machine)\n- src/dashboard/react-components/settings/WorkspaceSettingsPanel.tsx (deletion redirect)","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-05T17:10:00.072986+01:00","updated_at":"2026-01-05T17:10:10.425185+01:00"} -{"id":"agent-relay-457","title":"Responsive: metrics/trajectories missing from hamburger menu","description":"On mobile/responsive views, the navigation options for metrics and trajectories disappear entirely instead of being added to the hamburger menu. Users cannot access these features on smaller screens. Expected: Nav items should collapse into hamburger menu at mobile breakpoints.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"Frontend","created_at":"2026-01-05T20:45:18.983136+01:00","updated_at":"2026-01-05T20:45:28.504+01:00"} +{"id":"agent-relay-456","title":"Create dedicated /app/onboarding route for workspace deletion flow","description":"Currently when a workspace is deleted, we redirect to /app which handles multiple states (workspace selection, no-workspaces, etc). This works but could be more robust with a dedicated onboarding route.\n\n**Current behavior:**\n- Workspace deleted → redirect to /app\n- /app re-initializes and shows appropriate state based on remaining workspaces/repos\n\n**Proposed improvements:**\n1. Create /app/onboarding route with explicit onboarding UI\n2. Different UX for first-time users vs users who deleted their workspace\n3. Better analytics tracking for onboarding funnel\n4. Cleaner separation of concerns between workspace selection and onboarding\n\n**Files involved:**\n- src/dashboard/app/app/page.tsx (current state machine)\n- src/dashboard/react-components/settings/WorkspaceSettingsPanel.tsx (deletion redirect)","status":"closed","priority":3,"issue_type":"task","assignee":"Frontend","created_at":"2026-01-05T17:10:00.072986+01:00","updated_at":"2026-01-05T21:08:00.860915+01:00","closed_at":"2026-01-05T21:08:00.860915+01:00"} +{"id":"agent-relay-457","title":"Responsive: metrics/trajectories missing from hamburger menu","description":"On mobile/responsive views, the navigation options for metrics and trajectories disappear entirely instead of being added to the hamburger menu. Users cannot access these features on smaller screens. Expected: Nav items should collapse into hamburger menu at mobile breakpoints.","status":"closed","priority":1,"issue_type":"bug","assignee":"Frontend","created_at":"2026-01-05T20:45:18.983136+01:00","updated_at":"2026-01-05T20:59:32.834122+01:00","closed_at":"2026-01-05T20:59:32.834122+01:00"} +{"id":"agent-relay-458","title":"Implement Free to Paid plan upgrade flow","description":"Users on the Free plan need a way to upgrade to paid plans (Pro, Team). Includes: upgrade UI in dashboard, Stripe checkout integration, plan limit updates, and success confirmation flow.","status":"closed","priority":1,"issue_type":"feature","assignee":"Billing","created_at":"2026-01-05T21:03:16.765398+01:00","updated_at":"2026-01-05T21:10:36.286785+01:00","closed_at":"2026-01-05T21:10:36.286785+01:00"} +{"id":"agent-relay-459","title":"Header status text overflow - truncate with hover tooltip","description":"Status text in the header overflows and gets cut off when too long. Need to: 1) Limit/truncate the status text with ellipsis 2) Show full text on hover via tooltip. See screenshot - text like 'Frontend starting onboarding route work...' overflows into the New Message button area.","status":"closed","priority":2,"issue_type":"bug","assignee":"Frontend","created_at":"2026-01-05T21:06:15.400581+01:00","updated_at":"2026-01-05T21:19:17.729021+01:00","closed_at":"2026-01-05T21:19:17.729021+01:00"} {"id":"agent-relay-47z","title":"Express 5 may have breaking changes from Express 4 patterns","description":"package.json uses express@5.2.1 which is a major version with breaking changes from Express 4. Verify: (1) Error handling middleware patterns, (2) Router behavior, (3) Body parsing (express.json vs body-parser).","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-20T00:18:49.269841+01:00","updated_at":"2025-12-20T00:18:49.269841+01:00"} {"id":"agent-relay-4e0","title":"Fix message truncation - messages cut off at source","description":"Root cause found: parser.ts:40 inline regex only captures single line. Multi-line messages are split by parsePassThrough() at line 206. Fix options: (1) Allow continuation lines in inline format, (2) Use block format for multi-line, (3) Add heuristic to join lines until next @relay pattern.","status":"closed","priority":2,"issue_type":"bug","assignee":"MistyShelter","created_at":"2025-12-19T23:40:35.082717+01:00","updated_at":"2025-12-20T00:03:54.806087+01:00","closed_at":"2025-12-20T00:03:54.806087+01:00"} {"id":"agent-relay-4ft","title":"Merge project info into status command","status":"closed","priority":2,"issue_type":"task","assignee":"Pruner","created_at":"2025-12-19T21:59:52.685495+01:00","updated_at":"2025-12-19T22:06:44.276187+01:00","closed_at":"2025-12-19T22:06:44.276187+01:00"} diff --git a/.gitignore b/.gitignore index 67e17f6fb..aa598f95d 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ src/dashboard/out .env.local .env +.env.production diff --git a/.trajectories/active/traj_22bhyulruouw.json b/.trajectories/active/traj_22bhyulruouw.json deleted file mode 100644 index 26a8dd080..000000000 --- a/.trajectories/active/traj_22bhyulruouw.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "id": "traj_22bhyulruouw", - "version": 1, - "task": { - "title": "Lead agent coordination - responsive nav fix and team assignments", - "source": { - "system": "plain", - "id": "agent-relay-457" - } - }, - "status": "active", - "startedAt": "2026-01-05T19:45:50.807Z", - "agents": [ - { - "name": "Lead", - "role": "lead", - "joinedAt": "2026-01-05T19:45:50.808Z" - } - ], - "chapters": [ - { - "id": "chap_aqd40zd682kf", - "title": "Work", - "agentName": "default", - "startedAt": "2026-01-05T19:52:06.440Z", - "events": [ - { - "ts": 1767642726441, - "type": "decision", - "content": "Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis: Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis", - "raw": { - "question": "Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis", - "chosen": "Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis", - "alternatives": [], - "reasoning": "P1 bug blocking mobile users - provided line numbers and fix approach for Header.tsx" - }, - "significance": "high" - } - ] - } - ], - "commits": [], - "filesChanged": [], - "projectId": "/Users/khaliqgant/Projects/agent-workforce/relay", - "tags": [] -} \ No newline at end of file diff --git a/.trajectories/completed/2026-01/traj_22bhyulruouw.json b/.trajectories/completed/2026-01/traj_22bhyulruouw.json new file mode 100644 index 000000000..e950a7105 --- /dev/null +++ b/.trajectories/completed/2026-01/traj_22bhyulruouw.json @@ -0,0 +1,113 @@ +{ + "id": "traj_22bhyulruouw", + "version": 1, + "task": { + "title": "Lead agent coordination - responsive nav fix and team assignments", + "source": { + "system": "plain", + "id": "agent-relay-457" + } + }, + "status": "completed", + "startedAt": "2026-01-05T19:45:50.807Z", + "agents": [ + { + "name": "Lead", + "role": "lead", + "joinedAt": "2026-01-05T19:45:50.808Z" + } + ], + "chapters": [ + { + "id": "chap_aqd40zd682kf", + "title": "Work", + "agentName": "default", + "startedAt": "2026-01-05T19:52:06.440Z", + "events": [ + { + "ts": 1767642726441, + "type": "decision", + "content": "Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis: Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis", + "raw": { + "question": "Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis", + "chosen": "Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis", + "alternatives": [], + "reasoning": "P1 bug blocking mobile users - provided line numbers and fix approach for Header.tsx" + }, + "significance": "high" + }, + { + "ts": 1767643225079, + "type": "decision", + "content": "Assigned onboarding route task (agent-relay-456) to Frontend: Assigned onboarding route task (agent-relay-456) to Frontend", + "raw": { + "question": "Assigned onboarding route task (agent-relay-456) to Frontend", + "chosen": "Assigned onboarding route task (agent-relay-456) to Frontend", + "alternatives": [], + "reasoning": "P3 frontend task - cleaner separation of workspace selection and onboarding flows" + }, + "significance": "high" + }, + { + "ts": 1767643436615, + "type": "decision", + "content": "Coordinated parallel work between Frontend and Billing: Coordinated parallel work between Frontend and Billing", + "raw": { + "question": "Coordinated parallel work between Frontend and Billing", + "chosen": "Coordinated parallel work between Frontend and Billing", + "alternatives": [], + "reasoning": "Verified no file conflicts - Frontend in react-components, Billing in cloud/billing and cloud/services" + }, + "significance": "high" + }, + { + "ts": 1767643545171, + "type": "decision", + "content": "Implementing webhook handler to sync user plan: Implementing webhook handler to sync user plan", + "raw": { + "question": "Implementing webhook handler to sync user plan", + "chosen": "Implementing webhook handler to sync user plan", + "alternatives": [], + "reasoning": "Webhook currently logs but doesn't update database. Need to update users.plan field when subscription created/updated." + }, + "significance": "high" + }, + { + "ts": 1767643671803, + "type": "decision", + "content": "Webhook handler implemented to update user plan: Webhook handler implemented to update user plan", + "raw": { + "question": "Webhook handler implemented to update user plan", + "chosen": "Webhook handler implemented to update user plan", + "alternatives": [], + "reasoning": "When Stripe subscription is created/updated/canceled, the webhook now updates users.plan in database. This completes the backend portion of the upgrade flow." + }, + "significance": "high" + }, + { + "ts": 1767643818368, + "type": "decision", + "content": "Implemented billing success/cancel pages: Implemented billing success/cancel pages", + "raw": { + "question": "Implemented billing success/cancel pages", + "chosen": "Implemented billing success/cancel pages", + "alternatives": [], + "reasoning": "Created BillingResult component that shows success confirmation after Stripe checkout (with plan features) or friendly cancel message. Added URL path detection in App.tsx to render these on /billing/success and /billing/canceled routes." + }, + "significance": "high" + } + ], + "endedAt": "2026-01-05T20:10:25.523Z" + } + ], + "commits": [], + "filesChanged": [], + "projectId": "/Users/khaliqgant/Projects/agent-workforce/relay", + "tags": [], + "completedAt": "2026-01-05T20:10:25.523Z", + "retrospective": { + "summary": "Implemented complete upgrade flow: webhook updates user plan on Stripe events, success/cancel pages show after checkout", + "approach": "Standard approach", + "confidence": 0.85 + } +} \ No newline at end of file diff --git a/.trajectories/completed/2026-01/traj_22bhyulruouw.md b/.trajectories/completed/2026-01/traj_22bhyulruouw.md new file mode 100644 index 000000000..04a641383 --- /dev/null +++ b/.trajectories/completed/2026-01/traj_22bhyulruouw.md @@ -0,0 +1,57 @@ +# Trajectory: Lead agent coordination - responsive nav fix and team assignments + +> **Status:** ✅ Completed +> **Task:** agent-relay-457 +> **Confidence:** 85% +> **Started:** January 5, 2026 at 08:45 PM +> **Completed:** January 5, 2026 at 09:10 PM + +--- + +## Summary + +Implemented complete upgrade flow: webhook updates user plan on Stripe events, success/cancel pages show after checkout + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis +- **Chose:** Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis +- **Reasoning:** P1 bug blocking mobile users - provided line numbers and fix approach for Header.tsx + +### Assigned onboarding route task (agent-relay-456) to Frontend +- **Chose:** Assigned onboarding route task (agent-relay-456) to Frontend +- **Reasoning:** P3 frontend task - cleaner separation of workspace selection and onboarding flows + +### Coordinated parallel work between Frontend and Billing +- **Chose:** Coordinated parallel work between Frontend and Billing +- **Reasoning:** Verified no file conflicts - Frontend in react-components, Billing in cloud/billing and cloud/services + +### Implementing webhook handler to sync user plan +- **Chose:** Implementing webhook handler to sync user plan +- **Reasoning:** Webhook currently logs but doesn't update database. Need to update users.plan field when subscription created/updated. + +### Webhook handler implemented to update user plan +- **Chose:** Webhook handler implemented to update user plan +- **Reasoning:** When Stripe subscription is created/updated/canceled, the webhook now updates users.plan in database. This completes the backend portion of the upgrade flow. + +### Implemented billing success/cancel pages +- **Chose:** Implemented billing success/cancel pages +- **Reasoning:** Created BillingResult component that shows success confirmation after Stripe checkout (with plan features) or friendly cancel message. Added URL path detection in App.tsx to render these on /billing/success and /billing/canceled routes. + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis: Assigned responsive nav bug (agent-relay-457) to Frontend with complete file analysis +- Assigned onboarding route task (agent-relay-456) to Frontend: Assigned onboarding route task (agent-relay-456) to Frontend +- Coordinated parallel work between Frontend and Billing: Coordinated parallel work between Frontend and Billing +- Implementing webhook handler to sync user plan: Implementing webhook handler to sync user plan +- Webhook handler implemented to update user plan: Webhook handler implemented to update user plan +- Implemented billing success/cancel pages: Implemented billing success/cancel pages diff --git a/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.json b/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.json new file mode 100644 index 000000000..6b76cb9e9 --- /dev/null +++ b/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.json @@ -0,0 +1,22 @@ +{ + "id": "traj_hhxte7w4gjjx", + "version": 1, + "task": { + "title": "test" + }, + "status": "abandoned", + "startedAt": "2026-01-05T20:14:01.563Z", + "agents": [ + { + "name": "khaliqgant", + "role": "lead", + "joinedAt": "2026-01-05T20:14:01.564Z" + } + ], + "chapters": [], + "commits": [], + "filesChanged": [], + "projectId": "/Users/khaliqgant/Projects/agent-workforce/relay", + "tags": [], + "completedAt": "2026-01-05T20:14:01.617Z" +} \ No newline at end of file diff --git a/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.md b/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.md new file mode 100644 index 000000000..11e39414d --- /dev/null +++ b/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.md @@ -0,0 +1,5 @@ +# Trajectory: test + +> **Status:** ❌ Abandoned +> **Started:** January 5, 2026 at 09:14 PM +> **Completed:** January 5, 2026 at 09:14 PM diff --git a/.trajectories/index.json b/.trajectories/index.json index 2bceb4577..3b2696fd0 100644 --- a/.trajectories/index.json +++ b/.trajectories/index.json @@ -1,6 +1,6 @@ { "version": 1, - "lastUpdated": "2026-01-05T19:52:06.442Z", + "lastUpdated": "2026-01-05T20:14:01.627Z", "trajectories": { "traj_ozd98si6a7ns": { "title": "Fix thinking indicator showing on all messages", @@ -284,9 +284,17 @@ }, "traj_22bhyulruouw": { "title": "Lead agent coordination - responsive nav fix and team assignments", - "status": "active", + "status": "completed", "startedAt": "2026-01-05T19:45:50.807Z", - "path": "/Users/khaliqgant/Projects/agent-workforce/relay/.trajectories/active/traj_22bhyulruouw.json" + "completedAt": "2026-01-05T20:10:25.523Z", + "path": "/Users/khaliqgant/Projects/agent-workforce/relay/.trajectories/completed/2026-01/traj_22bhyulruouw.json" + }, + "traj_hhxte7w4gjjx": { + "title": "test", + "status": "abandoned", + "startedAt": "2026-01-05T20:14:01.563Z", + "completedAt": "2026-01-05T20:14:01.617Z", + "path": "/Users/khaliqgant/Projects/agent-workforce/relay/.trajectories/completed/2026-01/traj_hhxte7w4gjjx.json" } } } \ No newline at end of file diff --git a/src/cli/index.ts b/src/cli/index.ts index fb7f97b28..739f8ac92 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1940,6 +1940,7 @@ program .action(async (args: string[]) => { const { spawn } = await import('node:child_process'); const { getProjectPaths } = await import('../utils/project-namespace.js'); + const { getPrimaryTrajectoriesDir, ensureTrajectoriesDir } = await import('../trajectory/config.js'); const paths = getProjectPaths(); @@ -1965,6 +1966,11 @@ program process.exit(1); } + // Get trajectory storage path based on config (respects opt-in/opt-out) + // Uses TRAJECTORIES_DATA_DIR env var which trail CLI reads + const trajectoriesDir = getPrimaryTrajectoriesDir(paths.projectRoot); + ensureTrajectoriesDir(paths.projectRoot); + // Spawn trail with the provided arguments const trailProc = spawn('trail', args, { cwd: paths.projectRoot, @@ -1972,7 +1978,7 @@ program env: { ...process.env, TRAJECTORIES_PROJECT: paths.projectId, - TRAJECTORIES_DATA_DIR: paths.dataDir, + TRAJECTORIES_DATA_DIR: trajectoriesDir, }, }); diff --git a/src/cloud/api/billing.ts b/src/cloud/api/billing.ts index 9b6d10130..ea8f89fa5 100644 --- a/src/cloud/api/billing.ts +++ b/src/cloud/api/billing.ts @@ -8,6 +8,8 @@ import { Router, Request, Response, NextFunction } from 'express'; import { getBillingService, getAllPlans, getPlan, comparePlans } from '../billing/index.js'; import type { SubscriptionTier } from '../billing/types.js'; import { getConfig } from '../config.js'; +import { db } from '../db/index.js'; +import type Stripe from 'stripe'; // Extend express session with user info declare module 'express-session' { @@ -404,8 +406,15 @@ billingRouter.post( const billing = getBillingService(); try { - // Get raw body - const rawBody = JSON.stringify(req.body); + // Use the preserved raw body from express.json verify callback + // This is critical for Stripe signature verification - JSON.stringify(req.body) won't work + const rawBody = (req as Request & { rawBody?: string }).rawBody; + + if (!rawBody) { + console.error('Raw body not available for Stripe webhook verification'); + res.status(400).json({ error: 'Raw body not available' }); + return; + } // Verify and parse event const event = billing.verifyWebhookSignature(rawBody, sig as string); @@ -423,18 +432,32 @@ billingRouter.post( // Handle specific events switch (billingEvent.type) { case 'subscription.created': - case 'subscription.updated': - // Update user's subscription in database - // This would integrate with your user/database layer - console.log('Subscription updated for user:', billingEvent.userId); + case 'subscription.updated': { + // Extract subscription tier and update user's plan + if (billingEvent.userId) { + const subscription = billingEvent.data as unknown as Stripe.Subscription; + const tier = billing.getTierFromSubscription(subscription); + + // Update user's plan in database + await db.users.update(billingEvent.userId, { plan: tier }); + console.log(`Updated user ${billingEvent.userId} plan to: ${tier}`); + } else { + console.warn('Subscription event received without userId:', billingEvent.id); + } break; - - case 'subscription.canceled': - console.log('Subscription canceled for user:', billingEvent.userId); + } + + case 'subscription.canceled': { + // Reset user to free plan + if (billingEvent.userId) { + await db.users.update(billingEvent.userId, { plan: 'free' }); + console.log(`User ${billingEvent.userId} subscription canceled, reset to free plan`); + } break; + } case 'invoice.payment_failed': - // Notify user of failed payment + // Log payment failure (don't immediately downgrade - Stripe retries) console.log('Payment failed for user:', billingEvent.userId); break; } diff --git a/src/cloud/api/workspaces.ts b/src/cloud/api/workspaces.ts index cb97522e5..ad177a6ec 100644 --- a/src/cloud/api/workspaces.ts +++ b/src/cloud/api/workspaces.ts @@ -9,6 +9,7 @@ import { requireAuth } from './auth.js'; import { db, Workspace } from '../db/index.js'; import { getProvisioner, getProvisioningStage } from '../provisioner/index.js'; import { checkWorkspaceLimit } from './middleware/planLimits.js'; +import { getConfig } from '../config.js'; export const workspacesRouter = Router(); @@ -301,8 +302,9 @@ workspacesRouter.get('/:id/status', async (req: Request, res: Response) => { const provisioner = getProvisioner(); const status = await provisioner.getStatus(id); - // Include provisioning progress info if status is 'provisioning' - const provisioningProgress = status === 'provisioning' ? getProvisioningStage(id) : null; + // Include provisioning progress info if it exists (even after status changes to 'running') + // This allows the frontend to see all stages including 'complete' + const provisioningProgress = getProvisioningStage(id); res.json({ status, @@ -441,6 +443,89 @@ workspacesRouter.post('/:id/repos', async (req: Request, res: Response) => { } }); +/** + * POST /api/workspaces/:id/autoscale + * Trigger auto-scaling based on current agent count + * Supports both user session auth and workspace token auth + * Called by workspace container when spawning new agents + */ +workspacesRouter.post('/:id/autoscale', async (req: Request, res: Response) => { + const { id } = req.params; + const { agentCount } = req.body; + + if (typeof agentCount !== 'number' || agentCount < 0) { + return res.status(400).json({ error: 'agentCount must be a non-negative number' }); + } + + try { + const workspace = await db.workspaces.findById(id); + + if (!workspace) { + return res.status(404).json({ error: 'Workspace not found' }); + } + + // Verify auth: either user session or workspace token + const userId = req.session?.userId; + const authHeader = req.get('authorization'); + + if (userId) { + // User session auth + if (workspace.userId !== userId) { + return res.status(403).json({ error: 'Unauthorized' }); + } + } else if (authHeader?.startsWith('Bearer ')) { + // Workspace token auth (for calls from within the workspace) + const crypto = await import('crypto'); + const config = getConfig(); + const providedToken = authHeader.slice(7); + const expectedToken = crypto.default + .createHmac('sha256', config.sessionSecret) + .update(`workspace:${id}`) + .digest('hex'); + + const isValid = crypto.default.timingSafeEqual( + Buffer.from(providedToken), + Buffer.from(expectedToken) + ); + + if (!isValid) { + return res.status(401).json({ error: 'Invalid workspace token' }); + } + } else { + return res.status(401).json({ error: 'Authentication required' }); + } + + const provisioner = getProvisioner(); + const currentTier = await provisioner.getCurrentTier(id); + const recommendedTier = provisioner.getRecommendedTier(agentCount); + + // Check if scaling is needed + if (recommendedTier.memoryMb <= currentTier.memoryMb) { + return res.json({ + scaled: false, + currentTier: currentTier.name, + message: 'Current tier is sufficient', + }); + } + + // Perform the scale-up (respects plan limits) + const result = await provisioner.autoScale(id, agentCount); + + res.json({ + scaled: result.scaled, + previousTier: result.currentTier || currentTier.name, + newTier: result.targetTier || currentTier.name, + reason: result.reason, + message: result.scaled + ? `Scaled up to ${result.targetTier} tier` + : result.reason || 'Scaling not required', + }); + } catch (error) { + console.error('Error auto-scaling workspace:', error); + res.status(500).json({ error: 'Failed to auto-scale workspace' }); + } +}); + /** * POST /api/workspaces/:id/domain * Add or update custom domain (Premium feature - Team/Enterprise only) diff --git a/src/cloud/billing/plans.ts b/src/cloud/billing/plans.ts index f603a4686..7625ae828 100644 --- a/src/cloud/billing/plans.ts +++ b/src/cloud/billing/plans.ts @@ -13,15 +13,15 @@ export const BILLING_PLANS: Record = { free: { id: 'free', name: 'Free', - description: 'For individuals exploring AI agent workflows', + description: 'Try AI agent workflows with limited access', priceMonthly: 0, priceYearly: 0, limits: { - maxWorkspaces: 2, - maxAgentsPerWorkspace: 3, + maxWorkspaces: 1, + maxAgentsPerWorkspace: 2, maxTeamMembers: 1, maxStorageGB: 1, - maxComputeHoursPerMonth: 10, + maxComputeHoursPerMonth: 5, // Limited to 5 hours/month customDomains: false, prioritySupport: false, sla: false, @@ -30,10 +30,10 @@ export const BILLING_PLANS: Record = { sessionPersistence: false, }, features: [ - 'Up to 2 workspaces', - 'Up to 3 agents per workspace', - '10 compute hours/month', - '1 GB storage', + '1 workspace', + 'Up to 2 agents', + '5 compute hours/month', + 'Shared CPU (may throttle)', 'Community support', ], }, @@ -41,17 +41,17 @@ export const BILLING_PLANS: Record = { pro: { id: 'pro', name: 'Pro', - description: 'For professional developers and small teams', - priceMonthly: 2900, // $29/month - priceYearly: 29000, // $290/year (2 months free) + description: 'For professional developers building with AI agents', + priceMonthly: 4900, // $49/month + priceYearly: 47000, // $470/year (2 months free) stripePriceIdMonthly: process.env.STRIPE_PRO_MONTHLY_PRICE_ID, stripePriceIdYearly: process.env.STRIPE_PRO_YEARLY_PRICE_ID, limits: { - maxWorkspaces: 10, - maxAgentsPerWorkspace: 10, - maxTeamMembers: 5, + maxWorkspaces: 5, + maxAgentsPerWorkspace: 5, + maxTeamMembers: 3, maxStorageGB: 10, - maxComputeHoursPerMonth: 100, + maxComputeHoursPerMonth: 50, customDomains: true, prioritySupport: false, sla: false, @@ -60,11 +60,11 @@ export const BILLING_PLANS: Record = { sessionPersistence: true, }, features: [ - 'Up to 10 workspaces', - 'Up to 10 agents per workspace', - '100 compute hours/month', + 'Up to 5 workspaces', + 'Up to 5 agents per workspace', + '50 compute hours/month', '10 GB storage', - '5 team members', + '3 team members', 'Custom domains', 'Session persistence', 'Email support', diff --git a/src/cloud/provisioner/index.ts b/src/cloud/provisioner/index.ts index cba371066..073099ff4 100644 --- a/src/cloud/provisioner/index.ts +++ b/src/cloud/provisioner/index.ts @@ -6,9 +6,15 @@ import * as crypto from 'crypto'; import { getConfig } from '../config.js'; -import { db, Workspace } from '../db/index.js'; +import { db, Workspace, PlanType } from '../db/index.js'; import { vault } from '../vault/index.js'; import { nangoService } from '../services/nango.js'; +import { + canAutoScale, + canScaleToTier, + getResourceTierForPlan, + type ResourceTierName, +} from '../services/planLimits.js'; const WORKSPACE_PORT = 3888; const FETCH_TIMEOUT_MS = 10_000; @@ -299,13 +305,16 @@ export interface ResourceTier { cpuCores: number; memoryMb: number; maxAgents: number; + cpuKind: 'shared' | 'performance'; } +// Resource tiers sized for Claude Code agents (~1-2GB RAM per agent) +// cpuKind: 'shared' = cheaper but can be throttled, 'performance' = dedicated export const RESOURCE_TIERS: Record = { - small: { name: 'small', cpuCores: 1, memoryMb: 512, maxAgents: 5 }, - medium: { name: 'medium', cpuCores: 2, memoryMb: 1024, maxAgents: 10 }, - large: { name: 'large', cpuCores: 4, memoryMb: 2048, maxAgents: 20 }, - xlarge: { name: 'xlarge', cpuCores: 8, memoryMb: 4096, maxAgents: 50 }, + small: { name: 'small', cpuCores: 2, memoryMb: 2048, maxAgents: 2, cpuKind: 'shared' }, + medium: { name: 'medium', cpuCores: 2, memoryMb: 4096, maxAgents: 5, cpuKind: 'shared' }, + large: { name: 'large', cpuCores: 4, memoryMb: 8192, maxAgents: 10, cpuKind: 'performance' }, + xlarge: { name: 'xlarge', cpuCores: 8, memoryMb: 16384, maxAgents: 20, cpuKind: 'performance' }, }; /** @@ -548,10 +557,18 @@ class FlyProvisioner implements ComputeProvisioner { ], protocol: 'tcp', internal_port: WORKSPACE_PORT, - // Auto-stop after 5 minutes of inactivity - auto_stop_machines: true, + // Auto-stop after inactivity to reduce costs + // Fly Proxy automatically wakes machines on incoming requests + auto_stop_machines: 'stop', // stop (not suspend) for faster wake auto_start_machines: true, min_machines_running: 0, + // Idle timeout before auto-stop (in seconds) + // Longer timeout = better UX, shorter = lower costs + concurrency: { + type: 'requests', + soft_limit: 25, + hard_limit: 50, + }, }, ], checks: { @@ -564,6 +581,8 @@ class FlyProvisioner implements ComputeProvisioner { grace_period: '10s', }, }, + // Start with small tier (shared CPUs) - scales up based on plan + // Free tier uses shared CPUs for cost efficiency guest: { cpu_kind: 'shared', cpus: 2, @@ -714,7 +733,8 @@ class FlyProvisioner implements ComputeProvisioner { body: JSON.stringify({ config: { guest: { - cpu_kind: tier.cpuCores <= 2 ? 'shared' : 'performance', + // Use tier-specific CPU type (shared for cost, performance for power) + cpu_kind: tier.cpuKind, cpus: tier.cpuCores, memory_mb: tier.memoryMb, }, @@ -1297,6 +1317,25 @@ export class WorkspaceProvisioner { // Auto-accept the creator's membership await db.workspaceMembers.acceptInvite(workspace.id, config.userId); + // Initialize stage tracking immediately + updateProvisioningStage(workspace.id, 'creating'); + + // Run provisioning in the background so frontend can poll for stages + this.runProvisioningAsync(workspace, config).catch((error) => { + console.error(`[provisioner] Background provisioning failed for ${workspace.id}:`, error); + }); + + // Return immediately with 'provisioning' status + return { + workspaceId: workspace.id, + status: 'provisioning', + }; + } + + /** + * Run the actual provisioning work asynchronously + */ + private async runProvisioningAsync(workspace: Workspace, config: ProvisionConfig): Promise { // Get credentials const credentials = new Map(); for (const provider of config.providers) { @@ -1336,11 +1375,13 @@ export class WorkspaceProvisioner { publicUrl, }); - return { - workspaceId: workspace.id, - status: 'running', - publicUrl, - }; + // Schedule cleanup of provisioning progress after 30s (gives frontend time to see 'complete') + setTimeout(() => { + clearProvisioningProgress(workspace.id); + console.log(`[provisioner] Cleaned up provisioning progress for ${workspace.id.substring(0, 8)}`); + }, 30_000); + + console.log(`[provisioner] Workspace ${workspace.id} provisioned successfully at ${publicUrl}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; @@ -1348,11 +1389,10 @@ export class WorkspaceProvisioner { errorMessage, }); - return { - workspaceId: workspace.id, - status: 'error', - error: errorMessage, - }; + // Clear provisioning progress on error + clearProvisioningProgress(workspace.id); + + console.error(`[provisioner] Workspace ${workspace.id} provisioning failed:`, errorMessage); } } @@ -1474,6 +1514,95 @@ export class WorkspaceProvisioner { const tierName = workspace.config.resourceTier || 'small'; return RESOURCE_TIERS[tierName] || RESOURCE_TIERS.small; } + + /** + * Get recommended tier based on agent count + * Uses 1.5-2GB per agent as baseline for Claude Code + */ + getRecommendedTier(agentCount: number): ResourceTier { + // Find the smallest tier that supports this agent count + const tiers = Object.values(RESOURCE_TIERS).sort((a, b) => a.maxAgents - b.maxAgents); + for (const tier of tiers) { + if (tier.maxAgents >= agentCount) { + return tier; + } + } + // If agent count exceeds all tiers, return the largest + return RESOURCE_TIERS.xlarge; + } + + /** + * Auto-scale workspace based on current agent count + * Respects plan limits - free tier cannot scale, others have max tier limits + * Returns { scaled: boolean, reason?: string } + */ + async autoScale(workspaceId: string, currentAgentCount: number): Promise<{ + scaled: boolean; + reason?: string; + currentTier?: string; + targetTier?: string; + }> { + const workspace = await db.workspaces.findById(workspaceId); + if (!workspace) { + throw new Error('Workspace not found'); + } + + // Get user's plan + const user = await db.users.findById(workspace.userId); + const plan = (user?.plan as PlanType) || 'free'; + + // Check if plan allows auto-scaling + if (!canAutoScale(plan)) { + return { + scaled: false, + reason: 'Auto-scaling requires Pro plan or higher', + }; + } + + const currentTier = await this.getCurrentTier(workspaceId); + const recommendedTier = this.getRecommendedTier(currentAgentCount); + + // Only scale UP, never down (to avoid disruption) + if (recommendedTier.memoryMb <= currentTier.memoryMb) { + return { + scaled: false, + currentTier: currentTier.name, + }; + } + + // Check if plan allows scaling to the recommended tier + if (!canScaleToTier(plan, recommendedTier.name as ResourceTierName)) { + // Find the max tier allowed for this plan + const maxTierName = getResourceTierForPlan(plan); + const maxTier = RESOURCE_TIERS[maxTierName]; + + if (maxTier.memoryMb <= currentTier.memoryMb) { + return { + scaled: false, + reason: `Already at max tier (${currentTier.name}) for ${plan} plan`, + currentTier: currentTier.name, + }; + } + + // Scale to max allowed tier instead + console.log(`[provisioner] Auto-scaling workspace ${workspaceId.substring(0, 8)} from ${currentTier.name} to ${maxTierName} (max for ${plan} plan)`); + await this.resize(workspaceId, maxTier); + return { + scaled: true, + currentTier: currentTier.name, + targetTier: maxTierName, + reason: `Scaled to max tier for ${plan} plan`, + }; + } + + console.log(`[provisioner] Auto-scaling workspace ${workspaceId.substring(0, 8)} from ${currentTier.name} to ${recommendedTier.name} (${currentAgentCount} agents)`); + await this.resize(workspaceId, recommendedTier); + return { + scaled: true, + currentTier: currentTier.name, + targetTier: recommendedTier.name, + }; + } } // Singleton instance diff --git a/src/cloud/services/planLimits.ts b/src/cloud/services/planLimits.ts index e0c3e1237..f305dbd02 100644 --- a/src/cloud/services/planLimits.ts +++ b/src/cloud/services/planLimits.ts @@ -33,17 +33,17 @@ export interface PlanLimits { export const PLAN_LIMITS: Record = { free: { maxWorkspaces: 1, - maxRepos: 3, + maxRepos: 2, maxConcurrentAgents: 2, - maxComputeHoursPerMonth: 10, + maxComputeHoursPerMonth: 5, // Limited free tier coordinatorsEnabled: false, sessionPersistence: false, }, pro: { maxWorkspaces: 5, - maxRepos: 20, - maxConcurrentAgents: 10, - maxComputeHoursPerMonth: 100, + maxRepos: 10, + maxConcurrentAgents: 5, + maxComputeHoursPerMonth: 50, coordinatorsEnabled: true, sessionPersistence: true, }, @@ -403,3 +403,67 @@ export async function updateActiveAgentCount( recordedAt: new Date(), }); } + +/** + * Resource tier name type + */ +export type ResourceTierName = 'small' | 'medium' | 'large' | 'xlarge'; + +/** + * Get the default resource tier for a plan + * Maps plans to appropriate compute resources + */ +export function getResourceTierForPlan(plan: PlanType): ResourceTierName { + switch (plan) { + case 'free': + return 'small'; // 2GB, 2 CPUs - suitable for 2 agents + case 'pro': + return 'medium'; // 4GB, 4 CPUs - suitable for 5 agents + case 'team': + return 'large'; // 8GB, 4 CPUs - suitable for 10 agents + case 'enterprise': + return 'xlarge'; // 16GB, 8 CPUs - suitable for 20 agents + default: + return 'small'; + } +} + +/** + * Get the maximum resource tier a plan can scale to + * Prevents over-scaling beyond plan entitlements + */ +export function getMaxResourceTierForPlan(plan: PlanType): ResourceTierName { + switch (plan) { + case 'free': + return 'small'; // Free tier cannot scale up + case 'pro': + return 'medium'; // Pro can scale to medium + case 'team': + return 'large'; // Team can scale to large + case 'enterprise': + return 'xlarge'; // Enterprise can use any tier + default: + return 'small'; + } +} + +/** + * Check if user's plan allows auto-scaling + */ +export function canAutoScale(plan: PlanType): boolean { + // Only Pro and above can auto-scale + return plan !== 'free'; +} + +/** + * Check if auto-scale to a specific tier is allowed for a plan + */ +export function canScaleToTier(plan: PlanType, targetTier: ResourceTierName): boolean { + const tierOrder: ResourceTierName[] = ['small', 'medium', 'large', 'xlarge']; + const maxTier = getMaxResourceTierForPlan(plan); + + const targetIndex = tierOrder.indexOf(targetTier); + const maxIndex = tierOrder.indexOf(maxTier); + + return targetIndex <= maxIndex; +} diff --git a/src/dashboard/app/app/onboarding/page.tsx b/src/dashboard/app/app/onboarding/page.tsx new file mode 100644 index 000000000..255dfe4d2 --- /dev/null +++ b/src/dashboard/app/app/onboarding/page.tsx @@ -0,0 +1,394 @@ +/** + * Onboarding Page - Dedicated route for new users and post-deletion flow + * + * This page provides a cleaner onboarding experience separate from workspace selection. + * It handles two scenarios: + * 1. First-time users with no workspaces + * 2. Users who just deleted their workspace + * + * URL params: + * - reason=deleted: User just deleted a workspace + * - reason=new: First-time user (default) + */ + +'use client'; + +import React, { useState, useEffect, useCallback, Suspense } from 'react'; +import { useSearchParams } from 'next/navigation'; +import { LogoIcon } from '../../../react-components/Logo'; + +interface Repository { + id: string; + fullName: string; + isPrivate: boolean; + defaultBranch: string; + syncStatus: string; + hasNangoConnection: boolean; +} + +type OnboardingReason = 'new' | 'deleted'; + +// Analytics event types for onboarding funnel +type OnboardingEvent = + | 'onboarding_page_view' + | 'onboarding_repo_selected' + | 'onboarding_workspace_created' + | 'onboarding_connect_repos_clicked' + | 'onboarding_skipped'; + +// Simple analytics hook - can be extended to integrate with actual analytics service +function useOnboardingAnalytics() { + const trackEvent = useCallback((event: OnboardingEvent, properties?: Record) => { + // Log to console in development + if (process.env.NODE_ENV === 'development') { + console.log('[Onboarding Analytics]', event, properties); + } + + // TODO: Integrate with actual analytics service (e.g., Posthog, Mixpanel, etc.) + // Example: posthog.capture(event, properties); + + // For now, send to a hypothetical analytics endpoint + try { + fetch('/api/analytics/track', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ event, properties, timestamp: Date.now() }), + }).catch(() => { + // Silently fail - analytics should not block user experience + }); + } catch { + // Silently fail + } + }, []); + + return { trackEvent }; +} + +function OnboardingContent() { + const searchParams = useSearchParams(); + const reason = (searchParams.get('reason') as OnboardingReason) || 'new'; + + const [repos, setRepos] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isCreating, setIsCreating] = useState(false); + const [error, setError] = useState(null); + const [csrfToken, setCsrfToken] = useState(null); + + const { trackEvent } = useOnboardingAnalytics(); + + // Fetch repositories and check session + useEffect(() => { + const init = async () => { + try { + // Check session + const sessionRes = await fetch('/api/auth/session', { credentials: 'include' }); + + if (sessionRes.status === 404) { + // Local mode - redirect to main app + window.location.href = '/app'; + return; + } + + // Capture CSRF token + const token = sessionRes.headers.get('X-CSRF-Token'); + if (token) { + setCsrfToken(token); + } + + const session = await sessionRes.json(); + + if (!session.authenticated) { + window.location.href = '/login'; + return; + } + + // Check if user already has workspaces - if so, redirect to /app + const workspacesRes = await fetch('/api/workspaces', { credentials: 'include' }); + if (workspacesRes.ok) { + const workspacesData = await workspacesRes.json(); + if ((workspacesData.workspaces || []).length > 0) { + // User has workspaces, redirect to main app + window.location.href = '/app'; + return; + } + } + + // Fetch repos + const reposRes = await fetch('/api/github-app/repos', { credentials: 'include' }); + if (reposRes.ok) { + const reposData = await reposRes.json(); + setRepos(reposData.repositories || []); + } + + // Track page view + trackEvent('onboarding_page_view', { reason }); + + setIsLoading(false); + } catch (err) { + console.error('Onboarding init error:', err); + setError(err instanceof Error ? err.message : 'Failed to initialize'); + setIsLoading(false); + } + }; + + init(); + }, [reason, trackEvent]); + + const handleCreateWorkspace = useCallback(async (repoFullName: string) => { + setIsCreating(true); + setError(null); + + trackEvent('onboarding_repo_selected', { repository: repoFullName }); + + try { + const headers: Record = { 'Content-Type': 'application/json' }; + if (csrfToken) { + headers['X-CSRF-Token'] = csrfToken; + } + + const res = await fetch('/api/workspaces/quick', { + method: 'POST', + credentials: 'include', + headers, + body: JSON.stringify({ repositoryFullName: repoFullName }), + }); + + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.error || 'Failed to create workspace'); + } + + trackEvent('onboarding_workspace_created', { + workspaceId: data.workspaceId, + repository: repoFullName, + }); + + // Redirect to main app - it will handle provisioning state + window.location.href = '/app'; + } catch (err) { + console.error('Create workspace error:', err); + setError(err instanceof Error ? err.message : 'Failed to create workspace'); + setIsCreating(false); + } + }, [csrfToken, trackEvent]); + + const handleConnectRepos = useCallback(() => { + trackEvent('onboarding_connect_repos_clicked', { reason }); + window.location.href = '/connect-repos'; + }, [reason, trackEvent]); + + // Loading state + if (isLoading) { + return ( +
+
+ + + + +

Loading...

+
+
+ ); + } + + // Creating workspace state + if (isCreating) { + return ( +
+
+ + + + +

Creating your workspace...

+

This may take a few minutes

+
+
+ ); + } + + // Determine content based on reason + const isDeletedWorkspace = reason === 'deleted'; + + return ( +
+ {/* Background grid */} +
+
+
+ +
+ {/* Logo and Header */} +
+ +

+ {isDeletedWorkspace ? 'Workspace Deleted' : 'Welcome to Agent Relay'} +

+

+ {isDeletedWorkspace + ? 'Your workspace has been deleted. Create a new one to continue working with AI agents.' + : 'Get started by creating your first workspace. Connect a repository and let AI agents help you build.'} +

+
+ + {/* Error message */} + {error && ( +
+

{error}

+
+ )} + + {/* Main content card */} +
+ {/* Step indicator for first-time users */} + {!isDeletedWorkspace && ( +
+
+
+ 1 +
+ Select Repository +
+
+
+
+ 2 +
+ Connect AI Provider +
+
+
+
+ 3 +
+ Start Building +
+
+ )} + +

+ {isDeletedWorkspace ? 'Create a New Workspace' : 'Choose a Repository'} +

+

+ {isDeletedWorkspace + ? 'Select a repository to create a new workspace for your AI agents.' + : 'Your workspace will be set up with this repository. You can add more repos later.'} +

+ + {repos.length > 0 ? ( +
+ {repos.map((repo) => ( + + ))} +
+ ) : ( +
+
+ + + +
+

No Repositories Connected

+

+ Connect your GitHub repositories to create a workspace and start working with AI agents. +

+ +
+ )} +
+ + {/* Footer navigation */} +
+ {repos.length > 0 && ( + + )} + + Back to Dashboard + + +
+
+
+ ); +} + +// Wrap in Suspense for useSearchParams +export default function OnboardingPage() { + return ( + +
+ + + + +

Loading...

+
+
+ } + > + + + ); +} diff --git a/src/dashboard/landing/LandingPage.tsx b/src/dashboard/landing/LandingPage.tsx index bee8ffc34..7b0e1f708 100644 --- a/src/dashboard/landing/LandingPage.tsx +++ b/src/dashboard/landing/LandingPage.tsx @@ -479,17 +479,17 @@ function PricingSection() { name: 'Free', price: '$0', period: 'forever', - description: 'Try it out on a side project', - features: ['1 workspace', '3 repositories', '2 concurrent agents', '10 compute hours/month', 'Community support'], + description: 'Try AI agent workflows', + features: ['1 workspace', '2 repositories', '2 concurrent agents', '5 compute hours/month', 'Shared CPU', 'Community support'], cta: 'Get Started', highlighted: false, }, { name: 'Pro', - price: '$29', + price: '$49', period: '/month', description: 'For professional developers', - features: ['5 workspaces', '20 repositories', '10 concurrent agents', '100 compute hours/month', 'Coordinator agents', 'Email support'], + features: ['5 workspaces', '10 repositories', '5 concurrent agents', '50 compute hours/month', 'Auto-scaling', 'Session persistence', 'Email support'], cta: 'Start Free Trial', highlighted: true, }, @@ -498,14 +498,14 @@ function PricingSection() { price: '$99', period: '/month', description: 'For growing teams', - features: ['20 workspaces', '100 repositories', '50 concurrent agents', '500 compute hours/month', 'Coordinator agents', 'Priority support', 'Audit logs'], + features: ['20 workspaces', '100 repositories', '50 concurrent agents', '500 compute hours/month', 'Dedicated CPU', 'Priority support', 'Audit logs'], cta: 'Start Free Trial', highlighted: false, }, { name: 'Enterprise', - price: 'Custom', - period: '', + price: '$499', + period: '/month', description: 'For organizations at scale', features: ['Unlimited workspaces', 'Unlimited repositories', 'Unlimited agents', 'Unlimited compute', 'SSO/SAML', 'SLA guarantee', 'Dedicated support'], cta: 'Contact Sales', diff --git a/src/dashboard/landing/PricingPage.tsx b/src/dashboard/landing/PricingPage.tsx index aba961983..c4d92fbc8 100644 --- a/src/dashboard/landing/PricingPage.tsx +++ b/src/dashboard/landing/PricingPage.tsx @@ -18,25 +18,25 @@ const PLANS = [ name: 'Free', price: 0, period: 'forever', - tagline: 'Try it out on a side project', - description: 'Perfect for exploring AI agent orchestration on personal projects.', + tagline: 'Try AI agent workflows', + description: 'Explore agent orchestration with limited resources. Perfect for testing.', limits: { workspaces: 1, - repos: 3, + repos: 2, concurrentAgents: 2, - computeHours: 10, + computeHours: 5, }, features: [ { name: 'Workspaces', value: '1', included: true }, - { name: 'Repositories', value: '3', included: true }, + { name: 'Repositories', value: '2', included: true }, { name: 'Concurrent agents', value: '2', included: true }, - { name: 'Compute hours/month', value: '10', included: true }, + { name: 'Compute hours/month', value: '5', included: true }, + { name: 'CPU type', value: 'Shared', included: true }, { name: 'Coordinator agents', value: null, included: false }, - { name: 'Team collaboration', value: null, included: false }, + { name: 'Auto-scaling', value: null, included: false }, + { name: 'Session persistence', value: null, included: false }, { name: 'Priority support', value: null, included: false }, - { name: 'Audit logs', value: null, included: false }, { name: 'SSO/SAML', value: null, included: false }, - { name: 'SLA guarantee', value: null, included: false }, ], cta: 'Get Started Free', ctaLink: '/signup', @@ -45,27 +45,27 @@ const PLANS = [ { id: 'pro', name: 'Pro', - price: 29, + price: 49, period: '/month', tagline: 'For professional developers', - description: 'Unlock coordinator agents and scale your AI workforce.', + description: 'Build with AI agents daily. Includes auto-scaling and session persistence.', limits: { workspaces: 5, - repos: 20, - concurrentAgents: 10, - computeHours: 100, + repos: 10, + concurrentAgents: 5, + computeHours: 50, }, features: [ { name: 'Workspaces', value: '5', included: true }, - { name: 'Repositories', value: '20', included: true }, - { name: 'Concurrent agents', value: '10', included: true }, - { name: 'Compute hours/month', value: '100', included: true }, + { name: 'Repositories', value: '10', included: true }, + { name: 'Concurrent agents', value: '5', included: true }, + { name: 'Compute hours/month', value: '50', included: true }, + { name: 'CPU type', value: 'Shared', included: true }, { name: 'Coordinator agents', value: 'Yes', included: true }, - { name: 'Team collaboration', value: null, included: false }, + { name: 'Auto-scaling', value: 'Yes', included: true }, + { name: 'Session persistence', value: 'Yes', included: true }, { name: 'Priority support', value: null, included: false }, - { name: 'Audit logs', value: null, included: false }, { name: 'SSO/SAML', value: null, included: false }, - { name: 'SLA guarantee', value: null, included: false }, ], cta: 'Start Free Trial', ctaLink: '/signup?plan=pro', @@ -78,24 +78,24 @@ const PLANS = [ price: 99, period: '/month', tagline: 'For growing teams', - description: 'Collaborate with your team and ship faster together.', + description: 'Dedicated CPUs, priority support, and team collaboration features.', limits: { workspaces: 20, repos: 100, - concurrentAgents: 50, - computeHours: 500, + concurrentAgents: 10, + computeHours: 200, }, features: [ { name: 'Workspaces', value: '20', included: true }, { name: 'Repositories', value: '100', included: true }, - { name: 'Concurrent agents', value: '50', included: true }, - { name: 'Compute hours/month', value: '500', included: true }, + { name: 'Concurrent agents', value: '10', included: true }, + { name: 'Compute hours/month', value: '200', included: true }, + { name: 'CPU type', value: 'Dedicated', included: true }, { name: 'Coordinator agents', value: 'Yes', included: true }, - { name: 'Team collaboration', value: 'Yes', included: true }, + { name: 'Auto-scaling', value: 'Yes', included: true }, + { name: 'Session persistence', value: 'Yes', included: true }, { name: 'Priority support', value: 'Yes', included: true }, - { name: 'Audit logs', value: 'Yes', included: true }, { name: 'SSO/SAML', value: null, included: false }, - { name: 'SLA guarantee', value: null, included: false }, ], cta: 'Start Free Trial', ctaLink: '/signup?plan=team', @@ -104,10 +104,10 @@ const PLANS = [ { id: 'enterprise', name: 'Enterprise', - price: null, - period: '', + price: 499, + period: '/month', tagline: 'For organizations at scale', - description: 'Custom solutions with dedicated support and SLA guarantees.', + description: 'Unlimited resources, SSO, SLA guarantees, and dedicated support.', limits: { workspaces: Infinity, repos: Infinity, @@ -119,12 +119,12 @@ const PLANS = [ { name: 'Repositories', value: 'Unlimited', included: true }, { name: 'Concurrent agents', value: 'Unlimited', included: true }, { name: 'Compute hours/month', value: 'Unlimited', included: true }, + { name: 'CPU type', value: 'Dedicated', included: true }, { name: 'Coordinator agents', value: 'Yes', included: true }, - { name: 'Team collaboration', value: 'Yes', included: true }, + { name: 'Auto-scaling', value: 'Yes', included: true }, + { name: 'Session persistence', value: 'Yes', included: true }, { name: 'Priority support', value: 'Dedicated', included: true }, - { name: 'Audit logs', value: 'Yes', included: true }, { name: 'SSO/SAML', value: 'Yes', included: true }, - { name: 'SLA guarantee', value: '99.9%', included: true }, ], cta: 'Contact Sales', ctaLink: '/contact?subject=enterprise', diff --git a/src/dashboard/react-components/App.tsx b/src/dashboard/react-components/App.tsx index 5f890a045..e444409bd 100644 --- a/src/dashboard/react-components/App.tsx +++ b/src/dashboard/react-components/App.tsx @@ -30,6 +30,7 @@ import { TypingIndicator } from './TypingIndicator'; import { OnlineUsersIndicator } from './OnlineUsersIndicator'; import { UserProfilePanel } from './UserProfilePanel'; import { CoordinatorPanel } from './CoordinatorPanel'; +import { BillingResult } from './BillingResult'; import { useWebSocket } from './hooks/useWebSocket'; import { useAgents } from './hooks/useAgents'; import { useMessages } from './hooks/useMessages'; @@ -844,6 +845,33 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) { return () => window.removeEventListener('keydown', handleKeyDown); }, [handleSpawnClick, handleNewConversationClick]); + // Handle billing result routes (success/cancel after Stripe checkout) + const pathname = typeof window !== 'undefined' ? window.location.pathname : ''; + const searchParams = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : new URLSearchParams(); + + if (pathname === '/billing/success') { + return ( + { + window.location.href = '/'; + }} + /> + ); + } + + if (pathname === '/billing/canceled') { + return ( + { + window.location.href = '/'; + }} + /> + ); + } + return (
@@ -899,6 +927,12 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) { onThreadSelect={setCurrentThread} onClose={() => setIsSidebarOpen(false)} onSettingsClick={handleSettingsClick} + onTrajectoryClick={() => setIsTrajectoryOpen(true)} + hasActiveTrajectory={trajectoryStatus?.active} + onFleetClick={() => setIsFleetViewActive(!isFleetViewActive)} + isFleetViewActive={isFleetViewActive} + onCoordinatorClick={handleCoordinatorClick} + hasMultipleProjects={mergedProjects.length > 1} />
diff --git a/src/dashboard/react-components/BillingResult.tsx b/src/dashboard/react-components/BillingResult.tsx new file mode 100644 index 000000000..d89c6147c --- /dev/null +++ b/src/dashboard/react-components/BillingResult.tsx @@ -0,0 +1,440 @@ +/** + * Billing Result Component + * + * Premium success/cancel pages after Stripe checkout. + * Features celebratory animations, confetti, and polished micro-interactions. + */ + +import React, { useEffect, useState, useMemo } from 'react'; +import { cloudApi } from '../lib/cloudApi'; + +export interface BillingResultProps { + type: 'success' | 'canceled'; + sessionId?: string; + onClose: () => void; +} + +interface SubscriptionInfo { + plan: string; + status: string; +} + +// Confetti particle component +function Confetti() { + const particles = useMemo(() => { + return Array.from({ length: 50 }, (_, i) => ({ + id: i, + x: Math.random() * 100, + delay: Math.random() * 0.5, + duration: 2 + Math.random() * 2, + size: 4 + Math.random() * 8, + color: ['#00d9ff', '#00b8d9', '#4ade80', '#facc15', '#f472b6', '#a78bfa'][Math.floor(Math.random() * 6)], + rotation: Math.random() * 360, + })); + }, []); + + return ( +
+ {particles.map((p) => ( +
+ ))} + +
+ ); +} + +// Animated checkmark with draw effect +function AnimatedCheckmark() { + return ( +
+ {/* Glow ring */} +
+ + {/* Main circle with gradient border */} +
+
+ + + + + + + + + +
+
+ + {/* Sparkle particles */} +
+
+
+ + +
+ ); +} + +// Premium loading spinner +function LoadingSpinner() { + return ( +
+ {/* Outer rotating ring */} +
+ + {/* Inner pulsing core */} +
+ + {/* Center dot */} +
+
+
+ + {/* Orbiting particles */} +
+
+
+
+
+
+
+ ); +} + +// Benefit item with staggered animation +function BenefitItem({ children, delay }: { children: React.ReactNode; delay: number }) { + return ( +
  • +
    + + + +
    + {children} + +
  • + ); +} + +export function BillingResult({ type, sessionId, onClose }: BillingResultProps) { + const [subscription, setSubscription] = useState(null); + const [isLoading, setIsLoading] = useState(type === 'success'); + const [error, setError] = useState(null); + const [showConfetti, setShowConfetti] = useState(false); + + useEffect(() => { + if (type !== 'success') return; + + const fetchSubscription = async () => { + try { + await new Promise(resolve => setTimeout(resolve, 1500)); + + const result = await cloudApi.getSubscription(); + if (result.success && result.data) { + setSubscription({ + plan: result.data.subscription?.tier || 'pro', + status: result.data.subscription?.status || 'active', + }); + // Trigger confetti after successful load + setTimeout(() => setShowConfetti(true), 200); + } + } catch (err) { + setError('Could not verify subscription. Please check your billing settings.'); + } finally { + setIsLoading(false); + } + }; + + fetchSubscription(); + }, [type, sessionId]); + + const planName = subscription?.plan === 'pro' ? 'Pro' : subscription?.plan === 'team' ? 'Team' : subscription?.plan === 'enterprise' ? 'Enterprise' : 'Premium'; + + if (type === 'success') { + return ( +
    + {/* Background gradient orbs */} +
    +
    +
    +
    + + {showConfetti && } + +
    + {/* Decorative top gradient line */} +
    + + {isLoading ? ( +
    +
    + +
    +

    + Processing your upgrade +

    +

    + Confirming your subscription... +

    +
    + ) : error ? ( +
    +
    + + + + + +
    +

    + Almost there! +

    +

    {error}

    + +
    + ) : ( + <> +
    + +
    + +

    + Welcome to {planName}! +

    + +

    + Your subscription is active. Time to supercharge your workflow. +

    + +
    +

    Your new powers

    +
      + Expanded workspace & repository limits + More compute hours for your agents + Increased concurrent agent capacity + Priority support & faster responses +
    +
    + + + + )} +
    + + +
    + ); + } + + // Canceled flow + return ( +
    + {/* Subtle background */} +
    +
    +
    + +
    +
    + + + +
    + +

    + No problem! +

    +

    + Checkout was canceled and you haven't been charged. + You can upgrade anytime when you're ready. +

    + +
    + + +
    +
    + + +
    + ); +} diff --git a/src/dashboard/react-components/WorkspaceStatusIndicator.tsx b/src/dashboard/react-components/WorkspaceStatusIndicator.tsx index c40974427..aeaf06321 100644 --- a/src/dashboard/react-components/WorkspaceStatusIndicator.tsx +++ b/src/dashboard/react-components/WorkspaceStatusIndicator.tsx @@ -158,7 +158,10 @@ export function WorkspaceStatusIndicator({ title={statusMessage || config.label} > {config.icon} - + {config.label} {config.pulseColor && ( diff --git a/src/dashboard/react-components/layout/Header.tsx b/src/dashboard/react-components/layout/Header.tsx index 0fde29fdc..9b8e4e467 100644 --- a/src/dashboard/react-components/layout/Header.tsx +++ b/src/dashboard/react-components/layout/Header.tsx @@ -134,11 +134,14 @@ export function Header({
    {selectedAgent.status && ( - + {selectedAgent.status} )} diff --git a/src/dashboard/react-components/layout/Sidebar.tsx b/src/dashboard/react-components/layout/Sidebar.tsx index 31f062bf3..4173668e0 100644 --- a/src/dashboard/react-components/layout/Sidebar.tsx +++ b/src/dashboard/react-components/layout/Sidebar.tsx @@ -47,6 +47,18 @@ export interface SidebarProps { onClose?: () => void; /** Handler for opening settings */ onSettingsClick?: () => void; + /** Mobile nav: Trajectory viewer toggle */ + onTrajectoryClick?: () => void; + /** Mobile nav: Whether there's an active trajectory */ + hasActiveTrajectory?: boolean; + /** Mobile nav: Fleet view toggle */ + onFleetClick?: () => void; + /** Mobile nav: Whether fleet view is active */ + isFleetViewActive?: boolean; + /** Mobile nav: Coordinator toggle */ + onCoordinatorClick?: () => void; + /** Mobile nav: Whether multiple projects are connected (shows coordinator) */ + hasMultipleProjects?: boolean; } export function Sidebar({ @@ -71,6 +83,12 @@ export function Sidebar({ onThreadSelect, onClose, onSettingsClick, + onTrajectoryClick, + hasActiveTrajectory, + onFleetClick, + isFleetViewActive, + onCoordinatorClick, + hasMultipleProjects, }: SidebarProps) { const [searchQuery, setSearchQuery] = useState(''); const [activeTab, setActiveTab] = useState(() => { @@ -307,6 +325,68 @@ export function Sidebar({ )}
    + {/* Mobile Navigation - shows items hidden in header on mobile */} +
    +

    Quick Actions

    +
    + {onFleetClick && ( + + )} + {onTrajectoryClick && ( + + )} + {hasMultipleProjects && onCoordinatorClick && ( + + )} + onClose?.()} + > + + Metrics + +
    +
    + {/* Footer Actions */}
    + ))} +
    + ) : ( +
    +
    + + + +
    +

    No Repositories Connected

    +

    + Connect your GitHub repositories to create a workspace and start working with AI agents. +

    + +
    + )} +
    + + {/* Footer navigation */} +
    + {repos.length > 0 && ( + + )} + + Back to Dashboard + + +
    +
    +
    + ); +} + +// Wrap in Suspense for useSearchParams +export default function OnboardingPage() { + return ( + +
    + + + + +

    Loading...

    +
    +
    + } + > + + + ); +} diff --git a/src/dashboard/landing/LandingPage.tsx b/src/dashboard/landing/LandingPage.tsx index bee8ffc34..7b0e1f708 100644 --- a/src/dashboard/landing/LandingPage.tsx +++ b/src/dashboard/landing/LandingPage.tsx @@ -479,17 +479,17 @@ function PricingSection() { name: 'Free', price: '$0', period: 'forever', - description: 'Try it out on a side project', - features: ['1 workspace', '3 repositories', '2 concurrent agents', '10 compute hours/month', 'Community support'], + description: 'Try AI agent workflows', + features: ['1 workspace', '2 repositories', '2 concurrent agents', '5 compute hours/month', 'Shared CPU', 'Community support'], cta: 'Get Started', highlighted: false, }, { name: 'Pro', - price: '$29', + price: '$49', period: '/month', description: 'For professional developers', - features: ['5 workspaces', '20 repositories', '10 concurrent agents', '100 compute hours/month', 'Coordinator agents', 'Email support'], + features: ['5 workspaces', '10 repositories', '5 concurrent agents', '50 compute hours/month', 'Auto-scaling', 'Session persistence', 'Email support'], cta: 'Start Free Trial', highlighted: true, }, @@ -498,14 +498,14 @@ function PricingSection() { price: '$99', period: '/month', description: 'For growing teams', - features: ['20 workspaces', '100 repositories', '50 concurrent agents', '500 compute hours/month', 'Coordinator agents', 'Priority support', 'Audit logs'], + features: ['20 workspaces', '100 repositories', '50 concurrent agents', '500 compute hours/month', 'Dedicated CPU', 'Priority support', 'Audit logs'], cta: 'Start Free Trial', highlighted: false, }, { name: 'Enterprise', - price: 'Custom', - period: '', + price: '$499', + period: '/month', description: 'For organizations at scale', features: ['Unlimited workspaces', 'Unlimited repositories', 'Unlimited agents', 'Unlimited compute', 'SSO/SAML', 'SLA guarantee', 'Dedicated support'], cta: 'Contact Sales', diff --git a/src/dashboard/landing/PricingPage.tsx b/src/dashboard/landing/PricingPage.tsx index aba961983..c4d92fbc8 100644 --- a/src/dashboard/landing/PricingPage.tsx +++ b/src/dashboard/landing/PricingPage.tsx @@ -18,25 +18,25 @@ const PLANS = [ name: 'Free', price: 0, period: 'forever', - tagline: 'Try it out on a side project', - description: 'Perfect for exploring AI agent orchestration on personal projects.', + tagline: 'Try AI agent workflows', + description: 'Explore agent orchestration with limited resources. Perfect for testing.', limits: { workspaces: 1, - repos: 3, + repos: 2, concurrentAgents: 2, - computeHours: 10, + computeHours: 5, }, features: [ { name: 'Workspaces', value: '1', included: true }, - { name: 'Repositories', value: '3', included: true }, + { name: 'Repositories', value: '2', included: true }, { name: 'Concurrent agents', value: '2', included: true }, - { name: 'Compute hours/month', value: '10', included: true }, + { name: 'Compute hours/month', value: '5', included: true }, + { name: 'CPU type', value: 'Shared', included: true }, { name: 'Coordinator agents', value: null, included: false }, - { name: 'Team collaboration', value: null, included: false }, + { name: 'Auto-scaling', value: null, included: false }, + { name: 'Session persistence', value: null, included: false }, { name: 'Priority support', value: null, included: false }, - { name: 'Audit logs', value: null, included: false }, { name: 'SSO/SAML', value: null, included: false }, - { name: 'SLA guarantee', value: null, included: false }, ], cta: 'Get Started Free', ctaLink: '/signup', @@ -45,27 +45,27 @@ const PLANS = [ { id: 'pro', name: 'Pro', - price: 29, + price: 49, period: '/month', tagline: 'For professional developers', - description: 'Unlock coordinator agents and scale your AI workforce.', + description: 'Build with AI agents daily. Includes auto-scaling and session persistence.', limits: { workspaces: 5, - repos: 20, - concurrentAgents: 10, - computeHours: 100, + repos: 10, + concurrentAgents: 5, + computeHours: 50, }, features: [ { name: 'Workspaces', value: '5', included: true }, - { name: 'Repositories', value: '20', included: true }, - { name: 'Concurrent agents', value: '10', included: true }, - { name: 'Compute hours/month', value: '100', included: true }, + { name: 'Repositories', value: '10', included: true }, + { name: 'Concurrent agents', value: '5', included: true }, + { name: 'Compute hours/month', value: '50', included: true }, + { name: 'CPU type', value: 'Shared', included: true }, { name: 'Coordinator agents', value: 'Yes', included: true }, - { name: 'Team collaboration', value: null, included: false }, + { name: 'Auto-scaling', value: 'Yes', included: true }, + { name: 'Session persistence', value: 'Yes', included: true }, { name: 'Priority support', value: null, included: false }, - { name: 'Audit logs', value: null, included: false }, { name: 'SSO/SAML', value: null, included: false }, - { name: 'SLA guarantee', value: null, included: false }, ], cta: 'Start Free Trial', ctaLink: '/signup?plan=pro', @@ -78,24 +78,24 @@ const PLANS = [ price: 99, period: '/month', tagline: 'For growing teams', - description: 'Collaborate with your team and ship faster together.', + description: 'Dedicated CPUs, priority support, and team collaboration features.', limits: { workspaces: 20, repos: 100, - concurrentAgents: 50, - computeHours: 500, + concurrentAgents: 10, + computeHours: 200, }, features: [ { name: 'Workspaces', value: '20', included: true }, { name: 'Repositories', value: '100', included: true }, - { name: 'Concurrent agents', value: '50', included: true }, - { name: 'Compute hours/month', value: '500', included: true }, + { name: 'Concurrent agents', value: '10', included: true }, + { name: 'Compute hours/month', value: '200', included: true }, + { name: 'CPU type', value: 'Dedicated', included: true }, { name: 'Coordinator agents', value: 'Yes', included: true }, - { name: 'Team collaboration', value: 'Yes', included: true }, + { name: 'Auto-scaling', value: 'Yes', included: true }, + { name: 'Session persistence', value: 'Yes', included: true }, { name: 'Priority support', value: 'Yes', included: true }, - { name: 'Audit logs', value: 'Yes', included: true }, { name: 'SSO/SAML', value: null, included: false }, - { name: 'SLA guarantee', value: null, included: false }, ], cta: 'Start Free Trial', ctaLink: '/signup?plan=team', @@ -104,10 +104,10 @@ const PLANS = [ { id: 'enterprise', name: 'Enterprise', - price: null, - period: '', + price: 499, + period: '/month', tagline: 'For organizations at scale', - description: 'Custom solutions with dedicated support and SLA guarantees.', + description: 'Unlimited resources, SSO, SLA guarantees, and dedicated support.', limits: { workspaces: Infinity, repos: Infinity, @@ -119,12 +119,12 @@ const PLANS = [ { name: 'Repositories', value: 'Unlimited', included: true }, { name: 'Concurrent agents', value: 'Unlimited', included: true }, { name: 'Compute hours/month', value: 'Unlimited', included: true }, + { name: 'CPU type', value: 'Dedicated', included: true }, { name: 'Coordinator agents', value: 'Yes', included: true }, - { name: 'Team collaboration', value: 'Yes', included: true }, + { name: 'Auto-scaling', value: 'Yes', included: true }, + { name: 'Session persistence', value: 'Yes', included: true }, { name: 'Priority support', value: 'Dedicated', included: true }, - { name: 'Audit logs', value: 'Yes', included: true }, { name: 'SSO/SAML', value: 'Yes', included: true }, - { name: 'SLA guarantee', value: '99.9%', included: true }, ], cta: 'Contact Sales', ctaLink: '/contact?subject=enterprise', diff --git a/src/dashboard/react-components/App.tsx b/src/dashboard/react-components/App.tsx index 5f890a045..e444409bd 100644 --- a/src/dashboard/react-components/App.tsx +++ b/src/dashboard/react-components/App.tsx @@ -30,6 +30,7 @@ import { TypingIndicator } from './TypingIndicator'; import { OnlineUsersIndicator } from './OnlineUsersIndicator'; import { UserProfilePanel } from './UserProfilePanel'; import { CoordinatorPanel } from './CoordinatorPanel'; +import { BillingResult } from './BillingResult'; import { useWebSocket } from './hooks/useWebSocket'; import { useAgents } from './hooks/useAgents'; import { useMessages } from './hooks/useMessages'; @@ -844,6 +845,33 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) { return () => window.removeEventListener('keydown', handleKeyDown); }, [handleSpawnClick, handleNewConversationClick]); + // Handle billing result routes (success/cancel after Stripe checkout) + const pathname = typeof window !== 'undefined' ? window.location.pathname : ''; + const searchParams = typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : new URLSearchParams(); + + if (pathname === '/billing/success') { + return ( + { + window.location.href = '/'; + }} + /> + ); + } + + if (pathname === '/billing/canceled') { + return ( + { + window.location.href = '/'; + }} + /> + ); + } + return (
    @@ -899,6 +927,12 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) { onThreadSelect={setCurrentThread} onClose={() => setIsSidebarOpen(false)} onSettingsClick={handleSettingsClick} + onTrajectoryClick={() => setIsTrajectoryOpen(true)} + hasActiveTrajectory={trajectoryStatus?.active} + onFleetClick={() => setIsFleetViewActive(!isFleetViewActive)} + isFleetViewActive={isFleetViewActive} + onCoordinatorClick={handleCoordinatorClick} + hasMultipleProjects={mergedProjects.length > 1} />
    diff --git a/src/dashboard/react-components/BillingResult.tsx b/src/dashboard/react-components/BillingResult.tsx new file mode 100644 index 000000000..d89c6147c --- /dev/null +++ b/src/dashboard/react-components/BillingResult.tsx @@ -0,0 +1,440 @@ +/** + * Billing Result Component + * + * Premium success/cancel pages after Stripe checkout. + * Features celebratory animations, confetti, and polished micro-interactions. + */ + +import React, { useEffect, useState, useMemo } from 'react'; +import { cloudApi } from '../lib/cloudApi'; + +export interface BillingResultProps { + type: 'success' | 'canceled'; + sessionId?: string; + onClose: () => void; +} + +interface SubscriptionInfo { + plan: string; + status: string; +} + +// Confetti particle component +function Confetti() { + const particles = useMemo(() => { + return Array.from({ length: 50 }, (_, i) => ({ + id: i, + x: Math.random() * 100, + delay: Math.random() * 0.5, + duration: 2 + Math.random() * 2, + size: 4 + Math.random() * 8, + color: ['#00d9ff', '#00b8d9', '#4ade80', '#facc15', '#f472b6', '#a78bfa'][Math.floor(Math.random() * 6)], + rotation: Math.random() * 360, + })); + }, []); + + return ( +
    + {particles.map((p) => ( +
    + ))} + +
    + ); +} + +// Animated checkmark with draw effect +function AnimatedCheckmark() { + return ( +
    + {/* Glow ring */} +
    + + {/* Main circle with gradient border */} +
    +
    + + + + + + + + + +
    +
    + + {/* Sparkle particles */} +
    +
    +
    + + +
    + ); +} + +// Premium loading spinner +function LoadingSpinner() { + return ( +
    + {/* Outer rotating ring */} +
    + + {/* Inner pulsing core */} +
    + + {/* Center dot */} +
    +
    +
    + + {/* Orbiting particles */} +
    +
    +
    +
    +
    +
    +
    + ); +} + +// Benefit item with staggered animation +function BenefitItem({ children, delay }: { children: React.ReactNode; delay: number }) { + return ( +
  • +
    + + + +
    + {children} + +
  • + ); +} + +export function BillingResult({ type, sessionId, onClose }: BillingResultProps) { + const [subscription, setSubscription] = useState(null); + const [isLoading, setIsLoading] = useState(type === 'success'); + const [error, setError] = useState(null); + const [showConfetti, setShowConfetti] = useState(false); + + useEffect(() => { + if (type !== 'success') return; + + const fetchSubscription = async () => { + try { + await new Promise(resolve => setTimeout(resolve, 1500)); + + const result = await cloudApi.getSubscription(); + if (result.success && result.data) { + setSubscription({ + plan: result.data.subscription?.tier || 'pro', + status: result.data.subscription?.status || 'active', + }); + // Trigger confetti after successful load + setTimeout(() => setShowConfetti(true), 200); + } + } catch (err) { + setError('Could not verify subscription. Please check your billing settings.'); + } finally { + setIsLoading(false); + } + }; + + fetchSubscription(); + }, [type, sessionId]); + + const planName = subscription?.plan === 'pro' ? 'Pro' : subscription?.plan === 'team' ? 'Team' : subscription?.plan === 'enterprise' ? 'Enterprise' : 'Premium'; + + if (type === 'success') { + return ( +
    + {/* Background gradient orbs */} +
    +
    +
    +
    + + {showConfetti && } + +
    + {/* Decorative top gradient line */} +
    + + {isLoading ? ( +
    +
    + +
    +

    + Processing your upgrade +

    +

    + Confirming your subscription... +

    +
    + ) : error ? ( +
    +
    + + + + + +
    +

    + Almost there! +

    +

    {error}

    + +
    + ) : ( + <> +
    + +
    + +

    + Welcome to {planName}! +

    + +

    + Your subscription is active. Time to supercharge your workflow. +

    + +
    +

    Your new powers

    +
      + Expanded workspace & repository limits + More compute hours for your agents + Increased concurrent agent capacity + Priority support & faster responses +
    +
    + + + + )} +
    + + +
    + ); + } + + // Canceled flow + return ( +
    + {/* Subtle background */} +
    +
    +
    + +
    +
    + + + +
    + +

    + No problem! +

    +

    + Checkout was canceled and you haven't been charged. + You can upgrade anytime when you're ready. +

    + +
    + + +
    +
    + + +
    + ); +} diff --git a/src/dashboard/react-components/WorkspaceStatusIndicator.tsx b/src/dashboard/react-components/WorkspaceStatusIndicator.tsx index c40974427..aeaf06321 100644 --- a/src/dashboard/react-components/WorkspaceStatusIndicator.tsx +++ b/src/dashboard/react-components/WorkspaceStatusIndicator.tsx @@ -158,7 +158,10 @@ export function WorkspaceStatusIndicator({ title={statusMessage || config.label} > {config.icon} - + {config.label} {config.pulseColor && ( diff --git a/src/dashboard/react-components/layout/Header.tsx b/src/dashboard/react-components/layout/Header.tsx index 0fde29fdc..9b8e4e467 100644 --- a/src/dashboard/react-components/layout/Header.tsx +++ b/src/dashboard/react-components/layout/Header.tsx @@ -134,11 +134,14 @@ export function Header({
    {selectedAgent.status && ( - + {selectedAgent.status} )} diff --git a/src/dashboard/react-components/layout/Sidebar.tsx b/src/dashboard/react-components/layout/Sidebar.tsx index 31f062bf3..4173668e0 100644 --- a/src/dashboard/react-components/layout/Sidebar.tsx +++ b/src/dashboard/react-components/layout/Sidebar.tsx @@ -47,6 +47,18 @@ export interface SidebarProps { onClose?: () => void; /** Handler for opening settings */ onSettingsClick?: () => void; + /** Mobile nav: Trajectory viewer toggle */ + onTrajectoryClick?: () => void; + /** Mobile nav: Whether there's an active trajectory */ + hasActiveTrajectory?: boolean; + /** Mobile nav: Fleet view toggle */ + onFleetClick?: () => void; + /** Mobile nav: Whether fleet view is active */ + isFleetViewActive?: boolean; + /** Mobile nav: Coordinator toggle */ + onCoordinatorClick?: () => void; + /** Mobile nav: Whether multiple projects are connected (shows coordinator) */ + hasMultipleProjects?: boolean; } export function Sidebar({ @@ -71,6 +83,12 @@ export function Sidebar({ onThreadSelect, onClose, onSettingsClick, + onTrajectoryClick, + hasActiveTrajectory, + onFleetClick, + isFleetViewActive, + onCoordinatorClick, + hasMultipleProjects, }: SidebarProps) { const [searchQuery, setSearchQuery] = useState(''); const [activeTab, setActiveTab] = useState(() => { @@ -307,6 +325,68 @@ export function Sidebar({ )}
    + {/* Mobile Navigation - shows items hidden in header on mobile */} +
    +

    Quick Actions

    +
    + {onFleetClick && ( + + )} + {onTrajectoryClick && ( + + )} + {hasMultipleProjects && onCoordinatorClick && ( + + )} + onClose?.()} + > + + Metrics + +
    +
    + {/* Footer Actions */}