Skip to content

fix(desktop): restore v2 sidebar LOC indicator#4222

Merged
saddlepaddle merged 1 commit intomainfrom
fix-loc-indicator-sidebar
May 8, 2026
Merged

fix(desktop): restore v2 sidebar LOC indicator#4222
saddlepaddle merged 1 commit intomainfrom
fix-loc-indicator-sidebar

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented May 8, 2026

Summary

  • perf(host-service): listBranches single git spawn instead of 4×N #4160 rewrote `useDiffStats` from imperative `client.git.getStatus.query()` to `workspaceTrpc.git.getStatus.useQuery()`. This broke the v2 dashboard sidebar's LOC indicator — `workspaceTrpc.Provider` only mounts inside `v2-workspace/$workspaceId`, but the sidebar is a sibling of that ``, so its tRPC hooks had no provider above them. Even when one provider was present, it points at a single hostUrl and can't fan out to sidebar workspaces living on different hosts (local vs remote vs cloud).
  • Resolve hostUrl per-workspaceId via `useWorkspaceHostUrl`, call the imperative `getHostServiceClientByUrl(hostUrl).git.getStatus.query(...)` inside a react-query `queryFn` keyed on `(hostUrl, workspaceId)`. Concurrent callers for the same workspace (sidebar tile + hover overlay) still dedup through the global `QueryClient` — same end-state perf perf(host-service): listBranches single git spawn instead of 4×N #4160 intended, without the broken Provider assumption.
  • `staleTime: Number.POSITIVE_INFINITY` since `git:changed` is push-authoritative — sidebar tile remounts (virtualization, route changes) reuse the cached value.

Test plan

  • Open desktop app on a project with multiple v2 workspaces, confirm `+N −N` chip appears on tiles with local changes
  • Edit a file in a workspace, confirm chip updates without manual refresh
  • Hover a workspace, confirm hover-card LOC matches the tile (no double-fetch in network panel)
  • Verify chip works for both local-device and remote-device workspaces in the same sidebar

Summary by cubic

Restores the v2 sidebar LOC (+N/−N) indicator by fetching diff stats per workspace without relying on a workspaceTrpc provider. Works across local, remote, and cloud workspaces.

  • Bug Fixes
    • Switched to @tanstack/react-query useQuery with getHostServiceClientByUrl(hostUrl).git.getStatus.query(...).
    • Resolve hostUrl per workspaceId via useWorkspaceHostUrl; cache key: ["diff-stats", hostUrl, workspaceId].
    • Invalidate on git:changed; staleTime: Infinity; refetchOnWindowFocus: false; dedup concurrent fetches via the global QueryClient.

Written for commit 3a45414. Summary will update on new commits.

Summary by CodeRabbit

  • Refactor
    • Improved internal infrastructure for diff statistics tracking to enhance performance and maintainability. No user-visible changes.

#4160 swapped useDiffStats from imperative client.git.getStatus.query()
to workspaceTrpc.git.getStatus.useQuery(). That broke the v2 dashboard
sidebar: workspaceTrpc.Provider only mounts inside v2-workspace/$id, but
the sidebar is a sibling of that Outlet — so its useQuery calls had no
provider, and even with one couldn't fan out to workspaces on different
hosts.

Resolve hostUrl per-workspaceId, call the imperative client inside a
react-query queryFn keyed on (hostUrl, workspaceId). Concurrent callers
(tile + hover overlay for the same workspace) still dedup via the global
QueryClient. staleTime: Infinity since git:changed is push-authoritative.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

The useDiffStats hook is refactored to use React Query directly instead of TRPC utilities. It now derives the query key from a host URL and workspace ID, fetches git status via the host service client, and invalidates the cache when git changes occur. The public interface and aggregation logic remain unchanged.

Changes

TRPC to React Query Migration for useDiffStats

Layer / File(s) Summary
Imports & Query Key Construction
apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts
Updated imports to React Query hooks and host URL resolver. Introduced memoized queryKey derived from hostUrl and workspaceId.
Query Execution & Fetching
apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts
Replaced workspaceTrpc.git.getStatus.useQuery with useQuery calling getHostServiceClientByUrl(hostUrl).git.getStatus.query, gated by both workspaceId and hostUrl availability. Configured with refetchOnWindowFocus: false and infinite staleTime.
Cache Invalidation
apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts
Changed from TRPC workspaceId-keyed invalidation to React Query invalidateQueries by the computed queryKey, still triggered by the "git:changed" workspace event.
Aggregation & Return
apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts
File deduplication and additions/deletions summation logic across againstBase, staged, and unstaged remains unchanged; returns null when status is unavailable.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 From TRPC's grasp, a query springs free,
React Query now holds the cache key,
Host URL flows through memoized design,
Git changes cascade, invalidations align—
Same diff aggregation, new path to the sea! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately describes the main change: restoring the v2 sidebar LOC indicator by fixing the useDiffStats hook from broken tRPC to react-query approach.
Description check ✅ Passed Description includes comprehensive context, implementation details, caching strategy, and a test plan; it exceeds the template requirements with thorough explanation of the problem and solution.
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-loc-indicator-sidebar

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.

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: 1

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

Inline comments:
In `@apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts`:
- Around line 29-31: The query config in useDiffStats() sets staleTime to
Number.POSITIVE_INFINITY which risks permanent stale data before
useWorkspaceEvent() subscribes; update the query options in the useDiffStats
hook to ensure an initial fresh load by either setting a short staleTime (e.g.,
30_000 ms) or adding refetchOnMount: true (and optionally a conservative
cacheTime/gcTime) so the first mount always refetches while keeping
refetchOnWindowFocus: false; locate the query options object in useDiffStats and
adjust the staleTime/refetchOnMount (and optionally cacheTime) accordingly.
🪄 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: 21fb1afe-15c6-4d25-89a4-c662ee8a4940

📥 Commits

Reviewing files that changed from the base of the PR and between 26b847b and 3a45414.

📒 Files selected for processing (1)
  • apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts

Comment on lines +29 to +31
refetchOnWindowFocus: false,
staleTime: Number.POSITIVE_INFINITY,
});
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 | ⚡ Quick win

staleTime: POSITIVE_INFINITY creates a silent stale-data risk on first mount.

With git:changed as the sole refresh trigger, any change that occurs between component mount and listener registration (useWorkspaceEvent subscribing) will leave the UI permanently stale for that session — no background refetch or window-focus refetch will rescue it. The PR notes this is intentional ("push-authoritative"), but it's worth confirming the event subscription is guaranteed to be in place before any relevant git:changed events can fire.

If there's a realistic race on initial mount (e.g., workspace git state changes during the first render cycle), consider a short staleTime (e.g., 30 s) or a conservative gcTime paired with an initial refetchOnMount: true to ensure the first load is always fresh, while still avoiding redundant window-focus refetches.

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

In `@apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts`
around lines 29 - 31, The query config in useDiffStats() sets staleTime to
Number.POSITIVE_INFINITY which risks permanent stale data before
useWorkspaceEvent() subscribes; update the query options in the useDiffStats
hook to ensure an initial fresh load by either setting a short staleTime (e.g.,
30_000 ms) or adding refetchOnMount: true (and optionally a conservative
cacheTime/gcTime) so the first mount always refetches while keeping
refetchOnWindowFocus: false; locate the query options object in useDiffStats and
adjust the staleTime/refetchOnMount (and optionally cacheTime) accordingly.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 8, 2026

Greptile Summary

This PR restores the v2 sidebar LOC indicator by replacing workspaceTrpc.git.getStatus.useQuery (which requires a workspaceTrpc.Provider that doesn't exist in the sidebar route) with an imperative getHostServiceClientByUrl(hostUrl).git.getStatus.query call wrapped in a standard useQuery, keyed by (hostUrl, workspaceId) so concurrent sidebar consumers naturally dedup through the global QueryClient.

  • Removes the broken tRPC React hook and re-routes the fetch through the imperative host-service client, resolving the missing Provider crash.
  • Uses useWorkspaceHostUrl to dynamically resolve the correct host per workspace, enabling the sidebar to fan out to workspaces on different hosts (local vs. remote).
  • Sets staleTime: Number.POSITIVE_INFINITY so cached data survives sidebar tile remounts, with git:changed push events as the sole refresh mechanism via queryClient.invalidateQueries.

Confidence Score: 4/5

Safe to merge; the core fix is correct and useEffectEvent in useWorkspaceEvent ensures no stale-closure issues with the callback.

The logic replacing the broken tRPC hook is sound — getHostServiceClientByUrl is cached, queryKey is stable, and useEffectEvent ensures the latest invalidate is always called. Two edge-cases are flagged: invalidate issues a no-op invalidateQueries call while hostUrl is null, and staleTime: Infinity with no reconnect-triggered refetch means a WebSocket gap during active editing could leave the chip showing an outdated count indefinitely.

apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts — specifically the invalidate guard and the reconnect-refresh strategy.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts Replaces broken tRPC React hook with an imperative client call wrapped in a standard react-query useQuery, keyed by (hostUrl, workspaceId). Logic is sound; the main concern is that staleTime: Infinity makes the display exclusively reliant on push events with no fallback if git:changed is missed during a WebSocket reconnect.

Sequence Diagram

sequenceDiagram
    participant SC as Sidebar Component
    participant uDS as useDiffStats
    participant uWHU as useWorkspaceHostUrl
    participant uWE as useWorkspaceEvent
    participant QC as QueryClient
    participant HSC as getHostServiceClientByUrl
    participant EB as Event Bus (git:changed)

    SC->>uDS: render(workspaceId)
    uDS->>uWHU: useWorkspaceHostUrl(workspaceId)
    uWHU-->>uDS: hostUrl (null while loading, URL when ready)
    uDS->>QC: "useQuery({ queryKey: [diff-stats, hostUrl, workspaceId], enabled: hostUrl != null })"
    QC->>HSC: "getHostServiceClientByUrl(hostUrl).git.getStatus.query({ workspaceId })"
    HSC-->>QC: GitStatus (staleTime: Infinity, cached)
    QC-->>uDS: "{ data: status }"
    uDS->>uWE: useWorkspaceEvent(git:changed, workspaceId, invalidate)
    uWE->>EB: subscribe via WebSocket

    Note over EB, QC: On file change
    EB->>uWE: git:changed event (useEffectEvent to latest invalidate)
    uWE->>QC: "invalidateQueries({ queryKey: [diff-stats, hostUrl, workspaceId] })"
    QC->>HSC: refetch git.getStatus
    HSC-->>QC: updated GitStatus
    QC-->>uDS: "{ data: updated status }"
    uDS-->>SC: "{ additions, deletions }"
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts:29-31
**Stale indicator if `git:changed` event is missed on reconnect**

`staleTime: Number.POSITIVE_INFINITY` + `refetchOnWindowFocus: false` means the LOC chip updates *only* when a `git:changed` WebSocket event is received. If the event bus connection drops and reconnects, any file edits made during the gap are silently invisible — the chip keeps showing the pre-disconnect value until the user makes another change that triggers a fresh push event. In the previous `staleTime: 0` regime a component remount (e.g., after a tab switch) would naturally re-prime the cache; that safety net is now gone.

Consider adding `refetchOnReconnect: true` (verify it fires for the non-window WebSocket path) or having the event bus emit a synthetic `git:changed` after reconnect so the existing invalidation path kicks in.

### Issue 2 of 2
apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts:33-35
The `invalidate` callback captures `queryKey` at the time it was last memoized. When `hostUrl` is still `null` (the workspace host is loading), `queryKey` is `["diff-stats", null, workspaceId]` and any `git:changed` event fired in that window calls `invalidateQueries` with that null key — targeting a query that was never enabled and has no cached data, so the invalidation is silently dropped. Adding an early-return guard makes the intent explicit and avoids the vacuous call.

```suggestion
	const invalidate = useCallback(() => {
		if (!hostUrl) return;
		void queryClient.invalidateQueries({ queryKey });
	}, [queryClient, queryKey, hostUrl]);
```

Reviews (1): Last reviewed commit: "fix(desktop): restore v2 sidebar LOC ind..." | Re-trigger Greptile

Comment on lines +29 to +31
refetchOnWindowFocus: false,
staleTime: Number.POSITIVE_INFINITY,
});
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 Stale indicator if git:changed event is missed on reconnect

staleTime: Number.POSITIVE_INFINITY + refetchOnWindowFocus: false means the LOC chip updates only when a git:changed WebSocket event is received. If the event bus connection drops and reconnects, any file edits made during the gap are silently invisible — the chip keeps showing the pre-disconnect value until the user makes another change that triggers a fresh push event. In the previous staleTime: 0 regime a component remount (e.g., after a tab switch) would naturally re-prime the cache; that safety net is now gone.

Consider adding refetchOnReconnect: true (verify it fires for the non-window WebSocket path) or having the event bus emit a synthetic git:changed after reconnect so the existing invalidation path kicks in.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts
Line: 29-31

Comment:
**Stale indicator if `git:changed` event is missed on reconnect**

`staleTime: Number.POSITIVE_INFINITY` + `refetchOnWindowFocus: false` means the LOC chip updates *only* when a `git:changed` WebSocket event is received. If the event bus connection drops and reconnects, any file edits made during the gap are silently invisible — the chip keeps showing the pre-disconnect value until the user makes another change that triggers a fresh push event. In the previous `staleTime: 0` regime a component remount (e.g., after a tab switch) would naturally re-prime the cache; that safety net is now gone.

Consider adding `refetchOnReconnect: true` (verify it fires for the non-window WebSocket path) or having the event bus emit a synthetic `git:changed` after reconnect so the existing invalidation path kicks in.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 33 to +35
const invalidate = useCallback(() => {
void utils.git.getStatus.invalidate({ workspaceId });
}, [utils, workspaceId]);
void queryClient.invalidateQueries({ queryKey });
}, [queryClient, queryKey]);
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 The invalidate callback captures queryKey at the time it was last memoized. When hostUrl is still null (the workspace host is loading), queryKey is ["diff-stats", null, workspaceId] and any git:changed event fired in that window calls invalidateQueries with that null key — targeting a query that was never enabled and has no cached data, so the invalidation is silently dropped. Adding an early-return guard makes the intent explicit and avoids the vacuous call.

Suggested change
const invalidate = useCallback(() => {
void utils.git.getStatus.invalidate({ workspaceId });
}, [utils, workspaceId]);
void queryClient.invalidateQueries({ queryKey });
}, [queryClient, queryKey]);
const invalidate = useCallback(() => {
if (!hostUrl) return;
void queryClient.invalidateQueries({ queryKey });
}, [queryClient, queryKey, hostUrl]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts
Line: 33-35

Comment:
The `invalidate` callback captures `queryKey` at the time it was last memoized. When `hostUrl` is still `null` (the workspace host is loading), `queryKey` is `["diff-stats", null, workspaceId]` and any `git:changed` event fired in that window calls `invalidateQueries` with that null key — targeting a query that was never enabled and has no cached data, so the invalidation is silently dropped. Adding an early-return guard makes the intent explicit and avoids the vacuous call.

```suggestion
	const invalidate = useCallback(() => {
		if (!hostUrl) return;
		void queryClient.invalidateQueries({ queryKey });
	}, [queryClient, queryKey, hostUrl]);
```

How can I resolve this? If you propose a fix, please make it concise.

@saddlepaddle saddlepaddle merged commit c29c1b3 into main May 8, 2026
10 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

MocA-Love pushed a commit to MocA-Love/superset that referenced this pull request May 8, 2026
Recorded as integrated via -s ours after batch PRs #455-#464.

Taken via individual PRs:
- PR  1 (#455): v2 polish 前半 safe set (9 commits)
- PR  2 (#456): v2/host-service polish 中盤 (12 commits)
- PR  3 (#457): sidebar polish + jwt API (5 commits)
- PR  4 (#458): host-service tRPC retry/cache/timeout (3 commits)
- PR  5 (#459): v2 diff pane / file pane polish (2 commits)
- PR  7 (#462): host-service v2 canonical workspace.create + attachment store (PR1 superset-sh#3893 + PR2 superset-sh#3916)
- PR 11 (#463): agents API + onboarding (7 commits + 1 cleanup)
- PR 12 (#464): v1→v2 import flow rewrite (11 commits + 2 follow-ups)
- PR 13 (#460): host-service shell env probe + typo (2 commits)
- PR 16 (#461): marketplace 19 themes (1 commit)

Skipped / deferred (recorded as integrated for behind=0):
- PR  6: CLI v1 launch (superset-sh#3898 + 30+ CLI/SDK followups) — defer to dedicated migration
- PR  9: v2 PR3 (superset-sh#3940) + revert (superset-sh#4017) — net-zero pair
- PR 10: pty-daemon (superset-sh#3896, superset-sh#3971, superset-sh#4054) — fork keeps its terminal-host
- PR 14: Slack MCP-v2 (superset-sh#4197, superset-sh#4208) — depends on mcp-v2/sdk divergence
- PR 15: onboarding remaining (superset-sh#4115, superset-sh#4125, superset-sh#4214, superset-sh#4213, superset-sh#4222, superset-sh#4225) — depends on fork's deleted setup pages

Behind: 0 after this merge.
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