Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@

## [Unreleased]

## [v0.51.126] — 2026-05-24 — Release CX (stage-batch8 — 2-PR low-risk batch — kanban markdown + live activity timeline)

### Added

- **PR #2819** by @humayunak — Kanban task descriptions and comments now render as full GFM Markdown instead of plain-text. `_kanbanRenderMarkdown()` in `static/panels.js` rewrote the line-per-`<p>` wrapper as a block-parsing pipeline supporting headings, code blocks (fenced + indented), ordered/unordered lists, task lists with checkboxes, tables, blockquotes, horizontal rules, and strikethrough. `_kanbanRenderMarkdownInline()` gains `~~strikethrough~~` and tightens the italic regex to avoid mid-identifier `*` matches. CSS adds table borders, code-block background, checkbox styling, blockquote accent, and heading sizing scoped to `.hermes-kanban-md`. Frontend-only, scoped to the kanban panel. 95 existing kanban tests pass.

### Changed

- **PR #2847** by @AJV20 — Live chat Activity disclosure now shows observable run telemetry instead of an empty `Thinking…` placeholder when no reasoning text is available (squashed from 2 author commits). New baseline rows surface run-start metadata (model, profile), `Waiting on model` / `Waiting on tool result` / `Working for …` status, tool start/finish in the timeline alongside the existing compact tool cards, and a `No recent activity for …` state after quiet periods. Frontend-only telemetry derived from existing stream events — no new backend event types. Adds `tests/test_live_activity_timeline.py` (4 tests). The compact/calm default Activity disclosure is preserved; it only becomes informative when expanded.

## [v0.51.125] — 2026-05-24 — Release CW (stage-batch7 — 10-PR low-risk batch — UI/UX polish + bug fixes + diagnostics)

### Fixed
Expand Down
179 changes: 175 additions & 4 deletions static/panels.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => `<del>${text}</del>`)
.replace(/`([^`\n]+)`/g, (_m, code) => `<code>${code}</code>`)
.replace(/\*\*([^*\n]+)\*\*/g, (_m, text) => `<strong>${text}</strong>`)
.replace(/(^|[^*])\*([^*\n]+)\*/g, (_m, prefix, text) => `${prefix}<em>${text}</em>`)
.replace(/(^|[^*a-zA-Z0-9])\*([^*\n]+)\*/g, (_m, prefix, text) => `${prefix}<em>${text}</em>`)
.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+|mailto:[^\s)]+)\)/g, (_m, text, href) => `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`);
}

/**
* 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 `<div class="hermes-kanban-md">${esc(source).split(/\r?\n/).map(line => line.trim() ? `<p>${_kanbanRenderMarkdownInline(line)}</p>` : '').join('')}</div>`;
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
? `<pre class="hermes-kanban-code"><code class="language-${_kanbanRenderMarkdownInline(lang)}">${codeHtml}</code></pre>`
: `<pre class="hermes-kanban-code"><code>${codeHtml}</code></pre>`);
continue;
}

// ── Horizontal rule ──
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(trimmed)) {
out.push('<hr>');
i++;
continue;
}

// ── Heading ──
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
out.push(`<h${level}>${_kanbanRenderMarkdownInline(headingMatch[2])}</h${level}>`);
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(`<blockquote>${_kanbanRenderMarkdownInline(quoteLines.join('<br>'))}</blockquote>`);
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 `<td${align}>${_kanbanRenderMarkdownInline(c.trim())}</td>`;
}).join(''));
}
i++;
}
if (tableRows.length) {
out.push(`<table><tbody>${tableRows.map(r => `<tr>${r}</tr>`).join('')}</tbody></table>`);
}
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(`<li class="hermes-kanban-task${checked ? ' checked' : ''}"><input type="checkbox"${checked ? ' checked' : ''} disabled> ${_kanbanRenderMarkdownInline(text)}</li>`);
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(`<li class="hermes-kanban-task${c ? ' checked' : ''}"><input type="checkbox"${c ? ' checked' : ''} disabled> ${_kanbanRenderMarkdownInline(nextTask[2])}</li>`);
i++;
} else if (nextLi) {
items.push(`<li>${_kanbanRenderMarkdownInline(nextLi[1])}</li>`);
i++;
} else {
break;
}
}
out.push(`<ul>${items.join('')}</ul>`);
continue;
}

// ── Unordered list item ──
const ulMatch = trimmed.match(/^[-*+]\s+(.+)$/);
if (ulMatch) {
const items = [];
items.push(`<li>${_kanbanRenderMarkdownInline(ulMatch[1])}</li>`);
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(`<li>${_kanbanRenderMarkdownInline(nextUl[1])}</li>`);
i++;
} else {
break;
}
}
out.push(`<ul>${items.join('')}</ul>`);
continue;
}

// ── Ordered list item ──
const olMatch = trimmed.match(/^\d+\.\s+(.+)$/);
if (olMatch) {
const items = [];
items.push(`<li>${_kanbanRenderMarkdownInline(olMatch[1])}</li>`);
i++;
while (i < lines.length) {
const next = lines[i].trim();
const nextOl = next.match(/^\d+\.\s+(.+)$/);
if (nextOl) {
items.push(`<li>${_kanbanRenderMarkdownInline(nextOl[1])}</li>`);
i++;
} else {
break;
}
}
out.push(`<ol>${items.join('')}</ol>`);
continue;
}

// ── Empty line ──
if (!trimmed) {
out.push('');
i++;
continue;
}

// ── Paragraph ──
out.push(`<p>${_kanbanRenderMarkdownInline(trimmed)}</p>`);
i++;
}
return `<div class="hermes-kanban-md">${out.join('\n')}</div>`;
}

function _kanbanFormatDuration(seconds){
Expand Down Expand Up @@ -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 `<div class="kanban-detail-row">
<div class="kanban-detail-row-main">${esc(body)}</div>
<div class="kanban-detail-row-main">${_kanbanRenderMarkdown(body)}</div>
<div class="kanban-detail-row-meta">${esc([by, at].filter(Boolean).join(' · '))}</div>
</div>`;
}
Expand Down Expand Up @@ -2403,7 +2574,7 @@ function _kanbanRenderTaskDetail(data){
<div class="kanban-task-preview-title">${esc(title)}</div>
<button class="btn secondary kanban-edit-btn" onclick="openKanbanEdit('${esc(task.id)}')" data-i18n="kanban_edit_task" title="${esc(t('kanban_edit_task') || 'Edit task')}">${esc(t('kanban_edit_task') || 'Edit task')}</button>
</div>
<div class="kanban-task-preview-body">${esc(body)}</div>
<div class="kanban-task-preview-body">${_kanbanRenderMarkdown(body)}</div>
${meta.length ? `<div class="kanban-meta">${esc(meta.join(' · '))}</div>` : ''}
<div class="kanban-status-actions">${statusButtons}</div>
<div class="kanban-detail-grid">
Expand Down
15 changes: 12 additions & 3 deletions static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2330,6 +2330,15 @@ body.resizing .sidebar{transition:none!important;}
.tool-call-group:not(.tool-call-group-collapsed) .tool-call-group-chevron{transform:rotate(90deg);}
.tool-call-group-body{display:block;padding-left:var(--space-3);}
.tool-call-group.tool-call-group-collapsed .tool-call-group-body{display:none;}
.agent-activity-status{display:grid;grid-template-columns:18px minmax(0,1fr) auto;align-items:start;gap:var(--space-2);padding:5px 0;color:var(--muted);font-size:var(--font-size-xs);line-height:1.45;border-bottom:1px solid color-mix(in srgb,var(--border-subtle) 60%,transparent);}
.agent-activity-status:last-child{border-bottom:0;}
.agent-activity-status-icon{display:inline-flex;align-items:center;justify-content:center;min-height:18px;opacity:.72;color:var(--muted);}
.agent-activity-status-copy{display:flex;flex-direction:column;min-width:0;gap:1px;}
.agent-activity-status-label{color:var(--text);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.agent-activity-status-detail{color:var(--muted);opacity:.72;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.agent-activity-status-time{font-variant-numeric:tabular-nums;opacity:.55;white-space:nowrap;}
.agent-activity-status-waiting .agent-activity-status-label{color:var(--muted);}
.agent-activity-status-error .agent-activity-status-label{color:var(--error);}
.tool-call-group-label{font-weight:600;color:var(--muted);position:relative;display:inline-block;overflow:hidden;}
.tool-call-group[data-live-tool-call-group="1"] .tool-call-group-label{
color:var(--muted);
Expand Down Expand Up @@ -4221,7 +4230,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
Expand Down Expand Up @@ -4291,7 +4300,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);}
Expand Down Expand Up @@ -4323,7 +4332,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;}
Expand Down
Loading
Loading