638693a5d75c6b9e91513ad138a47d6f18dfab56
177
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9b5f6f33f6 |
style(app): indent the sidebar new workspace row
The row only renders under its project header, so stepping it in reads as part of that project rather than as a peer of it. Extra padding rather than margin keeps the hover and pressed fills on the same box as every other sidebar row. |
||
|
|
5d0560d4c1 |
feat(app): add a checks display selector and name running services in the sidebar
Checks stopped being a toggle. Showing the icon without its word is a real third answer rather than half of "off" — the state stays on the row for a glance and a passing workspace stops paying a word for it on every line — so it left the boolean row-item record instead of widening it, and a stored `sidebarRowItems.checks === false` migrates to the new "none". The services item narrowed to actual services. It used to fall back to any running command, but a test watcher running says nothing about whether a workspace is up, and the item now says "Services" so drawing a terminal glyph under that label was a control lying about itself. It reports the name rather than the port because the name is what you called it. `health` was already on the wire and nothing in the sidebar read it, so a failed check now turns the whole item danger; where several services run, the failing one takes the single slot, since a healthy one is the unremarkable case and any of them says "this is up". That narrowing is why the rename went through the code and the persisted key rather than stopping at the label: the concept genuinely changed, and a UI saying "Services" over a `scripts` key is the synonym docs/glossary.md calls a design error. Both key migrations are tagged COMPAT. The rest is the sidebar row's geometry. The show-more and new-workspace rows each carried their own smaller, inset, less-rounded version of a workspace row — 24pt of left margin, a 14pt icon column, `borderRadius.md`, one fill for hover and press — which under a column of real rows reads as a mistake rather than a distinction. They now take the workspace row's geometry, and the show-more row asks its caller whether it is ending an indented list, because the two groupings disagree. Group spacing moved from margin to padding so a collapsed group gives it back instead of staying spaced for content that is gone. SPACING gained a 0.5 rung for the 2pt gap between rows. |
||
|
|
211489c658 |
refactor(app): unify status colours on one token family
Three systems were reporting the same states: a full and a muted status tier in the theme, plus raw palette steps in StatusBadge and getStatusDotColor. A PR icon was muted in the sidebar and full-strength in the PR panel, and the Resolved badge used a theme-independent green so it painted a dark block in light mode. The muted tier is deleted and every status surface now reads the one token per signal. Dots keep a band of their own. A dot is a few points of solid colour with no shape to read and no label attached, and the running one pulses, so at the status band's lightness it sat below the check icons and host badge on its own row — backwards, since the dot is the row's state. Chroma caps at 90% of gamut rather than 100% because at the edge green reaches C=0.215 while blue manages 0.116 and the set stops reading as one family; dark lightness caps at 0.72 because red desaturates to pink above it. Host badges needed a foreground variant of the identity table. That table is tuned for a white letter on the colour as a fill; used as text it cannot clear 4.5:1 on both themes at once, since light needs luminance <= 0.183 and dark needs >= 0.212. The new set is generated on the status family's exact lightness and chroma fraction, so a host name and a CI check in the same meta row carry equal weight instead of the brighter one winning the row. Also: PR avatars had their own eight-hex palette and hash duplicating identity-colors; workspace rows under a project group now indent to the same rail as status-grouped rows; group headers are muted; and the pulsing dot is faster and fades further. |
||
|
|
b020a1d1ed | Merge remote-tracking branch 'origin/main' | ||
|
|
22cb7f9996 |
refactor(app): give the host badge one home
The server glyph, its identity-colour table and the muted label lived inside the sidebar meta row, and the three stores that decide whether a host has a badge at all were assembled at one call site. Both are about to have a second caller in the workspace header. Move the badge to hosts/host-badge and the resolution to useHostBadges. The badge now owns its glyph size, and the meta row matches it, since it is the one item on that line that also appears off it. |
||
|
|
32705cb3f2 |
fix(app): acknowledge native sidebar presses immediately
Sidebar rows previously waited for JavaScript-driven selection or navigation before showing feedback. Keep the transient highlight on the UI worklet while leaving activation, cancellation, and drag ownership with the existing pressable. |
||
|
|
df32e6ddcd |
Fix the CI failures on main (#2850)
* fix(app): keep an open workspace menu from unmounting its own trigger The sidebar kebab is revealed by hover, and on a compact layout the menu it opens is a sheet that slides up over the row it came from. The row un-hovers, the trailing overlay unmounts the trigger, and the menu's open state goes with it — nothing is left that can close the surface already presented in the portal, so a full-screen backdrop swallows every click until reload. Lift the menu's open state to the component that decides whether to render the kebab, and keep the kebab rendered while the menu is up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(app): follow the sidebar and host badge redesigns in the e2e suite Three specs were still driving surfaces that moved out from under them, so CI has been red on main since the sidebar polish landed. Display preferences became one row per decision with the options a page down, so grouping and the host filter need that page walked first. Five specs had their own copy of the two-line dance; it lives in the sidebar helper now, and the host filter has its own opener next to the menu it belongs to. The host badge is plain secondary text on the meta line and its identity colour lives on the server icon alone, so the helpers stop asserting a tinted label and a pill fill that the redesign removed. Alignment rails are compared across canvas glyph metrics and SVG bounding boxes, so the residual depends on the host's font; a 1px and a 0.5px tolerance both landed on the wrong side of it in CI. Two pixels still catches the whole- pixel offsets these rails encode. The compact sheet spec also read its rails while the panel was still sliding in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(app): let a browser e2e run take a private Metro cache Metro's on-disk cache is shared by every checkout on the machine. Editing one app source file rebuilds that module against everyone else's cached output and the Unistyles styles can come back desynced, which surfaces as alignment specs failing with real-looking pixel numbers that go away when the edit is reverted. E2E_METRO_CACHE_VERSION namespaces a run's cache without deleting anyone else's entries, so the cache can be ruled out before a layout failure is believed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(app): stop the chat scroll helper from hanging on a stalled sampler `scrollChatAwayFromBottom` waited on a promise that could only ever resolve from a `requestAnimationFrame` sampler started by a `wheel` listener. rAF does not fire while the page is occluded, which is reachable on a CI runner, so the sampler could stall on its first frame and the wait never returned — the test died on its own 240s timeout with nothing to read. A missed wheel event did the same. Settling the scroll before measuring is an optimization; the assertion after it is what decides the test. Sample on a timer with a deadline so a stall falls through to that assertion instead of consuming the test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(app): remove layout geometry e2e coverage Rail and pixel-alignment assertions depend on host rendering details and should not define the browser E2E contract. Preserve the surrounding behavior coverage while dropping the Metro cache escape hatch that only supported diagnosing those assertions. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
a361d1cb2f |
fix(app): polish workspace navigation surfaces
Use mobile action sheets and long-press file menus while preserving provider context across sheet portals. Keep sidebar and file-pane glyphs on shared visual rails, avoid scrollbar layout shifts, and hide keyboard affordances where shortcuts are unavailable. |
||
|
|
ab8151d33f |
feat(app): sidebar display preferences, a shared menu engine, and a colour pass
Sidebar polish round. Menus. `dropdown-menu` and `context-menu` were two copies of one engine that differed only in how the trigger fires — identical from `computePosition` down — and neither could nest. Both are now thin wrappers over `components/ui/menu/`, which owns anchoring, a pure page-stack navigation model, and one presentation fork: flyout on pointer, pushed pages in a sheet on compact. All 31 call sites are unchanged. Items got a rounded inset fill, a visible separator, and `selected` (a check) split from `active` (a fill). Sidebar display preferences. The flat wall of four sections becomes one row per decision with its value, and per-feature toggles over what a workspace row shows: host, pull request, checks, scripts. Each is independent — a ticked toggle that draws nothing is a lie. `display-preferences/` hides that grouping lives in zustand while title source and row items are app settings. Timestamps. "Last activity" now ticks itself. The label rounds to the minute and never shows seconds; past a week it becomes a date. The timer lives in the leaf `<Text>`, at most one per resolution tier shared by every row, and a tick only re-renders when the string actually changed — so a memoized row that legitimately never re-renders still stays current. Colour. Full status tier regenerated at 90% of gamut max (was 85), same rule as the muted tier, lightness and hue untouched. File icons are pulled toward neutral in OKLab behind `getFileIconSvg` — material-icon-theme picks its palette for a tree where the icon is the only colour on the row, and ours is not. New/deleted files use the forge square-plus/square-minus glyphs at muted weight instead of tinted pills. Project icons round a little more; uploaded icons never do. The project status badge is a knockout again: it fills with the colour of the row behind it, so the ring reads as a gap in the icon rather than a white halo. File rows. The always-visible kebab is gone from the file explorer and diff tree in favour of right-click, which also reclaims the reserved action slot. |
||
|
|
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 |
||
|
|
429155b536 |
Add collapsed-project status badge to the sidebar (#2340)
* Add collapsed-project status badge to the sidebar Collapsed project rows now surface the most urgent hidden workspace's status as a corner badge on the project icon, instead of giving no signal at all: a loader for running, an alert for needs_input, a dot for attention/failed, nothing for done. Priority order (needs_input > failed > running > attention > done) means a working project keeps its loader even if another workspace is also awaiting review. Every surfaced status shares one badge shell (size, ring, offset) so the loader, alert, and dot never draw a differently-sized circle - previously the loader/alert used a 12pt ring while the dot used a 7-9pt one with a different corner offset. The running loader is also nudged half a point left so its visual center lands on the circle's center rather than reading as shifted right. Adds an e2e capture spec (run on demand, not part of CI) that seeds every state and edge case through the mock provider and dumps DOM geometry, for re-verifying the badge shell stays uniform. * Add PR review gallery images for the status badge PR Temporary evidence assets for pull request review, embedded via blob URLs in the PR description. Safe to delete after merge. * Swap misleading before/after PR images for clean per-state stills This feature never shipped on main, so there is no real "before" to compare against for a reviewer — the earlier before/after images were an artifact of local dev iteration, not a regression fix. Replaced with one clean still per status. * Fatten needs-input alert and dot to fill the shared badge shell Lucide's circle-alert only paints ~83% of its nominal size, so the prior 10pt glyph read as smaller than the loader inside the same 14pt shell. Sized both up so all three glyphs draw the same visual footprint against the shared ring. * Address review: extract pure badge-content selection, drop JSDOM tests Greptile flagged getStatusDotColorStyle's needs_input/running cases as dead (unreachable — shouldRenderSyncedStatusLoader and an explicit check already intercept those buckets earlier). Extracting the selection logic into a plain-TS descriptor function (getProjectStatusBadgeContent) makes that structural: the "dot" variant is typed to failed|attention only, so there's no switch left with unreachable branches. That extraction also resolves the JSDOM component test Greptile flagged (project-leading-visual.test.tsx used vi.hoisted/vi.mock/ createRoot mounting, all banned by docs/testing.md's two test categories) — the pure selector is now tested directly with a plain vitest file, no mounting needed. Dropped that test file and use-sidebar-project-status-bucket.test.tsx (same banned pattern); the real coverage was already redundant with sidebar-workspaces-view-model .test.ts's existing priority-order assertions and the e2e capture spec's real-app gating checks. Also split a conditional branch in the e2e capture spec's assertion loop into two deterministic loops (badge-expected vs no-badge cases) per Greptile's third note. * Size status-badge glyphs even so they center in the shell The badge shell's interior is 12pt (14pt minus the 1pt ring on each side). A centered glyph of size N sits at a (12 - N) / 2 offset, which is fractional for odd N and gets snapped to a device-pixel boundary, rendering visibly off-center (~1.5 device px at 3x). Drop the dot to 8pt and the alert to 10pt so both divide the interior into whole pixels and land dead center; Lucide's circle-alert paints ~83% of nominal, so 10pt draws a ~8.3pt disc that matches the 8pt dot. Also extends the on-demand capture spec to cover the failed bucket, the priority aggregation cases, dark mode, hover, a compact viewport, and a recorded running-to-expand transition, dumping DOM geometry that proves every shell shares one size/offset and every even glyph centers at 0px. * Refresh status-badge PR assets for the even-glyph sizing Recaptures every state at the approved 8pt-dot / 10pt-alert sizing, adds the failed bucket to the per-status set, a crosshair sizing/ centering proof, dark-mode and compact-form-factor shots, and replaces the soft upscaled spinner GIF with a crisp one assembled from native 3x-scale frames. Drops the stale pre-sizing images. * Add capture spec proving workspace statuses roll up to the project badge Seeds three collapsed projects, each with several same-directory workspaces in different buckets, so each resolves to a different highest-priority winner (needs_input / failed / running). Films the collapse and expand so the aggregation is provable as motion, not a single frame. * Add propagation recording showing statuses resolving to the project badge * Add higher-quality mp4 of the status propagation recording * Refactor project status badge coverage --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> |
||
|
|
3778d0ad7b |
Fix mobile sidebar swipe and drag failures (#2709)
* fix(app): prevent text selection during Android swipes Gesture interception did not cancel the native view where the touch began, leaving Android's pending text-selection action alive. * fix(app): restore iOS sidebar close swipes Fabric could fail to attach close handlers when their target lived inside retained but display-hidden panel content. Keep a registered native gesture host mounted outside the presentation wrapper so close swipes work over nested lists without activating rows. * fix(app): recover sidebar drags after cancellation Cancelled native drags could leave a lifted row and the shared outer list locked. Scope terminal cleanup to the owning gesture session and release the lock on cancellation or unmount. * test(app): clean up partial drag fixtures |
||
|
|
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> |
||
|
|
99eca19a76 |
Keep selected projects when switching hosts (#2700)
* fix(app): preserve project selection across hosts Rendered project identity and persisted equivalence were conflated after duplicate-project handling. Keep them separate so host changes resolve selectable equivalents deterministically without resetting legacy sidebar identity. * fix(app): restore selected clone after host round trip Keep manual origin and current destination placements separately so switching back restores the chosen checkout while archive gaps preserve the active host selection. * fix(app): key status icons by project view * fix(app): preserve origin during archive gaps |
||
|
|
87ef631ac4 |
Show project icons in status-grouped sidebar rows (#2416)
* feat: show project icons in sidebar status view * test(sidebar): keep status icon regression active The ten-second mock can finish during app startup, removing the Working bucket before the icon assertion. Keep the seeded workspace active for the full scenario. --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
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. |
||
|
|
89c2fac3c6 |
Open project and workspace folders from the sidebar (#2491)
* feat(app): open project folder from project context menu (#2487) * refactor(app): give file manager action a home --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
b97d6d13f3 |
Load complete chat history when reaching the top (#2481)
* fix(chat): load older timeline history consistently Pagination depended on scroll events, so short or compacted initial pages could never request older history. Reevaluate edge visibility as layout and history change, and standardize loading indicators on the canonical app spinner. * fix(chat): keep older history loading reliably Use authoritative timeline cursor progress so merged rows cannot stall pagination. Wait for bottom anchoring before evaluating the history edge, and re-arm retries on a fresh upward gesture without automatic failure loops. |
||
|
|
cf36b2cc40 |
fix(app): pin the active workspace from a collapsed sidebar section (#2299)
Cmd+Shift+P did nothing whenever the active workspace's sidebar row was not rendered. The `workspace.pin` handler was registered by the row itself, gated on `selected && canPin`, so collapsing the row's section unmounted the only handler for the action and the dispatcher found zero candidates. The keypress was swallowed with no toast and no error. Move the action to a single always-mounted handler keyed on the active route selection, following the existing `useGlobalNewWorkspaceAction` / `useActiveWorktreeNewAction` pattern, and delete the two per-row registrations. Besides the reported case this also fixes a collapsed status group, a collapsed Pinned section (so unpinning works too), and focus mode. The handler lives in a headless component rather than being called from the root layout, so subscribing to the active workspace's pin state does not re-render the whole app shell. Two supporting changes: - The controller now takes a narrow `PinnableWorkspace` instead of a full `SidebarWorkspaceEntry`, so a caller without a sidebar row can build one. Its in-flight guard moves to module scope, because the row menus and the shortcut hold separate controller instances and a per-instance guard would let a keypress and a menu click fire two concurrent, opposite RPCs. - The handler resolves the descriptor id via `useWorkspaceFields` rather than reusing the route id. The route carries an opaque workspace id that is not guaranteed to equal the descriptor id, which is why `selectWorkspace` resolves it through `resolveWorkspaceMapKeyByIdentity`. Both the RPC and the in-flight key need the descriptor id so that rows and this handler agree on one identity. `workspace.archive` has the same row-scoped defect and is deliberately left alone: it carries per-row state (`isArchiving`, optimistic hiding, the risky-worktree confirm) and needs its own change. Covered by a Playwright spec: the three collapse cases fail without this fix, plus one-RPC-per-press and a rejected-pin case that asserts the error toast and that the next press still succeeds. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
9603397aed |
perf(app): limit initial sidebar workspace rendering
Large project, status, and pinned groups now render 20 rows until expanded. Newly discovered workspaces are prepended without disturbing the saved order. |
||
|
|
5c93ac4aa5 | fix(app): balance sidebar divider spacing | ||
|
|
d6dc309408 |
fix(app): keep pinned workspace labels aligned
Reserve the leading status slot for idle pinned rows so labels do not shift when workspace status changes. |
||
|
|
e0e50c9a8e |
fix(app): preserve persisted theme styles after startup
Module-level style composites could materialize the temporary adaptive theme before persisted settings loaded. Keep Unistyles reads in render and guard against eager module-scope access. |
||
|
|
a7cbf4f61d |
fix(app): make sidebar reordering respond immediately
Mouse input inherited the touch hold delay. Split activation by input so mouse dragging begins after deliberate movement while touch retains long-press arbitration. |
||
|
|
a8ebd390fa |
feat(forge): pluggable forge abstraction + GitLab and Gitea/Forgejo/Codeberg (#1913)
* refactor(forge): forge-neutral foundation (GitHub-only) Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged. - Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry. - GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved. - forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client). - createPullRequest reports typed CLI/auth errors instead of a generic message. - forge-resolver host/remote caches are LRU-bounded. - Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com. - Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology. - Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs. * feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals. - threadIsResolved is part of the neutral timeline item. - Pipeline load failures show an error instead of an empty section. - Manual pipeline jobs render as pending. - Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request). * feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg) One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts. - CI status aggregates commit statuses and Actions runs together. - Gitea's terminal "warning" state maps to failure on server and client. - Gitea Actions check details are reachable from the PR pane by workflowRunId. * refactor(forge): localize compatibility handling * test(forge): expect normalized GitLab facts --------- Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
f350c716b9 |
Keep forked chats focused in new tabs (#2038)
* fix(app): focus explicitly opened workspace tabs Explicit tab navigation could be overwritten by attention-aware workspace navigation when the visible agent had just finished. Make workspace navigation own both normal attention selection and explicit targets so callers cannot sequence them inconsistently. * test(app): strengthen forked tab focus assertion |
||
|
|
a1e81685a1 |
feat(app): pin chats to the top of the sidebar (#1981)
* feat(app): pin projects to top of the sidebar Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app): dedicated Pinned sidebar section with UX fixes Replaces the invisible float-in-place pinning with a Codex-style Pinned section at the top of the sidebar that hoists pinned chats and projects. - Pin-aware keyboard shortcut numbering (badges match visual order) - Pinned chats show their project name as subtitle (context after hoisting) - Pinned entries ordered most-recently-pinned first - Collapsible Pinned section (persisted) - Double-click guard on workspace pin toggle Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app): move Pinned section above the Workspaces header Pinned now sits at the top of the scroll, above the "Workspaces" section header (which moves into the scroll below it), matching the market-standard sidebar layout. Chevron sits beside the "Pinned" label. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app): reveal Pinned collapse chevron on hover only Extracts PinnedSectionHeader with its own hover state; the chevron hides until hover on web and stays visible on touch/compact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app): pin the active chat via keyboard shortcut and inline pin button Adds a ⌘⇧P / Ctrl+Shift+P shortcut that pins the selected chat, mirroring the worktree.archive action across actions/dispatcher/route-shortcut, gated by `selected && supportsWorkspacePinning`. Also surfaces an inline pin button on row hover (always-on for touch) alongside the kebab in the shared trailing overlay, swapping Pin/PinOff by pinned state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): unify project and workspace pin handlers Collapses the two near-identical pin.set handlers into a shared handlePinSetRequest that owns control flow, error handling and the workspace-update fan-out; each caller only supplies its registry apply step and typed response payload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): drop empty pinned project shells and empty Workspaces header Pinning every chat of a project hoisted them into Pinned but left an empty project header (plus its new-workspace ghost row) below, duplicating the project. Skip projects emptied by pinning while keeping genuinely empty projects. Also hide the "Workspaces" section header when no unpinned projects remain, so it no longer floats above nothing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(protocol): correct workspacePinning COMPAT version to v0.1.105 Feature ships in 0.1.105 per the changelog, not the placeholder v0.1.103. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): hide redundant chat pin control inside pinned projects A pinned project already hoists all its chats to the top, so the per-chat pin was a no-op that just toggled a dead icon. Suppress the inline pin button, the kebab pin item, and the pin keyboard shortcut on chats whose project is pinned; they still work for chats in unpinned projects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): hide project pin control on empty projects Project pin state is derived from workspace descriptors, so a project with no workspaces would set pinnedAt on the server but emit no descriptor update — the pin silently never surfaced and the user got no feedback. Offer the pin only once the project has a chat, matching how chat pins are suppressed inside already-pinned projects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): drop collapsed Pinned rows from keyboard shortcut numbering When the Pinned section is collapsed its chats and projects are hidden, but the shortcut model kept counting them: visible unpinned rows showed off-by-N badge numbers and pressing a number key jumped to a hidden pinned chat. Exclude the pinned rows from numbering when Pinned is collapsed, matching what renders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): hydrate pinned sidebar rows * fix(app): keep filtered workspace controls reachable * feat(app): scope sidebar pinning to chats, drop project pinning Per PR review: pinning whole projects overlapped with project reordering and mixed projects into the pinned list. Keep chat pinning only. - Remove the project.pin.set RPC, its server handler, the projectPinnedAt descriptor field, and the client setProjectPinned method. - Drop the project pin control, isProjectPinned reads, and the suppressPin plumbing (its only source — a pinned project — is gone). - Fold pin-aware shortcut ordering into SidebarModelProvider (the live shortcut owner) and delete the orphaned use-sidebar-shortcut-model hook, so pinned chats and their shortcut numbers share one projection. - Prune the now-unused project pin i18n keys across all locales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(server): inline the pin handler into the workspace path With project pinning gone, handlePinSetRequest had a single caller. Fold it back into handleWorkspacePinSetRequest and drop the project/workspace generalization. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * revert: keep main's use-sidebar-shortcut-model untouched This hook (and its test) already live on main. The scope-down had deleted them, but that's out of scope for this PR — the pin-aware shortcut ordering lives in SidebarModelProvider now, so main's hook is left exactly as it was. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(protocol): align workspacePinning COMPAT version in websocket-server Match the v0.1.105 version already corrected in messages.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app): reshape pinned sidebar updates * chore(app): remove stale pin button styles * refactor(app): simplify pinned workspace rows --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com> |
||
|
|
03dcda7c41 |
Archive any workspace from the app (#2003)
* feat(app): expose archive for every workspace Workspace actions now archive one workspace by id. The daemon remains responsible for backing-specific cleanup, including removing a managed worktree only after its final workspace is archived. * refactor(app): clarify archive descriptor name * fix(app): preserve workspace archive safety * fix(app): allow archive without upstream * refactor(app): remove stale archive callback * test(app): cover workspace archive ownership |
||
|
|
28a27b02d3 |
Prevent Android chats from freezing or going blank (#1989)
* fix(app): stabilize streamed chat rendering Use deterministic Markdown AST keys and preserve unique row identities when an assistant message resumes after a tool. Keep native gesture and plan-card children correctly keyed to avoid unsupported Fabric reconciliation paths. * fix(app): stabilize retained native panels Keep retained workspace roots in a stable native order and make active panels present in the selection commit. Hidden panels now stop expensive subscriptions and animations without losing mounted state. * fix(app): harden retained panel activity |
||
|
|
cac71ee8f5 |
Hide keyboard shortcut badges in workspace menu on native (#1964)
* Hide keyboard shortcut badges in workspace menu on native The archive item's shortcut badge (e.g. Ctrl+Shift+⌫) rendered in the workspace kebab/context menu on mobile, where there's no physical keyboard to invoke it. Gate the trailing badge behind !isNative in both kebab menu variants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(app): hide native shortcuts in every workspace menu Workspace menus were duplicated across project and status views, so the status path missed the native gate. Centralize the menu so every sidebar view shares the same shortcut policy. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a743a22496 |
Keep large sidebars responsive (#1966)
* perf(app): keep large sidebars responsive Sidebar rows rescanned every agent for each session update, multiplying work with both workspace and agent count. Build workspace activity once and share the derived sidebar model so updates scale with the underlying collections. * test(app): use valid agent fixture for workspace restore * refactor(app): keep sidebar model surface focused |
||
|
|
807d0d6d69 |
fix(app): show New workspace action on non-git sidebar projects (#1857)
* fix(app): show New workspace action on non-git sidebar projects The per-project "New workspace" affordance in the sidebar (the project- header + button and the empty-project ghost row) was gated on host.canCreateWorktree, i.e. projectKind === "git". Non-git projects (non_git / directory) therefore showed no way to add a workspace, even though a host with the workspaceMultiplicity capability can create additional local workspaces for them. Thread the per-host workspaceMultiplicity flag into the sidebar project row model and show the affordance when canCreateWorktree || supportsWorkspaceMultiplicity, matching the gate already used by the global "New workspace" button and the Cmd+N handler. Rename the internal trailing-action kind new_worktree -> new_workspace (a client-only UI model, not a wire type) to reflect that it now also covers non-git workspaces. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(app): expect the per-row New workspace icon on non-git projects The sidebar "+" now shows for non-git projects on a multiplicity-capable host, so the Model B sidebar spec no longer asserts the non-git project has zero new-worktree icons — it now expects the icon, like git projects. Also refresh the now-stale "no new-worktree icon" comments in the entry-points spec (the picker is still one valid entry point). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d3e8f77914 |
Keep composer autocomplete visible after route hops (#1851)
* fix(app): keep composer autocomplete visible after route hops Workspace callers now express only the target workspace. The navigation helper chooses whether to pop to a mounted host route or fall back, and active-workspace tracking ignores stale hidden route params while app-wide routes are foregrounded. * test(app): address workspace navigation review * fix(app): parse decoded legacy workspace routes * fix(app): ignore decoded legacy tab routes * test(app): use canonical offline workspace route |
||
|
|
96573d0bcc |
Keep slash commands visible after New Workspace (#1760)
* fix(app): keep slash commands visible after New Workspace Repeated returns from the app-wide New Workspace route were using dismissTo with the workspace leaf URL. That updated the root URL without popping the nested host stack, so hidden duplicate workspace deck entries could remain mounted and steal composer popover measurements. Dispatch a root-stack POP_TO into the host workspace route instead, with a Playwright regression for the slash-command popover. * test(app): tighten workspace autocomplete regression Derive the expected deck count from the seeded workspaces and make the duplicate-deck assertion hard. Keep the Expo Router pop hint documented because removing it reproduces the hidden deck entries. |
||
|
|
9b6aa99396 |
feat(sidebar): show host name on every row in multi-host setups (#1775)
The host name previously appeared only in project grouping mode, and only for projects shared across hosts. It now shows on every workspace row in both project and status grouping modes whenever the sidebar spans more than one host, so you can always tell which machine a workspace lives on. Status rows read "Project · Host". Single-host setups are unchanged. |
||
|
|
1ce00dca1f |
fix(sidebar): align workspace row titles
Workspace rows inherited an extra indent in the merged sidebar, so child titles no longer lined up with project titles. |
||
|
|
3fbd82664d |
Make New Workspace an app-wide screen (#1746)
* fix(app): make new workspace global New Workspace now opens as an app-wide screen. Host and project context can still preselect the form through query params, but the route itself no longer lives under a host. * fix(app): simplify new workspace selection |
||
|
|
a49c658d1f |
Merge sidebar workspaces across all connected hosts (#1538)
* Merge workspaces across all hosts in the sidebar Replace per-host sidebar sections with a single merged project list. Projects that exist on multiple hosts (matched by projectKey) are collapsed into one row. Host identity is surfaced via: - A status-only footer pill showing online/offline host counts - Hover cards with a mandatory host row on every workspace entry - Subtitles showing the host name when a project spans >1 host - A host filter in the grouping dropdown (not a sidebar switcher) Order and group-mode stores migrated from per-server to global keys with zustand persist migrations. New-workspace screen now shows a merged project list with a host selector combobox. Key files: - workspace-structure.ts: merge algorithm by projectKey - sidebar-view-store.ts / sidebar-order-store.ts: global state - sidebar-workspace-list.tsx: context-based subtitle injection - new-workspace-screen.tsx: host selector in footer row * Add host filtering to the merged sidebar The sidebar now merges workspaces from every connected host, so a single global "active host" no longer fits. Host selection becomes explicit per action via a host chooser, and agent history pages across all hosts at once. Removes the active-host / active-server-id model and the all-agents-list hook that fed it. * Fix rebase fallout: translate archive host error, update stale workspace-key test archiveWorkspacesOptimistically used a raw "Host is not connected" string that the i18n completeness test forbids; route it through i18n.t with the existing hostDisconnected key. The selectors memoization test asserted un-prefixed workspace keys, but merged-sidebar keys are serverId-prefixed. * Fix sidebar review fallout * Address sidebar review followups * Fix sidebar workspace identity migration * Address sidebar review followups * Handle partial host history failures * Fix merged sidebar project removal and E2E drift * Restore history routes and project removal shape * Remove sidebar host context plumbing * Tighten sidebar status and host filter state * Keep new workspace project selection host-compatible * Migrate sidebar view storage * Streamline host selection flows * Update all-host history on archive * Fix empty project sidebar action Code regression: the main merge dropped the empty-project New workspace child row while keeping the empty project persistence contract. Restore the row using the project host target so the sidebar stays aligned with the no-host-selection model. * Fix Japanese shortcut translation key Stale-base integration: main added the Japanese locale after this branch added cycleAgentMode to the English shortcut help map. Merging main exposed the missing ja key in CI typecheck and resources.test. * Fix Playwright speech teardown race |
||
|
|
4534754617 |
Add projects without creating workspaces
Keep the legacy open-project request creating a workspace for old clients; new clients now add projects and create workspaces through separate flows. |
||
|
|
9a09ccb3d6 |
Fix sidebar project removal
Remove projects through a daemon RPC so the sidebar can delete the project record after archiving its active workspaces. Reopening a removed directory recreates the missing project record so local projects keep their derived basename, and History skips orphaned archived workspace rows. Fixes #1583. |
||
|
|
c3321ea8ff |
Show PR numbers without the # and make header buttons icon-only
PR badges in the sidebar rows, hover card, and right-sidebar PR tab now show the number alone. The Scripts and Open-in-editor header buttons drop their labels and gain a min height so they keep the same size icon-only. |
||
|
|
de84c8f179 | Align sidebar header rows and tighten button spacing | ||
|
|
e202ca5036 |
Remove a worktree when its last workspace is archived (#1562)
* Remove a worktree when its last workspace is archived
Archiving is now workspace-centric: the UI always archives a workspace,
and a worktree's directory is removed only when its last referencing
workspace is archived (derived from ownership + a single reference
count). The "remove the worktree from disk?" prompt is gone; archiving
warns only when the workspace is dirty or has unpushed commits.
MCP archive_worktree and the CLI keep the explicit lower-level behavior:
archive every workspace backing the directory, then remove it.
Auto-archive-after-merge and create-agent auto-archive respect the
reference count like any other workspace archival.
The policy now lives in two modules - a server archive service and a
client archive hook - with the reference-count check in exactly one
place. The deleteWorktreeFromDisk request field is retained but ignored
for wire back-compat.
* Tidy archive-policy internals and close test gaps
Follow-up polish on the archive-policy consolidation; no behavior change.
Structure: the server archive service is renamed to reflect that it now
owns all archive policy (the file and its types no longer say "worktree"
where they mean "archive"); the client archive input derives the
workspace kind from the canonical type instead of re-listing it; the
not-found target is typed string|null instead of an empty-string
sentinel; a redundant workspace-update emit and the ambiguous
worktrees-root naming are cleaned up. Wire, SDK, and keybinding names are
deliberately left unchanged for back-compat.
Tests: re-home the archiving-state lifecycle and per-workspace snapshot
assertions; pin worktree-kind acceptance through archive_workspace, the
ignored deleteWorktreeFromDisk field, and three-workspace archive-all;
add a Playwright spec for the dirty/unpushed worktree archive flow (the
warning gates it, confirming removes the directory) and the bulk
remove-project confirm branch.
* Fix the hanging worktree-archive risk-warning e2e spec
The spec added in the previous commit hung deterministically, locally and
in CI. Two test-only bugs, no product change.
The archive click opens a synchronous window.confirm(); the spec
registered a passive dialog waiter but only dismissed the dialog after
awaiting the click, so the click never resolved (Playwright won't settle
a click while a dialog is open) - a circular wait. Answer the dialog
inline via page.once("dialog", ...) before the click resolves, matching
the proven worktree-archive spec.
The seed also committed on a never-pushed branch, so aheadOfOrigin was
null and the "1 unpushed commit" warning never appeared. Push the branch
to set its upstream before the local commit, so it reports exactly one
unpushed commit.
Verified: the spec now passes in ~6s (was a 45s timeout); both the
dismiss-gates and accept-removes paths run.
|
||
|
|
754f6336c0 | Remove pinned launchers from workspace rows | ||
|
|
713a042dc5 |
Add pinnable new-tab targets to the tab row and sidebar
Any new-tab target (new agent, terminal, browser, or a terminal profile) can be pinned as a quick-launch button. Pins are global client app state (persisted), not daemon config, so the same set shows on every workspace; profile pins resolve per host and are dropped where that profile is absent. The sidebar mirror launches via a navigate-then-launch intent store rather than calling the create handlers directly: terminal creation only works in the mounted/focused workspace, so the sidebar requests the launch and the workspace screen consumes it on focus. |
||
|
|
71b5c35d9b |
Run multiple independent workspaces per directory (#1539)
* feat(workspace): bind agents/terminals/status to workspaceId (Model B phase 1)
Model B EX-Phase 1: the binding layer. Records gain an additive
workspaceId that answers "which workspace does this belong to" without
replacing the existing required cwd.
- protocol: add optional workspaceId to agent/terminal records and a
workspaceOwnership capability under server_info.features.*
- server: resolve and persist workspaceId on agent and terminal
creation; carry it through agent-manager, projections, storage,
loading, sessions, and the terminal manager/worker pipeline
- workspace-directory / workspace-registry-model: derive and expose
workspace ownership; bind status to workspaceId
- client: surface workspaceId via daemon-client
- app: filter terminals and agent visibility by workspaceId, thread it
through workspace screen / terminal panel / session store
- tests: cover workspaceId binding across create, agent-manager,
mcp-server, workspace-directory, workspace-registry-model, and
agent-visibility
cwd stays required on every record; workspaceId is purely additive and
back-compatible (optional/defaulted), so old and new peers still parse
each other's messages.
Known follow-ups: none.
* feat(workspace): empty projects + editable titles, fix same-cwd terminal isolation (Model B phase 2)
- Address Phase 1 review fixes and add same-cwd terminal isolation e2e coverage
- Isolate terminal subscriptions per workspaceId so two workspaces sharing one cwd no longer cross-wire terminals (terminal-subscription-key)
- Add editable workspace title, decoupled from the backing directory/branch
- Persist empty projects (no workspaces yet) across daemon restarts
- Add e2e coverage: empty-project-persists (server + app), workspace-same-cwd-isolation, sidebar-workspace-rename
Known follow-ups: none
* feat(workspace): create multiple workspaces per directory — local or worktree (Model B phase 3)
- workspace.create RPC always creates a new workspace record; no directory dedup
- dropped directory dedup in open_project and createPaseoWorktree paths
- non-git directories are first-class: local-checkout workspaces no longer require a repo
- service-proxy collision resolved by defaulting to the owning workspace instead of failing
- new creation UI: choose backing directory (local checkout or worktree) per workspace
- e2e coverage for creating multiple workspaces over the same directory
Known follow-ups:
- COMPAT cwd->workspaceId resolver remains until client floor advances
- prune any remaining workspace==directory assumptions surfaced by usage
* feat(cli): paseo run workspace policy — bare run creates a workspace, --workspace/PASEO_WORKSPACE_ID target (Model B phase 4)
- A workspace is now the explicit home of a `paseo run`: run.ts resolves a
workspace before creating any agent, then stamps the agent with that
workspaceId — no run leans on createAgent's legacy cwd->workspace fallback.
- Precedence: --workspace <id> > $PASEO_WORKSPACE_ID > --worktree > bare run.
--worktree mints its own workspace and overrides the ambient
PASEO_WORKSPACE_ID; --worktree + --workspace is rejected upstream.
- New-workspace-per-bare-run: every bare run mints a fresh local-backed
workspace for its cwd rather than reattaching to an existing one in that dir.
- Env support: $PASEO_WORKSPACE_ID (exported by workspace terminals) targets an
existing workspace, same as --workspace.
- Help text: new --workspace option documents the default (new workspace per
run) and the env fallback; created runs print the workspace id plus a tip.
- e2e: cli-run-workspace-precedence.e2e.test.ts covers bare, distinct-per-cwd,
--workspace attach, and env attach against an isolated daemon.
Known follow-ups:
- createAgent's COMPAT cwd->workspace resolver stays for old clients; the CLI no
longer relies on it but it is not yet removable.
* feat(workspace): archive removes the workspace record, never the directory; last worktree ref offers disk delete (Model B phase 5)
- Archive is now scoped to a single workspace RECORD (by workspaceId), not by cwd/worktree path. It tears down only the agents and terminals owned by the target workspaceId.
- Sibling isolation: a directory can back multiple workspaces, so archiving one workspace no longer destroys a sibling workspace's agents, terminals, or directory.
- Decoupled on-disk worktree deletion into an explicit, last-reference-only option (deleteWorktreeFromDisk). The directory is removed only when the archived workspace was the last active reference to a Paseo-owned worktree; local checkouts are never deleted.
- Unified archive UI with a keep/delete prompt: "Keep on disk" is the default non-destructive choice, "Delete" removes the worktree directory.
- Protocol: new optional, defaulted deleteWorktreeFromDisk field (COMPAT-tagged, back-compat preserved).
- Tests: record-scoped archive e2e (sibling isolation, last-ref disk delete, sibling-keeps-directory) and an app e2e for the keep prompt.
Known follow-ups:
- Surface the keep/delete prompt's "delete" path coverage in app e2e (only the keep path is exercised today).
- COMPAT(worktreeDiskDeletion): drop the optional gate when floor >= v0.1.97.
* feat(workspace): uniform expandable projects + status inbox; merged worktrees cleaned, explicit-id archive (Model B phase 6)
- P5 archive fixes: worktree archive now targets an explicit workspaceId, and auto-archive-on-merge cleans the worktree directory from disk when it is the last reference.
- Dropped non-git project flattening: every project — git or non-git, single- or multi-workspace — renders as the same expandable parent.
- Every project is expandable and every workspace is archivable; each carries its own "+ New workspace" affordance regardless of kind.
- Added a status inbox grouping (Ready for review / Working / Done) where each workspace is bucketed independently by last update.
- The deepest sidebar level is the workspace row: tabs, agents, and terminals never appear in the sidebar.
- Added Model B sidebar e2e coverage proving the expandable-parent, no-leaf, and independent-status-bucketing invariants.
Known follow-ups: the inline header FolderPlus worktree shortcut stays git-only (canCreateWorktreeForProjectKind); non-git projects create workspaces via the "+ New workspace" row.
* fix(protocol): use literal :: separator in terminal subscription key
The committed blob contained a NUL byte as the separator instead of the
:: the comment documents, which broke text diffs and review tooling
(git flagged the file as binary). Functionally the key is an in-memory
Map key only, but source must stay text.
* feat(workspace): shared directory right-sidebar boundary + docs; final consolidation (Model B phase 7)
- Pin and document the directory-backed vs workspace-owned right-sidebar boundary:
same-cwd workspaces share directory-keyed git/PR/file surfaces but never share
workspace-owned drafts, attachments, or file-explorer state. New docs section in
architecture.md plus data-model.md keying convention and two glossary terms.
- Add e2e coverage for the boundary: same-directory-workspaces.spec.ts proves the
right sidebar is shared (a directory change appears in both same-dir workspaces)
while tabs/terminals stay independent per workspace.
- Accumulated follow-up fixes:
- MCP child agents created in a new worktree are stamped with the new worktree's
workspaceId (not the parent's, not unstamped), mirroring the session path so
workspaceId-scoped archive can find and tear them down.
- session-store mergeWorkspaces prunes a stale empty-project descriptor when a
workspace lands in that project, so it stops governing the project's metadata.
- CLI run: reject --worktree alongside an ambient PASEO_WORKSPACE_ID (not just
--workspace), so worktree resolution never races an existing-workspace select.
New run.test.ts pins the three validation outcomes.
- Full-branch reshape outcomes:
- Consolidate the four placeholder server_info.features flags (workspaceOwnership,
workspaceTitles, workspaceProjects, workspaceMultiplicity) into the single
workspaceMultiplicity capability gate.
- Remove the now-dead ensureLocalCheckoutWorkspace and its deps interface; explicit
creation always mints a new same-cwd record via createLocalCheckoutWorkspace.
- Resolve every Model B COMPAT marker from v0.1.X to v0.1.97.
- Drop the obsolete workspaceOwnership feature assertion from the same-cwd
isolation e2e; broaden a sidebar-shortcuts test to cover both project kinds.
- Add workspace-create-errors.e2e.test.ts pinning each early-reject error branch.
- typecheck, lint, format all green.
Known follow-ups: pre-existing COMPAT(rewind) markers still carry v0.1.X placeholders
(out of Model B scope, left untouched).
* test(workspace): de-slop Model B tests per audit-tests (reshape mocks/assertions)
Reshape Model B test changes flagged by audit-tests so they exercise real
behavior and observable state instead of mock scaffolding.
Categories fixed:
- Mocks -> real dependencies + persisted state: create.test.ts and
mcp-server.test.ts dropped hand-built AgentManager/AgentStorage mock objects
and mock.calls[...] assertions, now run the real AgentManager/AgentStorage
with a fake agent client and assert on the stored agent record (workspaceId,
parent label).
- Module-internal spying -> injected seam: acp-agent stopped spying on the
tree-kill module (vi.spyOn(treeKillModule, ...)). Added a ProcessTerminator
injection seam to ACPAgentClient/ACPAgentSession (production defaults to
terminateWithTreeKill); tests inject a typed FakeTerminator and assert on the
recorded children plus observable stream .destroyed state.
- Poking internals -> public API: acp close()/killTerminal/releaseTerminal
tests now create terminals through the public createTerminal API instead of
mutating internals.terminalEntries.
- Reimplemented production logic deleted: cli-run-workspace-precedence dropped
its inline copy of resolveRunWorkspace flag precedence (a fake reimplementation
it then asserted against) and now proves only the daemon behaviors the CLI
builds on; flag precedence stays covered in the CLI's own run.test.ts.
- Sleep -> poll: workspace-same-cwd-isolation replaced a fixed setTimeout with
expect.poll on the observed snapshot state to remove the race.
- Internal protocol-frame assertions removed: sidebar-workspace-rename dropped
the captureWsSessionFrames workspace.title.set.request assertions, relying on
the user-visible rename + reload checks already present.
* fix: green CI — new-workspace status bucket regression + branch-picker flow tests + sdk emptyProjects
Source regression:
- packages/app/src/screens/new-workspace-screen.tsx
- packages/app/src/screens/new-workspace-empty.ts
When a new workspace is created with an initial agent, optimistically merge it
with status "running" (statusEnteredAt now) so it lands in the "Working" bucket
instead of defaulting into the wrong bucket. The empty-workspace path passes
withInitialAgent: false so a bare workspace keeps its descriptor status.
Test-drift (intentional P3 flow change — backing picker now required):
- packages/app/e2e/new-workspace.spec.ts
- packages/app/e2e/helpers/new-workspace.ts
The reshaped creation flow requires choosing a backing ("New worktree") before
the branch / starting-ref picker is reachable. Branch-picker specs now call
selectWorkspaceBacking(page, "worktree") first, and the helper waits for the
worktree control to drop aria-disabled (it stays disabled until the checkout
status query confirms the project is a git repo) before clicking.
Test-drift (protocol field added — sdk emptyProjects):
- packages/client/src/index.test.ts
fetch_workspaces response now carries emptyProjects; the toEqual expectation
includes emptyProjects: [].
Flake hardening:
- packages/app/e2e/same-directory-workspaces.spec.ts
- packages/app/e2e/helpers/seed-client.ts
Out-of-band working-tree writes raced the daemon's debounced filesystem
watcher, so the UI could subscribe before the new file was in the git snapshot.
Force a checkout refresh (same path as the UI's manual refresh) to make the
write authoritative before asserting, removing the timing dependency.
* test(app): expect withInitialAgent:false in empty-workspace create call
The withInitialAgent flag (fix for the new-workspace Done-bucket regression)
added a field to the ensureWorkspace call; the empty-path unit test pinned the
exact args. Test-drift, not behavior — update the expectation.
* feat(app): stack new-workspace creation params (Project / Isolation dropdown / Base) with reserved base row + keyboard avoidance
Restructure the new-workspace creation screen into a vertical stack of
Project / Isolation / Base parameters instead of the previous mixed
layout.
- Isolation/backing is now a dropdown (Local vs New worktree) matching the
app's existing dropdown/combobox primitives, replacing the inline
segmented switcher.
- The Base (starting ref) row now reserves its space even when the backing
is Local and the picker is hidden, so switching backing no longer causes
the form to shift vertically.
- The form avoids the keyboard so the title input and submit stay visible
while typing on native and web.
- Updated the e2e new-workspace helper to drive the dropdown-based
backing selector and the reserved Base row.
* fix(app): simplify new-workspace form — stacked ghost rows above composer, no card/title, project-matched dropdowns
- Render the three rows (formStack) at the top, under the "New workspace"
heading and above the composer input, not in the composer footer.
- Drop the card/surface/border chrome; rows sit on the plain background.
- Stack Project, Isolation (multiplicity only), and Base; remove the title
field — the title is set server-side and create works with no user title.
- Each row is a Label immediately followed by its dropdown control; no
columns, fixed label widths, or reserved horizontal space, with the label
glyph aligned to the heading's text x.
- All three triggers share the ghost badge style of the Project control
(ProjectPickerTrigger / IsolationPickerTrigger / RefPickerTrigger +
Combobox); keep the workspace-create-backing-* test IDs.
- Omit "New worktree" from Isolation entirely when the project is non-git.
- Reserve the Base row height always but render nothing on Local backing,
showing the Base label + ref picker only for New worktree.
- Keep keyboard avoidance on the centered content.
- E2E: drive selectWorkspaceBacking via the Isolation trigger + Combobox and
remove the now-gone workspace-create-title-input usage and title field.
* feat(app): sidebar new-workspace entry points + non-git isolation hidden + project auto-select
- Q1: hide the Isolation control on non-git projects — gate the row on canCreateWorktree (multiplicity && selectedIsGit) so a project with no git checkout never offers a worktree backing choice.
- Q2: reset the stale project preselect across the reused 'new' screen — clear the manual picker choice on route project identity change so each route-driven navigation preselects its own project; align the nav verb to router.navigate.
- Q3: remove the per-project "+ New workspace" sidebar row and add one global "New workspace" entry above Sessions in both mobile and desktop sidebars (testID sidebar-global-new-workspace); creation stays reachable per-project via the existing git new-worktree icon.
- Q4: match the Sessions / New-workspace header button sizing to the workspace rows — SidebarHeaderRow icon md->sm, label fontSize base->sm.
- e2e: new new-workspace-entry.spec covering global entry, project preselect reset across reused screen, and non-git isolation hidden; update sidebar-model-b, empty-project-persists, workspace-multiplicity, and helpers for the removed per-project row.
* fix(app): group New-workspace/Sessions header (no divider, workspace-row sized) + remove banned useUnistyles
- Q5: wrap the New-workspace and Sessions header entries in a single sidebarHeaderGroup that owns one bottom divider, so the two rows sit tight together with no gap and no per-row separator (both mobile and desktop sidebars).
- Add a compact variant to SidebarHeaderRow: workspace-row sized (minHeight 36, surfaceSidebarHover, borderRadius.lg) with horizontal padding that aligns its icon/label with the Workspaces section title and the workspace rows below; the default header variant (settings Back-to-workspace) keeps its sidebar-height row and own separator.
- Remove the banned useUnistyles() from sidebar-header-row.tsx per docs/unistyles.md: theme-reactive icon color now goes through withUnistyles(Icon) + uniProps mappings; static sizing reads ICON_SIZE.
- Taste: new-workspace-screen dedupes the project-icon styles (single projectIcon/projectIconFallback/projectIconFallbackText) and replaces repeated magic numbers with BADGE_HEIGHT and a named fallback-font-size constant.
* fix(app): no layout shift on git<->non-git (reserve Isolation row) + symmetric sidebar divider spacing
- Q6: Isolation row reserves its height and renders an invisible spacer for non-git projects, matching the Base-row pattern, so switching between git and non-git projects keeps a constant form height with no layout shift.
- Q7: sidebar header group splits paddingVertical into paddingTop/paddingBottom so the Sessions-row-to-divider gap equals the divider-to-Workspaces-header gap, centering the divider.
* feat(app): rename Sessions to History (clock icon, Agent history header)
- Sidebar label now reads "History" with a clock icon (was Sessions / MessagesSquare)
- Sessions screen header now reads "Agent history"
- Updated i18n strings across all 6 locales (en, ar, ru, zh-CN, fr, es)
- Route and testIDs unchanged (sidebar-sessions, /sessions)
* fix(app): symmetric sidebar header padding (top == bottom), traffic-light inset preserved
The sidebarHeaderGroup wrapper (New-workspace + History rows) had paddingTop: theme.spacing[1] against paddingBottom: theme.spacing[2]. Equalize paddingTop to theme.spacing[2] so the header group's top padding matches its bottom padding. The divider stays centered since paddingBottom still matches WorkspacesSectionHeader's paddingTop. The desktop window-controls (traffic-light) spacer — paddingTopSpacerStyle plus TitlebarDragRegion — is a separate inset and is left untouched.
* fix(app): hover card shows branch; sidebar copy-branch copies the real branch (not the title)
- Add SidebarWorkspaceEntry.currentBranch, sourced from gitRuntime.currentBranch (normalized; detached HEAD/blank/missing -> null)
- Fix copy-branch smear: handleCopyBranchName copied workspace.name (the title); now copies the real currentBranch (guarded)
- Workspace hover card: add a branch row (GitBranch icon + branch name), shown only when it differs from the title
- Header branch-switcher left untouched; diff-pane re-home decision still pending
* feat(app): branch switcher moves into the git diff panel; header title is static (Model B coherence)
- branch switcher now lives in the diff panel Changes header on desktop+mobile
- workspace header title is a plain static title; branch removed from it
- no new workspace-screen header row
- git diff-stat unchanged, stays where it is
- no duplicate git actions
- unified descriptor name fallbacks via resolveWorkspaceName
- rename-then-switch e2e proves header title and real branch stay independent
|
||
|
|
21deb08673 |
Make workspace IDs opaque, independent of the filesystem path (#1503)
* Make workspace IDs opaque, independent of the filesystem path Workspace IDs were the resolved checkout/worktree path, so code could treat an ID as a path: prefix matching, deriving directories from it, falling back to a path when a lookup missed. IDs are now opaque - compared only by exact equality and never used as a path. Anything path-shaped resolves the workspace record first and reads its cwd. New workspaces get a generated `wks_` ID; existing path-shaped IDs are read from disk and never regenerated, so there is no migration and no change to the wire or persisted schemas. Groundwork for running multiple workspaces in a single directory. * Keep attachment scope key stable and align refetch test The attachment scope key keeps `workspace=` instead of `wsid=` so existing persisted drafts are not orphaned; the rename was cosmetic. The SDK refetch test no longer asserts a filter, matching refetch fetching one page and selecting by id client-side. * Fix live agent updates in directories without a registered workspace The opaque-ID work removed buildProjectPlacementForCwd's directory fallback, so an agent running in a folder with no registered workspace (e.g. a fresh non-git dir) produced a null placement. forwardAgentUpdate then threw "Workspace not found", the error was swallowed by its catch, and no agent_update was emitted — live model/thinking switches and status updates silently stopped. Caught by the live-preferences e2e suite. The fallback builds a directory-scoped project placement keyed by the path. That key is a project grouping key (non-git projects group by path), not a workspace id, so it stays within the opaque-id rule. * Always run server worktree archive, even without a resolved workspace Archiving bailed out entirely when the workspace was not found in the client store, so a race or stale state could make "archive" do nothing server-side with only a console.warn. The server archive is keyed by worktreePath, which is always available, so it now runs regardless; only the optimistic client-side updates (keyed by workspace id) are gated on the workspace being resolved. * Expect a directory-scoped placement for unregistered agent dirs This unit test asserted the no-placement behavior reverted in the live agent-update fix, which had broken live model/thinking switching. Update it to expect the directory-scoped placement now emitted for an agent in a directory with no registered workspace. * Fix opaque workspace routing in app E2E * Preserve workspace IDs during partial bootstrap * Fix archive flows for opaque workspace IDs * Stop treating opaque workspace IDs as filesystem paths Workspace IDs are opaque (wks_<hex>), but several call sites still passed the id where a directory was expected, which broke those flows for opaque IDs. The branch switcher sent the id as a cwd to branch/stash/checkout git operations, and server archive/reconcile cleanup keyed git-watch and subscription teardown by id, leaking that state. Git and filesystem operations now take the workspace directory; the opaque id is used only for identity and cache keys. Archive/reconcile cleanup routes through a single teardownArchivedWorkspace helper that keeps the key split explicit: runtime store by id, git watch and subscription by cwd. Path-derived grouping keys are renamed to directory keys, and the helpers are split into workspace-identity and workspace-directory so the id-vs-path boundary is obvious. |
||
|
|
08ebe5e7e1 |
Complete client UI i18n migration
Adds client-side i18n support, language settings, translated UI copy, and locale parity coverage. |
||
|
|
df635b570a |
feat(desktop): support multiple windows
Closes #250 |
||
|
|
e961ceef98 | Hide mobile sidebar for new workspace |