fix(prompt): unmount model controls in shell mode (#20886)

This commit is contained in:
Shoubhit Dash
2026-04-03 18:46:26 +05:30
committed by GitHub
parent c307505f8b
commit 2002f08f2e
2 changed files with 90 additions and 76 deletions

View File

@@ -1,6 +1,7 @@
import type { ToolPart } from "@opencode-ai/sdk/v2/client" import type { ToolPart } from "@opencode-ai/sdk/v2/client"
import { test, expect } from "../fixtures" import { test, expect } from "../fixtures"
import { withSession } from "../actions" import { withSession } from "../actions"
import { promptModelSelector, promptSelector, promptVariantSelector } from "../selectors"
const isBash = (part: unknown): part is ToolPart => { const isBash = (part: unknown): part is ToolPart => {
if (!part || typeof part !== "object") return false if (!part || typeof part !== "object") return false
@@ -9,15 +10,6 @@ const isBash = (part: unknown): part is ToolPart => {
return "state" in part return "state" in part
} }
async function setAutoAccept(page: Parameters<typeof test>[0]["page"], enabled: boolean) {
const button = page.locator('[data-action="prompt-permissions"]').first()
await expect(button).toBeVisible()
const pressed = (await button.getAttribute("aria-pressed")) === "true"
if (pressed === enabled) return
await button.click()
await expect(button).toHaveAttribute("aria-pressed", enabled ? "true" : "false")
}
test("shell mode runs a command in the project directory", async ({ page, project }) => { test("shell mode runs a command in the project directory", async ({ page, project }) => {
test.setTimeout(120_000) test.setTimeout(120_000)
@@ -27,7 +19,12 @@ test("shell mode runs a command in the project directory", async ({ page, projec
await withSession(project.sdk, `e2e shell ${Date.now()}`, async (session) => { await withSession(project.sdk, `e2e shell ${Date.now()}`, async (session) => {
project.trackSession(session.id) project.trackSession(session.id)
await project.gotoSession(session.id) await project.gotoSession(session.id)
await setAutoAccept(page, true) const button = page.locator('[data-action="prompt-permissions"]').first()
await expect(button).toBeVisible()
if ((await button.getAttribute("aria-pressed")) !== "true") {
await button.click()
await expect(button).toHaveAttribute("aria-pressed", "true")
}
await project.shell(cmd) await project.shell(cmd)
await expect await expect
@@ -57,3 +54,18 @@ test("shell mode runs a command in the project directory", async ({ page, projec
.toEqual(expect.objectContaining({ cwd: project.directory, output: expect.stringContaining("README.md") })) .toEqual(expect.objectContaining({ cwd: project.directory, output: expect.stringContaining("README.md") }))
}) })
}) })
test("shell mode unmounts model and variant controls", async ({ page, project }) => {
await project.open()
const prompt = page.locator(promptSelector).first()
await expect(page.locator(promptModelSelector)).toHaveCount(1)
await expect(page.locator(promptVariantSelector)).toHaveCount(1)
await prompt.click()
await page.keyboard.type("!")
await expect(prompt).toHaveAttribute("aria-label", /enter shell command/i)
await expect(page.locator(promptModelSelector)).toHaveCount(0)
await expect(page.locator(promptVariantSelector)).toHaveCount(0)
})

View File

@@ -1480,27 +1480,60 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/> />
</TooltipKeybind> </TooltipKeybind>
</div> </div>
<div data-component="prompt-model-control"> <Show when={store.mode !== "shell"}>
<Show <div data-component="prompt-model-control">
when={providers.paid().length > 0} <Show
fallback={ when={providers.paid().length > 0}
fallback={
<TooltipKeybind
placement="top"
gutter={4}
title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")}
>
<Button
data-action="prompt-model"
as="div"
variant="ghost"
size="normal"
class="min-w-0 max-w-[320px] text-13-regular text-text-base group"
style={control()}
onClick={() => {
void import("@/components/dialog-select-model-unpaid").then((x) => {
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
})
}}
>
<Show when={local.model.current()?.provider?.id}>
<ProviderIcon
id={local.model.current()?.provider?.id ?? ""}
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
/>
</Show>
<span class="truncate">
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
</span>
<Icon name="chevron-down" size="small" class="shrink-0" />
</Button>
</TooltipKeybind>
}
>
<TooltipKeybind <TooltipKeybind
placement="top" placement="top"
gutter={4} gutter={4}
title={language.t("command.model.choose")} title={language.t("command.model.choose")}
keybind={command.keybind("model.choose")} keybind={command.keybind("model.choose")}
> >
<Button <ModelSelectorPopover
data-action="prompt-model" model={local.model}
as="div" triggerAs={Button}
variant="ghost" triggerProps={{
size="normal" variant: "ghost",
class="min-w-0 max-w-[320px] text-13-regular text-text-base group" size: "normal",
style={control()} style: control(),
onClick={() => { class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
void import("@/components/dialog-select-model-unpaid").then((x) => { "data-action": "prompt-model",
dialog.show(() => <x.DialogSelectModelUnpaid model={local.model} />)
})
}} }}
> >
<Show when={local.model.current()?.provider?.id}> <Show when={local.model.current()?.provider?.id}>
@@ -1514,63 +1547,32 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
{local.model.current()?.name ?? language.t("dialog.model.select.title")} {local.model.current()?.name ?? language.t("dialog.model.select.title")}
</span> </span>
<Icon name="chevron-down" size="small" class="shrink-0" /> <Icon name="chevron-down" size="small" class="shrink-0" />
</Button> </ModelSelectorPopover>
</TooltipKeybind> </TooltipKeybind>
} </Show>
> </div>
<div data-component="prompt-variant-control">
<TooltipKeybind <TooltipKeybind
placement="top" placement="top"
gutter={4} gutter={4}
title={language.t("command.model.choose")} title={language.t("command.model.variant.cycle")}
keybind={command.keybind("model.choose")} keybind={command.keybind("model.variant.cycle")}
> >
<ModelSelectorPopover <Select
model={local.model} size="normal"
triggerAs={Button} options={variants()}
triggerProps={{ current={local.model.variant.current() ?? "default"}
variant: "ghost", label={(x) => (x === "default" ? language.t("common.default") : x)}
size: "normal", onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)}
style: control(), class="capitalize max-w-[160px] text-text-base"
class: "min-w-0 max-w-[320px] text-13-regular text-text-base group", valueClass="truncate text-13-regular text-text-base"
"data-action": "prompt-model", triggerStyle={control()}
}} triggerProps={{ "data-action": "prompt-model-variant" }}
> variant="ghost"
<Show when={local.model.current()?.provider?.id}> />
<ProviderIcon
id={local.model.current()?.provider?.id ?? ""}
class="size-4 shrink-0 opacity-40 group-hover:opacity-100 transition-opacity duration-150"
style={{ "will-change": "opacity", transform: "translateZ(0)" }}
/>
</Show>
<span class="truncate">
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
</span>
<Icon name="chevron-down" size="small" class="shrink-0" />
</ModelSelectorPopover>
</TooltipKeybind> </TooltipKeybind>
</Show> </div>
</div> </Show>
<div data-component="prompt-variant-control">
<TooltipKeybind
placement="top"
gutter={4}
title={language.t("command.model.variant.cycle")}
keybind={command.keybind("model.variant.cycle")}
>
<Select
size="normal"
options={variants()}
current={local.model.variant.current() ?? "default"}
label={(x) => (x === "default" ? language.t("common.default") : x)}
onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)}
class="capitalize max-w-[160px] text-text-base"
valueClass="truncate text-13-regular text-text-base"
triggerStyle={control()}
triggerProps={{ "data-action": "prompt-model-variant" }}
variant="ghost"
/>
</TooltipKeybind>
</div>
<TooltipKeybind <TooltipKeybind
placement="top" placement="top"
gutter={8} gutter={8}