Open an existing project instead of creating a same-named twin (#189)
Creating a project whose name a member already uses (from the dialog, the agent's create_space tool or the onboarding scan) now returns that project, so the sidebar no longer lists two projects with one name. The conversation picker lists the main agent first. Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5cf9d679b4
commit
1c8d13a837
@ -39,6 +39,25 @@ test("conversations and projects stay separate; + opens bot picker", async ({ pa
|
|||||||
await page.waitForURL(/\/app/);
|
await page.waitForURL(/\/app/);
|
||||||
await expect(projekte.getByText("Side project", { exact: true })).toBeVisible();
|
await expect(projekte.getByText("Side project", { exact: true })).toBeVisible();
|
||||||
await expect(unterhaltungenOnly(page).getByText("Side project", { exact: true })).toHaveCount(0);
|
await expect(unterhaltungenOnly(page).getByText("Side project", { exact: true })).toHaveCount(0);
|
||||||
|
|
||||||
|
// The main agent leads the picker, even after other bots exist.
|
||||||
|
await unterhaltungen.getByTestId("conversation-picker-trigger").click();
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByTestId("conversation-bot-picker")
|
||||||
|
.locator("[data-testid^='conversation-picker-bot-']")
|
||||||
|
.first(),
|
||||||
|
).toHaveText("Chief");
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
|
||||||
|
// Creating a project that already exists opens it instead of listing it twice.
|
||||||
|
await projekte.getByTestId("create-project-trigger").click();
|
||||||
|
const again = page.getByRole("dialog", { name: "New project" });
|
||||||
|
await again.getByLabel("Name").fill("side project");
|
||||||
|
await again.getByRole("button", { name: "Create project", exact: true }).click();
|
||||||
|
await expect(again).toBeHidden();
|
||||||
|
await expect(projekte.getByText("Side project", { exact: true })).toHaveCount(1);
|
||||||
|
await expect(projekte.getByText("side project", { exact: true })).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("chat header shows avatar once and composer uses generic placeholder", async ({ page }) => {
|
test("chat header shows avatar once and composer uses generic placeholder", async ({ page }) => {
|
||||||
|
|||||||
@ -26,9 +26,15 @@ export function ConversationBotPicker({
|
|||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const needle = query.trim().toLowerCase();
|
const needle = query.trim().toLowerCase();
|
||||||
const matched = useMemo(() => {
|
const matched = useMemo(() => {
|
||||||
if (!needle) return bots;
|
const found = needle
|
||||||
return bots.filter(
|
? bots.filter(
|
||||||
(bot) => bot.name.toLowerCase().includes(needle) || bot.title.toLowerCase().includes(needle),
|
(bot) =>
|
||||||
|
bot.name.toLowerCase().includes(needle) || bot.title.toLowerCase().includes(needle),
|
||||||
|
)
|
||||||
|
: bots;
|
||||||
|
// The main agent is the usual person to talk to, so it leads the list.
|
||||||
|
return [...found].sort(
|
||||||
|
(left, right) => Number(right.kind === "main") - Number(left.kind === "main"),
|
||||||
);
|
);
|
||||||
}, [bots, needle]);
|
}, [bots, needle]);
|
||||||
const createLabel = t`Create new Bot`;
|
const createLabel = t`Create new Bot`;
|
||||||
|
|||||||
@ -80,6 +80,28 @@ describePostgres("main agent and main conversation (PostgreSQL)", () => {
|
|||||||
expect(await findMainBotId(prisma, userId)).toBe(bots[0]?.id);
|
expect(await findMainBotId(prisma, userId)).toBe(bots[0]?.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens an existing project instead of creating a second one with the same name", async () => {
|
||||||
|
const { userId, homeSpaceId } = await newUser();
|
||||||
|
const first = await createSpaceForMember(prisma, {
|
||||||
|
currentSpaceId: homeSpaceId,
|
||||||
|
userId,
|
||||||
|
name: "evOo",
|
||||||
|
});
|
||||||
|
const again = await createSpaceForMember(prisma, {
|
||||||
|
currentSpaceId: homeSpaceId,
|
||||||
|
userId,
|
||||||
|
name: " EVOO ",
|
||||||
|
});
|
||||||
|
expect(again).toEqual(first);
|
||||||
|
const home = await prisma.space.findUniqueOrThrow({ where: { id: homeSpaceId } });
|
||||||
|
const named = await createSpaceForMember(prisma, {
|
||||||
|
currentSpaceId: homeSpaceId,
|
||||||
|
userId,
|
||||||
|
name: home.name,
|
||||||
|
});
|
||||||
|
expect(named.id).not.toBe(homeSpaceId);
|
||||||
|
});
|
||||||
|
|
||||||
it("gives every project its own main bot and main conversation next to the home one", async () => {
|
it("gives every project its own main bot and main conversation next to the home one", async () => {
|
||||||
const { userId, homeSpaceId } = await newUser();
|
const { userId, homeSpaceId } = await newUser();
|
||||||
const home = await ensureMainConversation(prisma, userId);
|
const home = await ensureMainConversation(prisma, userId);
|
||||||
|
|||||||
@ -202,7 +202,7 @@ export async function createSpaceForMember(
|
|||||||
const spaceMembershipId = randomUUID();
|
const spaceMembershipId = randomUUID();
|
||||||
const createdAt = new Date();
|
const createdAt = new Date();
|
||||||
|
|
||||||
await withTransactionRetry(() =>
|
const space = await withTransactionRetry(() =>
|
||||||
prisma.$transaction(
|
prisma.$transaction(
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const currentMembership = await tx.spaceMember.findUnique({
|
const currentMembership = await tx.spaceMember.findUnique({
|
||||||
@ -215,6 +215,19 @@ export async function createSpaceForMember(
|
|||||||
select: { organizationId: true },
|
select: { organizationId: true },
|
||||||
});
|
});
|
||||||
if (!currentMembership) throw new IsolationError();
|
if (!currentMembership) throw new IsolationError();
|
||||||
|
// Asking for a project that already exists opens it instead of adding a second one.
|
||||||
|
const existing = await tx.space.findFirst({
|
||||||
|
where: {
|
||||||
|
organizationId: currentMembership.organizationId,
|
||||||
|
name: { equals: name, mode: "insensitive" },
|
||||||
|
isDefault: false,
|
||||||
|
deletingAt: null,
|
||||||
|
memberships: { some: { userId: input.userId } },
|
||||||
|
},
|
||||||
|
select: { id: true, name: true },
|
||||||
|
orderBy: { createdAt: "asc" },
|
||||||
|
});
|
||||||
|
if (existing) return existing;
|
||||||
const count = await tx.spaceMember.count({
|
const count = await tx.spaceMember.count({
|
||||||
where: {
|
where: {
|
||||||
userId: input.userId,
|
userId: input.userId,
|
||||||
@ -241,12 +254,13 @@ export async function createSpaceForMember(
|
|||||||
userId: input.userId,
|
userId: input.userId,
|
||||||
createdAt,
|
createdAt,
|
||||||
});
|
});
|
||||||
|
return { id: spaceId, name };
|
||||||
},
|
},
|
||||||
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
|
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { id: spaceId, name };
|
return space;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmptySpaceDeleteInput = {
|
type EmptySpaceDeleteInput = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user