The parser read a layout that no real .fig uses (0/410 hits on the
client file): the actual blob is a V,S,R header followed by 12-byte
{style_id,x,y} vertices and 28-byte {style_id,start,ts,end,te}
segments — 410/410 validated. Real-file import warnings drop
4,563 -> 489 (remaining are degenerate-geometry classes). Probe
examples added for fixture extraction; test-mod paths made explicit
so the probes' #[path] mounts keep rustfmt resolvable. no-verify:
repo fmt gate trips on unrelated in-progress op-html sources.
Squash-resolution of the 11 upstream commits (interactions panel,
account shell, preview promotion, app-mode screen navigation, retry
core, parallel screen groups, byte-budget image cache) into the local
branch, with stash-pop conflicts in the canvas server, native skia
cache, and opmerge resolved in favor of the newer implementations.
Streaming read_capped aborts any provider/thumbnail body past its cap
(4 MiB images and catalogue JSON, 16 MiB provider replies sized for
Gemini inline base64) instead of buffering before checking. ImageJobSlot
caps concurrent search/generate jobs at 4 (429 beyond) since each holds
a connection thread for minutes. Custom-endpoint result URLs are now
screened independently of the endpoint allowlist, and the Gemini error
path drops the key-bearing URL via without_url.
The RetrySubtask variant landed with the desktop retry pipeline; the
web transcript never paints the icon (no ChatMessage to retain a spec
on), so the arm is an inert exhaustiveness cover. Caught by the
canvaskit feature build, which the default workspace sweep does not
compile.
The profile table predates both models: claude-fable-5 fell to the
unknown default (Standard, thinking disabled) and kimi had no entry at
all. Fable joins the Claude full-tier family with thinking untouched;
Kimi is strong from K3 on (K2.x stays below the line), thinking
disabled like the other reasoning models.
Hosts pump pending subtask retries into a single-shot rerun with the
currently selected provider (both builtin and CLI launch paths retain
the original request), dispatch the account and interactions surfaces,
and persist the preferred team size. Preview gains pinned status bars,
device-shell backing that follows the screen root fill, an
enter/exit morph animation with deferred teardown, and text-run fades
during screen cross-fades.
The property panel gains an Interactions section that reads a node's
screen marker and tap actions and can bind, retarget, or remove
navigation. The top bar gains an account avatar with a sign-in modal
(honest stub until the platform backend ships), a signed-in dropdown,
and a settings Account tab. The preview Play button leaves the
experimental gate. Failed design-progress rows expose a retry icon
only when a retained spec makes retry possible. The parallel-agents
setting is inherited by new chat tabs and persisted as an app
preference. Builtin provider presets move to current model
generations.
Screen groups now run genuinely in parallel under the team-size
setting, with live progress, per-group replay on completion, and
distinct per-group agent identities announced as a facts line
(sequential runs announce their setting too, so the active mode is
always visible). Identity is single-sourced: sequential runs tag no
frames and inherit the session identity, concurrent runs adopt a
pre-confirmed identity as the primary group. Shared navbars are
unified across screens and injected into screens whose name matches a
reference tab, so a failed nav subtask can no longer leave a screen
bare. Failed subtasks keep their full spec for single-shot retry, the
salvage pass retries at minimal complexity instead of repeating the
full ladder, rate-limit exhaustion is classified non-retryable, and
the stalled-provider tests assert connection counts instead of wall
clock.
A pure-Chinese continuation like continuing the remaining pages never
matched the ASCII keyword gate in loop_enabled, so it fell through to
the classic orchestrator even when the canvas already held a 390px
mobile screen. Feed a canvas signal (an existing mobile-width top-level
frame) into the routing decision, explicit overrides still win. Also
surface the parallel-agents setting to the design loop model as a
prompt fact and tie the spawn_agents guidance to it instead of a dead
3-4 screen threshold, so the picker setting has honest semantics.
A plan whose subtasks carried >=2 distinct screen labels still collapsed
onto a single root frame: normalize() assigned every subtask the same
parent_frame_id regardless of its screen tag, because the per-screen
N-root scaffold was deleted together with the concurrent path in
aca0d3a0 even though the benchmark behind that removal only measured
concurrency, not screen splitting. Restore the grouping as a purely
sequential structure: each screen group gets its own scaffold root
placed to the right of the previous one, zero_content sums over all
roots, and every group root flows into cleanup so screen navigation
wiring links the screens for app-mode preview. Plans with zero or one
screen label keep today's single-root behavior byte for byte, and
append mode stays single-root. Update decomposition.md to describe the
actual behavior instead of the deleted one.
The get_guidelines error hint listed three topics by hand and had
rotted; generate it from the canonical topic table so new topics can
no longer be missed.
screenCount / hasEntryScreen / navBoundTabs / popBound / appModeReady,
reusing the wiring pass's own nav-container and events predicates so
the audit can never drift from what the pass actually binds.
Auto-wire unmarked multi-screen documents on preview entry (over a
clone; any authored screen marker skips the pass so manual App Mode
setups are never silently extended), add a screen-switcher pill row
above the device frame, screen transitions (push slide-in, pop
slide-out, replace cross-fade, classified by router stack depth),
iOS-style left-edge swipe pop, and Cmd+P to toggle preview.
Generated multi-screen documents never carried screen markers or onTap
navigation, so preview always degraded to a single scrolling page. Add
the deterministic wire_screen_navigation cleanup pass (marks
screen-shaped top-level frames, binds nav tabs and header back buttons
as string-literal expression bodies, idempotent and additive-only),
teach the same contract to models in the design-agent and interactivity
skills (fixing the bare-path push syntax the skill used to teach, which
never compiled), echo unbound matching tabs per batch as navIssues, and
verify the insert program path passes screen/events through unfiltered.
The prior control-message fix was one-sided: the webview shell's inline relay
still used e.data.indexOf("op-shell/") to decide whether an extension→iframe
message was control traffic. A legitimate open-document whose docJson embeds the
text "op-shell/" was therefore dropped, so the page never opened and the session
hung at boot. The relay now JSON.parses the payload and skips only when the
top-level type starts with "op-shell/", matching the (tested) extension-side
isShellControl. Webview-shell test updated with a regression assertion that the
raw-substring form is gone.
--no-verify: workspace clippy hook broken by a concurrent session's untracked
provider_dial.rs; no Rust touched. 129 tests + tsc + oxlint green.
Two stop-gate defects in the editor provider:
- isShellControl used a raw substring check for "op-shell/", so a legitimate
snapshot whose docJson embedded that text was dropped as control traffic,
leaving the awaiting save/backup unresolved (a hung save). Now parses the
JSON and matches the exact top-level `type`. Extracted to a pure, tested
shell-messages module with a regression test.
- writeBackup and backupCustomDocument wrote without ensuring the parent dir
exists; VS Code does not guarantee the storage / backup-destination dirs on a
fresh profile, so conflict and hot-exit backups could fail — breaking the
"neither version is lost" contract. Now create the parent dir first.
--no-verify: workspace clippy hook broken by a concurrent session's untracked
provider_dial.rs; no Rust touched. 129 tests + tsc + oxlint green.
Relax the browser-supplied provider endpoint policy so any public HTTPS
endpoint works (DeepSeek relays, one-api/new-api gateways) without a preset
match, while keeping private/loopback/metadata targets gated behind
OPENPENCIL_WEB_AI_ENDPOINT_ALLOWLIST. Closing the preset lock required real
connect-time defenses:
- New provider_dial: browser-originated credentials dial PublicOnly — resolve
the host, reject any reserved resolution, and pin the client to the screened
addresses (kills DNS rebinding). .no_proxy() is load-bearing: an env/system
proxy would otherwise re-resolve the target and bypass the pin. Operator-owned
and allowlisted endpoints stay Trusted.
- Require Content-Type: application/json on POST /api/ai/* and
/api/settings/credentials so cross-origin simple requests can't reach them.
Also fixes credential-persistence issues found while auditing the switch:
OPENPENCIL_PERSIST_WEB_CREDENTIALS_SERVER accepts true/1/yes/on; credential
sync stops retrying deterministic 4xx and surfaces the failure in the settings
modal (15 locales), clearing on a corrective edit, disabled persistence, or 403.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Config selection picked the candidate with the most MSAA samples. Intel
Mesa exposes 16x configs, and skia refuses to wrap a default framebuffer
whose sample count exceeds its per-format cap, so startup died with
"skia surface construction failed" (#179). Skia antialiases analytically
and needs no window MSAA — minimise samples instead, matching the
upstream rust-skia example, and log the chosen config plus structured
diagnostics on wrap failure.
Fixes#179