chore: merge dev into v2 (#36770)
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Aiden Cline <63023139+rekram1-node@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: Luke Parker <10430890+Hona@users.noreply.github.com> Co-authored-by: Brendan Allan <git@brendonovich.dev> Co-authored-by: Kit Langton <kit.langton@gmail.com> Co-authored-by: James Long <longster@gmail.com> Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com> Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> Co-authored-by: Jay <53023+jayair@users.noreply.github.com> Co-authored-by: Simon Klee <hello@simonklee.dk> Co-authored-by: Jay <air@live.ca> Co-authored-by: Jack <jack@anoma.ly> Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Co-authored-by: opencode <opencode@sst.dev> Co-authored-by: Aiden Cline <aidenpcline@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: 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>
This commit is contained in:
co-authored by
opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Aiden Cline
usrnk1
Aarav Sareen
Luke Parker
Brendan Allan
Kit Langton
James Long
Adam
opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Jay
Simon Klee
Jay
Jack
David Hill
Brendan Allan
opencode
Aiden Cline
James Long
Dustin Deus
冯基魁
Frank
Aiden Cline
Victor Navarro
Dax Raad
Dax
Nabs
Vladimir Glafirov
AidenGeunGeun
Mark
parent
26e27c7a7f
commit
634386fe0f
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { FilePart } from "@opencode-ai/sdk/v2"
|
||||
import { attached, inline, kind } from "./message-file"
|
||||
import { attached, inline, kind, typeLabel } from "./message-file"
|
||||
|
||||
function file(part: Partial<FilePart> = {}): FilePart {
|
||||
return {
|
||||
@@ -52,4 +52,14 @@ describe("message-file", () => {
|
||||
expect(kind(file({ mime: "image/png" }))).toBe("image")
|
||||
expect(kind(file({ mime: "application/pdf" }))).toBe("file")
|
||||
})
|
||||
|
||||
test("labels attachment types from the basename extension", () => {
|
||||
expect(typeLabel("list.md", "text/plain")).toBe("Markdown")
|
||||
expect(typeLabel("/repo/src/main.ts", "text/plain")).toBe("TypeScript")
|
||||
expect(typeLabel("/tmp/report.pdf", "application/pdf")).toBe("PDF")
|
||||
expect(typeLabel("notes.xyz", "text/plain")).toBe("XYZ")
|
||||
expect(typeLabel("/home/user/my.project/Makefile", "text/plain")).toBe("File")
|
||||
expect(typeLabel(".gitignore", "text/plain")).toBe("File")
|
||||
expect(typeLabel("/repo/.env", "text/plain")).toBe("File")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { bundledLanguagesInfo } from "shiki"
|
||||
import { getFilename } from "@opencode-ai/core/util/path"
|
||||
import type { FilePart } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export function attached(part: FilePart) {
|
||||
@@ -12,3 +14,22 @@ export function inline(part: FilePart) {
|
||||
export function kind(part: FilePart) {
|
||||
return part.mime.startsWith("image/") ? "image" : "file"
|
||||
}
|
||||
|
||||
// language metadata only; grammars stay behind shiki's lazy imports
|
||||
const LANGUAGE_NAMES = new Map<string, string>(
|
||||
bundledLanguagesInfo.flatMap((info) =>
|
||||
[info.id, ...(info.aliases ?? [])].map((alias) => [alias, info.name] as [string, string]),
|
||||
),
|
||||
)
|
||||
|
||||
// attachments carry text/plain for all text files, so the label comes from the extension;
|
||||
// filename may be an absolute path, so extract the basename before looking for one
|
||||
export function typeLabel(filename: string, mime: string) {
|
||||
if (mime === "application/pdf") return "PDF"
|
||||
const base = getFilename(filename)
|
||||
// idx 0 is a dotfile like .gitignore, not an extension
|
||||
const idx = base.lastIndexOf(".")
|
||||
const suffix = idx <= 0 ? "" : base.slice(idx + 1).toLowerCase()
|
||||
if (!suffix) return "File"
|
||||
return LANGUAGE_NAMES.get(suffix) ?? suffix.toUpperCase()
|
||||
}
|
||||
|
||||
@@ -57,8 +57,20 @@
|
||||
}
|
||||
|
||||
&[data-type="image"] {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: relative;
|
||||
width: 58px;
|
||||
height: 46px;
|
||||
border: none;
|
||||
|
||||
/* inset box-shadows do not paint over <img> content, so the hairline is an overlay */
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-base);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-type="file"] {
|
||||
@@ -1301,6 +1313,16 @@ body:not([data-new-layout]) {
|
||||
&:hover {
|
||||
border-color: var(--border-strong-base);
|
||||
}
|
||||
|
||||
&[data-type="image"] {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 1px solid var(--border-weak-base);
|
||||
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="user-message-attachment-name"] {
|
||||
|
||||
@@ -45,6 +45,8 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
|
||||
import { Markdown } from "./markdown"
|
||||
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
||||
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/core/util/path"
|
||||
import { AttachmentCardV2 } from "../v2/components/attachment-card-v2"
|
||||
import { CommentCardV2 } from "../v2/components/comment-card-v2"
|
||||
import { checksum } from "@opencode-ai/core/util/encode"
|
||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
@@ -58,7 +60,7 @@ import { ToolStatusTitle } from "./tool-status-title"
|
||||
import { patchFiles } from "./apply-patch-file"
|
||||
import { animate } from "motion"
|
||||
import { useLocation } from "@solidjs/router"
|
||||
import { attached, inline, kind } from "./message-file"
|
||||
import { attached, inline, kind, typeLabel } from "./message-file"
|
||||
import { readPartText } from "./message-part-text"
|
||||
import { SessionProgressIndicatorV2 } from "../v2/components/session-progress-indicator-v2"
|
||||
|
||||
@@ -165,6 +167,7 @@ export interface MessageProps {
|
||||
showAssistantCopyPartID?: string | null
|
||||
showReasoningSummaries?: boolean
|
||||
useV2Actions?: boolean
|
||||
comments?: UserMessageComment[]
|
||||
}
|
||||
|
||||
export type SessionAction = (input: { sessionID: string; messageID: string }) => Promise<void> | void
|
||||
@@ -172,6 +175,16 @@ export type SessionAction = (input: { sessionID: string; messageID: string }) =>
|
||||
export type UserActions = {
|
||||
fork?: SessionAction
|
||||
revert?: SessionAction
|
||||
openAttachment?: (file: FilePart) => void
|
||||
}
|
||||
|
||||
export type UserMessageComment = {
|
||||
path: string
|
||||
comment: string
|
||||
selection?: {
|
||||
startLine: number
|
||||
endLine: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface MessagePartProps {
|
||||
@@ -937,6 +950,7 @@ export function Message(props: MessageProps) {
|
||||
parts={props.parts}
|
||||
actions={props.actions}
|
||||
useV2Actions={props.useV2Actions}
|
||||
comments={props.comments}
|
||||
/>
|
||||
)}
|
||||
</Match>
|
||||
@@ -1162,6 +1176,7 @@ export function UserMessageDisplay(props: {
|
||||
parts: PartType[]
|
||||
actions?: UserActions
|
||||
useV2Actions?: boolean
|
||||
comments?: UserMessageComment[]
|
||||
}) {
|
||||
const data = useData()
|
||||
const dialog = useDialog()
|
||||
@@ -1183,6 +1198,8 @@ export function UserMessageDisplay(props: {
|
||||
|
||||
const attachments = createMemo(() => files().filter(attached))
|
||||
|
||||
const messageComments = createMemo(() => (newLayout() ? (props.comments ?? []) : []))
|
||||
|
||||
const inlineFiles = createMemo(() => files().filter(inline))
|
||||
|
||||
const agents = createMemo(() => (props.parts?.filter((p) => p.type === "agent") as AgentPart[]) ?? [])
|
||||
@@ -1239,35 +1256,59 @@ export function UserMessageDisplay(props: {
|
||||
|
||||
return (
|
||||
<div data-component="user-message" data-timeline-part-id={textPart()?.id}>
|
||||
<Show when={attachments().length > 0}>
|
||||
<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 (
|
||||
<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={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>
|
||||
}
|
||||
>
|
||||
<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>
|
||||
}
|
||||
<AttachmentCardV2
|
||||
title={getFilename(name)}
|
||||
hover={name}
|
||||
clickable={!!props.actions?.openAttachment}
|
||||
onClick={() => props.actions?.openAttachment?.(file)}
|
||||
>
|
||||
<img data-slot="user-message-attachment-image" src={file.url} alt={name} />
|
||||
</Show>
|
||||
</div>
|
||||
{typeLabel(name, file.mime)}
|
||||
</AttachmentCardV2>
|
||||
</Show>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
[data-component="attachment-card-v2"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
box-sizing: border-box;
|
||||
width: 160px;
|
||||
min-width: 160px;
|
||||
max-width: 160px;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background: var(--v2-overlay-simple-overlay-hover);
|
||||
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-base);
|
||||
cursor: default;
|
||||
|
||||
&[data-active] {
|
||||
box-shadow: inset 0 0 0 0.5px var(--v2-border-border-strong);
|
||||
}
|
||||
|
||||
&[data-clickable] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[data-slot="attachment-card-v2-title"],
|
||||
[data-slot="attachment-card-v2-subtitle"] {
|
||||
max-width: 100%;
|
||||
font-family: var(--v2-font-family-sans, "Inter", sans-serif);
|
||||
font-style: normal;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
letter-spacing: 0.05px;
|
||||
font-variation-settings: "slnt" 0;
|
||||
}
|
||||
|
||||
[data-slot="attachment-card-v2-title"] {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 530;
|
||||
color: var(--v2-text-text-base);
|
||||
}
|
||||
|
||||
[data-slot="attachment-card-v2-subtitle"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-weight: 440;
|
||||
color: var(--v2-text-text-muted);
|
||||
|
||||
[data-component="file-icon"] {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { JSX } from "solid-js"
|
||||
import "./attachment-card-v2.css"
|
||||
|
||||
/** Shared 160px two-line card used by v2 file and comment attachments in the composer and timeline. */
|
||||
export function AttachmentCardV2(props: {
|
||||
title: string
|
||||
active?: boolean
|
||||
clickable?: boolean
|
||||
/** native title attribute */
|
||||
hover?: string
|
||||
onClick?: () => void
|
||||
children: JSX.Element
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-component="attachment-card-v2"
|
||||
data-active={props.active ? "true" : undefined}
|
||||
data-clickable={props.clickable ? "true" : undefined}
|
||||
title={props.hover}
|
||||
onClick={() => props.onClick?.()}
|
||||
>
|
||||
<span data-slot="attachment-card-v2-title">{props.title}</span>
|
||||
<span data-slot="attachment-card-v2-subtitle">{props.children}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Show } from "solid-js"
|
||||
import { FileIcon } from "@opencode-ai/ui/file-icon"
|
||||
import { getFilenameTruncated } from "@opencode-ai/core/util/path"
|
||||
import { AttachmentCardV2 } from "./attachment-card-v2"
|
||||
|
||||
export function CommentCardV2(props: {
|
||||
comment: string
|
||||
path: string
|
||||
selection?: { startLine: number; endLine: number }
|
||||
active?: boolean
|
||||
title?: string
|
||||
onClick?: () => void
|
||||
}) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
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"
|
||||
|
||||
type LineCommentControllerV2Props<T extends LineCommentShape> = {
|
||||
comments: Accessor<T[]>
|
||||
@@ -23,6 +24,7 @@ type LineCommentControllerV2Props<T extends LineCommentShape> = {
|
||||
onDelete?: (comment: T) => void
|
||||
renderCommentActions?: (comment: T, controls: { edit: VoidFunction; remove: VoidFunction }) => JSX.Element
|
||||
editSubmitLabel?: string
|
||||
mention?: LineCommentEditorV2Mention
|
||||
}
|
||||
|
||||
type CommentProps = {
|
||||
@@ -43,6 +45,7 @@ type DraftProps = {
|
||||
onSubmit: (value: string) => void
|
||||
cancelLabel?: string
|
||||
submitLabel?: string
|
||||
mention?: LineCommentEditorV2Mention
|
||||
}
|
||||
|
||||
function lineCommentElementV2(view: Accessor<CommentProps>) {
|
||||
@@ -70,6 +73,7 @@ function lineCommentElementV2(view: Accessor<CommentProps>) {
|
||||
onSubmit={view().editor!.onSubmit}
|
||||
cancelLabel={view().editor!.cancelLabel}
|
||||
submitLabel={view().editor!.submitLabel}
|
||||
mention={view().editor!.mention}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -87,6 +91,7 @@ function lineCommentDraftElementV2(view: Accessor<DraftProps>) {
|
||||
onSubmit={view().onSubmit}
|
||||
cancelLabel={view().cancelLabel}
|
||||
submitLabel={view().submitLabel}
|
||||
mention={view().mention}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -142,6 +147,7 @@ export function createLineCommentControllerV2<T extends LineCommentShape>(props:
|
||||
},
|
||||
cancelLabel: i18n.t("ui.lineComment.cancel"),
|
||||
submitLabel: props.editSubmitLabel,
|
||||
mention: props.mention,
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
@@ -165,6 +171,7 @@ export function createLineCommentControllerV2<T extends LineCommentShape>(props:
|
||||
},
|
||||
cancelLabel: i18n.t("ui.lineComment.cancel"),
|
||||
submitLabel: i18n.t("ui.lineComment.submit"),
|
||||
mention: props.mention,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -202,6 +209,7 @@ export function createLineCommentControllerV2<T extends LineCommentShape>(props:
|
||||
}
|
||||
|
||||
return {
|
||||
note,
|
||||
annotations,
|
||||
renderAnnotation,
|
||||
renderGutterUtility,
|
||||
|
||||
@@ -76,6 +76,11 @@
|
||||
color: var(--text-strong, var(--v2-text-text-base));
|
||||
}
|
||||
|
||||
[data-component="session-review-v2-sidebar-root"]
|
||||
[data-slot="session-review-v2-sidebar-title"]:not(:has([data-component="select-v2-root"])) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
[data-component="session-review-v2-sidebar-root"]
|
||||
[data-slot="session-review-v2-sidebar-title"]
|
||||
[data-component="select-v2-root"] {
|
||||
@@ -97,7 +102,7 @@
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
margin-top: 2px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] {
|
||||
|
||||
@@ -26,7 +26,6 @@ export type SessionReviewV2Props = {
|
||||
empty?: JSX.Element
|
||||
sidebarOpen?: boolean
|
||||
sidebar?: JSX.Element
|
||||
sidebarToggle?: JSX.Element
|
||||
activeFile?: string
|
||||
files: string[]
|
||||
onSelectFile: (file: string) => void
|
||||
@@ -199,7 +198,6 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
|
||||
const toolbarStart = () => (
|
||||
<>
|
||||
{props.sidebarToggle}
|
||||
<Show when={showCollapsedMeta()}>
|
||||
<div data-slot="session-review-v2-toolbar-collapsed-meta">
|
||||
<Show when={title()}>
|
||||
@@ -319,7 +317,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) {
|
||||
)
|
||||
}
|
||||
|
||||
export function SessionReviewV2SidebarToggle(props: { opened: boolean; onToggle: () => void }) {
|
||||
export function SessionReviewV2SidebarToggle(props: { opened: boolean; disabled?: boolean; onToggle: () => void }) {
|
||||
const i18n = useI18n()
|
||||
|
||||
return (
|
||||
@@ -330,6 +328,7 @@ export function SessionReviewV2SidebarToggle(props: { opened: boolean; onToggle:
|
||||
class="session-review-v2-sidebar-toggle"
|
||||
aria-label={i18n.t("ui.sessionReviewV2.toggleSidebar")}
|
||||
aria-expanded={props.opened}
|
||||
disabled={props.disabled}
|
||||
onClick={props.onToggle}
|
||||
icon={<Icon name="filetree" />}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user