Skip to content

fix(github): a twin project's PR cards, rows and dialogs act on the host that returned the item - #31252

Merged
nwparker merged 2 commits into
mainfrom
nwparker/github-twin-item-host
Oct 20, 2026
Merged

nwparker merged 2 commits into
mainfrom
nwparker/github-twin-item-host

Conversation

@nwparker

@nwparker nwparker commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​287 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​287
Prod 11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​111 $\color{#cf222e}{\Huge{\mathbf{−}}}$​27 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​84

ELI5

If the same project is added on two machines (say this laptop and an SSH server), the GitHub task page lists PRs from both. When you reviewed, merged, edited or started work on one of those PRs, Orca could run the action on the other machine. Each PR now remembers which machine listed it, and its buttons act on that machine.

What Changed

Before: the task page selects both copies of a twin project and fetches items from each host. But a GitHubWorkItem carried only repoId. Every place that acted on an item rebuilt its host with repoMap.get(item.repoId), which returns whichever twin was listed last:

  • the row cells: review, merge, status and assignees;
  • PR checks loading;
  • the PR page or item dialog;
  • "start work" (the composer's task source).

The cache sync and refresh reconcile also keyed rows by repoId + id, so one twin's row was silently swapped for the other twin's copy.

After:

  • The fetcher stamps an optional, renderer-only repoHostId on each item. It is the host of the row the request read (getGitHubWorkItemRowHostId): the id row on the source's host, else the id+path row, else the only row.
  • Every GitHub item action site resolves its row through one function, getGitHubWorkItemRepo (src/renderer/src/lib/github-work-item-repo-row.ts):
    • a twin resolves on the stamped host;
    • a project on only one host keeps its row whatever the stamp says;
    • an unstamped item keeps the old by-id row.
  • The task-page row key (taskPageWorkItemKey), the React row key and the dialog's cache lookup include the host, so the two hosts' copies of a PR stay separate.

Why

  • The host that answered the fetch is the only reliable fact about an item's origin, so it is captured at the fetch boundary, and every reader goes through one resolver instead of patching each call site.
  • Alternatives considered:
    • Keying the whole task page by host-qualified repo rows. That is a much larger change.
    • Refusing twins on the task page. That would remove a working list.
  • No wire change: the field is optional and stamped in the renderer, older hosts never send it, and main never reads it. The in-memory work-item cache isn't persisted, so unstamped copies only come from optimistic stubs and keep their old behaviour.

Not changed (out of scope, reported to main):

  • GitLab items still rebuild their host from repoMap.get(item.repoId).
  • Main's validateRegisteredRepo looks repos up by id only, which can refuse the second twin's local+SSH fetch.
  • The pending-mutation overlay keys stay shared across twins. Both rows are the same GitHub PR, so showing an optimistic edit on both is accurate.

Linked Issue

N/A (internal follow-up).

Visual Proof

N/A: routing only. Rows look the same; the actions go to a different host.

Testing

New tests that fail on main:

  • components/task-page/github/rows-twin-host-routing.test.tsx: the review, merge, status and assignees cells get the twin row that returned the item.
  • components/use-task-page-workspace-actions-twin-host.test.tsx: starting work from a PR card opens the composer for that host.
  • store/slices/github-work-items-twin-host-stamp.test.ts: first and later pages stamp the row's host.
  • components/task-page-cache-selectors.test.ts: the dialog opens that host's cached copy, and the cache sync keeps each twin row's own copy.

Also added: lib/github-work-item-repo-row.test.ts, which covers twin, lone-row and unstamped resolution.

Runs:

  • store/github, store/slices/github*, components/task-page*, components/use-task-page* and lib/github* (120 files) pass single-worker.

  • Full static-analysis job run locally.

  • Not verified live on real hosts: this is renderer routing, covered by unit tests.

  • Automated tests added/updated

Review

Codex (gpt-6.1-sol, high) and an independent Opus review, two rounds each.

  • Round 1, Opus: twin copies collapsed in the cache sync. Fixed by keying on host.
  • Round 1, Codex: a local source for a runtime-owned single repo was stamped local. Fixed with the lone-row rule.
  • Round 2: both reviews clean.

Agent skill upstream boundary

  • Not applicable

Notes

  • Security: no new inputs.
  • Cross-platform: no platform code.
  • SSH and runtime: covers local, SSH and runtime twins.
  • Mobile: not touched.
  • Performance: one linear scan of repos per row render, which is small.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • N/A visual proof with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote impact considered
  • Lint, typecheck and related tests pass locally

@coderabbitai

coderabbitai Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 6 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1a40ba19-2f76-4d76-9616-08829bb6ebff
📥 Commits

Reviewing files that changed from the base of the PR and between f812c67 and 0894b56.

📒 Files selected for processing: 16
  • src/renderer/src/components/task-page-cache-selectors.test.ts
  • src/renderer/src/components/task-page-cache-selectors.ts
  • src/renderer/src/components/task-page-work-item-signatures.ts
  • src/renderer/src/components/task-page/github/Rows.tsx
  • src/renderer/src/components/task-page/github/rows-twin-host-routing.test.tsx
  • src/renderer/src/components/use-task-page-github-detail.ts
  • src/renderer/src/components/use-task-page-github-list-projection.ts
  • src/renderer/src/components/use-task-page-workspace-actions-twin-host.test.tsx
  • src/renderer/src/components/use-task-page-workspace-actions.ts
  • src/renderer/src/lib/github-work-item-repo-row.test.ts
  • src/renderer/src/lib/github-work-item-repo-row.ts
  • src/renderer/src/store/github/work-item-aggregate-actions.ts
  • src/renderer/src/store/github/work-item-fetch-actions.ts
  • src/renderer/src/store/github/work-item-routing.ts
  • src/renderer/src/store/slices/github-work-items-twin-host-stamp.test.ts
  • src/shared/github/work-item-types.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@nwparker
nwparker merged commit 9693204 into main Oct 20, 2026
39 of 40 checks passed
nwparker added a commit that referenced this pull request Oct 20, 2026
…t that returned them (#31271)

GitLab task items now carry the host of the repo row that fetched them, and the
project-view dialog stamps the row its slug matched. Both resolve through one
provider-neutral helper (getWorkItemRepo, generalized from #31252's GitHub-only
getGitHubWorkItemRepo), so a repo id published by two hosts never routes an
action to the other host's row.
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