From 0ed03c4e9d18f15d04c485628b81248cd4e8c20d Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Tue, 31 Mar 2026 06:55:05 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20fit=20view=20=E2=80=94=20clear=20stale?= =?UTF-8?q?=20layout=20on=20recalc,=20revert=20to=20position:absolute=20bo?= =?UTF-8?q?ttom:0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1: applyFitLayout now clears grid-template-rows and tile heights before recalculating. Prevents stale layout from empty-grid calls on page reload from interfering with subsequent calculations. Bug 2: Removed flex justify-content:flex-end approach — didn't work because pre filled 100% of parent (flex-end had no effect). Reverted to base CSS position:absolute + bottom:0 which anchors content to the bottom. With 80 lines in fit mode, content always overflows the tile and excess is clipped at the top by tile-body overflow:hidden. --- muxplex/frontend/app.js | 7 +++ muxplex/frontend/style.css | 19 +++----- muxplex/frontend/tests/test_app.mjs | 69 +++++++++++++++++++++++++---- muxplex/tests/test_frontend_css.py | 50 +++++++++------------ 4 files changed, 96 insertions(+), 49 deletions(-) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index e693798..7c08a9c 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -1103,6 +1103,13 @@ function saveDisplaySettings(settings) { * @param {Element} grid - The session grid element */ function applyFitLayout(grid) { + // Clear stale layout from previous calls (prevents interference on page reload and + // when returning from a session where the grid was display:none during measurement) + grid.style.removeProperty('grid-template-rows'); + grid.querySelectorAll('.session-tile').forEach(function(t) { + t.style.removeProperty('height'); + }); + var count = grid.querySelectorAll('.session-tile').length; if (count === 0) return; diff --git a/muxplex/frontend/style.css b/muxplex/frontend/style.css index ca9c3f0..57cf1b8 100644 --- a/muxplex/frontend/style.css +++ b/muxplex/frontend/style.css @@ -1382,19 +1382,12 @@ body { overflow: hidden; } -/* In fit mode, tile body uses flex to anchor content at the bottom (like a real terminal) */ -.session-grid--fit .tile-body { - display: flex; - flex-direction: column; - justify-content: flex-end; /* content anchored to bottom */ -} - -/* In fit mode, pre participates in flex layout (not absolute-positioned) */ -.session-grid--fit .tile-body pre { - position: static; /* remove absolute positioning — flex layout controls position */ - max-height: 100%; /* don't overflow the tile body */ - overflow: hidden; /* clip excess content at the top */ -} +/* In fit mode, tile body and pre use the base CSS: + .tile-body { position: relative; overflow: hidden; } + .tile-body pre { position: absolute; bottom: 0; left: 0; right: 0; } + The pre's natural height from 80 lines of content exceeds the tile height, so it + overflows from bottom:0 upward, and tile-body clips excess at the top — showing + the bottom-most content (the prompt area) just like a real terminal. */ /* ============================================================ Responsive overlay sidebar at <960px diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index 514a401..c83fab2 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -2234,17 +2234,70 @@ test('buildTileHTML shows up to 80 lines in fit mode', () => { ); }); -test('CSS style.css uses flex layout (not scrollbar hack) for fit mode content anchoring', () => { +test('CSS style.css uses base position:absolute bottom:0 for fit mode content anchoring (no flex override)', () => { const source = fs.readFileSync(new URL('../style.css', import.meta.url), 'utf8'); - // New approach: flex + justify-content:flex-end anchors content to bottom without JS scrollTop hacks + // Reverted approach: base CSS position:absolute + bottom:0 anchors content to bottom. + // The flex + justify-content:flex-end approach failed because
 filled 100% of parent,
+  // making flex-end a no-op (content started at top, excess clipped at bottom).
+  // The fit-mode tile-body flex override must be REMOVED.
   assert.ok(
-    source.includes('.session-grid--fit .tile-body'),
-    'style.css must have .session-grid--fit .tile-body rule for flex layout'
+    !source.includes('.session-grid--fit .tile-body {'),
+    'style.css must NOT have .session-grid--fit .tile-body flex override — base position:absolute handles anchoring'
   );
+  // The pre static-positioning override must also be removed
   assert.ok(
-    source.includes('justify-content: flex-end') || source.includes('justify-content:flex-end'),
-    'style.css must use justify-content: flex-end to anchor fit mode content at bottom'
+    !source.includes('.session-grid--fit .tile-body pre {'),
+    'style.css must NOT have .session-grid--fit .tile-body pre override — base position:absolute + bottom:0 is correct'
+  );
+  // Base .tile-body pre must still use position:absolute + bottom:0
+  assert.ok(
+    source.includes('position: absolute') && source.includes('bottom: 0'),
+    'base .tile-body pre must retain position:absolute and bottom:0 for content anchoring'
   );
-  // Old scrollbar-hiding hack is no longer needed with flex approach
-  // (scrollbar-width: none was only needed when overflow-y:scroll was used for scrollTop positioning)
+});
+
+// --- Fit view Bug 1: applyFitLayout clears stale layout before recalculating ---
+
+test('applyFitLayout clears stale grid-template-rows and tile heights before recalculating', () => {
+  const removedGridProps = [];
+  const removedTileProps = [];
+
+  const mockTile = {
+    style: { removeProperty: (prop) => removedTileProps.push(prop) },
+  };
+
+  const mockGrid = {
+    style: {
+      removeProperty: (prop) => removedGridProps.push(prop),
+      gridTemplateColumns: '',
+      gridTemplateRows: 'repeat(2, 1fr)',  // stale value from previous call
+    },
+    querySelectorAll: (sel) => {
+      if (sel === '.session-tile') return [mockTile];
+      return [];
+    },
+    clientWidth: 800,
+    parentElement: { clientHeight: 600 },
+  };
+
+  const origGetComputedStyle = globalThis.getComputedStyle;
+  globalThis.getComputedStyle = () => ({
+    paddingTop: '16px', paddingBottom: '16px',
+    paddingLeft: '16px', paddingRight: '16px',
+    gap: '8px',
+  });
+
+  app.applyFitLayout(mockGrid);
+
+  assert.ok(
+    removedGridProps.includes('grid-template-rows'),
+    'applyFitLayout must removeProperty("grid-template-rows") before recalculating to prevent stale layout interference'
+  );
+  assert.ok(
+    removedTileProps.includes('height'),
+    'applyFitLayout must removeProperty("height") from each tile before recalculating'
+  );
+
+  if (origGetComputedStyle) globalThis.getComputedStyle = origGetComputedStyle;
+  else delete globalThis.getComputedStyle;
 });
diff --git a/muxplex/tests/test_frontend_css.py b/muxplex/tests/test_frontend_css.py
index 1ab7158..78164e9 100644
--- a/muxplex/tests/test_frontend_css.py
+++ b/muxplex/tests/test_frontend_css.py
@@ -1744,41 +1744,35 @@ def test_view_uses_dvh_fallback() -> None:
     )
 
 
-def test_fit_view_tile_body_uses_flex_end() -> None:
-    """Fit mode tile-body must use flex + justify-content: flex-end for bottom anchoring.
+def test_fit_view_no_tile_body_flex_override() -> None:
+    """Bug fix: .session-grid--fit .tile-body must NOT have a flex override.
 
-    Bug: .tile-body pre had position:absolute + scrollTop=scrollHeight hack.
-    The scrollTop hack resets every 2s when innerHTML is rebuilt by the poll cycle,
-    and has no effect when content doesn't overflow.
-    Fix: make tile-body a flex container with justify-content:flex-end so the pre
-    naturally anchors to the bottom without JS.
+    The flex + justify-content:flex-end approach failed because the 
 with
+    max-height:100% fills the parent entirely, making flex-end a no-op. Content
+    started at the top and excess was clipped at the bottom — the opposite of what
+    we want.
+
+    Fix: delete the .session-grid--fit .tile-body rule entirely. The base CSS
+    position:absolute + bottom:0 on the 
 anchors content to the bottom.
     """
     css = read_css()
-    assert ".session-grid--fit .tile-body" in css, (
-        "Missing .session-grid--fit .tile-body rule — needed for flex-end bottom anchoring"
-    )
-    block = _extract_rule_block(css, ".session-grid--fit .tile-body {")
-    assert "display: flex" in block or "display:flex" in block, (
-        ".session-grid--fit .tile-body must use display: flex"
-    )
-    assert "justify-content: flex-end" in block or "justify-content:flex-end" in block, (
-        ".session-grid--fit .tile-body must use justify-content: flex-end to anchor content to bottom"
+    assert ".session-grid--fit .tile-body {" not in css, (
+        ".session-grid--fit .tile-body must be removed — flex-end approach does not work "
+        "when 
 fills 100% of the parent. Use base position:absolute + bottom:0."
     )
 
 
-def test_fit_view_pre_is_static() -> None:
-    """Fit mode pre must use position:static (not absolute) for flex layout.
+def test_fit_view_no_pre_static_override() -> None:
+    """Bug fix: .session-grid--fit .tile-body pre must NOT override position to static.
 
-    Bug: .tile-body pre uses position:absolute which takes it out of flex flow.
-    In fit mode, we need position:static so flex layout controls the pre's position.
-    The parent .tile-body is a flex column with justify-content:flex-end, so the
-    pre will naturally sit at the bottom.
+    The position:static override removed the pre from absolute positioning, breaking
+    the bottom anchoring. Base CSS already has position:absolute + bottom:0 which
+    anchors content to the bottom of the tile.
+
+    Fix: delete the .session-grid--fit .tile-body pre rule entirely.
     """
     css = read_css()
-    assert ".session-grid--fit .tile-body pre" in css, (
-        "Missing .session-grid--fit .tile-body pre rule"
-    )
-    block = _extract_rule_block(css, ".session-grid--fit .tile-body pre {")
-    assert "position: static" in block or "position:static" in block, (
-        ".session-grid--fit .tile-body pre must have position: static to participate in flex layout"
+    assert ".session-grid--fit .tile-body pre {" not in css, (
+        ".session-grid--fit .tile-body pre override must be removed — revert to base "
+        "position:absolute + bottom:0 for correct bottom anchoring."
     )