From 96998b328e9cfee96a0779424be9c4fc6efa304b Mon Sep 17 00:00:00 2001 From: Nikolas Syring Date: Sat, 3 Jan 2026 19:35:26 +0100 Subject: [PATCH] feat: Parse and display status/queue control tags in chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add parseStatusTags() to extract tags - Add parseQueueTags() to extract tags - StatusInfo component shows agent status changes as info badges - QueueInfo component indicates when reading queued messages - Control-only messages render as centered info lines - Control tags with content show tags above the message 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/src/components/MessageList.jsx | 108 +++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/MessageList.jsx b/frontend/src/components/MessageList.jsx index 4ead612..3c095e4 100644 --- a/frontend/src/components/MessageList.jsx +++ b/frontend/src/components/MessageList.jsx @@ -130,6 +130,91 @@ function parseThinking(text) { return { content, thinking }; } +// Helper to extract status tags from content +function parseStatusTags(text) { + if (!text || typeof text !== 'string') return { content: text || '', statusTags: [] }; + + const statusTags = []; + // Match self-closing tags with attributes + const statusRegex = /]*)\/?>/g; + let match; + while ((match = statusRegex.exec(text)) !== null) { + const attrsStr = match[1]; + const attrs = {}; + // Parse attributes like mode="working" project="claude-web-ui" + const attrRegex = /(\w+)="([^"]*)"/g; + let attrMatch; + while ((attrMatch = attrRegex.exec(attrsStr)) !== null) { + attrs[attrMatch[1]] = attrMatch[2]; + } + if (Object.keys(attrs).length > 0) { + statusTags.push(attrs); + } + } + + const content = text.replace(/]*\/?>/g, '').trim(); + return { content, statusTags }; +} + +// Helper to extract queue-next tags from content +function parseQueueTags(text) { + if (!text || typeof text !== 'string') return { content: text || '', queueTags: 0 }; + + // Count or tags + const queueRegex = //g; + const matches = text.match(queueRegex); + const queueTags = matches ? matches.length : 0; + + const content = text.replace(//g, '').trim(); + return { content, queueTags }; +} + +// Status info display component (non-collapsible info badge) +// Rendered as a centered info line, similar to system messages +function StatusInfo({ statusTags }) { + if (!statusTags || statusTags.length === 0) return null; + + // Mode display names and colors + const modeConfig = { + ready: { label: 'Ready', color: 'text-green-400' }, + working: { label: 'Working', color: 'text-yellow-400' }, + focus: { label: 'Focus', color: 'text-orange-400' }, + }; + + return ( +
+ {statusTags.map((tag, idx) => { + const config = modeConfig[tag.mode] || { label: tag.mode, color: 'text-dark-400' }; + return ( +
+ + Status set to + {config.label} + {tag.project && ( + ({tag.project}) + )} +
+ ); + })} +
+ ); +} + +// Queue info display component +// Shows when agent is reading next queued message +function QueueInfo({ count }) { + if (!count || count === 0) return null; + + return ( +
+
+ + Reading next queued message +
+
+ ); +} + // Collapsible thinking block component function ThinkingBlock({ thinking }) { const [show, setShow] = useState(false); @@ -535,10 +620,14 @@ const Message = memo(function Message({ message, onSendMessage, hostConfig }) { if (type === 'assistant') { const withoutReminders = parseSystemReminders(content); const withoutThinking = parseThinking(withoutReminders.content); + const withoutStatus = parseStatusTags(withoutThinking.content); + const withoutQueue = parseQueueTags(withoutStatus.content); return { - content: withoutThinking.content, + content: withoutQueue.content, reminders: withoutReminders.reminders, thinking: withoutThinking.thinking, + statusTags: withoutStatus.statusTags, + queueTags: withoutQueue.queueTags, }; } return null; @@ -593,7 +682,19 @@ const Message = memo(function Message({ message, onSendMessage, hostConfig }) { ); case 'assistant': { - const { content: cleanContent, reminders, thinking } = parsedContent; + const { content: cleanContent, reminders, thinking, statusTags, queueTags } = parsedContent; + + // If only control tags (no actual content), show just the info components + const hasOnlyControlTags = !cleanContent.trim() && (statusTags.length > 0 || queueTags > 0); + if (hasOnlyControlTags) { + return ( + <> + {statusTags.length > 0 && } + {queueTags > 0 && } + + ); + } + return (
{assistantAvatar?.startsWith('/') ? ( @@ -609,6 +710,9 @@ const Message = memo(function Message({ message, onSendMessage, hostConfig }) { )}
{assistantName}
+ {/* Control tags shown above the message if there's also content */} + {statusTags.length > 0 && cleanContent.trim() && } + {queueTags > 0 && cleanContent.trim() && }