Skip to content

fix(desktop): derive v2 workspace locality from workspace.hostId, not joined v2Hosts#3837

Merged
saddlepaddle merged 1 commit into
mainfrom
fix-null-id-local-default
Apr 28, 2026
Merged

fix(desktop): derive v2 workspace locality from workspace.hostId, not joined v2Hosts#3837
saddlepaddle merged 1 commit into
mainfrom
fix-null-id-local-default

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Apr 28, 2026

Summary

  • Workspaces that loaded while Electric was still syncing v2Hosts were routed through the cloud relay instead of the local host service. The locality check in v2-workspace/layout.tsx (and 5 sibling sites) leftJoined v2Workspaces → v2Hosts and compared joined.hostMachineId === machineId — when Electric hadn't synced the host row, the joined value was null, the equality failed, and the entire WorkspaceTrpcProvider got pointed at ${RELAY_URL}/hosts/....
  • v2Workspaces.hostId is notNull and FKs to v2Hosts.machineId, so the workspace already carries the value the join was producing. Switched locality to workspace.hostId === machineId everywhere. Routing-only paths drop the v2Hosts join entirely; metadata-display paths (sidebar / list) keep an innerJoin for host name + isOnline.
  • Side-fixes for the same root cause: useAccessibleV2Workspaces now stays innerJoin (cleaner; rows briefly hide while Electric catches up but workspaces remain routable via direct URL); DashboardSidebarPortsList synthesizes a local-host port-query target so local ports show without a synced v2Hosts row; DevicePicker synthesizes "this device" so the picker never loses the local entry.

Test plan

  • bun run lint:fix — clean
  • bun run typecheck — clean
  • bun test (desktop) — 1870 pass
  • Manually clear v2Hosts in tanstack-db inspector, navigate to /v2-workspace/$workspaceId, confirm WorkspaceTrpcProvider connects to http://127.0.0.1:<port> (not the relay) and the workspace loads
  • DevicePicker: with v2Hosts cleared, "this device" still appears as default
  • Sidebar ports: with v2Hosts cleared, local ports still render
  • Don't regress remote routing: open a workspace whose hostId is a different machine; confirm it still routes via ${RELAY_URL}/hosts/<key>

Summary by cubic

Fixes v2 workspace routing by deriving locality from v2Workspaces.hostId === machineId, avoiding relay misrouting when v2Hosts hasn’t synced. Also keeps local ports and “this device” visible even if the local host row is missing.

  • Bug Fixes

    • Route workspace TRPC, notifications, terminal, and “Open in” via local host when hostId === machineId; otherwise use the relay URL.
    • Prevent local ports from disappearing by synthesizing a local host target from machineId + activeHostUrl.
    • Ensure DevicePicker always shows “this device” using machineId when the local v2Hosts row is missing.
  • Refactors

    • Removed v2Hosts leftJoin from routing-only queries; compare hostId directly.
    • Kept innerJoin to v2Hosts only where host metadata (name, isOnline) is displayed.
    • Simplified hostType derivation to hostId === machineId ? "local-device" : "remote-device".

Written for commit 5e6513a. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Refactor
    • Simplified workspace-to-host relationship handling across the application.
    • Streamlined local and remote device classification logic.
    • Optimized database queries for improved performance.

… joined v2Hosts

Locality decisions previously leftJoined v2Workspaces -> v2Hosts and compared
the joined hostMachineId to the local machineId. Electric was syncing v2Hosts
spottily, so the joined value was often null and the code routed local
workspaces through the cloud relay (most visibly: v2-workspace/layout.tsx
sending the entire WorkspaceTrpcProvider at the relay URL while waiting on
Electric).

v2Workspaces.hostId is notNull and itself FKs to v2Hosts.machineId, so the
workspace already carries the same value the join was producing. Switch
locality to workspace.hostId === machineId everywhere; routing-only paths
drop the v2Hosts join entirely. Metadata-display paths keep an innerJoin to
v2Hosts (for host name + isOnline) — when v2Hosts lags, the row briefly
omits from the listing but stays routable via direct URL.

Also: synthesize a local target in DashboardSidebarPorts and DevicePicker so
local ports / "this device" never disappear while Electric catches up.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Walkthrough

This PR systematically removes the derived hostMachineId field from workspace-related data structures across multiple hooks and components. Logic determining local/remote workspace routing is refactored to compare hostId directly with machineId, eliminating left-joins with the hosts table and simplifying host classification to "local-device" or "remote-device".

Changes

Cohort / File(s) Summary
Workspace host URL routing hooks
useWorkspaceHostUrl.ts, useOpenInExternalEditor.ts, useGlobalTerminalLifecycle.ts
Removed leftJoin with v2Hosts and hostMachineId derivation. Updated local-workspace detection to compare hostId directly against machineId.
Dashboard sidebar data utilities
useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts, useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts
Removed hostMachineId from DashboardSidebarWorkspaceRow interface and simplified host classification logic. Refactored deriveHostPortQueryTargets with fallback synthesis of local ports query target using activeHostUrl.
Dashboard sidebar ports data tests
useDashboardSidebarPortsData/useDashboardSidebarPortsData.test.ts
Removed hostMachineId fields from test workspace fixtures and updated machineId values to align with host ID expectations.
Dashboard sidebar and layout components
useDashboardSidebarData.ts, layout.tsx, V2WorkspaceOpenInButton.tsx
Changed workspace-to-host fetching from nullable leftJoin to mandatory innerJoin. Updated local/remote determination and hostType derivation to use hostId comparison, eliminating "cloud" classification.
Workspace accessibility and selection hooks
useAccessibleV2Workspaces.ts, useWorkspaceHostOptions.ts
Removed hostMachineId from AccessibleV2Workspace interface. Updated host classification and fallback logic to rely on hostId matching; added fallback for currentDeviceName when localHost is absent.
Notification and lifecycle management
V2NotificationController.tsx
Removed hostMachineId from WorkspaceHostRow and updated URL-selection condition to compare hostId === machineId directly.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hops through workspaces, swift and clean,
No more joins to hosts between,
HostId speaks the truth so clear—
Local whispers in your ear!
Clouds have vanished, paths align,
Refactored code—simply divine!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: switching workspace locality derivation from a joined v2Hosts field to the direct hostId field comparison.
Description check ✅ Passed The description provides a comprehensive summary of the root cause, primary fix, side fixes, and detailed test plan. It covers all required template sections and explains the technical rationale clearly.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-null-id-local-default

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

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 28, 2026

Greptile Summary

This PR fixes a race condition where v2 workspaces loaded while Electric was still syncing v2Hosts were incorrectly routed through the cloud relay instead of the local host service. The fix replaces leftJoin(v2Workspaces → v2Hosts) + hostMachineId comparisons with direct workspace.hostId === machineId checks across all 6 routing-critical call sites, exploiting the fact that v2Workspaces.hostId is notNull and already carries the FK value the join was producing. Metadata-display paths (sidebar, list) keep an innerJoin for host name/isOnline but acknowledge the brief hide window. The synthesized local-target logic in deriveHostPortQueryTargets and useWorkspaceHostOptions provides additional robustness for the ports list and device picker during the sync gap.

Confidence Score: 5/5

Safe to merge — the fix is logically sound, consistently applied across all affected sites, and all remaining findings are P2 or lower.

The root cause is well-understood and the fix correctly exploits the FK invariant (v2Workspaces.hostId IS v2Hosts.machineId). All 6 routing sites are updated consistently, the test fixtures are corrected to reflect the real schema invariant, and the synthesis fallbacks for ports and device picker are well-guarded. No P0/P1 issues were found.

No files require special attention.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts Core routing fix: drops leftJoin with v2Hosts, compares workspace.hostId directly to machineId — eliminates race condition that sent local workspaces through relay.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx Primary layout fix: drops leftJoin, uses workspace.hostId === machineId for isLocal — WorkspaceTrpcProvider now points at local host immediately without waiting for Electric to sync v2Hosts.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts Removes hostMachineId from DashboardSidebarWorkspaceRow interface; adds synthesis of local port-query target when v2Hosts row hasn't synced; eliminates spurious "cloud" hostType.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts leftJoin → innerJoin for v2Hosts (intentional: rows briefly hidden during sync); hostMachineId removed; hostIsOnline now non-nullable from innerJoin (compatible with boolean
apps/desktop/src/renderer/routes/_authenticated/components/V2NotificationController/V2NotificationController.tsx Removes leftJoin and hostMachineId; getHostUrlForWorkspace uses hostId === machineId instead of the old hostMachineId guard — local notifications route correctly before Electric syncs.
apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts Synthesizes "This device" fallback when v2Hosts row hasn't synced, ensuring the picker always shows the local entry.
apps/desktop/src/renderer/routes/_authenticated/components/GlobalTerminalLifecycle/hooks/useGlobalTerminalLifecycle/useGlobalTerminalLifecycle.ts Drops leftJoin; terminal URL mapping now uses workspace.hostId directly, same fix pattern as other routing sites.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts Removes hostMachineId from AccessibleV2Workspace interface; hostId now sourced from workspaces.hostId (same FK value as hosts.machineId); "cloud" hostType eliminated.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.test.ts Test fixtures updated to remove hostMachineId; machineId corrected to match hostId (reflecting the real FK invariant), fixing an artificial split in the old test setup.

Sequence Diagram

sequenceDiagram
    participant App
    participant LiveQuery
    participant v2Workspaces
    participant v2Hosts
    participant LocalHostService

    Note over App,v2Hosts: Before fix (race condition)
    App->>LiveQuery: leftJoin(v2Workspaces, v2Hosts)
    v2Workspaces-->>LiveQuery: workspace { hostId: "abc" }
    v2Hosts-->>LiveQuery: null (Electric not synced yet)
    LiveQuery-->>App: hostMachineId = null
    App->>App: null === machineId? → false → RELAY URL ❌

    Note over App,v2Hosts: After fix
    App->>LiveQuery: from(v2Workspaces) only
    v2Workspaces-->>LiveQuery: workspace { hostId: "abc" }
    LocalHostService-->>App: machineId = "abc"
    App->>App: hostId === machineId? → true → LOCAL URL ✅

    Note over App,v2Hosts: Sidebar / list (innerJoin — display only)
    App->>LiveQuery: innerJoin(v2Workspaces, v2Hosts)
    v2Hosts-->>LiveQuery: null (not synced) → row hidden briefly
    Note right of App: Workspace still routable via direct URL
Loading

Reviews (1): Last reviewed commit: "fix(desktop): derive v2 workspace locali..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts (1)

152-156: Narrow this hook’s host-type contract.

After this change, hostType is binary here, but V2WorkspaceHostType and V2WorkspaceDeviceCounts still expose a "cloud" state that this hook can no longer produce. That leaves an impossible branch and a permanently-zero counts.cloud. Consider dropping "cloud" from this hook-local contract, or documenting why callers still need it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts`
around lines 152 - 156, The hook useAccessibleV2Workspaces now only produces two
host values but still uses V2WorkspaceHostType and V2WorkspaceDeviceCounts which
include a "cloud" branch; update the hook to use a narrowed local type and
counts shape (or map/omit "cloud") so the impossible "cloud" branch and zero
counts.cloud are removed. Specifically, replace/alias the hook-local hostType
with a two-value union (e.g., "local-device" | "remote-device") or create a
V2WorkspaceHostTypeLocalRemote used only by useAccessibleV2Workspaces, and
adjust any counts construction (V2WorkspaceDeviceCounts usage) to exclude or map
the "cloud" key so callers of useAccessibleV2Workspaces no longer see an
unreachable "cloud" branch (ensure symbols to change include
useAccessibleV2Workspaces, hostType, and
V2WorkspaceDeviceCounts/V2WorkspaceHostType).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts`:
- Around line 152-156: The hook useAccessibleV2Workspaces now only produces two
host values but still uses V2WorkspaceHostType and V2WorkspaceDeviceCounts which
include a "cloud" branch; update the hook to use a narrowed local type and
counts shape (or map/omit "cloud") so the impossible "cloud" branch and zero
counts.cloud are removed. Specifically, replace/alias the hook-local hostType
with a two-value union (e.g., "local-device" | "remote-device") or create a
V2WorkspaceHostTypeLocalRemote used only by useAccessibleV2Workspaces, and
adjust any counts construction (V2WorkspaceDeviceCounts usage) to exclude or map
the "cloud" key so callers of useAccessibleV2Workspaces no longer see an
unreachable "cloud" branch (ensure symbols to change include
useAccessibleV2Workspaces, hostType, and
V2WorkspaceDeviceCounts/V2WorkspaceHostType).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 095a53cb-ac73-46af-8942-063709bd9e83

📥 Commits

Reviewing files that changed from the base of the PR and between 725c633 and 5e6513a.

📒 Files selected for processing (12)
  • apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.utils.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/hooks/useAccessibleV2Workspaces/useAccessibleV2Workspaces.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/GlobalTerminalLifecycle/hooks/useGlobalTerminalLifecycle/useGlobalTerminalLifecycle.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/V2NotificationController/V2NotificationController.tsx

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 12 files

@saddlepaddle saddlepaddle merged commit 1078469 into main Apr 28, 2026
7 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch

Thank you for your contribution! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant