chore: merge dev into v2 (#36918)

Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Simon Klee <hello@simonklee.dk>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: James Long <jlongster@users.noreply.github.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: 冯基魁 <56265583+fengjikui@users.noreply.github.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: Victor Navarro <vn4varro@gmail.com>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: Nabs <nabil@instafork.com>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com>
Co-authored-by: Mark <geraint0923@users.noreply.github.com>
Co-authored-by: Jay <air@live.ca>
This commit is contained in:
opencode-agent[bot]
2026-07-14 15:23:34 -05:00
committed by GitHub
co-authored by opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Aiden Cline opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Luke Parker usrnk1 Aarav Sareen Brendan Allan Jack David Hill Jay Brendan Allan Simon Klee opencode Aiden Cline Adam James Long James Long Dustin Deus 冯基魁 Frank Aiden Cline Kit Langton Victor Navarro Dax Raad Dax Nabs Vladimir Glafirov AidenGeunGeun Mark Jay
parent a32c480d45
commit 0d4f9797f4
123 changed files with 3206 additions and 770 deletions
@@ -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);
@@ -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>
)
+8 -1
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;
}
@@ -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;
@@ -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>
)
@@ -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>
)
}
@@ -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);
@@ -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` }}
>