mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 17:52:49 +00:00
fa1f01ebbcce84b3e8e27ef4f2b3473d5d7b5a33
126
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e34aea206e |
feat(plugins): add live timeline rows and client slash commands
Plugins can transform live timeline source items at render time, append durable plugin rows from daemon handlers, and register client-only slash commands in the composer. Related to #3940 and #3952. |
||
|
|
8dd4da6ee0 |
Keep mobile streaming and panel motion stable under JS stalls (#4190)
* fix(app): activate mobile panels after motion settles Gesture previews were publishing React presentation state and activating retained content before the native motion finished. Keep transient motion on the UI thread and publish the active panel only from the accepted animation completion. * perf(app): re-render only changed history rows Every coalesced stream tick re-rendered all mounted history rows because the inverted FlatList recreates each cell with a new index and ref. Keep layout item identity for unchanged rows and memoize the row on it, and resolve tool-call groups through a stable event so the render callbacks stop churning. * perf(app): count replica row bytes once Budget accounting measured every stored row of a touched host on each persist through the Buffer polyfill. Cache the UTF-8 length per immutable row and count without materialising bytes. * fix(app): settle mobile panels on position Activity now follows the UI-thread position reaching the canonical anchor instead of an animation completion callback. A successful gesture keeps its finishing motion and the matching command adopts it; settlement and command acceptance may arrive in either order. * fix(app): keep settled panel props across JS stalls Turn off Reanimated's FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS. Its settled-props collector drops entries older than two seconds before React receives them, so a JS stall across a settle left the overlay display and backdrop opacity reverted on the next commit while the store held the new panel. * fix(app): reconcile hidden keyboard end on iOS Only a hidden end event resets the keyboard shift. A late visible end no longer resurrects a keyboard that already dismissed. * fix(app): focus the replacement input through autoFocus after clearing Clearing swaps the native input for a fresh instance. Focusing the doomed instance asked Android for the keyboard and then tore the focused view down, which cancelled the show; a JS focus command sent alongside the remount reached a view that was not attached yet and the IME ignored it. Focus and blur requested while the replacement is pending now decide the replacement's autoFocus, which native applies once the view is attached. * fix(app): revise history rows in every viewport The history row memo boundary bails out until its item identity changes, but only the native viewport cloned rows flagged by historyRowRevision. On web and desktop a history host whose tool-call group kept updating from the live head rendered stale. Move the revision into useRevisedHistoryRows and run both viewports' history through it. Regression test renders the web strategy with a memoized row and asserts only the revised host re-renders. |
||
|
|
6688c92bc6 |
fix(app): stop calling new agent work reopened (#4068)
Task snapshots reuse identities after completed work, so backward status transitions do not prove that an old task resumed. Report active work as started and keep pending work in the current task snapshot instead of inventing a reopened event. |
||
|
|
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 |
||
|
|
667664847d |
Connect to remote daemons over SSH (#3989)
Add transport-only SSH connectivity for the CLI and Electron desktop through the user's OpenSSH configuration. Remote installation and daemon lifecycle remain explicit, separate operations. Preserve actionable SSH failure details when the proxy socket closes before the SSH child exits. Closes #3949 Co-authored-by: James Strain <jstrain@nvidia.com> Co-authored-by: Reid Levesque <reidlevesque@users.noreply.github.com> |
||
|
|
25eb322ef7 |
fix(app): prevent archived workspaces returning from cache (#3975)
Durable replica reads could race accepted directory replacements and restore rows that had already been removed in memory. Serialize reads behind pending writes and discard reads when the host changes in flight; local archives also suppress targeted cache restoration until removal is acknowledged. |
||
|
|
7ae5133ed2 |
feat(plugins): let plugins transform and render timeline items (#3940)
* feat(plugins): add timeline item contributions Keep canonical daemon history unchanged while plugins project and render custom timeline items in clients. Live matches refresh authoritative projected history before replacement so lifecycle deltas stay collapsed. * fix(nix): update npm dependency hash The plugin SDK lockfile change invalidated the fixed-output npm dependency derivation. Use the hash calculated by the macOS Nix build for the current lockfile. |
||
|
|
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. |
||
|
|
579c6a58f5 |
feat(app): switch replica cache to row storage
Persist only changed replica entities so steady-state writes scale with the update rather than the complete cached world. Restore remains an all-or-nothing validated boundary, with whole-host byte-budget eviction. |
||
|
|
f59db03d67 |
feat(app): add replica cache row store
Ship the platform storage engines behind a dark interface so the cache migration can wire them separately. Both engines share a contract, including atomic writes and destructive schema-version resets. |
||
|
|
fd22784e21 |
perf(app): move replica cache validation to restore
Add an observational live measurement harness for the row-store migration baseline. Trust already-shaped cache values on persist while retaining full schema validation at the restore boundary. |
||
|
|
e19d3914ae |
fix(rewind): replace long timelines without replay (#3642)
* fix(rewind): refresh canonical timeline after rewind A rewind replaces the daemon timeline and epoch, but replaying the rebuilt history made long conversations visibly reload from the beginning. Capable clients now refresh the existing bounded tail path after a replacement signal while legacy clients retain replay compatibility. * test(rewind): cover coherent timeline replacement Hold rewind completion in the browser regression so it can verify the existing pending state and detect any reconstructed timeline rows before the normal tail refresh is allowed to finish. |
||
|
|
33cf6cf337 |
perf(app): defer replica persistence until user inactivity
Whole-replica capture and serialization blocked the JS thread during navigation and streaming. Defer that work until the user has been inactive while allowing long-running streams to persist without becoming the idle signal. |
||
|
|
c779ef06c5 |
feat(workspaces): add workspace labels (#3510)
* feat(app): workspace labels baseline Existing labels implementation, committed as-is as the baseline for the UI redesign. Server, protocol, catalog store and sync are sound; the UI layer is reworked in the commits that follow. Co-authored-by: Christoph Leiter <christoph@leiter.io> * feat(app): show workspace labels on the sidebar meta line Labels could be assigned and then appeared nowhere. They now draw as a chip on the workspace meta row, after the host, change request, checks and service, and they can be switched off from the Show submenu like any other item on that line. One palette, not two. `baseColors.workspaceLabel` was ten raw Tailwind 500s spanning a 1.9:1 to 4.5:1 contrast range — the exact set identity-colors.ts was written to replace. Label color is now the identity color of the same name, so a chip, a host badge and a CI check on one row sit in one contrast band. `workspace-labels/colors.ts` and its per-file `labelColorViolet` blocks are gone; `chip.tsx` and `swatch.tsx` are the only places a label color becomes ink. A row resolves its labels against its own host's catalog and nothing else. The cross-host projection answers too, but two hosts may give one name two colors and the merge settles that by target precedence and then alphabetical serverId — a row painted in a color its host never assigned is lying about the one thing the color is for. The meta row's doc comment argued against tinted chrome on that line. The chip is a deliberate exception — everything else there reports state and shares the state colors, a label is a name a person chose — so the comment now says that instead of contradicting the code. Alignment: - The chip is exactly as tall as the line: text at the row's 16pt lineHeight, no vertical padding, so a labelled row is the same height as an unlabelled one. - A label run that opens the line hangs left by the chip's own padding, landing the name on the rail the title above it uses. Mid-line the ground stays in the flow. - The swatch row reaches 44pt through hitSlop growing outward from a 20pt swatch, replacing ten 44pt colored blobs in the manager modal. - The picker's raw `<Plus>`, `<CheckSquare>` and `<Square>` go through withUnistyles with module-level mappings, at the menu's 14pt. Also fixes a bug this change exposed: `SidebarRowItemsSchema` is a strict object, so adding an item without a key there fails the whole settings write and silently loses every other Show toggle with it. A test now covers each item in SIDEBAR_ROW_ITEMS. Phase 1 of the workspace labels redesign. The filter menu, the picker's interaction model and the manager modal's layout are later phases. Co-authored-by: Christoph Leiter <christoph@leiter.io> * feat(app): one Labels filter page with tri-state rows Filtering by label cost three levels: Display -> Labels -> Include -> a page of labels, or -> Exclude -> the same page again. It is now one page: every label across every host, one row each, leading with its colour dot. A row cycles off -> include -> exclude -> off. Include is MenuItem's own check; exclude is lucide's X, the check's own pair, at the same 16 in the same trailing slot, so the column does not shift as a row cycles. Exclude also dims the row, because a state you have to identify by its glyph is not one you read at a glance. Unlabelled is a row in the same list, with a hollow ring where the others carry their colour. Match any / all still appears only once two labels are included. Clear is absent, not disabled, when nothing is filtered. Manage labels sits at the bottom below a separator. The whole root row is gone when no host has a label — unless a filter is still active, since the merged catalog only counts online hosts and losing one would otherwise strand a filter that is still hiding workspaces. The page is titled Labels, so its rows stop repeating the word: "Match any", "Match all", "Clear filter". The two Match rows sit directly under the list they qualify and only appear once two labels are included, so the subject is already on screen. All nine locales move together, translated rather than transliterated — Arabic keeps its noun, where "أي" is a determiner that needs one to stay grammatical. SidebarLabelFilter collapses from four collections plus match to one Record<labelKey, "include" | "exclude"> plus match: a tri-state row is one value per label, so include and exclude can no longer disagree about the same name. Unlabelled keys off the empty string, which normalizeWorkspaceLabelName makes unreachable for a real label. toggleSidebarLabelSelection, countSidebarLabelSelections, sidebarLabelSelectionIncludes and deduplicateSidebarLabelNames go with the old shape; setLabelFilter becomes cycleLabelFilter and setLabelMatch, so the store owns its own transitions. No migration: labels are unreleased, so the old persisted shape never shipped, and migrateSidebarViewState already resets on a safeParse failure. The e2e manager step asserted "Red label color" on a swatch that Phase 1 renamed to "Red"; the selector follows the code, and the accessibility.color key behind the old string is deleted from all nine locales as unreferenced. Co-authored-by: Christoph Leiter <christoph@leiter.io> * feat(app): assign labels on the menu's own rails The assign picker was a search box, a create row that unfolded an inline form under it, and hand-rolled label rows whose 44x44 checkbox pushed every dot and name off the rail the rows above them sat on. Nothing lined up with anything. Rows are `MenuItem` now: the colour dot in the 16pt leading slot, assignment as the trailing check the engine already draws for a chosen row. Using the engine's row is the alignment fix; there is no second row implementation to keep in step. Every press toggles and the menu stays open — assigning labels is multi-select, so the dual-target model (`source: "row" | "checkbox"`, `shouldCloseWorkspaceLabelPicker`) is gone along with the model's `close` dependency. You leave by dismissing the menu. Search is deleted. Nobody has enough labels to search, and the field was the thing pushing the list down the surface. `workspaceLabels.search` and `createNamed` go with it across all nine locales; `clearSearch` stays because the manager modal still owns a search field (Phase 4's copy, untouched here). `Create label` moves below a separator at the bottom and opens a page that replaces the list: a name field, the swatch row, one commit row. Picking a colour picks a colour — it no longer creates the label. A failed create keeps the draft and shows the error on the page; a successful one creates, assigns, and returns to the list with the row ticked. Both menus share one page definition. `SidebarWorkspaceMenu` and `SidebarWorkspaceContextMenu` each built the same `pages` memo; they now call `useWorkspaceLabelMenuPages`. Menu engine, all of it forced by putting a text input on a menu page: - `MenuTextField` — the form kit's `FormTextInput` carries a screen's geometry and lands its text 4pt inside the labels above it. This one is drawn as a row's own fill, so the box and the text sit on the row's two rails at once. - `MenuPageDefinition.hoverIntent` — a page you type into is not one the pointer opens or dismisses. Without it the create flyout opened on a pointer passing over the row, and threw the draft away when the mouse drifted off it while the hands were on the keyboard. - The sheet keeps `keyboardBehavior="interactive"`, not `extend`. With `enableDynamicSizing` the largest snap point is the content's own height, so a short page did not grow and the keyboard came up over the field. Caught on an iOS simulator, fixed, and re-shot. - `MenuItem` announces `selected` as `aria-checked`. The deleted checkbox said so; a check drawn and never announced would have been an accessibility regression. Tri-state rows (the filter page's exclude) still cannot announce themselves — menu-wide, still Phase 5's. `sidebar-workspace-row.tsx`'s `isTouchPlatform = platformIsNative || isCompact` is kept deliberately. On a compact web viewport there is no hover, so the row's trailing actions and kebab were unreachable and the labels menu with them; this is the documented `isHovered || isNative || isCompact` shape from CLAUDE.md. Evidence: /tmp/labels-p3-{wide,compact}-{light,dark}-{list,create}.png, the create page on iOS with the keyboard up (/tmp/labels-p3-native-compact-keyboard.png) and the clipped sheet it replaced (-before.png). `packages/app/e2e/browser/workspace-labels.spec.ts` rewritten for the new model, daemon-backed failure coverage kept for both the toggle and the create path, green with the menu unit suites. Co-authored-by: Christoph Leiter <christoph@leiter.io> * feat(app): edit a label in one operation `workspace.label.rename.request` and `workspace.label.recolor.request` become one `workspace.label.update.request { name, newName?, color? }`. Both old RPCs are deleted outright — no COMPAT shim, because the labels feature is unreleased and neither name has ever been on a wire a user owns. The `workspaceLabels` capability flag stays; that one is how a new app detects an old daemon. Two calls could land half-applied, which left the catalog in a state nobody asked for and the UI with nothing true to say. One catalog commit now applies the name, the colour, or both. Renaming onto a name the host already has is rejected rather than merged, and the check runs inside the planner so a refused collision applies neither field. A label keeping its own key is not colliding with itself, so case-only edits pass. The manager modal follows: - The host selector is `HostFilter`, the pill History and Schedules use. `includeAllHost` is now a prop: a surface that edits one host's catalog cannot carry out "all". - Search moves to the sheet header, where it spans the header's own rails. As a body control it capped at the component's 420pt and left a hole on the right whenever a host filter wasn't beside it. `workspaceLabels.clearSearch` goes with the box that had a clear button. - A row no longer opens an editor. It carries a pencil in its trailing slot, hover-revealed on web and always visible on native and compact, on the canonical plain-View hover pattern with the press on an inner Pressable. - The edit view is a name field, the swatch row, and one Save that issues one update. Delete keeps its confirm and its affected-workspace count, and moves to `outline` — red belongs to the dialog, not to the surface that opens it. - Copy says edit, never rename or recolor, in all nine locales. `workspaceLabelErrorMessage` drops the `requestType=… code=…` the daemon client appends to every RPC failure. That is what a log wants, not what a user reads under a name field. `workspaceLabels.accessibility.color` stays deleted: the swatch's accessible name is already the colour's name, so nothing needed it back. Co-authored-by: Christoph Leiter <christoph@leiter.io> * chore(app): converge the labels phases The sweep at the end of the labels redesign: the three items earlier phases found and kept out of their own commits, one cross-phase pass, and the docs. `AdaptiveModalSheet`'s close button reaches its target with `hitSlop` instead of `padding`, which had inset its glyph 8pt from the rail the title, the search field and every trailing glyph below it sit on. This is shared chrome, so it moves the close glyph in every sheet in the app; measured on four of them across both paths — the labels manager desktop and compact, the shortcuts dialog, the rename sheet — and each one's trailing rail now agrees with its leading one. `MenuItem` takes `selected: boolean | "mixed"`. A tri-state row was silent to a screen reader: the check announced itself after the last phase, but exclude had no state at all, so an excluded label was indistinguishable from an untouched one. `"mixed"` is ARIA's own third state, the engine still draws the check only for `true`, and the row keeps supplying its own trailing mark — nothing in the menu engine learns what a label is. CLAUDE.md's platform-gating rules said "NEVER use `onPointerEnter`/ `onPointerLeave`", which is the opposite of what `docs/hover.md` and `constants/platform.ts` both mandate. It now names the real rule — hover on a plain `View`, press on a separate inner `Pressable`, and no `onHoverIn`/ `onHoverOut` on a `Pressable` that contains other pressables — and links the doc that owns it rather than restating it. The cross-phase pass turned up one real defect: the replica cache never learned about labels. `StoredWorkspaceSchema` had no `labels`, so a restored workspace came back unlabelled, and since the directory cursor is current on reconnect the daemon has nothing newer to send — the chips stayed gone for the whole session and the filter matched nothing. An optional field, a serializer line, and an assertion in the restore spec. Everything else measured onto its rails already. Left alone and logged: the per-row `Map` in `useWorkspaceLabelDefinitions`, and the display menu closing when the sidebar swaps to its empty state, which is confined to that swap and predates every phase here. `docs/architecture.md` and `docs/data-model.md` described rename and recolor; they now describe the one update commit, which fields it rewrites, and the per-host resolution a row's chips use. `docs/menus.md` documents the third item state where it already documents the other two. Co-authored-by: Christoph Leiter <christoph@leiter.io> * feat(app): split the labels filter into include and exclude controls The Labels page rotated one row through off -> include -> exclude -> off, which billed the rare operation to the common one: clearing an include cost two presses and routed through exclude, so the sidebar visibly re-filtered into a state nobody asked for on the way out. The row now owns include and a control on its trailing rail owns exclude. Each is one press each way and neither is reached through the other. `cycleLabelFilter` becomes `toggleLabelInclude` and `toggleLabelExclude`; the store's map shape is unchanged, and setting either state clears the other for that label, so including an excluded row is still one press. A row is never both, so the trailing rail holds exactly one 16pt glyph in every state: the check, a permanent `X`, or the same `X` revealed on hover. `X` rather than a circle-slash for the reason Phase 2 settled it — `Ban` fills its box where the check does not, and the column twitches. The control fades rather than mounts and takes its 44pt target from `hitSlop`, so neither revealing it nor changing state moves anything: measured 453-469 on every row, wide and compact. The control carries a tooltip, gated to web where hover exists. It portals into the overlay root above the flyout with no backdrop of its own, and resting on it does not read as the pointer leaving the surface, so the menu stays up. Accessibility: the control is a `checkbox` named "Exclude {label}" with its own checked state, and it stays in the tree at opacity 0 on rows that have no visible mark, so it is reachable without hover. Keyboard focus reveals it, so tabbing to it never means pressing something invisible. The row keeps Phase 5's `selected="mixed"` for exclude. Co-authored-by: Christoph Leiter <christoph@leiter.io> * feat(app): put include and exclude on their own rails on every label row Phase 6 gave the row include and its trailing rail a hover-revealed exclude. The rotation was gone but the awkwardness was not: `X` is the glyph this app dismisses things with, so on a filter row it reads as delete rather than exclude, and one rail with a changing occupant still means the row's trailing area keeps changing identity as you read down the column. Both controls are now on every row in fixed positions, and state is which one is lit. Nothing appears, disappears, swaps places or shifts: the check box is 429-445 and the ban box 453-469 on an included row, an excluded row and an untouched one alike, wide and compact. Because the ban is always there, include -> exclude is one press instead of two. The row still includes, and the check is that same action named. `Ban` rather than `X`. Phase 2 rejected it because its ink fills its 16pt box where a check's does not, so the column twitched when the two swapped on one rail. Nothing swaps here, so what is left is a weight difference: the ban is drawn at 14 inside a 16pt box, which is the visible optical correction available before it stops being a glyph. The box is what the rail is measured from and it stays 16. Lit is `foreground`, unlit `foregroundExtraMuted`. Not `foregroundMuted`, which is the obvious choice and is wrong here specifically: the Match rows further down the same page carry `MenuItem`'s own check in that colour to mean *selected*, so an unlit check in the same ink would say the opposite thing eight pixels above it. Excluded keeps Phase 2's dimmed row. Accessibility: the row keeps `selected` — `true` include, `"mixed"` exclude — and the two buttons carry no checked state of their own, so a label's state is announced once and its two controls read as the actions they are. Both buttons take a tooltip, gated to web with `enabledOnDesktop={isWeb}`, because on non-compact native `TooltipContent` falls back to a `Modal` with its own backdrop. Deleted with it: the `isHovered || isFocused || isNative || isCompact` reveal, the `opacity: 0` mounting, the hover-tracking `View` and the focus plumbing that existed to keep an invisible control reachable. Net -7 lines of code on this row, all of the growth in comments. The store is unchanged; both buttons and the row press map onto `toggleLabelInclude` / `toggleLabelExclude` as they are. The e2e's hover-reveal and `toHaveCount(0)` steps encoded the deleted behaviour and are replaced by a rail measurement taken in every state — relative to the row, since filtering moves the anchor the menu hangs off, and behind a settle poll, since the flyout scales in and a box read mid-animation is not a rail. Co-authored-by: Christoph Leiter <christoph@leiter.io> * feat(app): reveal the label filter's controls, and stop the empty state closing the menu Phase 7 put the check and the ban on every label row at all times, which made a resting list of labels read as two columns of buttons with a filter somewhere behind them. A row now shows its own state and nothing else — a marked row its mark, an untouched row nothing — and hovering any row brings both controls up, the mark you have lit and the other one unlit beside it. So the column you read down is the filter, and the row you are pointing at is the one offering to change it. Nothing about the rails changed. The two boxes are laid out in every state and only their ink comes and goes: check 429-445, ban 453-469 on an included row, an excluded row, an untouched one, hovered or not, wide and compact. The reveal is opacity, never mounting, so it cannot move the row out from under the pointer that fired it. Hover is tracked on a plain `View` wrapping the `MenuItem` with the presses on the buttons inside it, since `MenuItem` is itself a `Pressable` (docs/hover.md); each control counts its own keyboard focus as revealed, so tabbing to one never means pressing something invisible; and `pointerEvents` follows visibility so a hidden control cannot be clicked. On native and compact there is no pointer, so both simply stand there. Everything Phase 7 settled is kept: `Ban` at 14 in a 16pt box, `foregroundExtraMuted` for unlit against `foregroundMuted` meaning *selected* eight pixels below, `spacing[2]` between the two with `hitSlop` tiling the gap, the row owning `aria-checked` while the buttons stay plain named actions, and both tooltips gated `enabledOnDesktop={isWeb}`. --- Excluding a label that empties the sidebar closed the display menu. Logged since Phase 2, narrowed in Phase 5 to the empty-state swap, owned by nobody. `SidebarLabelFilterEmptyState` was a component rendered *instead of* the list, with its own container and scroll view, and both branches were handed the same header element. Different subtrees, so React unmounted the header on the swap — and the header holds the display menu's trigger, so the popover went with its anchor. The empty state is now a body: both list modes render it where their groups would go, inside the scroll they already own and below the header they already render, and the swap can no longer reach anything above it. Cleared from that same still-open page, the way back out is the swap in reverse and it holds too. `ProjectModeList` was already the right shape — its "no projects yet" body sits below the header — but its header was conditional on `unpinnedProjects.length > 0 || hasActiveHostFilter`, which is the same bug written as a guard: keep the header when there is a filter to undo. The label filter joins that condition rather than getting a second mechanism. Both empty states move into `sidebar/empty-states.tsx` so the two list modes can render either without importing each other, which also leaves the card's geometry defined once instead of once per body. Still conditional, and left alone: a sidebar with no projects and no filter has no header at all, so clearing the last host filter on an empty sidebar closes the menu the same way. That one is the onboarding empty state, where a section heading over "Add a project" would be a heading over nothing, and no menu is reachable in the state it lands in. The e2e gets the step that would have caught this: exclude into the empty state, assert the page you pressed on is still up, clear from it, assert the same. Verified by reverting both list files to Phase 7 and watching it fail on "element(s) not found". Phase 7's rail assertions are kept as they are — offsets within the row, behind a settle poll — and joined by computed-opacity reads for the four states of the reveal. Evidence: /tmp/labels-qa-p8/, 28 shots plus INDEX.md — the resting list, a hovered row, an included row with the pointer away, and the empty state with the menu still open over it, at wide and compact in both schemes. Co-authored-by: Christoph Leiter <christoph@leiter.io> * feat(app): make the label filter include-only Phases 2, 6, 7 and 8 each tried to give exclusion a comfortable shape inside a 232pt menu row: tri-state rotation, a hover-revealed control, two permanent buttons, two hover-or-lit buttons. Every fix bought a new awkwardness, because two controls in one menu row is a lot of machinery for a filter. The label filter is now the host filter — one tap in, one tap out — and nobody has ever complained about that one. A row is a plain `MenuItem` with the colour dot leading and the engine's own check trailing. Nothing else. Which makes the alignment law trivial to satisfy, and that is the point. Deleted rather than disabled, since 6, 7 and 8 are in the branch history if exclusion ever comes back: both trailing controls, the hover-tracking `View`, the `opacity: 0` mounting, the focus/blur plumbing, the `pointerEvents` gating, both tooltips and their `enabledOnDesktop={isWeb}` gates, the lit/unlit token pair, `Ban`, and the two rails they held. `SidebarLabelFilter` collapses onto the shape of `hostFilters` sitting three fields above it in the same store: `labels: string[]` plus `match`, one `toggleLabelFilter`, one `clearLabelFilter`. The exclude arm of `filterWorkspacesByLabels` goes with it, and `filter.include` / `includeLabel` / `exclude` / `excludeLabel` leave all nine locales. Persisted version 4 with no migration — the record shape never shipped, and `createValidatedPersistStorage` drops state that fails the current schema before `migrate` ever runs. Two things this leaves unused, both deleted: - `MenuItem`'s `selected: boolean | "mixed"`, added in Phase 5 so a tri-state row could announce itself. The filter row was the only caller, so the union collapses back to `boolean` and the two `selected === true` guards that existed for `"mixed"` go back to plain truthiness. - The `docs/menus.md` paragraphs from Phases 7 and 8 on hover-revealing a pressable trailing control inside a menu row. No caller does that any more, so the doc was describing a pattern the codebase no longer has. Kept: `SIDEBAR_UNLABELLED_LABEL_KEY`, because `Unlabelled` is still a row and now includes like any other; `Match any` / `Match all`, because multi-select include still means two different things. Phase 8's empty-state fix is untouched and still holds — `hasActiveSidebarLabelFilter` is what the header's survival condition reads, and it answers the same question against the new shape. Evidence: /tmp/labels-qa-p9/, 12 shots plus INDEX.md — resting, two selected, and the empty state with the menu still open over it, at wide and compact in both schemes. Co-authored-by: Christoph Leiter <christoph@leiter.io> * fix(app): give label-grouped rows their project icons, and label groups a tag Grouping the sidebar by label rendered every workspace row with its placeholder initial. The same workspace under status or project grouping showed its real project icon. The components were reused; the data feeding them was not. One component served both grouping modes and threaded the icon map through correctly. Fourteen lines above it, a second `useProjectIcons` call was gated on `groupMode === "status"`, written when status was the only mode that put icons on rows. Phase 2 extended the render branch to `status || label` and left the gate alone. Nothing forces a render branch and the data it renders to agree, and these two were far enough apart to drift in silence. Reuse buys you one implementation of the drawing; it buys you nothing about who decides what to fetch. So the fix is the shape, not the line. There is now one `useProjectIcons` call for every mode, and its targets come from `buildSidebarProjection` — the same function that produced the rows, from the same `projects` list. A mode can only render a row for a project the projection already knows about, so the map cannot disagree with the screen. The gate is gone rather than extended: extending it would leave the identical trap for the next grouping mode. `useProjectIcons` dedupes by `serverId:projectId` and caches through `useQueries`, so the gate was a premature optimization that bought a bug. Pinned by `sidebar-projection.test.ts`: for each of the three modes, every project view key the projection will render — pinned rows, grouped rows, project headers and the rows under them — has an icon target. Red on the old gate: restoring `groupMode === "status" ? … : []` in its new home fails project and label grouping with `['project', 'other-project']` uncovered, and status passes, which is exactly the bug's signature. The sibling sweep over everything `SidebarStatusModeWrapper` is handed found one more of the same shape, correct today: the projection chose its shortcut sections with `status || label`, so a fourth mode would have silently walked the project rows. Both the groups and the sections now come from one exhaustive `switch` on the mode, which makes the next mode a compile error rather than an empty list. `workspaceEntriesByKey` is already written the safe way round (`!== "project"`), and the rest — host badges, pinning support, the empty-filter state — never read the mode at all. Two names moved with the shape: `SidebarStatusModeWrapper` is `SidebarGroupedModeList`, and its branch is `groupMode !== "project"`, so a new mode lands in the grouped list instead of the project list's `else`. Also: label group headers draw lucide `Tag` instead of `CircleDot`, in the label's colour, muted for `Unlabelled` — the glyph the Labels menu and the workspace menu already use for this feature. It keeps `StatusGroupIcon`'s 14 with no optical nudge, and that is measured rather than assumed: in the header's leading slot both ink a 14x14 box at the same offset, and the tag lays down 76 ink pixels to the circle's 84, so reaching the corners of its box costs it no weight beside the status glyphs. QA: /tmp/labels-qa-p10/ — wide and compact, light and dark, label grouping and the same workspaces under status grouping, plus the hover chevron swap, the glyph zooms, and a before/after pair taken with the fix reverted to the bug's shape. Co-authored-by: Christoph Leiter <christoph@leiter.io> * fix(app): align label chip typography Co-authored-by: Christoph Leiter <christoph@leiter.io> * docs: format architecture module table Co-authored-by: Christoph Leiter <christoph@leiter.io> * fix(app): narrow workspace labels workflow Co-authored-by: Christoph Leiter <christoph@leiter.io> * fix(app): mirror bottom sheet test exports Co-authored-by: Christoph Leiter <christoph@leiter.io> * test(desktop): assert project browse by destination Co-authored-by: Christoph Leiter <christoph@leiter.io> * fix(app): retain empty projects without label filters Co-authored-by: Christoph Leiter <christoph@leiter.io> * fix(app): preserve status grouping test contract Co-authored-by: Christoph Leiter <christoph@leiter.io> * fix(app): retain filters after label load failures Co-authored-by: Christoph Leiter <christoph@leiter.io> --------- Co-authored-by: Christoph Leiter <christoph@leiter.io> |
||
|
|
405c6e07a9 | fix(app): retain workspace pull request status across reloads | ||
|
|
42245d139a |
Add active-turn steering (#3394)
* feat(agents): add active-turn steering Let users choose whether an immediate prompt interrupts or joins the active turn. The daemon owns exact-turn admission, provider delivery, safe fallback, and canonical timeline identity while queueing remains client-side. * fix(agents): stabilize steering history and replay QA * fix(agents): preserve steering timeline order * fix(agents): reconcile resumed provider history * fix(agents): serialize steering with cancellation * test(agents): prove Claude multi-tool steering * test(agents): force explicit history hydration * fix(agents): flush output before steer admission |
||
|
|
f0bd2c8483 |
Resume cached timelines without replaying their tail (#3329)
* perf(timeline): resume cached history from its exact range Warm launches were repainting cached history and then downloading the same bounded tail to rediscover that nothing changed. Persist synchronization authority only when the complete current canonical window can be encoded losslessly; otherwise retain the existing display-only fallback. * test(timeline): hide replica setup behind E2E helper * fix(timeline): bound persisted resume overflow * test(timeline): cover workspace eviction catch-up order * test(timeline): read persisted range from IndexedDB |
||
|
|
373a98c64d |
Restore host directories instantly before reconnect (#3259)
* feat(sync): restore host directories before reconnect Persist complete project, workspace, and active-agent replicas so every registered host can paint immediately, including offline hosts. Reconcile those replicas through monotonic per-entity cursors on the existing directory RPCs without retaining an event journal. * fix(sync): preserve active turn identity during catch-up Own Agent-to-wire projection beside the existing snapshot normalizer so replica persistence does not reach through directory-sync internals. This also keeps identified active turns intact when unchanged agents are folded into an incremental directory response. * test(e2e): inspect the real IndexedDB replica Keep cache setup, reload, and hydration coverage on the production browser storage backend after the replica moved off localStorage. Measure writes at the actual IndexedDB put boundary as well. * fix warm reconnect cache paths * feat(sync): persist project icons and deepen directory sync * fix(sync): preserve project update order * fix(cache): preserve validated replica persistence * fix(sync): retain cached agents during catch-up |
||
|
|
36312352db |
fix(app): validate persisted client state (#3289)
* fix(app): validate persisted client state Persisted replica data could satisfy TypeScript through an assertion while missing fields required by renderers. Treat every AsyncStorage value as untrusted input, clear values that fail strict schemas, and version the replica DTO so incompatible caches cannot hydrate. * fix(app): preserve validated legacy state * fix(app): preserve validated preferences Preserve supported legacy location fields and every shipped theme while keeping unknown persisted fields rejected. Align browser test seeds with the current preference shape so strict validation exercises the intended provider. * fix(app): migrate legacy agent preferences Released clients stored host/location fields and per-provider thinkingOptionId together. Migrate those exact strict bytes into current preferences without admitting unknown fields. * fix(app): preserve drafts with legacy reviews Accept the exact strict shape released for workspace review attachments so migration can discard that obsolete attachment without clearing every persisted draft. |
||
|
|
9a7301d9a0 | test(app): remove unintended replica cache case | ||
|
|
44b9980a70 | chore(release): cut 0.4.0-beta.2 | ||
|
|
1b887e534b |
fix(app): invalidate incompatible replica cache
Todo timeline entries gained required activity metadata in beta.1 while the persisted cache format remained unchanged. Reject version 3 snapshots so upgrades hydrate authoritative state instead of rendering legacy rows. |
||
|
|
b9d7d4b18e |
Reduce storage writes during agent streaming (#3232)
* perf(app): skip transient replica cache writes The replica cache subscribed to the entire session store, so live stream-head updates forced repeated JSON serialization and browser storage writes even though the head is not persisted. Select the cache's persisted inputs once and schedule writes only when that projection changes. * test(app): move perf cleanup into fixture Keep the replica-cache performance scenario as a direct setup, stream, measure, and assert flow while the spec-local Playwright fixture owns deterministic mock-agent cleanup. |
||
|
|
6aa9f92156 |
Stop notifications after removing a host (#3176)
* fix(notifications): stop pushes after host removal Push tokens were persisted indefinitely with no relationship to app pairing state. Give registrations a 48-hour renewable lease so offline unpairing converges, and revoke immediately when the host is reachable. * fix(notifications): make token revocation bounded and durable |
||
|
|
fd9cc5d693 |
Restore macOS direct LAN connections (#3071)
* fix(desktop): restore direct LAN connections Desktop 0.3.0 routed remote WebSockets through the Electron main process, causing previously working macOS LAN connections to fail with EHOSTUNREACH. Revert custom direct headers and restore the renderer WebSocket path used before the regression. * fix(app): replace host connections by identity Keep one connection per stable ID when TLS or password settings change. This preserves the current-main upsert invariant while the custom-header rollback removes the unrelated transport feature. |
||
|
|
a5184fd08a |
fix(app): keep browser relay WebSockets protocol-neutral (#2976)
Use the standard browser WebSocket constructor without the Node/React Native options object, while retaining the options-capable factory for supported runtimes and the desktop bridge. |
||
|
|
c1f7ed3198 |
Improve reconnect status feedback (#2931)
* fix(app): debounce reconnect status toast Keep reconnect transport immediate while delaying the visible status toast so brief outages stay quiet. Scope the toast to the active retained panel, keep its copy and status dot consistent across locales, and add a manual real relay deployment journey that verifies streaming recovery without user action. * fix(app): isolate manual relay deployment check Keep the deployment journey out of the provider-wide real test project so it runs only when its local relay prerequisites are explicitly requested. Also keep the relay teardown active when packaged daemon startup or later cleanup fails. * fix(app): preserve reconnect toast across tabs Keep reconnect presentation timing at the host level so switching between retained panels does not restart the debounce or flash the toast. Add a browser regression covering a visible outage across retained tab switches. * fix(app): anchor reconnect debounce to host status Record host connection status transitions independently of mounted agent panels and preserve the outage start across reconnecting states. Use that host-level timestamp when a visible agent panel appears, with runtime coverage for the transition lifecycle. * fix(app): reset reconnect toast state per outage * test(app): build relay journey prerequisites |
||
|
|
73ba37a07a |
Add custom headers to direct connections (#2922)
* Add custom headers to direct connections * Address custom header review feedback * Fix direct connection header integration * Fix desktop relay E2E transport setup --------- Co-authored-by: Claude <claude@Claudes-Mac-mini.local> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
48bdc5610f |
fix(app): preserve active host connection choices (#2905)
Settings lost the workspace host when entering the app-wide route, while desktop bootstrap could restore a removed local connection before the saved registry was ready. Use the remembered workspace host for Settings and only register the managed daemon when its server is unknown after registry hydration. |
||
|
|
7dbeff8290 |
perf(app): reduce startup websocket work
Make provider catalogs compact and content-addressed so repeat launches can reuse the persisted snapshot instead of transferring it again. Remove eager global catalog loading and the stopped-agent defensive tail fetch, while keeping sequence-gap and reconnect recovery as the correctness paths. Add an opt-in production-ID Android profiling build for local startup traces. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
d0456b1943 |
fix(sync): preserve host replicas across provider remounts
Make the host runtime own session and setup replicas for the registered host lifetime. Provider remounts can then reattach without clearing directory snapshots or timeline cursors. |
||
|
|
afeb4d18f8 |
fix(app): keep reload splash continuous
Treat desktop settings evaluation as part of daemon startup so restored app chrome cannot render between bootstrap phases. |
||
|
|
07d988488e |
fix(sync): show complete workspace and agent history (#2263)
Cached focused state was leaking into the sidebar before authoritative workspace hydration, while catch-up limits counted raw events instead of projected entries. Keep the focused cache fast, gate directory presentation on hydration, and preserve canonical cursors while paging projected history. |
||
|
|
5ea311f243 |
feat(app): restore recent chat while reconnecting (#2206)
Keep a small non-authoritative view of the last focused chat so the app can paint it before the daemon finishes revalidation. Bound persistence to one focused agent per host and evict whole host entries to limit storage and serialization work. |
||
|
|
39cb3dbb9c |
Treat every added folder as an independent project (#2098)
* fix(projects): detect when projects become Git repositories Project identity was coupled to Git placement, while non-Git roots were dropped by session-scoped observation. Keep identity tied to the selected root and observe Git transitions daemon-wide so empty projects update without rehoming workspaces. * fix(projects): preserve metadata across Git read failures Refresh archived workspace facts before persistence and treat only confirmed non-repositories as non-Git so transient failures cannot rewrite stored project metadata. * fix(projects): close project update races * fix(projects): refresh checkout metadata when reopening folders Persist worktree ownership separately from workspace kind and gate exact-root project creation on stable host identity. Refresh active and archived records so missed Git transitions cannot return stale checkout descriptors. * test(projects): accept Windows aliases for repaired roots * test(server): retry transient Windows cleanup locks * fix(projects): propagate project metadata refreshes Project-only Git transitions now fan out workspace updates for legacy clients. Explicit project creation refreshes stored kind, and equivalent cwd spellings reuse existing workspace records. * fix(projects): refresh worktree source project kind * fix(projects): preserve exact-folder runtime isolation * fix(projects): preserve exact cwd across worktree lifecycle * fix(projects): harden exact-folder worktree flows * fix(projects): derive exact cwd from matched path identity * fix(projects): preserve nested worktree lifecycle * refactor(projects): centralize workspace placement * test(e2e): verify isolated server ports * fix(projects): preserve placement reshape compatibility * fix(projects): validate created worktree placement * fix(projects): preserve placement through workspace lifecycle Archive and recovery were rediscovering or guessing checkout roots instead of consuming the persisted workspace placement. Make the record authoritative for exact cwd, backing worktree, and source repository, and classify stale paths before Git reconciliation. * fix(worktrees): preserve source checkout root Convert Git's common administrative directory back to the source checkout root when legacy archive placement is reconstructed. * fix(worktrees): compare filesystem identities Resolve discovered worktrees and teardown locations through the shared realpath-aware matcher so Windows short and long path spellings cannot split placement identity. * fix(worktrees): centralize path containment Route worktree ownership, listing, resolution, and deletion through the realpath-aware containment primitive so Windows path aliases cannot be filtered by an earlier raw prefix check. * fix(worktrees): remove duplicate ownership discovery * test(e2e): isolate daemon restart ownership * fix(worktrees): make lifecycle operations transactional * fix(workspaces): share git watches by cwd * test(worktrees): make teardown proof portable * fix(sync): integrate project updates with directory owner * fix(workspaces): close reconciliation edge cases * refactor(sync): remove duplicate project reconciliation Keep workspace and project deltas behind the host directory transaction, with owner-boundary coverage for snapshot replay and queued full reconciliation. * fix(sync): buffer project updates before hydration Start the workspace transaction with the online connection epoch so project broadcasts cannot publish a partial directory before the authoritative snapshot commits. |
||
|
|
1977d330ed |
Reduce workspace, agent, and chat sync traffic (#2028)
* feat(sync): keep live data scoped and current Only viewed chats receive live timeline rows, while directory state now uses one subscribed bootstrap followed by ordered deltas. Legacy clients and daemons retain their existing behavior through centralized compatibility gates. * fix(sync): preserve selective delivery boundaries Keep selective timeline capability app-owned, union viewed sets across shared sockets, and reconcile directory side effects from accepted state. Visibility and archive suppression now follow their existing authoritative boundaries. * test(app): align browser fixtures with runtime contracts * fix(sync): preserve mixed-client delivery guarantees * fix(sync): finish paged history before advancing * fix(sync): replay live deltas after refresh failures * test(server): model socket capability lookup * test(app): use platform shortcut for split-pane coverage * fix(app): keep hidden timelines dormant * test(app): normalize split-pane shortcut setup * fix(app): preserve sync state across retries * fix(app): preserve background sync state across races * fix directory bootstrap reconciliation edge cases * fix selective timeline compatibility races * fix superseded directory sync races * fix(sync): centralize directory replica ordering Own agent and workspace refresh transactions in HostRuntime so reconnect epochs, buffered updates, and lifecycle invalidation share one ordering boundary. Route timeline metadata and mixed-capability stream delivery through their authoritative runtime sources. * fix(app): keep timeline requests runtime-scoped * fix(app): close directory bootstrap races |
||
|
|
ed67e6e0f1 |
fix(app): preserve renamed host labels (#1940)
Daemon-advertised hostnames still name new/default hosts, but connection refreshes and re-pairing no longer overwrite labels that users already changed. |
||
|
|
d9b65e269f |
Scheduled runs each get their own workspace in the sidebar (#1934)
* feat(schedules): per-run workspaces, isolation and archive controls Scheduled agents never appeared in the sidebar: each schedule reused one stamped workspace and archived the run's agent on completion. Now every run mints its own workspace — the entity the sidebar shows — with per-schedule controls for isolation (local or worktree) and whether to archive that workspace when the run finishes. The schedule form is rebuilt on a non-React form model (open/commands/close) to end the flicker, edit-into-create contamination, and multi-host hydration bugs that came from effect-choreographed shared state. That lands the reusable foundations it needed: a form kit over a single control-geometry owner, a data-access taxonomy (replica/snapshot/fetch) with real load-state semantics, and lint/boundary guardrails so new screens inherit the paved road. Also fixes desktop combobox popovers truncating options when the trigger is narrower than the content. New schedule protocol fields are optional on both create and update, so old and new clients/daemons stay compatible. * test(app): complete mock themes for control-geometry tokens switch and terminal-profile-edit-modal now style through createControlGeometry, which reads theme.borderWidth[1], theme.opacity[50], and theme.colors.borderAccent. The hand-rolled mock themes in these two suites lacked those tokens, so the eager StyleSheet.create mock threw at import (collection error), not on any assertion. Add the missing tokens. * fix(schedules): address review findings on run cleanup, edit isolation, load error, sheet dismiss, and preference hydration - Server: archive the run's workspace even when agent creation fails, so a misconfigured provider no longer orphans a sidebar workspace/worktree. - Edit form: keep a stored worktree isolation while worktree eligibility is still resolving, instead of silently downgrading a saved schedule to local. - Schedules screen: show the load error/retry UI when every host fails, rather than spinning forever behind the loading branch. - Form sheet: fire the parent onClose on native gesture/backdrop dismiss so the sheet closes instead of re-opening. - Form model: apply late-loading saved preferences to untouched create-mode fields (never to edited schedules or user-modified fields). * fix(schedules): review batch — reconnect resubscribe, worktree prune, Android dismiss, cadence and provider edit safety - Push-router: re-send terminal and checkout-diff subscriptions after a reconnect; the old per-hook effect resubscribed on isConnected and the extracted router did not, so daemon restarts silently stopped terminals_changed pushes (root cause of the terminal-activity e2e failures). - Server: pass the run's source repo root when archiving worktree runs so the worktree is unregistered (git worktree remove/prune), not just deleted; also archive the workspace when agent creation fails even with archive-off (an agentless workspace has nothing to inspect). - Sheet: RN Modal onDismiss is iOS-only — notify dismiss once-per-close on the Android native path too; replace the JSDOM component test with a real Playwright dismiss spec and inline the trivial dismiss decision. - Form model: editing an interval schedule no longer rewrites its cadence to cron unless the cadence UI is touched (90-minute intervals have no cron equivalent); switching projects clears stale provider/model state and gates submit until the new host's snapshot resolves. * chore(lint): drop custom lint wrapper and boundary script, plain oxlint The import bans stay in .oxlintrc.json (oxlint-native no-restricted-imports). The receiver-sensitive checks the custom script enforced are not worth a parallel lint pipeline; if they come back it will be as oxlint rules. * fix(schedules): crash-safe run cleanup, capability-gated run options, resilient mutations - Persist the run's workspace/agent ids on the running-run record so a daemon crash mid-run can be recovered: restart archives the interrupted run's workspace under the same policy as normal cleanup (agentless always archives, otherwise archiveOnFinish is respected). - Hide Archive on finish (and omit archiveOnFinish/isolation from payloads) on hosts without workspaceMultiplicity — an old daemon ignores the fields, so offering the switch there would lie. - Optimistic pause/resume/delete no longer throw when the schedules cache holds a still-connecting entry alongside loaded data. - Mode field is gated on provider mode options, not a selected model, so model-less providers can pick their advertised modes. |
||
|
|
b28f291493 |
Generalize browser automation hosting beyond the desktop app (#1878)
* feat(browser): generalize browser automation hosts Rename the browser automation client capability to a host-neutral payload, advertise supported commands, and route tab-scoped commands by browserId affinity so future browser hosts can register without another protocol shape change. * fix(browser): harden browser host registration Reject unusable browser host capabilities, clean up pending requests when a host registration is replaced, and defer list_tabs affinity updates until every host in the aggregation succeeds. * fix(browser): preserve host tab affinity across reconnects Keep stranded tab ownership tied to the disconnected host so the same host can reclaim open tabs after reconnect, and tolerate future command names in browser_host capability payloads while retaining only known commands. |