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:
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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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, "&")
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user