Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions packages/app/e2e/commands/panels.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,42 @@ const expanded = async (el: { getAttribute: (name: string) => Promise<string | n
return value === "true"
}

test("review panel can be toggled via keybind", async ({ page, gotoSession }) => {
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")
})
146 changes: 142 additions & 4 deletions packages/app/src/components/prompt-input/submit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = []
Expand All @@ -20,12 +21,24 @@ const storedSessions: Record<string, Array<{ id: string; title?: string }>> = {}
const promoted: Array<{ directory: string; sessionID: string }> = []
const sentShell: string[] = []
const syncedDirectories: string[] = []
const promptAsyncCalls: Array<Record<string, unknown>> = []
const commandCalls: Array<Record<string, unknown>> = []
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)
Expand All @@ -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<string, unknown>) => {
promptAsyncCalls.push(input)
return { data: undefined }
},
command: async (input: Record<string, unknown>) => {
commandCalls.push(input)
return { data: undefined }
},
abort: async () => ({ data: undefined }),
},
worktree: {
Expand Down Expand Up @@ -140,7 +159,7 @@ beforeAll(async () => {

mock.module("@/context/sync", () => ({
useSync: () => ({
data: { command: [] },
data: { command: commandDefinitions },
session: {
optimistic: {
add: (value: {
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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]
})

Expand Down Expand Up @@ -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<Record<string, unknown>> = []

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<string, unknown>),
})

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")
})
})
6 changes: 6 additions & 0 deletions packages/app/src/components/prompt-input/submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type FollowupDraft = {
context: (ContextItem & { key: string })[]
agent: string
model: { providerID: string; modelID: string }
locale?: string
variant?: string
}

Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = {
Expand All @@ -398,6 +402,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
context,
agent,
model,
locale,
variant,
}

Expand Down Expand Up @@ -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"),
Expand Down
25 changes: 25 additions & 0 deletions packages/app/src/components/session/session-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,31 @@ export function SessionHeader() {
</TooltipKeybind>

<div class="hidden md:flex items-center gap-1 shrink-0">
<TooltipKeybind
title={language.t("command.review.toggle")}
keybind={command.keybind("review.toggle")}
>
<Button
variant="ghost"
class="titlebar-icon w-8 h-6 p-0 box-border"
onClick={() => view().sidePanel.toggleTab("changes")}
aria-label={language.t("command.review.toggle")}
aria-expanded={view().sidePanel.opened() && view().sidePanel.tab() === "changes"}
aria-controls="review-panel"
>
<div class="relative flex items-center justify-center size-4">
<Icon
size="small"
name={view().sidePanel.opened() && view().sidePanel.tab() === "changes" ? "review-active" : "review"}
classList={{
"text-icon-strong": view().sidePanel.opened() && view().sidePanel.tab() === "changes",
"text-icon-weak": !(view().sidePanel.opened() && view().sidePanel.tab() === "changes"),
}}
/>
</div>
</Button>
</TooltipKeybind>

<TooltipKeybind
title={language.t("command.fileTree.toggle")}
keybind={command.keybind("fileTree.toggle")}
Expand Down
21 changes: 21 additions & 0 deletions packages/app/src/components/session/session-new-view-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { PawworkSkillName } from "./pawwork-skill-meta"

export function buildSkillSessionCommandInput(input: {
sessionID: string
command: PawworkSkillName
agent: string
model: string
variant?: string
locale?: string
}) {
return {
sessionID: input.sessionID,
command: input.command,
arguments: "",
agent: input.agent,
model: input.model,
variant: input.variant,
locale: input.locale,
parts: [],
}
}
26 changes: 26 additions & 0 deletions packages/app/src/components/session/session-new-view.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, test } from "bun:test"
import { buildSkillSessionCommandInput } from "./session-new-view-command"

describe("new session skill cards", () => {
test("passes locale through the built-in skill launch command", () => {
expect(
buildSkillSessionCommandInput({
sessionID: "session-1",
command: "document-processing",
agent: "build",
model: "openai/gpt-5",
variant: "fast",
locale: "pt-BR",
}),
).toEqual({
sessionID: "session-1",
command: "document-processing",
arguments: "",
agent: "build",
model: "openai/gpt-5",
variant: "fast",
locale: "pt-BR",
parts: [],
})
})
})
Loading