Commit - use types from SDK

This commit is contained in:
Shantur Rathore
2025-11-11 21:06:37 +00:00
parent 063a11db76
commit 89dbe43d87
17 changed files with 691 additions and 244 deletions

View File

@@ -1,5 +1,6 @@
import { createMemo, Show, onMount, createEffect } from "solid-js"
import { DiffView, DiffModeEnum } from "@git-diff-view/solid"
import type { DiffHighlighterLang } from "@git-diff-view/core"
import { getLanguageFromPath } from "../lib/markdown"
import { normalizeDiffText } from "../lib/diff-utils"
import { setToolRenderCache } from "../lib/tool-render-cache"
@@ -34,11 +35,11 @@ export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
return {
oldFile: {
fileName,
fileLang: language as any, // Cast to any to avoid type issues with DiffHighlighterLang
fileLang: (language || "text") as DiffHighlighterLang | null,
},
newFile: {
fileName,
fileLang: language as any, // Cast to any to avoid type issues with DiffHighlighterLang
fileLang: (language || "text") as DiffHighlighterLang | null,
},
hunks: [normalized],
}

View File

@@ -1,5 +1,7 @@
import { Component, createSignal, createEffect, For, Show, onCleanup } from "solid-js"
import type { OpencodeClient } from "@opencode-ai/sdk/client"
interface FileItem {
path: string
added?: number
@@ -12,7 +14,7 @@ interface FilePickerProps {
onSelect: (path: string) => void
onNavigate: (direction: "up" | "down") => void
onClose: () => void
instanceClient: any
instanceClient: OpencodeClient
searchQuery: string
textareaRef?: HTMLTextAreaElement
workspaceFolder: string

View File

@@ -1,5 +1,5 @@
import { Component, Show, For, onMount, createSignal } from "solid-js"
import type { Instance } from "../types/instance"
import type { Instance, RawMcpStatus } from "../types/instance"
interface InstanceInfoProps {
instance: Instance
@@ -12,12 +12,12 @@ type ParsedMcpStatus = {
error?: string
}
function parseMcpStatus(status: unknown): ParsedMcpStatus[] {
function parseMcpStatus(status: RawMcpStatus): ParsedMcpStatus[] {
if (!status || typeof status !== "object") return []
const result: ParsedMcpStatus[] = []
for (const [name, value] of Object.entries(status as Record<string, unknown>)) {
for (const [name, value] of Object.entries(status)) {
if (!value || typeof value !== "object") continue
const rawStatus = (value as { status?: string }).status
if (!rawStatus) continue
@@ -47,7 +47,7 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
const metadata = () => props.instance.metadata
const mcpServers = () => {
const status = metadata()?.mcpStatus
return parseMcpStatus(status)
return status ? parseMcpStatus(status) : []
}
onMount(async () => {
@@ -64,7 +64,7 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
])
const project = projectResult.status === "fulfilled" ? projectResult.value.data : undefined
const mcpStatus = mcpResult.status === "fulfilled" ? mcpResult.value.data : undefined
const mcpStatus = mcpResult.status === "fulfilled" ? mcpResult.value.data as RawMcpStatus : undefined
const { updateInstance } = await import("../stores/instances")
updateInstance(props.instance.id, {

View File

@@ -1,14 +1,13 @@
import { For, Show } from "solid-js"
import type { Message } from "../types/message"
import type { Message, SDKPart, MessageInfo, ClientPart } from "../types/message"
import { partHasRenderableText } from "../types/message"
import MessagePart from "./message-part"
interface MessageItemProps {
message: Message
messageInfo?: any
messageInfo?: MessageInfo
isQueued?: boolean
parts?: any[]
parts?: ClientPart[]
onRevert?: (messageId: string) => void
onFork?: (messageId?: string) => void
}
@@ -23,9 +22,10 @@ export default function MessageItem(props: MessageItemProps) {
const messageParts = () => props.parts ?? props.message.parts
const errorMessage = () => {
if (!props.messageInfo?.error) return null
const info = props.messageInfo
if (!info || info.role !== "assistant" || !info.error) return null
const error = props.messageInfo.error
const error = info.error
if (error.name === "ProviderAuthError") {
return error.data?.message || "Authentication error"
}
@@ -50,7 +50,8 @@ export default function MessageItem(props: MessageItemProps) {
}
const isGenerating = () => {
return !hasContent() && props.messageInfo?.time?.completed === 0
const info = props.messageInfo
return !hasContent() && info && info.role === "assistant" && info.time.completed !== undefined && info.time.completed === 0
}
const handleRevert = () => {
@@ -66,29 +67,17 @@ export default function MessageItem(props: MessageItemProps) {
const agentIdentifier = () => {
if (isUser()) return ""
return (
props.messageInfo?.agent ||
props.messageInfo?.mode ||
props.messageInfo?.agentID ||
props.messageInfo?.agentId ||
props.messageInfo?.metadata?.agentID ||
""
)
const info = props.messageInfo
if (!info || info.role !== "assistant") return ""
return info.mode || ""
}
const modelIdentifier = () => {
if (isUser()) return ""
const modelID =
props.messageInfo?.modelID ||
props.messageInfo?.modelId ||
props.messageInfo?.model?.modelID ||
props.messageInfo?.model?.id ||
""
const providerID =
props.messageInfo?.providerID ||
props.messageInfo?.providerId ||
props.messageInfo?.model?.providerID ||
""
const info = props.messageInfo
if (!info || info.role !== "assistant") return ""
const modelID = info.modelID || ""
const providerID = info.providerID || ""
if (modelID && providerID) return `${providerID}/${modelID}`
return modelID
}

View File

@@ -4,10 +4,12 @@ import { isItemExpanded, toggleItemExpanded } from "../stores/tool-call-state"
import { Markdown } from "./markdown"
import { useTheme } from "../lib/theme"
import { preferences } from "../stores/preferences"
import { partHasRenderableText } from "../types/message"
import { partHasRenderableText, SDKPart, TextPart, ClientPart } from "../types/message"
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
interface MessagePartProps {
part: any
part: ClientPart
messageType?: "user" | "assistant"
}
export default function MessagePart(props: MessagePartProps) {
@@ -21,22 +23,30 @@ export default function MessagePart(props: MessagePartProps) {
const plainTextContent = () => {
const part = props.part
if (typeof part?.text === "string") {
if ((part.type === "text" || part.type === "reasoning") && typeof part.text === "string") {
return part.text
}
const segments: string[] = []
const contentArray = Array.isArray(part?.content) ? part.content : []
return ""
}
for (const item of contentArray) {
if (typeof item === "string") {
segments.push(item)
} else if (item && typeof item === "object" && typeof (item as { text?: unknown }).text === "string") {
segments.push((item as { text: string }).text)
const createTextPartForMarkdown = (): TextPart => {
const part = props.part
if ((part.type === "text" || part.type === "reasoning") && typeof part.text === "string") {
return {
id: part.id,
type: "text",
text: part.text,
synthetic: part.type === "text" ? part.synthetic : false,
version: (part as { version?: number }).version
}
}
return segments.join("\n")
return {
id: part.id,
type: "text",
text: "",
synthetic: false
}
}
function handleReasoningClick(e: Event) {
@@ -47,25 +57,23 @@ export default function MessagePart(props: MessagePartProps) {
return (
<Switch>
<Match when={partType() === "text"}>
<Show when={!props.part.synthetic && partHasRenderableText(props.part)}>
<Show when={!(props.part.type === "text" && props.part.synthetic) && partHasRenderableText(props.part)}>
<div class={textContainerClass()}>
<Show
when={isAssistantMessage()}
fallback={<span>{plainTextContent()}</span>}
>
<Markdown part={props.part} isDark={isDark()} size={isAssistantMessage() ? "tight" : "base"} />
<Markdown part={createTextPartForMarkdown()} isDark={isDark()} size={isAssistantMessage() ? "tight" : "base"} />
</Show>
</div>
</Show>
</Match>
<Match when={partType() === "tool"}>
<ToolCall toolCall={props.part} toolCallId={props.part?.id} />
<ToolCall toolCall={props.part as ToolCallPart} toolCallId={props.part?.id} />
</Match>
<Match when={partType() === "error"}>
<div class="message-error-part"> {props.part.message}</div>
</Match>
<Match when={partType() === "reasoning"}>
<Show when={preferences().showThinkingBlocks && partHasRenderableText(props.part)}>
@@ -77,7 +85,7 @@ export default function MessagePart(props: MessagePartProps) {
</div>
<Show when={isReasoningExpanded()}>
<div class={`${textContainerClass()} mt-2`}>
<Markdown part={props.part} isDark={isDark()} size={isAssistantMessage() ? "tight" : "base"} />
<Markdown part={createTextPartForMarkdown()} isDark={isDark()} size={isAssistantMessage() ? "tight" : "base"} />
</div>
</Show>
</div>

View File

@@ -1,5 +1,31 @@
import { For, Show, createSignal, createEffect, createMemo, onCleanup } from "solid-js"
import type { Message, MessageDisplayParts } from "../types/message"
import type { Message, MessageDisplayParts, SDKPart, MessageInfo, ClientPart } from "../types/message"
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
// Import ToolState types from SDK
type ToolState = import("@opencode-ai/sdk").ToolState
type ToolStateRunning = import("@opencode-ai/sdk").ToolStateRunning
type ToolStateCompleted = import("@opencode-ai/sdk").ToolStateCompleted
type ToolStateError = import("@opencode-ai/sdk").ToolStateError
// Type guards
function isToolStateRunning(state: ToolState): state is ToolStateRunning {
return state.status === "running"
}
function isToolStateCompleted(state: ToolState): state is ToolStateCompleted {
return state.status === "completed"
}
function isToolStateError(state: ToolState): state is ToolStateError {
return state.status === "error"
}
// Type guard to check if a part is a tool part
function isToolPart(part: ClientPart): part is ToolCallPart {
return part.type === "tool"
}
import MessageItem from "./message-item"
import ToolCall from "./tool-call"
import { sseManager } from "../lib/sse-manager"
@@ -51,25 +77,26 @@ function navigateToTaskSession(location: TaskSessionLocation) {
}
// Calculate session tokens and cost from messagesInfo (matches TUI logic)
function calculateSessionInfo(messagesInfo?: Map<string, any>, instanceId?: string) {
function calculateSessionInfo(messagesInfo?: Map<string, MessageInfo>, instanceId?: string) {
if (!messagesInfo || messagesInfo.size === 0)
return { tokens: 0, cost: 0, contextWindow: 0, isSubscriptionModel: false }
let tokens = 0
let cost = 0
let contextWindow = 0
let isSubscriptionModel = false
let modelID = ""
let providerID = ""
let isSubscriptionModel = false
// Go backwards through messages to find the last relevant assistant message (like TUI)
const messageArray = Array.from(messagesInfo.values()).reverse()
for (const info of messageArray) {
if (!info) continue
if (info.role === "assistant" && info.tokens) {
const usage = info.tokens
if (usage.output > 0) {
if (usage.output && usage.output > 0) {
if (info.summary) {
// If summary message, only count output tokens and stop (like TUI)
tokens = usage.output || 0
@@ -145,7 +172,7 @@ interface MessageStreamProps {
instanceId: string
sessionId: string
messages: Message[]
messagesInfo?: Map<string, any>
messagesInfo?: Map<string, MessageInfo>
revert?: {
messageID: string
partID?: string
@@ -160,16 +187,16 @@ interface MessageStreamProps {
interface MessageDisplayItem {
type: "message"
message: Message
combinedParts: any[]
combinedParts: ClientPart[]
isQueued: boolean
messageInfo?: any
messageInfo?: MessageInfo
}
interface ToolDisplayItem {
type: "tool"
key: string
toolPart: any
messageInfo?: any
toolPart: ToolCallPart
messageInfo?: MessageInfo
messageId: string
messageVersion: number
partVersion: number
@@ -178,22 +205,25 @@ interface ToolDisplayItem {
type DisplayItem = MessageDisplayItem | ToolDisplayItem
interface MessageCacheEntry {
message: Message
version: number
showThinking: boolean
isQueued: boolean
messageInfo?: any
messageInfo?: MessageInfo
displayParts: MessageDisplayParts
item: MessageDisplayItem
}
interface ToolCacheEntry {
toolPart: any
messageInfo?: any
toolPart: ClientPart
messageInfo?: MessageInfo
signature: string
contentKey: string
item: ToolDisplayItem
}
interface SessionCache {
messageItemCache: Map<string, MessageCacheEntry>
toolItemCache: Map<string, ToolCacheEntry>
@@ -231,18 +261,17 @@ export default function MessageStream(props: MessageStreamProps) {
const scrollStateKey = () => makeScrollKey(props.instanceId, props.sessionId)
const connectionStatus = () => sseManager.getStatus(props.instanceId)
function createToolSignature(message: Message, toolPart: any, toolIndex: number, messageInfo?: any): string {
function createToolSignature(message: Message, toolPart: ClientPart, toolIndex: number, messageInfo?: MessageInfo): string {
const messageId = message.id
const partId = typeof toolPart?.id === "string" ? toolPart.id : `${messageId}-tool-${toolIndex}`
return `${messageId}:${partId}`
}
function createToolContentKey(toolPart: any, messageInfo?: any): string {
const state = toolPart?.state ?? {}
const version = typeof toolPart?.version === "number" ? toolPart.version : null
function createToolContentKey(toolPart: ClientPart, messageInfo?: MessageInfo): string {
const state = isToolPart(toolPart) ? toolPart.state : undefined
const version = typeof toolPart?.version === "number" ? toolPart.version : 0
const status = state?.status ?? "unknown"
return `${toolPart.id}:${version}:${status}`
}
const sessionInfo = createMemo(() => {
@@ -383,7 +412,7 @@ export default function MessageStream(props: MessageStreamProps) {
const hasRenderableContent =
message.type !== "assistant" ||
combinedParts.length > 0 ||
Boolean(messageInfo?.error) ||
Boolean(messageInfo && messageInfo.role === "assistant" && messageInfo.error) ||
message.status === "error"
if (hasRenderableContent) {
@@ -415,6 +444,7 @@ export default function MessageStream(props: MessageStreamProps) {
messageInfo,
}
newMessageCache.set(message.id, {
message,
version,
showThinking,
isQueued,
@@ -426,13 +456,15 @@ export default function MessageStream(props: MessageStreamProps) {
}
}
for (let toolIndex = 0; toolIndex < displayParts.tool.length; toolIndex++) {
const toolPart = displayParts.tool[toolIndex]
const toolKey = typeof toolPart?.id === "string" ? toolPart.id : `${message.id}-tool-${toolIndex}`
const toolParts = displayParts.tool.filter(isToolPart)
for (let toolIndex = 0; toolIndex < toolParts.length; toolIndex++) {
const toolPart = toolParts[toolIndex]
const originalIndex = displayParts.tool.indexOf(toolPart)
const toolKey = toolPart?.id || `${message.id}-tool-${originalIndex}`
const messageVersion = typeof message.version === "number" ? message.version : 0
const partVersion = typeof toolPart?.version === "number" ? toolPart.version : 0
const toolSignature = createToolSignature(message, toolPart, toolIndex, messageInfo)
const toolSignature = createToolSignature(message, toolPart, originalIndex, messageInfo)
const contentKey = createToolContentKey(toolPart, messageInfo)
const toolEntry = toolItemCache.get(toolKey)
if (toolEntry && toolEntry.signature === toolSignature) {
@@ -450,7 +482,7 @@ export default function MessageStream(props: MessageStreamProps) {
toolEntry.signature = toolSignature
toolEntry.contentKey = contentKey
toolEntry.item = updatedItem
console.debug("[ToolCall] update", toolKey, toolPart?.state?.status)
console.debug("[ToolCall] update", toolKey, toolPart.state?.status)
newToolCache.set(toolKey, toolEntry)
items.push(updatedItem)
} else {
@@ -475,7 +507,7 @@ export default function MessageStream(props: MessageStreamProps) {
messageVersion,
partVersion,
}
console.debug("[ToolCall] create", toolKey, toolPart?.state?.status)
console.debug("[ToolCall] create", toolKey, toolPart.state?.status)
newToolCache.set(toolKey, { toolPart, messageInfo, signature: toolSignature, contentKey, item: toolItem })
items.push(toolItem)
}
@@ -674,7 +706,9 @@ export default function MessageStream(props: MessageStreamProps) {
const toolPart = item.toolPart
const taskSessionId =
typeof toolPart?.state?.metadata?.sessionId === "string" ? toolPart.state.metadata.sessionId : ""
(isToolStateRunning(toolPart.state) || isToolStateCompleted(toolPart.state) || isToolStateError(toolPart.state))
? toolPart.state.metadata?.sessionId === "string" ? toolPart.state.metadata.sessionId : ""
: ""
const taskLocation = taskSessionId ? findTaskSessionLocation(taskSessionId) : null
const handleGoToTaskSession = (event: Event) => {

View File

@@ -4,6 +4,7 @@ import { addToHistory, getHistory } from "../stores/message-history"
import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments"
import { createFileAttachment, createTextAttachment, createAgentAttachment } from "../types/attachment"
import type { Attachment } from "../types/attachment"
import type { Agent } from "../types/session"
import Kbd from "./kbd"
import HintRow from "./hint-row"
import { getActiveInstance } from "../stores/instances"
@@ -516,7 +517,7 @@ export default function PromptInput(props: PromptInputProps) {
setAtPosition(null)
}
function handlePickerSelect(item: { type: "agent"; agent: any } | { type: "file"; file: any }) {
function handlePickerSelect(item: { type: "agent"; agent: Agent } | { type: "file"; file: { path: string; isGitFile: boolean } }) {
if (item.type === "agent") {
const agentName = item.agent.name
const existingAttachments = attachments()
@@ -642,7 +643,7 @@ export default function PromptInput(props: PromptInputProps) {
for (let i = 0; i < files.length; i++) {
const file = files[i]
const path = (file as any).path || file.name
const path = (file as File & { path?: string }).path || file.name
const filename = file.name
const mime = file.type || "text/plain"

View File

@@ -7,20 +7,40 @@ import { getLanguageFromPath } from "../lib/markdown"
import { isRenderableDiffText } from "../lib/diff-utils"
import { getToolRenderCache, setToolRenderCache } from "../lib/tool-render-cache"
import { preferences, setDiffViewMode, type DiffViewMode } from "../stores/preferences"
import type { TextPart } from "../types/message"
import type { TextPart, SDKPart, ClientPart } from "../types/message"
type ToolCallPart = Extract<ClientPart, { type: "tool" }>
// Import ToolState types from SDK
type ToolState = import("@opencode-ai/sdk").ToolState
type ToolStateRunning = import("@opencode-ai/sdk").ToolStateRunning
type ToolStateCompleted = import("@opencode-ai/sdk").ToolStateCompleted
type ToolStateError = import("@opencode-ai/sdk").ToolStateError
// Type guards
function isToolStateRunning(state: ToolState): state is ToolStateRunning {
return state.status === "running"
}
function isToolStateCompleted(state: ToolState): state is ToolStateCompleted {
return state.status === "completed"
}
function isToolStateError(state: ToolState): state is ToolStateError {
return state.status === "error"
}
const toolScrollState = new Map<string, { scrollTop: number; atBottom: boolean }>()
function makeRenderCacheKey(
baseId?: string | null,
toolCallId?: string | null,
messageId?: string,
messageVersion?: number,
partVersion?: number,
) {
if (!baseId && !messageId) return undefined
const suffix = `${messageVersion ?? 0}:${partVersion ?? 0}`
const keyBase = baseId || messageId || "tool"
const keyBase = `${messageId}:${toolCallId}`
return `${keyBase}::${suffix}`
}
@@ -55,7 +75,7 @@ function restoreScrollState(id: string, element: HTMLElement) {
interface ToolCallProps {
toolCall: any
toolCall: Extract<ClientPart, { type: "tool" }>
toolCallId?: string
messageId?: string
messageVersion?: number
@@ -122,12 +142,17 @@ interface DiffPayload {
filePath?: string
}
function extractDiffPayload(toolName: string, state: any): DiffPayload | null {
function extractDiffPayload(toolName: string, state: ToolState): DiffPayload | null {
if (!diffCapableTools.has(toolName)) return null
if (!state) return null
const metadata = state.metadata || {}
const candidates = [metadata.diff, state.output, metadata.output]
const metadata = (isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state))
? state.metadata || {}
: {}
const output = isToolStateCompleted(state) ? state.output : undefined
const candidates = [metadata.diff, output, metadata.output]
let diffText: string | null = null
for (const candidate of candidates) {
@@ -141,8 +166,12 @@ function extractDiffPayload(toolName: string, state: any): DiffPayload | null {
return null
}
const input = state.input || {}
const filePath = input.filePath || metadata.filePath || input.path
const input = (isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state))
? state.input as Record<string, unknown>
: {}
const filePath = (typeof input.filePath === "string" ? input.filePath : undefined) ||
(typeof metadata.filePath === "string" ? metadata.filePath : undefined) ||
(typeof input.path === "string" ? input.path : undefined)
return { diffText, filePath }
}
@@ -202,7 +231,12 @@ export default function ToolCall(props: ToolCallProps) {
createEffect(() => {
if (props.toolCall?.tool !== "task") return
const summarySignature = JSON.stringify(props.toolCall?.state?.metadata?.summary ?? [])
const state = props.toolCall?.state
const summarySignature = JSON.stringify(
state && (isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state))
? state.metadata?.summary ?? []
: []
)
requestAnimationFrame(() => {
void summarySignature
handleScrollRendered()
@@ -288,14 +322,20 @@ export default function ToolCall(props: ToolCallProps) {
const renderToolTitle = () => {
const toolName = props.toolCall?.tool || ""
const state = props.toolCall?.state || {}
const input = state.input || {}
const state = props.toolCall?.state
if (state.status === "pending") {
return renderToolAction()
if (!state) return renderToolAction()
if (state.status === "pending") return renderToolAction()
const input = (isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state))
? (state.input as Record<string, unknown>)
: {} as Record<string, unknown>
if (isToolStateRunning(state) && state.title) {
return state.title
}
if (state.title) {
if (isToolStateCompleted(state)) {
return state.title
}
@@ -303,20 +343,20 @@ export default function ToolCall(props: ToolCallProps) {
switch (toolName) {
case "read":
if (input.filePath) {
if (typeof input.filePath === "string") {
return `${name} ${getRelativePath(input.filePath)}`
}
return name
case "edit":
case "write":
if (input.filePath) {
if (typeof input.filePath === "string") {
return `${name} ${getRelativePath(input.filePath)}`
}
return name
case "bash":
if (input.description) {
if (typeof input.description === "string") {
return `${name} ${input.description}`
}
return name
@@ -344,7 +384,7 @@ export default function ToolCall(props: ToolCallProps) {
return "Plan"
case "invalid":
if (input.tool) {
if (typeof input.tool === "string") {
return getToolName(input.tool)
}
return name
@@ -454,7 +494,7 @@ export default function ToolCall(props: ToolCallProps) {
)
}
function renderMarkdownTool(toolName: string, state: any) {
function renderMarkdownTool(toolName: string, state: ToolState) {
const content = getMarkdownContent(toolName, state)
if (!content) {
return null
@@ -496,53 +536,69 @@ export default function ToolCall(props: ToolCallProps) {
)
}
function getMarkdownContent(toolName: string, state: any): string | null {
const input = state?.input || {}
const metadata = state?.metadata || {}
function getMarkdownContent(toolName: string, state: ToolState): string | null {
if (!state) return null
const input = (isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state))
? state.input as Record<string, unknown>
: {}
const metadata = (isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state))
? state.metadata || {}
: {}
switch (toolName) {
case "read": {
const preview = typeof metadata.preview === "string" ? metadata.preview : null
const language = getLanguageFromPath(input.filePath || "")
const language = getLanguageFromPath(typeof input.filePath === "string" ? input.filePath : "")
return ensureMarkdownContent(preview, language, true)
}
case "edit": {
const diffText = typeof metadata.diff === "string" ? metadata.diff : null
const fallback = typeof state.output === "string" ? state.output : null
const fallback = isToolStateCompleted(state) && typeof state.output === "string" ? state.output : null
return ensureMarkdownContent(diffText || fallback, "diff", true)
}
case "write": {
const content = typeof input.content === "string" ? input.content : null
const metadataContent = typeof metadata.content === "string" ? metadata.content : null
const language = getLanguageFromPath(input.filePath || "")
const language = getLanguageFromPath(typeof input.filePath === "string" ? input.filePath : "")
return ensureMarkdownContent(content || metadataContent, language, true)
}
case "patch": {
const patchContent = typeof metadata.diff === "string" ? metadata.diff : null
const fallback = typeof state.output === "string" ? state.output : null
const fallback = isToolStateCompleted(state) && typeof state.output === "string" ? state.output : null
return ensureMarkdownContent(patchContent || fallback, "diff", true)
}
case "bash": {
const command = typeof input.command === "string" && input.command.length > 0 ? `$ ${input.command}` : ""
const outputResult = formatUnknown(metadata.output ?? state.output)
const outputResult = formatUnknown(
isToolStateCompleted(state) ? state.output :
(isToolStateRunning(state) || isToolStateError(state)) && metadata.output ? metadata.output :
undefined
)
const parts = [command, outputResult?.text].filter(Boolean)
const combined = parts.join("\n")
return ensureMarkdownContent(combined, "bash", true)
}
case "webfetch": {
const result = formatUnknown(state.output ?? metadata.output)
const result = formatUnknown(
isToolStateCompleted(state) ? state.output :
(isToolStateRunning(state) || isToolStateError(state)) && metadata.output ? metadata.output :
undefined
)
if (!result) return null
return ensureMarkdownContent(result.text, result.language, true)
}
default: {
const result = formatUnknown(
state.output ?? metadata.output ?? metadata.diff ?? metadata.preview ?? input.content,
isToolStateCompleted(state) ? state.output :
(isToolStateRunning(state) || isToolStateError(state)) && metadata.output ? metadata.output :
metadata.diff ?? metadata.preview ?? input.content,
)
if (!result) return null
return ensureMarkdownContent(result.text, result.language, true)
@@ -618,8 +674,12 @@ export default function ToolCall(props: ToolCallProps) {
}
const renderTodowriteTool = () => {
const state = props.toolCall?.state || {}
const metadata = state.metadata || {}
const state = props.toolCall?.state
if (!state) return null
const metadata = (isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state))
? state.metadata || {}
: {}
const todos = metadata.todos || []
if (!Array.isArray(todos) || todos.length === 0) {
@@ -677,8 +737,12 @@ export default function ToolCall(props: ToolCallProps) {
}
const renderTaskTool = () => {
const state = props.toolCall?.state || {}
const metadata = state.metadata || {}
const state = props.toolCall?.state
if (!state) return null
const metadata = (isToolStateRunning(state) || isToolStateCompleted(state) || isToolStateError(state))
? state.metadata || {}
: {}
const summary = metadata.summary || []
if (!Array.isArray(summary) || summary.length === 0) {

View File

@@ -1,5 +1,6 @@
import { Component, createSignal, createEffect, For, Show, onCleanup } from "solid-js"
import type { Agent } from "../types/session"
import type { OpencodeClient } from "@opencode-ai/sdk/client"
interface FileItem {
path: string
@@ -15,7 +16,7 @@ interface UnifiedPickerProps {
onSelect: (item: PickerItem) => void
onClose: () => void
agents: Agent[]
instanceClient: any
instanceClient: OpencodeClient | null
searchQuery: string
textareaRef?: HTMLTextAreaElement
workspaceFolder: string