Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions apps/desktop/src/main/__tests__/browser-panel-layout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* CSS contract test for BrowserPanel narrow layout (#546 closeout).
*
* Narrow source contract: locks ONLY the declaration that prevents the
* disappearing-browser defect — `min-height` on the narrow `.maka-browser-panel`
* rule. BrowserPanel's strip reports its rect to main each animation frame;
* without a panel min-height the strip's `flex: 1 1 auto` resolves to ~0 in the
* auto grid row and the native WebContentsView is mirrored to nothing, so the
* embedded browser disappears. `min-height` is genuinely exclusive to the
* narrow rule (the base `.maka-browser-panel` rule does not declare it), so a
* source contract can lock its effective value within the block.
*
* This does NOT simulate the CSS cascade and does NOT claim to lock width,
* border-left, max-height, border-top, or appearance. `width` / `border-left`
* are also declared by the base rule and the narrow values win only by source
* order, which a text contract cannot verify; `max-height` is a space cap, not
* the disappearance-prevention floor; `border-top` / `box-shadow` are visual.
* Those belong to visual smoke review, not a text contract. Locking the full
* computed layout would need a real browser computed-style check.
*/
import { strict as assert } from 'node:assert';
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { describe, it } from 'node:test';
import postcss from 'postcss';
import { REPO_ROOT, stripCssComments } from './css-test-helpers.js';

const CHAT_DETAIL_CSS = resolve(REPO_ROOT, 'apps/desktop/src/renderer/styles/chat-detail.css');

interface MinHeightDecl {
value: string;
important: boolean;
}

/** All `min-height` declarations on exact-`.maka-browser-panel` rules inside
* `@media (max-width: 990px)` blocks, in source order with their !important
* flag. Pure over a CSS string so the collection/guard logic is unit-testable. */
function narrowBrowserMinHeights(css: string): MinHeightDecl[] {
const root = postcss.parse(stripCssComments(css), { from: CHAT_DETAIL_CSS });
const decls: MinHeightDecl[] = [];
root.walkAtRules('media', (atRule) => {
if (!/^\(\s*max-width\s*:\s*990px\s*\)$/.test(atRule.params)) return;
atRule.walkRules((rule) => {
if (!rule.selectors.includes('.maka-browser-panel')) return;
for (const node of rule.nodes) {
if (node.type !== 'decl' || node.prop !== 'min-height') continue;
const d = node as postcss.Declaration;
decls.push({ value: d.value.trim(), important: Boolean(d.important) });
}
});
});
return decls;
}

describe('BrowserPanel narrow layout CSS contract', () => {
it('reserves a usable non-zero strip height at <=990px via a single unchallenged min-height', async () => {
const decls = narrowBrowserMinHeights(await readFile(CHAT_DETAIL_CSS, 'utf8'));
assert.equal(decls.length, 1, 'expected exactly one min-height on .maka-browser-panel in the 990px block (a second rule or duplicate property would compete)');
assert.equal(decls[0].value, '220px', 'expected min-height:220px so the strip (and native view) is non-zero');
assert.equal(decls[0].important, false, 'expected the narrow min-height not to need !important (a !important override would escape the contract)');
});

it('collects min-height across all matching rules so the guard catches a second overriding rule, a duplicate property, !important, or removal', () => {
const block = (browserRule: string) => `@media (max-width: 990px){.maka-artifact-pane{width:100%}${browserRule}}`;
// one clean declaration -> a single entry (the integration guard passes)
assert.equal(narrowBrowserMinHeights(block('.maka-browser-panel{min-height:220px}')).length, 1);
// a second overriding rule -> two entries (the integration length===1 guard fails)
assert.equal(narrowBrowserMinHeights(block('.maka-browser-panel{min-height:220px}.maka-browser-panel{min-height:0}')).length, 2);
// a duplicate property within one rule -> two entries (the guard fails)
assert.equal(narrowBrowserMinHeights(block('.maka-browser-panel{min-height:220px;min-height:0}')).length, 2);
// a !important -> recorded with the flag (the integration important===false guard fails)
assert.equal(narrowBrowserMinHeights(block('.maka-browser-panel{min-height:220px !important}'))[0].important, true);
// removal -> zero entries (the guard fails)
assert.equal(narrowBrowserMinHeights(block('.maka-browser-panel{width:100%}')).length, 0);
});
});
28 changes: 25 additions & 3 deletions apps/desktop/src/renderer/styles/chat-detail.css
Original file line number Diff line number Diff line change
Expand Up @@ -632,15 +632,37 @@
background: oklch(from var(--destructive) l c h / 0.10);
}

/* Narrow window: ArtifactPane becomes a bottom sheet below the composer
instead of continuing to shrink the chat column. This keeps the send path
visible and usable while preserving artifact access on small windows. */
/* Narrow window: ArtifactPane and BrowserPanel become bottom sheets
below the composer instead of continuing to squeeze the chat column,
preserving artifact and embedded-browser access on small windows.

BrowserPanel previously had no narrow 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 gap.
`min-height` below forces the auto row (and thus the strip) to a usable,
bounded size; `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; ArtifactPane as the third
child uses an implicit `auto` row (grid-auto-rows defaults to auto), so no
extra explicit row is needed. With both auxiliary panels open on a very
short window the chat column can collapse; the user recovers by collapsing
ArtifactPane or closing the browser (tracked in the PR notes). */
@media (max-width: 990px) {
.maka-detail-with-artifacts {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
}

.maka-browser-panel {
width: 100%;
min-height: 220px;
max-height: min(42dvh, 360px);
border-left: 0;
border-top: var(--border-width-hairline) solid var(--border);
box-shadow: 0 -8px 24px oklch(from var(--foreground) l c h / 0.08);
}
.maka-artifact-pane {
width: 100%;
min-height: 220px;
Expand Down
Loading