diff --git a/packages/app/e2e/commands/panels.spec.ts b/packages/app/e2e/commands/panels.spec.ts index 7e5d7bd6e..a175f59cc 100644 --- a/packages/app/e2e/commands/panels.spec.ts +++ b/packages/app/e2e/commands/panels.spec.ts @@ -7,27 +7,42 @@ const expanded = async (el: { getAttribute: (name: string) => Promise { +test("desktop side-panel buttons switch between review and files without an in-panel tab strip", async ({ + page, + gotoSession, +}) => { await gotoSession() const reviewPanel = page.locator("#review-panel") - - const treeToggle = page.getByRole("button", { name: "Toggle file tree" }).first() - await expect(treeToggle).toBeVisible() - if (await expanded(treeToggle)) await treeToggle.click() - await expect(treeToggle).toHaveAttribute("aria-expanded", "false") - const reviewToggle = page.getByRole("button", { name: "Toggle review" }).first() + const fileToggle = page.getByRole("button", { name: "Toggle file tree" }).first() + await expect(reviewToggle).toBeVisible() + await expect(fileToggle).toBeVisible() + if (await expanded(reviewToggle)) await reviewToggle.click() - await expect(reviewToggle).toHaveAttribute("aria-expanded", "false") - await expect(reviewPanel).toHaveAttribute("aria-hidden", "true") + if (await expanded(fileToggle)) await fileToggle.click() - await page.keyboard.press(`${modKey}+Shift+R`) + await expect(reviewPanel.getByRole("tab", { name: "Files" })).toHaveCount(0) + await expect(reviewPanel.getByRole("tab", { name: "Changes" })).toHaveCount(0) + + await reviewToggle.click() await expect(reviewToggle).toHaveAttribute("aria-expanded", "true") + await expect(fileToggle).toHaveAttribute("aria-expanded", "false") await expect(reviewPanel).toHaveAttribute("aria-hidden", "false") - await page.keyboard.press(`${modKey}+Shift+R`) + await fileToggle.click() + await expect(reviewToggle).toHaveAttribute("aria-expanded", "false") + await expect(fileToggle).toHaveAttribute("aria-expanded", "true") + await expect(reviewPanel).toHaveAttribute("aria-hidden", "false") + + await fileToggle.click() + await expect(fileToggle).toHaveAttribute("aria-expanded", "false") await expect(reviewToggle).toHaveAttribute("aria-expanded", "false") await expect(reviewPanel).toHaveAttribute("aria-hidden", "true") + + await page.keyboard.press(`${modKey}+Shift+R`) + await expect(reviewToggle).toHaveAttribute("aria-expanded", "true") + await expect(fileToggle).toHaveAttribute("aria-expanded", "false") + await expect(reviewPanel).toHaveAttribute("aria-hidden", "false") }) diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index 03bece2e3..522c3d4a4 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -2,6 +2,7 @@ import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test" import type { Prompt } from "@/context/prompt" let createPromptSubmit: typeof import("./submit").createPromptSubmit +let sendFollowupDraft: typeof import("./submit").sendFollowupDraft const createdClients: string[] = [] const createdSessions: string[] = [] @@ -20,12 +21,24 @@ const storedSessions: Record> = {} const promoted: Array<{ directory: string; sessionID: string }> = [] const sentShell: string[] = [] const syncedDirectories: string[] = [] +const promptAsyncCalls: Array> = [] +const commandCalls: Array> = [] +const commandDefinitions: Array<{ name: string }> = [] let params: { id?: string } = {} let selected = "/repo/worktree-a" let variant: string | undefined -const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }] +let currentIntl = "zh-Hans" +let promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }] + +const waitForCall = async (check: () => boolean) => { + for (let attempt = 0; attempt < 20; attempt++) { + if (check()) return + await Promise.resolve() + } + throw new Error("timed out waiting for async request") +} const clientFor = (directory: string) => { createdClients.push(directory) @@ -45,8 +58,14 @@ const clientFor = (directory: string) => { return { data: undefined } }, prompt: async () => ({ data: undefined }), - promptAsync: async () => ({ data: undefined }), - command: async () => ({ data: undefined }), + promptAsync: async (input: Record) => { + promptAsyncCalls.push(input) + return { data: undefined } + }, + command: async (input: Record) => { + commandCalls.push(input) + return { data: undefined } + }, abort: async () => ({ data: undefined }), }, worktree: { @@ -140,7 +159,7 @@ beforeAll(async () => { mock.module("@/context/sync", () => ({ useSync: () => ({ - data: { command: [] }, + data: { command: commandDefinitions }, session: { optimistic: { add: (value: { @@ -194,11 +213,13 @@ beforeAll(async () => { mock.module("@/context/language", () => ({ useLanguage: () => ({ t: (key: string) => key, + intl: () => currentIntl, }), })) const mod = await import("./submit") createPromptSubmit = mod.createPromptSubmit + sendFollowupDraft = mod.sendFollowupDraft }) beforeEach(() => { @@ -208,11 +229,16 @@ beforeEach(() => { optimistic.length = 0 optimisticSeeded.length = 0 promoted.length = 0 + promptAsyncCalls.length = 0 + commandCalls.length = 0 + commandDefinitions.length = 0 params = {} sentShell.length = 0 syncedDirectories.length = 0 selected = "/repo/worktree-a" variant = undefined + currentIntl = "zh-Hans" + promptValue = [{ type: "text", content: "ls", start: 0, end: 2 }] for (const key of Object.keys(storedSessions)) delete storedSessions[key] }) @@ -342,4 +368,116 @@ describe("prompt submit worktree selection", () => { expect(storedSessions["/repo/worktree-a"]).toEqual([{ id: "session-1", title: "New session 1" }]) expect(optimisticSeeded).toEqual([true]) }) + + test("sends locale with promptAsync requests", async () => { + params = { id: "session-existing" } + currentIntl = "pt-BR" + + const submit = createPromptSubmit({ + info: () => ({ id: "session-existing" }), + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => false, + mode: () => "normal", + working: () => false, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + onSubmit: () => undefined, + }) + + await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) + await waitForCall(() => promptAsyncCalls.length > 0) + + expect(promptAsyncCalls.at(-1)?.locale).toBe("pt-BR") + }) + + test("queues locale on followup drafts", async () => { + params = { id: "session-existing" } + const queued: Array> = [] + + const submit = createPromptSubmit({ + info: () => ({ id: "session-existing" }), + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => false, + mode: () => "normal", + working: () => false, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + shouldQueue: () => true, + onQueue: (draft) => queued.push(draft as unknown as Record), + }) + + await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) + + expect(queued.at(-1)?.locale).toBe("zh-Hans") + }) + + test("sends locale with direct slash-command submits", async () => { + params = { id: "session-existing" } + currentIntl = "nb-NO" + commandDefinitions.push({ name: "summarize" }) + promptValue = [{ type: "text", content: "/summarize this", start: 0, end: 15 }] + + const submit = createPromptSubmit({ + info: () => ({ id: "session-existing" }), + imageAttachments: () => [], + commentCount: () => 0, + autoAccept: () => false, + mode: () => "normal", + working: () => false, + editor: () => undefined, + queueScroll: () => undefined, + promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), + addToHistory: () => undefined, + resetHistoryNavigation: () => undefined, + setMode: () => undefined, + setPopover: () => undefined, + onSubmit: () => undefined, + }) + + await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) + await waitForCall(() => commandCalls.length > 0) + + expect(commandCalls.at(-1)?.locale).toBe("nb-NO") + }) + + test("sends locale with slash-command followups", async () => { + await sendFollowupDraft({ + client: clientFor("/repo/main") as any, + globalSync: { + child: () => [{}, () => undefined], + } as any, + sync: { + data: { command: [{ name: "summarize" }] }, + session: { + optimistic: { + add: () => undefined, + remove: () => undefined, + }, + }, + } as any, + draft: { + sessionID: "session-1", + sessionDirectory: "/repo/main", + prompt: [{ type: "text", content: "/summarize this", start: 0, end: 15 }], + context: [], + agent: "agent", + model: { providerID: "provider", modelID: "model" }, + locale: "zh-Hans", + }, + }) + + expect(commandCalls.at(-1)?.locale).toBe("zh-Hans") + }) }) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 2a3a3d0e9..80a2746c7 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -34,6 +34,7 @@ export type FollowupDraft = { context: (ContextItem & { key: string })[] agent: string model: { providerID: string; modelID: string } + locale?: string variant?: string } @@ -88,6 +89,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) { arguments: tail.join(" "), agent: input.draft.agent, model: `${input.draft.model.providerID}/${input.draft.model.modelID}`, + locale: input.draft.locale, variant: input.draft.variant, parts: images.map((attachment) => ({ id: Identifier.ascending("part"), @@ -153,6 +155,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) { sessionID: input.draft.sessionID, agent: input.draft.agent, model: input.draft.model, + locale: input.draft.locale, messageID, parts: requestParts, variant: input.draft.variant, @@ -389,6 +392,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { modelID: currentModel.id, providerID: currentModel.provider.id, } + const locale = language.intl() const agent = currentAgent.name const context = prompt.context.items().slice() const draft: FollowupDraft = { @@ -398,6 +402,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { context, agent, model, + locale, variant, } @@ -462,6 +467,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { arguments: args.join(" "), agent, model: `${model.providerID}/${model.modelID}`, + locale, variant, parts: images.map((attachment) => ({ id: Identifier.ascending("part"), diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index ac5ebd934..6b4669f68 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -435,6 +435,31 @@ export function SessionHeader() {