Commit Graph
4944 Commits
Author SHA1 Message Date
Mohamed Boudra d42e0ca00c fix(app): preserve provisional syntax at cached reveal boundaries 2026-10-15 15:32:02 +02:00
Mohamed Boudra eedf916023 fix(app): preserve native prose entities and links while bounding preparation cost 2026-10-15 15:32:02 +02:00
Mohamed Boudra 7023d20a5b fix(app): preserve markdown source and file actions in native prose
Prepare original source blocks with source-position edits from the assistant parser. Keep reference scope and live/history projection consistent, and retain link text when dispatching workspace file actions.
2026-10-15 15:32:02 +02:00
Mohamed Boudra 5bf0e4b588 fix(app): preserve inline image formatting and reference scope 2026-10-15 15:32:02 +02:00
Mohamed Boudra 9ef289e7f4 fix(app): allow additive task lists and admonitions in native prose 2026-10-15 15:32:02 +02:00
Mohamed Boudra 83061e0312 fix(app): preserve markdown semantics and render limits in prose runs 2026-10-15 15:32:02 +02:00
Mohamed Boudra 75907b360b fix(app): render selectable native prose with enriched markdown 2026-10-15 15:32:01 +02:00
Mohamed Boudra d8f79925d1 fix(app): group native assistant prose into selectable runs 2026-10-15 15:32:01 +02:00
paseo-bot[bot]andGrant Ammons 80badb3b8f fix(opencode): answer multi-select v2 questions from the question card (#6149)
Closes #6135

Co-authored-by: Grant Ammons <grant@grant.dev>
2026-10-15 15:15:19 +02:00
71f575e16a Keep malformed Claude question answers retryable (#6936)
* fix: reject a question permission that would deliver no answer

Answering an `AskUserQuestion` permission in any shape but
`updatedInput.answers` keyed by one of its questions resolves as `allow` and
delivers nothing. The waiting agent is told "The user did not answer the
questions." — an affirmative falsehood rather than silence, so neither side can
see that anything failed. The responder sees success; the agent believes the
human declined and acts on that.

The field it is most natural to reach for cannot work. `selectedActionId` is
`z.string().optional()` with no membership check, and is read only for `plan`
kinds; a question advertises `actions: undefined`, so there is no action id to
name in the first place.

It is also unrecoverable. `respondToPermission` deletes the request from
`pendingPermissions` on entry, so the malformed answer consumes it and the retry
fails with "No pending permission request". The guard runs before that delete,
and `AgentManager` drops its own copy only once the call resolves, so both maps
survive the throw and the request stays answerable.

The answer contract is per provider, so the guard is in the Claude provider
rather than in `AgentManager`. Claude keeps an answer only when its key is a
question's full text or its header and its value is a non-empty string; Codex's
`mapCodexQuestionResponseByHeader` reads headers only and deliberately selects
each question's first option when nothing maps, which a shared guard would have
rejected; OpenCode reads headers only as well.

`resolveClaudeAskUserQuestionAnswers` is extracted from
`normalizeClaudeAskUserQuestionUpdatedInput` with no change in behaviour, and
both it and the guard now use it. So "would this deliver anything" is answered
by the code that does the delivering, rather than by a second copy of its rule —
which matters more than it looks, because that rule has two details easy to
restate wrongly: `readNonEmptyString` tests `value.trim()` for emptiness but
returns the *original* string, so a question named `" Which colour? "` is keyed
by that padding; and an empty `updatedInput.questions` array is a list that wins
over the request's own questions and maps nothing, not an absent one.

Verified on a live daemon over the MCP `respond_to_permission` path against a
seat whose question text is literally `" Which colour? "`: `selectedActionId`,
the trimmed key, an empty echoed question list and a numeric answer are each
rejected with the request still pending, and
`{"answers":{" Which colour? ":"Ochre"}}` then reaches the agent.

One known limit: the WebSocket path (`session.ts handleAgentPermissionResponse`)
catches the throw and emits an `activity_log` error rather than failing the
call, because a permission response is modelled as a notification. So
`paseo permit allow` still prints `allowed` and exits 0 for a rejected shape.
The destructive half is closed on every path — the request survives and the
agent is never told a falsehood — but closing the reporting half needs a
protocol change.

* test(claude): cover the question-answer-required retry path

A non-deliverable answer to an AskUserQuestion permission is rejected, but
the pending request survives it: a corrected retry against the same request
id is accepted, and the waiting agent receives the answer.

Mutation testing on a fork found that the existing suite does not pin this
ordering — moving the deliverability check to run after the request is
deleted from pendingPermissions still leaves every existing test green. This
test reds against that change.

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

---------

Co-authored-by: Thomas Van <minhthong@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: paseo-bot <paseo-bot@users.noreply.github.com>
2026-10-15 14:21:03 +02:00
paseo-bot[bot]andplatanus-kr 0708b8295c Preserve explicit Claude session names on import (#6930)
* fix(server): name an imported Claude agent after its /rename

Importing a Claude session named the agent after its first prompt even when the
session had been renamed in the CLI, so the name had to be set again by hand.
#4216 fixed the Import Session listing; the imported agent still kept the
prompt.

Read the last usable `custom-title` from the transcript and pass it as the
imported config title. The record is re-emitted on every save, so searching
backwards from the end of the file finds the current name without walking a
transcript that can reach tens of megabytes.

Only an explicit rename is carried over. Agents are named after their first
prompt line since #1563, so adopting the generated `ai-title` here would put
that LLM summary back.

Refs #2513

* refactor(server): drop unused generality from the Claude title reader

readLastClaudeTitle took content, type and field so it could read both
custom-title and ai-title records, which the listing needed before #4216 took
that over. Only the custom-title read is left, so the two extra parameters are
dead flexibility that also broke the single-object-parameter rule in
packages/server/CLAUDE.md.

Collapse it to readLastClaudeCustomTitle(content) and pull the record type into
a constant so the needle and the type check share one literal.

Hooks skipped: the repo-wide typecheck fails on two pre-existing Electron
typing errors in packages/desktop/src/main.ts that reproduce with this change
stashed. packages/server typecheck, lint and format all pass.

* test(server): drive the real resume path in the import title tests

The tests assigned an inline arrow function to client.resumeSession to keep
Claude Code from spawning. Importing replays the transcript from disk and never
starts a process, so the stub was never needed. Dropping it runs the real
resume path, which is what docs/testing.md asks for, and removes the arrow
assignment that docs/coding-standards.md rules out.

Hooks skipped: the repo-wide typecheck fails on two pre-existing Electron
typing errors in packages/desktop/src/main.ts that reproduce with this change
stashed. packages/server typecheck, lint, format and the 83 claude tests pass.

* fix(server): narrow the transcript read error instead of asserting it

The ENOENT check cast the caught value to NodeJS.ErrnoException to reach
`code`, which is the kind of assertion docs/coding-standards.md rules out:
narrow with `if`, do not silence the compiler. Add the same local type guard
pid-lock.ts already uses and test through it.

Behaviour is unchanged. A value that is not an Error still falls through to
the rethrow, exactly as reading `code` off the assertion did.

Hooks skipped: the repo-wide typecheck fails on two pre-existing Electron
typing errors in packages/desktop/src/main.ts that reproduce with this change
stashed. packages/server typecheck, lint, format and the 83 claude tests pass.

* fix(server): let claudeProjectDirSync resolve the Claude config dir

The import title reader built the config dir itself through os.homedir(),
but os is no longer imported here, so the fallback threw a ReferenceError
whenever CLAUDE_CONFIG_DIR was unset. claudeProjectDirSync already resolves
the same default.

* test(server): give the import title helpers object parameters

* fix(server): preserve imported session renames across history locations

The import title reader must use the same provider environment and transcript
lookup as history replay. Otherwise an imported session has its history but
loses its explicit name when its configuration directory or project changed.

Co-authored-by: platanus-kr <platanus.kr@gmail.com>

---------

Co-authored-by: platanus-kr <platanus.kr@gmail.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-15 13:17:01 +02:00
paseo-bot[bot]andbwestlund17 4d5c3c13d4 fix(acp): preserve slash command argument hints (#6920)
ACP command updates discarded input hints before the autocomplete command list reached consumers. Preserve the advertised hint, retaining the empty hint for commands without input metadata.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: bwestlund17 <brian.westlund@proton.me>
2026-10-15 12:13:18 +02:00
Mohamed Boudra d3efddb0b5 test(server): await provider discovery before readiness assertion (#6994)
Repeated snapshot reads supersede pending launch resolution and keep readiness loading. Await the refresh RPC before asserting the discovered state.
2026-10-15 15:35:14 +07:00
paseo-bot[bot]andAlexander Adam 0ed57187a9 Keep fork owners in Gitea and Forgejo checkout names (#6829)
* fix(gitea): prefix fork PR branches with the owner

Two forks with the same branch name (say patch-1) ended up on the same
local branch. The gitea service had no buildPrLocalBranchName or
defaultCheckoutRefs, github has both.

getPullRequestCheckoutTarget also fell back to refs/heads/<name> on
origin for fork PRs. Thats the base repo's branch, not the fork's, so
a failed refs/pull fetch checked out the wrong branch without an error.

fixes #3515

* refactor(forge): share fork branch naming with github

moved the owner normalizer and the branch name builder to
utils/change-request-checkout.ts, gitea and github both use it.

github changes a bit: a fork PR with an unknown or unusable owner (eg
deleted fork) now gets pr-N/<branch> instead of the bare branch name,
which could collide with a local branch the same way #3515 did.

* fix(gitea): PRs from a deleted fork are still cross repo

the API drops head.repo once the fork is gone, we read that as a same
repo PR and fetched refs/heads/<name> from origin

* fix(gitea): handle AGit PRs and deleted head branches

neither has a branch on origin. forgejo reports refs/pull/N/head as
head.ref for both, so skip the refs/heads fallback and dont track
origin.

also try refs/pull/N/head on upstream for clones where origin is your
fork, like github does. that list lives in the shared util now.

* test: resolver harness uses the real branch helper

plus deleted forks and same repo PRs without an origin branch

* fix(gitea): collapse pull-ref-only heads to pr-N in local branch names

buildForkLocalBranchName still used the raw headRef even when it was
just the pull ref (refs/pull/N/head), which agit prs and deleted head
branches report as their headRefName. that gave local branches literal
names like refs/heads/refs/pull/3/head.

* fix(gitea): surface hasHeadBranch on the pr checkout target

resolveForkCheckoutFacts already knew whether a pr had a real head
branch (agit flow, or head.ref == the pull ref both mean no). now that
signal reaches buildPrLocalBranchName so worktree names and local
branches stop being named after the raw pull ref.

covers same-repo, fork with owner known, and deleted fork all with no
real head branch. added an end to end gitea-service test plus resolver
cases since the worktree slug follows localBranchName too.

* refactor(forge): one head ref kind on the checkout target

hasOriginHeadBranch and hasHeadBranch could describe contradictory
states (cross-repo checkout claiming an origin head branch). collapse
both into headRefKind: "branch" | "pull-ref" and derive origin
tracking in the resolver instead of storing it separately.

Co-authored-by: Alexander Adam <alexander.adam@vade.io>

* fix(worktrees): avoid branch namespace conflicts in fork checkouts

Fork owner prefixes can name an existing branch as a parent, which Git rejects even when the full proposed name is unused. Allocate names against parent and child refs, suffixing the conflicting segment while preserving existing branches and fetched commits.

* test(worktrees): make fork fixture line endings deterministic

Windows Git converts the fixture checkout to CRLF when core.autocrlf is enabled. Set the repository fixture to preserve its written LF bytes so the content assertion checks the fetched patch on every platform. Reproduced the failure locally with inherited core.autocrlf=true and verified the corrected setup against that same configuration.

---------

Co-authored-by: Alexander Adam <alexander.adam@vade.io>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-15 10:10:54 +02:00
Mohamed Boudra a96c892302 fix(website): start the sponsors band heart at the column edge (#6993) 2026-10-15 15:04:10 +07:00
Mohamed Boudra 74282dac12 fix(app): return to chat after fullscreen submission (#6987) 2026-10-15 14:55:19 +07:00
Mohamed Boudra 12066431ad feat(website): head the sponsors band with a heart and one Sponsors title (#6988) 2026-10-15 14:36:12 +07:00
paseo-bot[bot] 4d8bed9ddd Keep Claude's context meter on the active model (#6814)
* fix(claude): keep context meter on the active model

Claude result modelUsage accumulates models across the session. Reading its largest window replaced Haiku capacity with a previous Opus window at turn completion. Select the runtime model entry and retain the seeded limit when that entry is absent or invalid.

* refactor(claude): name context fixture inputs and simplify result usage

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-15 09:13:13 +02:00
paseo-bot[bot]andhadan8977 b2ca7dcf03 Check generated IDs in the daemon upload test (#6810)
* test(server): stop expecting request-derived upload IDs

* test(server): check full uuid shape for upload id

* test(server): format generated upload ID assertion

Retain the contributor correction to the stale request-derived ID expectation while satisfying the repository formatter.

Co-authored-by: hadan8977 <zc-huang23@mails.tsinghua.edu.cn>

---------

Co-authored-by: hadan8977 <zc-huang23@mails.tsinghua.edu.cn>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-15 08:08:40 +02:00
Mohamed Boudra 8b1ba3ca58 Lighter landing sections, FAQ last, followed sponsor links (#6970)
* feat(website): follow sponsor links and lay landing sections on the page background

* feat(website): keep landing card copy to two lines
2026-10-15 12:52:09 +07:00
Mohamed BoudraandClaude e5124b039a Put sheets on one stack and show an agent's facts in its sheet (#6958)
* Put sheet tabs on the one sheet stack

Sheets are one primitive with one stack. A sheet opened from any sheet
stacks on top of it, and closing it uncovers the sheet underneath. The
compact workspace's file and subagent sheets were an in-tree Gorhom
BottomSheet with their own stacking, so a subagent chosen from the
subagents list sheet replaced the list instead of covering it.

- SheetHostScope: a subtree hosts the sheets presented inside it, so the
  sheet tabs draw inside the compact column and read the workspace
  providers while still joining the app-wide stack. Needs Gorhom's
  BottomSheetModalInternalProvider, which the patch exports.
- IsolatedBottomSheetModal takes enableDismissOnClose; false keeps the
  content mounted through a close so the unsaved-changes guard can veto
  it. Its backdrop then closes the sheet rather than dismissing the top
  modal. The Gorhom patch also keeps a dismissing modal's status through
  its final onChange(-1), without which such a modal unmounting while
  presented never left the portal.
- SheetTabs (was WorkspaceSheetHost) renders one IsolatedBottomSheetModal
  per store entry. The store never reorders sheets; a reveal of a covered
  sheet drops the unmodified sheets above it.
- Subagent rows that open a sheet keep the list open (closeOnSelect
  false); rows that open a tab still close it.
- A menu closes when its retained panel goes inactive, instead of hiding
  and reappearing with the panel on the next tab switch.

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

* Show an agent's model and thinking in its sheet, and fit Open as tab in the header rail

A Paseo agent shown in a sheet has no composer, so the facts the composer's
model and thinking controls would show now sit in one line under the sheet
header, the same row provider subagents already get from the daemon. The line
resolves exactly as the controls do (resolveAgentModelSelection), from the
session agent and the providers snapshot, and shows wherever the pane host is
a sheet.

Open as tab becomes the full-screen glyph beside Close, drawn by the shared
SheetHeaderIconButton, so the title keeps its width. The tab switcher sheet is
titled "Workspace tabs" so it reads as what it lists.

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

* Show mode and context usage in the sheet facts line

The line under a sheet's header now carries everything the composer's
controls and context meter show: model, thinking, mode, and used/max context
tokens with the share, each resolved as its control resolves it.

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

* Re-render subagent rows when the workspace's tabs change

A row decides at render time whether choosing it opens a sheet or reveals
a tab, and keeps the list open only for a sheet. The hook behind it only
subscribed to the layout mode, so a row rendered while its agent was a
full tab kept closing the list after that tab was gone: on iOS, promoting
a managed child, closing its tab, and reopening it from the list dropped
the list under the sheet. Subscribe to the workspace's layout too.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-15 12:16:45 +07:00
paseo-bot[bot] 5c108f345c Preserve renames in committed file diffs (#6895)
* fix(git): preserve renames in committed file diffs

Follow the requested destination through rename detection so the patch agrees with commit history instead of showing the whole file as newly added.

* test(git): keep rename fixtures valid across platforms

* fix(git): keep added copies out of committed rename diffs

Detect rename sources across the first-parent commit before narrowing the patch. Follow mode also detected copies, causing added files to open as partial diffs against unchanged files.

* perf(git): share committed rename discovery across file requests

Opening a commit requests all of its file diffs concurrently. Share pending and completed rename scans for immutable commit IDs in a bounded cache, while symbolic refs always read fresh metadata.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-15 07:05:42 +02:00
Mohamed Boudra 9ab99a6e1e feat(website): show sponsors on paseo.sh and at the top of the README (#6956) 2026-10-15 11:19:32 +07:00
paseo-bot[bot] 8ff2424da1 Return checkout diff hunks from subdirectory workspaces (#6882)
* fix(git): return checkout diff hunks from subdirectory workspaces

Checkout paths are repository-relative, but one-shot diff reads used the workspace subdirectory for Git pathspecs. Resolve the worktree root once so patch selection and file reads use the same coordinates as the live subscription.

* test(server): await bounded discard fixture cleanup on Windows

The discard assertions pass but Windows can retain a directory lock during cleanup. Await asynchronous removal with bounded retries after daemon shutdown, preserving failure for persistent locks. Matches the verified cleanup correction in #6912 for #6918.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-15 05:08:39 +02:00
Mohamed Boudra 6db47b3c37 fix(app): keep the keyboard closed after mobile sidebar dismissal (#6945)
Separate composer keyboard eligibility from pane focus restoration. A sidebar dismisses the keyboard without hiding the pane, so its close must not restore editing focus.
2026-10-15 08:59:49 +07:00
paseo-bot[bot] 73221ce5bd fix(app): render local Markdown images with encoded paths (#6877)
Decode assistant Markdown image destinations before resolving a filesystem read. Keep plain filesystem paths and direct image URIs unchanged so Markdown Preview and literal percent filenames are decoded only once.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-15 03:14:42 +02:00
Mohamed Boudra 6ad124758c Stack workspace sheets and tighten sidebar density (#6885)
* Stack workspace sheets and tighten sidebar density

- Workspace sheets stack: opening a target from a sheet covers it, Back and
  drag close only the top sheet, and the header is the sheet's drag handle.
- The workspace sheet reuses the shared sheet handle, background and header.
- Fix the shared sheet handle indicator rendering black.
- Sidebar text returns to semantic sizes; Pinned and Workspaces share one
  section header.

* Pause covered workspace sheets and mock the shared sheet handle in tests

* Export BottomSheetHandle from the browser test Gorhom stub

* Let a stacked sheet's backdrop catch taps on iOS

* Bring a revealed sheet to the top instead of duplicating it as a tab
2026-10-15 06:30:17 +07:00
Mohamed Boudra f3ddf99fe3 fix(app): restore fullscreen composer keyboard motion and clearing (#6902)
* fix(app): keep fullscreen composer in the keyboard dock

* fix(app): preserve composer caret and scope keyboard actions
2026-10-15 06:30:13 +07:00
paseo-bot[bot]andKiraPC 404d08d24b Report failed terminal launches and remove early exits (#6792)
* fix(server): report a terminal whose command cannot start instead of leaving a dead tab

Opening a terminal profile whose command is not installed left a tab that could
never be closed. Two problems sat on the same path.

A terminal that dies during creation was resurrected by its own create. The
worker sends terminalCreated, the createTerminal response and terminalExit in
the same tick, so the parent drains all three before the awaiting create
continuation resumes; that continuation called registerRecord a second time and
put back a terminal the exit had just removed. Removal is owned solely by the
terminalExit event and that event never comes twice, so the record outlived
every close attempt: getTerminals kept listing it, capture returned nothing,
kill was a silent no-op that still reported success, and only a daemon restart
cleared it.

The parent now tracks the ids with a create in flight and keeps the finished
record when an exit removes one of them, so the create continuation returns the
exited session instead of registering a live one. Removal still happens exactly
once, in the terminalExit path.

With the stuck tab gone, a missing command produced nothing at all: no tab and
no message. node-pty spawns directly through execve on POSIX, with no shell, so
a command that does not exist fails inside the spawn helper and surfaces as exit
code 1 with no output -- exactly what `false` produces, so the exit carries no
usable signal. createTerminal now resolves the command before spawning and fails
the create when it cannot be found, so the daemon answers create_terminal with a
null terminal and "<command>: command not found", which the app already renders.
A command that starts and then exits non-zero still opens its tab.

That resolution has to match what execve will do in the child, or a working
profile is refused: it uses the terminal's own environment, built first so the
check sees the per-cwd registered PATH and the entries buildTerminalEnvironment
prepends, and it resolves a command containing a separator against the
terminal's cwd rather than through PATH. Windows keeps resolving inside
resolveTerminalSpawnCommand, which has its own shim handling and fallback.

* fix(terminal): check exact executable names and verify launch output

---------

Co-authored-by: KiraPC <pasqualecarmine.carbone@gmail.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 23:02:39 +02:00
paseo-bot[bot]and见安 6d43a8c5b0 Restore selected changes from subdirectory workspaces (#6777)
* fix(git): discard repository paths from the worktree root

* test(git): cover discard from nested workspaces through daemon RPC

* test(git): close the daemon before removing discard fixtures

Windows CI reaches the discard assertions but cannot remove the observed
repository while the test daemon is alive. Run fixture removal after the
daemon's afterEach cleanup, preserving all behavioral assertions.

Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com>

---------

Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 20:02:03 +02:00
paseo-bot[bot] e1a5664d9b fix(app): keep malformed font settings from breaking the UI (#6695)
Unfinished CSS strings in font preferences consumed the shared theme stylesheet. Reject incomplete syntax at the settings boundary, recover persisted malformed values through the existing default path, and restore rejected edits in the font field.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 19:17:06 +02:00
paseo-bot[bot] b58af43a06 fix(app): render mixed HTML headings without literal tags (#6650)
Keep a matched heading together before splitting its image and text contents. Splitting first stranded the opening heading tag in a text slice without its closing tag.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 18:14:08 +02:00
56448842f0 Download files over the connected host (#6595)
* Let readFile callers disable the request timeout

readFile resolves only when FileEnd arrives, so the default 60 s request
timeout bounds the whole transfer. A large file over a slow relay fails
before it finishes even though every chunk is arriving. Thread an optional
timeout through requestFileExplorer; 0 disables the timer, as the
workspace.create request already does, and a dead connection still rejects
on disconnect.

The relay e2e case reads a 1 MiB file through a real daemon and a local
relay with timeout 0 and compares SHA-256 digests with the source.

* Reject a binary file transfer whose byte count differs from FileBegin.size

concatByteChunks allocated FileBegin.size bytes and copied the chunks in:
a short transfer resolved as a zero-padded file, and a long one threw a
RangeError inside the transport callback and left the request pending.
Count the received bytes at FileEnd and reject the read when they differ
from the announced size.

* Download files over the session socket when no direct connection is active

The download store only knew the HTTP token route and picked the first
directTcp connection in the host profile, whatever connection the host
runtime was using. A relay-only host has none, so every download ended in
"Download host is unavailable." (#543). SSH, socket and pipe hosts failed
the same way, and a stale directTcp entry won over a working relay (#3753).

Choose the transport from the active connection: directTcp keeps the HTTP
token download unchanged; anything else reads the file over the session
socket with the binary frames previews already use, then hands the bytes
to the browser's download manager on web and Electron or writes them to
the cache directory and offers the share sheet on native. Files over
64 MiB are refused before any byte moves, using the existing fileTooLarge
string, and a daemon response without content fails instead of saving an
empty file.

* fix(app): enforce the session download size limit during the read

The token size is sampled before the file is read. Pass the download byte budget through the actual read so a changed file cannot bypass the in-memory limit.

Co-authored-by: Michael Vandesompel <mv@droggol.be>
Co-authored-by: YASICJUNWOO <joonoo3@naver.com>
Co-authored-by: JrDw0 <JrDw0@users.noreply.github.com>

* test: verify TCP downloads and own relay fixture cleanup

---------

Co-authored-by: Michael Vandesompel <mv@droggol.be>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: YASICJUNWOO <joonoo3@naver.com>
Co-authored-by: JrDw0 <JrDw0@users.noreply.github.com>
2026-10-14 17:25:31 +02:00
paseo-bot[bot] 14e6384516 fix(desktop): keep running when launcher console pipes close (#6639)
Desktop logging reaches inherited stdout and stderr before the CLI guards run. A launcher that closes its output reader raised an uncaught EPIPE dialog. Handle closed console pipes at desktop startup while preserving console logging and surfacing other stream errors.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 15:27:12 +02:00
paseo-bot[bot]andRaisal P Wardana cafa04d3fb fix(app): traverse workspaces beyond nine (#6586)
Relative navigation shared the list capped for digit shortcuts, so it wrapped at the ninth workspace. Keep all sidebar targets and cap only numbered badges. Carry forward #4128 with keyboard-routing and browser regressions.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Raisal P Wardana <IEatCodeDaily@users.noreply.github.com>
2026-10-14 13:14:08 +02:00
paseo-bot[bot]and见安 938c4e2632 Show Git diffs for filenames with quotes and control characters (#6583)
* fix(diff): decode quoted Git patch paths

* fix(server): preserve Git paths in diff and commit listings

* refactor(diff): make quoted-path conversions explicit

* test(server): verify quoted filename diffs through daemon RPC

Keep the checkout and commit-file regression on the public client interface, and align the large-file command assertion with NUL-delimited statistics.

Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com>

* test(server): run filename RPC regression in integration checks

---------

Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 12:09:47 +02:00
paseo-bot[bot] d57096a456 Start newly configured workspace services without restarting (#6542)
* fix(server): start newly configured workspace services without restarting

Extend the workspace port plan when paseo.json declares another service, preserving existing peer ports. Serialize extensions and roll back only their new reservations on failure.

* fix(server): serialize service restarts with port plan extensions

Keep concurrent restart and newly added service allocations in the same workspace update transaction so neither overwrites the other.

* fix(server): keep service port update failures scoped and release cancelled reservations

* test(hub): show creation errors in re-enrollment failures

Assert the error field along with success so failed CI reports include the reason agent creation was rejected.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 11:07:06 +02:00
paseo-bot[bot] ada7ba43f0 Keep long custom models removable in provider settings (#6507)
* fix(app): keep long custom models removable

Custom model labels and IDs could consume the row width and push Remove outside the sheet. Allow those text cells to shrink while preserving the button width and short-name alignment.

* test(app): wait for legacy agent creation before checking prompts

Agent-list membership includes initialization. Waiting for finish at that point can return on the initial idle transition before the initial prompt starts. Wait for the matching creation response before checking each completed turn, preserving the existing exact-once assertions.

* test(app): wait for directory search keyboard focus

The search page becomes visible before its deferred focus callback runs. Wait for the input to own keyboard focus so the journey types its query rather than dropping it during navigation.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 10:21:52 +02:00
paseo-bot[bot] fc8f085b9e Reject oversized messages without disconnecting the host (#6285)
* fix(client): reject a message over the daemon's size limit instead of reconnecting in a loop

The daemon closes the WebSocket with 1009 when a message exceeds its 100 MiB
limit. A workspace or agent creation that loses its connection mid-request
stays pending and, after reconnecting, finds no record of itself on the
daemon (the oversized frame was never read), so it resubmits the same frame
and the connection drops again every couple of seconds until the app reloads.

The client now checks each outbound frame against the same limit, which
protocol exports and the daemon sets explicitly, and fails that request with
a size error while the connection stays up.

Refs #5501

* test(client): cover the UTF-8 byte count at the message size limit

* fix(client): reject oversized relay messages without disconnecting

Check the encrypted wire size synchronously before submission, so a frame below the direct limit cannot close the relay and leave workspace creation resubmitting forever. Keep the relay limit shared with the existing checkout diff budget.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 08:20:40 +02:00
paseo-bot[bot]andGabriel Giordano 019e5279e9 Show Claude option previews in the question card, readable on Android (#6171)
* Show Claude option previews in the question card

* Wrap long lines in question option previews

* Check option previews under the option that owns them

* Show the whole option preview on native so every line is reachable

On Android, a slow drag inside a preview capped at 240px did not scroll
it, so lines past the cap could not be read. The preview's nested
scroll sits inside the option's Pressable; the same drag scrolls it
once the preview is outside the Pressable. Native now renders the
preview at full height so the chat scroll reaches every line, and a tap
on it still selects the option. Web keeps the 240px cap.

The mock agent's rollout preview is now taller than the cap so the
Android replay can check the last line.

---------

Co-authored-by: Gabriel Giordano <hello@gabrielgiordano.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 07:13:06 +02:00
paseo-bot[bot] 4a3ff87eec fix(omp): show manual /compact progress and refresh the context meter (#6106)
* fix(omp): show manual compaction progress and refresh context usage

OMP answers a manual compact with its RPC response alone, without
compaction_start or compaction_end, so /compact on an OMP agent showed no
row and left the context meter at the pre-compaction value until the next
turn. The compact request now emits the loading and completed rows itself
and publishes the session's context usage once it returns.

The fake OMP runtime no longer emits compaction events for a manual
compact, matching what OMP sends.

Closes #5966

* fix(omp): keep a running turn's usage polling through a manual compact

Refresh the compacted context usage without ending the active turn's
poll, wait for the usage update in the real OMP test, and cover a failed
manual compact.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 06:10:51 +02:00
paseo-bot[bot]andkhimaros a4f1df3aed Let voice mode TTS use the model and voice an OpenAI-compatible server serves (#6060)
* fix(server): accept voice and model names from self-hosted speech servers

features.voiceMode.tts.voice and TTS_VOICE were validated against the
OpenAI voice catalog, so a server whose voices live under other names
could not be configured at all: config.json failed to parse and an env
var threw inside the speech config resolver, which config.ts calls
without a catch, so the daemon refused to start. TTS_MODEL had the same
problem for model ids the server serves itself. Both are free-form now,
with model ids keeping their case because rosters use values like
kokoro-v1:Q8_0. Regenerate the config JSON schema.

* fix(server): send TTS voice names as written and keep the config schema current

A self-hosted OpenAI-compatible server owns its voice ids and their case, so
lowercasing the configured voice could turn a valid id into one the server
does not know. Keep the published config schema in its existing form and
change only the voice field.

Co-authored-by: khimaros <me@khimaros.com>

* fix(server): drop the TTS baseUrl from the init log

The configured baseUrl can carry a token in its query string, and logging
it is not needed to accept self-hosted voice and model names.

* test(server): await bounded worktree fixture cleanup on Windows

---------

Co-authored-by: khimaros <me@khimaros.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 05:10:35 +02:00
paseo-bot[bot]andHTeo a4b15455b7 fix(cli): list every agent in paseo agent ls, past the first 200 (#6033)
* fix(cli): list every agent page in agent ls

agent ls made one fetch_agents call and ignored pageInfo.nextCursor, so
it stopped at the daemon's default page of 200 agents without saying
more existed. Follow the cursor until the daemon reports no next page,
as workspace ls and agent delete already do.

Co-authored-by: HTeo <hteo1337@gmail.com>

* fix(cli): page agent ls in creation order

fetch_agents defaults to updated_at order, so an agent that updated
after the first page moved ahead of the cursor and was skipped. Page by
created_at, which never changes; ls sorts the rows itself afterwards.

* test(cli): exercise agent listing with a supplied client

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: HTeo <hteo1337@gmail.com>
2026-10-14 03:55:27 +02:00
paseo-bot[bot] bd9af256bd Keep a symlinked config.json linked when Paseo saves it (#6023)
* fix(server): keep a symlinked config.json linked when saving

Paseo saved config.json by renaming a temporary file over its path, which
replaced a symlink with a regular file and left the linked file unchanged.
Private file writes now resolve the symlink and replace the file it points at.

Closes #5932

* test(server): check a symlinked config.json stays private

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 03:06:36 +02:00
paseo-bot[bot]andAhmet İzgi b1a638482d Cover child completion before create_agent returns (#6008)
Adds a regression test for #5928: a child that finishes its initial turn before its turn start returns still sends one finish notification to its parent. The fix itself is on main from 0009a7e.

Co-authored-by: Ahmet İzgi <ahmetizgi@gmail.com>
2026-10-14 02:05:44 +02:00
paseo-bot[bot]andElior Fureraj 12aaded6c5 fix(app): show the configured label for the xhigh thinking option (#6005)
* fix(app): respect provider label for xhigh thinking option

Only fall back to the localized Extra high string for bare forms.
Custom labels such as XHigh (56 | 8m) now render with their suffix.

* fix(app): show Extra high only for a bare xhigh label

A configured xhigh label with extra detail keeps its text; only a label
that is just xhigh or Extra high uses the localized string.

Co-authored-by: Elior Fureraj <efureraj@gmail.com>

---------

Co-authored-by: Elior Fureraj <efureraj@gmail.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 01:05:05 +02:00
paseo-bot[bot] 0614e96ee0 fix(claude): rewind works when the provider sets CLAUDE_CONFIG_DIR (#6325)
* fix(claude): rewind a session that lives in the provider's CLAUDE_CONFIG_DIR

Rewinding the conversation forks the Claude session through the SDK's
forkSession, which looks for the transcript under the daemon's own
CLAUDE_CONFIG_DIR or ~/.claude. A provider whose env sets CLAUDE_CONFIG_DIR
has its transcripts elsewhere, so every rewind failed with
"Session <id> not found".

Fork through a session store rooted at the folder of the transcript the
agent already resolves with the provider's config dir, so the SDK reads
the session where Claude Code wrote it and writes the fork beside it.

Closes #6321

* fix(claude): pass only transcript entries to the rewind session store

The session store parsed each transcript line and cast it to a session
entry without checking it. Keep only JSON objects with a string type, and
cover transcripts with non-entry lines in the real-SDK rewind test.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-13 21:12:33 +00:00
Mohamed Boudraandpaseo-bot[bot] e135e9c101 Open files and subagents in a closable sheet on phones (#6827)
* feat(app): open files and subagents in a closable sheet on compact

On phones, opening a file, a provider subagent, or a Paseo subagent now shows it
in a bottom sheet over the current tab instead of adding a tab, so the tab strip
stays for terminals and root agents. Paseo subagents get "Open as tab" to move
the same tab (same tabId and state) into a full tab with a composer; agents in a
sheet hide the composer.

The decision lives in the workspace layout store's openTab, driven by a
per-kind `compactSheet` rule in the panel manifest, so every open path (chat
links, Explorer, subagents track, notifications, plugins) follows it without
call-site branching, and a new kind opts in with one manifest entry. The app
shell publishes the layout mode (compact / single-pane / split-panes) once, so
callers stop passing isCompact for placement.

A sheet holding unsaved edits is never implicitly displaced; later opens go to
tabs underneath, and closing it goes through the existing discard guard. The
editor host is position: sticky because CodeMirror's scrollIntoView otherwise
scrolls the bottom sheet's container and drags the sheet to full height.

* test(app): fail the compact sheet snap check when the sheet is oversized

---------

Co-authored-by: paseo-bot[bot] <paseo-bot[bot]@users.noreply.github.com>
2026-10-14 00:20:10 +07:00
335a11eb82 Reopen Codex chats containing U+2028 or U+2029 on Node 24 (#5999)
* fix(codex): split app-server stdout on newline only, not U+2028/U+2029

node:readline treats U+2028 and U+2029 as line terminators, but the Codex
app-server writes them raw inside JSON strings. Any message containing them
(e.g. tool output or code that mentions JS whitespace) was split into
fragments that failed JSON.parse and were dropped as "non-JSON stdout lines".

Dropped live events could leave an agent stuck in "running", and a
thread/resume response containing them never resolved, leaving the UI on
"Updating messages" indefinitely.

Replace readline with a newline-only line reader (StringDecoder for UTF-8
chunk boundaries) and add a regression test.

* fix(codex): read large app-server stdout lines in linear time

The newline-only stdout reader rescanned the whole accumulated line on
every chunk, so a multi-megabyte thread/resume response took seconds to
frame. Search only the newly decoded text and join the unterminated
parts once the line ends.

Co-authored-by: James Tsang <wtzeng1@gmail.com>

* test(codex): check the large chunked line without a wall-clock limit

Co-authored-by: bebeebebbebebbe <54549544+kotusenka@users.noreply.github.com>

---------

Co-authored-by: Graham <graham@eger.dev>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: James Tsang <wtzeng1@gmail.com>
Co-authored-by: bebeebebbebebbe <54549544+kotusenka@users.noreply.github.com>
2026-10-13 19:07:45 +02:00
paseo-bot[bot]andMatt Cowger 17562fcb66 fix(app): stop New workspace sending a mode the provider doesn't offer (#5847)
The New workspace screen submitted the composer's remembered mode as-is,
so a saved mode for a provider that offers no modes (or no longer offers
that one) failed creation with "Invalid mode '<mode>' for provider
'<provider>'". A new chat tab and the workspace setup dialog already
reconciled the mode against the provider's modes, each with its own copy
of the rule.

The draft composer state now exposes effectiveModeId, the mode the picker
shows, and all three create paths submit it. While the provider is still
loading, the selected mode is kept because its modes aren't known yet.

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Matt Cowger <matt@cowger.us>
2026-10-13 18:16:33 +02:00