/* ============================================================
app.jsx — shell + landing, wired to the gradio.Server backend.
Adapted from the designer's prototype: the demo's fake upload
is replaced with a real file picker that calls /analyze_trace
through @gradio/client; the tweaks panel is dropped and the
theme is pinned to the dusk-survey dark mode.
============================================================ */
function BrandMark({ size = 34 }) {
return (
);
}
function TopBar() {
return (
Trace Field Notesnarrative analysis for coding-agent traces
build small
);
}
const ENGINES = [
["minicpm", "Quick analysis", "MiniCPM5 1B"],
["nemotron", "Deeper analysis", "Nemotron 3 Nano 30B-A3B"],
["deterministic", "Rule-based", "no model, always on"],
];
const EXEC_MODES = [
["zerogpu", "GPU", "Space GPU · faster"],
["cpu", "CPU", "no GPU quota · slower"],
];
function Toggle({ on, set, label, sub, locked }) {
return (
);
}
function LandingView({ onAnalyze, onSample, error }) {
const [staged, setStaged] = React.useState(null); // { name, file }
const [redact, setRedact] = React.useState(true);
const [userCtx, setUserCtx] = React.useState(true);
const [engine, setEngine] = React.useState("minicpm");
const [execMode, setExecMode] = React.useState("zerogpu");
const [dragOver, setDragOver] = React.useState(false);
const [copied, setCopied] = React.useState(false);
const fileRef = React.useRef(null);
const chosen = ENGINES.find((e) => e[0] === engine) || ENGINES[2];
const engineLabel = chosen[1] + ": " + chosen[2];
function onFiles(list) {
const f = list && list[0];
if (f) setStaged({ name: f.name, file: f });
}
function pick() { if (fileRef.current) fileRef.current.click(); }
function run() {
if (!staged) return;
onAnalyze({ file: staged.file, include_user_context: userCtx, redact_secrets: redact, analysis_engine: engine, execution_mode: execMode, engineLabel });
}
const AGENT_PROMPT = `Use this Space as a tool.
1. Read its /agents.md endpoint.
2. Find my latest local agent session log
(Codex ~/.codex/sessions, Claude ~/.claude/projects).
3. Review and redact secrets before upload.
4. Upload the JSONL and request a narrative difficulty analysis.
5. Return the report. Do not publish the raw trace.`;
return (
See how your coding agent got stuck, detoured, recovered & claimed success.
Upload a Codex, Claude Code, or Pi Agent session log. Trace Field Notes reads only the agent's
narrated messages — what it planned, where it snagged, how it rerouted, and how honestly it called it done —
and charts the session as a trail you can walk.
!
Agent traces can carry prompts, command output, local paths, screenshots, secrets, and private code.
Review and redact before uploading or sharing. This app analyzes only visible narrative messages, ignores raw tool telemetry by default, and scrubs secrets and personal data with pattern rules plus OpenAI's privacy-filter model.
Drop a .jsonl traceor click to choose · .json .txt .log accepted
)}
{}} locked label="Ignore tool contents" sub="locked for this release" />
{ENGINES.map(([key, name, detail]) => (
))}
Quick uses MiniCPM5 1B on the Space GPU. Deeper uses Nemotron 3 Nano 30B-A3B. Rule-based needs no model and never fails.
{EXEC_MODES.map(([key, name, detail]) => (
))}
ZeroGPU is fast but spends your Space GPU quota. CPU needs no quota and still works if you've run out — just slower, so the progress bar will move more gradually.
);
}
function EmptyReport({ data, onReset }) {
return (
The trace yielded {data.narrative_message_count} visible narrative messages, but none carried clear
self-reported blockage, detour, or recovery language. That does not prove the session was trouble-free —
only that the narrative did not say so. Try the redacted-narrative export to read it yourself.
);
}
function App() {
const [stage, setStage] = React.useState("landing"); // landing | analyzing | report
const [data, setData] = React.useState(null);
const [engineLabel, setEngineLabel] = React.useState("");
const [error, setError] = React.useState("");
const [step, setStep] = React.useState(0);
const [progress, setProgress] = React.useState(null);
async function analyze({ file, include_user_context, redact_secrets, analysis_engine, execution_mode, engineLabel }) {
setError("");
setEngineLabel(engineLabel || analysis_engine);
setStep(0);
setProgress(null);
setStage("analyzing");
window.scrollTo({ top: 0 });
try {
const g = window.__gradio;
if (!g) throw new Error("Client is still loading — reload the page and try again.");
const client = await g.clientPromise;
const sub = client.submit("/analyze_trace", {
trace_file: g.handle_file(file),
include_user_context: !!include_user_context,
redact_secrets: !!redact_secrets,
analysis_engine,
execution_mode,
});
let result = null;
for await (const msg of sub) {
if (msg.type === "data") {
const p = Array.isArray(msg.data) ? msg.data[0] : msg.data;
if (p && typeof p === "object") {
if (p.result) {
result = p.result;
} else if (typeof p.step === "number") {
setStep(Math.min(p.step, PIPELINE.length - 1));
}
if (typeof p.pct === "number") {
setProgress({
pct: p.pct,
elapsed: p.elapsed,
eta: p.eta,
total: p.total,
processed: p.processed,
stage: p.stage,
});
}
}
} else if (msg.type === "status") {
if (msg.stage === "error") throw new Error(msg.message || "The analyzer failed on the server.");
if (msg.stage === "generating") setStep((s) => (s < 1 ? 1 : s));
}
}
if (!result || typeof result !== "object") throw new Error("The analyzer returned no result.");
setStep(PIPELINE.length);
setData(result);
setStage("report");
} catch (e) {
setError(String((e && e.message) || e));
setStage("landing");
}
window.scrollTo({ top: 0 });
}
function loadSample(key) {
const base = key === "short" ? window.TFN.SHORT : window.TFN.LONG;
setError("");
setEngineLabel(base.engine);
setData(base);
setStage("report");
window.scrollTo({ top: 0 });
}
function reset() { setStage("landing"); setData(null); window.scrollTo({ top: 0 }); }
const reportData = data ? Object.assign({}, data, { requested_engine: engineLabel || data.engine }) : null;
const hasEpisodes = reportData && reportData.episodes && reportData.episodes.length;
return (