Skip to content

Commit b5b00e6

Browse files
ndemiancclaude
andcommitted
feat(levelcode): memory — Facts panel controls (confirm / not-true)
Completes the inferred→confirmed loop with a surface: the Memory tab now opens with a Facts section above the sessions. Each fact shows its state — dimmed "inferred" or accent "confirmed" — and how many sessions it was seen in; hover gives Confirm (promote an inferred fact to load-bearing) and Not true (remove a wrong one). Both immediately re-consolidate the injected digest, so confirming or correcting a fact changes what the agent is told on the very next session. - extension.js: listMemory now also serves factsList(); a factAction message is routed through handleFactAction (confirm / remove by the fact's key), which refreshes the panel + the welcome-back digest. - media/sessionsView.html: factCardHtml (text + inferred/confirmed badge + "seen N×", with the line-2 hover-swap to Confirm / Not true); renderMemory now lays out Facts then Sessions; the list click handler routes data-fact buttons to factAction. Inferred cards are dimmer (low-trust); Not true tints danger, Confirm tints accent. - tests: sessionsUi panel test now covers the facts controls. 31 files green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1ca73a1 commit b5b00e6

3 files changed

Lines changed: 56 additions & 8 deletions

File tree

‎extensions/levelcode-ai/extension.js‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -838,11 +838,24 @@ async function handleMemoryAction(action, id, webview) {
838838
if (text == null || !text.trim()) { return; }
839839
m.refineSummary(id, text);
840840
} else { return; }
841-
if (webview) { try { webview.postMessage({ type: 'memoryList', items: m.memoryItems() }); } catch (e) { /* view closed */ } }
841+
if (webview) { try { webview.postMessage({ type: 'memoryList', items: m.memoryItems(), facts: m.factsList() }); } catch (e) { /* view closed */ } }
842842
postMemoryDigest();
843843
} catch (e) { dbg('sessions.memoryAction.error', { action, id, msg: String((e && e.message) || e) }); }
844844
}
845845

846+
// A fact-panel action (§M3/§6): Confirm promotes an inferred fact to load-bearing; Not-true removes it. Keyed
847+
// by the fact's normalized text (not a session). Refreshes the panel + the injected digest.
848+
async function handleFactAction(action, key, webview) {
849+
const m = sessionsManager();
850+
if (!m || !key) { return; }
851+
dbg('sessions.factAction', { action, key });
852+
try {
853+
m.factAction(key, action === 'remove' ? 'remove' : 'confirm');
854+
if (webview) { try { webview.postMessage({ type: 'memoryList', items: m.memoryItems(), facts: m.factsList() }); } catch (e) { /* view closed */ } }
855+
postMemoryDigest();
856+
} catch (e) { dbg('sessions.factAction.error', { action, key, msg: String((e && e.message) || e) }); }
857+
}
858+
846859
// A card action from either surface. resume reopens the session; done/delete/rename/pin are append-only edits
847860
// (§4.9): Done archives (reversible, never deletes), Delete soft-trashes, Rename retitles, Pin toggles.
848861
// Done/Delete offer an Undo (restore) so an accidental click is recoverable. Best-effort — a History action
@@ -2071,9 +2084,10 @@ class SessionsViewProvider {
20712084
case 'listSessions': view.webview.postMessage({ type: 'sessions', entries: sessionList() }); break;
20722085
case 'newSession': newChat(); vscode.commands.executeCommand('levelcodeAi.chat.focus'); break;
20732086
case 'sessionAction': await handleSessionAction(msg.action, msg.id); break;
2074-
// Memory tab (§M3): list the recorded outcomes; act on them (resume / edit / forget); open the file.
2075-
case 'listMemory': { const mm = sessionsManager(); view.webview.postMessage({ type: 'memoryList', items: mm ? mm.memoryItems() : [] }); break; }
2087+
// Memory tab (§M3): list the recorded outcomes + facts; act on them; open the file.
2088+
case 'listMemory': { const mm = sessionsManager(); view.webview.postMessage({ type: 'memoryList', items: mm ? mm.memoryItems() : [], facts: mm ? mm.factsList() : [] }); break; }
20762089
case 'memoryAction': await handleMemoryAction(msg.action, msg.id, view.webview); break;
2090+
case 'factAction': await handleFactAction(msg.action, msg.key, view.webview); break;
20772091
case 'openMemory': await openMemory(); break;
20782092
}
20792093
});

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

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@
4646
.memcard .memact { background: transparent; border: none; color: var(--cc-text2); border-radius: 4px; padding: 2px 8px; font: inherit; font-size: 11px; cursor: pointer; white-space: nowrap; }
4747
.memcard .memact:hover { background: color-mix(in srgb, var(--cc-text3) 20%, transparent); color: var(--cc-text); }
4848
.memcard .memact[data-act="forget"]:hover { background: color-mix(in srgb, var(--cc-danger) 18%, transparent); color: var(--cc-danger); }
49+
/* Facts section (§M3/§6): durable truths, marked inferred vs confirmed, with confirm / not-true. */
50+
.memsection { font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--cc-text3); margin: 12px 4px 6px; }
51+
.factcard { position: relative; padding: 8px 8px 9px; margin-bottom: 1px; border-radius: 6px; }
52+
.factcard:hover { background: color-mix(in srgb, var(--cc-text3) 10%, transparent); }
53+
.factcard.inferred { opacity: .82; }
54+
.factcard .facttext { font-size: 13px; line-height: 1.5; color: var(--cc-text); }
55+
.factbadge { font-size: 9.5px; letter-spacing: .05em; text-transform: uppercase; color: var(--cc-text3); border: 1px solid var(--cc-line); border-radius: 4px; padding: 0 4px; margin-left: 2px; white-space: nowrap; }
56+
.factbadge.ok { color: var(--cc-accent); border-color: color-mix(in srgb, var(--cc-accent) 45%, var(--cc-line)); }
57+
.factcard .memline2 { display: flex; align-items: center; min-height: 22px; margin-top: 3px; }
58+
.factcard .memmeta { flex: 1 1 auto; font-size: 11px; color: var(--cc-text3); }
59+
.factcard .memacts { display: none; align-items: center; gap: 4px; flex: 0 0 auto; }
60+
.factcard:hover .memmeta, .factcard:focus-within .memmeta { display: none; }
61+
.factcard:hover .memacts, .factcard:focus-within .memacts { display: flex; }
62+
.factcard .memact[data-fact="remove"]:hover { background: color-mix(in srgb, var(--cc-danger) 18%, transparent); color: var(--cc-danger); }
63+
.factcard .memact[data-fact="confirm"]:hover { background: color-mix(in srgb, var(--cc-accent) 18%, transparent); color: var(--cc-accent); }
4964
.sesslist { flex: 1 1 auto; overflow-y: auto; padding: 0 10px 12px; }
5065
.sessbucket { display: flex; justify-content: space-between; align-items: baseline; 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; }
5166
.sessbucket .cnt { letter-spacing: 0; }
@@ -198,13 +213,28 @@
198213
+ '<button type="button" class="memact" data-act="forget" title="Forget this session">Forget</button></span></div>'
199214
+ '</div>';
200215
}
201-
function renderMemory(items){
216+
function factCardHtml(f){
217+
const badge = f.confirmed ? '<span class="factbadge ok">confirmed</span>' : '<span class="factbadge">inferred</span>';
218+
const acts = '<span class="memacts">'
219+
+ (f.confirmed ? '' : '<button type="button" class="memact" data-fact="confirm" title="Confirm — this is true">Confirm</button>')
220+
+ '<button type="button" class="memact" data-fact="remove" title="Not true — remove this fact">Not true</button>'
221+
+ '</span>';
222+
return '<div class="factcard ' + (f.confirmed ? 'confirmed' : 'inferred') + '" data-key="' + escAttr(f.key || '') + '">'
223+
+ '<div class="facttext">' + esc(f.text || '') + ' ' + badge + '</div>'
224+
+ '<div class="memline2"><span class="memmeta">seen ' + (f.count || 0) + '×</span>' + acts + '</div></div>';
225+
}
226+
function renderMemory(msg){
202227
const list = document.getElementById('svMemList'); if (!list) { return; }
203-
const arr = Array.isArray(items) ? items : [];
204-
if (!arr.length){ list.innerHTML = '<div class="sessempty">Nothing remembered yet. As sessions finish, what they achieved appears here — plain text you can edit or forget.</div>'; return; }
205-
list.innerHTML = arr.map(memCardHtml).join('');
228+
const items = Array.isArray(msg && msg.items) ? msg.items : [];
229+
const facts = Array.isArray(msg && msg.facts) ? msg.facts : [];
230+
let html = '';
231+
if (facts.length){ html += '<div class="memsection">Facts</div>' + facts.map(factCardHtml).join(''); }
232+
if (items.length){ html += '<div class="memsection">Sessions</div>' + items.map(memCardHtml).join(''); }
233+
list.innerHTML = html || '<div class="sessempty">Nothing remembered yet. As sessions finish, what they achieved appears here — plain text you can edit or forget.</div>';
206234
}
207235
document.getElementById('svMemList').addEventListener('click', (e) => {
236+
const fbtn = e.target.closest && e.target.closest('[data-fact]');
237+
if (fbtn){ const fc = fbtn.closest('.factcard'); vscode.postMessage({ type: 'factAction', action: fbtn.getAttribute('data-fact'), key: fc ? fc.getAttribute('data-key') : '' }); return; }
208238
const card = e.target.closest && e.target.closest('.memcard'); if (!card) { return; }
209239
const id = card.getAttribute('data-id'); const btn = e.target.closest('[data-act]');
210240
vscode.postMessage({ type: 'memoryAction', action: btn ? btn.getAttribute('data-act') : 'resume', id: id });
@@ -232,7 +262,7 @@
232262
vscode.postMessage({ type: 'sessionAction', action: 'restore', id: b.getAttribute('data-id') });
233263
document.getElementById('svUndo').hidden = true; clearTimeout(sessUndoTimer);
234264
});
235-
window.addEventListener('message', (ev) => { const m = ev.data; if (m && m.type === 'sessions'){ sessData = Array.isArray(m.entries) ? m.entries : []; renderSessions(); } else if (m && m.type === 'memoryList'){ renderMemory(m.items); } else if (m && m.type === 'sessionUndo'){ showUndo(m); } });
265+
window.addEventListener('message', (ev) => { const m = ev.data; if (m && m.type === 'sessions'){ sessData = Array.isArray(m.entries) ? m.entries : []; renderSessions(); } else if (m && m.type === 'memoryList'){ renderMemory(m); } else if (m && m.type === 'sessionUndo'){ showUndo(m); } });
236266
renderSessions();
237267
vscode.postMessage({ type: 'listSessions' });
238268
</script>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ test('MEMORY (panel M3): a Memory tab in the sidebar to view / edit / forget the
196196
assert.match(ext, /case 'listMemory'/, 'the provider serves the memory list');
197197
assert.match(ext, /async function handleMemoryAction/, 'and handles the actions');
198198
assert.match(ext, /m\.forget\(id\)/, 'forget drops the contribution (session stays in History)');
199+
// Facts controls (§6): inferred vs confirmed, with confirm / not-true
200+
assert.match(view, /function factCardHtml/, 'facts render with an inferred/confirmed badge');
201+
assert.match(view, /data-fact="confirm"|type: 'factAction'/, 'and a confirm / not-true control');
202+
assert.match(ext, /async function handleFactAction/, 'the host confirms / removes a fact by key');
199203
});
200204

201205
test('MEMORY (facts): the summarizer also extracts durable facts, recorded + injected (inferred until confirmed)', () => {

0 commit comments

Comments
 (0)