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 (
{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 && (
)}
)}
{props.message.role === "assistant" && props.part.type === "reasoning" && (
)}
{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 &&
}
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 (
}>
)
}