Skip to content

fix(ok): deliver menu actions sent before the first render (PRD-8245) (#3784) - #1367

Merged
inkeep-oss-sync[bot] merged 1 commit into
mainfrom
copybara/sync
Aug 23, 2026
Merged

fix(ok): deliver menu actions sent before the first render (PRD-8245) (#3784)#1367
inkeep-oss-sync[bot] merged 1 commit into
mainfrom
copybara/sync

Conversation

@inkeep-oss-sync

Copy link
Copy Markdown
Contributor

Copybara-translated 1 Inkeep OSS change. Rebase-merge this PR so the prepared commit lands directly on public main.

Linear: this mirror replays a change that already merged upstream, so it must not drive ticket status.
skip PRD-8245

… (#3784)

* fix(ok): deliver menu actions sent before the first render (PRD-8245)

The menu bar lives in the main process and is live from the moment a window
opens. The renderer's listener is not: local-menu-action-bus installs its
single onMenuAction forwarder lazily, on first subscribe, which happens inside
a React effect and therefore only after the first commit. Anything main pushed
on ok:menu-action in between landed on a channel with no listener, and an
Electron IPC event with no listener is not queued or retried. It is gone.

On a cold or loaded machine that gap is seconds wide, which is exactly when
someone reaches for CmdOrCtrl+J. The terminal simply does not open.

The preload runs before any renderer script, so it subscribes there instead and
holds what arrives until someone attaches, then replays in order. Every
OkMenuAction is a payload-free user intent, so honouring one a beat late is
what was asked for. The queue is bounded at 32 because a window type that never
subscribes can still be main's chosen target.

Why this surfaced now: the desktop smoke suite hid it. revealTerminalSurface
re-clicks the View menu item until the panel appears, which turns a dropped
action into a merely slow one. The Linux terminal gates opened on 2026-08-19
and the terminal specs began running on ubuntu-latest for the first time, where
the crossbuild failure rate went from 14.5% to 32.9% over the next two days.
The single test that cannot retry, because it is timing the toggle, ate the
drop as a bare 5s waitForSelector timeout against a healthy app.

Also in this change, from reading the uploaded Playwright traces of those runs:

* The toggle-latency test measured the harness, not the app. Its stopwatch
  started while the renderer was still booting, and on non-darwin it enclosed
  an editor-window rediscovery poll plus an extra renderer round trip that
  macOS never pays. Of a 3968ms "toggle" against a 2000ms budget, 413ms was the
  rediscovery and 1993ms was a dispatch waiting on a busy main thread. It now
  waits for the renderer to answer promptly before starting the clock, and
  hands the page it already holds to the dispatch.

* New Terminal Window was invoked before main had registered the window's
  project context. windowsByPath.set lands only after loadFile settles, while
  the window poll returns as soon as preload answers, so the command resolved
  no project and opened a HOME-cwd window with an empty collabUrl.

* The scrollback retention assertions read xterm's .xterm-accessibility node,
  which is a screen-reader announcement buffer rather than scrollback. Under a
  burst it collapses to "Too much output to announce" and truncates mid-token,
  so the assertion lost a marker while the terminal was perfectly healthy. They
  read .xterm-rows now, and scroll in half-viewport steps so two adjacent lines
  cannot straddle a step.

One hazard this had to be careful about. Once dropped clicks become real, a
retry fired while a queued dispatch is still in flight lands a SECOND toggle
and hides the surface it was asked to reveal. The reveal helpers were written
on the old assumption that a click could simply vanish, so they now observe
before acting and wait long enough to outlast a queued replay.

A blanket "wait for a responsive renderer" prelude in every openTerminal helper
was tried first. It fixed that, and broke the rail-admission test: the right
terminal column settled at its 324px drag floor instead of its 740px preferred
width, 2 of 4 runs against 4 of 4 on base. So the readiness barrier is used
only where an assertion genuinely needs a settled app, which is the toggle
latency measurement and New Terminal Window.

* fix(ok): bound what the menu-action queue holds (PRD-8245)

Review findings on the menu-action buffer, all of them about what the queue
should refuse to hold.

Destructive actions are no longer buffered at all. close-active-tab-or-window,
kill-terminal, delete and move-to-trash resolve their target when they RUN, so
replaying one after the window finally mounts aims it at whatever is selected
by then rather than at what the user was looking at when they pressed it.
Dropping an early kill-terminal costs a keystroke; honouring it late can cost a
session. They still deliver live.

A repeated show-or-hide action now collapses to the one intent it expresses.
While buffered the user gets no feedback at all, so pressing CmdOrCtrl+J three
times is one request restated, not open-close-open. Replaying all three left
the terminal shut, which is the exact complaint this whole change exists to
fix. Collapsed only against the immediately preceding entry, so an interleaved
A,B,A stays three intents and additive actions keep their multiplicity.

The replay batch is also put back rather than dropped when the listener set
empties during the microtask hop, since "queued or delivered, never lost" is
the buffer's whole contract. And the JSDoc now names registerPendingDelivery
and says why that one-shot dom-ready gate does not cover recurring actions.

Two test findings from the same review:

* readShellPid parsed a pid out of xterm's accessibility buffer. It was the one
  reader here that PARSES rather than substring-matching a whole marker, which
  makes truncation dangerous instead of merely slow: a MARKER=67818 cut short
  yields 67, clears the greater-than-zero gate, and is then compared against
  the real pid as proof a session survived a move. Reads the rows now.

* Two tests asserted a layout the app only owes at the width they asked for,
  while their own innerWidth poll happily caught the beat before the window
  manager clamped the window back to fit the display. Probing it directly on a
  settled window: at 1900px the right terminal column resolves to its 740px
  preferred width, and the 324px drag floor only appears when the window really
  did shrink to 1510. The app was right; the precondition had lapsed. Both now
  sample until the width settles, and skip with the display's work area in the
  reason when it cannot hold the window, the same way the suite skips a
  platform without a PTY. That poll shape is also the likeliest reading of the
  restore assertion that has been failing by hundreds of pixels rather than the
  twenty it allows: a window that shrank mid-test cannot give back the width it
  was asked to restore.

* fix(ok): keep the harness out of the move budget (PRD-8245)

The move-placement budget was measuring the test harness, the same way the
toggle-latency budget was. Its stopwatch starts inside the renderer when the
settlement observer arms, and the dispatch that follows re-discovered the
editor window before sending, so an editor-window rediscovery poll sat inside
the measured window on every non-darwin run. On darwin the menu click happens
inside main and never pays it, so one assertion meant two different things per
platform. It ran 540.9ms against a 300ms budget on ubuntu CI, of which the
poll is the measured ~400ms. Every caller that times a dispatch now hands over
the page it already holds; the rapid-settlement path needed it most, since it
was paying that poll seven times inside one settlement.

Second review round on the buffer.

The two classification sets were hand-maintained ReadonlySet<OkMenuAction>,
which type-checks against any subset including the empty one. A new action
added to the union would have compiled clean and landed in the default
buffer-and-replay bucket, which is exactly what must never happen to a future
destructive action. They are now one total Record keyed by the union, so a new
member fails the typecheck until somebody classifies it. Verified by deleting
kill-terminal from the record: tsc names the missing key.

The app package already guards this union with a drift check on
OK_MENU_ACTIONS. A record goes one step further than that pattern, and
deliberately: drift detection tells you an id is unclassified, a total record
refuses to build without a decision. Worth the extra verbosity for a
classification whose wrong default destroys a session.

Also from the review:

* Both dispatch loops now isolate a throwing listener, matching the contract
  the renderer-side bus already keeps for its own subscribers. In the replay
  path this is load bearing rather than tidy: the batch is spliced out of the
  queue before delivery, so an escaping throw silently discarded every intent
  after it, which is the loss the buffer exists to prevent.

* A destructive action dropped with nothing listening now leaves a debug line.
  It is the one drop a user might report, since they pressed something and saw
  nothing happen. The parity collapse stays quiet on purpose: it is the
  designed common path and would log on every repeated keypress.

* widenEditorWindow skipped whenever the window settled short, and attributed
  it to the display without checking. A genuine window-sizing regression on a
  wide enough display would have been converted into a skip reading "your
  display is too small". It now compares the work area first and only skips
  when the display really cannot hold the window; anything else fails.

* The replay tests passed whether or not the drain was deferred. A pre-settle
  assertion pins the microtask hop, which exists so the drain cannot re-enter
  the React effect that is still subscribing.

* Two comments overstated what they described: the scroll step is shorter than
  a wheel-page rather than half a viewport, and the reveal helper's real
  defence is its observe-before-acting check, not the length of the window
  after it.

GitOrigin-RevId: 37c4b5ea2b22ec52aaa184f807da61c286e6cd88
@inkeep-oss-sync
inkeep-oss-sync Bot merged commit a6f34df into main Aug 23, 2026
@inkeep-oss-sync
inkeep-oss-sync Bot deleted the copybara/sync branch August 23, 2026 08:51
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