570 lines
22 KiB
TypeScript
570 lines
22 KiB
TypeScript
import type {
|
|
AgentV2Info,
|
|
CommandV2Info,
|
|
IntegrationInfo,
|
|
LocationRef,
|
|
ModelV2Info,
|
|
PermissionSavedInfo,
|
|
PermissionV2Request,
|
|
ProviderV2Info,
|
|
QuestionV2Request,
|
|
ReferenceInfo,
|
|
SessionMessage,
|
|
SessionMessageAssistant,
|
|
SessionMessageAssistantReasoning,
|
|
SessionMessageAssistantText,
|
|
SessionMessageAssistantTool,
|
|
SessionV2Info,
|
|
SkillV2Info,
|
|
V2Event,
|
|
} from "@opencode-ai/sdk/v2"
|
|
import { createStore, produce } from "solid-js/store"
|
|
import { createSimpleContext } from "./helper"
|
|
import { useSDK } from "./sdk"
|
|
import { useEvent } from "./event"
|
|
import { createSignal, onCleanup, onMount } from "solid-js"
|
|
|
|
type LocationData = {
|
|
agent?: AgentV2Info[]
|
|
command?: CommandV2Info[]
|
|
integration?: IntegrationInfo[]
|
|
model?: ModelV2Info[]
|
|
provider?: ProviderV2Info[]
|
|
reference?: ReferenceInfo[]
|
|
skill?: SkillV2Info[]
|
|
}
|
|
|
|
type Data = {
|
|
session: {
|
|
info: Record<string, SessionV2Info>
|
|
message: Record<string, SessionMessage[]>
|
|
permission: Record<string, PermissionV2Request[]>
|
|
question: Record<string, QuestionV2Request[]>
|
|
}
|
|
project: {
|
|
permission: Record<string, PermissionSavedInfo[]>
|
|
}
|
|
location: Record<string, LocationData>
|
|
}
|
|
|
|
function locationKey(location: LocationRef) {
|
|
return JSON.stringify([location.directory, location.workspaceID])
|
|
}
|
|
|
|
function locationQuery(ref?: LocationRef) {
|
|
return ref ? { directory: ref.directory, workspace: ref.workspaceID } : undefined
|
|
}
|
|
|
|
export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|
name: "Data",
|
|
init: () => {
|
|
const [store, setStore] = createStore<Data>({
|
|
session: {
|
|
info: {},
|
|
message: {},
|
|
permission: {},
|
|
question: {},
|
|
},
|
|
project: {
|
|
permission: {},
|
|
},
|
|
location: {},
|
|
})
|
|
|
|
const sdk = useSDK()
|
|
const events = useEvent()
|
|
const [defaultLocation, setDefaultLocation] = createSignal<LocationRef>({
|
|
directory: sdk.directory ?? process.cwd(),
|
|
})
|
|
|
|
const message = {
|
|
update(sessionID: string, fn: (messages: SessionMessage[]) => void) {
|
|
setStore(
|
|
"session",
|
|
"message",
|
|
produce((draft) => {
|
|
fn((draft[sessionID] ??= []))
|
|
}),
|
|
)
|
|
},
|
|
prepend(messages: SessionMessage[], item: SessionMessage) {
|
|
if (messages.some((existing) => existing.id === item.id)) return
|
|
messages.unshift(item)
|
|
},
|
|
activeAssistant(messages: SessionMessage[]) {
|
|
const item = messages.find((item) => item.type === "assistant" && !item.time.completed)
|
|
return item?.type === "assistant" ? item : undefined
|
|
},
|
|
assistant(messages: SessionMessage[], messageID: string) {
|
|
const item = messages.find((item) => item.type === "assistant" && item.id === messageID)
|
|
return item?.type === "assistant" ? item : undefined
|
|
},
|
|
activeShell(messages: SessionMessage[], callID: string) {
|
|
const item = messages.find((item) => item.type === "shell" && item.callID === callID)
|
|
return item?.type === "shell" ? item : undefined
|
|
},
|
|
latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
|
|
return assistant?.content.findLast(
|
|
(item): item is SessionMessageAssistantTool =>
|
|
item.type === "tool" && (callID === undefined || item.id === callID),
|
|
)
|
|
},
|
|
latestText(assistant: SessionMessageAssistant | undefined, textID: string) {
|
|
return assistant?.content.findLast(
|
|
(item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID,
|
|
)
|
|
},
|
|
latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) {
|
|
return assistant?.content.findLast(
|
|
(item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && item.id === reasoningID,
|
|
)
|
|
},
|
|
}
|
|
|
|
function handleEvent(event: V2Event) {
|
|
switch (event.type) {
|
|
case "catalog.updated":
|
|
void Promise.all([
|
|
result.location.model.refresh(event.location),
|
|
result.location.provider.refresh(event.location),
|
|
])
|
|
break
|
|
case "session.next.agent.switched":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
message.prepend(draft, {
|
|
id: event.data.messageID,
|
|
type: "agent-switched",
|
|
agent: event.data.agent,
|
|
time: { created: event.data.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.model.switched":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
message.prepend(draft, {
|
|
id: event.data.messageID,
|
|
type: "model-switched",
|
|
model: event.data.model,
|
|
time: { created: event.data.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.prompted": {
|
|
message.update(event.data.sessionID, (draft) => {
|
|
message.prepend(draft, {
|
|
id: event.data.messageID,
|
|
type: "user",
|
|
text: event.data.prompt.text,
|
|
files: event.data.prompt.files,
|
|
agents: event.data.prompt.agents,
|
|
time: { created: event.data.timestamp },
|
|
})
|
|
})
|
|
break
|
|
}
|
|
case "session.next.prompt.admitted":
|
|
break
|
|
case "session.next.context.updated":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
message.prepend(draft, {
|
|
id: event.data.messageID,
|
|
type: "system",
|
|
text: event.data.text,
|
|
time: { created: event.data.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.synthetic":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
message.prepend(draft, {
|
|
id: event.data.messageID,
|
|
type: "synthetic",
|
|
sessionID: event.data.sessionID,
|
|
text: event.data.text,
|
|
time: { created: event.data.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.shell.started":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
message.prepend(draft, {
|
|
id: event.data.messageID,
|
|
type: "shell",
|
|
callID: event.data.callID,
|
|
command: event.data.command,
|
|
output: "",
|
|
time: { created: event.data.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.shell.ended":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const match = message.activeShell(draft, event.data.callID)
|
|
if (!match) return
|
|
match.output = event.data.output
|
|
match.time.completed = event.data.timestamp
|
|
})
|
|
break
|
|
case "session.next.step.started":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
if (draft.some((message) => message.id === event.data.assistantMessageID)) return
|
|
const currentAssistant = message.activeAssistant(draft)
|
|
if (currentAssistant) currentAssistant.time.completed = event.data.timestamp
|
|
message.prepend(draft, {
|
|
id: event.data.assistantMessageID,
|
|
type: "assistant",
|
|
agent: event.data.agent,
|
|
model: event.data.model,
|
|
content: [],
|
|
snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
|
|
time: { created: event.data.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.step.ended":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const currentAssistant = message.assistant(draft, event.data.assistantMessageID)
|
|
if (!currentAssistant) return
|
|
currentAssistant.time.completed = event.data.timestamp
|
|
currentAssistant.finish = event.data.finish
|
|
currentAssistant.cost = event.data.cost
|
|
currentAssistant.tokens = event.data.tokens
|
|
if (event.data.snapshot)
|
|
currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.data.snapshot }
|
|
})
|
|
break
|
|
case "session.next.step.failed":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const currentAssistant = message.assistant(draft, event.data.assistantMessageID)
|
|
if (!currentAssistant) return
|
|
currentAssistant.time.completed = event.data.timestamp
|
|
currentAssistant.finish = "error"
|
|
currentAssistant.error = event.data.error
|
|
})
|
|
break
|
|
case "session.next.text.started":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
message.assistant(draft, event.data.assistantMessageID)?.content.push({
|
|
type: "text",
|
|
id: event.data.textID,
|
|
text: "",
|
|
})
|
|
})
|
|
break
|
|
case "session.next.text.delta":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const match = message.latestText(message.assistant(draft, event.data.assistantMessageID), event.data.textID)
|
|
if (match) match.text += event.data.delta
|
|
})
|
|
break
|
|
case "session.next.text.ended":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const match = message.latestText(message.assistant(draft, event.data.assistantMessageID), event.data.textID)
|
|
if (match) match.text = event.data.text
|
|
})
|
|
break
|
|
case "session.next.tool.input.started":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
message.assistant(draft, event.data.assistantMessageID)?.content.push({
|
|
type: "tool",
|
|
id: event.data.callID,
|
|
name: event.data.name,
|
|
time: { created: event.data.timestamp },
|
|
state: { status: "pending", input: "" },
|
|
})
|
|
})
|
|
break
|
|
case "session.next.tool.input.delta":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID)
|
|
if (match?.state.status === "pending") match.state.input += event.data.delta
|
|
})
|
|
break
|
|
case "session.next.tool.input.ended":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID)
|
|
if (match?.state.status === "pending") match.state.input = event.data.text
|
|
})
|
|
break
|
|
case "session.next.tool.called":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID)
|
|
if (!match) return
|
|
match.time.ran = event.data.timestamp
|
|
match.provider = event.data.provider
|
|
match.state = { status: "running", input: event.data.input, structured: {}, content: [] }
|
|
})
|
|
break
|
|
case "session.next.tool.progress":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID)
|
|
if (match?.state.status !== "running") return
|
|
match.state.structured = event.data.structured
|
|
match.state.content = [...event.data.content]
|
|
})
|
|
break
|
|
case "session.next.tool.success":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID)
|
|
if (match?.state.status !== "running") return
|
|
match.state = {
|
|
status: "completed",
|
|
input: match.state.input,
|
|
structured: event.data.structured,
|
|
content: [...event.data.content],
|
|
result: event.data.result,
|
|
}
|
|
match.provider = {
|
|
executed: event.data.provider.executed || match.provider?.executed === true,
|
|
metadata: match.provider?.metadata,
|
|
resultMetadata: event.data.provider.metadata,
|
|
}
|
|
match.time.completed = event.data.timestamp
|
|
})
|
|
break
|
|
case "session.next.tool.failed":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const match = message.latestTool(message.assistant(draft, event.data.assistantMessageID), event.data.callID)
|
|
if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return
|
|
match.state = {
|
|
status: "error",
|
|
error: event.data.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.data.result,
|
|
}
|
|
match.provider = {
|
|
executed: event.data.provider.executed || match.provider?.executed === true,
|
|
metadata: match.provider?.metadata,
|
|
resultMetadata: event.data.provider.metadata,
|
|
}
|
|
match.time.completed = event.data.timestamp
|
|
})
|
|
break
|
|
case "session.next.reasoning.started":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
message.assistant(draft, event.data.assistantMessageID)?.content.push({
|
|
type: "reasoning",
|
|
id: event.data.reasoningID,
|
|
text: "",
|
|
providerMetadata: event.data.providerMetadata,
|
|
})
|
|
})
|
|
break
|
|
case "session.next.reasoning.delta":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const match = message.latestReasoning(
|
|
message.assistant(draft, event.data.assistantMessageID),
|
|
event.data.reasoningID,
|
|
)
|
|
if (match) match.text += event.data.delta
|
|
})
|
|
break
|
|
case "session.next.reasoning.ended":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
const match = message.latestReasoning(
|
|
message.assistant(draft, event.data.assistantMessageID),
|
|
event.data.reasoningID,
|
|
)
|
|
if (match) {
|
|
match.text = event.data.text
|
|
if (event.data.providerMetadata !== undefined) match.providerMetadata = event.data.providerMetadata
|
|
}
|
|
})
|
|
break
|
|
case "session.next.retried":
|
|
case "session.next.compaction.started":
|
|
case "session.next.compaction.delta":
|
|
break
|
|
case "session.next.compaction.ended":
|
|
message.update(event.data.sessionID, (draft) => {
|
|
message.prepend(draft, {
|
|
id: event.data.messageID,
|
|
type: "compaction",
|
|
reason: event.data.reason,
|
|
summary: event.data.text,
|
|
recent: event.data.recent,
|
|
time: { created: event.data.timestamp },
|
|
})
|
|
})
|
|
break
|
|
case "reference.updated":
|
|
void result.location.reference.refresh()
|
|
break
|
|
case "integration.updated":
|
|
void Promise.all([
|
|
result.location.integration.refresh(event.location),
|
|
result.location.model.refresh(event.location),
|
|
result.location.provider.refresh(event.location),
|
|
])
|
|
break
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
const unsub = events.subscribe((event, metadata) => {
|
|
handleEvent({
|
|
...event,
|
|
data: event.properties,
|
|
location: { directory: metadata.directory, workspaceID: metadata.workspace },
|
|
} as V2Event)
|
|
})
|
|
onCleanup(unsub)
|
|
})
|
|
|
|
const result = {
|
|
session: {
|
|
get(sessionID: string) {
|
|
return store.session.info[sessionID]
|
|
},
|
|
async refresh(sessionID: string) {
|
|
const result = await sdk.client.v2.session.get({ sessionID }, { throwOnError: true })
|
|
setStore("session", "info", sessionID, result.data.data)
|
|
},
|
|
message: {
|
|
list(sessionID: string) {
|
|
return store.session.message[sessionID]
|
|
},
|
|
async refresh(sessionID: string) {
|
|
const result = await sdk.client.v2.session.messages({ sessionID }, { throwOnError: true })
|
|
setStore("session", "message", sessionID, result.data.data)
|
|
},
|
|
},
|
|
permission: {
|
|
list(sessionID: string) {
|
|
return store.session.permission[sessionID]
|
|
},
|
|
async refresh(sessionID: string) {
|
|
const result = await sdk.client.v2.session.permission.list({ sessionID }, { throwOnError: true })
|
|
setStore("session", "permission", sessionID, result.data.data)
|
|
},
|
|
},
|
|
question: {
|
|
list(sessionID: string) {
|
|
return store.session.question[sessionID]
|
|
},
|
|
async refresh(sessionID: string) {
|
|
const result = await sdk.client.v2.session.question.list({ sessionID }, { throwOnError: true })
|
|
setStore("session", "question", sessionID, result.data.data)
|
|
},
|
|
},
|
|
},
|
|
project: {
|
|
permission: {
|
|
list(projectID: string) {
|
|
return store.project.permission[projectID]
|
|
},
|
|
async refresh(projectID: string) {
|
|
const result = await sdk.client.v2.permission.saved.list({ projectID }, { throwOnError: true })
|
|
setStore("project", "permission", projectID, result.data.data)
|
|
},
|
|
},
|
|
},
|
|
location: {
|
|
default() {
|
|
return defaultLocation()
|
|
},
|
|
async refresh(ref?: LocationRef) {
|
|
const response = await sdk.client.v2.location.get({ location: locationQuery(ref) }, { throwOnError: true })
|
|
const location = response.data
|
|
const key = locationKey(location)
|
|
if (!store.location[key]) setStore("location", key, {})
|
|
if (!ref) setDefaultLocation({ directory: location.directory, workspaceID: location.workspaceID })
|
|
},
|
|
agent: {
|
|
list(location?: LocationRef) {
|
|
return store.location[locationKey(location ?? defaultLocation())]?.agent
|
|
},
|
|
async refresh(ref?: LocationRef) {
|
|
const result = await sdk.client.v2.agent.list({ location: locationQuery(ref) }, { throwOnError: true })
|
|
const key = locationKey(result.data.location)
|
|
setStore("location", key, "agent", result.data.data)
|
|
},
|
|
},
|
|
command: {
|
|
list(location?: LocationRef) {
|
|
return store.location[locationKey(location ?? defaultLocation())]?.command
|
|
},
|
|
async refresh(ref?: LocationRef) {
|
|
const result = await sdk.client.v2.command.list({ location: locationQuery(ref) }, { throwOnError: true })
|
|
const key = locationKey(result.data.location)
|
|
setStore("location", key, "command", result.data.data)
|
|
},
|
|
},
|
|
integration: {
|
|
list(location?: LocationRef) {
|
|
return store.location[locationKey(location ?? defaultLocation())]?.integration
|
|
},
|
|
async refresh(ref?: LocationRef) {
|
|
const result = await sdk.client.v2.integration.list(
|
|
{ location: locationQuery(ref) },
|
|
{ throwOnError: true },
|
|
)
|
|
const key = locationKey(result.data.location)
|
|
setStore("location", key, "integration", result.data.data)
|
|
},
|
|
},
|
|
model: {
|
|
list(location?: LocationRef) {
|
|
return store.location[locationKey(location ?? defaultLocation())]?.model
|
|
},
|
|
async refresh(ref?: LocationRef) {
|
|
const result = await sdk.client.v2.model.list({ location: locationQuery(ref) }, { throwOnError: true })
|
|
const key = locationKey(result.data.location)
|
|
setStore("location", key, "model", result.data.data)
|
|
},
|
|
},
|
|
provider: {
|
|
list(location?: LocationRef) {
|
|
return store.location[locationKey(location ?? defaultLocation())]?.provider
|
|
},
|
|
async refresh(ref?: LocationRef) {
|
|
const result = await sdk.client.v2.provider.list({ location: locationQuery(ref) }, { throwOnError: true })
|
|
const key = locationKey(result.data.location)
|
|
setStore("location", key, "provider", result.data.data)
|
|
},
|
|
},
|
|
reference: {
|
|
list(location?: LocationRef) {
|
|
return store.location[locationKey(location ?? defaultLocation())]?.reference
|
|
},
|
|
async refresh(ref?: LocationRef) {
|
|
const result = await sdk.client.v2.reference.list({ location: locationQuery(ref) }, { throwOnError: true })
|
|
const key = locationKey(result.data.location)
|
|
setStore("location", key, "reference", result.data.data)
|
|
},
|
|
},
|
|
skill: {
|
|
list(location?: LocationRef) {
|
|
return store.location[locationKey(location ?? defaultLocation())]?.skill
|
|
},
|
|
async refresh(ref?: LocationRef) {
|
|
const result = await sdk.client.v2.skill.list({ location: locationQuery(ref) }, { throwOnError: true })
|
|
const key = locationKey(result.data.location)
|
|
setStore("location", key, "skill", result.data.data)
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
onMount(() => {
|
|
void Promise.allSettled([
|
|
result.location.refresh(),
|
|
result.location.agent.refresh(),
|
|
result.location.integration.refresh(),
|
|
result.location.model.refresh(),
|
|
result.location.provider.refresh(),
|
|
result.location.reference.refresh(),
|
|
result.location.command.refresh(),
|
|
result.location.skill.refresh(),
|
|
]).then((settled) => {
|
|
for (const failure of settled.filter((item) => item.status === "rejected"))
|
|
console.error("Failed to refresh default location data", failure.reason)
|
|
})
|
|
})
|
|
|
|
return result
|
|
},
|
|
})
|