|
|
|
|
@ -18,6 +18,26 @@ class ClaudeRenderer {
|
|
|
|
|
return s.replace(/\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|.)/g, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_addCopyButton(container, text) {
|
|
|
|
|
container.classList.add('cc-copy-wrap');
|
|
|
|
|
const btn = document.createElement('button');
|
|
|
|
|
btn.className = 'cc-copy-btn';
|
|
|
|
|
btn.innerHTML = '⎘';
|
|
|
|
|
btn.title = 'Copy';
|
|
|
|
|
btn.addEventListener('click', (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
|
|
|
btn.classList.add('copied');
|
|
|
|
|
btn.textContent = '\u2713';
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
btn.classList.remove('copied');
|
|
|
|
|
btn.innerHTML = '⎘';
|
|
|
|
|
}, 1500);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
container.appendChild(btn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render(doc) {
|
|
|
|
|
const sig = this._signature(doc.blocks);
|
|
|
|
|
if (sig !== this._lastSignature) {
|
|
|
|
|
@ -97,6 +117,7 @@ class ClaudeRenderer {
|
|
|
|
|
case 'table': return this._renderTable(block);
|
|
|
|
|
case 'diff': return this._renderDiff(block);
|
|
|
|
|
case 'selection': return this._renderSelection(block);
|
|
|
|
|
case 'thinking': return this._renderThinking(block);
|
|
|
|
|
case 'text': return this._renderText(block);
|
|
|
|
|
case 'empty': return this._renderEmpty();
|
|
|
|
|
default: return null;
|
|
|
|
|
@ -200,6 +221,19 @@ class ClaudeRenderer {
|
|
|
|
|
body.appendChild(line);
|
|
|
|
|
}
|
|
|
|
|
div.appendChild(body);
|
|
|
|
|
|
|
|
|
|
const bodyLineCount = block.lines.length - 1;
|
|
|
|
|
if (bodyLineCount > 5) {
|
|
|
|
|
div.classList.add('cc-collapsible');
|
|
|
|
|
const toggle = document.createElement('button');
|
|
|
|
|
toggle.className = 'cc-collapse-toggle';
|
|
|
|
|
toggle.textContent = `Show all (${bodyLineCount} lines)`;
|
|
|
|
|
toggle.addEventListener('click', () => {
|
|
|
|
|
const expanded = div.classList.toggle('cc-expanded');
|
|
|
|
|
toggle.textContent = expanded ? 'Show less' : `Show all (${bodyLineCount} lines)`;
|
|
|
|
|
});
|
|
|
|
|
div.appendChild(toggle);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return div;
|
|
|
|
|
@ -230,6 +264,21 @@ class ClaudeRenderer {
|
|
|
|
|
pre.innerHTML = contentLines.map(l => ansiToHtml(l)).join('\n');
|
|
|
|
|
scrollable.appendChild(pre);
|
|
|
|
|
div.appendChild(scrollable);
|
|
|
|
|
|
|
|
|
|
// Collapsible for long results
|
|
|
|
|
if (contentLines.length > 8) {
|
|
|
|
|
div.classList.add('cc-collapsible');
|
|
|
|
|
const toggle = document.createElement('button');
|
|
|
|
|
toggle.className = 'cc-collapse-toggle';
|
|
|
|
|
toggle.textContent = `Show all (${contentLines.length} lines)`;
|
|
|
|
|
toggle.addEventListener('click', () => {
|
|
|
|
|
const expanded = div.classList.toggle('cc-expanded');
|
|
|
|
|
toggle.textContent = expanded ? 'Show less' : `Show all (${contentLines.length} lines)`;
|
|
|
|
|
});
|
|
|
|
|
div.appendChild(toggle);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._addCopyButton(div, contentLines.map(l => this._stripAnsi(l)).join('\n'));
|
|
|
|
|
return div;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -246,7 +295,9 @@ class ClaudeRenderer {
|
|
|
|
|
for (const line of block.lines) {
|
|
|
|
|
const lineEl = document.createElement('div');
|
|
|
|
|
lineEl.className = 'cc-line';
|
|
|
|
|
lineEl.innerHTML = this._renderLine(line);
|
|
|
|
|
// Strip ⎿ prefix marker, preserve ANSI colors
|
|
|
|
|
const content = line.replace(/^(\x1b\[[0-9;]*m)*\s*\u23BF\s?/, '');
|
|
|
|
|
lineEl.innerHTML = ansiToHtml(content);
|
|
|
|
|
div.appendChild(lineEl);
|
|
|
|
|
}
|
|
|
|
|
return div;
|
|
|
|
|
@ -258,7 +309,7 @@ class ClaudeRenderer {
|
|
|
|
|
for (const line of block.lines) {
|
|
|
|
|
const lineEl = document.createElement('div');
|
|
|
|
|
lineEl.className = 'cc-line';
|
|
|
|
|
lineEl.innerHTML = this._renderLine(line);
|
|
|
|
|
lineEl.innerHTML = ansiToHtml(line);
|
|
|
|
|
div.appendChild(lineEl);
|
|
|
|
|
}
|
|
|
|
|
return div;
|
|
|
|
|
@ -277,6 +328,7 @@ class ClaudeRenderer {
|
|
|
|
|
|
|
|
|
|
scrollable.appendChild(pre);
|
|
|
|
|
div.appendChild(scrollable);
|
|
|
|
|
this._addCopyButton(div, block.lines.map(l => this._stripAnsi(l)).join('\n'));
|
|
|
|
|
return div;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -367,6 +419,7 @@ class ClaudeRenderer {
|
|
|
|
|
|
|
|
|
|
scrollable.appendChild(content);
|
|
|
|
|
div.appendChild(scrollable);
|
|
|
|
|
this._addCopyButton(div, block.lines.map(l => this._stripAnsi(l)).join('\n'));
|
|
|
|
|
return div;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -377,7 +430,7 @@ class ClaudeRenderer {
|
|
|
|
|
if (block.options && block.options.length > 0) {
|
|
|
|
|
for (const opt of block.options) {
|
|
|
|
|
const btn = document.createElement('button');
|
|
|
|
|
btn.className = 'cc-option';
|
|
|
|
|
btn.className = 'cc-option' + (opt.focused ? ' cc-option-focused' : '');
|
|
|
|
|
btn.dataset.key = opt.key;
|
|
|
|
|
|
|
|
|
|
const indexEl = document.createElement('span');
|
|
|
|
|
@ -388,8 +441,13 @@ class ClaudeRenderer {
|
|
|
|
|
labelEl.className = 'cc-option-label';
|
|
|
|
|
labelEl.textContent = opt.label;
|
|
|
|
|
|
|
|
|
|
const hint = document.createElement('span');
|
|
|
|
|
hint.className = 'cc-option-hint';
|
|
|
|
|
hint.textContent = `⌨ ${opt.key}`;
|
|
|
|
|
|
|
|
|
|
btn.appendChild(indexEl);
|
|
|
|
|
btn.appendChild(labelEl);
|
|
|
|
|
btn.appendChild(hint);
|
|
|
|
|
btn.addEventListener('click', () => {
|
|
|
|
|
if (this._onAction) this._onAction('select', opt.key);
|
|
|
|
|
});
|
|
|
|
|
@ -420,15 +478,23 @@ class ClaudeRenderer {
|
|
|
|
|
this.statusBarEl.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
if (statusBar.mode) {
|
|
|
|
|
const pill = document.createElement('button');
|
|
|
|
|
pill.className = `cc-mode-pill cc-mode-${statusBar.mode}`;
|
|
|
|
|
const icons = { plan: '⏸', code: '●', bypass: '⏵' };
|
|
|
|
|
const labels = { plan: 'Plan', code: 'Code', bypass: 'Bypass' };
|
|
|
|
|
pill.textContent = `${icons[statusBar.mode] || ''} ${labels[statusBar.mode] || statusBar.mode}`;
|
|
|
|
|
const modes = ['plan', 'code', 'bypass'];
|
|
|
|
|
|
|
|
|
|
const modeGroup = document.createElement('div');
|
|
|
|
|
modeGroup.className = 'cc-mode-group';
|
|
|
|
|
for (const mode of modes) {
|
|
|
|
|
const pill = document.createElement('button');
|
|
|
|
|
pill.className = `cc-mode-pill cc-mode-${mode}`;
|
|
|
|
|
if (mode === statusBar.mode) pill.classList.add('cc-mode-active');
|
|
|
|
|
pill.textContent = `${icons[mode]} ${labels[mode]}`;
|
|
|
|
|
pill.addEventListener('click', () => {
|
|
|
|
|
if (this._onAction) this._onAction('mode-toggle', statusBar.mode);
|
|
|
|
|
if (this._onAction) this._onAction('mode-toggle', mode);
|
|
|
|
|
});
|
|
|
|
|
this.statusBarEl.appendChild(pill);
|
|
|
|
|
modeGroup.appendChild(pill);
|
|
|
|
|
}
|
|
|
|
|
this.statusBarEl.appendChild(modeGroup);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (statusBar.branch) {
|
|
|
|
|
@ -439,6 +505,19 @@ class ClaudeRenderer {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (statusBar.stats) {
|
|
|
|
|
const pctMatch = statusBar.stats.match(/(\d+)%/);
|
|
|
|
|
if (pctMatch) {
|
|
|
|
|
const pct = parseInt(pctMatch[1], 10);
|
|
|
|
|
const bar = document.createElement('div');
|
|
|
|
|
bar.className = 'cc-context-bar';
|
|
|
|
|
const fill = document.createElement('div');
|
|
|
|
|
fill.className = 'cc-context-fill';
|
|
|
|
|
fill.style.width = `${pct}%`;
|
|
|
|
|
if (pct > 80) fill.classList.add('cc-ctx-high');
|
|
|
|
|
else if (pct > 50) fill.classList.add('cc-ctx-mid');
|
|
|
|
|
bar.appendChild(fill);
|
|
|
|
|
this.statusBarEl.appendChild(bar);
|
|
|
|
|
}
|
|
|
|
|
const stats = document.createElement('span');
|
|
|
|
|
stats.className = 'cc-status-stats';
|
|
|
|
|
stats.textContent = statusBar.stats;
|
|
|
|
|
|