diff --git a/static/panels.js b/static/panels.js
index d1fa17e1..fdeeb510 100644
--- a/static/panels.js
+++ b/static/panels.js
@@ -1274,17 +1274,188 @@ function _kanbanRenderSidebar(columns){
}
+/**
+ * Render inline markdown (bold, italic, code, links, strikethrough).
+ * Input is already HTML-escaped.
+ */
function _kanbanRenderMarkdownInline(escaped){
return String(escaped || '')
+ .replace(/~~([^~\n]+)~~/g, (_m, text) => `${text}`)
.replace(/`([^`\n]+)`/g, (_m, code) => `${code}`)
.replace(/\*\*([^*\n]+)\*\*/g, (_m, text) => `${text} `)
- .replace(/(^|[^*])\*([^*\n]+)\*/g, (_m, prefix, text) => `${prefix}${text} `)
+ .replace(/(^|[^*a-zA-Z0-9])\*([^*\n]+)\*/g, (_m, prefix, text) => `${prefix}${text} `)
.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+|mailto:[^\s)]+)\)/g, (_m, text, href) => `${text} `);
}
+/**
+ * Render full markdown block content: headings, code blocks, lists, tables,
+ * task lists, blockquotes, horizontal rules, paragraphs + inline formatting.
+ */
function _kanbanRenderMarkdown(source){
if (!source) return '';
- return `
${esc(source).split(/\r?\n/).map(line => line.trim() ? `
${_kanbanRenderMarkdownInline(line)}
` : '').join('')}
`;
+ const lines = esc(source).split(/\r?\n/);
+ const out = [];
+ let i = 0;
+ while (i < lines.length) {
+ const line = lines[i];
+ const trimmed = line.trim();
+
+ // ── Code block ──
+ if (/^```/.test(trimmed)) {
+ const lang = trimmed.slice(3).trim();
+ const codeLines = [];
+ i++;
+ while (i < lines.length && !/^```/.test(lines[i].trim())) {
+ codeLines.push(lines[i]);
+ i++;
+ }
+ i++; // skip closing ```
+ const codeHtml = codeLines.join('\n');
+ out.push(lang
+ ? `${codeHtml} `
+ : `${codeHtml} `);
+ continue;
+ }
+
+ // ── Horizontal rule ──
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(trimmed)) {
+ out.push(' ');
+ i++;
+ continue;
+ }
+
+ // ── Heading ──
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
+ if (headingMatch) {
+ const level = headingMatch[1].length;
+ out.push(`${_kanbanRenderMarkdownInline(headingMatch[2])} `);
+ i++;
+ continue;
+ }
+
+ // ── Blockquote ──
+ if (/^>\s?/.test(trimmed)) {
+ const quoteLines = [];
+ while (i < lines.length && /^>\s?/.test(lines[i].trim())) {
+ quoteLines.push(lines[i].trim().replace(/^>\s?/, ''));
+ i++;
+ }
+ out.push(`${_kanbanRenderMarkdownInline(quoteLines.join(' '))} `);
+ continue;
+ }
+
+ // ── Table row ──
+ if (/^\|.+\|$/.test(trimmed)) {
+ const tableRows = [];
+ const tableAligns = [];
+ while (i < lines.length && /^\|.+\|$/.test(lines[i].trim())) {
+ const row = lines[i].trim();
+ // Detect alignment separator row
+ if (/^\|[\s:]*-{3,}[\s:]*\|/.test(row)) {
+ const cells = row.split('|').filter(c => c.trim().length > 0);
+ cells.forEach(c => {
+ const t = c.trim();
+ if (t.startsWith(':') && t.endsWith(':')) tableAligns.push('center');
+ else if (t.endsWith(':')) tableAligns.push('right');
+ else tableAligns.push('left');
+ });
+ } else {
+ const cells = row.split('|').filter(c => c.trim().length > 0);
+ tableRows.push(cells.map((c, ci) => {
+ const align = tableAligns[ci] ? ` style="text-align:${tableAligns[ci]}"` : '';
+ return `${_kanbanRenderMarkdownInline(c.trim())} `;
+ }).join(''));
+ }
+ i++;
+ }
+ if (tableRows.length) {
+ out.push(`${tableRows.map(r => `${r} `).join('')}
`);
+ }
+ continue;
+ }
+
+ // ── Task list item ──
+ const taskMatch = trimmed.match(/^[-*+]\s+\[( |x|X)\]\s+(.+)$/);
+ if (taskMatch) {
+ const checked = taskMatch[1] !== ' ';
+ const text = taskMatch[2];
+ const items = [];
+ items.push(` ${_kanbanRenderMarkdownInline(text)} `);
+ i++;
+ // Collect continuation items
+ while (i < lines.length) {
+ const next = lines[i].trim();
+ const nextTask = next.match(/^[-*+]\s+\[( |x|X)\]\s+(.+)$/);
+ const nextLi = next.match(/^[-*+]\s+(.+)$/);
+ if (nextTask) {
+ const c = nextTask[1] !== ' ';
+ items.push(` ${_kanbanRenderMarkdownInline(nextTask[2])} `);
+ i++;
+ } else if (nextLi) {
+ items.push(`${_kanbanRenderMarkdownInline(nextLi[1])} `);
+ i++;
+ } else {
+ break;
+ }
+ }
+ out.push(``);
+ continue;
+ }
+
+ // ── Unordered list item ──
+ const ulMatch = trimmed.match(/^[-*+]\s+(.+)$/);
+ if (ulMatch) {
+ const items = [];
+ items.push(`${_kanbanRenderMarkdownInline(ulMatch[1])} `);
+ i++;
+ while (i < lines.length) {
+ const next = lines[i].trim();
+ const nextUl = next.match(/^[-*+]\s+(.+)$/);
+ const nextTask = next.match(/^[-*+]\s+\[( |x|X)\]\s+(.+)$/);
+ if (nextTask) break; // let task list handler get it
+ if (nextUl) {
+ items.push(`${_kanbanRenderMarkdownInline(nextUl[1])} `);
+ i++;
+ } else {
+ break;
+ }
+ }
+ out.push(``);
+ continue;
+ }
+
+ // ── Ordered list item ──
+ const olMatch = trimmed.match(/^\d+\.\s+(.+)$/);
+ if (olMatch) {
+ const items = [];
+ items.push(`${_kanbanRenderMarkdownInline(olMatch[1])} `);
+ i++;
+ while (i < lines.length) {
+ const next = lines[i].trim();
+ const nextOl = next.match(/^\d+\.\s+(.+)$/);
+ if (nextOl) {
+ items.push(`${_kanbanRenderMarkdownInline(nextOl[1])} `);
+ i++;
+ } else {
+ break;
+ }
+ }
+ out.push(`${items.join('')} `);
+ continue;
+ }
+
+ // ── Empty line ──
+ if (!trimmed) {
+ out.push('');
+ i++;
+ continue;
+ }
+
+ // ── Paragraph ──
+ out.push(`${_kanbanRenderMarkdownInline(trimmed)}
`);
+ i++;
+ }
+ return `${out.join('\n')}
`;
}
function _kanbanFormatDuration(seconds){
@@ -1851,7 +2022,7 @@ function _kanbanCommentHtml(comment){
const by = comment.author || comment.created_by || comment.actor || '';
const at = _kanbanFormatTimestamp(comment.created_at || comment.ts || '');
return `
-
${esc(body)}
+
${_kanbanRenderMarkdown(body)}
${esc([by, at].filter(Boolean).join(' · '))}
`;
}
@@ -2403,7 +2574,7 @@ function _kanbanRenderTaskDetail(data){
${esc(title)}
${esc(t('kanban_edit_task') || 'Edit task')}
- ${esc(body)}
+ ${_kanbanRenderMarkdown(body)}
${meta.length ? `${esc(meta.join(' · '))}
` : ''}
${statusButtons}
diff --git a/static/style.css b/static/style.css
index 80f1e96e..5f7bb3e0 100644
--- a/static/style.css
+++ b/static/style.css
@@ -4221,7 +4221,7 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;}
.kanban-run-dispatch-btn:hover{
background:color-mix(in srgb,var(--accent,#FFD700) 24%,transparent);
}
-.kanban-task-preview-body{font-size:12px;color:var(--muted);line-height:1.45;white-space:pre-wrap;margin-bottom:6px;}
+.kanban-task-preview-body{font-size:12px;color:var(--muted);line-height:1.45;margin-bottom:6px;}
.kanban-status-actions{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0 4px;}
.kanban-status-actions .btn{font-size:11px;padding:4px 8px;}
/* Generic styled buttons used throughout the Kanban panel. The Kanban PR
@@ -4291,7 +4291,7 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;}
.kanban-detail-section h3{font-size:12px;font-weight:650;color:var(--text);margin:0 0 8px;}
.kanban-detail-row{padding:8px 0;border-top:1px solid var(--border);}
.kanban-detail-row:first-of-type{border-top:0;padding-top:0;}
-.kanban-detail-row-main{font-size:12px;color:var(--text);line-height:1.45;white-space:pre-wrap;}
+.kanban-detail-row-main{font-size:12px;color:var(--text);line-height:1.45;}.kanban-detail-row-main .hermes-kanban-md p:last-child{margin-bottom:0;}
.kanban-detail-row-meta{font-size:10px;color:var(--muted);margin-top:4px;}
.kanban-detail-pre{font-size:11px;line-height:1.4;white-space:pre-wrap;word-break:break-word;background:var(--input-bg);border:1px solid var(--border);border-radius:6px;padding:6px;margin:6px 0 0;color:var(--muted);}
.kanban-detail-empty{font-size:12px;color:var(--muted);}
@@ -4323,7 +4323,7 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;}
.kanban-card-stale-amber{border-color:rgba(245,197,66,.55)}
.kanban-card-stale-red{border-color:rgba(255,95,95,.65)}
.kanban-column.drop-target{outline:2px solid var(--accent);outline-offset:-2px}
-.hermes-kanban-md p{margin:0 0 4px}.hermes-kanban-md code{font-family:var(--mono);font-size:.95em}
+.hermes-kanban-md p{margin:0 0 4px}.hermes-kanban-md code{font-family:var(--mono);font-size:.95em}.hermes-kanban-md h1,.hermes-kanban-md h2,.hermes-kanban-md h3,.hermes-kanban-md h4,.hermes-kanban-md h5,.hermes-kanban-md h6{margin:10px 0 4px;font-weight:650;color:var(--text)}.hermes-kanban-md h1{font-size:15px}.hermes-kanban-md h2{font-size:14px}.hermes-kanban-md h3{font-size:13px}.hermes-kanban-md h4,.hermes-kanban-md h5,.hermes-kanban-md h6{font-size:12px}.hermes-kanban-md ul,.hermes-kanban-md ol{margin:4px 0;padding-left:20px}.hermes-kanban-md li{margin:2px 0}.hermes-kanban-md li.checked{opacity:.6}.hermes-kanban-md li input[type=checkbox]{margin:0 4px 0 0;vertical-align:middle}.hermes-kanban-md table{border-collapse:collapse;margin:6px 0;font-size:11px;width:100%}.hermes-kanban-md td{border:1px solid var(--border);padding:4px 6px;vertical-align:top}.hermes-kanban-md blockquote{margin:4px 0;padding:2px 8px;border-left:3px solid var(--accent);color:var(--muted);font-size:12px}.hermes-kanban-md pre{background:var(--input-bg);border:1px solid var(--border);border-radius:6px;padding:8px;margin:6px 0;overflow-x:auto;font-size:11px;line-height:1.4;color:var(--text)}.hermes-kanban-md hr{border:none;border-top:1px solid var(--border);margin:8px 0}
@media (max-width: 640px){
.kanban-board{scroll-snap-type:x mandatory;}