Repository navigation
feat(mobile): host-owned workspace data, single-stack nav, settings, new-chat widget - #5534
Conversation
|
Too many files changed for review. ( Bypass the limit by tagging |
|
Important Review skippedToo many files! This PR contains 161 files, which is 11 over the limit of 150. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (161)
You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThe 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. ChangesMobile app navigation and workspace feature overhaul
Package TypeScript declaration build (dist-types) infrastructure
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Ready to review this PR? Stage has broken it down into 13 individual chapters for you: Chapters generated by Stage for commit 61d1b83 on Jul 9, 2026 7:46pm UTC. |
🚀 Preview Deployment🔗 Preview Links
Preview updates automatically with new commits |
There was a problem hiding this comment.
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
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| } | ||
| } | ||
| return map; | ||
| }, [queryClient]); |
There was a problem hiding this comment.
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>
| }, [queryClient]); | |
| }, [queryClient, ..._queries.map((q) => q.dataUpdatedAt)]); |
| ); | ||
| const selectedProject = | ||
| sortedProjects.find((project) => project.id === projectFilter) ?? | ||
| sortedProjects[0]; |
There was a problem hiding this comment.
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} |
There was a problem hiding this comment.
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}`, |
There was a problem hiding this comment.
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(() => {}); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.)
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 { |
There was a problem hiding this comment.
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.)
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>
There was a problem hiding this comment.
Actionable comments posted: 12
🧹 Nitpick comments (9)
turbo.jsonc (1)
56-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider adding
dependsOn: ["^build:types"]to thebuild:typestask.The
build:typestask currently has nodependsOn, soturbo build:typesruns all packages in parallel. If a future package'sbuild:typesimports types from another package'sdist-typesexport (like host-service's./router), declaration generation would fail without dependency types built first. Thetypechecktask correctly gates on^build:types, but standalonebuild:typesruns 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 valueConsider excluding bench files from declaration build.
include: ["src"]will pick upsrc/cache-and-paths-memory.bench.ts, generating an unnecessary.d.tsartifact. Consider adding an explicitexcludefor 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 valueConsider excluding test files from declaration build.
include: ["src"]will pick up test files likesrc/Pty/Pty.test.ts, generating unnecessary.d.tsartifacts. If the basetsconfig.jsondoesn't already exclude**/*.test.ts, consider adding an explicitexcludehere.♻️ 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 valueWrap
handleSubmitinuseCallbackto avoid invalidating downstream memos.
handleSubmitis recreated every render. SincePromptInputmemoizes its internalsubmitwithonSubmitas 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 valueAttachments are orphaned on the host if workspace creation fails.
If
client.workspaces.create.mutatethrows after attachments are uploaded (lines 40–46), the uploaded attachment IDs are never cleaned up. TheonErrorhandler 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 valueConsider using
isReadyto distinguish loading from empty state.Same as the billing screen,
useLiveQueryhere only destructuresdata. During initial load the list is empty with no loading indicator. For consistency with the TanStack DB coding guideline, consider destructuringisReadyand 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 winAdd
partializeto explicitly exclude setters from persistence.The
persistmiddleware currently serializes the entire state object including setter functions. WhileJSON.stringifysilently drops functions and rehydration restores them from initial state, this is implicit behavior. Addingpartializemakes 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 valueConsider memoizing
sortedProjects.The spread-and-sort runs on every render even when
projectshasn't changed. Wrapping inuseMemokeyed onprojectsavoids 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 valueConsider tightening the type guard to preserve
TRPCClientErrorproperties.The guard narrows to
{ data: HostServiceErrorData }, discarding theTRPCClientErrorbase type. If any future caller needserror.messageorerror.shapeafter the guard, they'd have to re-cast. Narrowing toTRPCClientError<any> & { data: HostServiceErrorData }would be more precise while still safe.Additionally,
deleteInProgressandteardownFailureare typed asunknownbut are used in boolean contexts downstream (if (error.data.deleteInProgress)). Typing them asbooleanwould 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
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (141)
.gitignore.superset/lib/setup/steps.shapps/mobile/app.config.tsapps/mobile/app/(authenticated)/(home)/_layout.tsxapps/mobile/app/(authenticated)/(home)/filter/_layout.tsxapps/mobile/app/(authenticated)/(home)/filter/host.tsxapps/mobile/app/(authenticated)/(home)/filter/index.tsxapps/mobile/app/(authenticated)/(home)/filter/project.tsxapps/mobile/app/(authenticated)/(home)/filter/sort.tsxapps/mobile/app/(authenticated)/(home)/index.tsxapps/mobile/app/(authenticated)/(tabs)/(home)/index.tsxapps/mobile/app/(authenticated)/(tabs)/(more)/_layout.tsxapps/mobile/app/(authenticated)/(tabs)/(more)/index.tsxapps/mobile/app/(authenticated)/(tabs)/(more)/settings/index.tsxapps/mobile/app/(authenticated)/(tabs)/(search)/_layout.tsxapps/mobile/app/(authenticated)/(tabs)/(search)/index.tsxapps/mobile/app/(authenticated)/(tabs)/(tasks)/[id].tsxapps/mobile/app/(authenticated)/(tabs)/(tasks)/_layout.tsxapps/mobile/app/(authenticated)/(tabs)/(tasks)/index.tsxapps/mobile/app/(authenticated)/(tabs)/_layout.tsxapps/mobile/app/(authenticated)/_layout.tsxapps/mobile/app/(authenticated)/settings/account.tsxapps/mobile/app/(authenticated)/settings/billing.tsxapps/mobile/app/(authenticated)/settings/hosts.tsxapps/mobile/app/(authenticated)/settings/index.tsxapps/mobile/app/(authenticated)/settings/organization.tsxapps/mobile/hooks/useHostWorkspaces/index.tsapps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.tsapps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.utils.tsapps/mobile/lib/collections/collections.tsapps/mobile/lib/env.tsapps/mobile/lib/host-service/client.tsapps/mobile/lib/host-service/errors.tsapps/mobile/metro.config.jsapps/mobile/modules/alert-prompt/expo-module.config.jsonapps/mobile/modules/alert-prompt/ios/AlertPrompt.podspecapps/mobile/modules/alert-prompt/ios/AlertPromptModule.swiftapps/mobile/modules/alert-prompt/package.jsonapps/mobile/modules/alert-prompt/src/index.tsapps/mobile/package.jsonapps/mobile/screens/(authenticated)/(home)/filter/FilterScreen.tsxapps/mobile/screens/(authenticated)/(home)/filter/components/ProjectAvatar/ProjectAvatar.tsxapps/mobile/screens/(authenticated)/(home)/filter/components/ProjectAvatar/index.tsapps/mobile/screens/(authenticated)/(home)/filter/host/HostFilterScreen.tsxapps/mobile/screens/(authenticated)/(home)/filter/host/index.tsapps/mobile/screens/(authenticated)/(home)/filter/index.tsapps/mobile/screens/(authenticated)/(home)/filter/project/ProjectFilterScreen.tsxapps/mobile/screens/(authenticated)/(home)/filter/project/index.tsapps/mobile/screens/(authenticated)/(home)/filter/sort/SortFilterScreen.tsxapps/mobile/screens/(authenticated)/(home)/filter/sort/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/BranchPickerSheet/BranchPickerSheet.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/BranchPickerSheet/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/ContextSheet.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/PhotoCarousel/PhotoCarousel.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/PhotoCarousel/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/TargetPickerSheet/TargetPickerSheet.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/TargetPickerSheet/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useCreateChatWorkspace/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useCreateChatWorkspace/useCreateChatWorkspace.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useNewChatTargets/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useNewChatTargets/useNewChatTargets.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/stores/newChatPreferencesStore/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/stores/newChatPreferencesStore/newChatPreferencesStore.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/WorkspaceRow.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/components/WorkspaceRowMenu/WorkspaceRowMenu.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/components/WorkspaceRowMenu/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.tsapps/mobile/screens/(authenticated)/(home)/workspaces/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/stores/workspacesFilterStore/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/stores/workspacesFilterStore/workspacesFilterStore.tsapps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/WorkspacesScreen.tsxapps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceActionsSheet/WorkspaceActionsSheet.tsxapps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceActionsSheet/index.tsapps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceFilterSheet/WorkspaceFilterSheet.tsxapps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceFilterSheet/index.tsapps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceRow/WorkspaceRow.tsxapps/mobile/screens/(authenticated)/(tabs)/(home)/workspaces/components/WorkspaceRow/index.tsapps/mobile/screens/(authenticated)/(tabs)/(more)/MoreMenuScreen.tsxapps/mobile/screens/(authenticated)/(tabs)/(more)/index.tsapps/mobile/screens/(authenticated)/(tabs)/(more)/settings/SettingsScreen.tsxapps/mobile/screens/(authenticated)/(tabs)/(search)/SearchScreen.tsxapps/mobile/screens/(authenticated)/(tabs)/(search)/index.tsapps/mobile/screens/(authenticated)/(tabs)/(tasks)/[id]/TaskDetailScreen.tsxapps/mobile/screens/(authenticated)/(tabs)/(tasks)/[id]/index.tsapps/mobile/screens/(authenticated)/(tabs)/(tasks)/tasks/TasksScreen.tsxapps/mobile/screens/(authenticated)/(tabs)/(tasks)/tasks/index.tsapps/mobile/screens/(authenticated)/components/HostStatusDot/HostStatusDot.tsxapps/mobile/screens/(authenticated)/components/HostStatusDot/index.tsapps/mobile/screens/(authenticated)/components/ListRow/ListRow.tsxapps/mobile/screens/(authenticated)/components/ListRow/index.tsapps/mobile/screens/(authenticated)/components/ListRowCheck/ListRowCheck.tsxapps/mobile/screens/(authenticated)/components/ListRowCheck/index.tsapps/mobile/screens/(authenticated)/components/ListRowValue/ListRowValue.tsxapps/mobile/screens/(authenticated)/components/ListRowValue/index.tsapps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.tsxapps/mobile/screens/(authenticated)/components/OrgDropdown/index.tsapps/mobile/screens/(authenticated)/components/OrganizationAvatar/OrganizationAvatar.tsxapps/mobile/screens/(authenticated)/components/OrganizationAvatar/index.tsapps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.tsxapps/mobile/screens/(authenticated)/components/TabBarAccessory/index.tsapps/mobile/screens/(authenticated)/hooks/useOrganizations/useOrganizations.tsapps/mobile/screens/(authenticated)/settings/SettingsScreen.tsxapps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsxapps/mobile/screens/(authenticated)/settings/account/index.tsapps/mobile/screens/(authenticated)/settings/billing/BillingSettingsScreen.tsxapps/mobile/screens/(authenticated)/settings/billing/index.tsapps/mobile/screens/(authenticated)/settings/components/UserAvatar/UserAvatar.tsxapps/mobile/screens/(authenticated)/settings/components/UserAvatar/index.tsapps/mobile/screens/(authenticated)/settings/hosts/HostsSettingsScreen.tsxapps/mobile/screens/(authenticated)/settings/hosts/index.tsapps/mobile/screens/(authenticated)/settings/index.tsapps/mobile/screens/(authenticated)/settings/organization/OrganizationSettingsScreen.tsxapps/mobile/screens/(authenticated)/settings/organization/index.tsapps/mobile/screens/(authenticated)/settings/utils/activeSubscription/activeSubscription.tsapps/mobile/screens/(authenticated)/settings/utils/activeSubscription/index.tsapps/mobile/screens/(authenticated)/workspace/[id]/components/WorkspaceBackButton/WorkspaceBackButton.tsxapps/mobile/tsconfig.jsonpackages/chat/src/server/desktop/index.tspackages/host-service/package.jsonpackages/host-service/src/trpc/index.tspackages/host-service/src/trpc/router/project/handlers.tspackages/host-service/src/trpc/router/workspace-creation/procedures/search-github-issues.tspackages/host-service/src/trpc/router/workspace-creation/procedures/search-pull-requests.tspackages/host-service/tsconfig.types.jsonpackages/port-scanner/package.jsonpackages/port-scanner/tsconfig.types.jsonpackages/pty-daemon/package.jsonpackages/pty-daemon/tsconfig.types.jsonpackages/workspace-fs/package.jsonpackages/workspace-fs/tsconfig.types.jsonturbo.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
| const { data: hosts } = useLiveQuery( | ||
| (q) => q.from({ v2Hosts: collections.v2Hosts }), | ||
| [collections], | ||
| ); | ||
| const { data: projects } = useLiveQuery( | ||
| (q) => q.from({ v2Projects: collections.v2Projects }), | ||
| [collections], | ||
| ); |
There was a problem hiding this comment.
🎯 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 -20Repository: 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 -20Repository: 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.tsRepository: 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
| attachments.add( | ||
| result.assets.map((asset) => ({ | ||
| mediaType: asset.mimeType, | ||
| name: asset.fileName ?? undefined, | ||
| size: asset.fileSize, | ||
| type: "image" as const, | ||
| uri: asset.uri, | ||
| })), | ||
| ); |
There was a problem hiding this comment.
🎯 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.
| 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.
| const handleSignOut = () => { | ||
| onIsPresentedChange(false); | ||
| void signOut(); | ||
| }; |
There was a problem hiding this comment.
🎯 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.
| 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`.
| 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 }; | ||
| } |
There was a problem hiding this comment.
🎯 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 -nRepository: 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 -SRepository: 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 -SRepository: 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 -nRepository: 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 -nRepository: 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.
| 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]); |
There was a problem hiding this comment.
🎯 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.
| 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.
| const { data: subscriptions } = useLiveQuery( | ||
| (q) => q.from({ subscriptions: collections.subscriptions }), | ||
| [collections], | ||
| ); | ||
|
|
||
| const subscription = activeSubscription(subscriptions ?? []); |
There was a problem hiding this comment.
🎯 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
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/mobile/screens/(authenticated)/settings/hosts/HostsSettingsScreen.tsx (1)
14-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
isReadyto distinguish initial loading from empty state.The
useLiveQuerycall destructures onlydataand never usesisReady. Per the coding guidelines, when using TanStack DB / Electric live queries,isReadyshould 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
isReadyonly to decide the empty/loading state when no data exists, and never hide or replace existing data just becauseisReadyis false orisLoadingis 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
📒 Files selected for processing (20)
apps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.tsapps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.utils.tsapps/mobile/lib/collections/collections.tsapps/mobile/screens/(authenticated)/(home)/filter/FilterScreen.tsxapps/mobile/screens/(authenticated)/(home)/filter/host/HostFilterScreen.tsxapps/mobile/screens/(authenticated)/(home)/filter/project/ProjectFilterScreen.tsxapps/mobile/screens/(authenticated)/(home)/hooks/useSelectedHost/index.tsapps/mobile/screens/(authenticated)/(home)/hooks/useSelectedHost/useSelectedHost.tsapps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/HostOfflineView/HostOfflineView.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/HostOfflineView/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/BranchPickerSheet/BranchPickerSheet.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/ContextSheet.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ModelPickerSheet/ModelPickerSheet.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ModelPickerSheet/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/TargetPickerSheet/TargetPickerSheet.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/WorkspaceRow/WorkspaceRow.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/hooks/useVisibleDiffStats/useVisibleDiffStats.tsapps/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
There was a problem hiding this comment.
All reported issues were addressed across 20 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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"; | |||
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
apps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.ts (1)
91-121: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant query key recomputation inside
cacheuseMemo.
queryKeyis already computed at line 66 from the samemachineIdandhostUrlvalues that are in theuseMemodependency array. The localkeyat line 92 duplicates this computation. UsingqueryKeydirectly 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 winAdd
staleTimeto the branch query to avoid excessive refetches.The
useQueryfor branch data omitsstaleTime, defaulting to0. Combined withrefresh: trueandnetworkMode: "always", every app foreground event triggers a server-side git fetch. TheuseNewChatTargetshook setsstaleTime: 60_000on 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
📒 Files selected for processing (27)
apps/mobile/app/(authenticated)/(home)/_layout.tsxapps/mobile/app/(authenticated)/(home)/new-chat/_layout.tsxapps/mobile/app/(authenticated)/(home)/new-chat/branch.tsxapps/mobile/app/(authenticated)/(home)/new-chat/model.tsxapps/mobile/app/(authenticated)/(home)/new-chat/project.tsxapps/mobile/hooks/useHostWorkspaces/index.tsapps/mobile/hooks/useHostWorkspaces/useHostWorkspaces.tsapps/mobile/screens/(authenticated)/(home)/components/SheetCloseButton/SheetCloseButton.tsxapps/mobile/screens/(authenticated)/(home)/components/SheetCloseButton/index.tsapps/mobile/screens/(authenticated)/(home)/filter/FilterScreen.tsxapps/mobile/screens/(authenticated)/(home)/filter/project/ProjectFilterScreen.tsxapps/mobile/screens/(authenticated)/(home)/new-chat/branch/BranchPickerScreen.tsxapps/mobile/screens/(authenticated)/(home)/new-chat/branch/index.tsapps/mobile/screens/(authenticated)/(home)/new-chat/model/ModelPickerScreen.tsxapps/mobile/screens/(authenticated)/(home)/new-chat/model/index.tsapps/mobile/screens/(authenticated)/(home)/new-chat/project/ProjectPickerScreen.tsxapps/mobile/screens/(authenticated)/(home)/new-chat/project/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/NewChatWidget.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/ContextSheet.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/ScreenshotGrid.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/components/ContextSheet/components/ScreenshotGrid/index.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/hooks/useNewChatTargets/useNewChatTargets.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/NewChatWidget/stores/newChatPreferencesStore/newChatPreferencesStore.tsapps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.tsxapps/mobile/screens/(authenticated)/(home)/workspaces/utils/hslToHex/hslToHex.tsapps/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
| useEffect(() => { | ||
| if (!active) return; | ||
| void MediaLibrary.getPermissionsAsync().then(setPermission); | ||
| }, [active]); |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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; | ||
| }, | ||
| }); |
There was a problem hiding this comment.
📐 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.
277ff00 to
eaec939
Compare
…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)
…arch; settings via org switcher
…store; fix list header inset overlap
…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.
There was a problem hiding this comment.
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). |
There was a problem hiding this comment.
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>
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)
useHostWorkspacesfans outworkspace.listto every online host via the relay (user JWT; no host secret on device), merges results, and falls back to the Electricv2_workspacescollection for hosts that don't answer (R3 removes the fallback). 30s poll + focus/pull-to-refresh; no event bus.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).workspace.update; delete →workspaceCleanup.destroywith dirty-worktree/teardown force-retry alerts and Delete hidden formainworkspaces. Fixes the previous cloud-delete path that orphaned worktrees and left the authoritative host row alive.git.getStatus(settled-visible rows only, capped) aggregated exactly like desktop'suseDiffStats; rendered from the whole react-query cache so scroll-backs don't flicker. Replaces PR cumulative +/− on rows.${repositoryId}::${headBranch}with desktop's state derivation (mergedAt → draft → closed → open) and tiebreaks.Navigation & home
formSheetroute with native pushed subpages (Project / Host / Sort) backed by a persisted zustand store; generic filter toolbar icon.worktree missinghint.New-chat widget (workspace creation)
workspaces.createand launches an agent.workspaceCreation.searchBranches.Settings first pass
subscriptionsElectric collection; "manage from desktop").screens/(authenticated)/components(ListRowfamily) and reused by filter + settings.Native/dev plumbing
EXPO_PUBLIC_RELAY_URLenv (env.ts, setup script), zustand + date-fns deps,alert-promptlocal Swift module.expo-image-manipulator@56.0.17andexpo-media-library@56.0.6pinned to theexpo-modules-core 56.0.15batch (prebuilts released after core's date crash dyld at launch with missingRecord.fromsymbol).Test plan
biome check+ mobiletsc --noEmitgreenworkspace.list200s), cloud fallback renders for silent hosts, diff stats fetch only for settled-visible rowsSummary 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
workspace.listover the relay with 30s poll and focus/refresh; no Electric fallback.workspace.update; delete withworkspaceCleanup.destroyand conflict/teardown handling; Delete hidden formain.git.getStatuswith desktop-style aggregation; PR badges match desktop logic.formSheetwith native subpages and a persistedzustandstore; single-host view with an offline placeholder and Switch Host CTA.expo-routerformSheets, photos/screenshots context, and uploads to the host; callsworkspaces.createthen launches an agent; native close buttons on all sheets.subscriptions).useWorkspaceHostresolves the owning host by querying online hosts and ignores disabled queries while resolving.Migration
EXPO_PUBLIC_RELAY_URLto the mobile.env(setup script writes it).expo-modules-core@56.0.15,expo-image-manipulator@56.0.17, andexpo-media-library@56.0.6.@superset/alert-prompt; run a fresh prebuild/Pods install if needed.expo-media-libraryplugin with a photos permission prompt (alongsideexpo-image-picker).Written for commit 61d1b83. Summary will update on new commits.
Summary by CodeRabbit
New Features
Settings
UI & Navigation