Skip to content

fix(ui): give BrowserPanel a narrow bottom-sheet layout (#546) - #818

Merged
Astro-Han merged 1 commit into
mainfrom
fix/546-artifact-browser-closeout
Jul 12, 2026
Merged

Astro-Han merged 1 commit into
mainfrom
fix/546-artifact-browser-closeout

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Jul 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Gives BrowserPanel a <=990px bottom-sheet layout, mirroring ArtifactPane, so a live embedded browser stays usable on narrow windows instead of collapsing to an invisible 520px rail. Adds a narrow PostCSS-based CSS contract test that locks the one declaration preventing the disappearing-browser defect (min-height: 220px).

Why

BrowserPanel had no @media (max-width: 990px) rule. Its grid auto row sized to the panel's max-content (toolbar height); the strip's flex: 1 1 auto contributes 0 height without a definite container height, so the rAF loop mirrored a ~zero-height rect to main and the native WebContentsView was sized to nothing — the embedded browser disappeared beside a ~470px empty gap. ArtifactPane already had an explicit narrow bottom-sheet; BrowserPanel did not.

Part of the #546 Artifact pane + browser panel closure audit. Refs #546.

Scope

  • .maka-browser-panel gains a @media (max-width: 990px) bottom-sheet rule mirroring ArtifactPane (full width, min-height: 220px, max-height: min(42dvh, 360px), border-left: 0, top border + lift shadow). min-height: 220px is the load-bearing declaration: it forces the auto row (and thus the strip) to a usable non-zero size so the native view is visible; width: 100% turns the 520px right rail into a bottom sheet.
  • No grid-row change. BrowserPanel is always the second grid child (chat / browser / artifact), so it already occupies the explicit auto row; ArtifactPane as the third child uses an implicit auto row (grid-auto-rows defaults to auto), so the existing minmax(0, 1fr) auto already accommodates all three.
  • New browser-panel-layout.test.ts uses PostCSS (the repo's established contract-test parser) to lock ONLY the declaration that prevents the defect: exactly one min-height: 220px on .maka-browser-panel in the 990px media block, with no !important. min-height is genuinely exclusive to the narrow rule (the base rule does not declare it), so a source contract locks its effective value within the block. The test does NOT simulate the cascade and does NOT claim to lock width/border-left (also declared by the base rule, won only by source order — not verifiable by a text contract), max-height (a space cap), or border-top/box-shadow (visual); those belong to visual smoke review. A synthetic test locks the helper's collection/guard logic (a second overriding rule, a duplicate property, !important, and removal each surface so the integration guard fails).

Non-goals: redesigning the browser chrome; adding a deterministic browser visual-smoke fixture (filed as #819); changing the rAF viewport-mirroring mechanism; a shared-height policy for the both-present narrow case (tracked as #824); locking the full computed layout (would need a real browser computed-style check, out of scope for a CSS fix of this size).

Verification

  • TDD: the integration contract fails if min-height: 220px is removed or overridden (verified — removal -> 0 declarations -> RED "expected exactly one min-height"; a second rule / duplicate property -> 2 declarations -> RED; !important -> RED). A synthetic test locks the helper's collection so the guard logic is permanently covered (not just the real-CSS pass path).
  • No regression: artifact-pane-layout.test.ts 2/2 green; full main suite npm run test:dist -> 2348/2348 pass incl. console/a11y/copy checks; npm run build:renderer (vite) builds cleanly.
  • Visual evidence: not captured here. The screenshot pipeline is the repo's human-review floor (smoke.md: contract tests are the automated floor; the visual smoke path owns screenshot verification), and BrowserPanel has no deterministic visual-smoke fixture (tracked in feat(ui): add deterministic visual-smoke fixture + DOM contract for BrowserPanel renderer chrome #819). A reviewer should confirm the narrow bottom sheet via npm --workspace @maka/desktop run dev with a live browser session at ~900px, or the real-window smoke. Substitute evidence: the CSS contract test (locks the load-bearing min-height) + the unchanged rAF mirroring path (the strip's getBoundingClientRect already drives the native view; making the strip bounded via min-height is what produces the correct native rect); the bottom-sheet identity (width/borders/bound) is left to that visual review by design.

Impact

  • User-visible: at narrow window widths (<=990px), a live embedded browser renders as a full-width bottom sheet (usable) instead of an invisible 520px sliver with an empty gap. At >=991px behavior is unchanged.
  • Compatibility/migration: none — CSS-only, no IPC/contract/data change.
  • Release note: "Fix: embedded browser no longer disappears at narrow window widths."

Reviewer notes

  • Both-present + short window (#824): with both ArtifactPane and a live BrowserPanel open at <=990px and a window <=~440px tall, the chat row can collapse to 0 (two min-height: 220px bottom sheets). The browser close button stays at the top of the second row and is reachable down to the ~320px restore minimum, so closing the browser always recovers the chat column; at the shortest heights the ArtifactPane collapse button may be clipped, so recovery may require closing the browser and/or collapsing ArtifactPane. This is an edge-of-edge case (already unusable at similar heights before this change); a shared-height policy is a product decision tracked in fix(ui): reserve chat/composer height when both ArtifactPane and BrowserPanel are open at narrow widths #824, not bundled here. The CSS comment records this honestly.
  • The production CSS has been stable and correct across all review passes; the passes only reshaped the test's verification seam. The contract converged to a narrow source floor locking the single defect-prevention declaration (min-height: 220px, genuinely exclusive to the narrow rule), after earlier drafts over-reached by hand-modeling the CSS cascade (a parallel incomplete reimplementation of the browser's cascade resolver, which left a false-pass hole each round) and by locking cascade-dependent / cosmetic declarations a text contract cannot verify.

Ready for review

  • Verification is complete and the results above are current
  • Impact, compatibility, migration, documentation, and release-note needs are addressed
  • User-visible UI/UX changes include visual evidence, or Verification explains why it is not applicable

BrowserPanel had no @media(max-width:990px) rule. Its grid auto row sized to
the panel's max-content (toolbar height); the strip's flex:1 1 auto
contributes 0 height without a definite container height, so the rAF loop
mirrored a ~zero-height rect to main and the native WebContentsView was
sized to nothing — the embedded browser disappeared beside a ~470px empty
gap. ArtifactPane already had an explicit narrow bottom-sheet; BrowserPanel
did not.

Add a <=990px bottom-sheet rule for .maka-browser-panel mirroring ArtifactPane
(full width, min-height:220px, max-height:min(42dvh,360px), border-left:0,
top border + lift shadow). min-height:220px is the load-bearing declaration:
it forces the auto row (and thus the strip) to a usable non-zero size so the
native view is visible. width:100% turns the 520px right rail into a bottom
sheet. BrowserPanel is always the second grid child (chat / browser /
artifact), so it already occupies the explicit auto row; no grid-row change
is needed (ArtifactPane as the third child uses an implicit auto row, which
defaults to auto).

Locked by a narrow PostCSS-based CSS contract test (the repo's established
contract-test parser) that locks ONLY the declaration preventing the
disappearing-browser defect: exactly one min-height:220px on
.maka-browser-panel in the 990px media block, with no !important. min-height
is genuinely exclusive to the narrow rule (the base rule does not declare
it), so a source contract locks its effective value within the block. The
test does NOT simulate the CSS cascade and does NOT claim to lock
width/border-left (also declared by the base rule, won only by source order —
not verifiable by a text contract), max-height (a space cap), or
border-top/box-shadow (visual) — those belong to visual smoke review. A
synthetic test locks the helper's collection/guard logic (catches a second
overriding rule, a duplicate property, !important, and removal); the
integration test fails if min-height is removed or overridden (verified). The
existing ArtifactPane narrow contract and the 2348-test main suite stay green;
the renderer build is clean. The both-present + very-short-window case (chat
can collapse to 0) is a product-policy question tracked in #824; the CSS
comment records it honestly.

Part of the #546 Artifact pane + browser panel closure audit.
@Astro-Han
Astro-Han force-pushed the fix/546-artifact-browser-closeout branch from bc1e2a7 to e42f000 Compare July 12, 2026 13:23
@Astro-Han
Astro-Han merged commit 80952ec into main Jul 12, 2026
3 checks passed
@Astro-Han
Astro-Han deleted the fix/546-artifact-browser-closeout branch July 12, 2026 13:27
Astro-Han added a commit that referenced this pull request Jul 12, 2026
…826)

Follow-up to #818. The narrow-layout CSS comment said "the user recovers by
collapsing ArtifactPane or closing the browser (tracked in the PR notes)",
which overstates reachability at the shortest window heights (closing the
browser alone usually recovers the chat column, but at extreme short heights
the ArtifactPane collapse control may be clipped, so recovery may require
both) and points at the ephemeral PR notes instead of the durable tracker.

Replace with the wording from the #818 review consult: the chat row (not
"column") can collapse from the two 220px minima, the shared-height policy is
tracked in #824, and recovery may require closing the browser and/or
collapsing ArtifactPane. Doc-only; the browser-panel-layout contract test
strips comments and stays green (2/2).

Refs #546.
Astro-Han added a commit that referenced this pull request Jul 12, 2026
At <=990px both ArtifactPane and BrowserPanel become bottom sheets, each
with a fixed min-height: 220px. When both are open and the detail area is
short (<=~440px), the two fixed minima squeeze the chat row
`minmax(0, 1fr)` to 0 and the composer is clipped — a reachable state
since BrowserWindow sets no minHeight and the 320px restore minimum is
restore-only (runtime drag can go shorter).

Add a :has()-scoped rule inside the 990px block that, when BrowserPanel
AND a non-collapsed ArtifactPane are both present, scales each panel's
min-height to 22dvh so the chat row keeps the remainder. At >=1000px
tall 22dvh == 220px (unchanged); below that the panels yield
proportionally. Single-panel (or a collapsed ArtifactPane) keeps the
fixed 220px / 34px floor, so the rule scopes to both-present via :has()
(already used in 8 other places). Recovery (close browser / collapse
artifact) stays reachable because each panel's toolbar sits at the top of
a non-zero-height row.

Extend browser-panel-layout.test.ts with a both-present shared-height
contract locking the :has() rule's dvh min-height + both-panel coverage +
collapsed-artifact guard. Update the #818 narrow-layout comment to point
at the implemented rule instead of "tracked in #824".

Closes #824. Refs #546.
Astro-Han added a commit that referenced this pull request Jul 12, 2026
…w widths

At <=990px both ArtifactPane and BrowserPanel become bottom sheets, each
with a fixed min-height: 220px (from #818). When both are open and the
detail area is short (<=~440px), the two fixed minima squeeze the chat row
`minmax(0, 1fr)` to 0 and the composer is clipped — a reachable state
since the 320px restore minimum in window-state.ts was restore-only and
BrowserWindow set no minHeight, so runtime drag could go shorter.

Fix with two coordinated changes:

1. chat-detail.css: add a :has()-scoped rule inside the 990px block that,
   when BrowserPanel AND a non-collapsed ArtifactPane are both present,
   scales each panel's min-height to min(22dvh, 220px) so the chat row
   keeps the remainder. min(22dvh, 220px) shrinks below 220px at short
   heights and never grows above 220px at tall heights, so >=1000px tall
   (where 22dvh == 220px) is unchanged; below that the panels yield
   proportionally. Single-panel (or a collapsed ArtifactPane) keeps the
   fixed 220px / 34px floor, so the rule scopes to both-present via :has()
   (already used in 8 other places) and skips the collapsed artifact.
   Recovery (close browser / collapse artifact) stays reachable because
   each panel's toolbar sits at the top of a non-zero-height row.

2. main-window.ts + window-state.ts: extract SAFE_MIN_WIDTH / SAFE_MIN_HEIGHT
   from window-state.ts (consolidating the 480x320 literals already used by
   sanitizeBounds + visualSmokeWindowBounds) and set BrowserWindow
   `minHeight: SAFE_MIN_HEIGHT` so the runtime resize floor matches the
   sanitizeBounds restore floor. This makes the 320px floor the dvh fix
   relies on actually enforced at runtime, not just at restore.

Tests:
- browser-panel-layout.test.ts: both-present shared-height contract locking
  the :has() rule's exact `min(22dvh, 220px)` value, both-panel coverage,
  and per-selector that each selector carries the full both-present +
  non-collapsed condition (rejects an unconditional panel selector that
  would leak dvh into single-panel, a collapsed-condition leak, a wrong
  value, or a missing guard).
- app-region-hygiene-contract.test.ts: BrowserWindow `minHeight:
  SAFE_MIN_HEIGHT` contract tying the runtime floor to the shared constant
  exported from window-state.ts.

Closes #824. Refs #546.
Astro-Han added a commit that referenced this pull request Jul 12, 2026
…w widths

At <=990px both ArtifactPane and BrowserPanel become bottom sheets, each
with a fixed min-height: 220px (from #818). When both are open and the
detail area is short (<=~440px), the two fixed minima squeeze the chat row
`minmax(0, 1fr)` to 0 and the composer is clipped — a reachable state
since the 320px restore minimum in window-state.ts was restore-only and
BrowserWindow set no minHeight, so runtime drag could go shorter.

Fix with two coordinated changes:

1. chat-detail.css: add a :has()-scoped rule inside the 990px block that,
   when BrowserPanel AND a non-collapsed ArtifactPane are both present,
   scales each panel's min-height to min(22dvh, 220px) so the chat row
   keeps the remainder. min(22dvh, 220px) shrinks below 220px at short
   heights and never grows above 220px at tall heights, so >=1000px tall
   (where 22dvh == 220px) is unchanged; below that the panels yield
   proportionally. Single-panel (or a collapsed ArtifactPane) keeps the
   fixed 220px / 34px floor, so the rule scopes to both-present via :has()
   (already used in 8 other places) and skips the collapsed artifact.
   Recovery (close browser / collapse artifact) stays reachable because
   each panel's toolbar sits at the top of a non-zero-height row.

2. main-window.ts + window-state.ts: extract SAFE_MIN_WIDTH / SAFE_MIN_HEIGHT
   from window-state.ts (consolidating the 480x320 literals already used by
   sanitizeBounds + visualSmokeWindowBounds) and set BrowserWindow
   `minHeight: SAFE_MIN_HEIGHT` so the runtime resize floor matches the
   sanitizeBounds restore floor. This makes the 320px floor the dvh fix
   relies on actually enforced at runtime, not just at restore.

Tests:
- browser-panel-layout.test.ts: both-present shared-height contract locking
  the :has() rule's exact `min(22dvh, 220px)` value, both-panel coverage,
  and per-selector that each selector carries the full both-present +
  non-collapsed condition (rejects an unconditional panel selector that
  would leak dvh into single-panel, a collapsed-condition leak, a wrong
  value, or a missing guard).
- app-region-hygiene-contract.test.ts: BrowserWindow `minHeight:
  SAFE_MIN_HEIGHT` contract tying the runtime floor to the shared constant
  exported from window-state.ts.

Closes #824. Refs #546.
Astro-Han added a commit that referenced this pull request Jul 12, 2026
…w widths

At <=990px both ArtifactPane and BrowserPanel become bottom sheets, each
with a fixed min-height: 220px (from #818). When both are open and the
detail area is short (<=~440px), the two fixed minima squeeze the chat row
`minmax(0, 1fr)` to 0 and the composer is clipped — a reachable state
since the 320px restore minimum in window-state.ts was restore-only and
BrowserWindow set no minHeight, so runtime drag could go shorter.

Fix with two coordinated changes:

1. chat-detail.css: add a :has()-scoped rule inside the 990px block that,
   when BrowserPanel AND a non-collapsed ArtifactPane are both present,
   scales each panel's min-height to min(22dvh, 220px) so the chat row
   keeps the remainder. min(22dvh, 220px) shrinks below 220px at short
   heights and never grows above 220px at tall heights, so >=1000px tall
   (where 22dvh == 220px) is unchanged; below that the panels yield
   proportionally. Single-panel (or a collapsed ArtifactPane) keeps the
   fixed 220px / 34px floor, so the rule scopes to both-present via :has()
   (already used in 8 other places) and skips the collapsed artifact.
   Recovery (close browser / collapse artifact) stays reachable because
   each panel's toolbar sits at the top of a non-zero-height row.

2. main-window.ts + window-state.ts: extract SAFE_MIN_WIDTH / SAFE_MIN_HEIGHT
   from window-state.ts (consolidating the 480x320 literals already used by
   sanitizeBounds + visualSmokeWindowBounds) and set BrowserWindow
   `minHeight: SAFE_MIN_HEIGHT` so the runtime resize floor matches the
   sanitizeBounds restore floor. This makes the 320px floor the dvh fix
   relies on actually enforced at runtime, not just at restore.

Tests:
- browser-panel-layout.test.ts: both-present shared-height contract locking
  the :has() rule's exact `min(22dvh, 220px)` value, both-panel coverage,
  and per-selector that each selector carries the full both-present +
  non-collapsed condition (rejects an unconditional panel selector that
  would leak dvh into single-panel, a collapsed-condition leak, a wrong
  value, or a missing guard).
- app-region-hygiene-contract.test.ts: BrowserWindow `minHeight:
  SAFE_MIN_HEIGHT` contract tying the runtime floor to the shared constant
  exported from window-state.ts.

Closes #824. Refs #546.
Astro-Han added a commit that referenced this pull request Jul 12, 2026
…w widths

At <=990px both ArtifactPane and BrowserPanel become bottom sheets, each
with a fixed min-height: 220px (from #818). When both are open and the
detail area is short (<=~440px), the two fixed minima squeeze the chat row
`minmax(0, 1fr)` to 0 and the composer is clipped — a reachable state
since the 320px restore minimum in window-state.ts was restore-only and
BrowserWindow set no minHeight, so runtime drag could go shorter.

Fix with two coordinated changes:

1. chat-detail.css: add a :has()-scoped rule inside the 990px block that,
   when BrowserPanel AND a non-collapsed ArtifactPane are both present,
   scales each panel's min-height to min(22dvh, 220px) so the chat row
   keeps the remainder. min(22dvh, 220px) shrinks below 220px at short
   heights and never grows above 220px at tall heights, so >=1000px tall
   (where 22dvh == 220px) is unchanged; below that the panels yield
   proportionally. Single-panel (or a collapsed ArtifactPane) keeps the
   fixed 220px / 34px floor, so the rule scopes to both-present via :has()
   (already used in 8 other places) and skips the collapsed artifact.
   Recovery (close browser / collapse artifact) stays reachable because
   each panel's toolbar sits at the top of a non-zero-height row.

2. main-window.ts + window-state.ts: extract SAFE_MIN_WIDTH / SAFE_MIN_HEIGHT
   from window-state.ts (consolidating the 480x320 literals already used by
   sanitizeBounds + visualSmokeWindowBounds) and set BrowserWindow
   `minHeight: SAFE_MIN_HEIGHT` so the runtime resize floor matches the
   sanitizeBounds restore floor. This makes the 320px floor the dvh fix
   relies on actually enforced at runtime, not just at restore.

Tests:
- browser-panel-layout.test.ts: both-present shared-height contract locking
  the :has() rule's exact `min(22dvh, 220px)` value, both-panel coverage,
  and per-selector that each selector carries the full both-present +
  non-collapsed condition (rejects an unconditional panel selector that
  would leak dvh into single-panel, a collapsed-condition leak, a wrong
  value, or a missing guard).
- app-region-hygiene-contract.test.ts: BrowserWindow `minHeight:
  SAFE_MIN_HEIGHT` contract tying the runtime floor to the shared constant
  exported from window-state.ts.

Closes #824. Refs #546.
Astro-Han added a commit that referenced this pull request Jul 12, 2026
…w widths

At <=990px both ArtifactPane and BrowserPanel become bottom sheets, each
with a fixed min-height: 220px (from #818). When both are open and the
detail area is short (<=~440px), the two fixed minima squeeze the chat row
`minmax(0, 1fr)` to 0 and the composer is clipped — a reachable state
since the 320px restore minimum in window-state.ts was restore-only and
BrowserWindow set no minHeight, so runtime drag could go shorter.

Fix with two coordinated changes:

1. chat-detail.css: add a :has()-scoped rule inside the 990px block that,
   when BrowserPanel AND a non-collapsed ArtifactPane are both present,
   scales each panel's min-height to min(22dvh, 220px) so the chat row
   keeps the remainder. min(22dvh, 220px) shrinks below 220px at short
   heights and never grows above 220px at tall heights, so >=1000px tall
   (where 22dvh == 220px) is unchanged; below that the panels yield
   proportionally. Single-panel (or a collapsed ArtifactPane) keeps the
   fixed 220px / 34px floor, so the rule scopes to both-present via :has()
   (already used in 8 other places) and skips the collapsed artifact.
   Recovery (close browser / collapse artifact) stays reachable because
   each panel's toolbar sits at the top of a non-zero-height row.

2. main-window.ts + window-state.ts: extract SAFE_MIN_WIDTH / SAFE_MIN_HEIGHT
   from window-state.ts (consolidating the 480x320 literals already used by
   sanitizeBounds + visualSmokeWindowBounds) and set BrowserWindow
   `minHeight: SAFE_MIN_HEIGHT` so the runtime resize floor matches the
   sanitizeBounds restore floor. This makes the 320px floor the dvh fix
   relies on actually enforced at runtime, not just at restore.

Tests:
- browser-panel-layout.test.ts: both-present shared-height contract locking
  the :has() rule's exact `min(22dvh, 220px)` value, both-panel coverage,
  and per-selector that each selector carries the full both-present +
  non-collapsed condition (rejects an unconditional panel selector that
  would leak dvh into single-panel, a collapsed-condition leak, a wrong
  value, or a missing guard).
- app-region-hygiene-contract.test.ts: BrowserWindow `minHeight:
  SAFE_MIN_HEIGHT` contract tying the runtime floor to the shared constant
  exported from window-state.ts.

Closes #824. Refs #546.
Astro-Han added a commit that referenced this pull request Jul 12, 2026
…w widths

At <=990px both ArtifactPane and BrowserPanel become bottom sheets, each
with a fixed min-height: 220px (from #818). When both are open and the
detail area is short (<=~440px), the two fixed minima squeeze the chat row
`minmax(0, 1fr)` to 0 and the composer is clipped — a reachable state
since the 320px restore minimum in window-state.ts was restore-only and
BrowserWindow set no minHeight, so runtime drag could go shorter.

Fix with two coordinated changes:

1. chat-detail.css: add a :has()-scoped rule inside the 990px block that,
   when BrowserPanel AND a non-collapsed ArtifactPane are both present,
   scales each panel's min-height to min(22dvh, 220px) so the chat row
   keeps the remainder. min(22dvh, 220px) shrinks below 220px at short
   heights and never grows above 220px at tall heights, so >=1000px tall
   (where 22dvh == 220px) is unchanged; below that the panels yield
   proportionally. Single-panel (or a collapsed ArtifactPane) keeps the
   fixed 220px / 34px floor, so the rule scopes to both-present via :has()
   (already used in 8 other places) and skips the collapsed artifact.
   Recovery (close browser / collapse artifact) stays reachable because
   each panel's toolbar sits at the top of a non-zero-height row.

2. main-window.ts + window-state.ts: extract SAFE_MIN_WIDTH / SAFE_MIN_HEIGHT
   from window-state.ts (consolidating the 480x320 literals already used by
   sanitizeBounds + visualSmokeWindowBounds) and set BrowserWindow
   `minHeight: SAFE_MIN_HEIGHT` so the runtime resize floor matches the
   sanitizeBounds restore floor. This makes the 320px floor the dvh fix
   relies on actually enforced at runtime, not just at restore.

Tests:
- browser-panel-layout.test.ts: both-present shared-height contract locking
  the :has() rule's exact `min(22dvh, 220px)` value, both-panel coverage,
  and per-selector that each selector carries the full both-present +
  non-collapsed condition (rejects an unconditional panel selector that
  would leak dvh into single-panel, a collapsed-condition leak, a wrong
  value, or a missing guard).
- app-region-hygiene-contract.test.ts: BrowserWindow `minHeight:
  SAFE_MIN_HEIGHT` contract tying the runtime floor to the shared constant
  exported from window-state.ts.

Closes #824. Refs #546.
Astro-Han added a commit that referenced this pull request Jul 12, 2026
…w widths

At <=990px both ArtifactPane and BrowserPanel become bottom sheets, each
with a fixed min-height: 220px (from #818). When both are open and the
detail area is short (<=~440px), the two fixed minima squeeze the chat row
`minmax(0, 1fr)` to 0 and the composer is clipped — a reachable state
since the 320px restore minimum in window-state.ts was restore-only and
BrowserWindow set no minHeight, so runtime drag could go shorter.

Fix with two coordinated changes:

1. chat-detail.css: add a :has()-scoped rule inside the 990px block that,
   when BrowserPanel AND a non-collapsed ArtifactPane are both present,
   scales each panel's min-height to min(22dvh, 220px) so the chat row
   keeps the remainder. min(22dvh, 220px) shrinks below 220px at short
   heights and never grows above 220px at tall heights, so >=1000px tall
   (where 22dvh == 220px) is unchanged; below that the panels yield
   proportionally. Single-panel (or a collapsed ArtifactPane) keeps the
   fixed 220px / 34px floor, so the rule scopes to both-present via :has()
   (already used in 8 other places) and skips the collapsed artifact.
   Recovery (close browser / collapse artifact) stays reachable because
   each panel's toolbar sits at the top of a non-zero-height row.

2. main-window.ts + window-state.ts: extract SAFE_MIN_WIDTH / SAFE_MIN_HEIGHT
   from window-state.ts (consolidating the 480x320 literals already used by
   sanitizeBounds + visualSmokeWindowBounds) and set BrowserWindow
   `minHeight: SAFE_MIN_HEIGHT` so the runtime resize floor matches the
   sanitizeBounds restore floor. This makes the 320px floor the dvh fix
   relies on actually enforced at runtime, not just at restore.

Tests:
- browser-panel-layout.test.ts: both-present shared-height contract locking
  the :has() rule's exact `min(22dvh, 220px)` value, both-panel coverage,
  and per-selector that each selector carries the full both-present +
  non-collapsed condition (rejects an unconditional panel selector that
  would leak dvh into single-panel, a collapsed-condition leak, a wrong
  value, or a missing guard).
- app-region-hygiene-contract.test.ts: BrowserWindow `minHeight:
  SAFE_MIN_HEIGHT` contract tying the runtime floor to the shared constant
  exported from window-state.ts.

Closes #824. Refs #546.
Astro-Han added a commit that referenced this pull request Jul 12, 2026
…w widths (#830)

At <=990px both ArtifactPane and BrowserPanel become bottom sheets, each
with a fixed min-height: 220px (from #818). When both are open and the
detail area is short (<=~440px), the two fixed minima squeeze the chat row
`minmax(0, 1fr)` to 0 and the composer is clipped — a reachable state
since the 320px restore minimum in window-state.ts was restore-only and
BrowserWindow set no minHeight, so runtime drag could go shorter.

Fix with two coordinated changes:

1. chat-detail.css: add a :has()-scoped rule inside the 990px block that,
   when BrowserPanel AND a non-collapsed ArtifactPane are both present,
   scales each panel's min-height to min(22dvh, 220px) so the chat row
   keeps the remainder. min(22dvh, 220px) shrinks below 220px at short
   heights and never grows above 220px at tall heights, so >=1000px tall
   (where 22dvh == 220px) is unchanged; below that the panels yield
   proportionally. Single-panel (or a collapsed ArtifactPane) keeps the
   fixed 220px / 34px floor, so the rule scopes to both-present via :has()
   (already used in 8 other places) and skips the collapsed artifact.
   Recovery (close browser / collapse artifact) stays reachable because
   each panel's toolbar sits at the top of a non-zero-height row.

2. main-window.ts + window-state.ts: extract SAFE_MIN_WIDTH / SAFE_MIN_HEIGHT
   from window-state.ts (consolidating the 480x320 literals already used by
   sanitizeBounds + visualSmokeWindowBounds) and set BrowserWindow
   `minHeight: SAFE_MIN_HEIGHT` so the runtime resize floor matches the
   sanitizeBounds restore floor. This makes the 320px floor the dvh fix
   relies on actually enforced at runtime, not just at restore.

Tests:
- browser-panel-layout.test.ts: both-present shared-height contract locking
  the :has() rule's exact `min(22dvh, 220px)` value, both-panel coverage,
  and per-selector that each selector carries the full both-present +
  non-collapsed condition (rejects an unconditional panel selector that
  would leak dvh into single-panel, a collapsed-condition leak, a wrong
  value, or a missing guard).
- app-region-hygiene-contract.test.ts: BrowserWindow `minHeight:
  SAFE_MIN_HEIGHT` contract tying the runtime floor to the shared constant
  exported from window-state.ts.

Closes #824. Refs #546.
Astro-Han added a commit that referenced this pull request Jul 12, 2026
…act (#819)

BrowserPanel renderer chrome had no deterministic coverage, so layout/
a11y regressions (e.g. the #818 narrow-layout defect) could only be
caught by manual real-window smoke. Add two deterministic layers:

- VisualSmokeState.liveBrowserSessionIds + applyVisualSmokeFixture
  wiring + a `browser-empty` scenario that seeds the active turn
  session as a live browser session so BrowserPanel mounts. With no
  native WebContentsView in visual-smoke mode, browser.getState
  resolves null → EMPTY_STATE → the empty-state chrome (toolbar
  all-nav-disabled + <Empty> strip), exactly the #818 defect surface.
  Reuses the always-seeded turn session; no browser-specific on-disk
  seed.

- browser-panel-chrome source contract (symmetric to
  artifact-pane-layout.test.ts) locking the 5 declarative chrome
  invariants that don't need a screenshot: back/forward :disabled
  track canGoBack/canGoForward; reload/stop icon + aria-label +
  onClick swap with loading; address bar snaps to live URL on blur
  when not editing; empty state present when !hasPage. The wiring IS
  the rendered DOM for these declarative bindings, so a source regex
  is a reliable proxy without a render harness (the repo has no
  jsdom/testing-library; BrowserPanel is IPC-driven).

Loaded/loading/nav chrome states are intentionally not screenshot-
fixture'd: their layout is identical to empty (#818 is a layout
defect, not a state defect), loading/nav are transient, and their
state invariants are locked by the source contract. Seeding
BrowserState for a loaded screenshot would need a production test-
seam (main-side visual-smoke browser:state hook or BrowserPanel
visual-smoke read) for no layout-regression value.

8 baseline PNGs (1280/990 × light/dark × motion/reduced) generated
in apps/desktop/tests/screenshots/browser-empty/ (gitignored
generated captures) as a stable-subset candidate, pending visual
review + promotion to screenshots-baseline/ via a separate baseline
commit (the established pattern — baselines are committed separately
from fixture code).
Astro-Han added a commit that referenced this pull request Jul 12, 2026
…act (#819) (#838)

BrowserPanel renderer chrome had no deterministic coverage, so layout/
a11y regressions (e.g. the #818 narrow-layout defect) could only be
caught by manual real-window smoke. Add two deterministic layers:

- VisualSmokeState.liveBrowserSessionIds + applyVisualSmokeFixture
  wiring + a `browser-empty` scenario that seeds the active turn
  session as a live browser session so BrowserPanel mounts. With no
  native WebContentsView in visual-smoke mode, browser.getState
  resolves null → EMPTY_STATE → the empty-state chrome (toolbar
  all-nav-disabled + <Empty> strip), exactly the #818 defect surface.
  Reuses the always-seeded turn session; no browser-specific on-disk
  seed.

- browser-panel-chrome source contract (symmetric to
  artifact-pane-layout.test.ts) locking the 5 declarative chrome
  invariants that don't need a screenshot: back/forward :disabled
  track canGoBack/canGoForward; reload/stop icon + aria-label +
  onClick swap with loading; address bar snaps to live URL on blur
  when not editing; empty state present when !hasPage. The wiring IS
  the rendered DOM for these declarative bindings, so a source regex
  is a reliable proxy without a render harness (the repo has no
  jsdom/testing-library; BrowserPanel is IPC-driven).

Loaded/loading/nav chrome states are intentionally not screenshot-
fixture'd: their layout is identical to empty (#818 is a layout
defect, not a state defect), loading/nav are transient, and their
state invariants are locked by the source contract. Seeding
BrowserState for a loaded screenshot would need a production test-
seam (main-side visual-smoke browser:state hook or BrowserPanel
visual-smoke read) for no layout-regression value.

8 baseline PNGs (1280/990 × light/dark × motion/reduced) generated
in apps/desktop/tests/screenshots/browser-empty/ (gitignored
generated captures) as a stable-subset candidate, pending visual
review + promotion to screenshots-baseline/ via a separate baseline
commit (the established pattern — baselines are committed separately
from fixture code).
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