mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Let agents rename workspaces (#1876)
* feat(agent): let agents rename workspaces * fix(agent): scope workspace rename tool * fix(agent): allow explicit workspace renames
This commit is contained in:
@@ -73,7 +73,7 @@ Anyone who builds software:
|
||||
- Built-in providers: Claude Code (Agent SDK), Codex (app-server), GitHub Copilot (ACP), OpenCode, Pi, OMP
|
||||
- One-click ACP provider catalog: CodeWhale, Cursor, Hermes, Qwen Coder, Kimi Code, and others — plus custom ACP providers
|
||||
- Voice mode: dictate prompts or talk through problems hands-free
|
||||
- MCP server exposes the daemon to other agents (create_agent, send_agent_prompt, schedules, terminals, worktrees)
|
||||
- MCP server exposes the daemon to other agents (create_agent, send_agent_prompt, schedules, terminals, worktrees, workspace renaming)
|
||||
- Scheduled agents (cron-style triggers) via app, CLI, and MCP
|
||||
- Frequent releases (multiple per week)
|
||||
- Community contributions across packaging, providers, and bug fixes
|
||||
|
||||
@@ -23,7 +23,11 @@ import {
|
||||
AgentPermissionRequestPayloadSchema,
|
||||
AgentSnapshotPayloadSchema,
|
||||
} from "@getpaseo/protocol/messages";
|
||||
import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "../workspace-registry.js";
|
||||
import {
|
||||
createPersistedWorkspaceRecord,
|
||||
type PersistedProjectRecord,
|
||||
type PersistedWorkspaceRecord,
|
||||
} from "../workspace-registry.js";
|
||||
import type {
|
||||
CreateScheduleInput,
|
||||
StoredSchedule,
|
||||
@@ -3070,6 +3074,191 @@ describe("update_agent MCP tool", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("rename_workspace MCP tool", () => {
|
||||
const logger = createTestLogger();
|
||||
|
||||
it("renames the caller workspace when workspaceId is omitted", async () => {
|
||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||
const workspace = createPersistedWorkspaceRecord({
|
||||
workspaceId: "wks_parent",
|
||||
projectId: "proj_parent",
|
||||
cwd: REPO_CWD,
|
||||
kind: "local_checkout",
|
||||
displayName: "main",
|
||||
createdAt: "2026-07-03T09:00:00.000Z",
|
||||
updatedAt: "2026-07-03T09:00:00.000Z",
|
||||
});
|
||||
const workspaces = new Map([[workspace.workspaceId, workspace]]);
|
||||
const upsertedWorkspaces: PersistedWorkspaceRecord[] = [];
|
||||
const emittedWorkspaceIds: string[][] = [];
|
||||
spies.agentManager.getAgent.mockReturnValue(
|
||||
createManagedAgent({
|
||||
id: "parent-agent",
|
||||
cwd: REPO_CWD,
|
||||
workspaceId: workspace.workspaceId,
|
||||
}),
|
||||
);
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
workspaceRegistry: {
|
||||
get: async (workspaceId) => workspaces.get(workspaceId) ?? null,
|
||||
upsert: async (record) => {
|
||||
upsertedWorkspaces.push(record);
|
||||
workspaces.set(record.workspaceId, record);
|
||||
},
|
||||
},
|
||||
emitWorkspaceUpdatesForWorkspaceIds: async (workspaceIds) => {
|
||||
emittedWorkspaceIds.push(Array.from(workspaceIds));
|
||||
},
|
||||
callerAgentId: "parent-agent",
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "rename_workspace");
|
||||
|
||||
const response = await invokeToolWithParsedInput(tool, {
|
||||
title: " Payments flow ",
|
||||
});
|
||||
|
||||
expect(upsertedWorkspaces).toEqual([
|
||||
{
|
||||
...workspace,
|
||||
title: "Payments flow",
|
||||
updatedAt: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(response.structuredContent).toEqual({
|
||||
success: true,
|
||||
workspaceId: "wks_parent",
|
||||
title: "Payments flow",
|
||||
});
|
||||
expect(emittedWorkspaceIds).toEqual([["wks_parent"]]);
|
||||
});
|
||||
|
||||
it("renames an explicit workspace outside the caller workspace", async () => {
|
||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||
const parentWorkspace = createPersistedWorkspaceRecord({
|
||||
workspaceId: "wks_parent",
|
||||
projectId: "proj_parent",
|
||||
cwd: REPO_CWD,
|
||||
kind: "local_checkout",
|
||||
displayName: "main",
|
||||
createdAt: "2026-07-03T09:00:00.000Z",
|
||||
updatedAt: "2026-07-03T09:00:00.000Z",
|
||||
});
|
||||
const otherWorkspace = createPersistedWorkspaceRecord({
|
||||
workspaceId: "wks_other",
|
||||
projectId: "proj_other",
|
||||
cwd: TARGET_CWD,
|
||||
kind: "local_checkout",
|
||||
displayName: "other",
|
||||
createdAt: "2026-07-03T09:00:00.000Z",
|
||||
updatedAt: "2026-07-03T09:00:00.000Z",
|
||||
});
|
||||
const workspaces = new Map([
|
||||
[parentWorkspace.workspaceId, parentWorkspace],
|
||||
[otherWorkspace.workspaceId, otherWorkspace],
|
||||
]);
|
||||
const upsertedWorkspaces: PersistedWorkspaceRecord[] = [];
|
||||
const emittedWorkspaceIds: string[][] = [];
|
||||
spies.agentManager.getAgent.mockReturnValue(
|
||||
createManagedAgent({
|
||||
id: "parent-agent",
|
||||
cwd: REPO_CWD,
|
||||
workspaceId: parentWorkspace.workspaceId,
|
||||
}),
|
||||
);
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
workspaceRegistry: {
|
||||
get: async (workspaceId) => workspaces.get(workspaceId) ?? null,
|
||||
upsert: async (record) => {
|
||||
upsertedWorkspaces.push(record);
|
||||
workspaces.set(record.workspaceId, record);
|
||||
},
|
||||
},
|
||||
emitWorkspaceUpdatesForWorkspaceIds: async (workspaceIds) => {
|
||||
emittedWorkspaceIds.push(Array.from(workspaceIds));
|
||||
},
|
||||
callerAgentId: "parent-agent",
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "rename_workspace");
|
||||
|
||||
const response = await invokeToolWithParsedInput(tool, {
|
||||
workspaceId: "wks_other",
|
||||
title: "Payments flow",
|
||||
});
|
||||
|
||||
expect(upsertedWorkspaces).toEqual([
|
||||
{
|
||||
...otherWorkspace,
|
||||
title: "Payments flow",
|
||||
updatedAt: expect.any(String),
|
||||
},
|
||||
]);
|
||||
expect(response.structuredContent).toEqual({
|
||||
success: true,
|
||||
workspaceId: "wks_other",
|
||||
title: "Payments flow",
|
||||
});
|
||||
expect(emittedWorkspaceIds).toEqual([["wks_other"]]);
|
||||
});
|
||||
|
||||
it("rejects archived workspaces", async () => {
|
||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||
const workspace = createPersistedWorkspaceRecord({
|
||||
workspaceId: "wks_archived",
|
||||
projectId: "proj_parent",
|
||||
cwd: REPO_CWD,
|
||||
kind: "local_checkout",
|
||||
displayName: "main",
|
||||
archivedAt: "2026-07-03T10:00:00.000Z",
|
||||
createdAt: "2026-07-03T09:00:00.000Z",
|
||||
updatedAt: "2026-07-03T10:00:00.000Z",
|
||||
});
|
||||
const workspaces = new Map([[workspace.workspaceId, workspace]]);
|
||||
const upsertedWorkspaces: PersistedWorkspaceRecord[] = [];
|
||||
const emittedWorkspaceIds: string[][] = [];
|
||||
spies.agentManager.getAgent.mockReturnValue(
|
||||
createManagedAgent({
|
||||
id: "parent-agent",
|
||||
cwd: REPO_CWD,
|
||||
workspaceId: workspace.workspaceId,
|
||||
}),
|
||||
);
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
workspaceRegistry: {
|
||||
get: async (workspaceId) => workspaces.get(workspaceId) ?? null,
|
||||
upsert: async (record) => {
|
||||
upsertedWorkspaces.push(record);
|
||||
workspaces.set(record.workspaceId, record);
|
||||
},
|
||||
},
|
||||
emitWorkspaceUpdatesForWorkspaceIds: async (workspaceIds) => {
|
||||
emittedWorkspaceIds.push(Array.from(workspaceIds));
|
||||
},
|
||||
callerAgentId: "parent-agent",
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "rename_workspace");
|
||||
|
||||
await expect(
|
||||
invokeToolWithParsedInput(tool, {
|
||||
title: "Payments flow",
|
||||
}),
|
||||
).rejects.toThrow("Workspace wks_archived is archived");
|
||||
expect(upsertedWorkspaces).toEqual([]);
|
||||
expect(emittedWorkspaceIds).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("create_schedule MCP tool", () => {
|
||||
const logger = createTestLogger();
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
} from "../lifecycle-command.js";
|
||||
import type { GitHubService } from "../../../services/github-service.js";
|
||||
import type { WorkspaceGitService } from "../../workspace-git-service.js";
|
||||
import type { WorkspaceRegistry } from "../../workspace-registry.js";
|
||||
import { WorktreeRequestError } from "../../worktree-errors.js";
|
||||
import {
|
||||
archiveCommand,
|
||||
@@ -99,6 +100,7 @@ export interface PaseoToolHostDependencies {
|
||||
listActiveWorkspaces?: ArchiveDependencies["listActiveWorkspaces"];
|
||||
archiveWorkspaceRecord?: ArchiveDependencies["archiveWorkspaceRecord"];
|
||||
emitWorkspaceUpdatesForWorkspaceIds?: ArchiveDependencies["emitWorkspaceUpdatesForWorkspaceIds"];
|
||||
workspaceRegistry?: Pick<WorkspaceRegistry, "get" | "upsert">;
|
||||
markWorkspaceArchiving?: ArchiveDependencies["markWorkspaceArchiving"];
|
||||
clearWorkspaceArchiving?: ArchiveDependencies["clearWorkspaceArchiving"];
|
||||
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
|
||||
@@ -554,6 +556,22 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
|
||||
return options.ensureWorkspaceForCreate(resolvedCwd);
|
||||
}
|
||||
|
||||
function resolveWorkspaceIdForRename(requestedWorkspaceId?: string): string {
|
||||
const explicitWorkspaceId = requestedWorkspaceId?.trim();
|
||||
if (explicitWorkspaceId) {
|
||||
return explicitWorkspaceId;
|
||||
}
|
||||
|
||||
if (callerAgentId) {
|
||||
const callerAgent = resolveCallerAgent();
|
||||
if (!callerAgent?.workspaceId) {
|
||||
throw new Error(`Caller agent ${callerAgentId} has no current workspace`);
|
||||
}
|
||||
return callerAgent.workspaceId;
|
||||
}
|
||||
throw new Error("workspaceId is required outside an agent-scoped session");
|
||||
}
|
||||
|
||||
const buildCallerAgentScheduleConfigExtras = (
|
||||
callerAgent: NonNullable<ReturnType<typeof resolveCallerAgent>>,
|
||||
): Record<string, unknown> => {
|
||||
@@ -1781,6 +1799,66 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
|
||||
},
|
||||
);
|
||||
|
||||
registerTool(
|
||||
"rename_workspace",
|
||||
{
|
||||
title: "Rename workspace",
|
||||
description:
|
||||
"Rename a workspace by setting its user-visible title. Omit workspaceId to rename your current workspace.",
|
||||
inputSchema: {
|
||||
workspaceId: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe("Workspace id to rename. Omit to rename your current workspace."),
|
||||
title: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "title is required")
|
||||
.describe("New user-visible workspace title."),
|
||||
},
|
||||
outputSchema: {
|
||||
success: z.boolean(),
|
||||
workspaceId: z.string(),
|
||||
title: z.string(),
|
||||
},
|
||||
},
|
||||
async ({ workspaceId: requestedWorkspaceId, title }) => {
|
||||
if (!options.workspaceRegistry) {
|
||||
throw new Error("Workspace registry is required to rename workspaces");
|
||||
}
|
||||
if (!options.emitWorkspaceUpdatesForWorkspaceIds) {
|
||||
throw new Error("Workspace update emitter is required to rename workspaces");
|
||||
}
|
||||
|
||||
const workspaceId = resolveWorkspaceIdForRename(requestedWorkspaceId);
|
||||
const existing = await options.workspaceRegistry.get(workspaceId);
|
||||
if (!existing) {
|
||||
throw new Error(`Workspace ${workspaceId} not found`);
|
||||
}
|
||||
if (existing.archivedAt) {
|
||||
throw new Error(`Workspace ${workspaceId} is archived`);
|
||||
}
|
||||
|
||||
await options.workspaceRegistry.upsert({
|
||||
...existing,
|
||||
title,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
await options.emitWorkspaceUpdatesForWorkspaceIds([workspaceId]);
|
||||
|
||||
return {
|
||||
content: [],
|
||||
structuredContent: ensureValidJson({
|
||||
success: true,
|
||||
workspaceId,
|
||||
title,
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
registerTool(
|
||||
"list_terminals",
|
||||
{
|
||||
|
||||
@@ -976,6 +976,7 @@ export async function createPaseoDaemon(
|
||||
listActiveWorkspaces: listActiveWorkspacesExternal,
|
||||
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
|
||||
emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
|
||||
workspaceRegistry,
|
||||
markWorkspaceArchiving: markWorkspaceArchivingExternal,
|
||||
clearWorkspaceArchiving: clearWorkspaceArchivingExternal,
|
||||
ensureWorkspaceForCreate: ensureWorkspaceForCreateExternal,
|
||||
|
||||
Reference in New Issue
Block a user