Commit Graph
1939 Commits
Author SHA1 Message Date
Mohamed BoudraandGitHub b560c1184c test(server): wait for terminal env marker contents (#2715)
The Windows runner could observe the marker after creation but before its contents were written. Wait for the expected value so the test synchronizes on the behavior it asserts.
2026-08-01 00:35:27 +02:00
Mohamed BoudraandGitHub 9d19e90323 Show worktree names in workspace hover cards (#2711)
* fix(app): show worktree names in workspace hover cards

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

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

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

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

* fix(relay): keep pairing state authoritative

* fix(relay): refresh pairing state across transports

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

* fix(relay): surface transport startup failures

* fix(pairing): gate offers for older daemons

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

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

* fix(relay): synchronize config during daemon startup

* fix(relay): preserve mutability for ignored overrides

* fix(cli): use reachable daemon for pairing

* fix(app): redirect invalid pair device routes

* fix(pairing): support remote host device pairing

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

* fix(pairing): validate pairing destinations

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

* fix(pairing): render QR codes across platforms

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

* fix(cli): pair from persisted relay state

Allow explicit relay pairing while the daemon is stopped when relay is already enabled in persisted configuration.
2026-08-01 04:35:06 +08:00
Mohamed BoudraandGitHub d7ced4a77e test(server): avoid commit history fixture timeout (#2707)
The bulk-history cases spawned about 80 Git processes, which could exhaust the per-test timeout on loaded Windows runners. Import each linear history in one Git process while preserving the real repository behavior under test.
2026-07-31 20:25:05 +02:00
800bd2bef1 feat: add host-local custom project icons (#2335)
* feat(projects): add local icons and colors

* refactor(projects): simplify appearance handling

* fix(projects): serialize appearance saves

* test(app): update project appearance expectations

* test: fix project appearance CI expectations

* refactor(projects): organize appearance code

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

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

* fix(desktop): restore Linux development startup

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

* feat(projects): add custom project icons

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

* refactor(settings): scope projects by host

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Providers that report no task keep the previous behavior.

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

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

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

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

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

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

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

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

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

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

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

* feat(claude): read subagent effort from hooks

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

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

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

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

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

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

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

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

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

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

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

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

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

* Generalize provider subagent presentation

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-31 17:22:44 +00:00
Mohamed BoudraandGitHub 1ddca07d57 Show the current pull request after switching branches (#2699)
* fix(git): reconcile pull requests after branch changes

Treat persisted change-request metadata as a branch-bound hint and prefer current tracking configuration when it identifies the checked-out branch.

* fix(git): preserve enterprise fork identity

Use branch-bound metadata to supplement the owner when explicit tracking points at the same head on a self-hosted remote.

* fix(git): honor local branch upstreams

Treat Git remote dot with a merge ref as explicit current tracking so stale PR metadata cannot override a local upstream.

* fix(git): own change request branch bindings

Centralize persisted hint validation and rebinding so renames preserve change request identity while switches invalidate it, including normalized legacy Enterprise owners.

* fix(git): preserve PR identity through workspace renames

Resolve the backing worktree root during rename without adding a Git call, and retain fork owner identity from explicit same-name tracking configuration.

* fix(git): trust current enterprise fork identity

Explicit tracking now derives repository identity from the current remote path, so stale checkout metadata cannot override a repointed fork.

* fix(git): recognize enterprise repository aliases

GitHub repository identity is case-insensitive. Normalize equality checks so differently cased aliases cannot turn base tracking into a fork lookup.

* fix(git): reject ownerless legacy fork matches

Owner-qualified legacy hints only belong to their historical owner-prefixed branch names. A later ownerless branch reusing the same head must resolve from current Git state instead.

* fix(git): keep fork feature lookups local

A feature branch may track the upstream base branch for ancestry even when origin is a fork. Only treat that cross-repository base ref as PR identity when the local branch name identifies the tracked head.
2026-07-31 23:06:10 +08:00
d9b72e1c76 Use OpenCode busy status for background activity (#2696)
* fix(server): ignore OpenCode snapshot diffs when starting autonomous turns

OpenCode stamps its filesystem-snapshot diff onto the user message after
the turn goes idle. Since #2258 Paseo treats an unknown idle user
message.updated as the start of an autonomous turn, so that late metadata
write opened a turn no provider-idle boundary would ever close. The agent
stayed falsely running until the next submission interrupted it.

Reject user message.updated events carrying summary.diffs. Real prompts
never carry snapshot diffs, so genuine autonomous turns still start, and
filesystem snapshots stay enabled for undo and revert.

* fix(server): trust OpenCode busy status for autonomous activity

Mutable message records can receive metadata after execution ends. Treat the provider's busy status as the activity boundary and buffer preceding content until that boundary arrives.

* test(server): cover autonomous provider activity

* fix(server): reconcile OpenCode wakes from busy status

# Conflicts:
#	packages/server/src/server/agent/providers/opencode-agent.ts

* fix(server): preserve background activity boundary after stops

* fix(server): keep idle events outside background activity

Only provider execution status proves a new activity boundary. Reconcile adopted sessions once at subscription time instead of retaining unowned idle events for a future run.

* fix(server): order adopted session reconciliation

Live runner-status events supersede an in-flight status snapshot. Treat busy and retry as one active provider state.

* test(server): remove misleading snapshot gate

---------

Co-authored-by: Des Flynn <des@desflynn.com>
2026-07-31 13:58:08 +02:00
Mohamed BoudraandGitHub f3fee73976 Stop OpenCode cancels from killing the next turn (#2662)
* fix(opencode): prevent late aborts from canceling replacement turns

OpenCode aborts are session-scoped, so provider idle alone does not make a session safe to reuse. Keep abort retries owned by the stopping turn and require both abort settlement and provider idle before dispatching the next runner operation.

* fix(opencode): retain the stop barrier through provider idle

Keep the stopping operation authoritative until both its session-scoped abort and provider-idle evidence settle, including stale idle-session interrupts. Also keep the real drift fixture credential-allowlisted and wait for command output pipes to drain.

* test(opencode): launch Windows fixture shims safely

* fix(opencode): recover stop observation safely

* fix(opencode): preserve autonomous wake through stop

* fix(opencode): retain deferred wake completion

* fix(opencode): serialize stop boundary replay

* test(opencode): use shared real e2e lane

* fix(opencode): own stops started during replay

* fix(opencode): retry stop observation on demand

* fix(opencode): follow replacement stop boundaries

* fix(opencode): retain wakes during stop recovery

* fix(opencode): hold replay turns behind boundary

* fix(opencode): filter canceled reconnect output

* fix(opencode): bind status evidence to stop

* fix(opencode): acknowledge owned cancellation

* fix(opencode): reconcile stale busy evidence

* fix(opencode): bind replay terminals to wakes

* refactor(opencode): own the stop boundary in one explicit state

The run boundary had grown into a mutable flag bag plus a raw-event
buffer that was replayed and re-classified after the fact. Each new
interleaving needed another predicate, and the classification kept
losing autonomous wakes.

Restore stopping as a turn-state variant that owns exactly two things:
the session-scoped abort it issued (with its single owned retry) and the
canceled run's terminal. Runner operations wait on both; a failed abort
fails closed. Provider status reconciliation now ends the stop through
the same transition an SSE terminal takes.

The canceled run's residue is suppressed only up to that terminal.
Events published afterwards are a new run by construction and take the
normal live path, so a wake no longer waits for abort settlement and
cannot be dropped by a retention heuristic.

* fix(opencode): emit cancellation before adopting the next run

The acknowledgement ran off a promise combinator, so a wake adopted from
the event right after the terminal could reach subscribers before the
canceled turn's turn_canceled. Acknowledge synchronously from whichever
proof lands first instead.

* docs(opencode): attach the stop doc comment to its interface

* fix(opencode): retry the stop in progress instead of opening a second

Pressing Stop again after an abort failed built a detached boundary: its
fresh abort could succeed while the canceled turn kept waiting for an
acknowledgement, so the daemon sat out the full cancel timeout before
force-canceling. A repeated Stop now replaces the abort owned by the
stop already in progress, which is what carries the acknowledgement and
what lifts the fail-closed gate.

* fix(opencode): accumulate every abort a stop has issued

A repeated Stop replaced the stop's abort settlement instead of adding
to it, so a first request still in flight — the slow-teardown case the
cancel cap exists for — no longer held the gate. The replacement could
start while that request was live and get cancelled by it, which is the
race this boundary exists to close. Reuse now waits for every abort the
stop issued, while only the newest can hold the gate closed.

* fix(opencode): scope abort settlement to the session, not the stop

Abort is session-scoped, so its settlement has to be too. Tracking it on
the stop meant a new stop boundary silently dropped an older request
that was still in flight, and that request could still cancel whatever
run started next. The session now accumulates every abort it has issued;
the stop is left owning only the run it is stopping.

* fix(opencode): recheck the abort gate after waiting on it

A waiting prompt read the settlement once, so a Stop issued while it
waited was invisible to it: the older abort would settle, the prompt
would start, and the newer one could still cancel it. Drain until the
settlement observed is the current one.
2026-07-31 12:08:43 +02:00
Mohamed Boudra 6fc491e622 chore(release): cut 0.2.5 2026-07-30 13:22:40 +02:00
Mohamed Boudra 2f913736fa chore(release): cut 0.2.4 2026-07-30 01:48:57 +02:00
Mohamed Boudra c6df5ffd4d Revert "Carry local files and shared state into new worktrees (#2419)"
This reverts commit cbbf6c1684.
2026-07-30 00:37:52 +02:00
Mohamed BoudraandGitHub 20e46e3e3f Restore project grouping across hosts (#2565)
* fix(projects): restore cross-host grouping

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

* fix(projects): keep ambiguous clones separate

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

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

* fix(projects): strengthen grouping identity

* test(app): update project host shape contract

* fix(projects): preserve selected host identity

* fix(projects): close grouped routing gaps

* fix(projects): stabilize legacy subproject grouping

* fix(projects): preserve selected project identity

* fix(projects): isolate legacy host identity

* fix(projects): preserve symlinked bootstrap roots

* fix(projects): reconcile grouped custom names

* fix(projects): stabilize settings and path identity

* fix(projects): normalize default remote ports

* fix(projects): harden host and remote identity

* fix(projects): preserve host-scoped actions

* fix(projects): preserve remote path semantics

* fix(projects): model SCP remote semantics

* fix(projects): keep settings on routed host

* fix(projects): preserve SSH URL path semantics

* fix(projects): separate identity from presentation

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

* fix(projects): harden grouped project routing

* test(projects): expect framed settings route

* fix(projects): preserve exact placement identity

* fix(projects): harden grouping identity and routing

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

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

* fix(projects): prevent project key collisions

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

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

* fix(projects): preserve project key compatibility

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

* fix(projects): harden project reference identity

* fix(projects): retain archived reference errors

* fix(projects): scope legacy paths to hosts

* test(app): model persisted project keys

* fix(projects): retain absolute scp users

* fix(projects): resolve legacy mutation aliases

* fix(projects): preserve conservative identity boundaries

* fix(projects): preserve legacy placement identity

* test(projects): assert legacy placement identity

* test(projects): expect canonical Windows keys

* fix(projects): separate bootstrap identity fields

* fix(projects): scope legacy paths by host

* fix(projects): stabilize opaque project identity

* fix(projects): preserve remaining opaque identities

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

* fix(projects): preserve opaque selection keys

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

* fix(projects): canonicalize framed identities

* test(projects): isolate repeated remote fixtures

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

* fix(projects): reconcile hydrated project references

* fix(projects): preserve archived project identity

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

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

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

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

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

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

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

* fix(projects): keep derived project identity stable

Four gaps left by shrinking the key derivation:

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

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

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

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

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

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

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

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

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

* fix(app): preserve oversized diff state

* fix(diff): stay within relay frame limit

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-30 00:48:05 +08:00
Mohamed BoudraandGitHub 16bbead5a4 Stop Windows server CI from flaking on stderr timeouts (#2613)
* test(server): make stderr timeout coverage deterministic

The timeout assertion raced Windows child-process scheduling and stderr pipe delivery. Buffer stderr before starting the timed request so the test isolates the transport behavior it intends to verify.

* test(server): synchronize stderr timeout test in parent

Use the injected process adapter to populate stderr through the parent-side stream before starting the timeout. This removes cross-pipe ordering from the test.
2026-07-29 18:11:47 +02:00
paseo-bot[bot]GitHubpaseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>Mohamed BoudraClaude Opus 5
ee3a9b013b Support repository search on older GitHub CLI versions (#2611)
* fix(github): support repository lists from older gh versions

GitHub CLI versions before 2.28 reject the visibility JSON field before returning repository data. Retry that specific failure with the older isPrivate field while preserving exact visibility on newer clients.

* fix(github): stop requesting repository visibility

Repository cloning does not use visibility. Leave the released response field optional and keep repository discovery on JSON fields supported by older GitHub CLI versions.

* fix(github): keep the visibility enum on the wire, derive it from isPrivate

Restores the required visibility enum in the search-repositories response
schema. Omitting it broke old clients, which reject the whole response.

The daemon still never asks gh for visibility -- that JSON field does not
exist before gh 2.28 and Debian Bookworm ships 2.23. It asks for isPrivate,
supported on every version, and maps it to public/private.

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

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 18:05:59 +02:00
Mohamed BoudraandGitHub 891dd5a813 feat(providers): expose agent cwd to provider processes (#2563) 2026-07-29 16:42:42 +02:00
Slava GoltserGitHubpaseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
b897c59e9c fix: preserve context window data when turn_completed lacks usage (#2494)
* fix: preserve context window data when turn_completed lacks usage

Merge incoming usage into agent.lastUsage instead of replacing it
wholesale. Some providers (Claude autonomous turns, OpenCode) emit
turn_completed without a usage payload, which previously wiped the
contextWindowMaxTokens/contextWindowUsedTokens accumulated during
streaming usage_updated events. The result was the context window
meter disappearing from the composer once the agent finished work.

* test(server): preserve usage after autonomous completion

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-07-29 22:10:18 +08:00
Mohamed BoudraandGitHub acb8506d1f Revert the timeline optimistic and pagination rework (#2596)
* Add npm release track invariant and beta dist-tag procedure

* Revert "Keep older chat history and image previews stable (#2490)"

The timeline optimistic/pagination rework needs more work than main releases can wait for. Reverted together with #2484; both are reapplied on integration/timeline-optimistic-rework.

* Revert "Stop completed turns from appearing stuck (#2484)"

See the #2490 revert. #2490 built on this submission-authority change, so the two revert and reapply as a pair.
2026-07-29 13:35:43 +02:00
Matt CowgerandGitHub ab24070075 fix(server): suppress Pi interruption stream error (#2311)
* Handle Pi aborted responses after interruption

* fix(server): suppress late Pi interruption terminal response
2026-07-29 18:41:09 +08:00
Mohamed BoudraandGitHub fab975a059 Keep idle agents and their background work alive (#2590)
* fix(server): keep idle agents resident

Remove time-based runtime collection so background work and subsequent prompts are not destroyed during idle periods. Runtimes now close only through explicit lifecycle actions.

* fix(server): preserve explicit close coordination

* fix(server): support partial agent manager adapters
2026-07-29 18:36:44 +08:00
Mohamed BoudraandGitHub 504b687f89 Keep older chat history and image previews stable (#2490)
* fix(app): keep timeline history and previews stable

Treat persisted timeline replicas as display-only so authoritative pagination always comes from the daemon. Require renewed user intent between older-history pages unless the viewport genuinely remains at the history edge.

Model assistant image acquisition as loading, loaded, or failed so recreating a preview URL cannot be rendered as an error.

* fix(app): keep file image previews current

* fix(app): stabilize history pagination edges

* fix(app): recover transient timeline loads

* fix(app): retain assistant image previews

* Stabilize history pagination lifecycle

* Stabilize history settlement and image previews

* Retry file images after reconnect

* Keep active previews and pagination requests alive

* Replace image hook tests with typed ports

* Integrate timeline hydration with submission authority

* Harden pre-hydration timeline reconciliation

* Make rewind E2E use real scroll intent

* Preserve live timeline state through hydration

* Keep mounted image attachments retained

* Protect preview persistence and lifecycle hydration

* Preserve idless live assistant continuations

* Preserve timeline rows across delayed hydration

* test(app): cover real assistant image files

* Preserve assistant tool ordering during hydration
2026-07-28 22:29:03 +02:00
Mohamed BoudraandGitHub bbf3d0f9cc Keep plan approval focused on the latest proposal (#2534)
* fix(server): replace stale plan approvals

Synthetic plan approvals accumulated across planning turns because each proposal used a new permission ID. Dismiss the current proposal only after a follow-up prompt is accepted, and enforce one pending proposal when a newer plan arrives.

* fix(server): close plan approval submission race

Deactivate the existing proposal before starting a revision so it cannot be implemented concurrently. Restore it when submission fails, while preserving any newer approval emitted during the request.

* fix(server): deactivate plans before prompt setup

Start the plan-dismissal transaction before any asynchronous prompt setup so a stale proposal cannot be implemented from another client while the revision is preparing.

* fix(server): make prompt dismissal final

Treat prompt submission as the dismissal event instead of compensating after failures. This avoids resurrecting stale plans across ambiguous provider outcomes and manager failure cleanup.
2026-07-28 17:10:32 +02:00
Mohamed BoudraandGitHub 963d4f9240 Configure agent thinking from the CLI (#2533)
* feat(cli): update agent thinking from the CLI

* fix(cli): harden agent thinking updates

* feat(cli): configure thinking for schedules

* fix(cli): report applied thinking updates

* fix(cli): report current agent thinking state
2026-07-28 22:30:50 +08:00
Mohamed BoudraandGitHub fdee3236f7 Speed up server CI tests (#2537)
* perf(ci): reduce server test latency

Server test files use isolated resources, so they do not require suite-wide serialization.

* fix(ci): scope server test parallelism to unit suite

Keep real-provider and local-resource suites serialized because they may share user configuration and account limits.

* fix(terminal): isolate zsh runtimes by process

Prevent concurrent daemon and test processes from deleting or replacing shell integration files used by another process.

* fix(ci): preserve required matrix check names

GitHub evaluates job conditions before expanding a matrix. Expand active matrices first and gate their expensive steps so required check contexts are always reported. Allow superseded runs to cancel while retaining fail-open path detection.
2026-07-28 21:51:12 +08:00
Jason@HNDandGitHub f0d7eeb98c fix(quota): restore Grok Settings usage for current CLI auth/billing (#2353)
* fix(quota): restore Grok Settings usage for current CLI auth/billing

Grok CLI no longer stores a top-level access_token or usage.creditUsage.
Read nested auth key tokens and config.used.val so Settings → Usage
shows monthly credits again. Keep legacy shapes and env tokens working.

Fixes #2352

* fix(quota): make Grok auth-file tests work on Windows

Inject homeDir into GrokQuotaProvider like Kimi so nested
~/.grok/auth.json tests do not depend on os.homedir() which
ignores $HOME on Windows (USERPROFILE).
2026-07-28 18:49:21 +08:00
Mohamed BoudraandGitHub f91a984348 fix(claude): show a single 1M-context Opus 5 model (#2497) 2026-07-28 18:18:21 +08:00
cbbf6c1684 Carry local files and shared state into new worktrees (#2419)
* feat(server): support symlink worktree includes

* refactor(server): simplify worktree include handling

* fix(server): constrain worktree include traversal

* fix(server): clean up failed worktree branches

* fix(server): skip missing worktree include entries

* fix(server): make worktree includes best effort

* fix(server): skip failed worktree includes

* fix(server): harden worktree include planning

* fix(server): preserve reused worktree result shape

Materialization reports describe one creation attempt, not durable worktree identity. Carry them beside the worktree so newly created and reused results keep the same stable shape.

* fix(server): harden worktree include boundaries

Replace directory snapshots exactly, keep canonical Git metadata protected, report recovery skips, and only roll back branches owned before worktree creation.

* fix(server): follow safe include aliases

Traverse canonical in-checkout directory links during glob planning and treat coded revalidation failures as per-entry skips.

* fix(server): make include preflight race safe

Create fetched checkout refs atomically, retain overlapping copy entries for independent fallback, and protect the full managed-worktree base.

* fix(server): preserve staged recovery state

Validate completed directory snapshots, retain backups after failed restoration, and derive atomic ref guards from the repository object ID width.

* fix(server): preserve partial include progress

Keep safe glob matches, enforce recursive-directory types, validate staged roots, and retain OID guards through rollback.

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-28 03:18:40 +02:00
Dmitry SinevandGitHub b55acfc60f fix(omp): accept nullable model context windows (#2406) 2026-07-28 00:28:30 +02:00
Derek PerezandGitHub 5bd317205c fix(omp): expose injected Paseo tools directly (#2418) 2026-07-27 23:44:25 +02:00
维她命@andGitHub 1c8fabd293 fix(server): discover Codex project skills from cwd (#2423) 2026-07-27 22:12:22 +02:00
Mohamed BoudraandGitHub fa1198c2be Stop completed turns from appearing stuck (#2484)
* fix(app): stop completed turns from appearing stuck

Turn activity was inferred from a user-message row flag, so a stale duplicate row could keep the working footer active after the turn ended. Track submission lifecycle separately and route every send path through the shared submission flow.

* fix(app): guard pending message identity

* test(app): measure submission layout independent of scroll

* fix(app): keep submitted messages consistent through reconnects

Track each in-flight send until its own RPC establishes acceptance, and keep canonical timeline placement authoritative. Restore legacy cached rewind IDs during cache deserialization.

* fix(app): close canonical submission races

* test(app): cover canonical submission races in browser

* fix(app): keep replacement submissions authoritative

Keep current pending rows out of legacy cache migration and leave ambiguous terminal lifecycle events to the daemon snapshot. This prevents fabricated rewind identities and stale completion events from marking replacement turns idle.

* fix(app): keep submitted messages stable across sync

Give submission transport and canonical timeline ingestion separate authority. Preserve every unresolved local send across replacement, reconcile provider identity once, and bridge RPC acceptance to authoritative running state without deriving lifecycle from timeline rows.

* fix(app): settle submissions in either acknowledgement order

Complete submission transactions when RPC and provider acknowledgement arrive in either order. Cache only transaction-owned local rows as transient data, preserve canonical ID-less prompts, and invalidate ambiguous legacy display caches instead of inventing provider identity.

* fix(app): keep agent visible during history handoff

Running and terminal updates could clear create continuity before the initial authoritative timeline arrived, leaving a streaming agent behind the loading screen. End the handoff only when authoritative history is applied.

* fix(app): settle attachment-only submissions

Canonical providers can acknowledge image-only prompts with empty text. Reconcile those events by client identity without rendering a blank canonical row.

* fix(agent): settle out-of-band message submissions

Accepted commands that do not allocate a foreground turn previously had no canonical user acknowledgement. Record the command before its handler runs so submission state and reconnect history converge through the normal timeline producer.

* fix(app): settle out-of-band submissions compatibly

* fix(app): preserve canonical prompt order

* test(app): keep workspace status check timing-independent

The workspace-status scenario asserted footer settlement before initial agent creation had necessarily completed. The dedicated draft-handoff coverage owns that lifecycle contract.
2026-07-27 21:27:10 +02:00
Mohamed Boudra 43cf858c37 chore(release): cut 0.2.3 2026-07-27 19:46:38 +02:00
Mohamed BoudraandGitHub e1b1ca569d Keep large file views from disconnecting (#2482)
* fix(files): keep large downloads connected

Large file reads were emitted as one frame, crossing the physical socket high-water mark and terminating otherwise healthy connections. Stream bounded chunks from one file handle and pace each transfer by its own send completion without globally queueing unrelated traffic.

* fix(files): keep paced relay transfers bounded

Carry send completion through encryption to the physical WebSocket callback, cap growing files at their advertised size, and preserve text classification when a UTF-8 code point crosses the sample boundary.

* fix(files): reject changing transfer snapshots

Abort when a file shrinks below its advertised size, finalize UTF-8 validation for complete samples, and let the physical relay socket remain the sole authority for queued-byte accounting.

* fix(files): validate metadata before transfer

Keep the daemon handshake pending until the ready frame is physically sent, and classify file content with a bounded full-handle scan so streaming preserves the previous binary/text behavior.

* fix(files): detect changing transfer sources

* fix(files): enforce transfer snapshot integrity
2026-07-27 13:52:57 +02:00
Mohamed BoudraandGitHub 80c8a08393 Keep parent agents alive while child work runs (#2458)
* fix(server): keep active agent trees resident

Idle collection treated an inactive parent turn as process quiescence even when descendant work was still running. Protect managed and provider-owned subagent trees, carry descendant activity into the idle window, and use a conservative 30-minute fallback.

* fix(server): close idle collection races

Retain managed ancestry across closed intermediates, refresh descendant state after each awaited close, and invalidate running provider subagents when their runtime is terminated.

* fix(server): serialize agent tree collection

Keep ancestors resident until managed descendants are collected, serialize descendant registration with idle collection, and preserve provider subagent state across hot reloads.

* fix(server): retain closed descendant activity

Carry persisted descendant activity through runtime closure, avoid error children pinning parents, and cancel stale provider children when reload replacement aborts.

* test(server): make descendant expiry deterministic

* fix(server): scope idle child protection

* fix(server): tighten idle tree coordination

* fix(server): reduce aggressive idle cleanup

Keep the mitigation conservative: extend the idle timeout without inferring tree liveness from incomplete lifecycle signals.

* fix(server): keep parents alive during child work

Idle cleanup now respects running managed and provider-native children while retaining a conservative 30-minute fallback.

* fix(server): clear running child state on close

* fix(server): drain child events before close
2026-07-27 12:12:22 +02:00
Mohamed BoudraandGitHub 1d1132de9c perf(relay): reduce encrypted binary traffic overhead (#2480)
Preserve application frame identity through encryption so negotiated binary traffic avoids base64 expansion while mixed-version peers remain compatible.
2026-07-27 12:12:02 +02:00
Mohamed BoudraandGitHub c9fb31f709 Fix Claude 5 context window selection (#2433)
* fix(claude): select the correct Opus 5 context window

Bare Opus 5 IDs resolve to 200K behind third-party gateways. Expose explicit context variants and require a Claude Code version that recognizes the model.

* fix(claude): add Fable and Sonnet context variants

* test(cli): expect Claude context variants

* test(cli): compose Claude catalog expectations

* fix(claude): preserve catalog on version probe failure

* fix(claude): preserve parsed context variants

* chore(release): cut 0.2.2
2026-07-26 10:03:44 +02:00
Mohamed Boudra 8409e237ed Merge v0.2.1 release into main 2026-07-24 22:20:25 +02:00
Mohamed BoudraandGitHub be52347d67 Let Hub finish the executions it starts (#2395)
* feat(server): control Hub execution lifecycle

* fix(server): archive only execution-owned worktrees

* fix(server): serialize Hub execution control after create

* test(server): compare Hub worktree paths canonically

* fix(server): make absent Hub controls idempotent

A Hub can own an execution before agent creation materializes. Treating an absent daemon-scoped record as already stopped or archived lets finality complete without revealing or affecting another daemon's execution.
2026-07-24 22:03:33 +02:00
Jason@HNDandGitHub 830c9b62c4 fix(server): retry hub test temp cleanup on Linux ENOTEMPTY (#2233)
removeRoot() already retries EBUSY/ENOTEMPTY on main; also retry EPERM
and document the Linux CI teardown race.

Closes #2207
2026-07-24 20:39:02 +02:00
Mohamed Boudra 36f38245ca chore(release): cut 0.2.1 2026-07-24 19:40:41 +02:00
Mohamed Boudra 7ef3376b62 feat(claude): add Opus 5 model 2026-07-24 19:37:27 +02:00
a5942ef2de fix(omp): limit thinking levels to model's reported efforts (#2171)
* fix(omp): limit thinking levels to model's reported efforts

The omp provider exposed all six thinking levels (off/minimal/low/
medium/high/xhigh) whenever a model had reasoning enabled, ignoring the
per-model thinking config that omp reports via RPC. The OmpModelSchema
didn't parse the thinking field at all, so the model's efforts subset
and defaultLevel were discarded.

Parse model.thinking (efforts/defaultLevel/effortMap) in OmpModelSchema
and filter the thinking options in mapOmpModel to only the model's
reported efforts. The default is the reported defaultLevel when it's
in the filtered set, otherwise the first (lowest) effort. Older omp
versions that don't report thinking.efforts keep getting the full set.

Also fix createSession to not hardcode 'medium' as the thinking fallback
when the client doesn't send a thinkingOptionId — pass undefined so omp
uses its own model default instead of overriding it with a level that
may not even be in the model's effort set.

* test(omp): drop --thinking medium from provider-registry launch assertions

The thinking-level filtering PR changed createSession to pass undefined
instead of "medium" when no thinking option is selected, so buildOmpLaunch
no longer emits --thinking. agent.test.ts was updated but provider-registry.test.ts
still expected --thinking medium in two OMP launch assertions, failing server-tests
on ubuntu and windows.

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-24 21:54:46 +08:00
73e290bb57 fix(server): name both refs in the base ref mismatch error (#2161)
The base ref mismatch error printed the same value on both sides, so it
always read "expected master, got master". All four throw sites computed
`baseRef = compare.baseRef ?? resolvedBaseRef`, but the guard only fires
when the caller passed a ref, so `baseRef` and the "got" value were the
same string by construction. The value that actually differs -- the
stored `baseRefName` from the worktree metadata -- was never shown,
which left the error undiagnosable from the UI alone.

Build the message in one place and have the four guards throw it, naming
the refs `stored` and `requested`. The wording deliberately avoids
"expected": a caller's ref can be stale, but the stored ref can equally
be the wrong one, so the message states both facts and leaves the
diagnosis open.

The guards stay where they are. Only the message moves, so the condition
that fires each throw is still visible at the call site, and both refs
are narrowed to plain strings by the time the message is built.

Throw conditions are unchanged; only the message text differs.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:17:55 +08:00
e22c85373c fix(server): keep the client port in X-Forwarded-Host (#2288)
* fix(server): keep the client port in X-Forwarded-Host

The service proxy stripped the port when setting X-Forwarded-Host, so a
workspace service that derives its public origin from forwarded headers
emitted absolute URLs pointing at port 80. Opening a service at
http://api--branch--repo.localhost:6767/ and letting it redirect gave
back a Location without the port, which the browser followed to nothing.

Host was already forwarded intact, so services reading Host were fine.
Nothing rewrites Location on the way back, so the wrong URL reached the
browser unchanged.

The strip was not localhost-only: buildPublicServiceProxyUrl preserves
an explicit port in publicBaseUrl, so a public alias on :8443 hit the
same path. It was a no-op only on the default-port public case.

Forward the authority verbatim instead, and add X-Forwarded-Port under a
never-invent, never-clobber rule: report only a port observed in the
Host header, and leave any value an upstream proxy already set alone.
Deriving it from the scheme would overwrite nginx's port on a
non-default listener, and the upgrade path's hardcoded "http" would turn
a correct 443 into 80. An empty inbound value carries no port, so it
does not count as a value to preserve; out-of-range ports are dropped
rather than passed on, since the authority is client-supplied.

The header block existed in four copies. Collapse them: the subsystem
now delegates to createScriptProxyMiddleware and
createScriptProxyUpgradeHandler (passing passthroughUnknown explicitly,
since the factory defaults it to true), and both remaining call sites
share one buildForwardedHeaders helper.

Adds the first tests to cover any x-forwarded-* header on this path,
driven over a real proxy with an upstream that echoes what it received.

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

* fix(server): let the observed authority win over a forged port

X-Forwarded-Port deferred to an inbound value, so a client connecting
directly could send Host: svc.localhost:63735 with a forged
X-Forwarded-Port: 443 and have services build URLs for 443. Frameworks
apply X-Forwarded-Port after X-Forwarded-Host, so the forged value won.

This was inconsistent with the line above it: X-Forwarded-Host is
overwritten with the real authority unconditionally, and there is a test
asserting that. Both headers carry the same trust, so both now follow
the same rule.

First-hand observation wins; an inbound value is a fallback, never an
override. A port parsed from Host replaces any inbound X-Forwarded-Port.
When Host carries no port there is nothing to observe, so a proxy's
value survives untouched: the nginx-on-:8443 case, where $host drops
the port and X-Forwarded-Port is the only source.

The empty-inbound-value special case is gone; always overwriting when we
have an observation covers it with one branch less.

Reported by Greptile on #2288. Not a regression: before this branch an
inbound X-Forwarded-Port passed through untouched, so the forged value
already reached services.

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

* docs(server): correct the forwarded-port comment and note the limit

The comment claimed the parsed port was "the port the client really
connected on". It is not: parseHostHeaderPort reads the Host header,
which the client writes, and route lookup normalizes the port away
before matching, so any value reaches the proxy. The only observed
port would be req.socket.localPort, which this code does not use.

Replaced it with the actual reason the Host port wins, which is keeping
x-forwarded-host and x-forwarded-port from disagreeing: the former is
set from Host, and frameworks apply the latter afterwards, so a
mismatched inbound value would silently override the forwarded
authority.

Added a LIMITATION note recording that the forwarded authority is not
authenticated, that inbound x-forwarded-port is not checked against
trustedProxies, and that closing this needs that config threaded into
the subsystem and the upgrade path, which has no Express trust context.
Both predate this branch: Host has always carried a client-chosen port
and an inbound x-forwarded-port has always passed straight through.

Docs gain a matching section telling service authors to pin their public
origin in configuration rather than derive it from request headers.

No behavior change. Raised by Greptile on #2288.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:17:16 +08:00
99440201bd fix(server): show Claude model-scoped weekly usage limits (#2303)
The Claude usage card showed only Session and Weekly. Anthropic moved
model-scoped weekly limits out of the top-level `seven_day_<model>` keys
into a `limits[]` array, and `seven_day_opus` / `seven_day_omelette` now
return null, so the scoped bar silently disappeared. Nothing errored, so
the card looked healthy while under-reporting.

Scoped limits are now read from `limits[]`, restoring a `Weekly · Fable`
bar. Session and all-models weekly keep coming from the top-level keys,
which is also where the Claude CLI reads them from.

A response mid-migration can describe one limit twice, as a legacy key
and as a `limits[]` entry, so both shapes normalize into a single
`ScopedLimit` and one predicate decides whether two are the same limit:
same dimension, same id when both carry one, else same normalized name.
That is the only comparison in the file. Comparing display labels would
conflate a surface with a model of the same name, and collapse ids that
differ only by punctuation.

The `limits[]` entry supplies identity, since that is the shape the API
is migrating towards. Its `percent` and `resets_at` are nullable, so
each field falls back to the legacy twin rather than discarding a value
the response did carry.

Legacy scoped windows adopt the same id scheme, so `weekly_opus` becomes
`weekly_model_opus`. Window ids are internal React keys, not
user-facing, and unifying them is what lets a limit keep one id
whichever shape of the response carried it.

Two supporting changes:

- `limits[]` entries are validated one at a time. The response goes
  through a single parse, so one malformed entry would otherwise throw
  and take the windows that already parsed with it.
- The provider stores the logger it was already handed and warns when a
  response parses but yields no windows, when a scope resolves to no
  name, and when an entry is unparseable. This failure was silent at
  every level, which is why it went unnoticed. Warn rather than debug,
  because file logging defaults to info.

Scoped windows render even at 0% and inactive, so a bar does not appear
and disappear between refreshes.

Closes #2302

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:16:05 +08:00
Mohamed BoudraandGitHub 055db7454d Stop stale client sockets from exhausting daemon memory (#2169)
* fix(server): bound stale websocket connections

Track application ownership per physical socket and terminate abandoned or backpressured transports without disrupting replacement connections.

* fix(server): tighten relay backpressure accounting

Count pending encryption and transport backlog together, and release socket leases before forced termination.

* refactor(server): reuse client heartbeat for socket liveness

* refactor(server): remove unused websocket close codes
2026-07-24 15:14:37 +02:00
21404fbdec feat(cli): manage workspace scripts (#1992)
* feat(cli): manage workspace scripts

* docs: document workspace script management

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-24 21:12:14 +08:00
Mohamed Boudra d98c5e77f7 chore(release): cut 0.2.0 2026-07-24 13:27:13 +02:00
Mohamed BoudraandGitHub afcd972dd0 Keep terminal pairing QR codes scannable (#2381)
* fix(pairing): keep terminal QR codes scannable

Prompt framing could wrap dense QR codes and alter long pairing links. Print pairing instructions directly, add the standard quiet zone, and suppress codes that cannot fit without auto-wrapping.

* fix(pairing): contain terminal QR background
2026-07-24 18:47:23 +08:00