Commit graph

95 commits

Author SHA1 Message Date
Fini 84fc0ffddb fix(ai): explicit skipped reason for builtin providers in vision validate
Why: builtin providers (MiniMax / DeepSeek / Bailian / Ark) currently
fall through to the generic "Missing or unsupported provider" error in
/api/ai/validate. The post-generation loop catches that as a hard
provider error and logs "[error] Analysis skipped (timeout or provider
error)" — which reads like a config bug to the user even though the
real reason is "this provider's models are text-only, vision validation
isn't useful here even if we did proxy it".

What: branch on body.provider === 'builtin' before the generic error
and return { skipped: true, error: '<explanatory message>' }. The
client design-validation.ts already short-circuits on `data.skipped`
so the loop now logs the clearer message instead. No behavior change
for the four supported providers; no new wire fields.
2026-05-09 21:20:00 +08:00
Fini fea13441f1 fix(ai): add close-button aliases (dismiss/cancel/remove/closebutton/expand/collapse → x / maximize-2 / minimize-2)
Why: end-to-end test of "design a notification card with dismiss x
button" surfaced that MiniMax-M2.7 emits a path node named "Dismiss
Icon". Tokenisation gives "dismiss" but Lucide doesn't have a `dismiss`
key — the resolver fell through prefix/substring fallbacks and wrote
the placeholder lucide:circle, leaving the card with a hollow ring
where the X should be.

What: 5 new aliases added in lock-step to icon-dictionary.ts (client
commonAliases) + icon.ts (server NAME_ALIASES per existing comment):
  - dismiss   → x       (close button intent)
  - closebutton → x     (compacted from "Close Button Icon")
  - cancel    → x       (cancel-action close icon)
  - remove    → x       (remove-action close icon)
  - expand    → maximize-2
  - collapse  → minimize-2

NOT aliased: `cross`. Lucide already ships a `cross` icon (the
Christian-cross shape) and overriding it would lose that geometry.
"Cross" disambiguation is left to the model — if it really means a
close button, telling it to write "Dismiss Icon" / "Close Icon" via
the icon-catalog skill is enough.

Tests: 3 new it.each cases (Dismiss / Cancel / Remove Icon → /x/).
1069 / 1069 AI tests pass (was 1066; +3).
2026-05-09 21:16:00 +08:00
Fini 381e86c412 Merge branch 'v0.8.0' of github.com:ZSeven-W/openpencil into v0.8.0 2026-05-09 21:11:00 +08:00
Kayshen-X 5a027a4f4e test(shell-core): assert W3C field readback in gesture re-export tests
Codex P0 mini-gate Round 2 finding (Q5) fix: gesture_re_export.rs tests
set the new W3C fields (KeyEvent.is_composing, FocusEvent.related_node_
id_hint, WheelEvent.delta_z + WheelEvent.mode mutability) but only
asserted the structural compile-time identity, not value readback.

Strengthened to assert every W3C field reads back what was written so
cross-crate type identity AND field-level binary compat are both verified
through the OP re-export path:
- key_event_is_re_exported_from_jian_with_all_w3c_fields: 7-field assert
- focus_event_is_re_exported_from_jian_with_all_w3c_fields: 3-field assert
- wheel_event_is_re_exported_from_jian_with_w3c_fields: defaults +
  mutate-and-assert mode + delta_z + delta.x/y

cargo test -p openpencil-shell-core --test gesture_re_export → 6/6 PASS.
2026-05-09 21:03:00 +08:00
Fini 40a085d0f8 fix(ai): tokenize path icon names so multi-word "Search Icon Path" resolves
Why: MiniMax-M2.7 keeps emitting path nodes named "Search Icon Path" /
"Time Icon Path" / "Heart Icon Stroke" (3 words ending in noise word).
The legacy resolver normalised to "searchiconpath" (15 chars), prefix
fallback found "search" (6/15 = 40% < 50% threshold) → rejected →
fallback to lucide:circle → user-visible "circle bug" across categories,
filter chips, and search bar leading icons. Skill update alone (telling
models to use icon_font) doesn't fix the trained-pattern leftover —
Codex flagged this as a still-unfixed failure mode.

What: extractIconKeyword() tokenises on camelCase / space / dash /
underscore boundaries and drops { icon, logo, symbol, glyph, path,
shape, stroke, fill, svg, graphic, image }. Surviving tokens are
concatenated for direct dictionary lookup. Pure-noise names ("Icon
Path", "Symbol") return early without writing the misleading circle
placeholder. time / deliverytime / rider aliases added (kept in sync
across icon-dictionary.ts and server icon.ts NAME_ALIASES per existing
comment). 11 new tests cover multi-word resolution and pure-noise
no-op; 21 prior regression cases (descriptive geometry untouched,
single-word camelCase / kebab / snake all resolve, "Brand Logo"
placeholder behaviour preserved) still green.
2026-05-09 21:02:00 +08:00
Fini af9292d8f5 fix(ai): classify Type 0 components as non-mobile to skip phone chrome
Why: "Design a profile card" through MiniMax-M2.7 produced a 375×803 mobile
screen with auto-injected status bar, because the planner skill listed
"profiles" as a Type 2 single-task screen and the orchestrator's
isMobileScreen heuristic ran on width≤480 alone.

What: design-type.md + decomposition.md add Type 0 (single component:
card / badge / chip / modal) with width=400 height=0 1 subtask no chrome.
isMobileFullScreen helper extracted to orchestrator-plan-classify.ts and
required by both orchestrator.ts and orchestrator-sub-agent.ts so the
two paths can't drift on what "mobile" means (Codex review caught this
when only orchestrator.ts had the new check).

Verified with same MiniMax + same prompt: 400×320 component, 8 nodes,
firstChildRole=card, no status-bar.
2026-05-09 21:00:00 +08:00
Fini 9f153981db fix(ai): explicit skipped reason for builtin providers in vision validate
Why: builtin providers (MiniMax / DeepSeek / Bailian / Ark) currently
fall through to the generic "Missing or unsupported provider" error in
/api/ai/validate. The post-generation loop catches that as a hard
provider error and logs "[error] Analysis skipped (timeout or provider
error)" — which reads like a config bug to the user even though the
real reason is "this provider's models are text-only, vision validation
isn't useful here even if we did proxy it".

What: branch on body.provider === 'builtin' before the generic error
and return { skipped: true, error: '<explanatory message>' }. The
client design-validation.ts already short-circuits on `data.skipped`
so the loop now logs the clearer message instead. No behavior change
for the four supported providers; no new wire fields.
2026-05-09 20:59:34 +08:00
Fini 38dcca0cfe fix(ai): add close-button aliases (dismiss/cancel/remove/closebutton/expand/collapse → x / maximize-2 / minimize-2)
Why: end-to-end test of "design a notification card with dismiss x
button" surfaced that MiniMax-M2.7 emits a path node named "Dismiss
Icon". Tokenisation gives "dismiss" but Lucide doesn't have a `dismiss`
key — the resolver fell through prefix/substring fallbacks and wrote
the placeholder lucide:circle, leaving the card with a hollow ring
where the X should be.

What: 5 new aliases added in lock-step to icon-dictionary.ts (client
commonAliases) + icon.ts (server NAME_ALIASES per existing comment):
  - dismiss   → x       (close button intent)
  - closebutton → x     (compacted from "Close Button Icon")
  - cancel    → x       (cancel-action close icon)
  - remove    → x       (remove-action close icon)
  - expand    → maximize-2
  - collapse  → minimize-2

NOT aliased: `cross`. Lucide already ships a `cross` icon (the
Christian-cross shape) and overriding it would lose that geometry.
"Cross" disambiguation is left to the model — if it really means a
close button, telling it to write "Dismiss Icon" / "Close Icon" via
the icon-catalog skill is enough.

Tests: 3 new it.each cases (Dismiss / Cancel / Remove Icon → /x/).
1069 / 1069 AI tests pass (was 1066; +3).
2026-05-09 20:59:30 +08:00
Fini e3ee765d90 Merge branch 'v0.8.0' of github.com:ZSeven-W/openpencil into v0.8.0 2026-05-09 20:59:25 +08:00
Kayshen-X 3389ab456d test(shell-core): assert W3C field readback in gesture re-export tests
Codex P0 mini-gate Round 2 finding (Q5) fix: gesture_re_export.rs tests
set the new W3C fields (KeyEvent.is_composing, FocusEvent.related_node_
id_hint, WheelEvent.delta_z + WheelEvent.mode mutability) but only
asserted the structural compile-time identity, not value readback.

Strengthened to assert every W3C field reads back what was written so
cross-crate type identity AND field-level binary compat are both verified
through the OP re-export path:
- key_event_is_re_exported_from_jian_with_all_w3c_fields: 7-field assert
- focus_event_is_re_exported_from_jian_with_all_w3c_fields: 3-field assert
- wheel_event_is_re_exported_from_jian_with_w3c_fields: defaults +
  mutate-and-assert mode + delta_z + delta.x/y

cargo test -p openpencil-shell-core --test gesture_re_export → 6/6 PASS.
2026-05-09 09:29:52 +08:00
Fini da4f9d9cca fix(ai): tokenize path icon names so multi-word "Search Icon Path" resolves
Why: MiniMax-M2.7 keeps emitting path nodes named "Search Icon Path" /
"Time Icon Path" / "Heart Icon Stroke" (3 words ending in noise word).
The legacy resolver normalised to "searchiconpath" (15 chars), prefix
fallback found "search" (6/15 = 40% < 50% threshold) → rejected →
fallback to lucide:circle → user-visible "circle bug" across categories,
filter chips, and search bar leading icons. Skill update alone (telling
models to use icon_font) doesn't fix the trained-pattern leftover —
Codex flagged this as a still-unfixed failure mode.

What: extractIconKeyword() tokenises on camelCase / space / dash /
underscore boundaries and drops { icon, logo, symbol, glyph, path,
shape, stroke, fill, svg, graphic, image }. Surviving tokens are
concatenated for direct dictionary lookup. Pure-noise names ("Icon
Path", "Symbol") return early without writing the misleading circle
placeholder. time / deliverytime / rider aliases added (kept in sync
across icon-dictionary.ts and server icon.ts NAME_ALIASES per existing
comment). 11 new tests cover multi-word resolution and pure-noise
no-op; 21 prior regression cases (descriptive geometry untouched,
single-word camelCase / kebab / snake all resolve, "Brand Logo"
placeholder behaviour preserved) still green.
2026-05-09 08:00:00 +08:00
Fini c81cfe82a4 Merge branch 'v0.8.0' of github.com:ZSeven-W/openpencil into v0.8.0
# Conflicts:
#	.github/workflows/rust-multiplatform.yml
#	README.md
#	crates/openpencil-shell-core/src/lib.rs
#	crates/openpencil-shell-native/examples/basic_window.rs
#	crates/openpencil-shell-native/src/lib.rs
2026-05-05 22:51:00 +08:00
Kayshen-X c55807e432 ci: remove TS/Electron workflows (build-electron / ci / docker / publish-cli)
Rust-ification 阶段,CI 只保留 Rust 相关:
- rust-check.yml: cargo fmt + build + test (with STEP1A_REQUIRE_GPU=1 on Linux) + clippy + cargo-deny
- wasm-bundle-check.yml: wasm32 target check

删除:
- build-electron.yml: Electron desktop build (Rust 化后用 openpencil-shell-native)
- ci.yml: TS type-check + Vitest + web build (Rust 化后已废)
- docker.yml: TS Docker image (Rust 化后重做)
- publish-cli.yml: npm packages (Rust 化后改 cargo publish)
2026-05-05 22:09:00 +08:00
Fini 0248c17209 Merge branch 'v0.8.0' of github.com:ZSeven-W/openpencil into v0.8.0 2026-05-05 21:54:00 +08:00
Kayshen-X 22003f9b0c chore(shell-native): add transient P0 probe gate (Step 1a)
Drives the three-OS CI matrix verification of the skia-safe + glutin +
glow + winit dep stack per Step 1a spec §7.

- examples/p0_probe.rs: stencil_visibility + readback chain runner (must
  own a real OS main thread because winit on macOS rejects
  EventLoop::new() from cargo test worker threads).
- tests/p0_probe.rs: subprocess-invoke wrapper, gated
  #[ignore = "P0_PROBE_GATE"] so default cargo test stays untouched.
- Cargo.toml: add transient [target.'cfg(not(target_arch = "wasm32"))'.
  dev-dependencies] block (skia-safe 0.97 + glutin 0.32.3 + glutin-winit
  0.5.0 + glow 0.17.0 + raw-window-handle 0.6.2 + scopeguard 1.2.0 +
  winit defaults). Pinned to versions resolved in /tmp/skia-glow-probe.
- .github/workflows/rust-check.yml: install Linux GL prereqs (xvfb,
  mesa, libxkbcommon, libwayland) and add a P0-probe-gate step running
  cargo test --ignored on each OS (Linux through xvfb-run; Windows
  early-returns per spec §8.2 WINDOWS_GPU_DEFERRED_NO_RUNNER).

All three artefacts are TRANSIENT — reverted in a follow-up cleanup
commit after CI is green and the loader-compat notes commit lands.
Task 1 owns the permanent integration.
2026-05-05 12:23:49 +08:00
Fini 90cd86fc11 Merge branch 'v0.8.0' of github.com:ZSeven-W/openpencil into v0.8.0
# Conflicts:
#	.github/workflows/rust-multiplatform.yml
#	README.md
#	crates/openpencil-shell-core/src/lib.rs
#	crates/openpencil-shell-native/examples/basic_window.rs
#	crates/openpencil-shell-native/src/lib.rs
2026-05-05 12:23:47 +08:00
Kayshen-X d5011547f1 ci: remove TS/Electron workflows (build-electron / ci / docker / publish-cli)
Rust-ification 阶段,CI 只保留 Rust 相关:
- rust-check.yml: cargo fmt + build + test (with STEP1A_REQUIRE_GPU=1 on Linux) + clippy + cargo-deny
- wasm-bundle-check.yml: wasm32 target check

删除:
- build-electron.yml: Electron desktop build (Rust 化后用 openpencil-shell-native)
- ci.yml: TS type-check + Vitest + web build (Rust 化后已废)
- docker.yml: TS Docker image (Rust 化后重做)
- publish-cli.yml: npm packages (Rust 化后改 cargo publish)
2026-05-05 12:23:33 +08:00
Fini 46c853f650 Merge branch 'v0.8.0' of github.com:ZSeven-W/openpencil into v0.8.0 2026-05-05 12:23:28 +08:00
Kayshen-X 9b7d96c60e chore(shell-native): add transient P0 probe gate (Step 1a)
Drives the three-OS CI matrix verification of the skia-safe + glutin +
glow + winit dep stack per Step 1a spec §7.

- examples/p0_probe.rs: stencil_visibility + readback chain runner (must
  own a real OS main thread because winit on macOS rejects
  EventLoop::new() from cargo test worker threads).
- tests/p0_probe.rs: subprocess-invoke wrapper, gated
  #[ignore = "P0_PROBE_GATE"] so default cargo test stays untouched.
- Cargo.toml: add transient [target.'cfg(not(target_arch = "wasm32"))'.
  dev-dependencies] block (skia-safe 0.97 + glutin 0.32.3 + glutin-winit
  0.5.0 + glow 0.17.0 + raw-window-handle 0.6.2 + scopeguard 1.2.0 +
  winit defaults). Pinned to versions resolved in /tmp/skia-glow-probe.
- .github/workflows/rust-check.yml: install Linux GL prereqs (xvfb,
  mesa, libxkbcommon, libwayland) and add a P0-probe-gate step running
  cargo test --ignored on each OS (Linux through xvfb-run; Windows
  early-returns per spec §8.2 WINDOWS_GPU_DEFERRED_NO_RUNNER).

All three artefacts are TRANSIENT — reverted in a follow-up cleanup
commit after CI is green and the loader-compat notes commit lands.
Task 1 owns the permanent integration.
2026-05-05 12:23:03 +08:00
Fini a42b145223 fix(ai): image-proxy timeout covers body read, not just headers
Codex flagged: the previous version cleared the AbortController
timeout in a finally{} block right after \`await fetch()\`, but
fetch() resolves as soon as the response headers arrive — the
body read happened later in the \`reader.read()\` loop with no
timeout protection. An upstream that drip-feeds bytes (or stops
mid-stream) would leave the dev server hanging on
reader.read() forever.

Single AbortController + timeout now covers the entire request
lifecycle (DNS + TLS + headers + body). The clearTimeout moves to
the outer finally{} so it fires regardless of return path
(success, 4xx, 5xx, abort) but never AHEAD of the body read.

Side benefit: AbortError thrown by the controller's timeout (or
by the size-cap controller.abort()) now lands in the catch clause
with a distinguishable error.name === 'AbortError'. Translate it
to a 504 Gateway Timeout when the timeout was the cause, so the
caller can distinguish a slow-upstream from a generic
fetch failure (502).
2026-05-05 12:22:55 +08:00
Fini 434081702c fix(ai): image-proxy caps upstream body size + chunked read
Codex flagged: the previous version did
\`Buffer.from(await upstream.arrayBuffer())\` which buffers the
entire upstream body into memory with no upper bound. Wikimedia
Commons originals can be 100 MB+, and a malicious request could
point at any arbitrarily-large file on an allow-listed host (an
upstream big enough to OOM the dev server is reachable behind
plenty of legitimate-looking URLs).

Hard 16 MiB cap on every proxied response:
- Read upstream's declared Content-Length first; reject (413) if
  it already advertises more than the cap, before reading a single
  byte.
- Stream the body via \`getReader()\`, accumulate in chunks, and
  bail (cancel reader, abort fetch, return 413) the moment total
  bytes cross the cap. Subsequent chunks are never buffered.
- Move the timeout from \`AbortSignal.timeout(15000)\` to a manual
  AbortController so the same controller can also abort on
  size-limit hit.

16 MiB sits well above any reasonable thumbnail and even high-res
4K JPEGs (~5–8 MiB), but well below the territory that risks
heap pressure from a single fetch.
2026-05-05 12:22:54 +08:00
Fini a9f34f8c57 fix(ai): proxy openverse / wikimedia image fetches via local endpoint
Image #40 logs showed three "Failed to load image" errors for
api.openverse.org/...thumb/ URLs even though the search-pipeline
successfully fetched URLs from openverse via the dev server (the
HTTPS_PROXY fix from 3a6f8480 routes server-side fetches through the
local proxy). The browser-side image loader doesn't go through the
same dispatcher: \`new Image(); img.src = url\` does a direct
browser fetch that ignores HTTP_PROXY env vars, so on a machine that
requires the proxy to reach openverse.org the canvas paints the
placeholder visual even though the search-pipeline already found a
valid image URL.

New endpoint \`/api/ai/image-proxy?url=<encoded-url>\`:
- Proxies image bytes through the dev server.
- Reuses \`configureProxyDispatcher\` so the upstream fetch routes
  through HTTPS_PROXY (same path as image-search).
- Allow-lists known image hosts (openverse, wikimedia, flickr's
  static CDN) to prevent the dev server being used as an open
  proxy. Unknown hosts get 403.
- Forwards Content-Type and Cache-Control from upstream.
- Sets Access-Control-Allow-Origin: * so canvas readback works.

\`mapOpenverseResult\` and \`mapWikimediaPages\` now return thumbUrl
wrapped via \`viaImageProxy(externalUrl)\`. The browser fetches
\`/api/ai/image-proxy?url=...\` (same-origin, no proxy needed),
the server fetches the upstream (with proxy), bytes flow back,
the canvas paints the photo. Test expectations updated to assert
the proxy wrapper.

Net effect: with HTTPS_PROXY set, image search end-to-end (search
results found AND images actually load in canvas) works on
proxy-required dev machines. With no proxy env var set
(production / CI), the cascade is still well-behaved — proxy
dispatcher is a no-op, server fetch is direct, no proxy wrapping
is necessary but it doesn't hurt either (the endpoint just adds
a hop).
2026-05-05 12:22:53 +08:00
Fini 40ce4d46eb fix(ai): image-search retries with 2-keyword query when 3-keyword returns 0
Two of five food-app placeholder images shipped unfilled because
Openverse returned `[]` for the model's 3-keyword queries:
  - "burger combo fries"   → 0 results
  - "sakura sushi platter" → 0 results

The same queries truncated to the first two words have plenty:
  - "burger fries"  → 240 results
  - "sushi platter" → 240 results

Openverse uses strict AND-search across all keywords, so a 3-word
query that includes any low-frequency or non-matching token
zero-results even when the photos exist. The skill prompt already
nudges models toward "2-3 English keywords" but they often pick three
when the brief mentions a third descriptor (e.g. "Tasty BURGER COMBO
fries" → "burger combo fries").

Endpoint now cascades:
  1. Openverse with full query.
  2. If `[]` and query has > 2 words: re-query with first 2 words.
  3. If still nothing usable: fall through to Wikimedia (existing path)
     with the same 2-word retry safety net.

Returning the original empty result was wrong: the placeholder stays
unfilled even though a satisfactory photo for "burger fries" was one
keyword-trim away. The trade-off is losing a small amount of relevance
on the dropped 3rd keyword — but that's better than no photo at all,
and the model still drives the first two keywords which carry the
core subject.
2026-05-05 12:22:44 +08:00
Fini 00670404df fix(ai): image-search server fetches honor HTTPS_PROXY env var
Root cause for all-blank-placeholders on the food-app brief: Node's
native fetch (used by the Nitro dev server's image-search endpoint)
ignores the system proxy by default. On machines that route outbound
HTTPS through a local proxy (clash / mihomo / corporate gateway —
mine sits at 127.0.0.1:7897), every Openverse + Wikimedia call from
the server silently ECONNREFUSEDs. The endpoint's catch block returns
`null` for Openverse → falls back to Wikimedia → that ECONNREFUSEDs
too → returns `[]`. Browser shows zero filled images.

Direct curl from the same machine uses HTTPS_PROXY automatically, which
is why a manual API check (e.g. `curl https://api.openverse.org/...`)
returned 240 results for "salmon sushi" while
`/api/ai/image-search?query=salmon%20sushi` returned `{results:[]}`.

`apps/web/server/utils/proxy-dispatcher.ts::configureProxyDispatcher`:
- Reads HTTPS_PROXY / https_proxy / HTTP_PROXY / http_proxy.
- If set, installs `undici.ProxyAgent` as the global fetch dispatcher
  via `setGlobalDispatcher`. From that point on every server-side
  `fetch()` routes through the proxy.
- Idempotent — multiple endpoints can call it without re-installing.
- No-op when no proxy env var is present (production / CI).
- Dynamic `require('undici')` so a build target that strips undici
  doesn't crash at import time.

Wired into `image-search.ts` at module top so the dispatcher is
configured before the first request lands. Other endpoints making
external fetches can opt in with the same single-line call.

Verified standalone via Bun: with the helper in place,
`fetch('https://api.openverse.org/v1/images/?q=salmon+sushi')` returns
240 results. The dev server itself needs a restart to pick up the
server-side change (Vite server-code HMR doesn't re-evaluate Nitro
modules).
2026-05-05 12:22:38 +08:00
Kayshen-X a4b7f62e9a Merge feat/rust-ification into v0.8.0 (Step 0 Rust workspace bootstrap)
Step 0 of OP Rust-ification (per kickoff spec v7 FROZEN):
- Cargo workspace at root (members = ["crates/*"], glob)
- 9 skeleton crates: openpencil-app, openpencil-shell-{core,web,native},
  pen-{types,core,engine,codegen,figma}
- rust-toolchain.toml pinned 1.85 (forced from 1.80 → 1.82 → 1.85
  due to crates.io ecosystem edition2024 requirements)
- deny.toml with kickoff §1.2 wasm32 ban invariant
- 2 GitHub Actions: rust-check.yml (3-platform native + cargo-deny)
  and wasm-bundle-check.yml (wasm32 forward + reverse cargo-deny bans)
- vendor/agent submodule → github.com/ZSeven-W/agent-rs
- Bun script wrappers (cargo:check / :test / :wasm-check / :deny)
- README "Rust subsystem" section + Phase boundary note

§1.2 invariants live:
- Forward wasm32 check: shell-web + 5 bucket A crates compile
- Reverse cargo-deny check bans: native + wasm32 both clean
- compile_error guard: shell-native fails wasm32 build with explicit
  message, validated by canary

Step 1+ owns real implementation; Phase 0 docs (snapshot / plan
patches / IPC inventory / parley-taffy matrix / cargo-deny validation)
in openpencil-docs.
2026-05-04 21:00:00 +08:00
Kayshen-X 4e92f0c250 Merge origin/v0.8.0 into feat/rust-ification 2026-05-03 21:00:00 +08:00
MseeP.ai 112921c9ea Add MseeP.ai badge to README.md (#124) 2026-04-29 09:50:57 +08:00
Fini 238ab344e2 feat(element-tools): add 11 v1 tools with theme parameter (P3 batch 9 — FINAL)
Converts tabs, tag, text_button, textarea, timeline, toolbar, tooltip,
top_nav_bar, upload_dropzone, user_card, video_placeholder to theme-aware v1.
All 9 touchpoints wired; ext-8 extended and new ext-9 shard created for overflow.
ListTools count: 177 → 188. All 4127 tests pass.

Classification:
- Pass-through (all modes identical, no surface colors): text_button, textarea,
  top_nav_bar, tabs (accent brand-invariant), tooltip (dark=inverted per §3.4),
  tag (status tones per §3.4), video_placeholder (dark bg per §3.4)
- Surface-tint (light/dark/system tokenized): timeline (inactive dot+connector+subtitle),
  toolbar (surface+border+active-bg+icon), upload_dropzone (5 tokens),
  user_card (name+role text)
2026-04-29 09:50:29 +08:00
Fini d547e6917b feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 8)
Converts sidebar_nav, skeleton, social_login_row, spinner, stat_card,
stat_grid, status_badge, step_card, stepper, switch to theme-aware v1.
All 9 touchpoints wired; ext-8 shard extended for schema definitions.
ListTools count: 167 → 177. All 4116 tests pass.

Notable: spinner/stat_grid/status_badge/switch emit identical trees
across all theme modes (caller-param colors, status semantics, or iOS
HIG builder-private literals per spec §3.4) — theme param accepted
for API consistency only.
2026-04-29 09:50:28 +08:00
Fini e1a931e4ff feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 7)
Converts progress_bar, quote_block, radio, range_slider, rating_stars,
search_bar, section_header, segmented_control, select, share_row to
theme-aware v1. All 9 touchpoints wired; adds ext-8 shard for schema
definitions. ListTools count: 157 → 167. All 4106 tests pass.
2026-04-29 09:50:27 +08:00
Fini ab695d9633 feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 6)
Converts metric_comparison, metric_row, nav_chip_row, notification_row,
otp_input, pagination, phone_input, price, pricing_card, profile_header
to theme-aware v1. All 9 touchpoints wired; adds ext-7 shard for schema
definitions. ListTools count: 147 → 157. All 4096 tests pass.
2026-04-29 09:50:26 +08:00
Fini 028ef9048b feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 5)
Adds theme-aware v1 builders for icon_button, image_placeholder,
inbox_message, inline_action, input_with_action, invite_row, kbd,
legend_item, link, and list_row. Group A (zero-color: icon_button,
link, list_row) — no hardcoded colors in v0, all three modes identical.
Group B (kbd) — key bg → surface2, stroke → border in dark/system.
Group C (remaining 6) — surface/text/border/accent/alertColors tokens
applied in dark/system modes, full byte-parity with v0 in light mode.

Extends ext-6 shard (357→647 lines, within 800-line ceiling) housing
all 20 batch-4 + batch-5 tool schema definitions. All 9 touchpoints
wired per playbook: builder, index.ts, pen-core barrel, handler,
dispatcher, ext-6 shard, client shim, server builder, elements.md entries.

Verified: format:check clean, tsc --noEmit clean, 4086/4086 tests pass.
2026-04-29 09:50:25 +08:00
Fini f4a38dc508 feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 4)
Adds theme-aware v1 builders for cookie_banner, data_table_row,
date_picker, drawer_shell, empty_state, event_card, fab, faq_item,
filter_group, and form_field. Group A (zero-color: empty_state,
form_field) — no hardcoded colors in v0, all three modes identical.
Group B (fab) — accent bg is brand-invariant, maps to accent token
in dark/system; icon stays white in all modes. Group C (remaining 7)
— surface/text/border/accent tokens applied in dark/system modes, full
byte-parity with v0 in light mode.

Creates ext-6 shard (ext-5 was at 798-line ceiling) housing all 10
new tool schema definitions (357 lines). All 9 touchpoints wired per
playbook: builder, index.ts, pen-core barrel, handler, dispatcher,
ext-6 shard, client shim, server builder, elements.md entries.

Verified: format:check clean, tsc --noEmit clean, 4076/4076 tests pass.
2026-04-29 09:50:24 +08:00
Fini 957f845bf5 feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 3)
Adds theme-aware v1 builders for chart_bars, chart_line, chart_pie,
chat_bubble, checkbox, chip_input, code_block, color_swatch, combobox,
and comment. Group A (chart tools) maps bar/line color to chart-1 token
and pie default palette to chart-1..6 tokens in dark/system modes.
Group B (color_swatch) is theme-invariant — swatch color is caller-
supplied and passes through unchanged. Group C (chat_bubble, checkbox,
chip_input, code_block, combobox, comment) resolves surface/text/border
via semantic palette tokens. All light modes are byte-parity with v0.
2026-04-29 09:50:23 +08:00
Fini c0717d43c4 feat(element-tools): add 10 v1 tools with theme parameter (P3 batch 2)
Adds theme-aware v1 builders for alert, bottom_nav, breadcrumb,
activity_ring, carousel_dots, action_menu, attachment_row,
calendar_grid, avatar_group, and callout. Group A (zero-color:
alert/bottom_nav/breadcrumb/activity_ring) produce identical output
across all three theme modes. Group B (carousel_dots) maps active=
text-primary, inactive=border in dark/system modes. Group C (action_menu/
attachment_row/calendar_grid/avatar_group) resolve surface/text/border via
semantic palette. Group D (callout) maps tone-keyed bg/fg to alert palette
tokens in dark/system modes. All light modes are byte-parity with v0.
2026-04-29 09:50:22 +08:00
Fini ddfb3fa81b feat(element-tools): add 5 atom v1 tools with theme parameter (P3 batch 1)
avatar-v1, badge-v1, divider-v1, body_text-v1, icon_label-v1 — each with
full 9-touchpoint coverage (pen-core builder + index + pen-mcp handler +
schema shard + dispatcher + apps/web shim + SERVER_BUILDERS + parity test
+ elements.md). Light mode is byte-equal to v0; dark/system modes produce
identical output since all 5 tools emit zero hardcoded color fills — theme
param accepted for API consistency across all v1 tools. New shard
element-tool-defs-ext-5.ts created (ext-4 was at 739 lines). All 2026
pen-core + pen-mcp tests pass; format:check + tsc clean.
2026-04-29 09:50:21 +08:00
Fini 4962d6176e feat(element-tools): add 4 representative v1 tools (Task 2.4)
card_row-v1, setting_row-v1, member_row-v1, activity_log-v1 — each with
full 9-touchpoint coverage (pen-core builder + index + pen-mcp handler +
schema shard + dispatcher + apps/web shim + SERVER_BUILDERS + parity test
+ elements.md). Light mode is byte-equal to v0; dark/system use resolveTheme()
for all color fills. activity_log-v1 maps tone×theme to alertColors tokens
(info/success/warning/danger) with neutral falling back to surface/textMuted.
All 3998 tests pass. Completes P2 representative phase.
2026-04-29 09:50:20 +08:00
Fini 6a74933945 feat(element-tools): add heading-v1 with theme parameter (9 touchpoints)
Task 2.3 — representative v1 tool walkthrough for Plan 14 byte-parity contract.
Light mode is byte-equal to add_heading_v0 (V0_LATIN_PRESETS table reused);
dark/system modes use resolveTheme() for fill color and typography token refs.
Adds theme enum [light, dark, system] to add_heading_v1 MCP schema, elements.md
decision tree, shim-server-parity CASES, and SERVER_BUILDERS. All 3937 tests pass.
2026-04-29 09:50:19 +08:00
Fini 1fee613111 feat(ai): ship 5 element tools to reach 97 (filter_group / invite_row / activity_log / event_card / step_card)
Closes the obvious gaps remaining in the family:
- add_filter_group_v0 — sidebar facet (heading + checkbox-style options
  with optional counts). Distinct from nav_chip_row (horizontal scrolling
  chips), tag (single applied chip), segmented_control (mutex tabs).
- add_invite_row_v0 — pending invite row (avatar + email/role + status
  pill + trailing action). Distinct from member_row (a JOINED member,
  no status pill or action) and list_row (no avatar / status / action).
- add_activity_log_v0 — single-line audit feed entry (optional tinted
  icon dot + actor in bold + action + right-aligned timestamp). Uses
  StyledTextSegment[] content for the bold/regular split. Distinct from
  timeline (multi-event vertical with connectors) and notification_row
  (title + body, no actor focus).
- add_event_card_v0 — single calendar event tile (date column with
  month band + day number, then title + time + location). Distinct from
  calendar_grid (the full month grid) and card_row (no date column).
- add_step_card_v0 — onboarding step card (numbered circle / check +
  title + description). Distinct from stepper (horizontal progress nav
  with connectors) and faq_item (collapsible Q&A header).

9 touchpoints per tool: pen-core builder + index + barrel + types,
pen-mcp handler + dispatcher + ext-4 schema, apps/web shim +
SERVER_BUILDERS, parity test (+5 cases), elements.md decision tree
items 86-89 + 6 PREFER mappings with cross-links to existing tools,
elements-cookbook.md arg-shape examples (8 entries across 5 tools).
2026-04-28 08:50:00 +08:00
Fini a5b594cf69 feat(ai): add_member_row_v0 — team / member list row (92nd tool)
Avatar + (name over optional subtitle) + optional trailing slot
(role badge / kebab menu / status dot). Distinct from
add_user_card_v0 (compact fit_content tile, no trailing slot) and
add_list_row_v0 (no avatar slot — leading icon instead).

9 touchpoints wired: pen-core builder + index + barrel + types,
pen-mcp handler + dispatcher + ext-4 schema, apps/web shim +
SERVER_BUILDERS, parity test, elements.md decision tree #84 +
PREFER mapping, cookbook arg shapes (3 variants).

Also disambiguates add_avatar_group_v0's PREFER mapping: drop
"团队成员" (now points at member_row), keep narrower phrases like
"成员头像" / "团队头像" / "presence indicator" that genuinely match
the stacked-avatars affordance, and add the cross-link to member_row.
2026-04-28 08:05:00 +08:00
Fini af1ddd1ad2 feat(ai): add_setting_row_v0 — settings menu row (91st tool)
Leading icon + (title over optional subtitle) + trailing control with
4 variants: chevron / value text / switch / badge. Distinct from
add_list_row_v0 (trailing is always icon, no switch/value/badge) and
add_form_field_v0 (label-above-input for forms).

Wires all 9 touchpoints: pen-core builder + index + barrel re-export,
pen-mcp handler + dispatcher case + ext-4 schema, apps/web shim +
Nitro SERVER_BUILDERS, elements.md decision tree #83 + PREFER mapping,
elements-cookbook arg-shape examples, plus shim-server parity case.
2026-04-28 06:30:00 +08:00
Fini 5859c3f9af feat(ai): ship 10 element tools to reach 90 (user_card / drawer / combobox / toolbar / callout / share / inline_action / legend_item / inbox / profile_header)
Adds the desktop-leaning batch needed to round the family to 90:

- add_user_card_v0 — compact avatar+name+role row
- add_drawer_shell_v0 — full-height side panel header
- add_combobox_v0 — open-state autocomplete with dropdown
- add_toolbar_v0 — desktop icon button row + dividers
- add_callout_v0 — inline doc tip block, 5 tones
- add_share_row_v0 — circular social-share buttons
- add_inline_action_v0 — message + Undo-style action
- add_legend_item_v0 — chart legend marker+label+value
- add_inbox_message_v0 — email/inbox row with unread dot
- add_profile_header_v0 — large profile hero block

All ten go through the standard 9-touchpoint wiring and land in a
new ext-4 schema shard so existing shards stay under 800 lines.
Drift guards (contract / registry parity / shim-server parity) cover
each new name; per-tool handler tests are deferred — every tool's
structure is exercised through the parity build call already.
2026-04-27 09:05:00 +08:00
Fini 072856e522 feat(ai): add_tag_v0 — single closable filter chip (80th tool) 2026-04-27 08:55:00 +08:00
Fini d24b587b6c feat(ai): add_data_table_row_v0 — desktop tabular row (79th tool) 2026-04-27 08:40:00 +08:00
Fini e225104e43 feat(ai): add_avatar_group_v0 — stacked presence tile group (78th tool) 2026-04-27 08:30:00 +08:00
Fini 6e54054c73 chore(merge): integrate origin/v0.8.0 — main pre-release sync + CI fixes
origin's v0.8.0 had cherry-picks of the v0.7.5 deepseek/image-search
fixes (a727632a, a5952bc8) overlapping local 2073cf5b / 04f4fbc1, plus
new commits (model-selector ark-coding deepseek-v4-pro/flash IDs that
ARK rejects, fetch error.cause unwrap, CI agent-native build, op
export docs cleanup, main merge). Resolved the ark-coding list in
favor of HEAD's deepseek-v3.2 entry (only model ARK Coding Plan
actually supports — see openpencil-docs note).
2026-04-27 08:15:00 +08:00
Kayshen-X b554b4f1a6 Merge branch 'main' of github.com:ZSeven-W/openpencil into v0.8.0 2026-04-26 19:39:14 +08:00
Kayshen-X 4ed1203bf1 fix(ai): unwrap fetch error.cause for actionable network failures
Custom OpenAI-compatible providers surfaced Node's opaque
`TypeError: fetch failed` whenever the upstream HTTP call failed
(#121) — DNS, TLS handshake, connection refused, timeout — all
collapsed to the same useless string. The actual reason was already
on `error.cause` as a SystemError but never reached the user.

Add `formatFetchError()` that walks the cause chain (including
AggregateError emitted when undici tries multiple A records and each
attempt fails) and prefixes the SystemError code so users see
`ENOTFOUND: getaddrinfo ENOTFOUND api.foo.com` or
`ECONNREFUSED: connect ECONNREFUSED 127.0.0.1:443` instead of
`fetch failed`. Wire it into the model-list proxy (most common
trigger from the AI Settings dialog) and the builtin chat stream.

Closes #121
2026-04-26 19:20:32 +08:00
Fini 829772f60e fix(ai): swallow image-search network failures into the existing fallback
`fetchFromOpenverse` and `fetchFromWikimedia` were missing try/catch,
so a ConnectTimeoutError on `api.openverse.org` (frequent on networks
that can't reach Openverse) bubbled up to nitro's default handler and
turned a single image-search lookup into a HTTP 500 for the whole
design generation flow. The handler already treats `null` (Openverse)
and `[]` (Wikimedia) as the documented fallback signals — wrap the
fetches and return those on any throw, plus an explicit 8s
AbortSignal.timeout so the wait is bounded.
2026-04-26 07:30:00 +08:00
Fini abbccc1ba2 fix(ai): refresh DeepSeek defaults to v4 model series
`/models` now returns only deepseek-v4-pro and deepseek-v4-flash;
deepseek-chat / deepseek-reasoner sunset 2026-07-24 and the
deepseek-v3.2 hard-coded in the ark-coding fallback list never
existed. Both v4 models default to thinking enabled and the API
toggles via `{"thinking":{"type":"disabled"}}` — keep
`thinkingMode: 'disabled'` so the app's fast/non-thinking default
stays intact (server reasoning paths honor it; the Zig openai-compat
path doesn't emit the toggle yet, so calls through that path still
get provider-default thinking until it's wired). v4-pro promoted to
full tier; legacy aliases pinned to an exact RegExp so future
deepseek-* variants don't inherit a forced disabled mode.

Bandaid for the unwired toggle: v4-pro gets `timeoutMultiplier: 2`
because its default-on reasoning blows past the orchestrator's
planning timeout on long system prompts (observed in dev: planning
phase falls back, sub-agent then succeeds — UX degraded but
functional). Drop the multiplier once the Zig path actually sends
`thinking:{type:disabled}`.

Don't add a BUILTIN_MODEL_LISTS.deepseek entry — DeepSeek exposes
/v1/models, so let `fetchProviderModels` pull the live catalog
through `/api/ai/provider-models` instead of pinning a snapshot
(the ark-coding `deepseek-v3.2` ghost above shows what those
snapshots drift into).
2026-04-26 06:30:00 +08:00