import { createMemo, createSignal, For, onMount } from "solid-js" import type { ToolPart } from "@opencode-ai/sdk/v2" import { getFilename } from "@opencode-ai/util/path" import { useI18n } from "../context/i18n" import { prefersReducedMotion } from "../hooks/use-reduced-motion" import { ToolCall } from "./basic-tool" import { ToolStatusTitle } from "./tool-status-title" import { AnimatedCountList } from "./tool-count-summary" import { RollingResults } from "./rolling-results" import { GROW_SPRING } from "./motion" import { useSpring } from "./motion-spring" import { busy, updateScrollMask, useCollapsible, useRowWipe } from "./tool-utils" function contextToolLabel(part: ToolPart): { action: string; detail: string } { const state = part.state const title = "title" in state ? (state.title as string | undefined) : undefined const input = state.input if (part.tool === "read") { const path = input?.filePath as string | undefined return { action: "Read", detail: title || (path ? getFilename(path) : "") } } if (part.tool === "grep") { const pattern = input?.pattern as string | undefined return { action: "Search", detail: title || (pattern ? `"${pattern}"` : "") } } if (part.tool === "glob") { const pattern = input?.pattern as string | undefined return { action: "Find", detail: title || (pattern ?? "") } } if (part.tool === "list") { const path = input?.path as string | undefined return { action: "List", detail: title || (path ? getFilename(path) : "") } } return { action: part.tool, detail: title || "" } } function contextToolSummary(parts: ToolPart[]) { let read = 0 let search = 0 let list = 0 for (const part of parts) { if (part.tool === "read") read++ else if (part.tool === "glob" || part.tool === "grep") search++ else if (part.tool === "list") list++ } return { read, search, list } } export function ContextToolGroupHeader(props: { parts: ToolPart[] pending: boolean open: boolean onOpenChange: (value: boolean) => void }) { const i18n = useI18n() const summary = createMemo(() => contextToolSummary(props.parts)) return ( { if (!props.pending) props.onOpenChange(v) }} trigger={
} /> ) } export function ContextToolExpandedList(props: { parts: ToolPart[]; expanded: boolean }) { let contentRef: HTMLDivElement | undefined let bodyRef: HTMLDivElement | undefined let scrollRef: HTMLDivElement | undefined const updateMask = () => { if (scrollRef) updateScrollMask(scrollRef) } useCollapsible({ content: () => contentRef, body: () => bodyRef, open: () => props.expanded, onOpen: updateMask, }) return (
{(part) => { const label = createMemo(() => contextToolLabel(part)) return (
{label().action} {label().detail}
) }}
) } export function ContextToolRollingResults(props: { parts: ToolPart[]; pending: boolean }) { const wiped = new Set() const [mounted, setMounted] = createSignal(false) onMount(() => setMounted(true)) const reduce = prefersReducedMotion const show = () => mounted() && props.pending const opacity = useSpring(() => (show() ? 1 : 0), GROW_SPRING) const blur = useSpring(() => (show() ? 0 : 2), GROW_SPRING) return (
part.callID || part.id} render={(part) => { const label = createMemo(() => contextToolLabel(part)) const k = part.callID || part.id return (
{label().action} {(() => { const [detailRef, setDetailRef] = createSignal() useRowWipe({ id: () => k, text: () => label().detail, ref: detailRef, seen: wiped, }) return ( {label().detail} ) })()}
) }} />
) }