refactor(ui): promote current design system (#43200)

This commit is contained in:
Luke Parker
2026-08-19 11:36:11 +10:00
committed by GitHub
parent 044d04df06
commit 3b8d3ca639
290 changed files with 3941 additions and 10093 deletions
@@ -279,7 +279,7 @@
justify-content: center;
flex-shrink: 0;
[data-component="icon"] {
[data-slot="icon-svg"] {
color: var(--task-agent-color, var(--v2-icon-icon-base));
}
}
@@ -36,7 +36,7 @@ export const lineCommentStyles = `
border: none;
}
[data-component="line-comment"] [data-component="icon"] {
[data-component="line-comment"] [data-slot="icon-svg"] {
color: var(--white);
}
@@ -411,7 +411,7 @@ export const LineCommentEditor = (props: LineCommentEditorProps) => {
<Button size="small" variant="ghost" onClick={split.onCancel}>
{split.cancelLabel ?? i18n.t("ui.common.cancel")}
</Button>
<Button size="small" variant="primary" disabled={split.value.trim().length === 0} onClick={submit}>
<Button size="small" variant="contrast" disabled={split.value.trim().length === 0} onClick={submit}>
{split.submitLabel ?? i18n.t("ui.lineComment.submit")}
</Button>
</Show>
@@ -12,9 +12,9 @@ import {
splitProps,
} from "solid-js"
import { isServer, render } from "solid-js/web"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { canReusePendingBlock, completedProjection } from "./markdown-projection"
import type { Block, Projection } from "./markdown-stream"
import {
@@ -133,20 +133,20 @@ function createCopyButton(labels: CopyLabels) {
function MarkdownCopyButton(props: { labels: CopyLabels; copied: boolean }) {
const label = () => (props.copied ? props.labels.copied : props.labels.copy)
return (
<TooltipV2 placement="top" value={label()}>
<IconButtonV2
<Tooltip placement="top" value={label()}>
<IconButton
type="button"
size="normal"
variant="ghost-muted"
aria-label={label()}
icon={
<>
<IconV2 name="outline-copy" data-copy-icon />
<IconV2 name="check" data-check-icon />
<Icon name="outline-copy" data-copy-icon />
<Icon name="check" data-check-icon />
</>
}
/>
</TooltipV2>
</Tooltip>
)
}
@@ -71,6 +71,13 @@
border-radius: var(--radius-sm);
}
[data-slot="message-nav-diff-bars"] {
display: block;
width: 18px;
height: 14px;
flex-shrink: 0;
}
[data-slot="message-nav-title-preview"] {
font-size: 14px; /* text-14-regular */
color: var(--text-base);
@@ -1,7 +1,6 @@
import type { UserMessage } from "../presentation"
import { HoverCard } from "@kobalte/core/hover-card"
import { ComponentProps, For, Match, Show, createSignal, splitProps, Switch } from "solid-js"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { ComponentProps, For, Match, Show, createMemo, createSignal, splitProps, Switch } from "solid-js"
import { useI18n } from "@opencode-ai/ui/context/i18n"
export function MessageNav(
@@ -51,7 +50,7 @@ export function MessageNav(
</Match>
<Match when={local.size === "normal"}>
<button data-slot="message-nav-message-button" onClick={handleClick} onKeyDown={handleKeyPress}>
<DiffChanges changes={message.summary?.diffs ?? []} variant="bars" />
<MessageDiffBars changes={message.summary?.diffs ?? []} />
<div
data-slot="message-nav-title-preview"
data-active={message.id === local.current?.id || undefined}
@@ -100,3 +99,47 @@ export function MessageNav(
</Switch>
)
}
function MessageDiffBars(props: { changes: { additions: number; deletions: number }[] }) {
const additions = createMemo(() => props.changes.reduce((total, diff) => total + diff.additions, 0))
const deletions = createMemo(() => props.changes.reduce((total, diff) => total + diff.deletions, 0))
const colors = createMemo(() => {
const added = additions()
const deleted = deletions()
if (added === 0 && deleted === 0) return Array(5).fill("var(--icon-weak-base)")
if (added + deleted < 5) {
return [
...Array(added > 0 ? 1 : 0).fill("var(--icon-diff-add-base)"),
...Array(deleted > 0 ? 1 : 0).fill("var(--icon-diff-delete-base)"),
...Array(5 - (added > 0 ? 1 : 0) - (deleted > 0 ? 1 : 0)).fill("var(--icon-weak-base)"),
]
}
const total = added + deleted
const ratio = added > deleted ? added / deleted : deleted / added
const colored = total < 20 || ratio < 4 ? 4 : 5
const addedRaw = (added / total) * colored
const deletedRaw = (deleted / total) * colored
const addedBars =
added === 0 ? 0 : Math.min(added <= 5 ? 1 : added <= 10 ? 2 : colored, Math.max(1, Math.round(addedRaw)))
const deletedBars =
deleted === 0 ? 0 : Math.min(deleted <= 5 ? 1 : deleted <= 10 ? 2 : colored, Math.max(1, Math.round(deletedRaw)))
const overflow = Math.max(0, addedBars + deletedBars - colored)
const adjustedAdded = overflow > 0 && addedRaw > deletedRaw ? addedBars - overflow : addedBars
const adjustedDeleted = overflow > 0 && addedRaw <= deletedRaw ? deletedBars - overflow : deletedBars
return [
...Array(adjustedAdded).fill("var(--icon-diff-add-base)"),
...Array(adjustedDeleted).fill("var(--icon-diff-delete-base)"),
...Array(5 - adjustedAdded - adjustedDeleted).fill("var(--icon-weak-base)"),
]
})
return (
<svg data-slot="message-nav-diff-bars" viewBox="0 0 18 14" aria-hidden="true">
<For each={colors().slice(0, 5)}>
{(color, index) => <rect x={index() * 4} width="2" height="14" rx="1" fill={color} />}
</For>
</svg>
)
}
@@ -94,7 +94,7 @@
justify-content: center;
color: var(--icon-weak);
[data-component="icon"] {
[data-slot="icon-svg"] {
width: 20px;
height: 20px;
}
@@ -172,7 +172,7 @@
transition: opacity 0.15s ease;
will-change: opacity;
[data-component="tooltip-trigger"] {
[data-component="tooltip-v2-trigger"] {
display: inline-flex;
width: fit-content;
}
@@ -240,7 +240,7 @@
transition: opacity 0.15s ease;
will-change: opacity;
[data-component="tooltip-trigger"] {
[data-component="tooltip-v2-trigger"] {
display: inline-flex;
width: fit-content;
}
@@ -766,7 +766,7 @@
justify-content: center;
}
[data-slot="permission-icon"] [data-component="icon"] {
[data-slot="permission-icon"] [data-slot="icon-svg"] {
color: var(--icon-warning-base);
}
@@ -833,7 +833,7 @@
align-items: center;
gap: 8px;
[data-component="button"] {
[data-component="button-v2"] {
padding-left: 12px;
padding-right: 12px;
}
@@ -1024,7 +1024,7 @@
background-color 0.15s ease,
border-color 0.15s ease;
[data-component="icon"] {
[data-slot="icon-svg"] {
opacity: 0;
color: var(--icon-base);
}
@@ -1044,7 +1044,7 @@
&[data-picked="true"] {
border-color: var(--icon-interactive-base);
[data-component="icon"] {
[data-slot="icon-svg"] {
opacity: 1;
color: var(--icon-invert-base);
}
@@ -1170,7 +1170,7 @@
opacity: 0.6;
}
[data-component="icon"] {
[data-slot="icon-svg"] {
transform: rotate(180deg);
}
}
@@ -1331,7 +1331,7 @@
line-height: var(--line-height-large);
color: var(--v2-text-text-muted);
[data-component="icon"] {
[data-slot="icon-svg"] {
flex-shrink: 0;
color: var(--icon-weak);
}
@@ -39,7 +39,7 @@ import { Accordion } from "@opencode-ai/ui/accordion"
import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon"
import { Icon, type IconProps } from "@opencode-ai/ui/icon"
import { ToolErrorCard } from "./tool-error-card"
import { Checkbox } from "@opencode-ai/ui/checkbox"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
@@ -51,10 +51,7 @@ import { CommentCardV2 } from "../v2/components/comment-card-v2"
import { checksum } from "@opencode-ai/util/encode"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Button } from "@opencode-ai/ui/button"
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
import { AnimatedCountList } from "./tool-count-summary"
import { ToolStatusTitle } from "./tool-status-title"
@@ -213,34 +210,17 @@ function MessageActionButton(
) {
const icon = () => (props.icon === "copy" ? "outline-copy" : props.icon)
return (
<Show
when={props.useV2}
fallback={
<Tooltip value={props.label} placement="top" gutter={4}>
<IconButton
icon={props.icon}
size="normal"
variant="ghost"
disabled={props.disabled}
onMouseDown={props.onMouseDown}
onClick={props.onClick}
aria-label={props["aria-label"]}
/>
</Tooltip>
}
>
<TooltipV2 value={props.label} placement="top" gutter={4}>
<IconButtonV2
icon={<IconV2 name={icon()} size="small" />}
size="normal"
variant="ghost-muted"
disabled={props.disabled}
onMouseDown={props.onMouseDown}
onClick={props.onClick}
aria-label={props["aria-label"]}
/>
</TooltipV2>
</Show>
<Tooltip appearance={props.useV2 ? "compact" : "standard"} value={props.label} placement="top" gutter={4}>
<IconButton
icon={<Icon name={icon()} size="small" />}
size="normal"
variant="ghost-muted"
disabled={props.disabled}
onMouseDown={props.onMouseDown}
onClick={props.onClick}
aria-label={props["aria-label"]}
/>
</Tooltip>
)
}
@@ -363,7 +343,6 @@ function displayDirectory(path: string | undefined) {
return relativizeProjectPath(getDirectory(path), data.directory)
}
import type { IconProps } from "@opencode-ai/ui/icon"
import { normalize, resolveFileDiff } from "./session-diff"
export type ToolInfo = {
@@ -1191,9 +1170,9 @@ function UserMessageComments(props: { comments: UserMessageComment[]; bounded: b
)}
</For>
<Show when={props.bounded && props.comments.length > 5 && !state.expanded}>
<ButtonV2 size="small" variant="ghost-muted" onClick={() => setState("expanded", true)}>
<Button size="small" variant="ghost-muted" onClick={() => setState("expanded", true)}>
{i18n.t("ui.common.showMore")}
</ButtonV2>
</Button>
</Show>
</div>
)
@@ -2144,16 +2123,16 @@ function ConsoleOutput(props: { copy: string; children: JSX.Element }) {
return (
<div data-component="bash-output" dir="ltr">
<div data-slot="bash-copy">
<TooltipV2 value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top">
<IconButtonV2
icon={<IconV2 name={copied() ? "check" : "outline-copy"} size="small" />}
<Tooltip value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top">
<IconButton
icon={<Icon name={copied() ? "check" : "outline-copy"} size="small" />}
size="normal"
variant="ghost-muted"
onMouseDown={(event) => event.preventDefault()}
onClick={copy}
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
/>
</TooltipV2>
</Tooltip>
</div>
<div
data-slot="bash-scroll"
@@ -2350,7 +2329,7 @@ ToolRegistry.register({
</div>
<div data-slot="message-part-actions">
<Show when={!pending() && props.metadata.filediff}>
<DiffChanges changes={props.metadata.filediff} />
<DiffChanges appearance="standard" changes={props.metadata.filediff} />
</Show>
</div>
</div>
@@ -2361,7 +2340,7 @@ ToolRegistry.register({
path={path()}
actions={
<Show when={!pending() && props.metadata.filediff}>
<DiffChanges changes={props.metadata.filediff!} />
<DiffChanges appearance="standard" changes={props.metadata.filediff!} />
</Show>
}
>
@@ -2543,7 +2522,10 @@ ToolRegistry.register({
</span>
</Match>
<Match when={true}>
<DiffChanges changes={{ additions: file.additions, deletions: file.deletions }} />
<DiffChanges
appearance="standard"
changes={{ additions: file.additions, deletions: file.deletions }}
/>
</Match>
</Switch>
<Icon name="chevron-grabber-vertical" size="small" />
@@ -2599,7 +2581,10 @@ ToolRegistry.register({
</div>
<div data-slot="message-part-actions">
<Show when={!pending()}>
<DiffChanges changes={{ additions: single()!.additions, deletions: single()!.deletions }} />
<DiffChanges
appearance="standard"
changes={{ additions: single()!.additions, deletions: single()!.deletions }}
/>
</Show>
</div>
</div>
@@ -2625,7 +2610,10 @@ ToolRegistry.register({
</span>
</Match>
<Match when={true}>
<DiffChanges changes={{ additions: single()!.additions, deletions: single()!.deletions }} />
<DiffChanges
appearance="standard"
changes={{ additions: single()!.additions, deletions: single()!.deletions }}
/>
</Match>
</Switch>
}
@@ -58,7 +58,7 @@ export function SessionRetry(props: { status: SessionStatus; show?: boolean }) {
<Spinner class="size-4 mt-0.5" />
<div class="min-w-0">
<Show when={truncated()} fallback={<div data-slot="session-turn-retry-message">{message()}</div>}>
<Tooltip value={retry()?.message ?? ""} placement="top">
<Tooltip appearance="standard" value={retry()?.message ?? ""} placement="top">
<div data-slot="session-turn-retry-message" class="cursor-help truncate">
{message()}
</div>
@@ -45,20 +45,6 @@
column-gap: 12px;
}
[data-slot="session-review-actions"] [data-component="radio-group"] {
[data-slot="radio-group-wrapper"],
[data-slot="radio-group-indicator"],
[data-slot="radio-group-item-control"] {
border-radius: 6px;
}
[data-slot="radio-group-item-input"]:not([data-checked], [data-disabled])
+ [data-slot="radio-group-item-label"]:hover
[data-slot="radio-group-item-control"] {
border-radius: 4px;
}
}
[data-component="sticky-accordion-header"] {
--sticky-accordion-top: 0px;
}
@@ -237,3 +223,14 @@
margin-top: 10px;
}
}
[data-slot="segmented-control-v2"].session-review-diff-style {
width: fit-content;
height: 24px;
[data-slot="segmented-control-v2-item"] {
height: 24px;
padding: 0 8px;
font-size: 12px;
}
}
@@ -1,7 +1,7 @@
import { Accordion } from "@opencode-ai/ui/accordion"
import { Button } from "@opencode-ai/ui/button"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { RadioGroup } from "@opencode-ai/ui/radio-group"
import { Menu } from "@opencode-ai/ui/menu"
import { SegmentedControl, SegmentedControlItem } from "@opencode-ai/ui/segmented-control"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon"
@@ -127,26 +127,22 @@ function ReviewCommentMenu(props: {
}) {
return (
<div onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
<DropdownMenu gutter={4} placement="bottom-end">
<DropdownMenu.Trigger
<Menu appearance="standard" gutter={4} placement="bottom-end">
<Menu.Trigger
as={IconButton}
icon="dot-grid"
icon={<Icon name="dot-grid" />}
variant="ghost"
size="small"
class="size-6 rounded-md"
aria-label={props.labels.moreLabel}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item onSelect={props.onEdit}>
<DropdownMenu.ItemLabel>{props.labels.editLabel}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={props.onDelete}>
<DropdownMenu.ItemLabel>{props.labels.deleteLabel}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
<Menu.Portal>
<Menu.Content>
<Menu.Item onSelect={props.onEdit}>{props.labels.editLabel}</Menu.Item>
<Menu.Item onSelect={props.onDelete}>{props.labels.deleteLabel}</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
</div>
)
}
@@ -345,16 +341,24 @@ export const SessionReview = (props: SessionReviewProps) => {
</div>
<div data-slot="session-review-actions">
<Show when={hasDiffs() && props.onDiffStyleChange}>
<RadioGroup
options={["unified", "split"] as const}
current={diffStyle()}
size="small"
value={(style) => style}
label={(style) =>
i18n.t(style === "unified" ? "ui.sessionReview.diffStyle.unified" : "ui.sessionReview.diffStyle.split")
}
onSelect={(style) => style && props.onDiffStyleChange?.(style)}
/>
<SegmentedControl
class="session-review-diff-style"
value={diffStyle()}
onChange={(style) => {
if (style !== "unified" && style !== "split") return
props.onDiffStyleChange?.(style)
}}
>
<For each={["unified", "split"] as const}>
{(style) => (
<SegmentedControlItem value={style}>
{i18n.t(
style === "unified" ? "ui.sessionReview.diffStyle.unified" : "ui.sessionReview.diffStyle.split",
)}
</SegmentedControlItem>
)}
</For>
</SegmentedControl>
</Show>
<Show when={hasDiffs()}>
<Button
@@ -521,7 +525,7 @@ export const SessionReview = (props: SessionReviewProps) => {
</Show>
<span data-slot="session-review-filename">{getFilename(file)}</span>
<Show when={props.onViewFile && diffCanRender()}>
<Tooltip value={openFileLabel()} placement="top" gutter={4}>
<Tooltip appearance="standard" value={openFileLabel()} placement="top" gutter={4}>
<button
data-slot="session-review-view-button"
type="button"
@@ -544,7 +548,7 @@ export const SessionReview = (props: SessionReviewProps) => {
<span data-slot="session-review-change" data-type="added">
{i18n.t("ui.sessionReview.change.added")}
</span>
<DiffChanges changes={diff()} />
<DiffChanges appearance="standard" changes={diff()} />
</div>
</Match>
<Match when={isDeleted()}>
@@ -558,7 +562,7 @@ export const SessionReview = (props: SessionReviewProps) => {
</span>
</Match>
<Match when={true}>
<DiffChanges changes={diff()} />
<DiffChanges appearance="standard" changes={diff()} />
</Match>
</Switch>
<Show when={diffCanRender()}>
@@ -602,7 +606,7 @@ export const SessionReview = (props: SessionReviewProps) => {
<div data-slot="session-review-large-diff-actions">
<Button
size="normal"
variant="secondary"
variant="neutral"
onClick={() => setStore("force", file, true)}
>
{i18n.t("ui.sessionReview.largeDiff.renderAnyway")}
@@ -437,7 +437,7 @@ export function SessionTurn(
<span data-slot="session-turn-diffs-label">
{i18n.plural("ui.sessionTurn.diffs.changed", edited())}
</span>
<DiffChanges changes={diffs()} />
<DiffChanges appearance="standard" changes={diffs()} />
<Show when={overflow() > 0}>
<span data-slot="session-turn-diffs-toggle" onClick={toggleAll}>
{showAll() ? i18n.t("ui.sessionTurn.diffs.showLess") : i18n.t("ui.sessionTurn.diffs.showAll")}
@@ -490,7 +490,7 @@ export function SessionTurn(
</span>
<div data-slot="session-turn-diff-meta">
<span data-slot="session-turn-diff-changes">
<DiffChanges changes={diff} />
<DiffChanges appearance="standard" changes={diff} />
</span>
<span data-slot="session-turn-diff-chevron">
<Icon name="chevron-down" size="small" />
@@ -23,7 +23,7 @@
gap: 4px;
}
[data-component="tool-error-card-icon"] [data-component="icon"] {
[data-component="tool-error-card-icon"] [data-slot="icon-svg"] {
color: var(--card-accent);
}
@@ -136,12 +136,13 @@ export function ToolErrorCard(props: ToolErrorCardProps) {
<Show when={open()}>
<div data-slot="tool-error-card-copy">
<Tooltip
appearance="standard"
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.toolErrorCard.copyError")}
placement="top"
gutter={4}
>
<IconButton
icon={copied() ? "check" : "copy"}
icon={<Icon name={copied() ? "check" : "copy"} />}
size="normal"
variant="ghost"
onMouseDown={(e) => e.preventDefault()}
@@ -1,7 +1,7 @@
import { Collapsible } from "@kobalte/core/collapsible"
import { type ComponentProps, type JSX, For, Show, createMemo, splitProps } from "solid-js"
import { DiffChanges } from "@opencode-ai/ui/v2/diff-changes-v2"
import { TextShimmerV2 } from "@opencode-ai/ui/v2/text-shimmer-v2"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
import "./basic-tool-v2.css"
function ChevronIcon() {
@@ -91,7 +91,7 @@ export function BasicToolV2(props: BasicToolV2Props) {
{(title) => (
<>
<span data-slot="basic-tool-v2-title">
<TextShimmerV2 text={title().title} active={pending()} />
<TextShimmer text={title().title} active={pending()} />
</span>
<Show when={!pending() && title().subtitle}>
<span data-slot="basic-tool-v2-sep" aria-hidden="true">
@@ -1,7 +1,7 @@
import { createSignal, onCleanup, onMount, Show } from "solid-js"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { getFilenameTruncated } from "@opencode-ai/util/path"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { AttachmentCardV2 } from "./attachment-card-v2"
export function CommentCardV2(props: {
@@ -30,7 +30,7 @@ export function CommentCardV2(props: {
})
return (
<TooltipV2
<Tooltip
placement="top"
openDelay={1000}
value={props.title ?? props.comment}
@@ -59,6 +59,6 @@ export function CommentCardV2(props: {
</Show>
</span>
</AttachmentCardV2>
</TooltipV2>
</Tooltip>
)
}
@@ -10,8 +10,7 @@ import {
} from "../../components/line-comment-annotations"
import { useI18n } from "@opencode-ai/ui/context/i18n"
import { cloneSelectedLineRange, formatSelectedLineLabel } from "../../pierre/selection-bridge"
import { LineCommentEditorV2, LineCommentV2 } from "@opencode-ai/ui/v2/line-comment-v2"
import type { LineCommentEditorV2Mention } from "@opencode-ai/ui/v2/line-comment-v2"
import { LineCommentEditor, LineComment, type LineCommentEditorMention } from "@opencode-ai/ui/line-comment"
type LineCommentControllerV2Props<T extends LineCommentShape> = {
comments: Accessor<T[]>
@@ -24,7 +23,7 @@ type LineCommentControllerV2Props<T extends LineCommentShape> = {
onDelete?: (comment: T) => void
renderCommentActions?: (comment: T, controls: { edit: VoidFunction; remove: VoidFunction }) => JSX.Element
editSubmitLabel?: string
mention?: LineCommentEditorV2Mention
mention?: LineCommentEditorMention
}
type CommentProps = {
@@ -45,7 +44,7 @@ type DraftProps = {
onSubmit: (value: string) => void
cancelLabel?: string
submitLabel?: string
mention?: LineCommentEditorV2Mention
mention?: LineCommentEditorMention
}
function lineCommentElementV2(view: Accessor<CommentProps>) {
@@ -60,12 +59,12 @@ function lineCommentElementV2(view: Accessor<CommentProps>) {
onClick={view().onClick}
onMouseEnter={view().onMouseEnter}
>
<LineCommentV2 comment={view().comment} selection={view().selection} actions={view().actions} />
<LineComment comment={view().comment} selection={view().selection} actions={view().actions} />
</div>
}
>
<div data-prevent-autofocus="" data-comment-id={view().id} onMouseDown={(event) => event.stopPropagation()}>
<LineCommentEditorV2
<LineCommentEditor
value={view().editor!.value}
selection={view().editor!.selection}
onInput={view().editor!.onInput}
@@ -83,7 +82,7 @@ function lineCommentElementV2(view: Accessor<CommentProps>) {
function lineCommentDraftElementV2(view: Accessor<DraftProps>) {
return (
<div data-prevent-autofocus="" onMouseDown={(event) => event.stopPropagation()}>
<LineCommentEditorV2
<LineCommentEditor
value={view().value}
selection={view().selection}
onInput={view().onInput}
@@ -4,12 +4,10 @@ import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { useI18n } from "@opencode-ai/ui/context/i18n"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Button } from "@opencode-ai/ui/button"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Menu } from "@opencode-ai/ui/menu"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { AttachmentCardV2 } from "../attachment-card-v2"
import { CommentCardV2 } from "../comment-card-v2"
import { typeLabel } from "../../../components/message-file"
@@ -403,7 +401,7 @@ export function PromptInputV2Attachments(props: {
<For each={props.comments ?? []}>
{(comment) => (
<div class="relative group shrink-0">
<TooltipV2
<Tooltip
value={comment.comment}
placement="top"
openDelay={800}
@@ -416,14 +414,14 @@ export function PromptInputV2Attachments(props: {
active={comment.key === props.activeCommentID}
onClick={() => props.onCommentClick?.(comment)}
/>
</TooltipV2>
</Tooltip>
<button
type="button"
onClick={() => props.onCommentRemove?.(comment)}
class="absolute -top-1 -end-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
aria-label={props.removeLabel}
>
<IconV2 name="outline-xmark" class="text-v2-icon-icon-contrast" />
<Icon name="outline-xmark" class="text-v2-icon-icon-contrast" />
</button>
</div>
)}
@@ -431,7 +429,7 @@ export function PromptInputV2Attachments(props: {
<For each={props.attachments}>
{(attachment) => (
<div class="relative group shrink-0">
<TooltipV2 value={attachment.filename} placement="top" contentClass="break-all">
<Tooltip value={attachment.filename} placement="top" contentClass="break-all">
<Show
when={attachment.mime.startsWith("image/")}
fallback={
@@ -448,14 +446,14 @@ export function PromptInputV2Attachments(props: {
/>
<div class="absolute inset-0 rounded-[6px] shadow-[inset_0_0_0_0.5px_var(--v2-border-border-base)] pointer-events-none" />
</Show>
</TooltipV2>
</Tooltip>
<button
type="button"
onClick={() => props.onAttachmentRemove(attachment)}
class="absolute -top-1 -end-1 size-4 rounded-full bg-v2-icon-icon-muted outline-solid outline-1 outline-v2-icon-icon-contrast flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
aria-label={props.removeLabel}
>
<IconV2 name="outline-xmark" class="text-v2-icon-icon-contrast" />
<Icon name="outline-xmark" class="text-v2-icon-icon-contrast" />
</button>
</div>
)}
@@ -489,45 +487,45 @@ export function PromptInputV2AddMenu(props: {
onShell: () => void
}) {
return (
<TooltipV2
<Tooltip
placement="top"
value={
<>
{props.title}
<KeybindV2 keys={props.keybind ?? []} variant="neutral" />
<Keybind keys={props.keybind ?? []} variant="neutral" />
</>
}
>
<MenuV2 gutter={6} modal={false} placement="top-start">
<MenuV2.Trigger
as={IconButtonV2}
<Menu gutter={6} modal={false} placement="top-start">
<Menu.Trigger
as={IconButton}
data-action="prompt-attach"
type="button"
icon={<IconV2 name="plus" />}
icon={<Icon name="plus" />}
variant="ghost-muted"
size="large"
disabled={props.disabled}
aria-label={props.title}
/>
<MenuV2.Portal>
<MenuV2.Content style={{ "min-width": "180px" }}>
<MenuV2.Item onSelect={props.onAttach} shortcut={props.attachShortcut}>
<Menu.Portal>
<Menu.Content style={{ "min-width": "180px" }}>
<Menu.Item onSelect={props.onAttach} shortcut={props.attachShortcut}>
{props.attachLabel}
</MenuV2.Item>
<MenuV2.Separator />
<MenuV2.Item onSelect={props.onCommands} shortcut="/">
</Menu.Item>
<Menu.Separator />
<Menu.Item onSelect={props.onCommands} shortcut="/">
{props.commandsLabel}
</MenuV2.Item>
<MenuV2.Item onSelect={props.onContext} shortcut="@">
</Menu.Item>
<Menu.Item onSelect={props.onContext} shortcut="@">
{props.contextLabel}
</MenuV2.Item>
<MenuV2.Item onSelect={props.onShell} shortcut="!">
</Menu.Item>
<Menu.Item onSelect={props.onShell} shortcut="!">
{props.shellLabel}
</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</TooltipV2>
</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
</Tooltip>
)
}
@@ -566,18 +564,18 @@ export function PromptInputV2Select(props: {
onSelect: (id: string) => void
}) {
return (
<TooltipV2
<Tooltip
placement="top"
value={
<>
{props.title}
<KeybindV2 keys={props.keybind ?? []} variant="neutral" />
<Keybind keys={props.keybind ?? []} variant="neutral" />
</>
}
>
<MenuV2 gutter={6} modal={false} placement="top-start" onOpenChange={props.onOpenChange}>
<MenuV2.Trigger
as={ButtonV2}
<Menu gutter={6} modal={false} placement="top-start" onOpenChange={props.onOpenChange}>
<Menu.Trigger
as={Button}
variant="ghost-muted"
size="normal"
class={`max-w-[220px] justify-start ![font-weight:440] ${props.class ?? ""}`}
@@ -588,24 +586,24 @@ export function PromptInputV2Select(props: {
{props.options.find((option) => option.id === props.current)?.label ?? props.current}
</span>
<span class="-ms-0.5 -me-1 flex shrink-0">
<IconV2 name="chevron-down" />
<Icon name="chevron-down" />
</span>
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.RadioGroup value={props.current} onChange={props.onSelect}>
</Menu.Trigger>
<Menu.Portal>
<Menu.Content>
<Menu.RadioGroup value={props.current} onChange={props.onSelect}>
<For each={props.options}>
{(option) => (
<MenuV2.RadioItem value={option.id} class="capitalize" closeOnSelect>
<Menu.RadioItem value={option.id} class="capitalize" closeOnSelect>
{option.label}
</MenuV2.RadioItem>
</Menu.RadioItem>
)}
</For>
</MenuV2.RadioGroup>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
</TooltipV2>
</Menu.RadioGroup>
</Menu.Content>
</Menu.Portal>
</Menu>
</Tooltip>
)
}
@@ -687,7 +685,7 @@ export function PromptInputV2SubmitButton(props: {
onStop: () => void
}) {
return (
<TooltipV2
<Tooltip
placement="top"
inactive={!props.stopping && props.disabled}
value={props.stopping ? props.stopLabel : props.sendLabel}
@@ -697,8 +695,8 @@ export function PromptInputV2SubmitButton(props: {
type="button"
disabled={!props.stopping && props.disabled}
tabIndex={props.mode === "normal" ? undefined : -1}
icon={props.stopping ? "stop" : props.mode === "shell" ? "arrow-undo-down" : "arrow-up"}
variant="primary"
icon={<Icon name={props.stopping ? "stop" : props.mode === "shell" ? "arrow-undo-down" : "arrow-up"} />}
variant="contrast"
class="size-7 rounded-md p-[6px] shadow-[var(--v2-elevation-button-contrast)] disabled:opacity-50"
classList={{
"text-v2-text-text-contrast": !!props.accent && !props.stopping && !props.disabled,
@@ -721,7 +719,7 @@ export function PromptInputV2SubmitButton(props: {
props.onSubmit()
}}
/>
</TooltipV2>
</Tooltip>
)
}
@@ -1,5 +1,5 @@
import { useI18n } from "@opencode-ai/ui/context/i18n"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { Icon } from "@opencode-ai/ui/icon"
import "./session-review-v2.css"
export function SessionReviewEmptyChangesV2() {
@@ -1,6 +1,6 @@
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { useI18n } from "@opencode-ai/ui/context/i18n"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Button } from "@opencode-ai/ui/button"
import "./session-review-v2.css"
export type SessionReviewEmptyNoGitV2Props = {
@@ -18,11 +18,11 @@ export function SessionReviewEmptyNoGitV2(props: SessionReviewEmptyNoGitV2Props)
<div data-slot="session-review-v2-empty-no-git-description">
{i18n.t("ui.sessionReviewV2.empty.noGit.description")}
</div>
<ButtonV2 variant="neutral" size="normal" disabled={props.pending} onClick={props.onInitGit}>
<Button variant="neutral" size="normal" disabled={props.pending} onClick={props.onInitGit}>
{props.pending
? i18n.t("ui.sessionReviewV2.empty.noGit.actionLoading")
: i18n.t("ui.sessionReviewV2.empty.noGit.action")}
</ButtonV2>
</Button>
</div>
)
}
@@ -1,6 +1,6 @@
import { getDirectory, getFilename } from "@opencode-ai/util/path"
import type { SelectedLineRange } from "@pierre/diffs"
import { DiffChanges } from "@opencode-ai/ui/v2/diff-changes-v2"
import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { useFileComponent } from "@opencode-ai/ui/context/file"
import { useI18n } from "@opencode-ai/ui/context/i18n"
@@ -24,8 +24,8 @@ import type {
import type { SessionReviewExpandMode } from "./session-review-v2"
import { createLineCommentControllerV2 } from "./line-comment-annotations-v2"
import { shouldVirtualizeReviewDiff } from "./session-review-file-preview-v2-virtualize"
import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { LineCommentOverflowIcon } from "@opencode-ai/ui/line-comment"
import { Menu } from "@opencode-ai/ui/menu"
import "./session-review-v2.css"
type ReviewDiff = (PresentationFileDiff & { file: string }) | FileDiffInfo
@@ -68,29 +68,29 @@ function selectionPreview(diff: ViewDiff, range: SelectedLineRange) {
return previewSelectedLines(contents, range)
}
function ReviewCommentMenuV2(props: {
function ReviewCommentMenu(props: {
labels: SessionReviewCommentActions
onEdit: VoidFunction
onDelete: VoidFunction
}) {
return (
<div onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
<MenuV2 gutter={4}>
<MenuV2.Trigger
<Menu gutter={4}>
<Menu.Trigger
as="button"
type="button"
data-slot="line-comment-v2-overflow"
aria-label={props.labels.moreLabel}
>
<LineCommentV2OverflowIcon />
</MenuV2.Trigger>
<MenuV2.Portal>
<MenuV2.Content>
<MenuV2.Item onSelect={props.onEdit}>{props.labels.editLabel}</MenuV2.Item>
<MenuV2.Item onSelect={props.onDelete}>{props.labels.deleteLabel}</MenuV2.Item>
</MenuV2.Content>
</MenuV2.Portal>
</MenuV2>
<LineCommentOverflowIcon />
</Menu.Trigger>
<Menu.Portal>
<Menu.Content>
<Menu.Item onSelect={props.onEdit}>{props.labels.editLabel}</Menu.Item>
<Menu.Item onSelect={props.onDelete}>{props.labels.deleteLabel}</Menu.Item>
</Menu.Content>
</Menu.Portal>
</Menu>
</div>
)
}
@@ -156,7 +156,7 @@ export function SessionReviewFilePreviewV2(props: SessionReviewFilePreviewV2Prop
editSubmitLabel: props.lineCommentActions?.saveLabel,
renderCommentActions: props.lineCommentActions
? (comment, controls) => (
<ReviewCommentMenuV2 labels={props.lineCommentActions!} onEdit={controls.edit} onDelete={controls.remove} />
<ReviewCommentMenu labels={props.lineCommentActions!} onEdit={controls.edit} onDelete={controls.remove} />
)
: undefined,
})
@@ -1,11 +1,10 @@
import { IconButton } from "@opencode-ai/ui/icon-button"
import { useI18n } from "@opencode-ai/ui/context/i18n"
import { SegmentedControlItemV2, SegmentedControlV2 } from "@opencode-ai/ui/v2/segmented-control-v2"
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2"
import { Icon } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { SegmentedControlItem, SegmentedControl } from "@opencode-ai/ui/segmented-control"
import { TextInput } from "@opencode-ai/ui/text-input"
import { Keybind } from "@opencode-ai/ui/keybind"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import type { SessionReviewDiffStyle } from "../../components/session-review"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { ScrollView } from "@opencode-ai/ui/scroll-view"
@@ -87,7 +86,7 @@ export function SessionReviewV2Sidebar(props: SessionReviewV2SidebarProps) {
{props.stats}
</div>
<div data-slot="session-review-v2-sidebar-filter">
<TextInputV2
<TextInput
type="search"
value={props.filter}
onInput={(event) => props.onFilterChange(event.currentTarget.value)}
@@ -219,18 +218,18 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
</div>
</Show>
<div class="flex items-center">
<TooltipV2
<Tooltip
openDelay={2000}
inactive={!prev()}
value={
<>
{i18n.t("ui.sessionReviewV2.previousFile")}
<KeybindV2 keys={[locale.direction() === "rtl" ? "→" : "←"]} variant="neutral" />
<Keybind keys={[locale.direction() === "rtl" ? "→" : "←"]} variant="neutral" />
</>
}
>
<IconButton
icon="arrow-left"
icon={<Icon name="arrow-left" />}
variant="ghost"
size="small"
class="session-review-v2-file-nav-button"
@@ -238,19 +237,19 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
onClick={() => cycle(prev())}
aria-label={i18n.t("ui.sessionReviewV2.previousFile")}
/>
</TooltipV2>
<TooltipV2
</Tooltip>
<Tooltip
openDelay={2000}
inactive={!next()}
value={
<>
{i18n.t("ui.sessionReviewV2.nextFile")}
<KeybindV2 keys={[locale.direction() === "rtl" ? "←" : "→"]} variant="neutral" />
<Keybind keys={[locale.direction() === "rtl" ? "←" : "→"]} variant="neutral" />
</>
}
>
<IconButton
icon="arrow-right"
icon={<Icon name="arrow-right" />}
variant="ghost"
size="small"
class="session-review-v2-file-nav-button"
@@ -258,14 +257,14 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
onClick={() => cycle(next())}
aria-label={i18n.t("ui.sessionReviewV2.nextFile")}
/>
</TooltipV2>
</Tooltip>
</div>
</>
)
const toolbarEnd = () => (
<>
<SegmentedControlV2
<SegmentedControl
value={props.expandMode}
onChange={(value) => {
if (value !== "expand" && value !== "collapse") return
@@ -274,19 +273,19 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
aria-label={i18n.t("ui.sessionReviewV2.expandMode")}
>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.showAllLines")}>
<SegmentedControlItemV2 value="expand" aria-label={i18n.t("ui.sessionReviewV2.showAllLines")}>
<Tooltip openDelay={2000} value={i18n.t("ui.sessionReviewV2.showAllLines")}>
<SegmentedControlItem value="expand" aria-label={i18n.t("ui.sessionReviewV2.showAllLines")}>
<Icon name="expand" />
</SegmentedControlItemV2>
</TooltipV2>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<SegmentedControlItemV2 value="collapse" aria-label={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
</SegmentedControlItem>
</Tooltip>
<Tooltip openDelay={2000} value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<SegmentedControlItem value="collapse" aria-label={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
<Icon name="collapse" />
</SegmentedControlItemV2>
</TooltipV2>
</SegmentedControlV2>
</SegmentedControlItem>
</Tooltip>
</SegmentedControl>
<Show when={props.onDiffStyleChange}>
<SegmentedControlV2
<SegmentedControl
value={props.diffStyle}
onChange={(value) => {
if (value !== "unified" && value !== "split") return
@@ -295,17 +294,17 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
aria-label={i18n.t("ui.sessionReviewV2.diffView")}
>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<SegmentedControlItemV2 value="unified" aria-label={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<Tooltip openDelay={2000} value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<SegmentedControlItem value="unified" aria-label={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
<Icon name="unified" />
</SegmentedControlItemV2>
</TooltipV2>
<TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.splitDiff")}>
<SegmentedControlItemV2 value="split" aria-label={i18n.t("ui.sessionReviewV2.splitDiff")}>
</SegmentedControlItem>
</Tooltip>
<Tooltip openDelay={2000} value={i18n.t("ui.sessionReviewV2.splitDiff")}>
<SegmentedControlItem value="split" aria-label={i18n.t("ui.sessionReviewV2.splitDiff")}>
<Icon name="split" />
</SegmentedControlItemV2>
</TooltipV2>
</SegmentedControlV2>
</SegmentedControlItem>
</Tooltip>
</SegmentedControl>
</Show>
</>
)
@@ -330,8 +329,8 @@ export function SessionReviewV2SidebarToggle(props: { opened: boolean; disabled?
const i18n = useI18n()
return (
<TooltipV2 value={i18n.t("ui.sessionReviewV2.toggleSidebar")}>
<IconButtonV2
<Tooltip value={i18n.t("ui.sessionReviewV2.toggleSidebar")}>
<IconButton
variant="ghost"
size="small"
class="session-review-v2-sidebar-toggle"
@@ -342,6 +341,6 @@ export function SessionReviewV2SidebarToggle(props: { opened: boolean; disabled?
onClick={props.onToggle}
icon={<Icon name="filetree" />}
/>
</TooltipV2>
</Tooltip>
)
}
@@ -1,5 +1,5 @@
import { createSignal } from "solid-js"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { Button } from "@opencode-ai/ui/button"
import { ToolErrorCardV2, type ToolErrorCardV2Props } from "./tool-error-card-v2"
const docs = `### Overview
@@ -75,9 +75,9 @@ export const Controlled = {
const [open, setOpen] = createSignal(false)
return (
<div style={{ display: "flex", "flex-direction": "column", gap: "24px", "max-width": "420px" }}>
<ButtonV2 type="button" classList={{ "w-fit": true }} onClick={() => setOpen((o) => !o)}>
<Button type="button" classList={{ "w-fit": true }} onClick={() => setOpen((o) => !o)}>
Toggle from outside: {open() ? "Open" : "Closed"}
</ButtonV2>
</Button>
<ToolErrorCardV2
title="Grep"
subtitle="Timeout"