Skip to content

Commit d32f4de

Browse files
TeigenTeigen
authored andcommitted
feat: markdown rendering for response viewer
Add marked.js (39KB) for rich text display in the response viewer. Renders headings, code blocks, lists, tables, blockquotes, and inline formatting with dark theme styling. Falls back to escaped plain text if marked.js fails to load.
1 parent 3cb7b51 commit d32f4de

4 files changed

Lines changed: 113 additions & 5 deletions

File tree

src/web/public/app.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,18 @@ class CodemanApp {
873873
// Response Viewer — native-scroll panel for reading full Claude responses
874874
// ═══════════════════════════════════════════════════════════════
875875

876+
/** Render markdown to sanitized HTML, falling back to plain text if marked.js unavailable */
877+
_renderMarkdown(text) {
878+
if (typeof marked !== 'undefined' && marked.parse) {
879+
try {
880+
return marked.parse(text, { breaks: true, gfm: true });
881+
} catch { /* fall through */ }
882+
}
883+
// Fallback: escape HTML and preserve whitespace
884+
const escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
885+
return `<pre style="white-space:pre-wrap;word-break:break-word">${escaped}</pre>`;
886+
}
887+
876888
async toggleResponseViewer() {
877889
const viewer = document.getElementById('responseViewer');
878890
const backdrop = document.getElementById('responseViewerBackdrop');
@@ -912,7 +924,7 @@ class CodemanApp {
912924
}
913925

914926
const body = document.getElementById('responseViewerBody');
915-
body.textContent = lastResponse;
927+
body.innerHTML = this._renderMarkdown(lastResponse);
916928

917929
// Reset state for fresh open
918930
const title = document.getElementById('responseViewerTitle');
@@ -958,7 +970,7 @@ class CodemanApp {
958970

959971
const text = document.createElement('div');
960972
text.className = 'rv-text';
961-
text.textContent = msg.text;
973+
text.innerHTML = this._renderMarkdown(msg.text);
962974
div.appendChild(text);
963975

964976
body.appendChild(div);

src/web/public/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<!-- WebGL addon lazy-loaded by app.js on desktop only (skipped on mobile, saving 244KB) -->
2727
<script defer src="vendor/xterm-addon-unicode11.min.js"></script>
2828
<script defer src="vendor/xterm-zerolag-input.js"></script>
29+
<script defer src="vendor/marked.min.js"></script>
2930
<!-- Synchronous mobile detection — runs before first paint to prevent panel flash -->
3031
<script>if(window.innerWidth<768||(('ontouchstart' in window||navigator.maxTouchPoints>0)&&window.innerWidth<1024))document.documentElement.classList.add('mobile-init');</script>
3132
<!-- Inline critical CSS for instant skeleton paint (before styles.css loads) -->

src/web/public/styles.css

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7865,9 +7865,100 @@ kbd {
78657865
color: #51cf66;
78667866
}
78677867

7868+
/* Markdown rendered content inside response viewer */
78687869
.rv-text {
7869-
white-space: pre-wrap;
78707870
word-break: break-word;
7871+
line-height: 1.6;
7872+
}
7873+
7874+
.rv-text p {
7875+
margin: 0 0 0.6em;
7876+
}
7877+
7878+
.rv-text p:last-child {
7879+
margin-bottom: 0;
7880+
}
7881+
7882+
.rv-text h1, .rv-text h2, .rv-text h3, .rv-text h4 {
7883+
color: #e0e0e0;
7884+
margin: 1em 0 0.4em;
7885+
line-height: 1.3;
7886+
}
7887+
7888+
.rv-text h1 { font-size: 1.3em; }
7889+
.rv-text h2 { font-size: 1.15em; }
7890+
.rv-text h3 { font-size: 1.05em; }
7891+
7892+
.rv-text code {
7893+
background: #2a2a3e;
7894+
padding: 1px 5px;
7895+
border-radius: 3px;
7896+
font-size: 0.9em;
7897+
}
7898+
7899+
.rv-text pre {
7900+
background: #1e1e2e;
7901+
border: 1px solid #333;
7902+
border-radius: 6px;
7903+
padding: 10px 12px;
7904+
overflow-x: auto;
7905+
margin: 0.6em 0;
7906+
}
7907+
7908+
.rv-text pre code {
7909+
background: none;
7910+
padding: 0;
7911+
font-size: 0.85em;
7912+
line-height: 1.5;
7913+
}
7914+
7915+
.rv-text ul, .rv-text ol {
7916+
margin: 0.4em 0;
7917+
padding-left: 1.4em;
7918+
}
7919+
7920+
.rv-text li {
7921+
margin-bottom: 0.2em;
7922+
}
7923+
7924+
.rv-text blockquote {
7925+
border-left: 3px solid #444;
7926+
margin: 0.6em 0;
7927+
padding: 0.3em 0 0.3em 12px;
7928+
color: #999;
7929+
}
7930+
7931+
.rv-text strong {
7932+
color: #f0f0f0;
7933+
}
7934+
7935+
.rv-text a {
7936+
color: #5c7cfa;
7937+
text-decoration: none;
7938+
}
7939+
7940+
.rv-text table {
7941+
border-collapse: collapse;
7942+
margin: 0.6em 0;
7943+
width: 100%;
7944+
font-size: 0.9em;
7945+
}
7946+
7947+
.rv-text th, .rv-text td {
7948+
border: 1px solid #333;
7949+
padding: 4px 8px;
7950+
text-align: left;
7951+
}
7952+
7953+
.rv-text th {
7954+
background: #2a2a3e;
7955+
color: #e0e0e0;
7956+
}
7957+
7958+
.rv-text hr {
7959+
border: none;
7960+
border-top: 1px solid #333;
7961+
margin: 1em 0;
78717962
}
78727963

78737964
.response-viewer-body {
@@ -7879,8 +7970,6 @@ kbd {
78797970
font-size: 13px;
78807971
line-height: 1.5;
78817972
color: #d4d4d4;
7882-
white-space: pre-wrap;
7883-
word-break: break-word;
78847973
}
78857974

78867975
.response-viewer-body:empty::after {

src/web/public/vendor/marked.min.js

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)