feat: Parse and display status/queue control tags in chat
- Add parseStatusTags() to extract <status mode="..."/> tags - Add parseQueueTags() to extract <queue-next/> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <status .../> tags with attributes
|
||||
const statusRegex = /<status\s+([^/>]*)\/?>/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(/<status\s+[^/>]*\/?>/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 <queue-next/> or <queue-next /> tags
|
||||
const queueRegex = /<queue-next\s*\/?>/g;
|
||||
const matches = text.match(queueRegex);
|
||||
const queueTags = matches ? matches.length : 0;
|
||||
|
||||
const content = text.replace(/<queue-next\s*\/?>/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 (
|
||||
<div className="flex items-center gap-2 justify-center message-enter">
|
||||
{statusTags.map((tag, idx) => {
|
||||
const config = modeConfig[tag.mode] || { label: tag.mode, color: 'text-dark-400' };
|
||||
return (
|
||||
<div key={idx} className="flex items-center gap-1.5">
|
||||
<Info className="w-3.5 h-3.5 text-dark-500" />
|
||||
<span className="text-xs text-dark-500">Status set to</span>
|
||||
<span className={`text-xs font-medium ${config.color}`}>{config.label}</span>
|
||||
{tag.project && (
|
||||
<span className="text-xs text-dark-600">({tag.project})</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Queue info display component
|
||||
// Shows when agent is reading next queued message
|
||||
function QueueInfo({ count }) {
|
||||
if (!count || count === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 justify-center message-enter">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Info className="w-3.5 h-3.5 text-dark-500" />
|
||||
<span className="text-xs text-dark-500">Reading next queued message</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 && <StatusInfo statusTags={statusTags} />}
|
||||
{queueTags > 0 && <QueueInfo count={queueTags} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 message-enter">
|
||||
{assistantAvatar?.startsWith('/') ? (
|
||||
@@ -609,6 +710,9 @@ const Message = memo(function Message({ message, onSendMessage, hostConfig }) {
|
||||
)}
|
||||
<div className="max-w-[85%]">
|
||||
<div className="text-xs text-dark-500 mb-1">{assistantName}</div>
|
||||
{/* Control tags shown above the message if there's also content */}
|
||||
{statusTags.length > 0 && cleanContent.trim() && <StatusInfo statusTags={statusTags} />}
|
||||
{queueTags > 0 && cleanContent.trim() && <QueueInfo count={queueTags} />}
|
||||
<div className="bg-dark-850 rounded-lg rounded-tl-none p-4 border border-dark-700 claude-message inline-block">
|
||||
<ThinkingBlock thinking={thinking} />
|
||||
<ReactMarkdown
|
||||
|
||||
Reference in New Issue
Block a user