Commit Graph
2081 Commits
Author SHA1 Message Date
Mohamed Boudra ec00d57e01 fix(app): align workspace file tree rows
Keep the Files and Changes trees on shared icon, label, and trailing rails while applying the directory-chevron optical adjustments at the owning row.
2026-08-03 23:47:29 +02:00
Mohamed Boudra a361d1cb2f fix(app): polish workspace navigation surfaces
Use mobile action sheets and long-press file menus while preserving provider context across sheet portals. Keep sidebar and file-pane glyphs on shared visual rails, avoid scrollbar layout shifts, and hide keyboard affordances where shortcuts are unavailable.
2026-08-03 21:40:21 +02:00
Mohamed Boudra ab8151d33f feat(app): sidebar display preferences, a shared menu engine, and a colour pass
Sidebar polish round.

Menus. `dropdown-menu` and `context-menu` were two copies of one engine that
differed only in how the trigger fires — identical from `computePosition` down —
and neither could nest. Both are now thin wrappers over
`components/ui/menu/`, which owns anchoring, a pure page-stack navigation model,
and one presentation fork: flyout on pointer, pushed pages in a sheet on compact.
All 31 call sites are unchanged. Items got a rounded inset fill, a visible
separator, and `selected` (a check) split from `active` (a fill).

Sidebar display preferences. The flat wall of four sections becomes one row per
decision with its value, and per-feature toggles over what a workspace row shows:
host, pull request, checks, scripts. Each is independent — a ticked toggle that
draws nothing is a lie. `display-preferences/` hides that grouping lives in
zustand while title source and row items are app settings.

Timestamps. "Last activity" now ticks itself. The label rounds to the minute and
never shows seconds; past a week it becomes a date. The timer lives in the leaf
`<Text>`, at most one per resolution tier shared by every row, and a tick only
re-renders when the string actually changed — so a memoized row that legitimately
never re-renders still stays current.

Colour. Full status tier regenerated at 90% of gamut max (was 85), same rule as
the muted tier, lightness and hue untouched. File icons are pulled toward neutral
in OKLab behind `getFileIconSvg` — material-icon-theme picks its palette for a
tree where the icon is the only colour on the row, and ours is not. New/deleted
files use the forge square-plus/square-minus glyphs at muted weight instead of
tinted pills. Project icons round a little more; uploaded icons never do.

The project status badge is a knockout again: it fills with the colour of the row
behind it, so the ring reads as a gap in the icon rather than a white halo.

File rows. The always-visible kebab is gone from the file explorer and diff tree
in favour of right-click, which also reclaims the reserved action slot.
2026-08-03 18:58:06 +02:00
Mohamed BoudraandGitHub 777fe968f5 Keep Android terminal keyboard input stable (#2830)
* fix(app): stabilize native terminal keyboard input

Terminal focus requests blurred and remounted the hidden input even when the software keyboard was already visible, causing visible IME cycles and leaving clipboard commits coupled to stale native text. Keep focus requests idempotent while resetting only the native input buffer.

* test(app): remove misleading keyboard device scenario

Agent Device cannot durably assert software-keyboard continuity or commit a keyboard clipboard suggestion, so the scenario overstated coverage already owned by focused input-policy tests.

* fix(app): derive terminal keyboard focus from visibility

Use the keyboard-controller inset as the source of truth so a system-dismissed keyboard cannot leave focus activation suppressed. Avoid a duplicate focus request while explicitly showing the keyboard.

* fix(app): track terminal keyboard visibility across layouts

Keep keyboard visibility independent from compact-only terminal padding so tablet taps remain idempotent while the software keyboard is open.
2026-08-03 18:32:16 +02:00
Mohamed Boudra 4406bf588e fix(app): preserve host badge identity
Keep host badges visible under row pressure and apply the selected host color consistently to the icon, label, and tinted background.
2026-08-03 14:08:59 +02:00
Mohamed BoudraandGitHub adf33930b3 Preserve assistant selection formatting when copying (#2808)
* fix(app): preserve formatting when copying assistant selections

Browser selection serialized React Native Web layout nodes instead of the assistant message semantics. Recover semantic markup from renderer-owned metadata and reuse the rich clipboard generator used by Copy turn.

* fix(app): preserve assistant copy semantics

Carry source-only Markdown metadata through the selection copy boundary, unwrap generated code links, and cover ordered starts, hard breaks, and fence languages in Playwright. Refresh the Nix dependency hash for the added clipboard dependency.

* fix(app): preserve partial selection context

Wrap selected fragments with their shared semantic ancestor chain and enable GFM table and strikethrough conversion. Cover partial nested selections and GFM output through the browser clipboard.

* fix(app): preserve copied list and autolink text

Adjust partial ordered-list starts for omitted siblings, strip parser-generated anchors before Markdown reconstruction, and refresh the Nix dependency hash computed by CI.

* fix(app): normalize partial selection context

Normalize cloned ordered lists across block boundaries, degrade incomplete table fragments to selected text, and preserve table alignment provenance through Markdown reconstruction.

* fix(app): escape copied table delimiters

Rendered table cells no longer retain source escape characters. Escape literal pipes during DOM-to-Markdown conversion so copied tables preserve their column structure.

* fix(app): preserve authored link semantics

The rendered link boundary conflated parser-generated links with authored autolinks and discarded authored titles. Carry title semantics into the anchor and unwrap only links with explicit generated provenance.

* fix(app): preserve nested copy structure

Blank-line splitting separated loose nested lists before Markdown rendering, leaving no nesting for clipboard serialization to preserve. Derive protected separators from the Markdown block structure and keep copy metadata independent from monospace surface styling.

* fix(app): preserve selected file links

Assistant messages and rich clipboard HTML must apply the same link validation policy. A shared parser keeps file links semantic in both MIME representations while retaining markdown-it's unsafe-scheme rejection.

* fix(app): preserve partial table content

A selected body row wider than its retained header cannot be represented as GFM and is truncated when rich HTML is regenerated. Treat only those over-wide fragments, plus headerless fragments, as incomplete while preserving valid short rows.
2026-08-03 19:02:31 +08:00
Mohamed BoudraandGitHub 04fe30a3e2 Improve mobile terminal input, selection, and resizing (#1607)
* Improve mobile terminal typing and selection

* Address mobile terminal review feedback

* Remove dead native terminal input branch

* Replay bracketed paste in terminal preamble

* Stop per-key terminal resize claims

* Stabilize terminal resize browser test

* Use tracked terminal mode for native paste

* Restore terminal toolbar focus handling

* Adapt native terminal to active visibility state

* Stabilize native mobile terminal interactions

* Add native terminal fallback boundary

* test(app): expect explicit terminal resize reclaim

* fix(app): stabilize terminal buffer coordinates

* fix(app): restrict terminal resize ownership

* fix(terminal): gate restored input modes

* fix(app): recover stalled terminal WebViews

* chore(app): remove obsolete terminal harnesses

* fix(app): make terminal resize claims explicit

* test(app): stabilize native terminal device flows

* test(app): track absolute scrollback coordinates

* fix(app): isolate terminal WebView streams

* fix(terminal): arbitrate size ownership across clients

The daemon now distinguishes explicit ownership claims from passive geometry updates. This keeps the active client authoritative while allowing its split, viewport, and keyboard changes to resize the PTY without idle clients stealing it.

* test(server): make worker resize assertion portable

Query the worker's authoritative terminal snapshot instead of invoking a Unix-only shell command, so the resize regression exercises the same boundary on Windows and Unix.
2026-08-03 19:01:34 +08:00
Mohamed BoudraandGitHub f682156302 Keep background composer work out of creation progress (#2794)
* fix(composer): keep background context out of loading state

Pull request link resolution can block a new workspace until its checkout target is known, but it is not submission work. Keep that safety gate while reserving the send-button spinner and drop lockout for actual submission or upload progress.

* test(composer): cover link resolution at browser boundary

Exercise the real new-workspace composer while pull request context resolves, including the submit presentation and file-drop path. Keep the two presentation booleans local to their only caller instead of exporting policy solely for an internal-state unit test.

* fix(composer): preserve background lookup ownership

Composer edits could release the resolving gate while a pull request lookup still owned the request, briefly allowing workspace creation from the wrong branch. Track logical candidates independently of unrelated draft changes and settle each reference independently so only invalid work releases early.

* fix(composer): preserve pasted PR selection order

* fix(composer): renew re-added PR lookups

* fix(composer): track pasted PR source order

* fix(composer): preserve resolved attachment order

* fix(composer): coordinate active lookup order
2026-08-02 18:37:07 +02:00
Mohamed BoudraandGitHub ccb668b3b4 Improve sidebar workspace readability and host identity (#2790)
* fix(app): align turn footer actions

Keep running-turn actions in the same position as completed-turn actions and use the shared small icon size for both copy and fork controls.

* feat(app): improve sidebar workspace identity

Make dense sidebar workspace rows easier to scan and let each device control how connected hosts are identified.

* fix(app): keep host badges readable

Use a compact display menu and keep identity color on the host icon so translated controls fit and badge text retains standard contrast.

* fix(app): announce selected sidebar badge mode

* fix(app): retain icon-only host names for screen readers

* fix(app): name hoisted workspace rows

* fix(app): retain host identity in workspace labels

* fix(app): retain workspace status in row labels

* fix(app): defer host appearance until identity resolves

* fix(app): serialize host appearance writes

* fix(app): serialize host registry writes

* fix(app): bound sidebar host badge labels

* test(app): match workspace status labels

* fix(app): consolidate sidebar identity behavior

* test(app): assert status grouping behavior

* fix(app): keep sidebar row actions accessible

* fix(app): stabilize optimistic message layout
2026-08-02 16:58:20 +02:00
Mohamed BoudraandGitHub 553ac53643 feat(app): add chat outline navigation (#2792)
* feat(app): add chat outline navigation

Long agent sessions need prompt navigation without coupling viewport movement to disposable server windows. Keep fetched timeline pages in client state so repeated jumps stay local, and settle prompt jumps against virtualized row measurement changes.

* fix(app): preserve chat outline timeline coverage

* fix(app): reconcile outline window submissions

* fix(app): harden outline timeline navigation
2026-08-02 20:48:23 +08:00
Mohamed BoudraandGitHub 0ad51e26a1 fix(app): paste clipboard images on mobile (#2793)
Native composers had no path from the device clipboard into image attachments. Route clipboard data URLs through the existing attachment persistence flow and expose the action only in the native attachment sheet.
2026-08-02 14:46:25 +02:00
Mohamed BoudraandGitHub 2b474f7df9 Keep workspaces responsive across app-wide routes (#2791)
* perf(app): retain inactive workspace screens

App-wide routes cleared the active workspace selection, which removed the retained workspace deck from the React tree and made returning rebuild every panel. Keep mounted deck entries inactive across those routes and suspend inactive panel subscriptions so state is preserved without background work.

* perf(app): suspend remaining inactive queries

Retained composers and commit diff panels still subscribed through query hooks that did not receive panel activity. Propagate the existing activity gate so app-wide routes leave those hidden surfaces dormant.

* refactor(app): separate panel activity from connectivity

A hidden retained panel is still connected to its host. Keep connectivity truthful, pause work inside query and stream owners, and preserve terminal emulator identity while its stream is detached.

* test(app): target active retained controls

* fix(app): retain compact and terminal route content

* fix(app): hide inactive workspace overlays
2026-08-02 14:32:17 +02:00
Mohamed BoudraandGitHub 26bdb1d171 Keep timeline history duplicate-free across daemon upgrades (#2789)
* fix(app): keep timeline pagination duplicate-free

Older daemons page projected history by canonical sequence ranges, so one projected block can appear in adjacent responses. Accept each block on the page containing its projection anchor while still advancing across empty projected pages.\n\nProjected siblings can share a provider message ID, so render identity also includes the timeline cursor instead of collapsing distinct blocks onto one layout row.

* fix(e2e): preserve isolated daemon launch contract

Keep the source-daemon spawn path recognizable to the repository launch-contract check while selecting the built supervisor only for published package fixtures.

* fix(app): preserve stable timeline item identity
2026-08-02 12:21:47 +02:00
Mohamed BoudraandGitHub 048b82f2a7 Preserve interactive ACP permission choices (#2762)
* fix(acp): preserve permission choices

ACP question bridges encode answers as repeated permission options. Preserve each option through the UI and return the exact selected ID, while keeping auto-accept from choosing on the user's behalf.

* fix(acp): validate selected permission behavior

Reject inconsistent action IDs instead of allowing the action ID to override the response behavior. Move the misplaced browser spec so the current base CI contract can run.

* fix(acp): preserve invalid permission requests

Validate selected ACP actions before consuming the pending request so inconsistent client responses can be corrected and retried.

* test(acp): verify real permission questions

* fix(acp): reject empty permission action ids

* test(e2e): scope Kimi config to ACP spec
2026-08-01 20:56:19 +02:00
Mohamed BoudraandGitHub 5a30affd7f Skip unrelated required checks without losing their names (#2542)
* fix(ci): skip unrelated required checks cleanly

Matrix jobs had to start no-op runners to preserve interpolated required-check names. Static named jobs let GitHub report genuine skipped conclusions while keeping those names stable.

* fix(ci): gate changes to install patching script

* fix(ci): route cross-package test contracts narrowly

* fix(ci): keep routed contracts merge-blocking

* fix(server): exclude nested test utilities from builds

* fix(ci): cover app-owned native and browser domains

* refactor(ci): align tests with runtime domains

Own browser specs, desktop specs, and Electron-only app code by stable directories so PR routing does not depend on filename conventions. Keep desktop integration coverage inside the required desktop check.

* fix(ci): keep routing and desktop suites isolated

The routing regression must run before dependency installation, and desktop Vitest must not collect Playwright-owned specs.

* fix(ci): isolate desktop e2e runtime

* perf(test): isolate Playwright workers

* perf(test): parallelize Playwright within specs

* fix(test): keep Playwright file scheduling

* fix(test): share dynamic Metro fixture

* fix(test): preserve own-host restart fixtures

* fix(ci): preserve routed contracts after rebase

* Stabilize Playwright wheel-scroll assertions
2026-08-02 00:56:05 +08:00
Kevin AspesiGitHubpaseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
429155b536 Add collapsed-project status badge to the sidebar (#2340)
* Add collapsed-project status badge to the sidebar

Collapsed project rows now surface the most urgent hidden workspace's
status as a corner badge on the project icon, instead of giving no
signal at all: a loader for running, an alert for needs_input, a dot
for attention/failed, nothing for done. Priority order (needs_input >
failed > running > attention > done) means a working project keeps
its loader even if another workspace is also awaiting review.

Every surfaced status shares one badge shell (size, ring, offset) so
the loader, alert, and dot never draw a differently-sized circle -
previously the loader/alert used a 12pt ring while the dot used a
7-9pt one with a different corner offset. The running loader is also
nudged half a point left so its visual center lands on the circle's
center rather than reading as shifted right.

Adds an e2e capture spec (run on demand, not part of CI) that seeds
every state and edge case through the mock provider and dumps DOM
geometry, for re-verifying the badge shell stays uniform.

* Add PR review gallery images for the status badge PR

Temporary evidence assets for pull request review, embedded via blob
URLs in the PR description. Safe to delete after merge.

* Swap misleading before/after PR images for clean per-state stills

This feature never shipped on main, so there is no real "before" to
compare against for a reviewer — the earlier before/after images were
an artifact of local dev iteration, not a regression fix. Replaced
with one clean still per status.

* Fatten needs-input alert and dot to fill the shared badge shell

Lucide's circle-alert only paints ~83% of its nominal size, so the
prior 10pt glyph read as smaller than the loader inside the same
14pt shell. Sized both up so all three glyphs draw the same visual
footprint against the shared ring.

* Address review: extract pure badge-content selection, drop JSDOM tests

Greptile flagged getStatusDotColorStyle's needs_input/running cases as
dead (unreachable — shouldRenderSyncedStatusLoader and an explicit
check already intercept those buckets earlier). Extracting the
selection logic into a plain-TS descriptor function
(getProjectStatusBadgeContent) makes that structural: the "dot"
variant is typed to failed|attention only, so there's no switch left
with unreachable branches.

That extraction also resolves the JSDOM component test Greptile
flagged (project-leading-visual.test.tsx used vi.hoisted/vi.mock/
createRoot mounting, all banned by docs/testing.md's two test
categories) — the pure selector is now tested directly with a plain
vitest file, no mounting needed. Dropped that test file and
use-sidebar-project-status-bucket.test.tsx (same banned pattern); the
real coverage was already redundant with sidebar-workspaces-view-model
.test.ts's existing priority-order assertions and the e2e capture
spec's real-app gating checks.

Also split a conditional branch in the e2e capture spec's assertion
loop into two deterministic loops (badge-expected vs no-badge cases)
per Greptile's third note.

* Size status-badge glyphs even so they center in the shell

The badge shell's interior is 12pt (14pt minus the 1pt ring on each
side). A centered glyph of size N sits at a (12 - N) / 2 offset, which
is fractional for odd N and gets snapped to a device-pixel boundary,
rendering visibly off-center (~1.5 device px at 3x). Drop the dot to 8pt
and the alert to 10pt so both divide the interior into whole pixels and
land dead center; Lucide's circle-alert paints ~83% of nominal, so 10pt
draws a ~8.3pt disc that matches the 8pt dot.

Also extends the on-demand capture spec to cover the failed bucket, the
priority aggregation cases, dark mode, hover, a compact viewport, and a
recorded running-to-expand transition, dumping DOM geometry that proves
every shell shares one size/offset and every even glyph centers at 0px.

* Refresh status-badge PR assets for the even-glyph sizing

Recaptures every state at the approved 8pt-dot / 10pt-alert sizing,
adds the failed bucket to the per-status set, a crosshair sizing/
centering proof, dark-mode and compact-form-factor shots, and replaces
the soft upscaled spinner GIF with a crisp one assembled from native
3x-scale frames. Drops the stale pre-sizing images.

* Add capture spec proving workspace statuses roll up to the project badge

Seeds three collapsed projects, each with several same-directory
workspaces in different buckets, so each resolves to a different
highest-priority winner (needs_input / failed / running). Films the
collapse and expand so the aggregation is provable as motion, not a
single frame.

* Add propagation recording showing statuses resolving to the project badge

* Add higher-quality mp4 of the status propagation recording

* Refactor project status badge coverage

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-08-02 00:37:22 +08:00
76abc50f82 Fix/local branch worktree base (#2328)
* Add automatic plus branch maintenance workflow

* Build plus artifacts for desktop and CLI/server

* Document and summarize plus artifact workflow

* Document Apple signing and pin plus workflow actions

* Disable Linux plus desktop artifact

* Publish plus CLI packages to GitHub Packages

* Fix GitHub Packages publish paths

* Fix plus mise install summary package name

* Disable macOS x64 plus desktop artifact

* Tag plus npm packages as latest for mise

* fix(worktrees): respect selected local base ref

* chore(ci): remove fork-only plus workflow

* fix(worktrees): preserve exact branch picker refs

---------

Co-authored-by: Matt Cowger <1929548+mcowger@users.noreply.github.com>
2026-08-01 16:06:56 +00:00
47c18a9213 Fork an agent session mid-run from the in-flight turn footer (#2638)
* feat(app): fork an agent session mid-run from the tab menu

Forking already existed, but only on the footer of a *completed*
assistant turn. While an agent is running, the in-flight turn renders
RunningTurnFooter with no fork menu, so exploring another path meant
scrolling back to hunt for an older turn's footer — or waiting for the
run to finish.

Add two entries to the agent tab context menu ("Fork chat in a new tab",
"Fork chat in a new workspace"), reachable regardless of run state. They
omit the fork boundary, and selectForkContextRows projects the whole
timeline when no boundary is given, so a mid-run fork carries everything
up to now — including the partially streamed in-flight turn.

Fork logic moves out of agent-stream/view.tsx into a shared useForkAgent
hook so the turn footer and the tab menu share one code path; the
per-turn behavior is unchanged (it still pins a boundary to its turn).

No server, protocol, or capability changes — this reuses the existing
agent.fork_context RPC and agentForkContext host feature. The daemon
handler is already a pure in-memory timeline read, so it was safe
mid-turn before this change.

* fix(app): resolve missing agent records before failing a fork

handleForkAgent read the agent record from the session store and gave up
if it was absent from both agents and agentDetails. A persisted agent tab
can outlive its record — restored on startup before hydration — so both
fork entries failed with a generic toast even though the agent exists on
the daemon and is perfectly forkable.

Fall back to client.fetchAgent and store the result before failing,
mirroring the resolution path agent-panel.tsx already uses. Only report
forkFailed when the agent genuinely cannot be resolved; a disconnected
host now says so instead.

The resolution logic lands in resolveForkAgentSource with injected
dependencies, since workspace-screen.tsx has no handler-level test
harness and a sibling testable module is the pattern in this directory.

storeFetchedAgentDetail moves out of panels/agent-panel.tsx into
utils/agent-detail-store.ts so the workspace screen doesn't import a
panel module — and into its own file rather than agent-directory-sync.ts,
which would close an import cycle through legacy-daemon-workspaces.

* feat(app): fork from the in-flight turn instead of the tab menu

Move the mid-run fork affordance to where the fork button already lives:
the assistant turn footer, beside the progress loader.

RunningTurnFooter previously showed only a spinner and elapsed timer, so
the fork button vanished for the whole duration of a run. It now renders
AssistantForkMenu alongside them, reachable the moment a run starts.

The in-flight fork passes no boundary, so selectForkContextRows projects
the whole timeline and the fork captures the response being streamed. A
fork button sitting next to a live response that silently dropped that
response would be worse than no button. Completed turns still pin their
own boundary, enforced by a separate handler type that keeps boundary
required.

layout.ts is untouched — the affordance is added to the running footer
component rather than by lifting the footer suppression, so no copy or
duration affordance leaks onto an incomplete turn.

Reverts the workspace tab context menu approach, along with the agent
resolution helper and store-writer extraction that existed only to serve
it. The footer path already holds the agent record.

* test(app): cover the in-flight turn fork with a Playwright spec

Extends the existing assistant fork menu spec rather than adding a
sibling file, so the completed-turn and in-flight cases sit side by side.

The new test pins the behavior that defines the feature: the in-flight
fork omits the boundary fields entirely, while the completed-turn fork
sends one. Both assertions read the actual agent.fork_context frames off
the WebSocket, using the read-only page.on("websocket") idiom from
helpers/timeline-delivery.ts so the observer cannot perturb the flow it
measures. Note the request omits the fields rather than nulling them —
daemon-client spreads them conditionally — so absence is what's asserted
there, and null only on the response.

Also asserts the fork trigger is scoped inside turn-working-indicator,
that no copy or duration affordance leaks onto the incomplete turn, that
the attachment carries the streamed text, and that the source agent keeps
streaming afterwards.

* test(app): cover the in-flight fork call site, gate, and teardown

Three behaviors this PR introduced had no coverage below the Playwright
spec. A copy-paste of resolveAssistantTurnForkBoundary into
handleForkInFlightTurn would have left every unit test green.

view-fork-in-flight.test.tsx mounts the real AgentStreamView with the
render strategy stubbed to the live-auxiliary slot, so handleForkInFlightTurn,
useStableEvent and the readOnly expression all stay real. It asserts the
fork request has no boundary KEY — matching the wire contract, where the
field is absent rather than null — for both targets, and that the read-only
gate withholds the trigger, with positive controls either side so an empty
tree cannot pass for a working gate.

turn-footer.test.tsx covers the race this PR created: the fork menu now
mounts only while a run is in flight, so a fork can be pending when the
subtree is torn down. The behavior is benign — React 18 makes the orphaned
setState a no-op — so the test asserts the observable outcome and leaves a
tripwire for unhandled rejections, since AssistantForkMenu's finally has no
catch.

Each test was verified by mutation: adding boundary: undefined, dropping the
readOnly gate, and keeping the running subtree alive each fail the matching
test.

Scoped to what this PR changed. The relocated hook's error paths, draft
setup construction, and attachment metadata predate it and stay untested
here.

* test(app): replace mocked fork coverage with Playwright

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-01 16:41:30 +02:00
e35ad9f692 feat(app): preview HTML files in the file pane (#2712)
* feat(app): preview HTML files in the file pane

Renders .html/.htm files as a page in the file pane, with the same
Preview/Source toggle Markdown already has. The main use is reading
richer visual plans an agent wrote as a self-contained HTML page.

A preview is a viewer, not a browser. Each document is prefixed with a
doctype and a strict policy, so the policy is always the first element
and always lands in the parser-created head: inline script and style
run, and remote subresources, fetch, XHR, WebSocket, beacon, and form
posts are refused. The frame has an opaque origin, so it cannot reach
the app's DOM and storage and cookie APIs throw inside it.

One gap is left on web and cannot be closed there: a sandboxed
document may navigate itself, and no CSP directive in current browsers
prevents it -- navigate-to was dropped from CSP3 and is unenforced, and
meta refresh needs no script (both verified against the Chromium the
app ships against). The opaque origin bounds it to the page's own
contents, and SECURITY.md documents it. Native narrows it further by
refusing navigation after the initial document, with the caveat that
the decision runs in app JS.

Self-contained pages only -- a page that pulls a CDN script renders
unstyled, and Source still shows everything.

* fix(app): harden HTML preview behavior and tests

---------

Co-authored-by: Nicholas Salgueiro <nicholas.salgueiro@britecore.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-01 14:30:59 +00:00
Mohamed BoudraandGitHub fb9f7fb98a Make the command center stable and workspace-aware (#2749)
* feat(app): expand command center actions

Keep command center navigation stable by revealing only clipped rows without smooth centering. Prioritize a concise default list while making workspace, Git, pane, and creation actions searchable through the existing contribution and action-policy paths.

* test(app): search for query-only command action

Add project is intentionally absent from the unfiltered command center. Exercise the overlay flow through search before selecting it.
2026-08-01 15:38:07 +02:00
Mohamed BoudraandGitHub 8021d2b6c9 Keep agent timelines stable from submission through resume (#2718)
* Reapply: Stop completed turns from appearing stuck (#2484)

* Reapply: Keep older chat history and image previews stable (#2490)

* Lock timeline layout stability with user-driven specs

Two reds reproduce the reported jank:
- new agent, first prompt: the row paints at top=100 then jumps to 144 through
  authoritative hydration, because the draft tab and the agent panel each render
  their own AgentStreamView for the same logical row.
- long loaded timeline, submit: scrollHeight collapses 10793 -> 9111 mid-turn,
  because splitOrderedTail reclassifies measured rows into the virtualized segment
  where the virtualizer falls back to estimateStreamItemHeight.

Also restores centered timeline rows: the pagination anchor wrapper introduced in
#2490 was a plain block div, which broke alignSelf: center.

* Measure the turn footer blink from when the footer appears

Bounding the window at the prompt row conflated two things: the footer arriving a
frame after the row, and the footer leaving the layout and coming back. Bounded from
the footer's first frame, the blink still reproduces, so the footer-ownership race is
real rather than a sampling artifact.

* Rename the shadowed prompt row binding in the frame recorder

* fix(desktop): allow concurrent worktree dev instances

Desktop terminals can inherit another instance's profile path and debugging flags. Scope the profile to the current dev root and select an available debugging port so instances do not share global runtime state.

* fix(desktop): expose development providers in dev builds

Desktop-managed daemons inherited no Paseo runtime mode, so dev builds constructed the production provider registry. Mark unpackaged internal launches as development while continuing to force packaged launches to production.

* fix(timeline): stabilize optimistic turn layout

Keep initial history loading dormant until user intent and preserve active-turn layout across delayed transport updates. Add painted-frame coverage for optimistic timeline transitions.

* fix(timeline): preserve optimistic prompts through hydration

Canonical replacement relied on a transport submission record that can settle before a provider echoes the user row. Keep presentation continuity owned by the unreconciled row while destructive history replacement remains governed by active transactions.

* fix(timeline): make submitted prompts canonical

Submission settlement previously depended on status updates that could arrive after the optimistic row. Persist every accepted prompt in the canonical timeline and keep the footer active until RPC and canonical acknowledgement converge.

* fix(timeline): stop older history pages repeating entries

Backward pagination counted source sequence rows, so page boundaries inside a merged projected item could return that item repeatedly. Count projected entries and advance from their display anchors instead, with server and browser regressions covering the contract.

* fix(timeline): resume agents from the latest history

Use a bounded tail probe on resume so long background runs appear atomically instead of replaying every missed page. Preserve unchanged views, reconcile local/live rows, and keep skipped history reachable through backward pagination.

* fix(timeline): reconcile turn state after missed updates

Turn completion events can be missed while an agent timeline is unsubscribed, leaving footer activity stale even after the directory reports idle. Reconcile the single-owned turn lifecycle from authoritative resume snapshots while preserving newer live evidence.

* test(timeline): cover running state across refresh

Protect the hydration boundary where directory state must restore consistent tab, composer, and footer activity. Expose the running indicator semantically so the regression checks user-visible state instead of spinner implementation details.

* fix(timeline): keep history loading geometry stable

Reserve a fixed history-start gutter regardless of pagination state so showing or hiding the loader cannot shift timeline content.

* fix(timeline): keep turn activity coherent

Derive footer, tab, and composer activity from one lifecycle and submission projection. Idle directory transitions now request an authoritative resume so missed terminal events converge without treating directory delivery order as turn authority.

* fix(tests): align checks with runtime boundaries

* fix(desktop): stop dev app surviving its terminal

Electron was detached from the desktop runner, allowing it to outlive a stopped workspace terminal with broken output pipes and retain the single-instance lock. Keep Electron owned by the runner and preserve explicit process-group cleanup for detached child trees.

* fix(timeline): preserve activity through turn handoff

Correlate lifecycle events and snapshots with daemon-owned turn identity, and keep optimistic submissions active until their canonical row arrives. This prevents stale updates and RPC timing from exposing a false idle state.

* refactor(timeline): remove submission timing state

* fix(timeline): restore prompt identity on resume

* fix(timeline): exclude attachments from canonical prompt text

Structured provider prompts can include hidden history attachments before the user's text. Persist only plain prompt blocks so authoritative user messages match what the composer displayed while provider input remains unchanged.

* refactor(agent): remove unused pending-run message identity

* fix(desktop): let Chromium allocate the debugging port

* refactor(agent): remove impossible finalized-turn tracking

* fix(timeline): load history after keyboard scrolling

Treat observed upward displacement as user intent so keyboard and scrollbar navigation can wake dormant pagination.

* test(server): preserve accepted turn stream order

The manager-owned accepted turn boundary intentionally precedes coalesced output. Assert that lifecycle event explicitly while retaining the timeline-before-terminal ordering contract.

* test(app): scope sidebar fixtures by identity

Shared E2E daemon state may contain unrelated projects. Keep stale-route coverage tied to its named missing project and drag only the two rows seeded by the reorder spec.

* fix(timeline): gate prompt revisions by client capability

* fix(timeline): finalize canonical prompt delivery

Treat provider identity as a single stored-row transition so legacy clients receive one deliverable canonical prompt. Release identity-pending rows before later foreground output or the current terminal while preserving stale-turn isolation.

* fix(server): preserve legacy prompt pagination

* fix(timeline): keep assistant block identity stable

Assign canonical block IDs during live promotion so terminal finalization cannot remount image-bearing rows.

* fix(timeline): apply covered revisions during resume append

Run the exact canonical revision pass before overlapping resume pages append newer rows, while leaving cursor advancement to the incremental path.

* fix(server): preserve queued agent transitions

Project each queued live snapshot before awaiting stored metadata so later lifecycle state cannot overwrite an earlier transition.

* fix(server): snapshot queued agent updates

Capture each live agent payload before it enters the per-agent queue so shared nested state cannot change while an earlier storage read is pending.

* docs(app): date agent turn compatibility cleanup

* refactor(timeline): mint submitted prompt identity at acceptance

Canonical prompt rows now keep one immutable wire identity while provider identity remains internal for rewind translation. This removes the unreleased same-sequence revision and legacy projection contract.

* refactor(app): remove prompt revision reconciliation

Submitted prompt rows are immutable on the wire, so resume and live reducers no longer need to apply already-covered same-sequence updates.

* test(app): cover submitted prompt rewind roundtrip

* docs(timeline): describe immutable submitted prompt identity

Provider echoes now enrich daemon-internal identity only; wire rows keep their acceptance-time identity and rewind performs the provider translation.

* test(server): validate mock rewind targets

Reject unknown provider message IDs so app rewind coverage exercises wire-to-provider identity translation.

* fix(server): validate rewind before interrupting

* fix(server): omit rewind identity for daemon prompts

* test(app): guard rewind eligibility and rejection

* fix(server): preserve prompt identity across hydration

* refactor(timeline): remove stale revision remnants

* Revert "fix(server): preserve prompt identity across hydration"

This reverts commit 6adcf7e5b0bc8601761af59b461a746cb5e395fa.

* fix(timeline): settle missed submission acknowledgements
2026-08-01 14:13:47 +02:00
Mohamed BoudraandGitHub a059af1eda fix(app): preserve dictation text during submit (#2745)
Dictation auto-send bypassed the controlled composer update, so preserved draft surfaces could lock while showing an empty prompt. Publish the transcript before invoking the existing submit lifecycle so pending and failed submissions retain the user's spoken input.
2026-08-01 12:28:55 +02:00
Mohamed BoudraandGitHub 6177d85e99 Make sidebar shortcuts leave focus mode (#2717)
* fix(app): exit focus mode for sidebar shortcuts

Focus mode kept sidebar actions hidden after their handlers ran. Exit only after a sidebar action is handled so the requested action still takes effect.

* fix(app): order focus exit before sidebar action

Leave focus mode before dispatching the sidebar command so its handler runs against the visible panel state.

* fix(app): preserve focus mode outside workspaces

Only exit active workspace focus mode after a sidebar shortcut is handled. Keep persisted focus state unchanged when the shortcut has no foreground workspace target.
2026-08-01 11:16:48 +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 3778d0ad7b Fix mobile sidebar swipe and drag failures (#2709)
* fix(app): prevent text selection during Android swipes

Gesture interception did not cancel the native view where the touch began, leaving Android's pending text-selection action alive.

* fix(app): restore iOS sidebar close swipes

Fabric could fail to attach close handlers when their target lived inside retained but display-hidden panel content. Keep a registered native gesture host mounted outside the presentation wrapper so close swipes work over nested lists without activating rows.

* fix(app): recover sidebar drags after cancellation

Cancelled native drags could leave a lifted row and the shared outer list locked. Scope terminal cleanup to the owning gesture session and release the lock on cancellation or unmount.

* test(app): clean up partial drag fixtures
2026-08-01 04:05:54 +08:00
Mohamed BoudraandGitHub 79fccb368d chore(app): ship mobile updates through stores only (#2708)
Paseo does not publish EAS updates, so remove the update runtime and channel metadata from native builds.
2026-08-01 03:28:26 +08: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 de44c8cd66 Show New Workspace isolation controls immediately (#2702)
* fix(workspaces): show isolation without checkout delay

* fix(workspaces): trust hydrated project capability

* fix(workspaces): resolve branch before creation

* fix(workspaces): honor invalidated checkout status

* fix(workspaces): preserve isolation while projects load

* fix(workspaces): keep provisional project routes usable

Route-derived projects do not know their worktree capability. Represent that uncertainty at the placement boundary so remembered worktree intent survives hydration without disabling the New Workspace screen indefinitely.
2026-07-31 17:50:35 +02:00
35f985baff Switch reasoning, mode, plan, and fast from the Command Center (#2274)
* Switch reasoning, mode, plan, and fast from the Command Center

Extend the Command Center (Cmd-K) with selectors for a focused agent's
Thinking (reasoning effort), access Mode, Plan mode, and Fast mode,
mirroring the model switcher. Typing surfaces tiered "<Setting> › <Value>"
breadcrumb rows with icons and a check on the current value, for both a
running agent and a new draft tab. Rows only appear while typing, so the
default palette is unchanged.

- Thinking and Mode are multi-option selects; Plan mode and Fast mode are
  On/Off toggles.
- Plan mode is dual-natured: a permission mode for Claude/Copilot/OpenCode
  (excluded from the Mode list, given its own toggle) and the plan_mode
  feature for Codex. Fast mode drives the fast_mode feature (model-gated).
- All logic lives in command-center/agent-setting-contributions.ts as pure,
  unit-tested builders, with a small icon helper and a hook that registers
  the running-agent selectors; the draft registers from its composer tab.
  No changes to the command palette component and no protocol changes.

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

* Unify Command Center agent controls

* refactor: clarify agent control ownership

* refactor(app): tighten command center agent controls

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-31 17:37:04 +02:00
Mohamed BoudraandGitHub 99eca19a76 Keep selected projects when switching hosts (#2700)
* fix(app): preserve project selection across hosts

Rendered project identity and persisted equivalence were conflated after duplicate-project handling. Keep them separate so host changes resolve selectable equivalents deterministically without resetting legacy sidebar identity.

* fix(app): restore selected clone after host round trip

Keep manual origin and current destination placements separately so switching back restores the chosen checkout while archive gaps preserve the active host selection.

* fix(app): key status icons by project view

* fix(app): preserve origin during archive gaps
2026-07-31 15:42:08 +02:00
87ef631ac4 Show project icons in status-grouped sidebar rows (#2416)
* feat: show project icons in sidebar status view

* test(sidebar): keep status icon regression active

The ten-second mock can finish during app startup, removing the Working bucket before the icon assertion. Keep the seeded workspace active for the full scenario.

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-31 14:44:31 +02:00
Mohamed BoudraandGitHub 8e983c5d27 Stop transient file conflict callouts (#2694)
* fix(app): prevent transient file conflict callouts

Watcher versions are candidates until a matching disk read settles them. The editor now classifies the settled bytes once, preserving dirty buffers and keeping reload content paired with its revision.

* fix(app): preserve external BOM changes
2026-07-31 19:03:11 +08:00
91da0bac4d Choose which orchestration skills get installed (#2680)
* feat(settings): choose which orchestration skills to install

The skills row installed the whole bundle or nothing. A settings control
next to it now opens a sheet where you keep every bundled skill or pick a
subset, and saving converges ~/.agents, ~/.claude, and ~/.codex to it.

"All skills" reads the bundle directory instead of a hardcoded catalog, so
a skill added in a later release installs itself without another code
change. Retired bundle names stay in a separate cleanup list so old copies
on disk are still removed but never become selectable.

The selection persists under Electron user data, separate from install
state, so uninstalling does not erase the choice — a later install brings
back exactly the chosen set. Installs with no saved selection read as
"all", which is what they already had.

* Restore skill selection after failed save

* fix(settings): don't save a skill selection that never reached disk

A failed save wrote the preference, converged, then wrote the old preference
back. That leaves a window where a selection the user was told did not save
is the one on disk, and a crash inside it makes the window permanent.
Converge first and persist only on success — nothing to roll back, and a
failed save now leaves disk and preference exactly as Cancel would.

Resolving the bundle path when the command handlers are wired also broke
daemon-manager's tests: the packaged branch reads process.resourcesPath,
which is undefined outside Electron. Resolve targets per command instead —
registering handlers should not touch the environment.

* fix(settings): make skills convergence single-writer and confirm removals

Startup auto-update and the settings commands were separate writers over the
same skill directories. Either could apply a plan computed from a selection the
other had already replaced — narrow the selection while startup is repairing
drift and the repair puts the removed skill back. There is now one controller
that owns scan, apply, and persist, and every entry point queues through it, so
concurrent callers see a consistent result instead of an interleaved one.

Save also had no recovery once it had converged: if the selection could not be
committed afterwards, disk was changed while the preference still said the old
thing, and the next startup silently undid the change. Save now puts the
managed directories back to what the committed selection describes before it
reports the failure, so a failed Save lands where Cancel would.

Deselecting a skill deletes its directory in all three agent homes, including
files the user put inside it. That is now confirmed like every other
destructive action, naming the skills and the directories. Adding skills or
saving an unchanged selection still saves without a prompt, and a skill the
host has not installed yet deletes nothing so it does not prompt either.

* fix(settings): confirm removal for skills installed in only some agent homes

The removal confirmation read an `add` op as "not on disk yet". An `add` op
means "missing from at least one target", so a skill sitting in ~/.claude but
not the other two looked absent, and deselecting it deleted that directory —
with anything the user had put inside it — without asking.

The status snapshot now carries the installed skill names directly, taken from
union presence across the three agent homes, and the confirmation reads that.
`ops` keeps its one meaning.

A host that predates the field cannot say what is on disk, so the app bridge
assumes the saved selection is installed. Asking about a directory that turns
out to be absent is harmless; the reverse is not.

* fix(settings): warn about every skill directory a save deletes

The confirmation asked "was this selected before and is it selected now",
which is a different question from the one the host answers. The host
converges to the desired set and deletes every managed directory outside it,
so a skill on disk that the saved preference never mentioned — reinstalled by
hand, restored by a sync, left behind by drift — was deleted with no warning,
user files included.

The predicate now mirrors the host rule: everything installed that the draft
will not keep, regardless of what was saved before. It moved out of the sheet
into its own module so the rule has one owner and can be tested directly.
Saving with nothing to delete still does not prompt.

* fix(settings): a failed save keeps files the user added inside a skill

Save converges before it commits, and convergence deletes whole skill
directories. When the commit then failed, the recovery re-installed the
committed selection — which rebuilds the bundled files and nothing else, so
notes, hooks, and anything else the user had put inside a deselected skill
were gone for good. Cancel does not do that, and a failed save is supposed to
land where Cancel would.

Save now runs as a transaction. Before converging it stages a copy of every
managed skill directory across the three agent homes, next to the tree so the
copy stays on one filesystem. Committing drops the staging; failing anywhere
puts each directory back exactly as it was, deletes the ones the candidate had
created, and then drops the staging. Staging left by a process that died
mid-save is cleaned up by the next one.

The controller only sees begin/commit/rollback; the filesystem work lives in
skills-transaction.ts.

* fix(settings): make skill removal confirmation authoritative

* fix(settings): verify skill convergence before commit

* fix(settings): preserve concurrent skill edits on rollback

* fix(settings): preserve staged skill metadata

* fix(settings): stage skill deletions atomically

* fix(settings): stage removals per filesystem

* fix(settings): restore staged skill symlinks

* fix(settings): canonicalize skill transaction roots

* fix(settings): keep automatic skill updates non-destructive

* fix(settings): report committed skill saves accurately

* fix(settings): keep skill updates non-destructive

* fix(settings): route skill removals through selection

* fix(settings): close skill selection edge cases

* fix(settings): preserve skill data during maintenance

* fix(settings): hide unavailable skill install

* fix(desktop): keep install policy at command boundary

* fix(desktop): restore staged skill directories atomically

* test(app): retry transient skills sandbox cleanup

* fix(desktop): recover externally deleted skill stages

* fix(desktop): preserve conflicting skill rollback data

* fix(desktop): generalize skill rollback conflicts

* fix(desktop): preserve skill rollback collisions

* fix(desktop): recover quarantined skill backups

* fix(app): inset adaptive sheet content on compact

The compact Choose skills sheet rendered edge to edge: the All skills card
touched both screen edges, the section label was glued to it, and the skill
list ran to the edges too. Desktop was correct.

AdaptiveModalSheet expressed its content inset through
BottomSheetScrollView.contentContainerStyle. That is a library prop, not the
style prop Unistyles registers, and gorhom's scroller was never rewritten by
the Babel plugin, so on web the emitted class was never applied and the
padding and gap resolved to nothing. The desktop card uses a real ScrollView,
which Unistyles does track, which is why only compact was affected.

The inset now lives on a View the sheet owns, rendered by every presentation,
so one definition covers compact and desktop and a scroller only ever gets
theme-free layout. The caller-facing prop is contentStyle, since it no longer
lands on a scroll content container.

Choose skills drops its ad-hoc label for SettingsSection, so the label carries
the app's section rhythm instead of sitting equidistant between two cards.

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

* fix(desktop): prevent skill rollback data loss

Preserve externally replaced directory trees during recovery and reject managed child symlinks before synchronization can write outside a skill directory.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 12:28:32 +02:00
Mohamed BoudraandGitHub b6f1274f47 Clarify changed and deleted files in the file viewer (#2670)
* fix(app): distinguish changed and deleted files

Successful reads now reconcile transient missing events without letting stale
cached data mask a deletion. File notices expose only actions valid for the
current buffer state.

* fix(app): preserve file state ordering

* fix(app): allow retrying file checks

* fix(app): clear unchanged file conflicts

* fix(app): keep file conflict recovery consistent

Own disk observation and read ordering in one model so stale reads and failed subscriptions cannot produce contradictory actions.

* fix(app): allow retrying file preview errors

Preserve daemon read errors and expose retry for initial and non-editable file loads.
2026-07-30 22:50:58 +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
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 68a215d40c Prevent restored file trees from crashing desktop startup (#2595)
* fix(explorer): prevent persisted trees from crashing startup

Persisted expansion restored every directory and recursively flattened the resulting tree during render. Limit automatic restoration to five rendered directory levels and flatten iteratively so deep or broad workspaces cannot exhaust the JavaScript stack.

* fix(explorer): preserve expansion changes during restore

Reconcile completed restoration against the current expansion set atomically so folder interactions made while directory requests are in flight are not overwritten.

* fix(explorer): show hidden files before restoration

* fix(explorer): apply directory toggles atomically
2026-07-29 22:10:51 +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
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
e241e02afb feat(app): dismiss the chat keyboard on a fast upward flick (#2417)
* feat(app): dismiss the chat keyboard on a fast upward flick

Scrolling the history to read earlier messages left the keyboard up, so the
visible transcript stayed cramped and the keyboard had to be closed by hand
first — two steps for what should be one.

React Native's keyboardDismissMode cannot express the wanted behaviour:
"on-drag" fires on the first pixel and kills the keyboard on any peek-scroll,
and "interactive" is broken on inverted lists. So the gesture is measured
here: samples are taken from the scroll events and, at release, the speed over
the drag's final stretch decides. Measuring at release rather than averaging
the whole drag is what keeps a fast but controlled read-scroll from counting
as a flick, since such a gesture decelerates before the finger lifts.

Timestamps and offsets come from the events, never from a clock: with a busy
JS thread the callbacks arrive in a burst long after the gesture, and
wall-clock spacing then reads a calm scroll as a flick. The release offset
comes from the end-drag event for the same reason — the last onScroll can be
stale by the time a short flick lands.

Android needs both the blur and the dismiss. Dismissing alone leaves the
input focused and the keyboard inset applied, so the layout stays shifted
with an empty gap where the keyboard was; blurring alone releases focus but
leaves the IME on screen.

Verified on an Android device with a Release build: a slow drag and a
0.6 dp/ms scroll keep the keyboard, a 2.9 dp/ms flick dismisses it and the
composer settles back with no leftover gap. iOS was exercised by hand on
device only, without automated coverage of the gesture itself.

* refactor(app): isolate keyboard flick dismissal

* fix(app): isolate keyboard shift context

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-28 22:29:33 +08:00
89c2fac3c6 Open project and workspace folders from the sidebar (#2491)
* feat(app): open project folder from project context menu (#2487)

* refactor(app): give file manager action a home

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-28 00:48:29 +02:00
f8dd0fc2e0 Switch projects from New Workspace with ⌘P/Ctrl+P (#2110)
* feat(app): add ⌘P shortcut to switch project on New Workspace screen

Opens the existing project picker with its search focused so the project
can be switched from the keyboard (type + Enter) instead of clicking the
badge and then the project.

Wires a new "workspace.project.pick" action through the standard keyboard
pipeline (binding -> route passthrough -> dispatcher), handled by a
screen-scoped handler on the New Workspace screen that is only registered
while the screen is mounted and there are projects to pick. Because
preventDefault only fires when a handler handles the key, ⌘P/Ctrl+P still
triggers native print everywhere else. Adds a Settings -> Shortcuts help
row (rebindable) and the "Switch project" label across all locales.

* test(app): cover project picker shortcut

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-28 00:32:41 +02:00
Matt CowgerandGitHub 717f195f0c fix(app): render HTML in PR comments (#2432) 2026-07-28 04:01:43 +08:00
869edcbf11 fix(forge): preserve non-default port in forge web URLs (#2478)
"Open in browser" links for a self-hosted forge served on a non-standard
port (e.g. Forgejo/Gitea on :60443) dropped the port, producing
https://host/owner/repo/... instead of https://host:60443/owner/repo/...,
which 404s or hits the wrong service.

parseGitRemoteLocation discarded parsed.port (GitRemoteLocation had no
port field), and buildForgeBranchTreeUrl / buildForgeBlobUrl rebuilt the
origin from the portless host. Preserve the port on GitRemoteLocation and
reattach it in the web-URL builders, only for self-hosted http(s) origins
(an SSH port isn't the web port; a canonicalized cloud host uses the
default port). Host-identity matching (forge detection, cloud-host checks)
stays port-agnostic.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 04:00:46 +08: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 BoudraandGitHub 2acb10fce9 Keep provider settings above the model selector (#2476)
* fix(app): keep provider settings above model selector

Desktop web comboboxes used the browser top layer, so ordinary modal portals could not cover them. Keep web overlays in one relative layer stack while preserving native bottom-sheet stacking.

* fix(app): route overlay keyboard ownership

* fix(app): preserve nested overlay ownership

* fix(app): route command center overlay input

* fix(app): preserve global overlay ancestry

* fix(app): register global dialog hosts
2026-07-27 14:04:29 +02:00
Mohamed BoudraandGitHub b97d6d13f3 Load complete chat history when reaching the top (#2481)
* fix(chat): load older timeline history consistently

Pagination depended on scroll events, so short or compacted initial pages could never request older history. Reevaluate edge visibility as layout and history change, and standardize loading indicators on the canonical app spinner.

* fix(chat): keep older history loading reliably

Use authoritative timeline cursor progress so merged rows cannot stall pagination. Wait for bottom anchoring before evaluating the history edge, and re-arm retries on a fresh upward gesture without automatic failure loops.
2026-07-27 18:21:07 +08:00