Fix red main: mobile group kind, i18n catalogs, project picker check (#218)

* Fix red main: mobile group kind, i18n catalogs, project picker check

Main CI failed on typecheck, unit tests and web E2E after the last two
direct pushes. MobileGroup now carries the optional chat kind the mobile
screens read, the Lingui catalogs are re-extracted with German strings
for the new voice texts, the project overview plus icon meets the UI
scale guard, and the sidebar E2E expects the renamed project lead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8L4XgqjAemFR2remB5hVY

* Format files merged from main

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wy1md9QE1ayJbDCwJ4vAQs

* Align E2E with the canvas close button and per-chat drafts

The in-app browser now closes from the side panel header, and each chat
keeps its own unsent draft. The group chat test waits for the settings
save to land before switching chats, so the switch no longer races the
panel closing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wy1md9QE1ayJbDCwJ4vAQs

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
cynarAI 2026-10-20 09:34:40 +02:00 committed by GitHub
parent bb7cf3f55e
commit 263e3a31f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 23 additions and 24 deletions

View File

@ -2144,9 +2144,7 @@ describe("stopThreadRuns", () => {
updateMany: vi.fn().mockResolvedValue({ count: 1 }), updateMany: vi.fn().mockResolvedValue({ count: 1 }),
}, },
run: { run: {
updateManyAndReturn: vi updateManyAndReturn: vi.fn().mockResolvedValue([{ id: "run-old", taskId: "task-old" }]),
.fn()
.mockResolvedValue([{ id: "run-old", taskId: "task-old" }]),
create: vi.fn().mockResolvedValue({ id: "run-new", status: "queued" }), create: vi.fn().mockResolvedValue({ id: "run-new", status: "queued" }),
}, },
task: { task: {
@ -2230,4 +2228,3 @@ describe("stopThreadRuns", () => {
expect(events.notify).toHaveBeenCalledWith("thread-1", 42); expect(events.notify).toHaveBeenCalledWith("thread-1", 42);
}); });
}); });

View File

@ -1424,9 +1424,7 @@ export async function sendQueuedThreadMessagesNow(
status: "queued", status: "queued",
}, },
}); });
const runCreatedAt = new Date( const runCreatedAt = new Date(Math.max(Date.now(), last.message.createdAt.getTime() + 1));
Math.max(Date.now(), last.message.createdAt.getTime() + 1),
);
const run = await tx.run.create({ const run = await tx.run.create({
data: { data: {
spaceId: actor.spaceId, spaceId: actor.spaceId,

View File

@ -1660,9 +1660,7 @@ function Thread() {
marginTop: 4, marginTop: 4,
}} }}
> >
<Text style={{ color: tokens.mutedForeground, fontSize: 12 }}> <Text style={{ color: tokens.mutedForeground, fontSize: 12 }}>{t("Queued")}</Text>
{t("Queued")}
</Text>
<Pressable <Pressable
accessibilityRole="button" accessibilityRole="button"
disabled={sendingNowId === message.id} disabled={sendingNowId === message.id}

View File

@ -169,18 +169,26 @@ test("create group from + and see two bots in one transcript", async ({ page },
await expect(desktopSettings.getByRole("button", { name: "Save", exact: true })).toBeEnabled(); await expect(desktopSettings.getByRole("button", { name: "Save", exact: true })).toBeEnabled();
await page.unroute("**/rpc/groups/update"); await page.unroute("**/rpc/groups/update");
await desktopSettings.getByRole("button", { name: "Save", exact: true }).click(); await desktopSettings.getByRole("button", { name: "Save", exact: true }).click();
// The panel closes once the save lands; switching chats before that races the layout change.
await expect(desktopSettings).toHaveAttribute("data-panel", "closed");
await page.getByRole("combobox", { name: "Message" }).fill("@Researcher unfinished draft"); await page.getByRole("combobox", { name: "Message" }).fill("@Researcher unfinished draft");
await projekte await projekte
.getByRole("button", { name: /^Review team/ }) .getByRole("button", { name: /^Review team/ })
.first() .first()
.click(); .click();
await expect(page).toHaveURL(new RegExp(`/app/g/${reviewGroup.id}`));
await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue(""); await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue("");
// Each chat keeps its own unsent draft.
await projekte await projekte
.getByRole("button", { name: /^Draft team/ }) .getByRole("button", { name: /^Draft team/ })
.first() .first()
.click(); .click();
await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue(""); await expect(page).toHaveURL(new RegExp(`/app/g/${draftGroupId}`));
await expect(page.getByRole("combobox", { name: "Message" })).toHaveValue(
"@Researcher unfinished draft",
);
await page.getByRole("combobox", { name: "Message" }).fill("");
const composer = page.getByRole("combobox", { name: "Message" }); const composer = page.getByRole("combobox", { name: "Message" });
await composer.fill("@Res"); await composer.fill("@Res");

View File

@ -39,7 +39,7 @@ test("the in-app browser previews a page and follows it back to the browser", as
await browser.getByRole("button", { name: "Open in browser" }).click(); await browser.getByRole("button", { name: "Open in browser" }).click();
expect((await popup).url()).toBe("https://preview.example/app"); expect((await popup).url()).toBe("https://preview.example/app");
await browser.getByRole("button", { name: "Close panel" }).click(); await page.getByTestId("side-panel").getByRole("button", { name: "Close panel" }).click();
await expect(browser).toHaveCount(0); await expect(browser).toHaveCount(0);
// The last address comes back when the browser reopens. // The last address comes back when the browser reopens.
await page.getByRole("button", { name: "Browser" }).click(); await page.getByRole("button", { name: "Browser" }).click();
@ -157,6 +157,6 @@ test("on desktop the page is a native view glued to the panel", async ({ page })
await page.keyboard.press("Escape"); await page.keyboard.press("Escape");
await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("show"); await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("show");
await browser.getByRole("button", { name: "Close panel" }).click(); await page.getByTestId("side-panel").getByRole("button", { name: "Close panel" }).click();
await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("hide"); await expect.poll(async () => (await calls()).at(-1)?.[0]).toBe("hide");
}); });

View File

@ -47,14 +47,14 @@ test("conversations and projects stay separate; + opens bot picker", async ({ pa
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. // The new project's main agent leads the picker, even after other bots exist.
await unterhaltungen.getByTestId("conversation-picker-trigger").click(); await unterhaltungen.getByTestId("conversation-picker-trigger").click();
await expect( await expect(
page page
.getByTestId("conversation-bot-picker") .getByTestId("conversation-bot-picker")
.locator("[data-testid^='conversation-picker-bot-']") .locator("[data-testid^='conversation-picker-bot-']")
.first(), .first(),
).toHaveText("Chief"); ).toHaveText("Projektmanager");
await page.keyboard.press("Escape"); await page.keyboard.press("Escape");
// Creating a project that already exists opens it instead of listing it twice. // Creating a project that already exists opens it instead of listing it twice.

View File

@ -466,7 +466,7 @@ export function AgentOverview({
onClick={onAddRoutine} onClick={onAddRoutine}
className="gap-1 text-ui-label" className="gap-1 text-ui-label"
> >
<Plus size={13} strokeWidth={2.2} /> <Plus size={14} strokeWidth={2.2} />
<Trans>Routine</Trans> <Trans>Routine</Trans>
</Button> </Button>
) : null} ) : null}

View File

@ -509,9 +509,7 @@ export const appContract = {
) )
.output(z.object({ ok: z.literal(true) })), .output(z.object({ ok: z.literal(true) })),
stop: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })), stop: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
sendNow: oc sendNow: oc.input(threadTarget.safeExtend({ messageId: Id.optional() })).output(
.input(threadTarget.safeExtend({ messageId: Id.optional() }))
.output(
z.object({ z.object({
ok: z.literal(true), ok: z.literal(true),
runIds: z.array(Id), runIds: z.array(Id),