Repository navigation
fix(checks): keep cached check details on screen instead of re-spinning - #198
Merged
nplez1 merged 4 commits intoOct 28, 2026
Merged
Conversation
Expanding a check blanked the pane and showed "Loading check details…" every time, and a poll that landed while a detail was open deleted its cache entry before refetching, so the spinner came back on its own. Jenkins made it worse: it has no renderer cache at all, and one expand is up to three main-process GETs. A payload now stays on screen while a refresh runs, and `loading` means "refreshing": - `beginGitHubChecksTabDetails` carries the payload it already has (or one the shared cache holds) instead of nulling it; the spinner is rendered only when there is no payload yet, beside a subtle "Refreshing check details…" line when there is. - `resetGitHubChecksTabForSource` keeps its detail entries: a list refresh replacing row objects no longer re-reads every open detail. A context change (a push moves the head SHA) still drops them. - The sidebar stops deleting an entry whose status/conclusion moved. The shared `checkDetailsNeedRefresh` predicate decides instead: re-read when the row moved since the read, or when a still-running build outlives the checks TTL; a finished build is never re-read. - A failed refresh keeps the rows it had and reports itself beside them. - New `components/github/check-details-cache.ts` holds one bounded cache keyed by build identity, so the PR page, the item dialog and the sidebar share a payload rather than keeping three component-local maps. Refs #196
STYLEGUIDE's in-flight rule defers visible loading ~200ms; the full-details tab already does this. The inline panes showed "Refreshing check details…" immediately, so a local read that finished inside 200ms flashed a label beside rows that were already correct. Shared `useDelayedFlag` hook, used by the three inline surfaces. Refs #196
Review finding: a `gh pr checks` fallback row carries no checkRunId, only a workflow-run id that every job in that run shares, while the payload is filtered to one job by name — so two jobs of one run shared a cache key and could show each other's build. The url is per job, and it carries the host and repo, so it now leads the identity; a workflow-run key keeps the name. The same ordering bug was in both surfaces' row keys, so the shared helper now backs all three. Also from review: the failed-refresh merge moves into `settleGitHubChecksTabDetailsKeepingShown` (one tested helper instead of two hand-rolled ones), and the cache comment says what it actually saves. Refs #196
Delta-round finding: a check run's url is its `details_url`, and a third-party app can point several runs at one page — the Jenkins Checks plugin gives every run it publishes the build's url — so url-first made two runs, and two rows, share a key. Checks are back to id-first (check run, GitLab job), then url for the rows that carry no id at all, then the run id with the job name. Both cases are pinned: two runs sharing one details url, and two jobs of one workflow run with and without urls. Refs #196
nplez1
deleted the
nplez1/bug-jenkins-check-details-show-a-spinner-on-ever
branch
October 28, 2026 03:40
This was referenced Oct 28, 2026
nplez1
added a commit
that referenced
this pull request
Oct 28, 2026
) * fix(review): give the collapsed-thread bubble its gutter slot back The add-note "+" overlay and a collapsed PR review thread's bubble both claim the same gutter slot, so the "+" was drawn on top of the bubble and swallowed the click. Hovering a commented line therefore offered "add a note" and hid "there is a conversation here". The thread decorator now reports the lines it draws as bubbles and the note decorator's overlay skips them: one slot, one affordance, and the conversation wins. The Add Review Note chord still reads the editor selection, so a note can still be left on a threaded line from the keyboard — displaced, not dropped. Two fork-owned files split instead of growing past the max-lines budget: the line-set derivation and the overlay's own install effect move into their own modules. Closes #190. * fix(review): skip the "+" on threaded lines in the plain editor too Review round 1 on 68d29feb3e found the fix incomplete and the overlay fragile: - MonacoEditor's markdown annotations mount the same add-note decorator on the editor that carries the thread bubbles, so a markdown file with a collapsed thread still showed the "+" over the bubble. The thread decorator now runs first there and hands its occupied lines down. - Handing the occupied set in as an install dependency rebuilt the overlay whenever any thread anywhere was toggled or PR comments were refetched. That reinstall drops a gutter drag in flight and re-parks the button under a stationary pointer, so the running overlay is told through its handle instead, and the set is value-keyed like the commentable one. - A parked line that stops being ours (its thread collapsed) now hides the "+" instead of drawing it back over the bubble on the next scroll. - The test asserts the bubble decoration is really on the line, and that a thread opening does not rebuild the running button. * fix(review): hide a parked "+" whose line gained a thread, and pin the rest Delta re-check of d0e5714983 left three small gaps and two test holes: - the scroll handler skipped the "+" on a line that stopped being ours instead of hiding it, leaving a button parked at the wrong pixels for the wrong line; it now hides and clears the hovered line. - the occupied-line setter could hide the "+" mid-drag, where that button is the only sign the gesture is live; it stands down there. - the line-sets comment claimed a new set rebuilds the overlay, which the previous commit stopped doing. - the test now also covers a thread collapsing under a parked "+", the markdown editor's forwarding of the occupied lines, and the fake keeps one decoration record per collection so a clear by one collection cannot erase another's. * chore(lint): record the two flaky-test-pattern reds #198 merged The base commit 941acb7 lands two `check:flaky-test-pattern-ratchet` findings in its own test files, so the lint gate on any branch cut from it fails on work that branch never touched. Neither file is in this branch's diff, and both are static ast-grep findings rather than a bad run. Recorded with one narrow `match` per file, so a new finding in either still fails the gate, plus the evidence line LOCAL-PATCHES.md requires. The fix belongs to the checks-panel work: fake timers or a fixed timestamp.
nplez1
added a commit
that referenced
this pull request
Oct 28, 2026
The release gate on the merge found 140 failures across 25 test files. Three were merge damage in product code: - `updater-prerelease-feed.ts`: upstream's new `fetchLatestStableReleaseTag` hardcoded `stablyai/orca` in both the `releases/latest` URL and the redirect regex, so a fork build's stable-fallback probe never matched the fork's own redirect and every background check died in preflight. Retargeted to `nplez1/orca` with the rest of the file. - `use-checks-panel-ai-acknowledgement.tsx`: the resolution dropped the latest-consumer refresh ref both parents had, so a head-churn rerender bound the post-launch refresh to a stale `fetchComments` and the forced refresh was silently dropped. - `ai-vault-session-refresh.ts`: upstream declares the throttle test hook in this module; the fork's throttle lives in its own file, so the name is re-exported from where upstream's suites import it. The rest were expectations that predate the code they assert — the fork's own PRs (#134 refresh-on-refusal, #169 reply-only comment ack, #170 gh exec options, #198 keep-cached-check-details, #175 window binding) changed behaviour with `pr.yml` disabled and no unit lane backstopping them, so `main` was red before this merge. Each expectation now asserts the fork's contract; no test was deleted, skipped, or widened. Four reference repairs follow modules the merge moved (`filesystem-auth`, `filesystem-search-file-paths`), two fixtures gained fields upstream added, and two suites needed the WSL gate mocked so they stop spawning a real `wsl.exe` per case. `ws-inbound-message-queue.test.ts` is a GC-timing flake, not a fix: it passes 18/18 in isolation and the gate's isolation re-run reproduces it as green. Verified per file with `pnpm test <path>` — all 25 green — plus `pnpm tc:web` clean and the identity sweep still 0 fixable.
nplez1
added a commit
that referenced
this pull request
Oct 28, 2026
…s it moved LOCAL-PATCHES.md § Sync log gains the entry (three inline, the 2026-10-21 one rotated verbatim into local/sync/sync-log-archive.md), the convergence ledger gains the 2026-10-28 section with every question this sync settled, and BRANCHES.md records the new base. The lint baselines moved with the code: thirteen upstream-owned files over the max-lines budget are exempted by ownership, the focus-read and workspace-path baselines were pruned, `worktree-initial-terminal-close-records.test.ts` goes to three wall-clock reads for its restored TTL case, and `local/lint-known-red.json` follows upstream's step rename (`check:flaky-test-pattern-ratchet` folded into `lint:ast-grep-gates`), keeping only the two #198 foreign reds. Two fixes ride along, both found by the gate: - `check-flaky-test-pattern-ratchet.mjs` shells out to `git diff` with no `maxBuffer`, so a ~20k-path merge aborts with ENOBUFS and reads as a lint failure. It now uses the 256 MB ceiling `ast-grep-scan.mjs` already had. - Three jenkins suites carry a deliberate `// ast-grep-ignore: test-no-in-test-import-actual` with its reason: they redirect the real homedir. A duplicate import in `updater.publishing-window-feed.test.ts` is merged. Verified: gate-lint PASS (39 green, 1 failing = 2 known red, 0 new), oxfmt clean, fork-process-docs test green.
This was referenced Oct 29, 2026
nplez1
added a commit
that referenced
this pull request
Oct 29, 2026
#247) * fix(lint): drive the two #198 flaky-pattern reds with fake timers `lint:ast-grep-gates` carried a known-red entry for each of two files PR #198 merged: four `Date.now()` reads in `check-details-cache.test.ts` and a 250 ms real-time wait in `checks-list-expanded-details.test.tsx`. Both are the fork's own work, and `local/lint-known-red.json` had them down as `foreign-red`. Both are now fixed where the ratchet asked for them: - the cache suite pins the clock with `vi.useFakeTimers()` + `vi.setSystemTime()`, which is what the TTL cases needed — `checkDetailsNeedRefresh` compares a payload's `readAt` against the wall clock, so they were really asking how fast the machine ran them; - the checks-list suite drives `useDelayedFlag`'s 200 ms deferral with `vi.advanceTimersByTimeAsync(250)` instead of sleeping 250 real milliseconds to catch a label a fast machine never shows. The claim recorded alongside the entries — that each `match` naming the rule and the path means "a new finding in either file still fails the gate" — was false: the ratchet prints one line per rule and file, and the regex is anchored on the rule and the path alone, so a fifth `Date.now()` in the same file was masked. The section in LOCAL-PATCHES.md now says so. * fix(test): route the two Bun-missed SQLite suites to Node `findSqliteRuntimeTests` classifies a test by the imports it names, and it cannot follow an indirect one. Two suites reach `node:sqlite` through modules it does not know — `tests/e2e/helpers/persisted-profile-state.unit.test.ts` imports the profile-state modules directly, and `src/main/ai-vault-search/session-search-indexer.test.ts` reaches the driver through the indexer, the schema and its fixture — so both ran in the Bun project. Bun's `node:sqlite` on macOS cannot open a WAL database read-only once `close()` has removed `-wal`/`-shm`, so they failed 2/3 and 1/42 there and pass 3/3 and 42/42 here. Both are now in `NODE_RUNTIME_INCLUDE`, with the reason, and `vitest-node-runtime-boundary.test.ts` pins the pair: nothing else would notice their removal, because the only other evidence was the known-red manifest. That also retires the two `Unable to read profile state database` entries. Their `match` string is what a genuine profile-state open regression throws, so while they were allowlisted the gate could not have seen one. * fix(unit-gate): catch a dead allowlist entry, and prune nine `local/gate-unit.mjs` derives staleness only from an entry that *matched a failure*, so an entry naming a test that no longer exists can never be reported: it sits in the manifest for ever, and only a 25-minute full run could have said anything about it — and that run derives nothing from an entry nothing matched. One such entry was there. `src/main/sqlite/sync-database-portability.test.ts > SQLite runtime contract rejects explicit undefined bindings before modifying rows` names a case upstream stablyai#34661 (`e5f1eef466`) deleted, because Node 26.10 binds an explicit `undefined` as NULL. Its recorded cause — "the bundled SQLite's own binding behaviour" — was wrong, and `LOCAL-PATCHES.md` now says what actually happened. So the title is now checked against its file, statically, at PR time. `titleExistsInSource` in `local/unit-gate-judgement.mjs` rebuilds a Vitest full name — the `describe` chain and the case title joined by single spaces — from the file's own title literals, treating an `it.each` substitution as a wildcard, and `tests/tools/unit-gate.test.mjs` asserts the real manifest has no dead entry. Nine entries are pruned with it, every one re-measured today: - the four topology cases run in the Node project already, since upstream stablyai#33533 (`a886dcd1fe9`) seated their fixture in the import scan; 3/3, 1/1 and 9/9 pass; - `profile-state-streaming-validation`'s rowid case was rebuilt by stablyai#34661 to use `CREATE TABLE` over `ALTER TABLE … ADD COLUMN`; 16/16 pass; - the two re-routed suites from the previous commit, and the now-deleted case. What remains is 8 entries, all `host-toolchain`; the `sqlite-binding` class is empty. Each prune carries its reason and the revision that closed it in `prunedSince`, which keeps `observedRun`'s arithmetic honest. * fix(unit-gate): close the review round on the guard and the timers Adversarial review of the branch (Opus, high, over 6d7cb78..HEAD) found nothing blocking and two should-fix gaps in the new guard, plus five MINOR and four NIT. Each is addressed here. Guard soundness — it reported live tests as dead in shapes this repo writes: - `$name` off an `it.each` table row is a substitution like `%s`; the comment claimed it and the pattern did not have it. `%$` too. - case tables are named `itRunsUnixShell.each(...)`, not `it.each(...)`, so the caller pattern now accepts any identifier starting with describe/it/test/suite. `src/main/cli/packaged-cli-assets.test.ts` is exactly that shape and would have reported its live title as dead. - `src/main/runtime/orca-runtime.test.ts` imports every case from `*.spec` modules, so its titles are in files the entry does not name. `source` is now the file plus those modules, via `importedCaseSpecifiers`. Guard precision — it called dead entries live: - `it.skip` / `it.todo` never run, so a title behind one can never be a known red. Both now read as absent. A title surviving only in a comment still counts as present, and the doc comment says so. Coverage for both directions (`tests/tools/unit-gate.test.mjs`, 29 → 31 tests): nested describes, `$name`, an aliased table, a generic `.each`, an apostrophe in a double-quoted title, a template-literal title, skip/todo, and a real spec-imported title that must be found through the follow and not without it. The deferred-label case also asserted only half of what its comment claims: it now proves "Refreshing check details…" is absent at 199 ms and present at 250 ms, which is the deferral `useDelayedFlag` exists for and costs milliseconds with the clock faked. Three LOCAL-PATCHES claims were wrong and are corrected: the prune split is seven re-routed and two upstream-rebuilt (not six and three), the rowid diagnosis was right about SQLite 3.54's `ADD COLUMN` refusal and wrong only about the cause being fork-local, and the `local(lint)` entry count is now zero. * fix(unit-gate): keep a literal %% in an each title a wildcard The delta re-check (Opus, high, over R1..R2) came back clean with one MINOR regression from that round's own fix: the widened substitution class dropped `%%`, which Vitest renders as a single `%`, so a title written `shows 50%% done` would have looked dead at `shows 50% done`. `%` is back in the class, and the test pins `%%` and `%$` so neither can be dropped again. The doc comment also claimed the comment-only shape was the *one* thing the check cannot tell apart. It is not: a title behind `(cond ? it : it.skip)('t')` is unresolved callee, not a skip, so it reads as present too. Both are stated.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes #196. Expanding a check — a Jenkins build in particular — replaced the rows it already had with
"Loading check details…", and a poll that landed while a detail was open deleted its cache entry
before refetching, so the spinner came back on its own. Jenkins made it worse: details had no renderer
cache at all, and one expand is up to three main-process GETs.
loadingnow means refreshing. A payload stays on screen, a background read swaps in the new one,and the spinner belongs only to a build nothing has read yet.
Changed behaviour
beginGitHubChecksTabDetailscarries the payload it already has (or one the shared cache holds)instead of nulling
details. The spinner renders only when there is no payload; with one, a"Refreshing check details…" status line sits in the meta row.
resetGitHubChecksTabForSourcekeepsdetailsByCheckKey. A list refresh replaces every rowobject, and clearing there re-read every open detail on the next render — the "spinner comes back by
itself" half of the report. Keys are the build's identity, so they survive a poll; a context change
(a push moves the head SHA, and the context key with it) still drops them.
checkDetailsNeedRefreshdecides instead: re-read when the row moved since the read, or when a still-running build outlives
the checks TTL (the constant
getPRChecksCacheTtlalready used, now exported) — both behind the rowsalready on screen. A finished build is never re-read, so re-expanding one is a pure cache hit.
pane (
settleGitHubChecksTabDetailsKeepingShown).components/github/check-details-cache.ts— one bounded, module-level cache keyed by buildidentity, so the PR page, the item dialog and the right sidebar share a payload instead of each
keeping a map that dies with the mount. This is the "add the Jenkins equivalent beside it so the two
surfaces share one store" option the issue offered.
checkDetailsBuildIdentity: checkRunId → gitlabJobId → url → workflow-run:name → name. Ids leadbecause a check run's url is its
details_url, which an app can point several runs at; theworkflow-run branch carries the job name because a
gh pr checksfallback row has only the run's id,which every job in the run shares, while its payload is filtered to one job by name. One function
rather than three copies — [Bug]: Checks panel keeps a stale run after a push — workspace chip updates first; only a panel switch refreshes #102's root cause was reader/writer key drift.
useDelayedFlagdefers the refreshing label ~200ms, per the styleguide's in-flight rule andmatching the full-details tab, so a fast read never flashes a label beside rows that are correct.
CheckDetailsLoadStategainedreadAt/readFor(the row state at the read) in both of its shapes; thesidebar's old
errorAtfolded into them.Not done
local entry short-circuits the predicate, and a fresh mount has none. The rows render immediately
either way, so the read is invisible apart from the refresh label.
detail is re-read when its row moves or, for a still-running build, when the 60s TTL lapses.
Review nits, left deliberately
numeric id): the panel can briefly show the other's payload until its own read lands. Self-correcting
and capped, and closing it means parsing a host into every id key.
the payload is re-read on the next row change, the TTL, or Retry. The alternative polls a failing
endpoint once per row move.
answer is authoritative.
"Retrying…" button, which already spins while
loading.Verification
pnpm testoncomponents/right-sidebar,components/pull-request-page,components/github-item-dialog,components/github— 3,210 tests passed (1 skipped), 0 failed.check-details-cache.test.ts(build identity: two runs sharing one details url, two jobs of onerun with and without urls; cache sharing; predicate matrix — row move, TTL, finished build, failed
read pinned until the row moves), new
use-delayed-flag.test.tsx,github-checks-tab-state.test.tsreworked to the new contract plus coverage for a failed refresh keeping its rows.
stages again with no spinner and no second load (
jenkins-check-row-details.test.tsx); a poll thatmoves a row while its detail is open keeps the rows and refreshes behind them, and a failed refresh
keeps them too (
checks-list-expanded-details.test.tsx).pnpm tc:webclean;pnpm run check:code-quality:changedclean. Gate 1 is not required for thisdiff — every changed path is under
src/renderer, no global inputs.Rendered behaviour is covered by the component tests above. The PR page and item dialog have no
component harness in this repo, so their share is covered at the state-helper level (begin / settle)
rather than end to end.
Review
Reviewed by Opus 5.5, high: R1 (1 blocking fixed), round 2 (1 blocking fixed), round 3 clean on R4
Round 2 found the key-order collision between a check run's id and its shared
details_url; R4 appliesits prescribed ordering verbatim, pinned by the two identity tests above. Each round received only the
branch's own diff.