Skip to content

Commit 77d4f13

Browse files
ndemiancclaude
andcommitted
feat(levelcode): drop sessions search, cap the list at 30
With sessions bounded to a small working set there's nothing to search through, so remove the search field from both surfaces (the /sessions modal and the Sessions sidebar) and the now-orphaned sessMatch helper from the byte-shared SESSIONS-PURE block — the drift test keeps the two deletions identical. Simpler, quieter, more like VS Code's own history. - media/chat.html, media/sessionsView.html: remove the search input + its CSS, the sessQuery state + input listeners, and the "nothing matches" empty branch; drop sessMatch from the pure block on both sides. - extension.js: sessionList() now caps the displayed index at the 30 most-recently-touched sessions, with pinned sessions exempt (a pin is never dropped for being old). Older/archived work fades from the list (still reachable by resume later). - test/sessionsUi.test.js: drop the MATCH test + the sessMatch extraction; add a CAP invariant (≤30, pinned exempt, no search field on either surface). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2685c38 commit 77d4f13

4 files changed

Lines changed: 27 additions & 43 deletions

File tree

‎extensions/levelcode-ai/extension.js‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -669,8 +669,20 @@ function sessionsManager() {
669669
} catch (e) { dbg('sessions.init.error', { msg: String((e && e.message) || e) }); return null; }
670670
return _sessionsMgr;
671671
}
672-
/** The session index for the current workspace (what the panel/view list). Empty, never throws. */
673-
function sessionList() { const m = sessionsManager(); return m ? m.list() : []; }
672+
// The sessions shown in the /sessions modal + the sidebar: the 30 most-recently-touched, plus any pinned
673+
// session (a pin is never dropped just for being old). Capped because the surfaces carry no search — 30 is
674+
// a comfortable working set; older/archived work fades out (and stays reachable by resume later).
675+
const SESSIONS_SHOWN = 30;
676+
function sessionList() {
677+
const m = sessionsManager(); if (!m) { return []; }
678+
const all = m.list();
679+
if (all.length <= SESSIONS_SHOWN) { return all; }
680+
const recent = all.slice().sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')));
681+
const shown = recent.slice(0, SESSIONS_SHOWN);
682+
const have = new Set(shown.map((e) => e.id));
683+
for (const e of recent.slice(SESSIONS_SHOWN)) { if (e.pinned && !have.has(e.id)) { shown.push(e); } }
684+
return shown;
685+
}
674686

675687
function newChat() {
676688
// Seal the outgoing session (its terminal state + a final index row) BEFORE the transcript is cleared,

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

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,8 +1071,6 @@
10711071
--cc-warn: var(--vscode-editorWarning-foreground, var(--vscode-foreground)); --cc-danger: var(--vscode-errorForeground); }
10721072
.sesspanel .sessbar { display: flex; align-items: center; gap: 9px; padding: 12px 14px 10px; border-bottom: 1px solid var(--cc-line); }
10731073
.sesspanel .sesstitle { font-weight: 600; font-size: 14px; }
1074-
.sesspanel .sesssearch { margin: 10px 12px 6px; display: flex; align-items: center; gap: 7px; padding: 7px 10px; background: var(--cc-bg); border: 1px solid var(--cc-line); border-radius: 6px; color: var(--cc-text3); }
1075-
.sesspanel .sesssearch input { flex: 1; background: transparent; border: none; outline: none; color: var(--cc-text); font: inherit; font-size: 12.5px; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); }
10761074
.sesspanel .sesslist { flex: 1 1 auto; overflow-y: auto; padding: 2px 12px 14px; }
10771075
.sesspanel .sessbucket { font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--cc-text3); margin: 12px 4px 7px; }
10781076
.sesspanel .sessempty { padding: 44px 18px; text-align: center; color: var(--cc-text3); font-size: 13px; line-height: 1.6; }
@@ -1224,7 +1222,6 @@
12241222
<div class="sesspanel" role="dialog" aria-modal="true" aria-label="Sessions">
12251223
<button class="ovclose" id="sessClose" title="Close" aria-label="Close">&#10005;</button>
12261224
<div class="sessbar"><span class="sesstitle">Sessions</span></div>
1227-
<div class="sesssearch"><span aria-hidden="true">&#8981;</span><input id="sessSearch" type="text" placeholder="Search sessions, files…" aria-label="Search sessions" /></div>
12281225
<div class="sesslist" id="sessList"></div>
12291226
</div>
12301227
</div>
@@ -3298,13 +3295,6 @@
32983295
if (days <= 7) { return 'This week'; }
32993296
return 'Earlier';
33003297
}
3301-
function sessMatch(entry, q){
3302-
if (!q) { return true; }
3303-
const s = String(q).toLowerCase();
3304-
if (String(entry.title || '').toLowerCase().indexOf(s) >= 0) { return true; }
3305-
if (String(entry.preview || '').toLowerCase().indexOf(s) >= 0) { return true; }
3306-
return (entry.filesEdited || []).some((f) => String(f).toLowerCase().indexOf(s) >= 0);
3307-
}
33083298
function sessModelShort(m){ const s = String(m || ''); const i = s.lastIndexOf('/'); return i >= 0 ? s.slice(i + 1) : s; }
33093299
function sessSparkSvg(spark){
33103300
const a = (Array.isArray(spark) ? spark : []).map((x) => Math.max(0, Number(x) || 0));
@@ -3344,12 +3334,12 @@
33443334
{ id: 's2', title: 'Debug the flaky payment webhook test', createdAt: '2026-07-28T05:00:00Z', updatedAt: '2026-07-28T05:30:00Z', turns: 12, model: 'moonshotai/kimi-k2.7-code', state: 'interrupted', lifecycle: 'active', pinned: false, filesEdited: ['webhook_spec.rb'], spark: [2, 6, 3, 1] },
33453335
{ id: 's3', title: 'Tidy the CHANGELOG for v1.0.4', createdAt: '2026-07-27T12:00:00Z', updatedAt: '2026-07-27T12:20:00Z', turns: 6, model: 'anthropic/claude-opus-5', state: 'done', lifecycle: 'active', pinned: false, filesEdited: ['RELEASE-NOTES.md'], spark: [1, 2, 1] }
33463336
];
3347-
let sessData = null, sessQuery = '';
3337+
let sessData = null;
33483338
function renderSessions(){
33493339
const list = document.getElementById('sessList'); if (!list) { return; }
3350-
const entries = (sessData || []).filter((e) => (e.lifecycle || 'active') === 'active' && sessMatch(e, sessQuery));
3340+
const entries = (sessData || []).filter((e) => (e.lifecycle || 'active') === 'active');
33513341
if (!entries.length){
3352-
list.innerHTML = '<div class="sessempty">' + (sessQuery ? 'Nothing matches “' + esc(sessQuery) + '”.' : 'Every chat you have is saved here — automatically, on your machine.') + '</div>';
3342+
list.innerHTML = '<div class="sessempty">Every chat you have is saved here — automatically, on your machine.</div>';
33533343
return;
33543344
}
33553345
const now = Date.now(); let html = '';
@@ -3362,15 +3352,12 @@
33623352
function openSessions(){
33633353
const ov = document.getElementById('sessOverlay'); if (!ov) { return; }
33643354
if (sessData == null) { vscode.postMessage({ type: 'listSessions' }); sessData = MOCK_SESSIONS.slice(); } // TODO(adapter): drop the mock once extension.js posts 'sessions'
3365-
sessQuery = ''; const inp = document.getElementById('sessSearch'); if (inp) { inp.value = ''; }
33663355
renderSessions(); ov.style.display = 'flex';
3367-
if (inp && inp.focus) { try { inp.focus(); } catch (e) {} }
33683356
}
33693357
function closeSessions(){ const ov = document.getElementById('sessOverlay'); if (ov) { ov.style.display = 'none'; } }
33703358
(function wireSessions(){
33713359
const close = document.getElementById('sessClose'); if (close) { close.onclick = closeSessions; }
33723360
const ov = document.getElementById('sessOverlay'); if (ov) { ov.addEventListener('click', (e) => { if (e.target === ov) { closeSessions(); } }); }
3373-
const inp = document.getElementById('sessSearch'); if (inp) { inp.addEventListener('input', () => { sessQuery = inp.value; renderSessions(); }); }
33743361
const list = document.getElementById('sessList');
33753362
if (list) { list.addEventListener('click', (e) => {
33763363
const card = e.target.closest && e.target.closest('.sesscard'); if (!card) { return; }

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

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616
html, body { height: 100%; }
1717
body { margin: 0; background: var(--cc-surface); color: var(--cc-text); font-family: var(--vscode-font-family); font-size: var(--vscode-font-size); font-variant-numeric: tabular-nums; }
1818
.sessview { display: flex; flex-direction: column; height: 100vh; }
19-
.sesstoolbar { padding: 8px 10px 4px; }
20-
.sesstoolbar input { width: 100%; background: var(--cc-bg); border: 1px solid var(--cc-line); border-radius: 6px; padding: 6px 9px; color: var(--cc-text); font: inherit; font-size: 12px; outline: none; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); }
21-
.sesstoolbar input:focus { border-color: var(--cc-accent); }
2219
.sessnew { margin: 2px 10px 8px; padding: 7px 10px; background: var(--cc-bg); border: 1px solid var(--cc-line); border-radius: 6px; color: var(--cc-text); font: inherit; font-size: 12.5px; cursor: pointer; text-align: center; }
2320
.sessnew:hover { border-color: var(--cc-accent); }
2421
.sesslist { flex: 1 1 auto; overflow-y: auto; padding: 0 10px 12px; }
@@ -51,7 +48,6 @@
5148
</head>
5249
<body>
5350
<div class="sessview">
54-
<div class="sesstoolbar"><input id="svSearch" type="text" placeholder="Search sessions, files…" aria-label="Search sessions"></div>
5551
<button id="svNew" class="sessnew" type="button">New Session</button>
5652
<div class="sesslist" id="svList"></div>
5753
</div>
@@ -79,13 +75,6 @@
7975
if (days <= 7) { return 'This week'; }
8076
return 'Earlier';
8177
}
82-
function sessMatch(entry, q){
83-
if (!q) { return true; }
84-
const s = String(q).toLowerCase();
85-
if (String(entry.title || '').toLowerCase().indexOf(s) >= 0) { return true; }
86-
if (String(entry.preview || '').toLowerCase().indexOf(s) >= 0) { return true; }
87-
return (entry.filesEdited || []).some((f) => String(f).toLowerCase().indexOf(s) >= 0);
88-
}
8978
function sessModelShort(m){ const s = String(m || ''); const i = s.lastIndexOf('/'); return i >= 0 ? s.slice(i + 1) : s; }
9079
function sessSparkSvg(spark){
9180
const a = (Array.isArray(spark) ? spark : []).map((x) => Math.max(0, Number(x) || 0));
@@ -126,12 +115,12 @@
126115
{ id: 's2', title: 'Debug the flaky payment webhook test', createdAt: new Date(D - 5 * 3600e3).toISOString(), updatedAt: new Date(D - 5 * 3600e3).toISOString(), turns: 12, model: 'moonshotai/kimi-k2.7-code', state: 'interrupted', lifecycle: 'active', pinned: false, filesEdited: ['webhook_spec.rb'], spark: [2, 6, 3, 1] },
127116
{ id: 's3', title: 'Tidy the CHANGELOG for v1.0.4', createdAt: new Date(D - 2 * 86400e3).toISOString(), updatedAt: new Date(D - 2 * 86400e3).toISOString(), turns: 6, model: 'anthropic/claude-opus-5', state: 'done', lifecycle: 'active', pinned: false, filesEdited: ['RELEASE-NOTES.md'], spark: [1, 2, 1] }
128117
];
129-
let sessData = MOCK_SESSIONS.slice(), sessQuery = '';
118+
let sessData = MOCK_SESSIONS.slice();
130119
function renderSessions(){
131120
const list = document.getElementById('svList'); if (!list) { return; }
132-
const entries = (sessData || []).filter((e) => (e.lifecycle || 'active') === 'active' && sessMatch(e, sessQuery));
121+
const entries = (sessData || []).filter((e) => (e.lifecycle || 'active') === 'active');
133122
if (!entries.length){
134-
list.innerHTML = '<div class="sessempty">' + (sessQuery ? 'Nothing matches \u201c' + esc(sessQuery) + '\u201d.' : 'Your saved chats will appear here.') + '</div>';
123+
list.innerHTML = '<div class="sessempty">Your saved chats will appear here.</div>';
135124
return;
136125
}
137126
const now = Date.now(); let out = '';
@@ -141,7 +130,6 @@
141130
}
142131
list.innerHTML = out;
143132
}
144-
document.getElementById('svSearch').addEventListener('input', (e) => { sessQuery = e.target.value; renderSessions(); });
145133
document.getElementById('svNew').addEventListener('click', () => vscode.postMessage({ type: 'newSession' }));
146134
document.getElementById('svList').addEventListener('click', (e) => {
147135
const card = e.target.closest && e.target.closest('.sesscard'); if (!card) { return; }

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const path = require('path');
1616
const html = fs.readFileSync(path.join(__dirname, '..', 'media', 'chat.html'), 'utf8');
1717
const block = html.slice(html.indexOf('// [SESSIONS-PURE-START]'), html.indexOf('// [SESSIONS-PURE-END]'));
1818
// eslint-disable-next-line no-new-func
19-
const P = new Function(block + '; return { sessRelTime, sessBucket, sessMatch, sessModelShort, sessSparkSvg, sessGroup, sessCardHtml };')();
19+
const P = new Function(block + '; return { sessRelTime, sessBucket, sessModelShort, sessSparkSvg, sessGroup, sessCardHtml };')();
2020

2121
let n = 0;
2222
function test(name, fn) { fn(); n++; console.log(' ok - ' + name); }
@@ -45,15 +45,6 @@ test('BUCKET: today / yesterday / this week / earlier by local calendar day', ()
4545
assert.strictEqual(P.sessBucket(msAgo(20 * 86400), NOW), 'Earlier');
4646
});
4747

48-
test('MATCH: search hits title, files, and preview; empty query matches all', () => {
49-
const e = { title: 'Refund retries', filesEdited: ['refund.rb'], preview: 'add idempotency' };
50-
assert.ok(P.sessMatch(e, ''));
51-
assert.ok(P.sessMatch(e, 'refund'), 'title');
52-
assert.ok(P.sessMatch(e, 'refund.rb'), 'file — the lead over Cursor');
53-
assert.ok(P.sessMatch(e, 'idempotency'), 'preview');
54-
assert.ok(!P.sessMatch(e, 'zzz'));
55-
});
56-
5748
test('GROUP: pinned floats to its own bucket above the time buckets', () => {
5849
const g = P.sessGroup([
5950
{ id: 'p', pinned: true, updatedAt: msAgo(10 * 86400) },
@@ -162,6 +153,12 @@ test('VIEW: extension.js registers the provider and builds the HTML with a nonce
162153
assert.match(view, /nonce="__NONCE__"/);
163154
});
164155

156+
test('CAP: the surfaces list at most 30 sessions (no search → a bounded working set), pinned exempt', () => {
157+
assert.match(ext, /SESSIONS_SHOWN\s*=\s*30/, 'the displayed list is capped at 30');
158+
assert.match(ext, /function sessionList\(\)[\s\S]{0,400}pinned/, 'and a pinned session is never dropped for being old');
159+
assert.ok(!/id="sessSearch"|id="svSearch"/.test(html + view), 'neither surface still ships a search field');
160+
});
161+
165162
test('VIEW: theme-true across the three kinds and reduced-motion-safe', () => {
166163
assert.match(view, /:root\s*\{[^}]*--cc-accent:\s*#7d6bff/, 'One Dark defaults');
167164
assert.match(view, /body\.vscode-light\s*\{[^}]*--cc-accent:\s*#5b3fd6/, 'One Light');

0 commit comments

Comments
 (0)