chore: merge dev into v2 (#34788)

Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Affan Ali <93028901+affanali2k3@users.noreply.github.com>
Co-authored-by: affanali2k3 <affanalikhanxx@gmail.com>
Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Jay V <air@live.ca>
Co-authored-by: Dax Raad <d@ironbay.co>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: Ben Guthrie <benjee.012@gmail.com>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com>
Co-authored-by: Max Anderson <max.a.anderson95@gmail.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: Jay <53023+jayair@users.noreply.github.com>
Co-authored-by: runvip <164729189+runvip@users.noreply.github.com>
Co-authored-by: opencode <opencode@sst.dev>
Co-authored-by: Julian Coy <julian@ex-machina.co>
Co-authored-by: Vladimir Glafirov <vglafirov@gitlab.com>
This commit is contained in:
James Long
2026-07-01 17:12:00 -04:00
committed by GitHub
co-authored by Brendan Allan Kit Langton opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Affan Ali affanali2k3 Frank opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 Aiden Cline Jay V Dax Raad Aarav Sareen OpeOginni Luke Parker Ben Guthrie Dax Filip Max Anderson Brendan Allan Jack Shoubhit Dash Dustin Deus starptech Aiden Cline usrnk1 Jay runvip opencode Julian Coy Vladimir Glafirov
parent 932a40cfd9
commit 8c94e9005f
590 changed files with 15772 additions and 5530 deletions
@@ -408,7 +408,10 @@ export function createLineCommentController<T extends LineCommentShape>(
selection: formatSelectedLineLabel(range, i18n.t),
mention: props.mention,
onInput: note.setDraft,
onCancel: note.cancelDraft,
onCancel: () => {
note.cancelDraft()
note.select(null)
},
onSubmit: (comment) => {
props.onSubmit({ comment, selection: cloneSelectedLineRange(range) })
note.cancelDraft()
@@ -13,10 +13,24 @@ describe("inlineCodeKind", () => {
expect(inlineCodeKind(`@opencode-ai/app <StatusPopover />)`)).toBeUndefined()
expect(inlineCodeKind(`sync.data.session`)).toBeUndefined()
expect(inlineCodeKind(`window.api`)).toBeUndefined()
expect(inlineCodeKind(`1.2`)).toBeUndefined()
})
test("detects file and directory paths", () => {
expect(inlineCodeKind(`app.tsx`)).toBe("path")
expect(inlineCodeKind(`vite.config.mjs`)).toBe("path")
expect(inlineCodeKind(`eslint.config.cjs`)).toBe("path")
expect(inlineCodeKind(`app.d.ts`)).toBe("path")
expect(inlineCodeKind(`component.svelte`)).toBe("path")
expect(inlineCodeKind(`schema.graphql`)).toBe("path")
expect(inlineCodeKind(`Dockerfile`)).toBe("path")
expect(inlineCodeKind(`Dockerfile.dev`)).toBe("path")
expect(inlineCodeKind(`.gitignore`)).toBe("path")
expect(inlineCodeKind(`Cargo.lock`)).toBe("path")
expect(inlineCodeKind(`go.sum`)).toBe("path")
expect(inlineCodeKind(`bun.lockb`)).toBe("path")
expect(inlineCodeKind(`terraform.tfvars`)).toBe("path")
expect(inlineCodeKind(`pnpm-lock.yaml`)).toBe("path")
expect(inlineCodeKind(`packages/desktop-electron`)).toBe("path")
expect(inlineCodeKind(`~/.config/opencode`)).toBe("path")
expect(inlineCodeKind(`@opencode-ai/app`)).toBe("path")
File diff suppressed because it is too large Load Diff
+40 -72
View File
@@ -35,8 +35,8 @@
font-size: 17px;
color: var(--v2-text-text-base);
font-weight: 600;
margin-top: 0px;
margin-bottom: 24px;
margin-top: 28px;
margin-bottom: 12px;
line-height: var(--line-height-large);
}
@@ -44,8 +44,8 @@
font-size: 15px;
color: var(--v2-text-text-base);
font-weight: 600;
margin-top: 0px;
margin-bottom: 24px;
margin-top: 24px;
margin-bottom: 10px;
line-height: var(--line-height-large);
}
@@ -53,8 +53,8 @@
font-size: 13px;
color: var(--v2-text-text-base);
font-weight: var(--font-weight-medium);
margin-top: 0px;
margin-bottom: 24px;
margin-top: 20px;
margin-bottom: 8px;
line-height: var(--line-height-large);
}
@@ -64,8 +64,8 @@
font-size: 13px;
color: var(--v2-text-text-muted);
font-weight: var(--font-weight-medium);
margin-top: 0px;
margin-bottom: 24px;
margin-top: 20px;
margin-bottom: 8px;
line-height: var(--line-height-large);
}
@@ -145,7 +145,7 @@
/* Blockquotes */
blockquote {
border-left: 0.5px solid var(--v2-border-border-base);
margin: 1.5rem 0;
margin: 1rem 0;
padding-left: 0.5rem;
color: var(--v2-text-text-muted);
font-style: normal;
@@ -155,7 +155,7 @@
hr {
border: none;
height: 0;
margin: 40px 0;
margin: 32px 0;
}
.shiki {
@@ -192,71 +192,28 @@
opacity: 0;
transition: opacity 0.15s ease;
z-index: 1;
&::after {
content: attr(data-tooltip);
position: absolute;
left: 50%;
bottom: calc(100% + 4px);
transform: translateX(-50%);
z-index: 1000;
max-width: 320px;
border-radius: var(--radius-sm);
background: var(--surface-float-base);
color: var(--text-invert-strong);
padding: 2px 8px;
border: 0.5px solid var(--v2-border-border-base, rgba(0, 0, 0, 0.07));
box-shadow: var(--shadow-md);
pointer-events: none;
white-space: nowrap;
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
opacity: 0;
transition: opacity 0.15s ease;
}
}
[data-slot="markdown-copy-button"]:hover::after,
[data-slot="markdown-copy-button"]:focus-visible::after {
[data-component="markdown-code"]:hover [data-slot="markdown-copy-button"],
[data-slot="markdown-copy-button"]:focus-within {
opacity: 1;
}
[data-slot="markdown-copy-button"][data-variant="secondary"] {
box-shadow: none;
border: 0.5px solid var(--v2-border-border-base);
}
[data-slot="markdown-copy-button"][data-variant="secondary"] [data-slot="icon-svg"] {
color: var(--v2-icon-icon-base);
}
[data-component="markdown-code"]:hover [data-slot="markdown-copy-button"] {
opacity: 1;
}
[data-slot="markdown-copy-button"] [data-slot="check-icon"] {
[data-slot="markdown-copy-button"] [data-check-icon] {
display: none;
}
[data-slot="markdown-copy-button"][data-copied="true"] [data-slot="copy-icon"] {
[data-slot="markdown-copy-button"][data-copied="true"] [data-copy-icon] {
display: none;
}
[data-slot="markdown-copy-button"][data-copied="true"] [data-slot="check-icon"] {
[data-slot="markdown-copy-button"][data-copied="true"] [data-check-icon] {
display: inline-flex;
}
pre {
margin-top: 12px;
margin-bottom: 32px;
margin-bottom: 24px;
overflow: auto;
scrollbar-width: none;
@@ -296,7 +253,7 @@
table {
width: 100%;
border-collapse: collapse;
margin: 24px 0;
margin: 16px 0;
font-size: var(--font-size-base);
display: block;
overflow-x: auto;
@@ -327,6 +284,29 @@
}
}
body[data-new-layout] [data-component="markdown"] {
[data-slot="markdown-copy-button"]::after {
box-sizing: border-box;
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 6px;
padding: 5px 6px;
border: 0;
border-radius: 4px;
background: var(--v2-background-bg-layer-01);
box-shadow: var(--v2-elevation-floating);
color: var(--v2-text-text-base);
user-select: none;
font-size: 11px;
font-weight: 530;
line-height: 12px;
letter-spacing: 0.05px;
font-variant-numeric: tabular-nums;
font-variation-settings: "slnt" 0;
}
}
body:not([data-new-layout]) [data-component="markdown"] {
color: var(--text-strong);
@@ -366,18 +346,6 @@ body:not([data-new-layout]) [data-component="markdown"] {
background: none;
}
[data-slot="markdown-copy-button"]::after {
border: 1px solid var(--border-weak-base, rgba(0, 0, 0, 0.07));
}
[data-slot="markdown-copy-button"][data-variant="secondary"] {
border: 1px solid var(--border-weak-base);
}
[data-slot="markdown-copy-button"][data-variant="secondary"] [data-slot="icon-svg"] {
color: var(--icon-base);
}
th,
td {
border-bottom: 1px solid var(--border-weaker-base);
+96 -50
View File
@@ -3,16 +3,21 @@ import { useI18n } from "@opencode-ai/ui/context/i18n"
import morphdom from "morphdom"
import { checksum } from "@opencode-ai/core/util/encode"
import {
ComponentProps,
type Accessor,
type ComponentProps,
createEffect,
createMemo,
createResource,
createSignal,
createUniqueId,
onCleanup,
type Setter,
splitProps,
} from "solid-js"
import { isServer } from "solid-js/web"
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 { bundledLanguages } from "shiki"
import { canReusePendingBlock, project, type Block, type Projection } from "./markdown-stream"
import {
@@ -48,11 +53,6 @@ type RenderResult = {
const renderedCodeTokens = new WeakMap<HTMLDivElement, RenderedCodeState>()
const iconPaths = {
copy: '<path d="M6.2513 6.24935V2.91602H17.0846V13.7493H13.7513M13.7513 6.24935V17.0827H2.91797V6.24935H13.7513Z" stroke="currentColor" stroke-linecap="round"/>',
check: '<path d="M5 11.9657L8.37838 14.7529L15 5.83398" stroke="currentColor" stroke-linecap="square"/>',
}
function escape(text: string) {
return text
.replace(/&/g, "&amp;")
@@ -87,6 +87,14 @@ type CopyLabels = {
copied: string
}
type CopyButtonState = {
setLabels: Setter<CopyLabels>
setCopied: Setter<boolean>
dispose: () => void
}
const copyButtonState = new WeakMap<HTMLElement, CopyButtonState>()
const urlPattern = /^https?:\/\/[^\s<>()`"']+$/
function codeUrl(text: string) {
@@ -100,45 +108,67 @@ function codeUrl(text: string) {
}
}
function createIcon(path: string, slot: string) {
const icon = document.createElement("div")
icon.setAttribute("data-component", "icon")
icon.setAttribute("data-size", "small")
icon.setAttribute("data-slot", slot)
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
svg.setAttribute("data-slot", "icon-svg")
svg.setAttribute("fill", "none")
svg.setAttribute("viewBox", "0 0 20 20")
svg.setAttribute("aria-hidden", "true")
svg.innerHTML = path
icon.appendChild(svg)
return icon
}
function createCopyButton(labels: CopyLabels) {
const button = document.createElement("button")
button.type = "button"
button.setAttribute("data-component", "icon-button")
button.setAttribute("data-variant", "secondary")
button.setAttribute("data-size", "small")
button.setAttribute("data-slot", "markdown-copy-button")
button.setAttribute("aria-label", labels.copy)
button.setAttribute("data-tooltip", labels.copy)
button.appendChild(createIcon(iconPaths.copy, "copy-icon"))
button.appendChild(createIcon(iconPaths.check, "check-icon"))
return button
const host = document.createElement("div")
host.setAttribute("data-slot", "markdown-copy-button")
const state: Partial<CopyButtonState> = {}
const dispose = render(() => {
const [labelState, setLabels] = createSignal(labels, { equals: false })
const [copied, setCopied] = createSignal(false)
state.setLabels = setLabels
state.setCopied = setCopied
return <MarkdownCopyButton labels={labelState} copied={copied} />
}, host)
state.dispose = dispose
copyButtonState.set(host, state as CopyButtonState)
return host
}
function setCopyState(button: HTMLButtonElement, labels: CopyLabels, copied: boolean) {
function MarkdownCopyButton(props: { labels: Accessor<CopyLabels>; copied: Accessor<boolean> }) {
const label = () => (props.copied() ? props.labels().copied : props.labels().copy)
return (
<TooltipV2 placement="top" value={label()}>
<IconButtonV2
type="button"
size="normal"
variant="ghost-muted"
aria-label={label()}
icon={
<>
<IconV2 name="outline-copy" data-copy-icon />
<IconV2 name="check" data-check-icon />
</>
}
/>
</TooltipV2>
)
}
function setCopyState(host: HTMLElement, labels: CopyLabels, copied: boolean) {
const state = copyButtonState.get(host)
state?.setLabels(labels)
state?.setCopied(copied)
if (copied) {
button.setAttribute("data-copied", "true")
button.setAttribute("aria-label", labels.copied)
button.setAttribute("data-tooltip", labels.copied)
host.setAttribute("data-copied", "true")
return
}
button.removeAttribute("data-copied")
button.setAttribute("aria-label", labels.copy)
button.setAttribute("data-tooltip", labels.copy)
host.removeAttribute("data-copied")
}
function disposeCopyButton(host: HTMLElement) {
copyButtonState.get(host)?.dispose()
copyButtonState.delete(host)
}
function disposeCopyButtons(root: Element) {
const hosts = [
...(root instanceof HTMLElement && root.getAttribute("data-slot") === "markdown-copy-button" ? [root] : []),
...Array.from(root.querySelectorAll('[data-slot="markdown-copy-button"]')).filter(
(el): el is HTMLElement => el instanceof HTMLElement,
),
]
hosts.forEach(disposeCopyButton)
}
const shellLanguages = new Set(["bash", "sh", "shell", "zsh", "fish", "console", "terminal"])
@@ -196,6 +226,7 @@ function ensureCodeWrapper(block: HTMLPreElement, labels: CopyLabels) {
}
for (const button of buttons.slice(1)) {
disposeCopyButton(button)
button.remove()
}
}
@@ -250,9 +281,9 @@ function decorate(root: HTMLDivElement, labels: CopyLabels) {
}
function setupCodeCopy(root: HTMLDivElement, getLabels: () => CopyLabels) {
const timeouts = new Map<HTMLButtonElement, ReturnType<typeof setTimeout>>()
const timeouts = new Map<HTMLElement, ReturnType<typeof setTimeout>>()
const updateLabel = (button: HTMLButtonElement) => {
const updateLabel = (button: HTMLElement) => {
const labels = getLabels()
const copied = button.getAttribute("data-copied") === "true"
setCopyState(button, labels, copied)
@@ -263,7 +294,7 @@ function setupCodeCopy(root: HTMLDivElement, getLabels: () => CopyLabels) {
if (!(target instanceof Element)) return
const button = target.closest('[data-slot="markdown-copy-button"]')
if (!(button instanceof HTMLButtonElement)) return
if (!(button instanceof HTMLElement)) return
const code = button.closest('[data-component="markdown-code"]')?.querySelector("code")
const content = code?.textContent ?? ""
if (!content) return
@@ -280,7 +311,7 @@ function setupCodeCopy(root: HTMLDivElement, getLabels: () => CopyLabels) {
const buttons = Array.from(root.querySelectorAll('[data-slot="markdown-copy-button"]'))
for (const button of buttons) {
if (button instanceof HTMLButtonElement) updateLabel(button)
if (button instanceof HTMLElement) updateLabel(button)
}
root.addEventListener("click", handleClick)
@@ -290,6 +321,7 @@ function setupCodeCopy(root: HTMLDivElement, getLabels: () => CopyLabels) {
for (const timeout of timeouts.values()) {
clearTimeout(timeout)
}
disposeCopyButtons(root)
}
}
@@ -431,6 +463,7 @@ export function Markdown(
if (!container) return
if (isServer) return
if (content.length === 0) {
disposeCopyButtons(container)
container.innerHTML = ""
return
}
@@ -446,9 +479,14 @@ export function Markdown(
activeCodeKeys.clear()
nextCodeKeys.forEach((key) => activeCodeKeys.add(key))
content.forEach((block, index) => updateBlock(container, index, block, labels))
while (container.children.length > content.length) container.lastElementChild?.remove()
while (container.children.length > content.length) {
const child = container.lastElementChild
if (!child) break
disposeCopyButtons(child)
child.remove()
}
container
.querySelectorAll<HTMLButtonElement>('[data-slot="markdown-copy-button"]')
.querySelectorAll<HTMLElement>('[data-slot="markdown-copy-button"]')
.forEach((button) => setCopyState(button, labels, button.dataset.copied === "true"))
if (!copyCleanup)
copyCleanup = setupCodeCopy(container, () => ({
@@ -538,8 +576,8 @@ function updateBlock(container: HTMLDivElement, index: number, block: RenderedBl
morphdom(current, next, {
onBeforeElUpdated: (fromEl, toEl) => {
if (
fromEl instanceof HTMLButtonElement &&
toEl instanceof HTMLButtonElement &&
fromEl instanceof HTMLElement &&
toEl instanceof HTMLElement &&
fromEl.getAttribute("data-slot") === "markdown-copy-button" &&
toEl.getAttribute("data-slot") === "markdown-copy-button"
) {
@@ -548,6 +586,10 @@ function updateBlock(container: HTMLDivElement, index: number, block: RenderedBl
if (fromEl.isEqualNode(toEl)) return false
return true
},
onBeforeNodeDiscarded: (node) => {
if (node instanceof Element) disposeCopyButtons(node)
return true
},
})
}
@@ -616,8 +658,12 @@ function updateCodeBlock(
unstable: block.unstable,
raw: block.raw,
})
if (current) current.replaceWith(next)
else container.appendChild(next)
if (current) {
disposeCopyButtons(current)
current.replaceWith(next)
return
}
container.appendChild(next)
}
function sameToken(left: MarkdownToken, right: MarkdownToken | undefined) {
@@ -378,15 +378,6 @@
pointer-events: auto;
}
[data-slot="bash-copy"] [data-component="icon-button"][data-variant="secondary"] {
box-shadow: none;
border: 0.5px solid var(--v2-border-border-base);
}
[data-slot="bash-copy"] [data-component="icon-button"][data-variant="secondary"] [data-slot="icon-svg"] {
color: var(--icon-base);
}
[data-slot="bash-scroll"] {
width: 100%;
overflow-y: auto;
@@ -860,6 +851,13 @@
min-width: 0;
}
[data-slot="question-header-actions"] {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
[data-slot="question-progress"] {
display: flex;
align-items: center;
@@ -918,6 +916,7 @@
line-height: var(--line-height-large);
color: var(--v2-text-text-base);
padding: 0 10px;
flex-shrink: 0;
-webkit-user-select: text;
user-select: text;
}
@@ -929,6 +928,7 @@
line-height: var(--line-height-large);
color: var(--v2-text-text-muted);
padding: 0 10px;
flex-shrink: 0;
}
[data-slot="question-options"] {
@@ -936,8 +936,8 @@
flex-direction: column;
gap: 6px;
margin-top: 12px;
padding: 1px 1px 8px;
flex-shrink: 0;
padding: 1px 1px 16px;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
@@ -1126,6 +1126,42 @@
}
}
[data-component="question-minimized-dock"] {
margin-bottom: 8px;
[data-slot="question-minimized-trigger"] {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 12px;
border: 0;
background: transparent;
color: var(--v2-text-text-base);
text-align: left;
cursor: pointer;
touch-action: manipulation;
&:disabled {
cursor: not-allowed;
opacity: 0.6;
}
[data-component="icon"] {
transform: rotate(180deg);
}
}
[data-slot="question-minimized-summary"] {
flex: 1;
font-family: var(--font-family-sans);
font-size: 14px;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-large);
color: var(--v2-text-text-base);
}
}
[data-component="question-answers"] {
display: flex;
flex-direction: column;
@@ -1319,10 +1355,6 @@ body:not([data-new-layout]) {
[data-component="bash-output"] {
border: 1px solid var(--border-weak-base);
[data-slot="bash-copy"] [data-component="icon-button"][data-variant="secondary"] {
border: 1px solid var(--border-weak-base);
}
}
[data-component="edit-trigger"],
@@ -11,6 +11,7 @@ import {
onCleanup,
Index,
type JSX,
type ComponentProps,
} from "solid-js"
import { createStore } from "solid-js/store"
import stripAnsi from "strip-ansi"
@@ -49,6 +50,9 @@ import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/core/ut
import { checksum } from "@opencode-ai/core/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 { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { Spinner } from "@opencode-ai/ui/spinner"
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
import { AnimatedCountList } from "./tool-count-summary"
@@ -161,6 +165,7 @@ export interface MessageProps {
actions?: UserActions
showAssistantCopyPartID?: string | null
showReasoningSummaries?: boolean
useV2Actions?: boolean
}
export type SessionAction = (input: { sessionID: string; messageID: string }) => Promise<void> | void
@@ -182,6 +187,47 @@ export interface MessagePartProps {
onContentRendered?: () => void
showAssistantCopyPartID?: string | null
turnDurationMs?: number
useV2Actions?: boolean
}
function MessageActionButton(
props: Pick<ComponentProps<"button">, "disabled" | "onMouseDown" | "onClick" | "aria-label"> & {
icon: "check" | "copy" | "reset"
label: JSX.Element
useV2?: boolean
},
) {
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>
)
}
export type PartComponent = Component<MessagePartProps>
@@ -640,6 +686,7 @@ export function AssistantParts(props: {
messages: AssistantMessage[]
showAssistantCopyPartID?: string | null
turnDurationMs?: number
useV2Actions?: boolean
working?: boolean
showReasoningSummaries?: boolean
shellToolDefaultOpen?: boolean
@@ -724,6 +771,7 @@ export function AssistantParts(props: {
message={message()!}
showAssistantCopyPartID={props.showAssistantCopyPartID}
turnDurationMs={props.turnDurationMs}
useV2Actions={props.useV2Actions}
defaultOpen={partDefaultOpen(item()!, props.shellToolDefaultOpen, props.editToolDefaultOpen)}
/>
</Show>
@@ -851,7 +899,12 @@ export function Message(props: MessageProps) {
<Switch>
<Match when={props.message.role === "user" && props.message}>
{(userMessage) => (
<UserMessageDisplay message={userMessage() as UserMessage} parts={props.parts} actions={props.actions} />
<UserMessageDisplay
message={userMessage() as UserMessage}
parts={props.parts}
actions={props.actions}
useV2Actions={props.useV2Actions}
/>
)}
</Match>
<Match when={props.message.role === "assistant" && props.message}>
@@ -861,6 +914,7 @@ export function Message(props: MessageProps) {
parts={props.parts}
showAssistantCopyPartID={props.showAssistantCopyPartID}
showReasoningSummaries={props.showReasoningSummaries}
useV2Actions={props.useV2Actions}
/>
)}
</Match>
@@ -873,6 +927,7 @@ export function AssistantMessageDisplay(props: {
parts: PartType[]
showAssistantCopyPartID?: string | null
showReasoningSummaries?: boolean
useV2Actions?: boolean
}) {
const emptyTools: ToolPart[] = []
const part = createMemo(() => index(props.parts))
@@ -932,6 +987,7 @@ export function AssistantMessageDisplay(props: {
part={item()!}
message={props.message}
showAssistantCopyPartID={props.showAssistantCopyPartID}
useV2Actions={props.useV2Actions}
/>
</Show>
)
@@ -1060,7 +1116,12 @@ export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean; onS
)
}
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[]; actions?: UserActions }) {
export function UserMessageDisplay(props: {
message: UserMessage
parts: PartType[]
actions?: UserActions
useV2Actions?: boolean
}) {
const data = useData()
const dialog = useDialog()
const i18n = useI18n()
@@ -1199,38 +1260,30 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
</span>
</Show>
<Show when={props.actions?.revert}>
<Tooltip value={i18n.t("ui.message.revertMessage")} placement="top" gutter={4}>
<IconButton
icon="reset"
size="normal"
variant="ghost"
disabled={!!busy()}
onMouseDown={(e) => e.preventDefault()}
onClick={(event) => {
event.stopPropagation()
revert()
}}
aria-label={i18n.t("ui.message.revertMessage")}
/>
</Tooltip>
</Show>
<Tooltip
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")}
placement="top"
gutter={4}
>
<IconButton
icon={copied() ? "check" : "copy"}
size="normal"
variant="ghost"
onMouseDown={(e) => e.preventDefault()}
<MessageActionButton
icon="reset"
label={i18n.t("ui.message.revertMessage")}
useV2={props.useV2Actions}
disabled={!!busy()}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
event.stopPropagation()
void handleCopy()
revert()
}}
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")}
aria-label={i18n.t("ui.message.revertMessage")}
/>
</Tooltip>
</Show>
<MessageActionButton
icon={copied() ? "check" : "copy"}
label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")}
useV2={props.useV2Actions}
onMouseDown={(event) => event.preventDefault()}
onClick={(event) => {
event.stopPropagation()
void handleCopy()
}}
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyMessage")}
/>
</div>
</>
</Show>
@@ -1294,6 +1347,7 @@ export function Part(props: MessagePartProps) {
onContentRendered={props.onContentRendered}
showAssistantCopyPartID={props.showAssistantCopyPartID}
turnDurationMs={props.turnDurationMs}
useV2Actions={props.useV2Actions}
/>
</Show>
)
@@ -1574,20 +1628,14 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
</div>
<Show when={showCopy()}>
<div data-slot="text-part-copy-wrapper" data-interrupted={interrupted() ? "" : undefined}>
<Tooltip
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyResponse")}
placement="top"
gutter={4}
>
<IconButton
icon={copied() ? "check" : "copy"}
size="normal"
variant="ghost"
onMouseDown={(e) => e.preventDefault()}
onClick={handleCopy}
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyResponse")}
/>
</Tooltip>
<MessageActionButton
icon={copied() ? "check" : "copy"}
label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyResponse")}
useV2={props.useV2Actions}
onMouseDown={(event) => event.preventDefault()}
onClick={handleCopy}
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copyResponse")}
/>
<Show when={meta()}>
<span data-slot="text-part-meta" class="text-12-regular text-text-weak cursor-default">
{meta()}
@@ -1928,20 +1976,16 @@ ToolRegistry.register({
>
<div data-component="bash-output">
<div data-slot="bash-copy">
<Tooltip
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
placement="top"
gutter={4}
>
<IconButton
icon={copied() ? "check" : "copy"}
size="small"
variant="secondary"
<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" />}
size="normal"
variant="ghost-muted"
onMouseDown={(e) => e.preventDefault()}
onClick={handleCopy}
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
/>
</Tooltip>
</TooltipV2>
</div>
<div data-slot="bash-scroll" data-scrollable>
<pre data-slot="bash-pre">
@@ -1221,7 +1221,11 @@ function Playground() {
message: { [session().id]: state.messages },
part: state.parts,
provider: {
all: [{ id: "anthropic", models: { "claude-sonnet-4-20250514": { name: "Claude Sonnet" } } }],
all: new Map([
["anthropic", { id: "anthropic", models: { "claude-sonnet-4-20250514": { name: "Claude Sonnet" } } }],
]),
connected: ["anthropic"],
default: {},
},
}))
@@ -59,24 +59,21 @@ export function ToolStatusTitle(props: {
const animate = () => {
const first = contentWidth(widthRef)
const next = props.active
finish()
setState("active", next)
if (!first) return
setState("animating", true)
setState("active", props.active)
const last = contentWidth(props.active ? activeRef : doneRef)
if (!first || !last) {
finish()
return
}
setState("width", first)
if (first === last) {
finishTimer = setTimeout(finish, 600)
return
}
frame = requestAnimationFrame(() => {
frame = undefined
setState("width", last)
const last = contentWidth(next ? activeRef : doneRef)
if (!last) {
finish()
return
}
if (first !== last) setState("width", last)
finishTimer = setTimeout(finish, 600)
})
}