|
111 | 111 | .msg.user .body { background: var(--field-bg); border: 1px solid var(--border); border-radius: 10px; padding: 9px 11px; } |
112 | 112 | .msg.assistant .body { padding: 1px 2px; } |
113 | 113 | .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)); } |
114 | 125 | 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)); } |
115 | 126 | pre code { background: none; padding: 0; } |
116 | 127 | code { font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: .92em; } |
|
230 | 241 |
|
231 | 242 | /* ---- ask_user: clickable clarifying questions ---- */ |
232 | 243 | .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; } |
234 | 252 | .questions .qblock { margin: 8px 0 10px; } |
235 | 253 | .questions .qtag { display: inline-block; font-size: 10px; text-transform: uppercase; letter-spacing: .04em; opacity: .65; margin-bottom: 3px; } |
236 | 254 | .questions .qtext { font-size: 12.5px; margin-bottom: 6px; } |
|
345 | 363 | background: var(--vscode-sideBar-background, var(--vscode-editor-background, #1e1e1e)); |
346 | 364 | border-top: 1px solid var(--border); box-shadow: 0 -8px 14px -8px rgba(0,0,0,.4); } |
347 | 365 | #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; } |
355 | 383 | .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; } |
357 | 389 | .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; } |
359 | 392 | @keyframes agentpulse { 0%,100% { opacity: .45; } 50% { opacity: 1; } } |
360 | 393 | .diff { overflow: auto; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11.5px; line-height: 1.5; padding: 4px 0; } |
361 | 394 | .diff .dl { white-space: pre-wrap; overflow-wrap: anywhere; padding: 0 10px 0 22px; text-indent: -16px; } |
|
855 | 888 | '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"/>' }, |
856 | 889 | '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"/>' }, |
857 | 890 | '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"/>' }, |
858 | 893 | }; |
859 | 894 | 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>'; } |
860 | 895 |
|
|
1284 | 1319 | } |
1285 | 1320 |
|
1286 | 1321 | // 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 | + } |
1287 | 1346 | function addQuestions(m){ |
1288 | 1347 | clearEmpty(); clearStatus(); agentBubble = null; |
1289 | 1348 | const qs = m.questions || []; |
1290 | 1349 | const state = qs.map((q) => ({ multi: !!q.multiSelect, sel: new Set() })); |
1291 | 1350 | 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>'; |
1293 | 1353 | qs.forEach((q, qi) => { |
1294 | 1354 | h += '<div class="qblock">'; |
1295 | 1355 | if (q.header){ h += '<div class="qtag">' + esc(q.header) + (q.multiSelect ? ' · pick any' : '') + '</div>'; } |
|
1308 | 1368 | card.innerHTML = h; |
1309 | 1369 | log.appendChild(card); scrollIfStuck(); |
1310 | 1370 |
|
| 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 | + |
1311 | 1375 | card.querySelectorAll('.qopt').forEach((btn) => { |
1312 | 1376 | btn.onclick = () => { |
1313 | 1377 | const qi = +btn.dataset.q, oi = +btn.dataset.o, st = state[qi]; |
|
1510 | 1574 | renderBgTasks(); |
1511 | 1575 | } |
1512 | 1576 | const planBar = document.getElementById('planBar'); |
| 1577 | + let planCollapsed = false; // persists across the agent's repeated update_plan re-renders |
1513 | 1578 | function renderPlan(todos){ |
1514 | 1579 | if (!todos || !todos.length){ planBar.style.display = 'none'; planBar.innerHTML = ''; return; } |
1515 | 1580 | 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 = ''; |
1517 | 1583 | for (const t of todos){ |
1518 | 1584 | 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>'; |
1521 | 1587 | } |
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); } }; } |
1523 | 1595 | planBar.style.display = 'block'; |
1524 | 1596 | scrollIfStuck(); // log shrank as the sticky plan appeared — keep pinned if we were at bottom |
1525 | 1597 | } |
|
1904 | 1976 | else if (m.type === 'assistantDone'){ doneSignaled = true; ensurePump(); } |
1905 | 1977 | else if (m.type === 'assistantError'){ |
1906 | 1978 | 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); |
1910 | 1987 | } |
1911 | 1988 | else if (m.type === 'activeFile'){ activeFileLabel = m.label; renderChips(); } |
1912 | 1989 | else if (m.type === 'contextFiles'){ ctxFiles = m.files || []; renderChips(); } |
|
1939 | 2016 | else if (m.type === 'debug'){ addDebug(m); } |
1940 | 2017 | else if (m.type === 'account'){ renderAccount(m); if (m.open) openAccount(); } |
1941 | 2018 | 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>'); } } |
1943 | 2020 | else if (m.type === 'agentDone'){ clearStatus(); finishAgentBubble(); addAgentDone(m.reason, m.edits, m.credits, m.maxSteps); setStreaming(false); } |
1944 | 2021 | else if (m.type === 'context'){ selLabel = m.label; renderChips(); } |
1945 | 2022 | else if (m.type === 'clearContext'){ selLabel = null; renderChips(); } |
|
0 commit comments