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:
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
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue