Commit Graph

152 Commits

Author SHA1 Message Date
Éverton Toffanetto
b83ee957d1 ci: stabilize Electron dependency installs (#1409)
* ci: stabilize Electron dependency installs

Skip unused Electron binary downloads in non-desktop CI jobs and retry Electron installs in desktop tests to avoid external CDN 504 failures during npm ci.

* ci: preserve npm retry exit code

Preserve the final npm ci exit code in the non-Windows Electron retry path so CI diagnostics retain the original failure code.

* ci: preserve retry exit code under errexit

Keep npm ci inside the retry conditional so bash errexit does not bypass retries, while preserving the final npm exit code for diagnostics.
2026-06-09 17:05:11 +08:00
Mohamed Boudra
8234ecb7ba Upload only mac DMGs as workflow artifacts 2026-05-28 16:20:58 +07:00
Mohamed Boudra
a00152290f Upload desktop workflow artifacts 2026-05-28 15:40:37 +07:00
Mohamed Boudra
53c14d9855 Extract client SDK package (#1052)
* Extract client SDK package

* Polish SDK client identity defaults

* Build client before dependent CI jobs

* Restore daemon client server export

* Extract protocol and client SDK packages

* Fix provider override schema validation

* Fix app test daemon client imports

* Simplify workspace build targets

* Fix CLI test server build bootstrap

* Run SDK package tests in CI

* Fix rebase package split drift

* Restore lockfile registry metadata

* Update SDK config test for prompt default

* Move terminal stream router test to client package

* Fix rebase drift for protocol imports

* Fix SDK agent capability fixture

* Restore legacy server client exports

* Fix server export compatibility test

* Advertise custom mode icon client capability

* Remove server daemon-client exports

* Format rebased mode control import

* Fix rebase drift for protocol imports

Files added by upstream PRs (#893, #1147, #1154) referenced the pre-split
shared/ paths that this branch moves into @getpaseo/protocol. Redirect
those imports to the protocol package so typecheck stays green after the
rebase.
2026-05-28 01:58:18 +08:00
Mohamed Boudra
9c2d47ab34 Deploy website for docs changes 2026-05-27 16:17:10 +07:00
Mohamed Boudra
68bce623c8 Route OpenCode live tests through OpenRouter
Big Pickle's models.dev metadata changed upstream to route via the
Anthropic SDK against the Zen OpenAI-compatible endpoint, which the
endpoint rejects. Swap the live-turn smoke tests to
openrouter/~openai/gpt-mini-latest and gate them on OPENROUTER_API_KEY
so fork PRs skip cleanly when the secret is absent. Also unpin
opencode-ai so CI tracks the latest CLI.
2026-05-18 17:16:57 +07:00
Mohamed Boudra
0419346d6a Pin OpenCode CLI in CI 2026-05-18 16:01:30 +07:00
Mohamed Boudra
00e7ac9ec8 Fix OpenCode custom command hangs (#1063)
* Fix OpenCode slash command turns

* ci: install opencode in server-tests so real OpenCode e2e runs

Server tests previously only installed the Claude Code CLI, so every
opencode-*.real.e2e.test.ts self-skipped on CI. The custom-command and
turn-lifecycle real tests cover the OpenCode session SSE rewrite, so
add opencode-ai globally and let them run. Big Pickle is free so no
provider secrets needed.

* Fix OpenCode unit tests exposed by enabling opencode in CI

server-tests previously skipped every OpenCodeAgentClient test because
opencode-ai was not installed. Now that CI runs them, four real issues
surface:

- Drop unused mkdirSync import (was used on a code path 1812b1489
  refactored away on main; the rebase merged imports without rechecking
  usage). Fixes the lint job.
- Fix slash-command-timeout test passing four ctor args instead of three,
  which silently dropped the TestOpenCodeRuntime and ran against the real
  opencode runtime.
- Relax the listModels contextWindowMaxTokens assertion. Some providers
  routed through OpenCode (OpenAI in particular) don't expose a numeric
  context window; assert the type only when the field is present.
- Swallow late notifySubscribers calls once close() flips the closed
  flag and clear subscribers up front. The session-lifetime SSE stream
  introduced by the prior commit means a session.error arriving after
  the owning test has moved on can rejectCompletion on a deferred no
  one is awaiting, surfacing as an unhandled rejection in a downstream
  test file.
2026-05-17 19:51:20 +08:00
ezra
952b58c0eb Fix Windows Android app scripts (#1058)
* Fix Windows Android app scripts

* ci: skip onnxruntime CUDA download
2026-05-17 14:06:42 +08:00
Mohamed Boudra
1f79c4d039 Bump default desktop rollout to 36h 2026-05-15 14:47:42 +07:00
Mohamed Boudra
defb4f82f7 ci(nix): smoke test daemon boot (#939) 2026-05-12 16:05:15 +08:00
Mohamed Boudra
77c82dfdbd ci: split nix into two workflows
nix.yml: PR-only build check.
nix-update-hash.yml: push-to-main hash update via paseo-ai App token.
2026-05-12 12:57:38 +07:00
Mohamed Boudra
29277c900d ci: split nix workflow into build (anywhere) + update-hash (main only)
build runs on push to main and every PR including forks. Same model as
the test/lint/typecheck workflows: read-only token, no secrets injected,
runs scripts/update-nix.sh + nix build to validate. No commit, no push.

update-hash runs only on push to main. Mints the paseo-ai App token,
re-runs the update + build, and commits the refreshed hash via the
ruleset bypass. The default GITHUB_TOKEN no longer pushes anywhere.

Rename file to nix.yml since it's no longer just a build job.
2026-05-12 12:54:22 +07:00
Mohamed Boudra
ed1943058a ci: gate nix-build on same-repo PRs only
scripts/update-nix.sh and scripts/fix-lockfile.mjs are executed from
the PR's checked-out tree. A fork PR can modify those scripts to run
arbitrary code on the runner. Same-repo PRs and push-to-main still
run; fork PRs skip the job entirely.

Closes #365.
2026-05-12 12:37:01 +07:00
Mohamed Boudra
e0361ddd22 ci: drop unused contents: write override in nix-build
GITHUB_TOKEN no longer pushes anything in this workflow. PR runs are
read-only, and main pushes go through the paseo-ai App token. The
workflow-level 'contents: read' is sufficient.

Addresses #365.
2026-05-12 12:35:05 +07:00
Mohamed Boudra
ce9474055e ci: skip CI on nix-build auto hash commits
The hash-update commit only changes package-lock.json and
nix/npm-deps.hash. The parent commit already passed all 13 required
checks, and the file diff is content-only, so re-running the full
suite on every hash bump is wasted CI minutes.
2026-05-12 12:28:34 +07:00
Mohamed Boudra
8f9b4c8828 ci: skip App token on PRs in nix-build
Fork PRs cannot read repo secrets, so the App-token step failed with
'Input required and not supplied: app-id'. Gate the App-token step on
push-to-main and fall back to github.token for the checkout on PRs,
since PRs only need to validate the build.
2026-05-12 12:26:22 +07:00
Matan Bendix Shenhav
0759932dad nix: declarative config, typed relay options, desktop packaging (#923)
* nix: expose npmDepsHash as a callPackage arg

Downstream flakes that follow a different nixpkgs revision can hit a
hash mismatch on the npm-deps FOD even though package-lock.json is
unchanged, because fetchNpmDeps output is sensitive to nixpkgs version.
The standard fix — `.overrideAttrs { npmDepsHash = ...; }` — does not
work for buildNpmPackage: npmDepsHash is destructured from args, so the
default `npmDeps = fetchNpmDeps { hash = npmDepsHash; }` is already
bound by the time overrideAttrs runs.

Promote npmDepsHash to a callPackage arg with the current value as the
default. Consumers can now `.override { npmDepsHash = "sha256-..."; }`
and have it propagate to the npmDeps fetcher. Upstream CI behavior is
unchanged — update-nix.sh is adjusted to match the new
`npmDepsHash ? "..."` pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* nix: move npmDepsHash default to a sidecar file

Read the default `npmDepsHash` from `nix/npm-deps.hash` via
`lib.fileContents` instead of inlining it as a string literal in
`nix/package.nix`. The CI auto-updater becomes a one-line file write
instead of a regex against a .nix source — decoupling lockfile bumps
from the formatting of the package definition.

No behavior change: same hash, same default, same `.override` surface.
Lockfile diffs become smaller and the update path stops being load-
bearing on a sed pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* nix: declarative config via services.paseo.settings

Today only a handful of `config.json` fields are surfaced as module
options (listen, port, hostnames, relay.enable). Anything richer —
custom agent providers, MCP injection, log config, voice features —
requires hand-editing `$PASEO_HOME/config.json`.

Add `services.paseo.settings` as a freeform attrset rendered to JSON
via `pkgs.formats.json` and installed at `$PASEO_HOME/config.json`
on each service start. Standard NixOS idiom.

`install` on `preStart` rather than a `tmpfiles` symlink because the
daemon writes to `config.json` at runtime via `DaemonConfigStore.patch`
(MCP / provider toggles). A read-only symlink would break those writes;
a copy-on-start lets the daemon mutate freely within a session while
the Nix-managed file remains the source of truth at boot.

The full schema is `PersistedConfigSchema` in
`packages/server/src/server/persisted-config.ts`. Documented in the
option description that runtime mutations don't survive restarts when
`settings` is non-empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* nix: typed services.paseo.relay options with auto-wired endpoint

Addresses #224 (option surface only).

Today `services.paseo.relay.enable` is a bool that just toggles
`--no-relay`. Pointing the daemon at a self-hosted relay requires
hand-setting `PASEO_RELAY_ENDPOINT` and `PASEO_RELAY_USE_TLS` via
the freeform `environment` option.

Add a typed relay subtree:

- `relay.mode = "hosted" | "remote"` selects how the daemon reaches
  the relay when enabled. Default is `"hosted"` (current behavior).
- `relay.{host,port,useTls}` configure the `"remote"` case.
- The module auto-wires `PASEO_RELAY_ENDPOINT` and `PASEO_RELAY_USE_TLS`
  when `mode = "remote"`.
- Assertion fires at eval time when `mode = "remote"` but `host` is empty.
- `relay.enable` keeps its current semantics — bool answers "is it on?",
  the new options answer "how is it configured?".

The `"local"` mode from #224 (running a relay on the same host as a
systemd unit) is deliberately not added here: `packages/relay` ships
only a Cloudflare Workers adapter, so there's no Node.js runtime to
package as a binary. Adding a Node adapter is a TS-side feature change
worth its own design discussion; tracked as a follow-up.

No breaking changes — existing `relay.enable = true|false` configs
evaluate unchanged with the new `mode = "hosted"` default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* nix: package paseo desktop app for Linux

NixOS users have no easy way to run the desktop app today —
electron-builder's outputs (.deb, .rpm, .AppImage) don't fit Nix's
model, so `nix run github:getpaseo/paseo#desktop` doesn't exist.

Add `packages.<linux>.desktop` following the standard nixpkgs Electron
pattern (see e.g. signal-desktop, vscode): skip electron-builder
entirely, build the desktop main process with `tsc`, bundle the Expo
web export and built daemon workspaces, and wrap `pkgs.electron` with
`makeWrapper`. Output is a runnable derivation usable via `nix run` or
`environment.systemPackages`.

The install layout preserves the monorepo source tree
(`packages/desktop/dist/main.js`, `packages/app/dist`, `node_modules`
at the workspace root) so `main.ts`'s dev-mode path resolution
(`__dirname/../../app/dist`, `__dirname/../assets/icon.png`) works
without any source patches. When Electron is invoked unpackaged via
`electron path/to/main.js`, `app.isPackaged` is false and these
relative paths are used.

`--no-sandbox` is set on the launcher: Chromium's setuid sandbox can't
live in `/nix/store` (immutable, no setuid). A follow-up can wire
`security.wrappers` from a NixOS module for users who want the
renderer sandbox.

No CI changes — `desktop-release.yml` continues to produce
.deb/.AppImage/.rpm/macOS/Windows installers as today. This is purely
additive for NixOS users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* nix: copy full packages/ tree in desktop derivation

The previous installPhase selectively copied built artifacts (dist/
under server, cli, relay, highlight, expo-two-way-audio), which left
two workspace symlinks dangling and failed noBrokenSymlinks:

- node_modules/@getpaseo/expo-two-way-audio → packages/expo-two-way-audio
  (the Expo native module ships source + native projects, no built dist/)
- node_modules/.bin/paseo → @getpaseo/cli/bin/paseo
  (the CLI launcher script lives under bin/, not dist/)

npm workspace symlinks expect every workspace package to exist at its
source path. Copy the whole packages/ tree instead. The cleanSourceWith
filter already excludes the heavy platform-specific paths (android/ios
under packages/app, website, tests), and the remaining ~16MB of src is
acceptable for an Electron app derivation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* nix: route desktop renderer through paseo:// protocol handler

When `paseo-desktop` is launched via `electron path/to/main.js` (our
unpackaged Nix layout), `app.isPackaged` is false and main.ts loads
`DEV_SERVER_URL` — which defaults to http://localhost:8081 (the Expo
dev server). That URL has nothing listening in a Nix-installed run,
so the renderer fails with ERR_CONNECTION_REFUSED.

main.ts already supports overriding this via the `EXPO_DEV_URL` env
var. Set it to `paseo://app/` so the request goes through the
`paseo://` protocol handler that main.ts registers unconditionally.
The handler resolves files via `getAppDistDir()`, which in the
unpackaged branch returns `__dirname/../../app/dist` — exactly where
our install layout places the Expo web export.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: track nix/npm-deps.hash in nix-build commit step

The commit step still referenced nix/package.nix in its diff check
and git add. After moving the hash to nix/npm-deps.hash, the
auto-updated hash would never be staged and the new value would
sit unstaged in the working tree forever.

* ci: push nix-build hash commits via paseo-ai[bot] App token

The default GITHUB_TOKEN cannot bypass main's required status checks,
so the auto-commit of stale Nix hash updates has been silently failing.
Mint an installation token for the paseo-ai App (which is in the
ruleset bypass list) and use it for checkout and push.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-05-12 13:11:42 +08:00
Mohamed Boudra
478aa4b70e ci: cancel superseded PR runs to free runner capacity
cancel-in-progress is gated to pull_request events so merge_group and
push runs always complete — only redundant CI from rapid PR pushes
gets cancelled.
2026-05-10 14:12:56 +07:00
Mohamed Boudra
fd74abcdca ci: trigger required checks on merge_group events (#879)
Without merge_group on the workflow, PRs entering the merge queue would
fail because no required check ever reports a status against the
merge_group ref.
2026-05-10 15:07:39 +08:00
Mohamed Boudra
90cb20ce79 ci: remove duplicate server-ci workflow (#870)
The matrix in CI / server-tests (ci.yml) covers what Server CI / test was
doing, on Linux and Windows instead of Linux only. Two workflows running
the same test suite on every server-touching PR is wasted runner time.
2026-05-10 06:05:27 +00:00
João Sousa Andrade
9dd5c6e5f3 Patch production dependency advisories (#856)
* Patch production dependency advisories

* ci: harden dependency installs with npm ci --ignore-scripts

- Replace npm install with npm ci --ignore-scripts across all workflows
  to enforce lockfile parity and block dependency postinstall scripts.
- Run npm run postinstall explicitly after install so our root patch
  script still applies (the only legitimate postinstall use in this
  repo); dep lifecycle scripts stay blocked.
- Add lockfile-lint and npm audit signatures to the lint job to catch
  registry-host tampering and verify cryptographic signatures.
- Regenerate package-lock.json to match bumped package.json versions.

* ci: drop --ignore-scripts; rely on npm ci + lockfile-lint + audit signatures

Lavamoat allow-scripts only traverses root deps; it can't see workspace
deps like electron, esbuild, sharp. Without proper monorepo support, an
allowlist would be incomplete and electron's binary download breaks.

Keep the rest of the security stack:
- npm ci (strict lockfile parity)
- lockfile-lint (resolved-host check)
- npm audit signatures (cryptographic verification)

Real script-blocking would need pnpm 10+ migration. Tracking separately.

* ci(nix-build): use PR head SHA so fork checkouts work

The previous `ref: ${{ github.head_ref || github.ref }}` made
actions/checkout fetch a branch name that only exists on the fork,
not origin. Use the PR head SHA instead — origin mirrors PR commits
via refs/pull/N/head, so this works for fork PRs without changing
the push-to-main auto-commit behavior.

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-05-10 11:45:24 +08:00
Mohamed Boudra
1042270298 ci(nix): merge Fix Nix hash into Nix Build, commit only after green build
Single workflow now: update lockfile + hash, build, then commit the fix on
main only if the build passed. Eliminates the structural first-failure where
Nix Build always raced the autofixer and lost.
2026-05-09 10:26:36 +07:00
Mohamed Boudra
b7d2e6d3ec Revert "ci: gate Claude Code install to Linux server-tests"
This reverts commit 09ed46b997.
2026-05-08 16:28:49 +07:00
Mohamed Boudra
09ed46b997 ci: gate Claude Code install to Linux server-tests
Installing claude-code globally on the Windows server-tests runner
exposed 12 pre-existing Windows-specific test failures that were
silently skipped before (rmdir EBUSY during cleanup, .CMD spawn
EINVAL via the SDK, plus the launch-regression test that asserts
claude is *not* on PATH).

The Linux e2e tests (model-catalog, live-preferences, models) are
the only ones that needed the binary to flip isAvailable() true;
none of those run on Windows. Skip the install on Windows so those
Windows compatibility gaps stay surfaced separately and don't block
the SDK upgrade.
2026-05-08 16:26:43 +07:00
Mohamed Boudra
a4d365c8f2 fix server-ci: install only claude-code, set SDK path in contract test
Two regressions from the previous CI install:

- Installing opencode-ai globally in server-ci/server-tests caused
  opencode-agent.test.ts (gated on \`hasOpenCode\`) to start running
  for the first time and surface real but unrelated test breakage.
  Trim the install to claude-code, which is all that's needed for the
  Claude e2e tests that motivated the change.

- claude-agent.integration.test.ts's supportedModels test calls the
  SDK's \`query()\` directly (bypassing ClaudeAgentClient) and didn't
  pass \`pathToClaudeCodeExecutable\`. The 0.2.71 SDK had a bundled
  cli.js fallback; 0.2.133 doesn't, so on Ubuntu CI it tries the
  per-platform binary path (\`...claude-agent-sdk-linux-x64-musl/claude\`)
  which isn't installed via the \`claude-code\` global. Resolve the
  binary explicitly in the test and pass it as the SDK option.
2026-05-08 16:14:28 +07:00
Mohamed Boudra
13538dd710 ci: install claude-code globally in server-tests
server-tests in ci.yml and Server CI both need claude on PATH for the
e2e tests that probe model catalogs and live preferences. cli-tests
already installs the trio (claude-code, codex, opencode-ai); server
tests now match.
2026-05-08 16:05:30 +07:00
Mohamed Boudra
cb2fe91998 fix CI fallout from claude-agent-sdk 0.2.133 upgrade
- mcp-server tests: MCP SDK 1.29 renamed RegisteredTool.callback → handler
- ci: install @anthropic-ai/claude-code globally for cli-tests, matching
  codex/opencode now that bundled fallback is gone
2026-05-08 15:18:24 +07:00
Mohamed Boudra
c4e4a28bc0 Run server tests on Linux+Windows matrix (#809)
* Run server tests on Linux+Windows matrix

Replace the hand-curated Windows server test allow-list with a full-suite matrix run.\nBoth Ubuntu and Windows now run the same server test command with shared setup and secrets.

* Add isPlatform helper and gate Windows-hostile server tests

- Add a shared server test isPlatform helper.

- Migrate existing Windows-gated spawn, worktree, executable, and worktree-core tests to the helper.

- Gate Windows-hostile symlink, macOS path-normalization, and POSIX shell setup tests with skipIf.

* Replace POSIX shell calls in test fixtures with Node primitives

- Replaced test fixture mkdir/echo shell setup with fs mkdirSync/writeFileSync calls in touched server tests.

- Replaced git shell strings with execFileSync/spawnSync argv calls across checkout, worktree, MCP, script, and workspace git fixtures.

- Gated the directory suggestion symlink escape tests on Windows because those fixtures require POSIX symlink behavior.

* Fix Windows server-test failures and split POSIX-only suites into sibling files

- Fix Windows path/cwd assertions in terminal, session/workspace, git-service, checkout-git, MCP, logger, spawn, and registry bootstrap tests.\n- Keep terminal tests runnable on Windows by canonicalizing temp cwd fixtures and ensuring a Windows shell fallback.\n- Gate POSIX-only shell, signal, Unix socket, and git-worktree reuse fixtures that need dedicated Windows coverage later.

* Move POSIX-only test blocks into sibling .posix.test.ts files

- terminal.test.ts: moved PTY/bash interaction blocks into terminal.posix.test.ts.

- worktree.test.ts: moved git-worktree and teardown shell blocks into worktree.posix.test.ts.

- worktree-bootstrap.test.ts: moved setup shell and terminal-backed service blocks into worktree-bootstrap.posix.test.ts.

- worktree-core.test.ts: moved the POSIX-only worktree-core suite into worktree-core.posix.test.ts and removed the empty original.

- provider-availability.test.ts and file-explorer/service.test.ts: moved POSIX PATH/symlink blocks into sibling suites.

* Fix Windows 8.3 short-name, EBUSY cleanup, and node-pty shell-path failures in server tests

- Normalize temp home directories in directory suggestion assertions to avoid Windows short-name mismatches.

- Use Windows-valid terminal shells/cwds in terminal fixtures and wait for terminal-manager PTYs before cleanup.

- Replace hardcoded POSIX paths in workspace-git/MCP/loop fixtures with platform-resolved paths or command files.

- Gate the two explicitly Linux-only workspace-git watcher tests.

* Replace hardcoded POSIX paths with portable Node path constructions in server tests

- checkout-git.test.ts and worktree-session.test.ts: compare Windows temp paths with realpathSync.native to avoid 8.3 short-name mismatches.

- directory-suggestions.test.ts: canonicalize result and expected paths with realpathSync.native.

- session.workspaces.test.ts: replace literal /tmp and /Users fixtures with path.resolve/path.join constructions.

- workspace-git-service.primitive.test.ts, loop-service.test.ts, and mcp-server.test.ts: use canonical repo/temp paths and shell-safe relative verify commands.

* Fix loop-service verify-check shell and workspace-git-service path-separator on Windows

- Run the loop-service verify script through the current Node executable with a relative script path.

- Normalize the workspace-git-service expected repo cwd to forward slashes for the listWorktrees assertion.

* Fix loop-service worker PTY spawn on Windows

- Canonicalize the loop-service temporary root and workspace with realpathSync.native before worker agents use the path as cwd.

* Gate loop-service real-worker-PTY test on Windows

- Skip the real worker PTY loop test on Windows after ConPTY path resolution still fails with node-pty error 267.\n- Keep the test running on POSIX so the loop behavior remains covered.

* Stub getMetricsSnapshot in test mocks to suppress async-leak uncaught exceptions on Windows

- Add a no-op AgentManager metrics snapshot to the WebSocket notification test server stub.

* Fix terminal-manager Windows-path test to avoid PTY spawn into nonexistent dir

- Use an existing temporary cwd for the createTerminal absolute-path validation assertion so node-pty does not spawn into a missing Windows directory.
2026-05-08 10:57:22 +08:00
Mohamed Boudra
285d4edf23 fix(server): harden findExecutable + remove gratuitous realpath from spawn paths (#787)
* Phase 0: baseline tests for probeExecutable

* Phase 1: invert self node env handling

* Phase 2: refactor probeExecutable to execCommand

* Phase 3: push POSIX executable lookup to system which

* Fix probe test spawn-failure fixture

* Guard system which executable lookup
2026-05-07 14:03:15 +08:00
Mohamed Boudra
80bdb4d45b Use tree-kill for daemon shutdown (#788)
* Use tree-kill for daemon shutdown

* fix: update lockfile signatures and Nix hash

* Use tree-kill for daemon shutdown

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-07 13:18:39 +08:00
Mohamed Boudra
8d3946c36c perf(cli): run CLI E2E tests in parallel (#708)
* perf(cli): run E2E tests in parallel via worker pool

The custom CLI test runner ran 35 tsx test files sequentially, making the
cli-tests CI job the longest in main CI (~17 minutes). Each test file
already isolates its own daemon (ephemeral port + tmp PASEO_HOME), so
parallelism was just gated by the runner.

Replace the sequential recursion with a fixed-size worker pool (default
concurrency=4 to match GitHub Actions standard runners; override via
PASEO_CLI_TEST_CONCURRENCY). Buffer per-test stdout/stderr and flush as
a contiguous block on completion so concurrent output stays readable,
and report per-test wall clock plus the five slowest tests.

Local wall clock drops from ~12-15 minutes serial to ~2:49 with
concurrency=4. The slowest single test (05-agent-run, 49s) is now the
floor; CI should land near 4-5 minutes.

* fix(cli-tests): deflake 30-chat under load and shard CI across 3 runners

`chat wait` reads the latest message id and then subscribes for newer
messages. Under CI load the subprocess takes >1s to bootstrap, so the
old test's 250ms head start before posting "second message" raced
against that read. When the post landed first, the subprocess saw the
second message as latest and timed out waiting for a newer one. Replace
the brittle delay with a post-and-race loop: every iteration posts a
fresh "second message" and races a 250ms tick against the wait promise,
so whichever message lands after the snapshot wakes wait deterministically.

CI was also slower than local (10.3min vs 2.8min). On 4-vCPU GHA
runners, 35 sequential-CPU-time of ~1850s caps wall clock around 8 min
even at concurrency=4. Shard the suite across 3 GHA runners via matrix
strategy. The runner now reads PASEO_CLI_TEST_SHARD/SHARD_TOTAL and
distributes files into buckets — known long-pole tests
(05/06/11/13/14-...) round-robin first so they spread across shards
instead of clustering by their numeric prefixes, then the remainder
fills in the reverse direction to balance light load.

Local run is unchanged (single shard by default). Expected per-shard
wall on CI: ~2:30 + setup ≈ ~4:30 total.
2026-05-05 19:58:55 +07:00
Mohamed Boudra
e8a64fb569 ci: build server before desktop tests
f0d96f8e added packages/desktop/src/daemon/daemon-manager.test.ts,
which calls vi.mock("@getpaseo/server", ...). Vite's module
resolution still needs to find the package's main entry, so
desktop CI must build the server (and its dependency chain) before
running the tests. Mirrors the prebuild steps used by typecheck,
server-tests, and playwright.
2026-05-04 00:40:38 +07:00
Mohamed Boudra
d4fa12c3f6 fix(desktop): centralize desktop build script 2026-05-03 14:35:20 +07:00
Mohamed Boudra
d89ecae615 fix(server): clean up OpenCode process trees
Spawn the shared OpenCode server in its own POSIX process group and terminate provider child process trees with graceful timeout escalation before forcing SIGKILL. Reuse the same helper for Codex app-server cleanup so process-tree behavior is covered consistently across providers.

Add cross-platform unit coverage for macOS/Linux process groups, Windows taskkill cleanup, and timeout escalation. Add the process-tree test to the Windows-critical CI allowlist; Linux full server tests already include it.

Refs #227
2026-05-02 21:44:59 +07:00
Mohamed Boudra
90feb8050c feat(desktop): auto-update bundled skills on app launch
Skills installed via Paseo desktop never updated after first run, so
users were stuck on already-fixed skill content. On every launch, if
the paseo base skill is present in `~/.agents/skills/`, sync each
bundled skill (incl. `references/`) into agents, claude (via symlink/
junction), and codex — overwriting only files whose content differs.
Files on disk that are not in the bundle are left alone; deprecation
goes through tombstones (e.g. `paseo-orchestrate` redirects to
`paseo-epic`).

Also refreshes the bundled skill set: drops `paseo-chat`, adds
`paseo-advisor` and `paseo-epic`, and turns `paseo-orchestrate` into
a tombstone redirecting to `paseo-epic`.

Adds a desktop test job to CI on Ubuntu and Windows so the
junction/symlink path is exercised on both platforms.
2026-05-02 13:28:17 +07:00
Mohamed Boudra
84f5818e09 test(server): run worker terminal coverage on windows 2026-05-01 14:27:55 +07:00
Mohamed Boudra
111935f4eb ci: run workflows on node 22 2026-05-01 12:32:19 +07:00
Mohamed Boudra
0b78379b77 fix: percent escaping for git --format on Windows (#629)
* test(server): add real-spawn round-trip for % in argv

RED test: pass --format=%(refname)... through spawnProcess to a child
node script that echoes argv[2]. Asserts the child receives the original
string verbatim. Wired into the Windows CI matrix to expose the
escapeWindowsCmdValue %-doubling bug under cmd.exe.

* test(server): use bare 'node' command so cmd.exe path is exercised

process.execPath has both an extension and a path separator on Windows,
so shouldUseWindowsShell returns false and the broken %-escape pipeline
is never invoked. Use the bare command name 'node' instead.

* fix(server): stop doubling % in cmd.exe arg escaping on Windows

cmd.exe only collapses `%%` → `%` inside batch files; on the command
line / via `cmd /c "..."` `%%` stays literal. Doubling `%` in
escapeWindowsCmdValue meant args like `--format=%(refname)` reached git
as `--format=%%(refname)`, which git interprets as the escape sequence
for a literal `%`, so format atoms appeared verbatim and the branch
picker showed `%(refname)%09%(committerdate:unix)` instead of branch
names.

Update unit tests that pinned the broken doubling behavior.
2026-04-30 14:22:19 +08:00
Mohamed Boudra
9c3c0f9140 feat(desktop): build Windows arm64 alongside x64
Multi-arch Windows build in one electron-builder pass, with the website
self-healing across the rename. Asset names go from
`Paseo-Setup-${version}.exe` to `Paseo-Setup-${version}-{x64,arm64}.exe`.

The website now reads the actual installer filename off the latest
GitHub release, so legacy single-arch releases keep rendering a single
"Download" pill while dual-arch releases promote to "Intel / x64" +
"ARM64" automatically. No homepage code change needed when ARM ships.
2026-04-30 11:39:56 +07:00
Mohamed Boudra
a1db839306 fix(release): use bash 3.2-compatible array build for macOS upload
`mapfile` is bash 4+, but macOS GitHub runners ship bash 3.2, so the
upload step exited 127 on both arm64 and x64 jobs. Replaced with a
portable while-read loop.
2026-04-29 20:24:44 +07:00
Mohamed Boudra
4853433ad7 fix(release): publish desktop manifests atomically to close arch race
The macOS publish matrix had each arch run electron-builder with
--publish always, so each runner uploaded its own latest-mac.yml to the
GitHub release the moment it finished. The slower arch clobbered the
faster one, leaving a window (typically 1-3 minutes) where the live
manifest contained only one arch's files[]. Apple Silicon clients
polling during that window could install the x64 build, ending up
under Rosetta.

Also closed the related window where finalize-rollout stamped
releaseDate and rolloutHours after the merged manifest was already
public — auto-updater treats missing rollout fields as "admit", so 100%
of stable users could grab a release before staged rollout kicked in.

Now every build runs --publish never. Each platform job uploads its
non-yml artifacts directly via gh release upload --clobber, and stages
its manifest as an Actions artifact. The renamed finalize-rollout job
downloads all manifest artifacts, merges mac arm64+x64, stamps rollout
metadata, validates, and uploads every manifest in one pass. Public
manifests are never visible in an unmerged or unstamped state.

Refs #555.
2026-04-29 19:47:12 +07:00
Mohamed Boudra
9fb87b9d86 ci(deploy-app): install all workspaces so server/relay sources resolve
Typecheck pulls in @server/* sources via tsconfig path alias, but the
prior `npm install --workspace=@getpaseo/app` skipped server/relay deps,
leaving zod, @anthropic-ai/claude-agent-sdk, and @getpaseo/relay/e2ee
unresolvable on CI.
2026-04-28 18:05:27 +07:00
Mohamed Boudra
e41fee0366 fix(desktop): close two pre-ship rollout edge cases
Bucket divisor now produces [0, 1) so the single UUID hashing to
0xFFFFFFFF is no longer permanently excluded by the strict-less-than
admit condition.

Mirrors the post-stamp manifest validation step from desktop-rollout.yml
into desktop-release.yml's finalize-rollout job so a stamp-rollout.mjs
regression can't silently ship a broken manifest.

Adds boundary tests: bucket=0 stays blocked at exact release time,
max-bucket admits at and past rollout end, unparseable releaseDate
admits, bucketFromStagingUserId is strictly < 1 at the upper bound.
2026-04-28 16:58:54 +07:00
Mohamed Boudra
25eb26db0f feat(desktop): add time-based staged rollout for stable updates
Linear ramp from 0% at publish to 100% at releaseDate + rolloutHours
(default 24h, configurable per release). Beta channel and rolloutHours=0
short-circuit to admit everyone. Per-machine bucket derives from a
persistent UUID at <userData>/.updaterId and feeds electron-updater's
isUserWithinRollout hook.

Adds desktop-rollout.yml workflow for in-place rolloutHours edits on
already-published releases (hotfix to 0 or extend the ramp), serialized
against finalize-rollout in desktop-release.yml via a shared concurrency
group keyed on the tag. Replaces the hand-rolled mac manifest merge
with a js-yaml round-trip that preserves unknown fields.
2026-04-28 16:48:18 +07:00
Mohamed Boudra
90032570ec Add macOS packaged desktop smoke gate (#578)
* Fix packaged node entrypoint env preservation

* Add packaged desktop smoke gate
2026-04-27 09:08:56 +08:00
Mohamed Boudra
446dd9cfe6 chore: drive linter to zero, promote warn -> error in CI (#545)
* chore(lint): use Set for LOG_FORMATS membership check

* chore(lint): convert type aliases to interfaces (autofix)

- Remove no-use-before-define rule (conflicts with unistyles ordering)
- Add typescript/consistent-type-definitions: interface
- Run oxlint --fix: 606 type->interface conversions across 268 files

Typecheck green. Warnings: 5432 -> 3787 (-1645).

* chore(lint): clean up relay package warnings

* chore(lint): clean up cli package warnings

* chore(lint): flatten nested ternaries in server (no-nested-ternary)

* chore(lint): escape entities and hoist inline objects in website

* chore(lint): hoist inline arrays in app (jsx-no-new-array-as-prop)

* fix(types): restore Record assignability after type->interface autofix

* chore(lint): hoist inline arrays in app (jsx-no-new-array-as-prop)

* chore(lint): clean up desktop and highlight packages

* chore(lint): hoist inline arrays in app (jsx-no-new-array-as-prop)

* chore(lint): hoist inline arrays in app (jsx-no-new-array-as-prop)

* chore(lint): format keyboard-shortcuts-section

* chore(lint): rename shadowed bindings in server (no-shadow)

Rename inner bindings that shadow outer imports or function params:
- Promise executor `resolve` -> `resolvePromise` (shadowed path `resolve`)
- Method params `options` -> `input`/`target`/`update`/`runOptions`/`opts`/`killOptions`
- Misc loop/destructure renames for `workspaceId`, `scriptNames`, `path`, `query`, `taskNotificationItem`

Mechanical change only; no behavior change.

* fix(types): add index signatures for Record assignability

* chore(lint): extract nested callbacks in server (max-nested-callbacks)

* chore(lint): hoist inline callbacks in app (jsx-no-new-function-as-prop)

Work in progress: 30 of 369 warnings fixed across 23 files.

* chore(lint): hoist inline callbacks in app (jsx-no-new-function-as-prop)

* chore(lint): reduce cyclomatic complexity in server

* chore(lint): hoist inline callbacks in app components (jsx-no-new-function-as-prop)

* chore(lint): parallelize safe await-in-loop in server

* chore(lint): hoist inline callbacks in app (jsx-no-new-function-as-prop)

Finish eliminating jsx-no-new-function-as-prop warnings in
agent-status-bar.tsx and complete refactor of git-diff-pane.tsx
by extracting per-item components and using stable useCallback
handlers.

* chore(lint): parallelize more safe await-in-loop in server

* fix(server): restore microtask ordering in message dispatch and stream event handler

The complexity refactor added async dispatcher chains that inserted extra
microtasks before message handlers ran, and wrapped the stream event switch
in an await that fired between emitState and dispatchStream. Two tests
regressed on both counts. Route to the matching dispatcher synchronously
and skip the await when the stream handler has no async work.

* chore(lint): hoist/memoize inline arrays and objects in app JSX

* chore(lint): parallelize safe awaits in server (no-await-in-loop)

Converts three sequential `for ... await` loops to `Promise.all`:
- OpenCode: configure MCP servers in parallel
- Sherpa: download model files and ensure multiple models concurrently
- Speech runtime: check required model files across models in parallel

* chore(lint): clear website warnings to zero

Resolves all 61 oxlint warnings in packages/website/ by hoisting inline
callbacks/JSX, extracting sub-components to satisfy jsx-max-depth, using
stable data-derived keys, adding explicit button types, flattening nested
ternaries, and removing unused code.

* chore(lint): parallelize more safe awaits in server (no-await-in-loop)

- Workspace reconciliation: archive missing workspaces and orphaned
  projects concurrently instead of sequentially.
- Directory suggestions: resolve child directory candidates in parallel
  before filtering.

* chore(lint): allow css imports in import/no-unassigned-import, hoist host page styles

* chore(lint): hoist dictation-controls inline arrays

* fix(server): restore microtask ordering in session message dispatch

Converts per-group dispatchers to Promise<void> | undefined. The complexity
refactor broke two tests by inserting extra microtasks between each
dispatcher's await. The new pattern routes synchronously via a nullish-
coalescing chain and awaits only the matching dispatcher's promise.

* chore(lint): flatten nested ternaries in button/volume-meter/autocomplete

* chore(lint): narrow explicit any in misc server files

* chore(lint): flatten nested ternaries in host-runtime/host-page/providers

* chore(lint): clear desktop warnings to zero

Convert polling loops to recursive helpers to avoid no-await-in-loop,
and switch the ws import to the named export so the WebSocket type is
referenced directly.

* chore(lint): remove useless constructors in server tests

* chore(lint): hoist pair-scan inline arrays and objects

Extracts corner style arrays and barcode scanner settings to module
scope, memoizes insets-dependent body/helper text styles.

* chore(lint): hoist inline style arrays in question-form-card

* chore(lint): hoist/memoize inline styles in plan-card

Extracts markdown rule style arrays into dedicated MarkdownInlineText,
MarkdownListItemContent, and MarkdownParagraph subcomponents that
memoize their own style arrays. Memoizes PlanCard container/title/
description styles.

* chore(lint): narrow explicit any in server relay/loader/logger-likes

* chore(lint): hoist SheetBackground combined style

* chore(lint): hoist inline styles in screen-header

* chore(lint): hoist inline styles/objects in menu-header

* chore(lint): hoist top-level inline arrays in diff-viewer

* chore(lint): narrow explicit any in codex-app-server-agent thread items

* chore(lint): reduce max-depth in claude-sdk-behavior test

* chore(lint): memoize inline styles in command-center

* chore(lint): extract helper to reduce max-depth in agent-response-loop

* chore(lint): flatten nested conditions in pi-direct-agent history

* chore(lint): reduce max-depth in claude-agent query pump

* chore(lint): clear cli warnings to zero

Refactor polling loops into recursive helpers to satisfy
no-await-in-loop, fix WebSocket named import, and correct
an absolute-path import in tests/tmp.

* chore(lint): memoize inline styles in context-menu

* chore(lint): narrow explicit any in claude-agent/openai stt

* chore(lint): reduce max-depth in opencode-agent foreground loop

* chore(lint): memoize inline styles in explorer-sidebar

* chore(lint): memoize mobile sidebar styles in explorer-sidebar

* chore(lint): memoize inline styles in agent-list

* chore(lint): reduce relay test no-await-in-loop warnings

Refactor while-loops and for-retry loops in e2e.test.ts and
live-relay.e2e.test.ts into recursive poll/attempt helpers.
Remaining 8 warnings in encrypted-channel.ts concern the custom
Transport interface (on-handler slots, not DOM EventTarget) and a
sequential send loop; both reflect deliberate runtime contracts.

* chore(lint): fix typecheck errors from hoisting refactors

- pair-scan: type BARCODE_SCANNER_SETTINGS as BarcodeSettings
- explorer-sidebar: add missing desktopSidebarStyle useMemo
- sidebar-workspace-list: coerce null dotColor to transparent
- menu-header, e2e.test: formatting

* chore(lint): memoize inline styles in workspace-screen

* chore(lint): memoize inline styles in workspace-desktop-tabs-row

* chore(lint): hoist constant style arrays in desktop-updates-section

* chore(lint): memoize inline styles in combobox

* chore(lint): extract SplitGroupChild to memoize inline styles

* chore(lint): memoize inline styles in dropdown-menu

* chore(lint): memoize inline styles in workspace-hover-card

* chore(lint): memoize inline styles in autocomplete, message-input, composer

* chore(lint): remove explicit any in server (129 warnings)

- session.ts: catch(error: any) -> catch(error) with Error coercion at use
- daemon-client transport: any -> unknown in listener types
- sherpa/onnx loaders: introduce structural native types
- pocket-tts-onnx: typed ONNX session inputs/outputs/tensors
- tests: any -> unknown + named stub types

* chore(lint): hoist inline arrays and objects in app (145 warnings)

- Hoist static style arrays/objects to module-level consts
- Memoize dynamic ones with useMemo and correct deps
- jsx-no-new-array-as-prop: 145 -> 43
- jsx-no-new-object-as-prop: 64 -> 21

* chore(lint): memoize inline styles in _layout

Fixes 3 react-perf/jsx-no-new-object-as-prop warnings in root layout by
memoizing the stack screen options, the agent screen override, and the
GestureHandlerRootView root style.

* chore(lint): memoize inline styles and objects in stream/status panes

Clears 10 react-perf/jsx-no-new warnings across welcome-screen,
file-explorer-pane, terminal-pane, agent-stream-view, and
agent-status-bar by extracting per-item subcomponents, hoisting constant
style tuples, and memoizing derived arrays/objects.

* chore(lint): memoize inline style objects in list/pane components

Clears 6 react-perf/jsx-no-new-object-as-prop warnings across
synced-loader, stream-strategy-web, sortable-inline-list, file-pane, and
both draggable-list platform variants by memoizing or hoisting their
inline style objects.

* chore(lint): hoist test fixture arrays/objects

Clears 14 react-perf/jsx-no-new-* warnings across app test files by
hoisting constant fixtures to module scope or wrapping them in helper
factories so they no longer appear as inline JSX prop values.

* chore(lint): memoize inline styles in message components

Clears 5 react-perf/jsx-no-new-* warnings in message.tsx by memoizing
image source objects, extracting an AssistantMessageBlockContainer for
per-block spacing, and moving todo list item rendering into a
TodoListItemRow subcomponent so each row memoizes its own style arrays.
The remaining 8 warnings in react-native-markdown-display render rules
are inherent to that library's rule API.

* chore(lint): remove unused vitest imports in server tests

* chore(lint): remove unused helpers and vars in server tests

* chore(lint): add explicit returns in then() callbacks in server

* chore(lint): fix no-shadow and no-nested-ternary in server

* chore(lint): flatten nested blocks to satisfy max-depth in server

* chore(lint): extract helpers to satisfy max-depth in server

* chore(lint): parallelize Claude persisted agents lookup

Replace sequential parseClaudeSessionDescriptor loop with Promise.all
to fix no-await-in-loop warning.

* chore(lint): parallelize Linux watch directory traversal

Switch BFS to level-by-level Promise.all over readdir calls, replacing
the sequential pop/await loop that triggered no-await-in-loop.

* chore(lint): parallelize workspace registry bootstrap upserts

Collect per-workspace upsert inputs in a sync pass, then run registry
writes via Promise.all to eliminate no-await-in-loop warnings.

* chore(lint): parallelize test daemon cleanup rm calls

Run the paseoHomeRoot/staticDir removals concurrently in close() and
the startup catch block to fix no-await-in-loop.

* chore(lint): mechanical cleanup in app (unused, shadow, nested-ternary)

Recovers in-flight edits from the app mechanical agent that couldn't be
pushed due to concurrent tree contention. Removes unused helpers and
locals, flattens nested ternaries, narrows a few props where unused.

* chore(lint): parallelize pending permission approvals

approvePendingPermissions now filters and records handled IDs in a
sync pass, then awaits all allowPermission calls concurrently instead
of looping awaits.

* chore(lint): parallelize rebase head-name lookup

Try both rebase backends (rebase-merge, rebase-apply) concurrently
via Promise.all and return the first non-null result.

* chore(lint): no-unused-vars in app

* chore(lint): parallelize worktree terminal bootstrap

Start each bootstrap terminal concurrently via Promise.all. Since every
spec creates an independent terminal and returns a standalone result,
parallelization preserves order while removing two no-await-in-loop
warnings.

* chore(lint): parallelize worktree bootstrap test cleanup and script spawn

Run terminal cleanup across managers and spawnWorkspaceScript for api
and web concurrently to eliminate no-await-in-loop warnings while
preserving semantics.

* chore(lint): prefer-add-event-listener in app

* chore(lint): parallelize script-health-monitor afterEach and spawns

Close all stub TCP servers concurrently and spawn typecheck/api
scripts via Promise.all to drop two no-await-in-loop warnings.

* chore(lint): jsx-no-useless-fragment in app

* chore(lint): parallelize executable PATH probing

* chore(lint): parallelize client and relay transport test cleanup

* chore(lint): parallelize agent storage record file probing

* chore(lint): no-array-index-key in app

* chore(lint): parallelize bootstrap provider availability cleanup

* chore(lint): parallelize Codex rollout file search

* chore(lint): parallelize dictation wav fixture search

* chore(lint): parallelize Claude session id fixture test

* chore(lint): parallelize script health monitor server cleanup

* chore(lint): parallelize Codex skills directory scan

* chore(lint): promise/always-return in app

* chore(lint): no-shadow in use-dictation, hoist icons in splash screen

* chore(lint): no-shadow in composer callbacks

* chore(format): apply oxfmt to 3 server files

* chore(lint): no-explicit-any in stt-manager/chat-mentions tests

* chore(lint): no-shadow in small app files

Rename inner shadowing identifiers in:
- add-host-modal, agent-status-bar, sidebar-workspace-list, terminal-pane
- workspace-setup-dialog, session-context, use-is-local-daemon
- setup-panel, new-workspace-screen, workspace-desktop-tabs-row

* chore(lint): no-explicit-any in claude-agent.test

* chore(lint): no-shadow in small app test/e2e files

* chore(lint): no-explicit-any in workspace-git-service.test

* chore(lint): no-shadow in app test hoisted blocks

* chore(lint): no-explicit-any in websocket-server.runtime-metrics.test

* chore(lint): no-explicit-any in acp-agent.test

* chore(lint): no-shadow in app composer/message-input/e2e helpers

* chore(lint): no-explicit-any in session.workspace-git-watch.test

* chore(lint): no-explicit-any in websocket-server.notifications.test

* chore(lint): no-shadow in sidebar-workspace-list.test, use-pr-pane-data.test

* chore(lint): no-explicit-any in websocket-server.relay-reconnect.test

* chore(lint): no-explicit-any in codex-app-server-agent.test

* chore(lint): no-shadow in composer.test, host-runtime.test, new-workspace-screen.test

* chore(lint): no-explicit-any in codex-app-server-agent.features.test

* chore(lint): hoist jsx-as-prop in app components batch 1

Memoize inline JSX icons passed as props across add-host-modal,
branch-switcher, composer, pair-link-modal, pr-pane, and
sidebar-workspace-list to satisfy react-perf/jsx-no-jsx-as-prop.

* chore(lint): no-explicit-any in session.test

* chore(lint): hoist jsx-as-prop in app components batch 2

Memoize inline JSX passed as props across agent-list, agent-status-bar,
file-explorer-pane, git-actions-split-button, and message to satisfy
react-perf/jsx-no-jsx-as-prop.

* chore(lint): hoist jsx-as-prop in app components batch 3

Memoize inline JSX passed as props across combined-model-selector,
draggable-list.native, provider-diagnostic-sheet, and
workspace-setup-dialog to satisfy react-perf/jsx-no-jsx-as-prop.

* chore(lint): hoist jsx-as-prop in desktop components

Memoize inline JSX passed as props across desktop-permissions-section,
desktop-updates-section, integrations-section, and pair-device-section
to satisfy react-perf/jsx-no-jsx-as-prop.

* chore(lint): no-explicit-any in mcp-server.test

* chore(lint): hoist jsx-as-prop in workspace/new-workspace screens

* chore(lint): hoist jsx-as-prop in remaining screens

* chore(lint): no-explicit-any in session.workspaces.test

* chore(lint): no-explicit-any in snapshot-mutation-ownership.test

* chore(lint): no-explicit-any in dictation-stream-manager.test

* chore(lint): no-explicit-any in relay-transport.e2e.test

* chore(lint): no-explicit-any in worktree-session.test

* chore(lint): no-explicit-any in model-resolver.test

* chore(lint): no-explicit-any in sherpa-parakeet-stt.test

* chore(lint): no-explicit-any in speech-download.e2e.test

* chore(lint): no-explicit-any in script-health-monitor.test

* chore(lint): no-explicit-any in schedule/service.test

* chore(lint): no-explicit-any in persistence-hooks.test

* chore(lint): no-explicit-any in editor-targets.test

* chore(lint): no-explicit-any in send-while-running-stuck-test-utils.test

* chore(lint): no-explicit-any in agent-storage.test

* chore(lint): no-nested-ternary in app batch 1 (18 files)

* chore(lint): no-explicit-any in generate-sherpa-tts-matrix

* chore(lint): no-explicit-any in claude-agent

* chore(lint): no-explicit-any in websocket-server

* chore(lint): no-explicit-any in process-conversation

* chore(lint): no-explicit-any in daemon-client

* chore(lint): no-explicit-any in codex-app-server-agent

* chore(lint): no-nested-ternary in app batch 2 (26 files)

* chore(format): apply oxfmt to server test files

* chore(lint): parallelize agent archiving in test-mcp-inject

* chore(lint): no-explicit-any in small app files (batch 1)

* chore(lint): no-explicit-any in small app files (batch 2)

* chore(lint): no-explicit-any in app (batch 3)

Covers use-web-scrollbar, stream-strategy, dictation-stream-sender test,
terminal-perf helper, checkout-git-actions-store test, and
web-desktop-scrollbar.

* chore(lint): no-explicit-any in app (batch 4)

Covers composer, message-input, tooltip, workspace-desktop-tabs-row,
and the e2e helpers (app, node-ws-factory, terminal-probes).

* chore(lint): no-explicit-any in polyfills/crypto.ts

* chore(lint): no-explicit-any in components/sidebar-workspace-list.tsx

* chore(lint): prefer-array-find over filter().at/pop

Replace filter(pred).at(-1)/pop() patterns with findLast(pred) and
filter(pred)[0] with find(pred) across server and app.

* chore(lint): no-explicit-any in components/message.tsx

* chore(lint): no-explicit-any in components/plan-card.tsx

* chore(lint): no-map-spread replace with Object.assign

Replace { ...obj, override } inside map callbacks with Object.assign({}, obj, { override })
to satisfy oxc/no-map-spread. Preserves copy-on-write semantics.

* chore(lint): no-extraneous-class in test mocks

Add dispose() stubs to xterm addon mocks to satisfy typescript-eslint/no-extraneous-class, and convert static-only Notification mocks to plain objects.

* chore(lint): no-named-as-default use named imports for ws and openai

* chore(lint): jsx-no-new-array-as-prop hoist composed style in volume-display

Memoize the style array so it is not created inline on every render.

* chore(lint): prefer-add-event-listener use Object.assign for non-DOM handlers

These transports (Transport interface, StreamableHTTPServerTransport) use
plain on<event> properties rather than EventTarget, so addEventListener is
not an option. Using Object.assign to set the handlers avoids the lint
false-positive without changing behavior.

* chore(lint): no-multiple-resolved null pendingResolve after settling

Replace boolean-settled guards with a nullable captured resolve/reject.
Each promise callback captures the resolver, nulls it on first use, and
calls it. This preserves the existing single-resolution semantics while
making the no-multiple-resolved rule happy.

* chore(lint): reduce structural complexity in server

Extract nested-callback cleanup helpers in worktree-bootstrap.test. Extract Codex model definition builder to drop list-models complexity below 20.

* chore(lint): extract helpers in agent-activity and session-store-hooks.test

Split tool-call update handling in groupActivities into helpers to clear max-depth. Hoist useWorkspaceFields selector in the hooks test to drop callback nesting.

* chore(lint): rules-of-hooks and exhaustive-deps in agent-stream-view

Move useMemo calls for permission card styles above the early return
so hooks run unconditionally. Add missing dependencies to the inline
path press handler and the stream render callback.

* chore(lint): rules-of-hooks in diff-scroll via optional context hook

Replace try/catch around useExplorerSidebarAnimation with a new
useExplorerSidebarAnimationOptional hook that returns null when the
provider is absent.

* chore(lint): rules-of-hooks and exhaustive-deps in sidebar-workspace-list

Replace conditional useMemo calls with plain inline arrays, capture
creatingWorkspaceTimeoutsRef.current in the effect body before cleanup,
and depend on the full input object in armTimers.

* chore(lint): rules-of-hooks in split-container

Hoist useWorkspaceLayoutStore and useMemo calls above the pane-kind
early return so hooks always run in the same order.

* chore(lint): rules-of-hooks in context-menu and dropdown-menu

Inline the resolvedWidthStyle object inside the content useMemo and move
the early return below the hook so it is always invoked in the same order.

* chore(lint): rules-of-hooks in web-desktop-scrollbar

Move thumbRegionStyle and handleStyle useMemo calls above the visibility
early return.

* chore(lint): reduce complexity in types/stream

Split reduceStreamUpdate timeline branch into per-case helpers (tool call and compaction) to clear complexity and max-depth warnings.

* chore(lint): reduce complexity in diff-highlighter

Extract metadata detection, path extraction, hunk parsing, and content-line push into helpers to drop parseDiff complexity below 20.

* chore(lint): exhaustive-deps in composer and use-attachment-preview-url

Memoize githubSearchItems so the picker callbacks don't see a new array
every render, hoist realtimeVoiceButtonStyle above rightContent useMemo
that depends on it, and add missing style dependencies. Tighten
use-attachment-preview-url to read the attachment via a ref while keying
off stable field primitives.

* chore(lint): reduce complexity in tool-call-detail-state

Extract per-detail helpers for hasMeaningfulToolCallDetail to clear complexity warning.

* chore(lint): reduce complexity in agent-grouping

Split groupAgents into partition, project-activity map, active-project, and inactive-date helpers.

* chore(lint): reduce max-depth in voice-runtime

Extract retireFinishedGroup helper to flatten processPlaybackQueue nesting.

* chore(lint): reduce max-depth in use-branch-switcher

Extract maybeRestoreStashForBranch callback to flatten handleBranchSelect nesting.

* chore(lint): exhaustive-deps in components batch

Address exhaustive-deps warnings across file-explorer-pane, file-pane,
git-diff-pane, message, provider-diagnostic-sheet, stream-strategy-web,
terminal-emulator, terminal-pane, and volume-meter. Memoize values that
otherwise changed every render, read non-reactive values through refs
when the effect intentionally tracks a different key, and add missing
dependencies where they were genuinely absent.

* chore(lint): reduce complexity/max-depth in draft-store

Extract collectQueuedMessageAttachmentIds and collectStreamUserImageIds helpers from runAttachmentGc. Extract buildMigratedDraftRecord helper for migratePersistedState.

* chore(lint): reduce complexity in tooltip and workspace-scripts-button

Extract resolveActualSide and resolveAlignedCoordinate helpers in tooltip. Extract resolveScriptIconColor in workspace-scripts-button.

* chore(lint): exhaustive-deps in contexts and hooks batch

Fixes exhaustive-deps warnings across app contexts and hooks by adding
missing deps, stabilizing derived arrays with useMemo, capturing ref values
at effect entry, and using ref pattern where deps were intentionally omitted.

* chore(lint): final hook-rules in workspace-screen, editor-button, stores, e2e

Memoizes derived arrays (availableEditors, terminals) to stabilize
useMemo deps, adds missing deps (normalizedServerId, normalizedWorkspaceId,
explorerToggleStyle, workspaceIdsKey, eventName), and renames the Playwright
fixture callback parameter from `use` to `provide` so eslint-plugin-react-hooks
doesn't conflate it with React's use() hook.

* chore(lint): reduce complexity/max-nested-callbacks in workspace-tabs-store

Extract migrate function body into migrateWorkspaceTabsState top-level
helper with per-loop sub-helpers (migrateUiTabsForKey, mergeExplicitTabOrder,
mergeLegacyTabOrder, migrateFocusedTabIds) and replace the inline IIFE in
ensureTab with buildNextTabsForEnsure helper.

* chore(lint): reduce complexity in session-store and panel-store

Extract isSessionServerInfoUnchanged helper from updateSessionServerInfo
in session-store. In panel-store split migrate body into per-version
migration helpers (migratePanelV2Explorer, migratePanelV3Explorer,
migratePanelExplorerTabByCheckout, migratePanelDesktopFocusMode) and
a top-level migratePanelState function.

* chore(lint): reduce complexity in keyboard-shortcuts and use-keyboard-shortcuts

Split resolveKeyboardShortcut into resolveInitialChordStep and
resolveAdvancingChordStep helpers with a shared buildMatchFromBinding
factory. Split handleAction's large switch into handleDispatchOnlyAction,
handlePayloadAction, handleSettingsToggle, and handleCommandCenterToggle
helpers, with hasPayloadKey type guard replacing inline payload checks.

* chore(lint): reduce complexity in audio recorder web hooks

Extract assertMicrophoneEnvironment helper from useAudioRecorder start
callback. Split useDictationAudioSource stop callback with
disconnectDictationAudioGraph, stopMediaRecorderIfActive,
finalizeRecorderStoppedPromise, and safeDisconnectNode helpers.

* chore(lint): reduce complexity in use-git-actions

* chore(lint): reduce complexity in use-pr-pane-data

* chore(lint): reduce complexity in use-agent-autocomplete

* chore(lint): reduce complexity in use-agent-screen-state-machine

* chore(lint): reduce complexity in session-stream-reducers

* chore(lint): reduce complexity in desktop-updates-section

* chore(lint): reduce complexity in pair-device-section

* chore(lint): reduce complexity in update-callout-source

* chore(lint): reduce complexity in provider-diagnostic-sheet

* chore(lint): disable no-await-in-loop (sequential-by-necessity cases)

Most remaining no-await-in-loop warnings were in legitimate sequential
patterns: polling loops with sleep, streaming/pagination cursors, shared
mutable state, ordered side effects (audio playback, port allocation).
Parallelizing these would change observable behavior. Rather than force
restructures that add complexity without benefit, disable the rule.

* chore(lint): reduce complexity in setup-panel

Extract helpers (resolveAutoExpandIndex, resolveSetupStatusLabel,
resolveCommandLog, buildCommandRowState) and lift the standalone log
view and top-level error banner into dedicated sub-components to drop
SetupPanel below the complexity limit.

* chore(lint): reduce complexity in AssistantMarkdownImage

Extract error-text resolution into a helper so the image component
drops below the cyclomatic complexity threshold.

* chore(lint): reduce complexity in ProjectLeadingVisual

Extract the status-variant dispatch into a ProjectLeadingVisualStatus
sub-component so the outer function stays below the cyclomatic
complexity limit.

* chore(lint): extract ChatAgentContent agent-building helper

* chore(lint): reduce complexity in ChatAgentContent

Extract the chat-agent selector (selectChatAgentState +
resolveChatAgentFromSession), the agent-shape constructor
(buildChatAgentFromState), and the not-found/error/boot view
dispatch (renderChatAgentNonReadyView) into helpers so both the
selector callback and the component function drop below the
cyclomatic complexity limit.

* chore(lint): extract subcomponents to reduce JSX nesting depth

* chore(lint): reduce complexity in app components (status-bar, explorer, setup-dialog, tool-call-details)

* chore(lint): reduce complexity in app hooks/runtime/stores/misc components

* chore(lint): long-tail cleanup (no-async-endpoint-handlers, unicorn rules, unescaped-entities, unassigned-import allowlist)

* chore(lint): simplify agent useMemo deps to match helper signature

* chore(lint): rename ChatService.postMessage to dispatchMessage

* chore(lint): server cleanup (no-shadow, no-useless-*, no-unmodified-loop, control-regex, require-yield)

* chore(lint): app cleanup (jsx-max-depth, jsx-no-new-*-as-prop, complexity, misleading-regex, no-useless-*)

* chore(lint): e2e cleanup (no-unmodified-loop-condition, max-nested-callbacks)

* chore(lint): workspace-screen complexity + no-empty-pattern cleanup

* chore(lint): use Array.from for defensive snapshot iteration

* chore(lint): final mechanical tail (control-regex, max-depth, useless-expressions, examples cleanup)

* chore(lint): reduce agent-stream-view complexity

* chore(lint): reduce resolveAgentModelSelection complexity

* chore(lint): reduce globalSetup complexity

* chore(format): oxfmt draft-store single-line signature

* chore(lint): reduce GitDiffPane complexity

* chore(lint): reduce MessageInput complexity

* chore(lint): reduce Combobox complexity

* fix(server): bound listLinuxWatchDirectories readdir concurrency

Wraps the per-level readdir traversal in listLinuxWatchDirectories with a
module-level p-limit (default 16, tunable via PASEO_LINUX_WATCH_READDIR_CONCURRENCY).
On broad repos, the previous Promise.all over an entire BFS level could issue
an unbounded number of concurrent readdir calls. Behavior is otherwise
identical: still traverses all non-.git directories and swallows per-directory
readdir failures.

* chore(lint): promote oxlint warnings to errors

- package.json: lint/lint:fix now pass --deny-warnings so any warning fails
- .github/workflows/ci.yml: add lint job running npm run lint
- .oxlintrc.json: disable no-empty-pattern for e2e/fixtures.ts
  (Playwright requires the empty object destructure as the first arg)
- packages/app/e2e/fixtures.ts: restore async ({}, provide) signature
- packages/app/e2e/global-setup.ts: oxfmt single-line signature

* fix(test): relay-transport ws mock exports WebSocket named binding

Commit 38efad7d switched relay-transport.ts from default to named import
of ws.WebSocket, but the test mock still only exported default. Updated
vi.mock("ws") to return both default and named WebSocket bindings so the
mocked module matches the new import shape. Classification: test drifted
with production code; test now exports what the production import needs.

* chore(lint): make oxlint error at config level; add Lefthook pre-commit

Replaces --deny-warnings workaround with semantic severity at the config
level, and adds whole-repo pre-commit gates via Lefthook.

- .oxlintrc.json: all enabled categories (correctness, suspicious, perf)
  and intentional rule overrides now use "error" instead of "warn".
  Removed duplicate "require-await" key (last-wins "off" preserved).
- package.json: lint scripts are plain oxlint / oxlint --fix. Adds
  lefthook devDep and "prepare": "lefthook install --force" so hooks
  install on npm install even where core.hooksPath is set globally.
- lefthook.yml: pre-commit runs format:check, lint, typecheck in
  parallel. Whole-repo gates (no glob filters). Block-only, no
  auto-formatting.

* fix: update lockfile signatures and Nix hash

* chore(lint): pre-commit runs formatter in write mode, then lint+typecheck

Switches the pre-commit hook from format:check to format (write) with
stage_fixed: true, so the formatter rewrites files and re-stages them
into the commit. Lint and typecheck then run in parallel against the
formatted tree.

Sequencing uses Lefthook 2.x jobs with a nested group: top-level jobs
run sequentially, so the format job completes before the checks group
starts; within checks, lint and typecheck run in parallel.

CI continues to use format:check, so unformatted code pushed with
--no-verify still fails on the server.

* chore(lint): revert pre-commit to format:check (no stage_fixed)

stage_fixed: true with a whole-repo formatter (oxfmt .) can silently
re-stage the full reformatted file when a developer used git add -p
to stage only a hunk, losing their hunk-level intent. Safer to just
block the commit on unformatted code and let the dev run
`npm run format` themselves.

Reverts to the simple parallel check form: all three commands run
concurrently and are read-only.

* chore(ts7): tsgo compatibility across the monorepo

Preserves tsc 5.9.3 behavior while making TypeScript 7 / tsgo
(@typescript/native-preview 7.0.0-dev.20260423.1) green
package-by-package.

tsconfig:
- server/tsconfig.server.json: drop removed `baseUrl`; rewrite
  `@server/*` paths entry to be relative to the tsconfig.
- website/tsconfig.json: drop removed `baseUrl`; rewrite
  `~/*` paths entry to be relative.
- expo-two-way-audio/tsconfig.json: replace inherited
  moduleResolution=node10 with `bundler`, set `module: esnext`
  (required by `bundler`). Matches Metro/Expo runtime behavior
  and avoids forcing `.js` extensions in source.

source:
- app/e2e/helpers/terminal-probes.ts: WebSocket subclass `send`
  now types its argument as `Parameters<WebSocket["send"]>[0]`
  so the subclass signature tracks the platform type across
  tsc 5.9 and TS 7 libdom.
- relay/src/crypto.ts: tweetnacl setPRNG callback allocates a
  fresh Uint8Array for getRandomValues (owning ArrayBuffer) and
  copies into the caller-provided view. TS 7 libdom requires
  ArrayBufferView<ArrayBuffer> (not ArrayBufferLike).

All 8 packages green under both tsc --noEmit and tsgo --noEmit.

* fix(app): update os-notifications test mocks for addEventListener

Commit 46731a51 switched attachWebClickHandler from notification.onclick
to notification.addEventListener("click", ...) to satisfy the
prefer-add-event-listener lint rule. The test mocks still asserted
the old onclick property, so two specs failed on CI with
"notification.addEventListener is not a function". Update the three
MockNotification classes to record click listeners through
addEventListener and drive them via clickListeners[0] in the
assertions.

* chore: switch typecheck from tsc to tsgo (@typescript/native-preview)

Installs @typescript/native-preview 7.0.0-dev.20260423.1 as a root
devDependency and swaps every per-workspace `typecheck` script from
`tsc --noEmit` to `tsgo --noEmit`. Build scripts still use `tsc`, so
emit behavior is unchanged. Monorepo typecheck drops from ~28s to
~4.7s wall clock (~6x faster).

expo-two-way-audio previously ran `tsc` without --noEmit for its
typecheck step. tsgo is stricter about rootDir on emit, so the
typecheck script explicitly passes --noEmit here too.

* fix: update lockfile signatures and Nix hash

* fix(app): commit *.css module declaration for CI typecheck

expo generates expo-env.d.ts (which references expo/types where
*.css is declared) locally, but the file is gitignored. CI does a
fresh checkout + install and doesn't run expo, so the declaration
is missing and tsgo rejects the side-effect import
'@xterm/xterm/css/xterm.css' with TS2882. Add a committed
packages/app/global.d.ts with 'declare module "*.css"' so typecheck
is self-contained.

tsc tolerated the missing declaration more loosely; tsgo is stricter.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-24 12:26:10 +08:00
Mohamed Boudra
9c88850de0 chore: replace biome with oxc (oxfmt + oxlint)
- Remove @biomejs/biome from root and packages/expo-two-way-audio
- Delete biome.json
- Add .oxfmtrc.json (migrated from biome via oxfmt --migrate=biome)
- Add .oxlintrc.json with curated rule set: react, react-perf,
  react-hooks, typescript, unicorn, oxc, import, promise
  - Rules target functional gotchas, untyped code, perf issues,
    deep JSX, nested ternaries, complexity, unused code
  - Noisy stylistic rules explicitly disabled
- Add npm scripts: lint, lint:fix (oxfmt replaces format/format:check)
- Update CI to run oxfmt --check

Lint is wired up but not enforced — all rules at warn. Baseline:
6040 warnings, 0 errors.
2026-04-23 21:44:56 +07:00
Mohamed Boudra
ce4b0d54c7 chore: reformat codebase with oxfmt
Apply prettier-compatible formatting across the repo to match the
incoming oxfmt configuration. Mechanical reformat only — no logic
changes. Covers YAML quote normalization, package.json key sorting,
Markdown/TOML formatting, and minor TS whitespace tweaks.
2026-04-23 21:44:48 +07:00
Mohamed Boudra
e21ef2fc9b fix: prevent old release website redeploys 2026-04-23 10:17:37 +07:00