From 0b6d95556023d3e59064bf2fce1b06bc0a8765e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 30 Mar 2026 12:06:34 +0000 Subject: [PATCH 1/4] feat: add number key shortcuts to variant popover Add numbered selection (1-9) to the variant dialog so users can quickly pick a variant by pressing its number key. Also hide the search filter input since there are typically only a few variants. Up/down navigation and Enter selection still work as before. https://claude.ai/code/session_011xS58qyP1BjcuCrJcWPxQB --- .../cli/cmd/tui/component/dialog-variant.tsx | 2 + .../src/cli/cmd/tui/ui/dialog-select.tsx | 88 +++++++++++++------ 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-variant.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-variant.tsx index 28ee1b2825..12c1d08d42 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-variant.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-variant.tsx @@ -34,6 +34,8 @@ export function DialogVariant() { title={"Select variant"} current={local.model.variant.selected()} flat={true} + hideFilter={true} + numbered={true} /> ) } diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 34c6ee8787..13d8b53030 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -21,6 +21,8 @@ export interface DialogSelectProps { onFilter?: (query: string) => void onSelect?: (option: DialogSelectOption) => void skipFilter?: boolean + hideFilter?: boolean + numbered?: boolean keybind?: { keybind?: Keybind.Info title: string @@ -194,6 +196,21 @@ export function DialogSelect(props: DialogSelectProps) { if (evt.name === "home") moveTo(0) if (evt.name === "end") moveTo(flat().length - 1) + if (props.numbered && !evt.ctrl && !evt.alt && !evt.meta) { + const num = parseInt(evt.name, 10) + if (num >= 1 && num <= 9 && num <= flat().length) { + evt.preventDefault() + evt.stopPropagation() + const index = num - 1 + const option = flat()[index] + if (option) { + if (option.onSelect) option.onSelect(dialog) + props.onSelect?.(option) + } + return + } + } + if (evt.name === "return") { const option = selected() if (option) { @@ -240,29 +257,31 @@ export function DialogSelect(props: DialogSelectProps) { esc - - { - batch(() => { - setStore("filter", e) - props.onFilter?.(e) - }) - }} - focusedBackgroundColor={theme.backgroundPanel} - cursorColor={theme.primary} - focusedTextColor={theme.textMuted} - ref={(r) => { - input = r - setTimeout(() => { - if (!input) return - if (input.isDestroyed) return - input.focus() - }, 1) - }} - placeholder={props.placeholder ?? "Search"} - placeholderColor={theme.textMuted} - /> - + + + { + batch(() => { + setStore("filter", e) + props.onFilter?.(e) + }) + }} + focusedBackgroundColor={theme.backgroundPanel} + cursorColor={theme.primary} + focusedTextColor={theme.textMuted} + ref={(r) => { + input = r + setTimeout(() => { + if (!input) return + if (input.isDestroyed) return + input.focus() + }, 1) + }} + placeholder={props.placeholder ?? "Search"} + placeholderColor={theme.textMuted} + /> + + 0} @@ -293,6 +312,25 @@ export function DialogSelect(props: DialogSelectProps) { {(option) => { const active = createMemo(() => isDeepEqual(option.value, selected()?.value)) const current = createMemo(() => isDeepEqual(option.value, props.current)) + const flatIndex = createMemo(() => flat().findIndex((x) => isDeepEqual(x.value, option.value))) + const numberLabel = createMemo(() => { + if (!props.numbered) return undefined + const idx = flatIndex() + return idx >= 0 && idx < 9 ? `${idx + 1}` : undefined + }) + const gutter = createMemo(() => { + if (numberLabel()) { + return ( + + {numberLabel()} + + ) + } + return option.gutter + }) return ( (props: DialogSelectProps) { moveTo(index) }} backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)} - paddingLeft={current() || option.gutter ? 1 : 3} + paddingLeft={current() || gutter() ? 1 : 3} paddingRight={3} gap={1} > @@ -326,7 +364,7 @@ export function DialogSelect(props: DialogSelectProps) { description={option.description !== category ? option.description : undefined} active={active()} current={current()} - gutter={option.gutter} + gutter={gutter()} /> ) From 8bff03f83302a35decc23a5970c46e45c81f4600 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 30 Mar 2026 12:23:47 +0000 Subject: [PATCH 2/4] fix: show number labels consistently for current item in variant popover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an item is the current selection, show the number gutter instead of replacing it with the ● bullet, so users always see which key to press. https://claude.ai/code/session_011xS58qyP1BjcuCrJcWPxQB --- .../src/cli/cmd/tui/ui/dialog-select.tsx | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 13d8b53030..ece0d59399 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -321,12 +321,14 @@ export function DialogSelect(props: DialogSelectProps) { const gutter = createMemo(() => { if (numberLabel()) { return ( - - {numberLabel()} - + + + {numberLabel()} + + ) } return option.gutter @@ -407,16 +409,16 @@ function Option(props: { return ( <> - - - ● - - - + {props.gutter} + + + ● + + Date: Mon, 30 Mar 2026 12:24:41 +0000 Subject: [PATCH 3/4] fix: remove invalid alt/meta properties from KeyEvent check KeyEvent only has ctrl, not alt or meta. Simplify the modifier check. https://claude.ai/code/session_011xS58qyP1BjcuCrJcWPxQB --- packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index ece0d59399..71843657ee 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -196,7 +196,7 @@ export function DialogSelect(props: DialogSelectProps) { if (evt.name === "home") moveTo(0) if (evt.name === "end") moveTo(flat().length - 1) - if (props.numbered && !evt.ctrl && !evt.alt && !evt.meta) { + if (props.numbered && !evt.ctrl) { const num = parseInt(evt.name, 10) if (num >= 1 && num <= 9 && num <= flat().length) { evt.preventDefault() From 10516cc4cbb198dc941bf8c0eb2a7405309aea19 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 30 Mar 2026 12:25:10 +0000 Subject: [PATCH 4/4] chore: update bun.lock after dependency install https://claude.ai/code/session_011xS58qyP1BjcuCrJcWPxQB --- bun.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index 12e53065df..264a788fb4 100644 --- a/bun.lock +++ b/bun.lock @@ -1915,7 +1915,7 @@ "@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="], - "@solidjs/start": ["@solidjs/start@https://pkg.pr.new/@solidjs/start@dfb2020", { "dependencies": { "@babel/core": "^7.28.3", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.5", "@solidjs/meta": "^0.29.4", "@tanstack/server-functions-plugin": "1.134.5", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.9", "cookie-es": "^2.0.0", "defu": "^6.1.4", "error-stack-parser": "^2.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "fast-glob": "^3.3.3", "h3": "npm:h3@2.0.1-rc.4", "html-to-image": "^1.11.13", "micromatch": "^4.0.8", "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.3.2", "seroval-plugins": "^1.2.1", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", "srvx": "^0.9.1", "terracotta": "^1.0.6", "vite": "7.1.10", "vite-plugin-solid": "^2.11.9", "vitest": "^4.0.10" } }, "sha512-7JjjA49VGNOsMRI8QRUhVudZmv0CnJ18SliSgK1ojszs/c3ijftgVkzvXdkSLN4miDTzbkXewf65D6ZBo6W+GQ=="], + "@solidjs/start": ["@solidjs/start@https://pkg.pr.new/@solidjs/start@dfb2020", { "dependencies": { "@babel/core": "^7.28.3", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.5", "@solidjs/meta": "^0.29.4", "@tanstack/server-functions-plugin": "1.134.5", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.9", "cookie-es": "^2.0.0", "defu": "^6.1.4", "error-stack-parser": "^2.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "fast-glob": "^3.3.3", "h3": "npm:h3@2.0.1-rc.4", "html-to-image": "^1.11.13", "micromatch": "^4.0.8", "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.3.2", "seroval-plugins": "^1.2.1", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", "srvx": "^0.9.1", "terracotta": "^1.0.6", "vite": "7.1.10", "vite-plugin-solid": "^2.11.9", "vitest": "^4.0.10" } }], "@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="], @@ -3041,7 +3041,7 @@ "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], - "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="], + "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d"], "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],