Browse Source

feat(app): v2 thinking level selector (#30646)

Luke Parker 2 months ago
parent
commit
55bafa29d4

+ 86 - 0
packages/app/e2e/regression/prompt-thinking-level.spec.ts

@@ -0,0 +1,86 @@
+import { expect, test, type Page } from "@playwright/test"
+import { base64Encode } from "@opencode-ai/core/util/encode"
+import { mockOpenCodeServer } from "../utils/mock-server"
+
+const directory = "C:/OpenCode/PromptThinkingLevelRegression"
+const projectID = "proj_prompt_thinking_level_regression"
+const sessionID = "ses_prompt_thinking_level_regression"
+
+test("shows the V2 thinking level control while relevant", async ({ page }) => {
+  await mockOpenCodeServer(page, {
+    directory,
+    project: {
+      id: projectID,
+      worktree: directory,
+      vcs: "git",
+      name: "prompt-thinking-level-regression",
+      time: { created: 1700000000000, updated: 1700000000000 },
+      sandboxes: [],
+    },
+    provider: {
+      all: [
+        {
+          id: "opencode",
+          name: "OpenCode",
+          models: {
+            "thinking-model": {
+              id: "thinking-model",
+              name: "Thinking Model",
+              limit: { context: 200_000 },
+              variants: { high: {} },
+            },
+          },
+        },
+      ],
+      connected: ["opencode"],
+      default: { providerID: "opencode", modelID: "thinking-model" },
+    },
+    sessions: [
+      {
+        id: sessionID,
+        slug: "prompt-thinking-level-regression",
+        projectID,
+        directory,
+        title: "Prompt thinking level regression",
+        version: "dev",
+        time: { created: 1700000000000, updated: 1700000000000 },
+      },
+    ],
+    pageMessages: () => ({ items: [] }),
+  })
+  await page.addInitScript(() => {
+    localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
+  })
+
+  await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
+  const composer = page.locator('[data-component="session-composer"]')
+  const input = composer.locator('[data-component="prompt-input"]')
+  const control = composer.locator('[data-component="prompt-variant-control"]')
+  await expect(composer).toBeVisible()
+
+  await idleComposer(page)
+  await expect(control).toBeHidden()
+
+  await composer.hover()
+  await expect(control).toBeVisible()
+
+  await control.locator('[data-action="prompt-model-variant"]').click()
+  const high = page.getByRole("option", { name: "high" })
+  await expect(high).toBeVisible()
+  await page.mouse.move(0, 0)
+  await expect(control).toBeVisible()
+  await expect(high).toBeVisible()
+  await high.click()
+
+  await idleComposer(page)
+  await input.focus()
+  await expect(control).toBeVisible()
+
+  await idleComposer(page)
+  await expect(control).toBeVisible()
+})
+
+async function idleComposer(page: Page) {
+  await page.mouse.move(0, 0)
+  await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur())
+}

+ 38 - 1
packages/app/src/components/prompt-input.tsx

@@ -277,6 +277,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     draggingType: "image" | "@mention" | null
     mode: "normal" | "shell"
     applyingHistory: boolean
+    variantOpen: boolean
   }>({
     popover: null,
     historyIndex: -1,
@@ -285,6 +286,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
     draggingType: null,
     mode: "normal",
     applyingHistory: false,
+    variantOpen: false,
   })
   const [picker, setPicker] = createStore({
     projectOpen: false,
@@ -1101,6 +1103,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
   )
 
   const variants = createMemo(() => ["default", ...local.model.variant.list()])
+  // Check provider variants directly: `variants` also includes the UI-only default option.
+  const showVariantControl = createMemo(() => local.model.variant.list().length > 0)
   const accepting = createMemo(() => {
     const id = params.id
     if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
@@ -1571,6 +1575,39 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                     <ComposerPickerTrigger state={newProjectTriggerState()} />
                   </Show>
                   <ComposerModelControl state={modelControlState()} />
+                  <Show when={store.mode !== "shell" && showVariantControl()}>
+                    <div
+                      data-component="prompt-variant-control"
+                      classList={{
+                        "hidden group-hover/prompt-input:block group-focus-within/prompt-input:block":
+                          !local.model.variant.current() && !store.variantOpen,
+                      }}
+                    >
+                      <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)}
+                          onOpenChange={(open) => setStore("variantOpen", open)}
+                          onSelect={(value) => {
+                            local.model.variant.set(value === "default" ? undefined : value)
+                            restoreFocus()
+                          }}
+                          class="capitalize max-w-[160px] justify-start text-v2-text-text-faint"
+                          valueClass="truncate text-[13px] font-[440] leading-5 text-v2-text-text-faint"
+                          triggerStyle={control()}
+                          triggerProps={{ "data-action": "prompt-model-variant" }}
+                          variant="ghost"
+                        />
+                      </TooltipKeybind>
+                    </div>
+                  </Show>
                 </div>
                 <Tooltip placement="top" inactive={!working() && blank()} value={tip()}>
                   <IconButton
@@ -1890,7 +1927,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
                             </TooltipKeybind>
                           </Show>
                         </div>
-                        <Show when={variants().length > 2}>
+                        <Show when={showVariantControl()}>
                           <div
                             data-component="prompt-variant-control"
                             style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}