From 7983e025c40637f3ff86f71fdbe5842c68cf48cf Mon Sep 17 00:00:00 2001 From: humayunak Date: Wed, 20 May 2026 10:19:37 +0500 Subject: [PATCH 1/3] kanban: full markdown rendering for task description and comments - Rewrote _kanbanRenderMarkdown() from basic paragraph wrapper to a line-by-line block processor supporting headings, code blocks, lists, task lists, tables, blockquotes, horizontal rules, and strikethrough. - Added CSS for all new elements (table borders, code blocks, checkboxes, blockquote accent, heading sizing, etc.). - Dropped white-space: pre-wrap from .kanban-task-preview-body and .kanban-detail-row-main since markdown now handles layout. - Applied _kanbanRenderMarkdown() to task description (was esc()) and comment body (was esc()) in the task detail view. --- static/panels.js | 179 +++++++++++++++++++++++++++++++++++++++++++++-- static/style.css | 6 +- 2 files changed, 178 insertions(+), 7 deletions(-) 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(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;} From 237bab753a3a1f9b7df1aec5da0bda46b5e33602 Mon Sep 17 00:00:00 2001 From: AJV20 Date: Sun, 24 May 2026 16:13:00 +0000 Subject: [PATCH 2/3] feat: surface live activity timeline (#2847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed from 2 author commits: - d2237e23 feat: surface live activity timeline - eee57ec0 fix: satisfy activity timeline CI guards Frontend-only telemetry from existing stream events. Replaces empty Thinking… placeholder with observable run status (Waiting on model / Waiting on tool result / Working for …). New CSS, new test file. --- static/style.css | 9 +++ static/ui.js | 99 ++++++++++++++++++++++++++-- tests/test_live_activity_timeline.py | 45 +++++++++++++ 3 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 tests/test_live_activity_timeline.py diff --git a/static/style.css b/static/style.css index 5f7bb3e0..429500ca 100644 --- a/static/style.css +++ b/static/style.css @@ -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); diff --git a/static/ui.js b/static/ui.js index 6679c87a..0d4c8e4e 100644 --- a/static/ui.js +++ b/static/ui.js @@ -2208,6 +2208,7 @@ function _startCompressionElapsedTimer(){if(!_compressionElapsedTimer)_compressi function _clearCompressionElapsedTimer(){if(_compressionElapsedTimer){clearInterval(_compressionElapsedTimer);_compressionElapsedTimer=null;}} let _activityElapsedTimer=null; let _activityElapsedTimerGroup=null; +function _activityNowSeconds(){return Date.now()/1000;} function _activityElapsedStartedAt(group){ if(!group)return null; const raw=(group.dataset&&group.dataset.turnStartedAt!==undefined&&group.dataset.turnStartedAt!=='') @@ -2219,7 +2220,52 @@ function _activityElapsedStartedAt(group){ function _activityElapsedLabel(group){ const started=_activityElapsedStartedAt(group); if(!started)return''; - return _formatActiveElapsedTimer((Date.now()/1000)-started); + return _formatActiveElapsedTimer(_activityNowSeconds()-started); +} +function _activityMarkObserved(group, ts){ + if(!group||group.getAttribute('data-live-tool-call-group')!=='1')return; + const stamp=Number(ts||_activityNowSeconds()); + if(Number.isFinite(stamp)&&stamp>0) group.setAttribute('data-last-activity-at',String(stamp)); +} +function _activityLastObservedAge(group){ + const stamp=Number(group&&group.getAttribute('data-last-activity-at')); + if(!Number.isFinite(stamp)||stamp<=0)return null; + return Math.max(0,_activityNowSeconds()-stamp); +} +function _activityClockLabel(ts){ + const stamp=Number(ts||_activityNowSeconds()); + if(!Number.isFinite(stamp)||stamp<=0)return''; + try{return new Date(stamp*1000).toLocaleTimeString([], {hour:'numeric',minute:'2-digit'});}catch(_){return'';} +} +function _activityStatusNode({kind='info',label='',detail='',status='done',ts=null,id=''}){ + const row=document.createElement('div'); + row.className=`agent-activity-status agent-activity-status-${kind} agent-activity-status-${status}`; + if(id) row.setAttribute('data-activity-event-id',id); + if(ts) row.setAttribute('data-activity-at',String(ts)); + const iconMap={run:li('play',13),model:li('bot',13),waiting:'',thinking:li('lightbulb',13),tool:li('wrench',13),done:li('check',13),warning:li('alert-triangle',13)}; + row.innerHTML=`${iconMap[kind]||li('clock',13)}${esc(label)}${detail?`${esc(detail)}`:''}${esc(_activityClockLabel(ts))}`; + return row; +} +function _appendActivityEvent(group, event){ + if(!group)return null; + const body=group.querySelector('.tool-call-group-body'); + if(!body)return null; + const eventId=event&&event.id; + let row=eventId?body.querySelector(`.agent-activity-status[data-activity-event-id="${CSS.escape(eventId)}"]`):null; + const next=_activityStatusNode(event||{}); + if(row){row.replaceWith(next);row=next;} + else{body.appendChild(next);row=next;} + _activityMarkObserved(group,event&&event.ts); + return row; +} +function _ensureLiveActivityBaseline(group){ + if(!group||group.getAttribute('data-live-tool-call-group')!=='1')return; + const started=_activityElapsedStartedAt(group)||_activityNowSeconds(); + if(!group.getAttribute('data-turn-started-at')) group.setAttribute('data-turn-started-at',String(started)); + if(!group.getAttribute('data-last-activity-at')) group.setAttribute('data-last-activity-at',String(started)); + _appendActivityEvent(group,{id:'run-started',kind:'run',label:'Run started',detail:'Observable activity will appear here as the agent works.',status:'done',ts:started}); + const modelLabel=(S.session&&S.session.model)?getModelLabel(S.session.model):''; + if(modelLabel)_appendActivityEvent(group,{id:'run-model',kind:'model',label:`Model: ${modelLabel}`,detail:S.activeProfile&&S.activeProfile!=='default'?`Profile: ${S.activeProfile}`:'',status:'done',ts:started}); } function _setActivityElapsedStartedAt(group){ if(!group||group.getAttribute('data-live-tool-call-group')!=='1')return; @@ -2240,8 +2286,10 @@ function _updateActiveActivityElapsedTimer(){ group.removeAttribute('data-active-turn-elapsed'); } if(durationEl){ - durationEl.textContent=label?`Working ${label}`:''; - durationEl.style.display=label?'':'none'; + const activeText=label?`Working for ${label}`:''; + const progressText=_activityLiveProgressLabel(group); + durationEl.textContent=[progressText, activeText].filter(Boolean).join(' · '); + durationEl.style.display=durationEl.textContent?'':'none'; } } function _startActivityElapsedTimer(group){ @@ -5220,7 +5268,10 @@ function ensureActivityGroup(inner, opts){ }else if(activityKey&&!group.getAttribute('data-activity-disclosure-key')){ group.setAttribute('data-activity-disclosure-key',activityKey); } - if(live) _setActivityElapsedStartedAt(group); + if(live){ + _setActivityElapsedStartedAt(group); + _ensureLiveActivityBaseline(group); + } _syncToolCallGroupSummary(group); if(live) _startActivityElapsedTimer(group); return group; @@ -6658,7 +6709,9 @@ function _syncToolCallGroupSummary(group){ const label=group.querySelector('.tool-call-group-label'); const durationEl=group.querySelector('.tool-call-group-duration'); if(label){ - if(toolCount) label.textContent=`Activity: ${toolCount} tool${toolCount===1?'':'s'}`; + if(group.getAttribute('data-live-tool-call-group')==='1'){ + label.textContent=toolCount?`Activity: ${toolCount} tool${toolCount===1?'':'s'}`:'Activity · Running'; + }else if(toolCount) label.textContent=`Activity: ${toolCount} tool${toolCount===1?'':'s'}`; else label.textContent='Activity'; label.setAttribute('data-sweep-label', label.textContent); } @@ -6692,9 +6745,14 @@ function _activityProgressLabelForToolName(name){ function _activityLiveProgressLabel(group){ if(!group||group.getAttribute('data-live-tool-call-group')!=='1') return ''; + const idleAge=_activityLastObservedAge(group); + if(idleAge!==null&&idleAge>=90) return `No recent activity for ${_formatActiveElapsedTimer(idleAge)}`; const running=group.querySelector('.tool-card.tool-card-running .tool-card-name'); const latest=running || Array.from(group.querySelectorAll('.tool-card-name')).pop(); - return _activityProgressLabelForToolName(latest?latest.textContent:''); + const waiting=group.querySelector('.agent-activity-status-waiting .agent-activity-status-label'); + if(latest) return _activityProgressLabelForToolName(latest.textContent); + if(waiting&&waiting.textContent) return waiting.textContent; + return 'Starting agent'; } // ── Live tool card helpers (called during SSE streaming) ── @@ -6760,6 +6818,19 @@ function appendLiveToolCard(tc){ const anchor=children.filter(el=>el.matches('[data-live-assistant="1"],.tool-call-group,.tool-card-row,.agent-activity-thinking')).pop(); const group=ensureActivityGroup(inner,{live:true,collapsed:true,anchor,activityKey:_activityKeyForLiveTurn()}); const body=group.querySelector('.tool-call-group-body'); + const toolName=_toolDisplayName(tc); + const toolEventId=tid?`tool-${tid}`:`tool-${String(tc.name||'tool').replace(/[^a-z0-9_-]/gi,'_')}`; + const toolDone=tc.done!==false; + _appendActivityEvent(group,{ + id:toolEventId, + kind:'tool', + label:toolDone?`Tool finished: ${toolName}`:`Running tool: ${toolName}`, + detail:tc.preview||tc.snippet||'', + status:toolDone?(tc.is_error?'error':'done'):'waiting', + ts:_activityNowSeconds(), + }); + const waiting=body.querySelector('.agent-activity-status[data-activity-event-id="thinking-placeholder"] .agent-activity-status-label'); + if(waiting&&!toolDone) waiting.textContent='Waiting on tool result'; // Update existing card in place (tool_complete after tool_start) if(tid){ const existing=body.querySelector(`.tool-card-row[data-live-tid="${CSS.escape(tid)}"]`); @@ -7584,6 +7655,7 @@ function appendThinking(text='', options){ return; } const thinkingText=String(text||'').trim()||'Thinking…'; + const cleanThinking=_sanitizeThinkingDisplayText(thinkingText); const allChildren=Array.from(blocks.children); const anchor=allChildren.filter(el=> el.id!=='toolRunningRow' && @@ -7592,6 +7664,20 @@ function appendThinking(text='', options){ const group=ensureActivityGroup(blocks,{live:true,collapsed:true,anchor,activityKey:_activityKeyForLiveTurn()}); const body=group&&group.querySelector('.tool-call-group-body'); if(!body) return; + if(!cleanThinking||cleanThinking==='Thinking…'){ + const label=body.querySelector('.tool-card.tool-card-running')?'Waiting on tool result':'Waiting on model'; + const detail=body.querySelector('.tool-card-row') + ? 'The agent is running; tool results and response text will appear here.' + : 'No tool activity has been reported yet.'; + _appendActivityEvent(group,{id:'thinking-placeholder',kind:'waiting',label,detail,status:'waiting',ts:_activityNowSeconds()}); + const active=body.querySelector('.agent-activity-thinking[data-thinking-active="1"]'); + if(active) active.removeAttribute('data-thinking-active'); + _syncToolCallGroupSummary(group); + scrollIfPinned(); + return; + } + const placeholder=body.querySelector('.agent-activity-status[data-activity-event-id="thinking-placeholder"]'); + if(placeholder) placeholder.remove(); let row=body.querySelector('.agent-activity-thinking[data-thinking-active="1"]'); if(!row){ const thinkingCards=Array.from(body.querySelectorAll('.agent-activity-thinking')); @@ -7605,6 +7691,7 @@ function appendThinking(text='', options){ }else{ _renderThinkingInto(row,thinkingText); } + _activityMarkObserved(group); _syncToolCallGroupSummary(group); scrollIfPinned(); if(_scrollPinned){ diff --git a/tests/test_live_activity_timeline.py b/tests/test_live_activity_timeline.py new file mode 100644 index 00000000..21e04f2c --- /dev/null +++ b/tests/test_live_activity_timeline.py @@ -0,0 +1,45 @@ +"""Regression coverage for live Activity timeline UX. + +The live Activity disclosure should surface observable run telemetry instead of a +blank Thinking placeholder while preserving the quiet tool/thinking metadata +family. +""" + +import pathlib + + +REPO = pathlib.Path(__file__).parent.parent +UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8") +STYLE_CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8") + + +def test_live_activity_group_has_observable_baseline_events(): + assert "function _ensureLiveActivityBaseline(group)" in UI_JS + assert "Run started" in UI_JS + assert "Observable activity will appear here as the agent works." in UI_JS + assert "Model: ${modelLabel}" in UI_JS + assert "_ensureLiveActivityBaseline(group);" in UI_JS + + +def test_empty_thinking_placeholder_becomes_status_row_not_raw_thinking_card(): + assert "data-activity-event-id=\"thinking-placeholder\"" in UI_JS + assert "Waiting on model" in UI_JS + assert "No tool activity has been reported yet." in UI_JS + assert "Waiting on tool result" in UI_JS + assert "_thinkingActivityNode(thinkingText, false)" in UI_JS + + +def test_tool_events_update_activity_timeline_and_summary(): + assert "Tool finished: ${toolName}" in UI_JS + assert "Running tool: ${toolName}" in UI_JS + assert "No recent activity for ${_formatActiveElapsedTimer(idleAge)}" in UI_JS + assert "Activity · Running" in UI_JS + assert "Working for ${label}" in UI_JS + + +def test_activity_status_rows_have_quiet_metadata_styling(): + assert ".agent-activity-status{" in STYLE_CSS + assert "grid-template-columns:18px minmax(0,1fr) auto" in STYLE_CSS + assert ".agent-activity-status-detail" in STYLE_CSS + assert ".agent-activity-status-time" in STYLE_CSS + assert ".agent-activity-status-error .agent-activity-status-label{color:var(--error);}" in STYLE_CSS From 68af4399a6e08688b317938032a66036ecb22796 Mon Sep 17 00:00:00 2001 From: nesquena-hermes <[email protected]> Date: Sun, 24 May 2026 16:14:04 +0000 Subject: [PATCH 3/3] Stamp CHANGELOG for v0.51.126 (Release CX / stage-batch8 / 2-PR batch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked PRs: - #2819 (humayunak) — kanban markdown full GFM rendering - #2847 (AJV20) — live activity timeline observable telemetry (squashed from 2) --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c144ceb5..d1a18913 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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-`

    ` 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