Commit graph

24 commits

Author SHA1 Message Date
Fini dc73885015 feat(ai): add_otp_input_v0 — verification code input (65th tool)
Fills the auth-flow gap: 2FA / PIN / phone-verification codes.
Horizontal row of N square slots (4..8), one digit per slot.
Renders three states per caller intent:

  - blank (no `digits`): all slots empty, `focused_index` marks
    the currently-typing slot with an accent-color 2px outline
  - partial: first M slots filled with digit text, slot M+1
    focused, rest empty
  - full: all N slots filled (final submittable state)

Filled slots get role=otp-slot-filled + slate-700 border + 20/600
digit text. Focused empty slot gets role=otp-slot-focused +
2px accent border. Blank unfocused slots get role=otp-slot +
1px slate-300 border.

Wired through all standard points — schema into ext-2 (shorter
shard) + shim + SERVER_BUILDERS + parity CASES + contract
allow-list + elements.md decision tree + triggers + minimal
usage for each state.

Handler test covers 8 cases: registration + defaults (6 blank
focused-first) + partial state / full state / length clamp low
(< 4 → 4) + length clamp high (> 8 → 8) + accent color override
+ bogus parent_id rejection.
2026-04-22 21:48:05 +08:00
Fini 45214efb70 feat(ai): add_upload_dropzone_v0 — file drop zone (64th tool)
Fills a real gap in the element-tool family: upload / drag-and-
drop surfaces. Dashed border + cloud icon + two-line instruction
("Drop files to upload" / "or click to browse") — the classic
pattern from every modern file-upload UI.

Deliberately structurally similar to add_empty_chart_v0 (dashed
border + icon + title/subtitle) but semantically distinct:
  - empty_chart = "chart widget will render when data arrives"
    (320×200, icon chart-typed)
  - upload_dropzone = "users drop files here" (480×200, icon
    semantic: upload-cloud / upload / file-up)

elements.md routes them by intent, and the tool descriptions
cross-reference each other to prevent the AI from picking the
wrong one on ambiguous prompts.

Wired through all the standard points per the add-new-tool
checklist: builder + handler + schema (into ext-1, the shorter
shard) + shim + SERVER_BUILDERS + parity CASES + contract
allow-list + elements.md decision tree + keyword triggers +
minimal usage. Handler test covers 5 cases: defaults, dashed
stroke, overrides, size clamping, bogus parent_id rejection.
2026-04-22 21:41:13 +08:00
Fini dd5d156160 feat(ai): add_modal_shell_v1 — first theme-aware MCP tool (63rd, v1-family debut)
Wires the buildModalShellV1 pen-core builder through the full MCP
toolchain — handler + schema + dispatch + shim + SERVER_BUILDERS
+ parity CASES + handler tests + elements.md skill — so external
MCP clients (Claude Code / Codex / Gemini CLI) can call
add_modal_shell_v1 as a first-class tool alongside the 62 v0
tools.

Three theme variants exposed via `theme` param (enum [light, dark,
system]):
  - omitted / `'light'`: byte-parity with add_modal_shell_v0
    (same hex, same structure, same role tree)
  - `'dark'`: hardcoded dark palette (#1E293B card, #F1F5F9 title,
    #94A3B8 muted). No \$refs needed.
  - `'system'`: emits \$color-surface / \$color-text-primary /
    \$color-text-muted refs. Caller MUST have run
    applySemanticPalette(doc) first or refs resolve to undefined
    (documented in schema description).

Scrim stays #000000 in ALL themes — modal backdrops are a dim
effect, not a themeable surface. Pinned by handler test.

9 handler test cases cover: registration + schema shape +
required[title] + theme variants + scrim invariant + bogus
parent_id rejection. Parity test added a CASES entry with
\`theme:'dark'\` args (exercises the theme branch in both
shim and server paths).

elements.md gained:
  - §46b decision-tree entry pointing to the v1 variant
  - Trigger list entry for dark-mode / theme-aware prompts
  - Minimal usage showing \`theme:'dark'\` + \`theme:'system'\`

This is the reference implementation for the remaining 9 theme-
aware v1 tools in the top-10 offenders list (empty-chart,
chip-input, toast, pagination, notification-row, image-placeholder,
faq-item, comment, checkbox — per dark-theme-audit §offenders).
2026-04-22 11:00:00 +08:00
Fini 4a75b53c45 feat(ai): add_date_picker_v0 — date input closed state (62nd tool)
Adds an N-tool for the labeled date input + calendar-icon trigger.
Emits ONLY the CLOSED state; the open month grid lives in
add_calendar_grid_v0 and is typically shown inside a popover,
not stacked directly below. Two visual states:

- placeholder (no value): slate-400 "Select date" + calendar icon
- populated (value): slate-900 date text + calendar icon

`clearable: true` adds a small X affordance to the right of the
value (only when value is present — no-op for the placeholder
state since there's nothing to clear). `required: true` appends
" *" to the label.

Keeping closed + open as separate tools is intentional: AI specs
often ask for only the closed trigger inside a form, and a single
combined tool would either force an unwanted grid or require a
mode flag that splits the parameter surface. Separate narrow tools
compose cleanly via batch_design when a designer DOES want both.

Wired through all 3 paths + parity/contract/design-prompt tests.
Handler test covers 7 cases: placeholder state fills, populated
value fills, clearable X behaviors (both present + absent value),
custom placeholder override, required marker, bogus parent_id
rejection.
2026-04-22 09:45:00 +08:00
Fini 1b4f362d59 feat(ai): add_action_menu_v0 — context/kebab dropdown panel (61st tool)
Adds an N-tool for the floating card that drops from a "⋯ more"
button or appears on right-click. Emits the OPEN state: vertical
stack of padded icon+label rows in a white card with subtle stroke
and shadow. Positioning and show/hide are caller concerns (same
philosophy as add_modal_shell_v0 / add_toast_v0).

Destructive items (destructive=true) render in red with role
`action-menu-item-destructive` so renderers can style the hover
state separately. divider_before=true on any item (except first,
where it's ignored) inserts a 1px hairline above — useful for
"Edit / Share / Report / Delete" grouping patterns.

Wired through all 3 paths + parity/contract/design-prompt tests.
Handler test covers 7 cases: simple list, destructive red fill,
divider between groups, leading-divider ignored, label-only no
icon, width clamp, bogus parent_id rejection.
2026-04-22 09:40:00 +08:00
Fini 97343c1d74 feat(ai): add_empty_chart_v0 — chart-slot empty state (60th tool)
Adds an N-tool for the "no data yet" tile that sits in the exact
footprint where a real chart would go. Default 320×200 matches the
line/bar chart default footprint; dashed border + slate-50 fill +
slate icon signal "chart slot, currently empty". Caller can hint
at the widget type via icon ("line-chart" / "pie-chart" / default
"bar-chart-2").

Intentionally separate from add_empty_state_v0 — that tool is for
inbox/onboarding/no-results full-page empties (has optional CTA,
no dashed border). add_empty_chart_v0 reads as "chart widget is
live, just lacks data yet" rather than "nothing to show on this
screen at all".

Wired through all 3 paths + parity/contract/design-prompt tests.
Handler test covers 6 cases: defaults + dashed stroke + icon
override + size clamping + title/subtitle override + bogus
parent_id rejection.
2026-04-22 09:35:00 +08:00
Fini 6102d54cd9 feat(ai): add_chip_input_v0 — tag / multi-select input (59th tool)
Adds an N-tool for the "variable-N pill-plus-cursor" pattern: a
labeled form control that holds N removable tag pills followed by
an inline placeholder caret. Wrap layout (layoutWrap=wrap) so
chips flow onto additional rows as they accumulate — a horizontal
fit_content row would clip after ~4 chips.

Each chip: pill (cornerRadius=16, slate-100 fill, padding 10/4/6/6
L/R/T/B) + label + 14×14 lucide "x". Default caret placeholder is
"Add tag…" when chips is empty; caller overrides with `placeholder`
(e.g. "Enter emails" for recipient lists).

Wired through all 3 paths with matching handler test (7 cases) +
parity tests auto-picking-up the entry. elements.md: decision tree
§54, trigger list, minimal usage (populated + empty).
2026-04-22 09:30:00 +08:00
Fini e948a081d2 feat(ai): add_faq_item_v0 — accordion/FAQ item (58th tool)
Adds an N-tool for one row in a FAQ list. Collapsed (default):
bold question + chevron-right header. Expanded (expanded=true):
chevron-down + multi-line answer paragraph beneath. Optional
show_divider draws a 1px slate-200 rectangle at the bottom for
visual separation between items (no implicit padding — caller
stacks in a vertical parent).

Wired through all 3 paths (pen-core buildFaqItem + pen-mcp handler
+ apps/web shim + Nitro SERVER_BUILDERS); both parity tests and
the stale-integration guard auto-pick-up the entry. Handler test
covers 5 cases: collapsed default, expanded with answer, expanded
without answer (guards against undefined), show_divider hairline,
bogus parent_id rejection.
2026-04-22 09:25:00 +08:00
Fini fa69986fc8 feat(ai): add_pagination_v0 — Google-style pagination bar (57th tool)
Adds an N-tool for list/table footer pagination: row of page-
number pills flanked by optional prev/next chevron buttons. Active
page renders filled with the accent color, inactive pages are
ghost. Long ranges collapse with "…" Google-style (always show 1
and total, plus a ±siblings window around current).

Wired through all 3 paths: pen-core buildPagination + pen-mcp
handler + apps/web browser shim + Nitro SERVER_BUILDERS. Both
parity tests (shim-server-parity, element-tool-registry-parity)
pick up the entry automatically. Handler test covers 7 cases:
small range no ellipsis, 10-page ellipsis, start-edge current,
accent override, show_arrows=false, total=1 single pill, bogus
parent_id rejection.

Also updates packages/pen-ai-skills/skills/phases/generation/
elements.md: decision tree §52, keyword triggers (pagination /
page nav / 分页 / 分页条), minimal usage example. The stale-
integration guard (design-prompt-elements.test.ts) now passes.
2026-04-22 09:20:00 +08:00
Fini 6e1d523514 feat(ai): add_video_placeholder_v0 — video embed placeholder (56th)
Dark slate (#334155) box + centered white play icon + optional
caption. Default 320×180 for 16:9. The "future video embed"
affordance — semantically distinct from add_image_placeholder_v0:
dark bg + play icon reads as "play me later", not "picture coming".

Play affordance is a lucide `play` icon_font, NEVER a hand-drawn
path triangle (classic LLM anti-pattern for video placeholders).
Regression test locks that invariant.

Wired across all three paths + elements.md entry (44b) + keyword
map + example + parametric test CASES in 8 files. Handler test
has 6 assertions including the path-vs-icon_font anti-pattern
guard.

Tool count: 55 → 56. Test count: 3225 → 3241 (+16).
2026-04-22 09:05:00 +08:00
Fini 053e9d0b1f feat(ai): add_metric_comparison_v0 + add_notification_row_v0 (54th, 55th)
- add_metric_comparison_v0: KPI cell with trend. label above + big
  value + optional arrow icon + change amount. trend enum (up/down/
  flat) drives arrow icon (trending-up/-down/minus) + color (emerald/
  red/slate). Distinct from add_metric_row_v0 (scroll row of label+
  value cells without trend affordance). Required: label + value.
- add_notification_row_v0: leading icon + (title + optional
  timestamp + optional unread red dot) header + optional body
  preview. Distinct from add_list_row_v0 which has no timestamp
  or unread affordance. Required: title only.

Both wired across all three paths + elements.md ("Analytics / KPIs"
and "Notifications" sections) + keyword map + examples + 2 corpus
prompts in ab-v1/ (dashboard-revenue-trend, mobile-notification-item).

Fixed in same turn: ab-v1 file that was created with the wrong
filename got renamed.

Tool count: 53 → 55. Test count: 3180 → 3225 (+45).
2026-04-22 09:00:00 +08:00
Fini fb306dcf79 feat(ai): add_spinner_v0 + add_tooltip_v0 (52nd, 53rd tools)
- add_spinner_v0: static loading spinner — full ring (track) + 270°
  arc (active). Sits at size=32 default, clamped 16..128. Two
  ellipses at SAME origin with DIFFERENT sweep ranges — NOT the
  "stacked ellipses for ring" anti-pattern (rewriteLlmAntiPatterns
  only fires when both are full-sweep duplicates).
- add_tooltip_v0: small dark pill (#111827) + white text for
  hover hints. position param ("top"/"bottom"/"left"/"right")
  encodes a role hint (`tooltip-top` etc.) for downstream position
  logic; visual body is identical. NO arrow pointer (pen-core has
  no clean triangle primitive — caller composes via batch_design
  rectangle + rotate if needed).

Wired across all three paths. elements.md adds "Feedback / loading"
section (#48, #49). ab-v1 corpus +1 prompt (mobile-help-tooltip);
spinner omitted from corpus for now — the prompt wording is too
ambiguous for "obvious" difficulty.

Tool count: 51 → 53. Test count: 3138 → 3180 (+42).
2026-04-22 08:55:00 +08:00
Fini baa414bded feat(ai): add_status_badge_v0 — semantic status indicator (51st tool)
Small colored dot + short label: "● Online" / "● Busy" / "● Error"
pattern. Distinguished from the more general add_badge_v0 (just a
pill label) by always having a dot.

tone enum picks dot color:
- success → emerald #10B981
- warning → amber   #F59E0B
- error   → red     #EF4444
- info    → blue    #3B82F6
- neutral → slate   #94A3B8 (default)

Dot uses `frame + cornerRadius=4`, NEVER `ellipse` — an 8×8 ellipse
is the classic "status dot via stacked ellipses" anti-pattern bait.
Keeping it a frame stays clean of rewriteLlmAntiPatterns. Regression
test locked in pen-mcp/add-status-badge-v0.test.ts.

Wired across all three paths + elements.md entry + keyword map +
examples + ab-v1/dashboard-server-status.yaml corpus prompt +
parametric builder test CASES in 7 files.

Tool count: 50 → 51. Test count: 3114 → 3138 (+24).
2026-04-22 08:50:00 +08:00
Fini 54131c03c0 feat(ai): 3 new element tools — image_placeholder / comment / modal_shell (48th-50th)
Fills 3 common UI gaps the 47-tool set didn't cover:

- add_image_placeholder_v0: gray box + centered icon + optional
  caption. The "future image slot" affordance. Separate from G()
  (which fetches real images). Emits frame+fill, NEVER image node
  (empty image renders as broken indicator).
- add_comment_v0: avatar + (author + timestamp) header + body.
  Social / UGC / feedback unit. Does NOT handle replies / likes /
  action menu — compose via batch_design.
- add_modal_shell_v0: dimmed scrim + centered card (rounded,
  shadowed) + title + optional subtitle. "Shell" in the name is
  deliberate — this is chrome only; body content goes into the
  `modal-shell-card` role via a follow-up insert.

Wired across all three paths (pen-core buildX + pen-mcp handler +
routes + schema + apps/web shim + Nitro SERVER_BUILDERS) + elements.md
decision-tree entries + keyword map + examples + 3 pen-mcp handler
tests (22 cases total) + 3 A/B v1 corpus prompts in ab-v1/ +
parametric builder test CASES auto-extended in 9 files.

Milestones:
- Tool count: 47 → 50
- Test count: 3045 → 3114 (+69)
- A/B v1 corpus: 5 → 8 prompts
2026-04-22 08:30:00 +08:00
Fini 59f25dac82 feat(ai): add_chart_line_v0 + add_chart_pie_v0 — two chart skeletons (46th, 47th)
chart_line: polyline through N data points (normalized to max),
optional dots at each vertex. Emits a `path` node with computed SVG
`d`="M x y L x y …" + N `ellipse` dots. fit_content width = values
× point_spacing.

chart_pie: N colored slices via ellipse `startAngle`/`sweepAngle`
arc support. NOT the "stacked full ellipses" anti-pattern — each
slice has a UNIQUE sweep range (sums to 360°). Supports donut cut-
out via `inner_radius_ratio`. Default 6-color palette rotates.
All-zero input throws (degenerate chart can't be drawn).

Wired across all three paths + elements.md decision-tree entries +
keyword map + examples. Both use layout=none (absolute positioning
for vertices / slices stacked at origin).

Tests:
- pen-mcp: 8 cases chart_line + 8 cases chart_pie (height math,
  clamp bounds, custom colors, donut, error paths, id uniqueness)
- Parametric coverage auto-extended (+2 cases each in 9 files)
- Total delta: 2979 → 3027 passing (+48).

Closes #45 + #46; covers part of #50 (charts entries).
2026-04-22 06:35:00 +08:00
Fini 912ed1cd5a feat(ai): add_select_v0 — dropdown display element tool (45th)
Dropdown/picker closed-state display. Same label-above-input shape
as add_form_field_v0 with:
- always-present trailing chevron-down icon
- when `value` is set: black value text + chevron
- when absent: placeholder text styled gray (#94A3B8) + chevron
- justifyContent=space_between pushes the chevron to the right edge

Explicitly NOT modeled: open-menu state (dropdown list). An open
dropdown needs absolute positioning + scrim + per-option states
that belong in a different builder; compose via batch_design for now.

Wired across all three paths + elements.md keyword map + examples.
Tests:
- pen-mcp: 7 new cases (value/placeholder rendering, custom
  trailing_icon, required suffix, id uniqueness, parent_id rollback)
- Parametric coverage: auto-extended (+1 case each in 9 files).
  Total delta: 2955 → 2979 passing.

Closes #48; covers part of #50 (select entry).
2026-04-22 06:25:00 +08:00
Fini a08866cdda feat(ai): add_skeleton_v0 — loading placeholder element tool (44th)
N stacked gray rectangles (cornerRadius=4) mimicking future text
lines while content fetches. Parameters:
- rows (1..20, default 3)
- row_height (4..48, default 16)
- row_gap (0..32, default 12)
- last_row_short (default true): last row renders at ~60% width
  (220px) to suggest an unfinished paragraph — more organic than
  uniform stripes. Disabled when rows=1 (looks wrong otherwise).

Wired across all three paths (pen-core / pen-mcp / apps/web shim +
Nitro SERVER_BUILDERS). elements.md PREFER list + keyword map +
example lines updated.

Tests:
- pen-mcp: 8 new cases (default shape, rows/height/gap, clamp
  bounds, last_row_short=false, single-row corner case, id
  uniqueness, parent_id rollback)
- Parametric coverage: auto-extended (+1 case each in 9 files).
  Total delta: 2931 → 2955 passing.

Closes #49; covers part of #50 (skeleton entry).
2026-04-22 06:15:00 +08:00
Fini 552fecc504 feat(ai): add_textarea_v0 — multi-line input element tool (43rd)
Same label-above-input shape as add_form_field_v0 but the input
grows vertically by `rows` (default 4, clamped 2..12) for notes /
bio / feedback use cases. Input frame layout is vertical with
placeholder top-aligned, matching native iOS/Material behavior.

Wired across all three paths:
- pen-core: `buildTextarea` + TextareaParams in element-builders
- pen-mcp: `handleAddTextareaV0` + tool schema in element-tool-defs-ext
- apps/web: shim + Nitro SERVER_BUILDERS entry
- elements.md: PREFER list + keyword map + 2 example lines

Tests:
- pen-mcp: 8 new cases in add-textarea-v0.test.ts (height math,
  rows clamp, required suffix, placeholder wiring, id uniqueness,
  parent_id rollback)
- Parametric builder tests auto-extended (+1 case each in 9 files):
  layout smoke, post-process idempotency, normalize preservation,
  performance, role coverage, anti-patterns clean, detectors
  clean, shim-server parity. Total delta: 2907 → 2931 passing.

Closes #47; covers part of #50 (textarea entry).
2026-04-22 06:05:00 +08:00
Fini 9f7c8f7b54 feat(ai): complete embedded element-tool coverage to 42/42
Final 11 builders moved to pen-core: rating_stars, carousel_dots,
link, kbd, price, quote_block, code_block, color_swatch, chart_bars,
timeline, calendar_grid. pen-mcp handlers delegate; shim + Nitro
SERVER_BUILDERS now match the full 42-tool pen-mcp catalog.

With this batch the embedded orchestrator can execute any element
tool the AI emits — no more fallback-to-batch_design routing on
elements.md names that happened to be outside the shim registry.
The "advertised vs executable" asymmetry is closed: elements.md
catalog = pen-mcp handler set = shim set = SERVER_BUILDERS set.

Test suite updated: the "unsupported tool short-circuits before
HTTP" case now uses a fictional name (add_fictional_future_v1)
since every real add_*_v0 is now wired. 1907/1907 pass, zero
pen-mcp handler behavior regressions (builders are byte-identical
to the local tree build they replaced).
2026-04-22 00:19:30 +08:00
Fini ea6c571cb1 feat(ai): extend embedded element-tool coverage to 21 (+controls batch)
Batch B — five controls (switch, checkbox, radio, tabs,
segmented_control) moved to pen-core builders; pen-mcp delegates;
embedded shim + Nitro SERVER_BUILDERS gain direct coverage. 311/311
handler tests still pass unchanged. AI generation under the flag
can now emit common form controls without batch_design fallback.
2026-04-22 00:03:29 +08:00
Fini 804709d1f9 feat(ai): extend embedded element-tool coverage to 16 (+atoms batch)
Batch A — six atomic/single-node tools moved from pen-mcp-local to
pen-core builders so the embedded shim + Nitro SERVER_BUILDERS
actually cover them: divider, badge, avatar, icon_button,
icon_label, stat_grid. pen-mcp handlers delegate; 311/311 handler
tests still pass (zero behavior change). Dispatcher short-circuit
list now names 16 tools instead of 10 — AI generation under the
flag can emit these directly without bouncing through batch_design
fallback.
2026-04-22 00:01:35 +08:00
Fini 3a6a0b6dcf feat(ai): make advertised batch_design fallback actually executable
ELEMENT_TOOL_OUTPUT_FORMAT tells the AI to emit
`<op_tool>{"name":"batch_design", ...}` when no element-tool fits.
Prior Nitro implementation hard-coded a 501 for any DSL payload,
so the FALLBACK branch advertised to the AI was a lie — any AI
that actually took the guidance would see its generation fail.

Fix extracts pen-mcp's `handleBatchDesign` pure executor
(`runBatchDesignDsl`) from the file-I/O wrapper and exposes it on
the package's main barrel. Nitro's `/api/mcp/exec-tool` now
accepts `{dsl}`, runs the executor against a clone of the
sync-state doc (no file I/O, no post-processing hooks — those
belong to the pen-mcp server process), and calls setSyncDocument
to broadcast the result via SSE. Response shape gains
`insertedNodeIds: string[]` so batch inserts (multiple root
bindings in one DSL) surface all their root nodes to the
orchestrator's progress accounting, not just the first.

Client dispatcher updated to prefer the array form with fallback
to the legacy single-id field. Adds a test asserting the
dispatcher actually calls fetch when taking the DSL fallback
(proves the route wires end-to-end). JSDoc in dispatcher +
endpoint updated so code and docs agree.

handleBatchDesign's external behavior is unchanged — it still
opens / post-processes / saves around the refactored executor;
311/311 pen-mcp tests pass unchanged.
2026-04-21 22:25:10 +08:00
Fini 4731a73b79 fix(ai): dispatcher short-circuits + prompt names embedded coverage
elements.md's 42-tool catalog is authored for external MCP clients
that talk to pen-mcp's full handler set via stdio/HTTP. The embedded
orchestrator (this runtime) can only execute tools with BOTH a
client-side shim (element-tool-shims) AND a matching Nitro
SERVER_BUILDERS entry — currently 10 of the 42. Prior code
advertised the full catalog to the AI and promised HTTP fallback
coverage without restriction, so 32/42 tool names would silently
route through to a 404 → surfaced-error path.

Fix:
- Export SUPPORTED_EMBEDDED_ELEMENT_TOOLS from the shim module as
  the canonical covered list. Shim + Nitro registries stay in sync
  by convention; extending coverage requires updating both.
- Dispatcher short-circuits on tool names not in the list — no
  wasted HTTP roundtrip, diagnostic carries the covered-list so the
  caller can route to batch_design.
- ELEMENT_TOOL_OUTPUT_FORMAT in orchestrator-sub-agent names the
  available subset inline so the AI knows which add_*_v0 it can
  emit and when to fall back to batch_design.
- JSDoc in dispatcher, shim module, and exec-tool endpoint updated
  to reflect actual behavior (insertStreamingNode path, embedded-
  vs-external coverage asymmetry) instead of the stale "HTTP
  fallback covers everything" story.
- New test locks the short-circuit: calling an uncovered tool name
  (e.g. add_divider_v0) must not attempt fetch.

Real follow-up work is still to extract the remaining ~32 pen-mcp
tool tree-build functions into pen-core, shim them, and extend
SERVER_BUILDERS. Until then, the routes advertised to the AI
actually match what the runtime can execute.
2026-04-21 22:14:25 +08:00
Fini c58106c38e feat(ai): Nitro /api/mcp/exec-tool HTTP fallback for element tools
Server-side apply path the dispatcher calls when the browser has
no shim for a given tool (or a caller explicitly wants server
state). POST `{name, arguments, default_parent_id?}` — builds the
tree via the SAME pen-core builders the client shim uses (so the
shape is byte-identical across paths), resolves target page /
parent (honoring explicit arguments.pageId + arguments.parent_id
with existence validation; falls back to body.default_parent_id
forwarded from the dispatcher, then page root), calls
setSyncDocument to broadcast via SSE, returns `{ok:true, document,
insertedNodeId}`. Client applies the response doc synchronously
via applyExternalDocument so the apply lands inside the dispatcher's
history batch without racing the SSE roundtrip.

Errors surface as structured 4xx: 400 for missing name, 404 for
unknown tool / parent_id / pageId / stale default_parent_id, 501
for batch_design DSL (deferred — needs document-manager adaptation)
and for real .op filePaths (they belong to pen-mcp's file-aware
handler). The "live://canvas" sentinel is accepted the same as
omitted filePath so callers don't get false negatives when
explicitly naming the default target.

insertNodeInTree index=Infinity (append) matches the streaming
path's generation-order semantics. Default document-store.addNode
prepends (index=0) for user-created nodes on top of the layer
panel; AI generation wants each element to stack after earlier
siblings so multi-call output renders top-to-bottom as emitted.
2026-04-21 22:04:39 +08:00