Skip to content

feat(mobile): host-owned workspace data, single-stack nav, settings, new-chat widget - #5534

Merged
saddlepaddle merged 30 commits into
mainfrom
mobile-app-data
Jul 9, 2026
Merged

saddlepaddle merged 30 commits into
mainfrom
mobile-app-data

Conversation

@saddlepaddle

@saddlepaddle saddlepaddle commented Jul 9, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Moves the mobile app onto the host-owned workspace architecture (PR #5452) and builds out the surrounding product surface: single-stack navigation, native filter subpages, a settings first pass, and the bottom-docked new-chat widget with workspace creation.

Host-owned data layer (desktop parity)

  • Read path: useHostWorkspaces fans out workspace.list to every online host via the relay (user JWT; no host secret on device), merges results, and falls back to the Electric v2_workspaces collection for hosts that don't answer (R3 removes the fallback). 30s poll + focus/pull-to-refresh; no event bus.
  • Host-service client: plain tRPC httpLink + superjson against ${relay}/hosts/${orgId}:${machineId}/trpc, typed by a mobile-declared procedure contract (host-service's Node-flavored type graph can't compile under Expo TS; host now also ships compiled router declarations for future convergence).
  • Writes via the owning host: rename → workspace.update; delete → workspaceCleanup.destroy with dirty-worktree/teardown force-retry alerts and Delete hidden for main workspaces. Fixes the previous cloud-delete path that orphaned worktrees and left the authoritative host row alive.
  • Live diff stats: viewport-bounded git.getStatus (settled-visible rows only, capped) aggregated exactly like desktop's useDiffStats; rendered from the whole react-query cache so scroll-backs don't flicker. Replaces PR cumulative +/− on rows.
  • PR matching parity: repo-scoped ${repositoryId}::${headBranch} with desktop's state derivation (mergedAt → draft → closed → open) and tiebreaks.

Navigation & home

  • Single-stack nav (tab bar dropped), integrated header search, org switcher trimmed to orgs → separator → settings → log out.
  • Workspace filters as a formSheet route with native pushed subpages (Project / Host / Sort) backed by a persisted zustand store; generic filter toolbar icon.
  • Host status icons from merged-item reachability; worktree missing hint.

New-chat widget (workspace creation)

  • Bottom-docked prompt input: target (project+host) picker, branch picker, model selector, photo context carousel (expo-media-library + image-manipulator), creates a workspace via host workspaces.create and launches an agent.
  • Branch picker restyled: full-height sheet, X + centered title, pill search, Default/Recents sections with hairline rows, backed by host workspaceCreation.searchBranches.

Settings first pass

  • Native screens in the authenticated stack: root (account row, organization, hosts, billing, destructive log out w/ confirm), Account, Organization (+members from synced collections), Hosts (status/last-seen/workspace counts), Billing (read-only plan from a new org-scoped subscriptions Electric collection; "manage from desktop").
  • Shared row primitives promoted to screens/(authenticated)/components (ListRow family) and reused by filter + settings.

Native/dev plumbing

  • EXPO_PUBLIC_RELAY_URL env (env.ts, setup script), zustand + date-fns deps, alert-prompt local Swift module.
  • Expo prebuilt-module alignment: expo-image-manipulator@56.0.17 and expo-media-library@56.0.6 pinned to the expo-modules-core 56.0.15 batch (prebuilts released after core's date crash dyld at launch with missing Record.from symbol).

Test plan

  • biome check + mobile tsc --noEmit green
  • Simulator: fan-out serves live host rows through the local relay (workspace.list 200s), cloud fallback renders for silent hosts, diff stats fetch only for settled-visible rows
  • Dirty-worktree delete → CONFLICT alert → force retry; rename via owning host
  • Settings walkthrough incl. members list, host last-seen, Pro plan from synced subscription
  • Dev client rebuilt with aligned native modules; clean boot (no worklet/dyld errors)

Open in Stage

Summary by cubic

Moves mobile to host-owned workspaces with a single-host home view and single-stack navigation. Adds a docked new‑chat composer, settings, a typed host-service client over the relay, and removes the Electric fallback.

  • New Features

    • Host-owned data: single-host workspace.list over the relay with 30s poll and focus/refresh; no Electric fallback.
    • Host-service client: tRPC over the relay using the user JWT; types from compiled router declarations.
    • Writes via the host: rename with workspace.update; delete with workspaceCleanup.destroy and conflict/teardown handling; Delete hidden for main.
    • Live diff stats: viewport-bounded git.getStatus with desktop-style aggregation; PR badges match desktop logic.
    • Navigation and home: single-stack, header search; filters as a formSheet with native subpages and a persisted zustand store; single-host view with an offline placeholder and Switch Host CTA.
    • New‑chat widget: docked composer with project/host target, branch/model pickers as expo-router formSheets, photos/screenshots context, and uploads to the host; calls workspaces.create then launches an agent; native close buttons on all sheets.
    • Settings: Account, Organization (members), Hosts (status/last‑seen), Billing (read-only plan from subscriptions).
    • Workspace host lookup: useWorkspaceHost resolves the owning host by querying online hosts and ignores disabled queries while resolving.
  • Migration

    • Add EXPO_PUBLIC_RELAY_URL to the mobile .env (setup script writes it).
    • Rebuild the dev client after native module alignment: pin expo-modules-core@56.0.15, expo-image-manipulator@56.0.17, and expo-media-library@56.0.6.
    • iOS: include local module @superset/alert-prompt; run a fresh prebuild/Pods install if needed.
    • App config: add the expo-media-library plugin with a photos permission prompt (alongside expo-image-picker).

Written for commit 61d1b83. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a new authenticated home/workspaces experience with filtering (Project/Host/Sort), offline host messaging, and an improved “new chat” flow with project/host/branch/model pickers.
    • Added chat prompt attachments (photos, screenshots, files, camera) and a native text prompt dialog.
    • Added enhanced workspace actions (rename, delete, copy/share) and diff-stat visibility for workspaces.
  • Settings

    • Added settings screens for Account, Organization, Hosts, and Billing, including subscription-aware billing status.
  • UI & Navigation

    • Updated navigation by removing the previous tab-based Search/Tasks/More routes in favor of the new home stack and settings routes.

@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review. (142 files found, 100 file limit)

Bypass the limit by tagging @greptile-apps to review.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Too many files!

This PR contains 161 files, which is 11 over the limit of 150.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c406e8be-c6e8-4797-800c-2a396862e362

📥 Commits

Reviewing files that changed from the base of the PR and between 277ff00 and 61d1b83.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (161)
  • .gitignore
  • .superset/lib/setup/steps.sh
  • apps/mobile/app.config.ts
  • apps/mobile/app/(authenticated)/(home)/_layout.tsx
  • apps/mobile/app/(authenticated)/(home)/filter/_layout.tsx
  • apps/mobile/app/(authenticated)/(home)/filter/host.tsx
  • apps/mobile/app/(authenticated)/(home)/filter/index.tsx
  • apps/mobile/app/(authenticated)/(home)/filter/project.tsx
  • apps/mobile/app/(authenticated)/(home)/filter/sort.tsx
  • apps/mobile/app/(authenticated)/(home)/index.tsx
  • apps/mobile/app/(authenticated)/(home)/new-chat/_layout.tsx
  • apps/mobile/app/(authenticated)/(home)/new-chat/branch.tsx
  • apps/mobile/app/(authenticated)/(home)/new-chat/model.tsx
  • apps/mobile/app/(authenticated)/(home)/new-chat/project.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(home)/_layout.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(home)/index.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(more)/_layout.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(more)/index.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(more)/settings/index.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(search)/_layout.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(search)/index.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(tasks)/[id].tsx
  • apps/mobile/app/(authenticated)/(tabs)/(tasks)/_layout.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(tasks)/index.tsx
  • apps/mobile/app/(authenticated)/(tabs)/_layout.tsx
  • apps/mobile/app/(authenticated)/_layout.tsx
  • apps/mobile/app/(authenticated)/settings/account.tsx
  • apps/mobile/app/(authenticated)/settings/billing.tsx
  • apps/mobile/app/(authenticated)/settings/hosts.tsx
  • apps/mobile/app/(authenticated)/settings/index.tsx
  • apps/mobile/app/(authenticated)/settings/organization.tsx
  • apps/mobile/hooks/useHostWorkspaces/index.ts
  • apps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.ts
  • apps/mobile/hooks/useWorkspaceHost/index.ts
  • apps/mobile/hooks/useWorkspaceHost/useWorkspaceHost.ts
  • apps/mobile/lib/collections/collections.ts
  • apps/mobile/lib/host-service/client.ts
  • apps/mobile/lib/host-service/errors.ts
  • apps/mobile/metro.config.js
  • apps/mobile/modules/alert-prompt/expo-module.config.json
  • apps/mobile/modules/alert-prompt/ios/AlertPrompt.podspec
  • apps/mobile/modules/alert-prompt/ios/AlertPromptModule.swift
  • apps/mobile/modules/alert-prompt/package.json
  • apps/mobile/modules/alert-prompt/src/index.ts
  • apps/mobile/package.json
  • apps/mobile/screens/(authenticated)/(home)/components/SheetCloseButton/SheetCloseButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/components/SheetCloseButton/index.ts
  • apps/mobile/screens/(authenticated)/(home)/filter/FilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/components/ProjectAvatar/ProjectAvatar.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/components/ProjectAvatar/index.ts
  • apps/mobile/screens/(authenticated)/(home)/filter/host/HostFilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/host/index.ts
  • apps/mobile/screens/(authenticated)/(home)/filter/index.ts
  • apps/mobile/screens/(authenticated)/(home)/filter/project/ProjectFilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/project/index.ts
  • apps/mobile/screens/(authenticated)/(home)/filter/sort/SortFilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/sort/index.ts
  • apps/mobile/screens/(authenticated)/(home)/hooks/useSelectedHost/index.ts
  • apps/mobile/screens/(authenticated)/(home)/hooks/useSelectedHost/useSelectedHost.ts
  • apps/mobile/screens/(authenticated)/(home)/new-chat/branch/BranchPickerScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/new-chat/branch/index.ts
  • apps/mobile/screens/(authenticated)/(home)/new-chat/model/ModelPickerScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/new-chat/model/index.ts
  • apps/mobile/screens/(authenticated)/(home)/new-chat/project/ProjectPickerScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/new-chat/project/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/HostOfflineView/HostOfflineView.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/HostOfflineView/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/ContextSheet.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/PhotoCarousel/PhotoCarousel.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/PhotoCarousel/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/ScreenshotGrid.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useCreateChatWorkspace/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useCreateChatWorkspace/useCreateChatWorkspace.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useNewChatTargets/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useNewChatTargets/useNewChatTargets.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/stores/newChatPreferencesStore/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/stores/newChatPreferencesStore/newChatPreferencesStore.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/WorkspaceRow.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/components/WorkspaceRowMenu/WorkspaceRowMenu.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/components/WorkspaceRowMenu/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/stores/workspacesFilterStore/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/stores/workspacesFilterStore/workspacesFilterStore.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/utils/hslToHex/hslToHex.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/utils/hslToHex/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/WorkspacesScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceActionsSheet/WorkspaceActionsSheet.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceActionsSheet/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceFilterSheet/WorkspaceFilterSheet.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceFilterSheet/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceRow/WorkspaceRow.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceRow/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(more)/MoreMenuScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(more)/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(more)/settings/SettingsScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(search)/SearchScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(search)/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(tasks)/[id]/TaskDetailScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(tasks)/[id]/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(tasks)/tasks/TasksScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(tasks)/tasks/index.ts
  • apps/mobile/screens/(authenticated)/components/HostStatusDot/HostStatusDot.tsx
  • apps/mobile/screens/(authenticated)/components/HostStatusDot/index.ts
  • apps/mobile/screens/(authenticated)/components/ListRow/ListRow.tsx
  • apps/mobile/screens/(authenticated)/components/ListRow/index.ts
  • apps/mobile/screens/(authenticated)/components/ListRowCheck/ListRowCheck.tsx
  • apps/mobile/screens/(authenticated)/components/ListRowCheck/index.ts
  • apps/mobile/screens/(authenticated)/components/ListRowValue/ListRowValue.tsx
  • apps/mobile/screens/(authenticated)/components/ListRowValue/index.ts
  • apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.tsx
  • apps/mobile/screens/(authenticated)/components/OrgDropdown/index.ts
  • apps/mobile/screens/(authenticated)/components/OrganizationAvatar/OrganizationAvatar.tsx
  • apps/mobile/screens/(authenticated)/components/OrganizationAvatar/index.ts
  • apps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.tsx
  • apps/mobile/screens/(authenticated)/components/TabBarAccessory/index.ts
  • apps/mobile/screens/(authenticated)/hooks/useOrganizations/useOrganizations.ts
  • apps/mobile/screens/(authenticated)/settings/SettingsScreen.tsx
  • apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx
  • apps/mobile/screens/(authenticated)/settings/account/index.ts
  • apps/mobile/screens/(authenticated)/settings/billing/BillingSettingsScreen.tsx
  • apps/mobile/screens/(authenticated)/settings/billing/index.ts
  • apps/mobile/screens/(authenticated)/settings/components/UserAvatar/UserAvatar.tsx
  • apps/mobile/screens/(authenticated)/settings/components/UserAvatar/index.ts
  • apps/mobile/screens/(authenticated)/settings/hosts/HostsSettingsScreen.tsx
  • apps/mobile/screens/(authenticated)/settings/hosts/index.ts
  • apps/mobile/screens/(authenticated)/settings/index.ts
  • apps/mobile/screens/(authenticated)/settings/organization/OrganizationSettingsScreen.tsx
  • apps/mobile/screens/(authenticated)/settings/organization/index.ts
  • apps/mobile/screens/(authenticated)/settings/utils/activeSubscription/activeSubscription.ts
  • apps/mobile/screens/(authenticated)/settings/utils/activeSubscription/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/[sessionId]/hooks/useChatThread/useChatThread.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/hooks/useWorkspaceTerminalAgents/useWorkspaceTerminalAgents.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/components/WorkspaceBackButton/WorkspaceBackButton.tsx
  • apps/mobile/tsconfig.json
  • packages/chat/src/server/desktop/index.ts
  • packages/host-service/docs/interim-router-types.md
  • packages/host-service/package.json
  • packages/host-service/src/trpc/index.ts
  • packages/host-service/src/trpc/router/project/handlers.ts
  • packages/host-service/src/trpc/router/workspace-creation/procedures/search-github-issues.ts
  • packages/host-service/src/trpc/router/workspace-creation/procedures/search-pull-requests.ts
  • packages/host-service/tsconfig.types.json
  • packages/port-scanner/package.json
  • packages/port-scanner/tsconfig.types.json
  • packages/pty-daemon/package.json
  • packages/pty-daemon/tsconfig.types.json
  • packages/workspace-fs/package.json
  • packages/workspace-fs/tsconfig.types.json
  • turbo.jsonc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The authenticated mobile app now routes through a home-centered shell with filter, new-chat, workspace, and settings flows. It adds host-service querying, a native alert-prompt module, workspace and settings UI, and declaration-only TypeScript build output for several packages.

Changes

Mobile app navigation and workspace feature overhaul

Layer / File(s) Summary
Navigation restructure and route wiring
apps/mobile/app/(authenticated)/_layout.tsx, apps/mobile/app/(authenticated)/(home)/_layout.tsx, apps/mobile/app/(authenticated)/(home)/index.tsx, apps/mobile/app/(authenticated)/(home)/filter/*, apps/mobile/app/(authenticated)/(home)/new-chat/*, apps/mobile/app/(authenticated)/(tabs)/* (removed), apps/mobile/app/(authenticated)/settings/*, apps/mobile/screens/(authenticated)/hooks/useOrganizations/useOrganizations.ts, apps/mobile/screens/(authenticated)/workspace/[id]/components/WorkspaceBackButton/WorkspaceBackButton.tsx
Switches authenticated navigation to the home route, adds the filter, new-chat, and settings route entries, and updates fallback navigation targets.
Filter screens and shared list UI
apps/mobile/screens/(authenticated)/(home)/filter/*, apps/mobile/screens/(authenticated)/(home)/hooks/useSelectedHost/*, apps/mobile/screens/(authenticated)/components/ListRow*, apps/mobile/screens/(authenticated)/components/HostStatusDot/*, apps/mobile/screens/(authenticated)/(home)/workspaces/stores/workspacesFilterStore/*
Adds project/host/sort filter screens, a persisted filter store, selected-host lookup, and reusable row/check/value/status components.
Host-service client, collections, and host query hooks
apps/mobile/lib/host-service/*, apps/mobile/lib/env.ts, apps/mobile/lib/collections/collections.ts, apps/mobile/hooks/useHostWorkspaces/*, .superset/lib/setup/steps.sh
Adds a cached TRPC host-service client, relay URL env support, a subscriptions collection, and hooks for selected host and host workspaces.
Alert-prompt native module and app wiring
apps/mobile/modules/alert-prompt/*, apps/mobile/metro.config.js, apps/mobile/tsconfig.json, apps/mobile/app.config.ts, apps/mobile/package.json
Adds the @superset/alert-prompt Expo module plus Metro, TypeScript, package, and Expo plugin wiring.
Workspaces screen, row menu, diff stats, and offline host view
apps/mobile/screens/(authenticated)/(home)/workspaces/*, apps/mobile/screens/(authenticated)/(home)/components/SheetCloseButton/*, apps/mobile/screens/(authenticated)/(home)/workspaces/utils/hslToHex/*
Adds the home workspaces screen with filtering/searching, PR badges, diff stats, row actions, offline-host handling, and supporting UI/helpers.
New chat workspace creation widget and pickers
apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/*, apps/mobile/app/(authenticated)/(home)/new-chat/*, apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/*
Adds the new-chat widget, branch/model/project pickers, context/photo/screenshot flows, chat creation mutation, and organization settings/logout actions.
Settings screens and organization avatar wiring
apps/mobile/screens/(authenticated)/settings/*, apps/mobile/app/(authenticated)/settings/*, apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsx
Adds Account, Organization, Hosts, and Billing settings screens, wires them into navigation, and updates the organization avatar import path.

Package TypeScript declaration build (dist-types) infrastructure

Layer / File(s) Summary
dist-types build configuration
packages/{host-service,port-scanner,pty-daemon,workspace-fs}/tsconfig.types.json, packages/{host-service,port-scanner,pty-daemon,workspace-fs}/package.json, turbo.jsonc, .gitignore, packages/host-service/src/trpc/**
Adds declaration-only configs, redirects package type exports to dist-types, adds build:types scripts and turbo wiring, and exports internal interfaces needed for declaration generation.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant NewChatWidget
  participant HostServiceClient
  participant Router

  User->>NewChatWidget: choose target, branch, model, attachments
  NewChatWidget->>HostServiceClient: searchBranches / upload attachments / workspaces.create
  HostServiceClient-->>NewChatWidget: branch data or workspace result
  NewChatWidget->>Router: navigate to chat route or workspace page
Loading

Possibly related PRs

  • superset-sh/superset#4651: Both PRs update .superset/lib/setup/steps.sh to write a relay URL into the generated .env.
  • superset-sh/superset#4731: The new mobile diff-stats hook depends on host-service git.getStatus behavior that matches this PR’s host-service query changes.
  • superset-sh/superset#5505: Both PRs adjust the mobile authenticated navigation and workspaces/home routing surface.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main mobile navigation, settings, host-owned data, and new-chat changes.
Description check ✅ Passed The description is detailed and covers the main changes plus testing, though some template sections like Related Issues and Type of Change are omitted.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mobile-app-data

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) ✅ View Branch
Vercel API (Vercel) ✅ Open Preview
Vercel Web (Vercel) ✅ Open Preview
Vercel Marketing (Vercel) ✅ Open Preview
Vercel Admin (Vercel) ✅ Open Preview
Vercel Docs (Vercel) ✅ Open Preview

Preview updates automatically with new commits

@cubic-dev-ai cubic-dev-ai 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.

12 issues found across 142 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mobile/screens/(authenticated)/(home)/filter/FilterScreen.tsx">

<violation number="1" location="apps/mobile/screens/(authenticated)/(home)/filter/FilterScreen.tsx:40">
P2: A stale persisted project filter can make the filter sheet show the first project even though the workspace list is still filtered by the missing `projectFilter` id and may appear empty. Consider only defaulting to the first project when `projectFilter` is null, or clearing invalid persisted filters when project data changes.</violation>
</file>

<file name="apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.tsx">

<violation number="1" location="apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.tsx:135">
P2: Tapping “Log out” in the org switcher now signs the user out immediately, which makes accidental taps in this sheet irreversible compared with the existing Settings logout flow. Consider reusing the same confirmation step here so logout behavior is consistent across entry points.</violation>
</file>

<file name="apps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsx">

<violation number="1" location="apps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsx:167">
P3: Pull-to-refresh failures currently disappear with no diagnostic signal because `refetchQueries` errors are caught with an empty handler. Logging a warning with context would keep the refresh best-effort while making host/relay failures observable.</violation>
</file>

<file name="apps/mobile/screens/(authenticated)/(home)/workspaces/stores/workspacesFilterStore/workspacesFilterStore.ts">

<violation number="1" location="apps/mobile/screens/(authenticated)/(home)/workspaces/stores/workspacesFilterStore/workspacesFilterStore.ts:32">
P2: Switching organizations can leave the workspace list filtered by a project/host from the previous organization, producing an empty or misleading home view. The persisted `workspaces-filter` key is global rather than scoped to `activeOrganizationId` or reset on org switch; consider scoping these filter values per organization or clearing them when the active org changes.</violation>
</file>

<file name="apps/mobile/screens/(authenticated)/settings/SettingsScreen.tsx">

<violation number="1" location="apps/mobile/screens/(authenticated)/settings/SettingsScreen.tsx:24">
P3: Settings and Billing now duplicate the active-subscription query/selection flow, so future billing status changes need matching edits in both screens. Consider extracting a small `useActiveSubscription` hook shared by both screens.

(Based on your team's feedback about avoiding duplicated business logic across components.) [FEEDBACK_USED]</violation>
</file>

<file name="apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useCreateChatWorkspace/useCreateChatWorkspace.ts">

<violation number="1" location="apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useCreateChatWorkspace/useCreateChatWorkspace.ts:40">
P2: Failed workspace creation can leave host-local attachment files behind, because attachments are uploaded before `workspaces.create` and no cleanup runs if create rejects. Consider tracking uploaded IDs and deleting them in a catch path before rethrowing the original error.</violation>
</file>

<file name="apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/stores/newChatPreferencesStore/newChatPreferencesStore.ts">

<violation number="1" location="apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/stores/newChatPreferencesStore/newChatPreferencesStore.ts:27">
P2: After a model catalog change, mobile can keep an old persisted `modelId`, render a blank selected model, and send an unsupported `agents[0].model` when creating a chat workspace. Consider validating the rehydrated value against `SUPERSET_CHAT_MODELS` and falling back to `DEFAULT_MODEL_ID`, matching the desktop preference behavior.</violation>
</file>

<file name="apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx">

<violation number="1" location="apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx:80">
P2: Changing the default target outside the picker can create a workspace from a stale branch name, because `baseBranch` is not scoped to or reset when `selectedTarget` changes. Consider clearing or keying the selected branch by `selectedTarget.key` whenever the target changes.</violation>

<violation number="2" location="apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx:136">
P2: Submitting with the auto-selected target does not update the “last used target” preference; only manual picker selection calls `setTargetKey`. Consider persisting `selectedTarget.key` after a successful create so future defaults match the target actually used.</violation>
</file>

<file name="apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/components/WorkspaceRowMenu/WorkspaceRowMenu.tsx">

<violation number="1" location="apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/components/WorkspaceRowMenu/WorkspaceRowMenu.tsx:41">
P3: Rename failures are hard to diagnose because the caught error is discarded and only a generic alert is shown. Logging the error with `workspace.id` preserves useful host/relay context without changing the user flow.

(Based on your team's feedback about preserving useful async error diagnostics.) [FEEDBACK_USED]</violation>

<violation number="2" location="apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/components/WorkspaceRowMenu/WorkspaceRowMenu.tsx:130">
P2: Android sharing can drop the workspace link because React Native treats `Share.share({ url })` as iOS-only content. Including the link as `message` keeps the share action functional cross-platform while preserving `url` for iOS.</violation>
</file>

<file name="apps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts">

<violation number="1" location="apps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts:99">
P0: Diff stats never appear or update because the aggregated map is computed once and never recomputed. The final `useMemo` that builds `Map<string, DiffStats>` depends only on `queryClient` (a stable reference), so React never re-executes the factory when query data arrives or changes. The result is always an empty/stale map — the live diff stats feature is broken.

**Fix:** add the reactive subscription data to the dependency array so the memo recomputes when any diff-stats query resolves or updates.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Mobile as Mobile App
    participant Collections as Electric Collections
    participant Relay as Relay Proxy
    participant Host as Host Service
    participant DB as Database
    participant GitHub as GitHub API

    Note over Mobile,Host: NEW: Host-Owned Workspace Data Flow

    Mobile->>Collections: Subscribe v2Hosts, v2Projects, v2Workspaces, subscriptions
    Collections-->>Mobile: Live sync host/project/workspace rows

    Mobile->>Mobile: deriveHostWorkspacesQueryTargets(hosts)
    Mobile->>Mobile: buildRelayHostUrl(orgId, machineId)

    loop Every 30s (poll)
        Mobile->>Relay: workspace.list (JWT Bearer)
        Relay->>Host: Forward request
        alt Host online & answering
            Host-->>Relay: HostWorkspaceRow[]
            Relay-->>Mobile: HostWorkspaceRow[]
            Mobile->>Mobile: mark source="host", hostReachable=true
        else Host silent / error
            Relay-->>Mobile: Error/Timeout
            Mobile->>Collections: Fallback to v2_workspaces rows
            Mobile->>Mobile: mark source="cloud", hostReachable=false
        end
    end

    alt Pull-to-refresh / Focus
        Mobile->>Mobile: Invalidate ["host-service","workspaces","list"]
        Mobile->>Relay: workspace.list (fresh)
    end

    Note over Mobile,Host: NEW: Live Diff Stats (Viewport-Bounded)

    Mobile->>Mobile: onViewableItemsChanged → setVisibleIds (capped 20)
    loop For each visible workspace
        Mobile->>Mobile: resolveHostUrl(hostId)
        alt Host reachable & source=host & worktree exists
            Mobile->>Relay: git.getStatus(workspaceId, priority=background)
            Relay->>Host: Forward request
            Host-->>Relay: GitStatusSnapshot
            Relay-->>Mobile: GitStatusSnapshot
            Mobile->>Mobile: aggregateDiffStats() → additions/deletions
        end
    end
    Mobile->>Mobile: Read full React Query cache for ["diff-stats"] → no flicker on scroll-back

    Note over Mobile,Host: NEW: Writes via Owning Host

    alt Rename workspace
        Mobile->>Mobile: prompt() → new name
        Mobile->>Relay: workspace.update(id, name)
        Relay->>Host: Forward mutation
        Host-->>Relay: Success
        Relay-->>Mobile: Success
        Mobile->>Mobile: invalidateHost(hostId)
    else Delete workspace
        Mobile->>Mobile: Alert confirmation
        alt Dirty worktree / CONFLICT error
            Host-->>Mobile: TRPCClientError (code=CONFLICT)
            Mobile->>Mobile: Alert → "Force delete?" dialog
            alt Force delete
                Mobile->>Relay: workspaceCleanup.destroy(workspaceId, force=true)
                Relay->>Host: Forward mutation
                Host-->>Relay: Success
                Relay-->>Mobile: Success
                Mobile->>Mobile: removeWorkspace(hostId, workspaceId)
            end
        else Clean deletion
            Mobile->>Relay: workspaceCleanup.destroy(workspaceId, force=false)
            Relay->>Host: Forward mutation
            Host-->>Relay: Success
            Relay-->>Mobile: Success
            Mobile->>Mobile: removeWorkspace(hostId, workspaceId)
        end
    end

    Note over Mobile,Host: NEW: New-Chat Widget & Workspace Creation

    Mobile->>Mobile: useNewChatTargets() → project.list per online host
    alt No projects on online hosts
        Mobile-->>Mobile: Show "No projects on an online host"
    else
        Mobile->>Mobile: Show target picker (project + host)
        Mobile->>Relay: workspaceCreation.searchBranches(projectId, query, limit)
        Relay->>Host: Forward request
        Host-->>Relay: BranchSearchResult (default + items)
        Relay-->>Mobile: BranchSearchResult
        Mobile->>Mobile: Render Default/Recents branch picker
        alt User selects model
            Mobile-->>Mobile: Persist modelId via Zustand (AsyncStorage)
        end
        alt User adds photo context
            Mobile->>Mobile: expo-media-library → getAssets (recent 30)
            Mobile->>Mobile: expo-image-manipulator → transcode HEIC→JPEG
            Mobile->>Relay: attachments.upload (base64, mediaType)
            Relay->>Host: Forward upload
            Host-->>Relay: attachmentId
            Relay-->>Mobile: attachmentId
        end
        Mobile->>Relay: workspaces.create(id, projectId, baseBranch, agents)
        Relay->>Host: Forward mutation
        Host-->>Relay: workspaces.create result (workspace + agents)
        Relay-->>Mobile: Result
        alt Agent ok & kind=chat
            Mobile->>Mobile: router.push(workspaceId/chat/sessionId)
        else Agent error
            Mobile->>Mobile: router.push(workspaceId/chat) + Alert error
        end
    end

    Note over Mobile,Collections: NEW: Settings & Billing

    Mobile->>Collections: Subscribe subscriptions collection
    Collections-->>Mobile: Subscription/plan data (read-only)
    Mobile-->>Mobile: Render billing screen from synced subscription

    Note over Mobile,Collections: PR Matching Parity

    Mobile->>Collections: Subscribe githubPullRequests
    Collections-->>Mobile: PR records
    Mobile->>Mobile: Build map keyed by `${repositoryId}::${headBranch}`
    Mobile->>Mobile: State derivation: mergedAt → draft → closed → open
    Mobile-->>Mobile: Render PR badge on workspace rows
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}
}
return map;
}, [queryClient]);

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.

P0: Diff stats never appear or update because the aggregated map is computed once and never recomputed. The final useMemo that builds Map<string, DiffStats> depends only on queryClient (a stable reference), so React never re-executes the factory when query data arrives or changes. The result is always an empty/stale map — the live diff stats feature is broken.

Fix: add the reactive subscription data to the dependency array so the memo recomputes when any diff-stats query resolves or updates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts, line 99:

<comment>Diff stats never appear or update because the aggregated map is computed once and never recomputed. The final `useMemo` that builds `Map<string, DiffStats>` depends only on `queryClient` (a stable reference), so React never re-executes the factory when query data arrives or changes. The result is always an empty/stale map — the live diff stats feature is broken.

**Fix:** add the reactive subscription data to the dependency array so the memo recomputes when any diff-stats query resolves or updates.</comment>

<file context>
@@ -0,0 +1,100 @@
+			}
+		}
+		return map;
+	}, [queryClient]);
+}
</file context>
Suggested change
}, [queryClient]);
}, [queryClient, ..._queries.map((q) => q.dataUpdatedAt)]);

);
const selectedProject =
sortedProjects.find((project) => project.id === projectFilter) ??
sortedProjects[0];

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.

P2: A stale persisted project filter can make the filter sheet show the first project even though the workspace list is still filtered by the missing projectFilter id and may appear empty. Consider only defaulting to the first project when projectFilter is null, or clearing invalid persisted filters when project data changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/screens/(authenticated)/(home)/filter/FilterScreen.tsx, line 40:

<comment>A stale persisted project filter can make the filter sheet show the first project even though the workspace list is still filtered by the missing `projectFilter` id and may appear empty. Consider only defaulting to the first project when `projectFilter` is null, or clearing invalid persisted filters when project data changes.</comment>

<file context>
@@ -0,0 +1,110 @@
+	);
+	const selectedProject =
+		sortedProjects.find((project) => project.id === projectFilter) ??
+		sortedProjects[0];
+	const selectedHost = (hosts ?? []).find(
+		(host) => host.machineId === hostFilter,
</file context>

</Text>
</Pressable>
<Pressable
onPress={handleSignOut}

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.

P2: Tapping “Log out” in the org switcher now signs the user out immediately, which makes accidental taps in this sheet irreversible compared with the existing Settings logout flow. Consider reusing the same confirmation step here so logout behavior is consistent across entry points.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.tsx, line 135:

<comment>Tapping “Log out” in the org switcher now signs the user out immediately, which makes accidental taps in this sheet irreversible compared with the existing Settings logout flow. Consider reusing the same confirmation step here so logout behavior is consistent across entry points.</comment>

<file context>
@@ -97,6 +111,43 @@ export function OrganizationSwitcherSheet({
+								</Text>
+							</Pressable>
+							<Pressable
+								onPress={handleSignOut}
+								disabled={isSigningOut}
+								className="flex-row items-center gap-2.5 py-2.5"
</file context>

icon="square.and.arrow.up"
onPress={() =>
void Share.share({
url: `https://app.superset.sh/workspaces/${workspace.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.

P2: Android sharing can drop the workspace link because React Native treats Share.share({ url }) as iOS-only content. Including the link as message keeps the share action functional cross-platform while preserving url for iOS.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/components/WorkspaceRowMenu/WorkspaceRowMenu.tsx, line 130:

<comment>Android sharing can drop the workspace link because React Native treats `Share.share({ url })` as iOS-only content. Including the link as `message` keeps the share action functional cross-platform while preserving `url` for iOS.</comment>

<file context>
@@ -0,0 +1,140 @@
+						icon="square.and.arrow.up"
+						onPress={() =>
+							void Share.share({
+								url: `https://app.superset.sh/workspaces/${workspace.id}`,
+							})
+						}
</file context>

setRefreshing(true);
await queryClient
.refetchQueries({ queryKey: ["host-service", "workspaces", "list"] })
.catch(() => {});

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.

P3: Pull-to-refresh failures currently disappear with no diagnostic signal because refetchQueries errors are caught with an empty handler. Logging a warning with context would keep the refresh best-effort while making host/relay failures observable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsx, line 167:

<comment>Pull-to-refresh failures currently disappear with no diagnostic signal because `refetchQueries` errors are caught with an empty handler. Logging a warning with context would keep the refresh best-effort while making host/relay failures observable.</comment>

<file context>
@@ -0,0 +1,272 @@
+		setRefreshing(true);
+		await queryClient
+			.refetchQueries({ queryKey: ["host-service", "workspaces", "list"] })
+			.catch(() => {});
+		void queryClient.invalidateQueries({ queryKey: ["diff-stats"] });
+		setRefreshing(false);
</file context>

const { activeOrganization } = useOrganizations();
const { signOut, isSigningOut } = useSignOut();

const { data: subscriptions } = useLiveQuery(

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.

P3: Settings and Billing now duplicate the active-subscription query/selection flow, so future billing status changes need matching edits in both screens. Consider extracting a small useActiveSubscription hook shared by both screens.

(Based on your team's feedback about avoiding duplicated business logic across components.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/screens/(authenticated)/settings/SettingsScreen.tsx, line 24:

<comment>Settings and Billing now duplicate the active-subscription query/selection flow, so future billing status changes need matching edits in both screens. Consider extracting a small `useActiveSubscription` hook shared by both screens.

(Based on your team's feedback about avoiding duplicated business logic across components.) </comment>

<file context>
@@ -0,0 +1,137 @@
+	const { activeOrganization } = useOrganizations();
+	const { signOut, isSigningOut } = useSignOut();
+
+	const { data: subscriptions } = useLiveQuery(
+		(q) => q.from({ subscriptions: collections.subscriptions }),
+		[collections],
</file context>

id: workspace.id,
name: trimmed,
});
} catch {

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.

P3: Rename failures are hard to diagnose because the caught error is discarded and only a generic alert is shown. Logging the error with workspace.id preserves useful host/relay context without changing the user flow.

(Based on your team's feedback about preserving useful async error diagnostics.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/components/WorkspaceRowMenu/WorkspaceRowMenu.tsx, line 41:

<comment>Rename failures are hard to diagnose because the caught error is discarded and only a generic alert is shown. Logging the error with `workspace.id` preserves useful host/relay context without changing the user flow.

(Based on your team's feedback about preserving useful async error diagnostics.) </comment>

<file context>
@@ -0,0 +1,140 @@
+				id: workspace.id,
+				name: trimmed,
+			});
+		} catch {
+			Alert.alert("Rename failed");
+		}
</file context>

@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: 12

🧹 Nitpick comments (9)
turbo.jsonc (1)

56-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding dependsOn: ["^build:types"] to the build:types task.

The build:types task currently has no dependsOn, so turbo build:types runs all packages in parallel. If a future package's build:types imports types from another package's dist-types export (like host-service's ./router), declaration generation would fail without dependency types built first. The typecheck task correctly gates on ^build:types, but standalone build:types runs are unprotected. This is low-risk today given the current dependency direction, but adding the dependency would make the pipeline robust against future changes.

♻️ Suggested addition
 		"build:types": {
+			"dependsOn": ["^build:types"],
 			"outputs": ["dist-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 `@turbo.jsonc` around lines 56 - 60, The build:types task in turbo.jsonc
currently runs without dependency ordering, so standalone type generation can
start before upstream package declaration outputs are available. Update the
build:types task configuration to depend on upstream build:types outputs,
matching the protection already used by typecheck, so package type generation is
sequenced correctly when symbols like build:types and typecheck are resolved
across packages.
packages/workspace-fs/tsconfig.types.json (1)

1-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider excluding bench files from declaration build.

include: ["src"] will pick up src/cache-and-paths-memory.bench.ts, generating an unnecessary .d.ts artifact. Consider adding an explicit exclude for non-source files.

♻️ Optional: exclude bench/test files from declaration output
 {
 	"extends": "./tsconfig.json",
 	"compilerOptions": {
 		"noEmit": false,
 		"emitDeclarationOnly": true,
 		"declaration": true,
 		"declarationMap": true,
 		"rootDir": "src",
 		"outDir": "dist-types"
 	},
-	"include": ["src"]
+	"include": ["src"],
+	"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts"]
 }
🤖 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/workspace-fs/tsconfig.types.json` around lines 1 - 12, The
declaration-only TypeScript config is currently picking up bench files through
the broad src include, which creates unnecessary declaration artifacts. Update
the workspace-fs tsconfig.types settings to explicitly exclude non-source files
such as bench/test files while keeping the emitDeclarationOnly build focused on
the real source under src. Use the tsconfig.types.json include/exclude
configuration to prevent src/cache-and-paths-memory.bench.ts and similar files
from being emitted.
packages/pty-daemon/tsconfig.types.json (1)

1-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider excluding test files from declaration build.

include: ["src"] will pick up test files like src/Pty/Pty.test.ts, generating unnecessary .d.ts artifacts. If the base tsconfig.json doesn't already exclude **/*.test.ts, consider adding an explicit exclude here.

♻️ Optional: exclude test files from declaration output
 {
 	"extends": "./tsconfig.json",
 	"compilerOptions": {
 		"noEmit": false,
 		"emitDeclarationOnly": true,
 		"declaration": true,
 		"declarationMap": true,
 		"rootDir": "src",
 		"outDir": "dist-types"
 	},
-	"include": ["src"]
+	"include": ["src"],
+	"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts"]
 }
🤖 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/pty-daemon/tsconfig.types.json` around lines 1 - 12, The declaration
build in tsconfig.types.json is currently pulling in test files through the src
include, which can generate unwanted artifacts. Update the declaration-only
config to explicitly exclude test patterns such as test files, or rely on
tsconfig.json only if it already excludes them; verify the change around the
compilerOptions and include settings so the types build only covers production
sources.
apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx (1)

124-137: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Wrap handleSubmit in useCallback to avoid invalidating downstream memos.

handleSubmit is recreated every render. Since PromptInput memoizes its internal submit with onSubmit as a dependency, this causes the memo to break each render, potentially re-rendering all form context consumers unnecessarily.

♻️ Proposed refactor
-	const handleSubmit = (message: PromptInputMessage) => {
+	const handleSubmit = useCallback((message: PromptInputMessage) => {
 		if (!selectedTarget) {
 			Alert.alert("No project on an online host");
 			return Promise.reject(new Error("No target"));
 		}
 		return createChatWorkspace
 			.mutateAsync({
 				target: selectedTarget,
 				baseBranch,
 				modelId,
 				message,
 			})
 			.then(() => setBaseBranch(null));
-	};
+	}, [selectedTarget, createChatWorkspace, baseBranch, modelId]);
🤖 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/mobile/screens/`(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx
around lines 124 - 137, Wrap the NewChatWidget.handleSubmit function in
useCallback so its reference stays stable across renders and does not invalidate
PromptInput’s memoized submit. Update the handleSubmit definition in
NewChatWidget and include the relevant dependencies it closes over, such as
selectedTarget, baseBranch, modelId, and createChatWorkspace, while preserving
the current Alert.alert and mutateAsync behavior.
apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useCreateChatWorkspace/useCreateChatWorkspace.ts (1)

37-61: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Attachments are orphaned on the host if workspace creation fails.

If client.workspaces.create.mutate throws after attachments are uploaded (lines 40–46), the uploaded attachment IDs are never cleaned up. The onError handler shows an alert but doesn't attempt rollback. Consider whether the host-service should support a cleanup/abort call, or whether a TTL-based garbage collection on the host is sufficient for this first pass.

🤖 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/mobile/screens/`(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useCreateChatWorkspace/useCreateChatWorkspace.ts
around lines 37 - 61, The attachment upload flow in useCreateChatWorkspace
leaves orphaned files when client.workspaces.create.mutate fails after upload.
Add rollback handling in useCreateChatWorkspace so the uploaded attachment IDs
are cleaned up if workspace creation throws, ideally by invoking a host
cleanup/abort action on error; if that is not available, document and wire a
TTL-based cleanup path in the host service. Make sure the new error handling is
tied to the attachment upload and workspace creation steps in the
useCreateChatWorkspace hook.
apps/mobile/screens/(authenticated)/settings/hosts/HostsSettingsScreen.tsx (1)

16-19: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider using isReady to distinguish loading from empty state.

Same as the billing screen, useLiveQuery here only destructures data. During initial load the list is empty with no loading indicator. For consistency with the TanStack DB coding guideline, consider destructuring isReady and showing a loading state when !isReady && (!hosts || hosts.length === 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 `@apps/mobile/screens/`(authenticated)/settings/hosts/HostsSettingsScreen.tsx
around lines 16 - 19, The HostsSettingsScreen useLiveQuery call only reads data,
so the UI cannot tell initial loading from an empty result. Update
HostsSettingsScreen to also destructure isReady from useLiveQuery, then use
isReady alongside hosts to render a loading state when the query has not
finished and the list is still empty. Keep the logic aligned with the existing
billing screen pattern and update the hosts list rendering branch accordingly.

Source: Coding guidelines

apps/mobile/screens/(authenticated)/(home)/workspaces/stores/workspacesFilterStore/workspacesFilterStore.ts (1)

21-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add partialize to explicitly exclude setters from persistence.

The persist middleware currently serializes the entire state object including setter functions. While JSON.stringify silently drops functions and rehydration restores them from initial state, this is implicit behavior. Adding partialize makes the persistence contract explicit and guards against future issues.

♻️ Proposed refactor
 		{
 			name: "workspaces-filter",
 			storage: createJSONStorage(() => AsyncStorage),
+			partialize: (state) => ({
+				projectFilter: state.projectFilter,
+				hostFilter: state.hostFilter,
+				sort: state.sort,
+			}),
 		},
🤖 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/mobile/screens/`(authenticated)/(home)/workspaces/stores/workspacesFilterStore/workspacesFilterStore.ts
around lines 21 - 36, The Workspaces filter store persistence is implicitly
relying on JSON serialization to drop setter functions from the stored state.
Update the persist config in useWorkspacesFilterStore to add a partialize
function that explicitly returns only the serializable fields (projectFilter,
hostFilter, sort) and excludes setProjectFilter, setHostFilter, and setSort.
This keeps the persistence contract clear and prevents future state-shape
changes from accidentally persisting functions.
apps/mobile/screens/(authenticated)/(home)/filter/FilterScreen.tsx (1)

35-37: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider memoizing sortedProjects.

The spread-and-sort runs on every render even when projects hasn't changed. Wrapping in useMemo keyed on projects avoids unnecessary re-sorts.

♻️ Optional refactor
+import { useMemo } from "react";
+
 	// ...
-	const sortedProjects = [...(projects ?? [])].sort((a, b) =>
-		a.name.localeCompare(b.name),
-	);
+	const sortedProjects = useMemo(
+		() => [...(projects ?? [])].sort((a, b) => a.name.localeCompare(b.name)),
+		[projects],
+	);
🤖 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/mobile/screens/`(authenticated)/(home)/filter/FilterScreen.tsx around
lines 35 - 37, The sorted project list in FilterScreen is being recomputed on
every render even when the input projects array is unchanged. Update the
`sortedProjects` calculation to use `useMemo` in `FilterScreen.tsx`, keyed on
`projects`, and keep the existing `localeCompare` sort logic inside the memo so
the sort only reruns when `projects` changes.
apps/mobile/lib/host-service/errors.ts (1)

3-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider tightening the type guard to preserve TRPCClientError properties.

The guard narrows to { data: HostServiceErrorData }, discarding the TRPCClientError base type. If any future caller needs error.message or error.shape after the guard, they'd have to re-cast. Narrowing to TRPCClientError<any> & { data: HostServiceErrorData } would be more precise while still safe.

Additionally, deleteInProgress and teardownFailure are typed as unknown but are used in boolean contexts downstream (if (error.data.deleteInProgress)). Typing them as boolean would improve type safety.

♻️ Suggested type improvements
 interface HostServiceErrorData {
 	code?: string;
-	deleteInProgress?: unknown;
-	teardownFailure?: unknown;
+	deleteInProgress?: boolean;
+	teardownFailure?: boolean;
 }

 export function isTrpcErrorWithData(
 	error: unknown,
): error is { data: HostServiceErrorData } {
+): error is TRPCClientError<any> & { data: HostServiceErrorData } {
 	return (
 		error instanceof TRPCClientError &&
 		typeof error.data === "object" &&
 		error.data !== null
 	);
 }
🤖 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/mobile/lib/host-service/errors.ts` around lines 3 - 17, Tighten the type
guard in isTrpcErrorWithData so it preserves the TRPCClientError base type
instead of narrowing only to { data: HostServiceErrorData }, and update
HostServiceErrorData so deleteInProgress and teardownFailure are boolean since
they’re consumed as booleans downstream. Adjust the return type of
isTrpcErrorWithData accordingly, keeping the existing instanceof TRPCClientError
and data object checks intact.
🤖 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/mobile/hooks/useHostWorkspaces/useHostWorkspaces.ts`:
- Around line 50-57: `useHostWorkspaces` is marking readiness too early because
`isReady` only reflects host-derived queries and ignores the loading state of
the Electric `useLiveQuery` results. Update the readiness logic in
`useHostWorkspaces` to incorporate the `isReady`/loading status from the live
queries for `hosts` and `cloudRows`, so the hook stays not ready until data has
actually loaded when there is no cached data. Make sure the final `isReady`
condition in the hook combines the query readiness with the existing
host/workspace checks instead of relying on `queries.every(...)` alone.

In `@apps/mobile/modules/alert-prompt/ios/AlertPrompt.podspec`:
- Line 15: The AlertPrompt.podspec source currently points to a non-existent git
remote, which will break CocoaPods lint and publish flows. Update the s.source
value in AlertPrompt.podspec to the actual repository URL, or remove s.source
entirely if this pod is intended to remain local-only. Use the existing s.source
declaration in AlertPrompt.podspec as the place to fix this.

In `@apps/mobile/modules/alert-prompt/src/index.ts`:
- Around line 13-16: The AlertPrompt module is being loaded unconditionally via
requireNativeModule("AlertPrompt"), but this native module is Apple-only and
will crash on Android at import time. Update the AlertPromptModule
initialization in index.ts to guard by platform and provide a safe Android
fallback or alternate implementation before prompt() is called. Use the existing
AlertPromptModule and prompt() symbols to keep the API unchanged while
preventing module-load failures on Android.

In `@apps/mobile/screens/`(authenticated)/(home)/filter/FilterScreen.tsx:
- Around line 38-40: The selected project logic in FilterScreen.tsx is forcing a
fallback to sortedProjects[0], which prevents the “All” state from showing when
projectFilter is null. Update the selectedProject assignment so it behaves like
the host filter path: return undefined when projectFilter is null or no match is
found, and let the existing label fallback display “All”. Use the
selectedProject expression and the hostFilter handling in the same screen as the
reference points.

In `@apps/mobile/screens/`(authenticated)/(home)/filter/host/HostFilterScreen.tsx:
- Around line 26-33: Gate the filter row rendering in HostFilterScreen on the
`isReady` values returned by both `useLiveQuery` calls for `hosts` and
`projects`. Right now the screen can render with undefined data and show empty
rows/zero counts before the queries settle; update the component to detect when
either query is not ready and show a loading state instead, only rendering the
filter counts once both queries are ready.

In
`@apps/mobile/screens/`(authenticated)/(home)/filter/project/ProjectFilterScreen.tsx:
- Around line 25-28: The ProjectFilterScreen `useLiveQuery` result is only using
`data`, so the screen can treat the initial undefined state as an empty list
instead of loading. Update the `useLiveQuery` destructuring to also read
`isReady`, and use that flag in the render logic for `sortedProjects` so the
empty/loading state is gated correctly. Specifically, when `projects` is
undefined and the query is not ready, show the loading indicator first; only
fall back to the empty state once `projectsReady` is true.

In
`@apps/mobile/screens/`(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/PhotoCarousel/PhotoCarousel.tsx:
- Around line 115-124: The empty-state in PhotoCarousel is being shown too early
because `assets` is undefined while the query is still loading. Update the
conditional around the “No photos in your library” view in `PhotoCarousel` to
also check the loading state (`isLoading` or `isPending`) before treating
`(assets ?? []).length === 0` as truly empty. Use the existing query state and
`granted` guard together so the message only appears after fetching has finished
and the library is actually empty.

In
`@apps/mobile/screens/`(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/ContextSheet.tsx:
- Around line 72-80: The camera attachment mapping in ContextSheet currently
passes asset.mimeType directly, which can be undefined and cause inconsistent
classification. Update the asset transformation in the ContextSheet attachment
handling to fall back to "image/jpeg" for camera assets, matching the existing
library upload path, and keep the change scoped to the result.assets.map logic
that builds the attachment objects.

In
`@apps/mobile/screens/`(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.tsx:
- Around line 41-44: `handleSignOut` in `OrganizationSwitcherSheet` signs out
immediately, unlike `SettingsScreen` which confirms first. Update this handler
to mirror the `Alert.alert("Log out?", ...)` flow used in `SettingsScreen`, so
the sheet only closes and calls `signOut` after the user confirms. Keep the
change localized to `handleSignOut` and the sign-out action in
`OrganizationSwitcherSheet`.

In
`@apps/mobile/screens/`(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts:
- Around line 85-99: The `useMemo` inside `useVisibleDiffStats` is only keyed on
`queryClient`, so it never recomputes when the `useQueries` results resolve and
the diff stats map stays stale. Rename `_queries` to a meaningful variable from
`useQueries`, then include that query-results variable in the dependency array
for the `useMemo` that builds the `Map<string, DiffStats>` so the stats
recalculate whenever query data changes.
- Around line 17-33: The aggregateDiffStats helper is dropping contributions
when the same file appears in multiple buckets because it overwrites entries in
the byPath map by file.path; update aggregateDiffStats to merge
additions/deletions for duplicate paths instead of last-write-wins so
againstBase, staged, and unstaged all contribute to the final totals.

In
`@apps/mobile/screens/`(authenticated)/settings/billing/BillingSettingsScreen.tsx:
- Around line 13-18: The billing screen is treating the initial `useLiveQuery`
result as an empty subscription list, which makes `activeSubscription` fall back
to “Free plan” before data has loaded. Update `BillingSettingsScreen` to
destructure and use `isReady` from `useLiveQuery` alongside `data`, and use that
flag in the render logic to show a loading/empty state until the query is ready.
Keep the `activeSubscription` lookup for the loaded case, but distinguish
initial loading from a truly empty subscription set.

---

Nitpick comments:
In `@apps/mobile/lib/host-service/errors.ts`:
- Around line 3-17: Tighten the type guard in isTrpcErrorWithData so it
preserves the TRPCClientError base type instead of narrowing only to { data:
HostServiceErrorData }, and update HostServiceErrorData so deleteInProgress and
teardownFailure are boolean since they’re consumed as booleans downstream.
Adjust the return type of isTrpcErrorWithData accordingly, keeping the existing
instanceof TRPCClientError and data object checks intact.

In `@apps/mobile/screens/`(authenticated)/(home)/filter/FilterScreen.tsx:
- Around line 35-37: The sorted project list in FilterScreen is being recomputed
on every render even when the input projects array is unchanged. Update the
`sortedProjects` calculation to use `useMemo` in `FilterScreen.tsx`, keyed on
`projects`, and keep the existing `localeCompare` sort logic inside the memo so
the sort only reruns when `projects` changes.

In
`@apps/mobile/screens/`(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useCreateChatWorkspace/useCreateChatWorkspace.ts:
- Around line 37-61: The attachment upload flow in useCreateChatWorkspace leaves
orphaned files when client.workspaces.create.mutate fails after upload. Add
rollback handling in useCreateChatWorkspace so the uploaded attachment IDs are
cleaned up if workspace creation throws, ideally by invoking a host
cleanup/abort action on error; if that is not available, document and wire a
TTL-based cleanup path in the host service. Make sure the new error handling is
tied to the attachment upload and workspace creation steps in the
useCreateChatWorkspace hook.

In
`@apps/mobile/screens/`(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx:
- Around line 124-137: Wrap the NewChatWidget.handleSubmit function in
useCallback so its reference stays stable across renders and does not invalidate
PromptInput’s memoized submit. Update the handleSubmit definition in
NewChatWidget and include the relevant dependencies it closes over, such as
selectedTarget, baseBranch, modelId, and createChatWorkspace, while preserving
the current Alert.alert and mutateAsync behavior.

In
`@apps/mobile/screens/`(authenticated)/(home)/workspaces/stores/workspacesFilterStore/workspacesFilterStore.ts:
- Around line 21-36: The Workspaces filter store persistence is implicitly
relying on JSON serialization to drop setter functions from the stored state.
Update the persist config in useWorkspacesFilterStore to add a partialize
function that explicitly returns only the serializable fields (projectFilter,
hostFilter, sort) and excludes setProjectFilter, setHostFilter, and setSort.
This keeps the persistence contract clear and prevents future state-shape
changes from accidentally persisting functions.

In `@apps/mobile/screens/`(authenticated)/settings/hosts/HostsSettingsScreen.tsx:
- Around line 16-19: The HostsSettingsScreen useLiveQuery call only reads data,
so the UI cannot tell initial loading from an empty result. Update
HostsSettingsScreen to also destructure isReady from useLiveQuery, then use
isReady alongside hosts to render a loading state when the query has not
finished and the list is still empty. Keep the logic aligned with the existing
billing screen pattern and update the hosts list rendering branch accordingly.

In `@packages/pty-daemon/tsconfig.types.json`:
- Around line 1-12: The declaration build in tsconfig.types.json is currently
pulling in test files through the src include, which can generate unwanted
artifacts. Update the declaration-only config to explicitly exclude test
patterns such as test files, or rely on tsconfig.json only if it already
excludes them; verify the change around the compilerOptions and include settings
so the types build only covers production sources.

In `@packages/workspace-fs/tsconfig.types.json`:
- Around line 1-12: The declaration-only TypeScript config is currently picking
up bench files through the broad src include, which creates unnecessary
declaration artifacts. Update the workspace-fs tsconfig.types settings to
explicitly exclude non-source files such as bench/test files while keeping the
emitDeclarationOnly build focused on the real source under src. Use the
tsconfig.types.json include/exclude configuration to prevent
src/cache-and-paths-memory.bench.ts and similar files from being emitted.

In `@turbo.jsonc`:
- Around line 56-60: The build:types task in turbo.jsonc currently runs without
dependency ordering, so standalone type generation can start before upstream
package declaration outputs are available. Update the build:types task
configuration to depend on upstream build:types outputs, matching the protection
already used by typecheck, so package type generation is sequenced correctly
when symbols like build:types and typecheck are resolved across packages.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 482c1d53-7792-457c-881e-b87630b926d9

📥 Commits

Reviewing files that changed from the base of the PR and between 06fcba8 and 7f34cbc.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (141)
  • .gitignore
  • .superset/lib/setup/steps.sh
  • apps/mobile/app.config.ts
  • apps/mobile/app/(authenticated)/(home)/_layout.tsx
  • apps/mobile/app/(authenticated)/(home)/filter/_layout.tsx
  • apps/mobile/app/(authenticated)/(home)/filter/host.tsx
  • apps/mobile/app/(authenticated)/(home)/filter/index.tsx
  • apps/mobile/app/(authenticated)/(home)/filter/project.tsx
  • apps/mobile/app/(authenticated)/(home)/filter/sort.tsx
  • apps/mobile/app/(authenticated)/(home)/index.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(home)/index.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(more)/_layout.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(more)/index.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(more)/settings/index.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(search)/_layout.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(search)/index.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(tasks)/[id].tsx
  • apps/mobile/app/(authenticated)/(tabs)/(tasks)/_layout.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(tasks)/index.tsx
  • apps/mobile/app/(authenticated)/(tabs)/_layout.tsx
  • apps/mobile/app/(authenticated)/_layout.tsx
  • apps/mobile/app/(authenticated)/settings/account.tsx
  • apps/mobile/app/(authenticated)/settings/billing.tsx
  • apps/mobile/app/(authenticated)/settings/hosts.tsx
  • apps/mobile/app/(authenticated)/settings/index.tsx
  • apps/mobile/app/(authenticated)/settings/organization.tsx
  • apps/mobile/hooks/useHostWorkspaces/index.ts
  • apps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.ts
  • apps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.utils.ts
  • apps/mobile/lib/collections/collections.ts
  • apps/mobile/lib/env.ts
  • apps/mobile/lib/host-service/client.ts
  • apps/mobile/lib/host-service/errors.ts
  • apps/mobile/metro.config.js
  • apps/mobile/modules/alert-prompt/expo-module.config.json
  • apps/mobile/modules/alert-prompt/ios/AlertPrompt.podspec
  • apps/mobile/modules/alert-prompt/ios/AlertPromptModule.swift
  • apps/mobile/modules/alert-prompt/package.json
  • apps/mobile/modules/alert-prompt/src/index.ts
  • apps/mobile/package.json
  • apps/mobile/screens/(authenticated)/(home)/filter/FilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/components/ProjectAvatar/ProjectAvatar.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/components/ProjectAvatar/index.ts
  • apps/mobile/screens/(authenticated)/(home)/filter/host/HostFilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/host/index.ts
  • apps/mobile/screens/(authenticated)/(home)/filter/index.ts
  • apps/mobile/screens/(authenticated)/(home)/filter/project/ProjectFilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/project/index.ts
  • apps/mobile/screens/(authenticated)/(home)/filter/sort/SortFilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/sort/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/BranchPickerSheet/BranchPickerSheet.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/BranchPickerSheet/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/ContextSheet.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/PhotoCarousel/PhotoCarousel.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/PhotoCarousel/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/TargetPickerSheet/TargetPickerSheet.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/TargetPickerSheet/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useCreateChatWorkspace/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useCreateChatWorkspace/useCreateChatWorkspace.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useNewChatTargets/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useNewChatTargets/useNewChatTargets.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/stores/newChatPreferencesStore/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/stores/newChatPreferencesStore/newChatPreferencesStore.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/WorkspaceRow.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/components/WorkspaceRowMenu/WorkspaceRowMenu.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/components/WorkspaceRowMenu/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/stores/workspacesFilterStore/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/stores/workspacesFilterStore/workspacesFilterStore.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/WorkspacesScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceActionsSheet/WorkspaceActionsSheet.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceActionsSheet/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceFilterSheet/WorkspaceFilterSheet.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceFilterSheet/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceRow/WorkspaceRow.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceRow/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(more)/MoreMenuScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(more)/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(more)/settings/SettingsScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(search)/SearchScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(search)/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(tasks)/[id]/TaskDetailScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(tasks)/[id]/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(tasks)/tasks/TasksScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(tasks)/tasks/index.ts
  • apps/mobile/screens/(authenticated)/components/HostStatusDot/HostStatusDot.tsx
  • apps/mobile/screens/(authenticated)/components/HostStatusDot/index.ts
  • apps/mobile/screens/(authenticated)/components/ListRow/ListRow.tsx
  • apps/mobile/screens/(authenticated)/components/ListRow/index.ts
  • apps/mobile/screens/(authenticated)/components/ListRowCheck/ListRowCheck.tsx
  • apps/mobile/screens/(authenticated)/components/ListRowCheck/index.ts
  • apps/mobile/screens/(authenticated)/components/ListRowValue/ListRowValue.tsx
  • apps/mobile/screens/(authenticated)/components/ListRowValue/index.ts
  • apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.tsx
  • apps/mobile/screens/(authenticated)/components/OrgDropdown/index.ts
  • apps/mobile/screens/(authenticated)/components/OrganizationAvatar/OrganizationAvatar.tsx
  • apps/mobile/screens/(authenticated)/components/OrganizationAvatar/index.ts
  • apps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.tsx
  • apps/mobile/screens/(authenticated)/components/TabBarAccessory/index.ts
  • apps/mobile/screens/(authenticated)/hooks/useOrganizations/useOrganizations.ts
  • apps/mobile/screens/(authenticated)/settings/SettingsScreen.tsx
  • apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx
  • apps/mobile/screens/(authenticated)/settings/account/index.ts
  • apps/mobile/screens/(authenticated)/settings/billing/BillingSettingsScreen.tsx
  • apps/mobile/screens/(authenticated)/settings/billing/index.ts
  • apps/mobile/screens/(authenticated)/settings/components/UserAvatar/UserAvatar.tsx
  • apps/mobile/screens/(authenticated)/settings/components/UserAvatar/index.ts
  • apps/mobile/screens/(authenticated)/settings/hosts/HostsSettingsScreen.tsx
  • apps/mobile/screens/(authenticated)/settings/hosts/index.ts
  • apps/mobile/screens/(authenticated)/settings/index.ts
  • apps/mobile/screens/(authenticated)/settings/organization/OrganizationSettingsScreen.tsx
  • apps/mobile/screens/(authenticated)/settings/organization/index.ts
  • apps/mobile/screens/(authenticated)/settings/utils/activeSubscription/activeSubscription.ts
  • apps/mobile/screens/(authenticated)/settings/utils/activeSubscription/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/components/WorkspaceBackButton/WorkspaceBackButton.tsx
  • apps/mobile/tsconfig.json
  • packages/chat/src/server/desktop/index.ts
  • packages/host-service/package.json
  • packages/host-service/src/trpc/index.ts
  • packages/host-service/src/trpc/router/project/handlers.ts
  • packages/host-service/src/trpc/router/workspace-creation/procedures/search-github-issues.ts
  • packages/host-service/src/trpc/router/workspace-creation/procedures/search-pull-requests.ts
  • packages/host-service/tsconfig.types.json
  • packages/port-scanner/package.json
  • packages/port-scanner/tsconfig.types.json
  • packages/pty-daemon/package.json
  • packages/pty-daemon/tsconfig.types.json
  • packages/workspace-fs/package.json
  • packages/workspace-fs/tsconfig.types.json
  • turbo.jsonc
💤 Files with no reviewable changes (30)
  • apps/mobile/screens/(authenticated)/(tabs)/(tasks)/[id]/TaskDetailScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(more)/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(search)/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceActionsSheet/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(tasks)/[id]/index.ts
  • apps/mobile/app/(authenticated)/(tabs)/(tasks)/_layout.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(more)/index.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(tasks)/tasks/TasksScreen.tsx
  • apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.tsx
  • apps/mobile/screens/(authenticated)/components/OrgDropdown/index.ts
  • apps/mobile/app/(authenticated)/(tabs)/(tasks)/index.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(more)/MoreMenuScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceRow/index.ts
  • apps/mobile/app/(authenticated)/(tabs)/(tasks)/[id].tsx
  • apps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(tasks)/tasks/index.ts
  • apps/mobile/app/(authenticated)/(tabs)/(search)/index.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/WorkspacesScreen.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(home)/index.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(search)/_layout.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(more)/settings/index.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceActionsSheet/WorkspaceActionsSheet.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceFilterSheet/index.ts
  • apps/mobile/screens/(authenticated)/components/TabBarAccessory/index.ts
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceRow/WorkspaceRow.tsx
  • apps/mobile/app/(authenticated)/(tabs)/_layout.tsx
  • apps/mobile/app/(authenticated)/(tabs)/(more)/_layout.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(search)/SearchScreen.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceFilterSheet/WorkspaceFilterSheet.tsx
  • apps/mobile/screens/(authenticated)/(tabs)/(more)/settings/SettingsScreen.tsx

Comment on lines +26 to +33
const { data: hosts } = useLiveQuery(
(q) => q.from({ v2Hosts: collections.v2Hosts }),
[collections],
);
const { data: projects } = useLiveQuery(
(q) => q.from({ v2Projects: collections.v2Projects }),
[collections],
);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that useLiveQuery from `@tanstack/react-db` returns an isReady property.
# Check the package's type declarations for the return type of useLiveQuery.
fd -t f "useLiveQuery" --search-path node_modules/@tanstack/react-db -e d.ts -e ts | head -5
rg -n "isReady" node_modules/@tanstack/react-db/dist/ --type ts -g "*.d.ts" | head -20

Repository: superset-sh/superset

Length of output: 355


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file =="
sed -n '1,220p' apps/mobile/screens/'(authenticated)'/'(home)'/filter/host/HostFilterScreen.tsx

echo
echo "== useLiveQuery / isReady usage in mobile screens =="
rg -n "useLiveQuery|isReady" apps/mobile/screens apps/mobile -g '*.ts' -g '*.tsx' | head -200

echo
echo "== nearby filter components =="
fd -t f 'HostFilter' apps/mobile/screens apps/mobile | head -20

Repository: superset-sh/superset

Length of output: 12752


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== FilterScreen =="
sed -n '1,220p' apps/mobile/screens/'(authenticated)'/'(home)'/filter/FilterScreen.tsx

echo
echo "== ProjectFilterScreen =="
sed -n '1,220p' apps/mobile/screens/'(authenticated)'/'(home)'/filter/project/ProjectFilterScreen.tsx

echo
echo "== ChatSessionsScreen =="
sed -n '1,220p' apps/mobile/screens/'(authenticated)'/workspace/'[id]'/chat/ChatSessionsScreen.tsx

echo
echo "== useHostWorkspaces =="
sed -n '1,240p' apps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.ts

Repository: superset-sh/superset

Length of output: 12162


Gate the filter rows on isReady.
hosts and projects can be undefined on first render, which makes this screen look empty and shows zero counts until the queries settle. Use isReady from both useLiveQuery calls to show a loading state when there’s no cached data yet.

🤖 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/mobile/screens/`(authenticated)/(home)/filter/host/HostFilterScreen.tsx
around lines 26 - 33, Gate the filter row rendering in HostFilterScreen on the
`isReady` values returned by both `useLiveQuery` calls for `hosts` and
`projects`. Right now the screen can render with undefined data and show empty
rows/zero counts before the queries settle; update the component to detect when
either query is not ready and show a loading state instead, only rendering the
filter counts once both queries are ready.

Source: Coding guidelines

Comment on lines +72 to +80
attachments.add(
result.assets.map((asset) => ({
mediaType: asset.mimeType,
name: asset.fileName ?? undefined,
size: asset.fileSize,
type: "image" as const,
uri: asset.uri,
})),
);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Camera asset mediaType may be undefined.

asset.mimeType from ImagePicker.launchCameraAsync can be undefined on some platforms. The library path (line 97) hardcodes "image/jpeg", but the camera path passes asset.mimeType directly. If mimeType is undefined, downstream consumers may misclassify the attachment.

Proposed fix: fallback to "image/jpeg" for camera assets
 		attachments.add(
 			result.assets.map((asset) => ({
-				mediaType: asset.mimeType,
+				mediaType: asset.mimeType ?? "image/jpeg",
 				name: asset.fileName ?? undefined,
 				size: asset.fileSize,
 				type: "image" as const,
 				uri: asset.uri,
 			})),
 		);
📝 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
attachments.add(
result.assets.map((asset) => ({
mediaType: asset.mimeType,
name: asset.fileName ?? undefined,
size: asset.fileSize,
type: "image" as const,
uri: asset.uri,
})),
);
attachments.add(
result.assets.map((asset) => ({
mediaType: asset.mimeType ?? "image/jpeg",
name: asset.fileName ?? undefined,
size: asset.fileSize,
type: "image" as const,
uri: asset.uri,
})),
);
🤖 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/mobile/screens/`(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/ContextSheet.tsx
around lines 72 - 80, The camera attachment mapping in ContextSheet currently
passes asset.mimeType directly, which can be undefined and cause inconsistent
classification. Update the asset transformation in the ContextSheet attachment
handling to fall back to "image/jpeg" for camera assets, matching the existing
library upload path, and keep the change scoped to the result.assets.map logic
that builds the attachment objects.

Comment on lines +41 to +44
const handleSignOut = () => {
onIsPresentedChange(false);
void signOut();
};

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add sign-out confirmation to match SettingsScreen.

SettingsScreen shows an Alert.alert("Log out?", ...) confirmation before signing out, but handleSignOut here signs out immediately without confirmation. A user could accidentally tap "Log out" in the switcher sheet and be signed out without warning. Align both entry points for consistent UX.

🛡️ Proposed fix to add confirmation
 const handleSignOut = () => {
-	onIsPresentedChange(false);
-	void signOut();
+	Alert.alert("Log out?", undefined, [
+		{ style: "cancel", text: "Cancel" },
+		{
+			onPress: () => {
+				onIsPresentedChange(false);
+				void signOut();
+			},
+			style: "destructive",
+			text: "Log out",
+		},
+	]);
 };
📝 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 handleSignOut = () => {
onIsPresentedChange(false);
void signOut();
};
const handleSignOut = () => {
Alert.alert("Log out?", undefined, [
{ style: "cancel", text: "Cancel" },
{
onPress: () => {
onIsPresentedChange(false);
void signOut();
},
style: "destructive",
text: "Log out",
},
]);
};
🤖 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/mobile/screens/`(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.tsx
around lines 41 - 44, `handleSignOut` in `OrganizationSwitcherSheet` signs out
immediately, unlike `SettingsScreen` which confirms first. Update this handler
to mirror the `Alert.alert("Log out?", ...)` flow used in `SettingsScreen`, so
the sheet only closes and calls `signOut` after the user confirms. Keep the
change localized to `handleSignOut` and the sign-out action in
`OrganizationSwitcherSheet`.

Comment on lines +17 to +33
function aggregateDiffStats(status: GitStatusSnapshot): DiffStats {
const byPath = new Map<string, { additions: number; deletions: number }>();
for (const file of [
...status.againstBase,
...status.staged,
...status.unstaged,
]) {
byPath.set(file.path, file);
}
let additions = 0;
let deletions = 0;
for (const file of byPath.values()) {
additions += file.additions;
deletions += file.deletions;
}
return { additions, deletions };
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the git-status utility to understand againstBase vs staged/unstaged semantics
ast-grep outline packages/host-service/src/trpc/router/git/utils/git-status.ts --items all --type function --match 'getGitStatusSnapshot|getChangedFilesForDiff'

Repository: superset-sh/superset

Length of output: 269


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the status snapshot implementation and the visible diff stats consumer.
ast-grep outline packages/host-service/src/trpc/router/git/utils/git-status.ts --items all
ast-grep outline apps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts --items all

# Read the relevant sections with line numbers.
sed -n '1,220p' packages/host-service/src/trpc/router/git/utils/git-status.ts | cat -n
printf '\n---\n'
sed -n '1,220p' apps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts | cat -n

Repository: superset-sh/superset

Length of output: 634


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git_status='packages/host-service/src/trpc/router/git/utils/git-status.ts'
visible_hook='apps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts'

wc -l "$git_status" "$visible_hook"
printf '\n--- git-status outline ---\n'
ast-grep outline "$git_status" --items all
printf '\n--- useVisibleDiffStats outline ---\n'
ast-grep outline "$visible_hook" --items all

printf '\n--- git-status relevant lines ---\n'
sed -n '1,220p' "$git_status" | cat -n
printf '\n--- useVisibleDiffStats relevant lines ---\n'
sed -n '1,220p' "$visible_hook" | cat -n

printf '\n--- references to againstBase/staged/unstaged ---\n'
rg -n 'againstBase|staged|unstaged|ignoredPaths|ChangedFile|GitStatusSnapshot' packages/host-service/src/trpc/router/git -S

Repository: superset-sh/superset

Length of output: 19425


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the type definitions and the tests that describe the three diff buckets.
sed -n '1,220p' packages/host-service/src/trpc/router/git/types.ts | cat -n
printf '\n--- git-status tests around bucket semantics ---\n'
sed -n '240,340p' packages/host-service/src/trpc/router/git/v2-diff-surfaces.integration.test.ts | cat -n
printf '\n--- any comments mentioning overlap / distinctness / diff stats ---\n'
rg -n 'distinct|overlap|againstBase|staged|unstaged|diff stats|aggregateDiffStats' packages/host-service/src/trpc/router/git apps/mobile/screens -S

Repository: superset-sh/superset

Length of output: 13769


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every consumer of the visible diff stats hook and any UI text around the counts.
rg -n 'useVisibleDiffStats|DiffStats|additions|deletions|changed lines|diff stats' apps/mobile -S

# Read the hook's immediate callers if found.

Repository: superset-sh/superset

Length of output: 5379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file='apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/WorkspaceRow.tsx'

sed -n '1,220p' "$file" | cat -n

Repository: superset-sh/superset

Length of output: 5957


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the status endpoint and workspace screen usage for the diff stats summary.
sed -n '90,180p' packages/host-service/src/trpc/router/git/git.ts | cat -n
printf '\n--- workspace screen ---\n'
sed -n '100,170p' apps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsx | cat -n

Repository: superset-sh/superset

Length of output: 3202


Preserve all bucket contributions in aggregateDiffStats. Collapsing by file.path makes unstaged overwrite staged/againstBase, so a file that appears in more than one bucket loses earlier additions/deletions in the workspace totals. Merge the per-path stats instead of last-write-wins.

🤖 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/mobile/screens/`(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts
around lines 17 - 33, The aggregateDiffStats helper is dropping contributions
when the same file appears in multiple buckets because it overwrites entries in
the byPath map by file.path; update aggregateDiffStats to merge
additions/deletions for duplicate paths instead of last-write-wins so
againstBase, staged, and unstaged all contribute to the final totals.

Comment on lines +85 to +99
return useMemo(() => {
const map = new Map<string, DiffStats>();
for (const [
queryKey,
status,
] of queryClient.getQueriesData<GitStatusSnapshot>({
queryKey: ["diff-stats"],
})) {
const workspaceId = queryKey[2];
if (typeof workspaceId === "string" && status) {
map.set(workspaceId, aggregateDiffStats(status));
}
}
return map;
}, [queryClient]);

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

useMemo missing query results dependency — diff stats never update.

The useMemo at line 85 depends only on [queryClient], which is a stable reference from useQueryClient(). This means the diff stats Map is computed once (empty, since queries are still pending on first render) and never recomputes when queries resolve. useQueries triggers re-renders when data arrives, but useMemo returns the stale empty map. As a result, diff stats (+additions −deletions) will never appear in the UI.

The _queries variable (result of useQueries) changes when query data changes but is not included in the dependency array. Rename it and add it to the deps.

🐛 Proposed fix: add query results to useMemo dependencies
-	const _queries = useQueries({
+	const queries = useQueries({
 		queries: eligible.map(({ workspaceId, hostUrl }) => ({
 			queryKey: ["diff-stats", hostUrl, workspaceId] as const,
 			staleTime: DIFF_STATS_STALE_MS,
 			gcTime: DIFF_STATS_GC_MS,
 			retry: 1,
 			networkMode: "always" as const,
 			queryFn: () =>
 				getHostServiceClientByUrl(hostUrl).git.getStatus.query({
 					workspaceId,
 					priority: "background",
 				}),
 		})),
 	});
 
 	return useMemo(() => {
 		const map = new Map<string, DiffStats>();
 		for (const [
 			queryKey,
 			status,
 		] of queryClient.getQueriesData<GitStatusSnapshot>({
 			queryKey: ["diff-stats"],
 		})) {
 			const workspaceId = queryKey[2];
 			if (typeof workspaceId === "string" && status) {
 				map.set(workspaceId, aggregateDiffStats(status));
 			}
 		}
 		return map;
-	}, [queryClient]);
+	}, [queryClient, queries]);
📝 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 useMemo(() => {
const map = new Map<string, DiffStats>();
for (const [
queryKey,
status,
] of queryClient.getQueriesData<GitStatusSnapshot>({
queryKey: ["diff-stats"],
})) {
const workspaceId = queryKey[2];
if (typeof workspaceId === "string" && status) {
map.set(workspaceId, aggregateDiffStats(status));
}
}
return map;
}, [queryClient]);
return useMemo(() => {
const map = new Map<string, DiffStats>();
for (const [
queryKey,
status,
] of queryClient.getQueriesData<GitStatusSnapshot>({
queryKey: ["diff-stats"],
})) {
const workspaceId = queryKey[2];
if (typeof workspaceId === "string" && status) {
map.set(workspaceId, aggregateDiffStats(status));
}
}
return map;
}, [queryClient, queries]);
🤖 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/mobile/screens/`(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts
around lines 85 - 99, The `useMemo` inside `useVisibleDiffStats` is only keyed
on `queryClient`, so it never recomputes when the `useQueries` results resolve
and the diff stats map stays stale. Rename `_queries` to a meaningful variable
from `useQueries`, then include that query-results variable in the dependency
array for the `useMemo` that builds the `Map<string, DiffStats>` so the stats
recalculate whenever query data changes.

Comment on lines +13 to +18
const { data: subscriptions } = useLiveQuery(
(q) => q.from({ subscriptions: collections.subscriptions }),
[collections],
);

const subscription = activeSubscription(subscriptions ?? []);

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Show a loading state instead of "Free plan" during initial query load.

useLiveQuery destructures only data, ignoring isReady. During the initial load, subscriptions is undefined, activeSubscription([]) returns undefined, and the screen renders "Free plan" — misleading the user before data has arrived. Per coding guidelines, isReady should be used to decide the empty/loading state when no cached data exists.

Proposed fix
-	const { data: subscriptions } = useLiveQuery(
+	const { data: subscriptions, isReady } = useLiveQuery(
 		(q) => q.from({ subscriptions: collections.subscriptions }),
 		[collections],
 	);
 
 	const subscription = activeSubscription(subscriptions ?? []);

Then update the conditional to distinguish loading from truly empty:

 		)>
-			{subscription ? (
+			{subscription ? (
 				<>
 					{/* ... existing rows ... */}
 				</>
+			) : !isReady ? (
+				<View className="items-center py-12">
+					<Text
+						className="text-base font-medium"
+						style={{ color: theme.mutedForeground }}
+					>
+						Loading…
+					</Text>
+				</View>
 			) : (
 				<View className="items-center py-12">
 					<Text
 						className="text-base font-medium"
 						style={{ color: theme.foreground }}
 					>
 						Free plan
 					</Text>
 				</View>
 			)}
🤖 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/mobile/screens/`(authenticated)/settings/billing/BillingSettingsScreen.tsx
around lines 13 - 18, The billing screen is treating the initial `useLiveQuery`
result as an empty subscription list, which makes `activeSubscription` fall back
to “Free plan” before data has loaded. Update `BillingSettingsScreen` to
destructure and use `isReady` from `useLiveQuery` alongside `data`, and use that
flag in the render logic to show a loading/empty state until the query is ready.
Keep the `activeSubscription` lookup for the loaded case, but distinguish
initial loading from a truly empty subscription set.

Source: Coding guidelines

@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.

🧹 Nitpick comments (1)
apps/mobile/screens/(authenticated)/settings/hosts/HostsSettingsScreen.tsx (1)

14-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use isReady to distinguish initial loading from empty state.

The useLiveQuery call destructures only data and never uses isReady. Per the coding guidelines, when using TanStack DB / Electric live queries, isReady should be used to decide the empty/loading state when no data exists. Currently, during the initial fetch (before any cached data arrives), the screen renders an empty list with no loading indicator, making it indistinguishable from a genuinely empty host list.

As per coding guidelines: "use isReady only to decide the empty/loading state when no data exists, and never hide or replace existing data just because isReady is false or isLoading is true."

♻️ Suggested refactor
-	const { data: hosts } = useLiveQuery(
+	const { data: hosts, isReady } = useLiveQuery(
 		(q) => q.from({ v2Hosts: collections.v2Hosts }),
 		[collections],
 	);

 	const hostRows = useMemo(
 		() => [...(hosts ?? [])].sort((a, b) => a.name.localeCompare(b.name)),
 		[hosts],
 	);

 	return (
 		<ScrollView
 			className="bg-background flex-1"
 			contentContainerClassName="px-6 pb-12"
 		>
+			{!isReady && hostRows.length === 0 ? (
+				<Text className="text-muted-foreground text-sm">Loading hosts…</Text>
+			) : hostRows.length === 0 ? (
+				<Text className="text-muted-foreground text-sm">No hosts found.</Text>
+			) : (
 			{hostRows.map((host, index) => (
 				<ListRow
 					key={host.machineId}
 					icon={<HostStatusDot isOnline={host.isOnline} />}
 					label={host.name}
 					trailing={
 						<Text className="text-sm" style={{ color: theme.mutedForeground }}>
 							{host.isOnline
 								? "Online"
 								: `Last seen ${formatDistanceToNow(host.updatedAt, { addSuffix: true })}`}
 						</Text>
 					}
 					isLast={index === hostRows.length - 1}
 				/>
 			))}
+			)}
 		</ScrollView>
 	);
🤖 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/mobile/screens/`(authenticated)/settings/hosts/HostsSettingsScreen.tsx
around lines 14 - 22, The HostsSettingsScreen live query currently only reads
data, so initial loading is treated the same as an empty host list. Update the
useLiveQuery usage in HostsSettingsScreen to also destructure isReady, and use
that flag when deciding whether to show the loading/empty state versus the
actual empty list. Keep the existing hostRows sorting logic unchanged, and only
use isReady to distinguish the initial no-data state from a genuinely empty
result.

Source: Coding guidelines

🤖 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.

Nitpick comments:
In `@apps/mobile/screens/`(authenticated)/settings/hosts/HostsSettingsScreen.tsx:
- Around line 14-22: The HostsSettingsScreen live query currently only reads
data, so initial loading is treated the same as an empty host list. Update the
useLiveQuery usage in HostsSettingsScreen to also destructure isReady, and use
that flag when deciding whether to show the loading/empty state versus the
actual empty list. Keep the existing hostRows sorting logic unchanged, and only
use isReady to distinguish the initial no-data state from a genuinely empty
result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 65826894-1a6e-4e34-afb8-3f89c12cb2e8

📥 Commits

Reviewing files that changed from the base of the PR and between 7f34cbc and e7cb62b.

📒 Files selected for processing (20)
  • apps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.ts
  • apps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.utils.ts
  • apps/mobile/lib/collections/collections.ts
  • apps/mobile/screens/(authenticated)/(home)/filter/FilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/host/HostFilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/project/ProjectFilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/hooks/useSelectedHost/index.ts
  • apps/mobile/screens/(authenticated)/(home)/hooks/useSelectedHost/useSelectedHost.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/HostOfflineView/HostOfflineView.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/HostOfflineView/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/BranchPickerSheet/BranchPickerSheet.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/ContextSheet.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ModelPickerSheet/ModelPickerSheet.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ModelPickerSheet/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/TargetPickerSheet/TargetPickerSheet.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/WorkspaceRow.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts
  • apps/mobile/screens/(authenticated)/settings/hosts/HostsSettingsScreen.tsx
💤 Files with no reviewable changes (2)
  • apps/mobile/lib/collections/collections.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.ts
✅ Files skipped from review due to trivial changes (2)
  • apps/mobile/screens/(authenticated)/(home)/hooks/useSelectedHost/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ModelPickerSheet/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/mobile/screens/(authenticated)/(home)/filter/project/ProjectFilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/WorkspaceRow.tsx
  • apps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.ts

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 20 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 34 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/ScreenshotGrid.tsx">

<violation number="1" location="apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/ScreenshotGrid.tsx:58">
P2: The Screenshots view can dead-end first-time users: when photo permission is undetermined or denied, it only shows static text and never requests access. Consider matching `PhotoCarousel` by showing a Continue/Open Settings action here too.</violation>

<violation number="2" location="apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/ScreenshotGrid.tsx:74">
P2: The screenshot grid uses ScrollView with flexWrap + flexDirection: "row", which is an unreliable pattern in React Native for grid layouts. Items may not wrap correctly into rows depending on the RN version and container measurements. FlatList with numColumns is the standard approach and also provides virtualization — avoiding rendering all 100 screenshot tiles at once.</violation>
</file>

<file name="apps/mobile/screens/(authenticated)/(home)/new-chat/branch/BranchPickerScreen.tsx">

<violation number="1" location="apps/mobile/screens/(authenticated)/(home)/new-chat/branch/BranchPickerScreen.tsx:60">
P2: Branch results can be stale across target switches because the query keeps previous data even when `hostUrl/projectId` change. Disabling that placeholder avoids showing/selecting branches from the prior target during refetch.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

},
});

if (!granted) {

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.

P2: The Screenshots view can dead-end first-time users: when photo permission is undetermined or denied, it only shows static text and never requests access. Consider matching PhotoCarousel by showing a Continue/Open Settings action here too.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/ScreenshotGrid.tsx, line 58:

<comment>The Screenshots view can dead-end first-time users: when photo permission is undetermined or denied, it only shows static text and never requests access. Consider matching `PhotoCarousel` by showing a Continue/Open Settings action here too.</comment>

<file context>
@@ -0,0 +1,133 @@
+		},
+	});
+
+	if (!granted) {
+		return (
+			<Text
</file context>

const { data, isLoading } = useQuery({
queryKey: ["host-service", "branches", hostUrl, projectId, trimmedQuery],
enabled: hostUrl !== null && projectId !== null,
placeholderData: (previous) => previous,

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.

P2: Branch results can be stale across target switches because the query keeps previous data even when hostUrl/projectId change. Disabling that placeholder avoids showing/selecting branches from the prior target during refetch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/screens/(authenticated)/(home)/new-chat/branch/BranchPickerScreen.tsx, line 60:

<comment>Branch results can be stale across target switches because the query keeps previous data even when `hostUrl/projectId` change. Disabling that placeholder avoids showing/selecting branches from the prior target during refetch.</comment>

<file context>
@@ -0,0 +1,164 @@
+	const { data, isLoading } = useQuery({
+		queryKey: ["host-service", "branches", hostUrl, projectId, trimmedQuery],
+		enabled: hostUrl !== null && projectId !== null,
+		placeholderData: (previous) => previous,
+		networkMode: "always" as const,
+		queryFn: async () => {
</file context>

@@ -0,0 +1,133 @@
import { useQuery } from "@tanstack/react-query";

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.

P2: The screenshot grid uses ScrollView with flexWrap + flexDirection: "row", which is an unreliable pattern in React Native for grid layouts. Items may not wrap correctly into rows depending on the RN version and container measurements. FlatList with numColumns is the standard approach and also provides virtualization — avoiding rendering all 100 screenshot tiles at once.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/ScreenshotGrid.tsx, line 74:

<comment>The screenshot grid uses ScrollView with flexWrap + flexDirection: "row", which is an unreliable pattern in React Native for grid layouts. Items may not wrap correctly into rows depending on the RN version and container measurements. FlatList with numColumns is the standard approach and also provides virtualization — avoiding rendering all 100 screenshot tiles at once.</comment>

<file context>
@@ -0,0 +1,133 @@
+		GRID_COLUMNS;
+
+	return (
+		<ScrollView
+			style={{ flex: 1 }}
+			contentContainerStyle={{
</file context>

@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: 4

🧹 Nitpick comments (2)
apps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.ts (1)

91-121: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant query key recomputation inside cache useMemo.

queryKey is already computed at line 66 from the same machineId and hostUrl values that are in the useMemo dependency array. The local key at line 92 duplicates this computation. Using queryKey directly would eliminate the redundancy without behavior change.

♻️ Proposed refactor
 	const cache = useMemo<HostWorkspacesCacheOps>(() => {
-		const key = getHostWorkspacesQueryKey(machineId, hostUrl);
 		return {
 			resolveHostUrl: (hostId) => (hostId === machineId ? hostUrl : null),
 			upsertWorkspace: (row) => {
🤖 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/mobile/hooks/useHostWorkspaces/useHostWorkspaces.ts` around lines 91 -
121, The `cache` useMemo in `useHostWorkspaces` recomputes the same host
workspaces query key even though `queryKey` is already derived from `machineId`
and `hostUrl` in the surrounding scope. Replace the local `key` usage inside
`resolveHostUrl`, `upsertWorkspace`, `removeWorkspace`, and `invalidateHost`
with the existing `queryKey` value so the logic stays the same but avoids
redundant key construction.
apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx (1)

75-95: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add staleTime to the branch query to avoid excessive refetches.

The useQuery for branch data omits staleTime, defaulting to 0. Combined with refresh: true and networkMode: "always", every app foreground event triggers a server-side git fetch. The useNewChatTargets hook sets staleTime: 60_000 on its project queries — this query should follow the same pattern.

♻️ Proposed fix
 	const { data: branchData } = useQuery({
 		queryKey: [
 			"host-service",
 			"branches",
 			selectedTarget?.hostUrl ?? null,
 			selectedTarget?.projectId ?? null,
 			"",
 		],
 		enabled: selectedTarget !== null,
+		staleTime: 60_000,
 		networkMode: "always" as const,
 		queryFn: async () => {
🤖 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/mobile/screens/`(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx
around lines 75 - 95, Add a non-zero staleTime to the branch-data useQuery in
NewChatWidget so it matches the caching behavior used by useNewChatTargets and
avoids refetching on every foreground event. Update the query options in the
branch query block (the one using selectedTarget, getHostServiceClientByUrl, and
workspaceCreation.searchBranches.query) to keep results fresh for about 60
seconds, while preserving the existing queryKey, enabled, and networkMode
settings.
🤖 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/mobile/screens/`(authenticated)/(home)/new-chat/branch/BranchPickerScreen.tsx:
- Line 87: The JSX in BranchPickerScreen is closing the outer fragment
incorrectly, causing a syntax mismatch. Update the fragment opened near the top
of the return so it is closed with the matching fragment terminator instead of a
View closing tag, and verify the surrounding View and ScrollView closures in
BranchPickerScreen remain balanced.

In
`@apps/mobile/screens/`(authenticated)/(home)/new-chat/project/ProjectPickerScreen.tsx:
- Line 13: ProjectPickerScreen is calling useNewChatTargets without workspaces,
which can make defaultTarget differ from NewChatWidget when targetKey is unset.
In ProjectPickerScreen, get the current host via useSelectedHost and its
workspaces via useHostWorkspaces, then pass those workspaces into
useNewChatTargets alongside the existing targets/defaultTarget usage so the
picker highlights the same project as the widget. Keep the change localized to
ProjectPickerScreen and the useNewChatTargets call site.

In
`@apps/mobile/screens/`(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/ScreenshotGrid.tsx:
- Around line 36-56: The ScreenshotGrid query handling currently treats failed
media-library fetches the same as empty results because only data and isLoading
are read from useQuery. Update ScreenshotGrid to also destructure isError (and
use the existing queryFn for the media fetch) so the UI can render a distinct
error state when MediaLibrary.getAlbumsAsync or MediaLibrary.getAssetsAsync
fails, while keeping the “No screenshots found” message only for true empty
results.
- Around line 29-32: The ScreenshotGrid permission flow uses
MediaLibrary.getPermissionsAsync in the active useEffect, which only reads
status and never prompts the user. Update the effect to call
MediaLibrary.requestPermissionsAsync instead, and keep setPermission handling
the returned result so the screen can request access inline when ScreenshotGrid
becomes active.

---

Nitpick comments:
In `@apps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.ts`:
- Around line 91-121: The `cache` useMemo in `useHostWorkspaces` recomputes the
same host workspaces query key even though `queryKey` is already derived from
`machineId` and `hostUrl` in the surrounding scope. Replace the local `key`
usage inside `resolveHostUrl`, `upsertWorkspace`, `removeWorkspace`, and
`invalidateHost` with the existing `queryKey` value so the logic stays the same
but avoids redundant key construction.

In
`@apps/mobile/screens/`(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx:
- Around line 75-95: Add a non-zero staleTime to the branch-data useQuery in
NewChatWidget so it matches the caching behavior used by useNewChatTargets and
avoids refetching on every foreground event. Update the query options in the
branch query block (the one using selectedTarget, getHostServiceClientByUrl, and
workspaceCreation.searchBranches.query) to keep results fresh for about 60
seconds, while preserving the existing queryKey, enabled, and networkMode
settings.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99f3937e-8bab-4fde-b31b-75e8ebdd32a6

📥 Commits

Reviewing files that changed from the base of the PR and between e7cb62b and c3e1a73.

📒 Files selected for processing (27)
  • apps/mobile/app/(authenticated)/(home)/_layout.tsx
  • apps/mobile/app/(authenticated)/(home)/new-chat/_layout.tsx
  • apps/mobile/app/(authenticated)/(home)/new-chat/branch.tsx
  • apps/mobile/app/(authenticated)/(home)/new-chat/model.tsx
  • apps/mobile/app/(authenticated)/(home)/new-chat/project.tsx
  • apps/mobile/hooks/useHostWorkspaces/index.ts
  • apps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.ts
  • apps/mobile/screens/(authenticated)/(home)/components/SheetCloseButton/SheetCloseButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/components/SheetCloseButton/index.ts
  • apps/mobile/screens/(authenticated)/(home)/filter/FilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/project/ProjectFilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/new-chat/branch/BranchPickerScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/new-chat/branch/index.ts
  • apps/mobile/screens/(authenticated)/(home)/new-chat/model/ModelPickerScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/new-chat/model/index.ts
  • apps/mobile/screens/(authenticated)/(home)/new-chat/project/ProjectPickerScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/new-chat/project/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/ContextSheet.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/ScreenshotGrid.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/index.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useNewChatTargets/useNewChatTargets.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/stores/newChatPreferencesStore/newChatPreferencesStore.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/utils/hslToHex/hslToHex.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/utils/hslToHex/index.ts
✅ Files skipped from review due to trivial changes (8)
  • apps/mobile/app/(authenticated)/(home)/new-chat/branch.tsx
  • apps/mobile/screens/(authenticated)/(home)/components/SheetCloseButton/SheetCloseButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/utils/hslToHex/index.ts
  • apps/mobile/app/(authenticated)/(home)/new-chat/project.tsx
  • apps/mobile/screens/(authenticated)/(home)/new-chat/project/index.ts
  • apps/mobile/app/(authenticated)/(home)/new-chat/model.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/index.ts
  • apps/mobile/screens/(authenticated)/(home)/components/SheetCloseButton/index.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/stores/newChatPreferencesStore/newChatPreferencesStore.ts
  • apps/mobile/screens/(authenticated)/(home)/filter/project/ProjectFilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/filter/FilterScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.tsx
  • apps/mobile/app/(authenticated)/(home)/_layout.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useNewChatTargets/useNewChatTargets.ts
  • apps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/ContextSheet.tsx

Comment on lines +29 to +32
useEffect(() => {
if (!active) return;
void MediaLibrary.getPermissionsAsync().then(setPermission);
}, [active]);

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use requestPermissionsAsync instead of getPermissionsAsync.

getPermissionsAsync only checks the current permission status without prompting the user. If permission hasn't been granted, the user sees "Allow photo access to see screenshots" but has no way to trigger a permission request from this screen — they'd need to go to system settings. Call requestPermissionsAsync to prompt the user inline.

🐛 Proposed fix
 	useEffect(() => {
 		if (!active) return;
-		void MediaLibrary.getPermissionsAsync().then(setPermission);
+		void MediaLibrary.requestPermissionsAsync().then(setPermission);
 	}, [active]);
📝 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
useEffect(() => {
if (!active) return;
void MediaLibrary.getPermissionsAsync().then(setPermission);
}, [active]);
useEffect(() => {
if (!active) return;
void MediaLibrary.requestPermissionsAsync().then(setPermission);
}, [active]);
🤖 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/mobile/screens/`(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/ScreenshotGrid.tsx
around lines 29 - 32, The ScreenshotGrid permission flow uses
MediaLibrary.getPermissionsAsync in the active useEffect, which only reads
status and never prompts the user. Update the effect to call
MediaLibrary.requestPermissionsAsync instead, and keep setPermission handling
the returned result so the screen can request access inline when ScreenshotGrid
becomes active.

Comment on lines +36 to +56
const { data: assets, isLoading } = useQuery({
queryKey: ["media-library", "screenshots"],
enabled: active && granted,
staleTime: 30_000,
queryFn: async () => {
const albums = await MediaLibrary.getAlbumsAsync({
includeSmartAlbums: true,
});
const screenshotsAlbum = albums.find(
(album) => album.type === "smartAlbum" && album.title === "Screenshots",
);
if (!screenshotsAlbum) return [];
const page = await MediaLibrary.getAssetsAsync({
album: screenshotsAlbum,
first: SCREENSHOTS_LIMIT,
mediaType: "photo",
sortBy: [["creationTime", false]],
});
return page.assets;
},
});

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add an error state to distinguish query failures from empty results.

Only data and isLoading are destructured from the query. If the media library query fails (e.g., permission revoked mid-session, I/O error), isLoading is false and assets is undefined, so the user sees "No screenshots found" — misleading since the issue is an error, not an empty library. Destructure isError and render a distinct error message.

💚 Proposed fix
-	const { data: assets, isLoading } = useQuery({
+	const { data: assets, isLoading, isError } = useQuery({
 		queryKey: ["media-library", "screenshots"],
 		enabled: active && granted,
 		staleTime: 30_000,
 			{!isLoading && (assets ?? []).length === 0 ? (
 				<Text
 					className="w-full py-10 text-center text-sm"
 					style={{ color: theme.mutedForeground }}
 				>
+					{isError ? "Failed to load screenshots" : "No screenshots found"}
-					No screenshots found
 				</Text>
 			) : null}

Also applies to: 123-130

🤖 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/mobile/screens/`(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/ScreenshotGrid.tsx
around lines 36 - 56, The ScreenshotGrid query handling currently treats failed
media-library fetches the same as empty results because only data and isLoading
are read from useQuery. Update ScreenshotGrid to also destructure isError (and
use the existing queryFn for the media fetch) so the UI can render a distinct
error state when MediaLibrary.getAlbumsAsync or MediaLibrary.getAssetsAsync
fails, while keeping the “No screenshots found” message only for true empty
results.

…ete; live diff stats

- new host-service tRPC client over the relay (user JWT; no host PSK) with
  a mobile-declared procedure contract (the real AppRouter type graph is
  Node-flavored source that can't compile under Expo TS)
- useHostWorkspaces: per-host workspace.list fan-out via v2Hosts targets,
  30s poll + focus/pull-to-refresh, Electric v2Workspaces as read-only
  fallback (PR #5452 architecture)
- rename/delete now go through the owning host (workspace.update /
  workspaceCleanup.destroy) with dirty-worktree CONFLICT + teardown-failed
  force-retry handling; Delete hidden for main workspaces
- live working-tree diff stats (git.getStatus, desktop aggregation) for
  viewport-settled rows only, replacing PR cumulative +/-
- PR badge matching now repo-scoped with desktop's state derivation and
  open<draft<merged<closed tiebreak
- host filter section in the filter sheet; EXPO_PUBLIC_RELAY_URL env
  plumbing (env.ts, setup steps)
…runtime consumers

Expo can't typecheck host-service's Node-flavored source, so mobile carried a
hand-written HostServiceClient mirror. host-service (and the workspace deps
its router types reach: port-scanner, pty-daemon, workspace-fs) now emit
d.ts via build:types; a types-only ./router export exposes AppRouter, and
mobile's client is a real createTRPCClient<AppRouter> with inferred output
aliases. Also exports the handful of types tsc needs nameable for
declaration emit.
Cursor-style composer docked where the tab bar was: prompt + project/base-
branch pickers + model selector + Photos/Camera/Files context sheet. Submit
uploads attachments to the chosen host, then workspaces.create with the
superset agent sugar - the host creates the chat session and sends the
first message; on success navigates to the new session's thread.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/host-service/docs/interim-router-types.md">

<violation number="1" location="packages/host-service/docs/interim-router-types.md:65">
P2: The revert checklist's verification grep command uses `--include="*.json" --include="*.ts"`, which omits file types that the checklist itself requires modifying. In particular, step 5 says to clean up `turbo.jsonc` (`.jsonc` is not matched by `*.json`) and `.gitignore` (no extension, unmatched by any `--include`), and the mobile app contains `.tsx` files that could reference `host-service/router`. This means the command can falsely report "empty" while leaving stale references behind.

I suggest broadening the include filters so the verification step actually checks every relevant file type, or removing `--include` filters entirely and relying on a broader search.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

drop `dist-types` from `.gitignore`.
6. The nameability `export type` shims are harmless to keep, but no longer required.
7. `grep -rn "dist-types\|build:types\|host-service/router" --include="*.json" --include="*.ts"`
should come back empty (this doc excepted).

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.

P2: The revert checklist's verification grep command uses --include="*.json" --include="*.ts", which omits file types that the checklist itself requires modifying. In particular, step 5 says to clean up turbo.jsonc (.jsonc is not matched by *.json) and .gitignore (no extension, unmatched by any --include), and the mobile app contains .tsx files that could reference host-service/router. This means the command can falsely report "empty" while leaving stale references behind.

I suggest broadening the include filters so the verification step actually checks every relevant file type, or removing --include filters entirely and relying on a broader search.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/docs/interim-router-types.md, line 65:

<comment>The revert checklist's verification grep command uses `--include="*.json" --include="*.ts"`, which omits file types that the checklist itself requires modifying. In particular, step 5 says to clean up `turbo.jsonc` (`.jsonc` is not matched by `*.json`) and `.gitignore` (no extension, unmatched by any `--include`), and the mobile app contains `.tsx` files that could reference `host-service/router`. This means the command can falsely report "empty" while leaving stale references behind.

I suggest broadening the include filters so the verification step actually checks every relevant file type, or removing `--include` filters entirely and relying on a broader search.</comment>

<file context>
@@ -0,0 +1,65 @@
+   drop `dist-types` from `.gitignore`.
+6. The nameability `export type` shims are harmless to keep, but no longer required.
+7. `grep -rn "dist-types\|build:types\|host-service/router" --include="*.json" --include="*.ts"`
+   should come back empty (this doc excepted).
</file context>

@saddlepaddle
saddlepaddle merged commit 439ae4a into main Jul 9, 2026
10 of 11 checks passed
@Kitenite Kitenite mentioned this pull request Jul 12, 2026
1 of 3 tasks

This branch was previously deployed

1 inactive deployment
preview — 61d1b839 Deployed Jul 9, 2026 by saddlepaddle via Deploy Marketing #6293
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant