Skip to content

feat(code): cache per-task PR url + state for instant task switches#2343

Open
richardsolomou wants to merge 2 commits into
mainfrom
posthog-code/cache-task-pr-info
Open

feat(code): cache per-task PR url + state for instant task switches#2343
richardsolomou wants to merge 2 commits into
mainfrom
posthog-code/cache-task-pr-info

Conversation

@richardsolomou
Copy link
Copy Markdown
Member

@richardsolomou richardsolomou commented May 25, 2026

Problem

Switching between tasks in the sidebar/task detail used to block on a gh pr list --head <branch> call to figure out which PR each task corresponds to. That made the PR badge in the sidebar and the "Open PR" button in the task header pop in noticeably late on every task switch — and on cold start, every visible card paid the cost simultaneously.

Changes

Per-task PR info is now cached on the workspaces SQLite row (pr_url, pr_state, pr_fetched_at) and served stale-while-revalidate.

  • GitService.getTaskPrStatus returns the cached prState synchronously and schedules a deduplicated background revalidation. hasDiff for worktree tasks still computes inline because it's local-only and cheap.
  • The background revalidation runs the same gh lookups as before, writes through to the workspaces row, and — when the value changed — emits WorkspaceService.taskPrInfoChanged.
  • A new workspace.onTaskPrInfoChanged tRPC subscription is wired in App.tsx; the handler updates every matching getTaskPrStatus query via setQueriesData so the badge updates in place without a refetch.
  • workspace.getCachedPrUrl(taskId) lets useTaskPrUrl fall back to the cached URL so the task header's PR button opens the right PR before the live gh lookups return.
  • The taskPrInfoChanged emit uses a string literal (not the WorkspaceServiceEvent const) to avoid a circular import — workspace/service eagerly loads the DI container, which re-enters git/service.

Migration: 0007_stiff_reptil adds the three nullable columns to workspaces.

How did you test this?

  • pnpm --filter @posthog/code typecheck
  • pnpm exec biome check apps/code/src
  • pnpm --filter @posthog/code test ✅ (1436 pass; the 23 archive integration failures pre-exist on main because git commit is blocked in this signed-commit environment — verified by re-running them on a clean tree)

No manual UI verification was possible from this environment.

Publish to changelog?

no


Created with PostHog Code

Switching between tasks used to block on a `gh pr list --head <branch>` call
to figure out which PR the task corresponds to. That made the PR badge / "Open
PR" button pop in late on every task switch, especially after a cold start
where TanStack Query's in-memory cache is empty.

This stores the last-known PR URL and state on the workspace row in SQLite and
lets the renderer paint from that cache immediately, while a background
revalidation refreshes the value against GitHub. When the revalidated value
differs from cache, `WorkspaceService` broadcasts `taskPrInfoChanged` and the
renderer updates the matching `getTaskPrStatus` query in place.

- New `pr_url` / `pr_state` / `pr_fetched_at` columns on `workspaces`
  (migration `0007_stiff_reptil`) plus `updatePrCache` on the repository
- `GitService.getTaskPrStatus` returns cached state synchronously and schedules
  a deduplicated background `revalidateTaskPrStatus` that writes through and
  emits on change; `hasDiff` for worktree tasks still computes inline since
  it's local-only
- `workspace.onTaskPrInfoChanged` tRPC subscription + `App.tsx` handler
  pushes fresh values into the `getTaskPrStatus` cache via `setQueriesData`
- `workspace.getCachedPrUrl` lets `useTaskPrUrl` fall back to the cached
  PR URL so the task header opens the right PR before the live lookups
  return
- `taskPrInfoChanged` is emitted via string literal to avoid a circular
  import (workspace/service eagerly loads the DI container)

Generated-By: PostHog Code
Task-Id: 10fff210-3861-49ad-b1fc-dbabae2fcc17
@richardsolomou richardsolomou requested a review from a team May 25, 2026 03:15
@richardsolomou richardsolomou marked this pull request as ready for review May 25, 2026 03:15
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 25, 2026

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/code/src/main/services/git/service.ts:1858-1862
The "no-op" guard uses `!fresh.hasDiff` to skip emitting, but since `hasDiff` is never persisted to the DB there is no baseline to compare against. For a worktree task that has uncommitted changes but no PR (`prUrl: null, prState: null, hasDiff: true`), this condition will always be `false`, so `updatePrCache` and `emit("taskPrInfoChanged")` are called on every revalidation cycle even when nothing has actually changed since the last one. The repeated `setQueriesData` calls in `App.tsx` are harmless because React detects identical data, but the extra DB writes accumulate silently.

```suggestion
        const cachedHasDiff = !cachedPrState && !cachedPrUrl;
        if (
          cachedPrUrl === fresh.prUrl &&
          cachedPrState === fresh.prState &&
          cachedHasDiff === fresh.hasDiff
        ) {
```

### Issue 2 of 2
apps/code/src/renderer/App.tsx:132-152
The `prUrl` from the `taskPrInfoChanged` payload is silently dropped here. When a background revalidation discovers a new PR URL (e.g., a PR is created after the task was first opened), the DB is updated and the event fires with the URL, but the `getCachedPrUrl` React Query cache is never invalidated or updated. Because `getCachedPrUrl` has `staleTime: 60_000`, the `cached?.prUrl` fallback in `useTaskPrUrl` will serve the old (null) value for up to a minute, defeating the fast-path for the "Open PR" button that this cache was introduced to provide.

```suggestion
      onData: ({ taskId, prUrl, prState, hasDiff }) => {
        // Push the fresh PR info into every matching getTaskPrStatus query
        // (one per cloudPrUrl variant) so the renderer re-renders without
        // waiting for the next staleTime-driven refetch.
        queryClient.setQueriesData<{
          prState: typeof prState;
          hasDiff: boolean;
        }>(
          {
            ...trpcReact.workspace.getTaskPrStatus.pathFilter(),
            predicate: (query) => {
              const [, params] = query.queryKey as [
                unknown,
                { input?: { taskId?: string } } | undefined,
              ];
              return params?.input?.taskId === taskId;
            },
          },
          () => ({ prState, hasDiff }),
        );
        // Keep getCachedPrUrl in sync so the "Open PR" fast-path stays warm.
        queryClient.setQueryData(
          trpcReact.workspace.getCachedPrUrl.queryKey({ taskId }),
          { prUrl },
        );
      },
```

Reviews (1): Last reviewed commit: "feat(code): cache per-task PR url + stat..." | Re-trigger Greptile

Comment thread apps/code/src/main/services/git/service.ts Outdated
Comment thread apps/code/src/renderer/App.tsx Outdated
Addresses Greptile review feedback on #2343:

1. The no-op guard in `revalidateTaskPrStatus` used `!fresh.hasDiff` to skip
   emitting when "nothing changed", but `hasDiff` isn't persisted, so for a
   worktree with uncommitted changes and no PR the guard could never engage —
   every revalidation cycle wrote to the DB and emitted. `hasDiff` is now
   excluded from the event entirely (it's still computed inline by
   `getTaskPrStatus` and refreshed by TanStack on the staleTime cycle), and
   the emit decision is based purely on whether `prUrl` or `prState` changed.

2. The `App.tsx` subscription dropped `prUrl` from the event payload, so when
   a PR appeared after the task was first opened, the `getCachedPrUrl` query
   stayed at its 60s staleTime and `useTaskPrUrl`'s "Open PR" fast-path served
   `null` until the next refetch. The handler now `setQueryData`s the cached
   URL too, and merges `prState` into the existing `getTaskPrStatus` cache
   entry so any inline-computed `hasDiff` survives.

Generated-By: PostHog Code
Task-Id: 10fff210-3861-49ad-b1fc-dbabae2fcc17
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