Commit Graph
193 Commits
Author SHA1 Message Date
Mohamed BoudraandGitHub 4108960709 Manage orchestration skills per host (#3451)
* feat(skills): manage orchestration skills per host

Agent skills execute on the daemon host, so their bundle, selection, and filesystem convergence must share that owner. Preserve the existing install semantics while letting every capable client manage the selected host and migrating the legacy desktop preference once.

* fix(client): keep skill management internal

Keep host skill management on the low-level daemon client used by the app. Do not expose daemon settings or the desktop-only compatibility migration through the shared Paseo API handed to plugins.

* fix(e2e): declare skills fixture dependencies

Use Playwright's required destructuring form for the test-scoped daemon sandbox fixture so CI can collect the real-host skills suite.

* test(skills): use native managed paths

Assert fixed skill targets relative to the daemon user's real home with platform-native path joining so the ownership contract holds on Windows and POSIX hosts.
2026-08-18 09:51:46 +02:00
Mohamed Boudra 405c6e07a9 fix(app): retain workspace pull request status across reloads 2026-08-18 00:32:03 +02:00
Mohamed Boudra 3948bf4fd6 fix(app): resolve terminal profiles at creation boundary
Profile placeholders were reaching spawned processes because each menu decided whether to resolve them. Keep UI callers profile-only and perform empty-prompt resolution once when the workspace terminal is created.
2026-08-17 22:55:46 +02:00
Mohamed BoudraandGitHub 1e2089cef7 Summarize pull request checks and group them by status (#3483)
* fix(app): keep check rows the same height with and without "Add to chat"

A check row was minHeight 32 plus 8pt of vertical padding, but the xs
Button in its trailing slot is 28pt tall. The button stacked on the
padding rather than fitting inside the row, so a row grew to 44pt the
moment "Add to chat" appeared and the list visibly shifted as rows
gained the button.

Rows are single-line by construction, so the row now declares a fixed
height instead of a floor a child can outgrow. That height comes from
CONTROL_HEIGHTS, the same source the button sizes from, so the two
cannot drift apart later. GitLab pipeline rows share this style and get
the fix too.

* feat(app): summarize pull request checks and group them by status

The checks section listed every check inline under a bare "Checks"
header with three count pills. On a repo with a real matrix that meant
a wall of undifferentiated rows that pushed the activity timeline off
the pane, and the pills made the reader add up numbers to work out
whether the run was actually in trouble.

The section now leads with the answer — a headline that names the
outcome, a one-line count breakdown, and a ring showing the statuses in
proportion — then groups the rows by status with failures first, and
scrolls in place past roughly eight rows instead of growing without
bound. Groups collapse individually.

The derivation lives in checks-summary.ts as one pure function, so the
headline, the ring, and the group labels cannot disagree about the same
run. The rendering, the row, and the check identity move out of pane.tsx
into an owned module; the pane now wires it and nothing more.

Running checks also gain a timing. formatCheckRunDuration required both
a start and a completion, so anything still going showed nothing at all.
It now measures an unfinished run to now. The wire deliberately carries
formatted strings rather than raw timestamps — there is a test asserting
check timestamps are stripped — so the daemon does the measuring and the
client only decides how to say it: a bare "2m" on a running row would
read as "took 2m", so those rows say "running 2m".
2026-08-17 17:23:03 +02:00
Mohamed BoudraandGitHub 42245d139a Add active-turn steering (#3394)
* feat(agents): add active-turn steering

Let users choose whether an immediate prompt interrupts or joins the active turn. The daemon owns exact-turn admission, provider delivery, safe fallback, and canonical timeline identity while queueing remains client-side.

* fix(agents): stabilize steering history and replay QA

* fix(agents): preserve steering timeline order

* fix(agents): reconcile resumed provider history

* fix(agents): serialize steering with cancellation

* test(agents): prove Claude multi-tool steering

* test(agents): force explicit history hydration

* fix(agents): flush output before steer admission
2026-08-16 13:58:40 +02:00
Mohamed BoudraandGitHub 16120ebff1 Make local plugins manageable and debuggable (#3446)
* feat(plugins): expose Paseo API to local plugins

Plugin surfaces borrow the selected host connection while subprocess handlers receive an exclusively owned IPC session, so plugins can use normal workspace and agent operations without inventing another transport.

* feat(plugins): reload global enablement safely

Treat the global plugin switch as a live daemon setting and teach plugin automation to inspect it before acting. Enabling remains permission-gated because plugin backend code can access the daemon machine and client contributions execute inside the app.

* feat(plugins): expose plugin logs

Capture plugin stdout and stderr separately from IPC so authors can debug initialization, RPC, cleanup, and crash behavior from the app or CLI. Keep a bounded tail across lifecycle recovery while preserving structured output in the daemon log.

* fix(build): order plugin dependency builds

* test(plugins): align startup and CLI expectations
2026-08-16 18:54:44 +08:00
Mohamed BoudraandGitHub 4748aad103 Reload daemon configuration without restarting (#3365)
* feat(daemon): reload configuration without restarting

Keep config validation, reloadability classification, override handling, and atomic runtime application inside the daemon so local and remote clients share one authoritative workflow.

* fix(daemon): preserve reload resource ownership

Keep every materialized provider client under daemon-lifetime ownership and classify speech overrides from resolved providers. Restore the complete WebSocket config-store test contract exposed by CI.
2026-08-14 22:46:32 +02:00
Mohamed BoudraandGitHub 23853a1515 Add managed local plugin lifecycle (#3222)
* feat(plugins): add local plugin surfaces and RPCs

Keep plugin execution scoped to its daemon while allowing one local TypeScript entry point to contribute validated RPCs and native client surfaces. Paseo owns host selection, navigation chrome, and per-installation query caches.

* feat(plugins): add Linear attachment source

* fix(nix): refresh npm dependency hash

* test(server): exclude internal sessions from Hub counts

* test(server): distinguish Hub execution sessions

* feat(plugins): make local plugins explicitly opt-in

Keep plugin loading dormant until configuration enables it, and collect unsupported examples under one directory.

* refactor(plugins): establish host boundaries

* fix(nix): refresh plugin dependency hash

* fix(plugins): complete packaging and startup rollback

* feat(plugins): add managed local lifecycle

Make trusted local plugins recoverable and authorable through daemon-owned lifecycle operations, host settings, and a capability-gated CLI. Reloads fully stop the previous plugin before starting new code, while generated projects provide strict TSX checking without changing runtime dependency ownership.

* fix(nix): refresh plugin dependency hash
2026-08-14 17:12:49 +02:00
Mohamed BoudraandGitHub 373a98c64d Restore host directories instantly before reconnect (#3259)
* feat(sync): restore host directories before reconnect

Persist complete project, workspace, and active-agent replicas so every registered host can paint immediately, including offline hosts. Reconcile those replicas through monotonic per-entity cursors on the existing directory RPCs without retaining an event journal.

* fix(sync): preserve active turn identity during catch-up

Own Agent-to-wire projection beside the existing snapshot normalizer so replica persistence does not reach through directory-sync internals. This also keeps identified active turns intact when unchanged agents are folded into an incremental directory response.

* test(e2e): inspect the real IndexedDB replica

Keep cache setup, reload, and hydration coverage on the production browser storage backend after the replica moved off localStorage. Measure writes at the actual IndexedDB put boundary as well.

* fix warm reconnect cache paths

* feat(sync): persist project icons and deepen directory sync

* fix(sync): preserve project update order

* fix(cache): preserve validated replica persistence

* fix(sync): retain cached agents during catch-up
2026-08-13 23:10:44 +02:00
Mohamed Boudra b44bb63cf4 chore(release): cut 0.4.0 2026-08-13 21:31:47 +02:00
Mohamed BoudraandGitHub c190fed27c Keep new worktrees clean when setup changes are uncommitted (#3311)
* fix(worktrees): preserve selected base config

New worktrees were overwriting the selected ref's paseo.json with source-checkout bytes, which could create an accidental revert in an otherwise fresh workspace. Keep committed base config authoritative and warn when locally saved setup changes still need to be committed.

* fix(worktrees): seed config exclusively

* fix(projects): refresh config status on focus
2026-08-13 12:02:15 +02:00
Mohamed Boudra 44b9980a70 chore(release): cut 0.4.0-beta.2 2026-08-12 23:18:28 +02:00
Mohamed BoudraandGitHub 4426a6b11f Preserve opened subagents when parents are archived (#3279)
* fix(subagents): preserve opened children on parent archive

Opening a managed child represents user ownership only while that client's tab remains open. Track that intent per client, detach surviving children during parent archive, and keep single and bulk tab-close behavior consistent.

* fix(subagents): mark generic tab openings

Centralize managed child tab ownership in agent navigation so list, command-center, deep-link, notification, and subagent-track openings all mark ownership before creating the tab. Use stable localized errors for lifecycle failures.

* fix(subagents): classify cold tab openings

Fetch uncached agents before deciding whether tab ownership must be recorded, so notification, desktop, and cold-route openings cannot bypass parent archive protection.

* fix(app): translate navigation fallback errors

* fix(navigation): allow offline workspace intents

* refactor(app): derive subagent ownership from open tabs

* fix(server): serialize subagent lifecycle ownership
2026-08-12 21:18:55 +02:00
Mohamed Boudra 0c6dbefa40 chore(release): cut 0.4.0-beta.1 2026-08-12 20:39:35 +02:00
Mohamed BoudraandGitHub db6520bf36 Show live task progress while agents work (#3227)
* feat(agents): surface live task progress

Keep current task state above the composer and turn task snapshots into semantic timeline activity. Normalize provider task protocols while preserving genuine plan flows.

* fix(agents): admit tasks through stream sequencing

* test(agents): expect normalized task status
2026-08-11 18:23:30 +02:00
Mohamed BoudraandGitHub 308e9af845 Add reusable agent profiles (#3208)
* Search models across all providers from the picker root

* Store agent profiles in daemon config and apply agent config in one RPC

An agent profile is a named bundle of provider, model, mode, thinking
option, feature values and notes, stored host-wide next to terminal
profiles. It carries no system prompt: AgentSessionConfig.systemPrompt is
creation-only, so a profile holding one would apply when starting an agent
and silently do nothing when applied to a running one.

Applying a profile through the four single-field RPCs can fail halfway and
leave an agent matching no profile, so agent.config.apply takes the whole
bundle. Model runs first because a thinking option only means anything
against the model that offers it.

Old daemons parse their persisted config strictly and would drop an
unknown agentProfiles key, making a save appear to succeed and vanish, so
both capabilities are advertised on server_info for the client to gate on.

list_profiles exposes the profiles to orchestrating agents, which read the
notes to pick one and copy its values into create_agent.

* Add the agent profiles settings section

Profiles live behind one module boundary so removing the feature is a
directory plus one mount, which is what favourites never was. The picker
and composer will ask for rows and an apply function rather than learning
what a profile contains.

The edit form follows the form-model pattern instead of the terminal
profile modal's per-field state, because provider selection cascades into
which models, modes, thinking levels and features exist below it.

The section is gated on server_info.features.agentProfiles: an older daemon
drops the key on write, and a save that appears to work and then vanishes
is worse than an unavailable state.

* Document agent profiles and name the four senses of profile

The glossary reserved Profile for HostProfile, terminal profiles owned the
user-facing word, and custom-providers.md used it for provider aliases.
Agent profile is now the term, with all four cross-referenced.

data-model.md never documented terminalProfiles either, so both profile
lists are backfilled together along with the whole-list patch semantics and
why absent and empty differ.

* Pin agent profiles to the model picker and delete favourites

Applying a profile writes its values into the agent controls and is then
forgotten, so rows carry no checkmark and no selected state: after applying,
nothing is selected. Chevron still means navigate deeper, so the absence of
one is what marks a row as an action.

A running agent is its provider's process and agent.config.apply carries no
provider, so the live picker only offers profiles matching the agent's
provider rather than showing rows that cannot do what they say. The draft
path has no such limit and switches provider freely.

Draft feature values go through preferences rather than the feature
callback alone, because the draft feature hook clears local values whenever
the provider changes and a provider-switching profile would lose them.

Favourites are deleted rather than migrated: they were device-local
AsyncStorage keyed by provider and model, while profiles are host-wide
daemon config. ProviderSelectionModelRow.favoriteKey stays as row identity
and moves to its real owner; renaming it is its own commit.

* Cover agent profiles and cross-provider search with E2E specs

Model rows expressed selection only as a checkmark glyph, which assistive
tech cannot see and a spec cannot assert. Rows that draw the indicator now
carry aria-selected; profile rows deliberately carry none, because a
profile is an action rather than a selection. That is what makes "nothing
is marked selected after applying a profile" a real assertion instead of a
tautology, with the materialized model one level down as the control.

The search fixture extends claude rather than mock: provider-config.ts
only allows the six shipped providers as extends targets, so a derived
provider cannot inherit the dev-only mock provider.

* State what applying a config bundle does on partial failure

applyBundle is not a transaction and the earlier commit message implied it
was. A step that rejects aborts the rest and the steps that already applied
stay applied, which a profile omitting modelId can reach: provider setters
validate against the agent's live model, so mode can land while a feature
that model does not support is rejected.

Nothing is hidden by that. Every setter emits agent state before returning,
so what applied is streamed to the client next to the rejected response.
Rolling back would re-invoke the setters that just failed, usually against
a session that is gone, and pre-flight validation would restate each
provider's model-dependent checks where they would drift from the setters
that own them.

What sending the bundle in one request removes is the multi-round-trip
failure class: interruption between hops, and interleaving with another
client. The test already pinned this behaviour; its name now says so.

* feat(agent-profiles): refine profile discovery and appearance

Make profiles discoverable from empty and provider-scoped model pickers, keep global search stable and virtualized, and move profile settings under Agents. Replace emoji identity with a shared Lucide icon and host-color picker, while requiring concrete provider selections so profiles materialize predictably.

* fix(agent-profiles): defer profile swatch styles

Resolving Unistyles proxies at module load can freeze the temporary startup color scheme before the persisted theme is restored. Keep only style-key names at module scope and resolve the selected swatch during render.
2026-08-11 17:42:35 +02:00
Mohamed BoudraandGitHub 6aa9f92156 Stop notifications after removing a host (#3176)
* fix(notifications): stop pushes after host removal

Push tokens were persisted indefinitely with no relationship to app pairing state. Give registrations a 48-hour renewable lease so offline unpairing converges, and revoke immediately when the host is reachable.

* fix(notifications): make token revocation bounded and durable
2026-08-10 23:33:31 +02:00
Mohamed BoudraandGitHub 94bda1f925 Remove chat rooms and agent loops before storage migration (#3053)
* refactor(storage): remove chat rooms and agent loops

This is the prerequisite cleanup for the storage backend migration. Legacy wire schemas remain temporarily so mixed-version peers can still parse old messages, while daemon callers now use query-shaped AgentStorage methods that can map directly to SQL later.

* test(cli): remove retired chat and loop coverage
2026-08-10 21:06:47 +02:00
ca2b633286 feat: add file manager context actions (#3027)
* feat(app): add file manager context actions

* fix: reshape file context actions

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-10 18:18:23 +02:00
Mohamed BoudraandGitHub 7954babca6 Publish the TypeScript SDK and integration guides (#3141)
* feat(client): publish the TypeScript SDK surface

Make the high-level client API usable for external integrations and document complete agent, workspace, provider, event, and recipe flows.

* fix(client): correlate provider snapshots by daemon cwd

Return the daemon's canonical snapshot identity so waitForReady can match updates for relative, tilde, and otherwise non-canonical paths without duplicating filesystem rules in the SDK.

* test(server): make snapshot cwd assertion portable

* fix(client): gate canonical provider snapshot waits

Older daemons do not expose the canonical snapshot identity needed to correlate workspace updates safely. Require the advertised capability and fail with an update-host message instead of guessing across workspaces.
2026-08-10 13:22:20 +02:00
Mohamed BoudraandGitHub 05a0cabc07 Deploy multi-file Hub workflows (#3128)
* feat(hub): deploy canonical workflow bundles

Keep CLI deployment aligned with Hub's authored bundle boundary so validation and activation receive the same deterministic source files. Public documentation now teaches that boundary as the only configuration format.

* test(hub): assert validation diagnostics directly

* refactor(hub): deepen HTTP client module

* feat(hub): validate named agent configurations

* docs(hub): align Claude mode example
2026-08-10 12:32:59 +02:00
Mohamed Boudra bfec7ac3ad chore(release): cut 0.3.1 2026-08-09 13:30:33 +02:00
Mohamed BoudraandGitHub fd9cc5d693 Restore macOS direct LAN connections (#3071)
* fix(desktop): restore direct LAN connections

Desktop 0.3.0 routed remote WebSockets through the Electron main process, causing previously working macOS LAN connections to fail with EHOSTUNREACH. Revert custom direct headers and restore the renderer WebSocket path used before the regression.

* fix(app): replace host connections by identity

Keep one connection per stable ID when TLS or password settings change. This preserves the current-main upsert invariant while the custom-header rollback removes the unrelated transport feature.
2026-08-09 15:32:31 +08:00
Mohamed BoudraandGitHub ce7b3a90c0 Give every Hub execution its own workspace lifecycle (#3045)
* fix(server): archive Hub executions by workspace identity

Hub archive previously depended on backing-specific owner state and archived the agent directly, so local and reused workspaces could survive. Resolve the required workspaceId from the execution agent and let the shared workspace archive service own agent, terminal, and backing teardown.

* fix(server): preserve agent archive through workspace teardown

* test(server): keep Hub regression fixtures workspace-scoped

* fix(hub): own executions through fresh workspaces

Hub executions must own a workspace regardless of backing so archival has one lifecycle boundary for agents, terminals, and checkout cleanup. Retain the retired wire selector for parsing compatibility while removing its ownership semantics.

* fix(server): serialize archive metadata writes

* fix(server): preserve archive during reconciliation
2026-08-08 23:55:12 +02:00
Mohamed BoudraandGitHub f0eb7cea4b feat(providers): add native options and exact MCP grants (#3025)
* feat(providers): add native options and exact MCP grants

Keep provider policy in provider-owned schemas so unattended Hub runs can grant only requested tools from injected MCP servers. Preserve the native resolved sandbox policy when a workflow override is present, including configured writable roots.

* test(hub): verify real provider tool policy

* test(app): avoid pinning transient restart status
2026-08-08 16:49:29 +02:00
Mohamed Boudra 7392e1b767 chore(release): cut 0.3.0 2026-08-08 10:56:30 +02:00
Mohamed BoudraandGitHub b167ee02e1 feat(app): search history by workspace, agent, and branch (#2995)
History was a date-grouped list you scrolled. Finding a session from last
week meant eyeballing every row. This adds a search field to the existing
filter rail, next to the host picker.

Search runs on the daemon, not the client. The daemon already holds every
persisted agent in memory, so it ranks the whole history and the client
never has to warn that it only looked at the pages it had loaded.

A searched response is one ranked page with no cursor. The global top K is
always contained in the union of each host's own top K, so merging per-host
pages and slicing back to the limit is the exact global answer — no k-way
merge, and no offset into a ranking that is recomputed per request, which
would duplicate and skip rows as history changes underneath it. Past result
200 of a relevance order the answer is a narrower query, so the footer says
that instead of offering a "Load more" that cannot deliver.

Matching is the app's existing picker matcher, moved to the protocol package
so app and daemon share one implementation instead of drifting into two
"why didn't this show up?" rule sets. Typo tolerance is opt-in, so the
comboboxes keep exact narrowing; the budget is per token, and four-character
tokens get transpositions only, because at that length a free substitution
turns "main" into "mail", "maid" and "rain".

Ranked rows carry the spans they matched on, so results can mark them. The
subsequence and typo tiers match characters the eye would never find
unaided, and an unexplained row reads as a bug. The ranker computes the
spans anyway; sending them keeps the client from deriving a second opinion
that could disagree with the ranking it is explaining.

Hosts that fail or are offline are now named above the list. Without that,
"No sessions match" is a claim the app has no basis for.

Gated on server_info.features.agentHistorySearch, checked once against the
connected hosts. Reachability is reported by the banner instead, so one
offline host in the registry no longer hides the field.
2026-08-08 10:24:51 +02:00
Mohamed Boudra a7c7a6e482 chore(release): cut 0.3.0-beta.4 2026-08-07 23:59:18 +02:00
Mohamed Boudra 86ff0a18c4 chore(release): cut 0.3.0-beta.3 2026-08-07 18:09:43 +02:00
Mohamed BoudraandGitHub c352632b74 Launch a terminal instead of a chat from New workspace (#2941)
* feat(app): launch a terminal from the New workspace screen

New workspace only ever started a chat agent. Empty workspaces existed but
were reachable only by submitting an empty composer, which nothing advertised,
and terminals could not be started from this screen at all. People concluded
Paseo did not support terminals.

Adds a launch chip to the meta row, shaped from the same ComboboxTrigger as
its neighbours, that chooses between Chat and a terminal. Terminal profiles
mark where a typed prompt goes with a {{{prompt}}} sentinel in their command
or args, substituted client-side before create_terminal_request, so the daemon
learns nothing new and the protocol is untouched. A profile without the
sentinel takes no input: the composer goes read-only and shows the exact
command, so there is nothing to type wrong.

Prompt forms come from each CLI's own --help. claude, codex, and pi take a
trailing positional; opencode's positional is the project directory, so it
uses --prompt=. Profiles predating the sentinel adopt the right form on read,
keyed on the command's base name rather than the profile id, because profiles
created in settings get generated ids.

* fix(app): stop existing launchers passing the prompt sentinel literally

Adoption made every caller of resolveTerminalProfiles see sentinel-bearing
args, but only New workspace substituted them. Pinned targets and the
workspace terminal menu forwarded profile.args straight through, so launching
Claude from an existing workspace ran `claude "{{{prompt}}}"` with that text as
the prompt.

Every launcher now goes through resolveTerminalProfileLaunch. Surfaces with
nowhere to type pass an empty prompt, which drops the prompt-only arg and
gives back the bare command they had before.

Also from the same review:

- Adoption skipped when args lead with a subcommand. `opencode run` takes a
  positional message rather than --prompt, and `codex login` takes no prompt at
  all. Only the first arg is checked, so `pi --thinking high` still adopts.
- Settings edits the persisted list again, not the adopted view. It writes the
  whole list back on any change, so resolving first baked read-time adoption
  into the user's config the moment they reordered a row.
- A hidden draft no longer names a workspace. Text typed for a prompt-taking
  profile stayed in state after switching to a read-only one and still seeded
  the generated title and branch.
- Windows .cmd escaping now has a round-trip test: a real cmd.exe runs a real
  .cmd shim that echoes its argv, covering metacharacters, an injection
  attempt, and newlines. The existing tests only asserted the command line we
  generate.
- Fixed an ENOTEMPTY flake in the terminal suite. kill() only signals, so the
  shell was still writing into $ZDOTDIR while its directory was being removed.
  Cleanup waits for exit instead.

* fix(app): keep the host and isolation accessible names stable

Wiring the new tooltips also replaced these two accessible names with the
tooltip copy, which broke `sidebar-project-grouping` at the host picker: its
selector is `getByRole("button", { name: "Host", exact: true })` and the button
had become "Choose the host". It timed out on both attempts in CI.

An accessible name identifies a control; a tooltip explains it. Conflating them
renamed a control that other surfaces address by name. The names go back to
what they were and the tooltips keep the "Choose the ..." copy.

* test(server): stop temp-dir teardown failing unrelated terminal tests

The ENOTEMPTY flake survived both earlier attempts. Waiting for the session to
exit shrank the window; retrying the remove did not close it either. The writes
come from processes zsh forks on the way out, which outlive the pty and answer
to nothing the test holds.

Teardown is now best-effort and logs what it left behind. A temp directory we
cannot remove is not a defect in the terminal code under test, and failing on it
reports the race against whichever assertion ran last, which is exactly how this
surfaced: as a flake in an unrelated zsh title test. 10 consecutive runs green.

Also replaces a cast in the Windows .cmd helper with a real narrow, so a change
to ResolvedTerminalCommand fails loudly there. Test files are excluded from
typecheck (tsconfig.server.json), so a cast in one is never checked at all.

* fix(app): keep terminal launch policy in its owning workflows

Profile adoption now parses known global option values before deciding whether argv already contains a positional. Workspace naming is derived inside the terminal workflow so hidden launch-only drafts cannot affect branch or title generation.

* fix(server): preserve cmd shim arguments through both parses

npm command shims forward %*, so metacharacters lose one escape layer before the target process receives argv. Exercise the production node-pty string command line against a forwarding shim, and only tolerate the known ENOTEMPTY teardown race.

* fix(server): stabilize Windows subprocess tests

* test(server): isolate Windows argv recorder output

* test(server): allow native watcher integration to settle
2026-08-07 14:08:22 +08:00
Mohamed BoudraandGitHub f2054cc701 Preserve Claude model and thinking preferences (#2912)
* fix(agents): preserve Claude thinking selections

Model switches now resolve thinking against the target model's saved preference instead of resetting to the catalog default. Claude owns its configured-model defaults and legacy Fable alias handling, while persisted permission modes remain unchanged.

* chore(claude): tag Fable compatibility shim

* fix(app): resolve persisted model aliases

* fix(models): preserve aliases for older clients

* fix(models): hide compatibility aliases from listings

* fix(settings): hide compatibility model aliases

* test(claude): cover legacy catalog entry

* test(app): expect mock thinking controls in schedules

* fix(app): preserve queued thinking preferences

* fix(app): reconcile schedule provider snapshots

* fix(app): preserve schedule model selections

* fix(claude): preserve exact configured model ids

* fix(app): preserve committed thinking preferences

* fix(app): preserve aliased schedule thinking
2026-08-06 11:33:18 +02:00
73ba37a07a Add custom headers to direct connections (#2922)
* Add custom headers to direct connections

* Address custom header review feedback

* Fix direct connection header integration

* Fix desktop relay E2E transport setup

---------

Co-authored-by: Claude <claude@Claudes-Mac-mini.local>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-06 11:05:40 +02:00
Mohamed Boudra d227ff3e83 chore(release): cut 0.3.0-beta.2 2026-08-05 15:30:50 +02:00
Mohamed Boudra 9916e3d185 chore(release): cut 0.3.0-beta.1 2026-08-04 21:27:12 +02:00
Mohamed Boudra 7dbeff8290 perf(app): reduce startup websocket work
Make provider catalogs compact and content-addressed so repeat launches can reuse the persisted snapshot instead of transferring it again. Remove eager global catalog loading and the stopped-agent defensive tail fetch, while keeping sequence-gap and reconnect recovery as the correctness paths. Add an opt-in production-ID Android profiling build for local startup traces.
2026-08-04 15:45:55 +02:00
Mohamed BoudraandGitHub fb657ebc20 Default new worktrees to the upstream branch (#2848)
* fix(workspaces): default new worktrees to upstream refs

Use the daemon-resolved upstream as the untouched starting ref so unpushed local commits do not leak into new workspaces. Preserve exact base identity for fork remotes while keeping local-first behavior across version drift.

* fix(workspaces): preserve exact base refs in git actions

Resolve accepted display aliases back to the stored ref before comparisons and updates. Fail closed when a non-origin remote base has no explicit mutable merge target.

* fix(workspaces): reject conflicting qualified base refs

Display aliases remain accepted for compatibility, but a qualified ref carries remote identity and must not be canonicalized to a different stored upstream.

* test(server): make upstream fixtures portable

Keep the checkout-status fixture aligned with the optional upstream field, and select main explicitly when cloning a bare test remote whose default HEAD varies by platform.

* fix(workspaces): preserve exact upstream refs

Qualified remote refs now fail closed when pruned instead of switching lineage. Metadata accepts Git-valid upstream names rather than applying a narrower character whitelist.

* fix(workspaces): preserve qualified local base refs

Local tracking branches are exact upstream identities too. Keep heuristics only for legacy unqualified metadata so origin cannot replace a stored refs/heads base.
2026-08-04 09:01:38 +02:00
Mohamed BoudraandGitHub 5c7623997c feat(hub): pass execution MCP servers to agents (#2813)
* feat(hub): pass execution MCP servers to agents

Hub-owned execution capabilities need to reach provider sessions without becoming part of outward agent snapshots. Keep the configuration daemon-private so provider recovery continues to work.

* fix(hub): protect daemon MCP namespace

Hub execution MCP configuration must not replace the daemon-owned Paseo server. Validate only after owned-record lookup so idempotent replay remains stable while new launches fail before provider or durable state creation.

* fix(agent): reject unsupported MCP launches

* fix(agent): scope MCP support guard

* fix(agent): redact MCP metadata from wire payloads

* fix(agent): validate MCP support on restored sessions

* fix(session): rehydrate MCP config on resume

* fix(session): resume newest matching agent record

* fix(session): restore archive state after failed resume
2026-08-03 13:49:53 +02:00
Mohamed BoudraandGitHub 04fe30a3e2 Improve mobile terminal input, selection, and resizing (#1607)
* Improve mobile terminal typing and selection

* Address mobile terminal review feedback

* Remove dead native terminal input branch

* Replay bracketed paste in terminal preamble

* Stop per-key terminal resize claims

* Stabilize terminal resize browser test

* Use tracked terminal mode for native paste

* Restore terminal toolbar focus handling

* Adapt native terminal to active visibility state

* Stabilize native mobile terminal interactions

* Add native terminal fallback boundary

* test(app): expect explicit terminal resize reclaim

* fix(app): stabilize terminal buffer coordinates

* fix(app): restrict terminal resize ownership

* fix(terminal): gate restored input modes

* fix(app): recover stalled terminal WebViews

* chore(app): remove obsolete terminal harnesses

* fix(app): make terminal resize claims explicit

* test(app): stabilize native terminal device flows

* test(app): track absolute scrollback coordinates

* fix(app): isolate terminal WebView streams

* fix(terminal): arbitrate size ownership across clients

The daemon now distinguishes explicit ownership claims from passive geometry updates. This keeps the active client authoritative while allowing its split, viewport, and keyboard changes to resize the PTY without idle clients stealing it.

* test(server): make worker resize assertion portable

Query the worker's authoritative terminal snapshot instead of invoking a Unix-only shell command, so the resize regression exercises the same boundary on Windows and Unix.
2026-08-03 19:01:34 +08:00
Mohamed BoudraandGitHub 553ac53643 feat(app): add chat outline navigation (#2792)
* feat(app): add chat outline navigation

Long agent sessions need prompt navigation without coupling viewport movement to disposable server windows. Keep fetched timeline pages in client state so repeated jumps stay local, and settle prompt jumps against virtualized row measurement changes.

* fix(app): preserve chat outline timeline coverage

* fix(app): reconcile outline window submissions

* fix(app): harden outline timeline navigation
2026-08-02 20:48:23 +08:00
Mohamed BoudraandGitHub 5a30affd7f Skip unrelated required checks without losing their names (#2542)
* fix(ci): skip unrelated required checks cleanly

Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable.

* fix(ci): gate changes to install patching script

* fix(ci): route cross-package test contracts narrowly

* fix(ci): keep routed contracts merge-blocking

* fix(server): exclude nested test utilities from builds

* fix(ci): cover app-owned native and browser domains

* refactor(ci): align tests with runtime domains

Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check.

* fix(ci): keep routing and desktop suites isolated

The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs.

* fix(ci): isolate desktop e2e runtime

* perf(test): isolate Playwright workers

* perf(test): parallelize Playwright within specs

* fix(test): keep Playwright file scheduling

* fix(test): share dynamic Metro fixture

* fix(test): preserve own-host restart fixtures

* fix(ci): preserve routed contracts after rebase

* Stabilize Playwright wheel-scroll assertions
2026-08-02 00:56:05 +08:00
76abc50f82 Fix/local branch worktree base (#2328)
* Add automatic plus branch maintenance workflow

* Build plus artifacts for desktop and CLI/server

* Document and summarize plus artifact workflow

* Document Apple signing and pin plus workflow actions

* Disable Linux plus desktop artifact

* Publish plus CLI packages to GitHub Packages

* Fix GitHub Packages publish paths

* Fix plus mise install summary package name

* Disable macOS x64 plus desktop artifact

* Tag plus npm packages as latest for mise

* fix(worktrees): respect selected local base ref

* chore(ci): remove fork-only plus workflow

* fix(worktrees): preserve exact branch picker refs

---------

Co-authored-by: Matt Cowger <1929548+mcowger@users.noreply.github.com>
2026-08-01 16:06:56 +00:00
Mohamed BoudraandGitHub 8021d2b6c9 Keep agent timelines stable from submission through resume (#2718)
* Reapply: Stop completed turns from appearing stuck (#2484)

* Reapply: Keep older chat history and image previews stable (#2490)

* Lock timeline layout stability with user-driven specs

Two reds reproduce the reported jank:
- new agent, first prompt: the row paints at top=100 then jumps to 144 through
  authoritative hydration, because the draft tab and the agent panel each render
  their own AgentStreamView for the same logical row.
- long loaded timeline, submit: scrollHeight collapses 10793 -> 9111 mid-turn,
  because splitOrderedTail reclassifies measured rows into the virtualized segment
  where the virtualizer falls back to estimateStreamItemHeight.

Also restores centered timeline rows: the pagination anchor wrapper introduced in
#2490 was a plain block div, which broke alignSelf: center.

* Measure the turn footer blink from when the footer appears

Bounding the window at the prompt row conflated two things: the footer arriving a
frame after the row, and the footer leaving the layout and coming back. Bounded from
the footer's first frame, the blink still reproduces, so the footer-ownership race is
real rather than a sampling artifact.

* Rename the shadowed prompt row binding in the frame recorder

* fix(desktop): allow concurrent worktree dev instances

Desktop terminals can inherit another instance's profile path and debugging flags. Scope the profile to the current dev root and select an available debugging port so instances do not share global runtime state.

* fix(desktop): expose development providers in dev builds

Desktop-managed daemons inherited no Paseo runtime mode, so dev builds constructed the production provider registry. Mark unpackaged internal launches as development while continuing to force packaged launches to production.

* fix(timeline): stabilize optimistic turn layout

Keep initial history loading dormant until user intent and preserve active-turn layout across delayed transport updates. Add painted-frame coverage for optimistic timeline transitions.

* fix(timeline): preserve optimistic prompts through hydration

Canonical replacement relied on a transport submission record that can settle before a provider echoes the user row. Keep presentation continuity owned by the unreconciled row while destructive history replacement remains governed by active transactions.

* fix(timeline): make submitted prompts canonical

Submission settlement previously depended on status updates that could arrive after the optimistic row. Persist every accepted prompt in the canonical timeline and keep the footer active until RPC and canonical acknowledgement converge.

* fix(timeline): stop older history pages repeating entries

Backward pagination counted source sequence rows, so page boundaries inside a merged projected item could return that item repeatedly. Count projected entries and advance from their display anchors instead, with server and browser regressions covering the contract.

* fix(timeline): resume agents from the latest history

Use a bounded tail probe on resume so long background runs appear atomically instead of replaying every missed page. Preserve unchanged views, reconcile local/live rows, and keep skipped history reachable through backward pagination.

* fix(timeline): reconcile turn state after missed updates

Turn completion events can be missed while an agent timeline is unsubscribed, leaving footer activity stale even after the directory reports idle. Reconcile the single-owned turn lifecycle from authoritative resume snapshots while preserving newer live evidence.

* test(timeline): cover running state across refresh

Protect the hydration boundary where directory state must restore consistent tab, composer, and footer activity. Expose the running indicator semantically so the regression checks user-visible state instead of spinner implementation details.

* fix(timeline): keep history loading geometry stable

Reserve a fixed history-start gutter regardless of pagination state so showing or hiding the loader cannot shift timeline content.

* fix(timeline): keep turn activity coherent

Derive footer, tab, and composer activity from one lifecycle and submission projection. Idle directory transitions now request an authoritative resume so missed terminal events converge without treating directory delivery order as turn authority.

* fix(tests): align checks with runtime boundaries

* fix(desktop): stop dev app surviving its terminal

Electron was detached from the desktop runner, allowing it to outlive a stopped workspace terminal with broken output pipes and retain the single-instance lock. Keep Electron owned by the runner and preserve explicit process-group cleanup for detached child trees.

* fix(timeline): preserve activity through turn handoff

Correlate lifecycle events and snapshots with daemon-owned turn identity, and keep optimistic submissions active until their canonical row arrives. This prevents stale updates and RPC timing from exposing a false idle state.

* refactor(timeline): remove submission timing state

* fix(timeline): restore prompt identity on resume

* fix(timeline): exclude attachments from canonical prompt text

Structured provider prompts can include hidden history attachments before the user's text. Persist only plain prompt blocks so authoritative user messages match what the composer displayed while provider input remains unchanged.

* refactor(agent): remove unused pending-run message identity

* fix(desktop): let Chromium allocate the debugging port

* refactor(agent): remove impossible finalized-turn tracking

* fix(timeline): load history after keyboard scrolling

Treat observed upward displacement as user intent so keyboard and scrollbar navigation can wake dormant pagination.

* test(server): preserve accepted turn stream order

The manager-owned accepted turn boundary intentionally precedes coalesced output. Assert that lifecycle event explicitly while retaining the timeline-before-terminal ordering contract.

* test(app): scope sidebar fixtures by identity

Shared E2E daemon state may contain unrelated projects. Keep stale-route coverage tied to its named missing project and drag only the two rows seeded by the reorder spec.

* fix(timeline): gate prompt revisions by client capability

* fix(timeline): finalize canonical prompt delivery

Treat provider identity as a single stored-row transition so legacy clients receive one deliverable canonical prompt. Release identity-pending rows before later foreground output or the current terminal while preserving stale-turn isolation.

* fix(server): preserve legacy prompt pagination

* fix(timeline): keep assistant block identity stable

Assign canonical block IDs during live promotion so terminal finalization cannot remount image-bearing rows.

* fix(timeline): apply covered revisions during resume append

Run the exact canonical revision pass before overlapping resume pages append newer rows, while leaving cursor advancement to the incremental path.

* fix(server): preserve queued agent transitions

Project each queued live snapshot before awaiting stored metadata so later lifecycle state cannot overwrite an earlier transition.

* fix(server): snapshot queued agent updates

Capture each live agent payload before it enters the per-agent queue so shared nested state cannot change while an earlier storage read is pending.

* docs(app): date agent turn compatibility cleanup

* refactor(timeline): mint submitted prompt identity at acceptance

Canonical prompt rows now keep one immutable wire identity while provider identity remains internal for rewind translation. This removes the unreleased same-sequence revision and legacy projection contract.

* refactor(app): remove prompt revision reconciliation

Submitted prompt rows are immutable on the wire, so resume and live reducers no longer need to apply already-covered same-sequence updates.

* test(app): cover submitted prompt rewind roundtrip

* docs(timeline): describe immutable submitted prompt identity

Provider echoes now enrich daemon-internal identity only; wire rows keep their acceptance-time identity and rewind performs the provider translation.

* test(server): validate mock rewind targets

Reject unknown provider message IDs so app rewind coverage exercises wire-to-provider identity translation.

* fix(server): validate rewind before interrupting

* fix(server): omit rewind identity for daemon prompts

* test(app): guard rewind eligibility and rejection

* fix(server): preserve prompt identity across hydration

* refactor(timeline): remove stale revision remnants

* Revert "fix(server): preserve prompt identity across hydration"

This reverts commit 6adcf7e5b0bc8601761af59b461a746cb5e395fa.

* fix(timeline): settle missed submission acknowledgements
2026-08-01 14:13:47 +02:00
Mohamed BoudraandGitHub 9d19e90323 Show worktree names in workspace hover cards (#2711)
* fix(app): show worktree names in workspace hover cards

Managed worktree paths exposed Paseo's internal directory layout instead of the useful worktree name. Carry the owned worktree root through the sidebar model while preserving the full workspace path as the copy target.

* fix(workspaces): source owned worktree names from daemon

The daemon owns worktree path semantics, so expose the managed worktree slug and let the sidebar model produce a display-ready label. This keeps path parsing out of the hover-card component while preserving the full path for copying.

* fix(app): preserve worktree labels in replica cache
2026-08-01 05:17:47 +08:00
Mohamed BoudraandGitHub d2de309fc3 Make relay opt-in when pairing devices (#2706)
* feat: make relay opt-in for device pairing

* fix(relay): keep pairing state authoritative

* fix(relay): refresh pairing state across transports

* fix(onboard): honor explicit relay opt-out

* fix(relay): surface transport startup failures

* fix(pairing): gate offers for older daemons

* test(cli): rely on real pairing transport coverage

* fix(cli): report live relay without a pid file

* fix(relay): synchronize config during daemon startup

* fix(relay): preserve mutability for ignored overrides

* fix(cli): use reachable daemon for pairing

* fix(app): redirect invalid pair device routes

* fix(pairing): support remote host device pairing

Keep Pair Device host-relative through the existing settings and daemon RPC flow. Route CLI output through an injected adapter so workflow tests do not mutate process globals.

* fix(pairing): validate pairing destinations

Expose the host-relative Pair Device flow on every supported app platform. Reject a reachable daemon whose identity does not match the requested Paseo home before reading or mutating its pairing state.

* fix(pairing): render QR codes across platforms

Generate SVG text without DOM or canvas and render it through the existing cross-platform SVG stack.

* fix(cli): pair from persisted relay state

Allow explicit relay pairing while the daemon is stopped when relay is already enabled in persisted configuration.
2026-08-01 04:35:06 +08:00
800bd2bef1 feat: add host-local custom project icons (#2335)
* feat(projects): add local icons and colors

* refactor(projects): simplify appearance handling

* fix(projects): serialize appearance saves

* test(app): update project appearance expectations

* test: fix project appearance CI expectations

* refactor(projects): organize appearance code

* fix(server): adopt main's reconcile model in project appearance updates

* fix(projects): preserve host-local appearance targets

* fix(desktop): restore Linux development startup

Ubuntu can block Electron's unprivileged user-namespace fallback while the npm-installed sandbox helper is not SUID. Disable the Chromium process sandbox only in the Linux development runner so packaged desktop policy remains unchanged.

* feat(projects): add custom project icons

Keep URL acquisition in the client so the host daemon only validates uploaded bytes. Project name and icon changes share one adaptive edit sheet.

* refactor(settings): scope projects by host

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-01 01:34:42 +08:00
706ae3d9bb Read Claude subagents from the SDK task protocol (#2498)
* refactor(claude): derive subagents from the SDK task protocol

Claude Code announces subagent identity and lifecycle on the SDK stream
(task_started, task_updated, task_notification). Paseo consumed only
task_notification and reconstructed the rest from sidechain frames, in two
independent derivations that had drifted apart.

Both paths now translate into one small observation vocabulary and fold it
with one function, so identity and status are derived once. The fold is
stateless: the task protocol is edge-triggered and the store's sticky merge
already accumulates.

- Replay links a subagent through the agent-<id>.meta.json sidecar, which
  carries the same Task tool_use id the live stream keys on. The agentId
  text scrape stays as a fallback for sessions recorded without it.
- The sidechain tracker keeps mapping frames to timeline items but no longer
  owns the descriptor when the task protocol is active, detected from the
  session rather than from a version string.
- Adds replay coverage, which did not exist.

Subagent ids are unchanged, so no protocol, store, or client change.

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

* fix(claude): keep non-subagent tasks out of the track, open the child timeline with its prompt

Backgrounded shell commands announce as task_started with a tool_use_id but
task_type local_bash and no subagent_type, so filtering on the id alone put
`sleep 20` in the subagents track. Discriminate on task_type, falling back to
subagent_type for releases that predate it.

task_started also carries the prompt. Emitting it as the child's first timeline
item means the pane no longer opens mid-conversation, and matches replay, where
the sidechain transcript already begins with the prompt.

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

* fix(claude): drop status for tasks that were never declared as subagents

task_notification also fires for backgrounded shell commands and carries a
tool_use_id. Routing status off that id readmitted exactly what the declaration
filter rejected, as a descriptor with a status and no identity — rendered in the
track as a nameless "Loading..." row.

Status now routes only through the task_id map populated at declaration.

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

* fix(app): name a subagent row after its task, not its type

The descriptor carries the subagent type in title and the task in description.
The client collapsed them and rendered only the type, so every child in a
fan-out read "general-purpose". The task is what tells siblings apart, so it
names the row and the type moves to the muted secondary line, which the track
computed but never drew.

Providers that report no task keep the previous behavior.

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

* fix(claude): label the parent's Task card from the subagent declaration

A backgrounded Task subagent emits no frames carrying parent_tool_use_id, so
the sidechain tracker never runs for one and never supplies the sub_agent
detail. Its card in the parent transcript rendered as a bare "Task".

The declaration already carries the type and the task, so the card is built
from it. For a foreground subagent the tracker still emits the same card
enriched with the child's action log; the two collapse on the shared tool-call
id, so that case is unchanged.

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

* feat(subagents): surface a subagent's model, effort, and cost

Adds model, effort and usage to the provider subagent descriptor, populated
from observations the child itself reports.

- model: read per assistant frame, live and on replay. Never snapshotted, since
  a provider can swap models mid-flight on overload or refusal fallback, and
  never inherited from the parent.
- effort: replay only. The SDK's assistant message does not carry it and it can
  be silently downgraded for the chosen model, so a live value would be a guess.
- usage: task_progress reports tokens and tool count once per tool use, and
  task_notification reports the terminal totals. This matters most for
  backgrounded subagents, which emit no sidechain frames and would otherwise
  show no sign of activity at all.

Status becomes optional on the upsert event: a report that only carries a model
or a cost says nothing about whether the child is still running, and must not
revert a finished one. Usage merges rather than replaces, because duration only
arrives at the end.

Fields are optional on the wire, so an older daemon simply omits them and the
client renders nothing rather than substituting the parent's settings.

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

* feat(claude): read subagent effort from hooks

Effort appears nowhere on the SDK message stream — verified by scanning every
message type at depth — and the level Paseo requests is not necessarily the one
that runs, since a model that does not support it is silently downgraded.

Hooks are the source. Every hook input carries agent_id and effort.level, the
active level for that turn after any downgrade, and agent_id is the same id
task_started calls task_id, so it routes through the table already built at
declaration. PreToolUse, PostToolUse and SubagentStop are registered purely to
observe; they record what they see and always return empty, so they cannot
affect tool execution or turn control.

Paseo's hooks merge with any the user configured rather than replacing them:
extraClaudeOptions is spread after the base options, so assigning would have
silently stopped effort being observed for anyone with their own hooks.

Verified end to end through ClaudeAgentClient against a real session: a
subagent descriptor now carries model, effort and usage together.

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

* feat(subagents): show a subagent's runtime facts in its pane, and its cost on replay

The track row carried model, effort and cost; the pane carried none of it, so
opening a subagent showed only its task.

The pane gets a header strip rather than a longer tab subtitle: the subtitle
feeds the width-constrained tab strip, whose job is to name the task. Duration
appears in the pane, which has room, and stays out of the dense row.

Replay had no usage source, so a reopened agent lost its subagents' token and
tool counts permanently — descriptors are in-memory and cleared when an agent
closes. Usage is now derived from the child's own transcript, matching the live
producer exactly: total tokens are the four counters summed on the last
assistant turn (a context reading, not a cumulative sum, which would multiply
the cached prefix by the turn count), and tool uses are tool_use blocks across
assistant entries. Duration is an approximation — the transcript's first entry
is written after the task starts — and is the one field replay cannot match.

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

* docs(claude): record the task protocol's undocumented wire facts

The subagent sources depend on Claude Code internals the SDK types do not
explain: which announced tasks are subagents, why a status for an undeclared
task is dropped, why task ids outlive a turn, why effort is only reachable
through hooks, and why a replayed session must skip its grandchildren.

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

* Generalize provider subagent presentation

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-31 17:22:44 +00:00
Mohamed Boudra 6fc491e622 chore(release): cut 0.2.5 2026-07-30 13:22:40 +02:00
Mohamed Boudra 2f913736fa chore(release): cut 0.2.4 2026-07-30 01:48:57 +02:00
Mohamed BoudraandGitHub 20e46e3e3f Restore project grouping across hosts (#2565)
* fix(projects): restore cross-host grouping

Random host-local project IDs replaced the remote-derived value that had also served as grouping identity. Persist a separate opaque group key and let normal boot reconciliation backfill older records without a migration.

* fix(projects): keep ambiguous clones separate

A shared remote can identify a project across hosts, but it cannot distinguish two independent clones on one host. Treat those keys as ambiguous so host-local project IDs remain separate.

* fix(projects): preserve host-local project identity

* fix(projects): strengthen grouping identity

* test(app): update project host shape contract

* fix(projects): preserve selected host identity

* fix(projects): close grouped routing gaps

* fix(projects): stabilize legacy subproject grouping

* fix(projects): preserve selected project identity

* fix(projects): isolate legacy host identity

* fix(projects): preserve symlinked bootstrap roots

* fix(projects): reconcile grouped custom names

* fix(projects): stabilize settings and path identity

* fix(projects): normalize default remote ports

* fix(projects): harden host and remote identity

* fix(projects): preserve host-scoped actions

* fix(projects): preserve remote path semantics

* fix(projects): model SCP remote semantics

* fix(projects): keep settings on routed host

* fix(projects): preserve SSH URL path semantics

* fix(projects): separate identity from presentation

* test(projects): use grouped identity in UI specs

* fix(projects): harden grouped project routing

* test(projects): expect framed settings route

* fix(projects): preserve exact placement identity

* fix(projects): harden grouping identity and routing

* fix(projects): restore project key as grouping identity

Random host-local project IDs had replaced the persisted cross-host value carried by projectKey. Keep projectId for routing while reconciliation owns and persists projectKey for grouping.

* fix(projects): prevent project key collisions

Preserve platform path semantics and generic remote transports in cross-host keys, and compare legacy checkout roots using filesystem equivalence before materializing ownership.

* test(projects): assert host-scoped project keys

* fix(projects): preserve project key compatibility

Resolve legacy workspace project references at the daemon boundary while keeping projectKey as the cross-host identity. Complete generic SSH normalization without collapsing distinct repositories.

* fix(projects): harden project reference identity

* fix(projects): retain archived reference errors

* fix(projects): scope legacy paths to hosts

* test(app): model persisted project keys

* fix(projects): retain absolute scp users

* fix(projects): resolve legacy mutation aliases

* fix(projects): preserve conservative identity boundaries

* fix(projects): preserve legacy placement identity

* test(projects): assert legacy placement identity

* test(projects): expect canonical Windows keys

* fix(projects): separate bootstrap identity fields

* fix(projects): scope legacy paths by host

* fix(projects): stabilize opaque project identity

* fix(projects): preserve remaining opaque identities

* fix(projects): classify forward-slash UNC roots

* fix(projects): preserve opaque selection keys

* fix(projects): frame host-local project keys

* fix(projects): canonicalize framed identities

* test(projects): isolate repeated remote fixtures

* fix(projects): keep project key as UI identity

* fix(projects): reconcile hydrated project references

* fix(projects): preserve archived project identity

* refactor(projects): group projects from project records, not workspaces

Cross-host grouping had been built by hanging a projectKey off every
workspace descriptor. That scoped the key to the wrong entity: the app
then had to reconstruct project identity per workspace in six modules,
which forced projectKey fallbacks into RPC arguments and a server-side
resolver that turned grouping keys back into project ids.

A workspace has a projectId. The key belongs to the project, and
grouping is a display concern, so the daemon now publishes projects
over project.list plus the existing project.update channel and the app
groups them in one place. Projects are addressed by host-local
projectId everywhere, including the settings route.

Projects persisted without a key are backfilled by ordinary boot
reconciliation, with no migration. Old daemons advertise no projectList
capability and simply do not group.

* test(projects): seed projects alongside workspaces in fixtures

Sidebar fixtures seeded workspaces without their projects, which the
daemon cannot produce now that projects arrive on their own channel, so
nothing rendered. Adds a seeding helper that supplies both.

Also drops the provisioning test for accepting a project key where a
project id belongs; that resolver was removed deliberately.

* fix(projects): keep derived project identity stable

Four gaps left by shrinking the key derivation:

Local keys used a bare path resolve, so one directory reached by a
different spelling produced a different key and reset grouped identity.
Windows CI caught this with the 8.3 and long-name forms of one temp
path. Canonicalize through the shared realpath-aware normalization.

Subproject display names lost their selected-path input, so every
subproject of a monorepo rendered under the same remote-derived name.

Reconciliation only ever backfilled a missing key, so a project whose
remote changed stayed grouped with hosts on the old remote.

Project settings kept a disconnected host selected and rendered a dead
end with no way back to an editable host.

* fix(projects): read scheme remotes as URLs, not scp form

An `ssh://user@host:22/org/repo` remote matched the scp-form pattern
first, because `[^@]+` happily eats `ssh://git`, so the port landed in
the path and the project keyed as `remote:host/22/org/repo`. Two
spellings of one remote therefore never grouped.

Try scp form only when there is no scheme, and drop protocol-default
ports so an explicitly written `:22` matches the same remote without it.
2026-07-29 23:36:31 +02:00
9e5accee3c fix: reject oversized checkout diffs (#2488)
* fix: reject oversized checkout diffs

* fix(diff): keep large previews from disconnecting clients

The physical socket limit is an OOM backstop, not a frame-size contract. Bound structured diff construction incrementally and report oversized previews without closing the client's socket.

* fix(app): preserve oversized diff state

* fix(diff): stay within relay frame limit

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-30 00:48:05 +08:00