import map from "lang-map" import { DateTime } from "luxon" import { For, Show, Match, Switch, type JSX, createMemo, createSignal, type ParentProps } from "solid-js" import { IconHashtag, IconSparkles, IconGlobeAlt, IconDocument, IconPaperClip, IconQueueList, IconUserCircle, IconCommandLine, IconCheckCircle, IconChevronDown, IconChevronRight, IconDocumentPlus, IconPencilSquare, IconRectangleStack, IconMagnifyingGlass, IconDocumentMagnifyingGlass, } from "../icons" import { IconMeta, IconRobot, IconOpenAI, IconGemini, IconAnthropic, IconBrain } from "../icons/custom" import { ContentCode } from "./content-code" import { ContentDiff } from "./content-diff" import { ContentText } from "./content-text" import { ContentBash } from "./content-bash" import { ContentError } from "./content-error" import { formatDuration } from "../share/common" import { ContentMarkdown } from "./content-markdown" import type { MessageV2 } from "opencode/session/message-v2" import type { Diagnostic } from "vscode-languageserver-types" import styles from "./part.module.css" const MIN_DURATION = 2000 export interface PartProps { index: number message: MessageV2.Info part: MessageV2.Part last: boolean } export function Part(props: PartProps) { const [copied, setCopied] = createSignal(false) const id = createMemo(() => props.message.id + "-" + props.index) return (
{ e.preventDefault() const anchor = e.currentTarget const hash = anchor.getAttribute("href") || "" const { origin, pathname, search } = window.location navigator.clipboard .writeText(`${origin}${pathname}${search}${hash}`) .catch((err) => console.error("Copy failed", err)) setCopied(true) setTimeout(() => setCopied(false), 3000) }} > {(model) => } Copied!
{props.message.role === "user" && props.part.type === "text" && (
)} {props.message.role === "assistant" && props.part.type === "text" && (
{props.last && props.message.role === "assistant" && props.message.time.completed && (
{DateTime.fromMillis(props.message.time.completed).toLocaleString(DateTime.DATETIME_MED)}
)}
)} {props.message.role === "assistant" && props.part.type === "reasoning" && (
Thinking
)} {props.message.role === "user" && props.part.type === "file" && (
Attachment
{props.part.filename}
)} {props.part.type === "step-start" && props.message.role === "assistant" && (
{props.message.providerID}
{props.message.modelID}
)} {props.part.type === "tool" && props.part.state.status === "error" && (
{formatErrorString(props.part.state.error)}
)} {props.part.type === "tool" && props.part.state.status === "completed" && props.message.role === "assistant" && ( <>
)}
) } type ToolProps = { id: MessageV2.ToolPart["id"] tool: MessageV2.ToolPart["tool"] state: MessageV2.ToolStateCompleted message: MessageV2.Assistant isLastPart?: boolean } interface Todo { id: string content: string status: "pending" | "in_progress" | "completed" priority: "low" | "medium" | "high" } function stripWorkingDirectory(filePath?: string, workingDir?: string) { if (filePath === undefined || workingDir === undefined) return filePath const prefix = workingDir.endsWith("/") ? workingDir : workingDir + "/" if (filePath === workingDir) { return "" } if (filePath.startsWith(prefix)) { return filePath.slice(prefix.length) } return filePath } function getShikiLang(filename: string) { const ext = filename.split(".").pop()?.toLowerCase() ?? "" const langs = map.languages(ext) const type = langs?.[0]?.toLowerCase() const overrides: Record = { conf: "shellscript", } return type ? (overrides[type] ?? type) : "plaintext" } function getDiagnostics(diagnosticsByFile: Record, currentFile: string): JSX.Element[] { const result: JSX.Element[] = [] if (diagnosticsByFile === undefined || diagnosticsByFile[currentFile] === undefined) return result for (const diags of Object.values(diagnosticsByFile)) { for (const d of diags) { if (d.severity !== 1) continue const line = d.range.start.line + 1 const column = d.range.start.character + 1 result.push(
          
            Error
          
          
            [{line}:{column}]
          
          {d.message}
        
, ) } } return result } function formatErrorString(error: string): JSX.Element { const errorMarker = "Error: " const startsWithError = error.startsWith(errorMarker) return startsWithError ? (
      
        Error
      
      {error.slice(errorMarker.length)}
    
) : (
      {error}
    
) } export function TodoWriteTool(props: ToolProps) { const priority: Record = { in_progress: 0, pending: 1, completed: 2, } const todos = createMemo(() => ((props.state.input?.todos ?? []) as Todo[]).slice().sort((a, b) => priority[a.status] - priority[b.status]), ) const starting = () => todos().every((t: Todo) => t.status === "pending") const finished = () => todos().every((t: Todo) => t.status === "completed") return ( <>
Creating plan Completing plan
0}>
    {(todo) => (
  • {todo.content}
  • )}
) } export function GrepTool(props: ToolProps) { return ( <>
Grep “{props.state.input.pattern}”
0}>
) } export function ListTool(props: ToolProps) { const path = createMemo(() => props.state.input?.path !== props.message.path.cwd ? stripWorkingDirectory(props.state.input?.path, props.message.path.cwd) : props.state.input?.path, ) return ( <>
LS {path()}
) } export function WebFetchTool(props: ToolProps) { return ( <>
Fetch {props.state.input.url}
{formatErrorString(props.state.output)}
) } export function ReadTool(props: ToolProps) { const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd)) return ( <>
Read {filePath()}
{formatErrorString(props.state.output)}
) } export function WriteTool(props: ToolProps) { const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd)) const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath)) return ( <>
Write {filePath()}
0}> {diagnostics()}
{formatErrorString(props.state.output)}
) } export function EditTool(props: ToolProps) { const filePath = createMemo(() => stripWorkingDirectory(props.state.input.filePath, props.message.path.cwd)) const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath)) return ( <>
Edit {filePath()}
{formatErrorString(props.state.metadata?.message || "")}
0}> {diagnostics()} ) } export function BashTool(props: ToolProps) { return ( ) } export function GlobTool(props: ToolProps) { return ( <>
Glob “{props.state.input.pattern}”
0}>
) } interface ResultsButtonProps extends ParentProps { showCopy?: string hideCopy?: string } function ResultsButton(props: ResultsButtonProps) { const [show, setShow] = createSignal(false) return ( <> {props.children} ) } export function Spacer() { return
} function Footer(props: ParentProps<{ title: string }>) { return (
{props.children}
) } function ToolFooter(props: { time: number }) { return props.time > MIN_DURATION &&
{formatDuration(props.time)}
} function TaskTool(props: ToolProps) { return ( <>
Task {props.state.input.description}
“{props.state.input.prompt}”
) } export function FallbackTool(props: ToolProps) { return ( <>
{props.tool}
{(arg) => ( <>
{arg[0]}
{arg[1]}
)}
) } // Converts nested objects/arrays into [path, value] pairs. // E.g. {a:{b:{c:1}}, d:[{e:2}, 3]} => [["a.b.c",1], ["d[0].e",2], ["d[1]",3]] function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> { const entries: Array<[string, any]> = [] for (const [key, value] of Object.entries(obj)) { const path = prefix ? `${prefix}.${key}` : key if (value !== null && typeof value === "object") { if (Array.isArray(value)) { value.forEach((item, index) => { const arrayPath = `${path}[${index}]` if (item !== null && typeof item === "object") { entries.push(...flattenToolArgs(item, arrayPath)) } else { entries.push([arrayPath, item]) } }) } else { entries.push(...flattenToolArgs(value, path)) } } else { entries.push([path, value]) } } return entries } function getProvider(model: string) { const lowerModel = model.toLowerCase() if (/claude|anthropic/.test(lowerModel)) return "anthropic" if (/gpt|o[1-4]|codex|openai/.test(lowerModel)) return "openai" if (/gemini|palm|bard|google/.test(lowerModel)) return "gemini" if (/llama|meta/.test(lowerModel)) return "meta" return "any" } export function ProviderIcon(props: { model: string; size?: number }) { const provider = getProvider(props.model) const size = props.size || 16 return ( }> ) }