feat(web): response-viewer transcript fallback + code-block rendering (#102)

* feat(web): response-viewer transcript fallback + code-block rendering

- Add _cleanTerminalBuffer(): strip ANSI escapes and Claude CLI chrome
  (status bar, spinner, progress bar, prompt glyphs) from the terminal
  buffer so the response viewer renders clean text when the JSONL
  transcript is missing.
- Add _preprocessAsciiArt(): wrap box-drawing/block-element diagrams in
  fenced code blocks (narrow trigger that excludes arrows/geometric
  shapes common in prose) so marked.js preserves their whitespace.
- Extend .rv-text rules to .response-viewer-body so fallback-rendered
  content gets the same typography, code-block, and table styling.

* refactor(web): drop duplicate _cleanTerminalBuffer/_preprocessAsciiArt

These two methods already exist on master (added in #75). This branch
re-added byte-identical copies above _sanitizeHtml; in a JS class body the
later definition wins, so the duplicates were inert dead code. Remove them,
keeping only the genuinely new work: the _renderMarkdown null-safety fix and
the response-viewer CSS overhaul.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Teigen <teigen@TeigendeMac-mini.local>
Co-authored-by: arkon <arkon.85@hotmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tenggan Zhang
2026-06-01 19:43:52 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 Teigen arkon
parent ec38fd11bf
commit 68594ac395
2 changed files with 125 additions and 61 deletions
+3 -2
View File
@@ -1110,9 +1110,10 @@ class CodemanApp {
/** Render markdown to sanitized HTML, falling back to plain text if marked.js unavailable */
_renderMarkdown(text) {
const src = text || '';
if (typeof marked !== 'undefined' && marked.parse) {
try {
const prepared = this._preprocessAsciiArt(text);
const prepared = this._preprocessAsciiArt(src);
let html = this._sanitizeHtml(marked.parse(prepared, { breaks: true, gfm: true }));
// Wrap tables in a horizontal-scroll container so they overflow gracefully
// on mobile without collapsing into block-level cells.
@@ -1168,7 +1169,7 @@ class CodemanApp {
} catch { /* fall through */ }
}
// Fallback: escape HTML and preserve whitespace
const escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const escaped = src.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return `<pre style="white-space:pre-wrap;word-break:break-word">${escaped}</pre>`;
}
+122 -59
View File
@@ -7971,14 +7971,15 @@ kbd {
bottom: 0;
left: 0;
right: 0;
max-height: 85vh;
background: #1a1a2e;
border-top: 1px solid #333;
border-radius: 12px 12px 0 0;
max-height: 88vh;
background: #14141f;
border-top: 1px solid #2a2a3a;
border-radius: 14px 14px 0 0;
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.45);
z-index: 5000;
flex-direction: column;
transform: translateY(100%);
transition: transform 0.25s ease-out;
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
.response-viewer.visible {
@@ -7990,12 +7991,13 @@ kbd {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #333;
padding: 14px 20px;
border-bottom: 1px solid #2a2a3a;
flex-shrink: 0;
font-size: 14px;
font-weight: 600;
color: #e0e0e0;
color: #e8e8ec;
letter-spacing: 0.2px;
}
.response-viewer-actions {
@@ -8078,42 +8080,80 @@ kbd {
background: rgba(109, 219, 127, 0.12);
}
/* Markdown rendered content inside response viewer */
.rv-text {
word-break: break-word;
line-height: 1.6;
/* Markdown rendered content inside response viewer.
Prose uses a proportional font for readability; code keeps monospace. */
.rv-text,
.response-viewer-body > :not(.rv-message) {
word-break: normal;
overflow-wrap: anywhere;
line-height: 1.7;
}
.rv-text p {
margin: 0 0 0.6em;
.rv-text p,
.response-viewer-body > p {
margin: 0 0 0.85em;
}
.rv-text p:last-child {
.rv-text p:last-child,
.response-viewer-body > p:last-child {
margin-bottom: 0;
}
.rv-text h1, .rv-text h2, .rv-text h3, .rv-text h4 {
color: #e0e0e0;
margin: 1em 0 0.4em;
.rv-text h1, .rv-text h2, .rv-text h3, .rv-text h4,
.response-viewer-body > h1, .response-viewer-body > h2,
.response-viewer-body > h3, .response-viewer-body > h4 {
color: #f2f2f6;
margin: 1.4em 0 0.5em;
line-height: 1.3;
font-weight: 700;
letter-spacing: -0.01em;
}
.rv-text h1 { font-size: 1.3em; }
.rv-text h2 { font-size: 1.15em; }
.rv-text h3 { font-size: 1.05em; }
.rv-text h1:first-child, .rv-text h2:first-child,
.response-viewer-body > h1:first-child, .response-viewer-body > h2:first-child {
margin-top: 0;
}
.rv-text code {
background: #2a2a3e;
padding: 1px 5px;
border-radius: 3px;
.rv-text h1, .response-viewer-body > h1 {
font-size: 1.55em;
padding-bottom: 0.3em;
border-bottom: 1px solid #2d2d40;
}
.rv-text h2, .response-viewer-body > h2 {
font-size: 1.3em;
color: #ffd27a;
}
.rv-text h3, .response-viewer-body > h3 {
font-size: 1.13em;
color: #bfc8ff;
}
.rv-text h4, .response-viewer-body > h4 {
font-size: 1em;
color: #c9c9d5;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.rv-text code,
.response-viewer-body > :not(pre) code {
background: #262638;
color: #ffb4a2;
padding: 1px 6px;
border-radius: 4px;
font-family: 'Fira Code', 'JetBrains Mono', 'SF Mono', Menlo, Monaco, monospace;
font-size: 0.9em;
}
.rv-text pre {
background: #1e1e2e;
border: 1px solid #333;
border-radius: 6px;
padding: 10px 12px;
/* Descendant (not child) combinator: code blocks are wrapped in .rv-code-wrap,
so the latest-response view (markdown rendered straight into the body) nests
<pre> one level deeper than a direct child. The historical .rv-text path
already matched via descendant; keep both in lockstep. */
.rv-text pre,
.response-viewer-body pre {
background: #0f0f1a;
border: 1px solid #2a2a3d;
border-radius: 8px;
padding: 14px 16px;
overflow-x: auto;
margin: 1em 0;
-webkit-overflow-scrolling: touch;
@@ -8125,7 +8165,7 @@ kbd {
Preserve indentation (pre-wrap) but allow breaks inside long tokens
(URLs, paths, identifiers) so they don't overflow. */
.rv-text pre code,
.response-viewer-body > pre code {
.response-viewer-body pre code {
background: none;
color: #e6e6f0;
padding: 0;
@@ -8294,38 +8334,43 @@ kbd {
.rv-text ul, .rv-text ol,
.response-viewer-body > ul, .response-viewer-body > ol {
margin: 0.6em 0;
padding-left: 1.5em;
}
.rv-text pre code {
background: none;
padding: 0;
font-size: 0.85em;
line-height: 1.5;
.rv-text li,
.response-viewer-body > ul > li, .response-viewer-body > ol > li {
margin-bottom: 0.3em;
}
.rv-text ul, .rv-text ol {
margin: 0.4em 0;
padding-left: 1.4em;
.rv-text li > p { margin: 0.2em 0; }
.rv-text blockquote,
.response-viewer-body > blockquote {
border-left: 3px solid #5c7cfa;
background: rgba(92, 124, 250, 0.06);
margin: 0.8em 0;
padding: 0.5em 14px;
color: #b8b8c8;
border-radius: 0 6px 6px 0;
}
.rv-text li {
margin-bottom: 0.2em;
.rv-text strong,
.response-viewer-body > p strong,
.response-viewer-body > li strong {
color: #ffffff;
font-weight: 700;
}
.rv-text blockquote {
border-left: 3px solid #444;
margin: 0.6em 0;
padding: 0.3em 0 0.3em 12px;
color: #999;
.rv-text em,
.response-viewer-body em {
color: #e0e0ec;
}
.rv-text strong {
color: #f0f0f0;
}
.rv-text a {
color: #5c7cfa;
.rv-text a,
.response-viewer-body a {
color: #7aa2ff;
text-decoration: none;
border-bottom: 1px solid rgba(122, 162, 255, 0.35);
}
.rv-text a:hover,
@@ -8403,19 +8448,37 @@ kbd {
.rv-text hr,
.response-viewer-body > hr {
border: none;
border-top: 1px solid #333;
margin: 1em 0;
border-top: 1px solid #2d2d40;
margin: 1.5em 0;
}
.response-viewer-body {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: 16px;
font-family: 'Fira Code', 'Cascadia Code', 'JetBrains Mono', 'SF Mono', Monaco, monospace;
font-size: 13px;
line-height: 1.5;
color: #d4d4d4;
overscroll-behavior: contain;
padding: 20px 22px 28px;
/* Proportional font for prose — monospace only for code/pre */
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'PingFang SC',
'Hiragino Sans GB', 'Segoe UI', 'Helvetica Neue', Helvetica, Arial,
'Noto Sans CJK SC', sans-serif;
font-size: 15px;
line-height: 1.7;
color: #d8d8e0;
/* Comfortable reading width on wider viewports */
--rv-content-max: 720px;
}
/* Constrain content width for readability; code blocks can still scroll horizontally */
.response-viewer-body > * {
max-width: var(--rv-content-max);
margin-left: auto;
margin-right: auto;
}
.response-viewer-body > pre,
.response-viewer-body > table,
.response-viewer-body > .rv-message {
max-width: var(--rv-content-max);
}
.response-viewer-body:empty::after {