365 lines
14 KiB
TypeScript
365 lines
14 KiB
TypeScript
import { useEvent } from "@tui/context/event"
|
|
import type {
|
|
SessionMessage,
|
|
SessionMessageAssistant,
|
|
SessionMessageAssistantReasoning,
|
|
SessionMessageAssistantText,
|
|
SessionMessageAssistantTool,
|
|
} from "@opencode-ai/sdk/v2"
|
|
import { createStore, produce, reconcile } from "solid-js/store"
|
|
import { createSimpleContext } from "./helper"
|
|
import { useSDK } from "./sdk"
|
|
|
|
function activeAssistant(messages: SessionMessage[]) {
|
|
const index = messages.findIndex((message) => message.type === "assistant" && !message.time.completed)
|
|
if (index < 0) return
|
|
const assistant = messages[index]
|
|
return assistant?.type === "assistant" ? assistant : undefined
|
|
}
|
|
|
|
function ownedAssistant(messages: SessionMessage[], messageID: string) {
|
|
const message = messages.find((message) => message.type === "assistant" && message.id === messageID)
|
|
return message?.type === "assistant" ? message : undefined
|
|
}
|
|
|
|
function activeCompaction(messages: SessionMessage[]) {
|
|
const index = messages.findIndex((message) => message.type === "compaction")
|
|
if (index < 0) return
|
|
const compaction = messages[index]
|
|
return compaction?.type === "compaction" ? compaction : undefined
|
|
}
|
|
|
|
function activeShell(messages: SessionMessage[], callID: string) {
|
|
const index = messages.findIndex((message) => message.type === "shell" && message.callID === callID)
|
|
if (index < 0) return
|
|
const shell = messages[index]
|
|
return shell?.type === "shell" ? shell : undefined
|
|
}
|
|
|
|
function latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
|
|
return assistant?.content.findLast(
|
|
(item): item is SessionMessageAssistantTool => item.type === "tool" && (callID === undefined || item.id === callID),
|
|
)
|
|
}
|
|
|
|
function latestText(assistant: SessionMessageAssistant | undefined, textID: string) {
|
|
return assistant?.content.findLast(
|
|
(item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID,
|
|
)
|
|
}
|
|
|
|
function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) {
|
|
return assistant?.content.findLast(
|
|
(item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && item.id === reasoningID,
|
|
)
|
|
}
|
|
|
|
export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext({
|
|
name: "SyncV2",
|
|
init: () => {
|
|
const [store, setStore] = createStore<{
|
|
messages: {
|
|
[sessionID: string]: SessionMessage[]
|
|
}
|
|
}>({
|
|
messages: {},
|
|
})
|
|
|
|
const event = useEvent()
|
|
const sdk = useSDK()
|
|
|
|
function update(sessionID: string, fn: (messages: SessionMessage[]) => void) {
|
|
setStore(
|
|
"messages",
|
|
produce((draft) => {
|
|
fn((draft[sessionID] ??= []))
|
|
}),
|
|
)
|
|
}
|
|
|
|
event.subscribe((event) => {
|
|
switch (event.type) {
|
|
case "session.next.agent.switched":
|
|
update(event.properties.sessionID, (draft) => {
|
|
draft.unshift({
|
|
id: event.id,
|
|
type: "agent-switched",
|
|
agent: event.properties.agent,
|
|
time: { created: event.properties.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.model.switched":
|
|
update(event.properties.sessionID, (draft) => {
|
|
draft.unshift({
|
|
id: event.id,
|
|
type: "model-switched",
|
|
model: event.properties.model,
|
|
time: { created: event.properties.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.prompted": {
|
|
update(event.properties.sessionID, (draft) => {
|
|
draft.unshift({
|
|
id: event.id,
|
|
type: "user",
|
|
text: event.properties.prompt.text,
|
|
files: event.properties.prompt.files,
|
|
agents: event.properties.prompt.agents,
|
|
references: event.properties.prompt.references,
|
|
time: { created: event.properties.timestamp },
|
|
})
|
|
})
|
|
break
|
|
}
|
|
case "session.next.synthetic":
|
|
update(event.properties.sessionID, (draft) => {
|
|
draft.unshift({
|
|
id: event.id,
|
|
type: "synthetic",
|
|
sessionID: event.properties.sessionID,
|
|
text: event.properties.text,
|
|
time: { created: event.properties.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.shell.started":
|
|
update(event.properties.sessionID, (draft) => {
|
|
draft.unshift({
|
|
id: event.id,
|
|
type: "shell",
|
|
callID: event.properties.callID,
|
|
command: event.properties.command,
|
|
output: "",
|
|
time: { created: event.properties.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.shell.ended":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = activeShell(draft, event.properties.callID)
|
|
if (!match) return
|
|
match.output = event.properties.output
|
|
match.time.completed = event.properties.timestamp
|
|
})
|
|
break
|
|
case "session.next.step.started":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const currentAssistant = activeAssistant(draft)
|
|
if (currentAssistant) currentAssistant.time.completed = event.properties.timestamp
|
|
draft.unshift({
|
|
id: event.id,
|
|
type: "assistant",
|
|
agent: event.properties.agent,
|
|
model: event.properties.model,
|
|
content: [],
|
|
snapshot: event.properties.snapshot ? { start: event.properties.snapshot } : undefined,
|
|
time: { created: event.properties.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.step.ended":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const currentAssistant = ownedAssistant(draft, event.properties.assistantMessageID)
|
|
if (!currentAssistant) return
|
|
currentAssistant.time.completed = event.properties.timestamp
|
|
currentAssistant.finish = event.properties.finish
|
|
currentAssistant.cost = event.properties.cost
|
|
currentAssistant.tokens = event.properties.tokens
|
|
if (event.properties.snapshot)
|
|
currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.properties.snapshot }
|
|
})
|
|
break
|
|
case "session.next.step.failed":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const currentAssistant = ownedAssistant(draft, event.properties.assistantMessageID)
|
|
if (!currentAssistant) return
|
|
currentAssistant.time.completed = event.properties.timestamp
|
|
currentAssistant.finish = "error"
|
|
currentAssistant.error = event.properties.error
|
|
})
|
|
break
|
|
case "session.next.text.started":
|
|
update(event.properties.sessionID, (draft) => {
|
|
activeAssistant(draft)?.content.push({ type: "text", id: event.properties.textID, text: "" })
|
|
})
|
|
break
|
|
case "session.next.text.delta":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = latestText(activeAssistant(draft), event.properties.textID)
|
|
if (match) match.text += event.properties.delta
|
|
})
|
|
break
|
|
case "session.next.text.ended":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = latestText(activeAssistant(draft), event.properties.textID)
|
|
if (match) match.text = event.properties.text
|
|
})
|
|
break
|
|
case "session.next.tool.input.started":
|
|
update(event.properties.sessionID, (draft) => {
|
|
ownedAssistant(draft, event.properties.assistantMessageID)?.content.push({
|
|
type: "tool",
|
|
id: event.properties.callID,
|
|
name: event.properties.name,
|
|
time: { created: event.properties.timestamp },
|
|
state: { status: "pending", input: "" },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.tool.input.delta":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = latestTool(
|
|
ownedAssistant(draft, event.properties.assistantMessageID),
|
|
event.properties.callID,
|
|
)
|
|
if (match?.state.status === "pending") match.state.input += event.properties.delta
|
|
})
|
|
break
|
|
case "session.next.tool.input.ended":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = latestTool(
|
|
ownedAssistant(draft, event.properties.assistantMessageID),
|
|
event.properties.callID,
|
|
)
|
|
if (match?.state.status === "pending") match.state.input = event.properties.text
|
|
})
|
|
break
|
|
case "session.next.tool.called":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = latestTool(
|
|
ownedAssistant(draft, event.properties.assistantMessageID),
|
|
event.properties.callID,
|
|
)
|
|
if (!match) return
|
|
match.time.ran = event.properties.timestamp
|
|
match.provider = event.properties.provider
|
|
match.state = { status: "running", input: event.properties.input, structured: {}, content: [] }
|
|
})
|
|
break
|
|
case "session.next.tool.progress":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = latestTool(
|
|
ownedAssistant(draft, event.properties.assistantMessageID),
|
|
event.properties.callID,
|
|
)
|
|
if (match?.state.status !== "running") return
|
|
match.state.structured = event.properties.structured
|
|
match.state.content = [...event.properties.content]
|
|
})
|
|
break
|
|
case "session.next.tool.success":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = latestTool(
|
|
ownedAssistant(draft, event.properties.assistantMessageID),
|
|
event.properties.callID,
|
|
)
|
|
if (match?.state.status !== "running") return
|
|
match.state = {
|
|
status: "completed",
|
|
input: match.state.input,
|
|
structured: event.properties.structured,
|
|
content: [...event.properties.content],
|
|
result: event.properties.result,
|
|
}
|
|
match.provider = event.properties.provider
|
|
match.time.completed = event.properties.timestamp
|
|
})
|
|
break
|
|
case "session.next.tool.failed":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = latestTool(
|
|
ownedAssistant(draft, event.properties.assistantMessageID),
|
|
event.properties.callID,
|
|
)
|
|
if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return
|
|
match.state = {
|
|
status: "error",
|
|
error: event.properties.error,
|
|
input: typeof match.state.input === "string" ? {} : match.state.input,
|
|
structured: match.state.status === "running" ? match.state.structured : {},
|
|
content: match.state.status === "running" ? match.state.content : [],
|
|
result: event.properties.result,
|
|
}
|
|
match.provider = event.properties.provider
|
|
match.time.completed = event.properties.timestamp
|
|
})
|
|
break
|
|
case "session.next.reasoning.started":
|
|
update(event.properties.sessionID, (draft) => {
|
|
activeAssistant(draft)?.content.push({
|
|
type: "reasoning",
|
|
id: event.properties.reasoningID,
|
|
text: "",
|
|
providerMetadata: event.properties.providerMetadata,
|
|
})
|
|
})
|
|
break
|
|
case "session.next.reasoning.delta":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
|
|
if (match) match.text += event.properties.delta
|
|
})
|
|
break
|
|
case "session.next.reasoning.ended":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
|
|
if (match) {
|
|
match.text = event.properties.text
|
|
if (event.properties.providerMetadata !== undefined)
|
|
match.providerMetadata = event.properties.providerMetadata
|
|
}
|
|
})
|
|
break
|
|
case "session.next.retried":
|
|
break
|
|
case "session.next.compaction.started":
|
|
update(event.properties.sessionID, (draft) => {
|
|
draft.unshift({
|
|
id: event.id,
|
|
type: "compaction",
|
|
reason: event.properties.reason,
|
|
summary: "",
|
|
time: { created: event.properties.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.compaction.delta":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = activeCompaction(draft)
|
|
if (match) match.summary += event.properties.text
|
|
})
|
|
break
|
|
case "session.next.compaction.ended":
|
|
update(event.properties.sessionID, (draft) => {
|
|
const match = activeCompaction(draft)
|
|
if (!match) return
|
|
match.summary = event.properties.text
|
|
match.include = event.properties.include
|
|
})
|
|
break
|
|
}
|
|
})
|
|
|
|
const result = {
|
|
data: store,
|
|
session: {
|
|
message: {
|
|
async sync(sessionID: string) {
|
|
const response = await sdk.client.v2.session.messages({ sessionID })
|
|
setStore("messages", sessionID, reconcile(response.data?.data ?? []))
|
|
},
|
|
fromSession(sessionID: string) {
|
|
const messages = store.messages[sessionID]
|
|
if (!messages) return []
|
|
return messages
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
return result
|
|
},
|
|
})
|