Commit graph

56 commits

Author SHA1 Message Date
Fini 2aabe494f5 fix(core): design.md lives on PenDocument — kill cross-document leak
design.md was stored in a global Zustand store + per-file-key localStorage
in apps/web, and in a module-level cache in pen-mcp. Both leaked across
files: a newly-created document could pick up the previous file's dark
palette (async clearForNewDocument raced with AI chat reads; hydrate()
could rehydrate the last file's designMd on refresh; shared .pen files
lost the spec entirely because it wasn't inside the document).

Fix:
- Add `designMd?: DesignMdSpec` to PenDocument (pen-types). It now
  serializes with .pen/.op and travels across sessions/users.
- Add `setDesignMd` action to document-store.
- Rewrite design-md-store as a thin mirror over document-store so the
  legacy hook API still works. On document load it migrates any legacy
  localStorage entry into the opened document and deletes the localStorage
  key; hydrate() wipes the orphan `openpencil-design-md-current-key`.
- MCP handleGetDesignMd / handleSetDesignMd / handleExportDesignMd read
  `doc.designMd` directly and persist via saveDocument. Removed the
  process-level `_mcpDesignMd` cache.

Verified via MCP live round-trip: set on file A → persists to A's .op on
disk → new file B returns hasDesignMd:false (no leak).
2026-04-20 00:37:31 +08:00
Fini bc7e16fa20 fix(mcp): element tools with colored bg set foreground for readable contrast
pen-core's DEFAULT_FILL is gray-300 (#d1d5db) — any text/icon without
explicit fill renders light gray, unreadable on primary blue / dark /
white backgrounds. Set explicit foreground colors on toast text + icon,
fab icon, stepper step numbers, checkbox check, and segmented-control
labels. Added contrast-regression test to lock the invariant.
2026-04-20 00:02:59 +08:00
Fini 039fb89251 feat(mcp): add alert + toast + progress_bar + fab + breadcrumb + stepper (25 → 31 element tools)
Feedback + floating + nav batch. progress_bar uses fixed bar_width so the
fill can be derived from value/100 (pen-core has no percent sizing).
Stepper connectors use fill_container so the bar splits evenly between
circles.
2026-04-19 23:55:52 +08:00
Fini fc64af5609 fix(mcp): add_tabs_v0 tabs split bar evenly (width=fill_container)
Layout-engine trap: a fill_container child inside a fit_content parent
resolves to the grandparent's width (pen-core engine.ts:182-187), so the
active tab's underline rect blew the whole tab up to bar width. Switched
every tab to width=fill_container (Twitter/Material pattern) so the bar
splits evenly and the underline sits correctly inside its slot.
2026-04-19 23:25:12 +08:00
Fini 477c0b2377 fix(mcp): add_tabs_v0 underline uses sibling rectangle (directional stroke unsupported)
PenStroke.thickness only supports number | [T,R,B,L] — {bottom:N} silently
resolves to 0 in resolveStrokeWidth so the old tab underline never rendered.
Switched active tab to a vertical frame with a sibling rectangle underline
(role=tab-underline), matching how add_divider_v0 already handles this.
2026-04-19 23:17:28 +08:00
Fini 36c46a50ef feat(mcp): add switch + checkbox + radio + tabs + segmented + empty_state (19 → 25 element tools)
Controls + empty state batch. Schemas split into element-tool-defs-ext.ts
to keep the main route file under the 800-line limit as the family grows.
2026-04-19 23:08:06 +08:00
Fini c12390c98d feat(mcp): add search_bar + form_field (17 → 19 element tools)
Forms coverage. search_bar fixes 44/22 hit-target; form_field enforces
fill_container input + 48px height from design-guidelines ROLE_GUIDE.
2026-04-19 22:52:50 +08:00
Fini a59189303b refactor(mcp): split element tool defs out of design-routes (848 → 201+671)
Codex stop-hook: design-routes.ts reached 849 lines, violating the
repo's CLAUDE.md "Single files must not exceed 800 lines" rule.

Extract the 17 element-tool JSON schema definitions + names + dispatch
switch into a new file `routes/element-tool-defs.ts` (671 lines). The
core design-routes.ts keeps only:
  - 2 core tool defs (get_design_prompt, batch_design)
  - LAYERED_DESIGN_TOOLS spread
  - D0 spike tool def + dispatch (gated)
  - Combined DESIGN_TOOL_DEFINITIONS / DESIGN_TOOL_NAMES / handleDesignToolCall
    that merges core + element-tool via re-export

design-routes.ts: 849 → 201 lines
element-tool-defs.ts: 0 → 671 lines (both under the 800 cap; room to
add ~3-5 more element tools before element-tool-defs itself needs
splitting by category — e.g. atom-tool-defs vs row-tool-defs)

handleDesignToolCall falls through via `if (ELEMENT_TOOL_NAMES.has(name))
return handleElementToolCall(name, a)` instead of an inlined 17-case
switch. Same dispatch semantics, much shorter file.

DESIGN_TOOL_NAMES kept as a single exported Set so existing callers
(server.ts, test files) still see all 22 tool names (5 core + 17 element)
via one import. ELEMENT_TOOL_NAMES also exported for tests that want to
assert the split explicitly.

180/180 pen-mcp tests pass unchanged. format + tsc green. Bundle
rebuilt.
2026-04-19 22:42:38 +08:00
Fini fa8fb393ca feat(mcp): add icon_label + list_row (15 → 17 element tools)
Two composition primitives completing the "atoms + composition" tier.

- add_icon_label_v0: atomic icon + text horizontal pair (alignItems=
  center, gap=8, fit_content). Building block for menu items,
  breadcrumbs, status indicators. Narrow schema: icon always leads,
  sizes fixed (icon 16, text 14/500), no alignment enum.

- add_list_row_v0: iOS/Material list row — optional leading icon +
  vertical text stack (title + optional subtitle) + optional trailing
  icon (typically chevron-right).
  No-overlap invariant: middle text stack wrapped in VERTICAL
  container with width=fill_container so long titles wrap vertically
  instead of pushing the trailing icon out of frame — same pattern
  as add_section_header_v0. overflow.md rule: text with
  fill_container + fixed-width only propagates wrap height inside
  vertical-layout parents. The vertical wrapper is what prevents the
  overlap.

Tests: 11 new unit (5 icon-label + 6 list-row). List-row includes
an explicit no-overlap regression test asserting the text stack is
vertical + fill_container. contract test ELEMENT_TOOL_NAMES updated
15 → 17.

elements.md skill gets a new "Composition" category in the decision
tree (items 16-17) + 2 new PREFER phrases + 2 usage examples + role
list extended. d0 snapshot updated.

MCP live smoke: ListTools = 57 (40 baseline + 17 element);
icon_label produces 3-node tree; list_row full variant produces
6-node tree with text stack correctly vertical + fill_container.

180/180 pen-mcp tests pass. format + tsc green. Bundle rebuilt.
2026-04-19 22:32:02 +08:00
Fini b5dd3d52b2 fix(ai): cjk-typography.md body rule aligns with text-rules/tool/skill
Codex stop-hook #17: after fix #16 made add_body_text_v0 always use
Inter for CJK body, one source still allowed the alternative:
cjk-typography.md:16 said "Body: 'Inter' (system CJK fallback) or
'Noto Sans SC'". Every other authority in the repo says body=Inter
unconditionally:

  - text-rules.md (text section of get_design_prompt): body='Inter'
  - skills/phases/planning/decomposition.md:45: "body='Inter'"
  - packages/pen-mcp/src/tools/add-body-text-v0.ts: always 'Inter'
  - skills/phases/generation/elements.md: "Inter everywhere"
  - role-definitions.md:88: "body-text: lineHeight=1.5 (CJK: 1.6)"
    (no font override)

cjk-typography's "or Noto Sans SC" was the lone dissenter — an AI
reading the domain skill would see a contradictory option that no
other skill or tool supports. Remove the alternative so the repo is
single-voiced.

Also clarify the heading vs body split in the last two bullets: the
script-specific Noto rule is HEADING-only; body is Inter + CJK
lineHeight/letterSpacing. Cross-reference the other authorities so
a future editor knows which rule sources must stay in sync.

253/253 tests pass (pen-mcp + pen-ai-skills). format green.
2026-04-19 20:55:05 +08:00
Fini f7ce47fa0d fix(mcp): add_body_text_v0 uses Inter for ALL scripts (end CJK-rule conflict)
Codex stop-hook #16: CJK guidance was internally contradictory across
repo skills and the handler. Three sources disagreed:

  - text-rules.md (design-prompt TEXT_RULES): "body='Inter'" — unqualified
  - cjk-typography.md: body is "'Inter' (system CJK fallback) OR
    'Noto Sans SC'" — permissive
  - My previous add_body_text_v0 (e24c7fc): body was mapped per-script
    to Noto Sans SC / JP / KR — this combination is NOT authorized
    by any repo skill (cjk-typography only allows Inter or SC; never
    lists JP/KR for body)

Authoritative rule: text-rules.md. Body is Inter regardless of
script. Inter has system CJK fallback at render time so a single
body face serves all scripts. ONLY headings dispatch to
script-specific Noto faces (Noto Sans SC for Chinese / JP for
Japanese / KR for Korean) — that's add_heading_v0's job; body
doesn't need the same split.

Fix:
- add_body_text_v0 handler: fontFamily always 'Inter'; script
  detection is now used ONLY to decide lineHeight (1.5 Latin /
  1.6 CJK) + letterSpacing (undefined Latin / 0 CJK)
- tool description: rewritten to explicitly note "body ALWAYS Inter"
  and "only headings dispatch to Noto faces"
- elements.md usage examples: all 4 body examples now show Inter
  output with only lineHeight varying by script; added inline
  comment clarifying the text-rules.md derivation
- elements.md decision-tree item 15: reworded to "Inter everywhere"
- add-body-text-v0.test.ts: per-script tests now assert fontFamily
  ===Inter across zh/jp/ko + kanji+hiragana + mixed content
- d0 parity snapshot regenerated (description changes reach
  pre-D0 definitions; snapshot works as designed, catches drift
  and is updated deliberately)

169/169 pen-mcp tests pass. format + tsc green. Bundle rebuilt.
2026-04-19 20:48:27 +08:00
Fini 97388716c1 fix(ai): AI-facing descriptions reflect per-script CJK font dispatch
Codex stop-hook #15: previous fix (91b9054) updated the handlers to
dispatch by script (Chinese → SC / Japanese → JP / Korean → KR),
but four AI-consumable surfaces still said "all CJK → Noto Sans SC":

- add_body_text_v0 tool description: "Chinese/Japanese/Korean content
  gets fontFamily=Noto Sans SC + lineHeight=1.6"
- add_heading_v0 tool description: listed only Latin presets; no
  mention of CJK handling at all (silently stale)
- elements.md skill usage example: "// auto Noto Sans SC + 1.6" for a
  Chinese string but no example showing JP/KR getting their own faces
- elements.md decision tree line 14-15: just said "CJK detection
  (correct fontFamily)" — generic enough that drift was invisible

Fix:
- body_text description rewritten to enumerate the 4-way script
  dispatch explicitly (SC/JP/KR/Inter), note JP precedence for
  hiragana/katakana + kanji mixes, and document the letterSpacing=0
  invariant
- heading description expanded to list both Latin and CJK preset
  tables plus the script-specific font mapping with the explicit
  "NEVER use SC for JP/KR" admonition lifted from text-rules.md
- elements.md gets 2 new usage examples (JP, KR) showing the
  different Noto face selection so the AI reading the skill sees
  non-SC CJK fonts in practice, not just in prose
- decision-tree lines for heading/body now name SC/JP/KR explicitly
- d0-parity-spike snapshot regenerated (get_design_prompt enum + tool
  definitions have changed descriptions)

169/169 pen-mcp tests pass. format + tsc green. Bundle rebuilt.
2026-04-19 20:36:16 +08:00
Fini 620ffe57e3 fix(mcp): script-specific CJK fonts for heading + body_text (JP/KR)
Codex stop-hook #14: previous fix (8ca2cb9) mapped ALL CJK content to
'Noto Sans SC'. text-rules.md spec (and memory
project_pencil_optimization) requires script-specific fonts:

  Chinese  → Noto Sans SC
  Japanese → Noto Sans JP
  Korean   → Noto Sans KR

Using SC for JP/KR has reasonable Unicode coverage but violates the
explicit font contract. Each Noto face ships with script-native
punctuation + glyph variants that the dedicated face renders
correctly.

Fix: extract script detection + font mapping into shared helpers in
element-tool-helpers.ts so add_heading_v0 and add_body_text_v0 stay
in sync:

  detectCjkScript(s): 'chinese' | 'japanese' | 'korean' | null
    Detection order:
      1. Hiragana (U+3040-309F) / Katakana (U+30A0-30FF) → Japanese
         (these scripts are UNIQUE to Japanese even when mixed with
         Han ideographs — a heading like "今日は" is Japanese despite
         having kanji, because hiragana "は" disambiguates)
      2. Hangul Syllables (U+AC00-D7AF) → Korean
      3. CJK Unified Ideographs / Symbols → Chinese (Simplified default)
      4. Otherwise null

  cjkFontFamily(script): 'Noto Sans SC'|'Noto Sans JP'|'Noto Sans KR'|undefined

Apply to both add_heading_v0 (CJK_BASE preset table + per-script
fontFamily injection) and add_body_text_v0 (fontFamily = cjkFont ??
'Inter'; letterSpacing = 0 only when CJK).

Tests: 3 new per-script font assertions in each tool's test file
(Japanese → Noto Sans JP, Korean → Noto Sans KR, plus the kanji
+hiragana disambiguation edge case). 169/169 pen-mcp suite pass.
format + tsc green. Bundle rebuilt.
2026-04-19 20:32:18 +08:00
Fini 303c2a3b5a fix(mcp): add_heading_v0 CJK content gets CJK-specific typography
Codex stop-hook #13: heading presets hardcoded Latin typography
(lineHeight 1.0/1.1/1.2/1.25, display letterSpacing -0.5) and violated
three documented CJK rules when called with Chinese/Japanese/Korean
content:

  1. memory project_pencil_optimization: "CJK headings 1.3-1.4 (NOT
     1.1-1.2 like Latin)"
  2. text-rules.md: "CJK letterSpacing: 0, NEVER negative. Negative
     letterSpacing causes CJK character overlap."
  3. text-rules.md: "CJK font selection: heading=Noto Sans SC /
     Noto Sans JP / Noto Sans KR. NEVER Space Grotesk or Manrope —
     they have no CJK glyphs."

Fix: same auto-CJK detection used by add_body_text_v0 (regex scan for
\u3000-\u303f \u3040-\u309f \u30a0-\u30ff \u4e00-\u9fff \uac00-\ud7af).
CJK content selects a separate preset table:
  display 48/700/1.3 Noto Sans SC  (was 48/700/1.0/-0.5 Latin)
  h1      32/700/1.3 Noto Sans SC  (was 32/700/1.1)
  h2      24/600/1.35 Noto Sans SC (was 24/600/1.2)
  h3      20/600/1.4 Noto Sans SC  (was 20/600/1.25)
All CJK presets drop letterSpacing entirely (never negative, never
overridden from theme). Latin presets unchanged.

Tests: 6 new CJK cases (zh/jp/ko/mixed/Latin-still-works +
per-level lineHeight verification). Loop test uses unique filenames
per iteration to sidestep openDocument cache reuse.

13/13 heading tests pass. 160/160 pen-mcp overall. format + tsc green.
Bundle rebuilt.
2026-04-19 20:26:34 +08:00
Fini 78a8a8f81e feat(mcp): add text_button + heading + body_text (12 → 15 element tools)
Three text-primitive tools, each encoding a documented Pencil-demo or
memory-noted non-Claude failure mode.

- add_text_button_v0: padding-based button (padding=[12,20],
  cornerRadius=8, fit_content × 2, horizontal, centered). Pencil demo
  pattern — height auto-derives from padding, no explicit height.
  Optional leading icon at 16px. Narrow: single md preset (no size
  enum). Label + optional icon = 1-2 children.

- add_heading_v0: typographic heading with 4-level preset enum
  (display / h1 / h2 / h3; default h2). Each preset fixes fontSize
  / fontWeight / lineHeight / optional letterSpacing per memory
  data (display=48/700/1.0/-0.5, h1=32/700/1.1, h2=24/600/1.2,
  h3=20/600/1.25). Single text node output — the enum only changes
  typography, not structure ("应拆尽拆" compliant). Prevents the
  "default 1.5 lineHeight makes multi-word headings stack tight"
  failure mode.

- add_body_text_v0: body text with AUTO CJK detection via regex
  scan (/[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\u4e00-\u9fff
  \uac00-\ud7af]/). CJK → fontFamily='Noto Sans SC' + lineHeight 1.6
  + letterSpacing 0 (memory: NEVER Space Grotesk/Manrope for CJK).
  Latin → Inter + 1.5 + no letterSpacing override. Always sets
  width=fill_container + textGrowth=fixed-width (intended for
  vertical-layout parents per the documented rule). Mixed
  content triggers CJK.

Tests: 18 new unit (4 + 7 + 7). CJK detection tested across Chinese /
Japanese / Korean / mixed. 160/160 pen-mcp suite green.

Contract test's ELEMENT_TOOL_NAMES updated 12 → 15. elements.md skill
gets a new "Text + button primitives" category in the decision tree
(items 13-15) + PREFER phrases + usage examples; regression test
dynamically derives expected names from registry so no stale-drift.

MCP live smoke (/tmp/claude/mcp-test-phase-1c-tools.ts): ListTools =
55, element tools = 15; display heading gets correct typography
preset; Latin body gets Inter+1.5; CJK body gets Noto Sans SC+1.6+0
letterSpacing.

format + tsc green. Bundle rebuilt.
2026-04-19 20:17:16 +08:00
Fini b3a84525b5 fix(ai): purge hardcoded element-tool counts from description + test
Codex stop-hook #12: previous fix (e05ca79) introduced its own rot —

- Test "names EVERY production element tool" asserted
  elementTools.length >= 12. A hardcoded count is exactly what the
  whole regression suite is trying to prevent. If someone removes a
  tool, the count drops to 11 and the test keeps passing (because
  >=12 is a floor, not a target).

- get_design_prompt description said "12 tools covering rows[..]/
  containers[..]/atoms[..]". "12 tools" is stale the moment we add
  or remove a tool. The category names (rows/containers/atoms) are
  also hardcoded — if we ship a new category, the description is
  misleading.

Fix:
- Test: replace `>=12` with `>0`. Assertion is now: "registry has
  at least one element tool AND every element tool is mentioned in
  the elements skill." No hardcoded count.
- Description: replace the "12 tools covering [..]" enumeration
  with "N-tool element-tool family reference — decision tree,
  PREFER/FALLBACK rules, composition pattern; the section itself
  enumerates the current tools." Specific names + counts live in
  elements.md skill where the regression test keeps them in sync
  with the registry.
- Add regression test asserting the description does NOT match
  /\d+\s+tools?\s+(covering|in|across)/i — catches any future
  reintroduction of a hardcoded count.

d0 snapshot updated to reflect the new description.

142/142 pen-mcp tests pass. format + tsc green. Bundle rebuilt.
2026-04-19 20:09:03 +08:00
Fini 12761e743e fix(ai): refresh AI-facing integration for all 12 element tools
Codex stop-hook #11: the last 3 tools (divider / badge / avatar) were
registered in MCP + tested in unit tests, but the AI-facing integration
layer was stale:

- elements.md skill still listed only 9 tools (missing divider / badge
  / avatar from decision tree, PREFER phrases, usage examples, and
  role registry)
- get_design_prompt tool description still enumerated the ORIGINAL 5
  element tools ("add_card_row_v0 / add_metric_row_v0 / …")
- Both are read directly by external MCP clients (Claude Code / Codex
  / Gemini CLI / Cursor) to decide which tool to pick — stale content
  means the AI never learns the newer tools exist.

Fix:
- elements.md: add divider / badge / avatar to decision tree (new
  "Atoms" category after rows + containers), PREFER phrases list,
  usage example block, role guarantee list. Frontmatter description
  updated to "12 tools" with category breakdown. Budget bumped
  1500 → 1800 tokens to accommodate the 3 new sections.
- design-routes.ts: get_design_prompt description rewritten to
  describe the element-tool family as "12 tools covering rows
  [card/metric/nav_chip/stat_grid], containers [bottom_nav/
  top_nav_bar/section_header/icon_button/activity_ring], atoms
  [divider/badge/avatar]" — generic categorization plus named
  examples, not a stale list that rots on every addition.
- d0-parity-spike snapshot updated (get_design_prompt definition
  changed — intentional).

Regression tests to prevent future drift:
- "names EVERY production element tool": derives the expected list
  dynamically from DESIGN_TOOL_DEFINITIONS, so any future element
  tool added without updating elements.md trips the test. Was
  previously hard-coded to 5 tool names.
- "description has no stale element-tool references": any
  `add_*_v0` name appearing in get_design_prompt's description
  must correspond to an actually-registered tool.

142/142 pen-mcp tests pass (was 141; +1 stale-guard for description).
format + tsc green. Bundle rebuilt.
2026-04-19 20:03:24 +08:00
Fini ae00fd6e87 feat(mcp): add divider + badge + avatar element tools (9 → 12)
Three low-risk single-/double-node tools completing the first
"应拆尽拆" batch. ListTools now 52 (40 baseline + 12 element).

- add_divider_v0: hairline rectangle (horizontal default: fill_container
  width, height=1; vertical swaps axes). Memory-documented pattern
  (Pencil reverse engineering): "Dividers: rectangle(h=1,
  fill_container) or directional stroke". Ships colorless.

- add_badge_v0: short pill / tag (cornerRadius=999, padding=[4,10],
  font 11/600). Forces the documented constraint (overflow.md):
  CJK ≤8 chars / Latin ≤16 chars — longer labels should not be badges.

- add_avatar_v0: circular avatar with optional centered initial. Same
  frame+cornerRadius=size/2+flex-centering pattern as activity_ring —
  NEVER the ellipse+sibling text anti-pattern (layout.md §RING /
  CIRCLE WITH CENTER CONTENT). Initial font auto-scales (size × 0.4,
  floored at 12 for tiny avatars).

Tests: 16 new unit (6 divider + 4 badge + 6 avatar). contract test
updated from 9 → 12 tool names. 141/141 pen-mcp suite passes.

MCP live smoke (/tmp/claude/mcp-test-phase-1b-tools.ts): ListTools
= 52, all 3 new tools callable with expected node counts
(divider=1, badge=2, avatar-with-initial=2), avatar cornerRadius
verified as size/2.

format + tsc green. Bundle rebuilt.
2026-04-19 19:51:05 +08:00
Fini 3b677b9f3b fix(mcp): wrap section header title in vertical container for correct wrap height
Codex stop-hook #10: previous fix (72087f8) used
width=fill_container + textGrowth=fixed-width on the title text but
placed it DIRECTLY inside the horizontal section-header frame. Per
packages/pen-ai-skills/skills/phases/generation/overflow.md:
"Text in VERTICAL layout: width=fill_container + textGrowth=fixed-width.
In horizontal: width=fit_content." The layout engine only measures
wrap-grown height when text follows the vertical-layout rule. In our
horizontal header, a wrapped title rendered visually but did not
propagate its extra height to header.height=fit_content, so the
header stayed at single-line height and the NEXT sibling in the
parent vertical layout overlapped the wrapped lines.

Fix: introduce a fill_container + vertical + fit_content title
container that wraps the text node. The text now follows the
documented rule (fill_container + fixed-width in vertical parent),
wrap height is measured correctly, and header.height=fit_content
grows to match. Following content gets pushed down by the wrapped
height as intended.

Regression test: add a long-title case that asserts
  - header.height === 'fit_content'
  - title-container.layout === 'vertical'
  - title-text.width === 'fill_container'
  - title-text.textGrowth === 'fixed-width'
  - header has NO space_between AND NO fixed height
Existing id-count test updated: 5 → 6 nodes (added title container).

7/7 section-header tests pass; 125/125 pen-mcp suite green. format +
tsc clean. Bundle rebuilt.
2026-04-19 19:45:40 +08:00
Fini 0e5dda4b9e fix(mcp): add_section_header_v0 title+action cannot overlap on long titles
Codex stop-hook #9: previous implementation used
justifyContent:space_between with both children at natural
(fit_content) width. Long titles push past the action's starting
position and overlap it — flexbox space_between distributes REMAINING
space but does not clip items that collectively exceed the container
width.

Fix: title now takes width:fill_container + textGrowth:fixed-width so
it consumes all remaining horizontal space and wraps vertically when
too long (the header's height:fit_content accommodates wrapping).
Action stays width:fit_content on the right. Header adds gap:16 for
guaranteed visual breathing room. justifyContent:space_between is
intentionally removed — fill_container on one sibling makes it
redundant and the removal is what prevents the overlap.

Regression test: seed a header with a deliberately long title + short
action and assert title has fill_container/fixed-width, action has
fit_content, and header has NO justifyContent. 7/7 section-header
tests pass. 124/124 pen-mcp suite green.
2026-04-19 19:37:32 +08:00
Fini 75467676b1 feat(mcp): expand element tool family to 9 (add 4 layout-pattern tools)
Step 7 continues: 5 → 9 narrow element tools, each solving one
documented anti-pattern from pen-ai-skills prompt knowledge.

- add_stat_grid_v0: NON-scrolling 2-5 metric grid. Each cell uses
  width=fill_container so the renderer auto-distributes space.
  Directly solves the documented activity-rings overflow bug in
  packages/pen-ai-skills/skills/phases/generation/layout.md
  (three fixed 100px rings in a 279px inner card silently clip the
  third; with fill_container the third fits by construction).
  Different from add_metric_row_v0 which is a scrolling wrapper
  with fixed-px items.

- add_section_header_v0: heading + optional trailing action ("See
  all" / "View more"). Forces horizontal space_between alignItems=
  center so action stays flush-right. Common dashboard pattern that
  non-Claude models frequently vertical-stack instead.

- add_top_nav_bar_v0: mobile app bar. Leading icon (back/menu) +
  centered title + trailing icon (search/more). Dual of
  add_bottom_nav_v0. Empty slots become 44×44 spacers so the title
  stays visually centered even with asymmetric icons.

- add_icon_button_v0: 44×44 icon-only button with flex centering.
  Explicitly NOT layout=none (the documented anti-pattern in
  memory: layout=none + nested absolute-positioned children renders
  unreliably under Skia). Forces layout=horizontal + justifyContent
  /alignItems=center.

All four follow the established element-tool pattern:
- Sugar route via insertElementTree (parent_id pre-check, DSL
  escape pre-check, snapshot rollback, post-check parent-location
  verification)
- assignIdsRecursively on the built subtree
- No union types in schema; narrow required params; optional
  sizing/styling via follow-up batch_design U-op

Tests: 22 new unit (5+6+6+5 per tool) + element-tools-contract
updated to assert all 9 tools satisfy §4 invariants. All 124
pen-mcp tests pass.

skill file `elements.md` expanded from 5 → 9 tools (decision tree
updated, PREFER phrases mapped per tool, usage examples added,
role list extended). Registry regenerated from 44 → 44 skills
(same count, elements.md edit in place).

MCP live e2e smoke via StdioClientTransport confirms ListTools now
returns 49 tools (40 baseline + 9 element), all 4 new tools
callable, structural invariants verified on live output
(stat-grid cell.width=fill_container; icon-button layout \!= none;
3-slot top-nav structure; section-header action group).

format + tsc green. Bundle rebuilt.
2026-04-19 19:28:41 +08:00
Fini bff94b2ada fix(ai): gate elements skill behind hasMcpTools flag (no prompt pollution)
Codex stop-hook #8: elements.md had `trigger: null` which makes
resolveSkills('generation', ...) unconditionally load its 1500-token
N-tool reference into every generation prompt — including the
embedded orchestrator in apps/web/src/services/ai that emits
single-shot JSON and CANNOT call MCP tools. The content was dead
weight there (orchestrator-sub-agent.ts:333 / ai-prompts.ts:132 both
build generation prompt via resolveSkills without any tool-use path).

Fix: trigger: { flags: [hasMcpTools] }. Skill only auto-loads when
caller explicitly declares MCP tools are available. No existing caller
sets this flag, so the embedded orchestrator prompt is now clean
again.

External MCP clients (Claude Code / Codex / Gemini CLI / Cursor) still
get the content via get_design_prompt(section='elements'), which uses
getSkillByName direct lookup and bypasses resolveSkills' trigger
filter. That contract is preserved.

Added explanatory HTML comment in the skill header so future editors
understand the gating + opt-in rule.

Tests: 4 new cases in design-prompt-elements.test.ts verify:
- getSkillByName returns skill regardless of flags (direct lookup)
- resolveSkills('generation') WITHOUT flag → elements excluded
- resolveSkills('generation') WITH {hasMcpTools:true} → elements included
- buildDesignPrompt('elements') works regardless of flag (bypass path)

14/14 design-prompt-elements tests pass; 186/186 across pen-mcp +
pen-ai-skills. format + tsc green. Bundle rebuilt.
2026-04-19 19:06:35 +08:00
Fini 43a6b7b86f feat(ai): add 'elements' section to get_design_prompt + new skill
Step 4 of N-tool element design. Teach external MCP clients (Claude
Code / Codex / Gemini CLI / Cursor) when to reach for a narrow
element tool vs fall through to batch_design.

New skill: packages/pen-ai-skills/skills/phases/generation/elements.md
- Decision tree mapping item shape → tool (card_row / metric_row /
  nav_chip_row / bottom_nav / activity_ring)
- PREFER vs STILL-USE-batch_design conditions with concrete spec
  phrases ("horizontal scrolling cards", "KPI cards", "bottom nav")
- Minimal usage examples for all 5 tools
- Composition pattern: build section via batch_design → insert row
  with parent_id → post-hoc style via batch_design U-op
- Invariants enumeration (wrapper / id assignment / role set) so AI
  knows what NOT to rebuild
- Failure-mode guidance: if tool throws, inspect message and switch
  strategy rather than retry
- Priority 14 / budget 1500 tokens

design-prompt.ts: register 'elements' in SECTION_NAME_MAP,
PromptSection type, SECTION_MAP dispatch.
design-routes.ts: add 'elements' to get_design_prompt.section enum
+ updated description.

_generated/skill-registry.ts regenerates at vite build time (43 → 44
skills; gitignored, not committed).

Tests:
- 10 new unit tests (design-prompt-elements.test.ts) cover
  registration, content invariants, decision-tree presence, fallback
  teaching, composition pattern, invariants naming, unknown-section
  fallback behavior
- d0-parity-spike snapshot updated (get_design_prompt definition
  intentionally changed)

MCP live e2e smoke via StdioClientTransport confirms ListTools enum
shows 11 values including 'elements', CallTool returns 4122 char
content, all 5 tools named, all structural assertions pass.

98/98 pen-mcp tests pass. format + tsc green. Bundle rebuilt.
2026-04-19 18:52:00 +08:00
Fini b62e1e9bd9 fix(mcp): rollback preserves snapshot bytes exactly (no re-serialize)
Codex stop-hook #7: previous rollback (a0ace58) called
saveDocument(fp, restoredDoc) to trigger the live-canvas re-sync, but
saveDocument runs JSON.stringify(doc) with no indent and writes the
result to disk. That overwrites the snapshot we just restored via
writeFile — losing the user's original formatting (indentation, key
order, number representation, trailing newlines) and replacing it
with pen-mcp's canonical serialization.

Fix:
- Export pushLiveDocument from document-manager so callers can trigger
  a live-canvas push WITHOUT re-writing disk
- Rollback path: writeFile(snapshot) + invalidateCache + openDocument +
  pushLiveDocument. The file is restored byte-exact; pushLiveDocument
  reads the parsed doc from cache and pushes it, no disk rewrite

Semantically equivalent to before for the live-canvas side (same push
with same restored doc) but now preserves the snapshot bytes on disk
exactly. Important for users who hand-edit .op files or whose original
document was produced by a different serializer (pen-core, Electron
save-as, etc.).

88/88 pen-mcp tests pass (existing "file bytes unchanged" assertions
already verify this; they simply now rely on a path that doesn't
re-serialize). format + tsc green.
2026-04-19 18:39:11 +08:00
Fini b191b35a3c fix(mcp): rollback re-syncs live canvas after restoring file
Codex stop-hook #6: saveDocument is DUAL-WRITE for file-backed paths
(document-manager.ts:411) — it writes to disk AND calls
pushLiveDocument. handleBatchDesign uses saveDocument so a bad insert
gets pushed to the live canvas before our post-check runs. Prior
rollback (6dfa88b) only restored the file via writeFile, leaving the
live-canvas renderer showing the bad insert until the next refresh.

Fix: after writeFile + invalidateCache, call saveDocument(fp, restoredDoc)
in the rollback path. saveDocument will re-write the file (no-op, same
content) AND call pushLiveDocument again with the restored doc,
bringing the live canvas back in sync with disk. If the live-sync step
fails, re-throw with a diagnostic noting the live canvas may be stale
but disk is authoritative.

Wrap the re-sync in try/catch so a transient live-sync failure doesn't
swallow the original insert-failure reason. Disk is the source of
truth and is already restored when we hit this path.

If no sync URL is configured (common in unit-test contexts),
pushLiveDocument is a no-op — so this is safe in all scenarios.

Live-canvas-only paths (filePath='live://canvas') remain
non-rollback-able because pushLiveDocument is a one-way push without
a history mechanism; the pre-check is the primary defense there.

88/88 pen-mcp tests pass (rollback behavior unchanged at the
observable-test level since our tests don't configure a live sync
URL; the re-sync is correct by construction given saveDocument's
published semantics). format + tsc green.
2026-04-19 18:33:12 +08:00
Fini 36852d77ff fix(mcp): pre-check + rollback so wrong-parent never persists on disk
Codex stop-hook #5: previous post-insert check (5311313) throws on
wrong-parent detection, but by the time we detect it batch_design has
ALREADY written the bad insert to disk / pushed to live canvas. The
throw is a post-facto notification, not a guarantee.

Three-layer defense:

1. **Pre-check (NEW, prevents disk write)**: simulateDslParentResolve
   mirrors batch-design.ts:resolveRef (`raw.replace(/^"|"$/g, '')`, no
   JSON-unescape). If JSON.stringify(parent_id) → quote-strip ≠
   parent_id, the DSL round-trip is lossy → throw before ever calling
   handleBatchDesign. Covers ids containing `"` or `\` (the cases
   Codex #3 and #4 flagged).

2. **Snapshot + rollback (NEW, for file-backed docs)**: before calling
   handleBatchDesign we readFile the target into a snapshot. If any
   post-check (errors / missing insert / wrong parent) trips, we
   writeFile the snapshot back + invalidateCache, then throw.
   File-backed docs are now atomic at the element-tool boundary.

3. **Post-check (kept as defense-in-depth)**: node-in-tree +
   parent-location verification. With pre-check in place these should
   be unreachable, but they guard against future DSL-parser changes
   or silent-failure modes we haven't anticipated.

Known limitation: live canvas cannot be atomically rolled back because
pushLiveDocument is a one-way push. The rollback path skips live with
a clear error message noting the insert may still be visible until
next refresh. Pre-check is the primary defense for live; post-check
rollback-best-effort applies only to filePath'd calls.

Tests updated: existing "weird quoted parent_id" and
"A\"B decoy collision" regression tests now hit pre-check (cleaner
error, same invariant: throw + file unchanged). 88/88 pen-mcp pass.
format + tsc green.
2026-04-19 18:27:58 +08:00
Fini ea5057380c fix(mcp): post-insert must verify parent location, not just presence
Codex stop-hook #4: the prior post-insert check (e797584) verified that
the inserted nodeId is findable anywhere in the tree, but did not
verify it landed under the REQUESTED parent. Silent wrong-parent
inserts are still possible if batch_design's resolveRef quote-strip
produces a literal that matches a DIFFERENT node than the one
ensureParentExists validated.

Concrete example: doc has nodes with ids `A"B` (3 chars: A, ", B) and
`A\"B` (4 chars: A, \, ", B). User passes parent_id='A"B'.
ensureParentExists does a raw-string `===` match and finds the first
node. insertElementTree does JSON.stringify → `"A\"B"` in DSL source.
batch_design's parseInsertArgs → resolveRef → `/^"|"$/g` quote-strip
→ literal `A\"B` (4 chars). insertNodeInTree matches the DECOY and
inserts under it. Prior post-check: node is in tree → passes. Actual:
wrong parent.

Fix: when args.parent_id is provided, post-check also walks
findParentInTree(postChildren, insertedId) and confirms the resolved
parent id === the requested parent_id. Mismatch → throw with clear
diagnostic.

Regression test: element-tools-contract.test.ts adds the A"B / A\"B
decoy scenario and asserts the tool throws (not silent success).

88/88 pen-mcp tests pass (+1 new wrong-parent guard). format + tsc
green. Bundle rebuilt.
2026-04-19 18:12:43 +08:00
Fini 5847b50d19 fix(mcp): post-insert verification closes silent no-op path
Codex stop-hook #3: JSON.stringify(parent_id) in insertElementTree
doesn't prevent silent no-op. batch_design's resolveRef
(batch-design.ts:442-445) only does `/^"|"$/g` quote-stripping — it does
NOT JSON-unescape — so a parent_id containing `"` or `\` round-trips
as a different literal than what's actually stored in the document.
insertNodeInTree finds no match, silently returns the original tree,
and batch_design still reports success (pushes to results, bumps
nodeCount, errors stays empty).

ensureParentExists catches the "parent genuinely doesn't exist" case
via pre-check using the raw string for direct equality. It does NOT
catch the escape-mismatch case (which passes pre-check with the raw
string but fails in the DSL path).

Fix: insertElementTree now re-reads the document after handleBatchDesign
and confirms the inserted nodeId is actually findable via
findNodeInTree. If not present → throw with diagnostic info about
parent_id and pageId. This is the single source of truth for "did the
insert land?" regardless of what DSL parser subtleties occur
downstream.

Regression test: element-tools-contract.test.ts adds a case that seeds
a document with parent id containing a literal `"` (manually crafted —
nanoid never produces such ids, but imported/migrated docs might),
then calls handleAddBottomNavV0 with that parent_id. Previously the
tool would return success with an orphan record; now it throws and the
on-disk document is verified unchanged.

87/87 pen-mcp tests pass. format + tsc green. Bundle rebuilt.
2026-04-19 18:06:59 +08:00
Fini 70592537fd fix(mcp): Codex review #2 P0 fixes — tighten element-tool safety + contract
Four cleanup items from the second independent Codex review, all
scoped to element tools + their contract tests.

P0.1 insertElementTree safety boundary (element-tool-helpers.ts:112-124):
- parent_id now JSON.stringify'd so ids containing quotes/backslashes
  cannot escape DSL quoting and inject additional batch_design ops
- per-item batch_design errors are re-thrown with a concise summary
  instead of silently surfaced via result.errors (N-tool single-insert
  semantics require loud failure; callers can't distinguish "inserted
  with errors" from "didn't insert at all")

P0.2 add_activity_ring_v0 narrowing (add-activity-ring-v0.ts):
- strip ring_color / text_size / text_weight params (were 6 business
  params → now 3). Typography/color hardcoded (#000000, 16, 700) per
  spec D6 Style-Guide-orthogonal invariant. Callers override via a
  follow-up batch_design U-op, same as card/metric/nav_chip rows
- route schema + description + test updated accordingly

P0.3 test hardening:
- metric_row + nav_chip_row gain "every node has a unique non-empty id"
  regression test matching the other 3 element tools (id coverage parity)
- all 5 element tools gain "throws on bogus parent_id AND leaves file
  untouched" side-effect assertion. Previous tests only checked the
  throw (mirroring helper impl); now tests verify the actual invariant
  that matters to the user — the document doesn't get partially
  mutated when validation rejects

P0.4 schemaVersion input property (design-routes.ts + new
element-tools-contract.test.ts):
- every element tool (5 production + 1 spike) now declares schemaVersion
  as an input property (enum: ['1.0'], optional) per spec §4.2. Was
  previously only mentioned in description strings, which clients
  couldn't introspect
- new element-tools-contract.test.ts cross-validates §4.1 (additive),
  §4.2 (schemaVersion property), §4.4 (filePath/parent_id accepted),
  and "应拆尽拆" invariant (no children_type/variant unions leaked
  back in)

86/86 pen-mcp tests pass (+6 new contract + 2 new id coverage + 5 new
side-effect). format + tsc green. Bundle rebuilt.

P1 items still open (server-side JSON Schema enforcement, role
registration for nav-chip-active/nav-item-active, live-canvas race
condition in ensureParentExists, saveDocument silent-failure path)
tracked separately.
2026-04-19 17:53:28 +08:00
Fini 160c112876 fix(ai): overflow.md no longer advertises icon as required for nav chips
Codex stop-hook review: the stale AI prompt in
packages/pen-ai-skills/skills/phases/generation/overflow.md still told
clients add_nav_chip_row_v0 items require both label + icon, even
though the tool now accepts label-only chips (fixed in 451474f).

Update the §HORIZONTAL SCROLL ROWS preferred-path bullet for
add_nav_chip_row_v0 to mark icon as optional and call out label-only
support ("All / Videos / Photos" text-only filter tags).
2026-04-19 17:16:10 +08:00
Fini 0794cf9ebb fix(mcp): add_nav_chip_row_v0 icon is optional (restore label-only support)
Codex stop-hook review: splitting add_scroll_row_v0 into narrow tools
regressed label-only chip rows. The original children_type='nav_item'
variant accepted { title } with no icon (text-only filter tags like
"All" / "Videos" / "Photos"), but the new add_nav_chip_row_v0 made icon
required, breaking that use case.

- tool handler: if item.icon absent, skip the icon_font child and emit
  just the text label. Mirrors the original buildNavItem behavior from
  packages/pen-mcp/src/tools/add-scroll-row-v0.ts (removed).
- route schema: items[].required changed from ['label', 'icon'] →
  ['label']; description notes label-only chips are supported.
- test: new "builds label-only chips when icon is omitted" case covering
  3 plain-label items (All / Videos / Photos) asserting each chip has
  exactly 1 text child, no icon_font. Existing icon-present test still
  covers the with-icon path.

78/78 pen-mcp suite passes (was 77 + 1 new). format + tsc green. Bundle
rebuilt.
2026-04-19 17:11:09 +08:00
Fini b4937900f8 refactor(mcp): split add_scroll_row_v0 into 3 narrow tools + extract helpers
Per "应拆尽拆" guidance: remove children_type union from scroll-row
family. Each narrow tool now has ≤5 simple params, no union types, and
a single output pattern — so the LLM never has to decide between
variants inside a tool.

Replaced `add_scroll_row_v0({children_type, items})` with:
- add_card_row_v0({items: {title, subtitle?, icon?}}) — 140×160 cards
- add_metric_row_v0({items: {label, value, icon?}}) — 120×100 tiles,
  value=28/700
- add_nav_chip_row_v0({items: {label, icon, active?}}) — 72 chips with
  active state

Shared wrapper + id assignment + parent check + DSL insertion logic
extracted into element-tool-helpers.ts:
- buildScrollWrapper() — the fill_container+clipContent outer + inner
  fit_content row (identical across all 3 row tools)
- assignIdsRecursively() — moved from duplicated copies in each tool
- insertElementTree() — centralizes the batch_design DSL shape

Also refactored add_bottom_nav_v0 + add_activity_ring_v0 to use the
shared helpers (DRY).

- overflow.md: §HORIZONTAL SCROLL ROWS preferred-path section now
  points at the 3 narrow tools with a decision rule (what's in each
  item → which tool) instead of add_scroll_row_v0+children_type param
- design-routes.ts: unregister add_scroll_row_v0, register 3 new
  tools each with precise inputSchema (label/value/title fields match
  the semantic role)
- Delete obsolete add-scroll-row-v0.ts + test + snapshot

Tests: 77/77 pass (was 86 before removing scroll-row tests). Each new
tool has 3-4 tests covering registration + structure + id coverage +
parent_id validation. Live MCP smoke confirmed 45 tools in ListTools
(40 baseline + 5 element) with all narrow tools functional.

format + tsc green.
2026-04-19 16:59:32 +08:00
Fini 6b0feb3ba5 fix(mcp): fail fast on invalid parent_id in element tools
Codex stop-hook review: add_bottom_nav_v0 (and siblings) advertised a
parent_id contract that could silently no-op. pen-core's
insertNodeInTree returns the original tree unchanged when parentId
doesn't match any node (tree-utils.ts:200-234), so batch_design's
downstream call produces a success-looking response {results, nodeCount}
with an orphaned node that never lands on disk.

- tools/element-tool-helpers.ts: new ensureParentExists() helper —
  loads the target doc via openDocument + resolveDocPath, checks
  getDocChildren(pageId) with findNodeInTree, throws a descriptive
  Error listing parent_id and pageId if missing
- Apply to all 3 element tools (add_scroll_row_v0 / add_bottom_nav_v0 /
  add_activity_ring_v0) at the top of each handler, before DSL
  construction. Null parent_id short-circuits (root insertion is OK)
- routes/design-routes.ts: fix misleading "Page id" description on
  add_bottom_nav_v0.parent_id → now matches siblings ("Target parent
  node id (must exist...)")
- Tests: 4 new (throws on bogus parent_id for each of 3 tools, +
  positive "inserts under valid parent" for add_scroll_row_v0). 86/86
  pen-mcp suite pass

Bundle rebuilt. format + tsc green.
2026-04-19 16:16:11 +08:00
Fini 0799ad4843 feat(mcp): add_bottom_nav_v0 + add_activity_ring_v0 element tools
Step C of N-tool element design: expand MVP from 1 to 3 tools, each
targeting a documented non-Claude failure mode with file:line evidence.

- add_bottom_nav_v0: bottom tab bar. Solves layout.md §NO FIXED-POSITION
  LAYOUT anti-pattern (empty spacer siblings after nav; bottom-nav is
  inline flow, not position:fixed). Schema: items[]+height; output:
  frame(role=bottom-tab-bar, width=fill_container, layout=horizontal,
  justifyContent=space_around) with per-tab icon+label and
  role=nav-item-active for current tab.

- add_activity_ring_v0: Apple-style progress ring with centered text.
  Solves layout.md §RING / CIRCLE WITH CENTER CONTENT anti-pattern
  (ellipse+sibling text stacks wrong; layout=none+absolute renders
  unreliably). Schema: size/thickness/ring_color/center_text +
  text_size/text_weight; output: frame(cornerRadius=size/2, stroke,
  fill=[], layout=horizontal, alignItems=center, justifyContent=center)
  with single text child.

Both tools follow D1=A Sugar route (internal handleBatchDesign call)
and assignIdsRecursively pattern from 9ba64b7.

Tests: 8 new (4 nav + 4 ring), 82/82 pen-mcp suite pass. format + tsc
green. Live MCP smoke test via StdioClientTransport confirmed ListTools
now 43 tools (40 baseline + 3 element), spike still hidden, both new
tools produce correct structure + unique node ids.
2026-04-19 16:05:15 +08:00
Fini b593ba5272 docs(ai): teach overflow.md to prefer add_scroll_row_v0 MCP tool
Step 3 (prompt path, MVP scope). Update §HORIZONTAL SCROLL ROWS to
split into two paths:

- Preferred (MCP tool): call add_scroll_row_v0 directly. External MCP
  clients (Claude Code / Codex / Gemini CLI / Cursor) see the tool in
  ListTools and the prompt now explicitly teaches WHEN to pick it
  over hand-building JSON
- Fallback (hand-built JSON): existing structure teaching, unchanged,
  used when MCP tool isn't available (embedded AI flow, JSON-only)

Embedded orchestrator integration (apps/web/src/services/ai) is out of
scope for this MVP step — it requires architectural work to flip from
one-shot JSON emission to MCP tool_use flow. External clients already
get full benefit from tool registration + this prompt update.

84/84 pen-ai-skills tests pass. format:check + tsc green.
2026-04-19 15:59:04 +08:00
Fini bc2a9a38b1 fix(mcp): assign ids to every node in add_scroll_row_v0 subtree
Codex stop-hook review: child nodes were saved without ids. batch_design
only assigns an id to the top-level inserted node — nested children
(inner row, cards, texts, icon_fonts) come through the DSL unchanged,
which breaks any later tree operation that resolves by id (update /
delete / move / post-processing / spatial index lookup).

Fix: assignIdsRecursively walks the wrapper subtree before serializing
to DSL and stamps every node with generateId(). batch_design's own
overwrite of the top-level id is harmless.

Test: 4 new id-coverage tests (one per children_type + uniqueness
across a 3-card tree). Total 17 tests in add-scroll-row-v0, 74 in
pen-mcp suite. format + tsc green.
2026-04-19 15:39:42 +08:00
Fini b8744fbda3 feat(mcp): add_scroll_row_v0 MVP element tool for non-Claude stability
Step 2 of N-tool element design (spec v0 §7.1 pinned to this impl).
First production element-level tool, replacing the batch_design generic DSL
for the specific pattern LLMs most commonly get wrong: horizontal scroll
rows of cards / metric tiles / nav items.

Tool fixes the structure at schema level:
- Outer wrapper: fill_container + clipContent=true + vertical layout
- Inner row: fit_content + horizontal + gap + padding=[0,20]
- Children: fixed numeric width per variant (card=140 / metric=120 / nav=72)

Exactly matches packages/pen-ai-skills/skills/phases/generation/overflow.md
§HORIZONTAL SCROLL ROWS. Sugar over handleBatchDesign (D1=A route).

- tools/add-scroll-row-v0.ts: 230-line builder with 3 children_type presets
- routes/design-routes.ts: register in production DESIGN_TOOL_DEFINITIONS
- __tests__/add-scroll-row-v0.test.ts: 13 tests covering wrapper invariants,
  per-type structure, icon/subtitle optionality, overrides, persistence,
  golden snapshot
- d0-parity-spike.test.ts: rewrite "names exactly" assertion to "pre-D0
  tools still present" so new production tools don't break baseline; scope
  snapshot to pre-D0 5 tools only

70/70 pen-mcp tests pass. format:check + tsc --noEmit both green.
2026-04-19 15:32:25 +08:00
Fini 955adc1277 fix(mcp): gate add_section_v0 behind OPENPENCIL_D0_SPIKE flag
Codex stop-hook review: spike-only tool should not be exposed to
external MCP clients (Claude Code / Codex / Gemini CLI) by default.

- routes/design-routes.ts: move add_section_v0 out of DESIGN_TOOL_DEFINITIONS
  into separate D0_SPIKE_TOOL_DEFINITIONS / D0_SPIKE_TOOL_NAMES /
  handleD0SpikeToolCall exports, matching DEBUG_TOOL_* pattern
- server.ts: conditionally merge D0_SPIKE_TOOL_DEFINITIONS when
  OPENPENCIL_D0_SPIKE=1, mirroring OPENPENCIL_DEBUG_TOOLS=1 gating
- tests: assert default DESIGN_TOOL_DEFINITIONS is unchanged (spike
  not leaked) and D0_SPIKE_TOOL_DEFINITIONS contains the spike tool
  (only reachable when flag set). Snapshot regenerated.

57/57 pen-mcp tests pass. format:check + tsc --noEmit + vitest all green.
2026-04-19 15:17:19 +08:00
Fini fe2c37c711 feat(mcp): add_section_v0 tool for N-tool parity spike
D0 验证 N-tool "additive-only" 假设:新增元素工具不得改变现有
ListTools 输出或 batch_design 行为。实现为 sugar over handleBatchDesign
(仅 title + layout 两参数),不抽任何共用 helper。

- tools/add-section-v0.ts: 30-line handler,直接调 handleBatchDesign
- routes/design-routes.ts: 注册 tool definition / name / dispatch switch
- __tests__/d0-parity-spike.test.ts: 8 tests
  * 固定 5 个现有 design tool 的完整 definition 为 snapshot
  * 验证 batch_design baseline fixture 行为不变
  * 验证新工具水平/垂直/磁盘持久化三种场景

Spec: openpencil-docs/superpowers/specs/2026-04-19-element-tools-v0.md §D0
Report: openpencil-docs/superpowers/notes/2026-04-19-d0-parity-spike-report.md

结论: D1 = A (Sugar) parity 假设成立。add_section_v0 仅作 spike 验证,
MVP 生产将切换至 add_scroll_row_v0(基于 prompt 证据选型)。
2026-04-19 15:04:19 +08:00
Fini 11f94106f6 chore(release): bump to v0.7.4 2026-04-17 19:20:19 +08:00
Fini 549f244870 chore(agent): bump agent-native to v0.4.0 release 2026-04-17 19:04:15 +08:00
Fini 652174e0b5 chore(agent): bump agent-native to upstream v0.3.0 merge
Absorbs upstream v0.2.0 + v0.3.0 (openai-compat tool_calls streaming,
HTTP error diagnostics) while keeping the MiniMax Anthropic-compat
placeholder quirk. End-to-end MiniMax tool_use verified post-merge.
2026-04-17 18:55:44 +08:00
Fini d7f23b4ceb chore(agent): bump agent-native for Windows N-API symbol export 2026-04-17 00:20:57 +08:00
Fini 90254c1e87 chore(agent): bump agent-native for auto-detected MiniMax quirk 2026-04-17 00:18:49 +08:00
Fini 0b627ac9c4 chore(agent): bump agent-native for MiniMax tool_use 400 fix
Picks up c5d9e2a in the submodule: single-space placeholder text block
when an assistant turn's content is tool_use-only, fixing intermittent
HTTP 400s from MiniMax-M2 and similar reasoning-first models.
2026-04-16 21:27:13 +08:00
Fini c3215b55bb fix(types): re-export isBadgeOverlayNode as deprecated alias
The previous commit renamed isBadgeOverlayNode → isOverlayNode without
a compat export, which would break external consumers of the published
@zseven-w/pen-core package on upgrade. Add a deprecated alias so the
old import name keeps resolving. JSDoc flags the behavior change —
the alias no longer matches role:'badge'|'pill'|'tag', since those are
inline-component roles and must flow in auto-layout.
2026-04-16 21:09:56 +08:00
Fini 1be9ec4a19 fix(canvas): require explicit role:'overlay' for layout-flow escape hatch
isBadgeOverlayNode matched role:'badge'|'pill'|'tag' and pulled those
children out of their parent's auto-layout, rendering them at (0,0) of
the parent and stacking them on top of siblings. But in this repo
badge/pill/tag are inline-component roles (see role-resolver NAME_EXACT_MAP
and strip-redundant-section-fills PROTECTED_ROLES) — they're meant to
flow in layout like any other child.

Rename to isOverlayNode and narrow to role:'overlay'. Add matching
"Layout-escape roles" guidance in role-definitions.md so generation
prompts can reach the new opt-in. Inline roles now flow correctly;
true floating decorations (notification dots, corner ribbons) still
have a dedicated marker.
2026-04-16 21:07:40 +08:00
Kayshen Xu e0b606833b V0.7.3 (#111)
* fix(ai): stop white section bands on dark-themed pages

- role-resolver: skip fixSectionAlternation when parent fill luminance < 0.5, so we no longer paint #FFFFFF/#F8FAFC over a dark root
- strip-redundant-section-fills: add SAFE_LIGHT_HEXES so stale whites from earlier runs (or weak-model hedges) are cleaned up on the sink side
- regression tests for both layers

* feat(ai): design.md-driven background + sidebar color pipeline

- orchestrator-sidebar-color: extract sidebar surface picker; prefer design.md palette role (sidebar/panel/surface) over catalog style-guide legacy cell
- orchestrator-planning: force rootFrame fill from design.md background when a user spec is provided, so sections don't inherit a bright catalog default
- orchestrator-prompt-optimizer: infer design.md background + neutral theme fallback for sub-agent prompts
- orchestrator-sub-agent / ai-prompts: tell sub-agents to leave section root fills unset when design.md drives the palette
- design-md-style-policy: surface-colors policy block keeps MCP and web pipeline aligned
- add planning + prompt-optimizer regression tests

* chore: ignore .omx/ directory

* Enable local OS fonts with vector rendering and proper permission handling (#110)

* docs(readme): update cover screenshot

* fix(renderer): enable local OS fonts with vector rendering and proper permission handling

* test(renderer): refactoring names and creating vi.stubGlobal for the navigator as it's not available in the test environment.

---------

Co-authored-by: Fini <fini.yang@gmail.com>
Co-authored-by: Daniel Chettiar <danielc@snapwork.com>

* feat(types): add AppendContext and SubTask.existingSectionLabels

* feat(ai): add detectAppendIntent for continue/append prompts

* feat(ai): detect append intent before generate_design dispatch

* feat(ai): add applyAppendContextToPlan helper

* feat(ai): reuse existing content-root in append mode

* feat(ai): sub-agent APPEND MODE preamble for existing siblings

* docs(ai): teach horizontal scroll card-row pattern

* chore(ai): enable incremental-add skill in generation phase

* fix(canvas): render synchronously on resize to prevent white flash

Setting canvas.width/height clears the pixel buffer to transparent.
resize() previously only marked dirty, leaving the canvas transparent
until the next RAF and showing the container bg-muted through for one
frame whenever the flex layout shifted (e.g. RightPanel mount on first
selection after idle). Rendering inline after recreateSurface fills
the new surface before the browser paints, closing that window.

* style: apply oxfmt formatting drift across web and renderer files

Non-semantic line-break and wrapping adjustments picked up by oxfmt.
No behavior changes.

* fix(mcp): run codex via shell on Windows to handle .cmd shims

Since Node 18.20/20.12 (CVE-2024-27980) execFileSync refuses to spawn
.cmd/.bat files directly and throws EINVAL. On Windows route through
execSync with shell resolution so PATHEXT picks whichever shim exists
(codex.exe / codex.cmd / codex.ps1).

* feat(editor): anchor paste to selected container or sibling

Pressing Cmd/Ctrl+V now inserts pasted nodes into the selected
container (if it can hold children) or immediately after the selected
node as a sibling, falling back to the root when nothing is selected.
Previously every paste landed at document root, which broke expected
behavior when working inside nested frames.

* docs(ai): expand horizontal scroll card-row example in overflow skill

Flesh out the inline JSON example so the generation-phase skill shows
the full clipContent + nested fit_content row pattern, instead of a
truncated snippet that left model output inconsistent.

* style(lint): clear 7 oxlint warnings from recent commits

- orchestrator-planning.test.ts: narrow fill-array type to
  Array<{...}> | undefined and use ?.[0] instead of unchecked [0]
  so optional chain does not throw on short-circuit
- mcp-install.ts: drop `?? {}` fallbacks when spreading
  config.mcpServers; spread of undefined in an object literal
  is a no-op (ES2018+)

* style(lint): clear remaining 15 oxlint warnings across repo

Removes pre-existing warnings not related to any single feature:

- no-useless-fallback-in-spread (6): drop `?? {}` when spreading
  possibly-undefined records (document-store-variable-actions,
  pen-mcp/tools/{variables,theme-presets}, variable-theme-manager)
- no-useless-spread (2): replace `[...iterable]` with `Array.from`
  in for-of snapshots (document-events, agent-indicator), keeping
  the re-entry-safe copy intent explicit
- no-control-regex (2): use `\P{ASCII}` unicode property escape
  instead of `[^\x00-\x7F]` to express "non-ASCII" without
  referencing U+0000 (opencode clients)
- no-new-array (1): `Array.from({ length }, () => '..')` in
  document-assets
- no-unused-vars (3): drop unused catch params (agent.ts,
  code-generation-pipeline) and unused globSync import
  (patch-srvx-bun)
- no-useless-escape (1): `[[{]` instead of `[\[{]` in
  chat-message-content regex

---------

Co-authored-by: Fini <fini.yang@gmail.com>
Co-authored-by: Daniel Chettiar <74943095+1MochaChan1@users.noreply.github.com>
Co-authored-by: Daniel Chettiar <danielc@snapwork.com>
2026-04-15 22:19:12 +08:00
Kayshen Xu e9b0d0d822 V0.7.2-bugfix (#109)
* Stabilize synced main for AI handoff, drag nesting, and Electron dev (#104)

* docs(readme): update cover screenshot

* fix: stabilize electron dev sync and codex env passthrough

* Preserve nested frame behavior during drag reparenting

Reparenting across containers used raw local coordinates and root-only clipping assumptions, which made nodes jump visually and caused dragged frames to lose clip/corner semantics after nesting. This adapts the drag-reparent fix to the current upstream store architecture, keeps frame/shape nodes from auto-detaching on canvas drags, and promotes formerly root-only frame clipping to explicit clipContent when nested.

Constraint: Latest upstream workspace checkout is incomplete locally (missing workspaces/deps), so full upstream verification could not be rerun in this environment
Rejected: Keep using raw local x/y during parent changes | fails for auto-layout/padding-rendered positions
Rejected: Make all nested frames clip unconditionally | would change non-clipping containers
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Preserve visual-position conversion through rendered coordinates when parent changes; local coordinates alone are insufficient once layout participates
Not-tested: Fresh full workspace typecheck/test/build on latest upstream checkout (blocked by missing workspace/dependency setup in this local clone)

* Keep AI codegen requests bounded while exporting asset bundles

The AI codegen pipeline needed two stability fixes: exported design images had to flow through chunk/assembly prompts as reusable asset hints, and oversized chat payloads needed a local guard before hitting provider limits. This commit wires asset extraction into the planning pipeline, threads exported asset paths into prompt assembly, and rejects obviously overlarge chat requests with an actionable client-side error.

Constraint: This branch is split out from a larger local fix stack, so only codegen/prompt/context files are included here
Constraint: Provider request limits are approximate locally, so the payload guard must be conservative rather than exact
Rejected: Inline base64 assets directly into prompts | explodes request size and repeats the same payload per chunk
Rejected: Let provider errors handle oversized payloads | too slow and opaque for users
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep asset references flowing as stable ./assets paths and enforce payload limits before fetch to avoid silent request bloat
Tested: bun x tsc -p apps/web/tsconfig.json --noEmit; cd apps/web && bun --bun vitest run src/services/ai/__tests__/context-optimizer.test.ts src/services/ai/__tests__/codegen-assets.test.ts src/services/ai/__tests__/structure-bundle.test.ts; bun run build
Not-tested: Manual end-to-end AI generation with live providers

* Explain sanitized design views instead of leaving AI to guess

The sanitized structure bundle already stabilized asset paths, but it still exposed low-level image/layout/component fields that models had to interpret on their own. This change adds explicit consumer-view enrichment for fills, layout, text, variables, themes, and component semantics, carries original image size through the Figma import path, and augments sanitized bundles with summary/highlight guidance for downstream AI consumers.

Constraint: This branch is intentionally stacked on the asset-bundle PR because it extends the sanitized/codegen asset pipeline rather than replacing it
Constraint: Figma import data is not always complete, so original image size must be preserved when present and inferred only as a fallback downstream
Rejected: Keep sanitized.json as a pure field-level dump | still leaves AI to misread transforms, layout, and component relationships
Rejected: Put all explain text directly in asset extraction helpers | mixes resource stabilization with semantic enrichment responsibilities
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Treat consumer-view enrichment as a distinct layer on top of stable asset extraction; future AI-facing semantics should land there instead of leaking into unrelated pipeline code
Tested: bun x tsc -p apps/web/tsconfig.json --noEmit; cd apps/web && bun --bun vitest run src/services/ai/__tests__/consumer-view-enrichment.test.ts src/services/ai/__tests__/codegen-assets.test.ts src/services/ai/__tests__/structure-bundle.test.ts ../../packages/pen-figma/src/figma-fill-mapper.test.ts; bun run build
Not-tested: Manual prompt-to-code generation quality with live provider responses

* Restore code-panel bundle exports for AI handoff flows

The code generation backend still produced asset manifests and AI structure bundles, but the code panel UI no longer exposed those export paths after later sync work. This commit reconnects the panel to bundle export actions, restores ZIP download behavior when generated code includes exported assets, and locks the affordances with focused panel tests.

Constraint: Other local fixes are still in progress in the working tree, so this commit is intentionally limited to the code-panel export surface
Rejected: Rebuild export support in a separate panel | users expect the export actions to remain where generation results are shown
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep code-panel UI aligned with codegen asset/bundle backends whenever generation result shape changes
Tested: cd apps/web && bun --bun vitest run src/components/panels/code-panel.test.tsx src/services/ai/__tests__/codegen-assets.test.ts src/services/ai/__tests__/structure-bundle.test.ts; bun run build
Not-tested: Manual click-through of AI Bundle and Download ZIP in the desktop/web UI

* Unblock electron dev startup in the incomplete local workspace

The local workspace was failing before the app could even start: the skills plugin hard-required js-yaml from a node_modules layout that was not present, Vite dev under Bun hit Nitro NodeResponse incompatibilities, and the web tsconfig was missing path mappings for local packages. This commit removes the unnecessary js-yaml dependency from the skills loader, runs Vite under Node for dev startup, hardens readiness probing with socket checks, and points TypeScript/Vite at the in-repo package sources.

Constraint: The current local clone has incomplete hoisted/workspace installation state, so dev startup must not depend on root package links being perfectly present
Constraint: Bun + Nitro dev currently mis-handle NodeResponse in this environment, so the safest startup path is Node-hosted Vite
Rejected: Keep js-yaml and require everyone to fix local hoisting first | still leaves electron:dev broken in the current environment
Rejected: Continue running Vite dev through Bun | reproduces the NodeResponse/Parse Error failure on /api and /editor requests
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep the dev launcher biased toward resilient local startup, even when the workspace install shape is imperfect
Tested: bun -e import('./packages/pen-ai-skills/vite-plugin-skills.ts').then(() => console.log('SKILL_PLUGIN_IMPORT_OK')); bun electron:dev verified Vite ready, MCP/Electron compiled, Electron launched, MCP sync log emitted
Not-tested: Long-running interactive desktop session after startup

* fix(figma): preserve cropped image fill transforms

The synced branch started exporting original image dimensions but dropped the
existing crop transform semantics from the shared image-fill type and both
Figma mappers. That broke the new regression test and stripped metadata that
AI consumer-view/bundle code already relies on.

Constraint: keep app and package Figma mappers in lockstep
Rejected: loosen the new regression test | would hide a real metadata regression
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: when extending image fill metadata, update shared pen-types and both Figma mapper copies together
Tested: bun --bun run test (148/149 files passed; only server/__tests__/sse-keepalive.test.ts blocked by missing agent_napi.node), cd apps/web && bun --bun vitest run src/canvas/skia/drag-reparent-policy.test.ts src/components/panels/layer-dnd-utils.test.ts src/stores/document-position-utils.test.ts src/components/panels/code-panel.test.tsx ../../packages/pen-renderer/src/__tests__/document-flattener.test.ts ../../packages/pen-figma/src/figma-fill-mapper.test.ts, cd apps/web && bun --bun vitest run src/services/ai/__tests__/codegen-assets.test.ts src/services/ai/__tests__/structure-bundle.test.ts src/services/ai/__tests__/consumer-view-enrichment.test.ts, cd apps/web && bun --bun vitest run src/utils/__tests__/security.test.ts, bun test scripts/loopback-no-proxy.test.ts, npx tsc --noEmit, bun --bun run build
Not-tested: server/__tests__/sse-keepalive.test.ts without a locally built @zseven-w/agent-native addon

* docs(editor): normalize new PR comments to English

The PR had a handful of newly introduced Chinese code comments in dev, sync, and AI helper paths. This follow-up keeps the implementation unchanged while translating those comments to English so the PR stays consistent with the repository comment-language expectation.

Constraint: The request was limited to comment language cleanup after the conflict-resolution merge, so behavior had to remain unchanged
Rejected: Leave the mixed-language comments in place | conflicts with the PR requirement for English comments
Rejected: Broader repository-wide translation sweep | unnecessary scope expansion beyond the PR-introduced comments
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep code comments in English on this branch, even when local notes or working memory are in another language
Tested: bun test scripts/loopback-no-proxy.test.ts apps/desktop/__tests__/dev-utils.test.ts; cd apps/web && bun --bun vitest run server/__tests__/mcp-sync-state-active.test.ts src/canvas/skia/__tests__/skia-interaction.test.ts; npx tsc --noEmit; branch-diff comment scan for Han characters in comment lines
Not-tested: Manual runtime behavior, since this change only rewrote comments

* style(editor): apply repository formatting expected by CI

The PR was failing the CI Format check after the conflict-resolution and comment-normalization follow-ups. This commit applies the repository formatter output to the files touched by the branch so CI sees the exact formatting it expects, without changing behavior.

Constraint: The failing GitHub Actions job stopped at Format check, so the fix had to match oxfmt output rather than introduce functional changes
Rejected: Leave the branch as-is and rely on local formatting differences being acceptable | CI explicitly rejects the current formatting
Rejected: Broader code cleanup beyond formatter output | unnecessary scope while repairing the failing check
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: After conflict resolution or comment-only edits on this repo, run bun run format:check before pushing because formatter expectations are stricter than the existing file style in some touched files
Tested: bun run format:check; bun run lint; npx tsc --noEmit
Not-tested: Full test suite after this formatting-only commit (previous run showed formatting was the first CI blocker)

* refactor(editor): remove proxy-specific dev workarounds from PR

The PR no longer needs the loopback proxy bypass layer, so this cleanup removes the proxy-specific dev entrypoint, environment bootstrap, helper module, and its tests while keeping the unrelated Electron and AI handoff changes intact.

Constraint: Removal had to be limited to proxy-related code on PR #104 without undoing the other merged fixes on the branch
Rejected: Keep the helper and stop using it | leaves proxy-specific maintenance surface and tests in the PR
Rejected: Revert the entire Electron dev file to upstream earlier than necessary | would risk dropping unrelated local conflict-resolution choices beyond the proxy scope
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: If proxy handling is reintroduced later, keep it out of this PR unless there is a dedicated, separately justified change for it
Tested: bun run format:check; bun run lint; npx tsc --noEmit
Not-tested: Manual electron:dev behavior after removing the proxy-specific launcher path

* docs(ai): translate JSON-facing semantic descriptions to English

The PR still emitted Chinese semantic description strings inside the AI consumer-view and structure-bundle JSON outputs. This change translates those JSON-facing runtime descriptions and updates the affected tests so exported AI-facing structure data is consistently English.

Constraint: The request was limited to JSON description strings, so the change had to preserve the same semantics and structure while only translating output text
Rejected: Leave Chinese test fixtures and runtime descriptions in place | conflicts with the requirement for English JSON descriptions
Rejected: Broader i18n cleanup outside these AI JSON description paths | unnecessary scope expansion beyond the requested exported-description surface
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep AI/exported JSON explanation strings in English unless a future change explicitly adds localized output modes
Tested: cd apps/web && bun --bun vitest run src/services/ai/__tests__/consumer-view-enrichment.test.ts src/services/ai/__tests__/structure-bundle.test.ts src/services/ai/__tests__/codegen-assets.test.ts; bun run format:check; npx tsc --noEmit
Not-tested: Full app runtime flows that consume these JSON descriptions outside the covered unit tests

* refactor(ai): remove remaining network-proxy handling

The current project still carried Anthropic proxy-specific heuristics and environment handling outside the PR-specific cleanup. Since the earlier crashes and connectivity issues were unrelated to proxying, this removes the remaining network-proxy branches, model remapping, and TLS override advice while leaving unrelated request flows intact.

Constraint: The cleanup needed to remove proxy-specific logic without disturbing unrelated transport concepts such as app-internal API proxy routes or React proxy objects used in tests
Rejected: Keep the proxy heuristics as dormant fallback logic | preserves misleading operational guidance and dead maintenance surface
Rejected: Rename every remaining literal use of the word proxy in the repo | would overreach into unrelated concepts like internal API proxying and JS Proxy-based test setup
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: If endpoint-specific compatibility logic is needed later, add it as explicit endpoint handling rather than generic proxy heuristics
Tested: bun run format:check; bun run lint; npx tsc --noEmit; repo-wide search for network-proxy env references after cleanup
Not-tested: End-to-end Claude connection flows against custom base URLs after removing proxy-specific remapping

* fix(electron): keep Node-backed dev launch for Nitro compatibility

Comparing against upstream commit 7271a03 confirms the current Electron dev fix is not the same idea as the original Bun-based launcher. The upstream version starts Vite with Bun, while the observed failure shows Nitro now crashes in that path with "Vite environment nitro is unavailable". This keeps the non-proxy Node-backed launcher because it fixes the actual regression without restoring the removed proxy code.

Constraint: The request preferred reverting to the upstream original only if the intent matched, but the current Nitro/Electron failure proves the upstream Bun launcher is no longer equivalent in behavior
Rejected: Restore the exact 7271a03 Bun launcher | reproduces the Nitro dev-worker crash and ERR_EMPTY_RESPONSE in Electron
Rejected: Reintroduce the old proxy workaround bundle | unrelated to the reproduced failure and already removed by request
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep Electron dev on the Node-backed Vite launcher unless Nitro/Bun dev compatibility is revalidated with a real startup test
Tested: bun run electron:dev (reached Electron launch after Vite/MCP/Electron compile steps); bun test apps/desktop/__tests__/dev-utils.test.ts; bun run format:check; npx tsc --noEmit
Not-tested: Full interactive manual editor workflow after Electron launch

---------

Co-authored-by: Fini <fini.yang@gmail.com>

* fix(ai,cli): openai-compat turn-2, StepFun reasoning+451, Mac CLI discovery

Round up the v0.7.2 stability fixes for AI connectivity and local CLI
detection that surfaced during real user runs against GLM, StepFun, and
Mac users on nvm/fnm/pnpm/bun/mise/asdf/fish shells.

Provider (via @zseven-w/agent-native v0.3.0 submodule bump):
- OpenAI-compat providers can now complete multi-turn tool-calling loops:
  the request builder translates Anthropic-shaped message history
  (tool_use / tool_result blocks, thinking) into OpenAI's tool_calls +
  role="tool" form so turn 2 no longer 400s. system_prompt is finally
  injected instead of being silently dropped.
- The SSE parser accepts `delta.reasoning` (StepFun step_plan) alongside
  `reasoning_content` (GLM / DeepSeek / Qwen), and also streams tool_call
  fragments, which unblocks GLM / dashscope and stops the
  firstTextTimeout → fetch abort → std.http panic → Bun segfault cascade.
- HTTP 451 (StepFun content-safety) surfaces as InvalidRequest with a
  specific "content blocked by provider safety filter" message instead
  of an opaque error_server.

Server route + client watchdog:
- /api/ai/chat forwards the provider's last_error string
  (result.errors[0]) so users see "HTTP 451 content blocked" rather than
  "Provider error: error_server".
- streamChat clears firstTextTimeout on thinking chunks (when
  thinkingResetsTimeout=true), so models that stream long reasoning
  before any text aren't falsely killed as "stuck".

Orchestrator sub-agent resilience:
- Failed sub-agents (empty response / unparseable output) now retry once
  with a minimal ~3KB kernel prompt (schema + jsonl-format only). Only
  the failing subtask re-runs — successful earlier sections are kept.
- Deterministic refusals (HTTP 400/401/429/451, "content blocked",
  "censorship", "authentication failed") short-circuit the retry ladder
  so a 4-minute StepFun safety scan isn't spent twice in a row.

Local CLI discovery (Mac users on managed shells):
- New server/utils/cli-resolver-helpers.ts exports probeViaLoginShell()
  and posixUserBinDirs(). Login-shell probe asks $SHELL (or zsh/bash
  fallback — fish added at /opt/homebrew/bin/fish and friends) with
  `-ilc 'command -v <cli>'` so nvm/pnpm/bun/mise/asdf/volta/fnm shims
  are visible even when Electron scrubs the inherited PATH.
- resolveClaudeCli / resolveGeminiCli / resolveCopilotCli and the
  inline codex/opencode resolvers in connect-agent.ts all run the same
  PATH → login-shell → npm-prefix → user-bin candidates ladder. Each
  step logs via serverLog to ~/.openpencil/logs/server-YYYY-MM-DD.log
  for remote diagnosis.

Builtin provider preset:
- Add StepFun Coding Plan (api.stepfun.com/step_plan/v1, label "StepFun
  Coding Plan") alongside the existing StepFun preset.

Version bump 0.7.1 → 0.7.2 across all workspaces.

---------

Co-authored-by: RaisCui <857943+raiscui@users.noreply.github.com>
Co-authored-by: Fini <fini.yang@gmail.com>
2026-04-14 21:42:56 +08:00
Kayshen Xu 52efa884e6 V0.7.1 (#102)
* fix(desktop,web): rebuild Electron dev sync + bitmap dragging fix on v0.7.1 (#99)

Re-applies b046a0d from the closed PR #97 now that the base is v0.7.1.
Original conflict against v0.7.0 came from the release branch churn —
the cherry-pick onto v0.7.1 applies cleanly.

Keeps dev-startup, sync-noise, and bitmap-dragging fixes; drops the
loopback proxy helper scripts upstream rejected in PR #92. Readiness
probe now does direct socket checks inside the existing dev entrypoint;
sync hardening stays focused on request diagnostics, backpressure, and
drag-time clip-rect correctness.

Original commit: b046a0d
Supersedes: #97 (closed, head branch deleted)

Co-authored-by: Rais <vdcoolzi@gmail.com>

* fix(canvas): use ImageFill.url in skia-interaction test

The image-backed rectangle fixture in the skia-interaction test used
`{ type: 'image', src: '…' }`, but `ImageFill` in pen-types declares
the field as `url`. `npx tsc --noEmit` flagged it as TS2352 on the
`as PenNode` cast. One-word rename.

The squash-merge of #99 captured an earlier snapshot that did not
include this fix, so re-apply directly on v0.7.1.

* feat(cli): add `op install` / `op uninstall` for openpencil-skill

Bundle skill files at build time (scripts/bundle-skill.ts → skill-bundle.json)
so users without GitHub access can install directly. Falls back to git clone
when the bundle is empty.

Supports auto-detection of: Claude Code, Codex, Cursor, Gemini CLI, OpenCode.
CI workflows updated to checkout openpencil-skill before cli:compile.

* fix(panels): allow reparenting nodes into rectangle in layer panel

CONTAINER_TYPES was missing 'rectangle', preventing drag-drop into
rectangles even though the data model (ContainerProps) and store
(moveNode) both support it.

* fix(agent,ai): tool_exec reset + insert_node with after + move_node + CRUD tools

- fix(agent): reset StreamingToolExecutor between turns — prevents stale
  tool_use IDs that caused 400 errors on multi-turn tool calls (MiniMax etc.)
- feat(ai): add insert_node "after" parameter — auto-resolves sibling's
  parent and position for intuitive node insertion
- feat(ai): add move_node and insert_node to CRUD tool set
- feat(ai): add Chinese keywords (增加/添加/插入) to design intent detection
- fix(ai): insert_node uses addNode directly for existing parents instead
  of streaming pipeline, fixing parent resolution

* feat(ai): route CRUD intents to lightweight prompt and tool set

CRUD operations (read/update/delete) now get a focused system prompt
without design generation instructions, and use getCrudToolDefs()
(which includes insert_node and move_node) instead of the full design set.

* fix(agent,mcp): submodule update + MCP tool improvements

- Update agent-native submodule (tool_exec reset, HTTP error diagnostics)
- Improve MCP tool descriptions and parameter schemas
- Enhance agent.ts error handling

* feat(ai): add PenNode examples to CRUD prompt for complete node generation

The CRUD system prompt now includes button and text node examples
showing the full structure (fills, children, icons, layout) so models
generate complete nodes instead of empty frames.

* Update agent-native submodule to commit f9633a8, ensuring compatibility with recent changes and improvements in the agent's functionality.

* fix(ci,agent): generate skill-bundle before type check + fix moveNode arg

- Add `bun run cli:bundle-skill` step in CI before `tsc --noEmit` so
  skill-bundle.json exists when type-checking the CLI
- Fix moveNode index parameter: default to -1 when undefined

* fix(ci): add pen-engine and pen-react to npm publish workflow

Insert pen-engine and pen-react in topological order between
pen-renderer and pen-mcp so they are published before pen-sdk.

* docs: add MIT LICENSE and README to all packages

- Add MIT LICENSE to pen-ai-skills, pen-core, pen-engine, pen-figma,
  pen-mcp, pen-react, pen-renderer, pen-sdk, pen-types
- Add README.md to pen-engine, pen-react, pen-mcp, pen-ai-skills

* docs: comprehensive package metadata, README, CLAUDE.md, and LICENSE

- Add author, license, repository, bugs, homepage to all package.json
- Homepage points to each package's own directory on GitHub
- Rewrite README for pen-engine, pen-react, pen-mcp, pen-ai-skills
  with full API docs, usage examples, and feature tables
- Add CLAUDE.md to pen-types, pen-core, pen-engine, pen-figma,
  pen-mcp, pen-react, pen-renderer, pen-sdk
- Add MIT LICENSE to all packages
- Update root CLAUDE.md with index of all sub-CLAUDE.md files
- Fix git URL from nicepkg → ZSeven-W

* docs: package metadata, README, LICENSE, and CLAUDE.md for all packages

- Add author, license, repository, bugs, homepage to all package.json
- Homepage points to each package's own directory on GitHub
- Rewrite README for pen-engine, pen-react, pen-mcp, pen-ai-skills
  with full API docs, usage examples, and feature tables
- Add MIT LICENSE to all packages missing it
- Add CLAUDE.md to pen-types
- Update root CLAUDE.md with index of all sub-CLAUDE.md files
- Fix git URL from nicepkg to ZSeven-W

* docs: rewrite README for pen-core, pen-figma, pen-renderer, pen-sdk

Comprehensive READMEs with full API reference, usage examples,
feature tables, and architecture overview for each package.

* feat(acp): acpAgents store — persist, hydrate, CRUD actions

* feat(acp): pen-acp package + agent settings types + store

- pen-acp/types.ts: AcpAgentConfig, AcpAgentInfo, AcpConnectResult, AcpConnectionState
- pen-acp/client.ts: connectAcpAgent (local stdio + remote WebSocket), disconnectAcpAgent
- pen-acp/event-adapter.ts: acpUpdateToSSE (ACP session/update → SSE events)
- agent-settings.ts: AcpAgentConfig type, widen ModelGroup/GroupedModel.provider
- agent-settings-store.ts: acpAgents persist/hydrate/CRUD + acpConnectionStatus

* fix(acp): remove unused type imports in client.ts

* feat(acp): ACP agent settings UI — form, cards, connect/disconnect

* feat(acp): add AcpAgentSection to Agents settings tab

* refactor(agent): AgentSession as discriminated union (native | acp)

* feat(acp): connection manager — connect, disconnect, cleanup

* feat(acp): connect/disconnect API route

* feat(acp): ACP branch in agent result handler

* fix(agent): use NativeAgentSession type for runDelegateMember

* feat(acp): ACP prompt SSE stream in agent endpoint

* feat(acp): ACP agents in model list + request routing

* i18n(acp): add ACP agent translation keys for all 15 locales

* fix(i18n): translate ACP keys for all 15 locales + fix missing key references

- zh.ts/zh-tw.ts: proper Chinese translations (Agent not translated)
- All other locales: translated from English placeholders
- Fix acp.add → acp.addAgent, acp.disconnected → acp.notConnected

* chore: bump agent-native submodule — surface upstream HTTP errors

* feat(acp,build,codegen): comprehensive fixes for ACP integration + prod build

ACP agent integration:
- Rewrite system prompt to enforce layered design pipeline (get_design_prompt
  → design_skeleton → design_content → design_refine) for higher quality output
- Use correct PenNode field names: content for text, iconFontName for icons
- Strict JSON rules to prevent empty-key / trailing-comma / smart-quote errors
- Prefer icon_font over path icons (standalone MCP has no hooks registered)
- Auto-start MCP server before ACP session (lazy bootstrap)
- Auto-reconnect ACP on stale connection (dev server restart scenario)
- Auto-approve tool permission requests (trust model: user configured agent)
- Use type: 'http' + headers: [] for MCP server config (SDK schema requirement)
- Persist ACP connections via globalThis so they survive Vite HMR

Build / packaging:
- Place agent-native under server/node_modules for Nitro to resolve at runtime
- Copy agent_napi.node to napi/ as extraResource
- Kill detached MCP server on Electron quit (before-quit + dev SIGINT handlers)
- Capture drag-dropped filesystem path via webUtils.getPathForFile so recent
  files entries are clickable after reopening

Codegen:
- Compact JSON (no indent) + strip noise fields (id, parentId, default rotation
  /opacity/visible, layout-managed x/y) to reduce request body size by 60-70%
  so proxies don't reject with 403 'Request not allowed'

MCP batch_design robustness:
- splitOperations tracks bracket/quote balance → multi-line JSON now works
- Auto-normalize fill/stroke shorthand forms
- Collect per-line errors instead of aborting whole batch
- Repair empty keys, trailing commas, smart quotes in JSON
- Bindless I(...) form supported (auto-generates binding)

UI:
- ModelDropdown / ChatInput handle ACP model icons (Plug)
- Reset streaming state + abort controller on ACP error path
- Strip h3 JSON error wrapper so chat shows clean error messages
- ACP agent settings form + cards + connect/disconnect

* fix(types): resolve TS errors in CI typecheck

- acp-connection-manager.ts: correct relative import path (utils/ → src/types)
- ai-chat-handlers.ts: cast currentProvider to AIProviderType at design-generator callsites
- ai-chat-panel.tsx: explicitly type groups as ModelGroup[] so 'acp' string fits the widened union
- acp-agent-settings.tsx: cast window through unknown for Record lookup
- electron.d.ts: add getPathForFile to ElectronAPI declaration
- builtin-provider-presets.ts: drop now-redundant config.preset !== 'custom' check (handled by early return)
- pen-acp/client.ts: cast Writable/Readable.toWeb to typed Streams; coerce nullish agentInfo fields to undefined

---------

Co-authored-by: Rais <vdcoolzi@gmail.com>
2026-04-13 21:30:23 +08:00