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
165 changes: 153 additions & 12 deletions src/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -2750,11 +2750,16 @@ function loadSessionDetail(sessionId, project) {

if (found.format === 'claude') {
if (entry.type === 'user' || entry.type === 'assistant') {
const content = extractContent((entry.message || {}).content);
const rawContent = (entry.message || {}).content;
const content = extractContent(rawContent);
if (content) {
const msg = { role: entry.type, content: content.slice(0, 2000), uuid: entry.uuid || '' };
if (entry.type === 'user') {
if (isFilteredClaudeStructuredMessage(content)) continue;
const structured = parseStructuredMessage('claude', entry.type, content, entry);
if (structured) msg.structured = structured;
}
if (entry.type === 'assistant') {
const rawContent = (entry.message || {}).content;
if (Array.isArray(rawContent)) {
const tools = extractTools(rawContent);
if (tools.length > 0) msg.tools = tools;
Expand All @@ -2763,6 +2768,20 @@ function loadSessionDetail(sessionId, project) {
messages.push(msg);
}
}
if (entry.type === 'queue-operation') {
const content = extractContent(entry.content);
if (content) {
const structured = parseStructuredMessage('claude', 'queue', content, entry);
if (structured) {
messages.push({
role: 'queue',
content: content.slice(0, 2000),
uuid: entry.uuid || '',
structured: structured,
});
}
}
}
} else {
// Codex format: response_item with payload
if (entry.type === 'response_item' && entry.payload) {
Expand All @@ -2772,7 +2791,7 @@ function loadSessionDetail(sessionId, project) {
const content = extractContent(entry.payload.content);
if (content && !isSystemMessage(content)) {
const msg = { role: role, content: content.slice(0, 2000), uuid: '' };
const structured = parseStructuredMessage('codex', role, content);
const structured = parseStructuredMessage('codex', role, content, entry);
if (structured) msg.structured = structured;
messages.push(msg);
}
Expand Down Expand Up @@ -3278,6 +3297,10 @@ function extractContent(raw) {
return String(raw);
}

const STRUCTURED_TAG_PATTERN = '[a-z_][a-z0-9_-]*';
const STRUCTURED_WRAPPER_RE = new RegExp('^<(' + STRUCTURED_TAG_PATTERN + ')>\\s*([\\s\\S]*?)\\s*</\\1>$', 'i');
const STRUCTURED_FIELD_RE = new RegExp('<(' + STRUCTURED_TAG_PATTERN + ')>([\\s\\S]*?)</\\1>', 'ig');
const FILTERED_CLAUDE_STRUCTURED_TAGS = new Set(['local-command-caveat']);
const CODEX_STRUCTURED_MESSAGE_FIELDS = {
user_shell_command: [
{ field: 'command', max_length: 0 },
Expand All @@ -3290,6 +3313,36 @@ const CODEX_STRUCTURED_MESSAGE_FIELDS = {
],
};

const CLAUDE_STRUCTURED_MESSAGE_FIELDS = {
slash_command: [
{ tag: 'command-name', field: 'command_name', max_length: 200 },
{ tag: 'command-message', field: 'command_message', max_length: 200 },
{ tag: 'command-args', field: 'command_args', max_length: 500, required: false },
],
bash_result: [
{ tag: 'bash-stdout', field: 'stdout', max_length: 1500, required: false },
{ tag: 'bash-stderr', field: 'stderr', max_length: 1500, required: false },
],
task_notification: [
{ tag: 'task-id', field: 'task_id', max_length: 120 },
{ tag: 'tool-use-id', field: 'tool_use_id', max_length: 120 },
{ tag: 'output-file', field: 'output_file', max_length: 500 },
{ tag: 'status', field: 'status', max_length: 40 },
{ tag: 'summary', field: 'summary', max_length: 300 },
{ tag: 'result', field: 'result', max_length: 1500, required: false },
{ tag: 'usage', field: 'usage', max_length: 0, required: false },
],
task_notification_monitor: [
{ tag: 'task-id', field: 'task_id', max_length: 120 },
{ tag: 'summary', field: 'summary', max_length: 300 },
{ tag: 'event', field: 'event', max_length: 1500 },
],
task_usage: [
{ tag: 'total_tokens', field: 'total_tokens', max_length: 40 },
{ tag: 'tool_uses', field: 'tool_uses', max_length: 40 },
{ tag: 'duration_ms', field: 'duration_ms', max_length: 40 },
],
};
function normalizeStructuredField(value) {
return typeof value === 'string' ? value.trim() : '';
}
Expand All @@ -3303,7 +3356,7 @@ function truncateStructuredField(value, maxLength) {
function parseStructuredWrapper(content) {
const trimmed = typeof content === 'string' ? content.trim() : '';
if (!trimmed) return null;
const match = trimmed.match(/^<([a-z_][a-z0-9_]*)>\s*([\s\S]*?)\s*<\/\1>$/i);
const match = trimmed.match(STRUCTURED_WRAPPER_RE);
if (!match) return null;
return { tag: match[1], body: match[2] };
}
Expand All @@ -3312,27 +3365,31 @@ function parseStructuredFields(body, fieldDescriptors) {
if (!body || !Array.isArray(fieldDescriptors) || fieldDescriptors.length === 0) return null;

const fields = {};
const allowedFields = new Set(fieldDescriptors.map(function(def) { return def.field; }));
const pattern = /<([a-z_][a-z0-9_]*)>([\s\S]*?)<\/\1>/ig;
const fieldsByTag = new Map(fieldDescriptors.map(function(def) {
return [def.tag || def.field, def];
}));
// Clone the global regex so each parse starts with a clean lastIndex.
const pattern = new RegExp(STRUCTURED_FIELD_RE.source, STRUCTURED_FIELD_RE.flags);
let cursor = 0;
let match;

while ((match = pattern.exec(body))) {
if (body.slice(cursor, match.index).trim()) return null;

const tag = match[1];
if (!allowedFields.has(tag) || fields[tag] !== undefined) return null;
const def = fieldsByTag.get(tag);
if (!def || fields[def.field] !== undefined) return null;

const value = normalizeStructuredField(match[2]);
if (!value) return null;
fields[tag] = value;
if (!value && def.required !== false) return null;
fields[def.field] = value;
cursor = match.index + match[0].length;
}

if (body.slice(cursor).trim()) return null;
if (Object.keys(fields).length !== fieldDescriptors.length) return null;

for (const def of fieldDescriptors) {
if (def.required === false) continue;
if (!fields[def.field]) return null;
}

Expand All @@ -3347,6 +3404,15 @@ function applyStructuredFieldThresholds(fields, fieldDescriptors) {
return result;
}

function parseStructuredSingleTag(content, tag, fieldName, maxLength) {
const wrapped = parseStructuredWrapper(content);
if (!wrapped || wrapped.tag !== tag) return null;
const value = normalizeStructuredField(wrapped.body);
if (!value) return null;
const fields = {};
fields[fieldName] = truncateStructuredField(value, maxLength || 0);
return fields;
}
function parseCodexStructuredMessage(content) {
const wrapped = parseStructuredWrapper(content);
if (!wrapped) return null;
Expand All @@ -3364,9 +3430,78 @@ function parseCodexStructuredMessage(content) {
};
}

function parseStructuredMessage(agent, role, content) {
if (role !== 'user' || !content) return null;
function isFilteredClaudeStructuredMessage(content) {
const wrapped = parseStructuredWrapper(content);
return !!(wrapped && FILTERED_CLAUDE_STRUCTURED_TAGS.has(wrapped.tag));
}

function parseClaudeTaskNotification(content) {
const wrapped = parseStructuredWrapper(content);
if (!wrapped || wrapped.tag !== 'task-notification') return null;

let fieldDescriptors = CLAUDE_STRUCTURED_MESSAGE_FIELDS.task_notification;
let fields = parseStructuredFields(wrapped.body, fieldDescriptors);
if (!fields) {
fieldDescriptors = CLAUDE_STRUCTURED_MESSAGE_FIELDS.task_notification_monitor;
fields = parseStructuredFields(wrapped.body, fieldDescriptors);
}
if (!fields) return null;

const parsedFields = applyStructuredFieldThresholds(fields, fieldDescriptors);
if (parsedFields.usage) {
const usageFields = parseStructuredFields(parsedFields.usage, CLAUDE_STRUCTURED_MESSAGE_FIELDS.task_usage);
parsedFields.usage = usageFields
? applyStructuredFieldThresholds(usageFields, CLAUDE_STRUCTURED_MESSAGE_FIELDS.task_usage)
: null;
}

return {
agent: 'claude',
kind: 'task_notification',
fields: parsedFields,
};
}

function parseClaudeStructuredMessage(entry, content) {
if (!content) return null;

const slashCommandFields = parseStructuredFields(content, CLAUDE_STRUCTURED_MESSAGE_FIELDS.slash_command);
if (slashCommandFields) {
return {
agent: 'claude',
kind: 'slash_command',
fields: applyStructuredFieldThresholds(slashCommandFields, CLAUDE_STRUCTURED_MESSAGE_FIELDS.slash_command),
};
}

const bashInputFields = parseStructuredSingleTag(content, 'bash-input', 'input', 0);
if (bashInputFields) {
return { agent: 'claude', kind: 'bash_input', fields: bashInputFields };
}

const bashResultFields = parseStructuredFields(content, CLAUDE_STRUCTURED_MESSAGE_FIELDS.bash_result);
if (bashResultFields) {
return {
agent: 'claude',
kind: 'bash_result',
fields: applyStructuredFieldThresholds(bashResultFields, CLAUDE_STRUCTURED_MESSAGE_FIELDS.bash_result),
};
}

const localCommandStdoutFields = parseStructuredSingleTag(content, 'local-command-stdout', 'output', 1500);
if (localCommandStdoutFields) {
return { agent: 'claude', kind: 'local_command_stdout', fields: localCommandStdoutFields };
}

return parseClaudeTaskNotification(content);
}

function parseStructuredMessage(agent, role, content, entry) {
if (!content) return null;
if (agent === 'codex') return parseCodexStructuredMessage(content);
if (agent === 'claude' && (role === 'user' || role === 'queue')) {
return parseClaudeStructuredMessage(entry, content);
}
return null;
}

Expand Down Expand Up @@ -4951,5 +5086,11 @@ module.exports = {
normalizeProjectPath,
shortenHomePath,
detectWindowsWslHomes,
parseStructuredWrapper,
parseStructuredFields,
parseClaudeTaskNotification,
parseClaudeStructuredMessage,
parseStructuredMessage,
isFilteredClaudeStructuredMessage,
},
};
94 changes: 90 additions & 4 deletions src/frontend/detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ function closeDetail() {
var structuredMessageRenderers = {
'codex:user_shell_command': renderCodexUserShellCommand,
'codex:user_action': renderCodexUserAction,
'claude:slash_command': renderClaudeSlashCommand,
'claude:bash_input': renderClaudeBashInput,
'claude:bash_result': renderClaudeBashResult,
'claude:local_command_stdout': renderClaudeLocalCommandStdout,
'claude:task_notification': renderClaudeTaskNotification,
};

function renderStructuredBlock(text) {
Expand All @@ -203,6 +208,10 @@ function renderStructuredSection(label, text) {
return '<div class="structured-section-label">' + escHtml(label) + '</div>' + renderStructuredBlock(text);
}

function renderStructuredMeta(label, text) {
if (!text) return '';
return '<div class="structured-context"><span class="structured-context-label">' + escHtml(label) + '</span> ' + escHtml(text) + '</div>';
}
function renderCodexUserShellCommand(fields) {
if (!fields || !fields.command || !fields.result) return '';
var html = '<div class="msg-content structured-message">';
Expand All @@ -224,6 +233,78 @@ function renderCodexUserAction(fields) {
return html;
}

function renderClaudeSlashCommand(fields) {
if (!fields || !fields.command_name || !fields.command_message) return '';
var html = '<div class="msg-content structured-message">';
html += '<div class="structured-title">Slash Command:</div>';
html += renderStructuredMeta('Command:', fields.command_name);
html += renderStructuredMeta('Message:', fields.command_message);
if (fields.command_args) html += renderStructuredSection('Args:', fields.command_args);
html += '</div>';
return html;
}

function renderClaudeBashInput(fields) {
if (!fields || !fields.input) return '';
var html = '<div class="msg-content structured-message">';
html += '<div class="structured-title">Bash Input:</div>';
html += renderStructuredSection('Command:', fields.input);
html += '</div>';
return html;
}

function renderClaudeBashResult(fields) {
if (!fields || (!fields.stdout && !fields.stderr)) return '';
var html = '<div class="msg-content structured-message">';
html += '<div class="structured-title">Bash Result:</div>';
if (fields.stdout) html += renderStructuredSection('Stdout:', fields.stdout);
if (fields.stderr) html += renderStructuredSection('Stderr:', fields.stderr);
html += '</div>';
return html;
}

function renderClaudeLocalCommandStdout(fields) {
if (!fields || !fields.output) return '';
var html = '<div class="msg-content structured-message">';
html += '<div class="structured-title">Local Command Output:</div>';
html += renderStructuredSection('Output:', fields.output);
html += '</div>';
return html;
}

function renderClaudeTaskUsage(usage) {
if (!usage) return '';
var parts = [];
if (usage.total_tokens) parts.push(usage.total_tokens + ' tokens');
if (usage.tool_uses) parts.push(usage.tool_uses + ' tools');
if (usage.duration_ms) parts.push(usage.duration_ms + ' ms');
if (parts.length === 0) return '';
return renderStructuredMeta('Usage:', parts.join(', '));
}

function sanitizeClassToken(value, fallback) {
var token = String(value || '').toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
return token || (fallback || 'unknown');
}

function renderClaudeTaskNotification(fields) {
if (!fields || (!fields.summary && !fields.task_id && !fields.event && !fields.result)) return '';
var html = '<div class="msg-content structured-message">';
html += '<div class="structured-title">Task Notification:</div>';
if (fields.status) {
var statusClass = sanitizeClassToken(fields.status, 'unknown');
html += '<div class="structured-task-status"><span class="structured-context-label">Status:</span> <span class="structured-status structured-status-' + statusClass + '">' + escHtml(fields.status) + '</span></div>';
}
if (fields.summary) html += renderStructuredMeta('Summary:', fields.summary);
html += renderStructuredMeta('Task ID:', fields.task_id);
html += renderStructuredMeta('Tool Use ID:', fields.tool_use_id);
if (fields.output_file) html += renderStructuredSection('Output File:', fields.output_file);
if (fields.event) html += renderStructuredSection('Event:', fields.event);
if (fields.result) html += renderStructuredSection('Result:', fields.result);
html += renderClaudeTaskUsage(fields.usage);
html += '</div>';
return html;
}
function renderMessageContent(message) {
var structured = message && message.structured;
if (structured && structured.agent && structured.kind) {
Expand All @@ -237,6 +318,12 @@ function renderMessageContent(message) {
return '<div class="msg-content msg-content-plain">' + escHtml((message && message.content) || '') + '</div>';
}

function getMessageRoleMeta(role) {
if (role === 'user') return { className: 'msg-user', label: 'You' };
if (role === 'queue') return { className: 'msg-system', label: 'Queue' };
if (role === 'system') return { className: 'msg-system', label: 'System' };
return { className: 'msg-assistant', label: 'Assistant' };
}
function renderDetailMessages(container, messages) {
var sort = localStorage.getItem('codedash-msg-sort') || 'asc';
var sorted = sort === 'desc' ? messages.slice().reverse() : messages;
Expand All @@ -246,12 +333,11 @@ function renderDetailMessages(container, messages) {
msgsHtml += '<button class="theme-btn" onclick="toggleMsgSort()" title="Toggle sort order" style="font-size:11px;padding:3px 10px">' + btnLabel + '</button>';
msgsHtml += '</div>';
sorted.forEach(function(m) {
var roleClass = m.role === 'user' ? 'msg-user' : 'msg-assistant';
var roleLabel = m.role === 'user' ? 'You' : 'Assistant';
var roleMeta = getMessageRoleMeta(m.role);
var hasTools = m.tools && m.tools.length > 0;
msgsHtml += '<div class="message ' + roleClass + (hasTools ? ' has-tools' : '') + '">';
msgsHtml += '<div class="message ' + roleMeta.className + (hasTools ? ' has-tools' : '') + '">';
msgsHtml += '<div class="msg-inner">';
msgsHtml += '<div class="msg-role">' + roleLabel + '</div>';
msgsHtml += '<div class="msg-role">' + roleMeta.label + '</div>';
msgsHtml += renderMessageContent(m);
msgsHtml += '</div>';
if (hasTools) {
Expand Down
Loading
Loading