Commit Graph
1867 Commits
Author SHA1 Message Date
Mohamed Boudra 42b6d99d60 Wait for in-flight dictation commits before finishing (#3968)
* fix(dictation): preserve audio queued at stop

Stopping capture disabled native recording before the final microphone buffer had crossed the bridge, so accepting immediately could omit the end of a dictation. Wait for the native producer to publish its final buffer before completing stop, and serialize stop with restart and destruction.

* fix(voice): serialize capture lifecycle

A capture start already awaiting permission could resume after teardown, and repeated destroy calls did not share completion. Order capture lifecycle operations behind one promise and build the native module declarations before clean-CI typechecking.

* fix(dictation): drain queued web capture before finish

* fix(dictation): await in-flight segment commits

* Revert "fix(dictation): drain queued web capture before finish"

This reverts commit 5168a6554d.

* Revert "fix(voice): serialize capture lifecycle"

This reverts commit 00fd3e3556.

* Revert "fix(dictation): preserve audio queued at stop"

This reverts commit ec087dc9cd.
2026-08-28 15:29:42 +02:00
paseo-bot[bot] 23debf3af1 fix(server): honor configured commit signing (#3976)
Paseo forced signing off for commits created through the app and squash merges. Let Git apply the user's configured policy; the existing command timeout still bounds interactive signer waits.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-08-28 19:24:56 +08:00
Mohamed Boudra deb846eed6 Let plugins use Paseo modals, icons, and toasts (#3967)
* feat(plugins): add React Native UI primitives

Give client plugins host-owned adaptive UI without exposing app internals. Preserve plugin runtime contexts when modal content moves through the compact portal host.

* docs(plugins): clarify host UI reference

* refactor(plugins): harden host UI boundary

Replace mocked pass-through coverage with a real browser plugin journey, keep host runtime ownership inside the evaluator, preserve toast context across compact portals, and expose modal content as a compound component.

* fix(plugins): defer native UI presentation runtime

Loading the plugin evaluator initialized the modal and toast presentation stacks even when no plugin requested host UI. Keep the capability stable while delaying platform-heavy presentation code until Modal.Content renders.

* refactor(plugins): move modal metadata to root

Keep Modal.Content body-only while the top-level Modal owns the host-rendered title and leading icon. This leaves presentation metadata stable as the compound API gains future regions.
2026-08-28 11:43:30 +02:00
Desmond ChaiandMohamed Boudra a1ed5a8bad Stop PR status polling from rate-limiting your entire GitHub account (#3825)
* Batch GitHub PR status polls into aliased GraphQL requests

The PR status poller ran one `gh pr view` (GraphQL-backed) plus a
per-PR facts query for every workspace on its own timer, so cost
scaled linearly with workspace count and exhausted the 5,000 pts/hour
GraphQL quota machine-wide past ~50 workspaces (#3587, #2470).

Replace the per-target timers with a global scheduler that snaps poll
targets to a shared 5s grid and flushes all due targets together:

- One aliased discovery query per batch of up to 25 targets (grouped
  by GH host) resolves PR identity, state, mergeability, and the merge
  facts that previously required a second GraphQL call per PR.
- A second aliased query fetches statusCheckRollup contexts only for
  the PRs that matched. Checks for merged/closed PRs are frozen and
  cached by repo#number@headOid, so idle workspaces settle to the
  discovery query alone.
- A failed batch backs off every target in it with one request spent;
  healthy aliases are recovered from the body gh still prints on
  stdout when a single alias errors (ForgeCommandError now carries
  stdout for this).
- Targets whose origin slug/host is still resolving, and fork repos
  whose PR lives in the parent, fall back to the legacy per-target
  path for that cycle. Unowned lookups filter out other forks' PRs
  that share a branch name.

Fixes #3587. Addresses the API budget in #2470.

* fix(server): reserve the shared GitHub polling budget

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-28 11:29:34 +02:00
Christoph LeiterandClaude Opus 5 3d69b0f458 fix(app): render markdown verbatim instead of substituting characters (#3253)
markdown-it's typographer option rewrites characters after parsing, so
a literal (c) came out as ©. Agents label steps and options (a), (b),
(c) all the time, and an ordinary list rendering as (a), (b), © stops
your eye every time.

The same option turns ... into an ellipsis, --- into an em dash, and
straight quotes into curly ones. Agent output is text people paste
back into a shell or a file, so a curly quote in --name="my repo"
looks correct on screen and fails when pasted. The markdown file
preview had the same problem, showing characters that are not in the
file on disk.

Every in-app markdown surface now goes through one factory in
utils/markdown-parser.ts, which leaves typographer off: agent chat,
plan cards, pull request comment bodies and the file preview. The plan
card was rendering through its library's built-in parser because it
never passed a markdownit prop, and that default has typographer on.

Link behaviour is unchanged. The assistant parser keeps its file://
permission and no other surface gains it, and linkify stays on for
chat and the default renderer and off for plan cards, matching what
each did before.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:58:27 +00:00
Mohamed Boudra fb67d2cdc8 Keep the daemon responsive during Git queue pressure (#3945)
* fix(server): keep Git pressure from stalling daemon

Git process starts were refilled synchronously, so slow process spawning could monopolize the main loop long enough to trip connection liveness checks. Admit one process per event-loop turn and expose refresh/fetch provenance in runtime queue metrics.

* perf(server): avoid delaying isolated Git commands

Start the first command immediately when the scheduler is idle. Backlog admission and completion-driven refills still yield between spawns, preserving daemon liveness without adding a turn to normal sequential Git traffic.

* fix(server): keep Git admission outside fake clocks

Subprocess admission is event-loop ordering rather than simulated wall-clock behavior. Use the native Node immediate so service tests can fake debounce timers without deadlocking queued real Git processes.

* refactor(server): bind Git command provenance explicitly

* test(server): expect bound Git fetch runner
2026-08-28 10:25:20 +02:00
Mohamed Boudra fe803f6e70 Let plugins contribute contextual composer pills (#3956)
* feat(plugin): allow composer pill contributions

Keep pill presentation host-owned while plugin components render contextual content. Pill callbacks reuse the selected host's API, RPC, surface, and registered-panel navigation capabilities without exposing router or workspace layout internals.

* fix(plugin): place composer pills in track bar

* fix(plugin): make composer pills client-owned
2026-08-28 16:02:00 +08:00
Mohamed Boudra 5c7d62df8c revert(acp): restore prior turn handling 2026-08-27 18:11:37 +02:00
Mohamed Boudra a8734a9724 Update project licensing (#3944) 2026-08-27 14:25:31 +02:00
smagnusoandpaseo-bot[bot] 2d7896999c Add steering support for ACP providers that indicate support (#3921)
* Add steering support for ACP providers that indicate support

* fix(acp): keep steering inside the admitted turn

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-08-27 11:05:16 +00:00
Mohamed Boudra 12e3acd188 fix(plugins): restore native plugin setup flows (#3942)
Plugin client bundles bypass Metro, so authored async callbacks reached Hermes without Metro's syntax lowering. Plugin connection inputs also selected bottom-sheet behavior from device form factor instead of their actual render scope.
2026-08-27 12:50:25 +02:00
Omer Cohen 8e0af11b66 feat(plugin): add host-rendered Icon component (#3903) 2026-08-27 10:50:20 +00:00
Mohamed Boudra 7ae5133ed2 feat(plugins): let plugins transform and render timeline items (#3940)
* feat(plugins): add timeline item contributions

Keep canonical daemon history unchanged while plugins project and render custom timeline items in clients. Live matches refresh authoritative projected history before replacement so lifecycle deltas stay collapsed.

* fix(nix): update npm dependency hash

The plugin SDK lockfile change invalidated the fixed-output npm dependency derivation. Use the hash calculated by the macOS Nix build for the current lockfile.
2026-08-27 11:40:17 +02:00
PMandpaseo-bot[bot] b0ce40804d fix(acp): enable terminal client capability by default (#3910)
* fix(acp): enable terminal client capability by default (#3812)

* test(acp): remove non-asserting Kimi coverage

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-08-27 08:10:25 +00:00
Mohamed Boudra ecfb77b014 feat(plugins): install and update Git sources (#3920)
Make every trusted Git repository directly distributable without introducing a registry or package manager. Managed checkouts retain their resolved commit, track branches explicitly, and stage validation before activation so failed updates restore the running version.
2026-08-27 15:24:01 +08:00
4de913fb82 fix(server): plugin daemon sessions see all providers by pinning the daemon appVersion (#3902)
* fix(server): expose all providers to plugin sessions

* fix(app): route workspace actions semantically (#3896)

* fix(app): route workspace actions semantically

Separate Explorer visibility from view navigation, and give composer changes and pull request opens semantic routing owners. Unify diff destinations, keep compact PR navigation in Explorer, and derive maximize availability from visible workspace panes.

* test(app): cover composer changes routing

Update the browser journey for the Explorer-first composer action, then assert the second press follows the shared diff destination.

* test(server): track plugin runtime dependency parameter

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-08-26 20:57:22 +00:00
paseo-bot[bot] 96cf0f2ae5 chore(server): upgrade Claude Agent SDK to 0.3.246 (#3893)
* chore(server): upgrade Claude Agent SDK to 0.3.246

Bumps @anthropic-ai/claude-agent-sdk from 0.3.220 to 0.3.246, keeping the
pinned exact version so fresh installs stay reproducible.

The SDK's peer requirements are unchanged (@anthropic-ai/sdk >=0.93.0,
@modelcontextprotocol/sdk ^1.29.0, zod ^4.0.0) and already satisfied, so
nothing else in the tree moves.

Nothing Paseo imports was removed or narrowed. The type surface Paseo
touches is additive; the removed ExitReason member
'bypass_permissions_disabled' and the OnElicitation/OnUserDialog signature
changes are unused here.

* chore(nix): refresh npm deps hash for the SDK bump

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-08-26 20:32:56 +08:00
Mohamed Boudra 59e6232784 fix(server): resolve canceled permission requests (#3792)
Provider callbacks can be canceled without a user response. Publish a terminal resolution for that path so every surfaced request has a matching lifecycle event, while preserving the existing interruption cleanup.
2026-08-26 09:53:41 +02:00
nllptrxandMohamed Boudra 4a5716360b Feat/forge check semantics and followup fixes (#2337)
* feat(protocol): add forge check presentation metadata

Expose optional rawStatus, isManual, and requiresAction fields while keeping older check payloads backward-compatible.

* fix(gitea): model Actions jobs as tasks

Gitea's /actions/tasks endpoint returns individual jobs rather than workflow runs. Preserve sibling tasks from the latest execution, avoid collapsing same-name jobs, and enrich blocked tasks with run-level approval metadata.

* feat(app): distinguish forge check presentation states

* fix(gitlab): preserve pipeline job semantics

* refactor(app): centralize forge check presentation policy

Use one canonical category order, count shape, visual policy, and translated copy mapping across sidebar, hover-card, PR-pane, and GitLab pipeline surfaces.

* refactor: consolidate forge check handling

* feat(forge): auto-attach pasted forge references

* docs(compat): correct forge rollout metadata

* refactor(forge): model check nuances as traits

* fix(forge): harden checks from branch review

Structural Gitea Actions shadow dedup by run id (verified upstream),
rerun grouping without workflow_id, shared trait/status constants and
Gitea state mapping in the protocol package, all cloud host aliases in
pasted-link matching, logged glab job-detail failures, concurrent
approvals/pipeline fetch, checks aggregate from the same pipeline as
the job list, and removal of the unread check-level rawStatus. The
released pipeline-level rawStatus is staged for removal under
COMPAT(pipelineRawStatus).

* fix(gitea): detect fork-PR Actions approval hold as action_required

gitea.com represents a fork PR awaiting Actions approval as a run in
status "waiting" with no tasks and pending shadow commit statuses; it
does not expose need_approval. The previous mapping only set
action_required from a task with blocked+need_approval, so the gate
never surfaced (0 tasks -> early return; need_approval absent). Detect
the hold from /actions/runs: a waiting/blocked run whose PR is
cross-repo (head.repo.id != base.repo.id), or one that does expose
need_approval (self-hosted). Tag the run's pending shadow statuses
action_required. Runs are only fetched when a pending shadow has no
covering task, keeping the common path at two requests. Verified live
against gitea.com fork PR (run 715170, waiting, cross-repo).

* fix(gitea): resolve cross-fork PRs by head sha

The current-PR matcher gated candidates on the checkout owner
(giteaHeadOwner === expectedHeadOwner), which excludes every fork PR:
its head carries the fork owner while the checkout is the base repo, so
a fork PR - the only shape that can be approval-held on Gitea - never
resolved and its checks never showed in-app. GitLab has no such gate
(it resolves fork MRs from the target project by source branch). Accept
a recent PR whose head sha equals the workspace head sha regardless of
owner; the sha is an unambiguous identity, so the anti-collision owner
gate still applies when no sha match is available. Verified live against
gitea.com fork PR #6 (resolves + renders action_required).

* fix: resolve forge rebase drift

* fix: type forge auto-attach boundary

* fix(i18n): add Korean check status copy

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-25 23:37:50 +02:00
e22ad117fd Stream assistant text at a steady rate instead of in arrival lumps (#3612)
* Stream assistant text at a steady rate instead of in arrival lumps

Assistant text arrived in lumps and was painted that way. The daemon
coalesces one message per 60ms window, and that window carries however
many characters the model produced in it, which swings by an order of
magnitude within a turn. Painting each delta as it landed made the size
of those lumps visible.

Three changes:

- The agent stream coalescer is now leading + trailing. The first delta
  after an idle window flushes synchronously instead of waiting a full
  window, matching the invariant TerminalOutputCoalescer already
  documents. The window stays at 60ms; shrinking it would raise message
  rate on a loop that already contends with terminal frames and relay
  encryption, without making the stream smoother.

- The client reducer queue commits on a frame instead of an arbitrary
  48ms timer, with the timer kept as a ceiling so a hidden tab still
  advances.

- Assistant and reasoning text are revealed at a rate derived from the
  backlog rather than from arrival timing. Arrival changes the target,
  not the rate. The store keeps the full text and only the rendered
  slice is paced, so copy, selection, the chat outline, and scroll
  geometry all stay consistent with what is on screen.

The leading flush means a burst's first chunk lands as its own timeline
row. The projection already merges contiguous same-turn rows and clients
read the projected timeline, so history is unchanged; the server tests
that assert on raw rows were updated for the extra row.

Measured on Expo web against a local dev daemon. Mock bursty-stream
provider, 6s samples, paint-on-arrival vs paced: frames that advanced
the text 55 -> 244, chars-per-frame CV 2.66 -> 1.44, gap p50 100ms ->
17ms. Real Claude Haiku agent over 52s: 84% of frames advance the text,
CV 1.29, gap p50 and p95 both 17ms (one frame at 60Hz).

Adds a bursty-stream mock model with seeded burst sizes, a gated browser
perf spec, and docs/agent-stream-performance.md.

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

* Address review: safe reveal boundaries and a testable seam

Two findings from the PR review.

Unicode boundaries. Slicing the revealed text at an arbitrary index could
strand a lone surrogate, which paints as a replacement glyph, or split an
emoji ZWJ sequence so a family briefly rendered as its members. Both were
visible for a frame or two mid-stream — the exact flicker the paced reveal
exists to remove. The rendered cut is now pulled back off surrogate pairs,
combining marks, variation selectors, skin tone modifiers, and ZWJ joins.
The reveal counter stays monotonic and only the painted boundary moves, so
a long cluster cannot stall the reveal.

Test placement. The hook test used JSDOM, @testing-library renderHook, and
monkey-patched requestAnimationFrame, all of which docs/testing.md rules
out; it prescribes fixing the missing seam instead. The reveal policy is
now a pure state machine in text-reveal.ts (begin/retarget/advance/
complete/visible) covered by plain unit tests with no environment, and the
hook is reduced to the frame-clock binding. Rendered behavior is left to
the browser spec, which no longer carries frame sampling and statistics in
its body — those moved to e2e/support/helpers/stream-smoothness.ts.

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

* Measure total painted text, not the tail message

The smoothness sampler read the length of the last assistant-message
element. A real turn emits many assistant messages, so that element keeps
changing identity and its length is not monotonic — handovers read as
resets, and a stream that was plainly moving on screen reported almost no
growth at all. Sampling the total across every assistant message fixes it.

This invalidated the numbers previously recorded for the real-agent run,
so the doc table is replaced with a fresh A/B taken with the corrected
sampler against a Claude Haiku agent: frames that advanced the text 6% ->
87%, chars-per-frame CV 4.11 -> 1.86, gap p50 317ms -> 17ms, p95 383ms ->
17ms. The paced column also reported zero Unicode defects across a stream
containing emoji, which exercises the reveal boundary clamp end to end.

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

* perf(agent-stream): harden paced text reveal

Use standards-compliant grapheme segmentation when available and preserve full arrivals on older runtimes. Cap reveal commits at 60Hz and measure the real browser rendering path so high-refresh displays stay smooth without rendering at hardware frame rate.

* Hold back trailing fragments split by arrival boundaries

The reveal boundary is not the only place a grapheme gets cut. The
daemon's coalescing window ends a delta wherever the window closes, so a
caught-up reveal can be handed half a cluster and paint it. Measured on a
mixed-script Haiku stream: five frames in ~350 ending in a single regional
indicator, i.e. half a flag on screen for a frame.

While the turn is streaming, a trailing fragment that cannot stand alone —
a dangling high surrogate, a trailing joiner, an odd regional indicator —
now waits for the rest of its cluster. Once the turn ends everything
received is painted, because nothing more is coming. Complements the
grapheme-boundary clamp, which handles the reveal cut; this handles the
arrival cut.

The same stream now measures one such frame in ~400 rather than five. The
remainder is a flush boundary that coincides with the end of an assistant
message, putting the two halves in different messages, which the client
cannot rejoin — that one belongs in the coalescer.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-25 23:37:46 +02:00
Mohamed Boudra e19d3914ae fix(rewind): replace long timelines without replay (#3642)
* fix(rewind): refresh canonical timeline after rewind

A rewind replaces the daemon timeline and epoch, but replaying the rebuilt history made long conversations visibly reload from the beginning. Capable clients now refresh the existing bounded tail path after a replacement signal while legacy clients retain replay compatibility.

* test(rewind): cover coherent timeline replacement

Hold rewind completion in the browser regression so it can verify the existing pending state and detect any reconstructed timeline rows before the normal tail refresh is allowed to finish.
2026-08-25 23:37:12 +02:00
Mohamed Boudra 4bfb682cd9 Cut Playwright feedback time without losing behavioral coverage (#3852)
* test(app): consolidate Playwright journeys

* test(app): stabilize consolidated Playwright flows

* test(app): make streaming dependencies explicit

* test(app): await profile feature persistence

* test(app): sample only actual workspace tabs
2026-08-25 20:55:05 +02:00
Mohamed Boudra 8bfae15855 Keep current pull requests visible without wrongful auto-archive (#3799)
* fix(server): keep current PR visible without unsafe archival

PR discovery and merge-triggered archival answer different questions. Follow the checked-out branch for status, while requiring the daemon to observe the same PR move from open to merged before archiving a workspace.

* test(server): align managed worktree PR lookup expectation

Managed metadata without a matching PR target must not hide the unambiguous PR identity from the current branch tracking configuration.
2026-08-25 19:43:18 +02:00
Mohamed Boudra 6f6466d4cb Wait for authoritative provider readiness before prompting (#3821)
* fix(server): trust provider connection lifecycle events

Readiness now waits for the upstream connection event, and later connection events directly trigger the existing gap repair. This removes the parallel synthetic reconnection contract while preserving transport ownership and prompt gating.

* test(server): model OpenCode global event envelopes
2026-08-25 15:28:06 +02:00
Mohamed Boudra 54299a2e6c fix(server): let event-stream startup recover once (#3814)
The readiness wait expired at the same boundary as the producer watchdog, so a reconnect already initiated by the event consumer could never benefit the pending turn. Keep dispatch gated, allow that retry to land, and preserve the actual SDK failure details for logs and user-visible reports.
2026-08-24 19:31:11 +02:00
Mohamed Boudra 88f410567d Revert "fix(server): honor Claude bypass permission mode (#3732)"
This reverts commit f998ee1691.
2026-08-24 11:05:19 +02:00
wongangandMohamed Boudra f998ee1691 fix(server): honor Claude bypass permission mode (#3732)
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-24 10:49:12 +02:00
Mohamed Boudra 3979371901 fix(server): recover already-settled interrupts (#3742)
* fix(server): recover already-settled interrupts

Provider state can drift from Paseo’s tracked run. Treat an authoritative already-idle interrupt as settled and retire the captured run token so retrying can start new work without weakening ambiguous-cancellation protection.

* fix(codex): fence interruption during turn startup

Reserve foreground-start ownership before asynchronous provider setup. An interrupt can now cancel preflight before turn/start or wait for an issued start to become interruptible, preventing acknowledged cancellation from leaving unmanaged work.
2026-08-23 14:59:36 +02:00
Dan Berindei 71e375c2bc fix(server): stop quota fetcher from writing provider credentials (#3442) 2026-08-23 14:24:18 +02:00
Mohamed Boudra c763b80824 fix(files): keep large source previews responsive (#3665)
Large read-only files bypassed the editor's virtualization and eagerly mounted every highlighted token, which could stall the renderer for seconds and consume gigabytes of memory. Route source previews through bounded platform renderers and reject reads above their display budgets before accumulation.
2026-08-22 10:13:31 +02:00
Mohamed Boudra 8905ad4160 fix(relay): validate handshake keys (#3669) 2026-08-22 02:32:13 +02:00
BrianAguilarWascoandMohamed Boudra 60a5344cc6 fix(server): stop short start gates from undercutting provider startup budgets (#3621)
* fix(server): stop short start gates from undercutting provider startup budgets

Agent creation still failed on contended cold starts after #3578, because two
shorter gates on the same path fire before the event-stream readiness budget can
apply. A traced creation reached session.create twelve seconds in, past the 10s
session.create gate, and the agent was then killed by a bare "timeout" at fifteen
seconds from AGENT_RUN_START_TIMEOUT_MS while the SSE connect was still running.

A run-start bound of 15s wraps a path that is deliberately allowed 30s, so the
outer gate always wins and the inner budget is unreachable.

Give session.create the shared OPENCODE_SERVER_STARTUP_TIMEOUT_MS budget, since
creating the first session for a directory is part of OpenCode coming up, and
derive its error message from the constant instead of hardcoding "10s". Raise
AGENT_RUN_START_TIMEOUT_MS to 60s with a comment recording that it must stay
above the slowest provider startup budget; it stays a literal because that module
is provider-agnostic.

Related to #3571.

* test(server): drive the run start budget through a real AgentManager

The run start boundary tests replaced AgentManager.waitForAgentRunStart with a
bare timer, so they never exercised the production run-state transition or the
agent_state subscription the real wait depends on. That path could regress while
the tests stayed green.

Build the scenario on a real AgentManager with a provider session that holds its
turn start open for a configurable span, so pendingRun.started, the lifecycle
change to "running", and the agent_state notification are what the wait actually
observes. Budgets stay independent literals and the settled-before/after
assertions are unchanged.

* fix(server): describe agent run start timeouts

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-22 02:06:16 +02:00
Mohamed Boudra 956f6ea7ad fix(server): keep agent timelines in memory (#3647)
The filesystem timeline store rewrote each agent's full transcript on every buffered update, causing write amplification that grew with conversation length. Return production timelines to runtime memory and provider-history hydration while preserving the dormant storage interface for a future intentional design.
2026-08-21 18:59:09 +02:00
Mohamed Boudra c032df5b3d Keep Pi chats active through retry recovery (#3639)
* fix(server): preserve Pi turns through retries

Pi reports each provider attempt with agent_end, while agent_settled marks the final user-turn boundary. Keeping modern streams active until settlement prevents a recovered attempt from leaving the chat in an error state.

* fix(server): show Pi provider retries

Automatic retries keep the turn alive, but users still need to understand the delay. Surface Pi's retry-start signal through the existing non-terminal error activity row without changing terminal lifecycle handling.

* refactor(server): clarify Pi retry boundaries

Use the server's object-parameter convention for the retry boundary helpers so their event, turn, and retry semantics stay explicit as the lifecycle evolves.
2026-08-21 12:16:19 +02:00
Okan YILDIRIM e1805ef542 feat(plugins): add theme contributions
Adds plugin-contributed light and dark app themes, including persistence, host-aware coalescing, fallback behavior, documentation, examples, and browser/iOS QA.
2026-08-21 11:48:12 +02:00
Matt CowgerandMohamed Boudra bc02cfd8cd feat(opencode): steer active turns without interrupting (#3580)
* feat(opencode): add active-turn steering via native V2 prompt

Add OpenCode support for active-turn steering (PR #3394) using the
native V2 session prompt with delivery: steer. Implements
steerActiveTurn, pending steer echo correlation for multiple steers,
prompted event handling, and definitive vs ambiguous error handling.
Updates docs and SDK to 1.18.18 for typed V2 access.

* fix(opencode): use V1 prompt for steer so model sees it

V2 prompt was admitted to a separate inbox not visible to the V1
processor, so "How old is bigbird?" was shown inline but never in
the LLM context. Switch steer to session/prompt_async (V1) which
queues while busy and is included in the next LLM call's history.
Update tests to expect V1 and keep the timeline grouping.

* fix(opencode): honor steering permission contract

Admit the steer before denying permissions that block its delivery, matching the provider steering contract. Remove the abandoned V2 path and SDK upgrade because active-turn delivery uses the existing prompt endpoint.

* chore(opencode): preserve SDK patch metadata

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-21 10:38:36 +02:00
efa25ad3fe fix(server): show Claude usage again on macOS (#3597)
* fix(server): show Claude usage again on macOS

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

* fix(server): keep an incomplete Keychain item from hiding a working one

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

* chore(server): tighten Claude keychain fix

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-20 23:02:25 +02:00
BrianAguilarWasco 28a963795b fix(server): extend OpenCode event stream readiness timeout to match server startup budget
Closes #3571.
2026-08-20 22:56:18 +02:00
Mohamed Boudra 3f7188bc1c fix(codex): restore externally archived active threads (#3334)
Codex threads can be archived outside Paseo while their Paseo agent remains active. Repair that provider-state drift during interactive resume while preserving the existing read-only history path for Paseo-archived agents.
2026-08-21 04:39:26 +08:00
c6b04f302a fix(server): stop a steered prompt from deadlocking on a permission (#3585)
* fix(server): stop a steered prompt from deadlocking on a permission

Claude parks its turn inside the blocking canUseTool callback while a
plan approval or a tool prompt waits for an answer. The SDK stops to
read its input stream there, so a message sent at the open card is
queued and never read. The user must reject the card or interrupt the
agent by hand, and then send the message a second time.

Deny the pending permissions after the steer is admitted. The prompt is
already in the SDK input queue when the permission callback returns, so
Claude reads it in the same turn. Plan mode stays on and no turn
restarts.

One pass is not enough. A turn that made several tool calls opens its
next request 10 to 300 milliseconds after the previous one is answered,
so a single snapshot advances the batch by one and parks the turn again.
The Claude adapter already tracks the steers the SDK has not read, so
expose that as an optional AgentSession.hasUnreadSteer and answer each
new request while that method reports the prompt as unread. The release
stops the moment the prompt is read, so a permission the agent asks for
after it acts on the message still reaches the user.

Only the two entrypoints that carry a message from a person set the
flag. A scheduled prompt, a child-finished notification, and an
agent-to-agent prompt must not answer a card the user is looking at.

Denial left no timeline row, so a denied plan disappeared from the
transcript. Claude and Codex now push a plan_approval row with
approved: false. For Codex the row comes from resolvePlanPermission,
which is the path that dismissPendingPlanApprovals also uses.

In the app, queue mode held the message until the turn ended, and a
parked turn never ends. The composer now sends while a permission is
pending, and the button label agrees with the submit path.

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

* fix(server): keep a post-prompt permission out of the release

The release loop answered the pending requests before it asked whether
the agent had read the prompt. The agent can read the prompt during a
poll interval and then open a permission request because of it. The next
pass denied that request, so a decision that belongs to the user was
made for them.

The loop now asks first and answers second. The first pass stays
unconditional, because a provider that does not report unread steers
must still get one release.

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

* fix(server): make steer permission clearing provider-owned

Keep the daemon policy explicit while letting each provider queue the steer and release permissions that block it. This removes timing-based polling and preserves rejected plans as canonical timeline rows.

* fix(server): preserve omitted steer options

Only construct provider steer options when permission clearing is requested. System prompts otherwise keep the existing undefined options contract.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-20 14:02:00 +02:00
Mohamed Boudra 4dbd26c88c fix(server): accept Git-valid worktree branch names (#3591)
Branch-off creation reused the filesystem slug validator even when callers supplied a separate branch name and worktree slug. Validate branch refs with Git at the worktree boundary so exact valid refs are preserved while path slugs remain restricted.
2026-08-20 09:44:29 +02:00
Matt Cowger bed5d2b1be fix(server): add upstream remote fallback to GitHub PR checkout refs (#2997) 2026-08-20 02:54:30 +08:00
Mohamed Boudra 929d2b717e fix(plugins): publish the plugin SDK under the @getpaseo scope
The daemon imports the plugin SDK at runtime, but the workspace was named
@paseo/plugin and marked private, so it was never published. 0.5.0-beta.1
shipped @getpaseo/server depending on a package that does not exist and
`npm install @getpaseo/cli@beta` failed with a 404 for every user.

Two things hid it. sync-workspace-versions only rewrites deps matching
@getpaseo/*, so the "*" range on @paseo/plugin survived untouched into the
published tarball. And `npm pack --dry-run`, which is all release:check and
CI run, lists files without ever resolving the dependency graph.

Renaming to @getpaseo/plugin rather than publishing the old name: @paseo is
not our scope on npm, so that name could never have been published at all.
The rename also brings the package under the version-sync filter, which is
what pins it correctly from here on.

Plugins scaffolded against the old name still import @paseo/plugin. Both
spellings resolve through plugin-sdk-specifiers.ts, tagged COMPAT with a
removal date, so existing plugins keep loading.

This does not repair 0.5.0-beta.1 — a published tarball's dependencies
cannot be changed. The package publishes with the next release.
2026-08-19 15:52:51 +02:00
Mohamed Boudra e12bdc2fc5 test(server): align directory search expectations 2026-08-19 13:06:06 +02:00
Mohamed Boudra ab619f982e fix(server): isolate esbuild binary override
Esbuild reads its binary override during module evaluation, but the previous global environment mutation leaked that host-only path into desktop-launched agents. Scope the compatibility bridge to module loading and scrub the variable from external process environments.
2026-08-19 12:20:32 +02:00
Mohamed Boudra e55dd5a212 fix(search): match complete paths in file and project search
Treat slash-separated fragments as searchable text and use the same full-path fuzzy matcher for daemon results and remembered project paths.
2026-08-19 12:18:23 +02:00
Mohamed Boudra 91024bc029 fix(plugins): install local plugins on packaged desktop
The plugin compiler spawned esbuild through app.asar, and the subprocess
imported the React client barrel. Point esbuild at the unpacked binary,
add a react-free @paseo/plugin/server entry, and pass a typed PluginTheme
so plugin UI follows the active theme on desktop and mobile.
2026-08-19 11:54:55 +02:00
Mohamed BoudraandSlava Goltser 9c9a85a0cf feat(pi): emit live usage updates during turns (#3532)
Port the OMP usage poller into the Pi provider so context/token usage
updates during active turns instead of only at completion.

- Add PiUsagePoller with 3s throttle, dedup, and generation-based cancellation
- Wire poller into PiRpcAgentSession turn lifecycle
- Add unit tests for polling, dedup, error handling, and turn completion

Co-authored-by: Slava Goltser <slavikg@gmail.com>
2026-08-19 07:42:19 +02:00
Mohamed Boudra f9e1def954 fix(server): keep interrupt-window Claude output from starting a turn
After an interrupt, Claude keeps emitting messages about the request it
was told to kill — the notification for the tool it just stopped,
trailing assistant output. The router classified them as new work and
opened an autonomous turn, but the only message that could close that
turn is the interrupt result shouldSuppressStaleResult drops, so the
agent reported running forever.

Pre-existing, but masked until the steer-discard fix: the undiscarded
steer used to resume the turn and its result accidentally closed the
phantom one. pendingInterruptAbort already marks the window where
messages belong to the dying request, so the router now refuses to open
a turn while it is set.
2026-08-18 22:15:19 +02:00
Mohamed Boudra f7e4606452 fix(server): discard unread Claude steers on interrupt
A steer pushed into the live SDK query input survived query.interrupt():
the SDK dequeued it afterwards and resumed the turn the user had just
stopped. Codex clears pending turn input when it aborts a turn; Claude
does not, so its adapter now cancels the SDK messages it queued before
interrupting.

Interrupt-discards is the deliberate ceiling for steer cancellation:
Codex exposes no un-steer operation, so a cross-provider take-back
cannot exist. A steer the model never read keeps its transcript row.
2026-08-18 21:51:38 +02:00