Commit Graph
5390 Commits
Author SHA1 Message Date
Mohamed Boudra 6830c46e9a fix(app): keep composer layout stable across chat and workspace screens (#4973)
* fix(app): dock the New workspace composer like a chat on phones

Only the composer is bounded by the viewport; the setup fields sit above it in a
bottom-anchored stack that moves up as the draft grows. The keyboard reservation
is retained on every screen, so the composer keeps one height across keyboard
transitions. Removes the per-screen reserve policy.

* fix(app): share composer docking across mobile hosts

Extract chat's retained capacity and platform keyboard motion so chat,
workspace drafts, and New workspace share one sizing and dismissal boundary.
Keep the native drivers and settled portal reconciliation unchanged.

* fix(app): preserve overlays in centered composer docks

Honor the optional overlay child in both layouts so changing dock placement does not silently discard part of its content.

* test(app): resolve keyboard controller through its web entry

The shared keyboard entry point makes stream tests load the provider. Keep its real ESM dependencies inside the Vitest resolver so CommonJS does not bypass the React Native web alias and parse native Flow sources.

* fix(app): keep native chat scrolling responsive

Place keyboard dismissal behind scrollable content so an idle Android Fabric JS responder cannot intercept native drags. Keep decorative form padding transparent to iOS hit testing and exercise idle native scrolling after submission from every composer host.
2026-09-16 23:43:15 +02:00
Mohamed Boudra c7db3c5a8c Let plugins open external links and workspace browsers (#4972)
* feat(plugins): open external links and workspace browsers

* fix(plugins): refuse invalid links and unavailable browser workspaces

* docs(plugins): clarify external links and browser navigation
2026-09-16 23:25:17 +02:00
Mohamed Boudra a8a8da0472 fix(plugins): preserve nested provider subagent links (#4970)
* fix(plugins): preserve nested provider subagent links

The plugin adapter routed descendants to their managed root but discarded their direct parent when publishing descriptors. Preserve runtime-to-public child identities and forward optional spawning tool IDs.

* test(plugins): separate opening and live subagent scenarios

* test(server): retry terminal fixture directory cleanup

Match the daemon harness cleanup policy so transient Windows EBUSY locks do not fail terminal ownership tests after their assertions complete.
2026-09-16 22:50:58 +02:00
Mohamed Boudra 6ce6f4c961 Let plugins discover hosts and use host-targeted SDK clients (#4971)
* feat(plugins): expose hosts and host-targeted SDK clients

* test(plugins): verify exact cross-host observation cleanup

* fix(plugins): reacquire explicitly disposed host clients

* docs(plugins): clarify host client usage and lifecycle

* docs(plugins): use theme colors in host discovery example
2026-09-16 22:45:18 +02:00
paseo-bot[bot] 72e3d6957a Let plugins navigate across hosts (#4942)
* feat(plugin): support cross-host navigation

Keep route construction inside Paseo while letting plugin surfaces target a host other than the currently selected one. Calls without a server ID retain selected-host behavior.

* test(plugin): cover cross-host navigation

Exercise workspace and agent navigation against a second live daemon so the SDK contract is verified through the caller-visible route instead of mocked helper forwarding.

* test(plugin): clarify cross-host navigation journey

Keep the scenario at the caller-visible intent level while shared helpers own route waits and visibility mechanics.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-09-16 20:27:15 +02:00
Mohamed Boudra 46529146ac Show pending file attachments and keep uploads responsive (#4958)
Show a spinner attachment while a selected file is read and uploaded, yield between 128 KiB chunks so the UI stays responsive, validate every file in a batch before sending any of them, and leave one SelectedFile shape across the picker, drag-and-drop, and the upload path.
2026-09-16 20:06:02 +02:00
Mohamed Boudra 4f710d9930 feat(app): auto-open the pull request tab once when a PR is detected (#4956)
* feat(app): auto-open the pull request tab once when a PR is detected

* docs(app): clarify automatic pull request tab policy
2026-09-16 18:28:14 +02:00
Mohamed Boudra 0f20e6dfe4 Keep History search chronological and make matches clear (#4945)
* fix(history): preserve chronology and clarify search matches

Keep workspace names primary and highlight each searched field so results explain why they matched. Filter before cursor pagination and retain date buckets.

Constrain shared subsequence matching to each whitespace-delimited word while preserving fuzzy abbreviations in Command Center and other consumers.

* test(history): reuse response helper in pagination regression

* test(history): express browser journey through named helpers

* fix(app): start menu entering animation only once the surface is placed

Reanimated's web entering path snapshots the element rect at animationend and
writes top/left back when it cleans up the keyframe 750ms later. When the
150ms animation ends before anchor and content measurement resolve, that
snapshot is the off-screen measuring position and the open menu jumps back to
-9999. Remount the surface when its position resolves so the animation starts
from the final position.
2026-09-16 16:17:46 +02:00
Mohamed Boudra 0e8a71a5fe fix(app): stabilize native composer sizing after edits (#4946)
* fix(app): stabilize native composer sizing after edits

* test(app): verify workspace composer space after keyboard dismissal

* test(app): start the hold-to-delete regression mid-draft

From the end of the draft the pre-fix input also returns to baseline. Placing the cursor
eight lines into the draft before the first hold-delete leaves the old build at the
capped height with the placeholder visible, and the fixed build at one line.
2026-09-16 15:51:22 +02:00
Mohamed Boudra a78949d1d4 Preserve committed changes when restoring archived workspaces (#4926)
* fix(server): preserve comparison base when restoring worktrees

Restore archived worktrees from the workspace's saved comparison base instead of using the feature branch as its own base. Preserve the branch HEAD and allow restore when an exact comparison ref no longer exists.

* test(app): isolate draft preference checks from menu animation
2026-09-16 14:13:14 +02:00
Mohamed Boudra 64b1a62ed3 Fix voice reply latency and hide internal prompt wrappers (#4927)
* fix(voice): speed up replies and hide prompt wrappers

Keep voice prompt cleanup in the client presentation path so provider history and live messages render consistently without rewriting canonical input. Correct native TTS scheduling and threading, and reconnect interruption to speech detection and session cancellation.

* test(voice): clarify playback and timeline regression journeys

* test(voice): centralize native worker cleanup and repair fixture path

* test(app): exercise workspace eviction without reloading the app

* test(app): preserve streamed output during workspace eviction

Hold the turn until the workspace is evicted instead of dropping all stream events during navigation. Slow CI runners can finish the entire response before navigation ends; waiting for producer completion before release makes that ordering deterministic.

* test(app): stabilize subscriptions during workspace eviction

Opening another chat replaces the timeline subscription, making buffered frames from earlier generations unroutable. Preopen all chats before holding the stream, finish production before eviction, and assert both early and final response content with no subscription replacement.
2026-09-16 13:36:44 +02:00
0eac75be7d fix(server): stop an expired application lease from permanently killing a plugin session (#4912)
* fix(server): keep plugin sockets out of the application socket lease

A plugin's IPC socket is claimed by the application socket lease as soon as
its client sends its first ping. If that client's heartbeat is then starved
for longer than APPLICATION_SOCKET_LEASE_MS — a blocked event loop, a
SIGSTOP, a sleeping machine — the daemon reaps the socket with "Closing
physical WebSocket with expired application lease".

The plugin never comes back. plugin-process.ts builds its DaemonClient with
`reconnect: { enabled: false }`, so unlike an app or menubar client it cannot
redial, and there is no "Client disconnected; waiting for reconnect" because
no reconnect is ever scheduled. Every later `paseo.*` host API call then
fails with "Transport not connected (status: disconnected)" while
`paseo plugin ls` still reports the plugin as running. Only
`paseo plugin reload <id>` recovers it.

The lease is a backstop for an abandoned application socket. A plugin socket
is IPC to a child this daemon spawned and already supervises through the
child's own close, so heartbeat silence there is not evidence of abandonment
— it only means the child is busy or paused. Reaping it strands a plugin that
was about to resume, which is strictly worse than leaving it attached.

Ordinary sockets are unaffected and still reaped on lease expiry; the second
test pins that down so the exemption cannot widen unnoticed.

Fixes #4883

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(server): let a plugin session recover when the daemon closes its socket

The lease exemption stops the common cause, but any daemon-side close still
stranded a plugin for good: the child's DaemonClient was built with
`reconnect: { enabled: false }`, and even with it enabled the child had
nothing to redial onto, because the runtime attached exactly one
PluginSessionSocket per load and routed frames to that captured const.

Enable reconnect on the plugin client, and have the runtime stand up a
replacement session when a socket closes underneath a live child. Frames now
follow the current socket, and `loaded.sessionSocket` / `sessionClosed` track
the replacement so stop and teardown still await the right attachment. The
session host already supported this — attaching a second socket for the same
plugin id gives it a second, independently owned session.

A plugin on its way out must not be resurrected. Both stop paths
(`stopPluginById`, `stopAll`) clear the catalog entry before they close
anything, so the catalog check is what distinguishes a shutdown from a socket
worth replacing; a dead child is caught by `child.connected`.

The client waits DEFAULT_RECONNECT_BASE_DELAY_MS (1500ms) before redialling
and gives up after DEFAULT_CONNECT_TIMEOUT_MS, so the replacement is attached
long before the first attempt arrives, and a lost race would retry anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(server): trim the comments on the plugin session fix

The reasoning lives in the commit messages and the PR. Leave only what the
code cannot say for itself: why a plugin socket is not an application socket,
and why the catalog check is what distinguishes a shutdown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(server): re-attach a plugin session on the child's redial, not on close

Attaching eagerly when the socket closed put a socket in place that nobody
was speaking to yet. Two ways that goes wrong, both reachable under exactly
the conditions #4883 describes - a child whose event loop is starved:

- Nothing sends a hello on the replacement, so the host closes it after
  HELLO_TIMEOUT_MS (websocket-server.ts:1310), and that close attached
  another. A wedged child drives that cycle until it exits, filling the
  plugin's 500-entry log ring with re-attach lines.
- Frames the child had already queued arrive after the close and land on the
  replacement while it is still pending, which closes it with
  WS_CLOSE_INVALID_HELLO (websocket-server.ts:2151) and attaches another.

Route frames through the current socket instead, and stand a replacement up
only when a hello arrives while the old one is closed - the one point that
can tell leftovers of the dead session from a genuine redial, since the IPC
channel carries no session id. A starved child now attaches nothing until it
wakes up and redials.

Also re-check the guards after the attach await and close the replacement if
they no longer hold, and surface a failed re-attach through appendLog and
notify rather than only a daemon log line, so a plugin left without a session
is visible instead of silently repeating the original bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(server): follow the server style guide and drop test timing

Two review findings against packages/server/CLAUDE.md.

Helper shape (CLAUDE.md: `function` declarations over arrow assignments,
object parameters when a function takes more than one argument). The
re-attach helpers were arrow assignments closing over `loadPlugin` locals, and
two took `(frame, isBinary)` positionally. They are now private methods taking
a single object. The mutable session state they shared - the current socket,
the in-flight flag, the loaded plugin - moves into a PluginSessionBinding the
methods read, which is what made the closures hard to lift out before.

Test determinism (CLAUDE.md: no timing/randomness). The reconnect tests waited
on fixed 500ms sleeps and 15s vi.waitFor polling windows, so they inferred
lifecycle state from elapsed time. The tracked session host now exposes
waitForHellos, resolving on the handshake itself, and the lazy-attach case
runs against a stub child that is driven frame by frame: close, then a stale
frame that must not attach, then a hello that must. No sleeps and no polling
remain in these tests, and the file's suite drops from ~40s to ~11s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(plugins): verify host API and observation recovery

* test(plugins): await connection and catalog events

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-09-16 00:48:18 +02:00
Mohamed Boudra 7557992dff fix(app): stabilize composer height and isolate typing updates (#4902)
* fix(app): preserve composer capacity when the keyboard closes

Releasing the keyboard reservation enlarged an unchanged capped draft. Retain the last editing reservation while still recalculating capacity for viewport changes and subsequent keyboard openings.

* perf(app): isolate composer editing and resize updates

Draft and cursor subscriptions rerendered the chat and full composer on every native edit. Give editing consumers a live text source, preserve attachment identity on text updates, and read viewport settling state without a render.

Read live input text for web resize measurement so removing per-keystroke props does not collapse a draft when its width changes.

* fix(app): restore current draft when the composer remounts

The last replacement command does not include subsequent edits. Seed remounted editors from the live draft snapshot so switching launch modes restores the saved text without subscribing the composer to every edit.

* fix(app): synchronize composer resolution state before paint

Avoid an extra passive-effect delay when forwarding forge resolution state from the isolated binding. Exercise live-text width measurement through the actual composer E2E journey instead of a hook-only test component.
2026-09-16 00:05:19 +02:00
Mohamed Boudra 4251575950 Keep reconnect toast transitions continuous (#4925)
* fix(app): keep reconnect toast transitions continuous

Preserve the toast surface when its content changes, and apply initial appearance before mounting screens so settings hydration cannot restart a visible toast.

* test(app): use supported appearance QA coverage
2026-09-15 23:30:52 +02:00
Aditya BorakatiandABorakati f9fb992dcb fix(plugins): use spawnProcess for plugin build commands on Windows (#4776)
runPluginBuild called node:child_process spawn with shell: false, so bare
commands like `npm` failed on Windows with `spawn npm ENOENT` because npm
is a .cmd script. Route through the shared spawnProcess helper, which
already selects shell: true for bare commands and .cmd/.bat scripts on
Windows and quotes arguments for cmd.exe.

Side effects inherited from spawnProcess, on every platform:
- Child env is sanitized via createExternalCommandProcessEnv, stripping
  ELECTRON_RUN_AS_NODE, ESBUILD_BINARY_PATH and PASEO_* control keys that
  previously leaked from the daemon into plugin builds.
- windowsHide: true, so builds no longer flash a console window on Windows.

Adds behavioral tests for preparation.ts. Windows cases use it.runIf and
run only on Windows hosts; CI is ubuntu-only.

Co-authored-by: ABorakati <ABorakati@users.noreply.github.com>
2026-09-15 22:51:23 +02:00
Mohamed Boudra 5ae0c5b83b fix(client): preserve workspace and agent creation on older hosts (#4895)
* fix(client): preserve creation on older hosts

Creation keys must not disable workspace and agent creation when the host lacks request receipts. Keep capability selection inside the client and degrade to the original RPCs with the initial prompt intact.

* test(creation): own legacy daemon cleanup in fixtures

* fix(client): preserve original prompted creation on legacy hosts

Legacy receipt support does not preserve create-and-prompt semantics when split into a later send. Reuse the original creation RPC so naming, creation context and startup receive the same input as before.
2026-09-15 22:49:06 +02:00
Lukas BuckandMohamed Boudra 4be34f05ef fix(plugin): continue one timeline item across ACP chunks without messageId (#4701)
* fix(plugin): continue one timeline item across ACP chunks without messageId

The plugin ACP reducer minted a new fallback id for every
agent_message_chunk / agent_thought_chunk that carried no messageId, so
a streamed reply rendered as one block per chunk, splitting words and
Markdown delimiters (#4699). Keep one fallback id per chunk kind and end
it on a chunk of the other kind, a tool call, a plan, a user message, or
a turn boundary, matching the server-side ACP provider.

* fix(plugin): end the other chunk kind's item on any chunk

A chunk of one kind now ends the other kind's fallback item before the
content or id is looked at, so an explicit-id text chunk between two
id-less thoughts yields three items instead of merging the thoughts.
The test declares the streamed updates through the connector harness
instead of handling protocol frames itself.

* test(plugin): verify ACP message boundaries and rendered Markdown

* test(app): synchronize rewind setup and menu selection

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-09-15 21:25:27 +02:00
paseo-ai[bot] 7d74916f8e fix: update lockfile signatures and Nix hash [skip ci] 2026-09-15 16:06:06 +00:00
Mohamed Boudra 80b4387bc6 Find text throughout chat history (#4765)
* feat(app): add pane Find to file source

* fix(app): float the pane Find widget and tighten its chrome

Find pushed the first lines of the file down, ran 480px wide with 32px
ghost buttons, and let the replacement input start left of the query
input. Float the CodeMirror top panel out of flow so the widget overlays
the top right of the pane, move the disclosure toggle into a gutter that
spans both rows so the two fields share a leading rail, put the match
count inside the query field, and drop the icon controls to the shared
pane-toolbar chrome the file bar above already uses.

* fix(app): keep the active match visible and drop the Find button

The floating widget covered the active match whenever the match sat under
it and the document was too short to scroll clear. CodeMirror's own panel
scroll margin cannot reveal a first line, so the file integration now owns
placement: the widget renders as an overlay beside the editor, the model
measures it against the active match, and flips it to the opposite corner
only when the current corner collides. Hysteresis keeps navigation from
bouncing the widget between corners.

Also removes the standalone Find button shown while Find is closed, per
user direction. Cmd/Ctrl+F is the entry point.

* fix(app): stop the Find placeholder from hiding Go to line

Hiding `.cm-panels` suppressed every CodeMirror panel in the file editor,
including the Go to line dialog that Mod-Alt-g opens through the same
search keymap. Hide only the inert Find placeholder panel instead, by its
own class, and leave the shared panel container alone.

* feat(app): find text throughout chat history

Discover candidate transcript locations on the daemon, then reuse prompt navigation to load history and verify rendered occurrences on the client. Keep search state inside its own modules without changing timeline or task presentation contracts.

* fix(app): show actionable chat search failures

Keep transport error details in the search model while showing localized retry guidance. Use named client request options for timeline search.
2026-09-15 17:54:01 +02:00
Mohamed Boudra 326a37cc89 Find text in terminal scrollback (#4650)
* feat(app): add pane Find to file source

* fix(app): float the pane Find widget and tighten its chrome

Find pushed the first lines of the file down, ran 480px wide with 32px
ghost buttons, and let the replacement input start left of the query
input. Float the CodeMirror top panel out of flow so the widget overlays
the top right of the pane, move the disclosure toggle into a gutter that
spans both rows so the two fields share a leading rail, put the match
count inside the query field, and drop the icon controls to the shared
pane-toolbar chrome the file bar above already uses.

* fix(app): keep the active match visible and drop the Find button

The floating widget covered the active match whenever the match sat under
it and the document was too short to scroll clear. CodeMirror's own panel
scroll margin cannot reveal a first line, so the file integration now owns
placement: the widget renders as an overlay beside the editor, the model
measures it against the active match, and flips it to the opposite corner
only when the current corner collides. Hysteresis keeps navigation from
bouncing the widget between corners.

Also removes the standalone Find button shown while Find is closed, per
user direction. Cmd/Ctrl+F is the entry point.

* fix(app): stop the Find placeholder from hiding Go to line

Hiding `.cm-panels` suppressed every CodeMirror panel in the file editor,
including the Go to line dialog that Mod-Alt-g opens through the same
search keymap. Hide only the inert Find placeholder panel instead, by its
own class, and leave the shared panel container alone.

* feat(app): find text in terminal scrollback

* test(app): account for terminal write completion before barriers

* fix(terminal): only Cmd+F opens Find on macOS; Ctrl+F reaches the shell

* test(app): exercise terminal find shortcuts against real ptys

* test(app): wait for visible vim lines before terminal page down

* test(app): account for vim page overlap in terminal shortcut journey
2026-09-15 17:46:42 +02:00
Mohamed Boudra 26e0273acb Add pane Find to editable and read-only file source (#4589)
* feat(app): add pane Find to file source

* fix(app): float the pane Find widget and tighten its chrome

Find pushed the first lines of the file down, ran 480px wide with 32px
ghost buttons, and let the replacement input start left of the query
input. Float the CodeMirror top panel out of flow so the widget overlays
the top right of the pane, move the disclosure toggle into a gutter that
spans both rows so the two fields share a leading rail, put the match
count inside the query field, and drop the icon controls to the shared
pane-toolbar chrome the file bar above already uses.

* fix(app): keep the active match visible and drop the Find button

The floating widget covered the active match whenever the match sat under
it and the document was too short to scroll clear. CodeMirror's own panel
scroll margin cannot reveal a first line, so the file integration now owns
placement: the widget renders as an overlay beside the editor, the model
measures it against the active match, and flips it to the opposite corner
only when the current corner collides. Hysteresis keeps navigation from
bouncing the widget between corners.

Also removes the standalone Find button shown while Find is closed, per
user direction. Cmd/Ctrl+F is the entry point.

* fix(app): stop the Find placeholder from hiding Go to line

Hiding `.cm-panels` suppressed every CodeMirror panel in the file editor,
including the Go to line dialog that Mod-Alt-g opens through the same
search keymap. Hide only the inert Find placeholder panel instead, by its
own class, and leave the shared panel container alone.
2026-09-15 17:20:27 +02:00
Mohamed Boudra cb90806045 fix(app): soften the mobile diff tree button and remove row shadows (#4898)
Use a neutral surface for diff navigation and restrict Android elevation to document headers so shared tree rows stay flat after scrolling.
2026-09-15 17:06:21 +02:00
Mohamed Boudra d8dd189b94 ci: simplify and de-flake the test suite (#4868)
* chore(ci): audit packaging costs and repeated test execution

Read-only audit: Linux packaging and smokes consume 12-14 minutes; all three CLI shards repeat the 294-test source suite and build the server twice. Preserve the 17 required contexts, move packaging to main, run CLI source tests once, and consolidate repeated E2E setup into journeys. Historical logs and the complete audit were reported outside the repository.

* ci: move packaging to main and remove repeated CLI suites

PR runners were rebuilding distribution artifacts and executing the entire CLI source suite on all three E2E shards. Keep packaging coverage after merge, let the local E2E runner own its dependency build, and execute source tests once in an existing required check. Root typecheck already validates plugin examples.

* test(ci): consolidate journeys and remove repeated startup work

Run user journeys with shared setup, retain one Hub CLI contract, and weight the existing Playwright shards using observed costs. Remove implementation-only and ad-hoc measurement checks. Await root-watch closure and publish creation failure commands atomically so teardown and failure injection have completion boundaries.

* test(hub): remove incidental observation and speculative shutdown changes

Hub control RPCs need a connected client without an agent observation. Preserve the failed-create subscription contract and remove product shutdown changes whose Windows cleanup cause was not reproduced.

* fix(client): clear the scheduled retry when a connect attempt opens a transport

An explicit connect() during reconnect backoff opened a new transport but
left the scheduled retry armed. When it fired on the healthy connection it
disposed the transport and reconnected, dropping requests in flight. This
is why owned-subscriptions-old-daemon could still fail after #4858 added
the explicit reconnect. attemptConnect now owns the rule ensureConnected
already applied; the new unit test fails on the old client.

* test(e2e): keep the completed-block, font-readiness, and live-page guards inside journeys

- agent-stream-ui: the auto-scroll journey anchors on the completed intro
  block by content and requires its Markdown root to stay the same mounted
  node while later blocks stream (#1989 guard, reshaped off the tail block).
- changes-pane: the code-typography journey holds font loads and requires
  no diff geometry before they resolve, then applies the font change.
- agent-timeline-pagination: the live-output page load runs against a
  thirty-minute mock stream and gates the release on the running turn, so
  the status assertion no longer races a ten-second turn.

* test(desktop): install the Electron binary once before the unit suite

Electron 44 downloads its binary on the first Node require("electron")
instead of at npm ci. Vitest workers importing electron-log/main raced
that download and extraction (File exists on dist/electron, then
"Electron failed to install correctly" in a sibling worker) on both
Ubuntu and Windows desktop-tests jobs. Run the package's idempotent
install-electron in one process before vitest forks.

* ci(desktop-packages): pin actions and smoke packaging-path pull requests

Pin checkout, setup-node, and upload-artifact to full commit SHAs. Keep
packaging off ordinary PR CI, but run the Linux package smoke on pull
requests that touch packages/desktop, nix, or the workflow so a packaging
or sandbox regression cannot land on main unchecked. The desktop release
workflow already builds and smokes its own artifacts on all platforms.
2026-09-15 13:26:41 +02:00
Mohamed Boudra 0474c3e0af fix(app): stop the streaming Markdown spec at a word boundary (#4875)
The mock provider splits "**Bold" into two frames, "**Bo" and "ld". The
helper forwarded both back-to-back and asserted the strong span reads
"Bold", which only holds when the browser commits both frames to the
store in one reducer flush and the message first mounts with "**Bold".
When a frame boundary lands between the two tasks the message mounts
with "**Bo" and word pacing holds "ld" until the whitespace after it
arrives, so the span reads "Bo" until the gate releases. That is the
designed behaviour of the reveal, and the batching is up to the
browser's task scheduling.

Stop after "**Bold text" instead: the space after "Bold" ends the word,
so every batching reveals "Bold" and never more than "text". Forcing the
frames into separate commits reproduced the CI failure before the change
and passes after it.
2026-09-15 04:45:17 +02:00
paseo-ai[bot] c5c0536b46 fix: update lockfile signatures and Nix hash [skip ci] 2026-09-15 02:39:32 +00:00
Mohamed Boudra b21c004ffe Keep open chats current and show reconnection progress (#4863)
* fix(app): keep open chats current and show resume progress

Keep open-chat delivery independent of mounted views, freeze hidden chat rendering, and catch up visible chats before hidden ones. Distinguish host reconnection from timeline freshness so cached messages do not silently appear current after resume.

* fix(app): prevent sync status from overlapping action errors

Share the existing panel toast slot so action errors remain readable during reconnect and catch-up. Verify the interaction and draft retention in the real browser, preserve the tested freeze transition, and refresh the Nix dependency hash for react-freeze.

* test(hub): expose complete creation and message retry failures

Partial response assertions hid the creation error and allowed duplicate responses with incomplete agent data. Check complete success envelopes, the shared agent snapshot, and null message errors so future failures print the actual error payload.

* fix(app): dismiss retained chat menus before freezing

An async fork navigates before its closing state commits, so freezing the source chat could leave its portal backdrop blocking the destination. Remove the shared menu surface in the inactive commit before suspension.

Keep regression assertions aligned with open-chat subscriptions, per-agent incremental recovery, plugin contribution lifetimes, and rendered image completion.

* test(app): expect the reloaded plugin to contribute its pill again

A full reload discards the pressed pill with the page, then evaluates the plugin again and its agents snapshot re-adds the pill. The old count-zero assertion only passed on main because the chat was not ready yet when the URL settled; open chats now subscribe from session start, so the pill is rendered by then.

* fix(app): suspend hidden chats on web only

react-freeze is Suspense with a never-resolving thenable. Suspending a committed subtree hides its host nodes, tears down layout effects, and detaches refs, which changes Fabric ownership of the retained chat. Split the boundary by platform: the web file keeps the two-commit freeze, the native file keeps the chat mounted and live, and the retained-panel signal remains the only thing that stops background work. Restore the mobile-panels rule the branch had rewritten.

* fix(creation): never read a receipt while replacing it

Admission reads the creation receipt while the runner replaces it atomically. On Windows a rename over a file this process still has open for reading fails with EPERM, and that error marked an otherwise completed creation as failed (#4865). Queue reads and writes per receipt so one owner never overlaps them, and serialize the record when the write is queued because the runner mutates it afterwards. No retry on EPERM: the conflict is removed rather than tolerated.

Linux cannot reproduce the rename failure, so the Windows server-tests job (creation/index.test.ts and hub/execution-session.websocket.test.ts) is the verification for this change.
2026-09-15 04:30:25 +02:00
Mohamed Boudra 6215e08efe Smooth streaming replies with a continuous left-to-right fade (#4859)
* feat(app): reveal streaming text as one continuous fade front

Streaming assistant text is released by word and every character fades in
over the same 150 ms envelope from a start time owned by the message. Start
times never decrease in reading order, so a line paints as a single front
moving left to right, however the text is chunked. Per-word envelopes with an
internal stagger let the next word's first letter overtake the previous word's
last letter, which read as holes and flashing words at the real frame rate;
100 ms frame strips hid it.

Native renderers receive the fade ranges as a prop on a host view wrapping the
root Text, so ranges and text land in the same Fabric mount transaction. The
earlier binding sent ranges through a separate async module call keyed by view
tag and had to reconcile them against the view's text; on a fast phone the UI
thread lags JS by several commits, mounts and calls interleave, and every
reconciliation heuristic guessed wrong. Native code no longer infers newness
or creates start times.

The mock load-test provider now separates cycles with a blank line so a cycle
heading is not appended to the previous paragraph.

* fix(ci): refresh streaming dependencies and isolate E2E setup

* test(app): distinguish fade cleanup from Markdown replacement

* test(app): detect restarted fades during Markdown updates

* docs(app): keep streaming guidance focused on constraints and QA

* test(app): control animation time in Android fade assertions

Fixed wall-clock sleeps let emulator scheduling select the sampled animation frame. Inject the animator time source and drive paint at exact timestamps within one UI-thread operation; production keeps the system clock. Verify the mid-fade sample and the exact cleanup boundary without sleeps, polling, or retries.

* fix(test): compile the desktop renderer before lifecycle checks

The lifecycle fixture treated Metro /status as app readiness. Its first
Settings click therefore spent its 90-second UI budget compiling the cold
bundle. An isolated cold-cache run restricted to one CPU reproduced the
click timeout before the app runtime had loaded.

Reuse the browser suite's existing bundle warmup before launching Electron,
and preserve Metro output in the lifecycle artifacts. The UI timeout,
build-request timeout, lifecycle assertions and product behavior are unchanged.

CI did not preserve Metro output, so its precise startup timing is not
recoverable from the failed run. The retained log makes subsequent startup
failures diagnosable without relying only on an empty renderer text dump.

Validation: three complete lifecycle E2E runs under private Xvfb displays;
existing Metro readiness tests; app typecheck; targeted lint; root format.

* test(file-observer): await burst delivery through native recovery

The deletion phase required every event within 10 seconds, but native
coalescing recovery uses a 30-second safety audit followed by the existing
reconciliation debounce. An idle diagnostic excludes that delayed audit
and therefore cannot act as a completion barrier.

Await the required delivered events instead, preserving every deletion
assertion and the existing overall test watchdog. A real-filesystem test
with controlled native notifications reproduces 95 outstanding paths at
the old deadline and verifies completion when the safety audit delivers
them. No production observer behavior changes.

* test(file-observer): verify burst recovery through public events

The controlled-clock regression constructed a private native backend,
bypassing the interface used by consumers. Consolidate recovery coverage
into the public observer integration test instead: drive real writes,
deletes and directory renames, then await every required public event,
including the 450 surviving files at their renamed paths.

No public capability was added: subscribe callbacks already express
completion. No production behavior or test timeout changes.

* test(app): drive Android fade tests through the native host

The instrumentation suite constructed TailFadeInAnimator directly, advanced
its private clock, and counted FadeInSpan instances. It now mounts the
WordFadeHost that React mounts, feeds it the ranges prop the bridge sends,
and asserts rendered pixels: hidden before a start time, a monotone front
inside and across words, settled frames equal to the baseline, and no frame
callbacks left after settling. Span counts and internal timing state are no
longer asserted.

Choreographer cannot be advanced deterministically from a test and the fade
epoch is the message's wall-clock start time, so the host takes a
WordFadeFrameClock (time plus frame callbacks) as its smallest public seam;
production keeps the Choreographer-backed default.

Fixture text is sized in pixels so every sample stays on one line at any
density; at 420 dpi the 32sp fixture wrapped and the column helper summed
glyphs from the first line.
2026-09-15 03:15:24 +02:00
Mohamed Boudra c3bcad687d fix(app): prevent and recover workspace layout crash loops (#4844)
* fix(app): prevent and recover workspace layout crash loops

* fix(desktop): capture resized browser tabs without hanging

A resident guest can paint fresh pixels after resize while capturePage's
native surface-copy request remains pending. The same failed guest returns
a fresh frame through frame subscription, so waiting for resize or another
animation frame does not correct the failing copy request.

Capture one subscribed frame within the existing five-second budget and
release the subscription on completion or cancellation. Preserve temporary
frame production and its original throttling policy. Remove viewport copy
retries and E2E screenshot polling; assert the resized image dimensions.

Refs #4866. Three full real-Electron browser journeys passed under Xvfb,
including physical clicks and fresh screenshots while the window is hidden.

* test(app): choose Agent before exercising the runtime catalog

The newly merged catalog journey assumed an empty workspace opened an agent draft tab. This branch opens the New launcher, so create the agent draft tab explicitly before checking its composer and model choices.

* fix: settle closed browser captures and validate persisted layout fixtures

Destroying a guest during frame subscription left its screenshot pending until the capture deadline and blocked queued captures. Reject at destruction, remove the listener on every exit, and return browser_tab_closed. The regression proves immediate settlement and queue release.

Reuse the unchanged persisted layout schema from a UI-independent module so the recovery fixture validates storage before editing it.

* test(app): encapsulate Explorer layout setup and cleanup

Keep persisted layout schemas and corrupted-state construction inside the seedCorruptedWorkspaceLayout helper. Use the existing withWorkspace fixture for cleanup in all four Explorer journeys so test bodies follow a straight setup, action and assertion flow.

* test(app): target the selected workspace in layout recovery

The corruption helper selected the first persisted entry, so a multi-workspace journey could leave the intended workspace untouched. Resolve its exact persistence key from the requested workspace ID and fail setup when absent. The failing-first journey now preserves an earlier workspace with an agent draft tab while recovering the selected workspace.
2026-09-15 02:30:27 +02:00
Mohamed Boudra 174055a63c feat(app): give compact Changes an overview and a straight rail (#4861)
* feat(app): give compact Changes an overview and a straight rail

Three fixes to the Explorer's Changes tab on phones, where there is no
tree rail and the diff is the only view of what changed.

No placeholder stat while the sidebar slides in. `useCheckoutDiffQuery`
reported `isLoading` only when the query was enabled and connected, so an
inactive retained panel settled on `files: []`. The comparison toolbar
summed that into "+0 -0" and the body rendered "No changes" for a diff
nobody had fetched yet, both replaced a moment later by real numbers. The
result now derives from the payload alone: no payload means loading,
whatever the query is doing. Callers already handle loading correctly, so
nothing downstream needed a special case.

Jump to file. A floating action button over the diff opens a sheet with
the same changed-files tree the desktop rail renders; tapping a file
closes the sheet and scrolls the diff there. `ChangedFilesTree` moved out
of `diff-pane.tsx` into its own module so both surfaces own one tree, and
its list is now the sheet-aware one so it scrolls with the sheet gesture
on native. `DiffDocument` takes a `contentInsetBottom` so the button
never traps the last line.

Rails on the compact header. The tab labels sat at 20dp and the close X
at 16dp while every toolbar row below them used 12dp leading and 8dp
trailing. The close button was also a hand-rolled Pressable rather than a
ToolbarButton. `paneContentToolbarTrailingPadding` now takes whether the
row's last control is a bare glyph or a framed one: a glyph's hitbox
overhangs its ink so the row pads by less than the rail, but the compact
git-actions trigger paints its own frame and was landing flush against
the screen edge. The commits header chevron moved to the depth-0 tree
rail. Desktop geometry is unchanged.

* fix(app): preserve Explorer touch targets and reset file overview

* fix(app): preserve diff selections across repaints

Header typography refreshes and height resizes changed the repaint callback
and inadvertently cleared active source selections. Keep selection updates
stable while retaining explicit source and selection-layout invalidation.

* test(app): express diff copy regression as user actions

* test(app): express file overview flows as user actions
2026-09-15 00:30:35 +02:00
0aca3b605a fix(plugins): transform whole timeline items before grouping and Markdown splitting (#4675)
Send every original tool call and each whole accumulated assistant message through plugin transformers before display grouping or Markdown splitting. Preserve streaming callbacks, original payloads, native Markdown block identities, and chat outline reading position. Reject legacy fragment caches and retain failing-first plugin-interface and browser regressions.

Fixes #4673. Fixes #4789. Consolidates #4820.

Co-authored-by: Matt Cowger <matt@cowger.us>
Co-authored-by: Jegor Kitskerkin <jegor.kitskerkin@gmail.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-09-14 23:03:38 +02:00
Matt CowgerandMohamed Boudra 0c5f472da1 fix(plugins): expose server settings access (#4674)
* fix(plugins): expose server settings access

* fix(plugins): isolate settings subscriber state

* test(plugins): read persisted settings before clients connect

* test(app): reconnect seed client after old daemon restart

* test(server): drain file creation events before deletion assertions

* test(creation): expose errors when completion fails

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-09-14 23:03:20 +02:00
Mohamed Boudra 13979e9c38 fix(server): discover fork PRs with URL-valued branch remotes (#4862)
Resolve direct repository URLs as well as named remotes when deriving checkout PR identity, preserving fork-owner filtering and batched polling.
2026-09-14 23:02:45 +02:00
Mohamed Boudra 77c5c8f17e Prevent duplicate catalog choices from crashing agent drafts (#4839)
* fix(providers): keep duplicate catalogs from breaking agent drafts

Normalize model identities before publication and when materializing older
catalogs, preserving the first definition and configured overrides. Keep
explicit default variants separate from the base model selection.

* fix(providers): preserve saved variant identities across resumes

Persist the selection encoding so legacy names remain literal. Refresh provider persistence when a selection changes, before another prompt is sent.

* test(app): let fixtures own catalog journey cleanup

* fix(providers): preserve the existing default variant contract

Keep one Default catalog choice and let the provider resolve an omitted variant. Remove the encoding and persistence machinery; other variant IDs retain their literal meaning.

* test(creation): expose failed completion after conflicting requests

Creation returns a failed snapshot instead of rejecting. Assert the result before its side effects so platform failures report the underlying error.

* test(app): wait for the seed client after daemon restart

The browser and seed client reconnect independently. Wait for the seed connection before issuing post-restart RPCs instead of assuming browser readiness covers both clients.

* test(app): isolate visible prompts and checkout diff fixtures

Inactive tabs retain message DOM, so prompt assertions must inspect visible rows. Ignore the local fixture remote before observation rather than deleting it after the daemon has started tracking the checkout.

* fix(file-observer): preserve final deletions when batching events

A duplicate creation notification followed by deletion was collapsed into an update, hiding final absence from consumers that distinguish deletes. Keep final deletes while retaining update semantics for atomic replacement.

* fix(desktop): wait for Metro HTTP readiness before launching Electron

Expo briefly listens while probing port availability. A successful TCP connection can therefore launch Electron before Metro starts and leave it exiting on connection refusal. Require the configured renderer origin to report the running packager status.

* test(ci): consolidate fixture and readiness corrections

Await the connection and UI states each journey owns, isolate Git fixture contents before observation, and hold real Markdown frames at asserted checkpoints. Keep creation completion diagnostics explicit without claiming the original Windows failure is fixed.
2026-09-14 22:25:31 +02:00
Mohamed Boudra 836f1a9c23 fix(ci): consolidate E2E repairs and wait for Metro readiness (#4858)
* fix(desktop): wait for Metro HTTP readiness before launching Electron

Expo briefly listens while probing port availability. A successful TCP connection can therefore launch Electron before Metro starts and leave it exiting on connection refusal. Require the configured renderer origin to report the running packager status.

* test(ci): consolidate fixture and readiness corrections

Await the connection and UI states each journey owns, isolate Git fixture contents before observation, and hold real Markdown frames at asserted checkpoints. Keep creation completion diagnostics explicit without claiming the original Windows failure is fixed.

* test(desktop): describe Metro readiness through HTTP states

Keep protocol mechanics in the fixture and assert readiness only after a successful packager response. Cover non-success status and unrelated HTTP content without inline synchronization in the test body.
2026-09-14 21:25:38 +02:00
Mohamed Boudra fdb5540a94 docs(skills): clarify workspace comparison base selection (#4860) 2026-09-14 21:24:51 +02:00
799c91b39d fix(app): restore Android tablet model picker interactions (#4845)
Restore provider/model selection in Android tablet modal windows by giving both native modal hosts their own gesture-handler root. Preserve phone sheet and web portal behavior.

Include native regression scripts, test dependency resolution, and verified CI test corrections. Generated media and logs are excluded. Refs #4475 and #4377; consolidates #4721 and #4771.

Co-authored-by: Mike P. Kuhl <mkuhl@softmachine.at>
Co-authored-by: Cameron Chin <cjcrjc@users.noreply.github.com>
2026-09-14 19:10:32 +02:00
Mohamed Boudra d63132a37d fix(creation): prevent duplicate workspaces and agents on retries (#4442)
* fix(creation): prevent duplicate workspaces and agents on retries

* test(creation): cover plugin hooks across receipt replays

* fix(creation): recover draft retries without accepting partial workspaces

* refactor(creation): name receipt inputs and tag compatibility gate

* fix(file-observer): track files announced by native change events

* fix(creation): keep initial prompts in daemon-owned creation requests

* fix(creation): reuse connection-owned progress and recovery

* test(creation): adapt handoff gates to combined requests

* ci(desktop): avoid xz compression for smoke packages

* ci(desktop): pass valid RPM compression through FPM config

* refactor(creation): give legacy and modern requests one identity owner

* fix(creation): log observer failures without interrupting creation
2026-09-14 16:55:42 +02:00
Mohamed Boudra cdc669fde6 fix(agents): prevent OOM from cumulative tool history (#4838)
* fix(agents): prevent OOM from cumulative tool history

Repeated progress updates retained complete historical copies of growing tool output. Retain projected items on ingestion and serve them for every history fetch, preserving source sequence coverage for catch-up. Keep intermediate payloads only in streaming; confine compatibility degradation to child transcripts and overwritten fork checkpoints.

* test(client): expect projected subagent capability in hello

* fix(app): report unexpected child history failures

Keep connection failures recoverable while forwarding unexpected fetch and reconciliation errors to diagnostics. Name observer arguments to keep server and agent identities explicit.

* test(agents): assert projected history after rewind

* docs(protocol): tag projected child compatibility gates
2026-09-14 16:51:10 +02:00
Mohamed Boudra d1a53255fc fix(app): keep growing composers below the header (#4824)
* fix(app): keep growing composers below the header

Native keyboard translation left the dock at full height, so intrinsic composer growth could extend behind the header. Reserve the keyboard destination at motion boundaries and bound the composer to its stationary viewport while preserving native input scrolling and animation.

* docs: reuse existing test suites and automation
2026-09-14 12:25:29 +02:00
Mohamed Boudra d1b705a0cd fix(app): restore Android clipboard image pasting (#4758)
Decode clipboard image data URLs locally because native fetch rejects them. Add native-runtime and byte-integrity regression coverage, and clarify empty-clipboard guidance.
2026-09-12 09:50:25 +02:00
Mohamed Boudra 0d7760e68b fix(agents): keep rejected plans collapsed in conversation order (#4756)
Keep plan proposals at their original timeline position through approval, rejection, and cancellation. Make plan cards collapsible while retaining readable history after follow-up prompts and daemon restart.
2026-09-12 09:33:26 +02:00
Mohamed Boudra 03a54a171d Keep older daemons usable and let clients choose timeline recovery (#4737)
* fix(subscriptions): preserve older-host workflows and explicit timeline recovery

Keep established app and SDK workflows available on older daemons without emulating independent directory filters. Restore live timeline membership without choosing the consumer's history recovery policy. Preserve daemon filesystem paths separately from legacy workspace IDs.

* fix(client): keep legacy workspace replies scoped to the requested page
2026-09-12 08:50:16 +02:00
d0e1edca7f fix(server): show the right thinking options for each Cursor model (#4180)
* fix(server): probe Cursor thinking options per model

Cursor ACP only reports thought_level for the current model, so the catalog was copying Haiku's Off/On onto Grok 4.6 until the next probe happened to land on Grok.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(server): drop inherited thinking options when an ACP catalog probe fails

A rejected model switch was leaving the probe session's thinking list on the failed model, so Grok could still advertise Haiku's Off/On.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(server): probe per-model thinking when session/new has no thought_level

Composer/Auto as the Cursor ACP default omits thought_level, so the catalog helper skipped every model switch and Grok kept an empty thinking list.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(server): discover model options without changing preferences

Use the provider model catalog extension instead of probing model switches that persist user preferences. Keep provider-specific discovery in each shim and validate extension responses at the boundary.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-09-12 08:50:09 +02:00
Mohamed Boudra 2d7733cd65 fix(app): keep incomplete Markdown formatted while streaming (#4742)
* fix(app): keep incomplete Markdown formatted while streaming

Treat unfinished formatting at the growing assistant-message tail as provisional so delimiters and link destinations do not flash while tokens arrive. Completed messages retain ordinary Markdown parsing.

* test(app): express streaming Markdown checks as user actions
2026-09-12 08:49:25 +02:00
Mohamed Boudra a7ce602c83 fix(agents): restore archived Codex workspaces without losing focus (#4736)
* fix(agents): restore archived Codex workspaces without losing focus

History selection must survive without cached agent records, and reading an archived Codex thread must not acquire an interactive writer. Keep workspace restore explicit, serialize native lifecycle transitions, and leave transcript lifetime with its owner.

* test(app): express Codex restore specs as user journeys

* test(app): finish restore fixture cleanup after teardown errors

* test(app): align restore coverage and await fixture readiness

Workspace restore leaves agents archived until explicit Unarchive. Keep that distinction covered for multiple agents. Compile the recovery fixture before its interaction test starts, and wait for sidebar agent hydration before asserting tab presence.
2026-09-12 08:47:44 +02:00
paseo-bot[bot] fa93c4290e docs(changelog): update generated release notes (#4728)
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-09-11 18:58:13 +02:00
paseo-bot[bot] 192c517835 docs(changelog): clarify macOS version requirement (#4726)
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-09-11 18:53:27 +02:00
Mohamed Boudra f22a37e613 fix(app): let touch workspace titles use the available width (#4698)
* fix(app): let touch workspace titles use the available width

Omit trailing stats when the permanent touch menu replaces them. Keep concealed stats in layout only for temporary desktop shortcut hints, preserving hover and shortcut geometry.

* test(app): express sidebar width checks as user journeys
2026-09-11 10:16:33 +02:00
Mohamed Boudra 772ff8d6f3 fix(desktop): preserve Linux sandboxing wherever the host allows it (#4447)
* fix(desktop): preserve Linux sandboxing wherever the host allows it

Chromium checks its Linux sandbox before main.js, while package installation probes run as root and miss AppArmor restrictions on desktop users. Choose the launch flags before exec and always install the SUID alternative for native packages. Test published artifacts as an ordinary user with namespaces both denied and allowed; never repair permissions in the smoke harness.

* fix(desktop): retain the Linux release build baseline

* fix(ci): allow canceled desktop smoke uploads to stop

* test(desktop): verify the installed RPM sandbox under AppArmor

* fix(ci): resolve packaged desktop dependencies before installation

* chore(desktop): keep QA evidence out of the repository
2026-09-11 09:05:26 +02:00
Mohamed Boudra 0110302b6f fix(daemon): keep lifecycle operations on the selected instance (#4575)
* fix(daemon): keep lifecycle operations on the selected instance

Restart reconstructed launch inputs and address fallback could redirect a home-selected shutdown to another daemon. Let supervisors publish bound endpoints, retain their worker restart contract, and share captured local lifecycle ownership between CLI and Desktop.

* test(daemon): verify bounded desktop readiness and cancellable artifacts

* test(cli): verify reload target hints and durable offline pairing

* test(daemon): align lifecycle fixtures with ownership and targeting

* fix(daemon): preserve reachability when status details time out

Provider discovery can outlast a status request after authentication succeeds. Keep local connection facts separate from unavailable details, while explicit remote queries retain their error outcome.

Migrate CLI and packaged Desktop fixtures to persistent managed configuration and explicit targets. Isolate generic Git observation from forge discovery and reuse disposable CLI homes on Windows.

* test(daemon): observe restart through the worker status RPC

The bounded CLI status observation may omit worker details while an authenticated connection remains healthy. Exercise the app-style restart through its full RPC and retain the replacement observation, with a real slow provider probe covering the distinction.
2026-09-11 08:33:22 +02:00