* feat(storybook): prototype guided AI setup and task assignments
* refactor(storybook): adopt shared control foundations
* refactor(storybook): build AI setup on current settings foundations
Move the prototype to settings/ai-setup and compose SettingsSection, SettingsGroup, SettingsRow, AppAlert, AppBadge, and AppCheckbox instead of local section, status, and badge markup. Replace the nonexistent danger color and raw amber with the error and warning tokens.
* feat(ui): add a shared radio group
AppRadioGroup wraps the Reka radio group with typed options, labels each radio by its option text with any description as its accessible description, and supports all arrow keys unless an orientation is set. The AI setup wizard uses it for the spending choice, named by the step heading, and shares the choice card style with its checkboxes.
* fix(ui): draw unchecked checkboxes on the field background
AppCheckbox filled its box with the surface (text) color, so unchecked boxes were nearly black in the light theme and nearly white in the dark theme. Use the panel field background and accent hover border shared with the radio group and switch.
* refactor(storybook): drop the simplified AI connections panel
AI setup has two modes: skippable guided onboarding for most people and the existing advanced settings for power users, both editing the same model settings. Remove the third, simplified connections and tasks panel. The wizard's Advanced settings action and the returning-user screen now stand in for ModelsPanel, which offers Run guided setup. Removing Gateway's own Back button also fixes the blank screen it led to.
* feat(ai): plan guided AI setup from the model catalog
planOnboarding proposes design and vision models from the access a person already has, using the real provider and agent catalog, and falls back to OpenRouter only when pay-as-you-go is allowed. applyOnboardingPlan merges a confirmed plan into the model settings, reusing matching connections and profiles, keeping roles it was not asked about, and dropping only the empty fresh-install profile.
* refactor(ai): share model provider display names
Move the provider, agent, and Pi display-name lookup out of the model settings workflow so guided setup can reuse it.
* feat(ai): offer guided AI setup over the real model settings
Guided setup asks what AI should help with, what access the person
already has, and whether pay-as-you-go models are allowed, then
proposes design and vision models from the provider and agent catalog.
Connections reuse the provider key field and connection test, keys are
saved through the credential manager, and the confirmed plan is merged
into the model settings, keeping anything configured by hand. Saving
reports saved, partial, or failed like the profile editor.
A fresh install whose model settings are still the empty placeholder is
offered setup once with a skippable welcome; existing setups never see
it. Settings → AI & agents can run it again, and Advanced settings hands
off to the model editor. The Storybook fixtures for agents, OpenRouter
sign-in, Vercel AI Gateway, and the local server are replaced by the
real flow, with all copy translated.
Browser tests start with the offer dismissed through the shared
Playwright storage state; the first-run spec clears it.
* fix(ai): keep configured models and credentials safe in guided setup
Running guided setup again planned from the catalog defaults, so it
replaced hand-configured design and vision models and dropped their
settings; it now keeps a configured model while its access is still
selected or onboarding cannot offer that provider, and keeps vision when
nothing new covers it. Reused profiles must have the capabilities the
plan relies on, and an explicit vision assignment without image input is
cleared.
A server's saved key and its status now apply only to the connection at
the address being entered, and its connection test uses that
connection's API type. Entered keys are copied before saving, so closing
setup mid-save no longer drops them, and the Models list refreshes key
status after setup saves a key. Servers that do not check keys get a
hint to enter any value, and a step that only keeps configured models
says so instead of showing nothing.
* test(ai): check key status right after guided setup
The Models list must show a key saved by guided setup as connected without reopening Settings.
* feat(ai): sign in to OpenRouter from guided setup
OpenRouter can now be connected with its OAuth PKCE flow instead of a
pasted key. In the browser, sign-in opens in a popup that returns to a
static callback page on the app's origin, which relays the redirect to
the editor over a BroadcastChannel, so the editor never navigates away.
The desktop app opens the system browser and receives the redirect on
a one-shot 127.0.0.1 listener, the localhost callback OpenRouter
documents. Either way the editor checks the state, exchanges the code
for a key directly with OpenRouter, fills it in, and runs the
connection test. Waiting, blocked pop-ups, cancellation, expiry, and
failures are reported in the step, which keeps the pasted-key path.
Setup no longer offers Clear for a saved key, since removing keys
belongs to the advanced settings, and the service worker leaves
/oauth/ pages to the network.
* fix(settings): report unreadable model keys as unavailable
A saved key the browser credential store could not read, for example one left from an older session on the same origin, rejected the model status refresh and the startup credential check, which surfaced as a global error toast. Each read failure now marks only that connection as unavailable.
* feat(ai): map every role in guided setup and verify OpenRouter sign-in
Guided setup now proposes a model for design, vision, review, and fast
work, and the review step is a map of those roles with every suitable
model from the connected providers and a "Use recommended setup"
shortcut. Fast work defaults to the provider's catalog model tagged as
fast; behind an agent, review and fast work use the API model chosen
for vision. Review and fast work are never asked about, so a configured
choice, including none, stays unless it follows a design model it can
no longer follow.
The pay-as-you-go question only appears when the access already
selected leaves a requested role without a model, so choosing
OpenRouter or another account no longer asks it.
After signing in with OpenRouter, setup checks the key with
OpenRouter's key endpoint, which costs no credits, instead of a text
generation test. The step then says it is signed in, names the key,
warns when the account has no credits yet, and offers another account
in place of the key field and test button.
* feat(ai): offer OpenRouter only for goals nothing selected covers
The separate pay-as-you-go step asked an abstract question even when
the selected access already covered every goal. The connect step now
names a goal nothing selected can cover, such as visual review behind
a coding agent or a local server, and offers to add OpenRouter for it;
once added, it says OpenRouter fills the gap and can be removed again.
Setup can finish without visual review, but not without a design model.
A local or company server can be marked as able to read images, which
lets it cover visual review and makes it the preferred vision model
over a paid account.
* feat(ai): show provider logos and more providers in guided setup
Guided setup shows monochrome logos for coding agents, API accounts,
and local servers, from LobeHub's MIT-licensed static SVG set loaded as
an `ai` icon collection, so they follow the theme like Lucide icons.
DeepSeek, Z.ai, and MiniMax are offered under "More providers", and a
local server can start from the Ollama or LM Studio address instead of
typing it.
* feat(ai): guide coding agent setup in guided setup
Choosing Claude Code, Codex, or Gemini CLI in the desktop app now checks
whether the agent's ACP program and OpenPencil's MCP server, which
agents use to reach the canvas, are installed. An allowlisted
agent_lookup command finds the program on the same widened PATH as the
MCP lookup. The card shows install commands only for what is missing,
checks again on request, links a new setup guide, and copies a prompt
that asks an agent the person already uses to install both, confirm
they are on PATH, and sign in. In the browser, the agent section links
to the desktop app.
* fix(ai): space the More providers toggle like a group heading
The toggle sat flush against the account cards above and below it; it now reads as a group heading with the same rhythm as the other sections.
* feat(ai): detect and install coding agents in guided setup
Adopt the local agent discovery from #847. A desktop agent_lookup
command reports each agent's own CLI, its ACP adapter, npm, and
OpenPencil's MCP server on the widened PATH without starting any of
them, and the app can install a missing adapter or the MCP server with
npm, limited by the shell capability to those exact packages and the
MCP version that matches the app. Guided setup now tells "installed but
the OpenPencil adapter is missing" apart from "not installed", offers
one-click installs, links each vendor's own setup guide, and keeps the
manual commands and setup prompt for the browser, missing npm, or a
failed install. Codex install instructions move to
@agentclientprotocol/codex-acp, which replaces @zed-industries/codex-acp.
Kiro CLI support from the same pull request is left for a separate
change, since it needs ACP transport work.
Co-authored-by: GitttHomie <134371845+GitttHomie@users.noreply.github.com>
* build(app): resolve LobeHub icons with import.meta.resolve
The architecture lint forbids createRequire in ESM build code.
* test(app): seed AI setup specs through storageState
Follows the storage seeding used by other browser specs and the import type rule.
* feat(ai): set up Pi with its own sign-ins in guided setup
Pi now runs with the providers signed in to in the Pi CLI and Pi's
default model. The Harness companion reuses ~/.pi/agent; the app reads
only Pi's settings.json for the default model and never auth.json. A
saved key is still used as an AI Gateway key.
Guided setup offers Pi next to the other coding agents. On the desktop
it checks the Harness companion, the MCP server, and Pi's default
model, and installs the companion with one click through npm.
Agent discovery now reads the installed versions of the MCP server and
the Harness companion from their package.json without starting them.
Setup flags a version that does not match the app and shows the update
command for the package manager that installed it, instead of
reporting the server as installed and failing at the first message.
* feat(ai): check agent companions before a chat starts
A Pi chat without the Harness companion, or any agent chat whose
companion or MCP server version does not match the app, failed when the
process started and showed only the generic request error. The chat now
checks the companions through agent discovery first and names the fix,
with an action that opens guided setup. Pi sign-in and model problems
use the same path.
The Pi model editor shows the same companion, MCP server, and default
model status as guided setup, and no longer requires a model ID, since
Pi falls back to the default model set in Pi.
Supersedes the companion detection in #566, which ran the companion to
read its version and required an exact version match.
* fix(harness): start Pi sessions with MCP tools and keep unsent messages
Pi chats in the desktop app always configure OpenPencil's MCP server,
and three companion problems stopped them:
- @ai-sdk/harness-pi imports pi-mcp-adapter, which publishes TypeScript
sources. Node refuses to strip types under node_modules, so the
companion now strips them through a module load hook limited to
TypeScript dependencies. Bun runs them as is.
- pi-mcp-adapter imports @earendil-works/pi-tui, declared only as an
optional peer, so npm left it out. The companion depends on it at the
version pi-coding-agent uses.
- Pi reports live-process resume, yet the service handed it state saved
by an earlier session, and the just-bash sandbox cannot resume, so
every later session with that ID failed. Live-process backends now
start fresh and drop saved state.
When a chat cannot start, the composer now keeps the typed message
instead of discarding it.
* fix(harness): keep companion stdout for protocol messages
Pi prepares the packages listed in a person's Pi settings with npm,
which inherits the companion's stdout, and libraries log through
console.log. Both landed in the JSONL protocol stream, where the app
discarded them with warnings. The companion now keeps the real stdout
for protocol messages, sends other stdout writes to stderr, and quiets
npm on success through its environment.
The type-stripping hook no longer prints Node's experimental warning,
and the app logs companion stderr as diagnostics rather than errors,
since failures arrive as protocol errors.
Document Pi in the coding agents guide, the AI chat page, and the
README: guided setup installs the companion, Pi uses the Pi CLI's
sign-ins and default model, an AI Gateway key is optional, and the
companion needs Node.js 22.15 or later.
* test(harness): keep the pi-tui pin in step with pi-coding-agent
The companion depends on pi-tui only because pi-mcp-adapter imports it while declaring it optional (nicobailon/pi-mcp-adapter#805). Upgrading @ai-sdk/harness-pi moves pi-coding-agent, and a pin left behind would make npm install a second, mismatched pi-tui. The test fails until the pin matches.
* test(ai): follow the model catalog in guided setup tests
The plan and apply tests repeated catalog default and fast model IDs, so master's model update broke them without any change in setup behavior. They now read those models from the catalog.
* test(ai): keep the model catalog helper with the shared test helpers
Unit test homes under tests/app accept only *.test.ts files, so the onboarding tests' catalog helper moves to tests/helpers/ai.
* docs: tighten the guided setup and Pi changelog entries
Name every provider and server preset guided setup offers, describe the role step as it now works, and shorten the Pi entry.
* test(ai): type the guided setup test stubs for the test typecheck
Master now typechecks the test suites: fetch fakes go through fetchStub, the chat ref is shallow like the real one, and mocks declare the arguments the tests inspect.
* test: type the tabs module in the closed-documents spec
The spec imported the tabs module by its served URL without a type, which fails the test type check on master.
* test(ai): assert outcomes instead of copy in guided setup tests
Drop the setup-prompt test, which checked prompt prose, the onboarding wrapper cases that restated discovery, and the coversGoals case. Story plays and the OpenRouter E2E flow now assert controls and saved models instead of sentences and catalog model names, and the fast-model helper checks the planned model's catalog entry instead of recomputing the choice. tests/AGENTS.md states the rule.
* feat(ai): return desktop OpenRouter sign-in through a deep link
The desktop app ran a hand-written HTTP server on a localhost port to receive OpenRouter's redirect, and OpenRouter labels apps with a localhost callback by host and port. OpenRouter now redirects to a page on the web app that opens openpencil://oauth/openrouter with the same query, and the desktop shell forwards that link to the webview as an oauth-callback event. The attempt that started sign-in checks the state and exchanges the code with its PKCE verifier, which never leaves the app.
* feat(ai): ask OpenPencil's companions for their version
The desktop app read a companion's version by following its executable's symlink up to a package.json. That only worked for the Unix npm and bun layouts: Windows .cmd and .exe shims and version-manager shims such as Volta and mise are not links into the package, so the version was always unknown and an outdated companion went unreported. The MCP server, stdio bridge, and Harness companion now print their version for --version, and the app runs each installed one with --version --help under a timeout. A release older than --version prints its help or exits without a version line, which reads as outdated. A bun global install on Windows now gets the bun update command too.
* fix(ai): ask for a Pi sign-in when Pi has none
readPiAccount returned an account whenever a home folder existed, so a chat with no Pi sign-in reached the Harness and failed with a provider error instead of the guided pi-sign-in fix. It now reports whether Pi's auth.json exists, without reading it, and the capability allows that one check.
* fix(ai): keep the attachments of a message that was not sent
A message that never reached the chat came back to the composer as text only: its image previews were revoked and its referenced layers dropped. The composer now takes back the whole submission, or releases the previews when newer text replaced it. A message counts as sent once the chat holds it, so a failure after that no longer hands it back to be sent twice.
* refactor(app): read the app version from one constant
Four modules each derived the app version from the build define with the same test fallback.
* refactor(ai): report chat submission errors from their own module
Reverting turns from master and keeping unsent drafts together took useChatSubmission past the composition-root limit. The test for reverted turns now passes the setup messages the submission reports.
* refactor(app): keep the app version with the runtime config
Tools typecheck src/constants.ts through app imports without the Vite defines, so the version constant moves to src/app/runtime/version.ts.
* test(desktop): check npm installs of the companions at the app's release version
The scope test named the companion packages and version literally, so it would keep passing if the app requested something else. It now builds them from the app's package names and the release version a build embeds.
* refactor(ai): parse OpenRouter, Pi, and sign-in callback data with Valibot
The OpenRouter key info and code exchange checked their JSON with typeof chains, Pi's settings parsed JSON in a try before validating it, and the desktop sign-in trusted the shell's callback payload as typed. Each now goes through one schema.
* fix(ai): take back a message whose images could not be prepared
A message with images appears in the chat before its images are prepared, so a preparation failure counted as sent: the draft did not come back and its previews were already revoked. A message now counts as sent once it is dispatched; a failure before that removes the shown message and hands the draft back, and the composer's previews are revoked only after dispatch.
* fix(ai): restore an unsent message only in its own conversation
Switching conversations while a message was being sent could restore it into the newly opened one. The draft now comes back only if the conversation is unchanged, and its previews are released otherwise.
* refactor(app): keep one app version constant
The update window added an APP_VERSION to src/constants.ts beside the one in src/app/runtime/version.ts. The tools typecheck reaches src/constants.ts without the Vite defines, so the update window now reads the runtime one.
---------
Co-authored-by: GitttHomie <134371845+GitttHomie@users.noreply.github.com>
* feat(lint): suggest converting groups to frames and deleting hidden layers
no-groups and no-hidden-layers now carry suggestions the Lint panel, the
lint_fix tool and editor.applyLintFixes can apply. A group becomes a frame
in place, keeping its id, children, bounds and look; a hidden layer is
deleted with its children. Neither is offered for locked layers or inside
components and instances, and both are checked again when applied.
The editor gains convertGroupToFrame and deleteNodes, which deleteSelected
now uses, and applies lint fixes through a bridge so structure changes and
property updates share one undo step. lint_fix becomes a document mutation
because atomic tools cannot remove layers.
* fix(code): keep canvas edits made while replaced code waits to render
Replacing all the code drops its layer links until the preview links it
again, so a canvas edit in the preview delay could not be patched into the
code and the preview drew over it. Edits made while code waits to render
are now applied again once the preview has linked the code, in the same
undo step, and reach the code like any other canvas change.
* fix(figma-api): default paint opacity and visibility like Figma
Plugin scripts may leave out a paint's opacity and visible; Figma reads
them back as 1 and true. The fills and strokes setters stored the paint
as given, so the Design panel received an undefined opacity.
* build(dev): forward only errors from the browser console
Vite forwards browser logs when an agent starts the dev server. Serializing
a Vue warning's component props walks the editor state and freezes the
tab, so warnings stay in the browser console.
* fix(code): follow values while they are dragged or scrubbed
Live previews change layers without a new scene version, so the code kept
the old value until the gesture ended. The Code tab now follows preview
updates once per frame, as the Design panel does, and goes back when the
gesture is cancelled.
* fix(code): keep canvas edits when the replaced code fails to preview
A failed preview took the canvas edits waiting for it and dropped them,
and stopped recording new ones, so correcting the code drew over them.
The edits now wait for the next preview.
* refactor: rename @open-pencil/codegen to @open-pencil/emit
In design tools codegen means design-to-code, which is dom-css's job and
Core's codegen tools'. The package builds and prints the syntax trees
exporters emit, so name it for that. It has not been released yet.
* fix: alias @open-pencil/emit to its source in Vite
The rename missed the escaped regex, so Vite resolved the package through
its built dist output.
* refactor(codegen): share syntax-tree code generation between exporters
dom-css printed Tailwind JSX with its own esrap JSX builders and kept TypeScript template helpers under its Storybook export. The OpenPencil JSX exporter needs the same JSX builders, and design-jsx and dom-css may not depend on each other.
@open-pencil/codegen holds both: es for ESTree templates and modules (moved from dom-css) and jsx for JSX elements, attributes, text, and printing, including the literal rules that keep exported strings from being reinterpreted. dom-css no longer depends on acorn and esrap directly.
* refactor(design-jsx): print JSX export as syntax trees
sceneNodeToJSX concatenated strings with hand-written escaping and indentation. It now collects typed props (moved to export/props.ts) and prints them with @open-pencil/codegen's JSX builders. Output is unchanged except for text: special characters print as a string expression instead of entities, and multi-line text keeps its line breaks, which the old line-splitting lost on render.
* docs(codegen): fix package metadata and dependency rules
codegen's repository.directory still named design-jsx, the README mentioned es without showing it, and the design-jsx and dom-css guides still said they depend only on scene-graph.
* test(core): move the JSX export round-trips to the design-jsx test home
Covers tabs in layer names and text, which JSX keeps as written.
* refactor(design-jsx): extract design JSX into its own package
Design JSX elements, helpers, schema, reference, and JSX export only need
the scene graph, yet lived in Core, so every consumer of the authoring API
pulled in the renderer, layout, and file formats.
@open-pencil/design-jsx now owns them and depends only on scene-graph. The
renderer takes icon lookup, SVG conversion, vector creation, and layout as
DesignJSXServices; Core binds its own and exports the bound renderJSX and
renderTree from @open-pencil/core/design-jsx.
* feat(design-jsx): export the JSX runtime for TSX authoring
The package already had a JSX runtime, but nothing exported it, so design
trees could only be written as function calls or JSX strings. Export
`./jsx-runtime` and `./jsx-dev-runtime` so `jsxImportSource` works, and make
`Fragment` produce the same empty-type node as `<>` in `renderJSX` strings.
* fix(design-jsx): render fragments nested in other elements
A fragment builds a node with an empty type, which only renderJSX expanded, and only at the root. Nested fragments and fragments passed to renderTree failed with 'Unknown element: <>'. Inline fragment children when trees are built, and share root expansion between renderTree and renderJSX.
* refactor(dom-css): group sources into import, export, and runtime
dom-css kept both conversion directions in one flat folder: HTML and CSS
import next to SceneGraph export, a JSX runtime next to the Tailwind JSX
printer, and CSS parsing and formatting in one file.
Sources now live in `import/`, `export/`, and `runtime/`, with the CSS
value helpers split by direction and `export/index.ts` as the `./export`
entry. Tests mirror the new folders. Public entry points are unchanged.
* docs(dom-css): add a package guide for the import, export, and runtime layout
dom-css had no AGENTS.md. The guide records the source layout, the
scene-graph-only dependency, the browser-safe `./export` entry, printed
code generation, and Base64 validation for imported documents, and the
root map links to it.
* refactor!: register HTML and Tailwind JSX as IO formats
HTML and Tailwind JSX went around the IO registry: the CLI appended
`html` to its format list and had its own HTML and Tailwind export paths,
so the app's export options offered neither.
Core now registers `html` and `tailwind-jsx` adapters built on a new
browser-safe `@open-pencil/dom-css/export` entry. Export results can
carry assets written next to the main file, which covers standalone HTML
with external images and fonts, and the CLI writes every format the same
way. The CSS object model and Node file access load only when an export
needs them, so the app bundle stays free of the headless CSS runtime.
BREAKING CHANGE: `sceneNodesToTailwindJSX` and `designDocumentToTailwindJSX`
moved from `@open-pencil/dom-css/browser` to `@open-pencil/dom-css/export`.
* refactor(core): share export support and fixed-size options across IO formats
Five adapters export every target and six have no scale or quality
options; the new HTML and Tailwind JSX adapters repeated those blocks
again. Both are now named once and shared.
* fix(core): keep HTML asset paths relative for Windows output paths
The CLI passed the absolute output path as the export file name, and the
HTML adapter only split it on `/`, so on Windows the page referenced
absolute `C:\...\card.assets` paths and assets were written to a doubled
location. The CLI now passes the file name, and the adapter accepts
either separator.
* feat: refresh branding with generated platform icons
Keep the approved mark and optical micro master as the source of truth. Generate web, documentation, and native assets at their owning build boundaries instead of storing raster variants or linking web icons to desktop output.
* fix: refine small brand marks and loading artwork
* feat: adopt blue editing-handle brand mark
* feat: refine teal branding for light and dark themes
* feat: apply ivory tiles across brand surfaces
Use edge-to-edge web tiles with a larger mark and optical micro favicons. Preserve native spacing and platform-owned maskable/touch cropping. Address review feedback on story props, native dimensions, and brand test type checking.
* test: allow cold npm startup in packaging fixture
CI hit Bun's five-second default while running npm pack --dry-run. Give this integration test a scoped 30-second budget without changing production timeouts or other tests.
Unify preview-aware transforms across scene drawing, labels, selection and input. Give live property and creation edits explicit preview ownership, preserve cancellation and one-step undo, and reuse bounded text preparation resources.
Include retained device-grid handling, worktree HMR coverage, and nested/reflected interaction regressions in this cohesive progress checkpoint.
Validation: full check passes; 734 scoped unit tests and 53 targeted browser tests pass. This is NOT merge-ready: the unchanged exact nested filled-section held/released regression still fails with 63 differing pixels (maximum channel delta 5/255). The browser pass count excludes that separately run failure and the previously classified paint-field-width baseline. Raster-origin investigation and broader acceptance remain outstanding.
- Replace the fork-era Markdown integration with Comark and an explicit Shiki extension\n- Centralize OpenPencil theming, parser lifecycle, controls, and URL hardening\n- Remove Mermaid stubs and bundled math or diagram dependencies\n- Address contextual composer review findings and extend browser coverage\n\nCo-authored-by: Jason Kneen <jason.kneen@bouncingfish.com>
- Register the Vite-owned MCP child as a worktree-prefixed Portless sibling service\n- Inject HTTPS and WebSocket automation URLs into the browser instead of assuming port 7600\n- Isolate development socket and discovery files while preserving fixed-port non-Portless flows
- Rename first-party API, RPC, JSON, CORS, SVG, JSX, and related identifiers to preserve acronym casing
- Keep upstream and serialized boundary names unchanged
- Add a lint guardrail and migration notes for exported APIs
- Resolve MCP workspace subpaths during Vite development
- Remove discovery state before shutdown and close upgraded sockets
- Cover discovery cleanup with a connected WebSocket client
Co-authored-by: Joseph Cumines <joeycumines@gmail.com>
- Gate ingest rendering until required faces and script fallbacks resolve
- Key CanvasKit artifacts by font registration generation and invalidate affected nodes
- Load character-aware remote subsets with cumulative coverage and exact render aliases
- Use BCP-47 language hints for shaping and CJK fallback order
- Keep baked .fig glyphs as an exhaustion-only fallback
- Move the Kiwi runtime under schema-runtime to match folder naming
- Load the Figma schema from a .kiwi text asset with static validation
- Assert Figma clipboard-derived field numbers for fragile layout/text fields
- Configure oxfmt custom import groups for workspace, app, package, and test aliases
- Keep type imports grouped with their matching source category instead of one global tail group
- Expand the format script to cover formatter config, Vite files, and scripts
- Merge structural lint rules into oxlint.json and remove the second config file
- Add #tests/* path aliases and rewrite deep test helper imports
- Ban new deep parent-relative imports through the shared lint config
- Allow CJK fallback loading to use local variable system fonts
- Add Windows CJK fallback families
- Harden CanvasKit WASM asset resolution and serving