Skip to content

Commit 919fb72

Browse files
ndemiancclaude
andcommitted
refine(levelcode): bring back action labels — line 2 swaps file·time ⇄ buttons
The icon-only row was clean but cryptic. Labels are back — Rename · Done · Delete · Pin — without cramping the title or reintroducing the jump: on hover the SECOND line (file · time) swaps to the labeled buttons in the same fixed-height row, at full width. The title on line 1 is untouched, nothing wraps, reflows, or floats. - media/chat.html + media/sessionsView.html (shared block, byte-identical): sessActBtn now emits a <span class="sesslbl"> label; the card wraps sub + actions in a .sessline2 row; the pin rejoins the action set (no separate resting star — the Pinned bucket already groups them, and the Unpin button shows state in accent). "Archive" → "Done". - CSS per surface: line 2 has a reserved min-height; :hover hides .sesssub and shows .sessacts. The modal keeps icon + label (bordered); the narrow sidebar hides the icon for label-only compactness. Delete tints danger on hover; a pinned card's Pin button is accent. - test/sessionsUi.test.js: CARD (actions) asserts the visible labels + the shared line 2; NO-JUMP now pins the swap (line2 min-height, sub hides / acts show on hover). 30 files green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8098389 commit 919fb72

3 files changed

Lines changed: 57 additions & 52 deletions

File tree

‎extensions/levelcode-ai/media/chat.html‎

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,19 +1086,19 @@
10861086
.sesscard.warn { border-left: 2px solid var(--cc-warn); }
10871087
.sesscard.err { border-left: 2px solid var(--cc-danger); }
10881088
.sesscard.res { border-left: 2px solid var(--cc-accent); }
1089-
.sesscard .sesstop { display: flex; align-items: center; gap: 8px; min-height: 22px; }
1089+
.sesscard .sesstop { display: flex; align-items: center; gap: 8px; }
10901090
.sesscard .sessname { font-weight: 600; font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; }
1091-
.sesscard .sesssub { margin-top: 4px; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11.5px; color: var(--cc-text3); }
1092-
/* Row actions: compact icon buttons on the right, revealed on hover — no drawer, so the card never
1093-
reflows or floats over its neighbours. sesstop reserves the icon height so appearing icons don't
1094-
nudge the line. The pin stays visible at rest when pinned. */
1095-
.sesscard .sessacts { display: none; align-items: center; gap: 1px; flex: 0 0 auto; }
1091+
/* Line 2 swaps file·time ⇄ the labeled action buttons on hover — same fixed-height row, full width, so
1092+
nothing wraps, reflows, or floats and the title above is left intact. The modal keeps icons + labels. */
1093+
.sesscard .sessline2 { margin-top: 4px; min-height: 26px; display: flex; align-items: center; }
1094+
.sesscard .sesssub { font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11.5px; color: var(--cc-text3); }
1095+
.sesscard .sessacts { display: none; align-items: center; gap: 6px; flex-wrap: nowrap; }
1096+
.sesscard:hover .sesssub, .sesscard:focus-within .sesssub { display: none; }
10961097
.sesscard:hover .sessacts, .sesscard:focus-within .sessacts { display: flex; }
1097-
.sesscard .sessact { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 22px; padding: 0; border: none; background: transparent; color: var(--cc-text2); border-radius: 5px; cursor: pointer; }
1098-
.sesscard .sessact:hover { background: var(--cc-surface); color: var(--cc-text); }
1099-
.sesscard .sessact.sesspin { display: none; }
1100-
.sesscard.pinned .sesspin, .sesscard:hover .sesspin, .sesscard:focus-within .sesspin { display: inline-flex; }
1101-
.sesscard.pinned .sesspin { color: var(--cc-accent); }
1098+
.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; }
1099+
.sesscard .sessact:hover { border-color: var(--cc-accent); }
1100+
.sesscard .sessact[data-act="delete"]:hover { border-color: var(--cc-danger); color: var(--cc-danger); }
1101+
.sesscard.pinned [data-act="pin"] { color: var(--cc-accent); border-color: color-mix(in srgb, var(--cc-accent) 45%, var(--cc-line)); }
11021102
.sesscard .ci { width: 14px; height: 14px; }
11031103
@media (prefers-reduced-motion: reduce) { .sesscard { transition: none; } .sesscard:hover, .sesscard:focus-visible, .sesscard:focus-within { transform: none; } }
11041104
</style>
@@ -3313,7 +3313,7 @@
33133313
star: '<svg class="ci" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 1.5 L9.53 5.9 L14.18 5.99 L10.47 8.8 L11.82 13.26 L8 10.6 L4.18 13.26 L5.53 8.8 L1.82 5.99 L6.47 5.9 Z"/></svg>'
33143314
};
33153315
function sessActBtn(act, label, svg, cls){
3316-
return '<button type="button" class="sessact' + (cls ? ' ' + cls : '') + '" data-act="' + act + '" title="' + label + '" aria-label="' + label + '">' + svg + '</button>';
3316+
return '<button type="button" class="sessact' + (cls ? ' ' + cls : '') + '" data-act="' + act + '" title="' + label + '" aria-label="' + label + '">' + svg + '<span class="sesslbl">' + label + '</span></button>';
33173317
}
33183318
var SESS_BUCKETS = ['Pinned', 'Today', 'Yesterday', 'This week', 'Earlier'];
33193319
function sessGroup(entries, nowMs){
@@ -3322,10 +3322,11 @@
33223322
return SESS_BUCKETS.filter((b) => by[b].length).map((b) => ({ bucket: b, entries: by[b] }));
33233323
}
33243324
// esc/escAttr are INJECTED so this whole block stays pure (unit-testable without the webview globals).
3325-
// The card is two fixed-height lines: title + (primary file · time). Actions are compact icon buttons on
3326-
// the RIGHT of the title row — hidden at rest, revealed on hover — so nothing reflows or floats. Clicking
3327-
// the card body resumes (so there is no Resume icon); the pin stays visible at rest when pinned. Secondary
3328-
// detail (model · turns · files) rides the native tooltip.
3325+
// Two fixed-height lines: the title, then (primary file · time). On hover the SECOND line swaps to the
3326+
// labeled action buttons — Rename · Done · Delete · Pin — in the same fixed-height row, at full width, so
3327+
// nothing wraps, reflows, or floats and the title is left intact. Clicking the card body resumes (no
3328+
// Resume button). Pinned is shown by the Pinned bucket + the accent Unpin button; secondary detail
3329+
// (model · turns · files) rides the native tooltip.
33293330
function sessCardHtml(e, nowMs, escFn, escAttrFn){
33303331
const st = e.state === 'interrupted' ? ' warn' : e.state === 'error' ? ' err' : e.state === 'resumed' ? ' res' : '';
33313332
const files = e.filesEdited || [];
@@ -3335,13 +3336,13 @@
33353336
+ (e.model ? ' · ' + sessModelShort(e.model) : '') + (files.length ? ' · ' + files.join(', ') : '');
33363337
const acts = '<span class="sessacts">'
33373338
+ sessActBtn('rename', 'Rename', SESS_IC.edit)
3338-
+ sessActBtn('done', 'Archive', SESS_IC.check)
3339+
+ sessActBtn('done', 'Done', SESS_IC.check)
33393340
+ sessActBtn('delete', 'Delete', SESS_IC.trash)
3340-
+ '</span>'
3341-
+ sessActBtn('pin', e.pinned ? 'Unpin' : 'Pin', SESS_IC.star, 'sesspin');
3341+
+ sessActBtn('pin', e.pinned ? 'Unpin' : 'Pin', SESS_IC.star)
3342+
+ '</span>';
33423343
return '<div class="sesscard' + st + (e.pinned ? ' pinned' : '') + '" data-id="' + escAttrFn(e.id || '') + '" tabindex="0" role="button" aria-label="' + escAttrFn(e.title || 'Untitled') + '" title="' + escAttrFn(detail) + '">'
3343-
+ '<div class="sesstop"><span class="sessname">' + escFn(e.title || 'Untitled') + '</span>' + acts + '</div>'
3344-
+ '<div class="sesssub">' + sub + '</div></div>';
3344+
+ '<div class="sesstop"><span class="sessname">' + escFn(e.title || 'Untitled') + '</span></div>'
3345+
+ '<div class="sessline2"><span class="sesssub">' + sub + '</span>' + acts + '</div></div>';
33453346
}
33463347
// [SESSIONS-PURE-END]
33473348
// Placeholder data so /sessions renders the panel before the extension.js adapter posts real entries.

‎extensions/levelcode-ai/media/sessionsView.html‎

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,25 @@
2626
.sesscard { position: relative; padding: 7px 8px 8px; margin-bottom: 1px; border-radius: 6px; cursor: pointer; transition: background .1s; }
2727
.sesscard:hover { background: color-mix(in srgb, var(--cc-text3) 13%, transparent); }
2828
.sesscard:focus-visible { background: color-mix(in srgb, var(--cc-accent) 15%, transparent); outline: none; }
29-
.sesscard .sesstop { display: flex; align-items: center; gap: 10px; min-height: 20px; }
29+
.sesscard .sesstop { display: flex; align-items: center; gap: 10px; }
3030
.sesscard .sesstop::before { content: ''; flex: 0 0 auto; width: 6px; height: 6px; border-radius: 50%; background: var(--cc-text3); }
3131
.sesscard.warn .sesstop::before { background: var(--cc-warn); }
3232
.sesscard.err .sesstop::before { background: var(--cc-danger); }
3333
.sesscard.res .sesstop::before { background: var(--cc-accent); }
3434
.sesscard .sessname { font-weight: 400; font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; }
35-
.sesscard .sesssub { margin: 2px 0 0 16px; font-size: 12px; color: var(--cc-text3); }
36-
/* Row actions: compact icon buttons on the right, revealed on hover — no drawer, so the row never reflows
37-
or floats over its neighbours. sesstop reserves the icon height (min-height) so the appearing icons
38-
don't nudge the line. The pin stays visible at rest when pinned. */
39-
.sesscard .sessacts { display: none; align-items: center; gap: 1px; flex: 0 0 auto; }
35+
/* Line 2 shares its slot: at rest the file·time line; on hover the labeled buttons take the same
36+
fixed-height row (min-height) at full width — nothing wraps, reflows, or floats, and the title above is
37+
left intact. Sidebar buttons are label-only (icon hidden) to stay compact in a narrow panel. */
38+
.sesscard .sessline2 { margin: 2px 0 0 16px; min-height: 22px; display: flex; align-items: center; }
39+
.sesscard .sesssub { font-size: 12px; color: var(--cc-text3); }
40+
.sesscard .sessacts { display: none; align-items: center; gap: 4px; flex-wrap: nowrap; }
41+
.sesscard:hover .sesssub, .sesscard:focus-within .sesssub { display: none; }
4042
.sesscard:hover .sessacts, .sesscard:focus-within .sessacts { display: flex; }
41-
.sesscard .sessact { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 20px; padding: 0; border: none; background: transparent; color: var(--cc-text3); border-radius: 4px; cursor: pointer; }
42-
.sesscard .sessact:hover { background: color-mix(in srgb, var(--cc-text3) 22%, transparent); color: var(--cc-text); }
43-
.sesscard .sessact.sesspin { display: none; }
44-
.sesscard.pinned .sesspin, .sesscard:hover .sesspin, .sesscard:focus-within .sesspin { display: inline-flex; }
45-
.sesscard.pinned .sesspin { color: var(--cc-accent); }
46-
.sesscard .ci { width: 13px; height: 13px; }
43+
.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; }
44+
.sesscard .sessact .ci { display: none; }
45+
.sesscard .sessact:hover { background: color-mix(in srgb, var(--cc-text3) 20%, transparent); color: var(--cc-text); }
46+
.sesscard .sessact[data-act="delete"]:hover { background: color-mix(in srgb, var(--cc-danger) 18%, transparent); color: var(--cc-danger); }
47+
.sesscard.pinned [data-act="pin"] { color: var(--cc-accent); }
4748
@media (prefers-reduced-motion: reduce) { .sesscard { transition: none; } }
4849
</style>
4950
</head>
@@ -86,7 +87,7 @@
8687
star: '<svg class="ci" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 1.5 L9.53 5.9 L14.18 5.99 L10.47 8.8 L11.82 13.26 L8 10.6 L4.18 13.26 L5.53 8.8 L1.82 5.99 L6.47 5.9 Z"/></svg>'
8788
};
8889
function sessActBtn(act, label, svg, cls){
89-
return '<button type="button" class="sessact' + (cls ? ' ' + cls : '') + '" data-act="' + act + '" title="' + label + '" aria-label="' + label + '">' + svg + '</button>';
90+
return '<button type="button" class="sessact' + (cls ? ' ' + cls : '') + '" data-act="' + act + '" title="' + label + '" aria-label="' + label + '">' + svg + '<span class="sesslbl">' + label + '</span></button>';
9091
}
9192
var SESS_BUCKETS = ['Pinned', 'Today', 'Yesterday', 'This week', 'Earlier'];
9293
function sessGroup(entries, nowMs){
@@ -95,10 +96,11 @@
9596
return SESS_BUCKETS.filter((b) => by[b].length).map((b) => ({ bucket: b, entries: by[b] }));
9697
}
9798
// esc/escAttr are INJECTED so this whole block stays pure (unit-testable without the webview globals).
98-
// The card is two fixed-height lines: title + (primary file · time). Actions are compact icon buttons on
99-
// the RIGHT of the title row — hidden at rest, revealed on hover — so nothing reflows or floats. Clicking
100-
// the card body resumes (so there is no Resume icon); the pin stays visible at rest when pinned. Secondary
101-
// detail (model · turns · files) rides the native tooltip.
99+
// Two fixed-height lines: the title, then (primary file · time). On hover the SECOND line swaps to the
100+
// labeled action buttons — Rename · Done · Delete · Pin — in the same fixed-height row, at full width, so
101+
// nothing wraps, reflows, or floats and the title is left intact. Clicking the card body resumes (no
102+
// Resume button). Pinned is shown by the Pinned bucket + the accent Unpin button; secondary detail
103+
// (model · turns · files) rides the native tooltip.
102104
function sessCardHtml(e, nowMs, escFn, escAttrFn){
103105
const st = e.state === 'interrupted' ? ' warn' : e.state === 'error' ? ' err' : e.state === 'resumed' ? ' res' : '';
104106
const files = e.filesEdited || [];
@@ -108,13 +110,13 @@
108110
+ (e.model ? ' · ' + sessModelShort(e.model) : '') + (files.length ? ' · ' + files.join(', ') : '');
109111
const acts = '<span class="sessacts">'
110112
+ sessActBtn('rename', 'Rename', SESS_IC.edit)
111-
+ sessActBtn('done', 'Archive', SESS_IC.check)
113+
+ sessActBtn('done', 'Done', SESS_IC.check)
112114
+ sessActBtn('delete', 'Delete', SESS_IC.trash)
113-
+ '</span>'
114-
+ sessActBtn('pin', e.pinned ? 'Unpin' : 'Pin', SESS_IC.star, 'sesspin');
115+
+ sessActBtn('pin', e.pinned ? 'Unpin' : 'Pin', SESS_IC.star)
116+
+ '</span>';
115117
return '<div class="sesscard' + st + (e.pinned ? ' pinned' : '') + '" data-id="' + escAttrFn(e.id || '') + '" tabindex="0" role="button" aria-label="' + escAttrFn(e.title || 'Untitled') + '" title="' + escAttrFn(detail) + '">'
116-
+ '<div class="sesstop"><span class="sessname">' + escFn(e.title || 'Untitled') + '</span>' + acts + '</div>'
117-
+ '<div class="sesssub">' + sub + '</div></div>';
118+
+ '<div class="sesstop"><span class="sessname">' + escFn(e.title || 'Untitled') + '</span></div>'
119+
+ '<div class="sessline2"><span class="sesssub">' + sub + '</span>' + acts + '</div></div>';
118120
}
119121
// [SESSIONS-PURE-END]
120122
const D = Date.now();

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,13 @@ test('CARD (actions): four row icon buttons (rename/done/delete/pin); clicking t
7979
state: 'done', filesEdited: ['refund.rb', 'lock.rb'] };
8080
const h = P.sessCardHtml(e, NOW, esc, escAttr);
8181
for (const a of ['rename', 'done', 'delete', 'pin']) { assert.match(h, new RegExp('data-act="' + a + '"'), a + ' action present'); }
82-
assert.ok(!/data-act="resume"/.test(h), 'no Resume icon — clicking the card body resumes (default action)');
83-
assert.match(h, /class="sessacts">/, 'actions live in the row…');
84-
assert.ok(!/sessrich|sessbtn|sesschip|sessmeta/.test(h), '…not the old drawer/chip/meta markup');
85-
assert.match(h, /<svg class="ci"/, 'actions are icon buttons');
86-
assert.match(h, /aria-label="Rename"/, 'labelled for a11y (icon-only)');
82+
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)');
84+
assert.ok(!/sessrich|sessbtn|sesschip|sessmeta/.test(h), 'not the old drawer/chip/meta markup');
85+
assert.match(h, /<span class="sesslbl">Rename<\/span>/, 'buttons carry a visible text label…');
86+
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)');
88+
assert.match(h, /aria-label="Rename"/, 'and an a11y label');
8789
assert.match(h, /title="t — 41 turns · claude-opus-5 · refund\.rb, lock\.rb"/, 'model · turns · files fold into the tooltip');
8890
});
8991

@@ -179,12 +181,12 @@ test('VIEW: theme-true across the three kinds and reduced-motion-safe', () => {
179181
assert.match(view, /@media \(prefers-reduced-motion: reduce\)[\s\S]{0,120}\.sesscard[\s\S]{0,80}transition:\s*none/);
180182
});
181183

182-
test('NO-JUMP: actions reveal as row icon buttons (no drawer), so the row height stays fixed on hover', () => {
184+
test('NO-JUMP: line 2 swaps file·time ⇄ actions in a fixed min-height row (no drawer, no float, title intact)', () => {
183185
for (const [name, src] of [['modal', html], ['sidebar', view]]) {
184186
assert.ok(!/\.sesscard \.sessrich/.test(src), name + ': the old hover drawer is gone (nothing floats over neighbours)');
185-
assert.match(src, /\.sesscard \.sessacts \{[^}]*display: none/, name + ': actions are hidden at rest');
186-
assert.match(src, /\.sesscard:hover \.sessacts[^{]*\{[^}]*display: flex/, name + ': and revealed on hover');
187-
assert.match(src, /\.sesscard \.sesstop \{[^}]*min-height/, name + ': the row reserves the icon height so appearing icons never nudge it');
187+
assert.match(src, /\.sesscard \.sessline2 \{[^}]*min-height/, name + ': line 2 reserves a fixed height, so the card never grows');
188+
assert.match(src, /\.sesscard:hover \.sesssub[^{]*\{[^}]*display: none/, name + ': file·time hides on hover…');
189+
assert.match(src, /\.sesscard:hover \.sessacts[^{]*\{[^}]*display: flex/, name + ': …and the actions take that same row');
188190
}
189191
});
190192

0 commit comments

Comments
 (0)