Commit - use types from SDK
This commit is contained in:
@@ -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],
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user