chore: merge dev into v2

This commit is contained in:
Aiden Cline 2026-07-14 20:01:41 +00:00
commit 6911456c2f
122 changed files with 3190 additions and 754 deletions

View file

@ -133,10 +133,6 @@
align-items: flex-end;
}
[data-slot="user-message-attachments"] + [data-slot="user-message-body"] {
margin-top: 8px;
}
[data-slot="user-message-text"] {
display: inline-block;
white-space: pre-wrap;
@ -1302,10 +1298,63 @@
}
}
body[data-new-layout] [data-component="user-message"] {
font-weight: 440;
[data-slot="user-message-text"] {
background: var(--v2-background-bg-layer-01);
}
[data-slot="user-message-comments"] {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
width: min(320px, 82%);
margin-left: auto;
&[data-bounded] {
width: 318px;
max-width: 100%;
margin-top: 8px;
}
[data-component="tooltip-v2-trigger"] {
width: 100%;
}
[data-component="button-v2"] {
font-family: var(--v2-font-family-sans, "Inter", sans-serif);
font-weight: 530;
font-variation-settings:
"wght" 530,
"slnt" 0;
}
}
[data-slot="user-message-body"] + [data-slot="user-message-attachments"],
[data-slot="user-message-comments"] + [data-slot="user-message-attachments"] {
margin-top: 8px;
}
[data-slot="user-message-text"][data-comments] {
width: 342px;
padding-bottom: 12px;
}
}
[data-color-scheme="light"] body[data-new-layout] [data-component="user-message"] [data-slot="user-message-text"] {
background: var(--v2-background-bg-layer-02);
}
body:not([data-new-layout]) {
[data-component="user-message"] {
color: var(--text-strong);
[data-slot="user-message-attachments"] + [data-slot="user-message-body"] {
margin-top: 8px;
}
[data-slot="user-message-attachment"] {
background: var(--surface-weak);
border: 1px solid var(--border-weak-base);

View file

@ -52,6 +52,7 @@ 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 { Spinner } from "@opencode-ai/ui/spinner"
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
@ -1171,6 +1172,34 @@ export function ContextToolGroup(props: {
)
}
function UserMessageComments(props: { comments: UserMessageComment[]; bounded: boolean }) {
const i18n = useI18n()
const [state, setState] = createStore({ expanded: false })
const comments = createMemo(() => (props.bounded && !state.expanded ? props.comments.slice(0, 5) : props.comments))
return (
<div data-slot="user-message-comments" data-bounded={props.bounded ? "true" : undefined}>
<For each={comments()}>
{(comment) => (
<CommentCardV2
comment={comment.comment}
path={comment.path}
selection={comment.selection}
title={comment.comment}
tooltip
wide
/>
)}
</For>
<Show when={props.bounded && props.comments.length > 5 && !state.expanded}>
<ButtonV2 size="small" variant="ghost-muted" onClick={() => setState("expanded", true)}>
{i18n.t("ui.common.showMore")}
</ButtonV2>
</Show>
</div>
)
}
export function UserMessageDisplay(props: {
message: UserMessage
parts: PartType[]
@ -1254,107 +1283,114 @@ export function UserMessageDisplay(props: {
.finally(() => setState("busy", false))
}
const renderAttachments = () => (
<Show when={attachments().length > 0}>
<div data-slot="user-message-attachments">
<For each={attachments()}>
{(file) => {
const type = kind(file)
const name = file.filename ?? i18n.t("ui.message.attachment.alt")
return (
<Show
when={newLayout() && type === "file"}
fallback={
<div
data-slot="user-message-attachment"
data-type={type}
data-clickable={type === "image" ? "true" : undefined}
title={type === "file" ? name : undefined}
onClick={() => {
if (type === "image") openImagePreview(file.url, name)
}}
>
<Show
when={type === "image"}
fallback={
<div data-slot="user-message-attachment-file">
<FileIcon node={{ path: name, type: "file" }} />
<span data-slot="user-message-attachment-name">{name}</span>
</div>
}
>
<img data-slot="user-message-attachment-image" src={file.url} alt={name} />
</Show>
</div>
}
>
<AttachmentCardV2
title={getFilename(name)}
hover={name}
clickable={!!props.actions?.openAttachment}
onClick={() => props.actions?.openAttachment?.(file)}
>
{typeLabel(name, file.mime)}
</AttachmentCardV2>
</Show>
)
}}
</For>
</div>
</Show>
)
return (
<div data-component="user-message" data-timeline-part-id={textPart()?.id}>
<Show when={attachments().length > 0 || messageComments().length > 0}>
<div data-slot="user-message-attachments">
<For each={messageComments()}>
{(comment) => (
<CommentCardV2
comment={comment.comment}
path={comment.path}
selection={comment.selection}
title={comment.comment}
/>
)}
</For>
<For each={attachments()}>
{(file) => {
const type = kind(file)
const name = file.filename ?? i18n.t("ui.message.attachment.alt")
return (
<Show
when={newLayout() && type === "file"}
fallback={
<div
data-slot="user-message-attachment"
data-type={type}
data-clickable={type === "image" ? "true" : undefined}
title={type === "file" ? name : undefined}
onClick={() => {
if (type === "image") openImagePreview(file.url, name)
}}
>
<Show
when={type === "image"}
fallback={
<div data-slot="user-message-attachment-file">
<FileIcon node={{ path: name, type: "file" }} />
<span data-slot="user-message-attachment-name">{name}</span>
</div>
}
>
<img data-slot="user-message-attachment-image" src={file.url} alt={name} />
</Show>
</div>
}
>
<AttachmentCardV2
title={getFilename(name)}
hover={name}
clickable={!!props.actions?.openAttachment}
onClick={() => props.actions?.openAttachment?.(file)}
>
{typeLabel(name, file.mime)}
</AttachmentCardV2>
</Show>
)
}}
</For>
<Show when={!props.useV2Actions}>{renderAttachments()}</Show>
<Show
when={text()}
fallback={
<Show when={messageComments().length > 0}>
<UserMessageComments comments={messageComments()} bounded={false} />
</Show>
}
>
<div data-slot="user-message-body">
<div data-slot="user-message-text" data-comments={messageComments().length > 0 ? "true" : undefined}>
<HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
<Show when={messageComments().length > 0}>
<UserMessageComments comments={messageComments()} bounded />
</Show>
</div>
</div>
</Show>
<Show when={text()}>
<>
<div data-slot="user-message-body">
<div data-slot="user-message-text">
<HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
</div>
</div>
<div data-slot="user-message-copy-wrapper">
<Show when={metaHead() || metaTail()}>
<span data-slot="user-message-meta-wrap">
<Show when={metaHead()}>
<span data-slot="user-message-meta" class="text-12-regular text-text-weak cursor-default">
{metaHead()}
</span>
</Show>
<Show when={metaHead() && metaTail()}>
<span data-slot="user-message-meta-sep" class="text-12-regular text-text-weak cursor-default">
{"\u00A0\u00B7\u00A0"}
</span>
</Show>
<Show when={metaTail()}>
<span data-slot="user-message-meta-tail" class="text-12-regular text-text-weak cursor-default">
{metaTail()}
</span>
</Show>
</span>
</Show>
<Show when={props.actions?.revert}>
<MessageActionButton
icon="reset"
label={i18n.t("ui.message.revertMessage")}
useV2={props.useV2Actions}
disabled={!!busy()}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
event.stopPropagation()
revert()
}}
aria-label={i18n.t("ui.message.revertMessage")}
/>
</Show>
<Show when={props.useV2Actions}>{renderAttachments()}</Show>
<Show when={text() || (props.useV2Actions && messageComments().length > 0)}>
<div data-slot="user-message-copy-wrapper">
<Show when={metaHead() || metaTail()}>
<span data-slot="user-message-meta-wrap">
<Show when={metaHead()}>
<span data-slot="user-message-meta" class="text-12-regular text-text-weak cursor-default">
{metaHead()}
</span>
</Show>
<Show when={metaHead() && metaTail()}>
<span data-slot="user-message-meta-sep" class="text-12-regular text-text-weak cursor-default">
{"\u00A0\u00B7\u00A0"}
</span>
</Show>
<Show when={metaTail()}>
<span data-slot="user-message-meta-tail" class="text-12-regular text-text-weak cursor-default">
{metaTail()}
</span>
</Show>
</span>
</Show>
<Show when={props.actions?.revert}>
<MessageActionButton
icon="reset"
label={i18n.t("ui.message.revertMessage")}
useV2={props.useV2Actions}
disabled={!!busy()}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
event.stopPropagation()
revert()
}}
aria-label={i18n.t("ui.message.revertMessage")}
/>
</Show>
<Show when={text()}>
<MessageActionButton
icon={copied() ? "check" : "copy"}
label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")}
@ -1366,8 +1402,8 @@ export function UserMessageDisplay(props: {
}}
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")}
/>
</div>
</>
</Show>
</div>
</Show>
</div>
)

View file

@ -15,6 +15,13 @@ export type DiffProps<T = {}> = FileDiffOptions<T> & {
}
const unsafeCSS = `
:host {
--diffs-bg: var(
--opencode-diffs-bg,
light-dark(var(--diffs-light-bg, #fff), var(--diffs-dark-bg, #000))
);
}
[data-diff],
[data-file] {
/* Pierre 1.2 mixes these override targets at 12% in light mode and 20% in dark mode. */
@ -129,7 +136,7 @@ const unsafeCSS = `
height: 24px;
}
[data-column-number] {
background-color: var(--background-stronger);
background-color: var(--diffs-bg);
cursor: default !important;
}

View file

@ -12,6 +12,16 @@
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-base);
cursor: default;
&[data-wide] {
width: 100%;
min-width: 0;
max-width: none;
}
&[data-surface="base"] {
background: var(--v2-background-bg-base);
}
&[data-active] {
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-strong);
}
@ -32,6 +42,8 @@
}
[data-slot="attachment-card-v2-title"] {
width: 100%;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

View file

@ -6,8 +6,11 @@ export function AttachmentCardV2(props: {
title: string
active?: boolean
clickable?: boolean
wide?: boolean
surface?: "base"
/** native title attribute */
hover?: string
titleRef?: (element: HTMLSpanElement) => void
onClick?: () => void
children: JSX.Element
}) {
@ -16,10 +19,14 @@ export function AttachmentCardV2(props: {
data-component="attachment-card-v2"
data-active={props.active ? "true" : undefined}
data-clickable={props.clickable ? "true" : undefined}
data-wide={props.wide ? "true" : undefined}
data-surface={props.surface}
title={props.hover}
onClick={() => props.onClick?.()}
>
<span data-slot="attachment-card-v2-title">{props.title}</span>
<span ref={(element) => props.titleRef?.(element)} data-slot="attachment-card-v2-title">
{props.title}
</span>
<span data-slot="attachment-card-v2-subtitle">{props.children}</span>
</div>
)

View file

@ -1,6 +1,7 @@
import { Show } from "solid-js"
import { createSignal, onCleanup, onMount, Show } from "solid-js"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { getFilenameTruncated } from "@opencode-ai/core/util/path"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { AttachmentCardV2 } from "./attachment-card-v2"
export function CommentCardV2(props: {
@ -9,19 +10,55 @@ export function CommentCardV2(props: {
selection?: { startLine: number; endLine: number }
active?: boolean
title?: string
tooltip?: boolean
wide?: boolean
onClick?: () => void
}) {
let title: HTMLSpanElement | undefined
const [truncated, setTruncated] = createSignal(false)
onMount(() => {
const element = title
if (!element) return
const sync = () => setTruncated(element.scrollWidth > element.clientWidth)
const measure = () => requestAnimationFrame(sync)
const observer = new ResizeObserver(sync)
observer.observe(element)
measure()
void document.fonts?.ready.then(measure)
onCleanup(() => observer.disconnect())
})
return (
<AttachmentCardV2 title={props.comment} active={props.active} hover={props.title} onClick={props.onClick}>
<FileIcon node={{ path: props.path, type: "file" }} />
<span>
{getFilenameTruncated(props.path, 14)}
<Show when={props.selection}>
{(sel) =>
sel().startLine === sel().endLine ? `:${sel().startLine}` : `:${sel().startLine}-${sel().endLine}`
}
</Show>
</span>
</AttachmentCardV2>
<TooltipV2
placement="top"
openDelay={1000}
value={props.title ?? props.comment}
disabled={!props.tooltip || !truncated()}
class={props.wide ? "w-full" : undefined}
contentStyle={{ "max-width": "320px", "white-space": "pre-wrap" }}
>
<AttachmentCardV2
title={props.comment}
active={props.active}
clickable={!!props.onClick}
wide={props.wide}
surface="base"
titleRef={(element) => {
title = element
}}
onClick={props.onClick}
>
<FileIcon node={{ path: props.path, type: "file" }} />
<span>
{getFilenameTruncated(props.path, 14)}
<Show when={props.selection}>
{(sel) =>
sel().startLine === sel().endLine ? `:${sel().startLine}` : `:${sel().startLine}-${sel().endLine}`
}
</Show>
</span>
</AttachmentCardV2>
</TooltipV2>
)
}

View file

@ -38,7 +38,8 @@
border-right-width: 0;
}
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar"]:not([data-resizing]) {
[data-component="session-review-v2-sidebar-root"]
[data-slot="session-review-v2-sidebar"][data-transition]:not([data-resizing]) {
transition:
width 200ms cubic-bezier(0.22, 1, 0.36, 1),
border-right-width 200ms cubic-bezier(0.22, 1, 0.36, 1);

View file

@ -39,6 +39,7 @@ export type SessionReviewV2Props = {
export type SessionReviewV2SidebarProps = {
open: boolean
transition: boolean
title?: JSX.Element
stats?: JSX.Element
filter: string
@ -76,6 +77,7 @@ export function SessionReviewV2Sidebar(props: SessionReviewV2SidebarProps) {
<Show when={props.open}>
<aside
data-slot="session-review-v2-sidebar"
data-transition={props.transition ? "" : undefined}
data-resizing={resizing() ? "" : undefined}
style={{ width: `${width()}px` }}
>