Skip to content

feat(host-service): tRPC query timeout + retry, fix hung file tree and git status#3838

Merged
Kitenite merged 8 commits into
mainfrom
files-tab-instrumentation
Apr 30, 2026
Merged

feat(host-service): tRPC query timeout + retry, fix hung file tree and git status#3838
Kitenite merged 8 commits into
mainfrom
files-tab-instrumentation

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Apr 28, 2026

Summary

A hung host-service IPC (slow git status, slow fs.readdir, hung child process) was leaving the renderer spinning indefinitely on the Files tab and Changes tab. This PR adds a single server-side timeout middleware that bounds every tRPC query procedure, plus a TIMEOUT-aware retry policy on the client.

Server: queryProcedure builder

A new queryProcedure = protectedProcedure.use(timeoutMiddleware) in packages/host-service/src/trpc/index.ts:

  • Races next() against a per-procedure timeout, rejecting with TRPCError({ code: "TIMEOUT" })
  • Default 5s, override via .meta({ timeoutMs }) for procedures that legitimately take longer
  • Applies only to query procedures; mutations keep protectedProcedure because their latency varies

Routers updated:

  • filesystem.* (queries): listDirectory, getMetadata use the default 5s; readFile 30s; searchFiles 30s; searchContent 60s
  • git.* (queries): listBranches, getBaseBranch, getPullRequest use 5s; getStatus 15s; getCommitFiles 15s; listCommits/getDiff/getBranchSyncStatus/getPullRequestThreads 30s

Client: TIMEOUT-aware retry

In WorkspaceClientProvider.tsx, the QueryClient now retries TRPCClientError with code: "TIMEOUT" up to 2 times with linear backoff (300ms, 600ms). Other errors keep the previous single retry.

useFileTree simplification

Drop the bespoke client-side timeout/retry plumbing now that the server enforces a budget:

  • Removed: withAbortableTimeout, ListDirectoryTimeoutError, retry-timer set, retry recursion, loadDirectoryRef, activeLoadAbortControllersRef, LoadDirectoryOptions.attempt/ignoreInFlight
  • Kept: loadingDirectories/loadedDirectories/invalidatedDirectories state machine, cache fast path, fs:event-driven reload
  • ~120 lines removed from the hook

Other client-side changes carried over from earlier commits

  • abortOnUnmount: true on the workspace tRPC client so in-flight requests cancel on unmount/workspace switch
  • AbortSignal plumbed through filesystem.listDirectoryFsServicefs.ts so the post-readdir stat loop interrupts on cancel (Node's fs.readdir/fs.stat themselves ignore signals; we batch the stat loop in groups of 16 with a signal check between batches)
  • FilesTab loading spinner only on workspaceQuery.isLoading (not isFetching) to avoid flashing on background refetches
  • Removed unused workspaceName prop from FilesTab/WorkspaceSidebar/WorkspaceContent

Side benefits

  • Changes tab no longer spins forever on a slow git status
  • All filesystem/git queries inherit hung-IPC protection without per-feature opt-in
  • Future query authors get the same protection by default, with explicit .meta({ timeoutMs }) for known-slow ones

Caveats

  • Node's fs.readdir is uncancellable — a single huge flat directory on a slow disk still runs to completion server-side, then the procedure rejects via the middleware timeout. Bounded zombie work, not zero.
  • 5s default may be too aggressive for some procedures we haven't audited yet. Other routers (chat, ports, etc.) still use protectedProcedure and aren't covered; opt-in deliberately as needed.
  • Mutations are not covered. Long file writes / deletes hang as before. Separate concern.

Test plan

  • Open the Files tab on a normal workspace and confirm it loads
  • Switch between workspaces quickly and confirm no stale entries appear from a prior workspace
  • Open a workspace whose host-service is slow/throttled and confirm listDirectory retries and either recovers or fails cleanly without leaving the spinner up
  • Open the Files tab while the workspace query is still loading; confirm spinner shows instead of "not available"
  • Open the Changes tab on a slow git status and confirm it doesn't spin forever — retries 2x then surfaces an error
  • Confirm bun run typecheck and bun test pass

- Time out listDirectory after 5s and retry up to 3x with linear backoff
- Abort in-flight requests on unmount and workspace/root change
- Show a loading spinner in FilesTab while the workspace query resolves
- Enable abortOnUnmount globally on the workspace tRPC client
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 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

Refactors directory loading in useFileTree to use abortable, timeout-bounded fetches with retry-on-timeout and structured LoadDirectoryOptions; updates callers to use { force: true }. Removes workspaceName prop from sidebar/files components and adds FilesTab loading UI. TRPC client set to abort on unmount.

Changes

Cohort / File(s) Summary
useFileTree Hook Refactor
apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts
Rewrote loadDirectory to accept LoadDirectoryOptions (force, ignoreInFlight, attempt), apply cached listDirectory.getData immediately, perform abortable server fetches with a 5s timeout via AbortController, track/cleanup controllers and retry timers, and implement retry-on-timeout (up to 3 attempts with backoff). Updated internal call sites and fs event handlers to pass { force: true }.
FilesTab UI & Props
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx
Removed workspaceName prop; availability gating switched to rootPath. Shows Loader2 + "Loading files..." when workspaceQuery.isLoading; otherwise renders "Workspace worktree not available" if no rootPath. Updated Lucide icon imports.
WorkspaceSidebar & Page Prop Changes
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
Removed workspaceName?: string from props and stopped forwarding it to FilesTab.
TRPC Client Config
packages/workspace-client/src/workspace-trpc.ts
Initialized createTRPCReact<AppRouter> with { abortOnUnmount: true } so in-flight TRPC requests are aborted when React components unmount.
Server-side: filesystem router
packages/host-service/src/trpc/router/filesystem/filesystem.ts
listDirectory resolver now forwards the handler context signal into service.listDirectory so RPC can be cancelled.
FsService signature
packages/workspace-fs/src/core/service.ts
Extended FsService.listDirectory signature to accept optional options?: { signal?: AbortSignal }.
Workspace FS: listDirectory abortability
packages/workspace-fs/src/fs.ts
listDirectory accepts optional signal and calls signal.throwIfAborted() at key points (after validation, after readdir, and before per-entry processing) to make listing interruptible.
Host service forwarding
packages/workspace-fs/src/host/service.ts
listDirectory now accepts an options param and forwards options?.signal to underlying listDirectory implementation.

Sequence Diagram(s)

sequenceDiagram
    participant FilesTab as FilesTab (UI)
    participant Hook as useFileTree
    participant TRPC as workspaceTrpc client
    participant Router as filesystemRouter
    participant Service as FsHostService
    participant HostFS as WorkspaceFS

    FilesTab->>Hook: loadDirectory(options)
    Hook->>Hook: apply cached listDirectory.getData (if present)
    alt needs server fetch
        Hook->>TRPC: start fetch (with AbortController + 5s timeout)
        TRPC->>Router: RPC listDirectory (propagate signal)
        Router->>Service: service.listDirectory(..., { signal })
        Service->>HostFS: listDirectory(..., { signal })
        rect rgba(200,200,255,0.5)
            HostFS-->>Service: entries / error
        end
        Service-->>Router: entries / error
        Router-->>TRPC: response / error
        TRPC-->>Hook: response or error
        alt timeout
            Hook->>Hook: schedule retry (attempt+1) after backoff (up to 3)
            Hook-->>Hook: re-invoke loadDirectory({force:true, ignoreInFlight:true, attempt:next})
        end
    end
    FilesTab-->>Hook: unmount or root change
    Hook->>Hook: abort active controllers & clear retry timers
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I poked the tree, then pulled the thread,

Timeouts tapped and retries sped,
Controllers hum, the spinner spun,
Cached leaves shown, then fetching done,
Hooray — the explorer hops and spreads!

🚥 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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main fix: adding timeout and retry logic for tRPC queries to prevent hung file tree and git status operations.
Description check ✅ Passed The description is comprehensive and well-structured, covering server-side timeout middleware, client-side retry logic, hook simplifications, and test plan.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch files-tab-instrumentation

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

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 28, 2026

Greptile Summary

This PR adds resilience to the file-tree's listDirectory calls: a 5 s timeout with up to 3 linear-backoff retries, ref-based stale-request guards (abort on unmount/context change), a cache-before-fetch path, and a loading spinner in FilesTab while the workspace query resolves. The logic is well-structured — request lifecycle, retry scheduling, and cleanup are all handled correctly with no apparent correctness issues.

Confidence Score: 5/5

Safe to merge; all remaining findings are minor style/edge-case suggestions that do not affect correctness.

No P0/P1 issues found. The timeout + retry logic, abort-controller lifecycle, stale-request guards, and cleanup paths are all correct. The two P2 comments are optional improvements.

No files require special attention.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts Major resilience overhaul: adds 5 s timeout + linear-backoff retries, ref-based stale-request guards, cache-before-fetch, and proper cleanup on unmount/context change.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx Empty-state guard changed from checking workspaceQuery.data?.worktreePath to !rootPath, with a loading spinner when the workspace query is in-flight.
packages/workspace-client/src/workspace-trpc.ts Adds abortOnUnmount: true globally to the tRPC React client — safe one-liner that complements the hook-level abort logic.

Sequence Diagram

sequenceDiagram
    participant FT as FilesTab
    participant UFT as useFileTree
    participant AC as AbortController
    participant TRPC as tRPC (listDirectory)

    FT->>UFT: mount (rootPath, workspaceId)
    UFT->>UFT: isMountedRef = true
    UFT->>UFT: loadDirectory(rootPath, {force:true})
    UFT->>UFT: check tRPC cache (getData)
    alt cache hit
        UFT->>UFT: applyDirectoryEntries (cached)
    end
    UFT->>AC: new AbortController
    UFT->>TRPC: fetch(input, signal) + 5s timeout
    alt success
        TRPC-->>UFT: entries
        UFT->>UFT: isRequestCurrent? applyDirectoryEntries
    else timeout (5s)
        UFT->>AC: abort()
        UFT->>UFT: catch ListDirectoryTimeoutError
        alt nextAttempt <= MAX_ATTEMPTS (3)
            UFT->>UFT: schedule retry (300ms * attempt)
            UFT->>UFT: loadDirectory(path, {attempt:N, force, ignoreInFlight})
        else all retries exhausted
            UFT->>UFT: clearDirectoryLoading
        end
    else external abort
        AC->>TRPC: abort signal
        UFT->>UFT: isRequestCurrent false, discard
    end
    FT->>UFT: unmount
    UFT->>UFT: clearRetryTimers, abortActiveLoads, isMountedRef=false
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts
Line: 157-165

Comment:
**Only timeouts trigger retries — not other transient errors**

Network failures, server 5xx errors, or host-service crashes will fall through to the `console.error` + `clearDirectoryLoading` path without retrying. Given the PR goal of surviving a "slow or hung host service", a connection-reset or unexpected server error would still leave the directory in a permanent failed state. Consider whether `shouldRetryListDirectory` should also cover `DOMException` `AbortError` from non-timeout aborts or generic network errors if the intent is broad resilience.

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

---

This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx
Line: 474

Comment:
**`isFetching` triggers the spinner during background refetches**

`workspaceQuery.isFetching` is `true` whenever a fetch is in progress — including background refetches that happen after the query already has data. If a workspace genuinely has no `worktreePath` (confirmed by a previous fetch) and a background refetch starts, `!rootPath` and `isFetching` are both true, so the spinner replaces "Workspace worktree not available" for the duration of the refetch. Using `workspaceQuery.isLoading` (true only when there is no data yet) would avoid this flash while still covering the intended initial-load case.

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

Reviews (1): Last reviewed commit: "fix(desktop): make file tree resilient t..." | Re-trigger Greptile

Comment on lines +157 to +165
function shouldRetryListDirectory(
error: unknown,
nextAttempt: number,
): boolean {
return (
error instanceof ListDirectoryTimeoutError &&
nextAttempt <= LIST_DIRECTORY_MAX_ATTEMPTS
);
}
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 Only timeouts trigger retries — not other transient errors

Network failures, server 5xx errors, or host-service crashes will fall through to the console.error + clearDirectoryLoading path without retrying. Given the PR goal of surviving a "slow or hung host service", a connection-reset or unexpected server error would still leave the directory in a permanent failed state. Consider whether shouldRetryListDirectory should also cover DOMException AbortError from non-timeout aborts or generic network errors if the intent is broad resilience.

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

Comment:
**Only timeouts trigger retries — not other transient errors**

Network failures, server 5xx errors, or host-service crashes will fall through to the `console.error` + `clearDirectoryLoading` path without retrying. Given the PR goal of surviving a "slow or hung host service", a connection-reset or unexpected server error would still leave the directory in a permanent failed state. Consider whether `shouldRetryListDirectory` should also cover `DOMException` `AbortError` from non-timeout aborts or generic network errors if the intent is broad resilience.

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

<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
Workspace worktree not available
<div className="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground">
{workspaceQuery.isLoading || workspaceQuery.isFetching ? (
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 isFetching triggers the spinner during background refetches

workspaceQuery.isFetching is true whenever a fetch is in progress — including background refetches that happen after the query already has data. If a workspace genuinely has no worktreePath (confirmed by a previous fetch) and a background refetch starts, !rootPath and isFetching are both true, so the spinner replaces "Workspace worktree not available" for the duration of the refetch. Using workspaceQuery.isLoading (true only when there is no data yet) would avoid this flash while still covering the intended initial-load case.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx
Line: 474

Comment:
**`isFetching` triggers the spinner during background refetches**

`workspaceQuery.isFetching` is `true` whenever a fetch is in progress — including background refetches that happen after the query already has data. If a workspace genuinely has no `worktreePath` (confirmed by a previous fetch) and a background refetch starts, `!rootPath` and `isFetching` are both true, so the spinner replaces "Workspace worktree not available" for the duration of the refetch. Using `workspaceQuery.isLoading` (true only when there is no data yet) would avoid this flash while still covering the intended initial-load case.

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

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 the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts`:
- Around line 157-164: The retry check in shouldRetryListDirectory incorrectly
limits retries because attempt starts at 1; change the inequality so the final
planned retry is allowed (e.g. compare nextAttempt against
LIST_DIRECTORY_MAX_ATTEMPTS + 1 instead of LIST_DIRECTORY_MAX_ATTEMPTS) to
restore the intended 3 retries; update the same fix where the same pattern
appears (the other checks around the blocks referencing
ListDirectoryTimeoutError and LIST_DIRECTORY_MAX_ATTEMPTS at the other
occurrences).
🪄 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: a5ab21cb-f33b-48fe-b3da-e9dc3271fb72

📥 Commits

Reviewing files that changed from the base of the PR and between a8e8556 and 2f7d840.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx
  • packages/workspace-client/src/workspace-trpc.ts

Comment on lines +157 to +164
function shouldRetryListDirectory(
error: unknown,
nextAttempt: number,
): boolean {
return (
error instanceof ListDirectoryTimeoutError &&
nextAttempt <= LIST_DIRECTORY_MAX_ATTEMPTS
);
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

The retry budget is off by one.

attempt starts at 1, so nextAttempt <= LIST_DIRECTORY_MAX_ATTEMPTS only schedules retries for attempts 2 and 3. That gives you 2 retries total, not the 3 retries described in the PR, so a slow host request can still fail one timeout earlier than intended.

🔧 Proposed fix
-			const { force = false, ignoreInFlight = false, attempt = 1 } = options;
+			const { force = false, ignoreInFlight = false, attempt = 0 } = options;-					}, getListDirectoryRetryDelayMs(attempt));
+					}, getListDirectoryRetryDelayMs(nextAttempt));

Also applies to: 361-363, 427-440

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

In `@apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts`
around lines 157 - 164, The retry check in shouldRetryListDirectory incorrectly
limits retries because attempt starts at 1; change the inequality so the final
planned retry is allowed (e.g. compare nextAttempt against
LIST_DIRECTORY_MAX_ATTEMPTS + 1 instead of LIST_DIRECTORY_MAX_ATTEMPTS) to
restore the intended 3 retries; update the same fix where the same pattern
appears (the other checks around the blocks referencing
ListDirectoryTimeoutError and LIST_DIRECTORY_MAX_ATTEMPTS at the other
occurrences).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

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.

1 issue found across 3 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/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx">

<violation number="1" location="apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx:474">
P2: `workspaceQuery.isFetching` is `true` during background refetches even when cached data already exists. If a workspace genuinely has no `worktreePath`, any background refetch will cause the spinner to flash instead of showing "Workspace worktree not available". Use `workspaceQuery.isLoading` alone here, which is only `true` when there is no cached data yet.</violation>
</file>

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

…orkspace name

- Use AbortController membership in the active set as the "still relevant" check, replacing isMountedRef + activeContextRef + isRequestCurrent
- Inline single-use helpers (markDirectoryLoading, clearDirectoryLoading, shouldRetryListDirectory, getListDirectoryRetryDelayMs)
- Drop the workspaceName prop from FilesTab/WorkspaceSidebar/WorkspaceContent; the files header is just "Explorer"
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.

♻️ Duplicate comments (1)
apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts (1)

295-297: ⚠️ Potential issue | 🟠 Major

Retry budget is still off by one (2 retries instead of 3).

Line 295 starts attempt at 1, and Line 366 caps on nextAttempt <= 3, so only attempts 2 and 3 are retried. That yields 2 retries, not the stated 3.

Suggested fix
-			const { force = false, ignoreInFlight = false, attempt = 1 } = options;
+			const { force = false, ignoreInFlight = false, attempt = 0 } = options;
...
-					}, LIST_DIRECTORY_RETRY_DELAY_MS * attempt);
+					}, LIST_DIRECTORY_RETRY_DELAY_MS * nextAttempt);

Also applies to: 363-367, 375-375

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

In `@apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts`
around lines 295 - 297, The retry budget is off by one because options
destructures attempt = 1 while the retry gating uses nextAttempt <= 3; change
the logic so three retries are allowed by either initializing attempt to 0
(options: attempt = 0) or by raising the cap (nextAttempt <= 4) so nextAttempt
progression (attempt -> attempt+1) yields three retry attempts; update the
symbols attempt and nextAttempt in the useFileTree retry logic (the options
destructuring and the nextAttempt <= 3 check) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts`:
- Around line 295-297: The retry budget is off by one because options
destructures attempt = 1 while the retry gating uses nextAttempt <= 3; change
the logic so three retries are allowed by either initializing attempt to 0
(options: attempt = 0) or by raising the cap (nextAttempt <= 4) so nextAttempt
progression (attempt -> attempt+1) yields three retry attempts; update the
symbols attempt and nextAttempt in the useFileTree retry logic (the options
destructuring and the nextAttempt <= 3 check) accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 15a6398b-423c-46a9-b77c-d88b713c0218

📥 Commits

Reviewing files that changed from the base of the PR and between 2f7d840 and c44a81a.

📒 Files selected for processing (4)
  • apps/desktop/src/renderer/hooks/host-service/useFileTree/useFileTree.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
💤 Files with no reviewable changes (2)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/FilesTab.tsx

…tion

# Conflicts:
#	apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
When the renderer aborts a listDirectory query (timeout or workspace switch),
the host-service was running fs.readdir + per-symlink fs.stat to completion
and discarding the result. Node's fs API doesn't honor AbortSignal, but we
can short-circuit between operations — useful in symlink-heavy directories
(node_modules) where the per-entry stat loop dominates.
…e abort

Process per-entry symlink stats in batches of 16 with a signal check
between batches. With Promise.all-over-everything, all stats kick off in
the same microtask and an in-flight abort can't interrupt any of them.
Batching bounds the zombie work to one batch (~16 stats) per abort.
Hung host-service IPC (slow git, slow filesystem ops) was leaving the
renderer spinning indefinitely. Replace the bespoke per-hook retry/timeout
in useFileTree with a single server-side middleware that bounds every
query procedure.

Server (queryProcedure builder + middleware):
- t.middleware races next() against a per-procedure timeout, rejecting
  with a TRPCError({ code: "TIMEOUT" }). Default 5s, override via
  `.meta({ timeoutMs })` on procedures that legitimately take longer
  (search, listCommits, getDiff, getStatus, getBranchSyncStatus,
  getPullRequestThreads, readFile).
- Switch all query procedures in filesystem and git routers to
  queryProcedure; mutations remain on protectedProcedure.

Client (workspace-client QueryClient):
- defaultOptions.queries.retry retries TIMEOUT errors up to 2 times with
  linear backoff (300ms, 600ms). Other errors keep the previous single
  retry.

useFileTree:
- Drop withAbortableTimeout, ListDirectoryTimeoutError, retry-timer set,
  retry recursion, loadDirectoryRef, activeLoadAbortControllersRef.
- Just await utils.filesystem.listDirectory.fetch(input). React Query's
  retry policy handles TIMEOUT; abortOnUnmount cancels in-flight on
  unmount/workspace switch. ~120 lines removed.

Side benefit: git.getStatus / listBranches / listCommits etc. all gain
hung-IPC protection automatically. The Changes tab no longer spins
forever on a slow `git status`.
@Kitenite Kitenite changed the title fix(desktop): make file tree resilient to slow directory loads feat(host-service): tRPC query timeout + retry, fix hung file tree and git status Apr 29, 2026
Documents the queryProcedure / timeoutMiddleware pattern: where it
lives, how to set per-procedure budgets via .meta({ timeoutMs }),
the current budget table, and what timeouts do (and don't) interrupt.
@Kitenite Kitenite merged commit 0d6225d into main Apr 30, 2026
14 checks passed
@Kitenite Kitenite deleted the files-tab-instrumentation branch April 30, 2026 00:36
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