Commit graph

66 commits

Author SHA1 Message Date
Kayshen-X 8654c04b00 feat(collab): replace public invites with 10-char region-tagged pairing codes
The ~500-char opc1_ fragment is retired from every production surface; a
relay session now shares one 10-char Crockford code (1 region char + 9
random, 45 bits). The full invite is sealed with a blake3 encrypt-then-MAC
under keys derived from the code and stored on the locator control plane
under an independent code_id; guests claim from exactly the region the
code names, so neither the id nor the bearer ticket reaches uninvolved
regions. The store tombstones exhausted claim budgets (an id can never be
re-published under a burned code), caps codes per device key, and keeps
per-route ingress body limits. Connect failures now distinguish invalid,
expired, and relay-not-configured across all 15 locales, and the threat
model documents the operator-grindable 45-bit residual risk.
2026-08-02 08:31:13 +08:00
Kayshen-X 6212b486e3 feat(collab): minimize the relay credential and confirm the owner on LAN
Two halves of the same problem: the relay learned who collaborates with
whom, and a guest could not safely join a stranger's session without an
invite.

Claim-minimized relay bearer. The relay authenticated each WSS connection
with the full collaboration ticket, whose claims carry the account subject,
device id, and optional display name and avatar. Now that collaboration is
cross-account, that let a relay operator reconstruct a social graph. The
relay reads exactly one field out of that ticket — the expiry it clamps the
session deadline to — and its authorization output, (route, role, expiry),
comes from the signed locator and the route capability, not from identity.
The disclosure was gratuitous.

A separate audience-scoped token now carries only issuer, audience,
version, scope, the channel binding to the caller's X25519 key, and the
time bounds. `VerifiedRelayTokenClaims` deliberately exposes no identity
accessor at all, so the relay cannot regress into reading one. No route or
role claim was added: route authorization already comes from the locator
plus the capability secret, and putting route ids in the token would move
the graph to the issuer, which also knows the account.

Scope of the guarantee, stated in the code so nobody over-reads it: this
defends against a third-party or regional relay operator. It does not
defend against the first party, who runs both the issuer and the relay and
can rejoin on the channel-binding key and the issuance time. It also
de-identifies rather than making the view unlinkable — the device's X25519
static is persistent and in the clear in every hello, so the operator still
builds a device graph, it just cannot name the nodes or join them to the
account namespace.

The two token types are domain-separated by both JWS `typ` and `aud`,
strictly compared, with `deny_unknown_fields` on disjoint claim structs, so
each is structurally invalid against the other's parser. That property is
what makes sharing one signing key defensible, so it is tested in both
directions. The relay dual-accepts during migration, discriminating on
`typ` before claim parsing, behind an env flag. The client never retries a
rejected minimized token with the full ticket — that would be a downgrade
any curious relay could trigger at will.

Guest owner confirmation. A guest joining over unpinned LAN discovery still
required the same account, because it has no approval prompt of its own and
mDNS names nobody. It now gets the explicit decision the owner already had:
the verified owner identity is surfaced and confirmed before the peer is
authorized, so nothing from the session — snapshot, presence, session name
— exists before the user decides. With that gate in place the unpinned LAN
path admits any issued account too.

Display name and avatar are attacker-chosen, so the projection separates
them from the account subject and device id at the type level, strips
invisible and bidi-control characters, and labels them as claimed. A
display name cannot occupy an authoritative row; the test uses another
account's UUID as the display name to prove it.

Both halves are pinned in the boundary gate: losing either asymmetry is
silent, because the code still compiles and every other check still passes
while nothing authenticates the peer.
2026-08-01 14:24:46 +08:00
Kayshen-X f9f9c8574a fix(collab): harden p2p collaboration against resource exhaustion
Addresses a security review of the collaboration subsystem. No auth
bypass, key leak, or document-plaintext exposure was found; every
finding below is availability or trust-boundary hardening.

Landed as one commit because the pieces are not separable: the
inbound-direction ceiling spans op-collab, op-collab-transport, and the
desktop host atomically, the guarded accept spans transport, smoke, and
the desktop host, and the boundary-gate rules only hold against the
final state. Splitting would produce commits that fail to build or fail
the gate.

Relay server (public, internet-facing):
- Charge pre-pairing capacity per source address. The auth-concurrency
  semaphore was taken before the WebSocket upgrade and the peer address
  was discarded, so one host could pin every permit by connecting and
  going silent.
- Give renewals their own budget. Reauthentication competed for the same
  semaphore, so an unauthenticated flood progressively closed live
  tunnels with a policy error.
- Release the pair registration when the ready status fails to send; the
  counterpart only reclaims it if it reads its pairing notice.
- Require the X25519 key file to be owned by the running user; mode bits
  alone do not establish trust.
- Summarise capacity rejections instead of logging one line each.

Locator service:
- Rate-limit publishes per client instead of process-wide. One
  unauthenticated caller could consume the whole budget and 429 every
  tenant's invite issuance.

Collaboration protocol:
- Size the inbound envelope ceiling from the authenticated remote role
  rather than sharing the 64 MiB snapshot ceiling in both directions, so
  an admitted guest cannot force a 64 MiB JSON parse per frame. The
  ceiling is applied before the discriminator and before the generic
  value decode; a peer-declared snapshot kind cannot raise it.
- Reject display names carrying Unicode format characters, which render
  identically to an existing participant's name.
- Reject avatar URLs pointing at non-globally-routable addresses.

Transport:
- Reclaim a pending-handshake seat from a peer that has not produced a
  valid first handshake message, and raise the global ceiling. Sixteen
  seats held for the full handshake window let four addresses deny every
  join.
- Put inbound reassembly under an aggregate budget; only the outbound
  aggregate was bounded.
- Stop heartbeats from refreshing the idle deadline in receive_transfer.
- Filter IPv4 link-local discovery advertisements, matching IPv6.

Relay client and trust roots:
- Bound server-initiated reauthentication per connection by count and
  minimum interval, sized from the protocol's own cadence.
- Close the policy-file TOCTOU window by identity-checking the opened
  file, and reject group/world-writable or foreign-owned policy files.
- Stop discarding bootstrap cache-write failures, which silently
  disabled the anti-rollback generation floor.
2026-08-01 09:48:23 +08:00
Kayshen-X 2b713635f5 feat(collab): ship public relay collaboration 2026-07-29 21:42:33 +08:00
Kayshen-X da2c90fe9e build(collab): enable local ABI v2 auth debugging 2026-07-29 16:46:03 +08:00
Kayshen-X 2e9de6d2a2 fix(ci): update collaboration security gate 2026-07-29 00:06:58 +08:00
Kayshen-X 0bd9947310 feat(collab): add authenticated p2p collaboration 2026-07-29 00:06:58 +08:00
Kayshen-X 02770eb689 feat(i18n): complete locale coverage across menus, providers, and panels
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.
2026-07-26 11:24:06 +08:00
Fini 124db9d144 fix(agent): retire gemini cli provider, add antigravity and grok build 2026-07-24 21:11:15 +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
Fini dc5f946592 fix(build): align release gate with VSIX artifacts 2026-07-20 21:32:22 +08:00
Kayshen-X 0daddb3bf4 test: isolate tag environment in version guard fixtures 2026-07-16 22:09:56 +08:00
Kayshen-X f82f4e4ea3 fix: enforce centralized version inputs 2026-07-15 21:17:21 +08:00
Kayshen-X e28506d0af fix: cover all version guard inputs 2026-07-15 21:17:21 +08:00
Kayshen-X e5b7425f3a build: add version synchronization gate 2026-07-15 21:17:21 +08:00
Kayshen-X 68579c4c86 fix(release): detect punctuated version examples 2026-07-15 21:17:21 +08:00
Kayshen-X 84993cadc9 fix(release): gate publishing on Cargo version 2026-07-15 21:17:21 +08:00
Kayshen-X 567453bfc5 fix: reject commented version derivation checks 2026-07-15 21:17:21 +08:00
Kayshen-X d3fc4fed6d build: derive package versions from Cargo 2026-07-15 21:17:21 +08:00
Kayshen-X ec3dbf964d test: handle fixture version collision 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 3a41f2ea7f feat(web): make credential persistence deployment-aware 2026-07-14 23:36:37 +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
Kayshen-X a7d73ebb62 feat(ai): pencil-style agentic design tool-loop, multi-chat tabs, #27 panel restyle
Built-in design generation now runs as an agentic MCP tool-loop (reusing the
agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the
Settings experimental toggle; the orchestrator stays the default.

- design-agent system prompt + in-process design toolset (parity-locked with
  the MCP surface) + flag-gated Intent::Design routing
- spawn_agents execution as sequential sub-loops + live creation-mode badges
  (per-agent glow + 'N/M designing...' header)
- new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state,
  export_nodes, spawn_agents; style-guide local audit
- #27 AI panel restyle: rounded tool cards + green check-rings, gray user
  bubbles, model-pill bottom toolbar, header, empty-state pills, the
  PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown
- multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI
  (switch / close / + / Cmd+T) with each run bound to its tab

Large checkpoint commit spanning the working tree (Rust shell crates).
2026-07-02 21:21:06 +08:00
Kayshen-X 6a43c90774 build(ci): binaryen-tolerant wasm-opt flags + refresh cargo-deny advisories
The wasm bundle gates (tools/check-wasm-bundle.sh and
crates/op-web-sdk/tools/build-wasm.sh) hard-passed --enable-bulk-memory-opt
to wasm-opt, which older binaryen (the CI runner's apt package) does not
recognize, failing the WASM bundle build (#56) and op-web-sdk bundle
workflows at the size gate. Probe wasm-opt --help and keep only the feature
flags the installed binaryen advertises; on those older versions the single
--enable-bulk-memory already covers memory.copy/fill, so dropping the unknown
flag is safe. Local binaryen (v117+) keeps both.

cargo-deny advisories also failed: ttf-parser flagged unmaintained
(RUSTSEC-2026-0192, a transitive font-stack dep with no maintained
replacement) and memmap2 flagged unsound (RUSTSEC-2026-0186). Ignore the
former and bump memmap2 0.9.10 -> 0.9.11 (the patched release) for the
latter. advisories now pass.
2026-07-02 01:53:40 +08:00
Kayshen-X 77dca8c01b chore: retire TypeScript OpenPencil, delete apps/* + pen-* packages
The product is now Rust (crates/) + the Zig agent runtime
(packages/agent-native) + the wasm-backed web SDK (packages/op-web-sdk*).
Delete the retired TypeScript editor/desktop/CLI (apps/*), the pen-*
packages, and their orphaned TS tooling, Dockerfile, and root tsconfig.

Rust-side hooks into the deleted TS are rehomed so the workspace still
builds, tests, and releases green:
- op-mcp: vendor the 10 element-tool-defs shards it include_str!'d from
  pen-mcp into crates/op-mcp/assets/element-tool-defs/ (git renames), and
  repoint the compile-time includes + the runtime parity test's read_dir.
- op-orchestrator: reword the parity panic that named the retired
  dump-planner-golden.ts generator (goldens stay as frozen baselines).
- CI: drop the TS-derive golden-drift + planner-parity oracle jobs and the
  TS path filters from rust-check.yml; repoint the cargo-bundle icon in
  rust-release.yml to crates/op-host-desktop/assets/icon.png.
- Release (Windows): recover the deleted apps/desktop/build/icon.ico into
  crates/op-host-desktop/assets/icon.ico and repoint the NSIS installer
  (rust-release.yml /DICON_FILE + package-windows.nsi ICON_FILE fallback +
  its example invocation), so the Windows release build still finds it.
- Remove the dead .githooks/post-commit hook — it rebuilt the deleted
  out/mcp-server.cjs via the removed mcp:compile script and only watched
  now-deleted pen-* paths; the Rust MCP is built by cargo.
- Recover apps/web/public/logo-discord.svg to screenshot/ and repoint the
  15 READMEs' logo/link paths.

package.json is now Rust-first (dev/build/test -> cargo + serve-web) and
docs (CLAUDE.md/AGENTS.md/packages+crates CLAUDE.md) describe the Rust
product. The retired TS remains recoverable via git (last tag v0.7.5).

Verified: cargo check --workspace green; cargo test -p op-mcp 353 passed;
cargo test -p op-orchestrator 848 passed; cargo test --workspace --no-run
compiles all targets; rust-release.yml parses and references no deleted
build assets.

Pre-commit fmt hook bypassed: its cargo fmt --check is pre-red from a
repo-wide stable-vs-nightly rustfmt config drift affecting unrelated files;
the .rs files touched here produce no fmt diff.
2026-07-01 21:12:39 +08:00
Kayshen-X 521e44c92c test(web): add rust web browser smoke 2026-06-20 19:24:59 +08:00
Kayshen-X 88a571c028 refactor(host): rename op-web-daemon crate to op-host-services
The extracted headless crate is consumed by BOTH op-host-desktop (the GUI binary, for its
embedded --serve-web/MCP/chat/export) AND op-host-web-server — so 'web-daemon' was misleading.
Renamed crate dir + package + lib (op_web_daemon -> op_host_services) across all consumer files
(114 refs in 24 files) + the 4 Cargo.toml deps + Dockerfile/guard comments; identity docs
rewritten (it's the GUI-free host backend — daemon/MCP/AI/export/persistence — not web-specific).
No behavior change. (Lock renamed accordingly; the concurrent actor's op-host-web serde line excluded.)
2026-06-19 22:35:22 +08:00
Kayshen-X b7951fc598 ci(host): build web image from op-host-web-server + headless-boundary guard (Phase 6, Tasks 6.2-6.4)
Dockerfile.web-rust builds -p op-host-web-server (was op-host-desktop) and drops ALL GL/X11
build + runtime apt deps (libegl/libgles/libgbm/libxkbcommon/libwayland/libxcb) — the raster
server links none; only freetype/fontconfig + CJK fonts remain for skia text. COPY + CMD repointed
to /app/op-host-web-server. New tools/check-web-server-headless.sh fails CI if op-host-web-server's
isolated dep graph pulls winit/glutin/casement/muda/accesskit-adapters or skia-safe with gl (bare
accesskit core allowed per Codex Issue 1); wired into rust-check.yml + its paths filter. Stale
web_static.rs path comment fixed (op-host-desktop -> op-web-daemon). Desktop-app build job untouched.
2026-06-19 22:14:56 +08:00
Kayshen-X a172b74023 refactor(host): move chat_intent to op-web-daemon; extract host-coupled test (Phase 5, Task 5.3a)
The CLI standard-mode intent router moves to op_web_daemon::chat_intent; its 23
headless tests (which reach chat_intent's #[cfg(test)] internals) move with it as the
#[path] sibling. The 1 host-coupled test (cli_new_design_clears_agent_frame_indicators_after_done
— drives the GUI design-session pumps via WidgetHostNative) is extracted to
op-host-desktop/src/chat_intent_host_tests.rs against the pub run_cli_turn/CliTurnPlan API +
crate::design_session pumps. chat_session/chat_session_launch/web_chat_standard refs repointed.
op-web-daemon 276 + host test 1 green; no dep/lock change.
2026-06-19 21:55:33 +08:00
Kayshen-X 75ec668463 feat(sdk): add op-web-sdk read-only web viewer crate
Plan 1 of the web embedding SDK (TS-retirement Phase 2): a wasm Viewer that
parses a .op document, renders it read-only via CanvasKit by reusing
op-editor-ui's canvas_viewport, supports pan/zoom navigation, exposes
read-only JSON snapshots, and exports SVG. Type-gen reuses jian-ops-schema's
ts-rs export.

Additive: new crate plus surgical cold pub exposures (CanvasViewport::from_scene
in op-editor-ui; pub mod canvaskit + pub init_backend in op-host-web). No TS
deleted. 17 tests; wasm 2.2 MiB gzip (0 env.* imports); clippy -D warnings clean.
2026-06-19 17:52:12 +08:00
Kayshen-X f0c882298f fix(web): close native parity gaps in rust web host 2026-06-19 12:56:43 +08:00
Kayshen-X 227da7fa11 refactor(renderer): consolidate web shell on CanvasKit, retire skia path
Retire the from-scratch wasm32 skia raster web backend and the vendored
skia-safe fork now that CanvasKit is the sole web renderer.

- remove gl-webgl-shim + the `webgl` feature + backend/webgl.rs
- absorb chat streaming, live-sync, codegen, icon search, Figma/file IO,
  and system fonts into the `canvaskit` build via a backend-agnostic
  `RepaintContext` trait; wire them into `mount_ck` (chat send, image
  paste routing, window resize)
- delete the skia raster modules (backend/, a11y, shell_drop/resize,
  ime_target, boolean_ops, caret_pump, repaint_scheduler) + the skia
  `Inner` mount; op-host-web is now `web` (stub) + `canvaskit` only
- drop [patch.crates-io]; delete vendor/skia-safe-op + crates/wasm-libc-shim
  so skia-safe reverts to upstream crates.io 0.97.x (native unchanged,
  byte-identical; upstream has no wasm32-unknown-unknown target)
- switch the bundle gate (check-wasm-bundle.sh) to --features canvaskit

Also float new shapes above the frame (z-order), inset the shape-picker
selected-row highlight, and align the web file-menu hover without the
traffic-light inset.

Builds clean: canvaskit + web-stub (wasm32) + native desktop; bundle gate
passes (0 env.*, 4.5 MiB gzip).
2026-06-18 09:34:37 +08:00
Kayshen-X 989dd916d9 chore(editor): drop unused dropdown sample superseded by jian-widgets Select 2026-06-14 00:56:18 +08:00
Kayshen-X 4c7a2a52a1 chore(build): release packaging scripts, wasm bundle gate with codegen, workspace docs + lockfile 2026-06-12 23:28:09 +08:00
Kayshen-X a30206222b feat(web): daemon-backed chat/codegen/live-sync, a11y mirror, kit browser, bundle export
Real chat streaming (echo stub retired; production bundle now builds with
codegen), live_sync glue for browser<->daemon<->MCP, codegen panel actions,
structure-bundle zip (stored-zip encoder), iconify web search, component
browser dispatch, accessibility DOM mirror v1, IME + clipboard paths.
2026-06-12 23:28:03 +08:00
Kayshen-X e763bff0f0 Improve git panel parity and macOS bundle identity 2026-05-31 20:23:28 +08:00
Kayshen-X 0948e97de9 feat(panels,canvas): editable gradients/effects + SVG/image import + locale-aware dialogs
Continues the gradient + property-panel polish from the previous
commit and rounds out two new flows the TS app already has:

Gradient stops + effects:
- ColorTarget gains GradientStop(i) + EffectColor(i); HSV picker
  preserves alpha across hue/SV drags so a transparent stop stays
  transparent. Hex pill stays 6-char; alpha is reattached at commit
  and the swatch sits on a 2x2 alpha checker so #00000000 reads as
  transparent rather than empty.
- Effects section reflowed into card-style blocks (image #9 spec):
  title + minus, X/Y and Blur/Spread 2-col grids, color row with
  swatch + rgba(...) text; clicking the swatch opens an HSV picker
  bound to that effect index via SetEffectColor.
- Press dispatch on both hosts anchors picker overlays at the
  clicked y so they pop adjacent to the swatch instead of the top.

Image + SVG import (toolbar + Fill section "图片" row):
- New FileAction::ImportImageOrSvg / PickFillImage; persistence_image
  pops rfd, decodes raster as data: URL, inserts an Image node or
  rewrites the selected node's primary fill.
- ImageNode actually renders on the canvas: NodePayload + SceneNode
  carry image_src, canvas_viewport_paint.rs decodes the data URL
  once and hands raw bytes to RenderBackend::draw_image with a
  src-hash cache id. Grey placeholder paints only when decode fails
  so transparent PNGs don't get a grey matte underneath.
- SVG import ported to TS-parity (packages/pen-engine svg-parser):
  recursive <g> tree walk with inherited fill/stroke/style="...",
  viewBox-aware scaling with maxDim cap, multi-subpath split, raw
  d preserved on PathNode. Imports land wrapped in a Group named
  after the source file.

Locale-aware first run:
- settings_io detects the OS locale (LC_ALL/LANG/LC_MESSAGES with
  zh-Hans/zh-Hant heuristics) and seeds editor_ui.locale before
  settings.json is read; persisted user choice still wins.
- macOS bundle declares CFBundleLocalizations + AllowMixedLocalizations
  so NSOpenPanel / NSSavePanel render in the same language as the
  rest of the chrome.

Web host kept exhaustive across the new variants (PickFillImage,
OpenEffectColorPicker, ColorTarget::EffectColor, GradientStop). Two
new files: persistence_image.rs (file-pick handlers, ≤120 lines) and
svg_path_data.rs (path-d tokenizer + bbox + normaliser, split from
svg_import.rs to stay under the 800-line cap). 277 op-editor-core
tests pass.
2026-05-23 23:11:38 +08:00
Fini 8cff5abaf5 feat(ai): implement op-design-lint Rust crate (S1)
Port the pen-ai-skills diagnostics layer to a new pure Rust crate
`op-design-lint`: 14 design-lint detectors, the detect_all aggregator,
apply_fixes / detect_and_fix, and golden parity tests against the TS
oracle. Wire it into op-mcp as the read-only debug_validation_report
tool, gated by OPENPENCIL_DEBUG_TOOLS=1.

Detectors: empty_paths, unexpected_rotation, excessive_frame_effects,
invisible_containers, text_explicit_heights, text_effect,
text_corner_radius, text_stroke, text_bg_contrast, edge_section_padding,
stacked_horizontal_padding, sibling_inconsistencies (+ check_consistency),
detect_all.

Also includes: node_util shared helpers + pen-core color/visibility
ports, node_mut field accessors, set_property issue->node mutation
dispatch, golden fixture corpus + TS dump script, structural-parity
test, a CI golden-drift guard, and the gitignore fix so the fixture
docs/ dir is tracked.

This branch's per-commit history was squashed: the original 28 commits
carried fabricated timestamps and could not be honestly reconstructed,
so the work is recorded as a single commit at its real completion time.
2026-05-23 18:39:08 +08:00
Fini 1fa2d9415a Merge branch 's3a-orchestrator' into v0.8.0-new 2026-05-23 01:27:30 +08:00
Fini b1c3136deb feat(editor): EditorCommand::InsertSubtree — nested PenNode subtree insert
Existing EditorCommand variants are leaf-only (BatchInsertItem carries
only kind/name/x/y/w/h/fill_hex). The design orchestrator (S3a) must
apply rich nested designs — frames with children, layout, text — so
add InsertSubtree { nodes: Vec<PenNode>, parent_id }.

cmd_insert_subtree validates the parent is a container (or NONE = page
root), remaps every incoming node id to a fresh editor id via
remap_subtree_ids (so an externally-authored subtree can't collide
with live ids), and appends under the parent. The apply arm wraps it
in a history snapshot so the insert is one undo step.

NOT verified locally: op-editor-core does not currently build —
vendor/jian is pinned to unpushed commit 80121906 whose DesignMd*
types op-editor-core depends on are absent from every available jian.
The 8 InsertSubtree tests in command_subtree_tests.rs run once the
jian build is restored.

S3a Plan A.
2026-05-22 22:56:10 +08:00
Kayshen-X fb5542e778 build(macos): add bundle-macos.sh dev-run wrapper
Wraps the release binary in a minimal `OpenPencil.app`
(Info.plist + icon) so a dev run gets the proper Dock name +
icon — an unbundled binary shows the raw executable name and a
generic icon, and the runtime objc2 fallback in `macos_app.rs`
can't fully override that. Run the binary from inside the bundle
(`OpenPencil.app/Contents/MacOS/openpencil-desktop`) and macOS
picks up the bundle identity.
2026-05-22 18:05:31 +08:00
Kayshen-X e775fd49c4 feat(desktop): winit shell platform integration
Add five native-platform features to the winit desktop host
(op-host-desktop), closing the gap with the Electron app:

- Native menu bar (muda) — File / Edit / View / Help plus the macOS
  app menu; selections route to the same host actions the keyboard
  shortcuts use. Gated to macOS / Windows — muda needs GTK, which
  this winit build does not link, so Linux keeps the in-canvas File
  menu.
- Auto-update — a background probe of the GitHub releases API
  reports status into the settings System tab; a found update
  offers to open the download page, and a "Check for Updates" menu
  item re-runs the probe.
- File association — argv parsing opens a .op / .pen document on
  launch; [package.metadata.bundle] declares the OS-level handler.
- Window-state persistence — position / size / maximized restore
  across restarts, with an off-screen guard for monitor changes.
- Drag-and-drop — dropping a .op / .pen file opens it.

Codex review round 1 findings (1 MAJOR + 3 MINOR) all addressed:
monitor-aware restore, failed-startup geometry guard, single-flight
update probe, case-insensitive extension match.

Also sink the agent-settings modal's hand-maintained EN/ZH string
table into the canonical 15-locale op-i18n tables, so the settings
chrome (including the new auto-update strings) is fully translated;
agent_settings_i18n.rs is now a thin op-i18n adapter.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-18 08:29:36 +08:00
Kayshen-X 6a29fb17c7 docs: translate remaining Chinese source comments to English 2026-05-17 09:33:41 +08:00
Kayshen-X 08c07604d0 chore(ci): re-point tooling + CI workflows at the op-host-* crates
Phase 7.3 strangler reorg — update every reference to the renamed /
dissolved crates across the boundary scripts and CI workflows.

tools/:
- check-wasm-bundle.sh: openpencil-shell-web -> op-host-web; the
  wasm-bindgen output filenames follow the op_host_web lib name
- check-jian-boundaries.sh: shell-native -> op-host-native,
  shell-web -> op-host-web
- check-widget-boundary.sh: WEB_SRC + path-exclusion regexes ->
  crates/op-host-web; openpencil_shell_core::widgets ->
  op_editor_ui::widgets (the dissolved shim's real source crate)

.github/workflows/:
- rust-multiplatform.yml: wasm + mobile-check jobs -> op-host-web /
  op-host-native; the mobile shell-core clean check -> op-editor-ui
- rust-release.yml: cargo build -p openpencil-desktop ->
  -p op-host-desktop (the shipped executable name is unchanged)
- wasm-bundle-check.yml: -p openpencil-shell-web -> -p op-host-web

Cargo.toml skia-patch comment updated. Boundary checks pass; the
wasm-bundle gate cleanly skips while EMSDK is unset.
2026-05-17 00:01:35 +08:00
Kayshen-X 5f68ab3c1a refactor: extract op-editor-ui crate
Relocate the widget facade (widgets/, including the Widget trait,
render primitives, the editor-UI compositions, the CanvasViewport
center canvas, the lucide icon drawer, and editor_state_ext), the
theme tokens, the layout-resolved render scene (layout_scene +
layout_scene_hit), and the design-variable aggregation (scene_vars)
out of openpencil-shell-core into a dedicated op-editor-ui crate.

The canvas widgets (canvas_viewport*) stay inside op-editor-ui rather
than splitting into a separate op-canvas crate: they depend on the
widgets/ siblings editor_state_ext + icons and on the Widget trait, so
a clean mechanical split is not possible — per the task's explicit
allowance not to force a fragile split.

op-editor-ui's lib.rs mirrors the old shell-core crate-root re-exports
(render_backend facade types + jian gesture types + the i18n alias) so
every intra-module `crate::Color` / `crate::theme` / `crate::widgets`
path resolves unchanged — a pure relocation with no path rewrites
inside the moved modules. openpencil-shell-core becomes a thin
re-export shim (`pub use op_editor_ui::{widgets, theme, ...}`) so the
hosts keep resolving `openpencil_shell_core::*` until the Task 7.3
host rename dissolves the crate. The widgets_static integration test
moves to op-editor-ui/tests with its imports rewritten. The widget
boundary script's reverse-check path is updated to the new crate.
No behaviour change; all tests move with their code.
2026-05-16 23:33:47 +08:00
Kayshen-X 0ab38e1ca0 fix(i18n): point convert-locales.py output at the op-i18n crate 2026-05-16 14:25:58 +08:00
Kayshen-X e3a163dc2d fix(ci): scrub remaining refs to the deleted stub crates
Codex review of Task 1.2 found four dangling references to the deleted
stub crates (pen-types/core/engine/codegen/figma + openpencil-app):

- package.json: drop the five -p <crate> args from cargo:wasm-check
- rust-release.yml: restore the build job, re-pointed at the real
  openpencil-desktop crate so release-draft has artifacts to publish
- check-jian-boundaries.sh: 4 -> 3 invariants in the success message
- README.md: remove the deleted crate rows from the crate-list table
2026-05-16 12:56:39 +08:00
Kayshen-X 0a90f13aae chore: delete dead pen-* stub crates + openpencil-app 2026-05-16 12:45:07 +08:00
Kayshen-X 2cda18318d feat(shell): selection handles + drag-create + per-node flags + LayerPanel polish
Re-apply 4 reset commits (1854dfa6 → b94274c6) bundled with session
follow-ons. Native + web hosts share the new behavior end-to-end.

Selection + canvas interaction:
- bounded Frame drag now translates descendants too
- 8 selection handles with hover-cursor feedback
- thinner selection outline + smaller AA handles
- handle-drag resize for rect/ellipse/polygon/line/frame/text
- drag-to-create shapes / frames / text from the active tool
- per-NodeKind hit-test (oval / triangle / line slack / point-in-poly)
- rotation pivot is kind-aware (handles negative-size Lines)

Per-node flags (TS parity):
- Node.hidden / locked / collapsed / fill_type (moved off Document.ui)
- mutators gated by is_editable / is_subtree_editable so locked /
  hidden subtrees can't be translated, resized, rotated, recolored,
  or deleted as collateral

Multi-select + marquee + clipboard + keyboard shortcuts:
- selected_set + anchor; shift+click toggles set membership
- marquee rect-select with screen-px threshold + ADD-only shift
- copy / cut / paste / duplicate / nudge / reorder / select-all
- escape one-layer-per-press priority cascade (property-focus →
  locale picker → shape picker → fill-type picker → chat → selection)
- Cmd-letter chord guards (!shift) so Cmd-Shift-letter doesn't fall
  through to text input; !modifier guards on named keys

LayerPanel polish:
- hover-reveal eye/lock affordances (TS parity)
- Eye → EyeOff icon when hidden; Lock → LockOpen when unlocked
- locked Lock renders in warm orange
- chevron expand/collapse for container rows; collapsed subtree
  hides from tree (paint/hit-test unaffected)
- `+` add-page button wired end-to-end (mints fresh id past
  max_node_id + 1, names "Page N", overflow-safe)
- smaller, refined trailing icons (12 px @ 1.2 stroke)
- 18 px chevron-to-kind-icon gap

RenderBackend trait grew fill_oval / stroke_oval / fill_polygon /
stroke_polygon / rotate so both native and web backends can paint
the new node shapes.

Refactor:
- split native widget_host.rs (1799 lines) into spine + 7 sibling
  submodules under widget_host/ to stay under the 800-line ceiling
- split web widget_host.rs into spine + paint + keyboard siblings
- amend tools/check-widget-boundary.sh + spec § 1.4 to allow
  widget_host/* sibling files; tighten `// glue:` marker rule to
  the immediately-preceding line (rustfmt-stable)

Stop-hook iterations addressed:
- allocator overflow guards (checked_add) on duplicate / paste /
  add_page paths
- subtree-size precheck before any id mint in deep_clone
- hidden subtree skipped in paint AND selection overlay
- nested protected delete leak closed via is_subtree_editable
- per-FocusKind hex/numeric input gating; sticky `#` prefix on hex
- ScaleFactorChanged refreshes viewport from window.inner_size()

122 shell-core tests pass; cargo fmt --all --check clean;
cargo check --workspace clean; widget boundary check clean.
2026-05-11 21:30:06 +08:00