mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
feat(response-viewer): ASCII diagram wrap toggle, mobile code blocks, chrome-stripping fallback (#75)
* fix: restore clear message separation + proper table layout in response viewer * fix: capture Claude CLI's real session ID + robust ANSI/CLI-chrome stripping in response viewer fallback Session constructor seeded _claudeSessionId with Codeman's session.id as a placeholder, and the message-driven update was gated on !_claudeSessionId — meaning Claude CLI's actual session UUID was never adopted. This broke /api/sessions/:id/last-response JSONL lookups, silently falling through to the terminal-buffer path whose ANSI regex missed \x1b[>c / \x1b[>q queries. - session.ts: update _claudeSessionId whenever a message's session_id differs from current (covers placeholder and stale-resume cases) - app.js: extract _cleanTerminalBuffer with proper CSI regex (param bytes 0x30-0x3F now covers > ? < =) plus a chrome filter for status bar, progress bar, spinner, shell prompt, and hint lines * fix: wrap regular code blocks on mobile, keep ASCII diagrams rigid with scroll hint * feat: add per-block wrap toggle on ASCII-diagram code blocks * fix: wrap by default, pin toggle button outside scroll container * fix: narrow diagram detection to box-drawing + block elements only * feat: show last-response viewer eye icon on desktop too The response viewer button was mobile-only via a display:none default with a mobile.css override. Flip the default to inline-flex and drop the override so the eye icon appears in the header on every form factor — desktop users get the same quick "Last Response" pane as mobile. * fix(response-viewer): restore HTML sanitizer + fix undefined `src` in _renderMarkdown - `_renderMarkdown` referenced an undefined `src` (should be `text`), causing a ReferenceError on every markdown render. The try/catch swallowed it, so the new table-wrap and ASCII-diagram features never actually ran — output silently fell through to plain-text. app.js is excluded from ESLint, so this wasn't caught at lint time. - `_sanitizeHtml` was removed when refactoring the response viewer, leaving `marked.parse()` output going straight into `innerHTML` without sanitization (XSS regression vs. master). Restored the helper and re-applied it before any post-processing. Co-Authored-By: Claude Opus 4.7 (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.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
Teigen
arkon
parent
d07b59db4e
commit
e549e15cb8
+6
-3
@@ -1614,11 +1614,14 @@ export class Session extends EventEmitter {
|
|||||||
this._messages = this._messages.slice(-Math.floor(MAX_MESSAGES * 0.8));
|
this._messages = this._messages.slice(-Math.floor(MAX_MESSAGES * 0.8));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract Claude session ID from messages (can be in any message type)
|
// Extract Claude session ID from messages (can be in any message type).
|
||||||
// Support both sessionId (camelCase) and session_id (snake_case)
|
// Support both sessionId (camelCase) and session_id (snake_case).
|
||||||
|
// The constructor seeds _claudeSessionId with this.id as a placeholder;
|
||||||
|
// once Claude CLI emits its real session ID, adopt it so JSONL lookups
|
||||||
|
// (e.g. /api/sessions/:id/last-response) can find the transcript file.
|
||||||
const msgSessionId =
|
const msgSessionId =
|
||||||
((msg as unknown as Record<string, unknown>).sessionId as string | undefined) ?? msg.session_id;
|
((msg as unknown as Record<string, unknown>).sessionId as string | undefined) ?? msg.session_id;
|
||||||
if (msgSessionId && !this._claudeSessionId) {
|
if (msgSessionId && msgSessionId !== this._claudeSessionId) {
|
||||||
this._claudeSessionId = msgSessionId;
|
this._claudeSessionId = msgSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+157
-19
@@ -908,11 +908,9 @@ class CodemanApp {
|
|||||||
const tpl = document.createElement('template');
|
const tpl = document.createElement('template');
|
||||||
tpl.innerHTML = html;
|
tpl.innerHTML = html;
|
||||||
const frag = tpl.content;
|
const frag = tpl.content;
|
||||||
// Remove dangerous elements
|
|
||||||
for (const el of frag.querySelectorAll('script, iframe, object, embed, form, base, meta, link, style')) {
|
for (const el of frag.querySelectorAll('script, iframe, object, embed, form, base, meta, link, style')) {
|
||||||
el.remove();
|
el.remove();
|
||||||
}
|
}
|
||||||
// Strip dangerous attributes from all elements
|
|
||||||
for (const el of frag.querySelectorAll('*')) {
|
for (const el of frag.querySelectorAll('*')) {
|
||||||
for (const attr of [...el.attributes]) {
|
for (const attr of [...el.attributes]) {
|
||||||
const name = attr.name.toLowerCase();
|
const name = attr.name.toLowerCase();
|
||||||
@@ -926,17 +924,143 @@ class CodemanApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Serialize back via a container
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.appendChild(frag);
|
div.appendChild(frag);
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip ANSI escape sequences and Claude CLI chrome (status bar, hints,
|
||||||
|
* spinner, progress bar) from a terminal buffer so the response viewer can
|
||||||
|
* show just the conversational text when the JSONL transcript is missing.
|
||||||
|
*/
|
||||||
|
_cleanTerminalBuffer(buf) {
|
||||||
|
const stripped = buf
|
||||||
|
// CSI sequences — params (0x30-0x3F includes digits, ?, ;, <, =, >),
|
||||||
|
// intermediates (0x20-0x2F), final byte (0x40-0x7E). Catches \x1b[>c,
|
||||||
|
// \x1b[>q, \x1b[?25l etc. that the previous regex missed.
|
||||||
|
.replace(/\x1b\[[\x30-\x3F]*[\x20-\x2F]*[\x40-\x7E]/g, '')
|
||||||
|
// OSC sequences (window titles etc.) terminated by BEL or ST
|
||||||
|
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
||||||
|
// DCS / APC / PM / SOS sequences
|
||||||
|
.replace(/\x1b[PX^_][^\x1b]*\x1b\\/g, '')
|
||||||
|
// SS2/SS3 + charset selects + single-char escapes
|
||||||
|
.replace(/\x1b[NO()][A-Z0-9]?/g, '')
|
||||||
|
.replace(/\x1b[>=<78cDEHM]/g, '')
|
||||||
|
// Stray control chars (except \t \n)
|
||||||
|
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
||||||
|
.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
|
|
||||||
|
// Drop Claude CLI chrome lines that aren't part of the response.
|
||||||
|
const CHROME_PATTERNS = [
|
||||||
|
/^\s*❯\s*/, // shell prompt
|
||||||
|
/^\s*[⏵⏺⏸⏹]+\s*/, // status glyphs
|
||||||
|
/^\s*✻\s*(Crunching|Crunched|Thinking)/i, // spinner lines
|
||||||
|
/bypass permissions/i,
|
||||||
|
/\bshift\+tab to cycle\b/i,
|
||||||
|
/^\s*focus\s*$/,
|
||||||
|
/^\s*new task\?/i,
|
||||||
|
/\/clear to save/i,
|
||||||
|
/^\s*─{5,}\s*$/, // horizontal dividers
|
||||||
|
/\[(Opus|Sonnet|Haiku|GPT|Claude)[\s\S]*(tokens?|\$|¥|%|↑|↓)/i, // status bar
|
||||||
|
/^\s*\[\d+[km]?\/\d+[km]?\]/i, // token counter
|
||||||
|
/[█░▓▒]{3,}/, // progress bar
|
||||||
|
/^\s*\(.*\s*(tokens?|context).*\)\s*$/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
const lines = stripped.split('\n');
|
||||||
|
const kept = lines.filter((line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) return true; // keep blanks so paragraphs survive
|
||||||
|
return !CHROME_PATTERNS.some((re) => re.test(line));
|
||||||
|
});
|
||||||
|
|
||||||
|
return kept
|
||||||
|
.join('\n')
|
||||||
|
.replace(/[ \t]+$/gm, '')
|
||||||
|
.replace(/\n{4,}/g, '\n\n\n')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap ASCII/box diagrams in fenced code blocks so marked.js preserves whitespace.
|
||||||
|
* Claude often emits box-drawing diagrams without triple-backticks; without this
|
||||||
|
* step, HTML collapses the whitespace and the diagram becomes unreadable prose.
|
||||||
|
*/
|
||||||
|
_preprocessAsciiArt(text) {
|
||||||
|
// Only trigger on characters that rarely appear in prose:
|
||||||
|
// U+2500-U+257F Box Drawing (─│┌┐└┘├┤┬┴┼╔╗╚╝═║)
|
||||||
|
// U+2580-U+259F Block Elements (▀▄█▌▐░▒▓, progress bars)
|
||||||
|
// Deliberately excluded:
|
||||||
|
// U+2190-U+21FF Arrows (→←↑↓⇒ — common rhetorical prose)
|
||||||
|
// U+25A0-U+25FF Geometric Shapes (●○■□◆◇ — common bullets)
|
||||||
|
// Triggering on those would wrap numbered lists / prose that merely uses
|
||||||
|
// arrows in code blocks and break their markdown rendering.
|
||||||
|
const BOX_PATTERN = /[─-╿▀-▟]/;
|
||||||
|
|
||||||
|
// Preserve existing fenced code blocks as-is (hide them behind placeholders)
|
||||||
|
const fenceRe = /```[\s\S]*?```/g;
|
||||||
|
const placeholders = [];
|
||||||
|
const masked = text.replace(fenceRe, (m) => {
|
||||||
|
placeholders.push(m);
|
||||||
|
return ` | ||||||