Commit Graph
20 Commits
Author SHA1 Message Date
YOMXXXandGitHub 1b4084ced0 fix(desktop): route shell:open-path through explorer.exe on WSL (#1581) (#3298)
On WSL Electron, clicking "Continue in CLI" handed the validated project
directory to shell.openPath, which delegates to xdg-open on Linux.
xdg-open on common WSL installs routes inode/directory MIME through the
default browser via wslu, so the click landed on a Chrome tab showing a
file:// directory listing instead of a native file manager.

Detect WSL via os.release().includes("microsoft"), translate the resolved
POSIX path with `wslpath -w`, and hand the Windows path to explorer.exe so
the host's Explorer opens the folder. Fall back to shell.openPath for
non-WSL Linux, macOS, native Windows, and WSL setups where wslpath or
explorer.exe are missing.

Pull the routing decision into a pure helper (open-path.ts) so the WSL
branch is unit-testable with mocked deps, mirroring the dependency-
injection pattern createDesktopUpdater already uses. Path validation
(validateExistingDirectory, isOpenPathAllowedForProject) stays in front
of the new branch — no change to the allowlist gate.
2026-06-02 07:32:59 +00:00
kamiandGitHub 82ea01836e fix: polish Windows chrome and selected rows (#2169) 2026-05-31 14:44:44 +00:00
b5b975769a fix Windows folder import from project modal (#2863)
Co-authored-by: Lanzhou3 <217479610+Lanzhou3@users.noreply.github.com>
2026-05-26 06:52:07 +00:00
PerishFireandGitHub af66a929bc Fix release updater smoke recovery (#2687) 2026-05-22 16:18:33 +08:00
PerishFireandGitHub b4e94b0534 Harden packaged updater downloads and install handoff (#2677)
* Add managed download package for updater resumes

* fix(download): clear stale pid locks

* test(e2e): harden windows updater resume smoke

* feat(updater): make update downloads silent in ui

* fix(updater): keep install handoff prompt visible

* fix(ci): build platform before download in postinstall
2026-05-22 15:44:28 +08:00
PerishFireandGitHub ec96585a8f fix: harden Windows packaged updater flow (#2595) 2026-05-21 22:32:02 +08:00
PerishFireandGitHub 415c2e9502 fix: clear applied updater install state (#2592) 2026-05-21 21:50:26 +08:00
PerishFireandGitHub 1ef865dd31 fix: defer Windows packaged updater installer (#2575)
* fix: tighten packaged updater flow

* test: prune noisy extended ui coverage

* fix: hide unpublished release artifacts

* test: validate release updater channels

* fix: align prerelease release namespaces

* fix: align packaged updater validation
2026-05-21 19:13:18 +08:00
lefarcenandGitHub b2b94dbde7 feat(desktop): follow OS language in packaged builds (cherry-pick of #2544 into release/v0.8.0) (#2560)
* feat(desktop): follow OS language in packaged builds

Packaged Electron currently shows Open Design in en-US regardless of
the OS language setting, because the renderer's i18n picks its locale
from `navigator.language` and Chromium hard-codes that to en-US unless
the host process intervenes. Browser users and `tools-dev` users are
unaffected because their `navigator.language` already reflects the
OS / browser preference.

This change:

- Adds `applyOsLocaleSwitch(app)` in `@open-design/desktop/main`. It
  reads `app.getPreferredSystemLanguages()[0]` and (when called before
  Electron's `ready` event) points Chromium's `--lang` flag at it, so
  the renderer's `navigator.language` follows the OS. Safe to call
  more than once: `appendSwitch` is a no-op once `app.isReady()`.
- Calls the helper from both Electron entries: `apps/packaged` before
  its own `whenReady`, and `runDesktopMain` for tools-dev parity.
- Forwards the resolved locale through
  `BrowserWindow.webPreferences.additionalArguments` as
  `--od-os-locale=<bcp-47>`, parsed by the preload and exposed at
  `window.__od__.client.osLocale`. The host bridge type
  (`OpenDesignHostClient.osLocale`) is extended accordingly.
- Updates `detectInitialLocale` in `apps/web/src/i18n/index.tsx` to
  read that field as a new step between the existing localStorage and
  navigator fallbacks. Browser/web continues to fall through to
  `navigator.languages` unchanged.

The explicit `osLocale` channel exists in addition to `--lang` because
some `app.getPreferredSystemLanguages()` strings (e.g. `zh-Hant-TW`,
`pt-PT`) need to round-trip through `resolveSystemLocale` to land on
the right supported locale, which Chromium's `navigator.language`
cannot do on its own.

* fix(web): route OS locale read through getOpenDesignHost

The first cut of detectInitialLocale read `window.__od__.client.osLocale`
directly, which trips `tests/host-boundary.test.ts` — that guard test
keeps web source from referencing preload globals by name so the
boundary stays single-source. Switch to `getOpenDesignHost()` from
`@open-design/host`, and rewrite the i18n test to install the host via
`installMockOpenDesignHost` instead of poking the global directly.

* fix(tools-pack): unblock mac packaged build on pnpm workspaces

Two independent issues prevented `pnpm tools-pack mac build --to all`
from completing on a clean macOS workspace, both unrelated to the
desktop OS-locale change in this PR but bundled here because verifying
that change end-to-end required the packaged pipeline to actually
finish.

1. `apps/web/.next/standalone/node_modules/.pnpm/node_modules/<pkg>`
   contained dangling symlinks left by Next's nft trace (e.g. a
   `semver -> ../semver@5.7.2/node_modules/semver` link to a
   `.pnpm/<pkg>@<ver>` directory pnpm never created). The downstream
   `cp { dereference: true }` aborted the whole packaged pipeline
   with ENOENT. Walk every artifact tree before copy and unlink
   symlinks whose target doesn't resolve. Targets that *do* resolve
   stay untouched.

2. Next 16's standalone build under pnpm workspaces does not hoist
   peer-dep packages (react, react-dom, styled-jsx) into
   `<standalone>/apps/web/node_modules`. The downstream
   `web-standalone-after-pack.cjs` audit then does
   `createRequire(server.js).resolve('react/package.json')`, whose
   module walk falls out of the standalone tree and aborts the
   electron-builder phase. Add a `hoistStandaloneNextPeerDeps` step
   for the web standalone artifact only: it locates the
   `<pkg>@<version>` (not peer-resolved sibling) directory under
   `.pnpm` and symlinks it into `apps/web/node_modules/<pkg>`. The
   subsequent `cp { dereference: true }` then writes the real
   directory into the cache so the packaged tree stays self-contained.

Verified by `pnpm tools-pack mac build --to all` succeeding end-to-end
(zip + dmg + app), then `pnpm tools-pack mac install` and
`pnpm exec tools-pack mac inspect --expr` reading the desired
`__od__.client.osLocale` from the packaged renderer.

* feat(desktop): fold encodeURIComponent + manual locale source + pet window from #2554

Three defensive improvements lifted from @Eli-tangerine's parallel
implementation on #2554, kept consistent with the OS-locale chain
already on this branch:

- The argv value crossing main → preload is now wrapped with
  encodeURIComponent / decodeURIComponent so a locale string with `;`,
  `=`, or any other Chromium argv special char round-trips cleanly.
  BCP-47 region tags don't carry those today, but the renderer parser
  no longer has to assume it.
- `setLocale` now also writes `open-design:locale-source = "manual"`
  to localStorage, and `detectInitialLocale` only treats the stored
  locale as winning when that marker is present. An untagged value
  (left over from a future auto-write path, or a stale install) no
  longer pins the app to an old language once the host injects a
  fresh OS locale. Today `setLocale` is the only writer so the marker
  has no behaviour difference yet — this is a defensive net.
- `createDesktopPetWindow` now receives `osLocale` and forwards the
  same `additionalArguments` as the main `BrowserWindow`, so the
  pet renderer's `__od__.client.osLocale` is consistent with the main
  window's instead of being silently undefined.

Co-authored idea credit: changes mirror the locale-piece of
@Eli-tangerine on #2554 — that PR is closing in favour of this one.

Tests: detect-initial-locale gets a new "untagged localStorage value
loses to host locale" case. desktop 62/62, host 13/13, web i18n +
host-boundary 15/15 stay green.

* feat(web): fold onboarding view styles from #2554

Pulls the 747-line addition to `apps/web/src/styles/home/entry-layout.css`
from @Eli-tangerine's #2554 — the visual layer for the global onboarding
flow (`/onboarding` view, Connect / About-you / Design-system steps).
The view itself was already plumbed through `EntryShell.tsx`; this adds
the styling that makes it shippable on v0.8.0.

#2554 is closing in favour of this branch, so the CSS lands here so the
onboarding work doesn't get dropped on the floor.

Co-authored idea credit: @Eli-tangerine — original styling on #2554.

* fix(tools-pack): make hoistStandaloneNextPeerDeps idempotent across builds

Addresses non-blocking review by @PerishCode on #2560: the previous
`if (await pathExists(linkPath)) continue;` guard uses `access()`,
which follows symlinks. A stale symlink from a previous build whose
`.pnpm/<pkg>@<version>` target moved (e.g. after a react/react-dom
version bump that invalidates the workspace-build cache key and forces
a re-run) reports as missing through `pathExists`, then `symlink()`
rejects with EEXIST and the unhandled rejection aborts the packaged
build.

Switch to `lstat` (which does not follow the link) so we can tell
"genuinely empty slot", "real directory left by Next" and "stale
symlink" apart, then unlink stale entries before re-creating. Also
move `stripBrokenSymlinks` ahead of `hoistStandaloneNextPeerDeps` in
`copyWorkspaceBuildArtifactsToCache` so any leftover dangling links
that survived a previous run are cleared before hoist tries to write.
2026-05-21 18:23:20 +08:00
PerishFireandGitHub 526c7f7c26 Fix packaged auto-update release validation (#2565)
* fix: tighten packaged updater flow

* test: prune noisy extended ui coverage

* fix: hide unpublished release artifacts

* test: validate release updater channels

* fix: align prerelease release namespaces
2026-05-21 18:15:53 +08:00
PerishFireandGitHub 899c9fe4d8 Support nightly and preview package identity (#2437) 2026-05-20 19:46:39 +08:00
PerishFireandGitHub 31ca20f2c6 Add packaged update apply observations (#2429) 2026-05-20 19:11:36 +08:00
PerishFireandGitHub 32696d640c fix(desktop): keep the macOS Dock icon when the desktop-pet window opens (#2413)
The desktop-pet companion window calls setVisibleOnAllWorkspaces so it
floats across every Space. By default Electron's macOS implementation
transforms the whole process type between UIElementApplication and
ForegroundApplication while applying that call. On Electron 41 / macOS 26
that round-trip races during the launch burst — the pet window is
created alongside the main window — and the process can stay stuck as an
accessory app: no Dock icon, no menu bar, even though the windows render
fine.

Pass skipTransformProcessType: true so the pet window's all-Spaces
behavior never touches the app's process type. The desktop pet is a
cosmetic companion window; the main window keeps the app a regular Dock
app. The pet still floats on every Space via its alwaysOnTop floating
level.

Fixes #2394
2026-05-20 17:48:01 +08:00
PerishFireandGitHub 15d08d4158 feat: add windows packaged auto update flow (#2362) 2026-05-20 12:56:14 +08:00
PerishFireandGitHub ad37fd30cf Add desktop updater UI flow (#2270) 2026-05-19 21:36:51 +08:00
PerishFireandGitHub 2c128e0e91 refactor desktop host bridge (#2246) 2026-05-19 18:27:05 +08:00
PerishFireandGitHub 4424f08be0 [codex] Add packaged desktop auto-update (#1375)
* Add packaged desktop auto-update

* Handle counted beta nightly update versions

* Refresh desktop auto-update branch for main

* Serialize desktop updater operations

* Refresh auto-update branch for packaged paths
2026-05-19 11:20:05 +08:00
e64f1d8497 fix(desktop): export PDF saves to a file instead of the OS print dialog (#1920)
* fix(desktop): export PDF saves to a file instead of the OS print dialog

The `od:print-pdf` IPC handler called `webContents.print()`, which
opens the printer-first macOS system print dialog. That handler is the
destination of the renderer's `window.__odDesktop.printPdf()` bridge,
so on desktop "Export PDF" felt like a print flow rather than a file
export — from both the PreviewModal share menu (which always uses this
path) and the FileViewer share menu (whose daemon-route fallback lands
here too).

Route the handler through a direct Save-as-PDF flow instead: a native
Save dialog, then `webContents.printToPDF()` straight to the chosen
file — the same shape `exportPdfFromHtml` already uses for the
daemon-backed export path. The flow is extracted into
`savePrintReadyDocumentAsPdf` behind a structural `PrintReadyPdfTarget`
surface that has no `print()` method, so the regression cannot be
reintroduced.

Fixes #1774

Co-authored-by: multica-agent <github@multica.ai>

* fix(desktop): preserve PDF page sizing in print bridge

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: multica-agent <github@multica.ai>
2026-05-17 11:26:08 +08:00
4d0ea247a7 fix(desktop): swallow setTypeOfService EINVAL crashes in dev main (#647) (#1298)
* fix(desktop): swallow harmless setTypeOfService EINVAL crashes in dev main

The packaged Electron entry (apps/packaged/src/logging.ts) already
filters the undici "setTypeOfService EINVAL" crash that issue #895
introduced for the prod build, but the dev / source-built desktop
entry was missing the parallel guard. Result: switching settings
tabs in a from-source desktop run could fire a fresh fetch, undici
would try to set IP_TOS on the outbound socket, the kernel would
refuse on certain macOS / VPN configurations, and the rejection
bubbled to Electron's default handler as the "JavaScript error in
the main process" dialog reported in issue #647.

Add the same defensive filter to apps/desktop:

  - isHarmlessSocketOptionError matches only the canonical undici
    shape (syscall name AND EINVAL code). A contradicting code
    (EACCES, EPERM, etc) explicitly fails the match so real bugs
    don't get hidden.
  - The uncaughtException handler logs harmless cases at warn and
    returns silently. For anything else it removes itself from the
    listener list and re-throws via setImmediate, restoring Node's
    default crash path so Electron's native dialog renders exactly
    as it would without this filter.
  - unhandledRejection mirrors the same harmless / fall-through
    split.

The filter is installed BEFORE app.whenReady so it is armed by the
time the renderer fires its first fetch.

The helper is duplicated rather than imported from apps/packaged
because AGENTS.md forbids cross-app private-source imports. The file
header calls out the parallel and notes that the two copies should
stay in sync until the helper is promoted to a shared workspace
package (follow-up); the contract is identical so a regression in
one will surface in the other's test suite.

Tests in apps/desktop/tests/main/uncaught-exception.test.ts mirror
apps/packaged/tests/logging.test.ts: 8 cases pinning the matcher
shape, 2 cases pinning the handler's harmless-log-warn vs
fall-through-rethrow split.

Validated: pnpm guard, pnpm --filter @open-design/desktop typecheck,
pnpm --filter @open-design/desktop build, and pnpm --filter
@open-design/desktop test (14 passed, 10 new).

* fix(desktop,packaged): fail-fast on non-harmless unhandled rejections

The previous unhandledRejection listeners logged non-harmless reasons
and returned, which kept the main process alive after any rejected
promise. A real bug, a failed IPC registration, or any unexpected
async exception was reduced to a console line instead of surfacing
through Node/Electron's default crash path the filter was meant to
preserve.

Both copies now route non-harmless rejections through a parallel
factory (createDesktopUnhandledRejectionHandler /
createFatalUnhandledRejectionHandler) that mirrors the
uncaughtException policy: harmless setTypeOfService EINVAL shapes log
at warn and return, anything else logs at error, removes the
listener, and re-throws via setImmediate. Listener removal happens
before the scheduled throw, so the rethrown reason lands in the
uncaughtException path with no recursion.

Tests cover the harmless branch, the detach + ordered rethrow, and
non-Error / primitive rejection reasons (Promise.reject(42)) which
must fall through. Desktop suite: 13/13, packaged suite: 16/16.
Flagged on PR #1298 by Siri-Ray and the codex P2 review thread; the
two file copies stay in lockstep per the AGENTS.md sync invariant.

---------

Co-authored-by: Nagendhra <nagendhra405@gmail.com>
2026-05-12 16:50:57 +08:00
PerishFireandGitHub a797e079b1 fix(desktop): exit fullscreen before hiding window on macOS close (#1249)
* fix(desktop): exit fullscreen before hiding window on macOS close (#1215)

When a preview is in 演示 → 全屏 mode, the macOS close handler called
window.hide() directly, leaving the OS fullscreen Space orphaned as a
black screen — the window vanished but the Space stayed up.

Extract hideWindowExitingFullscreen as the named invariant ("hide,
but first leave fullscreen so the OS Space tears down with the window")
and route the darwin close handler through it. The hide is deferred
until 'leave-full-screen' fires so we don't race the OS Space teardown.

Bootstraps Vitest on apps/desktop with a single test under
tests/main/hide-window-exiting-fullscreen.test.ts that exercises the
helper through a structural mock — the bug shape is pure logic, no real
Electron window required. Spec was red against a hide-only helper and
green after the leave-full-screen sequencing.

* docs(agents): codify bug follow-up workflow

Distill the spec-first / cheapest-layer / scope-discipline /
invariant-shaped-fix / baseline-diff playbook used recently on #135 and
#1215 into a top-level subsection of root AGENTS.md, framed as a default
action shape with explicit room for case-by-case judgment rather than a
hard rule. Includes a single pointer back to the worked example spec.

* docs(agents): require staged human verification for visible bugs

Add the human-verification gate as a sixth bullet in the Bug follow-up
workflow. UI / platform-native / animation symptoms can pass green specs
and still ship the visible regression — proven by #1215, where the
desktop unit test green-lighted the helper logic but only a side-by-side
buggy-vs-fix run on a real macOS Space proved the black-screen actually
went away.

Reinforces the production-API-only seed constraint while we're there:
source-level backdoors prove a fake flow, not the real one, so they
invalidate the verification.

* fix(desktop): defer hide across the fullscreen-enter transition (#1215)

mrcfps observed on PR #1249 that the close handler only catches windows
already in fullscreen — Electron's enter-full-screen event is async on
macOS, so isFullScreen() can still read false during the OS Space
transition triggered by requestFullscreen(). A close in that window
took the plain hide() path and stranded the same black Space the fix
was meant to eliminate.

Track in-flight fullscreen entry from webContents.enter-html-full-screen
(set) and BrowserWindow.{enter,leave}-full-screen (clear), and surface
it through WindowFullscreenSurface.isEnteringFullscreen. The helper now
parks on enter-full-screen until the OS confirms the Space, then runs
the existing exit-then-hide path.

Adds a regression test ("waits out a fullscreen-enter transition before
exiting and hiding") that goes red against the previous helper.
2026-05-11 17:04:42 +08:00