Skip to content

feat: configure workspace worktree storage#1268

Open
artile wants to merge 1 commit into
getpaseo:mainfrom
tao-io:feat/worktree-storage-path
Open

feat: configure workspace worktree storage#1268
artile wants to merge 1 commit into
getpaseo:mainfrom
tao-io:feat/worktree-storage-path

Conversation

@artile
Copy link
Copy Markdown

@artile artile commented Jun 1, 2026

Summary

  • add per-workspace worktreeStoragePath persistence and protocol/client RPC support
  • route worktree create/list/archive flows through configured storage roots while preserving default behavior
  • expose the setting in project settings and propagate it through app workspace/project state
  • document custom worktree storage behavior

Verification

  • npx vitest run packages/server/src/server/workspace-registry.test.ts packages/server/src/utils/worktree.test.ts packages/server/src/server/paseo-worktree-service.test.ts packages/server/src/server/session.workspaces.test.ts packages/client/src/daemon-client.test.ts packages/app/src/utils/projects.test.ts packages/app/src/stores/session-store.test.ts packages/app/src/screens/projects-screen.test.tsx --bail=1
  • npm run format
  • npm run typecheck
  • npm run lint

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 1, 2026

Greptile Summary

This PR wires per-workspace worktreeStoragePath persistence end-to-end: from a new workspace.set_worktree_storage_path RPC through the session handler, workspace registry, and into every worktree create/list/archive path, with a new project-settings UI for editing and optional Electron directory picker.

  • Protocol & persistence: adds worktreeStoragePath to WorkspaceDescriptorPayloadSchema (.optional().default(null)) and to the persisted workspace record schema — both changes are backward compatible with old daemons and clients.
  • Worktree operations: createWorktree, listPaseoWorktrees, resolveExistingWorktreeForSlug, and archive ownership checks all thread worktreeStoragePath through a new resolvePaseoWorktreesRoot helper; isPaseoOwnedWorktreeCwd gains a worktreesRoot override option.
  • App: WorktreeStorageEditor in project settings lets users set or reset the storage directory, with tilde expansion handled server-side before persistence.

Confidence Score: 3/5

The core persistence and worktree-creation path looks correct, but two issues need fixing before merging: the ownership bypass in commands.ts and the missing capability gate on the new RPC.

The resolvePersistedWorktreeOwnership fallback in commands.ts uses dirname(normalizedTarget) when worktreeStoragePath is null, which causes any registry-registered worktree to pass the ownership check regardless of whether it lives under a Paseo-controlled directory — the three parallel implementations of this fallback disagree on the right behavior. Additionally, the new workspace.set_worktree_storage_path RPC is sent unconditionally without a server_info.features capability gate, violating the repo's explicit protocol contract: users on old daemons will hit an error toast instead of a graceful 'please update' message.

packages/server/src/server/worktree/commands.ts (ownership bypass) and packages/client/src/daemon-client.ts (missing capability gate) need attention before this is safe to merge.

Important Files Changed

Filename Overview
packages/protocol/src/messages.ts Adds WorkspaceSetWorktreeStoragePath request/response schemas and appends worktreeStoragePath to WorkspaceDescriptorPayloadSchema with .optional().default(null) — backward compatible.
packages/client/src/daemon-client.ts Adds setWorkspaceWorktreeStoragePath RPC and passes worktreeStoragePath through createPaseoWorktree; missing daemon capability gate for the new RPC.
packages/server/src/server/worktree/commands.ts Extends list/archive/slug-resolution commands to respect per-workspace storage paths; resolvePersistedWorktreeOwnership contains a dirname-fallback logic bug that bypasses ownership validation for workspaces with null worktreeStoragePath.
packages/server/src/server/session.ts Adds handler for workspace.set_worktree_storage_path.request that validates the workspace, expands tilde, upserts the record, and emits an update — logic looks correct.
packages/server/src/utils/worktree.ts Adds resolvePaseoWorktreesRoot helper and extends isPaseoOwnedWorktreeCwd/listPaseoWorktrees to accept custom storage root; implementation looks correct.
packages/server/src/server/workspace-registry.ts Adds nullable optional worktreeStoragePath to PersistedWorkspaceRecordSchema with .transform fallback — properly backward compatible with existing records.
packages/server/src/server/paseo-worktree-service.ts Routes createPaseoWorktree through custom storage path resolution and propagates it to upsertWorkspaceForWorktree; logic for inheriting from source workspace looks correct.
packages/app/src/screens/project-settings-screen.tsx Adds WorktreeStorageEditor component with directory picker (Electron-only), inline save, and reset; UI pattern looks correct but depends on empty workspaceId fallback in projects.ts.
packages/app/src/utils/projects.ts Adds workspaceId and worktreeStoragePath to ProjectHostEntry; canonical?.id ?? "" produces an empty string when no canonical workspace exists, which would cause a misleading error in the settings UI.
packages/server/src/server/workspace-git-service.ts Passes worktreeStoragePath through listWorktrees and resolvePersistedWorktreeStorageRoot to worktree listing; cache key updated to include storage path, preventing stale results.

Sequence Diagram

sequenceDiagram
    participant UI as ProjectSettingsScreen
    participant Client as DaemonClient
    participant Session as Session (daemon)
    participant Registry as WorkspaceRegistry
    participant WS as WorkspaceGitService

    UI->>Client: setWorkspaceWorktreeStoragePath(workspaceId, path)
    Client->>Session: workspace.set_worktree_storage_path.request
    Session->>Registry: get(workspaceId)
    Registry-->>Session: PersistedWorkspaceRecord
    Session->>Session: expandUserPath(path) → absolute
    Session->>Registry: "upsert({ ...workspace, worktreeStoragePath })"
    Session-->>Client: workspace.set_worktree_storage_path.response
    Session->>Client: workspace_update (descriptor with worktreeStoragePath)

    Note over UI,WS: On next createWorktree / listWorktrees / archive

    Session->>Registry: list() → find source workspace
    Registry-->>Session: worktreeStoragePath
    Session->>WS: "createWorktree({ worktreeStoragePath })"
    WS->>WS: resolvePaseoWorktreesRoot(cwd, worktreeStoragePath)
    WS-->>Session: WorktreeConfig at custom path
Loading

Comments Outside Diff (1)

  1. packages/server/src/server/worktree/commands.ts, line 183-213 (link)

    P1 dirname fallback bypasses ownership check for any registered worktree

    resolvePersistedWorktreeOwnership falls through to worktreeRoot = workspace.worktreeStoragePath ?? dirname(normalizedTarget) when worktreeStoragePath is null. Because normalizedTarget always starts with dirname(normalizedTarget) + sep, the resolvedCwd.startsWith(customRootPrefix) test in isPaseoOwnedWorktreeCwd is always true, so any workspace record in the registry with kind: "worktree" and worktreeStoragePath: null will pass the ownership check even if the path is not under any Paseo-controlled directory. This is inconsistent with how create-agent-lifecycle-dispatch.ts and archive-if-safe.ts handle the same case — both pass workspace.worktreeStoragePath ?? undefined, which skips the custom-root branch entirely and lets the default check fail normally.

    The fix is to return null (skip the fallback) when worktreeStoragePath is null; only the custom-storage code path needs this override.

Reviews (1): Last reviewed commit: "feat: configure workspace worktree stora..." | Re-trigger Greptile

Comment on lines 2061 to +2083
return { customName: payload.customName };
}

async setWorkspaceWorktreeStoragePath(
workspaceId: string,
worktreeStoragePath: string | null,
requestId?: string,
): Promise<{ worktreeStoragePath: string | null }> {
const payload: WorkspaceSetWorktreeStoragePathPayload = await this.sendCorrelatedSessionRequest(
{
requestId,
message: {
type: "workspace.set_worktree_storage_path.request",
workspaceId,
worktreeStoragePath,
},
responseType: "workspace.set_worktree_storage_path.response",
timeout: 10000,
},
);
if (!payload.accepted) {
throw new Error(payload.error ?? "setWorkspaceWorktreeStoragePath rejected");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 New RPC missing daemon capability gate

setWorkspaceWorktreeStoragePath sends workspace.set_worktree_storage_path.request unconditionally. Per CLAUDE.md, any new RPC that requires a new daemon capability must be guarded by a server_info.features.* flag, with a // COMPAT(featureName): added in v0.X.Y comment marking the cleanup site. Without the gate, a new client connected to an old daemon will call this RPC, get no recognised response, and surface a timeout error in the project settings toast instead of the expected "Update the host to use this" message.

Context Used: CLAUDE.md (source)

return {
serverId: group.serverId,
serverName: group.serverName,
workspaceId: canonical?.id ?? "",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 When canonical is undefined (e.g. a project whose only workspaces are worktrees), workspaceId falls back to "". The WorktreeStorageEditor will then call setWorkspaceWorktreeStoragePath("") which the server rejects with "Workspace not found", surfacing a confusing error toast. Prefer a more explicit sentinel so the UI can decide whether to render the editor at all.

Suggested change
workspaceId: canonical?.id ?? "",
workspaceId: canonical?.id ?? null,

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@artile artile mentioned this pull request Jun 1, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant