Commit graph

1508 commits

Author SHA1 Message Date
Kayshen-X cfb2435fa3 fix(web): recheck bridge token at fallback completion to close late-init window
The pass-1 late-init fix captured `managed` before the fallback reset was issued,
so a host `init` landing DURING the reset round-trip (up to ~30s with the XHR
timeout + retry) left `handle_init` firing an unregistered hook (no-op) while
`complete()` re-checked the stale flag -- neither emitting `ready` nor arming a
hook that could still fire. Wedge.

Decide readiness from the LIVE token in `complete()`: token present since capture
-> emit `ready` directly; token arrived during the round-trip -> run the managed
recovery inline; token still absent -> register the one-shot LATE_INIT_HOOK. The
inline and hook paths share one guarded `run_late_init_recovery` (tokened reset ->
emit_ready), so `ready` cannot double-fire. Add two source-order structural tests
(handle_init emits no `ready` directly; completion re-checks bridge_token live).
2026-07-17 04:18:11 +08:00
Kayshen-X 2fd88b601e fix(web): drop redundant external-apply dirty bump
The `if undoable { mark_document_changed() }` added to the live-sync glue was
redundant: `replace_document_with_undo` already bumps the content revision via
`history_push_past` -> `mark_document_changed`, so an undoable external apply is
dirty by construction. Remove the glue's manual bump (and its now-wrong comment);
the post-apply pair capture + `note_synced` stay put, since that pair already
carries the history bump.

Retarget the editor-core test to lock the real invariant:
`replace_document_with_undo` ALONE leaves the state dirty (revision != saved),
while plain `replace_document` stays clean -- so a future refactor of
`history_push_past` can't silently break external-apply dirtiness.
2026-07-17 04:06:17 +08:00
Kayshen-X ba9efd43fb fix(web): keep bridge ready reachable on late init and stalled reset
Two independent paths could leave the VS Code webview waiting forever for a
`ready` it never receives.

Late init: `mount_ck` captured `managed` once after `await_init`'s 2s timeout,
so an `init` arriving later stored the token but never re-ran the managed
bootstrap. Add a one-shot `LATE_INIT_HOOK` that the fallback (unmanaged)
bootstrap registers after its own reset completes; `handle_init` takes and fires
it, re-running a tokened sync-reset whose completion emits `ready`. Registration
is gated on `!managed && is_iframe` and happens only post-fallback-reset so the
two resets cannot interleave.

Stalled reset: `start_bootstrap_reset` used `post_json` (no XHR timeout), so a
hung connection fired neither success nor error. Route it through
`post_json_with_status`, which arms a timeout and delivers status 0 + empty body
on timeout, landing on the existing retry-then-complete-with-warning path.
2026-07-17 04:06:04 +08:00
Kayshen-X 154a7233f7 fix(editor): mark external MCP/AI applies dirty
The undoable external-apply path (`replace_document_with_undo`) leaves
revision == saved_revision, so an AI turn or MCP client write applied
into a live session reported `is_dirty() == false`. The bridge then
emitted `dirty:false` for genuinely unsaved daemon-side edits, and
closing the tab dropped them without a save prompt — the spec requires
MCP edits to mark the tab dirty.

Fix at the wiring level (leave `replace_document*` semantics intact so
opens stay clean): the live-sync glue's apply path now bumps the content
revision via `mark_document_changed()` after a successful undoable apply,
BEFORE capturing the post-apply pair for `note_synced`. Ordering:
apply → mark_document_changed (undoable only) → post_apply pair →
note_synced(post_apply). The gate baseline then equals the current pair
(no spurious echo-push next tick) while `is_dirty()` is true (revision 1
vs saved 0) so the observer emits `dirty:true`. The bootstrap apply
(undoable == false) stays clean.

The glue ordering is wasm-only (compile-gated); a native op-editor-core
test locks the primitives it relies on: replace_document_with_undo +
mark_document_changed is dirty with revision != saved_revision, while
the plain replace_document open path stays clean.
2026-07-17 03:36:26 +08:00
Kayshen-X 16927aa438 fix(web): serialize bridge sync-reset before ready
The managed webview emitted `ready` from `handle_init` the moment the
host's `init` landed, but the bootstrap sync-reset was issued only after
`await_init` resolved — independently and still in flight. The host,
seeing `ready`, could send `open-document`; its push landed on the
daemon, then the in-flight reset reset the daemon to its `--file`
content and bumped the version, and the next pull tick pulled that reset
document over the just-opened canvas (silent overwrite).

Serialize `ready` strictly after the reset: `handle_init` no longer
emits it, and canvaskit's managed bootstrap posts it (via the new
`emit_ready`) from the reset-completion callback, then starts the
live-sync ticks. `ready` is a request/response reply, not an edge event,
so a direct post keeps single-point edge discipline intact. A
transport/server reset error retries once then proceeds anyway with a
console warning — a wedged webview that never says `ready` is worse than
one on a best-effort daemon; a peer `"skipped":true` reply counts as
completion. Direct-open (no bridge token) is unchanged.
2026-07-17 03:36:08 +08:00
Kayshen-X 13ea1704b4 docs(web): refresh stale cross-task comments to describe landed behavior 2026-07-17 03:12:23 +08:00
Kayshen-X f0bdad77e6 test(web): end-to-end smoke for the managed daemon contract 2026-07-17 02:52:05 +08:00
Kayshen-X cd465f9899 fix(web): keep credential sync reset ahead of repaint wiring in mount
Task 7 reordered mount_ck so the daemon-dependent
web_credential_sync::start() runs only after the postMessage bridge
init gate. But start() bundled two things: a pure state reset
(*self = default) and a daemon policy fetch. Moving the whole call past
the gate also moved the reset past repaint_coalescer::install, so an
early repaint (e.g. during the 2s await_init in an iframe) could queue a
credential change via credential_changed() that the later reset silently
wiped — the pre-existing invariant guarded by
canvaskit_repaint_persists_local_settings_and_syncs_only_credential_changes.

Split the two phases: add web_credential_sync::reset() (pure state
clear, no daemon request) called BEFORE the first repaint and the rAF
coalescer install, and keep start() (now begin_policy_check only, the
daemon fetch) after the bridge gate. Both invariants hold: the reset
precedes repaint wiring, and no daemon request fires before the gate.
The daemon behavior is unchanged in the normal flow (reset() then
begin_policy_check equals the old start()), and strictly better in the
race edge — a change queued between reset and start is now preserved
instead of wiped.

Test updated to assert the reset (not the now-daemon-facing start)
precedes install; the semantic invariant it protects is unchanged and
is now expressed against the actual code order. start()'s ordering after
the gate stays covered by
canvaskit_mount_queues_an_initial_snapshot_only_when_local_credentials_exist.
2026-07-17 02:00:02 +08:00
Kayshen-X b6077ab09b fix(web): bind web save baseline to the serialized generation and revision 2026-07-17 01:22:07 +08:00
Kayshen-X 6de61689df fix(web): attach bridge token to the mcp server settings fetch
The Task 7 token audit missed a daemon-bound network call:
`agent_settings_mcp_server.rs::request_mcp_server_update` POSTs
`/api/mcp/server` via `window.fetch` (Reflect::get) and attached no
`X-OpenPencil-Token`. In managed mode (VS Code webview) the daemon's
auth gate rejects the request (fails closed), silently breaking the
MCP server start/stop toggle in the settings modal.

Fix: build the request URL from `daemon_base()` (absolute, matching
every other daemon call site in the crate) instead of a bare relative
path, and attach the token via a new shared `live_sync::daemon_token_for`
helper — factored out of `attach_daemon_headers` so XHR and non-XHR
call sites can't drift on the leak-guard policy (token only when set
AND the URL targets the daemon).

Corrected audit (grep -rn "XmlHttpRequest|fetch|Request::new"
crates/op-host-web/src, network-issuing sites only): 5 sites total —
the 4 live_sync XHR helpers + web_model_catalog + web_ai_transport +
iconify_web's fetch_text all already tokened via attach_daemon_headers
(iconify_web additionally verified to withhold the token from the
public Iconify CDN target); agent_settings_mcp_server's window.fetch
was the sole untokened site and is now fixed. No other window.fetch /
Request::new call sites exist in the crate.

Verification: cargo check --target wasm32-unknown-unknown -p
op-host-web --no-default-features --features canvaskit (pass, 1
pre-existing unrelated warning) / --features web (pass, clean);
cargo clippy -p op-host-web --all-targets -- -D warnings (pass, clean).
2026-07-17 01:17:08 +08:00
Kayshen-X 37c41db35a feat(web): postMessage bridge with token bootstrap and conflict resolution
Add the webview-facing postMessage bridge (vscode_bridge.rs): token
bootstrap via Init, OpenDocument probe-conditional push, uncapped
Snapshot flush, SaveCommitted, and UseLocal/AcceptRemote conflict
resolution. All three edge/state events (dirty-changed, sync-conflict,
opened) are emitted only from the tick observer draining SyncGate's
consumable latches; handlers only mutate the gate. Reorder mount_ck so
the bridge listener installs first, an iframe awaits Init (2s fallback),
and the 400ms pull tick starts only after the daemon sync-reset (managed:
token; direct: legacy reset moved out of index.html) completes.

Token audit (X-OpenPencil-Token attached only when url starts with
daemon_base(); public requests never carry it):
  live_sync.rs get                 -> daemon -> attach_daemon_headers
  live_sync.rs get_with_status     -> daemon -> attach_daemon_headers
  live_sync.rs post_json           -> daemon -> attach_daemon_headers
  live_sync.rs post_json_with_status -> daemon -> attach_daemon_headers
  web_model_catalog.rs:21 (/api/ai/models)   -> daemon -> attach_daemon_headers
  web_ai_transport.rs:93 (/api/ai/stream)    -> daemon -> attach_daemon_headers
  iconify_web.rs:311 fetch_text (daemon brand-catalog + public Iconify CDN)
      -> attach_daemon_headers with url-prefix guard: token only on the
         daemon URL, never on the public api.iconify.design requests
All other daemon-talking modules route through the live_sync helpers and
inherit the token. No window.fetch / Request::new sites in the crate.
2026-07-17 01:06:14 +08:00
Kayshen-X 8dfd5bfbe7 feat(web): route live sync through the shared gate with conditional pushes 2026-07-17 00:25:20 +08:00
Kayshen-X f0111bfc4f feat(web): idempotent sync-reset and base-version conditional document writes 2026-07-17 00:07:03 +08:00
Kayshen-X 51770c07d7 feat(web): serve-one layer token auth and origin allowlist for managed daemon 2026-07-16 23:54:38 +08:00
Kayshen-X 0574ec36a9 feat(web): managed serve-web contract with handshake and parent-death lease 2026-07-16 23:28:08 +08:00
Kayshen-X c277cfd012 feat(editor): sync gate state machine and bridge protocol codec 2026-07-16 23:13:34 +08:00
Kayshen-X e01542c18c feat(editor): scope saved-revision acks to a document generation 2026-07-16 22:58:23 +08:00
Fini f5ffbb7d14 fix(canvas): align generating label icon with text 2026-07-16 21:19:58 +08:00
Kayshen-X 70e2301aae fix(web): sharpen rendering and isolate local zode config 2026-07-16 21:15:10 +08:00
Kayshen-X f8540d81d8 test(editor): isolate canvas indicator state 2026-07-16 00:15:18 +08:00
Kayshen-X d5131c99e7 fix(orchestrator): center resolved radial content 2026-07-15 23:49:58 +08:00
Kayshen-X 77a984948e fix(desktop): order chat tool events deterministically 2026-07-15 23:49:32 +08:00
Kayshen-X d978a63d63 test(services): make settings temp paths portable 2026-07-15 23:13:08 +08:00
Kayshen-X c355719fd5 fix(ci): clear Rust workflow blockers 2026-07-15 22:47:39 +08:00
Kayshen-X ab1218eab0 fix(web): restore CanvasKit test coverage 2026-07-15 22:47:28 +08:00
Kayshen-X a37d9a8d3d fix(orchestrator): stabilize radial repair checks 2026-07-15 22:47:18 +08:00
Kayshen-X f82f4e4ea3 fix: enforce centralized version inputs 2026-07-15 21:17:21 +08:00
Kayshen-X 7c8e2abfa7 test(cli): enforce complete bundle templating 2026-07-15 21:17:21 +08:00
Kayshen-X 83063bc279 refactor(cli): render bundled skill version from Cargo 2026-07-15 21:17:21 +08:00
Kayshen-X 5b0d1edb4c test: decouple document fixtures from product releases 2026-07-15 21:17:20 +08:00
Kayshen-X e0a2bd4e69 refactor: derive Rust product versions from Cargo 2026-07-15 21:17:20 +08:00
Kayshen-X b542a7e5d1 test(panels): align legacy gutter assertions 2026-07-15 21:17:20 +08:00
Kayshen-X 9bd9adf505 test(panels): strengthen layer gutter regressions 2026-07-15 21:17:20 +08:00
Kayshen-X 8e2cca4f6b fix(panels): align labels with layer action gutter 2026-07-15 21:17:20 +08:00
Fini 050fac23cd fix(agent): separate CLI generation and canvas modes 2026-07-15 21:10:11 +08:00
Fini fff96c8ad5 feat(agent): unify generation UX and CLI providers 2026-07-15 03:48:29 +08:00
Fini 652afc9031 fix(orchestrator): preserve and repair progress rings 2026-07-15 03:29:39 +08:00
Kayshen-X 3a41f2ea7f feat(web): make credential persistence deployment-aware 2026-07-14 23:36:37 +08:00
Fini a02b124aae fix(desktop): align design session identity and lifecycle 2026-07-14 00:30:01 +08:00
Fini 6e2bbd379a refactor(agent): make design repair explicit and bounded 2026-07-14 00:30:00 +08:00
Fini eea8901042 fix(mcp): harden batch design and image semantics 2026-07-14 00:29:59 +08:00
Fini 44a4fe4e24 feat(editor): align canvas layout and sizing behavior 2026-07-14 00:29:58 +08:00
Fini c7e7e45810 fix(desktop): find image slots by geometry, not by name
DeepSeek ships a card's photo area as an UNNAMED rectangle sized
fill_container x fill_container - no keyword, no number - so every
name-and-size heuristic missed it and the whole page came out as grey
boxes. What a slot IS is a question about geometry, so the pass now asks
the real layout: an empty painted box that RESOLVES to picture size, in a
card that has words in it, is a photo slot. The words come from the card's
own subtree (a title nested in an info frame still names the picture), the
nearest ancestor that names the subject wins over them, and a cousin card
is never consulted.
2026-07-14 00:29:57 +08:00
Fini f6fe94bc1a fix(agent): explain the fresh-sandbox rule, and paint rich text at real widths
A batch died on "header is not defined": the model referenced a const from
the PREVIOUS batch's script, but every script runs in a fresh sandbox. The
error now says so and points at the fix (reference the node by its id
string), and the program-DSL contract states it up front.

The transcript's markdown also painted every span at the wrap ESTIMATE
(6.6px per unit) while skia draws real glyph advances, so each span after
the first sat at a slightly wrong x - code chips drifted off their words
and a sentence read as fragments. Paint now advances by the measured width;
wrapping stays estimate-based, since it must stay backend-free.
2026-07-14 00:29:56 +08:00
Fini f369e91390 fix(desktop): recognize the abbreviations weak models name image slots with
MiniMax-M3 shipped a whole travel page as grey boxes: it builds every card
around a rectangle named "img" (or a "ph" rectangle inside an "img"
wrapper), and neither word was in the image-slot keyword table, so the
enrichment pass never saw a single slot. Both are slots now - and because
such a name carries no subject of its own, the query falls through to the
card that names the picture ("Santorini").
2026-07-14 00:29:55 +08:00
Fini f04e0a8fb3 fix(editor): give the cursor a clean white rim that cannot jag
The rim was a polygon STROKE, and the trait's fallback draws a polygon
stroke as one capped line per edge - so every vertex of the densely
sampled arc notched and the rim's width visibly wobbled. Two fixes: the
native backend gains a real single-path, round-joined, antialiased
polygon stroke (every caller benefits), and the cursor's rim is now
FILLED geometry - the silhouette outset by an exact 1.6px, painted white,
then covered by the body. The halo layers clear the rim so the outline
reads crisp against both light and dark designs.
2026-07-14 00:29:54 +08:00
Fini 8c07452298 fix(agent): teach the token vocabulary, and stop broken refs from painting nothing
The search bar came out grey with an invisible filter icon. Two causes,
both mine to fix.

The model painted the bar $--accent believing it was the brand colour -
in the shadcn vocabulary --accent is a quiet NEUTRAL surface and the brand
colour is --primary. The design-system teaching I added told the model to
use tokens but never said what they MEAN; it now carries the semantics
table, with the neutral-vs-brand trap called out.

The filter glyph was painted $--white - a token in no table. An unresolvable
reference silently rendered as an invisible glyph. A broken reference is now
repaired against the surface it sits on: a glyph on a soft tint takes that
tint's colour at full strength, a glyph on a token surface takes that token's
-foreground partner, and a container's unknown fill is dropped rather than
guessed.
2026-07-14 00:29:53 +08:00
Fini f1178c76c1 fix(desktop): one subject, one photo - a rebuilt card keeps its picture
Watching a run frame by frame: a real Bali temple photo landed, then
halfway through it turned into a plain blue sky. The model had rebuilt
the section (fresh node ids), the same query searched again, and the
session-wide dedup - there to stop two cards sharing one picture - now
skipped the very photo that query had already chosen, leaving the junk
below it. A query this session already answered now resolves from a memo:
no network call, and no downgrade. Dedup still guards different subjects
from sharing a photo.
2026-07-14 00:29:52 +08:00
Fini c5feaeec01 fix(desktop): open a multi-page file where its design actually is
A 16-page document whose first page was a blank cover opened onto empty
canvas ("content bbox None") and read as a load failure - the design was
on page 2. With no editor metadata to restore, the file now lands on the
first page that has content; a file that is empty everywhere still opens
on page 1.
2026-07-14 00:29:51 +08:00
Fini 985654e913 feat(editor): render the narration as typed markdown, not a grey wall
The model writes markdown - bold labels, hex codes in backticks, bulleted
build summaries - and the panel flattened all of it: markers stripped,
bullets faked by gluing a dot into the string, code indistinguishable
from prose. The transcript now renders a deliberate subset (**strong**,
`code`, and - bullets) as real typography: labels in bold on the
foreground tone, body in the muted one, code in a tinted chip, bullets
with a hanging indent so wrapped lines align under the text. Unclosed
markers stay literal - a syntax that silently eats characters is worse
than none.
2026-07-14 00:29:50 +08:00