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:
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
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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` }}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user