The MCP spec says a ping result is empty, but both ping formatters put
the OpenPencil discovery identity (server/mode/token) at the result top
level. Gemini CLI validates ping with the TS SDK's strict EmptyResultSchema
and marked the server disconnected with 'Unrecognized keys: server, mode,
token' (issue #199). _meta is the spec's sanctioned extension point and
the only key the strict schema permits, so the identity now rides there;
the op CLI reads _meta first and falls back to the legacy top-level shape
so it still discovers a running pre-0.8.3 editor. (A pre-0.8.3 op CLI
cannot discover a 0.8.3 live editor — the CLI ships with the app, so only
a stale op on PATH hits this.)
A multi-phase campaign to make importing real modern web pages
(Tailwind output, landing pages, component-library HTML) faithful.
Layout: bake CSS transforms (translate/scale/rotate incl. the
translate(-50%,-50%) centering idiom), emulate flex-wrap via row
chunking, honor grid span/line placement, apply position:relative
offsets through synthetic wrapper frames (jian has no offset-without-
reflow), aspect-ratio, per-child auto-margin alignment, and a
configurable viewport height (threaded through the CLI and MCP).
Content: list markers (incl. reversed/roman/alpha), basic table layout
(tr->row, colspan), <picture>/srcset candidate selection, @font-face
visibility warnings, background-size/position mapped to the image
fill's crop transform, and text-shadow.
Diagnostics: replace the untyped Vec<String> warnings with a typed
ImportWarning enum (stable per-variant codes, byte-identical Display so
CLI/MCP output is unchanged), localized into all 15 locales, surfaced
through a non-modal post-import diagnostics panel wired into both hosts.
The desktop path now forwards every warning, not just the first.
Snapshot import: fix inverted child paint order (the cause of whole
overlays vanishing under full-bleed backgrounds), stop clipping text to
the browser-measured box under font-metric drift, vectorize inline SVG
to editable paths, and capture per-corner radii, background paint,
position/z-index, video posters, page background, and open shadow DOM.
The extension's tokenless snapshot-ingress route lives here too
(op-host-services), scoped to a chrome-extension origin and the single
insert-only import tool.
Known follow-up: rich inline text runs (links/code spans + wrapping)
can still overlap in the snapshot path.
The live MCP endpoint on 127.0.0.1 was a bypass of the collaboration
admission model. Its per-instance token authenticated only the ping probe
and shutdown, so document reads and writes were available to any local
process, and nothing validated Origin or Host — a page in a browser on the
same machine could reach it by DNS rebinding. During a session that is the
shared document, not just this user's file.
Every stateful call now requires the instance token, compared without an
early exit. Host must be a numeric loopback literal on the bound port, and
an Origin, when present, must match it; a request with no Origin still
works, which is what real CLI clients send. OPTIONS, initialize, and ping
stay tokenless so CLI discovery keeps working, and CollabGatePolicy is
untouched — this sits in front of it.
The `op` CLI did not send the token, so authenticating tool calls would
have returned 401 for every `op` invocation against a live editor. The
token was already in the port file next to the port; it is now resolved
with the port and travels as a header. Ping and shutdown keep their
existing tokenless wire contract.
Bootstrap cache: reads now degrade like writes already did. An unreadable
or corrupt cache leaves this start with no anti-rollback generation floor,
which is the position an absent cache has always left it in, and which the
threat model already accepts because deleting the file achieves the same
thing with no more privilege than corrupting it. Refusing bought no
security and cost the ability to collaborate at all. The tests state the
price plainly: with no floor the lower-generation document is accepted,
and `rollback_floor_armed` has to report it.
Threat model: correct an overstatement. Peer admission requires the remote
ticket's subject to equal the local account, so the product pairs only
devices of one account today. A relay operator reconstructs which devices
of an account sync and when — not a cross-account collaboration graph.
Also records that the relay reads exactly one field out of the ticket it
verifies, the expiry, which makes the identity disclosure gratuitous
rather than load-bearing, and states what a minimized credential would and
would not buy.
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.
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.
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.
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.
The opencode target wrote a plugin entry the loader can never use (no
JS entrypoint, and plugin packages' skills/ are never scanned), so
'op install --target opencode' delivered nothing. Mirror the codex
layout instead: bundle under ~/.config/opencode/openpencil-skill with
a skills/ symlink opencode's {skill,skills}/**/SKILL.md scan picks up.
The installer-owned entry is recreated on every install so stale
squatters can't shadow it; remove_path only treats NotFound as absent
and handles dangling Windows directory symlinks.
Preflight headless document loading so malformed or binary archives fail with a clear, actionable error before the MCP server starts.
Keep the CLI parser dependency lightweight by disabling op-pen-loader default features and include the updated lockfile plus regression coverage.
op design gains explicit script routing (--script flag, implied for
@file.js/.mjs payloads) and the vendored openpencil-skill bundle
(0.8.0) teaches script mode with the same contract the internal
SCRIPT_FORMAT prompt uses.
Four Windows runtime defects from the platform audit:
- The binary stayed in the console subsystem, parking a console window
behind the GUI when launched from Explorer. Release builds now set
windows_subsystem = "windows"; debug keeps stderr tracing visible.
- Background CLI probes (model discovery, provider version checks) and
the vendored Claude SDK's per-turn spawns lacked CREATE_NO_WINDOW,
flashing console windows once the GUI detaches from the console.
- MCP stdio servers naming .cmd/.bat shims (npx and most npm-installed
servers) could not spawn: CreateProcess cannot execute shims and Rust
1.77+ refuses them as program names. vendor/agent now resolves the
command PATHEXT-style against the PATH the server will actually see
(per-server env override wins) and routes only genuine shims through
cmd /c — real executables keep direct spawn semantics.
- cmd /C start truncated URLs at `&` (every OAuth authorize URL). The
URL now travels double-quoted via raw_arg so cmd keeps it literal.