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
co-authored by Frank Aarav Sareen Brendan Allan opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com> Jack Brendan Allan Shoubhit Dash opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com> James Long Dustin Deus starptech Luke Parker 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴 Dax usrnk1 Jay runvip opencode Julian Coy Vladimir Glafirov Adam Kit Langton Simon Klee Jay David Hill
parent f87998f37f
commit 9e0d3976e1
332 changed files with 24650 additions and 4497 deletions
@@ -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"] {
@@ -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
@@ -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(() => {
+8 -1
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 }))
})
@@ -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(() => {
@@ -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>