Commit graph

166 commits

Author SHA1 Message Date
Kayshen-X e6013d71da fix(collab): rotate production trust roots safely 2026-08-09 17:17:08 +08:00
Fini d00c763f6e fix(services): carry cli output into failures as bounded redacted tails
A failing CLI turn surfaced only its exit code: the stderr drain task
raced the tail read (a ~40% loss under concurrent subtasks), the
fallback branch discarded whatever the tail held, and friendly
classification swallowed the evidence when it did match. Failures now
join the drain within a grace window, quote a head-and-tail excerpt
with secrets redacted before truncation, keep the classified verdict
and the excerpt together, and the ACP connect path stops discarding
its child's stderr line by line. Auth keyword matching gains word
boundaries so 'authored' no longer reads as an auth failure.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-09 02:22:02 +08:00
Kayshen-X d909578ddb fix(web): close the review's high-severity online gaps
- SSE subscribers hold a single latest-value slot (weak-pruned) so a
  stalled reader coalesces ticks instead of growing an unbounded queue
- /api/file/save clears the same whole-document gate as open-recent:
  409 collab-active during a session instead of silently desyncing the
  runtime's hash and commit log
- document ingest returns a typed outcome; only Committed bumps the
  version, Rejected/Failed surface codes the browser's existing
  conflict recovery already handles, and the local-edit capture closes
  through an RAII guard on every path including unwind
- a ?tenant= request now passes persisted-ACL admission before any
  tenant materialises, so unauthorised probes cannot occupy capacity
- ACL edits and their durable write serialise under one mutex with
  unique temp files; a failed write rolls back and returns 500 rather
  than resurrecting revoked grants after restart
- API-token REST calls are scope-checked by method (session cookies
  exempt) and a token naming no mcp:* scope now gets nothing instead
  of everything — hub-issued tokens must carry explicit scopes
- SIGTERM/SIGINT in the online loop raise the existing shutdown flag
  so container stops run the tenant flush
2026-08-08 17:03:47 +08:00
Kayshen-X f70bf89006 fix(web): unblock session node creation, isolate account switches, persist online tenants
Three review blockers:

- the session push gate refused every id absent from the daemon
  snapshot, which included all ids the namespace allocator mints — so
  nodes created during a session never synced and were erased by the
  next pull. Unknown ids now pass when they parse into this peer's
  namespace (parsed comparison, not prefix match); with no allocator
  the old fail-closed behaviour stands
- a same-tab account switch kept the previous account's document,
  sync baseline, and stored credentials. An identity epoch keyed on
  the auth subject (tri-state, so sign-out then sign-in is not
  mistaken for first contact) resets the sync client, gate, document,
  collab state and latches; 401/403 suppress pushes until the reset;
  browser storage partitions by subject with legacy keys deliberately
  not adopted
- online tenants lived only in memory under the default deploy:
  compose now mounts a data volume, startup fails closed when
  eviction is enabled without a store (OPENPENCIL_ONLINE_EPHEMERAL=1
  is the loud demo bypass), a dedicated sweeper replaces the
  connection-triggered sweep that never ran while idle, and shutdown
  flushes every resident tenant
2026-08-08 16:29:41 +08:00
Kayshen-X 08a26548fe feat(web): daemon-side collaboration service behind /api/collab routes
Give the serve-web daemon a real collaboration runtime so browser and
VSCode-webview editors can drive public-relay sessions through REST,
mirroring the web_auth proxy pattern:

- WebCollabState + a dedicated driver thread (wake channel + 250/100ms
  tick) run CollabRuntime against the daemon document; documentRevision
  and collabSeq are separate so presence/UI changes never trigger a
  whole-document pull
- versioned wire DTOs (CollabStateWire/CollabActionWire, wireVersion 1)
  map through explicit validation onto the internal UI types instead of
  serde on opaque internals; GET state / POST action / POST presence
  routes ride the existing /api/ auth + origin guards
- document pushes during an Active session ingest through a split
  PreparedDocument::prepare (off-lock validation) +
  install_prepared_document (infallible, in-generation) inside a
  begin/finish_local_edit capture; identical pushes produce no txn
- gate_daemon_mutation centralizes session-time refusals (409
  collab-readonly/busy/active) across document push, sync-reset,
  open-recent, /mcp JSON-RPC, and AI apply paths, matching the desktop
  CollabGatePolicy semantics
2026-08-07 21:14:28 +08:00
Kayshen-X 89151683b0 refactor(desktop): extract collab runtime into op-collab-host crate
The 9.8k-line collaboration runtime was welded to the desktop GUI
(WidgetHostNative + winit EventLoopProxy), so the web daemon and future
satellite hosts could not run sessions. Split the host coupling behind a
CollabHost trait + wake notifier closure, then move the runtime, JWKS
fetcher, and tests into the new leaf crate op-collab-host:

- CollabHost (CollaborationEditorHost + dirty/id-namespace hooks) with a
  HeadlessCollabHost for daemon and test use; the WidgetHostNative impl
  lives behind op-host-native's gl-host feature
- async work reaches sync relay/JWKS code through an injected
  BlockingExecutor (process-global OnceLock) instead of depending on
  op-host-services, keeping the crate graph acyclic
- desktop keeps a thin shim (type alias + wake closure); call sequence
  and test assertions unchanged, 154 runtime tests moved as-is
2026-08-07 19:55:26 +08:00
Kayshen-X ff8b73420c chore: bump version to 0.8.3 2026-08-06 21:15:39 +08:00
Kayshen-X bc1f35ef9b feat(web): chrome extension offline download emits a ready-to-open .op file
The download fallback used to save the raw capture snapshot JSON, which
OpenPencil cannot open directly. Route the snapshot through op-html's
import_snapshot_document in the wasm core so the extension hands back a
canonical .op document (with node count reported and empty captures
surfaced as an actionable error instead of a broken file).
2026-08-05 22:15:09 +08:00
Kayshen-X a9c1401e1a feat(renderer): rasterize svg image sources at the byte-cache seam
Skia and CanvasKit decode PNG/JPEG/GIF/WebP but not SVG, so every
captured page's inline-svg fallback and remote .svg painted as the
dashed placeholder forever.

- Native: resvg (minimal features, no text/raster-images) rasterizes
  SVG bytes to PNG where they enter the shared byte cache, so both the
  data-URI decode and the remote-fetch store paths only ever cache
  bitmap codecs. Target-gated off wasm32: measured +0.9 MiB gzip
  against the web bundle's 6 MiB ceiling.
- Web: the CanvasKit bridge falls back to the browser's own SVG
  decoder (async Image + 2d canvas -> CK.MakeImage). Pending decodes
  report success so the id is not negative-cached; the repaint pump
  keeps frames coming until the raster lands.
- The remote-image fetcher's magic-byte sniff now accepts SVG markup,
  and percent-encoded (non-base64) svg data URIs decode too.
2026-08-05 22:15:09 +08:00
Fini 001ecfd617 fix(editor): accept ime punctuation that commits without a composition
Chinese IMEs insert CJK punctuation instantly, with no marked-text
session. On the web host those characters were invisible: the hidden
input only listened for composition events, and keydown was blocked
while composing. Text now flows through beforeinput whenever the
hidden input truly owns DOM focus, and the printable keydown branch
closes in that state so nothing double-inserts. Host-side contract
tests pin bare commits landing at the caret across mixed composed and
bare sequences on both the chat input and canvas text editing. The
matching macos platform-layer fix lives in the casement fork and
travels separately.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-05 00:50:22 +08:00
Kayshen-X 64d70d672d feat(extension): add OpenPencil web-capture Chrome extension
MV3 extension that captures the rendered active tab (via the shared
snapshot-extractor contract) and imports it into OpenPencil. Logic lives
in the new op-chrome-extension-core crate (wasm): endpoint rules,
chunked-transfer integrity, /mcp envelope + reply classification,
download-name sanitisation, SSO/account session parsing, and hub
snapshot-inbox delivery. JS is glue only (chrome.* APIs, fetch, popup
DOM, injected page functions).

Capture: full page + element pick. Delivery: local ingress
(POST /api/import/web-snapshot) with /mcp fallback, JSON download, and —
when signed in to OP Hub — the account snapshot inbox. Flat popup UI,
15-locale strings with an in-popup language switcher, store packaging.

Why a crate: keeps the security-sensitive logic in tested Rust rather
than glue JS, and the SW/popup split keeps dynamic import() out of the
service-worker graph (guarded by check-sw-imports).
2026-08-04 21:43:01 +08:00
Kayshen-X 45d962938b feat(collab): add isolated PKCS11 locator signer 2026-08-01 16:35:59 +08:00
Fini 188afc99d1 feat(agent): add a fixed-input modify mode to the smoke harness
The harness could only drive fresh generation, so the modify-category
prompts had no way to produce before/after thumbnails: their whole point
is what changes about an EXISTING document.

`OPENPENCIL_SMOKE_MODIFY_INPUT=<baseline.op>` runs a real document
through the same `build_modify_plan` -> `run_modify_turn` -> scoped host
apply path the desktop uses, so a thumbnail reflects the shipping code
rather than a harness-only shortcut.

The baseline is never overwritten — the output must go to a distinct
`OPENPENCIL_SMOKE_OUT` — and both files are SHA-256 addressed in the
summary, so a thumbnail set can prove every modify prompt started from
the identical input.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-07-31 21:06:35 +08:00
Kayshen-X 4b3d3334ba feat(desktop): load signed relay bootstrap 2026-07-30 22:51:48 +08:00
Kayshen-X af812daa77 chore(deps): update anyhow to 1.0.103 2026-07-29 22:39:41 +08:00
Kayshen-X 2b713635f5 feat(collab): ship public relay collaboration 2026-07-29 21:42:33 +08:00
Kayshen-X f1ae6002ec feat(editor): show authenticated profile avatars 2026-07-29 15:20:34 +08:00
Kayshen-X 0bd9947310 feat(collab): add authenticated p2p collaboration 2026-07-29 00:06:58 +08:00
Kayshen-X dcf8c3b1b3 fix(desktop): support Wayland image clipboard paste 2026-07-27 21:08:15 +08:00
Kayshen-X bba6f338d6 refactor: dedupe cross-crate code and converge hardcoded literals
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.
2026-07-26 11:24:22 +08:00
Kayshen-X e77355e193 feat(web): unified device login through the serve-web daemon
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.
2026-07-25 23:28:03 +08:00
Kayshen-X cacaeefc95 feat(desktop): browser device login through prebuilt op-auth library
The proprietary device-login client (ZSeven-W/op-platform) ships as a
C-ABI static library committed under op-auth-bridge/prebuilt/<target>;
builds without an artifact fall back to an inert stub so any checkout
compiles with login hidden. The desktop host starts the pairing flow
from the sign-in modal, opens the verification page in the system
browser, polls flow status each frame, restores persisted sessions at
startup, and revokes the device token on sign-out.
2026-07-25 19:08:38 +08:00
Kayshen-X a1e7151f4b chore(release): bump version to 0.8.2 2026-07-23 21:23:21 +08:00
Kayshen-X 70e202d802 feat(editor): improve document workflows and rendering 2026-07-23 21:15:04 +08:00
Kayshen-X 78aa8662cb feat(html): import multi-file HTML projects and ZIP archives
Add op-html project import beyond the single-document path: a directory
of HTML/asset files (project.rs) and packaged .zip archives
(project_zip.rs), each resolving relative resources against a virtual
project origin and picking an HTML entry point. Adds flate2 + zip deps
and re-exports the new import surface from lib.rs; the old 10MB
single-source truncation is dropped in favour of per-entry limits
(MAX_PROJECT_FILES / MAX_ZIP_ENTRIES).
2026-07-21 21:39:29 +08:00
Kayshen-X 01820bf5ea fix(editor): improve import and generation reliability
Expand HTML/CSS import fidelity, preserve layered fills and accurate fonts across HTML, Figma, and OP files, and make missing-font resolution selectable and case-insensitive. Harden code generation recovery and preview rendering, make model selection immediate, and keep image decode/compositing consistent across native and web hosts. Add mobile dependency and widget-boundary guards so all supported targets retain the intended feature surface.
2026-07-21 06:34:53 +08:00
Kayshen-X fb9b9094b9 perf(renderer): blur-up placeholders and a negative decode cache
Follow-up to the off-thread decode seam.

A payload that fails to decode used to be marked done without being
installed, so paint re-queued it every frame — a corrupt image spun the
native workers forever and kept the web repaint coalescer hot. Failed
ids now enter a bounded negative cache on both hosts.

Blur-up: import generates a <=32 px, <=4 KiB JPEG per image during the
decode it already performs, and paint draws that thumbnail (scaled up,
which is the blur) before falling back to the node fill and the grey
glyph. Thumbnails are explicitly allowed to decode on the paint thread
into a small dedicated cache — microseconds, and they cannot displace
sharp rasters. Full images still never decode during paint.

Every paint, cache, and thumbnail site now shares one canonical
jian-ops-schema paint id. Web raster accounting switches from encoded
to decoded bytes to match native.

Also lands the fill-type picker clamp from the import-fidelity line:
the popover flips above its dropdown when the panel bottom is too
close, and its hit-test follows the same rect, so the last rows stop
being cut off.

Measured on the 527 MB reference import: 317 thumbnails persisted
(+0.2 MB saved size), first reopened frame draws 1136 thumbnails and
zero sharp images, paint-thread full decodes stay at exactly 0.

Known, not addressed here: at full zoom-out on the image-dense page the
384 MiB raster LRU never settles (~22 installs/s against ~22 evictions
for 101 unique images) — decoded 2048 px rasters cannot all be resident,
so that page needs mip-level or on-screen-size-aware rasters.

--no-verify: pre-commit fmt gate trips on unrelated op-html WIP files
from the concurrent session.
2026-07-19 20:45:04 +08:00
Kayshen-X 4653242f72 chore(html): record serde_json and op-html dependency edges in Cargo.lock 2026-07-19 20:45:03 +08:00
Kayshen-X 4bda4cb0e9 feat(renderer): even-odd fill, per-corner radii, background blur end-to-end
Vector plan Tasks 3-5: winding rule imports onto PathNode.fillRule and
reaches skia/CanvasKit fill types through new *_with_fill_rule painter
entry points; per-corner radii flow payload->scene->RRect on both
backends; BACKGROUND_BLUR renders via backdrop save layers (native
SaveLayerRec backdrop, CanvasKit saveLayer backdrop). Includes jian
pointer bump (6bed2be). no-verify: repo fmt gate trips on unrelated
in-progress op-html sources.
2026-07-19 20:45:02 +08:00
Fini 4162006fab feat(renderer): adopt responsive-capable jian and adapt hosts
Merge Kayshen's feat/responsive-m1a (schema 1.2 + responsive opt-in,
breakpoint ranges, variant tables, transactional variant swap) plus
main's fill-rule/corner-radii/background-blur into the vendored pin
(merge/responsive-m1a-into-main @ 42772b6). Responsive stays double-
gated opt-in, so every existing document renders unchanged.

Adaptation is mostly new optional fields defaulted to None/false. The
one real behavior interaction: jian's node_rect now bakes a root's
authored origin into its returned rect (root_origins, added for the
responsive multi-root canvas), which doubled offsets at the three
places that added the origin themselves — the loader's layout harvest,
App Mode tap solving, and the preview's scene-to-runtime-space mapping
(the last now subtracts the root origin back out, gated on jian's own
is_origin_normalized so responsive roots stay untouched).
2026-07-19 02:27:09 +08:00
Kayshen-X 40da2ba9a6 feat(figma): offline .fig convert endpoint on the managed daemon 2026-07-18 15:02:25 +08:00
Kayshen-X 7365482570 chore(mcp): record op-html dependency in Cargo.lock 2026-07-18 14:23:31 +08:00
Kayshen-X a4706476bd chore(sync): integrate origin/v0.8.2 interactions and retry batch
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.
2026-07-18 13:11:52 +08:00
Kayshen-X 612a453942 feat(figma): route host image transform through fig import for downscaled embeds 2026-07-18 13:11:52 +08:00
Fini e2f4511b93 feat(canvas): interactive preview ux for app mode
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.
2026-07-17 21:12:39 +08:00
Kayshen-X f0bdad77e6 test(web): end-to-end smoke for the managed daemon contract 2026-07-17 02:52:05 +08:00
Fini fff96c8ad5 feat(agent): unify generation UX and CLI providers 2026-07-15 03:48:29 +08:00
Kayshen-X 3a41f2ea7f feat(web): make credential persistence deployment-aware 2026-07-14 23:36:37 +08:00
Danny Ahn f8cfcab7fd fix(cli): validate document before starting headless MCP server
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.
2026-07-13 14:29:48 +08:00
Kayshen-X 7ec373c9a7 feat(cli): export pages nodes and live selection 2026-07-12 17:52:36 +08:00
Kayshen-X 355d94e244 feat(canvas): device-frame preview with switcher, scroll and pinned nav
Preview now presents ONE framed root inside a fixed device frame —
Phone 390x844 / Desktop 1440x900, inferred from the root width
(<=500 -> Phone) with a floating 3-segment switcher (Phone / Desktop /
Canvas, i18n'd across all 15 locales) overriding inference until exit.
Content keeps authored size (no reflow, no content scaling); overflow
scrolls vertically inside the frame, clamped to the pinned-nav top
bound. A bottom nav (semantics.role=nav bottom-anchored, or the
flush/full-width/<=120px heuristic) pins to the frame bottom in a
second clipped paint pass and the content viewport shrinks past it.

One DeviceFrame struct owns every transform: paint, the screen->scene
inverses (strip + letterbox dead zones resolve at surface-resolution
time), and the per-gesture presentation capture, so drags never flip
surfaces mid-gesture. Device mode fails closed — no wheel / pan /
pinch / modifier-zoom ever reaches the editor viewport (pinch gets its
own host entry, split from the line-wheel path at the desktop call
sites). Reconcile now reports { repaint, switched } so warning-only
passes stop recentering; screen switches re-infer, reset scroll and
rebuild the frame.
2026-07-11 04:00:24 +08:00
Kayshen-X a116b15969 fix(ci): repair rust checks on v0.8.0-new 2026-07-07 22:37:30 +08:00
Kayshen-X ab6c6835d0 fix(mcp): bump rquickjs 0.9->0.12 for windows-aarch64; MSRV 1.87; switch test input token
- rquickjs 0.9->0.12 (op-mcp): 0.12 ships aarch64-pc-windows-msvc bindings
  (0.9 doesn't), so the native-only script feature builds on windows-arm64
  with full parity (as Zode's zode-core already uses 0.12). Clean drop-in;
  25 script_runner tests pass. Reverts the earlier per-target gating.
- Workspace MSRV 1.85->1.87 (rquickjs 0.12's floor; pinned toolchain is 1.94).
- agent_settings switch test: assert theme.input (the token the off-track
  paints) now the light theme gives input its own value distinct from border.
2026-07-07 21:26:38 +08:00
Kayshen-X a218115010 feat(web): user font import — family-aware CanvasKit + import UI + IndexedDB (phases 3-4)
Bring user-imported fonts to the browser host, matching the native flow.

Phase 3 — family-aware CanvasKit text (the web BLOCKER): drawText now
carries font_family and a new measureTextFamilyStyled FFI mirrors it, so
the editor measures caret/layout with the same family it draws (closing
the family-blind trap). op_ck_bridge.js keys imported typefaces by family
and resolves them PER CHARACTER: chars the imported face covers draw with
it, the rest fall to the existing script-segmented system/CJK/emoji path
(no tofu, no dropped family) — draw + measure split on identical
importedCoverage segments so advances agree. register/removeImportedFont
with replace + wasm-heap free.

Phase 4 — import UI + persistence: web ImportFont opens a hidden
.ttf/.otf file input -> FileReader -> 16 MiB cap -> family parsed in Rust
via ttf-parser (font_meta.rs; the vendored CanvasKit exposes no
getFamilyName) -> register + persist bytes in IndexedDB (font_store_idb.rs;
DB openpencil / store imported_fonts, keyed by family, async errors
logged) -> refresh snapshot + repaint. Remove drops registry + IndexedDB.
Mount re-registers persisted fonts (skipping any the user already changed
this session) before the first family-aware paint. font_import_supported
is true on web, so the picker's imported group + Import row are live.

font_meta family extraction is unit-tested (real .ttf bytes -> family)
so the core is verified headlessly; runtime IndexedDB/FileReader/rendering
need a browser smoke test. Codex-reviewed (2 rounds) — getFamilyName
BLOCKER, mixed-script fallback, IndexedDB async errors, and the mount
race all addressed.
2026-07-05 14:21:51 +08:00
Kayshen-X 686536a15e feat(openpencil): add code-to-design conversion flow 2026-07-04 13:23:28 +08:00
Kayshen-X ae11d8894a feat(ai): teach and promote radio-group, number-input and progress roles
Bumps vendor/jian to feat/promote-widen (single-source ROLE_TABLE,
9 promotable kinds, promotable_roles() export). The jian-components
generation skill teaches the five new role strings, and the loader's
role-vocabulary test now iterates promotable_roles() directly — the
skill can never silently drift from the promote table again
(jian-ops-schema joins op-ai-skills as a dev-dependency for that
test). Tabs is documented as deliberately not promotable.
2026-07-03 22:15:35 +08:00
Kayshen-X 2988729c76 feat(mcp): shared quickjs script runner behind native-only script feature
Migrates the QuickJS script->program expansion out of op-orchestrator
into op_mcp::script_runner (cargo feature `script`) with hard limits
(64 MiB heap / 2 s interrupt budget / 4096 recorded lines / 8 MiB
recorded bytes, whole-line fit + latch / 256 KiB source) plus the
truncation-repair retry that closes the syntax-error cliff which
originally demoted script-gen.
2026-07-03 22:08:19 +08:00
Fini f04c724c09 merge: sync mesh/shader rendering, web live-sync and platform fixes from remote
Conflicts were the two mesh/shader implementations meeting: kept the
remote's newer complete version (typed shader uniforms, shader color
uniform binding, mesh vertex editing defaults, status-bar shell stroke
handling); deduped two identically-replayed RenderBackend methods.
2026-07-03 01:25:08 +08:00
Kayshen-X 0ba60361d4 fix(canvas): don't arm an erase frame when a design run finishes empty
finish_if_epoch previously routed the empty-queue case through
drain_finished_run, which set the process-global needs_final_frame flag
even though a run that never queued a reveal never put a cursor on
screen. That stray flag made next_reveal_deadline_ms return a redraw
deadline out of an idle registry, and — because the registry is shared
across the whole test binary — perturbed an unrelated exact
animation-deadline assertion whenever a design-session test dropped an
empty session in parallel.

Clear an empty finish inline without arming the flag; the paint-path
drain still arms it after real reveals prune, where a cursor genuinely
was on screen. Adds a regression test.

Verification note: the op-editor-core suite could not be run for this
commit because the shared workspace is transiently non-compiling under a
concurrent mesh-gradient/SkSL-shader jian bump (new PenFill variants not
yet handled in fills.rs — unrelated files). Change is trace-verified and
touches only agent_indicators; re-run pending the tree compiling again.
2026-07-03 00:08:18 +08:00
Fini 6ed9548c0d merge: land the align-branch work onto the refreshed base
47 commits from the align branch merged onto the force-updated remote
base (which had replayed an earlier snapshot of the same work plus new
overlay/pointer features and CI fixes). Conflict resolution: kept the
newer align side for the generation pipeline (orchestrator, mcp, skills,
design tools), kept the base side for the chat-panel test semantics and
graceful overlay teardown, fused both in sub_agent_session (design-turn
thinking policy + graceful epoch finish), and dropped the files each
side had deleted (legacy concurrent/dashboard paths, retired TS skills).
Deduped two identical replayed hunks (export.rs, chat_session_tests.rs).

Known issue carried over: provider_probe_host::landed_connected_outcome_
without_models_is_failure fails on a host with a live provider config
(env-sensitive test, both sides byte-identical there; green on CI).
2026-07-03 00:08:17 +08:00