* feat(app): render mermaid diagrams in agent chat with pan/zoom
Fenced mermaid blocks in assistant messages now render as diagrams
instead of code blocks.
Web: mermaid renders inline via dynamic import (code-split, loads on
first diagram). Drag to pan, ctrl/cmd+wheel or trackpad pinch to zoom
about the cursor, double-click to reset; plain wheel still scrolls the
chat. Rendered SVGs are cached across virtualization remounts.
iOS/Android: diagrams render in a WebView running a self-contained
generated bundle (build:mermaid-webview, mirrors the native terminal's
xterm pattern — no CDN, mermaid ships in the app). The inline preview
is touch-shielded and height-driven by a bridge message; tapping opens
a fullscreen viewer where the platform WebView owns pinch-zoom.
Streaming source that doesn't parse yet falls back to the highlighted
code block and keeps the last good render; renders are serialized
through one queue with stale-task skipping so token-by-token updates
cost at most one in-flight layout.
Security: mermaid can fetch external resources during layout (image
shapes, CSS url()) before output sanitization runs — see
mermaid-js/mermaid#7645 — so resource-bearing source is rejected
up front (containsUnsafeMermaidSource) on every platform, htmlLabels
stay off, securityLevel is strict, and the mermaid secure list is
extended so directives can't override app theming or themeCSS.
The lockfile diff includes npm 11.17 normalization churn beyond the
mermaid closure; a no-op npm install produces the same rewrite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(app): serialize native mermaid renders and pin viewer to last good source
Serialize render messages inside the mermaid webview entry so a newer
streamed update can't re-initialize global mermaid state (e.g. flip the
theme) while a prior layout is still running; superseded queued renders
are skipped. Track the last successfully rendered source on the RN side
and open the fullscreen viewer with that, so tapping mid-stream while
the latest chunk is still invalid can't show a blank viewer.
Addresses PR review feedback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(app): pair native mermaid renders with their source via request id
A "rendered" bridge event was credited to the newest sent source, but an
older render can complete while a newer (possibly invalid) chunk's send
is in flight, mis-crediting the older render's success to the newer
source. Render messages now carry a request id that the entry echoes on
rendered/renderError; the RN side resolves the source by id, so the
fullscreen viewer always opens the exact source that produced the
visible render.
Addresses PR review feedback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(app): harden mermaid guard and native preview per adversarial review
- Guard bypass (blocker): mermaid accepts quoted and unicode-escaped
object keys, so quoted "img": evaded the resource denylist while
still parsing into a real img property. The guard now decodes u/x
escapes and strips quoting characters before matching, and tests
both raw and normalized source.
- structuredClone polyfill banner in the webview bundle: the app's iOS
floor is 15.1 but structuredClone exists only from 15.4; esbuild
targets don't polyfill runtime APIs.
- Preview height sizes the inner content view so fence padding/borders
no longer clip the reported SVG height, and measurement happens at
the final content width (measuring overlay keeps the fence insets).
- The invisible measuring overlay is hit-test transparent, so an
invalid diagram's fallback code block keeps its copy button and
gestures.
- renderError now prunes all superseded source-map entries, matching
the rendered path, so a long broken stream can't accumulate sources.
- The entry's render chain can no longer be poisoned: the whole task
body is inside try/catch and the chain appends a trailing catch.
- "Mermaid diagram" accessibility label is translated in all locales
(message.mermaidDiagram) on web and native.
- esbuild pinned in packages/app devDependencies so the generated
webview artifact doesn't depend on hoisting layout; </script is
escaped in the generated JS.
Addresses adversarial review round 2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(app): mermaid source toggle and visible zoom controls
Web: a hover control cluster on the diagram (zoom in/out anchored to
the viewport center, reset view, view source) mirroring the code
block's hover-copy idiom; always visible on compact form factors where
hover doesn't exist. View source swaps to the highlighted code block
(with its own copy button) and a view-diagram button swaps back.
Native: the fullscreen viewer gains a source toggle beside the close
button, swapping the webview for the highlighted source in a
scrollable view.
All control labels are translated in every locale under
message.mermaid.*; the flat mermaidDiagram key from the previous
commit is folded into the same group.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(app): align mermaid control icons with the app's icon vocabulary
Network is the settings Connections icon — reusing it for "view
diagram" gave one glyph two meanings; use Workflow instead. RotateCw is
the app's established reset/reload glyph (RotateCcw had no precedent).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(app): use Scan for mermaid reset-view control
RotateCw is the app's reload glyph (19 uses) — a reset-zoom button
wearing it invites a reload misread. Scan (corner-bracket fit-to-frame)
is unused and carries the intended fit/reset semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(app): remove raw NUL byte from mermaid cache key separator
A literal 0x00 in the template string made git classify the file as
binary, rendering the PR diff for it unreviewable. Use the escaped
sequence instead — identical runtime key, plain-text source.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(app): align mermaid source-view toggle with the copy button
The source view wrapped the code block while the fence margins stayed
inside it, so the floating view-diagram toggle hung in the margin
whitespace above the visible box — misaligned with the code block's
copy button and outside its hover region. Hoist the margins onto the
wrapper (both buttons now share the box edge) and render the toggle in
the copy button's plain style so the pair reads as one control row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(app): close mermaid review findings
* chore(app): finish Mermaid PR rebase cleanup
* refactor(app): isolate diagram rendering and stabilize streaming
* fix(app): keep Mermaid previews stable while streaming
* fix(nix): update npm dependency hash
* fix(app): preserve streamed rows through completion
---------
Co-authored-by: Damon Meledones <dmeledon@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
* fix(app): debounce reconnect status toast
Keep reconnect transport immediate while delaying the visible status toast so brief outages stay quiet. Scope the toast to the active retained panel, keep its copy and status dot consistent across locales, and add a manual real relay deployment journey that verifies streaming recovery without user action.
* fix(app): isolate manual relay deployment check
Keep the deployment journey out of the provider-wide real test project so it runs only when its local relay prerequisites are explicitly requested. Also keep the relay teardown active when packaged daemon startup or later cleanup fails.
* fix(app): preserve reconnect toast across tabs
Keep reconnect presentation timing at the host level so switching between retained panels does not restart the debounce or flash the toast. Add a browser regression covering a visible outage across retained tab switches.
* fix(app): anchor reconnect debounce to host status
Record host connection status transitions independently of mounted agent panels and preserve the outage start across reconnecting states. Use that host-level timestamp when a visible agent panel appears, with runtime coverage for the transition lifecycle.
* fix(app): reset reconnect toast state per outage
* test(app): build relay journey prerequisites
Pin the keyboard controller to the release that prevents stale keyboard positioning from leaving the composer underneath a keyboard that is still visible.
* fix(app): preserve formatting when copying assistant selections
Browser selection serialized React Native Web layout nodes instead of the assistant message semantics. Recover semantic markup from renderer-owned metadata and reuse the rich clipboard generator used by Copy turn.
* fix(app): preserve assistant copy semantics
Carry source-only Markdown metadata through the selection copy boundary, unwrap generated code links, and cover ordered starts, hard breaks, and fence languages in Playwright. Refresh the Nix dependency hash for the added clipboard dependency.
* fix(app): preserve partial selection context
Wrap selected fragments with their shared semantic ancestor chain and enable GFM table and strikethrough conversion. Cover partial nested selections and GFM output through the browser clipboard.
* fix(app): preserve copied list and autolink text
Adjust partial ordered-list starts for omitted siblings, strip parser-generated anchors before Markdown reconstruction, and refresh the Nix dependency hash computed by CI.
* fix(app): normalize partial selection context
Normalize cloned ordered lists across block boundaries, degrade incomplete table fragments to selected text, and preserve table alignment provenance through Markdown reconstruction.
* fix(app): escape copied table delimiters
Rendered table cells no longer retain source escape characters. Escape literal pipes during DOM-to-Markdown conversion so copied tables preserve their column structure.
* fix(app): preserve authored link semantics
The rendered link boundary conflated parser-generated links with authored autolinks and discarded authored titles. Carry title semantics into the anchor and unwrap only links with explicit generated provenance.
* fix(app): preserve nested copy structure
Blank-line splitting separated loose nested lists before Markdown rendering, leaving no nesting for clipboard serialization to preserve. Derive protected separators from the Markdown block structure and keep copy metadata independent from monospace surface styling.
* fix(app): preserve selected file links
Assistant messages and rich clipboard HTML must apply the same link validation policy. A shared parser keeps file links semantic in both MIME representations while retaining markdown-it's unsafe-scheme rejection.
* fix(app): preserve partial table content
A selected body row wider than its retained header cannot be represented as GFM and is truncated when rich HTML is regenerated. Treat only those over-wide fragments, plus headerless fragments, as incomplete while preserving valid short rows.
* fix(ci): skip unrelated required checks cleanly
Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable.
* fix(ci): gate changes to install patching script
* fix(ci): route cross-package test contracts narrowly
* fix(ci): keep routed contracts merge-blocking
* fix(server): exclude nested test utilities from builds
* fix(ci): cover app-owned native and browser domains
* refactor(ci): align tests with runtime domains
Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check.
* fix(ci): keep routing and desktop suites isolated
The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs.
* fix(ci): isolate desktop e2e runtime
* perf(test): isolate Playwright workers
* perf(test): parallelize Playwright within specs
* fix(test): keep Playwright file scheduling
* fix(test): share dynamic Metro fixture
* fix(test): preserve own-host restart fixtures
* fix(ci): preserve routed contracts after rebase
* Stabilize Playwright wheel-scroll assertions
* fix(claude): select the correct Opus 5 context window
Bare Opus 5 IDs resolve to 200K behind third-party gateways. Expose explicit context variants and require a Claude Code version that recognizes the model.
* fix(claude): add Fable and Sonnet context variants
* test(cli): expect Claude context variants
* test(cli): compose Claude catalog expectations
* fix(claude): preserve catalog on version probe failure
* fix(claude): preserve parsed context variants
* chore(release): cut 0.2.2
* feat(files): edit workspace files on web
Keep source buffers synchronized with host file changes and require an explicit overwrite or reload when revisions diverge.
* feat(panels): surface and protect modified tabs
Expose tooltip and modification state through the generic panel boundary so tabs can show stable metadata and guard every close route consistently.
* fix(tests): use portable fake timeout handle
* fix(files): harden editor conflict handling
Preserve modified panel state across tab eviction, use precise revisions for optimistic writes, coalesce concurrent file watchers, and localize the editor interface.
* fix(files): close editor concurrency gaps
Coalesce clean reloads, preserve subscriber identities and file permissions, suspend pending saves during close confirmation, and carry precise revisions through file reads.
* test(files): expect read revision metadata