diff --git a/frontend/index.html b/frontend/index.html index 5fa22d0..caa99fe 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -149,6 +149,8 @@ body { font: 14px/1.5 system-ui, sans-serif; background: var(--bg); color: var(- .side-tab.active { color: var(--accent); border-bottom-color: var(--accent); } .map-item { padding: 3px 8px; cursor: pointer; border-radius: 4px; font-size: 13px; color: var(--text); word-break: break-word; } .map-item:hover { background: var(--bg-input); } +.map-files-toggle { color: var(--accent); cursor: pointer; } +.map-files-toggle .map-arrow { color: var(--text-muted); } /* Status bar */ .status-bar { display: flex; padding: 8px 16px; background: var(--bg-panel); border-top: 1px solid var(--bg-input); font-size: 13px; gap: 16px; flex-shrink: 0; } @@ -359,6 +361,7 @@ const STATE = { serverVersion: '', captureMode: '', deviceConnected: null, + mapExpanded: new Set(), }; const API_BASE = () => STATE.serverUrl; @@ -1037,6 +1040,8 @@ function buildTraceMap() { if (ins === 'c0') { i++; continue; } // skip GET RESPONSE if (ins && MAP_RW_INS.has(ins)) { // Split the run into per-file records (delimited by SELECT). + // Each READ/UPDATE op contributes its own resolved file, so + // SFI-addressed files (never SELECTed) are attributed correctly. const access = []; let cur = null; let j = i; @@ -1046,47 +1051,50 @@ function buildTraceMap() { if (!jins || !MAP_RW_INS.has(jins)) break; if (jins === 'a4') { if (cur) access.push(cur); - cur = { file: map_file_label(msgs[j]), hasRead: false, hasWrite: false, start: j, end: j }; - } else if (is_write_ins(jins)) { - if (!cur) cur = { file: map_file_label(msgs[j]), hasRead: false, hasWrite: false, start: j, end: j }; - cur.hasWrite = true; - cur.end = j; - } else { // SEARCH / READ - if (!cur) cur = { file: map_file_label(msgs[j]), hasRead: false, hasWrite: false, start: j, end: j }; - cur.hasRead = true; + cur = { files: [], selNote: map_file_label(msgs[j]), hasRead: false, hasWrite: false, start: j, end: j }; + } else { + if (!cur) cur = { files: [], selNote: null, hasRead: false, hasWrite: false, start: j, end: j }; + const f = map_file_label(msgs[j]); + if (f && !cur.files.includes(f)) cur.files.push(f); + if (is_write_ins(jins)) cur.hasWrite = true; else cur.hasRead = true; cur.end = j; } j++; } if (cur) access.push(cur); + // Records with no read/write ops (pure catalog SELECT) fall back to + // the selected file's note. + for (const rec of access) { + if (rec.files.length === 0 && rec.selNote) rec.files = [rec.selNote]; + } let k = 0; while (k < access.length) { const rec = access[k]; if (rec.hasRead && rec.hasWrite) { - groups.push({ label: t('Read/write:') + ' ' + (rec.file || '—'), start: rec.start, end: rec.end }); + groups.push({ kind: 'rw', files: rec.files, start: rec.start, end: rec.end }); k++; } else if (rec.hasWrite) { const files = []; let end = rec.end; let m = k; while (m < access.length && access[m].hasWrite && !access[m].hasRead) { - if (access[m].file && !files.includes(access[m].file)) files.push(access[m].file); + for (const f of access[m].files) if (f && !files.includes(f)) files.push(f); end = access[m].end; m++; } - groups.push({ label: t('Writes:') + ' ' + (files.join(', ') || '—'), start: rec.start, end }); + groups.push({ kind: 'write', files, start: rec.start, end }); k = m; } else { const files = []; let end = rec.end; let m = k; while (m < access.length && !access[m].hasWrite) { - if (access[m].file && !files.includes(access[m].file)) files.push(access[m].file); + for (const f of access[m].files) if (f && !files.includes(f)) files.push(f); end = access[m].end; m++; } - groups.push({ label: t('Reads:') + ' ' + (files.join(', ') || '—'), start: rec.start, end }); + groups.push({ kind: 'read', files, start: rec.start, end }); k = m; } } @@ -1113,9 +1121,13 @@ function renderSidePanel() { ).join('') || t('No commands'); const mapEl = el('type-map'); - mapEl.innerHTML = STATE.traceMap.map(g => - `