Commit graph

51 commits

Author SHA1 Message Date
Danila Poyarkov daef57d52d
build: update dependencies (#873)
* build: update dependencies

Update the AI SDK providers, Vue, Reka UI, Valibot, Zod, es-toolkit,
CodeMirror, Storybook, Playwright, Hono and other dependencies to their
current releases, consistently across workspaces.

The Tauri plugin packages must match their Rust crates, and the new plugin
crates require Tauri 2.12, so Cargo.lock, @tauri-apps/api and the Tauri CLI
move to 2.12 as well.

* build(harness): update the AI SDK harness packages

@ai-sdk/harness 1.0.74 pinned ai 7.0.67, so the workspace carried a second
copy of ai next to the root one; 1.0.138 depends on the same ai release.

The Pi adapter no longer takes a model: HarnessAgent does. The settings
were spread from untyped records, so the compiler could not reject the
stale key and the chosen model would have been dropped; they are plain
literals now.

PiAuthOptions is now PiAuthenticationMode, and auth accepts an environment
record. Pass the gateway key that way instead of writing it into the
process-wide environment while a session is created. Derive the thinking
level from the adapter's settings, which adds 'max'.

* build: hold vue-tsc at 3.3.11

vue-tsc 3.3.12 no longer sees a v-slot binding inside a component that
also has an event listener, so check:vue reports "Cannot find name
'control'" in MCPConnectionEditor and ProfileEditor. 3.3.11 checks them
cleanly.

* fix(ai): keep retryability for provider errors reported mid-stream

From ai 7.0.80 a provider error after the response stream starts is a StreamProviderError rather than an APICallError, so classifyAIChatError lost its isRetryable.

* feat(desktop): accept updates only when signed for their version

Tauri CLI 2.12 records the app version in each updater signature, and
updater 2.13 checks it against the version latest.json announces. With
requireSignedVersion it also rejects signatures that carry no version, so a
tampered manifest cannot pair a newer version number with an older, still
validly signed bundle.

Release assembly now fails when a signature does not name the version
being released, instead of shipping one that installed apps would reject.

* docs: note the dependency update's security fixes in the changelog

* feat(ai): recommend the latest models

The provider packages now know Claude Sonnet 5.5 and Opus 5.5 and the GPT-6
series. Make Sonnet 5.5 and GPT-6.1 Sol the defaults, list Opus 5.5, Fable
5.1, GPT-6 Astra and GPT-6 Luna, and replace the two free OpenRouter models
that OpenRouter no longer serves.

* build: align the fig package's valibot with the workspace
2026-10-04 12:48:24 +00:00
Danila Poyarkov 8131401ead
fix: explain unsupported browsers instead of a blank window (#745)
* fix: explain unsupported browsers instead of a blank window

The desktop app on macOS 13 with WebKit older than Safari 17.4 opened an
empty window because startup called Promise.withResolvers, which Vite lowers
nothing for: build.target only rewrites syntax and never polyfills APIs, and
the target itself was an implicit Vite default (#744).

Make the supported baseline explicit in src/app/shell/support/baseline.ts and
feed it to build.target, a lint rule that rejects newer static built-ins in
browser-shipped sources, and the documented system requirements. Replace
Promise.withResolvers with a createDeferred() helper.

Turn src/main.ts into a small gate that checks sentinel features before
dynamically importing the app, so an old engine still evaluates enough code
to render platform-specific update guidance: macOS/Safari via Software
Update, WebKitGTK and WebView2 on Linux and Windows, and each browser's
own update path on the web, with a prefilled bug report link. Render-blocking
errors during the first route are captured through app.config.errorHandler
and shown the same way instead of leaving the window blank.

Desktop facts come from tauri-plugin-os and a webview_version command; the
bundle now declares macOS 13 as its minimum system version.

* build: enforce the browser baseline from compatibility data

Replace the hand-maintained list of built-ins newer than the baseline with
two data-driven checks. The app and browser-shipped packages pin their
TypeScript lib to ES2023, the last edition Chrome 111, Firefox 128 and
Safari 16.4 implement in full, so a newer built-in such as
Promise.withResolvers fails type-checking. Web APIs, which lib.dom does not
version, go through eslint-plugin-compat under oxlint with the same browsers
in settings.browsers, scoped to sources that ship to a browser.

A unit test keeps the oxlint browser list and the tsconfig libs derived from
src/app/shell/support/baseline.ts, so the three cannot drift apart.

* fix: recognise production error codes in the boot observer

Vue passes the error reference URL as the errorHandler info argument in
production builds instead of the development string, so the observer never
classified a setup or render failure as fatal in the shipped app and the
boot-failure notice only appeared on the dev server. Match Vue's exported
ErrorCodes in both forms, and cover the component-setup path in the E2E
spec; the scenario was also verified against a production build.
2026-09-22 14:40:59 +04:00
Danila Poyarkov 6cf1748e31 Release v0.15.1 2026-09-18 16:33:46 +03:00
Marc Went 511bb481ac
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 14:45:49 +03:00
Danila Poyarkov 7bc5c1fe2f Release v0.15.0 2026-09-16 17:57:35 +03:00
Danila Poyarkov 42225c604a
feat: refresh branding with generated platform icons (#707)
* feat: refresh branding with generated platform icons

Keep the approved mark and optical micro master as the source of truth. Generate web, documentation, and native assets at their owning build boundaries instead of storing raster variants or linking web icons to desktop output.

* fix: refine small brand marks and loading artwork

* feat: adopt blue editing-handle brand mark

* feat: refine teal branding for light and dark themes

* feat: apply ivory tiles across brand surfaces

Use edge-to-edge web tiles with a larger mark and optical micro favicons. Preserve native spacing and platform-owned maskable/touch cropping. Address review feedback on story props, native dimensions, and brand test type checking.

* test: allow cold npm startup in packaging fixture

CI hit Bun's five-second default while running npm pack --dry-run. Give this integration test a scoped 30-second budget without changing production timeouts or other tests.
2026-09-16 11:54:07 +03:00
Danila Poyarkov 62ba072b26
refactor(ai): ship Harness as optional companion (#561)
* Revert "feat(ai): add HarnessAgent sidecar foundation (#560)"

This reverts commit 83a5ea1b42.

* Revert "Revert "feat(ai): add HarnessAgent sidecar foundation (#560)""

This reverts commit 0d8c03515888c62dc47186d4a3b0b7b04e78f8af.

* refactor(ai): ship Harness as optional companion

* fix(ai): support Harness companion on Windows

* test(ai): restore navigator after Harness Windows test
2026-08-19 20:04:35 +03:00
Danila Poyarkov 83a5ea1b42
feat(ai): add HarnessAgent sidecar foundation (#560)
* feat(ai): add HarnessAgent sidecar foundation

* feat(ai): integrate Pi as a Harness provider

* fix(ai): harden Harness provider integration

* fix(ai): address Harness integration review

* fix(ai): align Harness code with project conventions

* fix(ai): preserve Harness sessions on cancellation

* test(ai): cover Harness JSONL framing

* refactor(ai): generalize Harness sidecar adapters

* fix(ci): build Harness sidecar for native checks
2026-08-19 17:41:10 +03:00
Danila Poyarkov 751195891f Release v0.14.0 2026-08-10 11:40:44 +03:00
rcoenen b8076adfbb
fix(desktop): enable image drag-and-drop in the Tauri window
Tauri's dragDropEnabled defaults to true, which makes the native OS file
drop intercept and swallow the event so the webview's HTML5 drop handler
(useCanvasDrop) never fires — drag-and-drop of images worked in the browser
but failed in the desktop app. Set dragDropEnabled: false so the webview
receives HTML5 drag/drop natively and the existing web handler works on
desktop too. No web-side code changes.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-06-30 09:50:15 +03:00
Danila Poyarkov 2dc51375ad Release v0.13.2 2026-05-30 19:14:12 +03:00
Danila Poyarkov 2d4e2a7b10 Release v0.13.1 2026-05-30 01:12:58 +03:00
Danila Poyarkov 6de7cd3871 Release v0.13.0 2026-05-30 00:16:35 +03:00
Danila Poyarkov 7a433584cc Release v0.12.2 2026-05-19 18:10:25 +03:00
Danila Poyarkov 8fa34fb0b8 Release v0.12.1 2026-05-19 13:27:24 +03:00
Danila Poyarkov 79583056a5 fix(tauri): refresh updater signing key 2026-05-18 23:23:20 +03:00
Danila Poyarkov 682160b75b Release v0.12.0 2026-05-18 21:06:06 +03:00
Danila Poyarkov ddf415ec30 feat(tauri): support desktop file associations
- Register .fig and .pen files with desktop bundles
- Open associated files on startup and via single-instance handoff
- Reuse the existing Tauri file open flow without broad filesystem scopes
2026-05-12 16:12:16 +03:00
Danila Poyarkov b248e0c53a feat(tauri): add signed updater 2026-05-01 13:00:50 +03:00
Danila Poyarkov f7b3f2de75 build(tauri): generate native menu before builds 2026-04-30 16:07:14 +03:00
Danila Poyarkov ba414e1af7 Release v0.11.8 2026-04-23 16:15:37 +03:00
Danila Poyarkov f3dbf2c271 Release v0.11.7 2026-04-22 20:31:33 +03:00
Joseph Cumines c38223a700
fix(tauri): correctly configure NSAllowsLocalNetworking
Fixes #213
2026-04-22 13:01:38 +03:00
Danila Poyarkov 837c09bd78 fix(tauri): ATS exception for localhost WebSocket on macOS
Allow insecure localhost connections via NSAllowsLocalNetworking so the
WebSocket bridge works in release builds on macOS.

Also add debug logging for WebSocket and MCP health check failures.

Closes #190
Co-authored-by: Anton Soldatov <sld0Ant@users.noreply.github.com>
2026-04-15 13:16:29 +03:00
Danila Poyarkov f8f4af5937 Release v0.11.6 2026-04-08 22:04:22 +03:00
Danila Poyarkov 2c3191cf96 Release v0.11.5 2026-04-08 21:38:44 +03:00
Danila Poyarkov 26a7d2d454 Release v0.11.4 2026-04-08 20:52:09 +03:00
Danila Poyarkov a20d170956 Release v0.11.3 2026-04-08 16:06:37 +03:00
Danila Poyarkov 33d293b221 Release v0.11.2 2026-03-30 18:38:36 +03:00
Danila Poyarkov 9be3514f2b Release v0.11.1 2026-03-30 17:24:00 +03:00
Danila Poyarkov 911bed3ede Release v0.11.0 2026-03-30 16:48:27 +03:00
Danila Poyarkov f4c68f7f41 Release v0.10.0 2026-03-15 19:27:37 +03:00
Danila Poyarkov fe755fbb85 Release v0.9.0 2026-03-09 17:43:11 +03:00
Danila Poyarkov 9882375b4f Release v0.8.0 2026-03-08 00:20:47 +03:00
Danila Poyarkov be44b4d404 Release v0.7.0 2026-03-05 10:32:35 +03:00
Danila Poyarkov 7ad4116860 Release v0.6.0 2026-03-04 01:47:37 +03:00
Danila Poyarkov 0b56cc1e70 Set up Apple code signing and notarization 2026-03-04 01:40:19 +03:00
Danila Poyarkov 9c9bfc38b2 Release v0.5.1 2026-03-03 17:30:09 +03:00
Danila Poyarkov 5eac653f4c Release v0.5.0 2026-03-03 14:51:56 +03:00
Danila Poyarkov 7d98201548 Release v0.4.2 2026-03-02 18:14:35 +03:00
Danila Poyarkov 7eb74c3969 Release v0.4.1 2026-03-02 15:51:34 +03:00
Danila Poyarkov 8fea6c086d Bump all packages to 0.4.0, fix Node.js ESM compatibility
- tsgo + fix-esm-import-path for .js extensions in dist/
- Core exports: bun condition → src, import → dist
- MCP depends on core ^0.4.0
2026-03-02 14:59:59 +03:00
Danila Poyarkov 518ddb0c5b Release v0.3.2 2026-03-02 11:36:18 +03:00
Danila Poyarkov 1b414bf680 Release v0.3.1 2026-03-02 11:14:15 +03:00
Danila Poyarkov f495f4072a Release v0.3.0 2026-03-01 22:46:06 +03:00
Danila Poyarkov 5e209eca1c Fix trackpad pinch-to-zoom on Safari macOS 2026-03-01 22:07:47 +03:00
Danila Poyarkov 13a95bf517 Release v0.2.1 2026-03-01 21:25:52 +03:00
Danila Poyarkov 6e4332a6ba Release v0.2.0 2026-03-01 18:27:12 +03:00
Danila Poyarkov faf7ba6387 Fix app name in macOS Dock: open-pencil-app → OpenPencil 2026-03-01 02:39:32 +03:00
Danila Poyarkov 7ff16f70e0 Fix app identifier to net.dannote.open-pencil, add CLI/headless plan to PLAN.md 2026-02-28 23:51:22 +03:00