From 1b4a88c72f2f70acd662f0dfd7a3922d147612ea Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Fri, 17 Apr 2026 22:14:55 -0600 Subject: [PATCH 1/2] initial version of structured messages support --- src/data.js | 75 ++++++++++++++++++++++++++++++++++++++++- src/frontend/detail.js | 49 ++++++++++++++++++++++++++- src/frontend/styles.css | 44 ++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 2 deletions(-) diff --git a/src/data.js b/src/data.js index 34ecf71..a9d0992 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,76 @@ function extractContent(raw) { return String(raw); } +const CODEX_STRUCTURED_MESSAGE_FIELDS = { + user_shell_command: ['command', 'result'], + user_action: ['context', 'action', 'results'], +}; + +function normalizeStructuredField(value) { + return typeof value === 'string' ? value.trim() : ''; +} + +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, requiredTags) { + if (!body || !Array.isArray(requiredTags) || requiredTags.length === 0) return null; + + const fields = {}; + 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 (!requiredTags.includes(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 !== requiredTags.length) return null; + + for (const tag of requiredTags) { + if (!fields[tag]) return null; + } + + return fields; +} + +function parseCodexStructuredMessage(content) { + const wrapped = parseStructuredWrapper(content); + if (!wrapped) return null; + + const requiredTags = CODEX_STRUCTURED_MESSAGE_FIELDS[wrapped.tag]; + if (!requiredTags) return null; + + const fields = parseStructuredFields(wrapped.body, requiredTags); + if (!fields) return null; + + return { + agent: 'codex', + kind: wrapped.tag, + fields, + }; +} + +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 { From 904c7b12158d01c183996aaa3dfa32589021c60d Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Fri, 17 Apr 2026 22:29:19 -0600 Subject: [PATCH 2/2] truncation for structured messages --- src/data.js | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/data.js b/src/data.js index a9d0992..04be80a 100644 --- a/src/data.js +++ b/src/data.js @@ -2486,14 +2486,27 @@ function extractContent(raw) { } const CODEX_STRUCTURED_MESSAGE_FIELDS = { - user_shell_command: ['command', 'result'], - user_action: ['context', 'action', 'results'], + 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; @@ -2502,10 +2515,11 @@ function parseStructuredWrapper(content) { return { tag: match[1], body: match[2] }; } -function parseStructuredFields(body, requiredTags) { - if (!body || !Array.isArray(requiredTags) || requiredTags.length === 0) return null; +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; @@ -2514,7 +2528,7 @@ function parseStructuredFields(body, requiredTags) { if (body.slice(cursor, match.index).trim()) return null; const tag = match[1]; - if (!requiredTags.includes(tag) || fields[tag] !== undefined) return null; + if (!allowedFields.has(tag) || fields[tag] !== undefined) return null; const value = normalizeStructuredField(match[2]); if (!value) return null; @@ -2523,29 +2537,37 @@ function parseStructuredFields(body, requiredTags) { } if (body.slice(cursor).trim()) return null; - if (Object.keys(fields).length !== requiredTags.length) return null; + if (Object.keys(fields).length !== fieldDescriptors.length) return null; - for (const tag of requiredTags) { - if (!fields[tag]) 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 requiredTags = CODEX_STRUCTURED_MESSAGE_FIELDS[wrapped.tag]; - if (!requiredTags) return null; + const fieldDescriptors = CODEX_STRUCTURED_MESSAGE_FIELDS[wrapped.tag]; + if (!fieldDescriptors) return null; - const fields = parseStructuredFields(wrapped.body, requiredTags); + const fields = parseStructuredFields(wrapped.body, fieldDescriptors); if (!fields) return null; return { agent: 'codex', kind: wrapped.tag, - fields, + fields: applyStructuredFieldThresholds(fields, fieldDescriptors), }; }