Skip to content

Commit acd1182

Browse files
AlsoTheZv3nclaude
andcommitted
feat: add /export-with-traces command
Adds a /export-with-traces slash command that downloads the current conversation's tool chain as Markdown, sourced from the trace store (trace/recorder.js -> IndexedDB) instead of the live message array, which is compacted, enriched and wrapped. /export stays messages-only and is byte-identical to upstream; this is a separate, additive command. A new pure serializer (trace-export.js, identical across chrome and firefox) renders per run: the submitted user message as the turn header, the planner turn (labelled, with the model's json code-fence unwrapped so it renders as one block), assistant prose, and each tool call as name(args) -> result, with failed results flagged and the recorder's truncation marker preserved. Screenshots, notes and vision sub-calls are recorded but not rendered; the file's footer says so and points to the Traces page. The handler hydrates the tab's conversation id from storage.session before reading it, so export survives service-worker eviction. A browser restart clears storage.session and is a documented known limit. New UI strings are localized across all 16 locales and both builds; the 15 non-English translations are machine-generated and carry no interpolation placeholders. Tests: a pure-serializer suite (event ordering, failure marker, recorder truncation, screenshot omission, planner label and fence unwrap, footer) plus a chrome/firefox byte-parity assertion. 775 passed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7f35071 commit acd1182

41 files changed

Lines changed: 543 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/chrome/src/agent/agent.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
PDF_PASSTHROUGH_MAX_BYTES,
2727
} from './pdf-tools.js';
2828
import * as trace from '../trace/recorder.js';
29+
import { tracesToMarkdown } from './trace-export.js';
2930
import { solveCaptcha, detectCaptcha, injectToken } from './captcha-solver.js';
3031
import { getRecordingStateFresh as recorderStateFresh } from '../recorder/host.js';
3132
import { Capability, CAPABILITY_LABEL, capabilitiesFor, requiredHosts, frameHostMatches, isNetworkMutation, normalizeHost, PermissionManager, UNTRUSTED_CONTENT_TOOLS } from './permission-gate.js';
@@ -6307,6 +6308,38 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
63076308
return idx >= 0 ? this._messageText(messages[idx]?.content) : '';
63086309
}
63096310

6311+
/**
6312+
* Serialize a tab's conversation to Markdown for /export-with-traces, sourced
6313+
* from the trace store (compaction-immune, raw structured results) — NOT from
6314+
* this.conversations. Hydrates first so it works across service-worker restarts.
6315+
* Returns { ok, markdown|null, turnCount, reason }: reason 'no-conversation', or
6316+
* 'no-traces' (tracing off / nothing recorded) so the UI can say so instead of
6317+
* downloading an empty-but-official-looking file.
6318+
*/
6319+
async exportTraces(tabId) {
6320+
await this._hydrate(tabId);
6321+
const conversationId = this.conversationIds.get(tabId);
6322+
if (!conversationId) return { ok: true, markdown: null, turnCount: 0, reason: 'no-conversation' };
6323+
let runs;
6324+
try {
6325+
const all = await trace.listRuns();
6326+
runs = all
6327+
.filter((r) => r && r.conversationId === conversationId)
6328+
.sort((a, b) => (a.startedAt || 0) - (b.startedAt || 0));
6329+
} catch (e) {
6330+
return { ok: false, error: String((e && e.message) || e) };
6331+
}
6332+
if (runs.length === 0) return { ok: true, markdown: null, turnCount: 0, reason: 'no-traces' };
6333+
const withEvents = [];
6334+
for (const run of runs) {
6335+
let events = [];
6336+
try { events = await trace.getRunEvents(run.runId); } catch (e) { events = []; }
6337+
withEvents.push({ run, events });
6338+
}
6339+
const { markdown, turnCount, toolCount } = tracesToMarkdown(withEvents);
6340+
return { ok: true, markdown, turnCount, toolCount };
6341+
}
6342+
63106343
_isAgentInjectedUserContent(content) {
63116344
const c = this._messageText(content).trimStart();
63126345
return c.startsWith('[Site guidance')
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* Pure trace → Markdown serializer for /export-with-traces.
3+
*
4+
* Consumes the trace store's per-run event log (trace/recorder.js) — an
5+
* append-only, compaction-immune record whose tool results are the RAW structured
6+
* values (pre-truncated by the recorder, never `_wrapUntrusted`-wrapped). That is
7+
* the right source for a tool chain; `this.conversations` is not (it is compacted,
8+
* enriched, and wrapped — see the closed PR #348 review).
9+
*
10+
* This renders the TOOL CHAIN: user/assistant/planner prose, tool calls (name,
11+
* args, result), and errors — in order. Screenshot / note / vision_sub_call events
12+
* are recorded but deliberately not rendered; the file says so in its footer, so it
13+
* never claims to be a complete record. The complete record is the Traces-page JSON.
14+
*
15+
* Pure and browser-neutral → unit-tested in test/run.js without a DOM or IndexedDB.
16+
*
17+
* @param {Array<{run: object, events: Array}>} runsWithEvents chronological runs,
18+
* each with its ordered event list.
19+
*/
20+
21+
const ARGS_LIMIT = 300;
22+
const RESULT_LIMIT = 600;
23+
const FOOTER = '_Screenshots, notes and vision sub-calls are recorded but not rendered here — see the Traces page for the complete record._';
24+
25+
function oneLine(t) { return String(t ?? '').replace(/\s+/g, ' ').trim(); }
26+
function humanSize(n) { return n >= 1024 ? `${(n / 1024).toFixed(1)}kb` : `${n}b`; }
27+
28+
function truncate(text, limit) {
29+
const s = String(text ?? '');
30+
if (s.length <= limit) return s;
31+
return `${s.slice(0, limit)}… +${humanSize(s.length - limit)} truncated`;
32+
}
33+
34+
// Wrap text in a fenced code block that survives content which is ITSELF fenced.
35+
// Planner responses usually arrive already wrapped in ```json … ```; naively
36+
// re-fencing them produces ```\n```json\n…, which no Markdown renderer parses.
37+
// So: unwrap a single enclosing fence (keeping its language hint), then choose a
38+
// fence longer than any backtick run left inside, per CommonMark, so nothing can
39+
// close the block early.
40+
function fencedBlock(content) {
41+
let body = String(content ?? '').trim();
42+
let info = '';
43+
const wrapped = body.match(/^```([^\n]*)\n([\s\S]*?)\n```$/);
44+
if (wrapped) { info = wrapped[1].trim(); body = wrapped[2].trim(); }
45+
const longestRun = (body.match(/`+/g) || []).reduce((n, s) => Math.max(n, s.length), 0);
46+
const fence = '`'.repeat(Math.max(3, longestRun + 1));
47+
return `${fence}${info}\n${body}\n${fence}`;
48+
}
49+
50+
function stringifyArgs(args) {
51+
if (args == null) return '';
52+
const s = typeof args === 'string' ? args : JSON.stringify(args);
53+
return truncate(oneLine(s), ARGS_LIMIT);
54+
}
55+
56+
// A trace tool result is a RAW value: a structured object ({success,error,...}),
57+
// a string, or the recorder's large-result marker { _truncated, length, head }.
58+
function renderResult(result) {
59+
if (result == null) return { text: '(no result recorded)', failed: false };
60+
if (typeof result === 'object' && result._truncated) {
61+
return {
62+
text: `${truncate(oneLine(String(result.head ?? '')), RESULT_LIMIT)} [recorder-truncated, ${humanSize(result.length || 0)} total]`,
63+
failed: false,
64+
};
65+
}
66+
const failed = typeof result === 'object' ? (result.success === false || !!result.error) : false;
67+
const s = typeof result === 'string' ? result : JSON.stringify(result);
68+
return { text: truncate(oneLine(s), RESULT_LIMIT), failed };
69+
}
70+
71+
export function tracesToMarkdown(runsWithEvents, { title = 'WebBrain Conversation — tool chain' } = {}) {
72+
const runs = Array.isArray(runsWithEvents) ? runsWithEvents : [];
73+
let md = `# ${title}\n\n`;
74+
let turnCount = 0;
75+
let toolCount = 0;
76+
77+
for (const entry of runs) {
78+
if (!entry || !entry.run) continue;
79+
turnCount += 1;
80+
const run = entry.run;
81+
const user = oneLine(run.userMessage || '');
82+
const meta = [run.model, run.status].filter(Boolean).join(' · ');
83+
md += `## Turn ${turnCount}${user ? ` — ${user}` : ''}\n`;
84+
if (meta) md += `_${meta}_\n`;
85+
md += '\n';
86+
87+
const events = Array.isArray(entry.events) ? [...entry.events].sort((a, b) => (a?.seq || 0) - (b?.seq || 0)) : [];
88+
for (const ev of events) {
89+
const d = (ev && ev.data) || {};
90+
if (ev.kind === 'llm_response') {
91+
const content = String(d.content || '').trim();
92+
if (!content) continue;
93+
// Plan-before-Act runs record the planner call with phase:'planner'; keep
94+
// it (derails often start in the plan) but label it and preserve its shape.
95+
if (d.phase === 'planner') {
96+
md += `**Planner:**\n${fencedBlock(content)}\n`;
97+
} else {
98+
md += `**WebBrain:** ${oneLine(content)}\n`;
99+
}
100+
} else if (ev.kind === 'tool') {
101+
toolCount += 1;
102+
const { text, failed } = renderResult(d.result);
103+
md += `- 🔧 \`${d.name || 'tool'}\`(${stringifyArgs(d.args)}) → ${failed ? '✗ ' : ''}${text}\n`;
104+
} else if (ev.kind === 'error') {
105+
md += `- ⚠️ error${d.phase ? ` (${d.phase})` : ''}: ${oneLine(d.message || '')}\n`;
106+
}
107+
// screenshot / note / vision_sub_call intentionally omitted — see FOOTER.
108+
}
109+
md += '\n';
110+
}
111+
112+
md += `${FOOTER}\n`;
113+
return { markdown: md, turnCount, toolCount };
114+
}

src/chrome/src/background.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1862,6 +1862,12 @@ async function handleMessage(msg, sender) {
18621862
return { ok: true, ...(await agent.getScratchpad(tabId)) };
18631863
}
18641864

1865+
case 'export_traces': {
1866+
const tabId = msg.tabId || sender.tab?.id;
1867+
if (!tabId) return { ok: false, error: 'No tab ID' };
1868+
return { ok: true, ...(await agent.exportTraces(tabId)) };
1869+
}
1870+
18651871
case 'get_progress': {
18661872
const tabId = msg.tabId || sender.tab?.id;
18671873
if (!tabId) return { ok: false, error: 'No tab ID' };

src/chrome/src/ui/locales/ar.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,4 +704,7 @@ export default {
704704
"st.redaction.toggle.label": "إخفاء المحتوى الحساس في لقطات الشاشة",
705705
"st.redaction.toggle.desc": "قبل إرسال لقطة الشاشة إلى نموذج رؤية، يقوم بتغبيش حقول النماذج والنص الذي يبدو كبريد إلكتروني أو رقم هاتف. يعمل الكشف بالكامل على جهازك — لا يُنقل أي شيء إضافي.",
706706
"st.redaction.warning": "⚠️ هذا تعتيم بأفضل جهد ومن نوع fail-open: إذا تعذّر تشغيل التعتيم على صفحة ما (مثلًا مباشرة بعد التنقل، أو في عارضات PDF، أو على صفحات المتصفح المقيّدة)، فستُرسل لقطة الشاشة كما هي دون تعتيم. يعتمد الكشف فقط على استدلالات DOM — النص المرسوم على عنصر canvas، أو البيانات الشخصية داخل الصور، أو أي شيء لا يُعرف كحقل نموذج أو نص بريد/هاتف قد يفلت من الكشف، كما أن نص الصفحة المُرسل إلى النموذج لا يُعتَّم بهذا الإعداد. هذا ليس ضمانًا أمنيًا. للخصوصية الكاملة، استخدم نموذجًا محليًا/دون اتصال (llama.cpp أو Ollama): عندها لن تغادر لقطات الشاشة جهازك إطلاقًا ولن تعود هناك حاجة للتعتيم.",
707+
"sp.slash.export_traces": "تصدير سلسلة الأدوات (التتبعات)",
708+
"sp.export_traces.none": "لا توجد تتبعات لهذه المحادثة. فعّل «تسجيل التتبعات» في الإعدادات ثم أعد التشغيل.",
709+
"sp.export_traces.error": "تعذّر تصدير التتبعات.",
707710
};

src/chrome/src/ui/locales/en.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ export default {
249249
'sp.api.badge_html': '<span>🔓 API mutations allowed</span>',
250250
'sp.permissions.disabled_html': '⚠️ <strong>Permission prompts are OFF.</strong> WebBrain will click, type, navigate, run JavaScript, upload, download, record, and schedule future work without asking first. Re-enable this from Settings → Permissions → Ask before consequential actions.',
251251

252-
'sp.help_html': '<strong>Slash Commands</strong><br><code>/help</code> — Show this list<br><code>/schedule</code> — Create a scheduled task<br><code>/list-schedules</code> — Show scheduled tasks<br><code>/check-progress</code> — Show current progress ledger<br><code>/show-scratchpad</code> — Show current scratchpad<br><code>/edit-scratchpad &lt;text&gt;</code> — Append text to the current scratchpad<br><code>/clear-scratchpad</code> — Clear the current scratchpad<br><code>/remember &lt;text&gt;</code> — Save a user preference to memory<br><code>/show-memory</code> — Show saved user memory<br><code>/forget-memory &lt;id&gt;</code> — Forget a saved memory<br><code>/allow-api</code> — Allow API mutations for this conversation<br><code>/dangerously-skip-permissions</code> — Disable permission prompts globally<br><code>/compact</code> — Compact this conversation context<br><code>/verbose</code> — Toggle verbose/compact tool display<br><code>/reset</code> — Clear conversation<br><code>/screenshot</code> — Capture current tab<br><code>/full-page-screenshot</code> — Capture the full page (Chrome only)<br><code>/record</code> — Start recording the current tab<br><code>/record --transcribe</code> — Record and save a Whisper transcript after stop<br><code>/record-full-screen</code> — Record a screen or window (Chrome only)<br><code>/record-full-screen --transcribe</code> — Record screen/window and save a transcript<br><code>/export</code> — Download conversation as Markdown<br><code>/profile</code> — Toggle profile auto-fill<br><code>/vision</code> — Toggle vision mode on active provider<br><code>/ask</code> — Switch to Ask mode<br><code>/act</code> — Switch to Act mode<br><code>/dev</code> — Switch to Dev mode<br><br><strong>Keyboard Shortcuts</strong><br><code>Ctrl/Cmd+/</code> — Focus the input<br><code>Ctrl/Cmd+Shift+A</code> — Switch to Ask mode<br><code>Ctrl/Cmd+Shift+X</code> — Switch to Act mode<br><code>Ctrl/Cmd+Shift+D</code> — Switch to Dev mode<br><code>Escape</code> — Stop the active run<br><code>Escape</code> twice — Stop an active recording',
252+
'sp.help_html': '<strong>Slash Commands</strong><br><code>/help</code> — Show this list<br><code>/schedule</code> — Create a scheduled task<br><code>/list-schedules</code> — Show scheduled tasks<br><code>/check-progress</code> — Show current progress ledger<br><code>/show-scratchpad</code> — Show current scratchpad<br><code>/edit-scratchpad &lt;text&gt;</code> — Append text to the current scratchpad<br><code>/clear-scratchpad</code> — Clear the current scratchpad<br><code>/remember &lt;text&gt;</code> — Save a user preference to memory<br><code>/show-memory</code> — Show saved user memory<br><code>/forget-memory &lt;id&gt;</code> — Forget a saved memory<br><code>/allow-api</code> — Allow API mutations for this conversation<br><code>/dangerously-skip-permissions</code> — Disable permission prompts globally<br><code>/compact</code> — Compact this conversation context<br><code>/verbose</code> — Toggle verbose/compact tool display<br><code>/reset</code> — Clear conversation<br><code>/screenshot</code> — Capture current tab<br><code>/full-page-screenshot</code> — Capture the full page (Chrome only)<br><code>/record</code> — Start recording the current tab<br><code>/record --transcribe</code> — Record and save a Whisper transcript after stop<br><code>/record-full-screen</code> — Record a screen or window (Chrome only)<br><code>/record-full-screen --transcribe</code> — Record screen/window and save a transcript<br><code>/export</code> — Download conversation as Markdown<br><code>/export-with-traces</code> — Export the tool chain (traces)<br><code>/profile</code> — Toggle profile auto-fill<br><code>/vision</code> — Toggle vision mode on active provider<br><code>/ask</code> — Switch to Ask mode<br><code>/act</code> — Switch to Act mode<br><code>/dev</code> — Switch to Dev mode<br><br><strong>Keyboard Shortcuts</strong><br><code>Ctrl/Cmd+/</code> — Focus the input<br><code>Ctrl/Cmd+Shift+A</code> — Switch to Ask mode<br><code>Ctrl/Cmd+Shift+X</code> — Switch to Act mode<br><code>Ctrl/Cmd+Shift+D</code> — Switch to Dev mode<br><code>Escape</code> — Stop the active run<br><code>Escape</code> twice — Stop an active recording',
253253
'sp.slash.busy_only_oob': 'Messages are queued while WebBrain is busy. Only /help, /check-progress, /show-scratchpad, /show-memory, /list-schedules, /dangerously-skip-permissions, /screenshot, /export, and /verbose can run immediately as slash commands.',
254254
'sp.compact.nothing_to_compact': 'Nothing to compact yet — there is not enough older context.',
255255
'sp.compact.busy': 'Cannot compact while a run is in progress — wait for it to finish.',
@@ -707,4 +707,7 @@ export default {
707707
'tr.event.args': 'args',
708708
'tr.event.result': 'result',
709709
'tr.event.step': 'step {step}',
710+
"sp.slash.export_traces": "Export the tool chain (traces)",
711+
"sp.export_traces.none": "No traces for this conversation. Enable Record traces in Settings, then run again.",
712+
"sp.export_traces.error": "Couldn't export traces.",
710713
};

src/chrome/src/ui/locales/es.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,4 +704,7 @@ export default {
704704
"st.redaction.toggle.label": "Redactar contenido sensible en las capturas de pantalla",
705705
"st.redaction.toggle.desc": "Antes de enviar una captura de pantalla a un modelo de visión, difumina los campos de formulario y el texto que parezca un correo electrónico o un número de teléfono. La detección se ejecuta enteramente en tu dispositivo — no se transmite nada adicional.",
706706
"st.redaction.warning": "⚠️ Es una redacción best-effort y fail-open: si no se puede aplicar en una página (por ejemplo justo después de una navegación, en visores de PDF o en páginas restringidas del navegador), la captura se envía igualmente sin redactar. La detección usa solo heurísticas del DOM — texto dibujado en un canvas, datos personales dentro de imágenes, o cualquier cosa no reconocida como campo de formulario o texto de correo/teléfono puede pasar desapercibida, y el texto de la página enviado al modelo no se redacta con este ajuste. No es una garantía de seguridad. Para privacidad total, usa un modelo local/sin conexión (llama.cpp, Ollama): las capturas nunca saldrán de tu equipo y la redacción deja de ser necesaria.",
707+
"sp.slash.export_traces": "Exportar la cadena de herramientas (trazas)",
708+
"sp.export_traces.none": "No hay trazas para esta conversación. Activa «Registrar trazas» en Ajustes y vuelve a ejecutar.",
709+
"sp.export_traces.error": "No se pudieron exportar las trazas.",
707710
};

src/chrome/src/ui/locales/fr.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,4 +704,7 @@ export default {
704704
"st.redaction.toggle.label": "Flouter le contenu sensible dans les captures d'écran",
705705
"st.redaction.toggle.desc": "Avant qu'une capture d'écran soit envoyée à un modèle de vision, floute les champs de formulaire et le texte qui ressemble à un e-mail ou un numéro de téléphone. La détection s'exécute entièrement sur votre appareil — rien de plus n'est transmis.",
706706
"st.redaction.warning": "⚠️ Meilleur effort et fail-open : si le floutage ne peut pas s'exécuter sur une page (par exemple juste après une navigation, dans les visionneuses PDF, ou sur des pages restreintes du navigateur), la capture est quand même envoyée non floutée. La détection utilise uniquement des heuristiques DOM — texte dessiné sur un canvas, données personnelles dans des images, ou tout ce qui n'est pas reconnu comme champ de formulaire ou texte e-mail/téléphone peut passer inaperçu, et le texte de la page envoyé au modèle n'est pas flouté par ce réglage. Ce n'est PAS une garantie de sécurité. Pour une confidentialité totale, utilisez un modèle local/hors ligne (llama.cpp, Ollama) : les captures ne quittent alors jamais votre machine et le floutage devient inutile.",
707+
"sp.slash.export_traces": "Exporter la chaîne d'outils (traces)",
708+
"sp.export_traces.none": "Aucune trace pour cette conversation. Activez « Enregistrer les traces » dans les paramètres, puis relancez.",
709+
"sp.export_traces.error": "Impossible d'exporter les traces.",
707710
};

src/chrome/src/ui/locales/he.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,4 +657,7 @@ export default {
657657
"st.redaction.toggle.label": "טשטש תוכן רגיש בצילומי מסך",
658658
"st.redaction.toggle.desc": "לפני שצילום מסך נשלח למודל ראייה, מטשטש שדות טופס וטקסט שנראה כמו אימייל או מספר טלפון. הזיהוי פועל כולו במכשיר שלך — שום דבר נוסף לא מועבר.",
659659
"st.redaction.warning": "⚠️ זהו טשטוש במאמץ סביר (best-effort) מסוג fail-open: אם הטשטוש לא יכול לפעול בדף מסוים (למשל מיד אחרי ניווט, בצפייה בקבצי PDF, או בדפי דפדפן מוגבלים), צילום המסך עדיין יישלח ללא טשטוש. הזיהוי מסתמך רק על היוריסטיקות DOM — טקסט המצויר על canvas, מידע אישי בתוך תמונות, או כל דבר שלא מזוהה כשדה טופס או כטקסט אימייל/טלפון עלול לחמוק, וטקסט הדף הנשלח למודל אינו מטושטש על ידי הגדרה זו. זו אינה ערבות אבטחה. לפרטיות מלאה, השתמש במודל מקומי/לא מקוון (llama.cpp, Ollama): כך צילומי מסך לעולם לא יעזבו את המכשיר שלך והטשטוש כבר לא יידרש.",
660+
"sp.slash.export_traces": "ייצוא שרשרת הכלים (מעקבים)",
661+
"sp.export_traces.none": "אין מעקבים לשיחה זו. הפעילו את «הקלטת מעקבים» בהגדרות והריצו שוב.",
662+
"sp.export_traces.error": "לא ניתן לייצא מעקבים.",
660663
};

0 commit comments

Comments
 (0)