Show prompt titles immediately for new workspaces (#1567)
* Set workspace prompt titles at creation * Cover existing workspace prompt titles * Fix rewind E2E prompt selector
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { Locator } from "@playwright/test";
|
||||
import { expect, test, type Page } from "./fixtures";
|
||||
import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent";
|
||||
import {
|
||||
@@ -14,6 +15,14 @@ async function expectUserMessageCount(page: Page, expected: number): Promise<voi
|
||||
await expect(page.getByTestId("user-message")).toHaveCount(expected);
|
||||
}
|
||||
|
||||
function userMessage(page: Page, text: string): Locator {
|
||||
return page.getByTestId("user-message").filter({ hasText: text });
|
||||
}
|
||||
|
||||
async function expectUserMessageVisible(page: Page, text: string): Promise<void> {
|
||||
await expect(userMessage(page, text)).toBeVisible();
|
||||
}
|
||||
|
||||
test.describe("Rewind sheet", () => {
|
||||
test("rewinds from a user message sheet option", async ({ page }) => {
|
||||
const firstPrompt = "emit 1 coalesced agent stream updates for first rewind turn.";
|
||||
@@ -29,14 +38,14 @@ test.describe("Rewind sheet", () => {
|
||||
await openAgentRoute(page, session);
|
||||
await expectComposerVisible(page);
|
||||
|
||||
await expect(page.getByText(firstPrompt, { exact: true })).toBeVisible();
|
||||
await expectUserMessageVisible(page, firstPrompt);
|
||||
await expectUserMessageCount(page, 1);
|
||||
await submitMessage(page, secondPrompt);
|
||||
await expect(page.getByText(secondPrompt, { exact: true })).toBeVisible();
|
||||
await expectUserMessageVisible(page, secondPrompt);
|
||||
await expect(page.getByText("Cycle 1", { exact: true })).toBeVisible();
|
||||
await expectUserMessageCount(page, 2);
|
||||
|
||||
await page.getByText(firstPrompt, { exact: true }).hover();
|
||||
await userMessage(page, firstPrompt).hover();
|
||||
await page.getByTestId("rewind-menu-trigger").first().click();
|
||||
const rewindSheet = page.getByTestId("rewind-menu-content");
|
||||
await expect(rewindSheet).toBeVisible();
|
||||
@@ -46,20 +55,20 @@ test.describe("Rewind sheet", () => {
|
||||
await page.getByTestId("rewind-menu-conversation").click();
|
||||
|
||||
await expect(page.getByTestId("rewind-menu-content")).toHaveCount(0);
|
||||
await expect(page.getByText(secondPrompt, { exact: true })).toHaveCount(0);
|
||||
await expect(userMessage(page, secondPrompt)).toHaveCount(0);
|
||||
await expect(page.getByText("Cycle 1", { exact: true })).toHaveCount(0);
|
||||
await expectUserMessageCount(page, 1);
|
||||
await expectComposerDraft(page, firstPrompt);
|
||||
|
||||
await submitMessage(page, replacementPrompt);
|
||||
await expect(page.getByText(replacementPrompt, { exact: true })).toBeVisible();
|
||||
await expect(page.getByText(secondPrompt, { exact: true })).toHaveCount(0);
|
||||
await expectUserMessageVisible(page, replacementPrompt);
|
||||
await expect(userMessage(page, secondPrompt)).toHaveCount(0);
|
||||
await expect(page.getByText("Cycle 1", { exact: true })).toHaveCount(0);
|
||||
await expectUserMessageCount(page, 2);
|
||||
|
||||
await fillComposerDraft(page, "");
|
||||
await composerLocator(page).evaluate((element) => element.blur());
|
||||
await page.getByText(replacementPrompt, { exact: true }).hover();
|
||||
await userMessage(page, replacementPrompt).hover();
|
||||
await page.getByTestId("rewind-menu-trigger").last().click();
|
||||
await expect(page.getByTestId("rewind-menu-content")).toBeVisible();
|
||||
await page.getByTestId("rewind-menu-files").click();
|
||||
@@ -70,7 +79,7 @@ test.describe("Rewind sheet", () => {
|
||||
const preservedDraft = "Keep this human draft after rewind.";
|
||||
await fillComposerDraft(page, preservedDraft);
|
||||
await composerLocator(page).evaluate((element) => element.blur());
|
||||
await page.getByText(replacementPrompt, { exact: true }).hover();
|
||||
await userMessage(page, replacementPrompt).hover();
|
||||
await page.getByTestId("rewind-menu-trigger").last().click();
|
||||
await expect(page.getByTestId("rewind-menu-content")).toBeVisible();
|
||||
await page.getByTestId("rewind-menu-files").click();
|
||||
@@ -80,7 +89,7 @@ test.describe("Rewind sheet", () => {
|
||||
|
||||
await fillComposerDraft(page, "");
|
||||
await composerLocator(page).evaluate((element) => element.blur());
|
||||
await page.getByText(replacementPrompt, { exact: true }).hover();
|
||||
await userMessage(page, replacementPrompt).hover();
|
||||
await page.getByTestId("rewind-menu-trigger").last().click();
|
||||
await expect(page.getByTestId("rewind-menu-content")).toBeVisible();
|
||||
await page.getByTestId("rewind-menu-both").click();
|
||||
@@ -108,9 +117,9 @@ test.describe("Rewind sheet", () => {
|
||||
await openAgentRoute(page, session);
|
||||
await expectComposerVisible(page);
|
||||
|
||||
await expect(page.getByText(firstPrompt, { exact: true })).toBeVisible();
|
||||
await expectUserMessageVisible(page, firstPrompt);
|
||||
|
||||
await page.getByText(firstPrompt, { exact: true }).hover();
|
||||
await userMessage(page, firstPrompt).hover();
|
||||
await page.getByTestId("rewind-menu-trigger").first().click();
|
||||
const rewindSheet = page.getByTestId("rewind-menu-content");
|
||||
await expect(rewindSheet).toBeVisible();
|
||||
@@ -122,7 +131,7 @@ test.describe("Rewind sheet", () => {
|
||||
await expect(page.getByTestId("app-toast-message")).toHaveText(rewindError);
|
||||
await expect(page.getByText("Uncaught Error")).toHaveCount(0);
|
||||
|
||||
await page.getByText(firstPrompt, { exact: true }).hover();
|
||||
await userMessage(page, firstPrompt).hover();
|
||||
await page.getByTestId("rewind-menu-trigger").first().click();
|
||||
await expect(page.getByTestId("rewind-menu-content")).toBeVisible();
|
||||
} finally {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "@getpaseo/protocol/agent-title-limits";
|
||||
import type { FirstAgentContext } from "@getpaseo/protocol/messages";
|
||||
|
||||
const MAX_INITIAL_AGENT_TITLE_CHARS = Math.min(60, MAX_EXPLICIT_AGENT_TITLE_CHARS);
|
||||
|
||||
@@ -35,3 +36,11 @@ export function resolveCreateAgentTitles(options: {
|
||||
provisionalTitle,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveFirstAgentPromptTitle(firstAgentContext?: FirstAgentContext): string | null {
|
||||
return (
|
||||
resolveCreateAgentTitles({
|
||||
initialPrompt: firstAgentContext?.prompt,
|
||||
}).provisionalTitle ?? null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
writePaseoWorktreeFirstAgentBranchAutoNameMetadata,
|
||||
} from "../utils/worktree-metadata.js";
|
||||
import type { WorktreeCreationIntent } from "./resolve-worktree-creation-intent.js";
|
||||
import { resolveFirstAgentPromptTitle } from "./agent/create-agent-title.js";
|
||||
import { buildAgentBranchNameSeed } from "./agent/prompt-attachments.js";
|
||||
import type { FirstAgentContext } from "@getpaseo/protocol/messages";
|
||||
|
||||
@@ -70,6 +71,7 @@ export async function createPaseoWorktree(
|
||||
projectId: input.projectId,
|
||||
repoRoot: createdWorktree.repoRoot,
|
||||
worktree: createdWorktree.worktree,
|
||||
title: resolveFirstAgentPromptTitle(input.firstAgentContext),
|
||||
deps,
|
||||
});
|
||||
|
||||
@@ -200,6 +202,7 @@ async function upsertWorkspaceForWorktree(options: {
|
||||
projectId?: string;
|
||||
repoRoot: string;
|
||||
worktree: WorktreeConfig;
|
||||
title?: string | null;
|
||||
deps: Pick<
|
||||
CreatePaseoWorktreeDeps,
|
||||
"projectRegistry" | "workspaceRegistry" | "workspaceGitService"
|
||||
@@ -241,6 +244,7 @@ async function upsertWorkspaceForWorktree(options: {
|
||||
kind: "worktree",
|
||||
displayName: options.worktree.branchName || normalizedCwd,
|
||||
branch: options.worktree.branchName || null,
|
||||
title: options.title ?? null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
archivedAt: null,
|
||||
|
||||
@@ -73,7 +73,10 @@ import {
|
||||
waitForAgentRunStartWithTimeout,
|
||||
unarchiveAgentState,
|
||||
} from "./agent/agent-prompt.js";
|
||||
import { resolveCreateAgentTitles } from "./agent/create-agent-title.js";
|
||||
import {
|
||||
resolveCreateAgentTitles,
|
||||
resolveFirstAgentPromptTitle,
|
||||
} from "./agent/create-agent-title.js";
|
||||
import { respondToAgentPermission } from "./agent/permission-response.js";
|
||||
import { experimental_createMCPClient } from "ai";
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
@@ -3234,6 +3237,7 @@ export class Session {
|
||||
...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
|
||||
...(attachments && attachments.length > 0 ? { attachments } : {}),
|
||||
};
|
||||
const workspacePromptTitle = resolveFirstAgentPromptTitle(firstAgentContext);
|
||||
const createdWorktree = await this.createAgentLifecycleDispatch.createWorktreeForRequest({
|
||||
cwd: config.cwd,
|
||||
target: worktree,
|
||||
@@ -3244,13 +3248,12 @@ export class Session {
|
||||
const createAgentConfig: AgentSessionConfig = createdWorktree
|
||||
? { ...config, cwd: createdWorktree.worktree.worktreePath }
|
||||
: config;
|
||||
// Ownership comes from an explicit id (worktree or request). An agent
|
||||
// created with no explicit workspace mints a fresh one — we never resolve
|
||||
// an existing workspace by cwd, because many workspaces may share a cwd.
|
||||
const workspaceId =
|
||||
createdWorktree?.workspace.workspaceId ??
|
||||
msg.workspaceId ??
|
||||
(await this.createWorkspaceForDirectory(createAgentConfig.cwd)).workspaceId;
|
||||
const workspaceId = await this.resolveOrCreateWorkspaceIdForCreateAgent({
|
||||
createdWorktree,
|
||||
requestedWorkspaceId: msg.workspaceId,
|
||||
cwd: createAgentConfig.cwd,
|
||||
initialTitle: workspacePromptTitle,
|
||||
});
|
||||
|
||||
const { snapshot, liveSnapshot } = await createAgentCommand(
|
||||
{
|
||||
@@ -3281,6 +3284,9 @@ export class Session {
|
||||
},
|
||||
);
|
||||
createdAgentId = snapshot.id;
|
||||
if (!createdWorktree && msg.workspaceId) {
|
||||
await this.writeInitialWorkspaceTitleIfUntitled(workspaceId, workspacePromptTitle);
|
||||
}
|
||||
await this.forwardAgentUpdate(snapshot);
|
||||
if (!createdWorktree && trimmedPrompt) {
|
||||
await this.scheduleAutoNameLocalWorkspaceTitleForFirstAgent({
|
||||
@@ -3711,31 +3717,52 @@ export class Session {
|
||||
await this.applyGeneratedWorkspaceTitle(input.workspace.workspaceId, {
|
||||
title: generatedTitle,
|
||||
branch: result.branchName,
|
||||
promptTitle: resolveFirstAgentPromptTitle(input.firstAgentContext),
|
||||
});
|
||||
await this.notifyGitMutation(input.workspace.cwd, "rename-branch");
|
||||
await this.emitWorkspaceUpdateForCwd(input.workspace.cwd);
|
||||
}
|
||||
|
||||
// applyGeneratedWorkspaceTitle fills the generated title only while the
|
||||
// workspace is still untitled. It re-reads the current record from the
|
||||
// registry so concurrent upserts that happened after workspace creation are
|
||||
// not clobbered, while still persisting branch metadata from the rename path.
|
||||
// Generated names may replace the prompt title set at creation, but not a user
|
||||
// rename that landed while the async generator was running.
|
||||
private async applyGeneratedWorkspaceTitle(
|
||||
workspaceId: string,
|
||||
input: { title: string; branch?: string | null },
|
||||
input: { title: string; branch?: string | null; promptTitle?: string | null },
|
||||
): Promise<void> {
|
||||
const current = await this.workspaceRegistry.get(workspaceId);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
let title = current.title;
|
||||
if (!title || (input.promptTitle && title === input.promptTitle)) {
|
||||
title = input.title;
|
||||
}
|
||||
await this.workspaceRegistry.upsert({
|
||||
...current,
|
||||
title: current.title || input.title,
|
||||
title,
|
||||
...(input.branch ? { branch: input.branch } : {}),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
private async writeInitialWorkspaceTitleIfUntitled(
|
||||
workspaceId: string,
|
||||
title: string | null,
|
||||
): Promise<void> {
|
||||
if (!title) {
|
||||
return;
|
||||
}
|
||||
const current = await this.workspaceRegistry.get(workspaceId);
|
||||
if (!current || current.title) {
|
||||
return;
|
||||
}
|
||||
await this.workspaceRegistry.upsert({
|
||||
...current,
|
||||
title,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Wraps the injected workspace-name generator for a directory workspace.
|
||||
private async generateWorkspaceTitleFromContext(input: {
|
||||
cwd: string;
|
||||
@@ -3754,8 +3781,7 @@ export class Session {
|
||||
}
|
||||
|
||||
// Generates a human title for a directory workspace from the firstAgentContext
|
||||
// prompt and writes it as displayName. No branch rename — directory workspaces
|
||||
// have no worktree git state.
|
||||
// prompt. No branch rename — directory workspaces have no worktree git state.
|
||||
// TODO(K7): same-dir directory-workspace display disambiguation not yet implemented.
|
||||
private async maybeAutoNameDirectoryWorkspaceTitle(input: {
|
||||
workspaceId: string;
|
||||
@@ -3772,7 +3798,10 @@ export class Session {
|
||||
}
|
||||
// K4: applyGeneratedWorkspaceTitle re-reads from the registry before writing.
|
||||
// Directory workspaces have no branch — write only the title.
|
||||
await this.applyGeneratedWorkspaceTitle(input.workspaceId, { title });
|
||||
await this.applyGeneratedWorkspaceTitle(input.workspaceId, {
|
||||
title,
|
||||
promptTitle: resolveFirstAgentPromptTitle(input.firstAgentContext),
|
||||
});
|
||||
await this.emitWorkspaceUpdateForWorkspaceId(input.workspaceId);
|
||||
}
|
||||
|
||||
@@ -6910,7 +6939,27 @@ export class Session {
|
||||
return this.createWorkspaceForDirectory(normalizedCwd);
|
||||
}
|
||||
|
||||
private async createWorkspaceForDirectory(cwd: string): Promise<PersistedWorkspaceRecord> {
|
||||
private async resolveOrCreateWorkspaceIdForCreateAgent(input: {
|
||||
createdWorktree: CreatePaseoWorktreeWorkflowResult | null;
|
||||
requestedWorkspaceId?: string;
|
||||
cwd: string;
|
||||
initialTitle: string | null;
|
||||
}): Promise<string> {
|
||||
if (input.createdWorktree) {
|
||||
return input.createdWorktree.workspace.workspaceId;
|
||||
}
|
||||
|
||||
if (input.requestedWorkspaceId) {
|
||||
return input.requestedWorkspaceId;
|
||||
}
|
||||
|
||||
return (await this.createWorkspaceForDirectory(input.cwd, input.initialTitle)).workspaceId;
|
||||
}
|
||||
|
||||
private async createWorkspaceForDirectory(
|
||||
cwd: string,
|
||||
title?: string | null,
|
||||
): Promise<PersistedWorkspaceRecord> {
|
||||
const checkout = await this.workspaceGitService.getCheckout(cwd);
|
||||
const membership = classifyDirectoryForProjectMembership({ cwd, checkout });
|
||||
const timestamp = new Date().toISOString();
|
||||
@@ -6927,6 +6976,7 @@ export class Session {
|
||||
cwd,
|
||||
kind: membership.workspaceKind,
|
||||
displayName: membership.workspaceDisplayName,
|
||||
title: title ?? null,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
@@ -7588,8 +7638,10 @@ export class Session {
|
||||
return;
|
||||
}
|
||||
|
||||
const explicitTitle = request.title?.trim() || null;
|
||||
const promptTitle = resolveFirstAgentPromptTitle(request.firstAgentContext);
|
||||
const workspace = await createLocalCheckoutWorkspace(
|
||||
{ cwd, title: request.title ?? null },
|
||||
{ cwd, title: explicitTitle ?? promptTitle },
|
||||
{
|
||||
projectRegistry: this.projectRegistry,
|
||||
workspaceRegistry: this.workspaceRegistry,
|
||||
|
||||
@@ -132,7 +132,7 @@ interface SessionTestAccess {
|
||||
emitWorkspaceUpdatesForWorkspaceIds(...args: unknown[]): Promise<unknown>;
|
||||
applyGeneratedWorkspaceTitle(
|
||||
workspaceId: string,
|
||||
input: { title: string; branch?: string | null },
|
||||
input: { title: string; branch?: string | null; promptTitle?: string | null },
|
||||
): Promise<void>;
|
||||
emit(message: unknown): void;
|
||||
onMessage(message: unknown): void;
|
||||
@@ -791,6 +791,122 @@ test("create_agent_request keeps requested child cwd when grouped under an exist
|
||||
}
|
||||
});
|
||||
|
||||
test("create_agent_request writes the first prompt title onto an untitled existing workspace", async () => {
|
||||
const workdir = mkdtempSync(path.join(tmpdir(), "paseo-create-agent-existing-title-"));
|
||||
try {
|
||||
const cwd = path.join(workdir, "repo");
|
||||
mkdirSync(cwd, { recursive: true });
|
||||
|
||||
const logger = {
|
||||
child: () => logger,
|
||||
trace: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
const agentStorage = new AgentStorage(path.join(workdir, "agents"), asSessionLogger(logger));
|
||||
const agentManager = new AgentManager({
|
||||
clients: { codex: new CreateAgentTestClient() },
|
||||
registry: agentStorage,
|
||||
logger: asSessionLogger(logger),
|
||||
idFactory: () => "00000000-0000-4000-8000-000000000552",
|
||||
});
|
||||
const projectRegistry = new FileBackedProjectRegistry(
|
||||
path.join(workdir, "projects.json"),
|
||||
asSessionLogger(logger),
|
||||
);
|
||||
const workspaceRegistry = new FileBackedWorkspaceRegistry(
|
||||
path.join(workdir, "workspaces.json"),
|
||||
asSessionLogger(logger),
|
||||
);
|
||||
|
||||
await projectRegistry.upsert(
|
||||
createPersistedProjectRecord({
|
||||
projectId: "proj-existing",
|
||||
rootPath: cwd,
|
||||
kind: "git",
|
||||
displayName: "repo",
|
||||
createdAt: "2026-05-07T00:00:00.000Z",
|
||||
updatedAt: "2026-05-07T00:00:00.000Z",
|
||||
}),
|
||||
);
|
||||
await workspaceRegistry.upsert(
|
||||
createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-existing",
|
||||
projectId: "proj-existing",
|
||||
cwd,
|
||||
kind: "local_checkout",
|
||||
displayName: "repo",
|
||||
title: null,
|
||||
createdAt: "2026-05-07T00:00:00.000Z",
|
||||
updatedAt: "2026-05-07T00:00:00.000Z",
|
||||
}),
|
||||
);
|
||||
|
||||
const session = asTestSession(
|
||||
new Session({
|
||||
clientId: "test-client",
|
||||
appVersion: null,
|
||||
onMessage: vi.fn(),
|
||||
logger: asSessionLogger(logger),
|
||||
downloadTokenStore: asDownloadTokenStore(),
|
||||
pushTokenStore: asPushTokenStore(),
|
||||
paseoHome: path.join(workdir, "paseo-home"),
|
||||
agentManager,
|
||||
agentStorage,
|
||||
projectRegistry,
|
||||
workspaceRegistry,
|
||||
chatService: asChatService(),
|
||||
scheduleService: asScheduleService(),
|
||||
loopService: asLoopService(),
|
||||
checkoutDiffManager: asCheckoutDiffManager({
|
||||
subscribe: async () => ({
|
||||
initial: { cwd, files: [], error: null },
|
||||
unsubscribe: () => {},
|
||||
}),
|
||||
scheduleRefreshForCwd: () => {},
|
||||
onWorkspaceStateMayHaveChanged: () => {},
|
||||
getMetrics: () => ({
|
||||
checkoutDiffTargetCount: 0,
|
||||
checkoutDiffSubscriptionCount: 0,
|
||||
checkoutDiffWatcherCount: 0,
|
||||
checkoutDiffFallbackRefreshTargetCount: 0,
|
||||
}),
|
||||
dispose: () => {},
|
||||
}),
|
||||
workspaceGitService: createNoopWorkspaceGitService(),
|
||||
daemonConfigStore: asDaemonConfigStore({
|
||||
get: () => ({ mcp: { injectIntoAgents: false }, providers: {} }),
|
||||
onChange: () => () => {},
|
||||
}),
|
||||
mcpBaseUrl: null,
|
||||
stt: null,
|
||||
tts: null,
|
||||
providerSnapshotManager: createProviderSnapshotManagerStub().manager,
|
||||
terminalManager: null,
|
||||
}),
|
||||
);
|
||||
|
||||
await session.handleMessage({
|
||||
type: "create_agent_request",
|
||||
requestId: "req-create-existing-title",
|
||||
workspaceId: "ws-existing",
|
||||
config: { provider: "codex", cwd },
|
||||
initialPrompt: "Fix login bug\nwith better validation",
|
||||
attachments: [],
|
||||
});
|
||||
|
||||
const [createdAgent] = agentManager.listAgents();
|
||||
expect(createdAgent?.workspaceId).toBe("ws-existing");
|
||||
await expect(workspaceRegistry.get("ws-existing")).resolves.toMatchObject({
|
||||
title: "Fix login bug",
|
||||
});
|
||||
} finally {
|
||||
rmSync(workdir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("unsupported persisted agents are excluded from active lists but preserved in history payloads", async () => {
|
||||
const session = createSessionForWorkspaceTests({ appVersion: "0.1.45" });
|
||||
const storedRecord = {
|
||||
@@ -6266,6 +6382,46 @@ test("applyGeneratedWorkspaceTitle writes branch metadata and does not clobber c
|
||||
expect(saved?.title).toBe("User-set title");
|
||||
});
|
||||
|
||||
test("applyGeneratedWorkspaceTitle replaces the unchanged prompt title", async () => {
|
||||
const session = createSessionForWorkspaceTests();
|
||||
const workspace = createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-prompt-title",
|
||||
projectId: "proj-prompt-title",
|
||||
cwd: REPO_CWD,
|
||||
kind: "directory",
|
||||
displayName: "repo",
|
||||
title: "Fix login bug",
|
||||
createdAt: "2026-03-01T12:00:00.000Z",
|
||||
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||
});
|
||||
const stored = new Map([[workspace.workspaceId, workspace]]);
|
||||
session.workspaceRegistry.get = async (id: string) => stored.get(id) ?? null;
|
||||
session.workspaceRegistry.upsert = async (record: unknown) => {
|
||||
const parsed = record as typeof workspace;
|
||||
stored.set(parsed.workspaceId, parsed);
|
||||
};
|
||||
|
||||
await session.applyGeneratedWorkspaceTitle(workspace.workspaceId, {
|
||||
title: "Generated login fix",
|
||||
promptTitle: "Fix login bug",
|
||||
});
|
||||
|
||||
expect(stored.get(workspace.workspaceId)?.title).toBe("Generated login fix");
|
||||
|
||||
stored.set(workspace.workspaceId, {
|
||||
...workspace,
|
||||
title: "User rename",
|
||||
updatedAt: "2026-03-01T12:01:00.000Z",
|
||||
});
|
||||
|
||||
await session.applyGeneratedWorkspaceTitle(workspace.workspaceId, {
|
||||
title: "Generated login fix",
|
||||
promptTitle: "Fix login bug",
|
||||
});
|
||||
|
||||
expect(stored.get(workspace.workspaceId)?.title).toBe("User rename");
|
||||
});
|
||||
|
||||
// Phase 7: branch is a git fact derived per-descriptor from each workspace's own
|
||||
// live git snapshot, and reconciliation re-persists `branch` per workspace from
|
||||
// its own cwd. handleCheckoutRenameBranchRequest renames the git branch and
|
||||
@@ -6327,3 +6483,42 @@ test("checkout.rename_branch.request renames the branch without a denormalized b
|
||||
expect(persisted?.displayName).toBe("Refactor auth flow");
|
||||
expect(persisted?.title).toBe("Refactor auth flow");
|
||||
});
|
||||
|
||||
test("workspace.create.response persists the first prompt as the initial title", async () => {
|
||||
const emitted: SessionOutboundMessage[] = [];
|
||||
const workspaces = new Map<string, ReturnType<typeof createPersistedWorkspaceRecord>>();
|
||||
const session = createSessionForWorkspaceTests({
|
||||
onMessage: (message) => emitted.push(message),
|
||||
workspaceRegistry: {
|
||||
initialize: async () => {},
|
||||
existsOnDisk: async () => true,
|
||||
list: async () => Array.from(workspaces.values()),
|
||||
get: async (workspaceId: string) => workspaces.get(workspaceId) ?? null,
|
||||
upsert: async (workspace) => {
|
||||
workspaces.set(workspace.workspaceId, workspace);
|
||||
},
|
||||
archive: async () => {},
|
||||
remove: async () => {},
|
||||
},
|
||||
});
|
||||
session.listAgentPayloads = async () => [];
|
||||
|
||||
await session.handleMessage({
|
||||
type: "workspace.create.request",
|
||||
requestId: "req-create-first-prompt",
|
||||
source: { kind: "directory", path: REPO_CWD },
|
||||
firstAgentContext: {
|
||||
prompt: "Add retries to the payments flow\nwith exponential backoff",
|
||||
},
|
||||
});
|
||||
|
||||
const response = findByType(emitted, "workspace.create.response");
|
||||
expect(response?.payload.error).toBeNull();
|
||||
expect(response?.payload.workspace?.title).toBe("Add retries to the payments flow");
|
||||
expect(response?.payload.workspace?.name).toBe("Add retries to the payments flow");
|
||||
|
||||
const workspaceId = response?.payload.workspace?.id;
|
||||
expect(workspaceId).toBeDefined();
|
||||
const persisted = await session.workspaceRegistry.get(workspaceId as string);
|
||||
expect(persisted?.title).toBe("Add retries to the payments flow");
|
||||
});
|
||||
|
||||
@@ -257,9 +257,9 @@ export function createPersistedWorkspaceRecord(input: {
|
||||
});
|
||||
}
|
||||
|
||||
// The single workspace-name rule: the user-set title always wins; otherwise fall
|
||||
// back to the freshest available derived display name (a live branch snapshot when
|
||||
// the caller has one, the persisted displayName otherwise).
|
||||
// The single workspace-name rule: the title always wins; otherwise fall back to
|
||||
// the freshest available derived display name (a live branch snapshot when the
|
||||
// caller has one, the persisted displayName otherwise).
|
||||
export function resolveWorkspaceName(input: {
|
||||
title: string | null;
|
||||
derivedDisplayName: string;
|
||||
|
||||
Reference in New Issue
Block a user