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
97 changes: 96 additions & 1 deletion src/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -2101,7 +2101,10 @@ function loadSessionDetail(sessionId, project) {
if (role === 'user' || role === 'assistant') {
const content = extractContent(entry.payload.content);
if (content && !isSystemMessage(content)) {
messages.push({ role: role, content: content.slice(0, 2000), uuid: '' });
const msg = { role: role, content: content.slice(0, 2000), uuid: '' };
const structured = parseStructuredMessage('codex', role, content);
if (structured) msg.structured = structured;
messages.push(msg);
}
}
// Codex function_call → attach as tool to last assistant message
Expand Down Expand Up @@ -2482,6 +2485,98 @@ function extractContent(raw) {
return String(raw);
}

const CODEX_STRUCTURED_MESSAGE_FIELDS = {
user_shell_command: [
{ field: 'command', max_length: 0 },
{ field: 'result', max_length: 1500 },
],
user_action: [
{ field: 'context', max_length: 200 },
{ field: 'action', max_length: 0 },
{ field: 'results', max_length: 1500 },
],
};

function normalizeStructuredField(value) {
return typeof value === 'string' ? value.trim() : '';
}

function truncateStructuredField(value, maxLength) {
if (typeof value !== 'string') return '';
if (!maxLength || maxLength < 0 || value.length <= maxLength) return value;
return value.slice(0, 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);
if (!match) return null;
return { tag: match[1], body: match[2] };
}

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;
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 value = normalizeStructuredField(match[2]);
if (!value) return null;
fields[tag] = 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 (!fields[def.field]) return null;
}

return fields;
}

function applyStructuredFieldThresholds(fields, fieldDescriptors) {
const result = {};
for (const def of fieldDescriptors) {
result[def.field] = truncateStructuredField(fields[def.field], def.max_length || 0);
}
return result;
}

function parseCodexStructuredMessage(content) {
const wrapped = parseStructuredWrapper(content);
if (!wrapped) return null;

const fieldDescriptors = CODEX_STRUCTURED_MESSAGE_FIELDS[wrapped.tag];
if (!fieldDescriptors) return null;

const fields = parseStructuredFields(wrapped.body, fieldDescriptors);
if (!fields) return null;

return {
agent: 'codex',
kind: wrapped.tag,
fields: applyStructuredFieldThresholds(fields, fieldDescriptors),
};
}

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

// Extract MCP/Skill tool_use blocks from a Claude assistant message content array.
// Returns deduplicated array of { type, server, tool } or { type, skill }.
function extractTools(contentBlocks) {
Expand Down
49 changes: 48 additions & 1 deletion src/frontend/detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,53 @@ function closeDetail() {
if (overlay) overlay.classList.remove('open');
}

var structuredMessageRenderers = {
'codex:user_shell_command': renderCodexUserShellCommand,
'codex:user_action': renderCodexUserAction,
};

function renderStructuredBlock(text) {
return '<pre class="structured-block">' + escHtml(text || '') + '</pre>';
}

function renderStructuredSection(label, text) {
return '<div class="structured-section-label">' + escHtml(label) + '</div>' + renderStructuredBlock(text);
}

function renderCodexUserShellCommand(fields) {
if (!fields || !fields.command || !fields.result) return '';
var html = '<div class="msg-content structured-message">';
html += '<div class="structured-title">User Shell Command:</div>';
html += renderStructuredSection('Command:', fields.command);
html += renderStructuredSection('Result:', fields.result);
html += '</div>';
return html;
}

function renderCodexUserAction(fields) {
if (!fields || !fields.context || !fields.action || !fields.results) return '';
var html = '<div class="msg-content structured-message">';
html += '<div class="structured-title">User Action:</div>';
html += '<div class="structured-context"><span class="structured-context-label">Context:</span> ' + escHtml(fields.context) + '</div>';
html += renderStructuredSection('Action:', fields.action);
html += renderStructuredSection('Results:', fields.results);
html += '</div>';
return html;
}

function renderMessageContent(message) {
var structured = message && message.structured;
if (structured && structured.agent && structured.kind) {
var key = structured.agent + ':' + structured.kind;
var renderer = structuredMessageRenderers[key];
if (renderer) {
var rendered = renderer(structured.fields || {});
if (rendered) return rendered;
}
}
return '<div class="msg-content msg-content-plain">' + escHtml((message && message.content) || '') + '</div>';
}

function renderDetailMessages(container, messages) {
var sort = localStorage.getItem('codedash-msg-sort') || 'asc';
var sorted = sort === 'desc' ? messages.slice().reverse() : messages;
Expand All @@ -199,7 +246,7 @@ function renderDetailMessages(container, messages) {
msgsHtml += '<div class="message ' + roleClass + (hasTools ? ' has-tools' : '') + '">';
msgsHtml += '<div class="msg-inner">';
msgsHtml += '<div class="msg-role">' + roleLabel + '</div>';
msgsHtml += '<div class="msg-content">' + escHtml(m.content) + '</div>';
msgsHtml += renderMessageContent(m);
msgsHtml += '</div>';
if (hasTools) {
msgsHtml += '<div class="msg-tools">';
Expand Down
44 changes: 44 additions & 0 deletions src/frontend/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1542,6 +1542,50 @@ body {
white-space: pre-wrap;
}

.msg-content-plain {
white-space: pre-wrap;
}

.structured-message {
display: flex;
flex-direction: column;
gap: 8px;
white-space: normal;
}

.structured-title,
.structured-section-label {
font-size: 12px;
font-weight: 700;
color: var(--text-primary);
}

.structured-context {
font-size: 13px;
line-height: 1.5;
color: var(--text-secondary);
}

.structured-context-label {
font-weight: 700;
color: var(--text-primary);
}

.structured-block {
margin: 0;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-primary);
font-family: monospace;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
overflow-x: auto;
}

/* ── Commits ────────────────────────────────────────────────── */

.commit-item {
Expand Down
Loading