* 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>
* refactor(app): share Markdown rendering outside chat
Move the vue-stream-markdown wrapper, inline code, token mapping, and
styles out of chat into components/markdown and theme/markdown, so other
surfaces can render Markdown without importing chat components. A density
attribute selects the compact chat styles or a comfortable reading size,
and Shiki highlighting is opt-in. ChatMarkdown keeps its streaming render
key and wraps the shared component.
* refactor(ui): extract AppProgress from the toast
The toast drew its own progress track, fill, and label. Move them into
AppProgress in ui/feedback, built on Reka's Progress so the bar reports
its value and indeterminate state, with an accent tone for panels and a
current-colour tone for coloured surfaces. ToastProgress becomes the
shared ProgressAmount. AppPlaceholder also accepts an h1 label for
placeholders that stand for a whole window.
* feat(desktop): show updates in a Software Update window
The update prompt passed the release's CHANGELOG section to the native
confirm dialog, which cannot format Markdown or scroll, so the 0.15.1
notes showed raw headings and pushed the buttons off screen.
When the main window finds an update it now opens a small `updater`
webview loading its own `updater.html` entry, which never boots the
editor. The window renders the notes with the shared Markdown component
in a scrolling box and links to the full release page.
Installing is split into stages. The download shows progress and can be
cancelled; Tauri cannot abort it, so Cancel detaches and a later Install
reuses the running download. On macOS and Linux the update then installs
and the window offers Restart Now or Later. A restart, and on Windows the
installer that quits the app, first asks the editor window to run the
same unsaved-documents approval as Quit. Failed checks, downloads,
installs, and restarts keep the release visible and can be retried. The
window's capability grants only update, restart, close, and link
opening.
Closes#743
* fix(desktop): stop waiting for a restart reply from a closed editor
The Software Update window waited for the editor's answer with no bound,
so an editor closed mid-request left Restart Now, and the Windows
install, stuck. Treat the editor window's destruction as approval: it
ran its own unsaved-changes prompt and holds no documents. A timeout
would instead fail people still answering that prompt.
* fix(desktop): pin every command line the app may start
On Windows the app starts npm-installed CLIs through cmd /c, and the shell scope let cmd take any arguments, so any code running in the webview could run any command. Each program now has a scope entry with its exact arguments, and Windows shims go through their own cmd-<name> entries with fixed /c <name> arguments. The agents and the MCP server no longer accept arbitrary arguments either. A test checks that every command the app starts has a matching entry on both platforms.
* test(desktop): expect Windows shims through their own scope entries
* test(desktop): check the executable of each shell scope entry
* test(desktop): allow no shell scope entry beyond the programs the app starts
An extra entry with a permissive validator passed the per-program checks.
* build: typecheck the test suites
Tests were in no TypeScript program: no tsconfig included tests/** or
packages/*/tests/**, and bun strips types without checking them, so a
fixture could drop a required field and keep passing until something
read it.
@types/bun moves to the root because it was installed per package only,
and #cli-tests/* joins the paths the root config already carries.
* test: fix the type errors the test suites were hiding
Typechecking the tests turned up 1123 errors. Most were ordinary
strictness, but some were real: `NodeChange` bound to Figma's plugin
typings rather than the Kiwi codec in thirteen .fig tests,
materializeInstance was called with six arguments against five so the
blobs and source children were dropped, CanvasKit pixels were written
to a plain object that never reached WASM, and assertions were made
through accessors that do not exist, so they asserted nothing.
Fixtures that had quietly lost a required field now carry it, nullable
results are narrowed through the existing expectDefined helper rather
than assumed, and stand-ins for CanvasKit and the editor go through one
named helper instead of an unexplained cast at each site.
No test was deleted, skipped, or weakened, and no `any`, non-null
assertion, or ts-expect-error was introduced.
* docs: record what typechecking the tests established
Pins the app program's global types with an assertion rather than a
note, since an unpinned types list lets any root @types package decide
which platform src/** is judged against.
The two environment faults that look like code regressions — Vite's
dependency pre-bundle outliving a package rebuild, and heavy .fig
suites failing under load — go to the development docs, where an
explanation belongs.
* fix: align @types/bun and keep node types resolvable when extended
The root manifest declared a newer @types/bun than every package, which
check:monorepo rejects, and pinning the app program's types left them
unresolvable from a config that extends this one out of tree.
* fix: fail the test typecheck when the compiler itself fails
The gate matched diagnostics by substring, so a compiler or config
failure that named no test file printed a pass while having checked
nothing. Diagnostics are now split by whether they name a file: an
unscoped one is the run failing and stops the gate, a test file's is a
finding, and a source file's stays out by design.
Also drops the parameter planComponentConstruction never read, and
makes the inner-shadow verification script exit non-zero when it
renders no image instead of logging and succeeding.
* chore: merge master into tests-typecheck
* fix(text): request script fallbacks for substituted text
When a text's font could not be loaded and the default family
substituted for it, font readiness returned before checking glyph
coverage. That check is what requests CJK and Arabic fallbacks, so text
such as Chinese in an unavailable PingFang SC drew missing glyphs unless
another layer happened to request the fallback first.
Substituted text now observes glyph coverage too. It waits while a
fallback loads and stays visible when none is available.
* fix(fonts): explain installed fonts with unsupported outlines
On macOS 15 and later PingFang ships only `hvgl` outlines, which neither
font-kit nor CanvasKit can read. The desktop loader spent over a second
parsing the collection per style, and the font banner showed PingFang as
substituted with no explanation.
The loader now reads the family's table directories first and returns a
structured unsupported-format error. The font manager records it per
face, document font status exposes it as `reason`, and the banner shows
it inline with the full explanation in a tooltip. The resolver reports
progress after each failed candidate so the banner updates before web
font lookups finish.
* fix(fonts): keep the unsupported-format reason after failed retries
A later host attempt that returns no font no longer clears the reason; only a loaded face does.
Report desktop update downloads through persistent determinate or indeterminate toasts while retaining native confirmation. Use a spinner during progress and cancel pending expiry when progress resumes.
Standardize substantial Storybook fixtures as colocated example SFCs, preserve shared SDK documentation examples, and enforce semantic anatomy instead of shared-layer test IDs.
* feat(settings): configure tool access and agent step limits
Built-in AI exposed only a hardcoded subset of the tool registry, and the
maximum agent steps was a constant, so users could neither enable
extended tools such as create_component nor adjust long-running tasks.
Built-in AI and the local MCP server now keep independent, locally saved
tool permissions over one shared catalog, with searchable read-only and
side-effect groups and per-target defaults. Chat settings gain a validated
maximum-steps field whose captured value drives the stop condition,
remaining-step warnings, and limit detection for each message.
Tool access, the local server, browser access, and MCP connections are
grouped under a single Automation settings page.
Closes#573Closes#584
* refactor(settings): split automation into MCP and Tool access pages
The Automation page mixed a permission matrix with server endpoints behind
a Tools/Connections switch, and the view switch was indistinguishable from
the provider switch. The nested scroll region showed three of 110 tools.
Rename the MCP-facing page to MCP and give tool permissions their own Tool
access page. The page owns a fixed toolbar for the target, count, defaults,
and search, so the list uses the full dialog body and no row is clipped.
* fix(automation): explain MCP startup failures with localized guidance
Every startup failure collapsed into "MCP server did not become healthy":
the spawn layer recorded the real error but the runtime discarded it, and
health probes could not distinguish a rejected token from a missing server.
The message also surfaced raw English text as the alert heading.
Classify failures by reason (not installed, denied command, early exit,
startup timeout, rejected token, unexpected response, unreachable) and
render translated heading and guidance from the catalog, keeping captured
stderr or HTTP status as labeled diagnostic detail.
* refactor(ui): share one collapsible disclosure primitive
Six features each wired Reka's collapsible with their own motion classes and
one settings-only theme token, so the same interaction drifted in spacing,
icon size, and reduced-motion handling.
Add AppCollapsible with a family theme and move the settings disclosure and
the model editor's advanced settings onto it. Chat and frame-preset call
sites keep their distinct visuals for a follow-up.
* fix(automation): explain MCP failures with localized details
The failure alert carried raw English error text as its heading, and the
diagnostic payload sat in a sibling block outside the alert with no
relationship to it.
Classify failures by reason, render translated heading and guidance from
the catalog, and keep the payload in a collapsible inside the alert, which
unmounts while collapsed so the live region announces only the summary.
Add a copy action for issue reports.
Find the executable where a graphical launch can: extend PATH with the
common global bin directories before the lookup and report the searched
directories as diagnostic detail.
* fix(automation): keep MCP failure details out of reasons already explained
An unreachable address and a rejected token already name their cause in the
translated guidance, so repeating it under Details added noise. Details now
carry only output the summary cannot: stderr, HTTP status, or an unknown
error message.
* test(settings): browse every MCP failure reason in Storybook
The failure copy lived inside the settings panel, so reviewing the eight
reasons meant reproducing each failure and the mapping could only be
checked through the panel's dependencies.
Extract MCPFailureAlert, which owns the reason-to-copy mapping, detail
visibility, copy action, and restart action, and add a story covering
every reason plus the collapsed-details behavior.
* fix(ui): order alert details above the recovery actions
The alert rendered its action buttons before the details slot, so the
collapsible explanation of a failure appeared under the controls it
explains. Details now render directly after the description.
* fix(automation): correct MCP failure classification and detail
Review follow-ups on the failure diagnostics.
Only 401 and 403 mean the server refused our token; any other status now
reports an unexpected response instead of telling the user to replace a
token that was never the problem.
The install hint rendered the whole diagnostic detail as its package
argument, so searched directories appeared inside the install command.
The install target is now a domain constant and the searched directories
stay as detail, which not-installed failures surface again since they are
the actionable desktop diagnostic.
Exited failures also record the process exit code and signal so copied
diagnostics stay conclusive when stderr is empty. The bundled PATH test
now covers the append branch instead of only the unchanged path.
* feat(settings): accept custom values for presets and retention
Retention was a closed set of three counts while the AI step limit was a
free number, so two bounded numeric preferences looked and behaved
differently for no product reason.
Add a shared preset-or-custom field: presets stay one click, the escape
hatch reveals a validated numeric field, and the model carries only the
resolved number. Diagnostics retention becomes a bounded number (50 to
20,000) with the presets as shortcuts, and the hardcoded revalidation in
the panel is replaced by one domain resolver.
* fix(settings): label the preset and custom fields
Replacing the labeled provider field with the shared control left the AI
step limit as a bare select with a detached hint paragraph, outside the
settings group, so nothing on screen said what the number meant. The
accessibility name came from aria-label, which is why behavior tests
passed while the panel was unreadable.
Move both controls into labeled settings rows with their descriptions, and
give the revealed field its own accessible name so the two controls in one
row differ. The specs now assert the control lives inside the row that
names it, which is the check that would have caught this.
* fix(mcp): allow the desktop app origin by default
A server started manually bound the port and answered curl but the app
webview could not use it: no CORS origin was configured, so the browser
blocked every fetch and the app reported the server as unhealthy. The
workaround required an undocumented environment variable.
Allow the desktop app origins by default, accept a comma-separated
override, and document the default in the CLI help and the security notes.
Authenticated requests still need the bearer token, and browsers set Origin
themselves, so only the app webview can present these origins.
* fix(settings): address review findings on the new controls
Copy details awaited nothing and confirmed the copy before the write
finished. VueUse never rejects and falls back to a legacy write, so the
await is what makes the confirmation honest rather than an error branch.
The preset field only left custom mode when a preset arrived; a non-preset
value assigned from the owner left the select showing a value absent from
its options with the field still hidden. The watcher now follows the model
in both directions.
The story play functions queried the revealed field by the row label, which
Testing Library matches as a whole string, so those interactions could not
find it. The Storybook smoke assertion also assumed a button or tab, which
skipped every story built from other primitives.
* refactor(i18n): migrate app copy to domain namespaces
- Replace the monolithic dialogs catalog with 16 flat product-domain catalogs
- Preserve every translated locale value while moving all 356 messages
- Expose narrow domain composables and retain useI18n as a compatibility aggregate
- Document namespace ownership and admission rules
* fix(i18n): complete migrated locale coverage
- Translate exposed MCP automation, code editor, credential, and media placeholders
- Restore missing German, Spanish, French, Italian, Polish, and Russian diacritics
- Preserve technical product terms and interpolation placeholders
* fix(i18n): polish remaining locale wording
- Correct the mixed-language German code source label
- Preserve stock_photo and Pexels semantics in German and Polish
- Improve Italian MCP and code-editor grammar
- Use the standard Russian term for MCP endpoint
* fix(i18n): migrate diagnostics copy after rebase
- Add a diagnostics domain for newly merged usage and telemetry settings
- Keep settings navigation labels in the settings domain
- Preserve translated diagnostics catalogs from current master
* style(app): format rebased settings components
* refactor(i18n): simplify domain message keys
- Remove redundant domain prefixes from settings, diagnostics, automation, and integration catalogs
- Move feature-specific actions out of the common namespace
- Preserve current-master language picker and diagnostics copy across every locale
- Update consumers to concise semantic keys
* fix(app): preserve font and updater contracts
- Restore browser and abort-aware font loading from current master
- Remove the unused parameter helper from common messages
- Update updater tests for semantic domain keys
* fix(i18n): finish semantic diagnostics access
* refactor(i18n): derive active MCP status message
* fix(i18n): preserve rendering settings after rebase
- Add a rendering domain for tiled canvas preferences
- Preserve the current-master language and rendering settings UI
- Move every translated renderer preference out of the retired dialogs catalog
* fix(i18n): polish reviewed locale semantics
* test(i18n): enforce translation completeness baseline
- Fail on interpolation placeholder drift and suspicious mixed-script values
- Reject new source-identical translations while tracking existing debt explicitly
- Report translated-message coverage for every supported locale
- Require baseline cleanup when existing placeholders are translated
* refactor(i18n): baseline reviewed mixed-script messages
- Replace brittle product-vocabulary exceptions with Unicode script detection
- Track reviewed mixed-script messages by stable locale and message identity
- Reject new entries and stale baseline debt without hardcoded terminology
* fix(fig): resolve relocated stroke test helper
* feat(app): show atomic document loading progress
- Preserve the existing full-canvas pencil loader while adding phase, detail, accessible status, and honest determinate progress
- Keep one generation-safe load owner across FIG decoding, graph preparation, page population, fonts, fallbacks, layout, viewport fitting, and first-render fade
- Prevent nested page setup and viewport cleanup from revealing partially prepared documents
- Cover obsolete sessions, font-resolution ownership, and staged loader UI
* refactor(app): scope editor preparation per tab
- Replace the shared loading boolean with one reactive preparation snapshot and one imperative controller per editor store
- Keep Core page work progress-only and inject canvas suspension from the app boundary
- Route FIG, storage, recovery, DOM import, and page switching through reusable tab-local preparation handles
- Abort only the closing tab's operation and cover generation safety, multi-tab isolation, progress UI, and disposal
* fix(editor): commit prepared pages atomically
- Prepare population, fonts, fallbacks, and layout without changing the visible page
- Reject cancelled and stale prepared pages before committing viewport, selection, and page events
- Keep the preparation overlay until the committed scene version is presented
- Cover call order, cancellation, stale generations, and presentation acknowledgement
* fix(app): stage imported documents before commit
- Prepare imported graphs in an isolated Core editor before replacing the live document
- Share font loading while keeping live selection, graph, renderers, and history untouched during staging
- Preserve the previous graph when staging is cancelled or fails and remove the duplicate pre-font layout pass
* refactor(app): namespace preparation UI
- Move canvas and tab preparation presentations into focused subfolders with concise component names
- Share progress and phase presentation helpers across preparation surfaces
- Show tab-local preparation status without covering the active canvas for background work
* fix(app): cancel preparation work at source
- Publish typed per-store preparation lifecycle events with explicit completion, cancellation, and failure outcomes
- Propagate tab-local AbortSignals through FIG parsing, population workers, and browser font downloads
- Keep cancellable font requests outside shared in-flight caches while retaining globally completed font registrations
- Stop FIG manifest previews from replacing the live graph before atomic document commit
* fix(app): cancel storage and DOM preparation
- Propagate preparation signals through S3 downloads, byte progress, local-cache boundaries, and DOM/CSS conversion checkpoints
- Reuse merged diagnostics and localized toasts for document, storage, and presentation failures
- Replace manual font concurrency and presentation timers with es-toolkit limitAsync and withTimeout
- Guard stalled first presentation and fix the merged recovery dialog title bindings
* fix(app): stage reload and font retry
- Prepare reload graphs in isolation and preserve the current document on read, decode, font, or layout failure
- Restore page and viewport state only after atomic graph commit with cancellable reload reads
- Run font Retry as a tab-local preparation with cache reset, final layout, picture invalidation, and presentation acknowledgement
- Keep completed document pixels visible while Retry reports activity in the tab
* fix(app): enforce exclusive preparation outcomes
- Complete document, storage, recovery, and DOM preparations only after successful commit
- Keep failed and cancelled handles terminal so lifecycle events cannot report contradictory outcomes
- Preserve external AbortError identity across storage timeouts and cancel streamed readers without returning partial bytes
- Cover credential-free pre-abort, mid-stream cancellation, progress cutoff, and terminal outcome exclusivity
* feat(diagnostics): record preparation outcomes
- Persist completed, cancelled, and failed preparation lifecycles through the validated diagnostics recorder
- Store only operation kind, outcome, cancellation or failure category, terminal phase, and coarse duration bucket
- Exclude document subjects, font families, storage identities, URLs, raw durations, messages, and stack traces
* chore(app): keep browser font tests with typography split
- Remove the browser font transport test inherited from a mixed cancellation commit; the source and coverage remain on the typography branch and safety snapshot
* test(vue): assert injected render suspension
- Exercise shouldSuspendRender instead of removed Core loading state\n- Preserve the contract that rendering resumes without a version change
* test(app): complete atomic preparation contracts
- Acknowledge first presentation in headless file-open tests\n- Assert the cancellable font-loading signature at the Tauri fallback boundary
* fix(app): preserve preparation cancellation
- Stage imported graphs before mutating live tabs and propagate aborts through page, DOM, font, and storage work\n- Use the accessible progress primitive and clamp determinate values\n- Cover fallback-font cancellation and yield pending-open test polling to the task queue
* test(text): await fallback font request cancellation
Start the mocked remote font request before aborting so the test proves that the active request receives the preparation signal.
- Keep New tabs provisional so opening or creating a design reuses the active tab
- Separate recent document, storage, menu, worker, and workspace responsibilities
- Add source-aware recents, responsive files UI, loading states, and localized copy
- Preserve native local Open Recent behavior while supporting remote storage history
* Revert "feat(ai): add HarnessAgent sidecar foundation (#560)"
This reverts commit 83a5ea1b42.
* Revert "Revert "feat(ai): add HarnessAgent sidecar foundation (#560)""
This reverts commit 0d8c03515888c62dc47186d4a3b0b7b04e78f8af.
* refactor(ai): ship Harness as optional companion
* fix(ai): support Harness companion on Windows
* test(ai): restore navigator after Harness Windows test
- Open multiple selected design files in separate tabs.\n- Support desktop, File System Access, and fallback pickers.\n- Continue opening later selections when one file fails.
- Serialize recovery cleanup with active snapshot writes
- Preserve version ordering across autosave, restore, and cleanup
- Fall back to memory when IndexedDB operations fail
* fix(tauri): prevent Windows font loading crashes
- Return native font files over binary Tauri IPC instead of JSON byte arrays
- Resolve desktop script fallbacks without parsing large system fonts in JavaScript
* fix(core): restore quality checks
- Use the shared Vector primitive for render-bound offsets
- Remove unsupported SLICE handling from SceneGraph rescaling
- Drop an unused generated-text test binding
* chore: address font fix review
- Place the release note under the Fixed heading
- Name fallback resolution options and use the shared Tauri constant
* fix(mcp): close orphaned servers that no app ever claims
- Add ServerOptions.appAttachTimeoutMs: if no app registers within this
window after startup, the server closes itself and removes its
discovery file, instead of squatting the port indefinitely.
- Wire it through the openpencil-mcp-http CLI as
OPENPENCIL_MCP_APP_TIMEOUT_MS (opt-in, unset/0 disables it — a bare
CLI invocation for manual testing should not self-terminate).
- The desktop app opts in with a 30s timeout when it spawns the server.
Without this, a server that outlives its spawning app (renderer crash,
forced reload) keeps holding its port with a stale discovery file. The
app's liveness check only asks whether something answers /health, not
whether an app has ever registered (see /health's no_app status) — so
every later launch finds the orphan already listening and defers to
it, and MCP tool calls fail with "app is not connected" until someone
manually kills the orphaned process. Closing self-caused orphans at
the source means the next launch finds no discovery file and takes
the normal fresh-spawn path.
Fixes#488
* fix(mcp): clean up servers after app disconnects
- Re-arm the orphan watchdog when the registered app disconnects
- Cancel pending shutdown when the app reconnects within the grace period
- Reject timeout values that overflow the runtime timer range
* test(mcp): make watchdog reconnect coverage deterministic
- Wait for the disconnected health state before reconnecting
- Report distinct safe-integer and timer-range validation errors
---------
Co-authored-by: swe-sanad <sanad.arousi@export119.com>
- Load Prism JSX only after exposing the Prism runtime
- Treat unavailable MCP automation as optional during startup
- Connect the desktop automation bridge only after MCP is ready
- 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
- Track remote storage bindings without replacing local file identity
- Route saves and autosaves through the durable local cache before enqueueing uploads
- Clear storage bindings on Save As and cover ordering and identity behavior
Co-authored-by: Rob Coenen <753704+rcoenen@users.noreply.github.com>
- Route npm-installed ACP and MCP launchers through cmd on Windows
- Preserve inherited agent environment and current MCP transport settings
- Cover Windows process resolution and document the user-facing fix
Co-authored-by: Damián Briones <insertnickname1@gmail.com>
- Prefer private Unix sockets with TCP fallback for local MCP clients
- Unify HTTP and WebSocket lifecycle, authentication, and cleanup
- Discover transport details from an owner-only runtime file
- Reject corrupt compressed Kiwi chunks instead of accepting raw garbage\n- Tolerate legacy source metadata without editedFields in renderer fallbacks\n- Align font, derived glyph, shared style, renderer mock, and ownership tests with current contracts\n- Restore clean quick and heavy unit quality gates
- Replace simple no-op test callbacks with explicit undefined returns
- Remove no-empty-function exceptions for autosave, stroke picker, font settings, pen, tauri, AI adapter, and undo helper tests
- Validate check plus affected engine and E2E tests
- 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
- Move top-level engine and e2e prefixed test files under domain folders
- Update fixture path helpers after moving render and pen tests
- Add lint coverage to prevent new top-level prefixed test files
- Refresh testing docs for the new fig and layout paths