diff --git a/src/data.js b/src/data.js index 34ecf71..04be80a 100644 --- a/src/data.js +++ b/src/data.js @@ -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 @@ -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) { diff --git a/src/frontend/detail.js b/src/frontend/detail.js index 0f2ef85..c17abbb 100644 --- a/src/frontend/detail.js +++ b/src/frontend/detail.js @@ -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 '
' + escHtml(text || '') + '
'; +} + +function renderStructuredSection(label, text) { + return '
' + escHtml(label) + '
' + renderStructuredBlock(text); +} + +function renderCodexUserShellCommand(fields) { + if (!fields || !fields.command || !fields.result) return ''; + var html = '
'; + html += '
User Shell Command:
'; + html += renderStructuredSection('Command:', fields.command); + html += renderStructuredSection('Result:', fields.result); + html += '
'; + return html; +} + +function renderCodexUserAction(fields) { + if (!fields || !fields.context || !fields.action || !fields.results) return ''; + var html = '
'; + html += '
User Action:
'; + html += '
Context: ' + escHtml(fields.context) + '
'; + html += renderStructuredSection('Action:', fields.action); + html += renderStructuredSection('Results:', fields.results); + html += '
'; + 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 '
' + escHtml((message && message.content) || '') + '
'; +} + function renderDetailMessages(container, messages) { var sort = localStorage.getItem('codedash-msg-sort') || 'asc'; var sorted = sort === 'desc' ? messages.slice().reverse() : messages; @@ -199,7 +246,7 @@ function renderDetailMessages(container, messages) { msgsHtml += '
'; msgsHtml += '
'; msgsHtml += '
' + roleLabel + '
'; - msgsHtml += '
' + escHtml(m.content) + '
'; + msgsHtml += renderMessageContent(m); msgsHtml += '
'; if (hasTools) { msgsHtml += '
'; diff --git a/src/frontend/styles.css b/src/frontend/styles.css index d810a4a..06560f4 100644 --- a/src/frontend/styles.css +++ b/src/frontend/styles.css @@ -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 {