Skip to content

Commit a4ad2b7

Browse files
ndemiancclaude
andcommitted
Editor chat: collapsible question/plan panels, plan redesign, cap upgrade card
ask_user and update_plan panels get a collapse/expand header (plan collapse state persists across re-renders). Plan redesign: professional .ci status icons (check / spinning sync / circle), a progress bar + count pill, refined rows. Free-tier cap (402 cap_reached) now renders a compelling upgrade card (plan-aware: paid users get a Manage-plan button) instead of raw error JSON. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a69e6e1 commit a4ad2b7

1 file changed

Lines changed: 96 additions & 19 deletions

File tree

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

Lines changed: 96 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,17 @@
111111
.msg.user .body { background: var(--field-bg); border: 1px solid var(--border); border-radius: 10px; padding: 9px 11px; }
112112
.msg.assistant .body { padding: 1px 2px; }
113113
.err { color: var(--vscode-errorForeground); }
114+
/* free-tier cap reached → a compelling upgrade CTA instead of raw 402 error text */
115+
.upgradecard { border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border)); border-radius: 12px; margin: 8px 2px; padding: 13px 15px 14px; background: color-mix(in srgb, var(--accent) 9%, var(--field-bg)); }
116+
.upgradecard .uchead { display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
117+
.upgradecard .uchead .ci { width: 15px; height: 15px; color: var(--accent); }
118+
.upgradecard .ucbody { font-size: 12.5px; line-height: 1.55; opacity: .92; margin-bottom: 12px; }
119+
.upgradecard .ucbtns { display: flex; gap: 8px; flex-wrap: wrap; }
120+
.upgradecard .ucbtn { cursor: pointer; border: none; border-radius: 7px; padding: 7px 16px; font-size: 12px; font-weight: 500; }
121+
.upgradecard .ucbtn.primary { background: var(--accent); color: var(--vscode-button-foreground, #fff); }
122+
.upgradecard .ucbtn.primary:hover { filter: brightness(1.1); }
123+
.upgradecard .ucbtn.ghost { background: transparent; border: 1px solid var(--border); color: var(--vscode-foreground); }
124+
.upgradecard .ucbtn.ghost:hover { background: var(--vscode-toolbar-hoverBackground, rgba(127,127,127,.14)); }
114125
pre { background: var(--vscode-editor-background, #282c34); border: 1px solid var(--border); padding: 9px 11px; border-radius: 8px; overflow-x: auto; margin: 8px 0; min-height: 1.3em; color: var(--vscode-editor-foreground, var(--vscode-foreground)); }
115126
pre code { background: none; padding: 0; }
116127
code { font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: .92em; }
@@ -230,7 +241,14 @@
230241

231242
/* ---- ask_user: clickable clarifying questions ---- */
232243
.questions { border: 1px solid var(--border); border-radius: 10px; margin: 6px 2px; padding: 10px 11px 11px; background: var(--field-bg); }
233-
.questions .qhead { font-size: 12px; font-weight: 600; margin-bottom: 8px; }
244+
.questions .qhead { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; margin-bottom: 8px; cursor: pointer; user-select: none; }
245+
.questions .qhead .qtitle { flex: 1 1 auto; min-width: 0; }
246+
.questions .qtoggle { flex: 0 0 auto; display: inline-flex; align-items: center; padding: 1px; border-radius: 4px; opacity: .7; }
247+
.questions .qhead:hover .qtoggle { opacity: 1; }
248+
.questions .qtoggle .ci { transition: transform .15s ease; }
249+
.questions.collapsed .qtoggle .ci { transform: rotate(-90deg); }
250+
.questions.collapsed .qhead { margin-bottom: 0; }
251+
.questions.collapsed .qblock, .questions.collapsed .qnotes, .questions.collapsed .qbtns { display: none; }
234252
.questions .qblock { margin: 8px 0 10px; }
235253
.questions .qtag { display: inline-block; font-size: 10px; text-transform: uppercase; letter-spacing: .04em; opacity: .65; margin-bottom: 3px; }
236254
.questions .qtext { font-size: 12.5px; margin-bottom: 6px; }
@@ -345,17 +363,32 @@
345363
background: var(--vscode-sideBar-background, var(--vscode-editor-background, #1e1e1e));
346364
border-top: 1px solid var(--border); box-shadow: 0 -8px 14px -8px rgba(0,0,0,.4); }
347365
#planBar .plancard { margin: 0; }
348-
.plancard { border: 1px solid var(--border); border-radius: 10px; margin: 6px 2px; overflow: hidden; background: var(--field-bg); }
349-
.plancard .phead { padding: 7px 11px; font-size: 12px; font-weight: 600; border-bottom: 1px solid var(--border); }
350-
.plancard .pcount { float: right; opacity: .65; font-weight: 400; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); }
351-
.plancard .pitems { padding: 4px 0 6px; }
352-
.plancard .pitem { display: flex; gap: 8px; align-items: baseline; padding: 3px 11px; font-size: 12px; line-height: 1.4; }
353-
.plancard .pitem .pic { width: 13px; flex: 0 0 auto; text-align: center; }
354-
.plancard .pitem.done .ptitle { opacity: .55; text-decoration: line-through; }
366+
.plancard { border: 1px solid var(--border); border-radius: 12px; margin: 6px 2px; overflow: hidden; background: var(--field-bg); }
367+
.plancard .phead { display: flex; align-items: center; gap: 8px; padding: 9px 12px 8px; font-size: 12px; font-weight: 600; letter-spacing: .01em; cursor: pointer; user-select: none; }
368+
.plancard .ptoggle { flex: 0 0 auto; display: inline-flex; color: var(--muted); opacity: .65; transition: transform .18s cubic-bezier(.4,0,.2,1); }
369+
.plancard .ptoggle .ci { width: 13px; height: 13px; }
370+
.plancard .phead:hover .ptoggle { opacity: 1; }
371+
.plancard.collapsed .ptoggle { transform: rotate(-90deg); }
372+
.plancard .phtitle { flex: 1 1 auto; min-width: 0; }
373+
.plancard .pcount { flex: 0 0 auto; opacity: .75; font-weight: 500; font-variant-numeric: tabular-nums; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11px; padding: 1px 8px; border-radius: 999px; background: rgba(127,127,127,.14); }
374+
.plancard.alldone .pcount { opacity: 1; color: var(--vscode-gitDecoration-addedResourceForeground, #4ec97a); background: color-mix(in srgb, var(--vscode-gitDecoration-addedResourceForeground, #4ec97a) 16%, transparent); }
375+
.plancard .pprog { height: 3px; margin: 0 12px 2px; border-radius: 3px; background: rgba(127,127,127,.16); overflow: hidden; }
376+
.plancard .pprog .pfill { height: 100%; border-radius: 3px; background: var(--accent); transition: width .4s cubic-bezier(.4,0,.2,1); }
377+
.plancard.alldone .pprog .pfill { background: var(--vscode-gitDecoration-addedResourceForeground, #4ec97a); }
378+
.plancard .pitems { padding: 6px 0 7px; }
379+
.plancard.collapsed .pprog, .plancard.collapsed .pitems { display: none; }
380+
.plancard .pitem { display: flex; gap: 9px; align-items: center; padding: 4px 12px; font-size: 12.5px; line-height: 1.45; position: relative; }
381+
.plancard .pitem .pic { flex: 0 0 auto; width: 15px; height: 15px; display: inline-flex; align-items: center; justify-content: center; }
382+
.plancard .pitem .pic .ci { width: 13px; height: 13px; }
355383
.plancard .pitem.done .pic { color: var(--vscode-gitDecoration-addedResourceForeground, #4ec97a); }
356-
.plancard .pitem.doing .pic { color: var(--accent); animation: agentpulse 1.3s ease-in-out infinite; }
384+
.plancard .pitem.done .ptitle { opacity: .5; text-decoration: line-through; text-decoration-thickness: 1px; }
385+
.plancard .pitem.doing { background: color-mix(in srgb, var(--accent) 11%, transparent); }
386+
.plancard .pitem.doing::before { content: ''; position: absolute; left: 0; top: 3px; bottom: 3px; width: 2px; border-radius: 2px; background: var(--accent); }
387+
.plancard .pitem.doing .pic { color: var(--accent); }
388+
.plancard .pitem.doing .pic .ci { animation: spin .9s linear infinite; }
357389
.plancard .pitem.doing .ptitle { font-weight: 600; }
358-
.plancard .pitem.todo .pic { opacity: .4; }
390+
.plancard .pitem.todo .pic { color: var(--muted); opacity: .5; }
391+
.plancard .pitem.todo .ptitle { opacity: .82; }
359392
@keyframes agentpulse { 0%,100% { opacity: .45; } 50% { opacity: 1; } }
360393
.diff { overflow: auto; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11.5px; line-height: 1.5; padding: 4px 0; }
361394
.diff .dl { white-space: pre-wrap; overflow-wrap: anywhere; padding: 0 10px 0 22px; text-indent: -16px; }
@@ -855,6 +888,8 @@
855888
'sparkle': { vb: '0 0 16 16', p: '<path d="M8 1 L9 7 L15 8 L9 9 L8 15 L7 9 L1 8 L7 7 Z"/>' },
856889
'trash': { vb: '0 0 16 16', p: '<path d="M10 3h3v1h-1v9l-1 1H4l-1-1V4H2V3h3V2a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1zM9 2H6v1h3V2zM4 13h7V4H4v9zm2-8H5v7h1V5zm1 0h1v7H7V5zm2 0h1v7H9V5z"/>' },
857890
'sync': { vb: '0 0 16 16', p: '<path d="M2.006 8.267 0.78 9.5 0 8.73l2.09-2.07.76.01 2.09 2.12-.76.76-1.167-1.18a5 5 0 0 0 9.4 1.983l.813.597A6 6 0 0 1 2.006 8.267Zm11.99-.518L15.22 6.51 16 7.29l-2.09 2.07-.76-.01-2.09-2.12.76-.76 1.167 1.18a5 5 0 0 0-9.4-1.983l-.813-.597A6 6 0 0 1 13.996 7.75Z"/>' },
891+
'chevron-down': { vb: '0 0 16 16', p: '<path d="M3.22 5.97a.75.75 0 0 1 1.06 0L8 9.69l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L3.22 7.03a.75.75 0 0 1 0-1.06z"/>' },
892+
'circle': { vb: '0 0 16 16', p: '<path fill-rule="evenodd" clip-rule="evenodd" d="M8 1C11.86 1 15 4.14 15 8C15 11.86 11.86 15 8 15C4.14 15 1 11.86 1 8C1 4.14 4.14 1 8 1ZM8 2C4.691 2 2 4.691 2 8C2 11.309 4.691 14 8 14C11.309 14 14 11.309 14 8C14 4.691 11.309 2 8 2Z"/>' },
858893
};
859894
function codicon(name, cls){ const i = IC[name]; if (!i) { return esc(name || ''); } return '<svg class="ci' + (cls ? ' ' + cls : '') + '" viewBox="' + i.vb + '" fill="currentColor" aria-hidden="true">' + i.p + '</svg>'; }
860895

@@ -1284,12 +1319,37 @@
12841319
}
12851320

12861321
// Blocking clarifying questions (ask_user) — clickable numbered options the agent waits on.
1322+
// A free-tier cap hit arrives as an error string like `<provider> API 402: {"error":{"code":"cap_reached",…}}`.
1323+
// Parse the payload so we can show an upgrade CTA instead of dumping raw red JSON.
1324+
function capReachedInfo(message){
1325+
const s = String(message || '');
1326+
if (!/cap_reached/.test(s)) { return null; }
1327+
const i = s.indexOf('{'); if (i < 0) { return null; }
1328+
try {
1329+
const err = (JSON.parse(s.slice(i)) || {}).error || {};
1330+
return err.code === 'cap_reached' ? { message: err.message || '', upgrade: !!err.upgrade_url } : null;
1331+
} catch (_) { return null; }
1332+
}
1333+
// Compelling cap-reached card (reuses the existing accountUpgrade action → opens /ai/pricing). Free
1334+
// tier → sell Pro; a paid user who hit their own cap → point at plan management, not "Upgrade to Pro".
1335+
function addUpgradeCard(info){
1336+
clearStatus(); finishAgentBubble();
1337+
const up = info.upgrade;
1338+
const card = document.createElement('div'); card.className = 'upgradecard';
1339+
card.innerHTML =
1340+
'<div class="uchead">' + codicon('sparkle') + '<span>' + (up ? 'Keep building with Pro' : 'You’ve hit your usage limit') + '</span></div>'
1341+
+ '<div class="ucbody">' + esc(info.message || 'Upgrade to keep building without interruptions.') + '</div>'
1342+
+ '<div class="ucbtns"><button class="ucbtn primary" data-act="upgrade">' + (up ? 'Upgrade to Pro' : 'Manage plan') + '</button></div>';
1343+
log.appendChild(card); scrollIfStuck();
1344+
card.querySelectorAll('[data-act]').forEach((b) => { b.onclick = () => vscode.postMessage({ type: 'accountUpgrade' }); });
1345+
}
12871346
function addQuestions(m){
12881347
clearEmpty(); clearStatus(); agentBubble = null;
12891348
const qs = m.questions || [];
12901349
const state = qs.map((q) => ({ multi: !!q.multiSelect, sel: new Set() }));
12911350
const card = document.createElement('div'); card.className = 'questions';
1292-
let h = '<div class="qhead">' + codicon('question') + ' A few quick decisions — click to choose</div>';
1351+
let h = '<div class="qhead"><span class="qtoggle" title="Collapse / expand">' + codicon('chevron-down') + '</span>'
1352+
+ '<span class="qtitle">' + codicon('question') + ' A few quick decisions — click to choose</span></div>';
12931353
qs.forEach((q, qi) => {
12941354
h += '<div class="qblock">';
12951355
if (q.header){ h += '<div class="qtag">' + esc(q.header) + (q.multiSelect ? ' · pick any' : '') + '</div>'; }
@@ -1308,6 +1368,10 @@
13081368
card.innerHTML = h;
13091369
log.appendChild(card); scrollIfStuck();
13101370

1371+
// Collapse/expand the whole panel by clicking its header (chevron rotates).
1372+
const qhead = card.querySelector('.qhead');
1373+
if (qhead) { qhead.onclick = () => card.classList.toggle('collapsed'); }
1374+
13111375
card.querySelectorAll('.qopt').forEach((btn) => {
13121376
btn.onclick = () => {
13131377
const qi = +btn.dataset.q, oi = +btn.dataset.o, st = state[qi];
@@ -1510,16 +1574,24 @@
15101574
renderBgTasks();
15111575
}
15121576
const planBar = document.getElementById('planBar');
1577+
let planCollapsed = false; // persists across the agent's repeated update_plan re-renders
15131578
function renderPlan(todos){
15141579
if (!todos || !todos.length){ planBar.style.display = 'none'; planBar.innerHTML = ''; return; }
15151580
const total = todos.length, done = todos.filter(t => t && t.status === 'done').length;
1516-
let h = '<div class="phead">Plan <span class="pcount">' + done + '/' + total + '</span></div><div class="pitems">';
1581+
const pct = Math.round(done / total * 100), allDone = done === total;
1582+
let items = '';
15171583
for (const t of todos){
15181584
const st = t.status === 'done' ? 'done' : t.status === 'in_progress' ? 'doing' : 'todo';
1519-
const ic = st === 'done' ? '✓' : st === 'doing' ? '◉' : '○';
1520-
h += '<div class="pitem ' + st + '"><span class="pic">' + ic + '</span><span class="ptitle">' + esc(t.title) + '</span></div>';
1585+
const ic = st === 'done' ? codicon('check') : st === 'doing' ? codicon('sync') : codicon('circle');
1586+
items += '<div class="pitem ' + st + '"><span class="pic">' + ic + '</span><span class="ptitle">' + esc(t.title) + '</span></div>';
15211587
}
1522-
planBar.innerHTML = '<div class="plancard">' + h + '</div></div>';
1588+
const head = '<div class="phead"><span class="ptoggle">' + codicon('chevron-down') + '</span>'
1589+
+ '<span class="phtitle">Plan</span><span class="pcount">' + done + '/' + total + '</span></div>'
1590+
+ '<div class="pprog"><div class="pfill" style="width:' + pct + '%"></div></div>';
1591+
planBar.innerHTML = '<div class="plancard' + (allDone ? ' alldone' : '') + (planCollapsed ? ' collapsed' : '') + '">'
1592+
+ head + '<div class="pitems">' + items + '</div></div>';
1593+
const phead = planBar.querySelector('.phead');
1594+
if (phead){ phead.onclick = () => { planCollapsed = !planCollapsed; const c = planBar.querySelector('.plancard'); if (c){ c.classList.toggle('collapsed', planCollapsed); } }; }
15231595
planBar.style.display = 'block';
15241596
scrollIfStuck(); // log shrank as the sticky plan appeared — keep pinned if we were at bottom
15251597
}
@@ -1904,9 +1976,14 @@
19041976
else if (m.type === 'assistantDone'){ doneSignaled = true; ensurePump(); }
19051977
else if (m.type === 'assistantError'){
19061978
pending = ''; flushAll = false; doneSignaled = false;
1907-
const html = '<span class="err">' + esc(m.message) + '</span>';
1908-
if (current){ streamSetHtml(current, html); } else { add('assistant', html); }
1909-
current = null; setStreaming(false);
1979+
const cap = capReachedInfo(m.message);
1980+
if (cap){ current = null; addUpgradeCard(cap); }
1981+
else {
1982+
const html = '<span class="err">' + esc(m.message) + '</span>';
1983+
if (current){ streamSetHtml(current, html); } else { add('assistant', html); }
1984+
current = null;
1985+
}
1986+
setStreaming(false);
19101987
}
19111988
else if (m.type === 'activeFile'){ activeFileLabel = m.label; renderChips(); }
19121989
else if (m.type === 'contextFiles'){ ctxFiles = m.files || []; renderChips(); }
@@ -1939,7 +2016,7 @@
19392016
else if (m.type === 'debug'){ addDebug(m); }
19402017
else if (m.type === 'account'){ renderAccount(m); if (m.open) openAccount(); }
19412018
else if (m.type === 'fileIndex'){ setFileIndex(m.files || []); }
1942-
else if (m.type === 'agentError'){ clearStatus(); finishAgentBubble(); add('assistant', '<span class="err">' + esc(m.message) + '</span>'); }
2019+
else if (m.type === 'agentError'){ clearStatus(); finishAgentBubble(); const cap = capReachedInfo(m.message); if (cap){ addUpgradeCard(cap); } else { add('assistant', '<span class="err">' + esc(m.message) + '</span>'); } }
19432020
else if (m.type === 'agentDone'){ clearStatus(); finishAgentBubble(); addAgentDone(m.reason, m.edits, m.credits, m.maxSteps); setStreaming(false); }
19442021
else if (m.type === 'context'){ selLabel = m.label; renderChips(); }
19452022
else if (m.type === 'clearContext'){ selLabel = null; renderChips(); }

0 commit comments

Comments
 (0)