Initial commit: Tauri + CanvasKit + React editor scaffold
- Tauri v2 desktop shell with 1280x800 window
- CanvasKit WASM (Skia) rendering via WebGL2
- Scene graph with typed nodes (Frame, Rectangle, Ellipse, Line, etc.)
- Skia renderer with fills, strokes, rounded corners, opacity, rotation
- Selection system with resize handles
- Undo/redo manager (inverse command pattern with batching)
- Bottom toolbar (Select, Frame, Rect, Ellipse, Line) matching Figma UI3
- Left layers panel, right properties panel
- Pan (scroll), zoom (Ctrl+scroll toward cursor)
- Keyboard shortcuts: V/F/R/O/L for tools, Cmd+Z undo, Backspace delete
- 5 demo shapes rendered on canvas
2026-02-27 12:48:29 +00:00
|
|
|
{
|
|
|
|
|
"$schema": "../gen/schemas/desktop-schema.json",
|
|
|
|
|
"identifier": "default",
|
|
|
|
|
"description": "Capability for the main window",
|
|
|
|
|
"windows": ["main"],
|
|
|
|
|
"permissions": [
|
|
|
|
|
"core:default",
|
fix: protect unsaved documents and defer credential access (#713)
* fix(app): protect unsaved documents when closing
Mark tabs with unsaved content updates and ask whether to save before
closing them. The prompt now covers tab closes, the desktop window close
button, and the application Quit action, which previously discarded work
when autosave had no writable target.
Track a content revision separately from scene and recovery versions so a
save only clears the indicator when it wrote the revision it captured.
Cancelled pickers, failed writes, and edits made during a save keep the
document open. Desktop uses the platform alert; the browser keeps the
styled dialog.
The desktop menu replaces the predefined Quit item so the accelerator and
Dock-independent quit path request confirmation instead of exiting.
* fix(ai): resolve credentials only when used
Opening a document, creating a chat, or browsing chat history connected
the provider and read saved secrets, which triggered system credential
prompts without user intent.
Startup now reads credential status only, migration runs inside the first
explicit resolution, and the chat panel initializes local history without
creating a transport. Stock-photo keys resolve per search instead of at
settings refresh, and credentials still marked legacy count as configured
so upgrading does not appear to lose them.
* refactor(ai): export diagnostics from Settings only
Chat kept its own debug log, copied mixed app-wide usage into a
conversation export, and reported a missing cache rate as zero. Remove
that surface and record AI requests, model steps, and tool activity as
correlated diagnostic events instead.
Settings remains the single export location, usage summaries can now
distinguish unreported telemetry from zero, and transcript or tool
payloads are no longer part of the export.
* fix(ai): clear legacy credentials for real
Clearing a Pexels, Unsplash, or provider key only removed the current
store entry. A value that still lived in legacy storage kept the key
configured, so a later search migrated and used the credential the user
had just removed.
Migrate before mutating so clearing also removes the legacy value, and
share one in-flight migration so the media and provider paths cannot
migrate the same plaintext twice.
* fix(ai): scope credential migration per source
Sharing one migration promise process-wide let a second storage return
the first migration's result, leaving its own legacy keys unmigrated
while reporting success. Track in-flight migrations per storage and
serialize them, because every migration writes to the same store and
concurrent runs could overwrite each other.
* fix(app): destroy the window after a confirmed close
Tauri's onCloseRequested helper destroys the window itself when a handler
returns without preventing the event. Approving a close therefore invoked
plugin:window|destroy, which the capability set did not grant, so the
window stayed open with a permission error after saving.
Always intercept the request and destroy the window explicitly once the
choice is confirmed, and grant core:window:allow-destroy in place of the
now-unused close permission.
* fix(app): show a filled dot for unsaved tabs
The unsaved indicator used a stroked Lucide circle whose fill attribute
kept it an empty outline, reading as a disabled control. Draw the
indicator as a filled accent dot matching the status dots used elsewhere
in the app.
* refactor(app): focus the unsaved prompt with VueUse
Replace the manual watcher, nextTick, and component $el focus with
useFocus, which focuses the Save button when the dialog mounts. Assert the
focus in the close-protection test so the Return-saves behavior stays
covered.
* refactor(app): route Quit through the shared menu channel
The Quit item emitted a bespoke app:request-exit event and the close
module listened for it, while every other native item travels as a
menu-event id dispatched by the shell and editor menu composables.
Emit menu-event "quit" for both the Quit item and the platform exit
request, handle it in useShellMenu beside check-updates, and share one
confirmAppExit so window closes and app exits agree on a single approval.
* refactor(app): generate the macOS app menu entries
The application menu hardcoded its labels and the Quit accelerator in
Rust while every other menu entry is generated from APP_MENU_SCHEMA.
Move the custom app entries (About, Check for Updates, Quit) into
APP_MENU_APP_ITEMS and emit desktop/generated/app-menu.json, keyed by id
so the native builder cannot silently drop a label.
Placement stays in Rust because the OS-predefined items sit between them,
and the menu title now comes from the packaged product name.
* build(tauri-menu): check generated menus against the schema
The generated menu files are committed but nothing verified them, so a
schema edit could silently leave desktop/generated stale until the next
release build regenerated it.
Split the renderers from the write step, register the tool as a workspace
so its dependencies resolve, and compare the committed files with the
schema in a test that runs with the other tool checks.
* fix(app): serialize exit confirmations
The window close handler and the Quit item both call confirmAppExit, and
the per-handler closing flag does not cover the two paths. Both could run
close preparation, so an unsaved document could be prompted twice.
Share one in-flight confirmation and clear it when it settles, so a
cancelled or failed attempt still prompts again on the next request.
2026-09-17 12:25:15 +00:00
|
|
|
"core:window:allow-destroy",
|
feat(desktop): show updates in a Software Update window (#936)
* 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.
2026-10-06 14:57:31 +00:00
|
|
|
"core:webview:allow-create-webview-window",
|
|
|
|
|
"core:window:allow-set-focus",
|
|
|
|
|
"core:window:allow-unminimize",
|
2026-02-28 10:54:21 +00:00
|
|
|
"opener:default",
|
|
|
|
|
"dialog:default",
|
|
|
|
|
"dialog:allow-save",
|
|
|
|
|
"dialog:allow-open",
|
2026-05-01 09:18:53 +00:00
|
|
|
"updater:default",
|
|
|
|
|
"process:default",
|
2026-07-01 07:40:29 +00:00
|
|
|
"clipboard-manager:allow-read-text",
|
|
|
|
|
"clipboard-manager:allow-write-html",
|
|
|
|
|
"clipboard-manager:allow-write-text",
|
fix: refuse risky desktop writes and run every Storybook play function (#933)
* fix(desktop): refuse writes where a written file would run
The fs scope let the webview write, create folders, and delete anywhere. Documents may still be saved anywhere, but the global scope now denies login items and startup folders, PowerShell profiles, and the global package and executable folders where coding agents and OpenPencil's companions live, and writes to the MCP discovery files agents trust. requireLiteralLeadingDot keeps hidden files and folders, such as shell profiles and agent settings, out of ** on Windows as Tauri already does on macOS and Linux. A native test saves a document and is refused a LaunchAgents file, a home dotfile, and the discovery file.
* fix(ui): draw segmented controls at panel field height
Panel fields moved to 24px when sizing tokens became plain utilities, but segmented control items stayed 22px inside a 2px padding, so the Typography and resizing controls stood 2px taller than the fields beside them. The panel foundation story renders its inputs at the panel size and checks 24px.
* test(storybook): run every story and its play function
No test ran the play functions, and five had gone stale: the layer tree example labelled a wrapper with the same name as its row, the chat composer's label gained an ellipsis, the MCP failure story queried a test id attribute the app does not use, and the property primitives story still collapsed sections whose titles are static now. bun run test:storybook now renders every story and fails on a story or play function that throws.
* fix(desktop): deny protected folders themselves and writable opens of the discovery files
Each protected folder is denied alongside its contents, so a recursive remove cannot target the folder itself, and the MCP discovery files are denied to open as well as write, since opening with truncate would empty them. The native test opens the discovery file for writing without truncating, and removes only files it created. The story test waits for storyFinished, which follows afterEach, and judges exceptions and non-accessibility reports.
* test(desktop): run the file scope check only on macOS
Its protected paths are macOS ones, so other platforms skip it rather than pass for another reason.
* docs: note that documents opened from hidden folders can still be saved
2026-10-06 14:52:12 +00:00
|
|
|
"fs:deny-default",
|
|
|
|
|
{
|
|
|
|
|
"identifier": "fs:scope",
|
|
|
|
|
"deny": [
|
|
|
|
|
{ "path": "$HOME/Library/LaunchAgents" },
|
|
|
|
|
{ "path": "$HOME/Library/LaunchAgents/**" },
|
|
|
|
|
{ "path": "$DATA/Microsoft/Windows/Start Menu/Programs/Startup" },
|
|
|
|
|
{ "path": "$DATA/Microsoft/Windows/Start Menu/Programs/Startup/**" },
|
|
|
|
|
{ "path": "$DOCUMENT/PowerShell" },
|
|
|
|
|
{ "path": "$DOCUMENT/PowerShell/**" },
|
|
|
|
|
{ "path": "$DOCUMENT/WindowsPowerShell" },
|
|
|
|
|
{ "path": "$DOCUMENT/WindowsPowerShell/**" },
|
|
|
|
|
{ "path": "/usr/local" },
|
|
|
|
|
{ "path": "/usr/local/**" },
|
|
|
|
|
{ "path": "/opt/homebrew" },
|
|
|
|
|
{ "path": "/opt/homebrew/**" },
|
|
|
|
|
{ "path": "$HOME/bin" },
|
|
|
|
|
{ "path": "$HOME/bin/**" },
|
|
|
|
|
{ "path": "$HOME/n" },
|
|
|
|
|
{ "path": "$HOME/n/**" },
|
|
|
|
|
{ "path": "$DATA/npm" },
|
|
|
|
|
{ "path": "$DATA/npm/**" },
|
|
|
|
|
{ "path": "$DATA/nvm" },
|
|
|
|
|
{ "path": "$DATA/nvm/**" },
|
|
|
|
|
{ "path": "$DATA/fnm" },
|
|
|
|
|
{ "path": "$DATA/fnm/**" },
|
|
|
|
|
{ "path": "$LOCALDATA/Volta" },
|
|
|
|
|
{ "path": "$LOCALDATA/Volta/**" },
|
|
|
|
|
{ "path": "$LOCALDATA/Programs" },
|
|
|
|
|
{ "path": "$LOCALDATA/Programs/**" },
|
|
|
|
|
{ "path": "$LOCALDATA/Microsoft/WindowsApps" },
|
|
|
|
|
{ "path": "$LOCALDATA/Microsoft/WindowsApps/**" },
|
|
|
|
|
{ "path": "$HOME/scoop" },
|
|
|
|
|
{ "path": "$HOME/scoop/**" }
|
|
|
|
|
]
|
|
|
|
|
},
|
2026-02-28 11:48:22 +00:00
|
|
|
{
|
|
|
|
|
"identifier": "fs:allow-read-file",
|
|
|
|
|
"allow": [{ "path": "**" }]
|
|
|
|
|
},
|
2026-10-04 13:03:20 +00:00
|
|
|
{
|
|
|
|
|
"identifier": "fs:allow-stat",
|
|
|
|
|
"allow": [{ "path": "**" }]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"identifier": "fs:allow-open",
|
fix: refuse risky desktop writes and run every Storybook play function (#933)
* fix(desktop): refuse writes where a written file would run
The fs scope let the webview write, create folders, and delete anywhere. Documents may still be saved anywhere, but the global scope now denies login items and startup folders, PowerShell profiles, and the global package and executable folders where coding agents and OpenPencil's companions live, and writes to the MCP discovery files agents trust. requireLiteralLeadingDot keeps hidden files and folders, such as shell profiles and agent settings, out of ** on Windows as Tauri already does on macOS and Linux. A native test saves a document and is refused a LaunchAgents file, a home dotfile, and the discovery file.
* fix(ui): draw segmented controls at panel field height
Panel fields moved to 24px when sizing tokens became plain utilities, but segmented control items stayed 22px inside a 2px padding, so the Typography and resizing controls stood 2px taller than the fields beside them. The panel foundation story renders its inputs at the panel size and checks 24px.
* test(storybook): run every story and its play function
No test ran the play functions, and five had gone stale: the layer tree example labelled a wrapper with the same name as its row, the chat composer's label gained an ellipsis, the MCP failure story queried a test id attribute the app does not use, and the property primitives story still collapsed sections whose titles are static now. bun run test:storybook now renders every story and fails on a story or play function that throws.
* fix(desktop): deny protected folders themselves and writable opens of the discovery files
Each protected folder is denied alongside its contents, so a recursive remove cannot target the folder itself, and the MCP discovery files are denied to open as well as write, since opening with truncate would empty them. The native test opens the discovery file for writing without truncating, and removes only files it created. The story test waits for storyFinished, which follows afterEach, and judges exceptions and non-accessibility reports.
* test(desktop): run the file scope check only on macOS
Its protected paths are macOS ones, so other platforms skip it rather than pass for another reason.
* docs: note that documents opened from hidden folders can still be saved
2026-10-06 14:52:12 +00:00
|
|
|
"allow": [{ "path": "**" }],
|
|
|
|
|
"deny": [
|
|
|
|
|
{ "path": "$HOME/Library/Application Support/OpenPencil/mcp.json" },
|
|
|
|
|
{ "path": "$LOCALDATA/OpenPencil/mcp.json" },
|
|
|
|
|
{ "path": "$RUNTIME/openpencil/mcp.json" },
|
|
|
|
|
{ "path": "$HOME/.openpencil/mcp.json" }
|
|
|
|
|
]
|
2026-10-04 13:03:20 +00:00
|
|
|
},
|
|
|
|
|
"fs:allow-fstat",
|
|
|
|
|
"fs:allow-seek",
|
|
|
|
|
"fs:allow-read",
|
2026-08-17 20:15:12 +00:00
|
|
|
{
|
|
|
|
|
"identifier": "fs:allow-read-text-file",
|
2026-08-19 13:49:21 +00:00
|
|
|
"allow": [
|
feat(ai): add guided AI setup for providers, coding agents, and Pi (#916)
* 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>
2026-10-07 08:46:57 +00:00
|
|
|
{ "path": "$HOME/.pi/agent/settings.json" },
|
2026-08-19 13:49:21 +00:00
|
|
|
{ "path": "$HOME/Library/Application Support/OpenPencil/mcp.json" },
|
|
|
|
|
{ "path": "$LOCALDATA/OpenPencil/mcp.json" },
|
|
|
|
|
{ "path": "$RUNTIME/openpencil/mcp.json" },
|
|
|
|
|
{ "path": "$HOME/.openpencil/mcp.json" }
|
|
|
|
|
]
|
2026-08-17 20:15:12 +00:00
|
|
|
},
|
2026-02-28 11:48:22 +00:00
|
|
|
{
|
|
|
|
|
"identifier": "fs:allow-write-file",
|
fix: refuse risky desktop writes and run every Storybook play function (#933)
* fix(desktop): refuse writes where a written file would run
The fs scope let the webview write, create folders, and delete anywhere. Documents may still be saved anywhere, but the global scope now denies login items and startup folders, PowerShell profiles, and the global package and executable folders where coding agents and OpenPencil's companions live, and writes to the MCP discovery files agents trust. requireLiteralLeadingDot keeps hidden files and folders, such as shell profiles and agent settings, out of ** on Windows as Tauri already does on macOS and Linux. A native test saves a document and is refused a LaunchAgents file, a home dotfile, and the discovery file.
* fix(ui): draw segmented controls at panel field height
Panel fields moved to 24px when sizing tokens became plain utilities, but segmented control items stayed 22px inside a 2px padding, so the Typography and resizing controls stood 2px taller than the fields beside them. The panel foundation story renders its inputs at the panel size and checks 24px.
* test(storybook): run every story and its play function
No test ran the play functions, and five had gone stale: the layer tree example labelled a wrapper with the same name as its row, the chat composer's label gained an ellipsis, the MCP failure story queried a test id attribute the app does not use, and the property primitives story still collapsed sections whose titles are static now. bun run test:storybook now renders every story and fails on a story or play function that throws.
* fix(desktop): deny protected folders themselves and writable opens of the discovery files
Each protected folder is denied alongside its contents, so a recursive remove cannot target the folder itself, and the MCP discovery files are denied to open as well as write, since opening with truncate would empty them. The native test opens the discovery file for writing without truncating, and removes only files it created. The story test waits for storyFinished, which follows afterEach, and judges exceptions and non-accessibility reports.
* test(desktop): run the file scope check only on macOS
Its protected paths are macOS ones, so other platforms skip it rather than pass for another reason.
* docs: note that documents opened from hidden folders can still be saved
2026-10-06 14:52:12 +00:00
|
|
|
"allow": [{ "path": "**" }],
|
|
|
|
|
"deny": [
|
|
|
|
|
{ "path": "$HOME/Library/Application Support/OpenPencil/mcp.json" },
|
|
|
|
|
{ "path": "$LOCALDATA/OpenPencil/mcp.json" },
|
|
|
|
|
{ "path": "$RUNTIME/openpencil/mcp.json" },
|
|
|
|
|
{ "path": "$HOME/.openpencil/mcp.json" }
|
|
|
|
|
]
|
2026-03-03 07:12:02 +00:00
|
|
|
},
|
2026-08-17 20:15:12 +00:00
|
|
|
{
|
|
|
|
|
"identifier": "fs:allow-exists",
|
2026-08-19 13:49:21 +00:00
|
|
|
"allow": [
|
feat(ai): add guided AI setup for providers, coding agents, and Pi (#916)
* 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>
2026-10-07 08:46:57 +00:00
|
|
|
{ "path": "$HOME/.pi/agent/auth.json" },
|
2026-08-19 13:49:21 +00:00
|
|
|
{ "path": "$HOME/Library/Application Support/OpenPencil/mcp.json" },
|
|
|
|
|
{ "path": "$LOCALDATA/OpenPencil/mcp.json" },
|
|
|
|
|
{ "path": "$RUNTIME/openpencil/mcp.json" },
|
|
|
|
|
{ "path": "$HOME/.openpencil/mcp.json" }
|
|
|
|
|
]
|
2026-08-17 20:15:12 +00:00
|
|
|
},
|
2026-04-22 14:05:54 +00:00
|
|
|
{
|
|
|
|
|
"identifier": "fs:allow-mkdir",
|
|
|
|
|
"allow": [{ "path": "**" }]
|
|
|
|
|
},
|
2026-05-05 18:50:31 +00:00
|
|
|
{
|
|
|
|
|
"identifier": "fs:allow-remove",
|
fix: refuse risky desktop writes and run every Storybook play function (#933)
* fix(desktop): refuse writes where a written file would run
The fs scope let the webview write, create folders, and delete anywhere. Documents may still be saved anywhere, but the global scope now denies login items and startup folders, PowerShell profiles, and the global package and executable folders where coding agents and OpenPencil's companions live, and writes to the MCP discovery files agents trust. requireLiteralLeadingDot keeps hidden files and folders, such as shell profiles and agent settings, out of ** on Windows as Tauri already does on macOS and Linux. A native test saves a document and is refused a LaunchAgents file, a home dotfile, and the discovery file.
* fix(ui): draw segmented controls at panel field height
Panel fields moved to 24px when sizing tokens became plain utilities, but segmented control items stayed 22px inside a 2px padding, so the Typography and resizing controls stood 2px taller than the fields beside them. The panel foundation story renders its inputs at the panel size and checks 24px.
* test(storybook): run every story and its play function
No test ran the play functions, and five had gone stale: the layer tree example labelled a wrapper with the same name as its row, the chat composer's label gained an ellipsis, the MCP failure story queried a test id attribute the app does not use, and the property primitives story still collapsed sections whose titles are static now. bun run test:storybook now renders every story and fails on a story or play function that throws.
* fix(desktop): deny protected folders themselves and writable opens of the discovery files
Each protected folder is denied alongside its contents, so a recursive remove cannot target the folder itself, and the MCP discovery files are denied to open as well as write, since opening with truncate would empty them. The native test opens the discovery file for writing without truncating, and removes only files it created. The story test waits for storyFinished, which follows afterEach, and judges exceptions and non-accessibility reports.
* test(desktop): run the file scope check only on macOS
Its protected paths are macOS ones, so other platforms skip it rather than pass for another reason.
* docs: note that documents opened from hidden folders can still be saved
2026-10-06 14:52:12 +00:00
|
|
|
"allow": [{ "path": "**" }],
|
|
|
|
|
"deny": [
|
|
|
|
|
{ "path": "$HOME/Library/Application Support/OpenPencil/mcp.json" },
|
|
|
|
|
{ "path": "$LOCALDATA/OpenPencil/mcp.json" },
|
|
|
|
|
{ "path": "$RUNTIME/openpencil/mcp.json" },
|
|
|
|
|
{ "path": "$HOME/.openpencil/mcp.json" }
|
|
|
|
|
]
|
2026-05-05 18:50:31 +00:00
|
|
|
},
|
2026-04-22 14:05:54 +00:00
|
|
|
{
|
|
|
|
|
"identifier": "fs:allow-watch",
|
|
|
|
|
"allow": [{ "path": "**" }]
|
|
|
|
|
},
|
2026-03-14 07:20:15 +00:00
|
|
|
"fs:allow-unwatch",
|
2026-03-14 15:58:25 +00:00
|
|
|
{
|
|
|
|
|
"identifier": "shell:allow-spawn",
|
|
|
|
|
"allow": [
|
2026-10-06 11:19:05 +00:00
|
|
|
{ "name": "claude-agent-acp", "cmd": "claude-agent-acp", "args": false },
|
|
|
|
|
{ "name": "codex-acp", "cmd": "codex-acp", "args": false },
|
|
|
|
|
{ "name": "gemini", "cmd": "gemini", "args": ["--acp"] },
|
feat(ai): add guided AI setup for providers, coding agents, and Pi (#916)
* 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>
2026-10-07 08:46:57 +00:00
|
|
|
{
|
|
|
|
|
"name": "npm",
|
|
|
|
|
"cmd": "npm",
|
|
|
|
|
"args": [
|
|
|
|
|
"install",
|
|
|
|
|
"--global",
|
|
|
|
|
{
|
|
|
|
|
"validator": "^(@agentclientprotocol/(claude-agent-acp|codex-acp)|@open-pencil/(mcp|harness)@[0-9]+\\.[0-9]+\\.[0-9]+)$"
|
|
|
|
|
},
|
|
|
|
|
"--registry=https://registry.npmjs.org"
|
|
|
|
|
]
|
|
|
|
|
},
|
2026-10-06 11:19:05 +00:00
|
|
|
{ "name": "openpencil-mcp-http", "cmd": "openpencil-mcp-http", "args": false },
|
2026-08-19 17:04:35 +00:00
|
|
|
{ "name": "openpencil-harness", "cmd": "openpencil-harness", "args": false },
|
2026-10-06 11:19:05 +00:00
|
|
|
{ "name": "cmd-claude-agent-acp", "cmd": "cmd", "args": ["/c", "claude-agent-acp"] },
|
|
|
|
|
{ "name": "cmd-codex-acp", "cmd": "cmd", "args": ["/c", "codex-acp"] },
|
|
|
|
|
{ "name": "cmd-gemini", "cmd": "cmd", "args": ["/c", "gemini", "--acp"] },
|
feat(ai): add guided AI setup for providers, coding agents, and Pi (#916)
* 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>
2026-10-07 08:46:57 +00:00
|
|
|
{
|
|
|
|
|
"name": "cmd-npm",
|
|
|
|
|
"cmd": "cmd",
|
|
|
|
|
"args": [
|
|
|
|
|
"/c",
|
|
|
|
|
"npm",
|
|
|
|
|
"install",
|
|
|
|
|
"--global",
|
|
|
|
|
{
|
|
|
|
|
"validator": "^(@agentclientprotocol/(claude-agent-acp|codex-acp)|@open-pencil/(mcp|harness)@[0-9]+\\.[0-9]+\\.[0-9]+)$"
|
|
|
|
|
},
|
|
|
|
|
"--registry=https://registry.npmjs.org"
|
|
|
|
|
]
|
|
|
|
|
},
|
2026-10-06 11:19:05 +00:00
|
|
|
{ "name": "cmd-openpencil-mcp-http", "cmd": "cmd", "args": ["/c", "openpencil-mcp-http"] },
|
|
|
|
|
{ "name": "cmd-openpencil-harness", "cmd": "cmd", "args": ["/c", "openpencil-harness"] }
|
2026-03-14 15:58:25 +00:00
|
|
|
]
|
|
|
|
|
},
|
2026-03-14 07:20:15 +00:00
|
|
|
"shell:allow-stdin-write",
|
|
|
|
|
"shell:allow-kill"
|
Initial commit: Tauri + CanvasKit + React editor scaffold
- Tauri v2 desktop shell with 1280x800 window
- CanvasKit WASM (Skia) rendering via WebGL2
- Scene graph with typed nodes (Frame, Rectangle, Ellipse, Line, etc.)
- Skia renderer with fills, strokes, rounded corners, opacity, rotation
- Selection system with resize handles
- Undo/redo manager (inverse command pattern with batching)
- Bottom toolbar (Select, Frame, Rect, Ellipse, Line) matching Figma UI3
- Left layers panel, right properties panel
- Pan (scroll), zoom (Ctrl+scroll toward cursor)
- Keyboard shortcuts: V/F/R/O/L for tools, Cmd+Z undo, Backspace delete
- 5 demo shapes rendered on canvas
2026-02-27 12:48:29 +00:00
|
|
|
]
|
feat: open documents and layers from openpencil:// and web links (#708)
* feat(desktop): register openpencil:// deep link scheme
Signed-off-by: Marc Went <marc@went.io>
* feat(desktop): parse openpencil://open?file&node links
Signed-off-by: Marc Went <marc@went.io>
* feat(desktop): queue openpencil:// links as pending opens
Signed-off-by: Marc Went <marc@went.io>
* fix(desktop): read cold-start deep links on windows/linux
Signed-off-by: Marc Went <marc@went.io>
* feat(app): resolve openpencil:// links and select the target node
A link's file is repo-relative, so it is resolved against the paths of the
open tabs and otherwise located once by the user through the dialog picker.
Nothing else is read from disk and no fs scope is widened. The node is matched
by exact name on the current page, selected and zoomed to; a missing node
raises a notice instead of failing silently.
Signed-off-by: Marc Went <marc@went.io>
* docs: document the openpencil:// URL scheme
Describe the link format, the relative-path rule, how the file is resolved
against open tabs or a one-time picker, and that the scheme can only open a
document and select a layer.
Signed-off-by: Marc Went <marc@went.io>
* test(app): cover cancelled deep-link picks
Inject the file picker and open entry points into openDeepLink so the test can
drive the branch where the picked file is not the requested one. The repository
lint forbids module registry mocking, and the existing file batch helper takes
its opener the same way.
Reword the module comment: the opened file joins the recent-files list like any
other opened file, and a one-segment file matches the first open tab whose path
ends with it.
Signed-off-by: Marc Went <marc@went.io>
* docs: sharpen the URL scheme notes
Selecting by name selects every layer with that name on the current page and
zooms to the whole selection. Record that the first matching open tab wins,
that path separators may stay literal in the query, and how the scheme reaches
the app on each platform.
Signed-off-by: Marc Went <marc@went.io>
* refactor(app): keep the deep-link io type internal
Nothing outside the module names the injected io type, so contextual typing at
the call site is enough. Drop the redundant recording array from the cancelled
pick test.
Signed-off-by: Marc Went <marc@went.io>
* fix(desktop): tag pending opens by producer
The frontend classified a pending entry by the shape of its path, which
called a canonicalized Windows path (`\\?\C:\…`) relative and sent a
double-clicked document into the deep-link resolver. Rust now says which
producer queued the entry, and the tail both producers shared moves into
`queue_pending`.
Signed-off-by: Marc Went <marc@went.io>
* test(app): assert the opener receives the resolved path
Signed-off-by: Marc Went <marc@went.io>
* test(desktop): refuse a percent-encoded parent segment
Signed-off-by: Marc Went <marc@went.io>
* chore(desktop): relax the deep-link plugin pin
Signed-off-by: Marc Went <marc@went.io>
* chore(desktop): drop the unused deep-link capability
Draining links is Rust-side, so the webview never calls
`deep-link:allow-get-current`.
Signed-off-by: Marc Went <marc@went.io>
* fix(app): clamp link values in notices
Signed-off-by: Marc Went <marc@went.io>
* fix(desktop): pass deep links through the linux desktop entry
The bundler's default desktop template writes `Exec={{exec}}` with no
field code, so a Linux cold start from a deb, rpm or AppImage never
receives the `openpencil://` link as an argument and `get_current()`
has nothing to recover. Ship a custom template that is the bundler
default plus `%U`, wired to both the deb and rpm bundlers (AppImage
reuses the deb data dir). MIME types still come from `{{mime_type}}`,
so the file associations are unchanged.
Signed-off-by: Marc Went <marc@went.io>
* feat(app): open documents from ?file= links in the browser
The desktop build takes openpencil:// links; the web app had no equivalent.
It now reads file and node off its own address bar on boot, fetches the
document from an absolute https URL without credentials and without
following redirects, selects the named layer through the same path the
deep link uses, and strips both params so a reload does not re-open.
Signed-off-by: Marc Went <marc@went.io>
* fix(app): keep router state coherent when stripping web link params
Rewriting history directly left the router's own record of the current URL
pointing at the un-stripped one, so the next router.push wrote file and node
back into the history entry. The strip is now an injected action that goes
through router.replace, preserving the route, hash and every other query key.
Also clamp the failure detail, take the last value of a repeated key like the
desktop parser does, share deep-link's clamp instead of copying it, and report
a failed fetch through toast.error.
Signed-off-by: Marc Went <marc@went.io>
* fix(app): resolve deep links by filesystem case and bound remote fetches
Deep links resolved their file by comparing path segments in JavaScript,
which is case-sensitive: on macOS and Windows `Web/Design/hikyo.pen` and
`web/design/hikyo.pen` name the same file, yet both the open-tab lookup and
the picker check refused it and the link was cancelled. The comparison now
goes through a `path_matches_suffix` Tauri command that canonicalizes the
candidate and folds ASCII case on macOS and Windows while staying exact on
Linux. `resolveDeepLinkFile` takes the comparator as an argument, so it
stays testable without Tauri, and the rule is tested in `deep_link.rs`.
An already open document is focused through `activateTabForPath` instead of
`openFileFromPath`, which re-read the file from disk first and rejected the
whole link when it had moved or lost its permissions since the tab opened
it. The picker branch still opens the file, and a tab that closed between
the snapshot and the activate falls back to opening it.
A web link's `file` URL drops its fragment. The tab identity compares source
URLs exactly, so two links to one document differing only in fragment opened
two tabs.
A document fetched from a URL is capped at 64 MiB, counted off the streamed
body rather than the sender's `Content-Length`, with the request aborted the
moment it goes over instead of buffering whatever the host decides to send.
Draining the pending-open queue goes through `openDesignFileBatch`, the
per-item catch every other open path already uses, so one failing entry no
longer skips the rest of the batch.
Signed-off-by: Marc Went <marc@went.io>
* fix(desktop): match a deep-link suffix against the literal path too
Canonicalizing the candidate resolves a symlink that sits inside the trailing
segments, so a monorepo checkout where `packages/web` links to `../apps/web`
would stop matching a link that spells the path the way the tab does. Compare
both spellings: the canonical path keeps `..` and prefix symlinks working, the
literal one keeps the path the user actually sees. Both inputs are already-open
or user-picked paths, so trying the literal one grants nothing new.
Signed-off-by: Marc Went <marc@went.io>
* fix(app): cap the automation fetch and chain a caller's abort signal
`openBrowserFileFromURL` replaced a caller-supplied `signal` with the one the
size cap needs, so a caller could no longer cancel its own request. The two
are chained instead: the caller's abort aborts the cap's controller, and an
already-aborted signal is honoured before the fetch goes out.
`handleOpenFile` in the automation bridge was the last fetch buffering an
unbounded body. It reads a document the same way, so it gets the same 64 MiB
ceiling, counted off the stream and aborted on overflow. Its relative-path
resolution and its lack of a format assert are unchanged.
`path_matches_suffix` runs `async`, so `canonicalize` cannot block the main
thread on a stale network mount, and it now refuses an absolute or
`..`-bearing suffix: `parse_open_url` already does, but this is the comparison
every caller funnels through and an absolute suffix would otherwise match on
its segments alone. The command itself gained tests over a real temp tree —
exact match, the platform case rule, the symlinked trailing directory that
motivated the literal fallback, a missing file, and the refusals.
The docs and the module header claimed an opened file always joins the recent
files list, in the same breath as saying an already open tab is focused
without re-reading it. Only the former opens anything, so only the former
touches the list.
Signed-off-by: Marc Went <marc@went.io>
* docs(changelog): note the 64 MiB ceiling on the automation bridge openFile
Signed-off-by: Marc Went <marc@went.io>
* fix(app): deliver cold-start deep links through the deep-link path
macOS hands a launch `openpencil://` link to the app as `RunEvent::Opened`
before the app's `setup` closure runs. Traced on a cold `open`:
`RunEvent::Opened` at T+0.085 s, `setup` at T+0.342 s, and `on_open_url` never
fired. The plugin's `deep-link://new-url` emit therefore reached no listener
and the URL survived only in the plugin's `current`, which was drained under
`#[cfg(any(windows, target_os = "linux"))]` on the assumption that macOS was
unaffected. It is not: a cold link launched the app to an empty tab with no
picker, no toast and no log line, while the same link fired at a running app
worked. The drain now runs on every desktop platform; `register_all` stays
gated, macOS does not support it.
Nothing is queued twice. `RunEvent::Opened` is dispatched on the thread that
runs `setup`, so a link cannot arrive between registering `on_open_url` and
reading `current`, and anything later is no longer in `current`. A cold
double-clicked document is unaffected: `current` now also yields its `file://`
URL, and the `scheme == "openpencil"` filter in `queue_deep_links` drops it,
leaving `queue_open_paths` the only producer for that path.
The pending-open routing moves out of `WorkspaceView.vue` into
`app/document/io/pending-open.ts`, so which entry reaches the deep-link
resolver and which reaches the plain opener is unit-testable without mounting
the view. A drain that fails wholesale — the `take_pending_open` invoke, the
event binding — now raises a toast instead of only a console line; per-entry
failures were already toasted.
Signed-off-by: Marc Went <marc@went.io>
* refactor(app): share one bounded body reader
readBodyWithLimit reimplemented the chunked cap that vectorize's
readBoundedResponse already applied, and it lived in the menu module while
the automation bridge imported it from there.
Move the reader to the browser document-io owner as readBoundedBody,
returning bytes with an optional overflow hook and error message, and have
both the document fetch and the vectorize providers use it. The automation
bridge now opens a browser file through openBrowserFileFromURL instead of
re-inlining fetch, cap and tab creation, so it also gets the same format
check as the Tauri path, and the caller's abort signal is combined with the
cap's controller through AbortSignal.any.
* fix(app): report a failed tab activation
activateTabForPath returned true after calling switchTab, but switchTab
silently does nothing when the tab is gone. A tab that closed while the
identity lookup awaited therefore looked focused, and the caller skipped
opening the file, so the link did nothing at all.
Return whether a tab was actually activated.
* fix(app): translate the document link notices
The four notices added for document links existed only in the English
defaults, so a localized build showed English toasts. check:i18n does not
cover the app-level notification catalog, which is why nothing caught it.
Also correct the docs: a `.` segment is refused along with `..`, matching
the matcher.
* fix(desktop): refuse a dot segment in deep links
The parser accepted `web/./design.pen` while path_ends_with_segments
refuses `.`, so such a link was queued and could then never match an open
tab or a picked file — it failed silently after asking the user to locate
the file.
Refuse `.` alongside `..` in the parser and drop the whitespace-only line
left in the capability file.
* refactor(app): tidy the document link plumbing
Four smaller things from review:
- A dismissed file picker is not a wrong file, so it no longer reports
"expected a file ending in …", which named a file the user never chose.
- Reuse es-toolkit's omit for stripping the link params, as the MCP
settings form already does.
- Drop the openDesignFileBatch re-export from menu/use.ts; nothing
imports it from there.
- Move the exact-name lookup out of the view: selectNodesByName lives with
the other selection helpers and walks the graph directly, instead of
building a whole FigmaAPI facade from the automation bridge to answer
one query.
* refactor(app): centralize focusing nodes
The name lookup was a link-shaped helper in the selection domain, and it
baked one strategy into the action. Split it into the two things a caller
actually needs: focusNodes(ids) is the select-and-zoom primitive that
share and collaboration references want, and focusNodesByName resolves an
exact name on the current page first.
The store dependency is a narrow interface, as with the viewport actions,
so the action is unit-testable and stale ids can be ignored instead of
selected.
---------
Signed-off-by: Marc Went <marc@went.io>
Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-09-18 11:45:49 +00:00
|
|
|
}
|