Commit graph

1225 commits

Author SHA1 Message Date
Kayshen-X
b0b52a7842 feat(panels): improve native property controls and icons
Some checks failed
Rust check (native) / ubuntu-latest / 1.94 (push) Failing after 2s
Rust check (native) / cargo-deny (native) (push) Failing after 1s
Rust check (native) / diagnostics golden drift (push) Failing after 2s
Rust multi-platform build / linux-x86_64 (push) Failing after 1s
Rust multi-platform build / wasm32-unknown-unknown / op-host-web (compile guard) (push) Failing after 2s
Rust multi-platform build / android-aarch64 (cargo check only) (push) Failing after 2s
Rust multi-platform build / android-x86_64 (cargo check only) (push) Failing after 2s
WASM bundle check (kickoff §1.2) / cargo check --target wasm32-unknown-unknown (push) Failing after 2s
WASM bundle check (kickoff §1.2) / cargo-deny --target wasm32-unknown-unknown check bans (push) Failing after 1s
Rust check (native) / macos-latest / 1.94 (push) Has been cancelled
Rust check (native) / windows-latest / 1.94 (push) Has been cancelled
Rust multi-platform build / linux-aarch64 (push) Has been cancelled
Rust multi-platform build / macos-aarch64 (push) Has been cancelled
Rust multi-platform build / windows-x86_64 (push) Has been cancelled
Rust multi-platform build / macos-x86_64 (push) Has been cancelled
Rust multi-platform build / windows-aarch64 (push) Has been cancelled
Rust multi-platform build / ios-aarch64 (cargo check only) (push) Has been cancelled
Rust multi-platform build / ios-aarch64-sim (cargo check only) (push) Has been cancelled
2026-05-24 23:30:00 +08:00
Fini
84e2b5f2bf fix(host): inline orchestrator system prompt into user message
Some checks failed
Rust check (native) / macos-latest / 1.94 (push) Waiting to run
Rust check (native) / windows-latest / 1.94 (push) Waiting to run
Rust multi-platform build / linux-aarch64 (push) Waiting to run
Rust multi-platform build / macos-aarch64 (push) Waiting to run
Rust multi-platform build / windows-x86_64 (push) Waiting to run
Rust multi-platform build / macos-x86_64 (push) Waiting to run
Rust multi-platform build / windows-aarch64 (push) Waiting to run
Rust multi-platform build / ios-aarch64 (cargo check only) (push) Waiting to run
Rust multi-platform build / ios-aarch64-sim (cargo check only) (push) Waiting to run
Rust check (native) / ubuntu-latest / 1.94 (push) Failing after 2s
Rust check (native) / cargo-deny (native) (push) Failing after 1s
Rust check (native) / diagnostics golden drift (push) Failing after 2s
Rust multi-platform build / linux-x86_64 (push) Failing after 2s
Rust multi-platform build / wasm32-unknown-unknown / op-host-web (compile guard) (push) Failing after 2s
Rust multi-platform build / android-aarch64 (cargo check only) (push) Failing after 2s
Rust multi-platform build / android-x86_64 (cargo check only) (push) Failing after 2s
WASM bundle check (kickoff §1.2) / cargo check --target wasm32-unknown-unknown (push) Failing after 2s
WASM bundle check (kickoff §1.2) / cargo-deny --target wasm32-unknown-unknown check bans (push) Failing after 1s
Codex stop-time review caught a silent prompt drop in the
`ChatProviderLlmClient` adapter from the previous commit. I put the
orchestrator's `CallRequest.system_prompt` into
`ChatRequest.system_prompt`, but all three CLI-backed `ChatProvider`
impls (`ClaudeCodeProvider`, `CopilotProvider`, `SubprocessProvider`)
ignore that field — they drive their respective CLIs through
subprocess / SDK channels with no per-turn system-prompt slot. Net
effect: the planner / sub-agent role prompt was discarded and the
model only saw the bare user prompt.

Fix matches the precedent in `chat_runtime::BuiltInProvider::send`
(line 138): prepend the system prompt to the user message with a
`\n\n---\n\n` separator, then leave `ChatRequest.system_prompt`
empty. If a CLI later grows a real system-prompt channel, unwrap the
prepend back into the field.

cargo test -p op-host-desktop 106 passed; fmt + clippy clean.
2026-05-24 20:45:45 +08:00
Fini
4f9665227b refactor(host): orchestrator uses chat-panel agent, drop Anthropic-key dependency
User pointed out: the desktop chat panel already has 5 CLI agents
(Claude Code / Codex / OpenCode / Copilot / Gemini) that manage their
own auth — Claude Code is logged in by the user, Copilot rides GitHub
auth, Gemini rides gcloud. The #27 intent-gate landing required a
separate `OPENPENCIL_ANTHROPIC_API_KEY` env var to launch the
orchestrator, which broke UX consistency and was a real design defect:
the existing CLI agents already provide LLM access, the orchestrator
should reuse them instead of requiring users to wire up a parallel
direct-API key.

Root cause was a trait mismatch: `Orchestrator` needs an
`LlmClient` impl, and `DesktopLlmClient` (now deleted) was written to
take `Arc<dyn agent::Provider>` — but `agent::Provider` is only
implemented by `AnthropicProvider` / `OpenAiCompatProvider`, NOT the
CLI-backed `ChatProvider`s (`ClaudeCodeProvider` etc).

Fix is a thin `ChatProvider → LlmClient` adapter:

- New `chat_provider_llm::ChatProviderLlmClient` (~80 lines): owns
  an `Arc<dyn ChatProvider>`, each `LlmClient::call` spawns a thread
  that drains the provider's blocking iterator into a futures mpsc
  channel and returns the receive half as a `BoxStream`. Same
  async↔sync bridge `BlockingRecvIter` uses in the opposite
  direction.
- `DesignSession::start` now generic over `L: LlmClient + Send +
  'static` instead of taking `Arc<dyn Provider>` + default_model;
  caller picks the LlmClient impl. Test e2e path
  (`from_channels`) unaffected.
- `chat_session::launch_if_pending`: Design branch reads
  `chat_selected_agent`, wraps the existing `provider_for_agent`
  output in `ChatProviderLlmClient`, hands it to `DesignSession`.
  Unwired agents (Codex / OpenCode) fall through to the chat-path
  unwired-agent error bubble — same UX as a chat send to an
  unwired agent.

Deletions:

- `chat_orchestrator.rs` — `DesktopLlmClient` was its last
  surviving content; with the new adapter no caller needs it.
  Removed the file + the `mod chat_orchestrator;` line.
- `chat_session::provider_for_design` and the
  `OPENPENCIL_ANTHROPIC_API_KEY` / `ANTHROPIC_API_KEY` /
  `OPENPENCIL_ORCHESTRATOR_MODEL` env reads.
- `op-host-desktop/Cargo.toml` `agent` crate's `["anthropic"]`
  feature — no path inside the host needs an `agent::Provider`
  impl anymore (the trait stays imported for the
  `BuiltInProvider` shim in `chat_runtime.rs`, future-facing).

`op-smoke` keeps `AnthropicProvider` + `OpenAiCompatProvider`
directly because the smoke deliberately bypasses any CLI auth path
to validate the orchestrator against a raw API endpoint
independently of the host UI.

cargo test -p op-host-desktop 106 passed (unchanged). cargo fmt
--all -- --check + cargo clippy --workspace --all-targets -- -D
warnings clean.
2026-05-24 20:41:36 +08:00
Fini
dbd5f1dc09 Merge branch 'v0.8.0-new' of github.com:ZSeven-W/openpencil into v0.8.0-new 2026-05-24 19:45:55 +08:00
Fini
91883ec5d6 style(smoke): apply rustfmt to op-smoke main.rs
Codex stop-time review caught: op-smoke was not rustfmt-clean. Three
hunks — outer `let model = ...unwrap_or_else` had its match body
inlined; one `eprintln!` line exceeded 100 chars and needed
expansion; one other formatting nit.

The previous commit's `cargo fmt --all -- --check` invocation reported
FMT_OK because the output scrolled past the diff hunks before I saw
them — the exit code path Codex relies on (not the truncated grep
output I read) caught the residual diffs. Lesson for the runbook:
trust the exit code, not eyeballing tail output.

`cargo fmt --all -- --check` clean now; `cargo clippy --workspace
--all-targets -- -D warnings` clean.
2026-05-24 19:03:10 +08:00
Fini
0477df456a feat(smoke): support OpenAI-compatible providers (DeepSeek/方舟/百炼/Moonshot)
Smoke binary now picks a Provider via `OPENPENCIL_LLM_PROVIDER`:

- `anthropic` (default) — `AnthropicProvider` + `OPENPENCIL_ANTHROPIC_API_KEY`
  (or legacy `ANTHROPIC_API_KEY`).
- `openai-compat` (or `openai`) — `OpenAiCompatProvider` +
  `OPENPENCIL_LLM_API_KEY` + `OPENPENCIL_LLM_BASE_URL`. Standard dialect;
  works against DeepSeek, 火山方舟, 百炼, Moonshot, OpenRouter, OpenAI,
  any OpenAI-compat vendor.

`agent` feature flags bumped from `["anthropic"]` to
`["anthropic", "openai"]` to pull in the `async-openai`-backed provider.

Default model selection branches on provider:
- anthropic → `claude-sonnet-4-6`
- everything else → `gpt-4o-mini` (override via
  `OPENPENCIL_ORCHESTRATOR_MODEL`)

Verified end-to-end against 方舟 CP / glm-5.1 — pipeline topology works
through to a partial-success `Ok` summary. Full run details + the
non-Claude quality finding (`stroke.fill` accepting bare string vs
expected sequence) archived in openpencil-docs
`superpowers/notes/2026-05-24-orchestrator-headless-smoke-result.md`.
2026-05-24 19:00:03 +08:00
Kayshen-X
d905369ec9 fix(editor): improve shape controls and drag snapping 2026-05-24 18:46:10 +08:00
Fini
ec8ac7984c chore: lockfile update for op-smoke crate added in e2c6e74b 2026-05-24 18:43:20 +08:00
Fini
e2c6e74b42 feat(smoke): headless op-orchestrator smoke runner (task #28)
New `op-smoke` binary crate — drives one `Orchestrator::run` against
`AnthropicProvider` without the desktop UI / `DesignSession` actor model,
single-threaded `block_on` on an inline `DocSink`, every event dumped to
stderr. Decouples task #28's "does the pipeline reach the LLM and apply
EditorCommands correctly" verification from GUI smoke (canvas paint /
chat panel rendering), which still needs the desktop binary.

Usage:

    export OPENPENCIL_ANTHROPIC_API_KEY=sk-ant-...
    cargo run -p op-smoke -- "design a login screen"

Optional `OPENPENCIL_ORCHESTRATOR_MODEL` (default `claude-sonnet-4-6`).

What's traced to stderr:
- `[SMOKE]` model + prompt
- `[LLM]` per-call system/user lengths + engine errors
- `[PROGRESS]` every `op_orchestrator::Progress` variant (Planning,
  ScaffoldDone, SubtaskStarted/Done/Failed, CleanupDone, validation +
  visual-ref variants)
- `[CMD]` per `EditorCommand::apply` one-line label + applied result
  (InsertSubtree shows parent_id + nodes.len(); SetNodeStrokeHex shows
  hex; etc.)
- `[UNDO]` begin / end batch boundaries
- `[FINAL]` Ok/Err + elapsed; on Ok prints root_frame_id, total_nodes,
  per-subtask outcomes

What this verifies vs the desktop GUI smoke
(`superpowers/notes/2026-05-24-orchestrator-smoke-steps.md`):
- LLM client construction (`AnthropicProvider::new` + auth)
- Network reachability (200 / 401 / 429 surfaces as `LlmError`)
- Planner → scaffold → subtask → cleanup transitions
- `InsertSubtree` ID-remapping (via post-apply state)
- Terminal `RunSummary` / `OrchestratorError`

What it deliberately skips:
- Canvas paint correctness — run the desktop binary
- chat panel progress rendering — run the desktop binary
- Pre-validation fixes — smoke uses `SkippedPreValidator` so the
  trace stays focused on orchestrator behaviour; the desktop binary
  uses `LintPreValidator`

`op-host-desktop::chat_orchestrator::DesktopLlmClient` would have been
ideal to reuse, but the host crate is binary-only (no lib target); the
op-smoke `SmokeLlmClient` is a ~50-line copy that swaps
`shared_runtime().spawn` for `tokio::spawn` (smoke owns its own tokio
runtime via `#[tokio::main]`).

cargo fmt + clippy --workspace --all-targets -D warnings clean.
Sanity-checked the no-prompt and no-API-key error paths.
2026-05-24 18:42:56 +08:00
Kayshen-X
743e8a902c fix(panels): render image fill controls 2026-05-24 17:45:53 +08:00
Kayshen-X
b6e9e91464 chore: enforce rust pre-commit checks 2026-05-24 17:44:42 +08:00
Fini
fa2d06c5af refactor(ai): drop dead node_base_role helper in validation_fixes_apply
memory-tagged follow-up — `node_base_role(node) -> Option<&str>` (line
568 of `validation_fixes_apply.rs`) was a thin shim over
`node.base().role.as_deref()` with zero call sites anywhere in
crates/* or vendor/*. Inlining at the (single, hypothetical) caller is
shorter than the helper itself.

`cargo test -p op-orchestrator` still 585 passed; workspace 1880
passed; fmt + clippy --workspace -D warnings clean.
2026-05-24 16:41:36 +08:00
Fini
91f05a1dc5 refactor(host): drop dead chat_orchestrator code now that DesignSession owns the route
Task #27 swapped the orchestrator route from `chat_orchestrator::run_design_request`
(direct `block_on` on the UI thread, never called) to
`design_session::DesignSession::start` (worker thread + RemoteDocSink +
ack channel). Confirming the leftover types are truly dead:

- `DesktopDocSink` — grep finds zero callers; replaced by `RemoteDocSink`
  in `design_session.rs` which owns its own mirror + sends `EditorCommand`
  back to the UI thread over mpsc.
- `run_design_request` — grep finds zero callers; replaced by the
  direct `Orchestrator::new().run(...)` invocation inside the worker
  closure in `DesignSession::start`.
- `DesktopLlmClient` — actively used (`design_session::start` builds one
  per turn). Kept.

So this commit:

- Deletes `DesktopDocSink` struct + impl block (~30 lines).
- Deletes `run_design_request` async fn (~40 lines).
- Removes `#![allow(dead_code)]` — the surviving code (`DesktopLlmClient`)
  is reachable so the module no longer needs the blanket allow.
- Trims module doc — drops the 4-item TODO list that described the
  pre-#27 wiring gap (intent gate / threading model / undo batch /
  validation providers) since #27 + #35 closed all four.
- Drops unused imports (`EditorCommand`, `EditorState`, `DesignRequest`,
  `Orchestrator`, `OrchestratorError`, `Progress`, `RunSummary`,
  `DocSink`, `AbortFlag`, `SkippedScreenshotProvider`,
  `SkippedVisionLlmClient`, `ValidationProviders`).
- Touches the `design_session.rs` comment that referenced `DesktopDocSink`
  by name — rephrased to describe the no-op behaviour without the
  dangling pointer.

`cargo test -p op-host-desktop` 106 passed (unchanged). cargo fmt +
clippy --workspace -D warnings clean.
2026-05-24 16:16:40 +08:00
Fini
22fe77b664 test(host): end-to-end actor-channel round-trip for DesignSession
Task #28 prep. The orchestrator side has its own end-to-end tests
under `crates/op-orchestrator/`, but the host's actor seam
(worker → mpsc → UI thread → ack → mirror update + Progress → chat
bubble) was only exercised piecewise by the three `RemoteDocSink`
tests. This adds a single end-to-end smoke that drives both
`pump_commands` + `pump_progress` from a fake worker thread against a
real `WidgetHostNative`, then asserts:

- Progress events rendered into the trailing assistant bubble
  (`• Planning…` line surfaces verbatim).
- `RunSummary::Ok` rendered as the `Done — K subtask(s) succeeded`
  trailer with `streaming = false`.
- `*current` cleared after `Done` so the event loop stops ticking
  at 33ms for this turn.

Internal hook: `DesignSession::from_channels` (cfg(test)) wraps
externally-supplied channels so a fake worker can drive the UI
pumps without spinning up `Orchestrator::run` / an `agent::Provider`
/ a live API key. Production path still goes through
`DesignSession::start`.

The full LLM smoke is documented separately in
openpencil-docs/superpowers/notes/2026-05-24-orchestrator-smoke-steps.md
— it needs `OPENPENCIL_ANTHROPIC_API_KEY` and can't live in cargo test.

cargo test -p op-host-desktop 106 passed (was 105). cargo fmt + clippy
--workspace -D warnings clean.
2026-05-24 15:35:25 +08:00
Fini
bba8e22f67 fix(host): drop in-flight design session when falling to chat path
Codex stop-time review caught a real cross-session leak in the intent
gate introduced by the previous commit. `launch_if_pending` clears
`current_chat` when the design branch is taken, but the chat branch
only cleared `current_chat` (for the unwired-agent error path) — it
never cleared `current_design`.

Symptom: if a design turn is in flight (worker pumping `apply` requests
+ `Progress` deltas into the trailing assistant bubble) and the user
sends a chat message, the new chat bubble begins streaming chat deltas
while the still-running design worker keeps acking apply requests on
the canonical doc and appending `Progress` strings to the same bubble.
The fix mirrors the design branch — when the chat path is taken (chat
intent, design intent without a Provider, or any unwired agent),
`*current_design = None` drops the worker's command receiver so its
next `apply` returns false and the turn ends.

`cargo test -p op-host-desktop` 105 passed. workspace fmt + clippy
clean.
2026-05-24 15:16:07 +08:00
Fini
35312b960b feat(host,orchestrator): wire intent gate + DesignSession into chat runtime
Task #27. Routes user messages through `op_orchestrator::classify_intent`
in `chat_session::launch_if_pending` — `Intent::Design` with a configured
`agent::Provider` launches into the orchestrator pipeline; everything
else (chat intent, or design intent with no Provider) falls through to
the existing `ChatProvider` CLI path.

The orchestrator is `async + &mut EditorState`; the chat path is built
around worker threads + blocking iterators (`ChatProvider::send`). Codex
architectural review picked Option E (UI-owned actor + RemoteDocSink +
ack channel):

- Worker thread owns a `RemoteDocSink` that forwards each `apply(cmd)`
  over an mpsc channel to the UI thread, which `apply()`s on the real
  `EditorState` and replies with an ack carrying a fresh state snapshot.
- `RemoteDocSink::state()` reads from a locally cached mirror updated
  by each ack — covers `EditorCommand::InsertSubtree`'s ID-remapping +
  history bookkeeping which must run on the UI thread.
- Two mpsc channels per turn: progress deltas (Planning / SubtaskStarted
  / etc.) into the chat transcript, and `DesignCmdReq` for apply +
  undo-batch boundaries.
- `BeginUndoBatch` / `EndUndoBatch` are forwarded as own `DesignCmdOp`
  variants so the UI can route them through real history batching once
  `op-editor-core` exposes that API (currently no-op, matching
  `DesktopDocSink`).

Provider source MVP: `OPENPENCIL_ANTHROPIC_API_KEY` (preferred) or
`ANTHROPIC_API_KEY` from env constructs an `AnthropicProvider`. Model
override via `OPENPENCIL_ORCHESTRATOR_MODEL` (defaults to
claude-sonnet-4-6). When neither key is set, design intent falls back
to the chat-CLI path so the user still gets an answer.

Wiring:
- `main.rs` `current_design: Option<DesignSession>` field next to
  `current_chat`; mutually exclusive routing in `launch_if_pending`.
- `app_handler.rs` `RedrawRequested` pumps both `pump_commands`
  (apply + ack) and `pump_progress` (deltas + summary); WaitUntil tick
  schedules a 33 ms wake when either session is in flight.
- `keyboard_input.rs` Enter / send sites pass both Option<&mut> args.

Provider features: `agent` crate gains the `anthropic` cargo feature in
`op-host-desktop/Cargo.toml` so `AnthropicProvider` is reachable
(previously `default-features = false` left it gated out — chat path
only needed the trait + engine wiring).

3 new `RemoteDocSink` tests cover ack round-trip + closed-channel safety
+ undo-batch signal distinguishability. Workspace 1882 passed / 0
failed. cargo fmt + clippy --workspace -D warnings clean.

The `chat_orchestrator.rs::run_design_request` legacy entry stays for
now (used by future single-shot programmatic callers); the live path is
`DesignSession::start`. Validation providers stay `Skipped*` until
jian-skia `captureRegion` + vision LLM crate land (task #28).
2026-05-24 15:11:34 +08:00
Kayshen-X
ad555231e4 fix(panels): keep image fill popover interactive
Some checks failed
Rust check (native) / macos-latest / 1.94 (push) Waiting to run
Rust check (native) / windows-latest / 1.94 (push) Waiting to run
Rust multi-platform build / linux-aarch64 (push) Waiting to run
Rust multi-platform build / macos-aarch64 (push) Waiting to run
Rust multi-platform build / windows-x86_64 (push) Waiting to run
Rust multi-platform build / macos-x86_64 (push) Waiting to run
Rust multi-platform build / windows-aarch64 (push) Waiting to run
Rust multi-platform build / ios-aarch64 (cargo check only) (push) Waiting to run
Rust multi-platform build / ios-aarch64-sim (cargo check only) (push) Waiting to run
Rust check (native) / ubuntu-latest / 1.94 (push) Failing after 3s
Rust check (native) / cargo-deny (native) (push) Failing after 1s
Rust check (native) / diagnostics golden drift (push) Failing after 2s
Rust multi-platform build / linux-x86_64 (push) Failing after 2s
Rust multi-platform build / wasm32-unknown-unknown / op-host-web (compile guard) (push) Failing after 1s
Rust multi-platform build / android-aarch64 (cargo check only) (push) Failing after 1s
Rust multi-platform build / android-x86_64 (cargo check only) (push) Failing after 1s
WASM bundle check (kickoff §1.2) / cargo check --target wasm32-unknown-unknown (push) Failing after 2s
WASM bundle check (kickoff §1.2) / cargo-deny --target wasm32-unknown-unknown check bans (push) Failing after 2s
2026-05-24 14:01:38 +08:00
Kayshen-X
66d58f2b09 feat(editor): add hover wash to vertical toolbar buttons
Mirror the existing file_menu / locale_picker / shape_picker hover
pattern: state-layer ToolbarHover enum on EditorUiState, widget reads
it during paint and renders theme.button_hover, host updates it on
apply_cursor_move AFTER drag detection so a path-anchor / node / pan
drag whose cursor crosses the toolbar isn't intercepted.

Extracted into widget_host/toolbar_hover.rs to keep geometry.rs under
the 800-line cap.
2026-05-24 14:01:38 +08:00
Fini
28f16e6d91 Merge branch 'v0.8.0-new' of github.com:ZSeven-W/openpencil into v0.8.0-new 2026-05-24 13:03:26 +08:00
Fini
722b0f4114 fix(editor-core,host): SetNode{Stroke,Fill}Hex accept $variable refs
Codex stop-time review caught a real regression in LintPreValidator:
when the doc declares a `color-border` variable, op-design-lint's
`border_stroke()` emits the suggested stroke fill as `$color-border`
(a design-token reference, not raw hex). The host adapter decomposes
`SetStroke` into `SetNodeStrokeHex` + `SetNodeStrokeWidth`. But
`cmd_set_node_stroke_hex` strict-parsed the hex via `parse_hex_rgb`
and rejected the `$`-prefixed string with `return false`, while
`SetNodeStrokeWidth` still applied successfully — so
`any_applied = true` and `applied_count += 1` overreported success
while the stroke color was silently dropped.

The TS counterpart (`fixes.rs::set_stroke_from_json` via
`serde_json::from_value::<PenStroke>`) treats `color` as a free
`String` and accepts `$ref` verbatim. The underlying
`set_primary_{stroke,fill}_hex` already writes `slot.color =
hex.to_string()` unconditionally — only the cmd-layer gate was wrong.

Fix:
- `cmd_set_node_stroke_hex` + `cmd_set_node_fill_hex` (op-editor-core):
  let `$`-prefixed strings bypass `parse_hex_rgb`. Literal hex paths
  unchanged; doc-comment notes the design-token contract.
- New fixture `invisible-container-with-var.json` (op-design-lint):
  doc declares `color-border` var so the detector emits
  `$color-border` ref. Regen'd golden confirms the ref reaches the
  Issue's `suggestedValue`.
- New equivalence test `equivalence_invisible_container_with_var` in
  `plan.rs`: `detect_and_plan + apply` produces same doc as
  `detect_and_fix`, AND the plan carries the `$color-border` ref
  (not a resolved hex).
- New parity test `parity_invisible_container_with_var` in
  `pre_validator.rs`: cross-crate adapter round-trip preserves the
  ref through EditorCommand application.

All 5 `Skipped*Provider` and 4 `PreValidator`-path stubs still in
place. drift guard re-runs clean (14 goldens, same as committed).

`cargo test` op-design-lint 143+13 / op-host-desktop 102 /
op-editor-core 280 / op-orchestrator 585+1 all green.
`cargo clippy --workspace -- -D warnings` clean.
`cargo fmt --all -- --check` clean.
2026-05-24 12:57:31 +08:00
Kayshen-X
f324a6fedb feat(editor): add hover wash to vertical toolbar buttons
Mirror the existing file_menu / locale_picker / shape_picker hover
pattern: state-layer ToolbarHover enum on EditorUiState, widget reads
it during paint and renders theme.button_hover, host updates it on
apply_cursor_move AFTER drag detection so a path-anchor / node / pan
drag whose cursor crosses the toolbar isn't intercepted.

Extracted into widget_host/toolbar_hover.rs to keep geometry.rs under
the 800-line cap.
2026-05-24 12:39:14 +08:00
Fini
061737d32d feat(host): LintPreValidator adapter — wire op-design-lint into PreValidator
Implements the D′ architecture host-side half:
- New `src/pre_validator.rs` with `LintPreValidator` struct and full
  `impl PreValidator` that calls `op_design_lint::detect_and_plan()`
  and translates each `PlannedFix → Vec<EditorCommand>` via
  `planned_fix_to_commands`.
- `ClearEffects` handled by reading current effect count from
  `sink.state()` and emitting N × `RemoveNodeEffect` highest→lowest.
- `SetStroke` decomposes into `SetNodeStrokeHex` + `SetNodeStrokeWidth`
  to work around the absence of a full-PenStroke EditorCommand.
- Parity tests in `#[cfg(test)]` (binary crate, can't use tests/) prove
  `LintPreValidator + EditorState` produces an equivalent document to
  `detect_and_fix` for 5 fixtures (invisible container, text explicit
  height, stacked horizontal padding, unexpected rotation, text effect).
  JSON comparison normalises away the `children: None→Some([])` side
  effect in the EditorState walker.
- `chat_orchestrator.rs` swaps `SkippedPreValidator` → `LintPreValidator`.
- `op-design-lint` added to `op-host-desktop/Cargo.toml` dependencies.
2026-05-24 12:35:02 +08:00
Fini
4fccaa65c7 feat(design-lint): editor-agnostic PlannedFix + detect_and_plan API
Add `PlannedFix` / `PlannedAction` types and `detect_and_plan()` in a
new `plan.rs` module. `detect_and_plan` runs `detect_all` then applies
the same filter as `apply_fixes` (skip Info severity, skip Fill no-op,
skip Remove on status-bar nodes) and returns a `Vec<PlannedFix>` — an
editor-agnostic plan that host adapters can translate to EditorCommands.

`PlannedAction` covers all 8 mutation paths in `apply_fixes` today:
`Remove`, `SetHeightFitContent`, `SetRotation`, `SetCornerRadius`,
`SetFontSize`, `ClearEffects`, `SetPadding`, `SetStroke`.

Adds 6 equivalence tests asserting that applying each `PlannedFix` via
`node_mut` primitives produces a document structurally identical to
`detect_and_fix` on the same input (invisible-container, text-explicit-
height, stacked-horizontal-padding, empty-path, status-bar guard,
unexpected-rotation fixtures). All 142 + 13 parity tests pass.
2026-05-24 12:24:52 +08:00
Kayshen-X
2e65c17d18 fix(editor): wire rust ui interactions 2026-05-24 09:31:46 +08:00
Fini
c3b8cd4a44 Merge branch 'v0.8.0-new' of github.com:ZSeven-W/openpencil into v0.8.0-new
Some checks failed
Rust check (native) / macos-latest / 1.94 (push) Waiting to run
Rust check (native) / windows-latest / 1.94 (push) Waiting to run
Rust multi-platform build / linux-aarch64 (push) Waiting to run
Rust multi-platform build / macos-aarch64 (push) Waiting to run
Rust multi-platform build / windows-x86_64 (push) Waiting to run
Rust multi-platform build / macos-x86_64 (push) Waiting to run
Rust multi-platform build / windows-aarch64 (push) Waiting to run
Rust multi-platform build / ios-aarch64 (cargo check only) (push) Waiting to run
Rust multi-platform build / ios-aarch64-sim (cargo check only) (push) Waiting to run
Rust check (native) / ubuntu-latest / 1.94 (push) Failing after 4s
Rust check (native) / cargo-deny (native) (push) Failing after 1s
Rust check (native) / diagnostics golden drift (push) Failing after 2s
Rust multi-platform build / linux-x86_64 (push) Failing after 1s
Rust multi-platform build / wasm32-unknown-unknown / op-host-web (compile guard) (push) Failing after 1s
Rust multi-platform build / android-aarch64 (cargo check only) (push) Failing after 1s
Rust multi-platform build / android-x86_64 (cargo check only) (push) Failing after 1s
WASM bundle check (kickoff §1.2) / cargo check --target wasm32-unknown-unknown (push) Failing after 1s
WASM bundle check (kickoff §1.2) / cargo-deny --target wasm32-unknown-unknown check bans (push) Failing after 1s
2026-05-24 01:52:13 +08:00
Kayshen-X
bf4ac35966 fix(ci): restore diagnostics golden formatting 2026-05-24 00:53:05 +08:00
Fini
6104fa277a fix(ci): protect op-design-lint goldens from oxfmt + restore TS-oracle format
CI 'Fail on golden drift' (rust-check.yml) was red because oxfmt
inlined the 4-element padding arrays in the committed goldens, while
the TS oracle's `JSON.stringify(value, null, 2)` emits them multi-line.
Every `bun run format` would re-introduce the drift.

Fix:
- .prettierignore: ignore crates/op-design-lint/tests/fixtures/golden/
  so oxfmt leaves them at the TS-oracle byte-exact format.
- regenerate the two affected goldens via the oracle
  (edge-section-padding.json, stacked-horizontal-padding.json) so they
  match what the CI drift guard expects.

`bun run tools/dump-diagnostics-golden.ts` + `git diff` now clean.
`cargo test -p op-design-lint` 149/149 green. fmt-check clean.
This was the only red workflow on the v0.8.0-new push; CI should be
fully green after this lands.
2026-05-24 00:50:31 +08:00
Fini
845e5ae7b5 Merge branch 'v0.8.0-new' of github.com:ZSeven-W/openpencil into v0.8.0-new 2026-05-24 00:39:09 +08:00
Kayshen-X
a124db05f1 fix(ci): clear rust check failures 2026-05-24 00:29:43 +08:00
Kayshen-X
22590b3119 feat(editor): enable figma import drop zone 2026-05-24 00:29:05 +08:00
Kayshen-X
4986be5cc6 chore(test): format golden fixtures 2026-05-23 23:55:00 +08:00
Kayshen-X
d5cbd2c977 fix(canvas): render image fills on canvas 2026-05-23 23:48:11 +08:00
Kayshen-X
68a7e534fd 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
c4fa1683f9 fix(orchestrator): port extractHtmlFromResponse — strip code fence + wrap bare
Codex stop-time review caught a regression: `generate_design_code`
returned the LLM response verbatim, missing the TS
`extractHtmlFromResponse` post-processing (design-code-generator.ts
L54-87). Without it, the common LLM output shape — ```html
<!DOCTYPE html>... ``` — leaks fence markers into
`extract_structure_summary`'s downstream regex scan.

Port the 4-stage fallback chain:
1. ```(html)?\s*\n?…\n?``` fence — if inner contains
   <!DOCTYPE or <html, return inner trimmed.
2. Trimmed response itself starts with <!DOCTYPE / <html → return as-is.
3. Case-insensitive find <!DOCTYPE…</html> → return slice (original case).
4. Wrap bare content in default <!DOCTYPE html><html lang=en>… scaffold.

8 new tests cover each stage + edge cases (empty, bare text,
case-insensitive doctype, fence without HTML marker, leading whitespace).
Existing `generate_design_code` tests still pass — the scripted
`<html><body>...</body></html>` outputs go through stage 2 verbatim,
identical to the old behaviour.
2026-05-23 21:02:54 +08:00
Fini
a50d305aef fix(workspace): unblock CI — host visual_ref_enabled + radial-gradient clippy allow + fmt catch-up
Three independent CI-unblockers grouped to keep history clean:

1. op-host-desktop/chat_orchestrator.rs — add `visual_ref_enabled: false`
   to the DesignRequest literal so the workspace builds after S4 added
   the field. Same stub-plumbing pattern as the S3b-2 `concurrency`,
   S3b-4 `append_context`, S3c `validation_enabled` additions. Real
   intent-gate routing is task #27.

2. op-editor-core/render_backend.rs::fill_round_rect_radial_gradient —
   #[allow(clippy::too_many_arguments)] on the trait default. The
   radial-gradient hook carries (rect, radius, stops, cx_frac, cy_frac,
   radius_frac, opacity) per the TS pen-renderer contract; that's the
   data shape, not refactorable without splitting the trait.

3. cargo fmt --all catch-up on 6 other files (op-editor-core /
   op-editor-ui / op-host-native) that pre-dated this branch and were
   never run through rustfmt. Pure whitespace, no semantic change. Lets
   `cargo fmt --all -- --check` pass workspace-wide.

`cargo clippy --workspace --all-targets -- -D warnings` + `cargo build
--workspace` + `cargo fmt --all -- --check` all clean. Tests across
op-orchestrator (574+1) / op-design-lint (149) / op-mcp (144) /
op-editor-core (273) / op-host-desktop (96) all green.
2026-05-23 20:43:39 +08:00
Fini
8c0f5d4e6f style(orchestrator): post-S4 clippy nits — manual_strip + flatten test modules
Three clippy warnings surfaced by the final-review workspace clippy
pass that the per-task gates didn't catch:

- design_system.rs::extract_code_fence — replace manual slice
  `&after_open[4..]` with `strip_prefix("json")`. Same behaviour,
  cleaner intent, satisfies clippy::manual_strip.
- design_system_tests.rs + visual_ref_tests.rs — flatten the inner
  `#[cfg(test)] mod tests { ... }` wrapper. The files are already
  wired via `#[path = "..."] mod tests;` in their parent modules
  (design_system.rs / visual_ref.rs), so the inner wrapper was double
  nesting (clippy::module_inception). Matches the established
  pattern in concurrent_tests.rs / plan_repair_tests.rs / run_tests.rs.

574 tests still pass. clippy + fmt + cargo build --workspace clean.
2026-05-23 20:43:16 +08:00
Fini
a247c5b792 feat(orchestrator): visual_ref — execute_visual_ref_orchestration 5-stage flow
Port executeVisualRefOrchestration from visual-ref-orchestrator.ts:45-166.
5-stage flow: design-system seed → HTML codegen → screenshot → enhanced
prompt → Orchestrator::run. Emits VisualRefStarted/DesignSystem/HtmlGenerated/
ScreenshotReady/Fallback progress variants. Falls back to plain Orchestrator::run
on empty HTML or None screenshot. Abort checked before each stage.
2026-05-23 20:31:05 +08:00
Fini
08a1d3d8c8 feat(orchestrator): visual_ref — generate_design_code + structure summary + enhanced prompt
S4 B2: port three HTML helper functions from the deleted TS visual-ref
pipeline. generate_design_code uses design-code + design-principles skills
as system prompt. extract_structure_summary uses a hand-rolled byte scanner
(no regex crate dep) to extract sections/headings/CTAs. build_enhanced_prompt
produces the byte-exact TS prompt template. 20 new tests; 569 total green.
2026-05-23 20:21:05 +08:00
Fini
0c4acd554d feat(orchestrator): design_system — generator + variable seeding + prompt context
Task B1 of S4. Adds three functions to design_system.rs:
- generate_design_system(): async LLM call with design-system skill as system
  prompt; parses response via parse_design_system; falls back to DEFAULT on
  any failure (port of design-system-generator.ts:20-29).
- design_system_to_seed_commands(): emits SetVariableColor/SetVariableScalar
  for 8 palette tokens (color-*), 6 spacing steps (spacing-xs..2xl), 3 radius
  steps (radius-sm/md/lg), 2 font strings (font-heading/font-body), and 6 type
  scale steps (font-size-1..6) — 25 commands for DEFAULT (port of TS:134-156).
- design_system_to_prompt_context(): byte-exact port of the TS template prose
  "DESIGN SYSTEM (use these values consistently):\nColors: bg ... Style: ..."
  (port of TS:161-170).

11 new TDD tests; full suite 550 green (baseline was 539).
2026-05-23 20:13:53 +08:00
Fini
8744947712 feat(orchestrator): types — VisualRefProvider trait + SkippedVisualRefProvider stub 2026-05-23 20:03:55 +08:00
Fini
0a4a34b3c2 feat(orchestrator): design_system — types + parse + defaults + Progress + visual_ref_enabled
Port TS design-system-generator.ts (deleted in 0f12b6e9) into Rust as Task A1 of S4:

- Add design_system.rs: DesignSystem struct (palette BTreeMap + Typography + Spacing + radius Vec<f64> + aesthetic), default_design_system() OnceLock accessor, parse_design_system() 4-stage fallback chain (direct → code-fence strip → brace-extract → DEFAULT).
- Add 5 Progress::VisualRef* variants to types.rs: VisualRefStarted, VisualRefDesignSystem{var_count}, VisualRefHtmlGenerated{byte_len}, VisualRefScreenshotReady{skipped}, VisualRefFallback{reason}.
- Add DesignRequest.visual_ref_enabled: bool with #[serde(default="default_visual_ref_enabled")] = false.
- Update all 35 DesignRequest struct literals across the crate to include the new field.
- 17 new tests covering all 4 parse stages, DEFAULT exact values, serde round-trip, and variant compilation.
2026-05-23 19:57:30 +08:00
Fini
0f12b6e998 refactor(ai): delete dead visual-ref pipeline + simplify design-validation
S4 deep-dive (S4 = pen-ai-skills migration's 4th sub-project) revealed
the entire 'visual reference' pipeline is dead code:

- executeVisualRefOrchestration has ZERO call sites. ai-chat-handlers
  → generateDesign() → executeOrchestration() directly. No
  visual-ref dispatch.
- mode: 'visual-ref' (described in older comments + memory note as
  'always-triggered since 2026-03-07') does not exist anywhere in src.
- design-validation.ts's referenceScreenshot path was wired to
  getCurrentVisualReference() which was wired to a module-level
  variable that nobody ever called setCurrentVisualReference on. The
  hasReference branch was always false.

Delete the 4 dead files (~734 LOC) + simplify design-validation:
- visual-ref-orchestrator.ts (243 LOC, dead entry)
- design-system-generator.ts (174 LOC, only called by dead entry)
- design-code-generator.ts (192 LOC, only called by dead entry)
- html-renderer.ts (125 LOC, only called by dead entry, browser-only)
- design-validation.ts: drop the import + 5 clearVisualReference()
  calls + the visualRef/hasReference branch in the round loop; pass
  undefined for referenceScreenshot. The validateDesignScreenshot
  signature keeps referenceScreenshot? for a future visual-ref source.

Rust-side note: the equivalent reference-comparison vision call IS
already implemented in crates/op-orchestrator/src/validation.rs
(reference_instruction text + timeout-doubling + plumb-through), it
just always passes None today — same pattern as ScreenshotProvider:
host wires real source when ready. No Rust changes needed for this
cleanup.

DesignSystem / VisualReference interfaces in ai-types.ts kept as
type-only declarations in case a future visual-ref provider lands.
2026-05-23 19:45:53 +08:00
Fini
25355a2d51 style(workspace): oxfmt catch-up — 8 pre-existing fixture/golden/tool files
oxfmt across the workspace was failing the pre-commit gate on 8 files
that predate this branch — S1 op-design-lint fixtures + S3b-1a planner
golden + planner dump-golden TS tool — none of which had ever been run
through oxfmt. Pure whitespace/trailing-newline normalization, no
semantic changes. Lets format:check pass cleanly across the workspace.
2026-05-23 19:45:53 +08:00
Kayshen-X
2a355ab33d feat(canvas): linear + radial gradient rendering with editable stops
Some checks failed
Rust check (native) / macos-latest / 1.94 (push) Waiting to run
Rust check (native) / windows-latest / 1.94 (push) Waiting to run
Rust multi-platform build / linux-aarch64 (push) Waiting to run
Rust multi-platform build / macos-aarch64 (push) Waiting to run
Rust multi-platform build / windows-x86_64 (push) Waiting to run
Rust multi-platform build / macos-x86_64 (push) Waiting to run
Rust multi-platform build / windows-aarch64 (push) Waiting to run
Rust multi-platform build / ios-aarch64 (cargo check only) (push) Waiting to run
Rust multi-platform build / ios-aarch64-sim (cargo check only) (push) Waiting to run
Rust check (native) / ubuntu-latest / 1.94 (push) Failing after 2s
Rust check (native) / cargo-deny (native) (push) Failing after 1s
Rust check (native) / diagnostics golden drift (push) Failing after 2s
Rust multi-platform build / linux-x86_64 (push) Failing after 2s
Rust multi-platform build / wasm32-unknown-unknown / op-host-web (compile guard) (push) Failing after 2s
Rust multi-platform build / android-aarch64 (cargo check only) (push) Failing after 2s
Rust multi-platform build / android-x86_64 (cargo check only) (push) Failing after 2s
WASM bundle check (kickoff §1.2) / cargo check --target wasm32-unknown-unknown (push) Failing after 2s
WASM bundle check (kickoff §1.2) / cargo-deny --target wasm32-unknown-unknown check bans (push) Failing after 2s
Wires the linear/radial gradient fill type from the property panel
through to a real skia shader on the canvas — previously switching
fill type only changed the model and the panel painted hardcoded
placeholder stops. Stop hex (with alpha preserved), angle, offset %,
and per-stop HSV picker are all live.

- SceneNode + NodePayload carry a resolved gradient body; native
  skia backend builds `linear_gradient` / `radial_gradient` shaders
  with TS-renderer geometry (angle - 90° ellipse projection, radius
  fraction of max(w, h)).
- PropertyFocus + EditContext gain GradientAngle / GradientStopHex /
  GradientStopOffset; commit, snapshot, hit-test, paint, keyboard
  validation, and the property-panel layout walker thread the new
  variants end-to-end.
- ColorPickerState carries a per-session alpha so the gradient-stop
  picker keeps transparency across HSV drags; hex pill stays 6-char
  and re-attaches the stop's existing alpha at commit. Swatch paints
  over a 2×2 checker so a fully transparent stop reads as such.
- skia.rs split into a spine + sibling gradient.rs to stay under the
  workspace 800-line cap.

# Conflicts:
#	crates/op-editor-core/src/lib.rs
2026-05-23 19:13:35 +08:00
Fini
d215731f9f feat(orchestrator,host): production-visible Skipped*Provider + unblock chat_orchestrator compile
Move the three S3c validation stub providers (SkippedPreValidator,
SkippedScreenshotProvider, SkippedVisionLlmClient) from cfg-test-only
test_support to a new pub module 'stub_providers'. Test code keeps
working via a pub(crate) re-export; host code can now construct a
no-op ValidationProviders bundle without re-implementing the trait
impls per host.

chat_orchestrator.rs (predecessor of task #27 'wire intent gate into
chat runtime') compiles again after S3b-2/S3b-4/S3c added required
fields + a required Orchestrator::run arg:

- DesignRequest literal grew append_context: None, concurrency: 1,
  validation_enabled: false (conservative — validation gated off
  until host wires real screenshot / vision LLM).
- Orchestrator::run takes a 6th &ValidationProviders<'_> arg; we pass
  a bundle of the three Skipped*Provider stubs.

The whole module is marked #![allow(dead_code)] because no caller has
yet been added to chat_runtime.rs — that's #27 proper. Delete that
allow in the same PR as #27 真接线.
2026-05-23 18:53:21 +08:00
Fini
561cc5830c fix(submodule): pin vendor/casement to op-file-open head
CI checkout was failing on a ghost superproject pointer
c8df95eb82df3487e77e96c20e20864eb35e107a — that commit is not in the
casement remote and not in the local clone either. Local disk state is
5ad98f1c (== origin/op-file-open head), which IS pushed and accessible
to GitHub Actions' submodule fetch.

This unblocks the 'Rust Check' / 'Rust multi-platform build' / 'WASM
bundle check' workflows whose actions/checkout@v4 step uses
submodules: recursive. Cargo workspace still excludes vendor/casement
itself (op-host-native/op-host-desktop use it via path = ... winit
fork).
2026-05-23 18:53:21 +08:00
Fini
793a21b0aa fix(ai): keep debug_validation_report out of the production MCP catalog
debug_validation_report was registered in rebuild_registry and listed
in tools/list unconditionally — only call() consulted
OPENPENCIL_DEBUG_TOOLS. A production client (flag unset) still saw the
debug tool in its catalog and could invoke it, getting a bare
ToolFailed. That leaks the debug surface into the production catalog.

Gate it everywhere instead of only at call time: rebuild_registry
registers the tool only when debug_tools_enabled(), and
tools_list_response appends the debug schema — now a separate
DEBUG_TOOL_SCHEMAS const, removed from TOOL_SCHEMAS — only when the
flag is set. A client without the flag never sees the tool at all,
matching the TS design where debug tools ship only in a debug build.

debug_tools_enabled() is promoted to pub and re-exported from op-mcp.
The tools/list catalog test now exercises both gate states (82 tools
flag-off, debug tool present flag-on).
2026-05-23 18:39:08 +08:00
Fini
5b3eb7afff 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
780e4607b3 Merge branch 'v0.8.0-new' of github.com:ZSeven-W/openpencil into v0.8.0-new
Some checks failed
Rust check (native) / macos-latest / 1.94 (push) Waiting to run
Rust check (native) / windows-latest / 1.94 (push) Waiting to run
Rust multi-platform build / linux-aarch64 (push) Waiting to run
Rust multi-platform build / macos-aarch64 (push) Waiting to run
Rust multi-platform build / windows-x86_64 (push) Waiting to run
Rust multi-platform build / macos-x86_64 (push) Waiting to run
Rust multi-platform build / windows-aarch64 (push) Waiting to run
Rust multi-platform build / ios-aarch64 (cargo check only) (push) Waiting to run
Rust multi-platform build / ios-aarch64-sim (cargo check only) (push) Waiting to run
Rust check (native) / ubuntu-latest / 1.94 (push) Failing after 2s
Rust check (native) / cargo-deny (native) (push) Failing after 1s
Rust multi-platform build / linux-x86_64 (push) Failing after 1s
Rust multi-platform build / wasm32-unknown-unknown / op-host-web (compile guard) (push) Failing after 2s
Rust multi-platform build / android-aarch64 (cargo check only) (push) Failing after 2s
Rust multi-platform build / android-x86_64 (cargo check only) (push) Failing after 1s
WASM bundle check (kickoff §1.2) / cargo check --target wasm32-unknown-unknown (push) Failing after 1s
WASM bundle check (kickoff §1.2) / cargo-deny --target wasm32-unknown-unknown check bans (push) Failing after 2s
2026-05-23 14:29:26 +08:00
Kayshen-X
413b80a479 feat(panels): editable inputs (arrows, caret, fill-opacity, effect params) + paint polish
Property-panel input editing now supports:
- Arrow keys: Up/Down step a numeric field; Left/Right move the text caret. Caret position is a real index into the draft, so typing inserts at the caret and Backspace deletes the char before it (not just append/pop).
- Layer opacity: full-width box with the localized 不透明度 label inside on the left, value next to it, % at the right edge; clipped so a long-locale label can't bleed past the half-width box.
- Fill opacity: new SolidFillBody.opacity getter / setter on the model + PropertyFocus::FillOpacity + the 100 % box in the Fill head row is now editable end-to-end.
- Effect-param values: each Drop Shadow X / Y / Blur / Spread cell is a click-to-type input box (new effect_param_focus state, FocusEffectParam action, commit_effect_param_focus_if_any path). The − / + steppers still work alongside. Web's apply_property_action makes the focus a no-op (no keyboard path on web yet) to avoid stranding focus.

Paint polish:
- Standardised input-text baselines to + 19.0 across prefix / suffix / icon helpers, the fill / stroke / opacity / hex paints, and the export section so every INPUT_HEIGHT row reads on the same baseline.
- Icon-prefixed inputs now have 10 px left padding (matching X / Y / W / H) and the icon is vertically centred ((30 - 14) / 2 = 8) instead of sitting at y + 5.
- Fill / stroke swatches vertically centred in their hex rows ((30 - 16) / 2 = 7).
- '-' / '#' caret-aware validation in apply_text so typing them at caret 0 of a non-empty draft is now a valid edit. Native input_tests seed property_caret_pos to mirror real focus state.

Codex review iterations: poison-guard the effect-param focus on web, content-clip the layer-opacity row, fix property-panel scroll clamping in paint, and a few related safety guards across hosts.
2026-05-23 12:59:28 +08:00