Procházet zdrojové kódy

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

Shoubhit Dash před 2 týdny
rodič
revize
2002f08f2e

+ 22 - 10
packages/app/e2e/prompt/prompt-shell.spec.ts

@@ -1,6 +1,7 @@
 import type { ToolPart } from "@opencode-ai/sdk/v2/client"
 import { test, expect } from "../fixtures"
 import { withSession } from "../actions"
+import { promptModelSelector, promptSelector, promptVariantSelector } from "../selectors"
 
 const isBash = (part: unknown): part is ToolPart => {
   if (!part || typeof part !== "object") return false
@@ -9,15 +10,6 @@ const isBash = (part: unknown): part is ToolPart => {
   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.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) => {
     project.trackSession(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 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") }))
   })
 })
+
+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)
+})

+ 68 - 66
packages/app/src/components/prompt-input.tsx

@@ -1480,27 +1480,60 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                     />
                   </TooltipKeybind>
                 </div>
-                <div data-component="prompt-model-control">
-                  <Show
-                    when={providers.paid().length > 0}
-                    fallback={
+                <Show when={store.mode !== "shell"}>
+                  <div data-component="prompt-model-control">
+                    <Show
+                      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
                         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} />)
-                            })
+                        <ModelSelectorPopover
+                          model={local.model}
+                          triggerAs={Button}
+                          triggerProps={{
+                            variant: "ghost",
+                            size: "normal",
+                            style: control(),
+                            class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
+                            "data-action": "prompt-model",
                           }}
                         >
                           <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")}
                           </span>
                           <Icon name="chevron-down" size="small" class="shrink-0" />
-                        </Button>
+                        </ModelSelectorPopover>
                       </TooltipKeybind>
-                    }
-                  >
+                    </Show>
+                  </div>
+                  <div data-component="prompt-variant-control">
                     <TooltipKeybind
                       placement="top"
                       gutter={4}
-                      title={language.t("command.model.choose")}
-                      keybind={command.keybind("model.choose")}
+                      title={language.t("command.model.variant.cycle")}
+                      keybind={command.keybind("model.variant.cycle")}
                     >
-                      <ModelSelectorPopover
-                        model={local.model}
-                        triggerAs={Button}
-                        triggerProps={{
-                          variant: "ghost",
-                          size: "normal",
-                          style: control(),
-                          class: "min-w-0 max-w-[320px] text-13-regular text-text-base group",
-                          "data-action": "prompt-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" />
-                      </ModelSelectorPopover>
+                      <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>
-                  </Show>
-                </div>
-                <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>
+                  </div>
+                </Show>
                 <TooltipKeybind
                   placement="top"
                   gutter={8}