Skip to content

Commit 165268e

Browse files
committed
fix(sessions): the row actions are visible, and they render at all
Reported from the panel: the buttons on each session row are invisible. Two separate problems, one of them a real bug rather than a preference. 1. BELOW THE LABEL THRESHOLD THE BUTTONS RENDERED EMPTY @container (max-width: 390px) { .sesscard .sesslbl { display: none; } } .sesscard .sessact .ci { display: none; } <- unconditional The container query collapses each button to its ICON on a narrow panel. The icon was hidden unconditionally, so it collapsed to NOTHING: six empty boxes with working tooltips, which is exactly what the report shows — a blank rectangle and a clipped "Done" label. The icon is the label's fallback, so it is now hidden only while the label is showing, and it has a size: an inline <svg> carrying only a viewBox has no intrinsic one, so revealing it without sizing it would have traded an invisible button for a broken one. 2. THE ACTIONS WERE HOVER-ONLY They swapped in over the file·time line on :hover. A control that exists only while the pointer is on it is one most people never find, and one a touch device cannot reach at all. They are always present now, sharing line 2 with the metadata — quiet at .5 opacity so a list of sessions still reads as a list, full strength on hover or keyboard focus. The row keeps its fixed min-height, so nothing jumps. The metadata is the half that truncates. Both surfaces get the same treatment. chat.html's /sessions modal had the identical hover-swap and the same missing icon size; only sessionsView.html made it visible, by hiding the icon outright. Fixing one and not the other is how the two drift. The NO-JUMP test asserted the SWAP — file·time on rest, actions on hover — which is the behaviour being removed, so it was rewritten rather than deleted: the row is still fixed-height and still never grows, but it now carries both halves. A second test pins the icon fallback and its size, which is the bug that was reported. Verified by rendering the real card markup at 360px and 520px: six icons below the threshold, six labels above it, the pinned star accented, the path truncating. Guards, each bypass-verified by reverting the fix: - the label collapsing to nothing (the reported bug) - the fallback icon left unsized - the actions hidden again; the hover-swap reintroduced - the metadata no longer truncating, which pushes the actions out of the card 27 tests in sessionsUi, 34 suites green.
1 parent eb28f30 commit 165268e

3 files changed

Lines changed: 60 additions & 17 deletions

File tree

extensions/levelcode-ai/media/chat.html

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,7 +1233,7 @@
12331233
/* Line 2 swaps file·time ⇄ the labeled action buttons on hover — same fixed-height row, full width, so
12341234
nothing wraps, reflows, or floats and the title above is left intact. The modal keeps icons + labels. */
12351235
.sesscard .sessline2 { margin-top: 4px; min-height: 26px; display: flex; align-items: center; }
1236-
.sesscard .sesssub { font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11.5px; color: var(--cc-text3); }
1236+
.sesscard .sesssub { font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11.5px; color: var(--cc-text3); flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
12371237
/* The action row is nowrap by design (the card is two fixed-height lines — nothing may reflow), so
12381238
a button that does not fit OVERFLOWS the card rather than wrapping. Six labelled buttons
12391239
(Rename · Fork · Copy · Done · Delete · Pin) measure 434px at this styling; icon-only they are
@@ -1244,10 +1244,15 @@
12441244
reader — only to the eye, and only when there is no room for it.
12451245
ADDING A BUTTON? Re-measure and raise this; test/webviewCss.test.js pins the floor. */
12461246
.sesscard { container-type: inline-size; }
1247-
@container (max-width: 460px) { .sesscard .sesslbl { display: none; } }
1248-
.sesscard .sessacts { display: none; align-items: center; gap: 6px; flex-wrap: nowrap; }
1249-
.sesscard:hover .sesssub, .sesscard:focus-within .sesssub { display: none; }
1250-
.sesscard:hover .sessacts, .sesscard:focus-within .sessacts { display: flex; }
1247+
/* Same contract as the Sessions panel (media/sessionsView.html): the actions are always present,
1248+
quiet until hover, and the icon is the label's fallback rather than a second hidden thing. */
1249+
.sesscard .sessact .ci { display: none; width: 13px; height: 13px; }
1250+
@container (max-width: 460px) {
1251+
.sesscard .sesslbl { display: none; }
1252+
.sesscard .sessact .ci { display: inline-flex; }
1253+
}
1254+
.sesscard .sessacts { display: flex; align-items: center; gap: 4px; flex-wrap: nowrap; flex: 0 0 auto; margin-left: auto; opacity: .5; transition: opacity .1s; }
1255+
.sesscard:hover .sessacts, .sesscard:focus-within .sessacts { opacity: 1; }
12511256
.sesscard .sessact { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 9px; border: 1px solid var(--cc-line); background: var(--cc-surface); color: var(--cc-text); border-radius: 5px; cursor: pointer; font: inherit; font-size: 11.5px; white-space: nowrap; }
12521257
.sesscard .sessact:hover { border-color: var(--cc-accent); }
12531258
.sesscard .sessact[data-act="delete"]:hover { border-color: var(--cc-danger); color: var(--cc-danger); }

extensions/levelcode-ai/media/sessionsView.html

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,32 @@
8585
/* Line 2 shares its slot: at rest the file·time line; on hover the labeled buttons take the same
8686
fixed-height row (min-height) at full width — nothing wraps, reflows, or floats, and the title above is
8787
left intact. Sidebar buttons are label-only (icon hidden) to stay compact in a narrow panel. */
88-
.sesscard .sessline2 { margin: 2px 0 0 16px; min-height: 22px; display: flex; align-items: center; }
89-
.sesscard .sesssub { font-size: 12px; color: var(--cc-text3); }
88+
.sesscard .sessline2 { margin: 2px 0 0 16px; min-height: 22px; display: flex; align-items: center; gap: 8px; }
89+
/* The metadata truncates so the actions beside it always have room; it is the expendable half. */
90+
.sesscard .sesssub { font-size: 12px; color: var(--cc-text3); flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
9091
/* Same reasoning as chat.html, different numbers — this pane's buttons are smaller (22px tall,
9192
8px padding, 11px text, no border), so six labelled ones measure 363px rather than 434px. The
9293
threshold sits above that; below it the row drops to icons, keeping title + aria-label, so only
9394
the eye loses anything and only when there is no room.
9495
ADDING A BUTTON? Re-measure and raise this; test/webviewCss.test.js pins the floor. */
9596
.sesscard { container-type: inline-size; }
96-
@container (max-width: 390px) { .sesscard .sesslbl { display: none; } }
97-
.sesscard .sessacts { display: none; align-items: center; gap: 4px; flex-wrap: nowrap; }
98-
.sesscard:hover .sesssub, .sesscard:focus-within .sesssub { display: none; }
99-
.sesscard:hover .sessacts, .sesscard:focus-within .sessacts { display: flex; }
97+
/* ALWAYS PRESENT, not hover-revealed. A control that exists only while the pointer is on it is a
98+
control most people never find — and it cannot be found at all on a touch device. Quiet by
99+
default so a list of sessions still reads as a list; full strength on hover or keyboard focus. */
100+
.sesscard .sessacts { display: flex; align-items: center; gap: 2px; flex-wrap: nowrap; flex: 0 0 auto; margin-left: auto; opacity: .5; transition: opacity .1s; }
101+
.sesscard:hover .sessacts, .sesscard:focus-within .sessacts { opacity: 1; }
102+
@media (prefers-reduced-motion: reduce) { .sesscard .sessacts { transition: none; } }
100103
.sesscard .sessact { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border: none; background: transparent; color: var(--cc-text2); border-radius: 4px; cursor: pointer; font: inherit; font-size: 11px; white-space: nowrap; }
101-
.sesscard .sessact .ci { display: none; }
104+
/* The icon is the FALLBACK for the label, so it is hidden only while the label is showing — and it
105+
needs a size, because an inline <svg> with just a viewBox has no intrinsic one.
106+
This is the bug that made the buttons invisible: `.ci` was display:none unconditionally, so below
107+
the label threshold each button fell back to nothing and rendered as an empty box. */
108+
.sesscard .sessact .ci { display: none; width: 13px; height: 13px; }
109+
@container (max-width: 390px) {
110+
.sesscard .sesslbl { display: none; }
111+
.sesscard .sessact .ci { display: inline-flex; }
112+
.sesscard .sessact { padding: 0 5px; }
113+
}
102114
.sesscard .sessact:hover { background: color-mix(in srgb, var(--cc-text3) 20%, transparent); color: var(--cc-text); }
103115
.sesscard .sessact[data-act="delete"]:hover { background: color-mix(in srgb, var(--cc-danger) 18%, transparent); color: var(--cc-danger); }
104116
.sesscard.pinned [data-act="pin"] { color: var(--cc-accent); }

extensions/levelcode-ai/test/sessionsUi.test.js

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ test('CARD (actions): six row icon buttons (rename/fork/export/done/delete/pin);
8080
const h = P.sessCardHtml(e, NOW, esc, escAttr);
8181
for (const a of ['rename', 'fork', 'export', 'done', 'delete', 'pin']) { assert.match(h, new RegExp('data-act="' + a + '"'), a + ' action present'); }
8282
assert.ok(!/data-act="resume"/.test(h), 'no Resume button — clicking the card body resumes (default action)');
83-
assert.match(h, /class="sessline2">.*class="sesssub">/, 'file·time and the actions share line 2 (swap on hover)');
83+
assert.match(h, /class="sessline2">.*class="sesssub">/, 'file·time and the actions share line 2');
8484
assert.ok(!/sessrich|sessbtn|sesschip|sessmeta/.test(h), 'not the old drawer/chip/meta markup');
8585
assert.match(h, /<span class="sesslbl">Rename<\/span>/, 'buttons carry a visible text label…');
8686
assert.match(h, /<span class="sesslbl">Done<\/span>.*<span class="sesslbl">Delete<\/span>/, '…Done and Delete too');
87-
assert.match(h, /<svg class="ci"/, 'with an icon alongside (hidden by CSS on the narrow sidebar)');
87+
assert.match(h, /<svg class="ci"/, 'with an icon alongside — the fallback CSS shows once the label collapses');
8888
assert.match(h, /aria-label="Rename"/, 'and an a11y label');
8989
assert.match(h, /title="t 41 turns · claude-opus-5 · refund\.rb, lock\.rb"/, 'model · turns · files fold into the tooltip');
9090
});
@@ -254,12 +254,38 @@ test('VIEW: theme-true across the three kinds and reduced-motion-safe', () => {
254254
assert.match(view, /@media \(prefers-reduced-motion: reduce\)[\s\S]{0,120}\.sesscard[\s\S]{0,80}transition:\s*none/);
255255
});
256256

257-
test('NO-JUMP: line 2 swaps file·time ⇄ actions in a fixed min-height row (no drawer, no float, title intact)', () => {
257+
test('NO-JUMP: line 2 carries file·time AND the actions in a fixed min-height row', () => {
258+
// This used to assert a SWAP — file·time on rest, actions on hover, same row. The row is still
259+
// fixed-height and still never grows, but the actions no longer wait for a pointer: a control that
260+
// exists only while hovered is one most people never find, and one a touch device cannot reach at
261+
// all. They share the row now, and the metadata is the half that truncates.
258262
for (const [name, src] of [['modal', html], ['sidebar', view]]) {
259263
assert.ok(!/\.sesscard \.sessrich/.test(src), name + ': the old hover drawer is gone (nothing floats over neighbours)');
260264
assert.match(src, /\.sesscard \.sessline2 \{[^}]*min-height/, name + ': line 2 reserves a fixed height, so the card never grows');
261-
assert.match(src, /\.sesscard:hover \.sesssub[^{]*\{[^}]*display: none/, name + ': file·time hides on hover…');
262-
assert.match(src, /\.sesscard:hover \.sessacts[^{]*\{[^}]*display: flex/, name + ': …and the actions take that same row');
265+
assert.match(src, /\.sesscard \.sessacts \{[^}]*display: flex/, name + ': the actions are not present until hovered');
266+
assert.ok(!/\.sesscard:hover \.sesssub[^{]*\{[^}]*display: none/.test(src),
267+
name + ': hovering still hides the file·time — the row is shared now, not swapped');
268+
assert.match(src, /\.sesscard \.sesssub \{[^}]*text-overflow: ellipsis/,
269+
name + ': the metadata must truncate, or it pushes the actions out of the card');
270+
assert.match(src, /\.sesscard:hover \.sessacts[^{]*\{[^}]*opacity: 1/,
271+
name + ': the actions should still come forward on hover rather than shouting all the time');
272+
}
273+
});
274+
275+
test('ACTIONS: below the label threshold the buttons fall back to an ICON, not to nothing', () => {
276+
// The bug this pins, reported from the panel: `.sessact .ci` was `display: none` UNCONDITIONALLY,
277+
// while the container query hid `.sesslbl` below the threshold. So in a narrow panel every action
278+
// button had no visible content at all — six empty boxes with working tooltips.
279+
for (const [name, src] of [['modal', html], ['sidebar', view]]) {
280+
const q = /@container \(max-width: (\d+)px\) \{([\s\S]*?)\n \}/.exec(src)
281+
|| /@container \(max-width: (\d+)px\) \{([^}]*\{[^}]*\}[\s\S]*?)\}/.exec(src);
282+
assert.ok(q, name + ': the label-collapse container query is gone');
283+
assert.match(q[2], /\.sesslbl \{ display: none/, name + ': the label no longer collapses');
284+
assert.match(q[2], /\.sessact \.ci \{ display: inline-flex/,
285+
name + ': the label collapses with no icon to fall back to — the button renders empty');
286+
// An inline <svg> carrying only a viewBox has no intrinsic size, so showing it is not enough.
287+
assert.match(src, /\.sesscard \.sessact \.ci \{[^}]*width:\s*\d+px[^}]*height:\s*\d+px/,
288+
name + ': the fallback icon has no size — an svg with only a viewBox does not have one');
263289
}
264290
});
265291

0 commit comments

Comments
 (0)