live-stream: separate <think> from the answer. Backend marks the think->answer boundary; UI labels the live text 'thinking' WHILE reasoning, then collapses it to a 'reasoned' tag and shows the answer distinctly - so the streaming think is never mistaken for the final answer.
token-level streaming: complete() streams via SSE + on_token callback; run_agent pushes the in-progress generation (think then action) to the UI live; run() renders it typing out. Eval stays non-streaming.
fix double-box user bubble: Gradio nests .message.user > [data-testid=user]; my selector styled BOTH -> two stacked boxes. Now the OUTER is the single bubble, the INNER is flattened (transparent/no box). Verified via live CSS injection.
fix chart blank: 1B was WRITING chart.png as TEXT (623-byte bogus png -> 0x0 render). write tool now REFUSES image-ext text-writes + steers to matplotlib savefig; _render_media_file skips files without a real image magic-number (no blank bubble).
fix chart/media not displaying: the 1B savefig's to an absolute /workspace/ path inside the script (bash rewrite can't reach in-code paths), so the PNG landed outside the scanned sandbox. Now: ensure /workspace exists at startup + _extra_media scans it this-turn-only (no cross-session leak; concurrency_limit=1). Refactored shared _render_media_file.