Branch status resolves an open PR across the whole fork network first, so a merged fork PR can never hide an open upstream PR for the same head. Only when no target has an open PR does the branch's newest closed/merged PR come back, as history. The panel shows that history as a compact note and offers creating the next PR below it, instead of either sticking on a terminal PR or going blank after a merge. Terminal associations stay persisted for reload continuity but are never treated as authority: they revalidate on the discovery cadence and on focus. History is looked up only for the branch's own remote and name, and remembered per repo+branch, so the extra lookup cannot exhaust the route's resolve budget. The checks summary and merge-permission lookup are skipped for a closed or merged PR, where neither is actionable.
9.7 KiB
9.7 KiB
GitHub Module Documentation
Purpose
- This module owns GitHub auth, Octokit access, repo resolution, and Pull Request status resolution for OpenChamber.
- From user perspective, this is the layer that lets the app know which PR belongs to a local branch and keeps that UI feeling current.
Entrypoints and structure
packages/web/server/lib/github/index.js: public server entrypoint.routes.jsloads it lazily withawait import('./index.js')and destructures the handler it needs, so a re-export removed from here breaks a route at request time rather than at build time. Static "unused export" reports do not see these consumers.packages/web/server/lib/github/routes.js: Express route registration for/api/github/*endpoints.packages/web/server/lib/github/auth.js: auth storage, multi-account support, client id, scope config.packages/web/server/lib/github/device-flow.js: OAuth device flow.packages/web/server/lib/github/octokit.js: Octokit factory for the current auth.packages/web/server/lib/github/repo/index.js: remote URL parsing and directory-to-repo resolution.packages/web/server/lib/github/pr-status.js: PR lookup across remotes, forks, and upstreams.packages/web/server/index.js: API route layer that calls this module.packages/web/src/api/github.ts: web client wrapper for GitHub endpoints.
Public exports
Auth
getGitHubAuth(): current auth entry.getGitHubAuthAccounts(): all configured accounts.setGitHubAuth({ accessToken, scope, tokenType, user, accountId }): save or update account.activateGitHubAuth(accountId): switch active account.clearGitHubAuth(): clear current account.getGitHubClientId(): resolve client id.getGitHubScopes(): resolve scopes.GITHUB_AUTH_FILE: auth file path.
Device flow
startDeviceFlow({ clientId, scope }): request device code.exchangeDeviceCode({ clientId, deviceCode }): poll for access token.
Octokit
getOctokitOrNull(): current Octokit ornull.
Repo
parseGitHubRemoteUrl(raw): parse SSH or HTTPS remote URL into{ owner, repo, url }.resolveGitHubRepoFromDirectory(directory, remoteName): resolve GitHub repo from a local git remote.
Auth storage and config
- Auth storage:
~/.config/openchamber/github-auth.json - Writes are atomic and file mode is
0o600. - Client ID resolution order:
OPENCHAMBER_GITHUB_CLIENT_ID->settings.json-> default. - Scope resolution order:
OPENCHAMBER_GITHUB_SCOPES->settings.json-> default. - Account id resolution order: explicit
accountId-> user login -> user id -> token prefix.
PR integration overview
- The UI asks
github.prStatus(directory, branch, remote?)frompackages/web/src/api/github.ts. - That hits
GET /api/github/pr/statusinpackages/web/server/index.js. - The route calls
resolveGitHubPrStatus(...)inpackages/web/server/lib/github/pr-status.js. - The resolver finds the most likely repo and PR for a local branch.
- The route then enriches that result with checks, mergeability, and permission-related fields.
- The client caches and shares the result between sidebar and Git view.
Consumers of PR data
packages/ui/src/components/session/SessionSidebar.tsxreads all PR entries and maps them todirectory::branch.packages/ui/src/components/session/sidebar/SessionGroupSection.tsxrenders the compact badge, PR number, title, checks summary, and GitHub link.packages/ui/src/components/views/git/PullRequestSection.tsxuses the same shared entry for the full PR workflow.packages/ui/src/components/ui/MemoryDebugPanel.tsxreads request counters for debugging.
How PR resolution works
- It reads local git status and remotes first.
- It ranks remotes in this order: explicit remote, tracking remote,
origin,upstream, then the rest. - It resolves those remotes into GitHub repos.
- It expands each repo through
parentandsourceso PRs in upstream repos can still be found. - It skips PR lookup when the current branch matches that repo's default branch.
- It first searches for open PRs by likely source owner plus exact head branch.
- If that fails, it falls back to broader GitHub search for open PRs on the branch name.
- An open PR from any candidate repo always wins over a closed/merged one, so a merged fork PR can never hide an open upstream PR for the same head.
- Only when no target has an open PR does it return the branch's newest closed/merged PR, as history.
- History is looked up only for the ranked-first remote and the branch's own name — the repo it actually pushes to. Live status is worth searching the whole fork network for; history is not, and asking every target for it multiplies serial GitHub calls until the route hits its
12sresolve timeout and returns no status at all. - The history answer is remembered per repo+branch so discovery polls do not re-query it: a found closed/merged record for
6h, and "no history yet" for10m. A found record only changes if a second PR appears on the same head, and while that one is open the open-PR path wins without ever reading this cache. - Creating, merging, or closing a PR invalidates both the shared repo pull list and that remembered history.
- The route skips the checks summary and the merge-permission lookup for a closed/merged PR: neither is actionable, and both cost extra GitHub calls.
403and404during repo lookups are treated as expected gaps, not hard errors.
Shared client state model
- Client key is effectively
directory::branch. - One entry stores last known status, loading state, error, timestamps, watcher count, identity, and resolved remote.
- Requests are deduplicated by branch signature, not by component instance.
- This keeps sidebar and Git view aligned and avoids duplicated fetches.
Persistence
- PR state is persisted in local storage under
openchamber.github-pr-status. - Persisted fields include status, timestamps, identity, and resolved remote.
- Runtime-only details are not persisted.
- Persisted entries expire after 12 hours.
- On reload, users get last known state first, then background refresh resumes.
Polling and refresh model
- There are two layers: entry-level polling in
useGitHubPrStatusStoreand repo scanning inuseGitHubPrBackgroundTracking. - Entry-level polling decides when a known branch should revalidate PR state.
- Background tracking decides which directories and branches should even be watched.
Entry-level polling rules
- Start watching -> immediate refresh.
- If no PR is found yet -> retry after
2sand5s. - Still no PR -> discovery refresh every
5m. - Open PR with pending checks -> refresh about every
1m. - Open PR with non-pending checks -> refresh about every
5m. - Open PR without a stable checks signal -> refresh about every
2m. - Closed or merged PR -> discovery refresh every
5m(do not permanently stop polling). - Hidden tab -> skip polling.
- Non-forced refreshes use a
90sTTL. - Failed non-forced attempts also observe the
90sTTL so transient server or rate-limit failures cannot retry on every sidebar update. Forced user/action refreshes bypass this guard.
Persistence notes for terminal PRs
- Closed/merged branch associations are persisted like open ones, so a reload still shows that the branch's PR was merged.
- Hydrate resets
lastDiscoveryPollAtfor them, so restored history revalidates on the first watcher tick instead of waiting out a discovery interval.
Background tracking rules
- Track up to
50likely directories. - Sources are current directory, projects, worktrees, active sessions, and archived sessions.
- Active directory branch TTL is
15s. - Background directory branch TTL is
2m. - Background scan wakes every
15s, but only fetches directories whose TTL expired. - Each scan reads
branch,tracking,ahead, andbehindfrom git status. - If any of those branch signals change, that branch's PR status refreshes immediately.
- After that, one more delayed refresh runs after
5sto catch GitHub eventual consistency.
UI refresh triggers
- App or tab becomes visible.
- Window regains focus.
- Current branch changes.
- Tracking branch changes.
- Ahead or behind changes.
- User selects a different remote in Git view.
- GitHub auth state changes.
Action-based refreshes in Git view
- After
Create PR-> refresh now, then after2sand5s. - After
Merge PR-> refresh now, then after2sand5s. - After
Mark ready for review-> refresh now, then after2sand5s. - After
Update PR-> refresh now, then after2sand5s.
Sidebar behavior
- Sidebar shows only compact PR state.
- Aggregation is by
directory::branch, so multiple sessions on one branch share one signal. - If multiple entries exist, sidebar keeps the strongest visible PR state.
- Visual state is based on PR health, not merge permissions.
Git view behavior
- Git view watches one branch directly.
- It supports create, edit, mark ready, and merge.
- It can probe alternate remotes so fork-heavy setups still find the right PR.
- It uses the same shared store as the sidebar.
Failure handling
- If GitHub is disconnected, API returns
connected: false. - If a repo is private or inaccessible, resolver calls may quietly return no PR.
- Sidebar stays quiet on missing or inaccessible PR state.
- Git view is where explicit PR-level problems should be shown.
Notes for contributors
- Keep the UI calm. Do not add noisy diagnostics to the sidebar.
- Prefer shared state over per-component fetches.
- Prefer event-shaped refreshes over blind frequent polling.
- Prefer correctness for fork and multi-remote setups over assuming
originis enough. - Device flow handles GitHub
authorization_pendingat caller level. - Repo parser supports
git@github.com:,ssh://git@github.com/, andhttps://github.com/.