Skip to content

feat: unified WS event bus, v2Hosts data model, real diff stats#3224

Merged
saddlepaddle merged 12 commits into
mainfrom
saddlepaddle/nickel-bladder
Apr 7, 2026
Merged

feat: unified WS event bus, v2Hosts data model, real diff stats#3224
saddlepaddle merged 12 commits into
mainfrom
saddlepaddle/nickel-bladder

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Apr 7, 2026

Summary

  • Unified WS event bus: Single /events WebSocket endpoint on host-service replacing per-workspace filesystem WS connections. Carries git:changed (auto-pushed for all workspaces) and fs:events (on-demand per client request). Client-side connection manager with reconnection and ref-counting.
  • v2Hosts/v2Clients data model: Replaced v2Devices/v2DevicePresence/v2UsersDevices with v2Hosts (machineId NOT NULL, lastSeenAt), v2Clients (machineId, userId, type: desktop/mobile/web), v2UsersHosts (owner/member). v2Workspaces.deviceIdhostId.
  • Real diff stats: Left sidebar chips show actual +N / -N from git.getStatus, triggered by git:changed events instead of polling. Multi-host routing ready (local → localhost, remote → proxy URL pattern).
  • GIT_OPTIONAL_LOCKS=0: Prevents index.lock contention between host-service git reads and user git operations.
  • Changes tab fix: Header stats now update when switching the filter dropdown (all/uncommitted/commit).
  • PR polling: Bumped to 10s (was 15s on client, 30s on host-service).

Test plan

  • Left sidebar diff stats show real data matching git diff --shortstat
  • Making code changes updates the diff stats chip (git watcher fires)
  • File tree still works (filesystem events over unified event bus)
  • Network tab shows single /events WS connection per host
  • git checkout / git commit in terminal doesn't hit index.lock errors
  • Changes tab header stats update when switching filter dropdown
  • Cloud workspaces show no diff stats chip
  • bun run lint:fix && bun run typecheck passes

Summary by cubic

Unifies host-service events behind a single /events WebSocket and completes the v2Hosts/v2Clients migration, delivering live diff stats and an event‑driven Changes tab. Reduces connections, improves multi‑host routing, and avoids git lock conflicts.

  • New Features

    • Single /events WS per host emits git:changed (auto) and fs:events (opt‑in); one shared connection with reconnection and ref‑counting.
    • Real diff stats (+N/−N) via useDiffStats and useWorkspaceEvent; dedupes by path to avoid double‑counting; Changes tab invalidates status/commits on git:changed (no polling).
    • Data model: v2Hosts, v2Clients, v2UsersHosts; workspaces now use hostId. ensureV2Host(machineId) and new ensureV2Client.
  • Bug Fixes

    • More reliable host URL resolution and multi‑host routing (local service when present, else proxy api/v2-hosts/:hostId/trpc).
    • Event bus fixes: ref‑counted watchFs/unwatchFs; 300ms per‑workspace debounce in GitWatcher; removed legacy FS event bridge in workspace-client.
    • Set GIT_OPTIONAL_LOCKS=0 to avoid index.lock contention.
    • PR polling set to 10s; small UI polish (diff‑stats pill sizing, LuFolderGit2, editor width); cleanup of stale DB enums; re‑exported FS types from @superset/workspace-fs/client.

Written for commit 1aeccdc. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Real-time event bus and client hooks for git and filesystem events; workspace components now subscribe to live git/fs notifications.
    • Live diff stats in the workspace sidebar (replacing mocked values).
  • Refactor

    • Platform model migrated from device→host: host-based selection, local vs remote detection, routing, and workspace metadata updated.
    • Host-focused UI: pickers, icons, and workspace forms reflect host/cloud semantics.
  • Performance

    • More responsive updates: reduced polling intervals and event-driven invalidation replacing frequent polling.

- Add unified /events WebSocket endpoint on host-service replacing
  per-workspace filesystem WS connections. Carries git:changed (auto)
  and fs:events (on-demand) over a single connection per host.
- Add v2_hosts, v2_clients, v2_users_hosts tables replacing v2_devices,
  v2_device_presence, v2_users_devices. Workspaces now reference hostId
  instead of deviceId. Hosts identified by machineId (null = cloud).
- Wire real git diff stats into left sidebar chips using event bus
  to trigger refetches instead of polling.
- Update tRPC routers (ensureV2Host, ensureV2Client, workspace create),
  Electric SQL sync, and all renderer references.
- Route diff stats per-workspace to correct host (local vs remote proxy URL)
- Set GIT_OPTIONAL_LOCKS=0 on all host-service git operations to prevent
  index.lock contention with user git commands
- Fix Changes tab header stats not updating when switching filter dropdown
- Fix diff stats pill padding (w-fit + justify-self-end)
- Restore LuFolderGit2 icon for local workspaces
- Update WorkspaceHostTarget kind from "device" to "host"
- Make v2_hosts.machineId NOT NULL (every host is a machine)
- Bump PR polling to 10s (host-service + client)
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces device-centric model with host/client entities, migrates DB (devices → hosts/clients/users_hosts), adds an EventBus and GitWatcher for fs/git events, removes legacy filesystem WebSocket APIs, and updates API, collections, trpc, workspace-client, and desktop UI/hooks to use hostId and the new event-bus plumbing.

Changes

Cohort / File(s) Summary
DB migration & schema
packages/db/drizzle/0031_v2_hosts_and_clients.sql, packages/db/drizzle/meta/_journal.json, packages/db/src/schema/enums.ts, packages/db/src/schema/schema.ts, packages/db/src/schema/relations.ts
Add v2_hosts/v2_clients/v2_users_hosts and enums; drop legacy device tables/enums; migrate v2_workspaces to host_id.
Electric / API where routing
apps/api/src/app/api/electric/[...path]/utils.ts, apps/electric-proxy/src/where.ts
Switch Electric table names and where-clause routing from v2_devices/v2_device_presence/v2_users_devicesv2_hosts/v2_clients/v2_users_hosts.
Collections provider
apps/desktop/.../providers/CollectionsProvider/collections.ts
Replace collections and OrgCollections surface to expose v2Hosts/v2Clients/v2UsersHosts instead of device collections.
TRPC & device/router changes
packages/trpc/src/router/device/device.ts, packages/trpc/src/router/v2-workspace/v2-workspace.ts
Rename inputs/logic from clientId/deviceIdmachineId/hostId; upserts target host/client tables; add ensureV2Client.
Host-service: EventBus & GitWatcher
packages/host-service/src/events/event-bus.ts, packages/host-service/src/events/git-watcher.ts, packages/host-service/src/events/types.ts, packages/host-service/src/events/index.ts, packages/host-service/src/app.ts, packages/host-service/package.json
Introduce EventBus with /events WebSocket route, fs:watch/fs:unwatch protocol, GitWatcher emitting git:changed, route registration, and new exports.
Host-service: remove legacy FS ws
packages/host-service/src/filesystem/events.ts, packages/host-service/src/filesystem/index.ts, packages/host-service/src/index.ts
Remove legacy workspace-filesystem WebSocket module and re-exports; expose EventBus types/aliases instead.
Host-service runtime & timing tweaks
packages/host-service/src/runtime/git/git.ts, packages/host-service/src/runtime/pull-requests/pull-requests.ts, packages/host-service/src/trpc/router/workspace/workspace.ts
Inject GIT_OPTIONAL_LOCKS=0 into git env, shorten project refresh interval (30s→10s), and create workspaces using hostId.
Workspace-client: client EventBus & hooks
packages/workspace-client/src/lib/eventBus.ts, packages/workspace-client/src/hooks/useEventBus/*, packages/workspace-client/src/hooks/useGitChangeEvents/*, packages/workspace-client/src/lib/workspaceFsEventRegistry.ts, packages/workspace-client/src/providers/WorkspaceClientProvider/*, packages/workspace-client/src/index.ts
Add client EventBus with reconnection and scoped listeners, getEventBus/useEventBus/useGitChangeEvents, remove legacy fs subscription registry and hooks, update provider and public exports.
Desktop host model, URL & queries
apps/desktop/src/renderer/lib/v2-workspace-host.ts, apps/desktop/.../v2-workspace/layout.tsx, apps/desktop/.../V2WorkspaceOpenInButton.tsx
Replace device-targets with host-targets (hostId); add getRemoteHostUrl(hostId); live queries use v2Hosts.machineId for local detection and route host URLs accordingly.
Desktop UI: diff stats, types & icons
apps/desktop/.../DashboardSidebarWorkspaceItem/*, .../useDashboardDiffStats/*, .../types.ts, .../DashboardSidebarWorkspaceDiffStats/*.tsx, .../useWorkspaceEvent/*, .../utils/getWorkspaceRowMocks.ts
Remove deterministic workspace mock generator; add useDiffStats to fetch git status from host; propagate diffStats through hover/expanded rows; workspace identity uses hostId; minor styling/icon tweaks; deleted mock utilities.
Desktop New Workspace modal & picker
apps/desktop/.../DevicePicker/*, .../useWorkspaceHostOptions/*
Rename device picker to host picker; option shape → { id, name, isCloud }; fetch hosts via v2UsersHosts/v2Hosts; selection produces kind: "host", hostId.
Workspace changes tab & polling→events
apps/desktop/.../useChangesTab/useChangesTab.tsx
Replace fixed polling with git:changed event invalidation; compute filtered/deduplicated file lists per filter and derive totals from those lists.
Files / FS hooks wiring
multiple files under apps/desktop/.../v2-workspace/... and packages/workspace-fs/src/client/index.ts
Replace useWorkspaceFsEvents/bridge usages with useWorkspaceEvent("fs:events", ...); switch some imports to local hooks and export FS types from workspace-fs client.
Barrels & re-exports
multiple hooks/*/index.ts, apps/desktop/.../utils/index.ts, packages/workspace-client/src/index.ts
Add/remove barrel exports to expose new event-bus hooks and remove filesystem subscription APIs and removed utilities.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant UI as Desktop UI / Hook
participant Client as Workspace Client EventBus
participant HostSvc as Host Service (/events)
participant GitW as HostService GitWatcher
participant HostFS as Host Filesystem

UI->>Client: getEventBus(hostUrl).on("git:changed", workspaceId, cb)
Client->>HostSvc: open/maintain ws wss://host/events?token=...
GitW->>HostSvc: detects git change -> emit {"type":"git:changed","workspaceId":...}
HostSvc->>Client: {"type":"git:changed","workspaceId":...}
Client->>UI: invoke registered callbacks with workspaceId

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped from device to host with glee,

Websockets hum and events drift free,
Git bell rings, files whisper to me,
I nibble diffs and hop with joy, you see,
Hooray — hosts hold the keys to tree.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely summarizes the main changes: unified WebSocket event bus, v2Hosts data model migration, and real diff stats implementation.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all major changes including unified WS event bus, v2Hosts/v2Clients data model migration, real diff stats, and git concurrency fixes.

✏️ 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 saddlepaddle/nickel-bladder

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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 7, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 7, 2026

Greptile Summary

This PR introduces a unified /events WebSocket endpoint on the host-service that replaces multiple per-workspace filesystem WebSocket connections, and refactors the v2Devices/v2DevicePresence/v2UsersDevices data model into a cleaner v2Hosts/v2Clients/v2UsersHosts schema. It also delivers real diff stats in the sidebar by subscribing to git:changed events from a new GitWatcher.

Key changes:

  • Unified event bus (EventBus class + registerEventBusRoute): single /events WS per host, multiplexing git:changed broadcasts and on-demand fs:events subscriptions with proper ref-counting and reconnect with exponential backoff
  • v2Hosts data model: machineId NOT NULL, lastSeenAt replaces clientId/type/presence table; new v2Clients tracks per-user session type; v2UsersHosts for owner/member ACL
  • Real diff stats: useDashboardDiffStats + GitWatcher replace polling; GIT_OPTIONAL_LOCKS=0 prevents index.lock contention
  • PR polling interval: bumped from 30s (host-service) / 15s (client) to 10s

Issues found:

  • The cleanup effect in useDashboardDiffStats has [workspaceHosts.map] as its dependency — a stable Array.prototype method reference that never changes — so stale stats entries will accumulate when workspaces are removed
  • The destructive DELETE FROM \"v2_workspaces\" at the top of migration 0031 permanently wipes all workspace records in every environment the migration runs against
  • GitWatcher fires one event per .git file change with no coalescence; a single git commit triggers 4–6 concurrent getStatus RPC calls
  • _workspaceHostsKey at line 57 is computed but never used — it was clearly intended as the stable dependency for the cleanup effect"

Confidence Score: 3/5

Not safe to merge without fixing the cleanup effect bug and reviewing the destructive migration

The unified event bus architecture is well-structured, the data model refactor is clean, and the client reconnection logic is solid. However, two issues require attention before merging: the [workspaceHosts.map] dependency bug in useDashboardDiffStats causes permanent stale state accumulation (a real correctness defect in a new feature), and the DELETE FROM v2_workspaces in the migration irreversibly destroys all workspace data. The debounce gap is a performance concern that adds unnecessary RPC load. Fixing the cleanup bug and documenting/reconsidering the migration deletion would bring this to a 4.

useDashboardDiffStats.ts (cleanup effect logic bug) and 0031_v2_hosts_and_clients.sql (destructive DELETE statement)

Important Files Changed

Filename Overview
packages/host-service/src/events/event-bus.ts New unified WebSocket event bus handling git:changed broadcasts and per-client fs:events subscriptions with proper client cleanup
packages/host-service/src/events/git-watcher.ts Watches .git directories via fs.watch for all workspaces; lacks debouncing so a single git operation fires multiple rapid events
packages/workspace-client/src/lib/eventBus.ts Client-side WS connection manager with ref-counting, exponential-backoff reconnect, and typed event dispatch
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardDiffStats/useDashboardDiffStats.ts New diff stats hook has a cleanup effect dependency bug ([workspaceHosts.map]) that prevents stale entries from being pruned, and an unused _workspaceHostsKey variable
packages/db/drizzle/0031_v2_hosts_and_clients.sql Migration replacing v2Devices model starts with DELETE FROM v2_workspaces, permanently destroying all workspace records before adding the NOT NULL host_id column
packages/db/src/schema/schema.ts Data model cleanly refactored from v2Devices/v2DevicePresence/v2UsersDevices to v2Hosts/v2Clients/v2UsersHosts with machineId as the stable identifier
packages/workspace-client/src/lib/workspaceFsEventRegistry.ts Refactored to use unified event bus instead of per-workspace WebSocket connections; subscription lifecycle correctly wired to bus.watchFs/unwatchFs

Sequence Diagram

sequenceDiagram
    participant C as Desktop Client
    participant EB as EventBus (/events WS)
    participant GW as GitWatcher
    participant FS as FilesystemManager
    participant GT as git.getStatus (tRPC)

    C->>EB: WebSocket connect /events
    EB->>GW: start()
    GW->>GW: rescan() every 30s, fs.watch(.git recursive)

    Note over GW,EB: On any .git file change (no debounce)
    GW-->>EB: onChanged(workspaceId)
    EB-->>C: {type:"git:changed", workspaceId}

    C->>GT: git.getStatus.query({workspaceId})
    GT-->>C: {againstBase, staged, unstaged}
    C->>C: update sidebar diffStats chip

    C->>EB: {type:"fs:watch", workspaceId}
    EB->>FS: watchPath(rootPath, recursive)
    loop fs events
        FS-->>EB: FsWatchEvent[]
        EB-->>C: {type:"fs:events", workspaceId, events}
    end

    C->>EB: {type:"fs:unwatch", workspaceId}
    EB->>FS: iterator.return() — stop stream
Loading

Reviews (1): Last reviewed commit: "fix: multi-host routing, GIT_OPTIONAL_LO..." | Re-trigger Greptile

Comment on lines +127 to +128
}, [workspaceHosts.map]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Cleanup effect never reruns — workspaceHosts.map is a stable method reference

workspaceHosts.map resolves to Array.prototype.map, which is always the exact same function reference regardless of the array's contents. React sees an unchanged dependency, so this effect only runs on mount/unmount and never reruns when workspaces are added or removed. Stale workspaceId entries will accumulate indefinitely in statsMap.

The fix is to use workspaceHosts (which is memoized upstream via useMemo) or the pre-computed _workspaceHostsKey string already computed at lines 57–59 for exactly this purpose but never wired up:

Suggested change
}, [workspaceHosts.map]);
}, [workspaceHosts]);

@@ -0,0 +1,63 @@
DELETE FROM "v2_workspaces";--> statement-breakpoint
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 DELETE FROM "v2_workspaces" — irreversible data loss on every migration run

This statement unconditionally deletes every row in v2_workspaces before adding the NOT NULL host_id column. While it sidesteps a constraint failure (no existing rows need a host_id), it permanently destroys all workspace data in every environment the migration executes against — staging, developer databases, and production if reached.

If the v2 workspace table genuinely contains only throwaway data at this stage, that intent should be documented with a comment in the migration. Otherwise, a safer approach is to add host_id as nullable first, backfill, then tighten to NOT NULL in a follow-up:

-- Safer alternative: make host_id nullable initially
ALTER TABLE "v2_workspaces" ADD COLUMN "host_id" uuid;
-- Then backfill host_id for existing rows, then:
ALTER TABLE "v2_workspaces" ALTER COLUMN "host_id" SET NOT NULL;

Comment on lines +126 to +131

try {
const watcher = watch(gitDir, { recursive: true }, () => {
this.emit(workspaceId);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 No debouncing on .git watcher — burst of RPC calls per git operation

watch(gitDir, { recursive: true }, ...) fires once per individual file changed inside .git. A single git commit touches COMMIT_EDITMSG, index, HEAD, and refs/... in rapid succession, emitting multiple git:changed events to all connected clients. Each emission causes fetchDiffStats in the client to make a full git.getStatus RPC call, so one commit can trigger 4–6 concurrent status queries.

Consider debouncing per workspace (e.g. 200 ms) so a burst collapses into a single notification:

private readonly debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();

private emit(workspaceId: string): void {
    const existing = this.debounceTimers.get(workspaceId);
    if (existing) clearTimeout(existing);
    this.debounceTimers.set(workspaceId, setTimeout(() => {
        this.debounceTimers.delete(workspaceId);
        for (const listener of this.listeners) listener(workspaceId);
    }, 200));
}

Also ensure close() iterates over debounceTimers and clears any pending timers.

Comment on lines +57 to +62
const _workspaceHostsKey = workspaceHosts
.map((wh) => `${wh.workspaceId}:${wh.hostUrl}`)
.join(",");

// Keep a ref so event handlers can access current mapping
const workspaceHostsRef = useRef(workspaceHosts);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 _workspaceHostsKey is computed but never used

This variable serializes the workspace-host list into a stable string, which is exactly the type of primitive value that works well as a useEffect dependency. It appears it was intended to be used in the cleanup effect at line 127 (in place of the incorrect workspaceHosts.map), but was never wired up. Using it as the dependency there would fix the logic bug above. Either wire it up or remove it to avoid confusion.

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.

10 issues found across 49 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/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardDiffStats/useDashboardDiffStats.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardDiffStats/useDashboardDiffStats.ts:49">
P2: Avoid empty `catch` blocks here; swallowing errors makes diff-stat failures invisible during runtime/debugging.</violation>

<violation number="2" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardDiffStats/useDashboardDiffStats.ts:57">
P3: `_workspaceHostsKey` is computed but never referenced. It was likely intended as the stable dependency for the cleanup effect at line 128 (which currently uses the broken `workspaceHosts.map`). Either wire it up as the effect dependency or remove it to avoid dead code.</violation>

<violation number="3" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardDiffStats/useDashboardDiffStats.ts:127">
P1: The cleanup effect depends on `workspaceHosts.map` instead of workspace host data, so stale stats may remain when the workspace list changes.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx:281">
P2: For the "all" filter, deduplicating with `map.set(path, f)` drops additions/deletions from earlier entries of the same file path, so stats can undercount when a file exists in multiple status groups.</violation>
</file>

<file name="packages/db/drizzle/0031_v2_hosts_and_clients.sql">

<violation number="1" location="packages/db/drizzle/0031_v2_hosts_and_clients.sql:1">
P1: `DELETE FROM "v2_workspaces"` unconditionally deletes every row before adding the `NOT NULL` `host_id` column. This permanently destroys all workspace data in every environment the migration runs against (dev, staging, production). If the table genuinely contains only throwaway data at this stage, add a comment documenting that intent. Otherwise, add `host_id` as nullable first, backfill existing rows, then tighten to `NOT NULL` in a follow-up migration.</violation>
</file>

<file name="packages/host-service/src/events/git-watcher.ts">

<violation number="1" location="packages/host-service/src/events/git-watcher.ts:81">
P2: Do not silently swallow rescan query failures; log the error context so git-watcher outages are diagnosable.

(Based on your team's feedback about handling async errors explicitly and avoiding silent catch blocks.) [FEEDBACK_USED]</violation>

<violation number="2" location="packages/host-service/src/events/git-watcher.ts:117">
P1: Absolute git-dir detection is not cross-platform; Windows absolute paths are treated as relative and produce invalid watch paths.</violation>

<violation number="3" location="packages/host-service/src/events/git-watcher.ts:128">
P2: `watch(gitDir, { recursive: true })` fires once per individual file changed inside `.git`. A single `git commit` touches `COMMIT_EDITMSG`, `index`, `HEAD`, and `refs/...` in rapid succession, emitting multiple `git:changed` events. Each emission triggers a full `git.getStatus` RPC call on every connected client, so one commit can cause 4–6 concurrent status queries.

Add a per-workspace debounce (e.g. 200ms) in `emit()` so a burst collapses into a single notification.</violation>
</file>

<file name="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts:275">
P2: Remote workspaces are fetched/subscribed for diff stats but then always forced to `null`, causing unnecessary host traffic and missing remote diff chips.</violation>
</file>

<file name="packages/host-service/src/events/event-bus.ts">

<violation number="1" location="packages/host-service/src/events/event-bus.ts:115">
P1: A throwing `send` for one client will abort the loop and prevent remaining clients from receiving the `git:changed` event. Wrap each `sendMessage` in a try-catch so a single broken connection doesn't block the broadcast.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/db/drizzle/0031_v2_hosts_and_clients.sql
Comment thread packages/host-service/src/events/git-watcher.ts
}
}

private broadcast(message: ServerMessage): void {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 7, 2026

Choose a reason for hiding this comment

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

P1: A throwing send for one client will abort the loop and prevent remaining clients from receiving the git:changed event. Wrap each sendMessage in a try-catch so a single broken connection doesn't block the broadcast.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/events/event-bus.ts, line 115:

<comment>A throwing `send` for one client will abort the loop and prevent remaining clients from receiving the `git:changed` event. Wrap each `sendMessage` in a try-catch so a single broken connection doesn't block the broadcast.</comment>

<file context>
@@ -0,0 +1,252 @@
+		}
+	}
+
+	private broadcast(message: ServerMessage): void {
+		for (const socket of this.clients.keys()) {
+			sendMessage(socket, message);
</file context>
Fix with Cubic

})
.from(workspaces)
.all();
} catch {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 7, 2026

Choose a reason for hiding this comment

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

P2: Do not silently swallow rescan query failures; log the error context so git-watcher outages are diagnosable.

(Based on your team's feedback about handling async errors explicitly and avoiding silent catch blocks.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/events/git-watcher.ts, line 81:

<comment>Do not silently swallow rescan query failures; log the error context so git-watcher outages are diagnosable.

(Based on your team's feedback about handling async errors explicitly and avoiding silent catch blocks.) </comment>

<file context>
@@ -0,0 +1,148 @@
+				})
+				.from(workspaces)
+				.all();
+		} catch {
+			return;
+		}
</file context>
Fix with Cubic

Comment thread packages/host-service/src/events/git-watcher.ts
- Add useWorkspaceEvent(type, workspaceId, callback) hook that resolves
  workspace → host → event bus connection automatically
- Simplify useDiffStats to a per-workspace hook called from each
  DashboardSidebarWorkspaceItem instead of batch-fetching in sidebar data
- Remove diff stats logic from useDashboardSidebarData (no more
  workspaceHosts mapping, diffStatsByWorkspaceId, or diffStats on type)
- Pass diffStats as prop to ExpandedWorkspaceRow and HoverCardContent
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.

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (1)
packages/db/src/schema/schema.ts (1)

452-468: ⚠️ Potential issue | 🟠 Major

Include type in the v2_clients uniqueness key.

Line 465 currently makes every client for the same user/machine collapse into one row. That means a valid desktop + web combination on the same host will conflict or overwrite type during upserts.

Suggested constraint change
-		unique("v2_clients_org_user_machine_unique").on(
+		unique("v2_clients_org_user_machine_type_unique").on(
 			table.organizationId,
 			table.userId,
 			table.machineId,
+			table.type,
 		),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/db/src/schema/schema.ts` around lines 452 - 468, The unique
constraint v2_clients_org_user_machine_unique currently only covers
organizationId, userId and machineId which forces different client types (field
type in the v2_clients schema) to collide; update the uniqueness definition in
the schema.ts table builder to include table.type (and rename the constraint to
reflect type, e.g., v2_clients_org_user_machine_type_unique) so rows are unique
by organizationId, userId, machineId and type instead of collapsing different
client types into one row.
🧹 Nitpick comments (3)
packages/host-service/src/runtime/pull-requests/pull-requests.ts (1)

25-25: Polling interval reduced to 10s - verify rate limit impact.

The refresh interval is now 3x more frequent (10s vs 30s). While the code has in-flight deduplication and per-project cooldowns, consider whether this could approach GitHub's rate limits (5000 req/hour authenticated) for organizations with many active projects and workspaces.

The existing nextProjectRefreshAt throttling helps, but worth monitoring in production.

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

In `@packages/host-service/src/runtime/pull-requests/pull-requests.ts` at line 25,
PROJECT_REFRESH_INTERVAL_MS was reduced from 30_000 to 10_000 which increases
polling frequency and may hit GitHub rate limits; revert to a safer default or
make the interval configurable and add safeguards: restore
PROJECT_REFRESH_INTERVAL_MS to 30_000 (or read from env e.g.
PROJECT_REFRESH_INTERVAL_MS_DEFAULT), ensure existing in-flight deduplication
and nextProjectRefreshAt per-project cooldown logic (refer to
nextProjectRefreshAt and the in-flight request tracking) remain intact, and add
exponential backoff/short-circuiting and a runtime metric/log when polls are
dropped due to cooldown so you can monitor rate-limit pressure in production.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx (1)

270-288: Consider consolidating filter/dedupe computation to a single source.

filteredFiles/totals are computed here, but similar "all" dedupe logic is repeated later for fileList. Keeping one shared derivation would reduce drift risk between header stats and rendered rows.

Refactor sketch
-const filteredFiles = useMemo(() => {
+const { filteredFiles, totalChanges, totalAdditions, totalDeletions } = useMemo(() => {
   if (!status.data) return [];
   if (filter.kind === "uncommitted") {
-    return [...status.data.staged, ...status.data.unstaged];
+    const files = [...status.data.staged, ...status.data.unstaged];
+    return {
+      filteredFiles: files,
+      totalChanges: files.length,
+      totalAdditions: files.reduce((sum, f) => sum + f.additions, 0),
+      totalDeletions: files.reduce((sum, f) => sum + f.deletions, 0),
+    };
   }
   ...
-  return Array.from(map.values());
+  const files = Array.from(map.values());
+  return {
+    filteredFiles: files,
+    totalChanges: files.length,
+    totalAdditions: files.reduce((sum, f) => sum + f.additions, 0),
+    totalDeletions: files.reduce((sum, f) => sum + f.deletions, 0),
+  };
 }, [status.data, filter.kind, commitFiles.data?.files]);
-
-const totalChanges = filteredFiles.length;
-const totalAdditions = filteredFiles.reduce((sum, f) => sum + f.additions, 0);
-const totalDeletions = filteredFiles.reduce((sum, f) => sum + f.deletions, 0);
🤖 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-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx
around lines 270 - 288, filteredFiles and the header totals duplicate the "all"
dedupe logic that is later repeated for fileList; consolidate by extracting a
single source-of-truth (e.g., compute a deduplicated fileList inside the same
useMemo or a shared helper used by both) and derive
totalChanges/totalAdditions/totalDeletions from that unified file list instead
of re-running dedupe logic elsewhere; update references to use filteredFiles (or
the new shared fileList) and ensure the useMemo dependency array (status.data,
filter.kind, commitFiles.data?.files) covers all inputs so header stats and
rendered rows stay consistent.
packages/host-service/src/events/git-watcher.ts (1)

128-130: Debounce fs.watch notifications before emitting git:changed.

Each watcher notification triggers an emit. Git operations like checkout or rebase mutate multiple files under .git, causing fs.watch to fire repeatedly for a single logical change. This creates redundant event bursts that can overload status refreshes. Add a debounce to coalesce rapid notifications per workspace.

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

In `@packages/host-service/src/events/git-watcher.ts` around lines 128 - 130, The
current fs.watch callback (created by watch(...) assigning to watcher) calls
this.emit(workspaceId) for every notification; change it to debounce emissions
per workspace by maintaining a map of timers keyed by workspaceId, so the watch
callback clears any existing timer for that workspace and sets a new short
timeout (e.g., 200–500ms) to call this.emit(workspaceId) once when the burst
subsides; reference the watcher/watch(...) callback and this.emit(workspaceId)
when making the change and ensure timers are cleared when the watcher is
disposed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardDiffStats/useDashboardDiffStats.ts`:
- Around line 56-127: The three useEffect hooks inside useDashboardDiffStats are
using unstable dependencies (workspaceHosts or workspaceHosts.map) so they don't
reliably rerun; replace those with the stable _workspaceHostsKey: for the
initial fetch effect and the git:changed subscription effect change their
dependency arrays from [fetchDiffStats, workspaceHosts] to [fetchDiffStats,
_workspaceHostsKey], and for the cleanup effect change its dependency from
[workspaceHosts.map] to [_workspaceHostsKey] while keeping setStatsMap as
before; this ensures fetchDiffStats, getEventBus subscription code, and the
stale-entry cleanup run whenever the workspace-host list actually changes.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts`:
- Around line 145-161: The current useDashboardSidebarData hook builds
workspaceHosts (useMemo) and subscribes remote hosts via useDashboardDiffStats
but then ignores those fetched diff stats for workspaces of type
"remote-device"; update the rendering branch that handles remote-device
workspaces (where the code currently discards stats) to consume and display the
corresponding diff stats from useDashboardDiffStats for each workspaceHost entry
instead of throwing them away, or alternatively stop including remote hosts in
workspaceHosts; locate the workspaceHosts useMemo and the place that
renders/handles workspace.type === "remote-device" and either (A) map the
matching WorkspaceHostInfo.hostUrl or workspaceId to the diff stats returned by
useDashboardDiffStats and render them, or (B) remove remote hosts from
workspaceHosts to avoid unnecessary subscriptions.

In `@packages/db/drizzle/0031_v2_hosts_and_clients.sql`:
- Line 1: The migration currently contains a destructive DELETE FROM
"v2_workspaces" which will drop all workspace data; remove that DELETE and
instead implement a safe backfill: in 0031_v2_hosts_and_clients.sql
create/insert a default or derived host row into the new hosts table (e.g.,
INSERT INTO hosts (...) VALUES (...)), capture its id, UPDATE v2_workspaces SET
host_id = <default_host_id> for existing rows, and only after verifying backfill
add the NOT NULL constraint on host_id (or add it in a separate migration step);
ensure the script preserves existing workspace rows and documents the
default-host fallback for production.

In `@packages/host-service/src/events/event-bus.ts`:
- Around line 121-127: startFsWatch currently treats any existing
state.fsSubscriptions.get(workspaceId) entry as an active watch, but the
streaming task can exit without calling dispose() or removing that entry,
preventing future fs:watch calls; modify startFsWatch (and the corresponding
streaming teardown code around the stream task at lines ~175-200) to detect
non-active/closed subscriptions and to always call dispose() and remove the
workspaceId from state.fsSubscriptions when the stream ends or errors (use a
finally / stream 'close'/'error' handler to ensure cleanup), so that after a
stream exit the entry is cleared and a new fs:watch can be established on the
same socket.

In `@packages/trpc/src/router/device/device.ts`:
- Around line 34-49: The upsert on v2Hosts using
dbWs.insert(...).onConflictDoUpdate currently always proceeds to create an {
role: "owner" } link for the caller (see the owner insert block around lines
59-75), which lets any caller claim ownership when a machineId already exists;
fix by only granting owner when the host was actually inserted (not on conflict)
or by rejecting the operation unless the caller is already linked: implement
this by detecting whether the upsert created a new row (use
RETURNING/affectedRows from the insert or perform an initial SELECT to check
existence) and only run the owner insert when the host was newly created,
otherwise check v2HostUsers for an existing link for userId and fail the
mutation if none exists. Ensure you update the logic around the dbWs.insert(...)
.onConflictDoUpdate and the subsequent owner-insert code to follow this
conditional flow.

In `@packages/trpc/src/router/v2-workspace/v2-workspace.ts`:
- Around line 35-50: getScopedHost only checks host belongs to organization but
doesn't verify the requesting user owns or is allowed to use that host; update
getScopedHost (and the other places noted around the v2 workspace handlers) to
additionally join or query v2_users_hosts to assert a row exists where userId
equals ctx.session.user.id and hostId equals the requested hostId before
returning the host; specifically modify the requireOrgScopedResource call (or
the DB query used inside it) so it filters for both v2Hosts.organizationId and
an existence check on v2_users_hosts (userId = ctx.session.user.id, hostId =
hostId) and return the same BAD_REQUEST error if no such mapping exists,
ensuring the checks are applied in the other referenced handlers that accept
hostId as well.

In `@packages/workspace-client/src/lib/eventBus.ts`:
- Around line 98-105: The reconnect logic currently closes over the
creation-time getWsToken causing stale tokens; modify ConnectionState to include
a mutable getWsToken field and update connect (and any reconnect handlers used
in the connect/reconnect flow such as the code paths referenced around lines
122-150 and 154-173) to read the token via state.getWsToken instead of the
original closed-over getter; also update callers that create or reuse
ConnectionState to assign the latest getter into state.getWsToken so all
subsequent reconnects use the refreshed token source.
- Around line 44-50: buildEventBusUrl currently uses new URL("/events", hostUrl)
which resets the pathname to root and breaks proxy routing for remote hosts;
change it to append a relative path (e.g., "events" instead of "/events") or
otherwise join to the existing hostUrl.pathname so the resulting origin+path
becomes .../api/v2-hosts/{id}/events rather than /events; keep the protocol
selection and token query logic (function buildEventBusUrl and its wsToken
handling) the same while ensuring the path is appended relative to hostUrl.

---

Outside diff comments:
In `@packages/db/src/schema/schema.ts`:
- Around line 452-468: The unique constraint v2_clients_org_user_machine_unique
currently only covers organizationId, userId and machineId which forces
different client types (field type in the v2_clients schema) to collide; update
the uniqueness definition in the schema.ts table builder to include table.type
(and rename the constraint to reflect type, e.g.,
v2_clients_org_user_machine_type_unique) so rows are unique by organizationId,
userId, machineId and type instead of collapsing different client types into one
row.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx:
- Around line 270-288: filteredFiles and the header totals duplicate the "all"
dedupe logic that is later repeated for fileList; consolidate by extracting a
single source-of-truth (e.g., compute a deduplicated fileList inside the same
useMemo or a shared helper used by both) and derive
totalChanges/totalAdditions/totalDeletions from that unified file list instead
of re-running dedupe logic elsewhere; update references to use filteredFiles (or
the new shared fileList) and ensure the useMemo dependency array (status.data,
filter.kind, commitFiles.data?.files) covers all inputs so header stats and
rendered rows stay consistent.

In `@packages/host-service/src/events/git-watcher.ts`:
- Around line 128-130: The current fs.watch callback (created by watch(...)
assigning to watcher) calls this.emit(workspaceId) for every notification;
change it to debounce emissions per workspace by maintaining a map of timers
keyed by workspaceId, so the watch callback clears any existing timer for that
workspace and sets a new short timeout (e.g., 200–500ms) to call
this.emit(workspaceId) once when the burst subsides; reference the
watcher/watch(...) callback and this.emit(workspaceId) when making the change
and ensure timers are cleared when the watcher is disposed.

In `@packages/host-service/src/runtime/pull-requests/pull-requests.ts`:
- Line 25: PROJECT_REFRESH_INTERVAL_MS was reduced from 30_000 to 10_000 which
increases polling frequency and may hit GitHub rate limits; revert to a safer
default or make the interval configurable and add safeguards: restore
PROJECT_REFRESH_INTERVAL_MS to 30_000 (or read from env e.g.
PROJECT_REFRESH_INTERVAL_MS_DEFAULT), ensure existing in-flight deduplication
and nextProjectRefreshAt per-project cooldown logic (refer to
nextProjectRefreshAt and the in-flight request tracking) remain intact, and add
exponential backoff/short-circuiting and a runtime metric/log when polls are
dropped due to cooldown so you can monitor rate-limit pressure in production.
🪄 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: b8da2691-c849-41a4-abd3-07b65ce13e75

📥 Commits

Reviewing files that changed from the base of the PR and between 97031ad and f9f443f.

📒 Files selected for processing (49)
  • apps/api/src/app/api/electric/[...path]/utils.ts
  • apps/desktop/src/renderer/lib/v2-workspace-host.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/DashboardSidebarWorkspaceDiffStats.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/DashboardSidebarWorkspaceHoverCardContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/getWorkspaceRowMocks.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardDiffStats/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardDiffStats/useDashboardDiffStats.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/DevicePicker.tsx
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts
  • apps/electric-proxy/src/where.ts
  • packages/db/drizzle/0031_v2_hosts_and_clients.sql
  • packages/db/drizzle/meta/0031_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schema/enums.ts
  • packages/db/src/schema/relations.ts
  • packages/db/src/schema/schema.ts
  • packages/host-service/package.json
  • packages/host-service/src/app.ts
  • packages/host-service/src/events/event-bus.ts
  • packages/host-service/src/events/git-watcher.ts
  • packages/host-service/src/events/index.ts
  • packages/host-service/src/events/types.ts
  • packages/host-service/src/filesystem/events.ts
  • packages/host-service/src/filesystem/index.ts
  • packages/host-service/src/index.ts
  • packages/host-service/src/runtime/git/git.ts
  • packages/host-service/src/runtime/pull-requests/pull-requests.ts
  • packages/host-service/src/trpc/router/workspace/workspace.ts
  • packages/trpc/src/router/device/device.ts
  • packages/trpc/src/router/v2-workspace/v2-workspace.ts
  • packages/workspace-client/src/hooks/useEventBus/index.ts
  • packages/workspace-client/src/hooks/useEventBus/useEventBus.ts
  • packages/workspace-client/src/hooks/useGitChangeEvents/index.ts
  • packages/workspace-client/src/hooks/useGitChangeEvents/useGitChangeEvents.ts
  • packages/workspace-client/src/index.ts
  • packages/workspace-client/src/lib/eventBus.ts
  • packages/workspace-client/src/lib/workspaceFsEventRegistry.ts
  • packages/workspace-client/src/providers/WorkspaceClientProvider/WorkspaceClientProvider.tsx
  • packages/workspace-client/src/providers/WorkspaceClientProvider/index.ts
💤 Files with no reviewable changes (5)
  • packages/workspace-client/src/providers/WorkspaceClientProvider/index.ts
  • packages/host-service/src/filesystem/index.ts
  • packages/workspace-client/src/providers/WorkspaceClientProvider/WorkspaceClientProvider.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/getWorkspaceRowMocks.ts
  • packages/host-service/src/filesystem/events.ts

Comment on lines +56 to +127
// Stable serialization key for the workspace-host list
const _workspaceHostsKey = workspaceHosts
.map((wh) => `${wh.workspaceId}:${wh.hostUrl}`)
.join(",");

// Keep a ref so event handlers can access current mapping
const workspaceHostsRef = useRef(workspaceHosts);
workspaceHostsRef.current = workspaceHosts;

// Fetch initial data for all workspaces
useEffect(() => {
for (const { workspaceId, hostUrl } of workspaceHosts) {
void fetchDiffStats(workspaceId, hostUrl);
}
}, [fetchDiffStats, workspaceHosts]);

// Subscribe to git:changed events per unique host
useEffect(() => {
if (workspaceHosts.length === 0) return;

// Group workspaces by hostUrl
const byHost = new Map<string, Set<string>>();
for (const { workspaceId, hostUrl } of workspaceHosts) {
let set = byHost.get(hostUrl);
if (!set) {
set = new Set();
byHost.set(hostUrl, set);
}
set.add(workspaceId);
}

const cleanups: Array<() => void> = [];

for (const [hostUrl, workspaceIds] of byHost) {
const bus = getEventBus(hostUrl, () => getHostServiceWsToken(hostUrl));

const removeListener = bus.on(
"git:changed",
"*",
(changedWorkspaceId) => {
if (workspaceIds.has(changedWorkspaceId)) {
void fetchDiffStats(changedWorkspaceId, hostUrl);
}
},
);

const release = bus.retain();
cleanups.push(removeListener, release);
}

return () => {
for (const cleanup of cleanups) {
cleanup();
}
};
}, [fetchDiffStats, workspaceHosts]);

// Clean up stale entries when workspace list changes
useEffect(() => {
const idSet = new Set(workspaceHosts.map((wh) => wh.workspaceId));
setStatsMap((prev) => {
let changed = false;
const next = new Map(prev);
for (const key of next.keys()) {
if (!idSet.has(key)) {
next.delete(key);
changed = true;
}
}
return changed ? next : prev;
});
}, [workspaceHosts.map]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd apps/desktop && find . -name "useDashboardDiffStats.ts" -type f

Repository: superset-sh/superset

Length of output: 192


🏁 Script executed:

cat -n "src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardDiffStats/useDashboardDiffStats.ts" | sed -n '50,130p'

Repository: superset-sh/superset

Length of output: 222


🏁 Script executed:

cat -n "./src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardDiffStats/useDashboardDiffStats.ts" | sed -n '50,130p'

Repository: superset-sh/superset

Length of output: 224


🏁 Script executed:

find . -name "useDashboardDiffStats.ts" -type f 2>/dev/null

Repository: superset-sh/superset

Length of output: 205


🏁 Script executed:

cat -n "./apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardDiffStats/useDashboardDiffStats.ts" | sed -n '50,130p'

Repository: superset-sh/superset

Length of output: 2659


Use the stable workspace-host key in the cleanup effect dependency array.

The computed _workspaceHostsKey is unused. The cleanup effect (line 114–127) depends on workspaceHosts.map (a method reference that never changes), so it never reruns when the workspace list changes. This leaves stale stats entries behind and prevents cleanup when workspaces are removed. Replace [workspaceHosts.map] with [_workspaceHostsKey] on line 127, and do the same for the other two effects to stabilize their dependencies against parent re-renders.

🤖 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/components/DashboardSidebar/hooks/useDashboardDiffStats/useDashboardDiffStats.ts`
around lines 56 - 127, The three useEffect hooks inside useDashboardDiffStats
are using unstable dependencies (workspaceHosts or workspaceHosts.map) so they
don't reliably rerun; replace those with the stable _workspaceHostsKey: for the
initial fetch effect and the git:changed subscription effect change their
dependency arrays from [fetchDiffStats, workspaceHosts] to [fetchDiffStats,
_workspaceHostsKey], and for the cleanup effect change its dependency from
[workspaceHosts.map] to [_workspaceHostsKey] while keeping setStatsMap as
before; this ensures fetchDiffStats, getEventBus subscription code, and the
stale-entry cleanup run whenever the workspace-host list actually changes.

@@ -0,0 +1,63 @@
DELETE FROM "v2_workspaces";--> statement-breakpoint
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Data loss: Migration deletes all workspaces.

DELETE FROM "v2_workspaces" will permanently remove all existing workspace data. This is extremely destructive for production environments.

If this is intentional for a breaking schema change, ensure:

  1. This migration is only applied during an early/beta phase where data loss is acceptable
  2. A data migration strategy exists for production deployments
  3. This is clearly documented in release notes

If not intentional, consider a migration path that preserves existing data by:

  • Creating a default/placeholder host for existing workspaces
  • Backfilling host_id before making it NOT NULL
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/db/drizzle/0031_v2_hosts_and_clients.sql` at line 1, The migration
currently contains a destructive DELETE FROM "v2_workspaces" which will drop all
workspace data; remove that DELETE and instead implement a safe backfill: in
0031_v2_hosts_and_clients.sql create/insert a default or derived host row into
the new hosts table (e.g., INSERT INTO hosts (...) VALUES (...)), capture its
id, UPDATE v2_workspaces SET host_id = <default_host_id> for existing rows, and
only after verifying backfill add the NOT NULL constraint on host_id (or add it
in a separate migration step); ensure the script preserves existing workspace
rows and documents the default-host fallback for production.

Comment on lines +121 to +127
private startFsWatch(
socket: WsSocket,
state: ClientState,
workspaceId: string,
): void {
// Already watching this workspace for this client
if (state.fsSubscriptions.has(workspaceId)) return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clear broken FS subscriptions when the stream exits.

Line 127 treats any existing entry as an active watch, but Lines 175-200 can exit the streaming task without calling dispose() or removing that entry. After the first stream error/end, the client cannot successfully fs:watch that workspace again on the same socket.

Suggested fix
 		void (async () => {
 			try {
 				while (!disposed && iterator) {
 					const next = await iterator.next();
 					if (disposed || next.done) return;
@@
 			} catch (error) {
 				if (disposed) return;
 				console.error("[event-bus] fs stream failed:", {
 					workspaceId,
 					error,
 				});
 				sendMessage(socket, {
 					type: "error",
 					message:
 						error instanceof Error
 							? error.message
 							: "Filesystem event stream failed",
 				});
+			} finally {
+				dispose();
+				const current = state.fsSubscriptions.get(workspaceId);
+				if (current?.dispose === dispose) {
+					state.fsSubscriptions.delete(workspaceId);
+				}
 			}
 		})();

Also applies to: 161-201

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

In `@packages/host-service/src/events/event-bus.ts` around lines 121 - 127,
startFsWatch currently treats any existing
state.fsSubscriptions.get(workspaceId) entry as an active watch, but the
streaming task can exit without calling dispose() or removing that entry,
preventing future fs:watch calls; modify startFsWatch (and the corresponding
streaming teardown code around the stream task at lines ~175-200) to detect
non-active/closed subscriptions and to always call dispose() and remove the
workspaceId from state.fsSubscriptions when the stream ends or errors (use a
finally / stream 'close'/'error' handler to ensure cleanup), so that after a
stream exit the entry is cleared and a new fs:watch can be established on the
same socket.

Comment on lines +34 to 49
const [host] = await dbWs
.insert(v2Hosts)
.values({
organizationId,
clientId: input.clientId,
machineId: input.machineId,
name: input.name,
type: "host",
lastSeenAt: now,
createdByUserId: userId,
})
.onConflictDoUpdate({
target: [v2Devices.organizationId, v2Devices.clientId],
target: [v2Hosts.organizationId, v2Hosts.machineId],
set: {
name: input.name,
type: "host",
lastSeenAt: now,
},
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don't grant owner on every machineId conflict.

Line 44 upserts solely on (organizationId, machineId), and Lines 59-73 then unconditionally insert { role: "owner" } for the caller. That lets any org member who can call this mutation with an existing machineId attach themselves to someone else’s host and rename it via Line 46. Only create the owner link on insert, or reject unless the caller is already linked to that host.

Also applies to: 59-75

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

In `@packages/trpc/src/router/device/device.ts` around lines 34 - 49, The upsert
on v2Hosts using dbWs.insert(...).onConflictDoUpdate currently always proceeds
to create an { role: "owner" } link for the caller (see the owner insert block
around lines 59-75), which lets any caller claim ownership when a machineId
already exists; fix by only granting owner when the host was actually inserted
(not on conflict) or by rejecting the operation unless the caller is already
linked: implement this by detecting whether the upsert created a new row (use
RETURNING/affectedRows from the insert or perform an initial SELECT to check
existence) and only run the owner insert when the host was newly created,
otherwise check v2HostUsers for an existing link for userId and fail the
mutation if none exists. Ensure you update the logic around the dbWs.insert(...)
.onConflictDoUpdate and the subsequent owner-insert code to follow this
conditional flow.

Comment on lines +35 to 50
async function getScopedHost(organizationId: string, hostId: string) {
return requireOrgScopedResource(
() =>
dbWs.query.v2Devices.findFirst({
dbWs.query.v2Hosts.findFirst({
columns: {
id: true,
organizationId: true,
},
where: eq(v2Devices.id, deviceId),
where: eq(v2Hosts.id, hostId),
}),
{
code: "BAD_REQUEST",
message: "Device not found in this organization",
message: "Host not found in this organization",
organizationId,
},
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Enforce per-user host access before accepting hostId.

getScopedHost() only proves that the host belongs to the org. A forged request can still create/update a workspace against another user's host unless this also verifies a matching v2_users_hosts row for ctx.session.user.id.

Also applies to: 112-123, 151-153

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

In `@packages/trpc/src/router/v2-workspace/v2-workspace.ts` around lines 35 - 50,
getScopedHost only checks host belongs to organization but doesn't verify the
requesting user owns or is allowed to use that host; update getScopedHost (and
the other places noted around the v2 workspace handlers) to additionally join or
query v2_users_hosts to assert a row exists where userId equals
ctx.session.user.id and hostId equals the requested hostId before returning the
host; specifically modify the requireOrgScopedResource call (or the DB query
used inside it) so it filters for both v2Hosts.organizationId and an existence
check on v2_users_hosts (userId = ctx.session.user.id, hostId = hostId) and
return the same BAD_REQUEST error if no such mapping exists, ensuring the checks
are applied in the other referenced handlers that accept hostId as well.

Comment on lines +44 to +50
function buildEventBusUrl(hostUrl: string, wsToken: string | null): string {
const url = new URL("/events", hostUrl);
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
if (wsToken) {
url.searchParams.set("token", wsToken);
}
return url.toString();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

How does the WHATWG URL constructor resolve new URL("events", "https://example.com/api/v2-hosts/123/trpc")` versus new URL("/events", "https://example.com/api/v2-hosts/123/trpc")?`

💡 Result:

  • new URL("events", "https://example.com/api/v2-hosts/123/trpc").href

    • Result: https://example.com/api/v2-hosts/123/events
    • Why: "events" is a path-relative URL string (it does not start with /) so it’s resolved relative to the base URL’s current directory (i.e., up to the last /). Since the base path is /api/v2-hosts/123/trpc (no trailing slash), the current directory is /api/v2-hosts/123/, and "events" is appended there. [1] [2]
  • new URL("/events", "https://example.com/api/v2-hosts/123/trpc").href

    • Result: https://example.com/events
    • Why: "/events" is a path-absolute URL string (starts with /), which is resolved from the origin root, ignoring the base URL’s path. [1] [2]

Sources:
[1] WHATWG URL Standard (definitions of path-absolute vs path-relative URL strings) (url.spec.whatwg.org)
[2] MDN: Resolving relative references to a URL (current-directory vs root-relative behavior) (developer.mozilla.org)

Citations:


🏁 Script executed:

# Find where buildEventBusUrl is called
rg "buildEventBusUrl" --type ts --type tsx -B 3 -A 3

# Find where hostUrl parameter comes from
rg "hostUrl" packages/workspace-client/src/lib/eventBus.ts -B 2 -A 2

Repository: superset-sh/superset

Length of output: 2033


🏁 Script executed:

# Check the entire eventBus.ts file to understand context
cat packages/workspace-client/src/lib/eventBus.ts

Repository: superset-sh/superset

Length of output: 6384


🏁 Script executed:

# Search for how hostUrl is constructed/obtained in the workspace-client
fd eventBus packages/workspace-client/src -type f | xargs rg "getOrCreateConnection|buildEventBusUrl" -l

Repository: superset-sh/superset

Length of output: 297


🏁 Script executed:

# Find all calls to getEventBus
rg "getEventBus" --type ts -B 2 -A 2

Repository: superset-sh/superset

Length of output: 5641


🏁 Script executed:

# Search for backend routing or endpoint definitions for /events
fd "events" packages --type f | head -20

Repository: superset-sh/superset

Length of output: 277


🏁 Script executed:

# Look for any host configuration or hostUrl construction
rg "hostUrl\s*=" --type ts -B 1 -A 1 | head -40

Repository: superset-sh/superset

Length of output: 1873


🏁 Script executed:

# Find how hostUrl is constructed/used in workspace-client provider
rg "hostUrl" packages/workspace-client/src/providers -B 2 -A 2 --type ts

Repository: superset-sh/superset

Length of output: 6914


🏁 Script executed:

# Look for backend routing or event endpoint definitions
fd -e ts -e tsx | xargs rg "/events" | grep -v node_modules | head -20

Repository: superset-sh/superset

Length of output: 1692


🏁 Script executed:

# Search for event endpoint in backend code (likely Python/Flask)
fd -e py | xargs rg "/events" | grep -v node_modules | head -20

Repository: superset-sh/superset

Length of output: 2040


🏁 Script executed:

# Check if there's proxy configuration or host resolution logic
rg "resolveCreateWorkspaceHostUrl|getHostServiceWsToken" --type ts -A 5 | head -40

Repository: superset-sh/superset

Length of output: 5849


🏁 Script executed:

# Find getRemoteHostUrl implementation
rg "getRemoteHostUrl|resolveCreateWorkspaceHostUrl" --type ts -B 1 -A 10 | head -60

Repository: superset-sh/superset

Length of output: 7071


🏁 Script executed:

# Check the host-service app setup to understand the full routing
rg "app\.use|registerEventBusRoute" packages/host-service/src/app.ts -B 2 -A 2

Repository: superset-sh/superset

Length of output: 698


🏁 Script executed:

# Find where host-service is mounted/proxied in the application
fd -e ts -e tsx | xargs rg "host-service|host.*service" | grep -E "(proxy|mount|route)" | head -20

Repository: superset-sh/superset

Length of output: 3896


Use a relative URL path for the /events WebSocket endpoint to preserve proxy routing.

Line 45 uses new URL("/events", hostUrl), which resets the pathname to the origin root. Since hostUrl for remote hosts is ${API_URL}/api/v2-hosts/{id}/trpc and the host-service's /events endpoint is registered at the app root (same level as /trpc), the absolute path /events routes the WebSocket connection to https://api.example.com/events instead of the correct https://api.example.com/api/v2-hosts/{id}/events. This causes event subscriptions to fail for remote/cloud hosts.

Suggested fix
-	const url = new URL("/events", hostUrl);
+	const url = new URL("events", hostUrl);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workspace-client/src/lib/eventBus.ts` around lines 44 - 50,
buildEventBusUrl currently uses new URL("/events", hostUrl) which resets the
pathname to root and breaks proxy routing for remote hosts; change it to append
a relative path (e.g., "events" instead of "/events") or otherwise join to the
existing hostUrl.pathname so the resulting origin+path becomes
.../api/v2-hosts/{id}/events rather than /events; keep the protocol selection
and token query logic (function buildEventBusUrl and its wsToken handling) the
same while ensuring the path is appended relative to hostUrl.

Comment on lines +98 to +105
function connect(
state: ConnectionState,
hostUrl: string,
getWsToken: () => string | null,
): void {
if (state.disposed) return;

const wsUrl = buildEventBusUrl(hostUrl, getWsToken());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Refresh the token source when reusing a shared connection.

Lines 158-160 reuse the existing ConnectionState by hostUrl only, while Lines 98-150 keep reconnect logic closed over the creation-time getWsToken. After a token rotation or session change, reconnects will still use the stale getter and can come back with expired or wrong credentials. Store the latest getter in ConnectionState and have reconnects read from that shared state.

Also applies to: 122-150, 154-173

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

In `@packages/workspace-client/src/lib/eventBus.ts` around lines 98 - 105, The
reconnect logic currently closes over the creation-time getWsToken causing stale
tokens; modify ConnectionState to include a mutable getWsToken field and update
connect (and any reconnect handlers used in the connect/reconnect flow such as
the code paths referenced around lines 122-150 and 154-173) to read the token
via state.getWsToken instead of the original closed-over getter; also update
callers that create or reuse ConnectionState to assign the latest getter into
state.getWsToken so all subsequent reconnects use the refreshed token source.

Delete getWorkspaceRowMocks entirely — no more fake data in the
sidebar. workspaceStatus will be null until wired up with real data.
Move all host-service-related hooks to renderer/hooks/host-service/
following repo structure guidelines (one hook per file, folder per hook,
matching names). These will eventually move to a shared package.

- Split useWorkspaceEvent.ts into useWorkspaceEvent/ and useWorkspaceHostUrl/
- Rename useDashboardDiffStats/ → useDiffStats/ (match hook name)
- Move useFileTree/ and useFileDocument/ from v2-workspace route
- Update all consumer import paths
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.

2 issues found across 5 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/src/events/git-watcher.ts">

<violation number="1" location="packages/host-service/src/events/git-watcher.ts:79">
P2: Guard debounced notifications so a pending timer does not emit events for a workspace that has already been removed/unwatched.</violation>
</file>

<file name="apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts">

<violation number="1" location="apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts:31">
P2: Path deduplication currently overwrites staged/unstaged entries, which can lose diff counts for partially staged files.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +79 to +81
for (const listener of this.listeners) {
listener(workspaceId);
}
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 7, 2026

Choose a reason for hiding this comment

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

P2: Guard debounced notifications so a pending timer does not emit events for a workspace that has already been removed/unwatched.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/events/git-watcher.ts, line 79:

<comment>Guard debounced notifications so a pending timer does not emit events for a workspace that has already been removed/unwatched.</comment>

<file context>
@@ -54,16 +59,28 @@ export class GitWatcher {
+			workspaceId,
+			setTimeout(() => {
+				this.debounceTimers.delete(workspaceId);
+				for (const listener of this.listeners) {
+					listener(workspaceId);
+				}
</file context>
Suggested change
for (const listener of this.listeners) {
listener(workspaceId);
}
if (this.closed || !this.watched.has(workspaceId)) return;
for (const listener of this.listeners) {
listener(workspaceId);
}
Fix with Cubic

- Add 300ms per-workspace debounce to GitWatcher — a single git
  operation no longer fires dozens of events
- Remove dead v2DeviceType and v2UsersDeviceRole enums from schema
- Make v2_hosts.machineId nullable (cloud hosts have no machine)
- Deduplicate files by path in useDiffStats to prevent double-counting
  when a file appears in both againstBase and staged/unstaged
- Remove getConnectionKey no-op function from event bus client
@saddlepaddle saddlepaddle force-pushed the saddlepaddle/nickel-bladder branch from d0bc94e to 1aeccdc Compare April 7, 2026 16:38
@saddlepaddle saddlepaddle merged commit 14de45b into main Apr 7, 2026
14 checks passed
MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request Apr 10, 2026
…rset-sh#3224)

* WIP - checkpoint

* feat: unified WS event bus, v2Hosts data model, real diff stats

- Add unified /events WebSocket endpoint on host-service replacing
  per-workspace filesystem WS connections. Carries git:changed (auto)
  and fs:events (on-demand) over a single connection per host.
- Add v2_hosts, v2_clients, v2_users_hosts tables replacing v2_devices,
  v2_device_presence, v2_users_devices. Workspaces now reference hostId
  instead of deviceId. Hosts identified by machineId (null = cloud).
- Wire real git diff stats into left sidebar chips using event bus
  to trigger refetches instead of polling.
- Update tRPC routers (ensureV2Host, ensureV2Client, workspace create),
  Electric SQL sync, and all renderer references.

* fix: multi-host routing, GIT_OPTIONAL_LOCKS, UI polish

- Route diff stats per-workspace to correct host (local vs remote proxy URL)
- Set GIT_OPTIONAL_LOCKS=0 on all host-service git operations to prevent
  index.lock contention with user git commands
- Fix Changes tab header stats not updating when switching filter dropdown
- Fix diff stats pill padding (w-fit + justify-self-end)
- Restore LuFolderGit2 icon for local workspaces
- Update WorkspaceHostTarget kind from "device" to "host"
- Make v2_hosts.machineId NOT NULL (every host is a machine)
- Bump PR polling to 10s (host-service + client)

* refactor: per-workspace diff stats via useWorkspaceEvent hook

- Add useWorkspaceEvent(type, workspaceId, callback) hook that resolves
  workspace → host → event bus connection automatically
- Simplify useDiffStats to a per-workspace hook called from each
  DashboardSidebarWorkspaceItem instead of batch-fetching in sidebar data
- Remove diff stats logic from useDashboardSidebarData (no more
  workspaceHosts mapping, diffStatsByWorkspaceId, or diffStats on type)
- Pass diffStats as prop to ExpandedWorkspaceRow and HoverCardContent

* chore: remove workspace status mock data

Delete getWorkspaceRowMocks entirely — no more fake data in the
sidebar. workspaceStatus will be null until wired up with real data.

* refactor: delete FS event adapter layer, event-driven Changes tab

- Extend useWorkspaceEvent to support "fs:events" (with watchFs/unwatchFs)
- Move useFileTree + useFileDocument from workspace-client to desktop app,
  swap useWorkspaceFsEvents → useWorkspaceEvent internally
- Update FilesTab to use useWorkspaceEvent directly for search invalidation
- Switch useChangesTab from 3s polling to git:changed event bus invalidation
- Delete workspaceFsEventRegistry, useWorkspaceFsEventBridge,
  useWorkspaceFsEvents from workspace-client entirely

* fix: re-export FS types from workspace-fs/client instead of /host

Renderer code shouldn't import from @superset/workspace-fs/host.
Re-export FsEntry, FsEntryKind, FsWatchEvent from the browser-safe
/client entry point.

* fix: host URL resolution race, editor width, watchFs ref counting

- Remove electron IPC dependency from useWorkspaceHostUrl — resolve
  local vs remote by checking if org has a running host-service in
  the services map instead of comparing machineId
- Fix CodeRenderer not taking full pane width (add w-full + min-w-0)
- Fix watchFs/unwatchFs lacking ref counting in event bus client
  (multiple subscribers could prematurely unwatch)

* fix: ref-count watchFs/unwatchFs in event bus client

Multiple subscribers to fs:events for the same workspace now
correctly share a single watch. Only the first subscriber sends
fs:watch, only the last unsubscriber sends fs:unwatch.

* refactor: co-locate host-service hooks in renderer/hooks/host-service

Move all host-service-related hooks to renderer/hooks/host-service/
following repo structure guidelines (one hook per file, folder per hook,
matching names). These will eventually move to a shared package.

- Split useWorkspaceEvent.ts into useWorkspaceEvent/ and useWorkspaceHostUrl/
- Rename useDashboardDiffStats/ → useDiffStats/ (match hook name)
- Move useFileTree/ and useFileDocument/ from v2-workspace route
- Update all consumer import paths

* fix: debounce git watcher, remove stale enums, fix double-counting

- Add 300ms per-workspace debounce to GitWatcher — a single git
  operation no longer fires dozens of events
- Remove dead v2DeviceType and v2UsersDeviceRole enums from schema
- Make v2_hosts.machineId nullable (cloud hosts have no machine)
- Deduplicate files by path in useDiffStats to prevent double-counting
  when a file appears in both againstBase and staged/unstaged
- Remove getConnectionKey no-op function from event bus client
MocA-Love added a commit to MocA-Love/superset that referenced this pull request Apr 10, 2026
After cherry-picking upstream superset-sh#3250 (relay) which renamed
host-service-manager → host-service-coordinator:

1. main/index.ts: alias import as getHostServiceManager for minimal
   diff with fork's quit lifecycle code
2. tray/index.ts: alias import, simplify tray menu to use new
   coordinator API (getProcessStatus instead of getServiceInfo,
   remove degraded/restarting statuses, remove restart button
   that now requires SpawnConfig)
3. FilesPane.tsx: migrate useWorkspaceFsEventBridge/useWorkspaceFsEvents
   to useWorkspaceEvent (from upstream superset-sh#3224 event bus)
4. WorkspaceFilePreviewContent.tsx: useFileDocument import path
5. WorkspaceFilesTreeItem.tsx: FileTreeNode import path
6. settings-state.ts: fix duplicate "project" entry, add "security"
7. settings/index.ts: fix missing closure in preventAgentSleep mutation
@Kitenite Kitenite deleted the saddlepaddle/nickel-bladder branch April 13, 2026 16:35
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