diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 91c7625903..f9e4c4930e 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" @@ -1059,7 +1060,7 @@ export const PromptInput: Component = (props) => { removeLabel={language.t("prompt.attachment.remove")} />
(scrollRef = el)} onMouseDown={(e) => { const target = e.target @@ -1094,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", @@ -1102,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")} - -
-
- - } - > - -
- - -
-
- - - -
-
-
-
+
@@ -1332,56 +1333,26 @@ export const PromptInput: Component = (props) => {
-
-
+ mode && setMode(mode)} + label={(mode) => ( +
+ + {mode === "shell" ? language.t("prompt.mode.shell") : mode} +
+ )} /> - -
diff --git a/packages/ui/src/components/radio-group.css b/packages/ui/src/components/radio-group.css index 3d672bb300..a9b880fd92 100644 --- a/packages/ui/src/components/radio-group.css +++ b/packages/ui/src/components/radio-group.css @@ -1,15 +1,30 @@ [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-transition-duration: 200ms; + --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 +33,55 @@ 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 var(--radio-group-transition-duration) ease-out, + box-shadow 120ms 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; } [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 +108,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 +120,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 +134,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 */ @@ -155,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/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,