Commit graph

801 commits

Author SHA1 Message Date
Danila Poyarkov 8a9059479f Baseline: 246 diffs (1 vis, 0 text, 10 fills, 161 radius, 74 size, 99 unmatched)\n\nResult: {"status":"keep","total_diffs":246,"visibility":1,"text":0,"fills":10,"radius":161,"size":74,"unmatched":99} 2026-03-14 13:08:30 +03:00
Danila Poyarkov 8719546b16 Set up autoresearch for Figma import fidelity optimization 2026-03-14 13:07:27 +03:00
Danila Poyarkov d93c4752cb Fix DSD resolution for swapped instance children
After an instance swap, the DSD guidPath still references the
pre-swap slot's overrideKey (e.g. bold icon), but the actual child
is the swapped-in component (e.g. code-2). resolveOverrideTarget
couldn't find the target because the child's componentId no longer
matched the guidPath element.

When findNodeByComponentId fails, fall back to the single child
of the parent node — after a swap, it occupies the same slot.

Fixes cropped toolbar icons (bold, italic, underline, strikethrough,
link, lists, code) in the WYSIWYG editor.
2026-03-14 12:47:51 +03:00
Danila Poyarkov f70338d18e Use child shape for drop shadows on transparent containers
When a frame/instance has no visible fills, Figma renders drop shadows
using the first child's shape instead of the node's own rectangle.
This makes shadows follow rounded children (e.g. pill-shaped badges
inside a rectangular wrapper).

Pass the first visible child as shadowShapeChild to renderEffects
when the container has no visible fills.
2026-03-14 12:25:51 +03:00
Danila Poyarkov 49423e406a Clip children to rounded corners when clipsContent is true
clipsContent was always using clipRect (sharp corners), ignoring the
node's corner radius. Frames and instances with rounded corners now
use clipRRect, matching Figma's behavior where content is clipped to
the rounded shape.

Fixes avatar images appearing as squares inside circular containers.
2026-03-14 12:17:08 +03:00
Danila Poyarkov 7d9eb2931c Move font loading and layout into SkiaRenderer.prepareForExport
The headless render path duplicated font collection, text measurer
setup, and layout recomputation separately from the renderer. Move
this into SkiaRenderer.prepareForExport() so browser and headless
share the same code path for export preparation.

Remove the now-redundant loadFonts from CLI headless and the manual
setTextMeasurer/collectFontKeys/computeAllLayouts from headless-render.
2026-03-14 12:06:46 +03:00
Danila Poyarkov b9d6afcdbc Fix headless layout: set text measurer and recompute after font load
Layout was computed before fonts were loaded, so Yoga used a rough
character-width approximation instead of real CanvasKit text
measurements. Also, setTextMeasurer was never called in headless mode,
so even a second layout pass would still use the approximation.

Set the text measurer from the renderer after loadFonts, then
recompute layout before rendering.
2026-03-14 11:51:59 +03:00
Danila Poyarkov a2b56ef15a Fix headless font rendering and deduplicate CLI/MCP setup
CLI headless only loaded one 'Regular' style per font family and never
initialized the CanvasKit font provider, so all text rendered with
fallback glyphs or wrong weights.

Extract headlessRenderNodes/headlessRenderThumbnail into core's
headless-render.ts — handles CanvasKit init, renderer creation, font
provider setup, per-weight font loading via collectFontKeys, and
rendering in one call. Both CLI and MCP server now use this shared
module instead of duplicating the setup.
2026-03-14 11:45:38 +03:00
Danila Poyarkov d7945694dd Fix set_radius returning unserializable Symbol for mixed corners
When corners have different radii, node.cornerRadius returns
Symbol('mixed') which can't be serialized to JSON, breaking AI SDK
tool results. Return individual radius values instead.

Also remove unused initFontService import from MCP server.

Co-authored-by: Anton Soldatov <eddclyde@yandex.ru>
Closes #109
2026-03-14 11:35:51 +03:00
Danila Poyarkov 0d33a14940 Skip variable fonts from local font access, use Google Fonts instead
CanvasKit-WASM renders variable fonts at their default weight (400)
regardless of the requested FontStyle weight. This caused all Inter
text in Chrome to appear as Regular instead of Medium/SemiBold/Bold
when Inter was installed locally as a variable font.

Detect variable fonts by checking for the fvar table in the font binary.
When found, skip the local font and fall through to Google Fonts, which
serves per-weight static files that CanvasKit handles correctly.

Also extract loadLocalFont() and registerAndCache() helpers to reduce
loadFont() complexity (was 21, now under limit).
2026-03-14 11:31:22 +03:00
Danila Poyarkov 72ec3ee576 Fix text/property overrides clobbered by second transitive sync
The second propagateOverridesTransitively (for component property
changes) was overwriting nodes that had explicit symbolOverride values
(text, fills, etc.) set in the first pass. The second sync's skip set
only contained propModified nodes, not the original symbolOverride
targets.

Pass overriddenNodes as a 'protect' set to the second sync. These
nodes are skipped at both the top-level clone check and during
recursive syncChildrenDeep.

Fixes: Step text ('Details'/'Terms'/'Members'), Input placeholder
('Enter name'), Tooltip title ('Bold') in gold-preview.fig.
2026-03-14 11:17:56 +03:00
Danila Poyarkov 4caf4281fd Fix file open dialog not working on first click in Safari
Replace hand-rolled input element creation with VueUse's useFileDialog,
which creates the input once and reuses it. Creating a new <input>
element and calling .click() immediately was being swallowed by Safari
on the first attempt.
2026-03-14 10:46:51 +03:00
Danila Poyarkov 512819c859 Disable autosave by default 2026-03-14 10:39:35 +03:00
Danila Poyarkov 204b225c64 Split instance-overrides.ts into focused modules
Break the 781-line single-function file into a kiwi/instance-overrides/
folder with domain files:

- types.ts — shared types and OverrideContext
- index.ts — thin orchestrator (99 lines)
- populate.ts — populate empty instances from components
- resolve.ts — componentId root, override target, repopulateInstance
- symbol-overrides.ts — apply kiwi symbolOverrides
- sync.ts — transitive sync through clone chains
- props.ts — component property assignments
- dsd.ts — derived symbol data (pre-computed sizes)

The shared closure state (6 maps) is now bundled in OverrideContext,
passed explicitly to all functions. No behavior change.

Fixes: complexity lint error (was 25, max 20), prefer-optional-chain,
nested ternary parentheses, import() type annotation.
2026-03-14 10:36:48 +03:00
Danila Poyarkov 2cecdff876 Fix component property override resolution through clone chains
Three changes to correctly resolve component property values:

1. Resolve empty symbolOverride values to component defaults.
   In kiwi binary, an empty ComponentPropValue {} (all fields absent)
   in a symbolOverride's componentPropAssignments means 'reset to
   the component's initialValue default'. This is distinct from
   {boolValue: false} which is an explicit false (field present).
   Build a propDefaults map from componentPropDefs and resolve empty
   values in symbolOverride context.

2. Remove componentId chain walk from applyInstanceDirectAssignments.
   Walking the chain applied base-component assignments to clones
   that should have different values from higher-level symbolOverrides.
   Only apply assignments from the instance's own kiwi data — clones
   inherit correct values via transitive sync.

3. Run a second transitive sync after componentProperties.
   The first sync propagates symbolOverride changes. Component
   property changes (visibility toggles, instance swaps) happen after
   and need their own propagation pass to reach visible-page clones.

Fixes Badge Avatar and Close-Icon visibility in gold-preview.fig
(was false, now correctly true). All 914 tests pass.
2026-03-14 10:21:30 +03:00
Danila Poyarkov a4524c88fb
Merge pull request #94 from open-pencil/feat/mcp-codegen
MCP codegen pipeline: tools, prompt, and pi skill
2026-03-13 22:56:23 +03:00
Danila Poyarkov ec741a9388 Remove dead files: codegen.md duplicate and planning doc
codegen.md is identical to codegen-prompt.ts (the TS export is what's
actually used — no code references codegen.md).

design-to-code-overview.md is a planning doc superseded by the actual
implementation. Same category as mcp-codegen-plan.md which was already
moved to discussion #99.
2026-03-13 22:29:14 +03:00
Danila Poyarkov f57c329eaf
Merge pull request #105 from mcdmags/fix/mcp-headless-export
Fix MCP headless export: font loading and window guard
2026-03-13 22:15:55 +03:00
mcdmags e3f0d2422d
Merge branch 'master' into fix/mcp-headless-export 2026-03-14 08:11:23 +13:00
Michael Magsuci 530af668bf Fix MCP headless export: font loading and window guard
Guard `window.queryLocalFonts` for non-browser runtimes (Bun/Node),
load fonts in MCP `export_image` handler before rasterization, and
ensure `stackChildAlignSelf` serialization block is properly scoped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 07:19:31 +13:00
Danila Poyarkov e954fdabff Treat FOREGROUND_BLUR as layer blur wrapping children
Figma's kiwi format uses FOREGROUND_BLUR for what the Plugin API calls
LAYER_BLUR — a blur that applies to the entire layer including children.

Previously FOREGROUND_BLUR was handled in the per-effect 'front' pass via
applyClippedBlur, which only blurred the node's own fills (drawn before
children). For nodes with no fills but with children (like groups),
the blur had no visible effect.

Now FOREGROUND_BLUR is handled alongside LAYER_BLUR in renderNode,
using a saveLayer that wraps both the node content and its children.
This fixes gradient blobs that should appear soft/blurred (e.g. the
decorative gradients in gold-preview.fig's Preview Thumbnail).
2026-03-13 21:09:43 +03:00
Danila Poyarkov 73668530b2 Don't clip content on group-like frames (resizeToFit)
Frames with resizeToFit=true are Figma's internal representation of
Groups. They auto-resize to fit children and never clip content,
regardless of the frameMaskDisabled flag.

Previously all frames with frameMaskDisabled=false were imported with
clipsContent=true, causing group-like frames to incorrectly clip their
children. This was visible as hard edges on gradient fills that should
extend beyond the group bounds.
2026-03-13 19:43:52 +03:00
Danila Poyarkov 4dbb583218 Fix radial, angular, and diamond gradient rendering
Use the full gradientTransform affine matrix via CanvasKit's localMatrix
parameter instead of manually extracting center and radius. The previous
approach ignored rotation and aspect ratio, producing circular gradients
where Figma specifies rotated ellipses.

Define gradients in Figma's unit gradient space (center 0.5,0.5, radius
0.5) and let the localMatrix (scale(w,h) * gradientTransform) handle the
transform to pixel coordinates. Also merge the GRADIENT_DIAMOND branch
with GRADIENT_RADIAL since they use the same shader.
2026-03-13 19:05:48 +03:00
Danila Poyarkov ef0b45f0c1 Fix instance swap override propagation through clone chains
Instance swap overrides (symbolOverrides with overriddenSymbolID) now
correctly propagate through multi-level clone chains:

- Track swapped instances in a dedicated set to distinguish real swaps
  from normal clone-chain componentId differences
- Reclone children when a source instance was swapped, copying name,
  componentId, and re-populating children from the swapped component
- Mark recloned targets as swapped too, enabling transitive propagation
  to deeper clone levels (e.g. Preview Thumbnail toolbar icons)
- Only rename on swap when the current name matches the root component
  name, preserving user-given names like 'Static Icon'
- Use getComponentRoot (with cycle detection) instead of manual chain walk
- Null-check source before deleting target children in recloneChildren
2026-03-13 18:58:33 +03:00
Danila Poyarkov c5e8bb431f Fix component override resolution for nested instances
- Fix population ordering: ensure source instances are populated
  before their clones (bottom-up via ensurePopulated recursive walk)
- Fix componentPropAssignment resolution: walk full componentId chain
  to find kiwi source with assignments, not just single-hop lookup
- Fixes visibility overrides not applied in deeply nested instances
- Fixes text content overrides not applied through clone chains
- Reduces missing nodes from 41 to 16 in gold-preview.fig test
2026-03-13 17:29:36 +03:00
Anton A S 402d791093 Add mobile UI example to system prompt 2026-03-13 14:56:50 +03:00
Anton A S aa6a8fa5a9 Guard set_layout on NONE frames, remove implicit auto-layout on childCount >= 2
- set_layout returns error if direction omitted on layoutMode: NONE frame
- Remove shouldEnableAutoLayout childCount >= 2 heuristic — require explicit flex prop
2026-03-13 14:54:12 +03:00
Anton A S d688d04bef Address PR #94 review: dedup, replace hand-rolled code, cleanup
- Replace calc.ts (150 lines) with expr-eval library
- Replace rgbLuminance with culori wcagLuminance
- Use @iconify/utils iconToSVG for viewBox normalization in iconify.ts
- Extract describe-shared.ts: findAncestorBackground, looksLikeButton, CONTAINER_TYPES
- Extract icon-render.ts: shared createIconFromPaths for create.ts and renderer.ts
- Inline codegen prompt as TS constant, remove runtime fs.readFile from MCP server
- Fix blur effect color alpha (a:1 → a:0)
- Encapsulate module-level mutable state in src/ai/tools.ts into RunState class
- Add network check to iconify tests (skip gracefully offline)
- Remove Caddyfile.dev and components.d.ts from tracking, add to .gitignore
2026-03-13 14:46:18 +03:00
Danila Poyarkov 00da8bde1b Add E2E test for fill color change (#100)
The fill color bug was caused by a missing `ref` import in FillPicker.vue,
introduced in 37da83a and fixed in 3b32b4d. Add a regression test that
opens the color picker, clicks the color area, and verifies the node's
fill color updates.
2026-03-13 13:40:12 +03:00
Anton A S 68b5cfcea1 Fix .fig export roundtrip: variable GUIDs colliding with document
Variable/collection IDs (e.g. 'VariableCollectionID:42:1') parsed via
stringToGuid produced NaN sessionID, which Kiwi encodes as 0 — same
GUID as the DOCUMENT node. This overwrote DOCUMENT in changeMap during
re-import, causing all pages to be lost.

Fix: generate unique GUIDs for collections, variables, and modes via
localIdCounter instead of parsing string IDs. Pass varIdToGuid map
through to serializeVariableBindings for consistent references.
2026-03-13 13:27:15 +03:00
Anton A S 1efb2e3734 Add diagnostic logging to fig export roundtrip test
Test has never passed since it was added (3b32b4d). Add console.log
to show page counts, node counts, and internal-only flags so the CI
log reveals what the export→parse roundtrip actually produces.
2026-03-13 13:18:56 +03:00
Danila Poyarkov 7c63fc3b22 Normalize colors at FigmaAPI proxy boundary
Root cause of 'Missing required field a' crash: fills/strokes/effects
set via the Figma Plugin API proxy were stored without color validation.
Colors missing the 'a' field (e.g. from AI tools or eval) caused Kiwi
encodeColor to crash on save/copy.

Fix: normalize colors in the proxy setters for fills, strokes, effects,
and gradient stops using normalizeColor() which defaults a to 1.
2026-03-13 13:04:29 +03:00
Anton A S 48abcf10e6 Fix all typecheck and lint errors (79 errors → 0)
Remove duplicate FigmaNodeProxy class from figma-api.ts — use the one
from figma-api-proxy.ts. The local class shadowed the imported proxy,
causing TS2440/TS2739 Symbol conflicts across all tool files.

Sync proxy's layoutSizingHorizontal/Vertical setters with the local
version (adds layoutGrow toggle on FILL, adds _parentLayout helper).

FigmaAPI now implements NodeProxyHost explicitly.

Other fixes:
- kiwi-serialize: safeColor returns Color (not {a?: number})
- chat-debug: eliminate nested ternary
- use-canvas-input: formatting

figma-api.ts: 1295 → 379 lines (under 600-line limit)
2026-03-13 12:42:07 +03:00
Anton A S 933511f43e Improve AI design quality: auto-infer flex, reduce false warnings, prompt patterns
Renderer: Frames with 2+ children auto-infer flex="col" when no explicit
flex/grid prop is set. Children with x/y keep ABSOLUTE positioning.
Extract shouldEnableAutoLayout() to stay under complexity limit.

Describe issues: suppress false positives that waste AI steps—
- checkNestedFlexWithoutFill: skip when parent uses justify=between/center
- checkGrowInHug/checkHugCollapse: skip when node will get concrete size
  from its own parent (grow>0, STRETCH, or effectively filling)
- checkSiblingHeightConsistency: skip when parent uses items=center

System prompt: add mandatory flex rule for multi-child frames,
tab-bar pattern, remove 'nested flex may collapse' from fix list.
2026-03-13 12:21:10 +03:00
Anton A S b5fd31b557 Fix Kiwi encodeColor crash on colors missing alpha field
Defensive safeColor() guard at serialization boundary — fills, strokes,
effects, and gradient stops all go through it before Kiwi encoding.
Also fix const→let for execResult in AI adapter (reassigned by step budget).
2026-03-13 11:48:43 +03:00
Anton A S d46b4b11a4 Fix layout axis mismatch, reduce describe noise, add bail-out strategy
Engine:
- layoutSizingVertical/Horizontal setters now use child's own layout
  direction (not parent's) to map primary/counter axis — matches Yoga
- FILL on cross-axis uses alignSelf:Stretch instead of flexGrow
  (flexGrow only works on main axis, caused even distribution bug)
- FILL on main axis auto-sets layoutGrow=1 for leaf nodes

This fixes the 20-step debugging spiral where AI set FIXED heights
on HORIZONTAL children inside VERTICAL parent but Yoga ignored them
(wrote primaryAxisSizing via parent axis, Yoga read counterAxisSizing
via child axis — never saw the FIXED value).

Describe noise reduction:
- Suppress 'absolutely positioned inside flex' — intentional for
  decorative overlays (selection handles, badges)
- Suppress 'justify=between with HUG' when node fills parent width
- Suppress 'fixed size + grow' when sizing is FILL (standard pattern)
- 'Inconsistent padding' requires 3+ siblings (not 2)

Prompt:
- Bail-out: delete and re-render after 2 failed set_* attempts
- eval restricted to operations, not layout debugging
2026-03-13 10:21:46 +03:00
Anton A S 19bef49d4e Show thinking indicator between tool calls during streaming
Previously the typing indicator only showed during 'submitted' status
(before first token). Once streaming began, gaps between tool calls
had no visual feedback — user saw silence and thought AI stopped.

Now isThinking is true when:
- status is submitted (waiting for first token)
- status is streaming and last part is a completed tool call or
  step-start (AI deciding what to do next)
2026-03-13 10:20:57 +03:00
Anton A S 417aaedcb3 Fix set_layout padding reset, enforce final describe after fixes
- set_layout: only change spacing/align/padding when explicitly passed.
  Previously spacing/align/padding defaulted to 0/MIN, wiping existing
  values when AI only wanted to change one property.
- search_icons: client-side slice to enforce limit (API ignores small
  limits, returned 31 results for limit=1)
- system-prompt: issues from describe are mandatory fixes, not advisory.
  Final describe after set_* calls is required workflow step.
2026-03-13 10:20:57 +03:00
Anton A S 1454afc036 Optimize AI token usage: ~61% reduction in per-step context
- System prompt: 19.5KB → 6.3KB (67% smaller), removed redundant
  rules, condensed spacing/typography/layout docs
- Tool tiers: CORE_TOOLS (34) vs EXTENDED_TOOLS (64), AI chat uses
  core only (~2.8K tokens vs ~6.5K). ALL_TOOLS for MCP/CLI unchanged.
- <Icon> element in render JSX: <Icon name="lucide:heart" size={20}
  color="#FFF" /> fetches and renders inline. Eliminates the
  search→fetch→insert×N→delete×N chain (~8 tool calls per design).
- search_icons default limit 20 → 5 (~300 fewer tokens per query)
- get_page_tree returns compact tree (id, type, name, w, h) instead
  of full recursive nodeToResult dump
- eval returns { ok: true } instead of null, saves a follow-up
  describe call
- renderTree/renderJSX/renderTreeNode now async to support Icon fetch
2026-03-13 10:20:56 +03:00
Anton A S fed6ea5734 Fix icon SVG primitives, harden describe issues, ban justify=evenly
- iconify: convert <circle>, <rect>, <ellipse>, <line>, <polygon>,
  <polyline> to path d-strings. Fixes lucide:search (missing circle),
  lucide:user (missing head), lucide:mail (missing envelope), etc.
- describe: show justify/items/sizing in layout description, detect
  icon role, recurse issues into children at every depth level
- describe-issues: split layout checks into describe-layout-issues.ts
- New checks: fill without flex parent, absolute in flex, nested flex
  without fill, duplicate sibling names, stroke color/weight mismatch,
  circular frame without clip, excessive nesting, same fill as parent,
  image placeholder without content, sibling height inconsistency,
  text wrapping detection, alignment issues (between+HUG, between<2,
  stretch with all fixed, equal children packed without gap)
- system-prompt: remove justify="evenly" — not supported
2026-03-13 10:20:55 +03:00
Anton A S 160d5474c9 Fix lint errors: eliminate all any casts, fix complexity and nested ternaries 2026-03-13 10:20:54 +03:00
Anton A S 9daf7ce6ea Fix icon vector positioning: zero x/y after appendChild 2026-03-13 10:18:47 +03:00
Anton A S 4d3a5341cd Fix lint errors: split describe.ts, fix TS errors, clean up type guards
- Extract issue detection from describe.ts into describe-issues.ts (609→167 lines)
- Fix graph not in scope in detectStructuralIssues (TS2304)
- Fix child.characters → child.text (TS2339, SceneNode uses text not characters)
- Remove unused DEFAULT_TEXT_LUMINANCE
- Split detectLayoutIssues (complexity 86→<20) into 8 focused helpers
- Fix non-null assertions in chat-debug.ts
- Fix nested ternary in chat-debug.ts
- Fix unnecessary optional chains in use-chat.ts, export-jsx.ts
- Use Map for MATH_FUNCTIONS in calc.ts (fixes no-unnecessary-condition)
- Use Partial<Record> for SKIP_ARGS and TokenEntry.values (honest nullable types)
- Remove redundant null checks in codegen.ts type guards

Errors: 32 → 1 (pre-existing applyTextOverrides complexity)
Warnings: 47 → 42
2026-03-13 10:18:45 +03:00
Anton A S 774cf6b90a Address PR #94 review feedback
- Remove docs/mcp-codegen-plan.md (internal planning doc, not for repo)
- Remove lol.fig test fixture left in repo root
- Fix inline type imports in codegen.ts — use top-level SceneGraph import
- Move get_codegen_prompt out of core/ALL_TOOLS into MCP server only
  (core must be browser-safe, node:fs is not available in browser)
- Remove skill from packages/mcp/skills/ — content saved to #97
- Slim down codegen.md overlap note (canonical reference stays there)

Closes review comments. Skill npm publishing tracked in #97.
2026-03-13 10:18:44 +03:00
Anton A S dd91b682d1 Fix icon positioning after insert, add emoji guidance to system prompt
- Zero icon frame x/y after appendChild to parent — fixes icons appearing
  at viewport origin instead of inside their parent container
- Add name→names fallback to insert_icon for backward compatibility with
  single-icon calls (name param still works, returns legacy format)
- Add emoji section to system prompt: emoji show as □ tofu on canvas,
  use insert_icon for UI iconography, emoji only in user content/reactions
- Strengthen flex requirement in layout docs: every parent with fill
  children MUST have flex='col'|'row'
2026-03-13 10:18:43 +03:00
Anton A S dec0a67e4f Batch insert_icon and search_icons, fetch_icons prefetch tool 2026-03-13 10:18:41 +03:00
Anton A S 733321fbb5 Add step budget warnings and Continue button for AI chat
- Track per-run step count, warn AI when 5 steps remain via _warning
  field injected into tool results
- Show 'Continue' button in chat when step limit (50) is exhausted
- Add step budget section to system prompt so AI plans ahead
- Reset run step counter on each new agent call via prepareCall
2026-03-13 10:18:20 +03:00
Anton A S 530b56acc5 Add calc tool for layout arithmetic, make set_layout direction optional
- Add calc tool (recursive descent parser) so AI uses exact math
  instead of error-prone mental arithmetic for layout sizing
- Make set_layout direction param optional — keeps current layoutMode
  when only changing spacing/padding (eliminates ~50% of NO-OP mutations)
- Add set_layout to SKIP_ARGS in NO-OP detection
- Update system prompt: calc in workflow, mandatory before rendering
  into fixed-size containers
2026-03-13 10:17:50 +03:00
Anton A S 54a418f555 Block chat input during AI streaming to prevent message spam
Input is now disabled during both 'submitted' and 'streaming' states.
handleSubmit guards against concurrent sends.
2026-03-13 10:17:49 +03:00
Anton A S 94160168ec AI chat: debug log with tool loop detection, prompt caching, system prompt tuning
Debug log (chat-debug.ts, ai-adapter.ts):
- Before/after node snapshots detect no-op mutations
- Duplicate tool call detection (mutating tools only)
- Per-step token usage with cache hit/miss breakdown
- TOKEN USAGE & CACHING section with hit rate and cost savings

Prompt caching (use-chat.ts):
- Enable Anthropic automatic caching via providerOptions for
  anthropic, anthropic-compatible, and OpenRouter anthropic models
- Track step usage via onStepFinish with cache read/write tokens

Provider fixes:
- anthropic-compatible now treated as custom provider in ChatInput
  (shows customModelID instead of stale modelID from previous provider)

System prompt:
- Sizing rule: calculate available space before rendering skeleton
- Concise final summary (2-3 lines, no section-by-section listing)
2026-03-13 10:17:47 +03:00