The stale 'Image — fill row' line made the crop/adjust feature read
as unimplemented; describe the actual popover. Record that
op-host-native tests need --features gl-host — the default run
compiles out widget_host entirely and covers none of it.
text-rules and cjk-typography co-load on every Chinese request with
contradictory lineHeight tiers and no cross-reference — the model
picks one silently, and a wrong pick renders cramped headings rather
than an error. Mark the tiers latin-only and route CJK text to the
cjk-typography values, keeping latin digits and labels inside a CJK
design on the latin tiers.
The inspector's font dropdown scrolled against the wheel: it added
delta to its offset while the settings-panel font picker — same
widget, same scroll field, same file — and a dozen other scroll
surfaces all subtract. Flip it on both hosts and pin the convention
with direction tests that drive the real wheel entry point and
require the rail popups to move the same way as the panel behind
them. Also gate agent_indicator_test_support behind the same cfg as
its only consumers inside gl-host'd widget_host, so a default-feature
clippy run is clean without blanket allows.
The variable-binding list was emitted by the section walker as plain
panel rows — no height cap, no viewport, no scroll — so forty
variables inflated the inspector past the window and the clipped tail
stayed unreachable (yet still clickable through the clip). Rebuild it
as an anchored overlay in the same family as the fill-type and font
pickers: 320px cap with its own wheel scroll, outside-click and
Escape to close, name column ellipsized against a right-aligned hex
column so long tokens never overlap their value. Bind/unbind emission
is unchanged, the stroke-section picker shares the implementation,
and a regression test compares every other panel rect open-vs-closed
so the inline-inflation shape cannot come back.
Every image-fill popover test built its fixture on a rectangle, but a
dropped screenshot lands on a frame — a different SectionCapabilities
branch. A regression there would present exactly as 'the dropped
image can't be adjusted' with no red test. Walk the real chain once
from apply_image_drop on a frame through the fill row, mode chips and
crop-editing gate.
A wide screenshot dropped into a tall frame and viewed in Fit mode
letterboxes, and closing the gap meant computing width x aspect by
hand. Add a 'Match image ratio' action to the image-fill popover:
keep the current width, write the aspect-derived height through the
same property-edit path a manual handle drag uses — one undo step,
selection kept. The source dimensions come from the fill's
originalSize, falling back to the encoded image header via the
popover's shared byte cache, which also serves standalone image nodes
and legacy fills that never recorded a size; with no resolvable size
the row is disabled and produces no hit rect.
A five-frame carousel took five rounds of select-frame → export. Add
File ▸ 'Export all frames' (and 'Export N frames' when several are
selected): one directory pick, every visible top-level frame written
as a PNG named by document order, per-frame failures collected into
one bilingual summary instead of aborting the batch. The planner
strips a frame-name ordinal only when it equals the assigned index, so
'01 Cover' at position one exports as 01-Cover.png while reordered
frames keep both numbers. Batching wraps the existing single-node
raster exporter — the Export image dialog and its pipeline are
untouched — and web hosts leave the capability flag off, so no dead
menu row appears in the browser.
Dropping a png/jpg/webp used to be silently ignored — placing a
screenshot meant importing, dragging into position and matching sizes
by hand. Now a drop resolves the deepest fillable node under the
cursor (walking up past text and icons), writes a cover image fill in
one undo step, and rings the target while hovering so the outcome is
visible before release. Empty-canvas drops insert an image node at the
point; document drops keep their open/import behavior. Drag positions
come from an AppKit probe on macOS; other platforms degrade to a
centered insert.
The cleanup passes and blocker detectors already run on every
generation, but their work was invisible: problems produced nudges
while a clean result said nothing. Count each accepted document edit
per check category through a DocSink decorator (pass bodies untouched),
carry the tally through loop finalize and the orchestrator's progress
events, and render an honest completion line — categories actually
run, repairs actually applied, open issues never papered over. A run
with nothing to fix still earns its positive credential; a turn that
checked nothing reports nothing.
The layout-scene cache keys on a revision counter, so any mutator that
edits the tree without bumping it leaves the canvas painting a stale
scene: handle-resize, rotation, pen and path-anchor drags all updated
the document every frame while the cache kept answering 'unchanged',
and the new geometry only appeared after some later click bumped the
counter. Register the change inside each mutator (gated on an actual
write), document the contract on mark_document_changed, and pin it
with an end-to-end test that settles the cache first and then asserts
the painted scene follows the drag within the same frame.
Desktop tests exercised agent_connect_store against the real
~/.openpencil, so a parallel test run could leave the user's actual
reconnect list in whichever state the last writer happened to save.
Add a process-level user-root override to op-config-store (a OnceLock
seam, not an env var — set_var races parallel test threads) and a
guard at the desktop store entry points that redirects every test
write to a per-pid scratch root, so isolation does not depend on
which test runs first.
Every probe outcome wrote the connected flags through to agents.json,
so one bad launch — a GUI environment missing the user's shell env, a
machine offline at login — persisted connected=false and removed the
provider from the startup replay list for good; the user had to press
Connect every launch. Treat the store as the last-known-good list:
only successes join it, and an explicit Disconnect is recognized by
the card's phase returning to Idle rather than by the flag snapshot,
which also spares providers still queued for replay. The replay queue
now advances through begin_provider_connect — a bare request-seam
write left the card outside Probing, so the landing hook discarded
the outcome and stranded every provider after the first.
The shared 10s probe budget cut off agy's real failure message, which
takes just over 11s to arrive when the process carries no proxy vars —
the card showed a bare timeout one beat before the actionable 'please
sign in' would have landed. Keep 10s for the local version gate and
give the models step, a network round trip, 20s in both the connect
and discovery chains.
cli_version folded spawn failures, non-zero exits and timeouts into
one None and discarded stderr, so a codex launched without the user's
PATH — whose shebang dies with 'env: node: No such file or directory'
— read as an unexplainable 'Codex CLI not responding'. Return a
structured failure carrying the output tail and word the card message
per cause, draining both pipes on capture threads.
The login-shell probe ran 'zsh -lc', which reads .zprofile but not
.zshrc — where homebrew, nvm and proxy exports conventionally live —
so a Dock launch still missed node for shebang CLIs and stalled
networked probes behind a proxy-only route. Switch the probe to -ilc
under a hard 8s deadline (an unbounded interactive rc could hang
startup), and graft an allowlisted set of proxy variables onto the
process alongside PATH, never overwriting values already present.
Killing a timed-out CLI does not close pipes its own children still
hold, so the drain thread's join waited on the grandchild instead of
the deadline — a 400ms probe budget measured 30s wall clock. Replace
drain_pipe with PipeCapture: normal exits still join to EOF, kill
paths take the captured bytes after a short grace instead of joining.
Completes the stringly-error conversion: 341 remaining
Result<_, String> signatures drop to two documented boundary sites
(a String-compat wrapper consumed across a crate boundary and a
test-only diagnostic helper). Eighty-plus enums follow the
established recipe — one enum per failure domain in a sibling
module, byte-identical Display text, From impls replacing map_err
adapters. mcp_live is fully typed (the screenshot channel included)
and all six temporary String bridges are deleted. The flaky
cli-model-discovery trio is made load-proof: exec'd sleeps so the
probe's kill actually closes the pipes, a serializing test lock,
and an escalating budget with a deadline-boundedness assertion.
Adds the campaign stale-path banner (839f474a..d2d8104c), documents
op-util, the shared host-logic module tables, the three input-ladder
spines, the i18n panel-shard key workflow, block_on_anywhere, and the
typed-error recipe; deletes claims the reorg made false (old file
splits, 113-tool count, shell-core paths, five-file property panel).
Every path named was existence-checked against the tree.
apply_cursor_move (1141 lines) and both hosts' apply_press (1052/897)
become short spines calling Option-returning tier helpers in sibling
modules, preserving the documented hit-test priority order exactly —
verified mechanically: every tier body is byte-identical to its
original line range after undoing the three systematic transforms,
and the spines invoke tiers in strictly ascending original order.
No file in the workspace exceeds the 800-line cap anymore.
Pure code motion: twenty-eight remaining oversized modules split into
spine + sibling layouts with re-exports keeping every import path and
test name stable. op-codegen, op-smoke, op-mcp, op-design-lint,
op-host-services, op-host-desktop, op-cli, op-i18n, op-editor-core,
op-editor-ui, and op-pen-loader no longer have any file over the cap.
Agent-settings press logic is single-sourced with an explicit
credential-ownership scope (operator vs browser); the blank-press
blur path uses the full commit chain on both hosts; the marquee
release commit is shared (web was missing the entered-container sync
— a real bug); sixteen inline screen-to-doc conversions route through
one geometry helper; the wall-clock-zero file-menu anchor is verified
safe and documented. Host spines, keyboard, input tests, agent
settings tests, and the skia backend split under the 800-line cap;
press_helpers drops from 837 to 165 lines.
Completes the widget_host fork unification: drag commit/preview
policy, image-crop gesture, scroll handlers, cursor-hover flows,
account/a11y/missing-fonts/variables-panel presses, drag-state
structs, and the canvas/overlay geometry (the coordinate invariant
now lives in one file) are all shared between the native and web
hosts. Proven drift fixed with history evidence: native shift-drag
skipped the undo snapshot, native painted two variables menus at once
(stale row-menu clears), commit sequencing dropped drafts on
Resize/Close, and the per-press full-document variable-table walk is
replaced by direct state reads. Intentional platform differences
remain host-side with comments.
McpServeError separates protocol faults from socket failures;
ExportError carries structured fields with byte-identical Display;
ProgramError gains precise InvalidValue/ValueOutOfRange variants.
Fifteen map_err adapters in the web canvas server collapse into From
impls. The flaky cli model-discovery trio is fixed at the root: the
test scripts now exec their sleep (the forked child used to inherit
the probe's pipes and hold the reader for the full duration) and poll
with a doubling budget, turning an assumed deadline into a proven one.
The native/web widget_host twins' press/click, keyboard, and
property/image dispatch layers now share one implementation
(op-editor-core host_*_transitions for pure state, op-editor-ui
press_flow/chat_click_flow/property_panel_dispatch for widget-typed
logic); hosts keep thin platform arms. Proven drift fixed along the
way: web's Interactions actions were silent no-ops, web left font
pickers open behind property popovers, native's image-src writes
skipped the pan-cache invalidation, and FocusEffectParam dropped the
previous draft on native. Intentional platform differences stay
host-side with comments.
Pure code motion across seven crates (orchestrator, host-services,
host-desktop, editor-core, pen-loader, editor-ui, mcp, canvaskit):
spine keeps the public surface, siblings take cohesive clusters,
re-exports keep every import path compiling unchanged. Test counts
are identical before and after per crate; op-orchestrator and
op-host-desktop no longer have any file over the 800-line cap.
Every remaining private current-thread runtime and bare block_on in
sync code now routes through block_on_anywhere, removing the latent
'runtime within runtime' abort on tokio workers. The desktop design
pump's per-apply-ack full EditorState clone (and the subtask-retry
seed) switch to the narrowed snapshot — chat/codegen/theme_presets
grow with session length and are read by nothing in the worker mirror.
Spine + seven siblings (all under the 800-line cap) with re-exports
keeping every import path stable; three measured field groups become
substructs (preview, size toggles, design-md panel — 192 -> 176 flat
fields; larger clusters rejected by workspace touch-count); MCP and
chat-launch request snapshots drop chat/codegen/theme_presets, which
grow with session length and are read by no consumer.
CliError/SkillInstallError carry every op-cli failure (94 sites),
ProgramError types batch_program (14), WebCanvasError maps route
failures onto their existing HTTP statuses (23 -> 6 deliberate
boundary adapters). Display output is byte-identical to the old
strings so caller-visible text and tests are unchanged.
One block_on_anywhere helper (block_in_place inside a multi-thread
runtime, shared runtime otherwise, loud failure on a current-thread
runtime where both options deadlock); acp transport channels bounded
at 1024 with reader-safe try_send; a stranded child process + reader
thread leak in model discovery fixed; detached threads either joined
or documented; Antigravity/Grok probe strings localized (catalog 1105).
New leaf crate op-util single-sources hex-color parsing (9 divergent
copies, one with a non-ASCII panic), JSON escaping (one copy was
lossy), and HTML/XML escaping (one copy missed the quote entity — an
attribute-injection gap). Desktop now delegates image generate/search,
settings payload serde, and the --mcp/--serve-web argv dispatch to
op-host-services / op-editor-host-core instead of carrying drifted
copies. Byte-identical widget_host twin files collapse into shared
op-editor-core host_ui_transitions. Auth routes, the MCP port, product
name, env-var names, service URLs, and status colors move to single
shared constants / theme tokens; the stale claude-sonnet-4-5 default
model id is corrected.
Native menu bar, provider status strings, export dialog, layer panel,
and image-panel popovers now resolve through op-i18n instead of
hardcoded English; the three ad-hoc mini locale tables fold into the
canonical catalogs. New *_panel.rs overflow shards keep every table
under the 800-line cap (catalog at 1098 keys). The desktop menu also
rebuilds live on locale change instead of waiting for the next launch.
Includes the pending interactions/quick-action/dialog locale entries
and locale.rs additions from the working tree.
Poison-tolerant locking in the ACP transport (a panicking reader task
used to brick every later call), checked thread spawns on UI paths
(thread creation fails under fd/memory pressure and took the editor
down with unsaved work), MCP tools return typed errors for malformed
LLM-controlled args, bounded name-suffix loops, and event-driven chat
stream polling instead of a 20ms sleep loop.
The SignIn press now opens a same-origin /auth/loading spinner page and
fires the begin request inside the same user gesture; the daemon holds
that request until the pairing's verification URI exists and returns it
inline, so the popup navigates straight to the sso page instead of
sitting on about:blank through a poll cycle. All account/login strings
moved out of hardcoded per-locale matches into the op-i18n tables and
are translated across all 15 locales.
The status poll fired in the same tick as the begin POST and observed
the daemon's pre-begin idle state, which tore the flow down after one
click (leaving only the blank placeholder popup). Polls are now
suppressed while begin is in flight, and idle only ends the flow after
a sustained streak.
Opening the verification page from the async poll callback was popup-
blocked (the 'click sign-in twice' bug); a placeholder window opened
synchronously inside the press is navigated once the URL arrives, and
closed on cancel/failure. The wasm login modal now says 'popup window'
instead of 'your browser' — the web editor already runs in one.
Bundled foreign archive members were fed to thin-LTO and failed with
'failed to get bitcode from object file for LTO'; -bundle defers the
archive to the final link.
The wasm bundle ships no auth code: the daemon proxies the device-login
flow over /api/auth/* using the same prebuilt op-auth library and the
same ~/.openpencil/auth credential store as the desktop GUI, so a
session started in either host signs both in. The web shell opens the
verification page in a popup, polls flow progress into the shared login
modal, and re-checks session health every 30s. The proxy refuses
non-loopback binds outside managed mode — the daemon session belongs to
its owner, not to whoever can reach the port.
The approval page already labels the product and platform, so the
device name must be just the machine (scutil ComputerName / hostname);
the account dropdown's workspace row was a no-op placeholder.
Extends the prebuilt matrix to x86_64-apple-darwin and both linux-gnu
architectures (built against glibc 2.17); the msvc artifact follows the
<name>.lib convention build.rs now resolves per target.