* fix(app): stop workspace tab reconciliation loop
Duplicate startup tabs could move workspace focus into Explorer. Once hydration removed the stale agent, empty-workspace reconciliation kept seeding drafts into Explorer and retriggering itself. Preserve ordinary-pane focus at the reconciliation boundary.
* fix(app): normalize workspace focus before reconciliation
Persisted Explorer focus must not steer newly reconciled tabs into the sidebar before the output invariant is applied.
* 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.
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>
Durable replica reads could race accepted directory replacements and restore rows that had already been removed in memory. Serialize reads behind pending writes and discard reads when the host changes in flight; local archives also suppress targeted cache restoration until removal is acknowledged.
* 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.
* 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>
* feat(app): find workspaces by PR/MR number in the command center
The command center matched workspaces on title, host, project, and
branch. Typing the change-request number you actually remember found
nothing, so you had to recall the branch name instead.
Workspace results now carry their pull/merge request number, and a
numeric query resolves it directly: 42, #42, !42, pr 42, and mr 42 all
reach the same workspace. Matching compares the parsed number exactly
rather than as a substring, so 42 no longer drags in PR 142 or a branch
that happens to contain those digits. Exact number hits sort above
ordinary text matches for the same query.
Nothing changes visually. The number is a search key only; result rows
render exactly as before.
The number is read from the pull request the daemon already sends.
selectPrHintFromStatus is the fallback only: it derives the number by
parsing the PR/MR URL, which yields nothing for a noncanonical or
absent URL even when the daemon sent a usable number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(app): exercise change-request parsing through the matcher
parseChangeRequestQuery was exported only so the test could reach it,
which made an implementation helper part of the module surface and let
the parser cases pass without covering how the parser composes with
workspace matching.
Unexport it and drive the same spellings through matchWorkspaceQuery.
The rejection cases now assert what they actually protect: a query like
fix-42-retries must not take the number path, so a workspace whose PR is
42 does not match it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* 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
* feat(sdk): expose a session's own commands on the agent handle
`DaemonClient.listCommands` has always been able to ask a running session
which slash commands and skills it loaded, but `PaseoAgentHandle` never
surfaced it, so SDK and plugin consumers could only see what they could
find on disk. Provider built-ins — the ones bundled inside the agent
binary rather than installed into a scannable directory — were therefore
invisible to them.
Add `agent.commands()` as a thin delegation to the existing RPC. Every
provider that implements `listCommands` (claude, codex, opencode, acp,
omp, pi) answers it, so a consumer no longer needs per-provider
filesystem knowledge to enumerate what a session can run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(sdk): expose the observed agent snapshot on the agent handle
The handle mirrored four snapshot fields (workspaceId, cwd, status, id)
and hid the rest behind current(). Add getters for capabilities,
availableModes, pendingPermissions, activeTurn, lastUsage, lastError,
features, runtimeInfo, and archivedAt so callers can read one field
without destructuring the whole snapshot and null-checking it.
Each reads the last observed snapshot and never fetches, matching the
existing getters. null means "not observed yet".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(sdk): clarify nullable snapshot getters
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: marvin-ambrozio <marvin@gustavo.eng.br>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* fix(app): restore rewound text atomically
The native composer could consume a replacement revision before the matching draft-store value arrived, leaving the mounted input blank until remount. Carry the replacement text with its revision so scheduling order cannot split the command from its payload.
* fix(app): isolate launch composer drafts
Give chat and terminal launch composers separate React identities so each uncontrolled editor lifetime remains owned by its draft mode.
Retained workspace panels collapse to zero width while Settings is visible. Treat that geometry as an absent surface so hidden measurements cannot overwrite the last valid composer height before reveal.
* 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>
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.
* 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.
Preserve unmodelled keys at the document, settings, nested settings, and migration levels when an older desktop build writes known settings.
Validate known fields independently and keep the public DesktopSettings value limited to fields this build understands.
The Changes view listed the same files in two different orders. The
tree rail put directories before files at each level of the path.
The scrolling diff compared the full path byte by byte. A root file
such as .gitlab-ci.yml therefore opened the diff, but sat below every
directory in the tree. Scrolling the diff did not walk the tree from
top to bottom.
Two sorts caused this. sortTree in git/diff-tree.ts sorted the tree.
compareCheckoutDiffPaths in git/diff-order.ts sorted the flat list at
ingest.
The tree is now the only ordering authority.
orderCheckoutDiffFiles builds the tree and reads the file sequence
back out of it. compareCheckoutDiffPaths is deleted, so the two
surfaces can no longer disagree.
The daemon keeps its own whole-path sort, so daemons that are not
updated also get this fix. Commit diffs keep the order that git
gives them, because that panel shows no tree rail.
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.
* fix(app): paint cached conversations before reconnect
Cold startup restored every saved host before publishing any session, so a valid cached chat was blocked behind unrelated directory validation and network-only readiness. Directory and timeline owners now resolve demand through typed storage and authoritative reconciliation while consumers remain source-agnostic.
* fix(app): stabilize demand-driven cache restore
Create host projections before network availability, route cached workspace and timeline preparation through their owners, and coalesce directory reconciliation per online connection. Update browser cache fixtures for per-agent timeline rows.
* fix(app): resume cached timeline through owner
* fix(app): surface timeline owner load errors
* 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.
* 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>
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.
Automatic readiness polls reapplied the stable rollout gate and cleared the same update that a manual check had already admitted. Keep same-version admission monotonic while still clearing withdrawn releases and rejecting newer releases that have not reached the user.
The Explorer pane rewrite routed semantic Changes and pull-request actions through ordinary workspace tabs on compact layouts. Centralize those actions so mobile and native dock layouts select the Explorer surface while desktop keeps its placement preferences.
Serialize pending batches at write execution time so retries cannot overwrite newer rows or resurrect removed hosts. Reject oversized hosts before storage writes and preserve unchanged directory identities so routine reconciliation stays row-grained.
Persist only changed replica entities so steady-state writes scale with the update rather than the complete cached world. Restore remains an all-or-nothing validated boundary, with whole-host byte-budget eviction.
Ship the platform storage engines behind a dark interface so the cache migration can wire them separately. Both engines share a contract, including atomic writes and destructive schema-version resets.
Add an observational live measurement harness for the row-store migration baseline. Trust already-shaped cache values on persist while retaining full schema validation at the restore boundary.
* add f-droid metadata support
* share version code calculation with website
* preserve blockquoted sections in changelogs
* refactor(android): simplify stable F-Droid metadata
Generate store changelogs only for stable releases and keep native version-code ownership in the existing release module.
* test(android): run F-Droid changelog suite in CI
---------
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
* 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>
* 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>
* 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.