Commit - use types from SDK
This commit is contained in:
13
src/App.tsx
13
src/App.tsx
@@ -2,6 +2,7 @@ import { Component, onMount, onCleanup, Show, createMemo, createEffect, createSi
|
||||
import { Toaster } from "solid-toast"
|
||||
import type { Session } from "./types/session"
|
||||
import type { Attachment } from "./types/attachment"
|
||||
import type { SDKPart, ClientPart } from "./types/message"
|
||||
import FolderSelectionView from "./components/folder-selection-view"
|
||||
import InstanceWelcomeView from "./components/instance-welcome-view"
|
||||
import CommandPalette from "./components/command-palette"
|
||||
@@ -99,12 +100,12 @@ const SessionView: Component<{
|
||||
return null
|
||||
}
|
||||
|
||||
const textParts = targetMessage.parts.filter((p: any) => p.type === "text")
|
||||
const textParts = targetMessage.parts.filter((p): p is ClientPart & { type: "text"; text: string } => p.type === "text")
|
||||
if (textParts.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return textParts.map((p: any) => p.text).join("\n")
|
||||
return textParts.map((p) => p.text).join("\n")
|
||||
}
|
||||
|
||||
async function handleRevert(messageId: string) {
|
||||
@@ -555,9 +556,9 @@ const App: Component = () => {
|
||||
modelID: session.model.modelId,
|
||||
},
|
||||
})
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("Failed to compact session:", error)
|
||||
const message = error?.message || "Failed to compact session"
|
||||
const message = error instanceof Error ? error.message : "Failed to compact session"
|
||||
alert(`Compact failed: ${message}`)
|
||||
}
|
||||
},
|
||||
@@ -630,11 +631,11 @@ const App: Component = () => {
|
||||
|
||||
// Restore the reverted user message to the prompt input
|
||||
if (revertedMessage && revertedInfo?.role === "user") {
|
||||
const textParts = revertedMessage.parts.filter((p: any) => p.type === "text")
|
||||
const textParts = revertedMessage.parts.filter((p): p is ClientPart & { type: "text"; text: string } => p.type === "text")
|
||||
if (textParts.length > 0) {
|
||||
const textarea = document.querySelector(".prompt-input") as HTMLTextAreaElement
|
||||
if (textarea) {
|
||||
textarea.value = textParts.map((p: any) => p.text).join("\n")
|
||||
textarea.value = textParts.map((p) => p.text).join("\n")
|
||||
textarea.dispatchEvent(new Event("input", { bubbles: true }))
|
||||
textarea.focus()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import { createSignal } from "solid-js"
|
||||
import {
|
||||
MessageUpdateEvent,
|
||||
MessageRemovedEvent,
|
||||
MessagePartUpdatedEvent,
|
||||
MessagePartRemovedEvent
|
||||
} from "../types/message"
|
||||
import type {
|
||||
EventSessionUpdated,
|
||||
EventSessionCompacted,
|
||||
EventSessionError,
|
||||
EventSessionIdle
|
||||
} from "@opencode-ai/sdk"
|
||||
|
||||
interface SSEConnection {
|
||||
instanceId: string
|
||||
@@ -7,19 +19,6 @@ interface SSEConnection {
|
||||
status: "connecting" | "connected" | "disconnected" | "error"
|
||||
}
|
||||
|
||||
interface MessageUpdateEvent {
|
||||
type: "message_updated"
|
||||
sessionId: string
|
||||
messageId: string
|
||||
parts: any[]
|
||||
status: string
|
||||
}
|
||||
|
||||
interface SessionUpdateEvent {
|
||||
type: "session_updated"
|
||||
session: any
|
||||
}
|
||||
|
||||
interface TuiToastEvent {
|
||||
type: "tui.toast.show"
|
||||
properties: {
|
||||
@@ -30,12 +29,17 @@ interface TuiToastEvent {
|
||||
}
|
||||
}
|
||||
|
||||
interface SessionIdleEvent {
|
||||
type: "session.idle"
|
||||
properties: {
|
||||
sessionID: string
|
||||
}
|
||||
}
|
||||
type SSEEvent =
|
||||
| MessageUpdateEvent
|
||||
| MessageRemovedEvent
|
||||
| MessagePartUpdatedEvent
|
||||
| MessagePartRemovedEvent
|
||||
| EventSessionUpdated
|
||||
| EventSessionCompacted
|
||||
| EventSessionError
|
||||
| EventSessionIdle
|
||||
| TuiToastEvent
|
||||
| { type: string; properties?: Record<string, unknown> } // Fallback for unknown event types
|
||||
|
||||
const [connectionStatus, setConnectionStatus] = createSignal<
|
||||
Map<string, "connecting" | "connected" | "disconnected" | "error">
|
||||
@@ -98,34 +102,36 @@ class SSEManager {
|
||||
}
|
||||
}
|
||||
|
||||
private handleEvent(instanceId: string, event: any): void {
|
||||
private handleEvent(instanceId: string, event: SSEEvent): void {
|
||||
console.log("[SSE] Received event:", event.type, event)
|
||||
|
||||
switch (event.type) {
|
||||
case "message.updated":
|
||||
this.onMessageUpdate?.(instanceId, event as MessageUpdateEvent)
|
||||
break
|
||||
case "message.part.updated":
|
||||
this.onMessageUpdate?.(instanceId, event)
|
||||
this.onMessagePartUpdated?.(instanceId, event as MessagePartUpdatedEvent)
|
||||
break
|
||||
case "message.removed":
|
||||
this.onMessageRemoved?.(instanceId, event)
|
||||
this.onMessageRemoved?.(instanceId, event as MessageRemovedEvent)
|
||||
break
|
||||
case "message.part.removed":
|
||||
this.onMessagePartRemoved?.(instanceId, event)
|
||||
this.onMessagePartRemoved?.(instanceId, event as MessagePartRemovedEvent)
|
||||
break
|
||||
case "session.updated":
|
||||
this.onSessionUpdate?.(instanceId, event)
|
||||
this.onSessionUpdate?.(instanceId, event as EventSessionUpdated)
|
||||
break
|
||||
case "session.compacted":
|
||||
this.onSessionCompacted?.(instanceId, event)
|
||||
this.onSessionCompacted?.(instanceId, event as EventSessionCompacted)
|
||||
break
|
||||
case "session.error":
|
||||
this.onSessionError?.(instanceId, event)
|
||||
this.onSessionError?.(instanceId, event as EventSessionError)
|
||||
break
|
||||
case "tui.toast.show":
|
||||
this.onTuiToast?.(instanceId, event as TuiToastEvent)
|
||||
break
|
||||
case "session.idle":
|
||||
this.onSessionIdle?.(instanceId, event as SessionIdleEvent)
|
||||
this.onSessionIdle?.(instanceId, event as EventSessionIdle)
|
||||
break
|
||||
default:
|
||||
console.warn("[SSE] Unknown event type:", event.type)
|
||||
@@ -162,13 +168,14 @@ class SSEManager {
|
||||
}
|
||||
|
||||
onMessageUpdate?: (instanceId: string, event: MessageUpdateEvent) => void
|
||||
onMessageRemoved?: (instanceId: string, event: any) => void
|
||||
onMessagePartRemoved?: (instanceId: string, event: any) => void
|
||||
onSessionUpdate?: (instanceId: string, event: SessionUpdateEvent) => void
|
||||
onSessionCompacted?: (instanceId: string, event: any) => void
|
||||
onSessionError?: (instanceId: string, event: any) => void
|
||||
onMessageRemoved?: (instanceId: string, event: MessageRemovedEvent) => void
|
||||
onMessagePartUpdated?: (instanceId: string, event: MessagePartUpdatedEvent) => void
|
||||
onMessagePartRemoved?: (instanceId: string, event: MessagePartRemovedEvent) => void
|
||||
onSessionUpdate?: (instanceId: string, event: EventSessionUpdated) => void
|
||||
onSessionCompacted?: (instanceId: string, event: EventSessionCompacted) => void
|
||||
onSessionError?: (instanceId: string, event: EventSessionError) => void
|
||||
onTuiToast?: (instanceId: string, event: TuiToastEvent) => void
|
||||
onSessionIdle?: (instanceId: string, event: SessionIdleEvent) => void
|
||||
onSessionIdle?: (instanceId: string, event: EventSessionIdle) => void
|
||||
|
||||
getStatus(instanceId: string): "connecting" | "connected" | "disconnected" | "error" | null {
|
||||
return connectionStatus().get(instanceId) ?? null
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface ConfigData {
|
||||
preferences: Preferences
|
||||
recentFolders: RecentFolder[]
|
||||
opencodeBinaries: OpenCodeBinary[]
|
||||
theme?: "light" | "dark" | "system"
|
||||
}
|
||||
|
||||
export interface InstanceData {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createContext, createSignal, useContext, onMount, createEffect, type JSX } from "solid-js"
|
||||
import { storage } from "./storage"
|
||||
import { storage, type ConfigData } from "./storage"
|
||||
|
||||
interface ThemeContextValue {
|
||||
isDark: () => boolean
|
||||
@@ -28,7 +28,7 @@ export function ThemeProvider(props: { children: JSX.Element }) {
|
||||
async function loadTheme() {
|
||||
try {
|
||||
const config = await storage.loadConfig()
|
||||
const savedTheme = (config as any)?.theme
|
||||
const savedTheme = config.theme
|
||||
let themeDark: boolean
|
||||
|
||||
if (savedTheme === "system") {
|
||||
@@ -60,7 +60,7 @@ export function ThemeProvider(props: { children: JSX.Element }) {
|
||||
try {
|
||||
const config = await storage.loadConfig()
|
||||
const nextPreference = dark ? "dark" : "light"
|
||||
;(config as any).theme = nextPreference
|
||||
config.theme = nextPreference
|
||||
themePreference = nextPreference
|
||||
await storage.saveConfig(config)
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSignal } from "solid-js"
|
||||
import type { Session, Agent, Provider } from "../types/session"
|
||||
import type { Message, MessageDisplayParts } from "../types/message"
|
||||
import type { Message, MessageDisplayParts, MessagePartRemovedEvent, MessagePartUpdatedEvent, MessageRemovedEvent, MessageUpdateEvent } from "../types/message"
|
||||
import { partHasRenderableText } from "../types/message"
|
||||
import { instances } from "./instances"
|
||||
|
||||
@@ -8,6 +8,22 @@ import { sseManager } from "../lib/sse-manager"
|
||||
import { decodeHtmlEntities } from "../lib/markdown"
|
||||
import { showToastNotification, ToastVariant } from "../lib/notifications"
|
||||
import { preferences, addRecentModelPreference, getAgentModelPreference, setAgentModelPreference } from "./preferences"
|
||||
import type {
|
||||
EventSessionUpdated,
|
||||
EventSessionCompacted,
|
||||
EventSessionError,
|
||||
EventSessionIdle
|
||||
} from "@opencode-ai/sdk"
|
||||
|
||||
interface TuiToastEvent {
|
||||
type: "tui.toast.show"
|
||||
properties: {
|
||||
title?: string
|
||||
message: string
|
||||
variant: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}
|
||||
}
|
||||
|
||||
interface SessionInfo {
|
||||
tokens: number
|
||||
@@ -275,13 +291,17 @@ export function computeDisplayParts(message: Message, showThinking: boolean): Me
|
||||
function initializePartVersion(part: any, version = 0) {
|
||||
if (!part || typeof part !== "object") return
|
||||
const partAny = part as any
|
||||
partAny.version = typeof partAny.version === "number" ? partAny.version : version
|
||||
// Ensure version is always set for client-side part tracking
|
||||
if (typeof partAny.version !== "number") {
|
||||
partAny.version = version
|
||||
}
|
||||
}
|
||||
|
||||
function bumpPartVersion(previousPart: any, nextPart: any): number {
|
||||
const prevVersion = typeof previousPart?.version === "number" ? previousPart.version : -1
|
||||
const nextVersion = prevVersion + 1
|
||||
initializePartVersion(nextPart, nextVersion)
|
||||
// Always initialize the version on the new part
|
||||
nextPart.version = nextVersion
|
||||
return nextVersion
|
||||
}
|
||||
|
||||
@@ -383,6 +403,7 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
||||
parentId: apiSession.parentID || null,
|
||||
agent: "",
|
||||
model: { providerId: "", modelId: "" },
|
||||
version: apiSession.version, // Include version from SDK
|
||||
time: {
|
||||
created: apiSession.time.created,
|
||||
updated: apiSession.time.updated,
|
||||
@@ -639,6 +660,7 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
||||
parentId: null,
|
||||
agent: selectedAgent,
|
||||
model: defaultModel,
|
||||
version: response.data.version, // Include version from SDK
|
||||
time: {
|
||||
created: response.data.time.created,
|
||||
updated: response.data.time.updated,
|
||||
@@ -743,6 +765,7 @@ async function forkSession(
|
||||
providerId: info.model?.providerID || "",
|
||||
modelId: info.model?.modelID || "",
|
||||
},
|
||||
version: "0", // Default version for forked sessions
|
||||
time: {
|
||||
created: info.time?.created || Date.now(),
|
||||
updated: info.time?.updated || Date.now(),
|
||||
@@ -1157,7 +1180,7 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
||||
updateSessionInfo(instanceId, sessionId)
|
||||
}
|
||||
|
||||
function handleMessageUpdate(instanceId: string, event: any): void {
|
||||
function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | MessagePartUpdatedEvent): void {
|
||||
const instanceSessions = sessions().get(instanceId)
|
||||
if (!instanceSessions) return
|
||||
|
||||
@@ -1330,7 +1353,165 @@ function handleMessageUpdate(instanceId: string, event: any): void {
|
||||
}
|
||||
|
||||
withSession(instanceId, part.sessionID, (session) => {
|
||||
// Session already mutated in place
|
||||
// All the message mutation logic should go here to ensure reactivity
|
||||
const index = getSessionIndex(instanceId, part.sessionID)
|
||||
let messageIndex = index.messageIndex.get(part.messageID)
|
||||
let replacedTemp = false
|
||||
|
||||
if (messageIndex === undefined) {
|
||||
// Search for queued message with status 'sending' and no server id
|
||||
for (let i = 0; i < session.messages.length; i++) {
|
||||
const msg = session.messages[i]
|
||||
if (msg.sessionId === part.sessionID && msg.status === "sending") {
|
||||
messageIndex = i
|
||||
replacedTemp = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messageIndex === undefined) {
|
||||
// Create new message
|
||||
const newMessage: Message = {
|
||||
id: part.messageID,
|
||||
sessionId: part.sessionID,
|
||||
type: "assistant" as const,
|
||||
parts: [part],
|
||||
timestamp: Date.now(),
|
||||
status: "streaming" as const,
|
||||
version: 0,
|
||||
}
|
||||
|
||||
initializePartVersion(part)
|
||||
newMessage.displayParts = computeDisplayParts(newMessage, preferences().showThinkingBlocks)
|
||||
|
||||
let insertIndex = session.messages.length
|
||||
for (let i = session.messages.length - 1; i >= 0; i--) {
|
||||
if (session.messages[i].id < newMessage.id) {
|
||||
insertIndex = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
session.messages.splice(insertIndex, 0, newMessage)
|
||||
rebuildSessionIndex(instanceId, part.sessionID, session.messages)
|
||||
} else {
|
||||
// Update existing message
|
||||
const message = session.messages[messageIndex]
|
||||
if (typeof message.version !== "number") {
|
||||
message.version = 0
|
||||
}
|
||||
|
||||
// Strip synthetic parts when real data arrives
|
||||
let filteredSynthetics = false
|
||||
if (message.parts.some((partItem: any) => partItem.synthetic === true)) {
|
||||
message.parts = message.parts.filter((partItem: any) => partItem.synthetic !== true)
|
||||
filteredSynthetics = true
|
||||
// Clear render cache from remaining parts when synthetic parts are removed
|
||||
message.parts.forEach((partItem: any) => {
|
||||
if (partItem.type === "text") {
|
||||
partItem.renderCache = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let baseParts: any[]
|
||||
if (replacedTemp) {
|
||||
baseParts = message.parts.filter((partItem: any) => partItem.type !== "text")
|
||||
message.parts = baseParts
|
||||
// Clear render cache when replacing temp content
|
||||
baseParts.forEach((partItem: any) => {
|
||||
if (partItem.type === "text") {
|
||||
partItem.renderCache = undefined
|
||||
}
|
||||
})
|
||||
} else {
|
||||
baseParts = message.parts
|
||||
}
|
||||
|
||||
// Update part in place
|
||||
let partMap = index.partIndex.get(message.id)
|
||||
if (!partMap) {
|
||||
partMap = new Map()
|
||||
index.partIndex.set(message.id, partMap)
|
||||
}
|
||||
|
||||
let shouldIncrementVersion = filteredSynthetics || replacedTemp
|
||||
const partIndex = partMap.get(part.id)
|
||||
|
||||
if (partIndex === undefined) {
|
||||
initializePartVersion(part)
|
||||
baseParts.push(part)
|
||||
if (part.id && typeof part.id === "string") {
|
||||
partMap.set(part.id, baseParts.length - 1)
|
||||
}
|
||||
shouldIncrementVersion = true
|
||||
// Clear render cache for new text parts
|
||||
if (part.type === "text") {
|
||||
part.renderCache = undefined
|
||||
}
|
||||
} else {
|
||||
const previousPart = baseParts[partIndex]
|
||||
const textUnchanged =
|
||||
!filteredSynthetics &&
|
||||
!replacedTemp &&
|
||||
part.type === "text" &&
|
||||
previousPart?.type === "text" &&
|
||||
previousPart.text === part.text
|
||||
|
||||
if (textUnchanged) {
|
||||
return
|
||||
}
|
||||
|
||||
bumpPartVersion(previousPart, part)
|
||||
baseParts[partIndex] = part
|
||||
if (part.type !== "text" || !previousPart || previousPart.text !== part.text) {
|
||||
shouldIncrementVersion = true
|
||||
// Clear render cache when text changes
|
||||
if (part.type === "text") {
|
||||
part.renderCache = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const oldId = message.id
|
||||
message.id = replacedTemp ? part.messageID : message.id
|
||||
message.status = message.status === "sending" ? "streaming" : message.status
|
||||
message.parts = baseParts
|
||||
|
||||
if (shouldIncrementVersion) {
|
||||
message.version += 1
|
||||
message.displayParts = computeDisplayParts(message, preferences().showThinkingBlocks)
|
||||
} else if (
|
||||
!message.displayParts ||
|
||||
message.displayParts.showThinking !== preferences().showThinkingBlocks ||
|
||||
message.displayParts.version !== message.version
|
||||
) {
|
||||
message.displayParts = computeDisplayParts(message, preferences().showThinkingBlocks)
|
||||
}
|
||||
|
||||
// Update message index if ID changed
|
||||
if (oldId !== message.id) {
|
||||
index.messageIndex.delete(oldId)
|
||||
index.messageIndex.set(message.id, messageIndex)
|
||||
const existingPartMap = index.partIndex.get(oldId)
|
||||
if (existingPartMap) {
|
||||
index.partIndex.delete(oldId)
|
||||
index.partIndex.set(message.id, existingPartMap)
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh part indexes after filtering synthetic parts or replacing optimistic content
|
||||
if (filteredSynthetics || replacedTemp) {
|
||||
const partMap = new Map<string, number>()
|
||||
message.parts.forEach((partItem, idx) => {
|
||||
if (partItem.id && typeof partItem.id === "string") {
|
||||
partMap.set(partItem.id, idx)
|
||||
}
|
||||
})
|
||||
index.partIndex.set(message.id, partMap)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
updateSessionInfo(instanceId, part.sessionID)
|
||||
@@ -1432,14 +1613,102 @@ function handleMessageUpdate(instanceId: string, event: any): void {
|
||||
session.messagesInfo.set(info.id, info)
|
||||
|
||||
withSession(instanceId, info.sessionID, (session) => {
|
||||
// Session already mutated in place
|
||||
const index = getSessionIndex(instanceId, info.sessionID)
|
||||
let messageIndex = index.messageIndex.get(info.id)
|
||||
|
||||
if (messageIndex === undefined) {
|
||||
// Look for queued message to replace
|
||||
let tempMessageIndex = -1
|
||||
for (let i = 0; i < session.messages.length; i++) {
|
||||
const msg = session.messages[i]
|
||||
if (
|
||||
msg.sessionId === info.sessionID &&
|
||||
msg.type === (info.role === "user" ? "user" : "assistant") &&
|
||||
msg.status === "sending"
|
||||
) {
|
||||
tempMessageIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (tempMessageIndex === -1) {
|
||||
for (let i = 0; i < session.messages.length; i++) {
|
||||
const msg = session.messages[i]
|
||||
if (msg.sessionId === info.sessionID && msg.status === "sending") {
|
||||
tempMessageIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tempMessageIndex > -1) {
|
||||
// Replace queued message
|
||||
const message = session.messages[tempMessageIndex]
|
||||
if (typeof message.version !== "number") {
|
||||
message.version = 0
|
||||
}
|
||||
|
||||
const oldId = message.id
|
||||
message.id = info.id
|
||||
message.type = (info.role === "user" ? "user" : "assistant") as "user" | "assistant"
|
||||
message.timestamp = info.time?.created || Date.now()
|
||||
message.status = "complete" as const
|
||||
message.version += 1
|
||||
message.displayParts = computeDisplayParts(message, preferences().showThinkingBlocks)
|
||||
|
||||
if (oldId !== message.id) {
|
||||
index.messageIndex.delete(oldId)
|
||||
index.messageIndex.set(message.id, tempMessageIndex)
|
||||
const existingPartMap = index.partIndex.get(oldId)
|
||||
if (existingPartMap) {
|
||||
index.partIndex.delete(oldId)
|
||||
index.partIndex.set(message.id, existingPartMap)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Append new message
|
||||
const newMessage: Message = {
|
||||
id: info.id,
|
||||
sessionId: info.sessionID,
|
||||
type: (info.role === "user" ? "user" : "assistant") as "user" | "assistant",
|
||||
parts: [],
|
||||
timestamp: info.time?.created || Date.now(),
|
||||
status: "complete" as const,
|
||||
version: 0,
|
||||
}
|
||||
|
||||
newMessage.displayParts = computeDisplayParts(newMessage, preferences().showThinkingBlocks)
|
||||
|
||||
let insertIndex = session.messages.length
|
||||
for (let i = session.messages.length - 1; i >= 0; i--) {
|
||||
if (session.messages[i].id < newMessage.id) {
|
||||
insertIndex = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
session.messages.splice(insertIndex, 0, newMessage)
|
||||
rebuildSessionIndex(instanceId, info.sessionID, session.messages)
|
||||
}
|
||||
} else {
|
||||
// Update existing message status
|
||||
const message = session.messages[messageIndex]
|
||||
if (typeof message.version !== "number") {
|
||||
message.version = 0
|
||||
}
|
||||
message.status = "complete" as const
|
||||
message.version += 1
|
||||
message.displayParts = computeDisplayParts(message, preferences().showThinkingBlocks)
|
||||
}
|
||||
|
||||
session.messagesInfo.set(info.id, info)
|
||||
})
|
||||
|
||||
updateSessionInfo(instanceId, info.sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSessionUpdate(instanceId: string, event: any): void {
|
||||
function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): void {
|
||||
|
||||
const info = event.properties?.info
|
||||
if (!info) return
|
||||
@@ -1455,11 +1724,12 @@ function handleSessionUpdate(instanceId: string, event: any): void {
|
||||
instanceId,
|
||||
title: info.title || "Untitled",
|
||||
parentId: info.parentID || null,
|
||||
agent: info.agent || "",
|
||||
agent: "",
|
||||
model: {
|
||||
providerId: info.model?.providerID || "",
|
||||
modelId: info.model?.modelID || "",
|
||||
providerId: "",
|
||||
modelId: "",
|
||||
},
|
||||
version: info.version || "0",
|
||||
time: {
|
||||
created: info.time?.created || Date.now(),
|
||||
updated: info.time?.updated || Date.now(),
|
||||
@@ -1481,13 +1751,6 @@ function handleSessionUpdate(instanceId: string, event: any): void {
|
||||
const updatedSession = {
|
||||
...existingSession,
|
||||
title: info.title || existingSession.title,
|
||||
agent: info.agent || existingSession.agent,
|
||||
model: info.model
|
||||
? {
|
||||
providerId: info.model.providerID || existingSession.model.providerId,
|
||||
modelId: info.model.modelID || existingSession.model.modelId,
|
||||
}
|
||||
: existingSession.model,
|
||||
time: {
|
||||
...existingSession.time,
|
||||
updated: info.time?.updated || Date.now(),
|
||||
@@ -1499,7 +1762,7 @@ function handleSessionUpdate(instanceId: string, event: any): void {
|
||||
snapshot: info.revert.snapshot,
|
||||
diff: info.revert.diff,
|
||||
}
|
||||
: undefined,
|
||||
: existingSession.revert,
|
||||
}
|
||||
|
||||
setSessions((prev) => {
|
||||
@@ -1512,6 +1775,15 @@ function handleSessionUpdate(instanceId: string, event: any): void {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSessionIdle(instanceId: string, event: EventSessionIdle): void {
|
||||
const sessionId = event.properties?.sessionID
|
||||
if (!sessionId) return
|
||||
|
||||
console.log(`[SSE] Session idle: ${sessionId}`)
|
||||
// Could be used to show user that the session is idle/finished processing
|
||||
// For now, just log it - could be extended to show a notification or update UI state
|
||||
}
|
||||
|
||||
function resolvePastedPlaceholders(prompt: string, attachments: any[] = []): string {
|
||||
if (!prompt || !prompt.includes("[pasted #")) {
|
||||
return prompt
|
||||
@@ -1779,7 +2051,7 @@ async function updateSessionModel(
|
||||
updateSessionInfo(instanceId, sessionId)
|
||||
}
|
||||
|
||||
function handleSessionCompacted(instanceId: string, event: any): void {
|
||||
function handleSessionCompacted(instanceId: string, event: EventSessionCompacted): void {
|
||||
const sessionID = event.properties?.sessionID
|
||||
if (!sessionID) return
|
||||
|
||||
@@ -1800,26 +2072,25 @@ function handleSessionCompacted(instanceId: string, event: any): void {
|
||||
})
|
||||
}
|
||||
|
||||
function handleSessionError(instanceId: string, event: any): void {
|
||||
function handleSessionError(instanceId: string, event: EventSessionError): void {
|
||||
const error = event.properties?.error
|
||||
const sessionID = event.properties?.sessionID
|
||||
console.error(`[SSE] Session error:`, error)
|
||||
|
||||
let message = error?.data?.message || error?.message || "Unknown error"
|
||||
let message = "Unknown error"
|
||||
|
||||
if (error?.data?.responseBody) {
|
||||
try {
|
||||
const body = JSON.parse(error.data.responseBody)
|
||||
if (body.error) {
|
||||
message = body.error
|
||||
}
|
||||
} catch {}
|
||||
if (error) {
|
||||
if ('data' in error && error.data && typeof error.data === 'object' && 'message' in error.data) {
|
||||
message = error.data.message as string
|
||||
} else if ('message' in error && typeof error.message === 'string') {
|
||||
message = error.message
|
||||
}
|
||||
}
|
||||
|
||||
alert(`Error: ${message}`)
|
||||
}
|
||||
|
||||
function handleMessageRemoved(instanceId: string, event: any): void {
|
||||
function handleMessageRemoved(instanceId: string, event: MessageRemovedEvent): void {
|
||||
const sessionID = event.properties?.sessionID
|
||||
if (!sessionID) return
|
||||
|
||||
@@ -1827,7 +2098,7 @@ function handleMessageRemoved(instanceId: string, event: any): void {
|
||||
loadMessages(instanceId, sessionID, true).catch(console.error)
|
||||
}
|
||||
|
||||
function handleMessagePartRemoved(instanceId: string, event: any): void {
|
||||
function handleMessagePartRemoved(instanceId: string, event: MessagePartRemovedEvent): void {
|
||||
const sessionID = event.properties?.sessionID
|
||||
if (!sessionID) return
|
||||
|
||||
@@ -1835,7 +2106,7 @@ function handleMessagePartRemoved(instanceId: string, event: any): void {
|
||||
loadMessages(instanceId, sessionID, true).catch(console.error)
|
||||
}
|
||||
|
||||
function handleTuiToast(_instanceId: string, event: any): void {
|
||||
function handleTuiToast(_instanceId: string, event: TuiToastEvent): void {
|
||||
const payload = event?.properties
|
||||
if (!payload || typeof payload.message !== "string" || typeof payload.variant !== "string") return
|
||||
if (!payload.message.trim()) return
|
||||
@@ -1853,11 +2124,13 @@ function handleTuiToast(_instanceId: string, event: any): void {
|
||||
}
|
||||
|
||||
sseManager.onMessageUpdate = handleMessageUpdate
|
||||
sseManager.onMessagePartUpdated = handleMessageUpdate
|
||||
sseManager.onMessageRemoved = handleMessageRemoved
|
||||
sseManager.onMessagePartRemoved = handleMessagePartRemoved
|
||||
sseManager.onSessionUpdate = handleSessionUpdate
|
||||
sseManager.onSessionCompacted = handleSessionCompacted
|
||||
sseManager.onSessionError = handleSessionError
|
||||
sseManager.onSessionIdle = handleSessionIdle
|
||||
sseManager.onTuiToast = handleTuiToast
|
||||
|
||||
export {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OpencodeClient } from "@opencode-ai/sdk/client"
|
||||
import type { Project as SDKProject } from "@opencode-ai/sdk"
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: number
|
||||
@@ -6,24 +7,23 @@ export interface LogEntry {
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ProjectInfo {
|
||||
id: string
|
||||
worktree: string
|
||||
vcs?: "git"
|
||||
time: {
|
||||
created: number
|
||||
initialized?: number
|
||||
}
|
||||
}
|
||||
// Use SDK Project type instead of our own
|
||||
export type ProjectInfo = SDKProject
|
||||
|
||||
export interface McpServerStatus {
|
||||
name: string
|
||||
status: "running" | "stopped" | "error"
|
||||
}
|
||||
|
||||
// Raw MCP status from server (SDK returns unknown for /mcp endpoint)
|
||||
export type RawMcpStatus = Record<string, {
|
||||
status?: string
|
||||
error?: string
|
||||
}>
|
||||
|
||||
export interface InstanceMetadata {
|
||||
project?: ProjectInfo
|
||||
mcpStatus?: unknown
|
||||
mcpStatus?: RawMcpStatus
|
||||
version?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
// SDK types
|
||||
import type {
|
||||
EventMessageUpdated as MessageUpdateEvent,
|
||||
EventMessageRemoved as MessageRemovedEvent,
|
||||
EventMessagePartUpdated as MessagePartUpdatedEvent,
|
||||
EventMessagePartRemoved as MessagePartRemovedEvent,
|
||||
Part as SDKPart,
|
||||
Message as SDKMessage
|
||||
} from "@opencode-ai/sdk"
|
||||
|
||||
// Re-export for other modules
|
||||
export type {
|
||||
MessageUpdateEvent,
|
||||
MessageRemovedEvent,
|
||||
MessagePartUpdatedEvent,
|
||||
MessagePartRemovedEvent,
|
||||
SDKPart,
|
||||
SDKMessage
|
||||
}
|
||||
|
||||
export interface RenderCache {
|
||||
text: string
|
||||
html: string
|
||||
@@ -5,11 +25,20 @@ export interface RenderCache {
|
||||
mode?: string
|
||||
}
|
||||
|
||||
// Client-specific part extensions (using intersection type since SDKPart is a union)
|
||||
export type ClientPart = SDKPart & {
|
||||
sessionID?: string
|
||||
messageID?: string
|
||||
synthetic?: boolean
|
||||
version?: number
|
||||
renderCache?: RenderCache
|
||||
}
|
||||
|
||||
export interface MessageDisplayParts {
|
||||
text: any[]
|
||||
tool: any[]
|
||||
reasoning: any[]
|
||||
combined: any[]
|
||||
text: ClientPart[]
|
||||
tool: ClientPart[]
|
||||
reasoning: ClientPart[]
|
||||
combined: ClientPart[]
|
||||
showThinking: boolean
|
||||
version: number
|
||||
}
|
||||
@@ -18,7 +47,7 @@ export interface Message {
|
||||
id: string
|
||||
sessionId: string
|
||||
type: "user" | "assistant"
|
||||
parts: any[]
|
||||
parts: ClientPart[]
|
||||
timestamp: number
|
||||
status: "sending" | "sent" | "streaming" | "complete" | "error"
|
||||
version: number
|
||||
@@ -34,42 +63,41 @@ export interface TextPart {
|
||||
renderCache?: RenderCache
|
||||
}
|
||||
|
||||
function hasTextSegment(segment: unknown): boolean {
|
||||
export type MessageInfo = SDKMessage
|
||||
|
||||
function hasTextSegment(segment: string | { text?: string }): boolean {
|
||||
if (typeof segment === "string") {
|
||||
return segment.trim().length > 0
|
||||
}
|
||||
|
||||
if (segment && typeof segment === "object") {
|
||||
const maybeText = (segment as { text?: unknown }).text
|
||||
if (typeof maybeText === "string") {
|
||||
return maybeText.trim().length > 0
|
||||
}
|
||||
if (segment && typeof segment === "object" && segment.text) {
|
||||
return typeof segment.text === "string" && segment.text.trim().length > 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function partHasRenderableText(part: any): boolean {
|
||||
export function partHasRenderableText(part: ClientPart): boolean {
|
||||
if (!part || typeof part !== "object") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (hasTextSegment(part.text)) {
|
||||
const typedPart = part as SDKPart
|
||||
|
||||
if (typedPart.type === "text" && hasTextSegment(typedPart.text)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const contentArray = Array.isArray(part?.content) ? part.content : []
|
||||
for (const item of contentArray) {
|
||||
if (hasTextSegment(item)) {
|
||||
return true
|
||||
}
|
||||
if (typedPart.type === "file" && (typedPart as any).filename) {
|
||||
return true
|
||||
}
|
||||
|
||||
const thinkingContent = Array.isArray(part?.thinking?.content) ? part.thinking.content : []
|
||||
for (const chunk of thinkingContent) {
|
||||
if (hasTextSegment(chunk)) {
|
||||
return true
|
||||
}
|
||||
if (typedPart.type === "tool") {
|
||||
return true // Tool parts are always renderable
|
||||
}
|
||||
|
||||
if (typedPart.type === "reasoning" && hasTextSegment(typedPart.text)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
@@ -1,29 +1,64 @@
|
||||
import type { Message } from "./message"
|
||||
import type { Message, MessageInfo } from "./message"
|
||||
import type {
|
||||
Session as SDKSession,
|
||||
Agent as SDKAgent,
|
||||
Provider as SDKProvider,
|
||||
Model as SDKModel
|
||||
} from "@opencode-ai/sdk"
|
||||
|
||||
export interface Session {
|
||||
id: string
|
||||
instanceId: string
|
||||
title: string
|
||||
parentId: string | null
|
||||
agent: string
|
||||
model: {
|
||||
// Export SDK types for external use
|
||||
export type {
|
||||
Session as SDKSession,
|
||||
Agent as SDKAgent,
|
||||
Provider as SDKProvider,
|
||||
Model as SDKModel
|
||||
} from "@opencode-ai/sdk"
|
||||
|
||||
// Our client-specific Session interface extending SDK Session
|
||||
export interface Session extends Omit<import("@opencode-ai/sdk").Session, 'projectID' | 'directory' | 'parentID'> {
|
||||
instanceId: string // Client-specific field
|
||||
parentId: string | null // Client-specific field (override parentID)
|
||||
agent: string // Client-specific field
|
||||
model: { // Client-specific field
|
||||
providerId: string
|
||||
modelId: string
|
||||
}
|
||||
time: {
|
||||
created: number
|
||||
updated: number
|
||||
}
|
||||
revert?: {
|
||||
messageID: string
|
||||
partID?: string
|
||||
snapshot?: string
|
||||
diff?: string
|
||||
}
|
||||
messages: Message[]
|
||||
messagesInfo: Map<string, any>
|
||||
messages: Message[] // Client-specific field
|
||||
messagesInfo: Map<string, MessageInfo> // Client-specific field
|
||||
version: string // Include version from SDK Session
|
||||
}
|
||||
|
||||
// Adapter function to convert SDK Session to client Session
|
||||
export function createClientSession(
|
||||
sdkSession: import("@opencode-ai/sdk").Session,
|
||||
instanceId: string,
|
||||
agent: string = "",
|
||||
model: { providerId: string; modelId: string } = { providerId: "", modelId: "" }
|
||||
): Session {
|
||||
return {
|
||||
...sdkSession,
|
||||
instanceId,
|
||||
parentId: sdkSession.parentID || null,
|
||||
agent,
|
||||
model,
|
||||
messages: [],
|
||||
messagesInfo: new Map(),
|
||||
}
|
||||
}
|
||||
|
||||
// Type guard to check if object is SDK Session
|
||||
export function isSdkSession(obj: unknown): obj is import("@opencode-ai/sdk").Session {
|
||||
return (
|
||||
typeof obj === "object" &&
|
||||
obj !== null &&
|
||||
"id" in obj &&
|
||||
"title" in obj &&
|
||||
"version" in obj &&
|
||||
"time" in obj
|
||||
)
|
||||
}
|
||||
|
||||
// Our client-specific Agent interface (simplified version of SDK Agent)
|
||||
export interface Agent {
|
||||
name: string
|
||||
description: string
|
||||
@@ -34,6 +69,7 @@ export interface Agent {
|
||||
}
|
||||
}
|
||||
|
||||
// Our client-specific Provider interface (simplified version of SDK Provider)
|
||||
export interface Provider {
|
||||
id: string
|
||||
name: string
|
||||
@@ -41,6 +77,7 @@ export interface Provider {
|
||||
defaultModelId?: string
|
||||
}
|
||||
|
||||
// Our client-specific Model interface (simplified version of SDK Model)
|
||||
export interface Model {
|
||||
id: string
|
||||
name: string
|
||||
|
||||
Reference in New Issue
Block a user