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.
The first-class Explorer reused the old Side panel persistence identity, so upgrades hydrated ordinary persisted tabs into the dock. Move those tabs into an ordinary side pane during the versioned migration while preserving main layouts, and keep the persisted key spelling stable at the storage boundary.
The Command Center tested the whole trimmed query as one substring
against one concatenated haystack, so a query only matched when its
words appeared consecutively, in that order, with that spacing. Typing
`shortcuts` found "Keyboard shortcuts"; typing `short key` found
nothing. Survivors were then ordered by a fixed group rank, so the
best match was rarely first.
Route both through `scoreTextFields`, the matcher the combobox,
provider selection, directory suggestions and the daemon's history
search already share. It splits on whitespace, scores each token
against each field on its own, and requires every token to land
somewhere. `short key`, `key short`, `workspace new` and `project add`
all find their row now.
The score is the filter: `scoreSearchFields` returns null when a token
finds no home, so filtering and ranking are one pass rather than a
boolean predicate that throws the scan away. Workspaces and agents are
scored too, not just filtered, with each row's own ordering kept as the
tiebreak. Fields are split into visible and hidden, so a match on an
agent's `cwd` — which is never rendered — cannot outrank a match on its
title.
Subsequence matching is deliberately off here, unlike the composer's
slash-command list where it makes `/pasbab` find `/paseo-babysit`. It
widens far enough that three-letter typos hit several rows each, and
this list preselects its first row, so a query that should find nothing
would put an unrelated action under Enter. `MatchOptions.subsequence`
defaults to on, leaving every other caller unchanged. The tier has to
be suppressed during scoring rather than filtered afterwards, because
a two-token total of tier 5 is either 0+5 or 4+1.
Sections carry a band so the ones that reorder by relevance are
compared before the pinned ones, which keeps the comparator total: file
rows are matched by the daemon and carry no client-side score, so they
cannot be relevance-compared against rows that do.
Contiguous per-token matching is a strict widening of the old test.
Tokens hold no whitespace and the old haystack joined fields with a
space, so any token found there lay entirely within one field and is
still found. Verified across a query set at zero rows lost, and pinned
by a test.
* 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.
* 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
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.
* test(e2e): boot the Playwright harness on Windows
The harness could not start on Windows. Metro was launched with
spawn("npx", ...) and the isolated daemon resolved its runner with
execSync("which tsx") — npx is a .cmd shim that Node's bare spawn
cannot launch, and `which` does not exist there.
Route both through process.execPath instead of the node_modules/.bin
shims. Node refuses to spawn .cmd or .bat without shell: true, and
shell: true is the wrong escape hatch: it concatenates argv unescaped
(DEP0190), splits paths containing spaces, and sends kill signals to
cmd.exe rather than the real child. Spawning Node directly has none of
those properties and is identical on POSIX, so no platform branch is
needed. The same reasoning fixes the published-server npm install,
which passed "npm.cmd" to execFileSync and had been failing with
EINVAL on Windows since Node's CVE-2024-27980 mitigation landed.
Teardown now kills the process tree. A Windows signal reaches only the
direct child, so Metro's and the supervisor's forked workers kept the
listening port and the next run failed to bind.
The daemon startup deadline goes from 20s to 90s. A cold tsx start was
measured at 25s on Windows and exceeded 60s under load; the loop still
fails immediately on a crashed child, so the ceiling only costs time
that a genuinely slow start needs.
The fake editor and gh binaries the harness puts on PATH gain .cmd
wrappers, because Windows resolves PATH entries by PATHEXT and cannot
execute an extension-less shebang file. The wrappers point at the same
payload file so the recorded command name stays "cursor", "code", or
"gh" on every platform.
The asdf-backed local Elixir relay stays POSIX-only and now says so
before spawning.
* test(ci): follow shared e2e daemon launcher
---------
Co-authored-by: ABorakati <ABorakati@users.noreply.github.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
* fix(settings): preserve settings from newer builds
Strict blob validation treated a single newer setting as corruption and then rewrote defaults. Read settings field by field and retain unrecognized data when materializing or migrating known preferences.
* refactor(settings): unify discarded-value warnings
Emit settings warnings where each stored value is parsed so validation cannot drift from diagnostics.
* refactor(settings): validate persisted settings declaratively
Use one loose Zod schema with field defaults so mixed-version settings remain loadable without parallel validation paths.
* fix(settings): preserve explicit services preferences
Keep compatibility fallbacks from overriding explicit settings and prevent read metadata from reaching migration writes.
* refactor(settings): derive migration persistence types
Let the schema prove normalized settings coverage and carry its persisted shape through migrations.
JavaScript height state competed with native intrinsic layout across draft and agent ownership transitions. Let native inputs own sizing and scrolling while preserving DOM measurement on web.
* 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.
Native Fabric can omit the initial content-size callback and retain stale size after deletion. Keep the common composer surface free of platform branches while native and web adapters own their measurement mechanisms.
* feat(app): open the tab chooser beside the pointer
Keep keyboard-created and empty-pane New tabs as persistent launcher surfaces, while giving pointer users an anchored chooser at the pane-local plus button. Both presentations resolve the same launch catalogue so future ordering and customization cannot drift.
* test(app): follow the anchored tab chooser
The browser helpers still searched inside the pane for persistent-launcher controls after clicking the portalled menu trigger. Exercise the menu through its own roles and stable catalogue IDs.
Equivalent visibility publications were acting as implicit retry commands, so unrelated tab and layout changes could restart failed timeline work. Keep visibility identity stable and reserve immediate retries for explicit user action.
A pairing offer remains valid across daemon restarts, so remove the incorrect rotation guidance and warn users where the durable credential is displayed.
* fix(app): stop a string SVG transform from crashing the app
A root <Svg> is a real React Native view, so react-native-svg rewrites a
*string* `transform` into RN style syntax in `extractTransformSvgView`,
using a PEG parser whose grammar only accepts SVG transform functions.
Upstream lets that parser's SyntaxError escape, so any string it does not
understand takes the whole app down during render:
SyntaxError: Expected transform functions but "n" found.
"found": "n", "location": { "start": { "offset": 0 } }
`none` reproduces that message character for character; so does any other
CSS value (`rotate(-90deg)`, `translate(10px, 0)`). Every sibling
extractor in that file already catches the same error and degrades, so
the asymmetry is the bug: a string that is merely ignored on a <Path>
crashes on an <Svg>.
We feed that path untrusted input. The daemon scans a project for
favicon.svg / icon.svg / logo.svg (packages/server/src/utils/project-icon.ts)
and ships it to the client, where ProjectIconImage hands it to SvgCss.
Running the library's own XML parser over `<svg style="transform:none">`
yields exactly `transform: "none"` on the root, which is why the crash
starts once a host connects and the project icons render, with no
terminal involved.
Note this is *not* reachable from an RN-style array transform: the
extractor returns arrays untouched and only parses strings, so
`style={{ transform: [{ rotate: "-90deg" }] }}` (the context window meter
and the checks ring) was never a factor.
Patch the three builds a consumer can resolve - Metro takes the package's
`react-native` field (src/*.ts), Node and Vitest take main/module - and
register react-native-svg with the postinstall patch runner so the patch
actually applies. The new test pins all three.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDwfsMWoSFfPm2cFXe5noQ
* test(app): cover parsed root SVG transforms
Exercise the dependency patch with root props produced by the library's XML and CSS parsers, including embedded style rules that can arrive through project icons. Keep dependency-patch documentation focused on the reusable install and resolution constraints.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
* fix(app): keep pinned project actions reachable
Preserve an empty project shell after its final workspace is hoisted into the pinned section so project-scoped workspace creation remains available.
Co-authored-by: Agent Op <agentop718@gmail.com>
* test(app): verify pinned project workspace context
---------
Co-authored-by: Agent Op <agentop718@gmail.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>