mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-05 07:54:38 +00:00
77f97f82bcbedaaff029311605fbefab13b2c53e
359
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a880c9619d |
feat: background agents for hidden helpers (#8683)
* feat(server): add background agents An agent can be created background in any workspace. It stays a durable, addressable agent, but default discovery, workspace status and attention leave it out. Effective background is agent.background || workspace.background. - Protocol: `create_agent_request.background` now marks the agent itself and stays the workspace intent for a workspace the request creates. Snapshots carry `background`. The daemon advertises `features.backgroundAgents`. Clients advertise the `background_agents` capability. - Daemon: the flag persists in the agent record, so restart and resume keep it. AgentDirectory owns visibility. Inclusive reads and subscriptions from sources without `background_agents` see only agents in background workspaces, because older apps would otherwise open a tab for each one. - Inheritance: omitted on agent-scoped creation, children and new workspaces take the caller agent's background. - MCP: create_agent accepts `background`. - SDK: placing a background agent in an existing workspace, or under a caller, fails fast with "Update the host to use background agents." on older hosts. - CLI: `ls -a` and `ls --background` add a BACKGROUND column. * feat(app): hide background agents from tabs, track and status * docs(plugins): point hidden helpers at background agents * fix: keep background agents hidden across cache reload and connected hosts * fix: keep remote creation working on older hosts and inherit foreign parents' background |
||
|
|
336e7d1b89 |
Add first-run setup that walks new users through Paseo (#8363)
* feat(app): add first-run setup modal New users landed on an empty "Add a project" screen and never found providers, Paseo tools or phone pairing, which all live in Settings. A one-time setup modal now walks them through Appearance, Providers, Projects, Orchestration and Phone. Every step renders the same component Settings uses, so changing something in setup changes it in Settings too. The modal opens only when the setup host is connected, its workspaces have loaded and it has no projects. Existing users already have projects, so they never see it after upgrading and no migration is needed. Finishing or skipping writes `@paseo:onboarding` on this client. It uses its own key because it records feature state, and e2e specs replace app settings wholesale. Settings > General > "Show setup again" reopens the modal on step 1. Native phones skip the modal for now. Shared pieces moved out so setup and Settings render the same thing: - ThemePicker is a tile grid and replaces the Settings theme dropdown. - PaseoToolsCard is extracted from the host page. - The skeleton pulse moves to components/ui. Provider rows show it while the first snapshot loads instead of "Loading…". - The pair-device direct-connection hint links to the Tailscale docs. The e2e fixture marks setup completed by default so existing specs keep their starting state. The desktop smoke and lifecycle scripts, which start from a fresh daemon, mark it before they continue. * feat(onboarding): suggest projects from agent history The setup's Projects step lists the repos the user already ran agents in, with busy recent ones preselected, so a new user adds their projects in one click instead of browsing for folders. With rows checked, the footer's main button adds them and only moves on once every one was added; a checked row always means "this will be added". The daemon resolves each session folder to its project root (main checkout for worktrees, repo root for subfolders) because only it can read the git state; the app asks once per provider so a slow or broken provider never holds up the others and can be retried on its own. Hosts without the projectSuggestions feature show only the Add project button. Onboarding e2e hosts read an empty Claude history with other providers off, so suggestions never depend on the machine running the tests. * fix(onboarding): keep late project adds on their step and show Retry early - A finished add only advances setup while the Projects step is still open. - A failed provider shows its Retry as soon as any provider answers. - The desktop lifecycle check no longer reloads the window: setup never opens there because no host is connected. * fix(onboarding): bind late project adds to their step visit and freeze rows while adding - Each step visit has an id; a finished add only advances the visit that started it, so leaving and returning to Projects, or reopening setup, ignores it. - Suggestion rows are disabled while their batch is being added. * test(app): drive theme selection through the tile picker Settings → Appearance no longer has a theme dropdown. The Escape test opens the syntax theme dropdown instead, and the Android system-bars check taps theme tiles. * fix(server): return project suggestion roots in the form project.add stores Git reports Windows repo roots with forward slashes, so an added suggestion kept showing because its path never matched the new project's root. * feat(app): two control sizes, orchestration switches and mobile connect choices in setup Controls had four sizes picked per call site, so a button in a dialog, a settings row and a setup step came out at different heights. Controls now have two: md (default; 32px with a pointer, 44px in the compact layout so phone controls are full touch targets) and sm (28px, for tight chrome and nested settings rows). The breakpoint picks md's height, not the caller. Setup: - Orchestration is three switches (Paseo tools, agent skills, CLI), all on, applied on Continue. Turning one off undoes it; the desktop CLI uninstall removes only the file this app installed. - The Mobile step has store buttons and a "Choose how to connect" section: Relay opens the existing pairing modal, Tailscale opens the docs. Settings section docs links are one pattern, SettingsSection's `link` prop, and modals sit on surface0 like a screen. The vitest unistyles stub now resolves breakpoint values by window width, as Unistyles does at runtime; without it md controls had no height in browser tests. * fix(app): finish setup on its last step and confirm undoing orchestration In the compact layout Orchestration is the last step, and Finish closed setup without applying its switches. The last step's work now runs before Finish, the same as Continue. Turning off skills now asks first, as Settings does. Removing the CLI keeps a paseo command this app didn't install and says so. A failed Paseo tools update shows a toast. Adding suggested projects no longer advances when a row was checked while the add ran. Chat sender attribution and the message preview toggle go back to the sm button: md padding is a breakpoint value, and on web its media-query rule beat the zero right padding that aligns the sender with its bubble. * fix(app): ignore a late Finish once the user left the last setup step Finish called complete(), which ignored the step visit, so pressing Back while Orchestration applied its choices still closed setup and recorded it as done. next(visit) now finishes setup from the last step, so a late Finish is dropped the same way a late Continue already was. * fix(app): hold the skills switch while setup applies its choices The Paseo tools and CLI switches were disabled while Continue or Finish applied them, but the skills switch only locked while a skills operation ran. With skills needing no change, it stayed editable during a pending tools update and the new value was silently dropped. * fix(app): read setup's skills choice as off when no skill is picked Saving an empty selection in the skills picker left setup's skills switch on. Continue then asked the host to install nothing, the host reported skills as not installed, and the step stayed put with no error. Setup now reads skills as off with nothing picked, as Settings does. |
||
|
|
e06adf7a3e |
Show new files in collapsed folders after Refresh (#8582)
* fix(app): show current contents of collapsed folders after Files refresh Refresh only re-listed expanded folders, and expanding a folder skips the request when a listing is cached, so a folder collapsed during Refresh kept showing its old contents. Drop the cached listings Refresh does not re-list. The refresh helper moves to file-explorer/tree.ts so it can be tested. * test(app): cover refreshing collapsed folders through the browser Exercise the reported file creation, refresh and reopen sequence through a real daemon, then check refresh while expanded. Co-authored-by: hadan8977 <zc-huang23@mails.tsinghua.edu.cn> * fix(app): ignore directory replies invalidated by Refresh A directory request started before Refresh could finish afterwards and restore a stale collapsed listing. Track the refresh generation in explorer state and accept completion only from the current generation. Keep directory loading and invalidation in the file-explorer model. * fix(app): preserve expansion when Refresh cancels restoration Keep an invalidated directory operation distinct from a failed listing. Propagate cancellation through saved-folder restoration so it preserves the current expansion state instead of committing an incomplete restored path set. --------- Co-authored-by: hadan8977 <zc-huang23@mails.tsinghua.edu.cn> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
385c29b655 |
Keep failed chat history available for retry (#8278)
* fix(app): keep failed chat history until retry Automatic retries repeatedly reopen provider sessions after persistent failures. Keep the error visible until the user retries, while retaining reconnect recovery. * fix(app): catch up chats after subscription failure A failed replacement subscription leaves chats unsubscribed, so discard its old catch-up state and fetch missed history after retry. * fix(app): recover gaps during history retry Keep gap recovery active while a user-initiated retry is running so the chat is not marked current before its follow-up fetch. * fix(app): show history errors in archived chats Cached archived chats need the same manual retry affordance when history synchronization fails. |
||
|
|
d6d5a266ad |
Open a subagent in place of the subagents list on mobile (#8093)
* fix(app): seat the subagent sheet's parent callout like the composer The sheet tab body padded the bottom safe-area inset, and the composer dock padded it again, so the parent callout in a subagent sheet floated a full navigation-bar height above where the composer sits in a tab. The sheet body (SheetBottomInset) now owns the inset: it pads it once and tells its content none is left, so the dock stays host-agnostic. Overlay sheets (adaptive modal sheet, menu sheet, combobox sheet) read the inset on the far side of their portal, so ones opened from sheet content still clear the screen edge. The callout takes the composer's rail and shows the parent's provider icon through StatusIcon: no icon box, with the running ring, attention dot or needs-input alert on the icon. * fix(app): give the attachment lightbox its own safe area An RN Modal passes its call site's context straight through, so a lightbox opened from a sheet tab read the bottom inset the sheet body had already spent (0) and drew over the navigation bar. The modal is its own window: it now gets its own SafeAreaProvider, which measures that window, and the image area reads its insets below it. floating-panels.md records the rule for overlays: read insets where the overlay draws. * feat(app): open a subagent in place of the subagents list on compact On phones, choosing a subagent from the subagents list now replaces the list inside the same sheet instead of stacking a second sheet over it. Back returns to the list, Close closes both, Android Back steps subagent -> list -> closed, and Open parent lands on the pane the list was opened from (for a nested subagent, its parent's sheet). - Menu engine: fill pages (`fill: true`) take the full sheet height, Back pops a page via `goBack`, and a page whose definition disappears falls back to the list. - One sheet pane (`screens/workspace/sheet-pane`) renders the header, body, bottom inset and focus for both containers: the workspace's detached sheet tabs and the in-list menu page. Only the container differs. - The header drags the sheet through `SheetDragArea`; compact menu sheets use the shared `SheetBackground`/`SheetHandle` chrome. - `selectSheetTab` describes the sheet a reveal would show without opening it. * fix(app): put the parent callout on one row The Open parent button already says what the callout is, so the Parent label goes. The parent's title shrinks to one line before the button does, so Open parent never wraps or leaves the row. * refactor(app): register panels once in the workspace screen Every reader of the panel registry called ensurePanelsRegistered, which imports every panel module. The panels render those readers (the agent panel's subagent track and parent callout, the new-tab panel's launcher, the diff panel's review surface), so each reader closed an import cycle through register-panels. The workspace screen, the only route that renders a registry reader, now registers the panels before anything under it renders, and the readers import only the leaf panel-registry. The pane content test registers a probe panel for real instead of mocking the registry. * test(app): transform the markdown WASM factory when Vite versions its URL Importing the sheet pane barrel in retained-chat-content.browser.test.tsx made Vite request md4c.js with a ?v= query, which the CommonJS-to-ESM transform did not match, so the test file failed to import. |
||
|
|
65ce4b590a |
fix(app): remove restored agent tabs from foreign workspaces (#7956)
A persisted explicit pin outlives archive recovery because its agent never joins the unrelated workspace directory. Drop positively foreign pins after hydration and let existing reconciliation remove only the misplaced view. Preserve unknown ownership and legitimate historical opens. Adapts the repair proposed by kaicot in getpaseo/paseo#6901 to current main, with persisted-store and real-browser regression evidence. Refs getpaseo/paseo#6900. |
||
|
|
6f07b0de48 |
feat(app): recall sent messages with Up and Down in an empty composer (#7710)
Fixing a prompt after interrupting it, or retrying one after a provider error, meant scrolling up and copying it out of the chat. Up in an empty agent composer now fills the last message sent to that agent; Up walks older, Down walks newer and past the newest back to empty. History is read from the loaded timeline on key press, not kept in a separate store, so it follows rewinds, reloads and other devices and costs nothing while a turn streams. Recall only starts from an empty composer, so it never discards a half-written draft. While a recalled message is unedited the autocomplete stays hidden, otherwise a recalled `/command` would open the picker and Enter would pick an option instead of sending. |
||
|
|
686588c44b |
feat(app): restore manual workspace sorting (#7771)
Keep manual workspace order separate from automatic sorting so surviving pre-beta arrangements and subsequent drags remain available across mode changes. Created stays the default; data already discarded by the beta cannot be reconstructed. |
||
|
|
582beeb10b |
fix(app): preserve compact sidebar context under sheets (#7693)
Keep compact sidebars open while users inspect Search, Import session, Add project, or file sheets. Reveal the resolved destination when navigation commits, and place compact workspace sheets above both sidebars while preserving shared editing and cache state. |
||
|
|
4fe006df86 |
feat(plugins): unify panel props and persist tab state (#7685)
* feat(plugins): unify panel props and persist tab state * refactor(plugins): make agent panel registration canonical * docs(plugins): update Chinese panel API reference and migration |
||
|
|
55a7590952 |
Sort sidebar workspaces and keep active overflow visible (#7635)
Order ordinary sidebar workspaces by Created or Last prompt, preserve project and pin ordering, and keep later active or unacknowledged work visible beyond the first seven rows. Preserve reveal state through live updates and verify sorting, visibility, compatibility, and rendering behavior. |
||
|
|
12e7a448f0 |
fix(app): open notification agents once in their current workspace (#7612)
* fix(app): open notification agents once in their current workspace Keep tap ownership across Activity and React remounts, clear native responses, and resolve agent workspaces through existing navigation after startup settles. Co-authored-by: Kassam Khoja <kassam@shopwrk.com> * fix(app): hydrate agent targets with the compact sidebar closed Hold directory demand for the agent route while resolving its workspace; the compact sidebar does not supply demand before it opens. * fix(app): keep deferred workspace opens in the layout model Key pending tab opens by workspace so redirects and retained screens cannot lose or consume another workspace's target. Legacy open URLs seed this state at their route boundary. * docs(app): document notification tap ownership and native replay * fix(ci): route feature-owned Electron sources to desktop checks * refactor(app): separate notification API from React integration * fix(app): consume pending opens before painting the restored tab A legacy open link showed the previously focused terminal for a frame before switching to the requested terminal. Seed and consume the pending open in layout effects so a ready target switches before paint. * fix(e2e): focus the visible retained terminal surface --------- Co-authored-by: Kassam Khoja <kassam@shopwrk.com> |
||
|
|
bdae614e95 |
fix(app): restore chats after reopening unread workspaces (#7608)
Parked pane focus was persisted without its in-memory restoration record, so cold starts exposed hidden Explorer tabs and never requested the agent timeline. Restore an ordinary pane at the layout boundary, including for existing saved layouts. |
||
|
|
91dca5702b |
Connect saved hosts and delegate agents across daemons (#7370)
* feat(hosts): connect to saved hosts through a daemon * fix(hosts): validate safe targets and verify encrypted relay routes * test(hosts): include relay coverage in existing CI contracts * fix(cli): preserve validated addresses in daemon command hints * fix(hosts): share daemon links across isolated routed sessions Multiplex logical sessions over the monitored daemon connection and wrap original binary bytes in routing headers. Admit channels under their authenticated parent and current grants; preserve scoped delivery and physical send completion. * fix(hosts): bound encrypted overload and initial acquisition Check encrypted wire capacity before the async channel converts a resource limit into a physical connection error. Bound initial routed acquisition with the existing client timeout, then enable reconnect after admission. * fix(hosts): keep compact row actions clear of host labels Give compact connected-host rows separate text and action widths so long names and producer labels cannot compete with primary controls. * refactor(hosts): remove management settings UI from phase one Keep connected-host management in the CLI and shared SDK while preserving app saved connections and plugin access. * test(hosts): control physical write backpressure across platforms Hold real TCP writes until assertions finish instead of assuming a paused receiver exhausts every platform receive buffer. Preserve physical send settlement, byte identity, capacity and sibling checks. * refactor(hosts): use the ordinary client contract over channel transports Keep queuing, permission codes and reconnection independent of transport selection. Track submitted request outcomes generically while retaining connection error codes and structured causes. * test(e2e): keep transient developer toasts from stalling checks Concurrent locator handlers can wait for a dismiss button another handler removed. Dismiss existing toast DOM at document lifetime, including its open shadow root, without intercepting app actions. * Create, prompt, and monitor agents across connected hosts (#7507) * feat(hosts): delegate agents across connected hosts Resolve MCP host operations through in-process commands for self and the existing routed client transport for other destinations. Keep ordinary parent labels, app sessions and idle finish notifications. * test(app): normalize subagent fixtures through snapshots * docs(protocol): clarify same-host parent labels * fix(hosts): resolve subagents through destination sessions A child address identifies its destination regardless of the connection used. Reuse the existing host candidate selection for discovered routes and direct connections, and keep navigation and row actions independent of the parent transport. * fix(hosts): keep routed destinations distinct during discovery A routed candidate names only the origin daemon. Match it within the named destination profile so two destinations discovered through the same origin retain separate identities and connections, including existing saved routes. * feat(hosts): prompt and query agents on selected hosts Extend the existing MCP host operation boundary with prompt, status, activity and list operations. Local calls retain their handlers and notification owner; selected destinations use ordinary client RPCs and the existing idle finish wait. * test(e2e): preserve toast coverage with CSS hiding * docs(hosts): translate connected host references |
||
|
|
2545250b2f |
fix(app): keep saved workspace tabs when a store changes before hydration (#7547)
On every page load the layout mode publisher sets the workspace layout store before its stored state has been read. Zustand persist wrote the unhydrated defaults, and hydration merges the stored layout into memory without writing it back, so storage held no tabs until the next layout change. Leaving the page inside that window lost every workspace's tabs. Validated persist storage now drops writes that arrive while its read is in flight. |
||
|
|
e464fd3bd3 |
feat(plugins): file panels (#7411)
* fix(files): report document MIME types for binary files
File reads now carry the document's MIME type for binary files, so a file panel can
hand the bytes to a viewer that needs the real type (application/pdf).
* feat(plugins): file panels
Plugins contribute file panels with addFilePanel. A file tab hosts the panels that claim the
file: the built-in Markdown, HTML and image panels, plugin panels, and Source, Paseo's editor,
which claims every text file and is always the last candidate. The panel is the tab body;
the bar shows the document's status and a switcher between the file's panels (side by side
up to three, two on a phone, a menu beyond that).
Paseo asks which panel opens a file only when a plugin panel is among several candidates,
once per open, and remembers the answer per extension or exact filename when told to. Open
with in the tab menu and the switcher change the tab's panel; Always use remembers it.
Opening a file at a line switches the tab to Source.
Panels read and write the host's document, so dirty state, autosave and conflicts stay
Paseo's. DocumentFrame shows bytes documents such as PDFs in the browser's viewer.
Replaces the file tab's Preview/Source mode toggle.
* feat(file-panels): choose and forget file panels in Settings > Files
Settings > Files lists each extension or filename where a plugin panel competes with
another candidate, or that has a remembered panel. Each row chooses the panel that opens
it, or Ask every time; Ask again forgets the choice. Source appears as a candidate for
text files. Panels no connected host offers can be forgotten but not chosen.
* docs(plugins): file panel examples and author reference
Adds the pdf-panel and markdown-panel examples, the File panels section of the plugin
reference, and an e2e spec that installs both examples, opens a PDF in the browser
viewer, and edits Markdown through Paseo's document across the switcher, Source,
autosave and conflicts.
* feat(file-panels): panels read and write through a file handle
The file tab stats the file and mounts its panel without reading it. Every
panel, built-in or plugin, gets a PluginFile handle: readText, readBytes, url,
version-checked writeText/writeBytes (PluginFileConflictError unless force),
and watch. useFileText, useFileBytes and useFileUrl read through it, and
Loading joins @getpaseo/plugin/client/ui. Panels own their loading state.
The host document pipeline, the explorer preview cache and the preview
lifecycle are deleted. The daemon classifies a file version from a bounded
sniff and writes base64 content as bytes behind fileByteWrites.
A deleted file keeps its panel mounted; a read confirms the deletion first,
so a remove-and-rewrite is not reported as one. Native Source decodes with
builtins and highlights in idle-time slices, the lines around the opening
line first, so a large file no longer blocks the JS thread.
* fix(file-panels): Settings offers Source where the picker does, and review follow-ups
- Settings > Files offers Source on every key except those a built-in panel of
another kind (Image) claims, so a plugin that alone claims a text extension
shows the same choices as the picker.
- Delete the unused isSourceFilePanel.
- The Markdown example warns with the file name when its unmount write fails,
rather than dropping the edit silently.
- Document that PluginFile stat fields are read at render time and that watch
or the read hooks are how a panel learns they changed.
* fix(plugins): file events are objects, deletion is a typed error, DocumentFrame owns its fallback
- watch listeners receive { type: "changed" } | { type: "deleted" }, so later
versions can add fields without breaking them.
- PluginFileDeletedError joins PluginFileConflictError. The read hooks report a
deleted file with it, and a write over a deleted file rejects with it; Source
shows that as the deleted notice. PluginFileConflictError now means only
"changed on disk".
- DocumentFrame on Android shows Paseo's "<TYPE> files can't be shown on this
device" message for documents its WebView cannot display, so the PDF example
is useFileUrl + Loading + DocumentFrame with no platform branch.
* test(file-panels): Settings offers Source for a remembered panel no host has
* test(e2e): hide the developer error toast instead of clicking it away
The locator handler that dismissed #error-toast could match a toast that closed before its
click, then wait on it until the test timed out (agent-timeline-resume.spec.ts:45 failed every
attempt on this branch). Hiding the toast with CSS keeps it off app actions with no race.
* test(e2e): desktop browser-tabs hides developer toasts on its attached page
|
||
|
|
adf27de7d0 |
Keep Explorer consistent across phone, tablet and desktop layouts (#7424)
* Upgrade to Expo 57 and unify Explorer across layouts * feat(app): retain Explorer across layout and appearance changes Keep one workspace Explorer host and destination-aware navigation so folding, theme updates and compact sheets preserve the current panel. Scope pull request discovery and timeline demand to active surfaces. * fix(app): keep Explorer resize handles stretched through the dock The shared frame moved the divider into an absolute column, leaving its horizontal handle without height. Restore the row so pointer drags reach it, preserving the existing resize and performance contract. * fix(app): keep Explorer drag widths out of the shared stylesheet Carry the existing inline sizing policy into the shared frame so pointer previews do not accumulate CSS rules. * fix(test): align isolated test runtimes with Expo 57 Hoisted React consumers need the matching DOM renderer at the repository root. Automated Metro processes must not install native DevTools into disposable homes, and development toast overlays must not cover compact actions. Keep Explorer journeys on the shared host and synchronize setup fixtures on daemon output rather than a one-second file-marker window. A delayed real child start reproduces the readiness failure on every platform. * fix(test): await retained shell layout after composer resize The gesture host now stays mounted across breakpoints, so its presence no longer identifies compact layout. Report the current layout on the retained host and synchronize composer journeys with that state. * fix(app): preserve browser interactions in retained workspace hosts Initially disabled gesture hosts still registered pointer and wheel listeners, while hover cards used window positions inside a workspace-local sheet portal. Respect initial gesture enablement and position cards relative to their measured floating host. Keep developer-toast dismissal independent of pointer input and replacement toast visibility so modal, hover, and focus checks retain their input state. * fix(test): share developer toast handling with Electron browser runs The standalone Electron browser runner bypassed the browser fixture, allowing developer errors to cover Settings. Use the same non-pointer dismissal in both runners while retaining their console logs. |
||
|
|
1c8c68f93e |
Keep workspace setup logs scoped to the visible setup panel (#7407)
* fix(workspaces): observe setup output only in the visible setup panel Setup logs were requested for the whole session, including unrelated workspaces. Use directory status for discovery and source-owned workspace observations for details, preserving legacy delivery only for clients without the negotiated capability. * fix(workspaces): publish external setup progress once per daemon MCP and scheduled setup enter through the broadcast API. Publish them to the shared setup owner before session fanout so each observation gets one update and snapshots survive periods without connected clients. Keep the setup action available on older hosts to explain the required host update. * fix(app): replace setup details with each observation snapshot A host restart can return an empty setup snapshot. Apply that authoritative snapshot so an open panel does not retain output from the previous daemon. Assert setup membership requests as well as delivery in the browser lifecycle regression. * test(workspaces): cover setup status catch-up after directory release * test(workspaces): synchronize setup output and release fixture resources |
||
|
|
c575464e8f |
Skip saving unchanged workspace layouts (#7358)
* perf(app): throttle workspace layout writes and skip no-op persists Zustand persist calls setItem after every set, even when the updater returns the same state. The layout store re-normalized every workspace tree, re-validated the envelope, stringified it, and wrote to AsyncStorage on each agent status change because reconcileTabs runs from an effect keyed on agent visibility. The draft store's throttled writer moves to storage/ as a generic throttled persist storage, gains a referential short-circuit for a value whose partialized state was already accepted, and now backs both the draft and layout stores. The layout store's partialize reuses its previous projection while every persisted slice is the same reference, and the store is flushed alongside drafts when the app leaves the foreground. Before: 50 no-op reconciles produced 50 writes. After: 0 writes; 50 real sets inside one interval produce 2 writes (leading and trailing). Fixes #4814 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(app): build layout stores with options and flush before rehydrating Six call sites still passed the id factories positionally, which this branch replaced with an options object, so those stores silently used default ids. The once-only PR test also rehydrated a second store before the throttled write landed; flush the source first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(app): avoid shadowing layout options --------- Co-authored-by: Ivan Malison <IvanMalison@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
8d424e039e |
Reduce rendering cost in long chats (#7052)
* perf(app): stop re-walking the timeline on every streamed delta Every coalesced delta rebuilds the stream render model with the same tail. The model re-sliced the tail at the history-window start on every call, so the rendered tail was a fresh array per delta and every WeakMap keyed on it (ordering, history split, turn timing, the layout history cache, row revision) missed each time. Cache the slice by tail identity and window start. Turn timing then still walked the whole rendered tail per delta and allocated a new assistant-timing Map, which the layout memo depends on. Split the walk: the tail is walked once per tail identity and the head is walked from the carried open turn per delta. The Map keeps its identity while the head adds no completed turn. Adds identity-contract tests and a vitest bench for the per-delta pipeline. 200 deltas over a 2,000-item timeline, mean per run: default mounted window 5.68ms -> 2.82ms, history fully revealed 16.95ms -> 1.81ms. Fixes #4816 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * perf(app): keep the stream view's per-delta work bounded by the head The chat outline concatenated tail and head, reduced over the result in the render body, and rebuilt jumpToPrompt on every delta even when the outline was disabled, which is every native viewport. Memoize the tail's contribution, scan only the head per delta, do nothing while disabled, make jumpToPrompt a stable event so the rail's memo holds, and republish the active prompt only when the index or reading position changes. The view built a Set of every mounted id plus the live head per delta for the outline's pending-jump check; key it on mounted history only and treat live head rows as always mounted. Split plugin projection so a head change does not re-project the tail, memoize the history-window boundary lookup, and read the turn presentation's submission flag with some() instead of allocating a filtered array. Refs #4816 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(app): confirm full history before measuring streaming --------- Co-authored-by: Ivan Malison <IvanMalison@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
0009a7e717 |
feat(workspaces): add background visibility (#5039)
* feat(server): let clients create internal agents The daemon's own branch-name and commit-message generators run as internal agents: never persisted, listed, flagged, announced, or seen by plugin lifecycle hooks. Nothing outside the daemon could ask for one, because the wire config schema strips `internal` and the agent.create transform re-pins it. A plugin that runs a one-turn helper per event therefore left an archived session in History every time. Add an optional `internal` flag to `create_agent_request` (inherited by `agent.create.request`), copy it onto the config in the session's create path, and skip provider session persistence for it like the generators do. The client SDK forwards the flag on both creation paths and gates it once on `server_info.features.internalAgents`. Two daemon paths assumed every agent is public and had to follow: `resolveAgentIdentifier` now accepts a live agent by exact id, since the id set it matched against came from listings that exclude internal agents and left `wait_for_finish` answering "Agent not found"; and archiving an internal agent now closes it without writing the record that `registry.applySnapshot` used to leave behind, so `autoArchive` does not accumulate hidden files on disk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(server): keep internal agents whole through failure, archive, and wait Review follow-ups for internal agents: - A create that fails after the runtime is registered now closes the internal agent before rethrowing, so a hidden runtime cannot outlive the failed request. Public agents keep today's behaviour. - Archiving an internal agent cascades to attached children like any parent. cascadeArchiveChildren takes the parent record instead of reading it back from storage, where an internal agent never is. - The archived snapshot and last message of an internal agent stay readable by exact id for ten minutes, so create-then-waitForFinish returns the finished turn whether the wait lands before or after an auto-archive. Storage plays that role for public agents. - The internal-agent e2e scenarios live in the existing create_agent_request lifecycle suite rather than a new file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(server): cascade to live internal children and keep a failed internal create's worktree until it closes Internal children never reach storage, so the parent archive cascade scans live agents for them as well. A failed internal create now clears the created id only after the runtime is archived, so worktree cleanup cannot delete the directory under a runtime that would not close. Stored agent payloads carry lastError, so a retained failed helper reports its error through wait_for_finish instead of the generic message. * feat(cli,mcp): expose internal agents and document --no-wait paseo run gains --internal and create_agent gains internal, both forwarded to the create request. --no-wait becomes the documented spelling for returning immediately, matching paseo send; --background and --detach stay as hidden aliases under COMPAT tags. * test: follow the --no-wait rename and stub getRetiredInternalAgent * feat: list internal agents on request and add internal workspaces Internal agents stay hidden by default, but a caller can now ask for them: `fetch_agents_request` takes `filter.includeInternal`, snapshots carry `internal: true`, and `paseo ls --internal` shows them in an INTERNAL column. The app never passes the flag. Workspaces get the same flag. `workspace.create.request` with `internal: true` persists a hidden workspace: listings, update subscriptions, sync reads, empty-project counting, open-by-path, MCP `list_workspaces`, and plugin workspace hooks all skip it unless the caller opts in with `filter.includeInternal`. Every agent created inside an internal workspace is internal and unpersisted; AgentManager applies that from the workspace id before the plugin hook so creators cannot opt out. Archive and shutdown enumerate internal agents so nothing inside an internal workspace outlives it. Surfaces: `paseo workspace create --internal`, `paseo workspace ls --internal`, `paseo run --internal --new-workspace`, MCP `create_workspace({ internal })`, SDK `workspaces.create({ internal })` and `list({ filter: { includeInternal } })`. Clients gate on `features.internalWorkspaces`. Co-Authored-By: Claude <noreply@anthropic.com> * feat(workspaces): add background visibility with inherited creation defaults * fix(cli): resolve workspace callers on the selected daemon * test(workspaces): keep caller integration coverage in server suite * test(cli): exercise workspace callers through CLI integration harness * test(mcp): remove obsolete execution flags from browser fixtures --------- Co-authored-by: marvin-ambrozio <marvin@gustavo.eng.br> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
b85b44aea1 |
Show a host's new version after its daemon restarts (#5351)
* Show a host's new version after its daemon restarts The app wrote a host's server info into the session store only when a client was first mounted, and a reconnect reuses the same client, so after a daemon upgrade or restart the host page and version checks kept the old version until the app restarted. The host runtime now projects the client's current server info into the session store on every connection change, keeping the last known value while disconnected. The React mount effect that duplicated this write is removed. Closes #5236 Co-authored-by: Leonid <127580858+bashrusakh@users.noreply.github.com> * Keep a caller-owned test daemon home across startup retries A failed start removed the home even with cleanup disabled, so a retry on a taken port came back with a new serverId. The host restart spec now drives a restartable host daemon helper that owns its port, home, and teardown. * Remove only the test daemon dirs a failed start created A failed attempt now removes the temp home and static dirs it created and leaves caller-supplied ones, so cleanup: false no longer leaks per retry. * Use function declarations in the test daemon helpers * test(app): cover host versions across daemon disconnect and reconnect The host settings badge retains its last known version during a transport failure; the help menu should show the replacement version after reconnect without changing another host. --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> Co-authored-by: Leonid <127580858+bashrusakh@users.noreply.github.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
a3fc59e818 |
Keep a project in the sidebar when its folder name ends in a space (#5205)
* fix(app): keep sidebar order keys that end in whitespace A project view key embeds the project's path, so a directory whose name ends in a space produces a key that ends in a space. The sidebar order store trimmed every key on write, so the stored key could never match the key the sidebar looks up. computeSidebarOrderUpdates saw the project as missing from the order on every pass, appended it, and the store trimmed it away again, setting a fresh array each time. The effect that applies those updates re-ran on every render until React gave up with error #185, and the app failed to load until the folder was renamed. Keep keys verbatim on the runtime setters, dropping only blank keys and duplicates. Migration of persisted state still trims, where a stray space is an artifact of an older format rather than part of the key. Fixes #4880 * test(app): give AsyncStorage a real in-memory stub instead of mocking it `docs/testing.md` bans `vi.mock`, and the test added here mocked `@react-native-async-storage/async-storage` only because the module has no Node implementation: its default export reaches for the native module, so every write from the persist middleware rejects and a test that touches a persisted store drowns in unhandled rejections. That is exactly the case `test-stubs/` exists for, reached through the resolve.alias in `vitest.config.ts` the same way react-native-unistyles, react-native-svg and expo-linking are. The stub is a real in-memory key-value store with the same contract, so a test can read back what a store wrote rather than inspect a recording double. `sidebar-view-store.test.ts` carried the same mock for the same reason and no longer needs it either. * test(app): give the AsyncStorage stub the multi-key calls the app uses `provider-snapshot-cache` declares its own `ProviderSnapshotStorage` port and is handed the real AsyncStorage, so it calls `multiGet` and `multiRemove` during reconciliation and cleanup. The stub stopped at the single-key calls, which is fine until a test reaches the provider cache and then fails with `storage.multiGet is not a function`. `multiSet` comes along with them so the three multi-key calls behave as a set rather than leaving the next caller to notice the gap the same way. * test(app): cover a project folder name ending in a space end to end Adds a browser regression for #4880: the daemon accepts a directory whose name ends in a space, and the sidebar has to render its project row instead of tearing the app down with "Maximum update depth exceeded" on every load. * test(app): move the trailing-space spec setup behind a seeded-project helper docs/testing.md asks for E2E bodies that read like plain English with the setup in helpers, and for helpers that own the project record until cleanup. The spec did its directory creation, daemon seeding, and removal inline. seedProjectNamedWithTrailingSpace owns the directory and the daemon project and returns a cleanup, the same shape seedWorkspace returns, which cannot be used here because mkdtemp appends the random suffix and so cannot produce a name that ends in a space. The body is now goto, assert the row, assert no error screen. The row is still located by its sidebar-project-row test id, which is how every other sidebar spec and the shared sidebar helpers address a project row. --------- Co-authored-by: L4XB <lukas.buck@e-mail.de> Co-authored-by: paseo-bot[bot] <paseo-bot[bot]@users.noreply.github.com> |
||
|
|
83f9fba209 |
Keep the draft-create handoff until the new chat is current (#5168)
* fix(app): keep the draft-create handoff until the new chat is current Creating an agent from the draft composer on web flashed the "Updating messages" panel toast for 40-200ms. That toast means "this chat may be out of date, we are catching up" — a chat this client just created cannot be. The timeline reducer released the create handoff at the first synchronized page, but the visibility catch-up flag only clears once catch-up settles, and a fresh chat always has a parked follow-up "after" page because live rows arrive during the tail fetch. Between the two releases the state machine saw a hydrated chat with no handoff and a pending catch-up, which resolves to the resume toast. #4863 turned that branch from silent to status, which is what made the window visible. Both signals now flip in one synchronous call: createViewedTimelineSync gains an onTimelineCurrent port, called from setVisibilityCatchUpReady — the single place the sync declares a chat current, already covering the parked follow-up and the fetchLatestTail overflow path. The owner implements it by clearing the handoff. The optimistic "running" override therefore lasts one extra round trip. The state machine and sync-status rules are unchanged; they were correct. * fix(app): release the create handoff when a chat leaves timeline demand Closing a new chat's tab before catch-up settled left its sent attempt in useCreateFlowStore forever. Reopening it in the same session then supplied optimistic-create continuity for an authoritative chat: the panel showed it as running and suppressed "Updating messages" while the real catch-up ran — the state this fix exists to protect. An offline or slow host widens that window from milliseconds to minutes. Leaving the demanded set ends the owner's obligation to a chat exactly as reaching current does, so onTimelineCurrent becomes onCatchUpEnded and fires on both. It reports after the membership commit, so the sync never describes itself mid-transition. Disconnect and backgrounding preserve demand and therefore preserve the handoff. |
||
|
|
9696f42264 |
Subscribe only the chats this session opened (#5040)
* fix(app): subscribe only the chats this session opened Open-chat membership was sourced from the persisted workspace layout, which is restored from disk at launch and carries a tab for every chat the user has ever opened on the host. Every member got an authoritative timeline catch-up at session start, and each of those fetches resumed its agent on the daemon: one provider session per historical tab, every launch. Two things followed from each resume. Registration stamped `updatedAt` over the restored timestamp, and the resumed agent started with no attention because the resume path never read the stored flag. Both were persisted, so restarting Paseo rewrote every workspace's last-used time in the sidebar and cleared Ready to review on chats the user never looked at. The timestamp loss was permanent: workspace `statusEnteredAt` is re-derived from `updatedAt` on every daemon start. Keep the open set on the session instead. A chat joins it the first time the user opens it and stays until its tab closes or the session ends, so hiding, switching workspaces, evicting a retained view, backgrounding and reconnect all preserve the subscription, with no recent-agent limit. Layout keeps its release role and loses its source role. Harden the producer as well, so a resume from any caller stays inert: registration adopts restored timestamps rather than stamping now, and resume carries the stored attention through. * fix(agents): keep a resumed agent's updatedAt from moving backwards Dropping the registration stamp exposed a skew the stamp had been erasing. `extractTimestamps` read `lastActivityAt ?? updatedAt`, but the two diverge: renaming, labelling, restoring from archive, marking unread, and clearing attention all move `updatedAt` on an unloaded agent without touching `lastActivityAt`. Eight of 854 agent records on one dev machine carry that skew, up to eight days of it. A resume then handed clients a timestamp older than one they already held, and `acceptAgentDirectoryUpdate` drops any state update that goes backwards. Marking a chat unread without opening it, then opening it, left the unread dot stuck in History, the agent list, Command Center and the tab: the daemon cleared attention and the client refused the update. Read both fields and take the newest, which is already the rule `resolveStoredAgentPayloadUpdatedAt` applies to the list payload. Move that function beside the other stored-record readers as `resolveStoredAgentUpdatedAt` so one rule covers both paths. Also replace the `!options?.updatedAt` inference in `registerSession` with an explicit `restoring` flag. The inference was correct for all four call sites, but it made a second meaning out of a field, and the next caller to seed a timestamp would have opted out of activity stamping silently. * test(app): assert a reloaded layout subscribes only the chat it restores into No e2e reloaded the page with a populated workspace layout and asserted which agents the daemon acknowledged, so the regression this branch fixes had no end-to-end guard. Opening four chats across two workspaces leaves a tab per chat in persisted layout; after a reload only the restored chat may be subscribed, because subscribing resumes that agent on the daemon. Verified it fails when session-context's layout callback is pointed back at replaceVisibleAgentIds: the three restored tabs nobody opened join the acknowledged set. |
||
|
|
4f710d9930 |
feat(app): auto-open the pull request tab once when a PR is detected (#4956)
* feat(app): auto-open the pull request tab once when a PR is detected * docs(app): clarify automatic pull request tab policy |
||
|
|
7557992dff |
fix(app): stabilize composer height and isolate typing updates (#4902)
* fix(app): preserve composer capacity when the keyboard closes Releasing the keyboard reservation enlarged an unchanged capped draft. Retain the last editing reservation while still recalculating capacity for viewport changes and subsequent keyboard openings. * perf(app): isolate composer editing and resize updates Draft and cursor subscriptions rerendered the chat and full composer on every native edit. Give editing consumers a live text source, preserve attachment identity on text updates, and read viewport settling state without a render. Read live input text for web resize measurement so removing per-keystroke props does not collapse a draft when its width changes. * fix(app): restore current draft when the composer remounts The last replacement command does not include subsequent edits. Seed remounted editors from the live draft snapshot so switching launch modes restores the saved text without subscribing the composer to every edit. * fix(app): synchronize composer resolution state before paint Avoid an extra passive-effect delay when forwarding forge resolution state from the isolated binding. Exercise live-text width measurement through the actual composer E2E journey instead of a hook-only test component. |
||
|
|
b21c004ffe |
Keep open chats current and show reconnection progress (#4863)
* fix(app): keep open chats current and show resume progress Keep open-chat delivery independent of mounted views, freeze hidden chat rendering, and catch up visible chats before hidden ones. Distinguish host reconnection from timeline freshness so cached messages do not silently appear current after resume. * fix(app): prevent sync status from overlapping action errors Share the existing panel toast slot so action errors remain readable during reconnect and catch-up. Verify the interaction and draft retention in the real browser, preserve the tested freeze transition, and refresh the Nix dependency hash for react-freeze. * test(hub): expose complete creation and message retry failures Partial response assertions hid the creation error and allowed duplicate responses with incomplete agent data. Check complete success envelopes, the shared agent snapshot, and null message errors so future failures print the actual error payload. * fix(app): dismiss retained chat menus before freezing An async fork navigates before its closing state commits, so freezing the source chat could leave its portal backdrop blocking the destination. Remove the shared menu surface in the inactive commit before suspension. Keep regression assertions aligned with open-chat subscriptions, per-agent incremental recovery, plugin contribution lifetimes, and rendered image completion. * test(app): expect the reloaded plugin to contribute its pill again A full reload discards the pressed pill with the page, then evaluates the plugin again and its agents snapshot re-adds the pill. The old count-zero assertion only passed on main because the chat was not ready yet when the URL settled; open chats now subscribe from session start, so the pill is rendered by then. * fix(app): suspend hidden chats on web only react-freeze is Suspense with a never-resolving thenable. Suspending a committed subtree hides its host nodes, tears down layout effects, and detaches refs, which changes Fabric ownership of the retained chat. Split the boundary by platform: the web file keeps the two-commit freeze, the native file keeps the chat mounted and live, and the retained-panel signal remains the only thing that stops background work. Restore the mobile-panels rule the branch had rewritten. * fix(creation): never read a receipt while replacing it Admission reads the creation receipt while the runner replaces it atomically. On Windows a rename over a file this process still has open for reading fails with EPERM, and that error marked an otherwise completed creation as failed (#4865). Queue reads and writes per receipt so one owner never overlaps them, and serialize the record when the write is queued because the runner mutates it afterwards. No retry on EPERM: the conflict is removed rather than tolerated. Linux cannot reproduce the rename failure, so the Windows server-tests job (creation/index.test.ts and hub/execution-session.websocket.test.ts) is the verification for this change. |
||
|
|
c3bcad687d |
fix(app): prevent and recover workspace layout crash loops (#4844)
* fix(app): prevent and recover workspace layout crash loops * fix(desktop): capture resized browser tabs without hanging A resident guest can paint fresh pixels after resize while capturePage's native surface-copy request remains pending. The same failed guest returns a fresh frame through frame subscription, so waiting for resize or another animation frame does not correct the failing copy request. Capture one subscribed frame within the existing five-second budget and release the subscription on completion or cancellation. Preserve temporary frame production and its original throttling policy. Remove viewport copy retries and E2E screenshot polling; assert the resized image dimensions. Refs #4866. Three full real-Electron browser journeys passed under Xvfb, including physical clicks and fresh screenshots while the window is hidden. * test(app): choose Agent before exercising the runtime catalog The newly merged catalog journey assumed an empty workspace opened an agent draft tab. This branch opens the New launcher, so create the agent draft tab explicitly before checking its composer and model choices. * fix: settle closed browser captures and validate persisted layout fixtures Destroying a guest during frame subscription left its screenshot pending until the capture deadline and blocked queued captures. Reject at destruction, remove the listener on every exit, and return browser_tab_closed. The regression proves immediate settlement and queue release. Reuse the unchanged persisted layout schema from a UI-independent module so the recovery fixture validates storage before editing it. * test(app): encapsulate Explorer layout setup and cleanup Keep persisted layout schemas and corrupted-state construction inside the seedCorruptedWorkspaceLayout helper. Use the existing withWorkspace fixture for cleanup in all four Explorer journeys so test bodies follow a straight setup, action and assertion flow. * test(app): target the selected workspace in layout recovery The corruption helper selected the first persisted entry, so a multi-workspace journey could leave the intended workspace untouched. Resolve its exact persistence key from the requested workspace ID and fail setup when absent. The failing-first journey now preserves an earlier workspace with an agent draft tab while recovering the selected workspace. |
||
|
|
d63132a37d |
fix(creation): prevent duplicate workspaces and agents on retries (#4442)
* fix(creation): prevent duplicate workspaces and agents on retries * test(creation): cover plugin hooks across receipt replays * fix(creation): recover draft retries without accepting partial workspaces * refactor(creation): name receipt inputs and tag compatibility gate * fix(file-observer): track files announced by native change events * fix(creation): keep initial prompts in daemon-owned creation requests * fix(creation): reuse connection-owned progress and recovery * test(creation): adapt handoff gates to combined requests * ci(desktop): avoid xz compression for smoke packages * ci(desktop): pass valid RPM compression through FPM config * refactor(creation): give legacy and modern requests one identity owner * fix(creation): log observer failures without interrupting creation |
||
|
|
a7ce602c83 |
fix(agents): restore archived Codex workspaces without losing focus (#4736)
* fix(agents): restore archived Codex workspaces without losing focus History selection must survive without cached agent records, and reading an archived Codex thread must not acquire an interactive writer. Keep workspace restore explicit, serialize native lifecycle transitions, and leave transcript lifetime with its owner. * test(app): express Codex restore specs as user journeys * test(app): finish restore fixture cleanup after teardown errors * test(app): align restore coverage and await fixture readiness Workspace restore leaves agents archived until explicit Unarchive. Keep that distinction covered for multiple agents. Compile the recovery fixture before its interaction test starts, and wait for sidebar agent hydration before asserting tab presence. |
||
|
|
6f2195904b |
fix(subscriptions): isolate observers and silence unrequested feeds (#4596)
Give each observation a server-issued ID owned by its physical connection. Preserve snapshot-only reads and combined list-and-subscribe, source-bound replies, lifecycle cleanup and legacy wire compatibility. |
||
|
|
c46aee4e6a |
Revert #4421 and narrow the cache restore fix (#4436)
* Revert "Restore cached conversations before reconnecting (#4421)"
This reverts commit
|
||
|
|
1c51b52d07 |
Restore cached conversations before reconnecting (#4421)
* fix(app): restore cached conversations before reconnecting Give each host one timeline write authority so cache restores, live events and fetched pages publish consistent rows and coverage. Persist a moving 40-entry restart page independently of loaded scrollback, and keep native navigation mounted while startup settings hydrate. * fix(app): recover cache writes after initialization failures Keep accepted writes pending and retry initialization without another UI update. Publish the storage index only after loading succeeds, while queued host identity changes still settle in order. * test(app): wait for sidebar and workspace transitions Viewport intersection and leaving the creation route can precede completed UI transitions. Wait for the layering hit test and selected agent tab before asserting or navigating again. * fix(app): restore provider loading on native clients The native AbortSignal lacks throwIfAborted, so successful provider responses failed before reaching every model selector. Preserve both cancellation boundaries using the supported aborted property and the existing query cancellation error. |
||
|
|
efd6023b90 |
Restore live sessions immediately after app resume (#4160)
* perf(app): restore live sessions immediately after resume Foregrounding now reconnects every host without waiting for scheduled backoff, while cached turns, timelines, agents, and workspaces remain internally consistent until authoritative catch-up arrives. Replica persistence is keyed and incremental so ordinary updates avoid whole-directory serialization. * fix(app): preserve turn ownership through session updates Stream turn closure now wakes queued-message delivery, while same-turn snapshots retain local cancellation state. Offline liveness remains visible until an authoritative close arrives. * fix(app): preserve pending chat cache writes and trim resume refactor Directory baseline replacement must preserve independently accepted timeline changes until the deferred flush. Keep the reconnect and persistence fixes while removing the session-data facade and broad read-hook migration. * fix(app): initialize reconnect policy from current app state Initialize visibility before host bootstrap so a background mount cannot leave reconnect enabled. Seed sidebar fixtures through the workspace owner so cache hydration preserves their rows. * test(app): verify submission settlement across reconnect Keep submission acknowledgement separate from turn liveness: disconnect preserves the running turn until authoritative catch-up. Wait for confirmed running chrome before exercising queued cancellation so the alternate-send shortcut cannot race turn startup. * docs: remove cache implementation recap |
||
|
|
f75dff2322 |
Require explicit setup for fork pull requests (#4215)
* feat(workspaces): gate fork repository automation Cross-repository change requests can execute contributor-controlled package hooks even when the setup command comes from the base repository. Persist the checkout provenance and require one explicit workspace-level setup action before repository automation runs. * fix(workspaces): clarify blocked script errors * fix(app): keep long toasts readable * test(server): expect fork repository provenance * test(server): make setup marker portable * test(app): retry transient sidebar layout |
||
|
|
b6bfdedf58 |
Fix archived agent navigation and recovery (#4033)
* fix(app): preserve archived agent navigation Make explicit archived-agent opens one atomic layout command and defer them until persisted layout hydration. Use the provider's numeric active-session sentinel so recovery can complete. * fix(app): preserve timeline resume during hydration Paint persisted canonical rows with the current live head and retain their resume cursor. This prevents tail replay without dropping stream updates that arrive while replica storage loads. * fix(app): serialize replica reads with cache writes |
||
|
|
7ea4288117 |
Keep restored workspaces out of reconciliation loops (#3987)
* fix(app): stop workspace tab reconciliation loop Duplicate startup tabs could move workspace focus into Explorer. Once hydration removed the stale agent, empty-workspace reconciliation kept seeding drafts into Explorer and retriggering itself. Preserve ordinary-pane focus at the reconciliation boundary. * fix(app): normalize workspace focus before reconciliation Persisted Explorer focus must not steer newly reconciled tabs into the sidebar before the output invariant is applied. |
||
|
|
0ad69d3eb0 |
fix(app): paint conversations immediately after restart (#3907)
* fix(app): paint cached conversations before reconnect Cold startup restored every saved host before publishing any session, so a valid cached chat was blocked behind unrelated directory validation and network-only readiness. Directory and timeline owners now resolve demand through typed storage and authoritative reconciliation while consumers remain source-agnostic. * fix(app): stabilize demand-driven cache restore Create host projections before network availability, route cached workspace and timeline preparation through their owners, and coalesce directory reconciliation per online connection. Update browser cache fixtures for per-agent timeline rows. * fix(app): resume cached timeline through owner * fix(app): surface timeline owner load errors |
||
|
|
0f68e55eca |
fix(app): harden replica row persistence
Serialize pending batches at write execution time so retries cannot overwrite newer rows or resurrect removed hosts. Reject oversized hosts before storage writes and preserve unchanged directory identities so routine reconciliation stays row-grained. |
||
|
|
c4dc701411 |
fix(app): migrate persisted Explorer layouts once (#3861)
The first-class Explorer reused the old Side panel persistence identity, so upgrades hydrated ordinary persisted tabs into the dock. Move those tabs into an ordinary side pane during the versioned migration while preserving main layouts, and keep the persisted key spelling stable at the storage boundary. |
||
|
|
bbe3f17327 | test(app): follow Explorer subagent placement | ||
|
|
c914bcb44b | feat(app): make Explorer a first-class pane host (#3826) | ||
|
|
c60fa098ac |
Give every pane a browser-style New tab (#3715)
* feat(app): replace empty panes with New tabs Give every retained pane an independently identified New tab, replace it in place when a destination is chosen, and preserve split close and move invariants. Keep existing workspace startup and compact explorer routing explicit, with focused unit and browser regression coverage. * test(app): count content tabs in subagent integration * fix(app): preserve workspace shortcut ownership |
||
|
|
a69dca2907 |
Expand workspace panel actions and keyboard navigation (#3685)
* fix(app): keep file navigation in place Retargeting a File tab allocated a new tab identity, causing the tab track to unmount and remount during navigation. Preserve the identity for File-to-File changes while retaining new instances for cross-kind replacements. * feat(app): expand workspace panel actions Keep the Command Center aligned with workspace tab and pane primitives. Add explicit panel placement, focused open behavior, keyboard navigation for empty panes, and a Changes pill that toggles the visible Side panel without closing its tab. |
||
|
|
a7f3bf7abb |
fix(app): open Changes on the populated comparison (#3636)
* fix(app): restore smart Changes comparison selection The tab migration persisted the selected comparison, pinning Changes to an empty view after checkout state changed. Keep comparison overrides ephemeral per surface and expire them when dirtiness changes, while preserving only presentation state in tabs. * chore(app): date Changes compatibility cleanup |