Skip to content

fix(desktop): reveal hidden windows on a bounded fallback and before recovery dialogs - #61743

Merged
Hona merged 1 commit into
devfrom
window-reveal-machine
Nov 4, 2026
Merged

Hona merged 1 commit into
devfrom
window-reveal-machine

Conversation

@Hona

@Hona Hona commented Nov 4, 2026

Copy link
Copy Markdown
Member

A new desktop window could stay hidden forever, and a recovery dialog could open modal to that hidden window. The reveal gate is now one explicit state machine with a bounded fallback.

Bugs

  • Hidden forever. A second window (Cmd+N, or the 2nd+ restored window) starts with show: false and shows only after both ready-to-show and WindowThemeReady. If the renderer throws before it applies the theme, or its IPC port never binds, WindowThemeReady never arrives. Nothing timed out, so the window stayed registered, persisted and invisible.
  • Recovery dialog on a hidden window. did-fail-load, did-fail-provisional-load, render-process-gone or unresponsive before the reveal opened dialog.showMessageBox(win, …) parented to an unmapped window. The user could not relaunch, export logs or quit.

Machine

packages/desktop/src/main/windows/reveal-machine.ts

type RevealState =
  | { status: "loading" }
  | { status: "content-ready" }
  | { status: "theme-ready" }
  | { status: "shown" }
  | { status: "closed" }

type RevealEvent = "content.ready" | "theme.ready" | "reveal.timed-out" | "recovery.requested" | "window.closed"
type RevealCommand = { type: "show"; reason: "ready" | "timeout" | "recovery" } | { type: "cancel-timeout" }
stateDiagram-v2
  [*] --> loading: new window (arms 3 s fallback)
  [*] --> shown: early window
  loading --> content_ready: content.ready
  loading --> theme_ready: theme.ready
  content_ready --> shown: theme.ready / show(ready)
  theme_ready --> shown: content.ready / show(ready)
  loading --> shown: reveal.timed-out / show(timeout)
  content_ready --> shown: reveal.timed-out / show(timeout)
  theme_ready --> shown: reveal.timed-out / show(timeout)
  loading --> shown: recovery.requested / show(recovery)
  content_ready --> shown: recovery.requested / show(recovery)
  theme_ready --> shown: recovery.requested / show(recovery)
  loading --> closed: window.closed / cancel-timeout
  content_ready --> closed: window.closed / cancel-timeout
  theme_ready --> closed: window.closed / cancel-timeout
  shown --> closed: window.closed
Loading
  • Every leave from a hidden state emits cancel-timeout; a second Linux content.ready (ready-to-show + did-finish-load), a late theme.ready and any event after closed return the same frozen state.
  • createRevealDriver owns the fallback setTimeout and calls show(reason). windows/index.ts keeps one driver per window in a WeakMap; setWindowThemeReady dispatches theme.ready into it.
  • wireWindowRecovery(win, id, { relaunch, reveal }) calls reveal() before the first dialog.showMessageBox.
  • The "main window visible" log line stays (startup bench milestone); a non-ready reveal adds reason: "timeout" | "recovery".

Deleted

  • contentReady, appliedTheme and revealed closure booleans and the themeReady callback WeakMap in windows/index.ts.

Product calls

  • Fallback is 3 s from window creation. On a slow renderer the window shows with its themed background colour and then paints; that beats an invisible window.
  • Recovery shows the window immediately, even before content loads, because the dialog is modal to it.

Deviations from the design

  • No isWindowRevealed / isWindowHidden selectors: nothing outside the driver reads the reveal state, so they would be dead code.
  • The sidecar background-service machine from the same design ships separately (branch sidecar-service-machine) to keep this PR reviewable.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" stroke="currentColor">
  <style>
    .win{animation:reveal 4s infinite}
    .bar{animation:fill 4s infinite;transform-origin:20px 62px}
    @keyframes reveal{0%,60%{opacity:.15}70%,100%{opacity:1}}
    @keyframes fill{0%{transform:scaleX(0)}60%,100%{transform:scaleX(1)}}
    @media (prefers-reduced-motion:reduce){.win,.bar{animation:none}}
  </style>
  <g class="win">
    <rect x="70" y="6" width="100" height="46" rx="0" stroke-opacity=".8"/>
    <line x1="70" y1="16" x2="170" y2="16" stroke-opacity=".5"/>
    <rect x="80" y="24" width="44" height="4" fill="currentColor" fill-opacity=".4" stroke="none"/>
    <rect x="80" y="34" width="70" height="4" fill="currentColor" fill-opacity=".25" stroke="none"/>
  </g>
  <line x1="20" y1="62" x2="220" y2="62" stroke-opacity=".2"/>
  <line class="bar" x1="20" y1="62" x2="220" y2="62" stroke-opacity=".7" stroke-width="2"/>
  <text x="222" y="66" font-size="8" fill="currentColor" fill-opacity=".6" stroke="none" text-anchor="end" font-family="sans-serif">3s</text>
</svg>

@Hona
Hona requested a review from Brendonovich as a code owner November 4, 2026 18:45
Copilot AI balanced review requested due to automatic review settings November 4, 2026 18:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks sound. I read the diff against the base and ran the window tests and tsc -b in packages/desktop; both pass. I couldn't run the desktop app here, so the reveal fallback and the recovery-dialog path aren't verified end to end.

Public API: unchanged. Only internal desktop main-process code changes: wireWindowRecovery now takes { relaunch, reveal }, and there's a new internal reveal-machine module.

The machine follows the desktop State Machines rule (one union, a pure transition, a driver at the edge). Every way out of a hidden state cancels the fallback timer, and the early window and the closed state behave as before. No blocking concerns.

@Hona
Hona merged commit d356b9b into dev Nov 4, 2026
14 checks passed
@Hona
Hona deleted the window-reveal-machine branch November 4, 2026 19:56
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.

2 participants