Skip to content

Commit 55bafa2

Browse files
authored
feat(app): v2 thinking level selector (anomalyco#30646)
1 parent f62ba5e commit 55bafa2

2 files changed

Lines changed: 124 additions & 1 deletion

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { expect, test, type Page } from "@playwright/test"
2+
import { base64Encode } from "@opencode-ai/core/util/encode"
3+
import { mockOpenCodeServer } from "../utils/mock-server"
4+
5+
const directory = "C:/OpenCode/PromptThinkingLevelRegression"
6+
const projectID = "proj_prompt_thinking_level_regression"
7+
const sessionID = "ses_prompt_thinking_level_regression"
8+
9+
test("shows the V2 thinking level control while relevant", async ({ page }) => {
10+
await mockOpenCodeServer(page, {
11+
directory,
12+
project: {
13+
id: projectID,
14+
worktree: directory,
15+
vcs: "git",
16+
name: "prompt-thinking-level-regression",
17+
time: { created: 1700000000000, updated: 1700000000000 },
18+
sandboxes: [],
19+
},
20+
provider: {
21+
all: [
22+
{
23+
id: "opencode",
24+
name: "OpenCode",
25+
models: {
26+
"thinking-model": {
27+
id: "thinking-model",
28+
name: "Thinking Model",
29+
limit: { context: 200_000 },
30+
variants: { high: {} },
31+
},
32+
},
33+
},
34+
],
35+
connected: ["opencode"],
36+
default: { providerID: "opencode", modelID: "thinking-model" },
37+
},
38+
sessions: [
39+
{
40+
id: sessionID,
41+
slug: "prompt-thinking-level-regression",
42+
projectID,
43+
directory,
44+
title: "Prompt thinking level regression",
45+
version: "dev",
46+
time: { created: 1700000000000, updated: 1700000000000 },
47+
},
48+
],
49+
pageMessages: () => ({ items: [] }),
50+
})
51+
await page.addInitScript(() => {
52+
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
53+
})
54+
55+
await page.goto(`/${base64Encode(directory)}/session/${sessionID}`)
56+
const composer = page.locator('[data-component="session-composer"]')
57+
const input = composer.locator('[data-component="prompt-input"]')
58+
const control = composer.locator('[data-component="prompt-variant-control"]')
59+
await expect(composer).toBeVisible()
60+
61+
await idleComposer(page)
62+
await expect(control).toBeHidden()
63+
64+
await composer.hover()
65+
await expect(control).toBeVisible()
66+
67+
await control.locator('[data-action="prompt-model-variant"]').click()
68+
const high = page.getByRole("option", { name: "high" })
69+
await expect(high).toBeVisible()
70+
await page.mouse.move(0, 0)
71+
await expect(control).toBeVisible()
72+
await expect(high).toBeVisible()
73+
await high.click()
74+
75+
await idleComposer(page)
76+
await input.focus()
77+
await expect(control).toBeVisible()
78+
79+
await idleComposer(page)
80+
await expect(control).toBeVisible()
81+
})
82+
83+
async function idleComposer(page: Page) {
84+
await page.mouse.move(0, 0)
85+
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur())
86+
}

packages/app/src/components/prompt-input.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
277277
draggingType: "image" | "@mention" | null
278278
mode: "normal" | "shell"
279279
applyingHistory: boolean
280+
variantOpen: boolean
280281
}>({
281282
popover: null,
282283
historyIndex: -1,
@@ -285,6 +286,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
285286
draggingType: null,
286287
mode: "normal",
287288
applyingHistory: false,
289+
variantOpen: false,
288290
})
289291
const [picker, setPicker] = createStore({
290292
projectOpen: false,
@@ -1101,6 +1103,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
11011103
)
11021104

11031105
const variants = createMemo(() => ["default", ...local.model.variant.list()])
1106+
// Check provider variants directly: `variants` also includes the UI-only default option.
1107+
const showVariantControl = createMemo(() => local.model.variant.list().length > 0)
11041108
const accepting = createMemo(() => {
11051109
const id = params.id
11061110
if (!id) return permission.isAutoAcceptingDirectory(sdk.directory)
@@ -1571,6 +1575,39 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
15711575
<ComposerPickerTrigger state={newProjectTriggerState()} />
15721576
</Show>
15731577
<ComposerModelControl state={modelControlState()} />
1578+
<Show when={store.mode !== "shell" && showVariantControl()}>
1579+
<div
1580+
data-component="prompt-variant-control"
1581+
classList={{
1582+
"hidden group-hover/prompt-input:block group-focus-within/prompt-input:block":
1583+
!local.model.variant.current() && !store.variantOpen,
1584+
}}
1585+
>
1586+
<TooltipKeybind
1587+
placement="top"
1588+
gutter={4}
1589+
title={language.t("command.model.variant.cycle")}
1590+
keybind={command.keybind("model.variant.cycle")}
1591+
>
1592+
<Select
1593+
size="normal"
1594+
options={variants()}
1595+
current={local.model.variant.current() ?? "default"}
1596+
label={(x) => (x === "default" ? language.t("common.default") : x)}
1597+
onOpenChange={(open) => setStore("variantOpen", open)}
1598+
onSelect={(value) => {
1599+
local.model.variant.set(value === "default" ? undefined : value)
1600+
restoreFocus()
1601+
}}
1602+
class="capitalize max-w-[160px] justify-start text-v2-text-text-faint"
1603+
valueClass="truncate text-[13px] font-[440] leading-5 text-v2-text-text-faint"
1604+
triggerStyle={control()}
1605+
triggerProps={{ "data-action": "prompt-model-variant" }}
1606+
variant="ghost"
1607+
/>
1608+
</TooltipKeybind>
1609+
</div>
1610+
</Show>
15741611
</div>
15751612
<Tooltip placement="top" inactive={!working() && blank()} value={tip()}>
15761613
<IconButton
@@ -1890,7 +1927,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
18901927
</TooltipKeybind>
18911928
</Show>
18921929
</div>
1893-
<Show when={variants().length > 2}>
1930+
<Show when={showVariantControl()}>
18941931
<div
18951932
data-component="prompt-variant-control"
18961933
style={providersShouldFadeIn() ? { animation: "fade-in 0.3s" } : undefined}

0 commit comments

Comments
 (0)