From 92e8b6a0ba7929e69738f0f80a7fb5724f41e618 Mon Sep 17 00:00:00 2001 From: luandnh Date: Fri, 26 Jun 2026 08:43:27 +0000 Subject: [PATCH 1/4] fix: wrap raw cron run output in code block for proper formatting When viewing cron job run logs (prompt/response), the raw content was passed through renderMd() which broke formatting for JSON and plain text (single newlines collapsed, special chars interpreted). Now detects if content looks like markdown and wraps raw content in triple backticks so it renders as a proper code block with preserved whitespace and formatting. --- static/panels.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/static/panels.js b/static/panels.js index 7bfdd568f..80dab58a2 100644 --- a/static/panels.js +++ b/static/panels.js @@ -894,9 +894,14 @@ 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); - // Render markdown content using the same renderer as chat messages + // Render markdown content using the same renderer as chat messages. + // Wrap raw output in triple backticks so JSON, logs, and structured text + // render as a code block with preserved whitespace and formatting. + const renderContent = output.trim(); + const needsCodeWrap = !renderContent.startsWith('#') && !renderContent.startsWith('|') && !renderContent.startsWith('>') && !renderContent.startsWith('```'); + const wrapped = needsCodeWrap ? '```\n' + renderContent + '\n```' : renderContent; if (typeof renderMd === 'function') { - body.innerHTML = renderMd(output); + body.innerHTML = renderMd(wrapped); } else { body.textContent = output; } @@ -915,7 +920,10 @@ async function _loadRunContent(jobId, filename, runId){ btn.onclick = () => { _cronExpansionSet(_cronRunExpandKey(jobId, filename), true); body.classList.add('expanded'); - body.innerHTML = renderMd ? renderMd(data.content) : data.content; + const fullContent = (data.content || '').trim(); + const needsCode = !fullContent.startsWith('#') && !fullContent.startsWith('|') && !fullContent.startsWith('>') && !fullContent.startsWith('```'); + const wrapped = needsCode ? '```\n' + fullContent + '\n```' : fullContent; + body.innerHTML = renderMd ? renderMd(wrapped) : data.content; btn.remove(); }; body.appendChild(btn); From ca1a4ee2dd8e0730ebc40523e4ef78c82b45b392 Mon Sep 17 00:00:00 2001 From: luandnh Date: Fri, 26 Jun 2026 08:53:49 +0000 Subject: [PATCH 2/4] fix: extract _wrapCronOutput helper, expand heuristic per review - Extracted duplicated detection logic into shared _wrapCronOutput(text) - Expanded heuristic to cover ~~~, -, *, _, [ markers - Added empty-output guard (returns '' for falsy/empty text) - Fixes Greptile review P2-P3 --- static/panels.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/static/panels.js b/static/panels.js index 80dab58a2..b44be0cb9 100644 --- a/static/panels.js +++ b/static/panels.js @@ -865,6 +865,21 @@ 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
 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;
@@ -894,12 +909,7 @@ 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);
-    // Render markdown content using the same renderer as chat messages.
-    // Wrap raw output in triple backticks so JSON, logs, and structured text
-    // render as a code block with preserved whitespace and formatting.
-    const renderContent = output.trim();
-    const needsCodeWrap = !renderContent.startsWith('#') && !renderContent.startsWith('|') && !renderContent.startsWith('>') && !renderContent.startsWith('```');
-    const wrapped = needsCodeWrap ? '```\n' + renderContent + '\n```' : renderContent;
+    const wrapped = _wrapCronOutput(output);
     if (typeof renderMd === 'function') {
       body.innerHTML = renderMd(wrapped);
     } else {
@@ -920,10 +930,7 @@ async function _loadRunContent(jobId, filename, runId){
       btn.onclick = () => {
         _cronExpansionSet(_cronRunExpandKey(jobId, filename), true);
         body.classList.add('expanded');
-        const fullContent = (data.content || '').trim();
-        const needsCode = !fullContent.startsWith('#') && !fullContent.startsWith('|') && !fullContent.startsWith('>') && !fullContent.startsWith('```');
-        const wrapped = needsCode ? '```\n' + fullContent + '\n```' : fullContent;
-        body.innerHTML = renderMd ? renderMd(wrapped) : data.content;
+        body.innerHTML = renderMd ? renderMd(_wrapCronOutput(data.content)) : data.content;
         btn.remove();
       };
       body.appendChild(btn);

From 046a0f68b8e6fe4da321ec62c44f2d393e8a8be7 Mon Sep 17 00:00:00 2001
From: luandnh 
Date: Fri, 26 Jun 2026 09:22:17 +0000
Subject: [PATCH 3/4] 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
---
 static/panels.js | 47 +++++++++++++++++++++++++----------------------
 static/style.css |  2 ++
 2 files changed, 27 insertions(+), 22 deletions(-)

diff --git a/static/panels.js b/static/panels.js
index b44be0cb9..30d19c2be 100644
--- a/static/panels.js
+++ b/static/panels.js
@@ -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 
 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 
 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);
diff --git a/static/style.css b/static/style.css
index ae87e257d..daa9cab3f 100644
--- a/static/style.css
+++ b/static/style.css
@@ -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);}

From 5c0cda025a4329b64f2267296f2267b53c164e4b Mon Sep 17 00:00:00 2001
From: nesquena-hermes 
Date: Fri, 26 Jun 2026 12:08:01 +0000
Subject: [PATCH 4/4] Release YG (v0.51.677): render cron run output as literal
 pre/code (#4977)

---
 CHANGELOG.md | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 88ebe46d5..3e269c55f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,12 @@
 
 ## [Unreleased]
 
+## [v0.51.677] — 2026-06-26 — Release YG (cron run logs render as literal text, not mangled markdown)
+
+### Fixed
+
+- **Cron job run logs (prompt/response in the cron detail panel) now render as literal preformatted text instead of being mangled by Markdown.** The expanded run body and the "View full output" view passed raw output through `renderMd()`, so JSON/plain-text logs lost their newlines, had special characters interpreted, and lines beginning with `#`/`|`/`>` turned into headings/tables/blockquotes. Both paths now render via a DOM-created `
` with `textContent`, preserving whitespace exactly and rendering nothing as Markdown (also XSS-safe — no `innerHTML` on raw output). The usage footer and action button stay outside the preformatted block. Thanks @luandnh. (#4977)
+
 ## [v0.51.676] — 2026-06-26 — Release YF (submitted message no longer renders twice — or vanishes — on active reload)
 
 ### Fixed