chore: merge dev into v2 (#35591)

Co-authored-by: Frank <frank@anoma.ly>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: Brendan Allan <git@brendonovich.dev>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: Jack <jack@anoma.ly>
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: Shoubhit Dash <shoubhit2005@gmail.com>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: Dustin Deus <deusdustin@gmail.com>
Co-authored-by: starptech <starptech@starptechs-MBP.fritz.box>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>
Co-authored-by: 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 <little-frank@opencord.local>
Co-authored-by: Dax <mail@thdxr.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>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: Simon Klee <hello@simonklee.dk>
Co-authored-by: Jay <air@live.ca>
Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com>
This commit is contained in:
Aiden Cline 2026-07-06 16:05:29 -05:00 committed by GitHub
commit 9e0d3976e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
332 changed files with 24739 additions and 4586 deletions

View file

@ -230,6 +230,7 @@
line-height: var(--line-height-large);
letter-spacing: var(--letter-spacing-normal);
text-transform: capitalize;
color: var(--task-agent-legacy-color, var(--text-strong));
}
[data-slot="basic-tool-tool-subtitle"] {
@ -247,6 +248,57 @@
}
}
body[data-new-layout] {
[data-component="task-tool-card"] {
padding: 8px 12px 8px 10px;
border-radius: 8px;
border: 0;
border-color: transparent;
background: var(--task-agent-background, light-dark(#fafafa, rgba(250, 250, 250, 0.15)));
box-shadow: inset 0 0 0 0.5px var(--task-agent-border, var(--v2-border-border-base));
[data-component="task-tool-spinner"],
[data-component="task-tool-title"] {
color: var(--task-agent-color, var(--text-strong));
}
[data-component="task-tool-title"] {
height: 20px;
font-family: var(--v2-font-family-sans, "Inter", sans-serif);
font-style: normal;
font-weight: 530;
font-size: 13px;
line-height: 20px;
letter-spacing: -0.04px;
flex: none;
flex-grow: 0;
}
[data-slot="basic-tool-tool-subtitle"] {
font-family: var(--v2-font-family-sans, "Inter", sans-serif);
font-style: normal;
font-weight: 440;
font-size: 13px;
line-height: 20px;
letter-spacing: -0.04px;
color: light-dark(var(--v2-text-text-base), #fafafa);
font-variation-settings: "slnt" 0;
flex: none;
flex-grow: 0;
}
[data-component="session-progress-indicator-v2"] {
color: var(--task-agent-color, light-dark(var(--v2-text-text-base), #ffffff));
}
&:hover,
&:focus-visible {
border-color: transparent;
background: var(--task-agent-background, light-dark(#fafafa, rgba(250, 250, 250, 0.15)));
}
}
}
body:not([data-new-layout]) {
[data-component="tool-trigger"] {
[data-slot="basic-tool-tool-spinner"] {

View file

@ -37,7 +37,9 @@ export interface BasicToolProps {
animated?: boolean
onSubtitleClick?: () => void
onTriggerClick?: JSX.EventHandlerUnion<HTMLElement, MouseEvent>
onTriggerKeyDown?: JSX.EventHandlerUnion<HTMLElement, KeyboardEvent>
triggerHref?: string
triggerAsLink?: boolean
clickable?: boolean
}
@ -254,7 +256,7 @@ export function BasicTool(props: BasicToolProps) {
return (
<Collapsible open={open()} onOpenChange={handleOpenChange} class="tool-collapsible">
<Show
when={props.triggerHref}
when={props.triggerAsLink || props.triggerHref}
fallback={
<Collapsible.Trigger
data-hide-details={props.hideDetails ? "true" : undefined}
@ -264,16 +266,17 @@ export function BasicTool(props: BasicToolProps) {
</Collapsible.Trigger>
}
>
{(href) => (
<Collapsible.Trigger
as="a"
href={href()}
data-hide-details={props.hideDetails ? "true" : undefined}
onClick={props.onTriggerClick}
>
{trigger()}
</Collapsible.Trigger>
)}
<Collapsible.Trigger
as="a"
href={props.triggerHref}
role={!props.triggerHref && props.clickable ? "button" : undefined}
tabIndex={!props.triggerHref && props.clickable ? 0 : undefined}
data-hide-details={props.hideDetails ? "true" : undefined}
onClick={props.onTriggerClick}
onKeyDown={props.onTriggerKeyDown}
>
{trigger()}
</Collapsible.Trigger>
</Show>
<Show when={props.animated && hasChildren() && !props.hideDetails}>
<div

View file

@ -1,5 +1,6 @@
import type { FileContent } from "@opencode-ai/sdk/v2"
import { createEffect, createMemo, createResource, Match, on, Show, Switch, type JSX } from "solid-js"
import { createEffect, createMemo, Match, on, onCleanup, Show, Switch, untrack, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { useI18n } from "@opencode-ai/ui/context/i18n"
import {
dataUrlFromMediaValue,
@ -30,6 +31,13 @@ function mediaValue(cfg: FileMediaOptions, mode: "image" | "audio") {
export function FileMedia(props: { media?: FileMediaOptions; fallback: () => JSX.Element }) {
const i18n = useI18n()
const [remote, setRemote] = createStore<{
key?: string
loading?: boolean
error?: boolean
src?: string
mime?: string
}>({})
const cfg = () => props.media
const kind = createMemo(() => {
const media = cfg()
@ -81,50 +89,66 @@ export function FileMedia(props: { media?: FileMediaOptions; fallback: () => JSX
}
})
const [loaded] = createResource(request, async (input) => {
return input.readFile(input.path).then(
createEffect(() => {
const input = request()
if (!input) {
setRemote({ key: undefined, loading: false, error: false, src: undefined, mime: undefined })
return
}
let active = true
// Keep the previous media visible while re-reading the same file (e.g. a vcs
// diff refresh); only a key change resets to the loading placeholder.
if (untrack(() => remote.key) === input.key) setRemote({ loading: true, error: false })
else setRemote({ key: input.key, loading: true, error: false, src: undefined, mime: undefined })
void input.readFile(input.path).then(
(result) => {
if (!active) return
const src = dataUrlFromMediaValue(result as any, input.kind)
if (!src) {
input.onError?.({ kind: input.kind })
return { key: input.key, error: true as const }
setRemote({ key: input.key, loading: false, error: true, src: undefined, mime: undefined })
return
}
return {
setRemote({
key: input.key,
loading: false,
error: false,
src,
mime: input.kind === "audio" ? normalizeMimeType(result?.mimeType) : undefined,
}
})
},
() => {
if (!active) return
input.onError?.({ kind: input.kind })
return { key: input.key, error: true as const }
setRemote({ key: input.key, loading: false, error: true, src: undefined, mime: undefined })
},
)
})
const remote = createMemo(() => {
const input = request()
const value = loaded()
if (!input || !value || value.key !== input.key) return
return value
onCleanup(() => {
active = false
})
})
const src = createMemo(() => {
const value = remote()
return direct() ?? (value && "src" in value ? value.src : undefined)
const input = request()
if (!input || remote.key !== input.key || remote.error) return direct()
return direct() ?? remote.src
})
const status = createMemo(() => {
if (direct()) return "ready" as const
if (!request()) return "idle" as const
if (loaded.loading) return "loading" as const
if (remote()?.error) return "error" as const
const input = request()
if (!input) return "idle" as const
if (remote.key !== input.key || remote.loading) return "loading" as const
if (remote.error) return "error" as const
if (src()) return "ready" as const
return "idle" as const
})
const audioMime = createMemo(() => {
const value = remote()
return value && "mime" in value ? value.mime : undefined
const input = request()
if (!input || remote.key !== input.key) return
return remote.mime
})
const svgSource = createMemo(() => {

View file

@ -456,11 +456,18 @@ function useAnnotationRerender<A>(opts: {
current: () => AnnotationTarget<A> | undefined
annotations: () => A[]
}) {
const applied = new WeakSet<AnnotationTarget<A>>()
createEffect(() => {
opts.viewer.rendered()
const active = opts.current()
if (!active) return
active.setLineAnnotations(opts.annotations())
const annotations = opts.annotations()
// renderViewer always draws with empty annotations, so skip the extra rerender
// when this instance has nothing applied and nothing to apply.
if (annotations.length === 0 && !applied.has(active)) return
if (annotations.length === 0) applied.delete(active)
else applied.add(active)
active.setLineAnnotations(annotations)
active.rerender()
requestAnimationFrame(() => opts.viewer.find.refresh({ reset: true }))
})

View file

@ -34,7 +34,7 @@ type HoverCommentLine = {
side?: "additions" | "deletions"
}
type LineCommentStateProps<T> = {
export type LineCommentStateProps<T> = {
opened: Accessor<T | null>
setOpened: (id: T | null) => void
selected: Accessor<SelectedLineRange | null>
@ -45,7 +45,7 @@ type LineCommentStateProps<T> = {
hoverSelected?: (range: SelectedLineRange) => void
}
type LineCommentShape = {
export type LineCommentShape = {
id: string
selection: SelectedLineRange
comment: string
@ -95,9 +95,14 @@ type DraftProps = {
submitLabel?: string
}
export function createLineCommentAnnotationRenderer<T>(props: {
renderComment: (comment: T) => CommentProps
renderDraft: (range: SelectedLineRange) => DraftProps
// Generic host machinery shared by the v1 and v2 annotation renderers: each
// annotation key gets a detached DOM host with its own Solid root, updated in
// place through a signal so Pierre can reparent the host without re-rendering.
export function createLineCommentAnnotationRenderer<T, C, D>(props: {
renderComment: (comment: T) => C
renderDraft: (range: SelectedLineRange) => D
commentElement: (view: Accessor<C>) => JSX.Element
draftElement: (view: Accessor<D>) => JSX.Element
}) {
const nodes = new Map<
string,
@ -123,37 +128,7 @@ export function createLineCommentAnnotationRenderer<T>(props: {
if (next.kind !== "comment") return props.renderComment(active.comment)
return props.renderComment(next.comment)
})
return (
<Show
when={view().editor}
fallback={
<LineComment
inline
id={view().id}
open={view().open}
comment={view().comment}
selection={view().selection}
actions={view().actions}
onClick={view().onClick}
onMouseEnter={view().onMouseEnter}
/>
}
>
<LineCommentEditor
inline
id={view().id}
value={view().editor!.value}
selection={view().editor!.selection}
onInput={view().editor!.onInput}
onCancel={view().editor!.onCancel}
onSubmit={view().editor!.onSubmit}
onPopoverFocusOut={view().editor!.onPopoverFocusOut}
cancelLabel={view().editor!.cancelLabel}
submitLabel={view().editor!.submitLabel}
mention={view().editor!.mention}
/>
</Show>
)
return props.commentElement(view)
}
const view = createMemo(() => {
@ -161,18 +136,7 @@ export function createLineCommentAnnotationRenderer<T>(props: {
if (next.kind !== "draft") return props.renderDraft(active.range)
return props.renderDraft(next.range)
})
return (
<LineCommentEditor
inline
value={view().value}
selection={view().selection}
onInput={view().onInput}
onCancel={view().onCancel}
onSubmit={view().onSubmit}
onPopoverFocusOut={view().onPopoverFocusOut}
mention={view().mention}
/>
)
return props.draftElement(view)
}, host)
const node = { host, dispose, setMeta: setCurrent }
@ -205,6 +169,55 @@ export function createLineCommentAnnotationRenderer<T>(props: {
return { render, reconcile, cleanup }
}
function lineCommentElement(view: Accessor<CommentProps>) {
return (
<Show
when={view().editor}
fallback={
<LineComment
inline
id={view().id}
open={view().open}
comment={view().comment}
selection={view().selection}
actions={view().actions}
onClick={view().onClick}
onMouseEnter={view().onMouseEnter}
/>
}
>
<LineCommentEditor
inline
id={view().id}
value={view().editor!.value}
selection={view().editor!.selection}
onInput={view().editor!.onInput}
onCancel={view().editor!.onCancel}
onSubmit={view().editor!.onSubmit}
onPopoverFocusOut={view().editor!.onPopoverFocusOut}
cancelLabel={view().editor!.cancelLabel}
submitLabel={view().editor!.submitLabel}
mention={view().editor!.mention}
/>
</Show>
)
}
function lineCommentDraftElement(view: Accessor<DraftProps>) {
return (
<LineCommentEditor
inline
value={view().value}
selection={view().selection}
onInput={view().onInput}
onCancel={view().onCancel}
onSubmit={view().onSubmit}
onPopoverFocusOut={view().onPopoverFocusOut}
mention={view().mention}
/>
)
}
export function createLineCommentState<T>(props: LineCommentStateProps<T>) {
const [state, setState] = createStore({
draft: "",
@ -314,7 +327,9 @@ export function createLineCommentController<T extends LineCommentShape>(
): {
note: ReturnType<typeof createLineCommentState<string>>
annotations: Accessor<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>
renderAnnotation: ReturnType<typeof createManagedLineCommentAnnotationRenderer<T>>["renderAnnotation"]
renderAnnotation: ReturnType<
typeof createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>
>["renderAnnotation"]
renderGutterUtility: ReturnType<typeof createLineCommentGutterRenderer>
onLineSelected: (range: SelectedLineRange | null) => void
onLineSelectionEnd: (range: SelectedLineRange | null) => void
@ -324,7 +339,9 @@ export function createLineCommentController<T extends LineCommentShape>(
): {
note: ReturnType<typeof createLineCommentState<string>>
annotations: Accessor<LineCommentAnnotation<T>[]>
renderAnnotation: ReturnType<typeof createManagedLineCommentAnnotationRenderer<T>>["renderAnnotation"]
renderAnnotation: ReturnType<
typeof createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>
>["renderAnnotation"]
renderGutterUtility: ReturnType<typeof createLineCommentGutterRenderer>
onLineSelected: (range: SelectedLineRange | null) => void
onLineSelectionEnd: (range: SelectedLineRange | null) => void
@ -353,8 +370,10 @@ export function createLineCommentController<T extends LineCommentShape>(
draftKey: props.draftKey,
})
const { renderAnnotation } = createManagedLineCommentAnnotationRenderer<T>({
const { renderAnnotation } = createManagedLineCommentAnnotationRenderer<T, CommentProps, DraftProps>({
annotations,
commentElement: lineCommentElement,
draftElement: lineCommentDraftElement,
renderComment: (comment) => {
const edit = () => note.openEditor(comment.id, comment.selection, comment.comment)
const remove = () => {
@ -471,77 +490,111 @@ export function createLineCommentAnnotations<T>(
const line = (range: SelectedLineRange) => Math.max(range.start, range.end)
if ("getSide" in props) {
return createMemo<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>(() => {
return createMemo<DiffLineAnnotation<LineCommentAnnotationMeta<T>>[]>(
() => {
const list = props.comments().map((comment) => {
const range = props.getCommentSelection(comment)
return {
side: props.getSide(range),
lineNumber: line(range),
metadata: {
kind: "comment",
key: `comment:${props.getCommentId(comment)}`,
comment,
} satisfies LineCommentAnnotationMeta<T>,
}
})
const range = props.draftRange()
if (!range) return list
return [
...list,
{
side: props.getSide(range),
lineNumber: line(range),
metadata: {
kind: "draft",
key: `draft:${props.draftKey()}`,
range,
} satisfies LineCommentAnnotationMeta<T>,
},
]
},
[],
// Stable identity for unchanged annotations avoids no-op diff rerenders downstream.
{ equals: sameAnnotationLists },
)
}
return createMemo<LineCommentAnnotation<T>[]>(
() => {
const list = props.comments().map((comment) => {
const range = props.getCommentSelection(comment)
return {
side: props.getSide(range),
const entry: LineCommentAnnotation<T> = {
lineNumber: line(range),
metadata: {
kind: "comment",
key: `comment:${props.getCommentId(comment)}`,
comment,
} satisfies LineCommentAnnotationMeta<T>,
},
}
return entry
})
const range = props.draftRange()
if (!range) return list
return [
...list,
{
side: props.getSide(range),
lineNumber: line(range),
metadata: {
kind: "draft",
key: `draft:${props.draftKey()}`,
range,
} satisfies LineCommentAnnotationMeta<T>,
},
]
})
}
return createMemo<LineCommentAnnotation<T>[]>(() => {
const list = props.comments().map((comment) => {
const range = props.getCommentSelection(comment)
const entry: LineCommentAnnotation<T> = {
const draft: LineCommentAnnotation<T> = {
lineNumber: line(range),
metadata: {
kind: "comment",
key: `comment:${props.getCommentId(comment)}`,
comment,
kind: "draft",
key: `draft:${props.draftKey()}`,
range,
},
}
return entry
})
return [...list, draft]
},
[],
{ equals: sameAnnotationLists },
)
}
const range = props.draftRange()
if (!range) return list
type AnnotationListItem = {
lineNumber: number
side?: unknown
metadata: { kind: string; key: string; comment?: unknown; range?: unknown }
}
const draft: LineCommentAnnotation<T> = {
lineNumber: line(range),
metadata: {
kind: "draft",
key: `draft:${props.draftKey()}`,
range,
},
}
return [...list, draft]
function sameAnnotationLists(previous: AnnotationListItem[], next: AnnotationListItem[]) {
if (previous.length !== next.length) return false
return previous.every((item, index) => {
const other = next[index]!
return (
item.lineNumber === other.lineNumber &&
item.side === other.side &&
item.metadata.kind === other.metadata.kind &&
item.metadata.key === other.metadata.key &&
item.metadata.comment === other.metadata.comment &&
item.metadata.range === other.metadata.range
)
})
}
export function createManagedLineCommentAnnotationRenderer<T>(props: {
export function createManagedLineCommentAnnotationRenderer<T, C, D>(props: {
annotations: Accessor<LineCommentAnnotation<T>[]>
renderComment: (comment: T) => CommentProps
renderDraft: (range: SelectedLineRange) => DraftProps
renderComment: (comment: T) => C
renderDraft: (range: SelectedLineRange) => D
commentElement: (view: Accessor<C>) => JSX.Element
draftElement: (view: Accessor<D>) => JSX.Element
}) {
const renderer = createLineCommentAnnotationRenderer<T>({
const renderer = createLineCommentAnnotationRenderer<T, C, D>({
renderComment: props.renderComment,
renderDraft: props.renderDraft,
commentElement: props.commentElement,
draftElement: props.draftElement,
})
createEffect(() => {

View file

@ -62,6 +62,7 @@ import { animate } from "motion"
import { useLocation } from "@solidjs/router"
import { attached, inline, kind } from "./message-file"
import { readPartText } from "./message-part-text"
import { SessionProgressIndicatorV2 } from "../v2/components/session-progress-indicator-v2"
async function writeClipboard(text: string): Promise<boolean> {
const body = typeof document === "undefined" ? undefined : document.body
@ -370,6 +371,44 @@ const agentTones: Record<string, string> = {
plan: "var(--icon-agent-plan-base)",
}
const v2AgentTones: Record<string, { color: string; border: string; background: string }> = {
build: {
color: "var(--v2-agent-build-solid)",
border: "var(--v2-agent-build-border)",
background: "var(--v2-agent-build-background)",
},
explore: {
color: "var(--v2-agent-explore-solid)",
border: "var(--v2-agent-explore-border)",
background: "var(--v2-agent-explore-background)",
},
plan: {
color: "var(--v2-agent-plan-solid)",
border: "var(--v2-agent-plan-border)",
background: "var(--v2-agent-plan-background)",
},
}
const agentThemeColors: Record<string, string> = {
primary: "var(--text-interactive-base)",
secondary: "var(--text-base)",
accent: "var(--icon-info-base)",
success: "var(--icon-success-base)",
warning: "var(--icon-warning-base)",
error: "var(--icon-critical-base)",
info: "var(--icon-info-base)",
}
const v2AgentThemeColors: Record<string, string> = {
primary: "var(--v2-text-text-accent)",
secondary: "var(--v2-text-text-muted)",
accent: "var(--v2-icon-icon-accent)",
success: "var(--v2-state-fg-success)",
warning: "var(--v2-state-fg-warning)",
error: "var(--v2-state-fg-danger)",
info: "var(--v2-state-fg-info)",
}
const agentPalette = [
"var(--icon-agent-ask-base)",
"var(--icon-agent-build-base)",
@ -394,16 +433,31 @@ function tone(name: string) {
function taskAgent(
raw: unknown,
list?: readonly { name: string; color?: string }[],
): { name?: string; color?: string } {
): { name?: string; color?: string; v2Color?: string; border?: string; background?: string } {
if (typeof raw !== "string" || !raw) return {}
const key = raw.toLowerCase()
const item = list?.find((entry) => entry.name === raw || entry.name.toLowerCase() === key)
const v2Tone = item?.color ? undefined : v2AgentTones[key]
const color = agentColor(item?.color, agentThemeColors) ?? agentTones[key] ?? tone(key)
const v2Color = agentColor(item?.color, v2AgentThemeColors) ?? v2Tone?.color ?? color
return {
name: item?.name ?? `${raw[0]!.toUpperCase()}${raw.slice(1)}`,
color: item?.color ?? agentTones[key] ?? tone(key),
color,
v2Color,
border: v2Tone?.border ?? `color-mix(in srgb, ${v2Color} 48%, transparent)`,
background: v2Tone?.background ?? `color-mix(in srgb, ${v2Color} 12%, transparent)`,
}
}
function agentColor(value: string | undefined, themeColors: Record<string, string>) {
if (!value) return
return themeColors[value] ?? value
}
function newLayout() {
return typeof document !== "undefined" && document.body.hasAttribute("data-new-layout")
}
function webSearchProviderLabel(provider: unknown) {
if (provider === "parallel") return "Parallel Web Search"
if (provider === "exa") return "Exa Web Search"
@ -1000,16 +1054,24 @@ export function AssistantMessageDisplay(props: {
)
}
export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean; onSizeChange?: () => void }) {
export function ContextToolGroup(props: {
parts: ToolPart[]
busy?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
onSizeChange?: () => void
}) {
const i18n = useI18n()
const [open, setOpen] = createSignal(false)
const [localOpen, setLocalOpen] = createSignal(false)
const open = () => props.open ?? localOpen()
const pending = createMemo(
() =>
!!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
)
const summary = createMemo(() => contextToolSummary(props.parts))
const handleOpenChange = (value: boolean) => {
setOpen(value)
if (props.open === undefined) setLocalOpen(value)
props.onOpenChange?.(value)
props.onSizeChange?.()
}
@ -1718,7 +1780,13 @@ ToolRegistry.register({
trigger={{ title: i18n.t("ui.tool.list"), subtitle: getDirectory(props.input.path || "/") }}
>
<Show when={props.output}>
<div data-component="tool-output" data-scrollable>
<div
data-component="tool-output"
data-scrollable
tabIndex={0}
role="region"
aria-label={i18n.t("ui.scrollView.ariaLabel")}
>
<Markdown text={props.output!} />
</div>
</Show>
@ -1742,7 +1810,13 @@ ToolRegistry.register({
}}
>
<Show when={props.output}>
<div data-component="tool-output" data-scrollable>
<div
data-component="tool-output"
data-scrollable
tabIndex={0}
role="region"
aria-label={i18n.t("ui.scrollView.ariaLabel")}
>
<Markdown text={props.output!} />
</div>
</Show>
@ -1769,7 +1843,13 @@ ToolRegistry.register({
}}
>
<Show when={props.output}>
<div data-component="tool-output" data-scrollable>
<div
data-component="tool-output"
data-scrollable
tabIndex={0}
role="region"
aria-label={i18n.t("ui.scrollView.ariaLabel")}
>
<Markdown text={props.output!} />
</div>
</Show>
@ -1864,6 +1944,9 @@ ToolRegistry.register({
const agent = createMemo(() => taskAgent(props.input.subagent_type, data.store.agent))
const title = createMemo(() => agent().name ?? i18n.t("ui.tool.agent.default"))
const tone = createMemo(() => agent().color)
const v2Tone = createMemo(() => agent().v2Color)
const border = createMemo(() => agent().border)
const background = createMemo(() => agent().background)
const subtitle = createMemo(() => {
const value =
typeof props.input.description === "string" && props.input.description
@ -1895,19 +1978,35 @@ ToolRegistry.register({
event.preventDefault()
open()
}
const navigateKey = (event: KeyboardEvent) => {
if (!clickable() || href()) return
if (event.key !== "Enter" && event.key !== " ") return
event.preventDefault()
open()
}
const trigger = () => (
<div data-component="task-tool-card">
<div
data-component="task-tool-card"
style={{
"--task-agent-color": v2Tone(),
"--task-agent-legacy-color": tone(),
"--task-agent-border": border(),
"--task-agent-background": background(),
}}
>
<div data-slot="basic-tool-tool-info-structured">
<div data-slot="basic-tool-tool-info-main">
<Show when={running()}>
<span data-component="task-tool-spinner" style={{ color: tone() ?? "var(--icon-interactive-base)" }}>
<Spinner />
<Show when={newLayout()} fallback={<Spinner />}>
<SessionProgressIndicatorV2
style={{ color: v2Tone() ?? "light-dark(var(--v2-text-text-base), #ffffff)" }}
/>
</Show>
</span>
</Show>
<span data-component="task-tool-title" style={{ color: tone() ?? "var(--text-strong)" }}>
{title()}
</span>
<span data-component="task-tool-title">{title()}</span>
<Show when={subtitle()}>
<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>
</Show>
@ -1927,9 +2026,11 @@ ToolRegistry.register({
status={props.status}
trigger={trigger()}
hideDetails
triggerAsLink
triggerHref={href()}
clickable={clickable()}
onTriggerClick={navigate}
onTriggerKeyDown={navigateKey}
/>
)
},
@ -1987,7 +2088,13 @@ ToolRegistry.register({
/>
</TooltipV2>
</div>
<div data-slot="bash-scroll" data-scrollable>
<div
data-slot="bash-scroll"
data-scrollable
tabIndex={0}
role="region"
aria-label={i18n.t("ui.scrollView.ariaLabel")}
>
<pre data-slot="bash-pre">
<code>{text()}</code>
</pre>