From fafc74b05224e48e2df80e25ccd847ad34de0daf Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:29:35 -0600 Subject: [PATCH 1/5] fix(app): virtualizer getting wrong scroll root --- packages/ui/src/pierre/virtualizer.ts | 29 ++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/pierre/virtualizer.ts b/packages/ui/src/pierre/virtualizer.ts index 4957afc125..42c3a4ca3c 100644 --- a/packages/ui/src/pierre/virtualizer.ts +++ b/packages/ui/src/pierre/virtualizer.ts @@ -19,12 +19,35 @@ export const virtualMetrics: Partial = { fileGap: 0, } +function scrollable(value: string) { + return value === "auto" || value === "scroll" || value === "overlay" +} + +function scrollRoot(container: HTMLElement) { + let node = container.parentElement + while (node) { + const style = getComputedStyle(node) + if (scrollable(style.overflowY)) return node + node = node.parentElement + } +} + function target(container: HTMLElement): Target | undefined { if (typeof document === "undefined") return - const root = container.closest("[data-component='session-review']") - if (root instanceof HTMLElement) { - const content = root.querySelector("[data-slot='session-review-container']") + const review = container.closest("[data-component='session-review']") + if (review instanceof HTMLElement) { + const content = review.querySelector("[data-slot='session-review-container']") + return { + key: review, + root: review, + content: content instanceof HTMLElement ? content : undefined, + } + } + + const root = scrollRoot(container) + if (root) { + const content = root.querySelector("[role='log']") return { key: root, root, From acb46679e3cd994e333158ab31fc6d80e2c12649 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:34:43 -0600 Subject: [PATCH 2/5] chore: cleanup --- packages/app/src/components/prompt-input.tsx | 70 +++++--------- packages/ui/src/components/radio-group.css | 99 +++++++++----------- 2 files changed, 67 insertions(+), 102 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 91c7625903..c1c9daf51a 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -26,6 +26,7 @@ import type { IconName } from "@opencode-ai/ui/icons/provider" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { IconButton } from "@opencode-ai/ui/icon-button" import { Select } from "@opencode-ai/ui/select" +import { RadioGroup } from "@opencode-ai/ui/radio-group" import { useDialog } from "@opencode-ai/ui/context/dialog" import { ModelSelectorPopover } from "@/components/dialog-select-model" import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid" @@ -1332,56 +1333,27 @@ export const PromptInput: Component = (props) => {
-
-
+ mode && setMode(mode)} + label={(mode) => ( +
+ + {mode === "shell" ? language.t("prompt.mode.shell") : "Prompt"} +
+ )} + class="w-[68px] [&_[data-slot=radio-group-wrapper]]:w-full [&_[data-slot=radio-group-item]]:flex-1 [&_[data-slot=radio-group-item-label]]:h-6 [&_[data-slot=radio-group-item-label]]:p-0" /> - -
diff --git a/packages/ui/src/components/radio-group.css b/packages/ui/src/components/radio-group.css index 3d672bb300..92e4918467 100644 --- a/packages/ui/src/components/radio-group.css +++ b/packages/ui/src/components/radio-group.css @@ -1,15 +1,29 @@ [data-component="radio-group"] { - display: flex; + display: inline-flex; flex-direction: column; gap: calc(var(--spacing) * 2); + width: fit-content; + + --radio-group-padding: 2px; + --radio-group-gap: 2px; + --radio-group-radius: var(--radius-sm); + --radio-group-border-width: 0.5px; + --radio-group-border-color: var(--border-weak-base); + --radio-group-bg: var(--surface-inset-base); + --radio-group-indicator-bg: var(--surface-raised-stronger-non-alpha); + --radio-group-indicator-shadow: var(--shadow-xs-border); [data-slot="radio-group-wrapper"] { all: unset; - background-color: var(--surface-base); - border-radius: var(--radius-md); + background-color: var(--radio-group-bg); + border: var(--radio-group-border-width) solid var(--radio-group-border-color); + border-radius: var(--radio-group-radius); box-shadow: var(--shadow-xs-border); + box-sizing: border-box; + display: inline-flex; + overflow: clip; margin: 0; - padding: 0; + padding: var(--radio-group-padding); position: relative; width: fit-content; } @@ -18,63 +32,52 @@ display: inline-flex; list-style: none; flex-direction: row; + gap: var(--radio-group-gap); + position: relative; + z-index: 1; } [data-slot="radio-group-indicator"] { - background: var(--button-secondary-base); - border-radius: var(--radius-md); - box-shadow: var(--shadow-xs-border); + background: var(--radio-group-indicator-bg); + border-radius: calc(var(--radio-group-radius) - var(--radio-group-padding)); + box-shadow: var(--radio-group-indicator-shadow); content: ""; opacity: var(--indicator-opacity, 1); position: absolute; transition: - opacity 300ms ease-in-out, - box-shadow 100ms ease-in-out, - width 150ms ease, - height 150ms ease, - transform 150ms ease; + opacity 200ms ease-out, + box-shadow 120ms ease-out, + width 200ms ease-out, + height 200ms ease-out, + transform 200ms ease-out; + will-change: transform, width, height; } [data-slot="radio-group-item"] { position: relative; } - /* Separator between items */ - [data-slot="radio-group-item"]:not(:first-of-type)::before { - background: var(--border-weak-base); - border-radius: var(--radius-xs); - content: ""; - inset: 6px 0; - position: absolute; - transition: opacity 150ms ease; - width: 1px; - transform: translateX(-0.5px); - } - - /* Hide separator when item or previous item is checked */ - [data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked])::before, - [data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked]) - + [data-slot="radio-group-item"]::before { - opacity: 0; - } - [data-slot="radio-group-item-label"] { color: var(--text-weak); font-family: var(--font-family-sans); font-size: var(--font-size-small); font-weight: var(--font-weight-medium); - border-radius: var(--radius-md); + border-radius: calc(var(--radio-group-radius) - var(--radio-group-padding) - 1px); cursor: pointer; - display: flex; + display: inline-flex; + align-items: center; + justify-content: center; flex-wrap: nowrap; gap: calc(var(--spacing) * 1); line-height: 1; - padding: 6px 12px; + min-height: 24px; + padding: 5px 10px; place-content: center; position: relative; - transition-duration: 150ms; - transition-property: color, opacity; - transition-timing-function: ease-in-out; + transition: + color 150ms ease-out, + background-color 150ms ease-out, + opacity 150ms ease-out; user-select: none; } @@ -101,6 +104,7 @@ [data-slot="radio-group-item-input"]:not([data-checked], [data-disabled]) + [data-slot="radio-group-item-label"]:hover { + background-color: var(--surface-inset-base-hover); color: var(--text-base); } @@ -112,7 +116,7 @@ /* Focus state */ [data-slot="radio-group-wrapper"]:has([data-slot="radio-group-item-input"]:focus-visible) [data-slot="radio-group-indicator"] { - box-shadow: var(--shadow-xs-border-focus); + box-shadow: var(--shadow-xs-border-focus), var(--radio-group-indicator-shadow); } /* Hide indicator when nothing is checked */ @@ -126,27 +130,16 @@ flex-direction: column; } - &[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before { - height: 1px; - width: auto; - inset: 0 6px; - transform: translateY(-0.5px); - } - /* Small size variant */ &[data-size="small"] { + --radio-group-padding: 1px; + --radio-group-gap: 1px; + [data-slot="radio-group-item-label"] { font-size: 12px; + min-height: 20px; padding: 4px 8px; } - - [data-slot="radio-group-item"]:not(:first-of-type)::before { - inset: 4px 0; - } - - &[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before { - inset: 0 4px; - } } /* Disabled root state */ From bfe875a651ad23a7d873ecbc5af5ca65decfccd9 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:05:27 -0600 Subject: [PATCH 3/5] chore: cleanup --- packages/app/src/components/prompt-input.tsx | 4 ++-- packages/ui/src/components/radio-group.css | 12 ++++++++---- packages/ui/src/components/radio-group.tsx | 5 ++++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index c1c9daf51a..38476a8e02 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1349,10 +1349,10 @@ export const PromptInput: Component = (props) => { "text-icon-weak": store.mode !== mode, }} /> - {mode === "shell" ? language.t("prompt.mode.shell") : "Prompt"} + {mode === "shell" ? language.t("prompt.mode.shell") : mode} )} - class="w-[68px] [&_[data-slot=radio-group-wrapper]]:w-full [&_[data-slot=radio-group-item]]:flex-1 [&_[data-slot=radio-group-item-label]]:h-6 [&_[data-slot=radio-group-item-label]]:p-0" + class="w-[68px] [&_[data-slot=radio-group-wrapper]]:h-7 [&_[data-slot=radio-group-wrapper]]:w-full [&_[data-slot=radio-group-wrapper]]:rounded-[4px] [&_[data-slot=radio-group-wrapper]]:border-[0.5px] [&_[data-slot=radio-group-wrapper]]:border-border-weak-base [&_[data-slot=radio-group-wrapper]]:bg-surface-inset-base [&_[data-slot=radio-group-wrapper]]:p-0 [&_[data-slot=radio-group-wrapper]]:shadow-none [&_[data-slot=radio-group-wrapper]]:overflow-visible [&_[data-slot=radio-group-items]]:h-full [&_[data-slot=radio-group-items]]:gap-1 [&_[data-slot=radio-group-indicator]]:rounded-[4px] [&_[data-slot=radio-group-indicator]]:bg-surface-raised-stronger-non-alpha [&_[data-slot=radio-group-indicator]]:shadow-[var(--shadow-xs-border)] [&_[data-slot=radio-group-item]]:h-full [&_[data-slot=radio-group-item]]:flex-1 [&_[data-slot=radio-group-item]]:p-0.5 [&_[data-slot=radio-group-item-label]]:size-full [&_[data-slot=radio-group-item-label]]:rounded-[2px] [&_[data-slot=radio-group-item-label]]:p-0" /> diff --git a/packages/ui/src/components/radio-group.css b/packages/ui/src/components/radio-group.css index 92e4918467..46bd8f701a 100644 --- a/packages/ui/src/components/radio-group.css +++ b/packages/ui/src/components/radio-group.css @@ -7,6 +7,7 @@ --radio-group-padding: 2px; --radio-group-gap: 2px; --radio-group-radius: var(--radius-sm); + --radio-group-transition-duration: 200ms; --radio-group-border-width: 0.5px; --radio-group-border-color: var(--border-weak-base); --radio-group-bg: var(--surface-inset-base); @@ -45,11 +46,14 @@ opacity: var(--indicator-opacity, 1); position: absolute; transition: - opacity 200ms ease-out, + opacity var(--radio-group-transition-duration) ease-out, box-shadow 120ms ease-out, - width 200ms ease-out, - height 200ms ease-out, - transform 200ms ease-out; + width var(--radio-group-transition-duration) ease-out, + height var(--radio-group-transition-duration) ease-out, + transform var(--radio-group-transition-duration) ease-out, + translate var(--radio-group-transition-duration) ease-out, + left var(--radio-group-transition-duration) ease-out, + top var(--radio-group-transition-duration) ease-out; will-change: transform, width, height; } diff --git a/packages/ui/src/components/radio-group.tsx b/packages/ui/src/components/radio-group.tsx index e1812d61a7..94145630e6 100644 --- a/packages/ui/src/components/radio-group.tsx +++ b/packages/ui/src/components/radio-group.tsx @@ -58,7 +58,10 @@ export function RadioGroup(props: RadioGroupProps) { onChange={(v) => local.onSelect?.(findOption(v))} >
- +
{(option) => ( From 19a1e1ed490f43cbd7a682a22bf2eb2111f5434a Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:23:09 -0600 Subject: [PATCH 4/5] chore: cleanup --- packages/app/src/components/prompt-input.tsx | 202 +++++++++---------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 38476a8e02..f0e269f69d 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1060,7 +1060,7 @@ export const PromptInput: Component = (props) => { removeLabel={language.t("prompt.attachment.remove")} />
(scrollRef = el)} onMouseDown={(e) => { const target = e.target @@ -1095,7 +1095,7 @@ export const PromptInput: Component = (props) => { onKeyDown={handleKeyDown} classList={{ "select-text": true, - "w-full pl-3 pr-2 pt-2 pb-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, + "w-full pl-3 pr-2 pt-2 pb-12 text-14-regular leading-[var(--line-height-large)] text-text-strong focus:outline-none whitespace-pre-wrap": true, "[&_[data-type=file]]:text-syntax-property": true, "[&_[data-type=agent]]:text-syntax-type": true, "font-mono!": store.mode === "shell", @@ -1103,123 +1103,123 @@ export const PromptInput: Component = (props) => { />
{placeholder()}
+
-
- { - const file = e.currentTarget.files?.[0] - if (file) addImageAttachment(file) - e.currentTarget.value = "" - }} - /> +
+ { + const file = e.currentTarget.files?.[0] + if (file) addImageAttachment(file) + e.currentTarget.value = "" + }} + /> -
+ + + + + + +
+ {language.t("prompt.action.stop")} + {language.t("common.key.esc")} +
+
+ +
+ {language.t("prompt.action.send")} + +
+
+ + } + > + +
+
+
+ + +
+
- - - -
- {language.t("prompt.action.stop")} - {language.t("common.key.esc")} -
-
- -
- {language.t("prompt.action.send")} - -
-
- - } - > - -
- - -
-
- - - -
-
-
-
+
From a32a46d219b1614ee07f1a6109c243ce5728a3ea Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:15:16 -0600 Subject: [PATCH 5/5] chore: cleanup --- packages/app/src/components/prompt-input.tsx | 1 - packages/ui/src/components/radio-group.css | 43 ++++++++++++++++++++ packages/ui/src/components/radio-group.tsx | 5 +-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index f0e269f69d..f9e4c4930e 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1352,7 +1352,6 @@ export const PromptInput: Component = (props) => { {mode === "shell" ? language.t("prompt.mode.shell") : mode}
)} - class="w-[68px] [&_[data-slot=radio-group-wrapper]]:h-7 [&_[data-slot=radio-group-wrapper]]:w-full [&_[data-slot=radio-group-wrapper]]:rounded-[4px] [&_[data-slot=radio-group-wrapper]]:border-[0.5px] [&_[data-slot=radio-group-wrapper]]:border-border-weak-base [&_[data-slot=radio-group-wrapper]]:bg-surface-inset-base [&_[data-slot=radio-group-wrapper]]:p-0 [&_[data-slot=radio-group-wrapper]]:shadow-none [&_[data-slot=radio-group-wrapper]]:overflow-visible [&_[data-slot=radio-group-items]]:h-full [&_[data-slot=radio-group-items]]:gap-1 [&_[data-slot=radio-group-indicator]]:rounded-[4px] [&_[data-slot=radio-group-indicator]]:bg-surface-raised-stronger-non-alpha [&_[data-slot=radio-group-indicator]]:shadow-[var(--shadow-xs-border)] [&_[data-slot=radio-group-item]]:h-full [&_[data-slot=radio-group-item]]:flex-1 [&_[data-slot=radio-group-item]]:p-0.5 [&_[data-slot=radio-group-item-label]]:size-full [&_[data-slot=radio-group-item-label]]:rounded-[2px] [&_[data-slot=radio-group-item-label]]:p-0" />
diff --git a/packages/ui/src/components/radio-group.css b/packages/ui/src/components/radio-group.css index 46bd8f701a..a9b880fd92 100644 --- a/packages/ui/src/components/radio-group.css +++ b/packages/ui/src/components/radio-group.css @@ -152,3 +152,46 @@ cursor: not-allowed; } } + +[data-component="prompt-mode-toggle"] [data-component="radio-group"] { + width: 68px; + + --radio-group-padding: 0; + --radio-group-gap: 4px; + --radio-group-radius: 4px; + + [data-slot="radio-group-wrapper"] { + height: 28px; + width: 100%; + box-shadow: none; + overflow: visible; + } + + [data-slot="radio-group-items"] { + height: 100%; + } + + [data-slot="radio-group-item"] { + display: flex; + flex: 1 1 0%; + height: 100%; + padding: 2px; + } + + [data-slot="radio-group-item-label"] { + height: 100%; + min-height: 100%; + padding: 0; + border-radius: 2px; + width: 100%; + } + + [data-slot="radio-group-indicator"] { + border-radius: 4px; + } + + [data-slot="radio-group-item-input"]:not([data-checked], [data-disabled]) + + [data-slot="radio-group-item-label"]:hover { + background-color: var(--surface-inset-base); + } +} diff --git a/packages/ui/src/components/radio-group.tsx b/packages/ui/src/components/radio-group.tsx index 94145630e6..e1812d61a7 100644 --- a/packages/ui/src/components/radio-group.tsx +++ b/packages/ui/src/components/radio-group.tsx @@ -58,10 +58,7 @@ export function RadioGroup(props: RadioGroupProps) { onChange={(v) => local.onSelect?.(findOption(v))} >
- +
{(option) => (