Skip to content

feat(billing-6): admin app support, build tooling, profiler security#1258

Open
Rod-Christensen wants to merge 9 commits into
developfrom
feat/billing-6
Open

feat(billing-6): admin app support, build tooling, profiler security#1258
Rod-Christensen wants to merge 9 commits into
developfrom
feat/billing-6

Conversation

@Rod-Christensen

@Rod-Christensen Rod-Christensen commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR adds platform infrastructure for permission-gated apps, fixes build tooling, closes profiler security holes, enforces a multi-org model with an active org switcher, adds invite-time team assignment, resend invite, and Zitadel cleanup on invite cancel.

What changed and why

1. Shell.tsx -- post-auth MF remote registration for permission-gated apps

Apps with requiredPermissions were excluded from the pre-auth probe. After authentication, Shell.tsx now detects ConnectResult apps absent from the probe set, registers their MF remotes, and appends them to the merged app list.

2. deps-tasks.js -- include pnpm-workspace.yaml in dependency hash

Adding new workspace entries now triggers auto pnpm install.

3. cmd_cprofile.py -- add task.control permission checks

All five cProfile DAP commands now require task.control permission.

4. Multi-org model with active org switcher

Problem: The original organizations: OrgInfo[] array was never used as multi-org -- every accessor read organizations[0]. Changed to organization: OrgInfo | null (single active org) with default_org_id on the User model to control which org is active.

AccountInfo model: organizations: list[OrgInfo] -> organization: Optional[OrgInfo] with simplified permission helpers.

ConnectResult type: organizations: OrgInfo[] -> organization: OrgInfo | null, plus optional memberships: OrgInfo[] and defaultOrgId: string for the org switcher UI.

OSS provider: emits organization={...} instead of organizations=[{...}].

Frontend: All organizations?.[0] accessors replaced with organization? across shell-ui, shared-ui, and all VSCode providers.

ProfilePanel org switcher:

  • Shows all org memberships; active org at full opacity with checkmark "Active"
  • Inactive orgs grayed out (opacity 0.45) with "Switch to" button
  • Teams only expanded under the active org

New SDK method: account.setDefaultOrg(orgId) -- switches active org, server pushes refreshed AccountInfo.

5. Invite with team pre-assignment

Problem: Inviting a user required three steps: invite, add to team, set permissions.

Solution: Invite modal now shows team checkboxes with per-team PermGrid. InviteMemberParams accepts optional teamAssignments array. Backend creates TeamMember rows in the same transaction. Selected permissions shown as outlined PermPill badges matching the Teams panel style.

6. Resend invite

Problem: No way to resend the Zitadel initialization email if the user missed it.

Solution: "Resend" button on pending members in MembersPanel with loading/sent feedback. New resendInvite SDK method calls rrext_account_members with resend_invite subcommand.

7. Avatar color palette

Replaced bright saturated colors with muted professional tones (steel blue, slate gray, sage green, dusty mauve, ocean blue, moss, warm taupe).

Files changed (22 files)

Python (ai packages): models.py, oss/init.py, base.py, cmd_task.py, cmd_debug.py, cmd_monitor.py, cmd_misc.py
TypeScript SDK: client.ts, types/client.ts, types/account.ts, account.ts
Shell-UI: Shell.tsx, AccountPage.tsx, EnvironmentPage.tsx
Shared-UI: AccountView.tsx, ProfilePanel.tsx, MembersPanel.tsx, shared.tsx
VSCode: AccountProvider.ts, EnvironmentProvider.ts, ProjectProvider.ts, SettingsProvider.ts, SidebarProvider.ts, connection-message-handler.ts, AccountWebview.tsx

Test plan

  • Log in -- verify ConnectResult contains organization: {...} (not array)
  • Verify org admin permissions, billing, teams all work with single-org model
  • Verify OSS mode (synthetic organization: {id: 'local', ...})
  • Invite a user with team pre-assignment -- verify they land with permissions on first login
  • Click "Resend" on a pending member -- verify button shows "Sending..." then "Sent!"
  • Switch org via Profile panel -- verify permissions/teams/billing update
  • Permission-gated apps load after auth without page reload
  • cProfile commands require task.control permission

Companion PR: rocketride-ai/rocketride-saas#214

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Comprehensive billing dashboard with transaction history and usage tracking by user/team.
    • In-app top-up purchases and subscription plan upgrades via embedded checkout modal.
    • Organization switching and team assignment during member invites.
    • Resend invite functionality for pending members.
    • Enhanced subscription gating UI with improved padlock icon visibility.
  • Changes

    • Simplified to single active organization per user.
    • Migrated from credit-pack to usage-based billing model.
    • Updated account management with new org/team controls.

@github-actions

Copy link
Copy Markdown
🤖 Internal: Discord sync marker

Auto-managed by the Discord notification workflow. Stores the linked Discord message ID. Do not edit or delete.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 45fdbc57-c124-49cc-ba95-b0c59b3147ac

📥 Commits

Reviewing files that changed from the base of the PR and between fa4d7ce and 0870057.

📒 Files selected for processing (68)
  • apps/shell-ui/src/components/layout/Shell.tsx
  • apps/shell-ui/src/components/layout/ShellLayout.tsx
  • apps/shell-ui/src/index.ts
  • apps/shell-ui/src/views/account/AccountPage.tsx
  • apps/shell-ui/src/views/billing/BillingPage.tsx
  • apps/shell-ui/src/views/environment/EnvironmentPage.tsx
  • apps/shell-ui/src/views/settings/SettingsPage.tsx
  • apps/vscode/src/providers/AccountProvider.ts
  • apps/vscode/src/providers/BarStatusProvider.ts
  • apps/vscode/src/providers/EnvironmentProvider.ts
  • apps/vscode/src/providers/ProjectProvider.ts
  • apps/vscode/src/providers/SettingsProvider.ts
  • apps/vscode/src/providers/SidebarProvider.ts
  • apps/vscode/src/providers/shared/connection-message-handler.ts
  • apps/vscode/src/providers/template.html
  • apps/vscode/src/providers/views/Account/AccountWebview.tsx
  • apps/vscode/src/providers/views/Settings/ConnectionSettings.tsx
  • apps/vscode/src/providers/views/Settings/DeploySettings.tsx
  • apps/vscode/src/providers/views/Settings/SettingsWebview.tsx
  • apps/vscode/src/providers/views/components/ConnectionConfig.tsx
  • apps/vscode/src/providers/views/components/panels/CloudPanel.tsx
  • apps/vscode/src/shared/util/subscriptionGate.ts
  • packages/ai/src/ai/account/base.py
  • packages/ai/src/ai/account/models.py
  • packages/ai/src/ai/account/oss/__init__.py
  • packages/ai/src/ai/modules/task/commands/cmd_cprofile.py
  • packages/ai/src/ai/modules/task/commands/cmd_debug.py
  • packages/ai/src/ai/modules/task/commands/cmd_misc.py
  • packages/ai/src/ai/modules/task/commands/cmd_monitor.py
  • packages/ai/src/ai/modules/task/commands/cmd_task.py
  • packages/ai/src/ai/modules/task/task_engine.py
  • packages/ai/src/ai/modules/task/task_metrics.py
  • packages/ai/src/ai/modules/task/task_server.py
  • packages/client-python/src/rocketride/billing.py
  • packages/client-python/src/rocketride/types/__init__.py
  • packages/client-python/src/rocketride/types/account.py
  • packages/client-python/src/rocketride/types/billing.py
  • packages/client-python/src/rocketride/types/client.py
  • packages/client-python/src/rocketride/types/events.py
  • packages/client-typescript/src/client/account.ts
  • packages/client-typescript/src/client/billing.ts
  • packages/client-typescript/src/client/client.ts
  • packages/client-typescript/src/client/types/account.ts
  • packages/client-typescript/src/client/types/billing.ts
  • packages/client-typescript/src/client/types/client.ts
  • packages/client-typescript/src/client/types/events.ts
  • packages/server/engine-lib/rocketlib-python/lib/depends.py
  • packages/shared-ui/src/assets/icons/PadlockIcon.tsx
  • packages/shared-ui/src/components/canvas/components/node/node-component/run-button/RunButton.tsx
  • packages/shared-ui/src/components/status/StatusHeader.tsx
  • packages/shared-ui/src/components/tab-panel/TabPanel.tsx
  • packages/shared-ui/src/index.ts
  • packages/shared-ui/src/modules/account/AccountView.tsx
  • packages/shared-ui/src/modules/account/components/BillingPanel.tsx
  • packages/shared-ui/src/modules/account/components/MembersPanel.tsx
  • packages/shared-ui/src/modules/account/components/ProfilePanel.tsx
  • packages/shared-ui/src/modules/account/components/shared.tsx
  • packages/shared-ui/src/modules/billing/BillingView.tsx
  • packages/shared-ui/src/modules/billing/components/BillingDashboard.tsx
  • packages/shared-ui/src/modules/billing/components/CreditsPanel.tsx
  • packages/shared-ui/src/modules/billing/components/TopUpModal.tsx
  • packages/shared-ui/src/modules/billing/components/UpgradeModal.tsx
  • packages/shared-ui/src/modules/billing/index.ts
  • packages/shared-ui/src/modules/billing/types.ts
  • packages/shared-ui/src/modules/checkout/CheckoutModal.tsx
  • packages/shared-ui/src/modules/checkout/PlanPicker.tsx
  • packages/shared-ui/src/modules/checkout/types.ts
  • scripts/deps-tasks.js
 _________________________________
< Pvt. Rabbit reporting for duty! >
 ---------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
📝 Walkthrough

Walkthrough

Replaces credit-pack billing with a metadata-driven Stripe checkout and admin billing dashboard; adds ledger APIs, org-scoped billing events, SDK billing methods, VS Code/webview checkout plumbing, and UI/type updates across shell and shared components.

Changes

Billing Platform Modernization

Layer / File(s) Summary
Post-auth App Registration & Subscription Gate Removal
apps/shell-ui/src/components/layout/Shell.tsx, apps/shell-ui/src/components/layout/ShellLayout.tsx, apps/shell-ui/src/views/settings/SettingsPage.tsx
Shell now merges identity-only apps post-auth via registerAndMapApps, overlays desktop metadata, and removes the local subscription placeholder branch; Settings adds a Pipe Builder subscribe prompt emitting shell:subscribe.
Billing Data Model & Types
packages/client-python/src/rocketride/types/billing.py, packages/client-typescript/src/client/types/billing.ts, packages/shared-ui/src/modules/checkout/types.ts, packages/shared-ui/src/modules/billing/types.ts
Introduces AppPrice/CheckoutPlan shape with metadata, simplifies CreditBalance, and adds LedgerTransaction, TransactionsResult, UsageRollup.
Checkout Plan Picker & Modal UI
packages/shared-ui/src/modules/checkout/PlanPicker.tsx, packages/shared-ui/src/modules/checkout/CheckoutModal.tsx
PlanPicker/readout now use metadata (action/order/description/displayAmount) and stripePriceId; CheckoutModal uses stripePriceId, filters top-up plans, updates copy and layout.
Billing Dashboard & Modals
packages/shared-ui/src/modules/billing/components/BillingDashboard.tsx, TopUpModal.tsx, UpgradeModal.tsx, (removed) BillingView.tsx
Adds admin BillingDashboard (spending, balance, usage leaderboard, transactions, active tasks) and TopUp/Upgrade modals; removes legacy BillingView.
Shell Account & Settings Integration
apps/shell-ui/src/views/account/AccountPage.tsx, apps/shell-ui/src/index.ts
AccountPage replaced credit-pack flows with dashboard data (plans, transactions, usage) and actions for top-ups and subscription upgrades; BillingPage export removed.
Shared-UI Account & Billing Panel Refactor
packages/shared-ui/src/modules/account/AccountView.tsx, BillingPanel.tsx, CreditsPanel.tsx, src/index.ts
AccountView/BillingPanel props updated for dashboard data and callbacks; CreditsPanel shows Account Balance table; billing module exports adjusted to new components.
Client SDK Billing API Expansion
packages/client-python/.../billing.py, packages/client-typescript/.../billing.ts, packages/client-typescript/src/client/constants.ts
SDKs now expose upgrade_subscription, purchase_topup, get_transactions, get_usage_by_user/team and return AppPrice; SDK version bumped to 1.3.0.
VS Code Account Provider & Webview Checkout
apps/vscode/src/providers/AccountProvider.ts, template.html, views/Account/AccountWebview.tsx
AccountProvider posts full account + billing payloads, registers billing monitor events, and handles checkout/top-up/upgrade RPCs to/from the webview. Webview stores billing datasets, resolves checkout promises, and renders CheckoutModal. CSP updated for Stripe endpoints.
VS Code Settings & Connection Checkout Wiring
apps/vscode/src/providers/SettingsProvider.ts, SettingsWebview.tsx, ConnectionSettings.tsx, DeploySettings.tsx, ConnectionConfig.tsx, CloudPanel.tsx
Settings provider/webview add checkout message handlers and Promise resolvers; Connection/Cloud UI propagate isSubscribed and checkout callbacks; legacy subscribe banner removed.
Backend Billing Interface & Task Metrics
packages/ai/src/ai/account/base.py, task_metrics.py, task_engine.py
AccountBase gains billing methods (OSS no-op); TaskMetrics writes async cumulative debits via apply_debit() with per-run idempotency billing_run_id; startup/shutdown/monitoring updated.
Event System Org Scoping & Permissions
packages/client-*/types/events, packages/ai/src/ai/modules/task/commands/cmd_monitor.py, task_server.py, cmd_cprofile.py
Adds EVENT_TYPE.BILLING; broadcast/send_server_event gains org_id scoping; cprofile commands add task.control permission checks.
Subscription Gate Tightening & UI Polish
apps/vscode/src/shared/util/subscriptionGate.ts, PadlockIcon.tsx, RunButton.tsx, StatusHeader.tsx, TabPanel.tsx, scripts/deps-tasks.js, depends.py
Gate now allows only 'subscribed' or 'trialing'; padlock SVG and lock placements updated; tab scrollbar styling and workspace dependency hashing added; minor startup log removal.

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly Related PRs

Suggested labels

module:ai

Suggested Reviewers

  • jmaionchi
  • stepmikhaylov

🐰 I hop and tally credits bright,
Plans and topups in a row tonight.
Dashboards hum, invoices sing,
Checkout bells and ledgers ring.
Hooray — the billing bunny takes a bite!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/billing-6

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 29

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
apps/vscode/src/providers/AccountProvider.ts (1)

1-1143: ⚠️ Potential issue | 🟡 Minor

Update apps/vscode/docs/ for the webview message contract (billing/checkout)
apps/vscode/src uses message types billing:purchaseTopup, billing:upgrade, checkout:fetchPlans, checkout:createSession, and checkout:confirmPending, but apps/vscode/docs/ (index.md, installation.md, usage.md) contains none of these strings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/vscode/src/providers/AccountProvider.ts` around lines 1 - 1143, Docs are
missing the webview message contract entries for billing/checkout; add
documentation for the message types handled by AccountProvider. Update the docs
to include the messages "billing:purchaseTopup", "billing:upgrade",
"checkout:fetchPlans", "checkout:createSession", and "checkout:confirmPending"
and document the expected payloads and responses (including errors)
corresponding to the handler functions handlePurchaseTopup,
handleUpgradeSubscription, handleCheckoutFetchPlans,
handleCheckoutCreateSession, and handleCheckoutConfirmPending so the docs match
the implementation and show request fields, response shape, and error cases.

Source: Coding guidelines

apps/vscode/src/providers/views/Settings/SettingsWebview.tsx (1)

984-984: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing subscribed in useMemo dependency array causes stale closure.

The panels memoization uses subscribed (passed as isSubscribed prop at lines 886 and 950) but it's not included in the dependency array. When subscription status changes, the panels won't re-render with the updated value.

Proposed fix
-		[settings, message, testMessage, engineVersions, engineVersionsLoading, serverCapabilities, cloudSignedIn, cloudUserName, teams, dockerStatus, dockerProgress, dockerError, dockerBusy, dockerAction, dockerVersionOptions, dockerSelectedVersion, serviceStatus, serviceProgress, serviceError, serviceBusy, serviceAction, serviceVersionOptions, serviceSelectedVersion, sudoPromptVisible, sudoPasswordInput]
+		[settings, message, testMessage, engineVersions, engineVersionsLoading, serverCapabilities, cloudSignedIn, cloudUserName, teams, dockerStatus, dockerProgress, dockerError, dockerBusy, dockerAction, dockerVersionOptions, dockerSelectedVersion, serviceStatus, serviceProgress, serviceError, serviceBusy, serviceAction, serviceVersionOptions, serviceSelectedVersion, sudoPromptVisible, sudoPasswordInput, subscribed, handleFetchPlans, handleCreateCheckout, handleConfirmPending, handleCheckoutSuccess]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/vscode/src/providers/views/Settings/SettingsWebview.tsx` at line 984,
The memoized "panels" value (created with useMemo in SettingsWebview) closes
over the "subscribed" state/prop (passed as isSubscribed) but does not include
it in the dependency array, causing stale UI when subscription status changes;
update the useMemo dependency array for the panels computation to include
"subscribed" (or the prop name isSubscribed if that's the local variable), so
panels re-compute when subscription state changes, and verify any related
memoized callbacks that reference subscribed also include it.
packages/ai/src/ai/modules/task/commands/cmd_cprofile.py (1)

160-166: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Proxy profiling still skips task-team authorization.

verify_permission('task.control') only checks the caller's default-team permissions. The proxy path still resolves the target with self._server.get_task_control(target) and never requires task.control on that task's team, so a user who has task.control anywhere can operate on another team's task if they know the token. Move the authorization check into _proxy_to_task() by resolving the target with account_info and require='task.control', then reuse that checked control.

Also applies to: 191-197, 220-226, 249-255, 281-287

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/ai/modules/task/commands/cmd_cprofile.py` around lines 160 -
166, The proxy branch currently bypasses per-task-team authorization because
verify_permission('task.control') checks only the caller's default team while
the proxy path calls _server.get_task_control(target) without enforcing
task.team permission; move the authorization into _proxy_to_task so it resolves
the target using the caller's account_info and passes require='task.control' to
_server.get_task_control (or otherwise performs the same checked lookup) and
then reuse that verified control object when forwarding; update all proxy call
sites (_proxy_to_task invocations around the blocks referenced) to remove the
earlier generic verify_permission call and instead rely on the checked control
returned by _proxy_to_task.
packages/client-python/src/rocketride/types/__init__.py (1)

157-270: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Update SDK documentation for new public billing types and events.

Per coding guidelines, public SDK signature changes require documentation updates:

  • packages/client-python/src/rocketride/types/__init__.py: New exports AppPrice, LedgerTransaction, TransactionsResult, UsageRollup → update packages/client-python/docs/
  • packages/client-python/src/rocketride/types/events.py: New EVENT_TYPE.BILLING flag → update packages/client-python/docs/
  • packages/client-typescript/src/client/types/events.ts: New EVENT_TYPE.BILLING flag → update packages/client-typescript/docs/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-python/src/rocketride/types/__init__.py` around lines 157 -
270, The public SDK surface changed: add the new billing exports (AppPrice,
LedgerTransaction, TransactionsResult, UsageRollup) to the Python docs and
update both Python and TypeScript event docs to document the new
EVENT_TYPE.BILLING flag; specifically, update the API reference pages to list
the new types exported from rocketride.types (mention AppPrice,
LedgerTransaction, TransactionsResult, UsageRollup), add examples and type
descriptions in the Python docs folder, and update the events documentation for
both client-python and client-typescript to include EVENT_TYPE.BILLING and its
semantics so generated docs, examples, and changelog reflect the new public API.

Source: Coding guidelines

packages/shared-ui/src/modules/billing/index.ts (1)

6-11: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Update module docstring — BillingView no longer exists.

The docstring still references "the pure BillingView component" (line 9), but BillingView has been removed from exports. Update the documentation to reflect the new component structure (BillingDashboard, CreditsPanel, TopUpModal, UpgradeModal).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared-ui/src/modules/billing/index.ts` around lines 6 - 11, The
module docstring mentions the removed BillingView; update it to list the actual
exported components and APIs instead: replace the "pure BillingView component"
phrase with the current component names (BillingDashboard, CreditsPanel,
TopUpModal, UpgradeModal), and mention the exported billingApi wrappers and
related types so the header accurately reflects the module exports (e.g.,
references to BillingDashboard, CreditsPanel, TopUpModal, UpgradeModal,
billingApi).
packages/shared-ui/src/modules/billing/components/CreditsPanel.tsx (1)

227-278: ⚠️ Potential issue | 🟠 Major

Fix missing credit pack grid rendering in CreditsPanel

  • The CreditsPanel.tsx render (lines 227–278) only shows the balance summary, optional “Add more capacity…”, and an error banner; it contains no JSX that renders the provided packs/uses the pack-related UI (e.g., S.packsRow/S.pack), leaving packs/onBuy without any visible purchase UI.
  • Restore the pack grid markup wired to handleBuy/onBuy, or remove the unused packs and onBuy props (and related pack styles/logic) if this component is meant to be balance-only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared-ui/src/modules/billing/components/CreditsPanel.tsx` around
lines 227 - 278, CreditsPanel is missing the UI that renders the provided packs
and purchase actions; either restore the pack grid markup and wire it to the
existing handler or remove unused pack props and styles. Locate the CreditsPanel
component and restore a section that maps over the packs prop (packs) to render
UI elements using S.packsRow / S.pack, call the existing handleBuy or prop onBuy
when a pack button is clicked, and ensure pack labels/prices use S.pack
text/styles and are keyboard/click accessible; alternatively, if this component
should be balance-only, remove the packs and onBuy props and any
S.packsRow/S.pack styles/logic to avoid unused code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/shell-ui/src/views/account/AccountPage.tsx`:
- Around line 489-490: The memberNames and teamNames lookup objects are being
recreated on every render; wrap their creation in React's useMemo to memoize
them based on members and teams respectively. Create const memberNames =
useMemo(() => Object.fromEntries(members.map(m => [m.userId, m.displayName ||
m.email || m.userId])), [members]) and const teamNames = useMemo(() =>
Object.fromEntries(teams.map(t => [t.id, t.name || t.id])), [teams]) at the
component level, then pass memberNames={memberNames} and teamNames={teamNames}
where currently Object.fromEntries(...) is used. Ensure useMemo is imported from
React.
- Line 104: Remove the redundant txPage React state and its setter setTxPage
from AccountPage: delete the useState declaration for txPage and remove any
calls to setTxPage inside handleTransactionPage and loadBilling (the updates on
lines mentioned). Rely on the page info inside TransactionsResult instead
(ensure handleTransactionPage and loadBilling use the TransactionsResult.page
value rather than txPage), and clean up any unused imports or variables
referencing txPage so there are no lingering references.
- Around line 256-265: The useEffect that loads data on connect calls loader
functions (loadProfile, loadKeys, loadOrg, loadMembers, loadTeams, loadBilling)
but only depends on isConnected and client, so it can miss re-running when those
loader functions or orgId change; update the effect to include the loader
functions or relevant state (e.g., orgId) in its dependency array or memoize the
loaders with useCallback so the effect re-triggers when they change, ensuring
loadProfile, loadKeys, loadOrg, loadMembers, loadTeams and loadBilling run for
new orgId or when loaders are recreated.
- Around line 284-294: The effect that reloads the current section uses
variables and functions (section, client, isConnected, setSectionError,
loadProfile, loadBilling, loadKeys, loadOrg, loadMembers, loadTeams) but only
lists reloadCounter in its dependency array, which can cause stale closures and
triggers linter warnings; fix by updating the useEffect dependency array to
include all referenced values (reloadCounter, section, client, isConnected,
loadProfile, loadBilling, loadKeys, loadOrg, loadMembers, loadTeams,
setSectionError) or, if you have a deliberate reason to run only on
reloadCounter, add a clear eslint-disable-next-line comment above the effect
explaining why and keeping only reloadCounter—pick one approach and apply it
consistently to the useEffect definition.

In `@apps/shell-ui/src/views/settings/SettingsPage.tsx`:
- Around line 396-405: The subscription derivation (info, pipeBuilderApp,
isSubscribed) is recomputed every render causing handleSubscribe (useCallback)
to re-create; wrap the derived values in useMemo so pipeBuilderApp and
isSubscribed are stable references and update handleSubscribe's dependency to
use the memoized pipeBuilderApp. Specifically, memoize the result of
client?.getAccountInfo() or directly memoize the derived pipeBuilderApp and
isSubscribed using useMemo (referencing client or client.getAccountInfo as the
dependency), then keep handleSubscribe as useCallback depending on the memoized
pipeBuilderApp (symbols: client.getAccountInfo, PIPE_BUILDER_APP_ID,
pipeBuilderApp, isSubscribed, handleSubscribe).

In `@apps/vscode/src/providers/AccountProvider.ts`:
- Around line 234-236: The handler for 'billing:upgrade' passes
message.newPriceId which doesn't exist on AccountWebviewMessage; update the call
in AccountProvider.ts to pass message.priceId (or alternatively add newPriceId
to the AccountWebviewMessage interface) so the third argument to
handleUpgradeSubscription receives the correct value; ensure
AccountWebviewMessage (the interface defined around lines 34-48) and the
handleUpgradeSubscription signature remain consistent after the change.
- Around line 46-47: The AccountWebviewMessage interface is missing newPriceId
which is read by the billing:upgrade handler and sent from AccountWebview.tsx;
update the AccountWebviewMessage definition (alongside existing priceId?: string
and subscriptionId?: string) to include newPriceId?: string so TypeScript
correctly types message.newPriceId wherever the billing:upgrade handler and
AccountWebview interact.
- Around line 789-800: The billing monitor registration in setupEventListeners
is skipped if getClient() is null; remove the addMonitor call from
setupEventListeners and instead register the billing monitor inside
handleConnectionStateChange when the new state is CONNECTED: call
this.connectionManager.getClient(), then call client.addMonitor({ token: '*' },
['billing']) with proper .catch logging (or swallow) and ensure you only
register once per connection (e.g., guard by a flag or rely on connection
lifecycle). Keep the existing billingEventListener on 'shell:event' as-is so
fetchBillingData is triggered when apaext_billing_update arrives.

In `@apps/vscode/src/providers/SettingsProvider.ts`:
- Around line 198-213: The catch block in the checkout:confirmPending handler
swallows errors and always posts {type: 'checkout:confirmResult', error: null},
misleading the webview; update the try/catch around
getConnectionManager()?.getClient() and (billingClient as any).dapRequest(...)
so that on failure you send a meaningful error to the webview (e.g.,
postMessage({type: 'checkout:confirmResult', error: err.message || err})) and
optionally include a boolean success flag; ensure you reference the same
message.subscriptionId/message.priceId and use panel.webview.postMessage in the
catch to return the actual error instead of null.
- Around line 183-196: The checkout:createSession branch passes message.priceId
directly into createCheckoutSession without validating it; add a check in that
case to ensure message.priceId is a non-empty string before calling
getConnectionManager()/billingClient.billing.createCheckoutSession (use the
existing billingClient and orgId extraction), and if invalid send
panel.webview.postMessage with an appropriate error (mirror the error response
shape used in the catch). Ensure you do not cast blindly (avoid (message.priceId
as string)) and return early with a clear error via panel.webview.postMessage
when priceId is missing/empty.

In `@apps/vscode/src/providers/views/Account/AccountWebview.tsx`:
- Around line 405-407: The onTransactionPage prop on AccountWebview is currently
a no-op, preventing transaction pagination; replace the empty function with a
real handler (e.g., a function named onTransactionPage or handleTransactionPage
inside AccountWebview) that accepts a page number and posts a message to the
host (using the webview messaging API such as vscode.postMessage or
acquireVsCodeApi().postMessage) containing the page index/action so the
extension can load that page; if pagination is intentionally disabled instead,
add a clear inline comment next to the onTransactionPage prop explaining that
limitation.

In `@apps/vscode/src/providers/views/components/panels/CloudPanel.tsx`:
- Line 13: Import list in CloudPanel.tsx includes an unused symbol `useRef`;
remove `useRef` from the React import (i.e., change "import React, { useEffect,
useState, useCallback, useRef } from 'react';" to omit useRef) so only used
hooks (`useEffect`, `useState`, `useCallback`) are imported and the linter
warning is resolved.

In `@apps/vscode/src/providers/views/Settings/SettingsWebview.tsx`:
- Around line 455-458: The SettingsIncomingMessage union is missing the
isSubscribed field for the settingsLoaded variant, which forces the unsafe cast
used when calling setSubscribed; update the SettingsIncomingMessage type's
settingsLoaded variant to include isSubscribed?: boolean (or boolean if always
present) so the compiler knows the field exists, then remove the (message as
any) cast and access message.isSubscribed directly when handling the
settingsLoaded branch that calls setSubscribed.

In `@packages/ai/src/ai/account/base.py`:
- Around line 224-257: The apply_debit method currently requires team_id: str
but callers may pass None; change its signature in the Account base class to
accept an optional team_id (team_id: str | None = None) to match apply_credit
and downstream use, and update the method docstring to indicate team_id is
optional (may be None) and how it is used for attribution; ensure any references
to team_id in apply_debit (and its SaaS override) handle None safely.

In `@packages/ai/src/ai/modules/task/commands/cmd_monitor.py`:
- Around line 140-147: The current org scoping only compares org_id to the first
entry in self._account_info.organizations (using first_org and conn_org) which
drops events for multi‑org users; update the check in the cmd_monitor.py logic
to iterate over all entries in self._account_info.organizations (or derive the
connected org from self._account_info.defaultTeam if present) and match org_id
against any organization's id (handle both dict and object shapes as done now),
returning only if no match is found.

In `@packages/ai/src/ai/modules/task/task_metrics.py`:
- Around line 491-515: While holding _metrics_lock the code awaits
account.apply_debit in _report_to_billing_system which blocks sampling/teardown;
fix by snapshotting all billing-related values (e.g., token_totals,
billing_run_id, org_id, user_id, team_id, pipeline_name, source_name, client_id,
_duration_seconds, _status.tokens.total) into local variables while holding
_metrics_lock, then release the lock and perform the await account.apply_debit
loop using those locals (keep idem_key construction and description logic the
same). Apply the same pattern to the other call sites that invoke
_report_to_billing_system while holding _metrics_lock so no external I/O is
awaited under the lock.

In `@packages/client-python/src/rocketride/billing.py`:
- Around line 268-302: Add client-side input validation to prevent malformed
billing queries: in Python BillingApi.get_transactions and the TypeScript
BillingApi.getTransactions validate that scope is one of 'org','team','user';
require scope_id when scope is 'team' or 'user' and forbid/ignore scope_id when
scope is 'org'; enforce pagination bounds (page >= 1, 1 <= page_size <= 100) and
validate optional since is a proper ISO datetime/string if needed; on violation
raise a synchronous error (e.g., ValueError / throw) with a clear message before
calling self._client.call.

In `@packages/client-python/src/rocketride/types/__init__.py`:
- Around line 157-167: The package's public exports were extended in
packages/client-python/src/rocketride/types/__init__.py to include new billing
types (AppPrice, LedgerTransaction, TransactionsResult, UsageRollup); update the
SDK documentation under packages/client-python/docs/ to list these new public
types wherever the public API/type exports are documented (update index/README
and any auto-generated API reference pages) so the docs reflect AppPrice,
LedgerTransaction, TransactionsResult, and UsageRollup as part of the public SDK
surface.

In `@packages/shared-ui/src/modules/account/AccountView.tsx`:
- Line 768: The useMemo that builds `panels` (which renders `BillingPanel`) is
missing many dependencies causing stale closures; update the dependency array
for the memo that defines `panels` to include all props used inside the memo:
`transactions`, `usageByUser`, `usageByTeam`, `activeTasks`, `dashboardLoading`,
`onTransactionPage`, `topupPlans`, `onBuyTopup`, `allPlans`, `onPurchaseTopup`,
`memberNames`, `teamNames`, `onUpgradeSubscription`, `apps`, `onSubscribe`, and
`isOrgAdmin` (in addition to the existing items like `sectionError`, `profile`,
`authUser`, `keys`, `org`, `teams`, `teamDetail`, `activeTeamId`, `members`,
`isConnected`, `subscriptions`, `billingLoading`, `billingError`,
`creditBalance`) so the `BillingPanel` receives fresh props whenever any of
those values change.
- Around line 170-177: Replace the loosened any types on billing-related props
with a concrete TopupPlan type: change topupPlans?: any[] to topupPlans?:
TopupPlan[], onBuyTopup?: (plan: any) => void to onBuyTopup?: (plan: TopupPlan)
=> void, allPlans?: any[] to allPlans?: TopupPlan[], and onPurchaseTopup?:
(plan: any) => Promise<{ status: string; clientSecret?: string }> to
onPurchaseTopup?: (plan: TopupPlan) => Promise<{ status: string; clientSecret?:
string }>; import or reference the TopupPlan type from BillingDashboard (or
define an equivalent interface) and update the prop types in AccountView so
TypeScript enforces the correct shape for these billing props.

In `@packages/shared-ui/src/modules/account/components/BillingPanel.tsx`:
- Around line 369-374: The UpgradeModal is being passed allPlans which allows
selecting a stripePriceId from a different app; filter the plans by
upgradeTarget.appId before passing them so only plans belonging to that app are
selectable. In BillingPanel, compute a filteredPlans (or similar) from allPlans
where plan.appId === upgradeTarget.appId and pass filteredPlans to the
UpgradeModal's plans prop; keep the existing onUpgrade handler that calls
onUpgradeSubscription(upgradeTarget.appId, newPriceId) to ensure the request
stays scoped to the subscription's app.

In `@packages/shared-ui/src/modules/billing/components/BillingDashboard.tsx`:
- Around line 301-302: The props topupPlans and onBuyTopup declared on
BillingDashboardProps and destructured in the BillingDashboard component are
currently unused; either remove them from the props/interface or wire them into
the component (e.g., pass topupPlans and onBuyTopup into your TopUpModal or a
future top-up UI) and add a TODO comment if you choose to keep them for upcoming
work. Locate the BillingDashboard component and the BillingDashboardProps type,
then either delete topupPlans and onBuyTopup from the prop definition and
destructuring, or ensure topupPlans is consumed (rendered or forwarded) and
onBuyTopup is called where purchase actions occur (or add a clear TODO
explaining why they remain unused). Ensure references like TopupPlan,
topupPlans, onBuyTopup, BillingDashboardProps, and BillingDashboard are updated
consistently.
- Line 550: The code is using (tx as any).description which hides a missing
field on the SDK LedgerTransaction type; fix by adding description?: string to
the LedgerTransaction type in the SDK (preferred) and then remove the any cast
so the cell becomes tx.description (or tx.description ?? '--'), or if you cannot
change the SDK immediately, create a small local typed extension (e.g. interface
ExtendedLedgerTransaction extends LedgerTransaction { description?: string })
and cast tx to that type (tx as ExtendedLedgerTransaction) before accessing
description; update uses in BillingDashboard.tsx to rely on the typed field
instead of any.
- Around line 342-343: The local variable `total` in the mapping/aggregation
block is computed with Math.max but never used; remove the unused `total`
declaration (or if intended for future use, incorporate it into the returned
object or logic). Update the code around the mapping that returns { resource,
purchased: p, consumed: c, net, pct: ... } by deleting the `const total =
Math.max(p, c + net, 1);` line (or add `total` to the returned object if needed)
so there are no unused variables.
- Around line 627-634: BillingDashboard currently returns SpendingVelocity,
UsageLeaderboard, ActiveTasksView, and TransactionLog but never renders the
previously defined BalanceBreakdown component; update the JSX returned by the
BillingDashboard component to include <BalanceBreakdown .../> (passing the
appropriate props such as balance, transactions, maybe memberNames/teamNames if
applicable) in the desired position (e.g., before SpendingVelocity or as
"section 1") so the BalanceBreakdown component defined in this file is actually
rendered.

In `@packages/shared-ui/src/modules/billing/components/CreditsPanel.tsx`:
- Around line 243-246: CreditsPanel.tsx is using (balance as any).granted and
.consumed to access fields that CreditBalance currently doesn't declare; update
the type and mapping instead of using any: add optional granted?: Record<string,
number> and consumed?: Record<string, number> to the CreditBalance interface in
packages/client-typescript/src/client/types/billing.ts (and update the API
response/mapping layer that constructs CreditBalance to populate those fields
when present), then remove the (balance as any) casts in CreditsPanel.tsx so the
component reads balance.granted?.[resource] and balance.consumed?.[resource]
safely; alternatively, if the API doesn't return them, compute granted/consumed
from existing balances in the SDK before returning a CreditBalance instance.

In `@packages/shared-ui/src/modules/billing/components/TopUpModal.tsx`:
- Around line 137-140: The modal builds (topupPlans in TopUpModal.tsx and the
analogous list in UpgradeModal.tsx) currently filter only by metadata.kind and
therefore include retired plans; update the useMemo filters to also check plan
active status (e.g., include p.isActive or CheckoutPlan.isActive) so the
predicate becomes metadata?.kind === 'topup' && p.isActive (and the equivalent
for upgrade), ensuring only active plans are returned and selectable.

In `@packages/shared-ui/src/modules/billing/components/UpgradeModal.tsx`:
- Around line 183-190: The changeDirection calculation compares raw amountCents
which misclassifies yearly vs monthly prices; update the useMemo in
changeDirection to compute a normalized per-period cost (e.g., per-month) for
both selectedPlan and currentPlan using each price's billing interval or
intervalCount (divide yearly amounts by 12 or by intervalCount) and compare
those normalized values instead of amountCents so upgrades/downgrades are
decided on equivalent periods while keeping the same dependencies (selectedPlan,
currentPriceId, subscriptionPlans).

In `@packages/shared-ui/src/modules/billing/index.ts`:
- Line 24: The index re-export list is missing the AppPrice type; update the
export statement in the billing module (the export that currently lists
BillingDetail, StripePlan, CreditBalance, CreditPack, LedgerTransaction,
TransactionsResult, UsageRollup) to also include AppPrice so that AppPrice from
types.ts is re-exported alongside the other billing types.

---

Outside diff comments:
In `@apps/vscode/src/providers/AccountProvider.ts`:
- Around line 1-1143: Docs are missing the webview message contract entries for
billing/checkout; add documentation for the message types handled by
AccountProvider. Update the docs to include the messages
"billing:purchaseTopup", "billing:upgrade", "checkout:fetchPlans",
"checkout:createSession", and "checkout:confirmPending" and document the
expected payloads and responses (including errors) corresponding to the handler
functions handlePurchaseTopup, handleUpgradeSubscription,
handleCheckoutFetchPlans, handleCheckoutCreateSession, and
handleCheckoutConfirmPending so the docs match the implementation and show
request fields, response shape, and error cases.

In `@apps/vscode/src/providers/views/Settings/SettingsWebview.tsx`:
- Line 984: The memoized "panels" value (created with useMemo in
SettingsWebview) closes over the "subscribed" state/prop (passed as
isSubscribed) but does not include it in the dependency array, causing stale UI
when subscription status changes; update the useMemo dependency array for the
panels computation to include "subscribed" (or the prop name isSubscribed if
that's the local variable), so panels re-compute when subscription state
changes, and verify any related memoized callbacks that reference subscribed
also include it.

In `@packages/ai/src/ai/modules/task/commands/cmd_cprofile.py`:
- Around line 160-166: The proxy branch currently bypasses per-task-team
authorization because verify_permission('task.control') checks only the caller's
default team while the proxy path calls _server.get_task_control(target) without
enforcing task.team permission; move the authorization into _proxy_to_task so it
resolves the target using the caller's account_info and passes
require='task.control' to _server.get_task_control (or otherwise performs the
same checked lookup) and then reuse that verified control object when
forwarding; update all proxy call sites (_proxy_to_task invocations around the
blocks referenced) to remove the earlier generic verify_permission call and
instead rely on the checked control returned by _proxy_to_task.

In `@packages/client-python/src/rocketride/types/__init__.py`:
- Around line 157-270: The public SDK surface changed: add the new billing
exports (AppPrice, LedgerTransaction, TransactionsResult, UsageRollup) to the
Python docs and update both Python and TypeScript event docs to document the new
EVENT_TYPE.BILLING flag; specifically, update the API reference pages to list
the new types exported from rocketride.types (mention AppPrice,
LedgerTransaction, TransactionsResult, UsageRollup), add examples and type
descriptions in the Python docs folder, and update the events documentation for
both client-python and client-typescript to include EVENT_TYPE.BILLING and its
semantics so generated docs, examples, and changelog reflect the new public API.

In `@packages/shared-ui/src/modules/billing/components/CreditsPanel.tsx`:
- Around line 227-278: CreditsPanel is missing the UI that renders the provided
packs and purchase actions; either restore the pack grid markup and wire it to
the existing handler or remove unused pack props and styles. Locate the
CreditsPanel component and restore a section that maps over the packs prop
(packs) to render UI elements using S.packsRow / S.pack, call the existing
handleBuy or prop onBuy when a pack button is clicked, and ensure pack
labels/prices use S.pack text/styles and are keyboard/click accessible;
alternatively, if this component should be balance-only, remove the packs and
onBuy props and any S.packsRow/S.pack styles/logic to avoid unused code.

In `@packages/shared-ui/src/modules/billing/index.ts`:
- Around line 6-11: The module docstring mentions the removed BillingView;
update it to list the actual exported components and APIs instead: replace the
"pure BillingView component" phrase with the current component names
(BillingDashboard, CreditsPanel, TopUpModal, UpgradeModal), and mention the
exported billingApi wrappers and related types so the header accurately reflects
the module exports (e.g., references to BillingDashboard, CreditsPanel,
TopUpModal, UpgradeModal, billingApi).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a7c14812-9b2f-4de8-aec0-03b4eebe84f9

📥 Commits

Reviewing files that changed from the base of the PR and between b5b106b and eba93c0.

📒 Files selected for processing (50)
  • apps/shell-ui/src/components/layout/Shell.tsx
  • apps/shell-ui/src/components/layout/ShellLayout.tsx
  • apps/shell-ui/src/index.ts
  • apps/shell-ui/src/views/account/AccountPage.tsx
  • apps/shell-ui/src/views/billing/BillingPage.tsx
  • apps/shell-ui/src/views/settings/SettingsPage.tsx
  • apps/vscode/src/providers/AccountProvider.ts
  • apps/vscode/src/providers/BarStatusProvider.ts
  • apps/vscode/src/providers/SettingsProvider.ts
  • apps/vscode/src/providers/template.html
  • apps/vscode/src/providers/views/Account/AccountWebview.tsx
  • apps/vscode/src/providers/views/Settings/ConnectionSettings.tsx
  • apps/vscode/src/providers/views/Settings/DeploySettings.tsx
  • apps/vscode/src/providers/views/Settings/SettingsWebview.tsx
  • apps/vscode/src/providers/views/components/ConnectionConfig.tsx
  • apps/vscode/src/providers/views/components/panels/CloudPanel.tsx
  • apps/vscode/src/shared/util/subscriptionGate.ts
  • packages/ai/src/ai/account/base.py
  • packages/ai/src/ai/modules/task/commands/cmd_cprofile.py
  • packages/ai/src/ai/modules/task/commands/cmd_monitor.py
  • packages/ai/src/ai/modules/task/task_engine.py
  • packages/ai/src/ai/modules/task/task_metrics.py
  • packages/ai/src/ai/modules/task/task_server.py
  • packages/client-python/src/rocketride/billing.py
  • packages/client-python/src/rocketride/types/__init__.py
  • packages/client-python/src/rocketride/types/billing.py
  • packages/client-python/src/rocketride/types/events.py
  • packages/client-typescript/src/client/billing.ts
  • packages/client-typescript/src/client/constants.ts
  • packages/client-typescript/src/client/types/billing.ts
  • packages/client-typescript/src/client/types/events.ts
  • packages/server/engine-lib/rocketlib-python/lib/depends.py
  • packages/shared-ui/src/assets/icons/PadlockIcon.tsx
  • packages/shared-ui/src/components/canvas/components/node/node-component/run-button/RunButton.tsx
  • packages/shared-ui/src/components/status/StatusHeader.tsx
  • packages/shared-ui/src/components/tab-panel/TabPanel.tsx
  • packages/shared-ui/src/index.ts
  • packages/shared-ui/src/modules/account/AccountView.tsx
  • packages/shared-ui/src/modules/account/components/BillingPanel.tsx
  • packages/shared-ui/src/modules/billing/BillingView.tsx
  • packages/shared-ui/src/modules/billing/components/BillingDashboard.tsx
  • packages/shared-ui/src/modules/billing/components/CreditsPanel.tsx
  • packages/shared-ui/src/modules/billing/components/TopUpModal.tsx
  • packages/shared-ui/src/modules/billing/components/UpgradeModal.tsx
  • packages/shared-ui/src/modules/billing/index.ts
  • packages/shared-ui/src/modules/billing/types.ts
  • packages/shared-ui/src/modules/checkout/CheckoutModal.tsx
  • packages/shared-ui/src/modules/checkout/PlanPicker.tsx
  • packages/shared-ui/src/modules/checkout/types.ts
  • scripts/deps-tasks.js
💤 Files with no reviewable changes (5)
  • apps/shell-ui/src/views/billing/BillingPage.tsx
  • apps/shell-ui/src/index.ts
  • packages/shared-ui/src/modules/billing/BillingView.tsx
  • packages/shared-ui/src/index.ts
  • packages/server/engine-lib/rocketlib-python/lib/depends.py

const [usageByUser, setUsageByUser] = useState<UsageRollup[]>([]);
const [usageByTeam, setUsageByTeam] = useState<UsageRollup[]>([]);
const [dashboardLoading, setDashboardLoading] = useState(false);
const [txPage, setTxPage] = useState(1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

txPage state is set but never read.

The txPage state is updated in handleTransactionPage and loadBilling but is never used anywhere. The TransactionsResult already contains the page number. Consider removing this redundant state.

-	const [txPage, setTxPage] = useState(1);

And remove the setTxPage calls on lines 223 and 234.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/shell-ui/src/views/account/AccountPage.tsx` at line 104, Remove the
redundant txPage React state and its setter setTxPage from AccountPage: delete
the useState declaration for txPage and remove any calls to setTxPage inside
handleTransactionPage and loadBilling (the updates on lines mentioned). Rely on
the page info inside TransactionsResult instead (ensure handleTransactionPage
and loadBilling use the TransactionsResult.page value rather than txPage), and
clean up any unused imports or variables referencing txPage so there are no
lingering references.

Comment on lines +256 to +265
// ── Load ALL data upfront on connect (badges, counts, billing) ──────────
useEffect(() => {
setSectionError(null);
if (!isConnected || !client) return;
loadProfile();
loadKeys();
loadOrg();
loadMembers();
loadTeams();
loadBilling();
}, [isConnected, client]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Missing loader function dependencies in upfront data loading effect.

This effect calls loader functions but doesn't include them in dependencies. If orgId changes after initial connection, the loaders will be recreated but this effect won't re-run, potentially leaving stale data.

Consider including the loader functions in dependencies, or use a separate mechanism to handle org changes.

 	useEffect(() => {
 		if (!isConnected || !client) return;
 		loadProfile();
 		loadKeys();
 		loadOrg();
 		loadMembers();
 		loadTeams();
 		loadBilling();
-	}, [isConnected, client]);
+	}, [isConnected, client, loadProfile, loadKeys, loadOrg, loadMembers, loadTeams, loadBilling]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/shell-ui/src/views/account/AccountPage.tsx` around lines 256 - 265, The
useEffect that loads data on connect calls loader functions (loadProfile,
loadKeys, loadOrg, loadMembers, loadTeams, loadBilling) but only depends on
isConnected and client, so it can miss re-running when those loader functions or
orgId change; update the effect to include the loader functions or relevant
state (e.g., orgId) in its dependency array or memoize the loaders with
useCallback so the effect re-triggers when they change, ensuring loadProfile,
loadKeys, loadOrg, loadMembers, loadTeams and loadBilling run for new orgId or
when loaders are recreated.

Comment on lines +284 to +294
// ── Reload current section on refresh signal ─────────────────────────────
useEffect(() => {
if (!reloadCounter || !isConnected || !client) return;
setSectionError(null);
if (section === 'profile') loadProfile();
else if (section === 'billing') loadBilling();
else if (section === 'api-keys') { loadProfile(); loadKeys(); }
else if (section === 'organization') loadOrg();
else if (section === 'members') { loadOrg(); loadMembers(); }
else if (section === 'teams') { loadOrg(); loadTeams(); }
}, [section, isConnected, client, reloadCounter]);
}, [reloadCounter]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if eslint-plugin-react-hooks exhaustive-deps is configured
rg -l 'exhaustive-deps' --type=js --type=json

Repository: rocketride-org/rocketride-server

Length of output: 58


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect ESLint config for react-hooks / exhaustive-deps settings
ls -la eslint.config.mjs || true
rg -n "react-hooks|exhaustive-deps|plugin-react-hooks|eslint-plugin-react-hooks" eslint.config.mjs package.json --hidden 2>/dev/null || true

# 2) Inspect the referenced effect and surrounding code
FILE="apps/shell-ui/src/views/account/AccountPage.tsx"
test -f "$FILE" && echo "Found $FILE" || (echo "Missing $FILE" && exit 1)

# Show effect region and a bit more context
sed -n '240,330p' "$FILE" | nl -ba | sed -n '1,130p'

# 3) Find definitions/usages of loadProfile/loadBilling/loadKeys/loadOrg/loadMembers/loadTeams
for fn in loadProfile loadBilling loadKeys loadOrg loadMembers loadTeams; do
  echo "----- $fn -----"
  rg -n "$fn" "$FILE"
done

# 4) Check whether the load* functions are memoized (useCallback) and their deps
rg -n "useCallback\\(\\s*\\(.*\\)\\s*=>|useCallback\\s*\\(" "$FILE" | head -n 80

Repository: rocketride-org/rocketride-server

Length of output: 578


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/shell-ui/src/views/account/AccountPage.tsx"
test -f "$FILE" && echo "Found $FILE" || exit 1

# Show effect block around the cited lines using cat -n (nl isn't available)
echo "==== Effect block (approx 260-310) ===="
cat -n "$FILE" | sed -n '260,310p'

# Show where 'section' is defined/updated (quick context)
echo "==== section state & updates (search) ===="
rg -n "const \\[section\\]|setSection\\(|section\\s*=" "$FILE" | head -n 50

# Find definitions of referenced load* functions and check if they are memoized
echo "==== load* definitions (search for useCallback) ===="
for fn in loadProfile loadBilling loadKeys loadOrg loadMembers loadTeams; do
  echo "----- $fn -----"
  rg -n "function \\b$fn\\b|const \\b$fn\\b\\s*=|\\b$fn\\b\\s*=\\s*\\(" "$FILE" || true
  rg -n "useCallback\\(\\s*.*\\b$fn\\b|const \\b$fn\\b\\s*=\\s*useCallback" "$FILE" || true
done

Repository: rocketride-org/rocketride-server

Length of output: 3647


Fix React hook deps in AccountPage reload effect
The reload effect reads section, client, and isConnected (plus loadProfile/loadBilling/loadKeys/loadOrg/loadMembers/loadTeams) but has }, [reloadCounter] only; react-hooks/exhaustive-deps is enabled as a warning, so this will be flagged and can still capture outdated values when reloadCounter changes. Either include the referenced deps, or add an eslint-disable with a comment explaining why the effect must only run on reloadCounter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/shell-ui/src/views/account/AccountPage.tsx` around lines 284 - 294, The
effect that reloads the current section uses variables and functions (section,
client, isConnected, setSectionError, loadProfile, loadBilling, loadKeys,
loadOrg, loadMembers, loadTeams) but only lists reloadCounter in its dependency
array, which can cause stale closures and triggers linter warnings; fix by
updating the useEffect dependency array to include all referenced values
(reloadCounter, section, client, isConnected, loadProfile, loadBilling,
loadKeys, loadOrg, loadMembers, loadTeams, setSectionError) or, if you have a
deliberate reason to run only on reloadCounter, add a clear
eslint-disable-next-line comment above the effect explaining why and keeping
only reloadCounter—pick one approach and apply it consistently to the useEffect
definition.

Comment on lines +489 to +490
memberNames={Object.fromEntries(members.map((m: any) => [m.userId, m.displayName || m.email || m.userId]))}
teamNames={Object.fromEntries(teams.map((t: any) => [t.id, t.name || t.id]))}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Memoize memberNames and teamNames to avoid recreating on every render.

These lookup objects are recreated on every render. Consider using useMemo to avoid unnecessary object allocations.

♻️ Proposed refactor

Add at the component level:

const memberNames = useMemo(
	() => Object.fromEntries(members.map((m: any) => [m.userId, m.displayName || m.email || m.userId])),
	[members]
);
const teamNames = useMemo(
	() => Object.fromEntries(teams.map((t: any) => [t.id, t.name || t.id])),
	[teams]
);

Then pass memberNames={memberNames} and teamNames={teamNames} directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/shell-ui/src/views/account/AccountPage.tsx` around lines 489 - 490, The
memberNames and teamNames lookup objects are being recreated on every render;
wrap their creation in React's useMemo to memoize them based on members and
teams respectively. Create const memberNames = useMemo(() =>
Object.fromEntries(members.map(m => [m.userId, m.displayName || m.email ||
m.userId])), [members]) and const teamNames = useMemo(() =>
Object.fromEntries(teams.map(t => [t.id, t.name || t.id])), [teams]) at the
component level, then pass memberNames={memberNames} and teamNames={teamNames}
where currently Object.fromEntries(...) is used. Ensure useMemo is imported from
React.

Comment on lines +396 to +405
// ── Subscription status ─────────────────────────────────────────────
const info = client?.getAccountInfo();
const pipeBuilderApp = (info?.apps ?? []).find((a: any) => a.id === PIPE_BUILDER_APP_ID);
const isSubscribed = pipeBuilderApp?.appStatus === 'subscribed' || pipeBuilderApp?.appStatus === 'trialing';

/** Opens the checkout modal via the shell:subscribe event. */
const handleSubscribe = useCallback(() => {
if (!pipeBuilderApp) return;
ConnectionManager.getInstance().emit('shell:subscribe', { app: pipeBuilderApp });
}, [pipeBuilderApp]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Memoize subscription status derivation to prevent unnecessary recalculations.

info, pipeBuilderApp, and isSubscribed are computed on every render. Since handleSubscribe depends on pipeBuilderApp, and pipeBuilderApp is recomputed each render (creating a new object reference even if the data is the same), handleSubscribe will be recreated on every render despite useCallback.

♻️ Proposed refactor to memoize subscription state
-	// ── Subscription status ─────────────────────────────────────────────
-	const info = client?.getAccountInfo();
-	const pipeBuilderApp = (info?.apps ?? []).find((a: any) => a.id === PIPE_BUILDER_APP_ID);
-	const isSubscribed = pipeBuilderApp?.appStatus === 'subscribed' || pipeBuilderApp?.appStatus === 'trialing';
-
-	/** Opens the checkout modal via the shell:subscribe event. */
-	const handleSubscribe = useCallback(() => {
-		if (!pipeBuilderApp) return;
-		ConnectionManager.getInstance().emit('shell:subscribe', { app: pipeBuilderApp });
-	}, [pipeBuilderApp]);
+	// ── Subscription status ─────────────────────────────────────────────
+	const { pipeBuilderApp, isSubscribed } = useMemo(() => {
+		const info = client?.getAccountInfo();
+		const app = (info?.apps ?? []).find((a: { id: string }) => a.id === PIPE_BUILDER_APP_ID);
+		const subscribed = app?.appStatus === 'subscribed' || app?.appStatus === 'trialing';
+		return { pipeBuilderApp: app, isSubscribed: subscribed };
+	}, [client]);
+
+	/** Opens the checkout modal via the shell:subscribe event. */
+	const handleSubscribe = useCallback(() => {
+		if (!pipeBuilderApp) return;
+		ConnectionManager.getInstance().emit('shell:subscribe', { app: pipeBuilderApp });
+	}, [pipeBuilderApp]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/shell-ui/src/views/settings/SettingsPage.tsx` around lines 396 - 405,
The subscription derivation (info, pipeBuilderApp, isSubscribed) is recomputed
every render causing handleSubscribe (useCallback) to re-create; wrap the
derived values in useMemo so pipeBuilderApp and isSubscribed are stable
references and update handleSubscribe's dependency to use the memoized
pipeBuilderApp. Specifically, memoize the result of client?.getAccountInfo() or
directly memoize the derived pipeBuilderApp and isSubscribed using useMemo
(referencing client or client.getAccountInfo as the dependency), then keep
handleSubscribe as useCallback depending on the memoized pipeBuilderApp
(symbols: client.getAccountInfo, PIPE_BUILDER_APP_ID, pipeBuilderApp,
isSubscribed, handleSubscribe).

Comment on lines +627 to +634
return (
<>
<SpendingVelocity balance={balance} transactions={transactions} onAddCapacity={onAddCapacity} />
<UsageLeaderboard usageByUser={usageByUser} usageByTeam={usageByTeam} memberNames={memberNames} teamNames={teamNames} />
<ActiveTasksView activeTasks={activeTasks} />
<TransactionLog transactions={transactions} onPageChange={onTransactionPage} memberNames={memberNames} />
</>
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

BalanceBreakdown component is defined but not rendered.

The BalanceBreakdown component (lines 322-367) is defined and documented as "section 1" in the file header, but it's never rendered in the main BillingDashboard component. This appears to be an oversight.

🐛 Proposed fix to include BalanceBreakdown
 	return (
 		<>
+			<BalanceBreakdown balance={balance} transactions={transactions} />
 			<SpendingVelocity balance={balance} transactions={transactions} onAddCapacity={onAddCapacity} />
 			<UsageLeaderboard usageByUser={usageByUser} usageByTeam={usageByTeam} memberNames={memberNames} teamNames={teamNames} />
 			<ActiveTasksView activeTasks={activeTasks} />
 			<TransactionLog transactions={transactions} onPageChange={onTransactionPage} memberNames={memberNames} />
 		</>
 	);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<>
<SpendingVelocity balance={balance} transactions={transactions} onAddCapacity={onAddCapacity} />
<UsageLeaderboard usageByUser={usageByUser} usageByTeam={usageByTeam} memberNames={memberNames} teamNames={teamNames} />
<ActiveTasksView activeTasks={activeTasks} />
<TransactionLog transactions={transactions} onPageChange={onTransactionPage} memberNames={memberNames} />
</>
);
return (
<>
<BalanceBreakdown balance={balance} transactions={transactions} />
<SpendingVelocity balance={balance} transactions={transactions} onAddCapacity={onAddCapacity} />
<UsageLeaderboard usageByUser={usageByUser} usageByTeam={usageByTeam} memberNames={memberNames} teamNames={teamNames} />
<ActiveTasksView activeTasks={activeTasks} />
<TransactionLog transactions={transactions} onPageChange={onTransactionPage} memberNames={memberNames} />
</>
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared-ui/src/modules/billing/components/BillingDashboard.tsx`
around lines 627 - 634, BillingDashboard currently returns SpendingVelocity,
UsageLeaderboard, ActiveTasksView, and TransactionLog but never renders the
previously defined BalanceBreakdown component; update the JSX returned by the
BillingDashboard component to include <BalanceBreakdown .../> (passing the
appropriate props such as balance, transactions, maybe memberNames/teamNames if
applicable) in the desired position (e.g., before SpendingVelocity or as
"section 1") so the BalanceBreakdown component defined in this file is actually
rendered.

Comment on lines +243 to +246
{Object.entries(balance.balances).map(([resource, net]) => {
const granted = (balance as any).granted?.[resource] ?? 0;
const consumed = (balance as any).consumed?.[resource] ?? 0;
const label = balance.labels?.[resource] ?? resource;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the CreditBalance type definition in the SDK
rg -nP 'interface\s+CreditBalance|type\s+CreditBalance' --type=ts -A 10

Repository: rocketride-org/rocketride-server

Length of output: 1157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section of CreditsPanel.tsx
sed -n '220,280p' packages/shared-ui/src/modules/billing/components/CreditsPanel.tsx | nl -ba | sed -n '1,120p'

# Search for granted/consumed usage in the repo
rg -n "granted\?\.\[|consumed\?\.\[|granted.*resource|consumed.*resource" packages -S || true
rg -n --glob '*/billing*' "granted|consumed" . -S || true

# Search for CreditBalance shape usage/typing
rg -n "CreditBalance" packages -S || true

Repository: rocketride-org/rocketride-server

Length of output: 119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Relevant section of CreditsPanel.tsx with line numbers
echo "---- CreditsPanel.tsx lines 230-270 ----"
sed -n '230,270p' packages/shared-ui/src/modules/billing/components/CreditsPanel.tsx | cat -n

# Search for granted/consumed usage in the repo
echo "---- Search granted/consumed patterns ----"
rg -n "granted\?\.\[|consumed\?\.\[|granted.*resource|consumed.*resource" . -S || true
rg -n --glob '*/billing*/*' "granted|consumed" . -S || true
rg -n "granted|consumed" packages -S || true

# Search for CreditBalance usage/typing
echo "---- Search CreditBalance usage ----"
rg -n "CreditBalance" . -S || true

Repository: rocketride-org/rocketride-server

Length of output: 50389


Fix CreditBalance typing: remove any casts for granted/consumed
CreditsPanel.tsx reads granted/consumed via (balance as any).granted?.[resource] and (balance as any).consumed?.[resource] (~lines 244-245), but the SDK CreditBalance type (packages/client-typescript/src/client/types/billing.ts) only defines balances and labels. Update the SDK type/response mapping to include granted and consumed (if the API returns them), or derive those columns from the available data so the UI doesn’t silently show 0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared-ui/src/modules/billing/components/CreditsPanel.tsx` around
lines 243 - 246, CreditsPanel.tsx is using (balance as any).granted and
.consumed to access fields that CreditBalance currently doesn't declare; update
the type and mapping instead of using any: add optional granted?: Record<string,
number> and consumed?: Record<string, number> to the CreditBalance interface in
packages/client-typescript/src/client/types/billing.ts (and update the API
response/mapping layer that constructs CreditBalance to populate those fields
when present), then remove the (balance as any) casts in CreditsPanel.tsx so the
component reads balance.granted?.[resource] and balance.consumed?.[resource]
safely; alternatively, if the API doesn't return them, compute granted/consumed
from existing balances in the SDK before returning a CreditBalance instance.

Comment on lines +137 to +140
const topupPlans = useMemo(
() => plans.filter((p) => p.metadata?.kind === 'topup'),
[plans],
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor isActive when building modal plan lists. packages/shared-ui/src/modules/billing/components/TopUpModal.tsx and packages/shared-ui/src/modules/billing/components/UpgradeModal.tsx both filter on metadata but never filter on CheckoutPlan.isActive, so retired app_prices rows remain selectable in both purchase flows. The shared root cause is one missing active-plan filter in each modal’s derived list.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared-ui/src/modules/billing/components/TopUpModal.tsx` around
lines 137 - 140, The modal builds (topupPlans in TopUpModal.tsx and the
analogous list in UpgradeModal.tsx) currently filter only by metadata.kind and
therefore include retired plans; update the useMemo filters to also check plan
active status (e.g., include p.isActive or CheckoutPlan.isActive) so the
predicate becomes metadata?.kind === 'topup' && p.isActive (and the equivalent
for upgrade), ensuring only active plans are returned and selectable.

Comment on lines +183 to +190
const changeDirection = useMemo(() => {
if (!selectedPlan) return null;
const currentPlan = subscriptionPlans.find((p) => p.stripePriceId === currentPriceId);
if (!currentPlan) return 'change';
if (selectedPlan.amountCents > currentPlan.amountCents) return 'upgrade';
if (selectedPlan.amountCents < currentPlan.amountCents) return 'downgrade';
return 'change';
}, [selectedPlan, currentPriceId, subscriptionPlans]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize billing periods before labeling an upgrade or downgrade.

amountCents is compared raw here, so a yearly plan is always treated as “larger” than a monthly plan with a lower monthly effective cost. That produces incorrect upgrade/downgrade messaging whenever the user switches intervals.

Proposed fix
+	const normalizedAmount = (plan: CheckoutPlan) => {
+		switch (plan.interval) {
+			case 'year':
+				return plan.amountCents / 12;
+			case 'month':
+				return plan.amountCents;
+			default:
+				return plan.amountCents;
+		}
+	};
+
 	const changeDirection = useMemo(() => {
 		if (!selectedPlan) return null;
 		const currentPlan = subscriptionPlans.find((p) => p.stripePriceId === currentPriceId);
 		if (!currentPlan) return 'change';
-		if (selectedPlan.amountCents > currentPlan.amountCents) return 'upgrade';
-		if (selectedPlan.amountCents < currentPlan.amountCents) return 'downgrade';
+		if (normalizedAmount(selectedPlan) > normalizedAmount(currentPlan)) return 'upgrade';
+		if (normalizedAmount(selectedPlan) < normalizedAmount(currentPlan)) return 'downgrade';
 		return 'change';
 	}, [selectedPlan, currentPriceId, subscriptionPlans]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared-ui/src/modules/billing/components/UpgradeModal.tsx` around
lines 183 - 190, The changeDirection calculation compares raw amountCents which
misclassifies yearly vs monthly prices; update the useMemo in changeDirection to
compute a normalized per-period cost (e.g., per-month) for both selectedPlan and
currentPlan using each price's billing interval or intervalCount (divide yearly
amounts by 12 or by intervalCount) and compare those normalized values instead
of amountCents so upgrades/downgrades are decided on equivalent periods while
keeping the same dependencies (selectedPlan, currentPriceId, subscriptionPlans).


// ── Types ───────────────────────────────────────────────────────────────────
export type { BillingDetail, StripePlan, CreditBalance, CreditPack } from './types';
export type { BillingDetail, StripePlan, CreditBalance, CreditPack, LedgerTransaction, TransactionsResult, UsageRollup } from './types';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Missing AppPrice type re-export.

types.ts exports AppPrice, but it's not included in the re-export here. Add it for consistency.

-export type { BillingDetail, StripePlan, CreditBalance, CreditPack, LedgerTransaction, TransactionsResult, UsageRollup } from './types';
+export type { BillingDetail, AppPrice, StripePlan, CreditBalance, CreditPack, LedgerTransaction, TransactionsResult, UsageRollup } from './types';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type { BillingDetail, StripePlan, CreditBalance, CreditPack, LedgerTransaction, TransactionsResult, UsageRollup } from './types';
export type { BillingDetail, AppPrice, StripePlan, CreditBalance, CreditPack, LedgerTransaction, TransactionsResult, UsageRollup } from './types';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared-ui/src/modules/billing/index.ts` at line 24, The index
re-export list is missing the AppPrice type; update the export statement in the
billing module (the export that currently lists BillingDetail, StripePlan,
CreditBalance, CreditPack, LedgerTransaction, TransactionsResult, UsageRollup)
to also include AppPrice so that AppPrice from types.ts is re-exported alongside
the other billing types.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/client-python/src/rocketride/types/account.py (1)

110-124: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align organization typing with the documented nullable payload.

Line 110 says organization can be None, but Line 124 types it as AccountOrganization only. This creates a public SDK contract mismatch and can break type-checked consumers when the server sends organization: null.

Suggested fix
 class AccountProfile(TypedDict, total=False):
@@
-    organization: AccountOrganization
+    organization: AccountOrganization | None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-python/src/rocketride/types/account.py` around lines 110 -
124, The field documentation says organization may be None but the type is
currently AccountOrganization; update the type annotation for the organization
field to allow null (e.g., Optional[AccountOrganization] or AccountOrganization
| None) and import typing.Optional if needed, and keep the docstring in sync;
ensure any constructors, validators, or type hints in the same module that
reference organization (e.g., the Account model/class and any from_dict/to_dict
helpers) are updated to accept and properly handle None values.
packages/client-python/src/rocketride/types/client.py (1)

299-316: ⚠️ Potential issue | 🟡 Minor

Align ConnectResult.organization docstring with its type

  • ConnectResult is total=False, so organization may be omitted, but the field is typed as organization: OrgInfo (not OrgInfo | None); update the docstring at lines 299-316 to remove | None / clarify “optional field of type OrgInfo”.
  • No SDK docs in packages/client-python/docs/index.md reference ConnectResult, organization, or organizations, so no doc update found to be required in that directory.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-python/src/rocketride/types/client.py` around lines 299 -
316, The ConnectResult dataclass is total=False so organization may be omitted,
but the type is organization: OrgInfo; update the docstring for ConnectResult
(the block describing fields including organization) to remove the "OrgInfo |
None" annotation and instead state that organization is an optional field of
type OrgInfo (or “may be omitted”) so the docstring matches the actual type and
optionality; reference the ConnectResult class and its organization field when
making this edit.

Source: Coding guidelines

apps/vscode/src/providers/AccountProvider.ts (1)

794-797: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Consume shell:event as the event object, not { event }.

Other providers handle 'shell:event' as the GenericEvent itself. Here, destructuring ({ event }) makes event the event-name string, so event?.event is always undefined and apaext_billing_update never triggers a refresh.

Suggested fix
-		const billingEventListener = this.connectionManager.on('shell:event' as any, ({ event }: any) => {
-			if (event?.event === 'apaext_billing_update' && AccountProvider.panel) {
+		const billingEventListener = this.connectionManager.on('shell:event' as any, (event: any) => {
+			if (event?.event === 'apaext_billing_update' && AccountProvider.panel) {
 				this.fetchBillingData(AccountProvider.panel).catch(() => {});
 			}
 		});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/vscode/src/providers/AccountProvider.ts` around lines 794 - 797, The
event handler for billingEventListener is destructuring the payload as ({ event
}: any) which makes `event` the event-name string instead of the GenericEvent
object, so `event?.event` is always undefined; update the connectionManager.on
handler to accept the GenericEvent object (e.g., (event: any) or (genericEvent:
GenericEvent)) and then check genericEvent.event === 'apaext_billing_update',
calling AccountProvider.fetchBillingData(AccountProvider.panel) when true;
adjust the parameter name in the handler and any type annotations accordingly
(referencing billingEventListener, connectionManager.on, fetchBillingData, and
AccountProvider.panel).
♻️ Duplicate comments (4)
apps/vscode/src/providers/AccountProvider.ts (4)

234-236: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Declare newPriceId on the webview message contract.

Line 235 reads message.newPriceId, but AccountWebviewMessage does not define that property. In strict TypeScript this is a compile-time error, so the upgrade flow never builds cleanly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/vscode/src/providers/AccountProvider.ts` around lines 234 - 236, The
webview message handler uses message.newPriceId in
AccountProvider.handleUpgradeSubscription but the message contract
AccountWebviewMessage lacks a newPriceId property; add newPriceId: string (or
string | undefined if optional) to the AccountWebviewMessage type/interface used
by the webview, update any related union/variant for the 'billing:upgrade' case
so TypeScript recognizes message.newPriceId, and ensure callers that post the
'billing:upgrade' message include the newPriceId field to keep types consistent
with handleUpgradeSubscription.

938-947: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate priceId at the webview boundary in apps/vscode/src/providers/AccountProvider.ts, apps/vscode/src/providers/ProjectProvider.ts, and apps/vscode/src/providers/SettingsProvider.ts. All three checkout session handlers still cast the incoming priceId and call createCheckoutSession() without checking that it is a non-empty string. This shared root cause turns a malformed webview message into an avoidable backend round-trip and a generic server-side failure instead of a deterministic client-side error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/vscode/src/providers/AccountProvider.ts` around lines 938 - 947, The
checkout handlers (e.g., handleCheckoutCreateSession) accept message.priceId and
call createCheckoutSession without validating it; add a guard at the webview
boundary to ensure message.priceId is a non-empty string before calling
resolveClient() / client.billing.createCheckoutSession. If priceId is missing or
not a string/empty, immediately postMessage back to the panel with type
'checkout:sessionResult' (or equivalent) and an explicit error indicating an
invalid priceId, and skip calling createCheckoutSession; apply the same
validation pattern to the corresponding handlers in ProjectProvider and
SettingsProvider.

957-971: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return real confirm_pending failures in apps/vscode/src/providers/AccountProvider.ts, apps/vscode/src/providers/ProjectProvider.ts, and apps/vscode/src/providers/SettingsProvider.ts. Each provider’s checkout:confirmPending handler catches DAP errors and still posts { error: null }. The shared root cause is that the UI cannot distinguish “payment confirmed” from “confirmation RPC failed,” which makes the embedded checkout flow acknowledge success on a broken path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/vscode/src/providers/AccountProvider.ts` around lines 957 - 971, The
handler handleCheckoutConfirmPending (and the equivalent handlers in
ProjectProvider and SettingsProvider) swallows DAP errors and always posts {
type: 'checkout:confirmResult', error: null }, preventing the UI from knowing
RPC failures; change the try/catch to capture the thrown error (catch (err)) and
post the real failure to the webview (e.g., await panel.webview.postMessage({
type: 'checkout:confirmResult', error: serializeError(err) })) while keeping the
success path posting error: null; update the dapRequest call site (dapRequest
'rrext_account_billing' with subcommand 'confirm_pending') to ensure errors are
propagated rather than masked so the UI can distinguish confirmed payments from
RPC failures.

789-792: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Register the billing monitor when the connection becomes live.

If the Account panel opens before the client connects, getClient() is null here and addMonitor() is skipped for the rest of that session. Billing updates then never auto-refresh after checkout/subscription changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/vscode/src/providers/AccountProvider.ts` around lines 789 - 792, The
billing monitor registration is only attempted once from AccountProvider when
getClient() returns non-null, so if the client connects later the monitor never
gets registered; change this to attempt registration immediately if
this.connectionManager.getClient() returns a client, otherwise subscribe to the
connection-ready event on this.connectionManager and call client.addMonitor({
token: '*' }, ['billing']) when the connection becomes live (use the same token
and event types), ensuring you catch errors as before. Add a guard (e.g., a
boolean flag like billingMonitorRegistered on AccountProvider) to avoid
duplicate registrations and remove/unsubscribe the event listener on disposal to
prevent leaks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/shell-ui/src/views/environment/EnvironmentPage.tsx`:
- Line 44: The computed teamId uses a wrong property name and an any cast:
replace the (authUser as any)?.defaultTeamId check with the correct
ConnectResult property (authUser?.defaultTeam) and remove the unnecessary any
cast so teamId is derived as authUser?.defaultTeam ??
authUser?.organization?.teams?.[0]?.id; ensure you update any references to
defaultTeamId to defaultTeam in EnvironmentPage to match the ConnectResult type.

In `@packages/ai/src/ai/modules/task/commands/cmd_task.py`:
- Around line 154-161: TaskCommands.on_execute currently forwards a
client-supplied teamId to TaskServer.start_task even when no matching org/team
is found, allowing tasks to run with an empty org_id and bypass billing; update
on_execute to resolve the client teamId against
self._account_info.organization['teams'] (use the same lookup that sets org_id)
and if no match is found either reject the request with an error or normalize to
the user's defaultTeam before calling TaskServer.start_task. Also update
permission checks to call verify_permission('task.control') against the resolved
teamId/org (not self._account_info.defaultTeam), and add defensive validation in
TaskServer.start_task to refuse unknown/empty teamId/org_id (return error rather
than starting the task). Finally add a unit test that calls on_execute with an
unknown teamId and asserts the task is rejected and billing
(TaskMetrics._report_to_billing_system) is not skipped incorrectly.

---

Outside diff comments:
In `@apps/vscode/src/providers/AccountProvider.ts`:
- Around line 794-797: The event handler for billingEventListener is
destructuring the payload as ({ event }: any) which makes `event` the event-name
string instead of the GenericEvent object, so `event?.event` is always
undefined; update the connectionManager.on handler to accept the GenericEvent
object (e.g., (event: any) or (genericEvent: GenericEvent)) and then check
genericEvent.event === 'apaext_billing_update', calling
AccountProvider.fetchBillingData(AccountProvider.panel) when true; adjust the
parameter name in the handler and any type annotations accordingly (referencing
billingEventListener, connectionManager.on, fetchBillingData, and
AccountProvider.panel).

In `@packages/client-python/src/rocketride/types/account.py`:
- Around line 110-124: The field documentation says organization may be None but
the type is currently AccountOrganization; update the type annotation for the
organization field to allow null (e.g., Optional[AccountOrganization] or
AccountOrganization | None) and import typing.Optional if needed, and keep the
docstring in sync; ensure any constructors, validators, or type hints in the
same module that reference organization (e.g., the Account model/class and any
from_dict/to_dict helpers) are updated to accept and properly handle None
values.

In `@packages/client-python/src/rocketride/types/client.py`:
- Around line 299-316: The ConnectResult dataclass is total=False so
organization may be omitted, but the type is organization: OrgInfo; update the
docstring for ConnectResult (the block describing fields including organization)
to remove the "OrgInfo | None" annotation and instead state that organization is
an optional field of type OrgInfo (or “may be omitted”) so the docstring matches
the actual type and optionality; reference the ConnectResult class and its
organization field when making this edit.

---

Duplicate comments:
In `@apps/vscode/src/providers/AccountProvider.ts`:
- Around line 234-236: The webview message handler uses message.newPriceId in
AccountProvider.handleUpgradeSubscription but the message contract
AccountWebviewMessage lacks a newPriceId property; add newPriceId: string (or
string | undefined if optional) to the AccountWebviewMessage type/interface used
by the webview, update any related union/variant for the 'billing:upgrade' case
so TypeScript recognizes message.newPriceId, and ensure callers that post the
'billing:upgrade' message include the newPriceId field to keep types consistent
with handleUpgradeSubscription.
- Around line 938-947: The checkout handlers (e.g., handleCheckoutCreateSession)
accept message.priceId and call createCheckoutSession without validating it; add
a guard at the webview boundary to ensure message.priceId is a non-empty string
before calling resolveClient() / client.billing.createCheckoutSession. If
priceId is missing or not a string/empty, immediately postMessage back to the
panel with type 'checkout:sessionResult' (or equivalent) and an explicit error
indicating an invalid priceId, and skip calling createCheckoutSession; apply the
same validation pattern to the corresponding handlers in ProjectProvider and
SettingsProvider.
- Around line 957-971: The handler handleCheckoutConfirmPending (and the
equivalent handlers in ProjectProvider and SettingsProvider) swallows DAP errors
and always posts { type: 'checkout:confirmResult', error: null }, preventing the
UI from knowing RPC failures; change the try/catch to capture the thrown error
(catch (err)) and post the real failure to the webview (e.g., await
panel.webview.postMessage({ type: 'checkout:confirmResult', error:
serializeError(err) })) while keeping the success path posting error: null;
update the dapRequest call site (dapRequest 'rrext_account_billing' with
subcommand 'confirm_pending') to ensure errors are propagated rather than masked
so the UI can distinguish confirmed payments from RPC failures.
- Around line 789-792: The billing monitor registration is only attempted once
from AccountProvider when getClient() returns non-null, so if the client
connects later the monitor never gets registered; change this to attempt
registration immediately if this.connectionManager.getClient() returns a client,
otherwise subscribe to the connection-ready event on this.connectionManager and
call client.addMonitor({ token: '*' }, ['billing']) when the connection becomes
live (use the same token and event types), ensuring you catch errors as before.
Add a guard (e.g., a boolean flag like billingMonitorRegistered on
AccountProvider) to avoid duplicate registrations and remove/unsubscribe the
event listener on disposal to prevent leaks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7e682851-6cc7-4efd-aac9-869d1a275efd

📥 Commits

Reviewing files that changed from the base of the PR and between eba93c0 and fa4d7ce.

📒 Files selected for processing (22)
  • apps/shell-ui/src/components/layout/Shell.tsx
  • apps/shell-ui/src/views/account/AccountPage.tsx
  • apps/shell-ui/src/views/environment/EnvironmentPage.tsx
  • apps/vscode/src/providers/AccountProvider.ts
  • apps/vscode/src/providers/EnvironmentProvider.ts
  • apps/vscode/src/providers/ProjectProvider.ts
  • apps/vscode/src/providers/SettingsProvider.ts
  • apps/vscode/src/providers/SidebarProvider.ts
  • apps/vscode/src/providers/shared/connection-message-handler.ts
  • packages/ai/src/ai/account/base.py
  • packages/ai/src/ai/account/models.py
  • packages/ai/src/ai/account/oss/__init__.py
  • packages/ai/src/ai/modules/task/commands/cmd_debug.py
  • packages/ai/src/ai/modules/task/commands/cmd_misc.py
  • packages/ai/src/ai/modules/task/commands/cmd_monitor.py
  • packages/ai/src/ai/modules/task/commands/cmd_task.py
  • packages/client-python/src/rocketride/types/account.py
  • packages/client-python/src/rocketride/types/client.py
  • packages/client-typescript/src/client/client.ts
  • packages/client-typescript/src/client/types/client.ts
  • packages/shared-ui/src/modules/account/AccountView.tsx
  • packages/shared-ui/src/modules/account/components/ProfilePanel.tsx

const teamId = (authUser as any)?.defaultTeamId ?? authUser?.organizations?.[0]?.teams?.[0]?.id;
const isOrgAdmin = authUser?.organizations?.[0]?.permissions?.includes('org.admin') ?? false;
const orgId = authUser?.organization?.id;
const teamId = (authUser as any)?.defaultTeamId ?? authUser?.organization?.teams?.[0]?.id;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for defaultTeam vs defaultTeamId in TypeScript type definitions
rg -n "defaultTeam" packages/client-typescript/src/client/types/

Repository: rocketride-org/rocketride-server

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for defaultTeamId in TS client types ==="
rg -n "defaultTeamId" packages/client-typescript/src/client/types/ || true

echo
echo "=== Search for authUser shape usage in EnvironmentPage ==="
rg -n "authUser" apps/shell-ui/src/views/environment/EnvironmentPage.tsx
echo
sed -n '1,120p' apps/shell-ui/src/views/environment/EnvironmentPage.tsx | nl -ba | sed -n '30,90p'

Repository: rocketride-org/rocketride-server

Length of output: 640


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for defaultTeamId across repo (limited to TS/TSX) ==="
rg -n --glob '**/*.{ts,tsx}' "defaultTeamId" . || true

echo
echo "=== Search for defaultTeam across repo (limited to TS/TSX) ==="
rg -n --glob '**/*.{ts,tsx}' "defaultTeam" apps/shell-ui/src packages/client-typescript/src || true

echo
echo "=== Inspect EnvironmentPage.tsx around teamId logic ==="
sed -n '30,60p' apps/shell-ui/src/views/environment/EnvironmentPage.tsx

Repository: rocketride-org/rocketride-server

Length of output: 2277


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for defaultTeamId across repo (limited to TS/TSX) ==="
rg -n --glob '**/*.{ts,tsx}' "defaultTeamId" . || true

echo
echo "=== Search for defaultTeam across repo (limited to TS/TSX) ==="
rg -n --glob '**/*.{ts,tsx}' "defaultTeam" apps/shell-ui/src packages/client-typescript/src || true

echo
echo "=== Inspect EnvironmentPage.tsx around teamId logic ==="
sed -n '30,60p' apps/shell-ui/src/views/environment/EnvironmentPage.tsx

Repository: rocketride-org/rocketride-server

Length of output: 2277


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find useAuthUser definition in apps/shell-ui ==="
rg -n "function useAuthUser|const useAuthUser|useAuthUser\\(" apps/shell-ui/src -S

echo
echo "=== Search for defaultTeamId in TypeScript client types ==="
rg -n "defaultTeamId" packages/client-typescript/src/client/types/

echo
echo
echo "=== Show auth user / connect result type portion around defaultTeam in client.ts ==="
sed -n '320,400p' packages/client-typescript/src/client/types/client.ts

Repository: rocketride-org/rocketride-server

Length of output: 620


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find useAuthUser definition in apps/shell-ui ==="
rg -n "function useAuthUser|const useAuthUser|useAuthUser\\(" apps/shell-ui/src -S

echo
echo "=== Search for defaultTeamId in TypeScript client types ==="
rg -n "defaultTeamId" packages/client-typescript/src/client/types/

echo
echo
echo "=== Show auth user / connect result type portion around defaultTeam in client.ts ==="
sed -n '320,400p' packages/client-typescript/src/client/types/client.ts

Repository: rocketride-org/rocketride-server

Length of output: 620


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Show apps/shell-ui/src/hooks/useAuthUser.ts ==="
sed -n '1,140p' apps/shell-ui/src/hooks/useAuthUser.ts

echo
echo "=== Locate ConnectResult definition in client.ts and show around defaultTeam ==="
rg -n "type ConnectResult|interface ConnectResult|export type ConnectResult|export interface ConnectResult" packages/client-typescript/src/client/types/client.ts
rg -n "defaultTeam" packages/client-typescript/src/client/types/client.ts

echo
# show a window around the first defaultTeam match
LINE=$(rg -n "defaultTeam" packages/client-typescript/src/client/types/client.ts | head -n1 | cut -d: -f1)
START=$((LINE-40))
END=$((LINE+40))
sed -n "${START},${END}p" packages/client-typescript/src/client/types/client.ts

Repository: rocketride-org/rocketride-server

Length of output: 5678


Fix teamId field mismatch in EnvironmentPage (defaultTeamId vs defaultTeam)

apps/shell-ui/src/views/environment/EnvironmentPage.tsx derives teamId from (authUser as any)?.defaultTeamId, but ConnectResult defines defaultTeam. The any cast hides a likely typo and can force the fallback to organization?.teams?.[0]?.id.

Suggested fix
-	const teamId = (authUser as any)?.defaultTeamId ?? authUser?.organization?.teams?.[0]?.id;
+	const teamId = authUser?.defaultTeam ?? authUser?.organization?.teams?.[0]?.id;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const teamId = (authUser as any)?.defaultTeamId ?? authUser?.organization?.teams?.[0]?.id;
const teamId = authUser?.defaultTeam ?? authUser?.organization?.teams?.[0]?.id;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/shell-ui/src/views/environment/EnvironmentPage.tsx` at line 44, The
computed teamId uses a wrong property name and an any cast: replace the
(authUser as any)?.defaultTeamId check with the correct ConnectResult property
(authUser?.defaultTeam) and remove the unnecessary any cast so teamId is derived
as authUser?.defaultTeam ?? authUser?.organization?.teams?.[0]?.id; ensure you
update any references to defaultTeamId to defaultTeam in EnvironmentPage to
match the ConnectResult type.

Comment on lines +154 to 161
# Resolve org_id from the user's single organization.
org_id = ''
for org in self._account_info.organizations or []:
org = self._account_info.organization
if org:
for team in org.get('teams', []):
if team.get('id') == team_id:
org_id = org.get('id', '')
break

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate relevant files =="
fd -i 'cmd_task.py$|task_server.py$|dap.py$' packages

echo
echo "== Current execute() callsite =="
rg -n -C4 'team_id = args\.get\(.+teamId|org_id =|start_task\(' packages/ai/src/ai/modules/task/commands/cmd_task.py

echo
echo "== verify_permission implementation(s) =="
fd -i 'dap.py$' packages -x rg -n -C4 'class DAPConn|def verify_permission'

echo
echo "== start_task validation paths =="
fd -i 'task_server.py$' packages -x rg -n -C4 'async def start_task|team_id|org_id|resolve_team_permissions|verify_permission|PermissionError'

Repository: rocketride-org/rocketride-server

Length of output: 10636


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cmd_task.py: show execute() around start_task call and verify_permission() definition =="
python3 - <<'PY'
import re,sys,os, pathlib
path="packages/ai/src/ai/modules/task/commands/cmd_task.py"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
# print around execute method block where start_task called
start=0
for i,l in enumerate(lines,1):
    if "async def on_execute" in l:
        start=i
        break
if start:
    lo=max(1,start-20); hi=min(len(lines), start+220)
    for j in range(lo,hi+1):
        print(f"{j}:{lines[j-1].rstrip()}")
else:
    print("on_execute not found")
# locate verify_permission
for i,l in enumerate(lines,1):
    if re.search(r"def verify_permission\\b|async def verify_permission\\b", l):
        vs=i
        break
else:
    vs=None
if vs:
    lo=max(1,vs-20); hi=min(len(lines), vs+120)
    print("\n== verify_permission block ==")
    for j in range(lo,hi+1):
        print(f"{j}:{lines[j-1].rstrip()}")
else:
    print("verify_permission not found")
PY

echo
echo "== task_server.py: inspect start_task permission/validation logic around resolve_task_permissions/resolve_team_permissions =="
python3 - <<'PY'
import re
path="packages/ai/src/ai/modules/task/task_server.py"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
# find start_task
si=None
for i,l in enumerate(lines,1):
    if "async def start_task" in l:
        si=i; break
if si is None:
    print("start_task not found"); sys.exit(0)
lo=max(1,si-10); hi=min(len(lines), si+420)
for j in range(lo,hi+1):
    print(f"{j}:{lines[j-1].rstrip()}")
print("\n== find resolve_task_permissions usage within start_task range ==")
# locate within start_task block range
block=lines[si-1:hi]
for offset,l in enumerate(block,si):
    if "resolve_task_permissions" in l or "resolve_team_permissions" in l:
        print(f"{offset}:{l.rstrip()}")
PY

echo
echo "== task_server.py: inspect for any explicit org_id/team_id validation ==" 
rg -n "org_id\\s*!=(?:\\s*''|\\s*None)|if .*org_id|team_id.*empty|orgId" packages/ai/src/ai/modules/task/task_server.py | head -n 80

Repository: rocketride-org/rocketride-server

Length of output: 31906


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cmd_task.py: on_execute around start_task call + verify_permission definition =="
python3 - <<'PY'
import re,sys
path="packages/ai/src/ai/modules/task/commands/cmd_task.py"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()

def dump(lo,hi):
    for j in range(lo,hi+1):
        print(f"{j}:{lines[j-1].rstrip()}")

# on_execute
si=None
for i,l in enumerate(lines,1):
    if "async def on_execute" in l:
        si=i; break
if si:
    dump(max(1,si-30), min(len(lines), si+260))
else:
    print("on_execute not found")

# verify_permission
vs=None
for i,l in enumerate(lines,1):
    if re.search(r"def verify_permission\\b|async def verify_permission\\b", l):
        vs=i; break
if vs:
    dump(max(1,vs-30), min(len(lines), vs+160))
else:
    print("verify_permission not found")
PY

echo
echo "== task_server.py: start_task block + permission resolution calls =="
python3 - <<'PY'
path="packages/ai/src/ai/modules/task/task_server.py"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()

si=None
for i,l in enumerate(lines,1):
    if "async def start_task" in l:
        si=i; break
if si is None:
    print("start_task not found"); sys.exit(0)

lo=max(1,si-10)
hi=min(len(lines), si+520)
for j in range(lo,hi+1):
    print(f"{j}:{lines[j-1].rstrip()}")

print("\n== resolve_task_permissions / resolve_team_permissions usages in that region ==")
for j in range(si,hi+1):
    l=lines[j-1]
    if "resolve_task_permissions" in l or "resolve_team_permissions" in l:
        print(f"{j}:{l.rstrip()}")
PY

echo
echo "== task_server.py: search for explicit handling of empty org_id/team_id ==" 
rg -n "org_id\\s*(==|!=|is)\\s*['\"]{0,1}|orgId|control\\.orgId|team_id\\s*(==|!=|is)\\s*['\"]{0,1}|control\\.teamId" packages/ai/src/ai/modules/task/task_server.py

Repository: rocketride-org/rocketride-server

Length of output: 39205


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate verify_permission implementation(s) in cmd_task.py (or its bases) =="
rg -n --hidden --no-ignore -S "def verify_permission\\b|async def verify_permission\\b" packages/ai/src/ai/modules/task/commands/cmd_task.py packages/ai/src/ai/modules/task | head -n 50

echo
echo "== show any verify_permission calls in cmd_task.py =="
rg -n "verify_permission\\(" packages/ai/src/ai/modules/task/commands/cmd_task.py | head -n 50

echo
echo "== find where control.teamId/control.orgId are used in task_server.py =="
rg -n "control\\.(teamId|orgId)\\b" packages/ai/src/ai/modules/task/task_server.py

echo
echo "== show env/billing/permission related code around those uses (task_server.py) =="
rg -n -C4 "defaultTeam=|orgId|teamId|resolve_team_permissions|resolve_task_permissions|merged_env|billing" packages/ai/src/ai/modules/task/task_server.py | head -n 120

echo
echo "== inspect Task.start_task for teamId/orgId usage =="
fd -i 'task_engine.py$' packages/ai/src/ai/modules/task | head -n 20
fd -i 'task_*.py$' packages/ai/src/ai/modules/task | head -n 50
rg -n --hidden --no-ignore -S "async def start_task|start_task\\(" packages/ai/src/ai/modules/task | head -n 80

echo
echo "== search Task engine code for orgId/teamId usage =="
rg -n --hidden --no-ignore -S "\\borgId\\b|\\bteamId\\b|control\\.orgId|control\\.teamId" packages/ai/src/ai/modules/task | head -n 200

Repository: rocketride-org/rocketride-server

Length of output: 9407


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== task_conn.py: inspect verify_permission() implementation =="
python3 - <<'PY'
path="packages/ai/src/ai/modules/task/commands/../task_conn.py"
import os
print(path)
PY
fd -i 'task_conn.py$' packages/ai/src/ai/modules/task -o 1>/dev/null || true
python3 - <<'PY'
path="packages/ai/src/ai/modules/task/task_conn.py"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
# find verify_permission method
si=None
for i,l in enumerate(lines,1):
    if 'def verify_permission' in l:
        si=i; break
print(f"verify_permission at {si}")
lo=max(1,si-40); hi=min(len(lines), si+80)
for j in range(lo,hi+1):
    print(f"{j}:{lines[j-1].rstrip()}")
PY

echo
echo "== task_engine.py: inspect where team_id/org_id passed around (around 1601) =="
python3 - <<'PY'
path="packages/ai/src/ai/modules/task/task_engine.py"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
# print around first occurrence of team_id=...org_id=... block
for idx,l in enumerate(lines):
    if 'team_id=getattr' in l:
        si=idx+1
        break
else:
    si=None
print("block at", si)
if si:
    lo=max(1,si-60); hi=min(len(lines), si+120)
    for j in range(lo,hi+1):
        print(f"{j}:{lines[j-1].rstrip()}")
PY

echo
echo "== task_engine.py: search for resolve_task_permissions / resolve_team_permissions usage =="
rg -n "resolve_(task|team)_permissions\\(" packages/ai/src/ai/modules/task/task_engine.py

echo
echo "== task_engine.py: search for control.orgId/teamId usage beyond the start RPC =="
rg -n "orgId|teamId|org_id|team_id" packages/ai/src/ai/modules/task/task_engine.py | head -n 80

echo
echo "== task_engine.py: inspect billing or env resolution areas (billing/plan/resolver) =="
rg -n "billing|invoice|charge|payment|org_id|team_id|orgId|teamId" packages/ai/src/ai/modules/task/task_engine.py | head -n 120

Repository: rocketride-org/rocketride-server

Length of output: 14171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== task_conn.py: inspect verify_permission() implementation =="
python3 - <<'PY'
path="packages/ai/src/ai/modules/task/task_conn.py"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
si=None
for i,l in enumerate(lines,1):
    if 'def verify_permission' in l:
        si=i; break
print("verify_permission at line", si)
lo=max(1,si-60); hi=min(len(lines), si+120)
for j in range(lo,hi+1):
    print(f"{j}:{lines[j-1].rstrip()}")
PY

echo
echo "== task_engine.py: inspect where team_id/org_id from control are used (around 1601) =="
python3 - <<'PY'
path="packages/ai/src/ai/modules/task/task_engine.py"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
si=None
for i,l in enumerate(lines,1):
    if 'team_id=getattr' in l and 'org_id=getattr' in lines[i]:
        pass
for i,l in enumerate(lines,1):
    if 'team_id=getattr(_control, ' in l and 'org_id=getattr(_control' in l:
        si=i; break
# fallback: just find first team_id=getattr(_control
if si is None:
    for i,l in enumerate(lines,1):
        if "team_id=getattr(_control" in l:
            si=i; break
print("team/org getattr block at", si)
if si:
    lo=max(1,si-80); hi=min(len(lines), si+140)
    for j in range(lo,hi+1):
        print(f"{j}:{lines[j-1].rstrip()}")
PY

echo
echo "== task_engine.py: search permission checks (resolve_*_permissions) =="
rg -n "resolve_(task|team)_permissions\\(" packages/ai/src/ai/modules/task/task_engine.py || true

echo
echo "== task_engine.py: search for orgId/teamId/org_id/team_id usage (head) =="
rg -n "\\b(orgId|teamId|org_id|team_id)\\b" packages/ai/src/ai/modules/task/task_engine.py | head -n 120

Repository: rocketride-org/rocketride-server

Length of output: 17733


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== find definition of get_merged_env =="
rg -n "def get_merged_env\\b|async def get_merged_env\\b|get_merged_env\\(" packages | head -n 50

echo
echo "== show get_merged_env implementation (first match) =="
python3 - <<'PY'
import subprocess,shlex,sys,os,glob
import re
from pathlib import Path

# find candidate files containing "get_merged_env"
import subprocess
res=subprocess.check_output(["bash","-lc","rg -n \"get_merged_env\\(\" packages | head -n 20"]).decode()
print(res)

PY

# pick one likely file by searching for "get_merged_env" in account store modules
fd -i '*' packages | head -n 1 >/dev/null 2>&1 || true

echo
echo "== list files likely containing get_merged_env =="
fd -i 'account*.py$' packages | head -n 50
fd -i '*/store*.py$' packages | head -n 50
fd -i 'deployment_store*.py$' packages | head -n 50

echo
echo "== show exact get_merged_env definition blocks =="
python3 - <<'PY'
import subprocess, re, sys
# get file:line matches
out=subprocess.check_output(["bash","-lc","rg -n \"(async\\s+def|def)\\s+get_merged_env\\b\" packages"]).decode().splitlines()
print("matches:", len(out))
for line in out[:20]:
    print(line)
PY

Repository: rocketride-org/rocketride-server

Length of output: 1812


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== packages/ai/src/ai/account/base.py: show get_merged_env() implementation =="
python3 - <<'PY'
path="packages/ai/src/ai/account/base.py"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()

# locate function
si=None
for i,l in enumerate(lines,1):
    if "async def get_merged_env" in l:
        si=i; break
print("get_merged_env at", si)

lo=max(1,si-40); hi=min(len(lines), si+220)
for j in range(lo,hi+1):
    print(f"{j}:{lines[j-1].rstrip()}")
PY

echo
echo "== find where get_merged_env references org_id/team_id validation =="
rg -n -C2 "get_merged_env\\b|org_id|team_id" packages/ai/src/ai/account/base.py | head -n 120

echo
echo "== check for helper functions referenced from get_merged_env (look for called names) =="
python3 - <<'PY'
import ast
path="packages/ai/src/ai/account/base.py"
src=open(path,'r',encoding='utf-8').read()
mod=ast.parse(src)
target=None
for node in mod.body:
    if isinstance(node, ast.ClassDef):
        for fn in node.body:
            if isinstance(fn, ast.AsyncFunctionDef) and fn.name=="get_merged_env":
                target=fn
                break
        if target: break
if not target:
    print("not found"); raise SystemExit(0)

# gather names of function calls inside get_merged_env
class CallVisitor(ast.NodeVisitor):
    def __init__(self): self.calls=set()
    def visit_Call(self,node):
        # record func name if simple
        if isinstance(node.func, ast.Attribute):
            self.calls.add(node.func.attr)
        elif isinstance(node.func, ast.Name):
            self.calls.add(node.func.id)
        self.generic_visit(node)

v=CallVisitor(); v.visit(target)
print("Calls in get_merged_env:", sorted(v.calls))
PY

echo
echo "== search for those helper calls definitions/usages (best-effort) =="
# just show any likely helper definitions
for name in account_id_from|validate|team|org; do :; done
rg -n "def .*\\b(merge|validate|team|org).*\\b|get_merged_env\\b" packages/ai/src/ai/account/base.py | head -n 200

Repository: rocketride-org/rocketride-server

Length of output: 14331


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== find overrides of get_merged_env() (non-base implementations) =="
rg -n "get_merged_env\\b" packages/ai/src/ai/account | head -n 200

echo
echo "== show any account_saas/base subclasses that override get_merged_env =="
rg -n "class .*Account|def get_merged_env\\b|async def get_merged_env\\b" packages/ai/src/ai/account | head -n 200

echo
echo "== find where apply_debit/apply_credit are called from task execution =="
rg -n "apply_debit\\(|apply_credit\\(" packages/ai/src/ai/modules/task | head -n 200
rg -n "apply_debit\\(|apply_credit\\(" packages/ai/src/ai | head -n 200

echo
echo "== locate TaskMetrics class and see how it uses team_id/org_id =="
rg -n "class TaskMetrics\\b" packages/ai/src/ai/modules/task/task_engine.py
python3 - <<'PY'
path="packages/ai/src/ai/modules/task/task_engine.py"
with open(path,'r',encoding='utf-8') as f: lines=f.readlines()
si=None
for i,l in enumerate(lines,1):
    if l.startswith("class TaskMetrics"):
        si=i; break
print("TaskMetrics at", si)
lo=max(1,si-40); hi=min(len(lines), si+220)
for j in range(lo,hi+1):
    print(f"{j}:{lines[j-1].rstrip()}")
PY

echo
echo "== search task_engine.py for orgId/teamId/control.orgId/control.teamId usage =="
rg -n "orgId|teamId|control\\.orgId|control\\.teamId|org_id|getattr\\(_control, 'orgId'\\)|team_id|getattr\\(_control, 'teamId'\\)" packages/ai/src/ai/modules/task/task_engine.py | head -n 200

echo
echo "== inspect tests referencing execute()/teamId/orgId behavior =="
rg -n "teamId|orgId" packages/ai/tests/ai/modules/task | head -n 200

Repository: rocketride-org/rocketride-server

Length of output: 2067


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== task_metrics.py: inspect around apply_debit() call (team_id/org_id usage) =="
python3 - <<'PY'
path="packages/ai/src/ai/modules/task/task_metrics.py"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
# find apply_debit call region
for i,l in enumerate(lines,1):
    if "apply_debit(" in l:
        si=i; break
else:
    si=None
print("first apply_debit call at", si)
lo=max(1,si-80); hi=min(len(lines), si+120)
for j in range(lo,hi+1):
    print(f"{j}:{lines[j-1].rstrip()}")
PY

echo
echo "== task_metrics.py: inspect where TaskMetrics fields set/used =="
rg -n "class TaskMetrics\\b|team_id|org_id|apply_debit\\b" packages/ai/src/ai/modules/task/task_metrics.py | head -n 120

echo
echo "== tests: cmd_task teamId/orgId behavior in test_cmd_task.py =="
rg -n "teamId|orgId|defaultTeam|organization\\.teams" packages/ai/tests/ai/modules/task/commands/test_cmd_task.py | head -n 200

echo
echo "== tests: show execute() tests around teamId/orgId =="
python3 - <<'PY'
import re
path="packages/ai/tests/ai/modules/task/commands/test_cmd_task.py"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
# find test names mentioning execute or teamId
idxs=[]
for i,l in enumerate(lines,1):
    if re.search(r"test_.*(execute|launch)|execute\\b|teamId|orgId", l):
        idxs.append(i)
print("candidate lines:", idxs[:40])
# print around each candidate region if there are many; limit
for i in idxs[:6]:
    lo=max(1,i-30); hi=min(len(lines), i+80)
    print("\n--- context around", i,"---")
    for j in range(lo,hi+1):
        print(f"{j}:{lines[j-1].rstrip()}")
PY

Repository: rocketride-org/rocketride-server

Length of output: 41068


Enforce validation of client-supplied teamId before starting a task

  • TaskCommands.on_execute() forwards team_id = args.get('teamId') or defaultTeam to TaskServer.start_task(...) even when org_id stays '' (no matching entry in self._account_info.organization['teams']).
  • verify_permission('task.control') checks permissions against self._account_info.defaultTeam, not the client-supplied teamId, and TaskServer.start_task() does not validate teamId/orgId.
  • Billing can be bypassed: TaskMetrics._report_to_billing_system() returns early when org_id is falsy (if not self.org_id: return), while task execution still proceeds.

Update on_execute()/start_task() to reject (or normalize) unknown teamId and resolve/validate permissions against the resolved teamId before launching; add a unit test for an unknown teamId.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/ai/modules/task/commands/cmd_task.py` around lines 154 - 161,
TaskCommands.on_execute currently forwards a client-supplied teamId to
TaskServer.start_task even when no matching org/team is found, allowing tasks to
run with an empty org_id and bypass billing; update on_execute to resolve the
client teamId against self._account_info.organization['teams'] (use the same
lookup that sets org_id) and if no match is found either reject the request with
an error or normalize to the user's defaultTeam before calling
TaskServer.start_task. Also update permission checks to call
verify_permission('task.control') against the resolved teamId/org (not
self._account_info.defaultTeam), and add defensive validation in
TaskServer.start_task to refuse unknown/empty teamId/org_id (return error rather
than starting the task). Finally add a unit test that calls on_execute with an
unknown teamId and asserts the task is rejected and billing
(TaskMetrics._report_to_billing_system) is not skipped incorrectly.

Rod-Christensen and others added 8 commits June 12, 2026 15:32
…Metrics to ledger

AccountBase gets four new billing methods as no-op defaults (OSS):
apply_credit, apply_debit, get_credit_balance, get_transactions.
SaaS overrides them with real ledger operations.

TaskMetrics._report_to_billing_system() is no longer a stub — it now
calls account.apply_debit() per resource with cumulative token totals
every CONST_BILLING_REPORT_INTERVAL seconds and on task completion.
Each resource gets its own UPSERT row keyed on task:{task_id}:{resource}.
OSS mode is a no-op via AccountBase polymorphism.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ul (#billing-5)

Billing Dashboard (new):
- BillingDashboard.tsx: 4 admin insight sections — spending velocity
  with top-up buttons (primary when <7 days remaining, secondary
  otherwise), usage leaderboard (by user/team toggle), paginated
  transaction log with user name resolution, and active tasks view.
- Wired into BillingPanel for org admins on both VSCode and shell-ui.

Plans served from local DB instead of Stripe API:
- AccountProvider.ts: fetchBillingData no longer calls listCreditPacks()
  or Stripe. All plan/price data comes from app_prices table via the
  existing getProductPrices() call. Billing page no longer breaks when
  Stripe is unreachable.
- AccountProvider.ts: sendInitialData() now fetches ALL account data
  upfront (org, members, teams, keys, billing) so tab badges and
  counts are visible immediately without clicking into each section.
- shell-ui AccountPage: combined loadBilling + loadDashboard into a
  single fetch to fix useEffect race condition where dashboard data
  would get stuck in loading state.

SDK type overhaul:
- CheckoutPlan type replaced with DB-native shape: id, appId,
  stripePriceId, nickname, amountCents, currency, interval, metadata,
  isActive, createdAt. Matches _price_to_dict() output directly.
- StripePlan renamed to AppPrice (StripePlan kept as deprecated alias).
- PlanPicker.tsx rewritten to use DB field names (nickname, stripePriceId,
  amountCents, metadata.action, metadata.order, metadata.description,
  metadata.displayAmount, metadata.kind).
- CheckoutModal.tsx updated for new field names.
- New types: LedgerTransaction, TransactionsResult, UsageRollup, TopupPlan.
- New BillingApi methods: getTransactions(), getUsageByUser(), getUsageByTeam().
- Python SDK: AppPrice type, updated BillingApi with matching methods.

Plan picker improvements:
- Filters out kind='topup' plans from subscription modal — top-ups
  only appear on the spending velocity pane.
- Cancel button changed from danger to secondary style.

CreditsPanel cleanup:
- Removed credit packs grid and "No credit packs configured" message.
  Credit packs concept is eliminated; replaced by top-up plans in
  app_prices with metadata.kind='topup'.

TaskMetrics improvements:
- billing_run_id: unique UUID per pipeline run for idempotency keys,
  preventing collisions when the same pipeline runs multiple times.
- pipeline_name and source_name passed to context for human-readable
  billing audit trail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ing-6)

Add live billing event infrastructure so the billing tab refreshes
automatically when credits are debited or credited, without manual reload.

Server-side:
- Add EVENT_TYPE.BILLING (1 << 8) to both Python and TypeScript SDKs
  and include it in the ALL convenience bitmask
- Extend send_server_event() and broadcast_server_event() with org_id
  parameter for org-scoped delivery — billing events only reach
  connections in the affected org
- Add org filtering (step 3) to the send_server_event filtering chain:
  wildcard check -> bitmask match -> org scope -> user scope

Client-side (cloud + VSCode):
- AccountPage: load all data upfront on connect instead of per-section;
  subscribe to billing monitor ('*', ['billing']) when billing tab is
  active; listen for 'apaext_billing_update' via ConnectionManager and
  re-fetch billing data on receipt; unsubscribe on tab change
- AccountProvider (VSCode): subscribe to billing monitor on panel open;
  re-fetch billing data on 'apaext_billing_update' events

UI polish:
- TabPanel: add thin scrollbar styling (scrollbarWidth + scrollbarColor)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…idation (#billing-6)

Add TopUpModal for purchasing credit packs via card on file. The modal
reuses PlanPicker from the checkout module filtered to kind='topup'.

SpendingVelocity card now shows a low-capacity warning message with a
primary "Add more capacity" button when < 7 days of credits remain.
Both capacity buttons (CreditsPanel + SpendingVelocity) open the same
TopUpModal via a unified onAddCapacity callback.

Consolidate billing UI into the Account page's Billing tab — remove
the standalone BillingView/BillingPage that duplicated the same data.

SDK changes: add purchase_topup to both Python and TypeScript clients.

Shell-ui and VSCode hosts wired to pass top-up plans, purchase callback,
and onAddCapacity through to BillingDashboard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…billing-6)

Add UpgradeModal component and wire Change Plan flow end-to-end:
- UpgradeModal in shared-ui shows available plans with current highlighted
- BillingPanel adds "Change Plan" button on active subscription rows
- AccountPage (shell-ui) and AccountProvider (vscode) handle the
  billing:upgrade message and call the SDK
- Python and TypeScript SDKs gain upgradeSubscription() method that
  calls rrext_account_billing subcommand='upgrade'
- depends.py: remove premature updateProgress() call that logged
  "Installing requirements.txt" via monitorStatus before dry-run
  determined no packages needed installing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ce hash, cprofile permissions (#billing-6)

Shell.tsx — register permission-gated apps as MF remotes after auth:

  The pre-auth server probe only returns public apps (owner_type='public'),
  so apps with requiredPermissions (e.g. ["sys.admin"]) were never included
  in the probe's registerRemotes() call. After authentication, the
  ConnectResult.apps payload includes the user's full entitled app set
  with complete manifest data (entry URL, moduleId, etc.), but Shell.tsx
  was only overlaying desktop metadata (appStatus, onDesktop) onto the
  existing probe entries — it never added new apps from the ConnectResult.

  This meant permission-gated apps appeared on the desktop tile grid
  (because the catalog list and desktop_add worked correctly) but clicking
  them did nothing: the MF remote was never registered, so loadRemote()
  had no entry to resolve, and no network request was ever made.

  The fix detects ConnectResult apps that are absent from the probe set,
  calls registerAndMapApps() to register their MF remotes, and appends
  them to the merged app list passed to WorkspaceProvider. This allows
  any app — regardless of requiredPermissions or authenticated flags —
  to be launched after auth as long as the server includes it in the
  ConnectResult.

deps-tasks.js — include pnpm-workspace.yaml in dependency hash:

  The builder's auto-install detection hashes all package.json dependency
  fields to decide whether pnpm install is needed. However, adding or
  removing entries in pnpm-workspace.yaml (which controls which packages
  are part of the pnpm workspace) did NOT trigger auto-install because
  the yaml file was not included in the hash.

  This caused build failures when a new app was added to the workspace:
  the builder skipped pnpm install, and the new package's dependencies
  were unresolved, resulting in npm 404 errors (e.g. "rsbuild@* not found")
  because the fallback npm resolver couldn't find workspace-linked packages.

  The fix hashes pnpm-workspace.yaml alongside the package.json files so
  any workspace topology change triggers pnpm install automatically.

cmd_cprofile.py — add task.control permission checks:

  All five cProfile DAP commands (start, stop, status, report, report_tree)
  were missing permission verification. Any authenticated user could start
  or stop CPU profiling sessions on the server or on pipeline tasks. Added
  verify_permission('task.control') to each handler to require the same
  permission level as other task control operations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Users may now belong to exactly one organization instead of many.
The multi-org array was never used in practice — every frontend accessor
already read organizations[0] — and the M:N model added unnecessary
complexity to permission resolution, billing scoping, and invite logic.

Backend (Python):
- AccountInfo.organizations: list[OrgInfo] -> organization: Optional[OrgInfo]
- resolve_task_permissions / resolve_team_permissions simplified from
  org-list iteration to single-org lookup
- OSS provider emits organization={...} instead of organizations=[{...}]
- _primary_org_id() in account_handler, app_handler simplified
- cmd_task, cmd_debug, cmd_monitor, cmd_misc all updated to read
  .organization instead of iterating .organizations
- connect_routes, stripe_webhooks, marketplace routes updated
- base.py docstring updated

TypeScript SDK:
- ConnectResult.organizations: OrgInfo[] -> organization: OrgInfo | null
- RocketRideClient.getOrgId() reads .organization?.id

Frontend (shell-ui, shared-ui, vscode):
- All organizations?.[0] accessors replaced with organization?
- ProfilePanel renders single org instead of iterating org list
- AccountView, EnvironmentPage, Shell, AccountPage simplified
- All VSCode providers (Account, Environment, Project, Settings,
  Sidebar, ConnectionMessageHandler) updated

Python SDK:
- ConnectResult.organizations -> organization
- AccountProfile.organizations -> organization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nc, resend invite (#billing-6)

Multi-org with active org switcher:
- Reverted single-org unique constraint; users can belong to multiple orgs
  but only one is active at a time via users.default_org_id
- ConnectResult.organization stays as single object (the active org)
- get_authentication_result scopes to default_org_id, falls back to first
  active membership and auto-sets if stale
- get_user_profile returns memberships[] (all orgs) + defaultOrgId for
  the org switcher UI
- New set_default_org service method + handler: validates membership,
  updates default_org_id, resets default_team_id to first team in new org
- provision_new_user sets default_org_id alongside default_team_id
- ConnectResult type: added optional memberships/defaultOrgId fields

Org switcher UI (ProfilePanel):
- Shows all org memberships; active org at full opacity with checkmark
- Inactive orgs grayed out (opacity 0.45) with "Switch to" button
- Teams only expanded under the active org
- Wired through AccountView, AccountPage, AccountWebview, AccountProvider

Invite with team assignment:
- InviteMemberParams: added optional teamAssignments array
- Invite modal: team checkboxes with per-team PermGrid for permissions
- Selected permissions shown as PermPill badges (outlined style matching
  Teams panel)
- Backend creates TeamMember rows in same transaction as the invite

Resend invite:
- New resendInvite SDK method + handler subcommand
- MembersPanel: "Resend" button on pending members with loading/sent
  feedback states (Sending... -> Sent! for 3s)
- Zitadel: resend_zitadel_init_email uses v1 _resend_initialization endpoint

Zitadel cleanup on invite cancel:
- delete_zitadel_user function: DELETE /management/v1/users/{id}, ignores 404
- remove_org_member: when cancelling a pending invite for an orphaned user
  (zero remaining memberships), deletes Zitadel account + local User row
- Only deletes locally if Zitadel delete succeeds (prevents orphaned state)

Other fixes:
- remove_org_member no longer deletes user on last org removal; clears
  default_org_id instead, user becomes org-less
- ensure_org_membership: auto-creates personal org on login for org-less
  users (e.g. after being removed from all orgs)
- Avatar colors: replaced bright palette with muted professional tones

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…g-6)

The upgrade/downgrade handler reads message.newPriceId but the type
interface was missing the field, causing a TS2551 compile error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant