mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-20 22:51:07 +00:00
fix: render cron run output as literal pre code, not via renderMd
Cron run output is never authored Markdown -- it is logs/JSON/plain text. Using renderMd() with any heuristic or synthetic fence is fragile: - Content starting with #, |, > becomes headings/tables/blockquotes - Embedded triple-backtick in content breaks the synthetic fence Fix: use DOM-created pre/code with textContent throughout: - No heuristic, no synthetic fence -- every byte renders verbatim - Both render paths (initial load + View full output) use the same approach
This commit is contained in:
+25
-22
@@ -865,21 +865,6 @@ async function _loadCronDetailRuns(jobId){
|
||||
} catch(e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function _wrapCronOutput(text){
|
||||
const trimmed = (text || '').trim();
|
||||
if (!trimmed) return '';
|
||||
// If content starts with a common markdown block marker, render as-is.
|
||||
// Otherwise wrap in triple backticks so JSON, logs, and structured text
|
||||
// render as a <pre><code> block with preserved whitespace.
|
||||
const isMarkdown =
|
||||
trimmed.startsWith('#') || trimmed.startsWith('|') ||
|
||||
trimmed.startsWith('>') || trimmed.startsWith('```') ||
|
||||
trimmed.startsWith('~~~') || trimmed.startsWith('-') ||
|
||||
trimmed.startsWith('*') || trimmed.startsWith('_') ||
|
||||
trimmed.startsWith('[');
|
||||
return isMarkdown ? trimmed : '```\n' + trimmed + '\n```';
|
||||
}
|
||||
|
||||
async function _loadRunContent(jobId, filename, runId){
|
||||
const body = document.querySelector(`#${runId} .detail-run-body`);
|
||||
if (!body) return;
|
||||
@@ -909,12 +894,17 @@ async function _loadRunContent(jobId, filename, runId){
|
||||
const expanded = _cronExpansionGet(_cronRunExpandKey(jobId, filename));
|
||||
const output = expanded ? (data.content || data.snippet || '') : (data.snippet || data.content || '');
|
||||
body.classList.toggle('expanded', expanded);
|
||||
const wrapped = _wrapCronOutput(output);
|
||||
if (typeof renderMd === 'function') {
|
||||
body.innerHTML = renderMd(wrapped);
|
||||
} else {
|
||||
body.textContent = output;
|
||||
}
|
||||
// Cron run output is never authored Markdown — render as literal
|
||||
// preformatted text using DOM-created <pre><code> so all content
|
||||
// (including shapes starting with #, |, >, ``` and embedded fences)
|
||||
// renders verbatim without Markdown interpretation.
|
||||
body.innerHTML = '';
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'cron-run-pre';
|
||||
const code = document.createElement('code');
|
||||
code.textContent = output;
|
||||
pre.appendChild(code);
|
||||
body.appendChild(pre);
|
||||
const usageStrip = _formatCronRunUsageStrip(data.usage);
|
||||
if (usageStrip) {
|
||||
const usage = document.createElement('div');
|
||||
@@ -930,7 +920,20 @@ async function _loadRunContent(jobId, filename, runId){
|
||||
btn.onclick = () => {
|
||||
_cronExpansionSet(_cronRunExpandKey(jobId, filename), true);
|
||||
body.classList.add('expanded');
|
||||
body.innerHTML = renderMd ? renderMd(_wrapCronOutput(data.content)) : data.content;
|
||||
body.innerHTML = '';
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'cron-run-pre';
|
||||
const code = document.createElement('code');
|
||||
code.textContent = data.content || '';
|
||||
pre.appendChild(code);
|
||||
body.appendChild(pre);
|
||||
const usageStrip = _formatCronRunUsageStrip(data.usage);
|
||||
if (usageStrip) {
|
||||
const usage = document.createElement('div');
|
||||
usage.className = 'cron-run-usage-strip cron-run-usage-footer';
|
||||
usage.textContent = usageStrip;
|
||||
body.appendChild(usage);
|
||||
}
|
||||
btn.remove();
|
||||
};
|
||||
body.appendChild(btn);
|
||||
|
||||
@@ -5533,6 +5533,8 @@ main.main > .main-view:not([id="mainChat"]):not([id="mainSettings"]) .main-view-
|
||||
.detail-run-body{display:none;margin-top:6px;font-size:12px;color:var(--muted);white-space:pre-wrap;line-height:1.5;max-height:260px;overflow-y:auto;background:var(--sidebar);border:1px solid var(--border);border-radius:6px;padding:8px 10px;}
|
||||
.detail-run-item.open .detail-run-body{display:block;}
|
||||
.detail-run-body.expanded{max-height:none;overflow-y:visible;}
|
||||
.detail-run-body .cron-run-pre{margin:0;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;}
|
||||
.detail-run-body .cron-run-pre code{background:transparent;padding:0;font-family:var(--font-mono);}
|
||||
.workspace-panel-tabs{display:flex;gap:4px;padding:6px 8px;border-bottom:1px solid var(--border);}
|
||||
.workspace-panel-tab{flex:1;border:1px solid transparent;background:transparent;color:var(--muted);border-radius:7px;padding:5px 8px;font-size:12px;cursor:pointer;}
|
||||
.workspace-panel-tab.active{background:var(--surface-subtle);color:var(--text);border-color:var(--border2);}
|
||||
|
||||
Reference in New Issue
Block a user