Simplify workspace creation with inline worktree API

Replace the multi-step new-agent route flow with a single
create_paseo_worktree endpoint that registers the workspace immediately
and creates the git worktree in the background. The sidebar now calls
this endpoint directly and shows a creating spinner inline.

Also auto-resolves the base branch from origin/HEAD when not explicitly
provided, removes the unused Tauri WebSocket transport layer, and
normalizes loopback endpoints to localhost.

Closes #125
This commit is contained in:
Mohamed Boudra
2026-03-19 23:33:35 +07:00
parent 3d4ac57bd0
commit 6ab97c579e
21 changed files with 514 additions and 531 deletions
@@ -1,3 +1,7 @@
import { execSync } from 'node:child_process'
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { describe, expect, test, vi } from 'vitest'
import { Session } from './session.js'
import type { AgentSnapshotPayload } from '../shared/messages.js'
@@ -335,55 +339,110 @@ describe('workspace aggregation', () => {
})
})
test('create paseo worktree request returns a registered workspace descriptor', async () => {
const emitted: Array<{ type: string; payload: unknown }> = []
const session = createSessionForWorkspaceTests() as any
const tempDir = realpathSync(mkdtempSync(path.join(tmpdir(), 'session-worktree-test-')))
const repoDir = path.join(tempDir, 'repo')
const paseoHome = path.join(tempDir, 'paseo-home')
execSync(`mkdir -p ${repoDir}`)
execSync('git init -b main', { cwd: repoDir, stdio: 'pipe' })
execSync("git config user.email 'test@test.com'", { cwd: repoDir, stdio: 'pipe' })
execSync("git config user.name 'Test'", { cwd: repoDir, stdio: 'pipe' })
writeFileSync(path.join(repoDir, 'file.txt'), 'hello\n')
execSync('git add .', { cwd: repoDir, stdio: 'pipe' })
execSync("git -c commit.gpgsign=false commit -m 'initial'", { cwd: repoDir, stdio: 'pipe' })
const workspaces = new Map()
const projects = new Map()
session.paseoHome = paseoHome
session.workspaceRegistry.get = async (workspaceId: string) => workspaces.get(workspaceId) ?? null
session.workspaceRegistry.list = async () => Array.from(workspaces.values())
session.workspaceRegistry.upsert = async (record: any) => {
workspaces.set(record.workspaceId, record)
}
session.projectRegistry.get = async (projectId: string) => projects.get(projectId) ?? null
session.projectRegistry.list = async () => Array.from(projects.values())
session.projectRegistry.upsert = async (record: any) => {
projects.set(record.projectId, record)
}
session.emit = (message: { type: string; payload: unknown }) => {
emitted.push(message)
}
try {
await session.handleCreatePaseoWorktreeRequest({
type: 'create_paseo_worktree_request',
cwd: repoDir,
worktreeSlug: 'worktree-123',
requestId: 'req-worktree',
})
} finally {
rmSync(tempDir, { recursive: true, force: true })
}
const response = emitted.find((message) => message.type === 'create_paseo_worktree_response') as
| { type: 'create_paseo_worktree_response'; payload: any }
| undefined
expect(response?.payload.error).toBeNull()
expect(response?.payload.workspace).toMatchObject({
projectDisplayName: 'repo',
projectKind: 'git',
workspaceKind: 'worktree',
name: 'worktree-123',
status: 'done',
})
expect(response?.payload.workspace?.id).toContain(path.join('worktree-123'))
expect(workspaces.has(response?.payload.workspace?.id)).toBe(true)
expect(projects.has(response?.payload.workspace?.projectId)).toBe(true)
})
test('workspace update fanout for multiple cwd values is deduplicated', async () => {
const emitted: Array<{ type: string; payload: unknown }> = []
const session = createSessionForWorkspaceTests() as any
session.emit = (message: any) => emitted.push(message)
session.workspaceUpdatesSubscription = {
subscriptionId: 'sub-dedupe',
subscriptionId: 'sub-dedup',
filter: undefined,
isBootstrapping: false,
pendingUpdatesByWorkspaceId: new Map(),
}
session.reconcileActiveWorkspaceRecords = async () => new Set()
session.reconcileActiveWorkspaceRecords = async () => new Set(['/tmp/repo', '/tmp/repo/worktree'])
session.listWorkspaceDescriptorsSnapshot = async () => [
{
id: '/tmp/repo',
projectId: '/tmp/repo',
projectDisplayName: 'repo',
projectRootPath: '/tmp/repo',
projectKind: 'non_git',
workspaceKind: 'directory',
name: 'repo',
projectKind: 'git',
workspaceKind: 'local_checkout',
name: 'main',
status: 'done',
activityAt: null,
},
{
id: '/tmp/repo/sub',
id: '/tmp/repo/worktree',
projectId: '/tmp/repo',
projectDisplayName: 'repo',
projectRootPath: '/tmp/repo',
projectKind: 'non_git',
workspaceKind: 'directory',
name: 'sub',
status: 'done',
activityAt: null,
projectKind: 'git',
workspaceKind: 'worktree',
name: 'feature',
status: 'running',
activityAt: '2026-03-01T12:00:00.000Z',
},
]
session.onMessage = (message: { type: string; payload: unknown }) => {
emitted.push(message)
}
await session.emitWorkspaceUpdatesForCwds([
'/tmp/repo',
'/tmp/repo/',
' /tmp/repo ',
'/tmp/repo/sub',
'/tmp/repo/sub/',
])
await session.emitWorkspaceUpdateForCwd('/tmp/repo/worktree')
const workspaceUpdates = emitted.filter((message) => message.type === 'workspace_update') as any[]
expect(workspaceUpdates).toHaveLength(2)
expect(workspaceUpdates.map((message) => message.payload.workspace.id)).toEqual([
expect(workspaceUpdates.map((entry) => entry.payload.kind)).toEqual(['upsert', 'upsert'])
expect(workspaceUpdates.map((entry) => entry.payload.workspace.id).sort()).toEqual([
'/tmp/repo',
'/tmp/repo/sub',
'/tmp/repo/worktree',
])
})