feat(app): more v2 ui alignment (#34465)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
This commit is contained in:
parent
4b1c7f28a0
commit
246dea3a58
6 changed files with 141 additions and 170 deletions
|
|
@ -1,9 +1,9 @@
|
||||||
import { Match, Show, Switch, createMemo } from "solid-js"
|
import { Match, Show, Switch, createMemo, type ComponentProps, type JSX } from "solid-js"
|
||||||
import { Tooltip, type TooltipProps } from "@opencode-ai/ui/tooltip"
|
|
||||||
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
|
||||||
import { ProgressCircleV2 } from "@opencode-ai/ui/v2/progress-circle-v2"
|
import { ProgressCircleV2 } from "@opencode-ai/ui/v2/progress-circle-v2"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
|
|
||||||
import { useFile } from "@/context/file"
|
import { useFile } from "@/context/file"
|
||||||
import { useLayout } from "@/context/layout"
|
import { useLayout } from "@/context/layout"
|
||||||
|
|
@ -18,7 +18,16 @@ import { createSessionTabs } from "@/pages/session/helpers"
|
||||||
interface SessionContextUsageProps {
|
interface SessionContextUsageProps {
|
||||||
variant?: "button" | "indicator"
|
variant?: "button" | "indicator"
|
||||||
buttonAppearance?: "default" | "v2"
|
buttonAppearance?: "default" | "v2"
|
||||||
placement?: TooltipProps["placement"]
|
placement?: ComponentProps<typeof TooltipV2>["placement"]
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextTooltipRow(props: { name: JSX.Element; value: JSX.Element }) {
|
||||||
|
return (
|
||||||
|
<div class="flex min-w-0 items-center gap-4">
|
||||||
|
<span class="shrink-0 text-v2-text-text-muted">{props.name}</span>
|
||||||
|
<span class="ml-auto min-w-0 truncate text-right text-v2-text-text-base">{props.value}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSessionContext(args: {
|
function openSessionContext(args: {
|
||||||
|
|
@ -113,38 +122,22 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||||
)
|
)
|
||||||
|
|
||||||
const tooltipValue = () => (
|
const tooltipValue = () => (
|
||||||
<div>
|
<div class="flex w-[120px] flex-col gap-2">
|
||||||
<Show when={tokens()}>
|
<ContextTooltipRow name={language.t("context.usage.cost")} value={cost()} />
|
||||||
{(value) => (
|
<ContextTooltipRow name={language.t("context.usage.usage")} value={`${context()?.usage ?? 0}%`} />
|
||||||
<div class="flex items-center gap-2">
|
<ContextTooltipRow
|
||||||
<span class="text-text-invert-strong">
|
name={language.t("context.usage.tokens")}
|
||||||
{getSessionTokenTotal(value())?.toLocaleString(language.intl())}
|
value={getSessionTokenTotal(tokens())?.toLocaleString(language.intl()) ?? "0"}
|
||||||
</span>
|
/>
|
||||||
<span class="text-text-invert-base">{language.t("context.usage.tokens")}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
<Show when={context()}>
|
|
||||||
{(ctx) => (
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-text-invert-strong">{ctx().usage ?? 0}%</span>
|
|
||||||
<span class="text-text-invert-base">{language.t("context.usage.usage")}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-text-invert-strong">{cost()}</span>
|
|
||||||
<span class="text-text-invert-base">{language.t("context.usage.cost")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={params.id}>
|
<Show when={params.id}>
|
||||||
<Switch>
|
<TooltipV2 value={tooltipValue()} placement={props.placement ?? "top"} shift={-8}>
|
||||||
<Match when={variant() === "indicator"}>{circle()}</Match>
|
<Switch>
|
||||||
<Match when={buttonAppearance() === "v2"}>
|
<Match when={variant() === "indicator"}>{circle()}</Match>
|
||||||
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
|
<Match when={buttonAppearance() === "v2"}>
|
||||||
<IconButtonV2
|
<IconButtonV2
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
|
|
@ -153,10 +146,8 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||||
onClick={openContext}
|
onClick={openContext}
|
||||||
aria-label={language.t("context.usage.view")}
|
aria-label={language.t("context.usage.view")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Match>
|
||||||
</Match>
|
<Match when={true}>
|
||||||
<Match when={true}>
|
|
||||||
<Tooltip value={tooltipValue()} placement={props.placement ?? "top"}>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -166,9 +157,9 @@ export function SessionContextUsage(props: SessionContextUsageProps) {
|
||||||
>
|
>
|
||||||
{circle()}
|
{circle()}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Match>
|
||||||
</Match>
|
</Switch>
|
||||||
</Switch>
|
</TooltipV2>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -192,65 +192,22 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.15s ease;
|
transition: opacity 0.15s ease;
|
||||||
z-index: 1;
|
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-component="markdown-code"]:hover [data-slot="markdown-copy-button"],
|
||||||
[data-slot="markdown-copy-button"]:focus-visible::after {
|
[data-slot="markdown-copy-button"]:focus-within {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="markdown-copy-button"][data-variant="secondary"] {
|
[data-slot="markdown-copy-button"] [data-check-icon] {
|
||||||
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"] {
|
|
||||||
display: none;
|
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;
|
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;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -389,18 +346,6 @@ body:not([data-new-layout]) [data-component="markdown"] {
|
||||||
background: none;
|
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,
|
th,
|
||||||
td {
|
td {
|
||||||
border-bottom: 1px solid var(--border-weaker-base);
|
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 morphdom from "morphdom"
|
||||||
import { checksum } from "@opencode-ai/core/util/encode"
|
import { checksum } from "@opencode-ai/core/util/encode"
|
||||||
import {
|
import {
|
||||||
ComponentProps,
|
type Accessor,
|
||||||
|
type ComponentProps,
|
||||||
createEffect,
|
createEffect,
|
||||||
createMemo,
|
createMemo,
|
||||||
createResource,
|
createResource,
|
||||||
createSignal,
|
createSignal,
|
||||||
createUniqueId,
|
createUniqueId,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
|
type Setter,
|
||||||
splitProps,
|
splitProps,
|
||||||
} from "solid-js"
|
} 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 { bundledLanguages } from "shiki"
|
||||||
import { canReusePendingBlock, project, type Block, type Projection } from "./markdown-stream"
|
import { canReusePendingBlock, project, type Block, type Projection } from "./markdown-stream"
|
||||||
import {
|
import {
|
||||||
|
|
@ -48,11 +53,6 @@ type RenderResult = {
|
||||||
|
|
||||||
const renderedCodeTokens = new WeakMap<HTMLDivElement, RenderedCodeState>()
|
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) {
|
function escape(text: string) {
|
||||||
return text
|
return text
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
|
|
@ -87,6 +87,14 @@ type CopyLabels = {
|
||||||
copied: string
|
copied: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CopyButtonState = {
|
||||||
|
setLabels: Setter<CopyLabels>
|
||||||
|
setCopied: Setter<boolean>
|
||||||
|
dispose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyButtonState = new WeakMap<HTMLElement, CopyButtonState>()
|
||||||
|
|
||||||
const urlPattern = /^https?:\/\/[^\s<>()`"']+$/
|
const urlPattern = /^https?:\/\/[^\s<>()`"']+$/
|
||||||
|
|
||||||
function codeUrl(text: string) {
|
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) {
|
function createCopyButton(labels: CopyLabels) {
|
||||||
const button = document.createElement("button")
|
const host = document.createElement("div")
|
||||||
button.type = "button"
|
host.setAttribute("data-slot", "markdown-copy-button")
|
||||||
button.setAttribute("data-component", "icon-button")
|
|
||||||
button.setAttribute("data-variant", "secondary")
|
const state: Partial<CopyButtonState> = {}
|
||||||
button.setAttribute("data-size", "small")
|
const dispose = render(() => {
|
||||||
button.setAttribute("data-slot", "markdown-copy-button")
|
const [labelState, setLabels] = createSignal(labels, { equals: false })
|
||||||
button.setAttribute("aria-label", labels.copy)
|
const [copied, setCopied] = createSignal(false)
|
||||||
button.setAttribute("data-tooltip", labels.copy)
|
state.setLabels = setLabels
|
||||||
button.appendChild(createIcon(iconPaths.copy, "copy-icon"))
|
state.setCopied = setCopied
|
||||||
button.appendChild(createIcon(iconPaths.check, "check-icon"))
|
return <MarkdownCopyButton labels={labelState} copied={copied} />
|
||||||
return button
|
}, 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) {
|
if (copied) {
|
||||||
button.setAttribute("data-copied", "true")
|
host.setAttribute("data-copied", "true")
|
||||||
button.setAttribute("aria-label", labels.copied)
|
|
||||||
button.setAttribute("data-tooltip", labels.copied)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
button.removeAttribute("data-copied")
|
host.removeAttribute("data-copied")
|
||||||
button.setAttribute("aria-label", labels.copy)
|
}
|
||||||
button.setAttribute("data-tooltip", labels.copy)
|
|
||||||
|
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"])
|
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)) {
|
for (const button of buttons.slice(1)) {
|
||||||
|
disposeCopyButton(button)
|
||||||
button.remove()
|
button.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -250,9 +281,9 @@ function decorate(root: HTMLDivElement, labels: CopyLabels) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupCodeCopy(root: HTMLDivElement, getLabels: () => 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 labels = getLabels()
|
||||||
const copied = button.getAttribute("data-copied") === "true"
|
const copied = button.getAttribute("data-copied") === "true"
|
||||||
setCopyState(button, labels, copied)
|
setCopyState(button, labels, copied)
|
||||||
|
|
@ -263,7 +294,7 @@ function setupCodeCopy(root: HTMLDivElement, getLabels: () => CopyLabels) {
|
||||||
if (!(target instanceof Element)) return
|
if (!(target instanceof Element)) return
|
||||||
|
|
||||||
const button = target.closest('[data-slot="markdown-copy-button"]')
|
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 code = button.closest('[data-component="markdown-code"]')?.querySelector("code")
|
||||||
const content = code?.textContent ?? ""
|
const content = code?.textContent ?? ""
|
||||||
if (!content) return
|
if (!content) return
|
||||||
|
|
@ -280,7 +311,7 @@ function setupCodeCopy(root: HTMLDivElement, getLabels: () => CopyLabels) {
|
||||||
|
|
||||||
const buttons = Array.from(root.querySelectorAll('[data-slot="markdown-copy-button"]'))
|
const buttons = Array.from(root.querySelectorAll('[data-slot="markdown-copy-button"]'))
|
||||||
for (const button of buttons) {
|
for (const button of buttons) {
|
||||||
if (button instanceof HTMLButtonElement) updateLabel(button)
|
if (button instanceof HTMLElement) updateLabel(button)
|
||||||
}
|
}
|
||||||
|
|
||||||
root.addEventListener("click", handleClick)
|
root.addEventListener("click", handleClick)
|
||||||
|
|
@ -290,6 +321,7 @@ function setupCodeCopy(root: HTMLDivElement, getLabels: () => CopyLabels) {
|
||||||
for (const timeout of timeouts.values()) {
|
for (const timeout of timeouts.values()) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
|
disposeCopyButtons(root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,6 +463,7 @@ export function Markdown(
|
||||||
if (!container) return
|
if (!container) return
|
||||||
if (isServer) return
|
if (isServer) return
|
||||||
if (content.length === 0) {
|
if (content.length === 0) {
|
||||||
|
disposeCopyButtons(container)
|
||||||
container.innerHTML = ""
|
container.innerHTML = ""
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -446,9 +479,14 @@ export function Markdown(
|
||||||
activeCodeKeys.clear()
|
activeCodeKeys.clear()
|
||||||
nextCodeKeys.forEach((key) => activeCodeKeys.add(key))
|
nextCodeKeys.forEach((key) => activeCodeKeys.add(key))
|
||||||
content.forEach((block, index) => updateBlock(container, index, block, labels))
|
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
|
container
|
||||||
.querySelectorAll<HTMLButtonElement>('[data-slot="markdown-copy-button"]')
|
.querySelectorAll<HTMLElement>('[data-slot="markdown-copy-button"]')
|
||||||
.forEach((button) => setCopyState(button, labels, button.dataset.copied === "true"))
|
.forEach((button) => setCopyState(button, labels, button.dataset.copied === "true"))
|
||||||
if (!copyCleanup)
|
if (!copyCleanup)
|
||||||
copyCleanup = setupCodeCopy(container, () => ({
|
copyCleanup = setupCodeCopy(container, () => ({
|
||||||
|
|
@ -538,8 +576,8 @@ function updateBlock(container: HTMLDivElement, index: number, block: RenderedBl
|
||||||
morphdom(current, next, {
|
morphdom(current, next, {
|
||||||
onBeforeElUpdated: (fromEl, toEl) => {
|
onBeforeElUpdated: (fromEl, toEl) => {
|
||||||
if (
|
if (
|
||||||
fromEl instanceof HTMLButtonElement &&
|
fromEl instanceof HTMLElement &&
|
||||||
toEl instanceof HTMLButtonElement &&
|
toEl instanceof HTMLElement &&
|
||||||
fromEl.getAttribute("data-slot") === "markdown-copy-button" &&
|
fromEl.getAttribute("data-slot") === "markdown-copy-button" &&
|
||||||
toEl.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
|
if (fromEl.isEqualNode(toEl)) return false
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
onBeforeNodeDiscarded: (node) => {
|
||||||
|
if (node instanceof Element) disposeCopyButtons(node)
|
||||||
|
return true
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -616,8 +658,12 @@ function updateCodeBlock(
|
||||||
unstable: block.unstable,
|
unstable: block.unstable,
|
||||||
raw: block.raw,
|
raw: block.raw,
|
||||||
})
|
})
|
||||||
if (current) current.replaceWith(next)
|
if (current) {
|
||||||
else container.appendChild(next)
|
disposeCopyButtons(current)
|
||||||
|
current.replaceWith(next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
container.appendChild(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameToken(left: MarkdownToken, right: MarkdownToken | undefined) {
|
function sameToken(left: MarkdownToken, right: MarkdownToken | undefined) {
|
||||||
|
|
|
||||||
|
|
@ -378,15 +378,6 @@
|
||||||
pointer-events: auto;
|
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"] {
|
[data-slot="bash-scroll"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
@ -1364,10 +1355,6 @@ body:not([data-new-layout]) {
|
||||||
|
|
||||||
[data-component="bash-output"] {
|
[data-component="bash-output"] {
|
||||||
border: 1px solid var(--border-weak-base);
|
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"],
|
[data-component="edit-trigger"],
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/core/ut
|
||||||
import { checksum } from "@opencode-ai/core/util/encode"
|
import { checksum } from "@opencode-ai/core/util/encode"
|
||||||
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
import { Tooltip } from "@opencode-ai/ui/tooltip"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
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 { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
|
||||||
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
|
||||||
import { Spinner } from "@opencode-ai/ui/spinner"
|
import { Spinner } from "@opencode-ai/ui/spinner"
|
||||||
|
|
@ -196,6 +197,7 @@ function MessageActionButton(
|
||||||
useV2?: boolean
|
useV2?: boolean
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
const icon = () => (props.icon === "copy" ? "outline-copy" : props.icon)
|
||||||
return (
|
return (
|
||||||
<Show
|
<Show
|
||||||
when={props.useV2}
|
when={props.useV2}
|
||||||
|
|
@ -215,7 +217,7 @@ function MessageActionButton(
|
||||||
>
|
>
|
||||||
<TooltipV2 value={props.label} placement="top" gutter={4}>
|
<TooltipV2 value={props.label} placement="top" gutter={4}>
|
||||||
<IconButtonV2
|
<IconButtonV2
|
||||||
icon={<Icon name={props.icon} size="small" />}
|
icon={<IconV2 name={icon()} size="small" />}
|
||||||
size="normal"
|
size="normal"
|
||||||
variant="ghost-muted"
|
variant="ghost-muted"
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
|
|
@ -1966,20 +1968,16 @@ ToolRegistry.register({
|
||||||
>
|
>
|
||||||
<div data-component="bash-output">
|
<div data-component="bash-output">
|
||||||
<div data-slot="bash-copy">
|
<div data-slot="bash-copy">
|
||||||
<Tooltip
|
<TooltipV2 value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="top">
|
||||||
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
<IconButtonV2
|
||||||
placement="top"
|
icon={<IconV2 name={copied() ? "check" : "outline-copy"} size="small" />}
|
||||||
gutter={4}
|
size="normal"
|
||||||
>
|
variant="ghost-muted"
|
||||||
<IconButton
|
|
||||||
icon={copied() ? "check" : "copy"}
|
|
||||||
size="small"
|
|
||||||
variant="secondary"
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</TooltipV2>
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="bash-scroll" data-scrollable>
|
<div data-slot="bash-scroll" data-scrollable>
|
||||||
<pre data-slot="bash-pre">
|
<pre data-slot="bash-pre">
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,10 @@ const icons = {
|
||||||
viewBox: "0 0 16 16",
|
viewBox: "0 0 16 16",
|
||||||
body: `<path d="M13.5555 6.66656V2.44434H9.33326M13.5555 2.44434L7.99993 7.99989M13.5555 9.33324V13.5555C13.5555 13.5555 12.7599 13.5555 11.7777 13.5555H2.44438C2.44438 13.5555 2.44438 12.7599 2.44438 11.7777V4.22213C2.44438 3.2399 2.44434 2.44435 2.44434 2.44435H6.66661" stroke="currentColor"/>`,
|
body: `<path d="M13.5555 6.66656V2.44434H9.33326M13.5555 2.44434L7.99993 7.99989M13.5555 9.33324V13.5555C13.5555 13.5555 12.7599 13.5555 11.7777 13.5555H2.44438C2.44438 13.5555 2.44438 12.7599 2.44438 11.7777V4.22213C2.44438 3.2399 2.44434 2.44435 2.44434 2.44435H6.66661" stroke="currentColor"/>`,
|
||||||
},
|
},
|
||||||
|
reset: {
|
||||||
|
viewBox: "0 0 20 20",
|
||||||
|
body: `<path d="M5.83333 4.16406L2.5 7.4974L5.83333 10.8307M3.33333 7.4974H17.9167V15.4141H10" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
|
},
|
||||||
archive: {
|
archive: {
|
||||||
viewBox: "0 0 16 16",
|
viewBox: "0 0 16 16",
|
||||||
body: `<path d="M13.1112 13.5555V14.0555H13.6112V13.5555H13.1112ZM2.889 13.5555H2.389L2.389 14.0555H2.889V13.5555ZM3.38901 5.55546L3.38901 5.05546L2.38901 5.05546L2.38901 5.55546L2.88901 5.55546L3.38901 5.55546ZM14.4446 2.44434H14.9446V1.94434L14.4446 1.94434L14.4446 2.44434ZM14.4446 5.55545L14.4446 6.05545L14.9446 6.05545V5.55545H14.4446ZM1.55566 5.55546L1.05566 5.55545L1.05566 6.05546L1.55566 6.05546L1.55566 5.55546ZM1.5557 2.44436L1.5557 1.94436L1.05571 1.94436L1.0557 2.44435L1.5557 2.44436ZM13.1112 5.55546H12.6112V13.5555H13.1112H13.6112V5.55546H13.1112ZM2.889 13.5555H3.389L3.38901 5.55546L2.88901 5.55546L2.38901 5.55546L2.389 13.5555H2.889ZM14.4446 2.44434H13.9446V5.55545H14.4446H14.9446V2.44434H14.4446ZM1.55566 5.55546L2.05566 5.55547L2.0557 2.44436L1.5557 2.44436L1.0557 2.44435L1.05566 5.55545L1.55566 5.55546ZM6.22234 8.22213V8.72213H9.7779V8.22213V7.72213H6.22234V8.22213ZM13.1112 13.5555V13.0555H2.889V13.5555V14.0555H13.1112V13.5555ZM1.5557 2.44436L1.5557 2.94436L14.4446 2.94434L14.4446 2.44434L14.4446 1.94434L1.5557 1.94436L1.5557 2.44436ZM14.4446 5.55545L14.4446 5.05545L1.55566 5.05546L1.55566 5.55546L1.55566 6.05546L14.4446 6.05545L14.4446 5.55545Z" fill="currentColor"/>`,
|
body: `<path d="M13.1112 13.5555V14.0555H13.6112V13.5555H13.1112ZM2.889 13.5555H2.389L2.389 14.0555H2.889V13.5555ZM3.38901 5.55546L3.38901 5.05546L2.38901 5.05546L2.38901 5.55546L2.88901 5.55546L3.38901 5.55546ZM14.4446 2.44434H14.9446V1.94434L14.4446 1.94434L14.4446 2.44434ZM14.4446 5.55545L14.4446 6.05545L14.9446 6.05545V5.55545H14.4446ZM1.55566 5.55546L1.05566 5.55545L1.05566 6.05546L1.55566 6.05546L1.55566 5.55546ZM1.5557 2.44436L1.5557 1.94436L1.05571 1.94436L1.0557 2.44435L1.5557 2.44436ZM13.1112 5.55546H12.6112V13.5555H13.1112H13.6112V5.55546H13.1112ZM2.889 13.5555H3.389L3.38901 5.55546L2.88901 5.55546L2.38901 5.55546L2.389 13.5555H2.889ZM14.4446 2.44434H13.9446V5.55545H14.4446H14.9446V2.44434H14.4446ZM1.55566 5.55546L2.05566 5.55547L2.0557 2.44436L1.5557 2.44436L1.0557 2.44435L1.05566 5.55545L1.55566 5.55546ZM6.22234 8.22213V8.72213H9.7779V8.22213V7.72213H6.22234V8.22213ZM13.1112 13.5555V13.0555H2.889V13.5555V14.0555H13.1112V13.5555ZM1.5557 2.44436L1.5557 2.94436L14.4446 2.94434L14.4446 2.44434L14.4446 1.94434L1.5557 1.94436L1.5557 2.44436ZM14.4446 5.55545L14.4446 5.05545L1.55566 5.05546L1.55566 5.55546L1.55566 6.05546L14.4446 6.05545L14.4446 5.55545Z" fill="currentColor"/>`,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue