* fix(app): reuse project icon cache in new workspace
New Workspace omitted each host placement's effective icon revision, so its icon queries could not share the sidebar's memory or persistent cache entries. Carry that revision through the target projection.
* refactor(app): centralize project icon targets
Project icon consumers constructed equivalent cache targets independently. One target factory now carries each host placement revision through the sidebar, New Workspace, Projects, and Project Settings.
* test(app): inline project icon fixture
* 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>
* 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
* 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.
* 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
* Add project rename so duplicates get distinguishable names
Two checkouts of the same remote collapse into a single project record
(same projectId), which left users unable to tell duplicates apart in
the UI. customName lives alongside the derived displayName as an
override that reconciliation never touches, so renames persist across
git-remote changes. The rename UI lives in project settings.
Closes#987 (rename half).
* Fix COMPAT(projectCustomName) version to v0.1.76
* Update useProjects shape test for new projectCustomName key
* Use sendCorrelatedSessionRequest for renameProject
Match the pattern used by checkoutPrMerge and other newer RPCs
instead of hand-rolling the select callback.
* Use project.rename.{request,response} dotted RPC names
Match the convention from docs/rpc-namespacing.md (added in 75a6f8277).
Schemas, type exports, daemon client, session handler, and tests all
move to the namespaced names. No back-compat shim needed since the RPC
hasn't shipped.
* feat(server): derive owner/repo display name for any remote host
Generalize deriveProjectGroupingName so any remote:<host>/<segments...>
project key returns the last two path segments (owner/repo) instead of
just the trailing segment. Path-fallback for non-remote keys is unchanged.
Brings GitLab, Gitea, Bitbucket, and self-hosted remotes to parity with
the prior github.com-only behavior — no separate special-case needed.
* feat(app): show projects from any git remote in Projects settings
Remove the isSupportedProjectKey filter so workspaces with non-GitHub
remotes (GitLab, Gitea, Bitbucket, self-hosted, ssh-style) appear in the
Projects list and route to the project settings screen. The daemon RPCs,
config schema, and registry were already host-agnostic; this lifts a
client-side filter that hid them.
Also remove the now-vestigial hiddenUnsupportedRemoteCount field from
ProjectSummary, BuildProjectsResult, and UseProjectsResult — once the
filter is gone, the count is always zero and the field is dead state.
This is an internal app-package type, not a wire schema, so deletion is
safe.
* chore(app): simplify Projects empty state to "No projects yet"
Drop the "Non-GitHub remote projects aren't supported yet" empty-state
branch — it can no longer fire now that any git remote is shown. The
empty state is unconditional now.
* test(app): cover non-GitHub remote in projects-settings e2e
Add a fixture that creates a temp git repo with origin pointing at a
gitlab.com URL and exercises the same paseo.json read/edit/save flow
already covered for local repos. Verifies the project surfaces with the
"acme/app" display name and that the round-trip persists correctly.
Extracted the remote-setup branch in createTempGitRepo into a small
configureRemote helper to keep the main function under the cyclomatic
complexity limit.
---------
Co-authored-by: Mathias Kurz <mkurz@stamus-networks.com>
Adds a Projects settings flow for inspecting and editing per-project
paseo.json (worktree setup/teardown, scripts, terminals) directly from the
app. Reads/writes go through new daemon RPCs with revision-based optimistic
concurrency, and the screen always keeps its header and host picker visible
so a hung host doesn't trap the user.