summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorDax <[email protected]>2025-07-13 17:22:11 -0400
committerGitHub <[email protected]>2025-07-13 17:22:11 -0400
commit90d6c4ab41bb097d7db354109e3616ff16778f0b (patch)
tree303861ce5789f6e0e8e843cb8184dea829b4885d /packages
parent736396fc70ab05204b886634ffbcd1318d82eca8 (diff)
downloadopencode-90d6c4ab41bb097d7db354109e3616ff16778f0b.tar.gz
opencode-90d6c4ab41bb097d7db354109e3616ff16778f0b.zip
Part data model (#950)
Diffstat (limited to 'packages')
-rw-r--r--packages/function/src/api.ts17
-rw-r--r--packages/opencode/package.json1
-rw-r--r--packages/opencode/src/cli/cmd/run.ts26
-rw-r--r--packages/opencode/src/cli/cmd/stats.ts84
-rw-r--r--packages/opencode/src/cli/ui.ts16
-rw-r--r--packages/opencode/src/id/id.ts1
-rw-r--r--packages/opencode/src/server/server.ts13
-rw-r--r--packages/opencode/src/session/index.ts444
-rw-r--r--packages/opencode/src/session/message-v2.ts320
-rw-r--r--packages/opencode/src/storage/storage.ts39
-rw-r--r--packages/opencode/src/tool/task.ts20
-rw-r--r--packages/tui/internal/app/app.go109
-rw-r--r--packages/tui/internal/components/chat/messages.go43
-rw-r--r--packages/tui/internal/id/id.go96
-rw-r--r--packages/tui/internal/tui/tui.go103
-rw-r--r--packages/tui/sdk/.stats.yml6
-rw-r--r--packages/tui/sdk/README.md23
-rw-r--r--packages/tui/sdk/api.md8
-rw-r--r--packages/tui/sdk/client_test.go44
-rw-r--r--packages/tui/sdk/event.go8
-rwxr-xr-xpackages/tui/sdk/scripts/lint4
-rw-r--r--packages/tui/sdk/session.go683
-rw-r--r--packages/tui/sdk/session_test.go18
-rw-r--r--packages/tui/sdk/usage_test.go7
-rw-r--r--packages/web/src/components/Share.tsx60
25 files changed, 1247 insertions, 946 deletions
diff --git a/packages/function/src/api.ts b/packages/function/src/api.ts
index 150afd887..c5ac8b4a4 100644
--- a/packages/function/src/api.ts
+++ b/packages/function/src/api.ts
@@ -42,7 +42,11 @@ export class SyncServer extends DurableObject<Env> {
async publish(key: string, content: any) {
const sessionID = await this.getSessionID()
- if (!key.startsWith(`session/info/${sessionID}`) && !key.startsWith(`session/message/${sessionID}/`))
+ if (
+ !key.startsWith(`session/info/${sessionID}`) &&
+ !key.startsWith(`session/message/${sessionID}/`) &&
+ !key.startsWith(`session/part/${sessionID}/`)
+ )
return new Response("Error: Invalid key", { status: 400 })
// store message
@@ -71,7 +75,7 @@ export class SyncServer extends DurableObject<Env> {
}
public async getData() {
- const data = await this.ctx.storage.list()
+ const data = (await this.ctx.storage.list()) as Map<string, any>
return Array.from(data.entries())
.filter(([key, _]) => key.startsWith("session/"))
.map(([key, content]) => ({ key, content }))
@@ -207,8 +211,13 @@ export default {
return
}
if (type === "message") {
- const [, messageID] = splits
- messages[messageID] = d.content
+ messages[d.content.id] = {
+ parts: [],
+ ...d.content,
+ }
+ }
+ if (type === "part") {
+ messages[d.content.messageID].parts.push(d.content)
}
})
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index c5b7e4ba9..6e61c1d82 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -33,6 +33,7 @@
"@openauthjs/openauth": "0.4.3",
"@standard-schema/spec": "1.0.0",
"ai": "catalog:",
+ "cli-markdown": "3.5.1",
"decimal.js": "10.5.0",
"diff": "8.0.2",
"env-paths": "3.0.0",
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index 0daa510d1..553bff818 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -9,6 +9,7 @@ import { Config } from "../../config/config"
import { bootstrap } from "../bootstrap"
import { MessageV2 } from "../../session/message-v2"
import { Mode } from "../../session/mode"
+import { Identifier } from "../../id/id"
const TOOL: Record<string, [string, string]> = {
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
@@ -83,14 +84,9 @@ export const RunCommand = cmd({
return
}
- const isPiped = !process.stdout.isTTY
-
UI.empty()
UI.println(UI.logo())
UI.empty()
- const displayMessage = message.length > 300 ? message.slice(0, 300) + "..." : message
- UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", displayMessage)
- UI.empty()
const cfg = await Config.get()
if (cfg.share === "auto" || Flag.OPENCODE_AUTO_SHARE || args.share) {
@@ -120,8 +116,10 @@ export const RunCommand = cmd({
)
}
+ let text = ""
Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
- if (evt.properties.sessionID !== session.id) return
+ if (evt.properties.part.sessionID !== session.id) return
+ if (evt.properties.part.messageID === messageID) return
const part = evt.properties.part
if (part.type === "tool" && part.state.status === "completed") {
@@ -130,13 +128,15 @@ export const RunCommand = cmd({
}
if (part.type === "text") {
- if (part.text.includes("\n")) {
+ text = part.text
+
+ if (part.time?.end) {
UI.empty()
- UI.println(part.text)
+ UI.println(UI.markdown(text))
UI.empty()
+ text = ""
return
}
- printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
}
})
@@ -156,8 +156,10 @@ export const RunCommand = cmd({
const mode = args.mode ? await Mode.get(args.mode) : await Mode.list().then((x) => x[0])
+ const messageID = Identifier.ascending("message")
const result = await Session.chat({
sessionID: session.id,
+ messageID,
...(mode.model
? mode.model
: {
@@ -167,15 +169,19 @@ export const RunCommand = cmd({
mode: mode.name,
parts: [
{
+ id: Identifier.ascending("part"),
+ sessionID: session.id,
+ messageID: messageID,
type: "text",
text: message,
},
],
})
+ const isPiped = !process.stdout.isTTY
if (isPiped) {
const match = result.parts.findLast((x) => x.type === "text")
- if (match) process.stdout.write(match.text)
+ if (match) process.stdout.write(UI.markdown(match.text))
if (errorMsg) process.stdout.write(errorMsg)
}
UI.empty()
diff --git a/packages/opencode/src/cli/cmd/stats.ts b/packages/opencode/src/cli/cmd/stats.ts
index 6c0b16003..39ae86ba0 100644
--- a/packages/opencode/src/cli/cmd/stats.ts
+++ b/packages/opencode/src/cli/cmd/stats.ts
@@ -1,7 +1,4 @@
-import { Storage } from "../../storage/storage"
-import { MessageV2 } from "../../session/message-v2"
import { cmd } from "./cmd"
-import { bootstrap } from "../bootstrap"
interface SessionStats {
totalSessions: number
@@ -27,87 +24,10 @@ interface SessionStats {
export const StatsCommand = cmd({
command: "stats",
- handler: async () => {
- await bootstrap({ cwd: process.cwd() }, async () => {
- const stats: SessionStats = {
- totalSessions: 0,
- totalMessages: 0,
- totalCost: 0,
- totalTokens: {
- input: 0,
- output: 0,
- reasoning: 0,
- cache: {
- read: 0,
- write: 0,
- },
- },
- toolUsage: {},
- dateRange: {
- earliest: Date.now(),
- latest: 0,
- },
- days: 0,
- costPerDay: 0,
- }
-
- const sessionMap = new Map<string, number>()
-
- try {
- for await (const messagePath of Storage.list("session/message")) {
- try {
- const message = await Storage.readJSON<MessageV2.Info>(messagePath)
- if (!message.parts.find((part) => part.type === "step-finish")) continue
-
- stats.totalMessages++
-
- const sessionId = message.sessionID
- sessionMap.set(sessionId, (sessionMap.get(sessionId) || 0) + 1)
-
- if (message.time.created < stats.dateRange.earliest) {
- stats.dateRange.earliest = message.time.created
- }
- if (message.time.created > stats.dateRange.latest) {
- stats.dateRange.latest = message.time.created
- }
-
- if (message.role === "assistant") {
- stats.totalCost += message.cost
- stats.totalTokens.input += message.tokens.input
- stats.totalTokens.output += message.tokens.output
- stats.totalTokens.reasoning += message.tokens.reasoning
- stats.totalTokens.cache.read += message.tokens.cache.read
- stats.totalTokens.cache.write += message.tokens.cache.write
-
- for (const part of message.parts) {
- if (part.type === "tool") {
- stats.toolUsage[part.tool] = (stats.toolUsage[part.tool] || 0) + 1
- }
- }
- }
- } catch (e) {
- continue
- }
- }
- } catch (e) {
- console.error("Failed to read storage:", e)
- return
- }
-
- stats.totalSessions = sessionMap.size
-
- if (stats.dateRange.latest > 0) {
- const daysDiff = (stats.dateRange.latest - stats.dateRange.earliest) / (1000 * 60 * 60 * 24)
- stats.days = Math.max(1, Math.ceil(daysDiff))
- stats.costPerDay = stats.totalCost / stats.days
- }
-
- displayStats(stats)
- })
- },
+ handler: async () => {},
})
-function displayStats(stats: SessionStats) {
+export function displayStats(stats: SessionStats) {
const width = 56
function renderRow(label: string, value: string): string {
diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts
index 9801b459e..1b6c3cace 100644
--- a/packages/opencode/src/cli/ui.ts
+++ b/packages/opencode/src/cli/ui.ts
@@ -1,6 +1,8 @@
import { z } from "zod"
import { EOL } from "os"
import { NamedError } from "../util/error"
+// @ts-ignore
+import cliMarkdown from "cli-markdown"
export namespace UI {
const LOGO = [
@@ -76,4 +78,18 @@ export namespace UI {
export function error(message: string) {
println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message)
}
+
+ export function markdown(text: string): string {
+ const rendered = cliMarkdown(text, {
+ width: process.stdout.columns || 80,
+ firstHeading: false,
+ tab: 0,
+ }).trim()
+
+ // Remove leading space from each line
+ return rendered
+ .split("\n")
+ .map((line: string) => line.replace(/^ /, ""))
+ .join("\n")
+ }
}
diff --git a/packages/opencode/src/id/id.ts b/packages/opencode/src/id/id.ts
index b705ff2ce..6c1edd50d 100644
--- a/packages/opencode/src/id/id.ts
+++ b/packages/opencode/src/id/id.ts
@@ -6,6 +6,7 @@ export namespace Identifier {
session: "ses",
message: "msg",
user: "usr",
+ part: "prt",
} as const
export function schema(prefix: keyof typeof prefixes) {
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index a958f48b6..6469b9bbc 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -269,6 +269,7 @@ export namespace Server {
zValidator(
"json",
z.object({
+ messageID: z.string(),
providerID: z.string(),
modelID: z.string(),
}),
@@ -405,7 +406,14 @@ export namespace Server {
description: "List of messages",
content: {
"application/json": {
- schema: resolver(MessageV2.Info.array()),
+ schema: resolver(
+ z
+ .object({
+ info: MessageV2.Info,
+ parts: MessageV2.Part.array(),
+ })
+ .array(),
+ ),
},
},
},
@@ -446,10 +454,11 @@ export namespace Server {
zValidator(
"json",
z.object({
+ messageID: z.string(),
providerID: z.string(),
modelID: z.string(),
mode: z.string(),
- parts: MessageV2.UserPart.array(),
+ parts: z.union([MessageV2.FilePart, MessageV2.TextPart]).array(),
}),
),
async (c) => {
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index 16b9de9d3..44879e9b8 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -12,6 +12,7 @@ import {
type ProviderMetadata,
type ModelMessage,
stepCountIs,
+ type StreamTextResult,
} from "ai"
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
@@ -190,7 +191,10 @@ export namespace Session {
await Storage.writeJSON<ShareInfo>("session/share/" + id, share)
await Share.sync("session/info/" + id, session)
for (const msg of await messages(id)) {
- await Share.sync("session/message/" + id + "/" + msg.id, msg)
+ await Share.sync("session/message/" + id + "/" + msg.info.id, msg.info)
+ for (const part of msg.parts) {
+ await Share.sync("session/part/" + id + "/" + msg.info.id + "/" + part.id, part)
+ }
}
return share
}
@@ -220,13 +224,19 @@ export namespace Session {
}
export async function messages(sessionID: string) {
- const result = [] as MessageV2.Info[]
+ const result = [] as {
+ info: MessageV2.Info
+ parts: MessageV2.Part[]
+ }[]
const list = Storage.list("session/message/" + sessionID)
for await (const p of list) {
const read = await Storage.readJSON<MessageV2.Info>(p)
- result.push(read)
+ result.push({
+ info: read,
+ parts: await parts(sessionID, read.id),
+ })
}
- result.sort((a, b) => (a.id > b.id ? 1 : -1))
+ result.sort((a, b) => (a.info.id > b.info.id ? 1 : -1))
return result
}
@@ -234,6 +244,16 @@ export namespace Session {
return Storage.readJSON<MessageV2.Info>("session/message/" + sessionID + "/" + messageID)
}
+ export async function parts(sessionID: string, messageID: string) {
+ const result = [] as MessageV2.Part[]
+ for await (const item of Storage.list("session/part/" + sessionID + "/" + messageID)) {
+ const read = await Storage.readJSON<MessageV2.Part>(item)
+ result.push(read)
+ }
+ result.sort((a, b) => (a.id > b.id ? 1 : -1))
+ return result
+ }
+
export async function* list() {
for await (const item of Storage.list("session/info")) {
const sessionID = path.basename(item, ".json")
@@ -289,12 +309,21 @@ export namespace Session {
})
}
+ async function updatePart(part: MessageV2.Part) {
+ await Storage.writeJSON(["session", "part", part.sessionID, part.messageID, part.id].join("/"), part)
+ Bus.publish(MessageV2.Event.PartUpdated, {
+ part,
+ })
+ return part
+ }
+
export async function chat(input: {
sessionID: string
+ messageID: string
providerID: string
modelID: string
mode?: string
- parts: MessageV2.UserPart[]
+ parts: (MessageV2.TextPart | MessageV2.FilePart)[]
}) {
const l = log.clone().tag("session", input.sessionID)
l.info("chatting")
@@ -306,16 +335,19 @@ export namespace Session {
if (session.revert) {
const trimmed = []
for (const msg of msgs) {
- if (msg.id > session.revert.messageID || (msg.id === session.revert.messageID && session.revert.part === 0)) {
- await Storage.remove("session/message/" + input.sessionID + "/" + msg.id)
+ if (
+ msg.info.id > session.revert.messageID ||
+ (msg.info.id === session.revert.messageID && session.revert.part === 0)
+ ) {
+ await Storage.remove("session/message/" + input.sessionID + "/" + msg.info.id)
await Bus.publish(MessageV2.Event.Removed, {
sessionID: input.sessionID,
- messageID: msg.id,
+ messageID: msg.info.id,
})
continue
}
- if (msg.id === session.revert.messageID) {
+ if (msg.info.id === session.revert.messageID) {
if (session.revert.part === 0) break
msg.parts = msg.parts.slice(0, session.revert.part)
}
@@ -327,7 +359,7 @@ export namespace Session {
})
}
- const previous = msgs.at(-1) as MessageV2.Assistant
+ const previous = msgs.filter((x) => x.info.role === "assistant").at(-1)?.info as MessageV2.Assistant
const outputLimit = Math.min(model.info.limit.output, OUTPUT_TOKEN_MAX) || OUTPUT_TOKEN_MAX
// auto summarize if too long
@@ -346,12 +378,21 @@ export namespace Session {
using abort = lock(input.sessionID)
- const lastSummary = msgs.findLast((msg) => msg.role === "assistant" && msg.summary === true)
- if (lastSummary) msgs = msgs.filter((msg) => msg.id >= lastSummary.id)
+ const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
+ if (lastSummary) msgs = msgs.filter((msg) => msg.info.id >= lastSummary.info.id)
+
+ const userMsg: MessageV2.Info = {
+ id: input.messageID,
+ role: "user",
+ sessionID: input.sessionID,
+ time: {
+ created: Date.now(),
+ },
+ }
const app = App.info()
- input.parts = await Promise.all(
- input.parts.map(async (part): Promise<MessageV2.UserPart[]> => {
+ const userParts = await Promise.all(
+ input.parts.map(async (part): Promise<MessageV2.Part[]> => {
if (part.type === "file") {
const url = new URL(part.url)
switch (url.protocol) {
@@ -406,11 +447,17 @@ export namespace Session {
})
return [
{
+ id: Identifier.ascending("part"),
+ messageID: userMsg.id,
+ sessionID: input.sessionID,
type: "text",
synthetic: true,
text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
},
{
+ id: Identifier.ascending("part"),
+ messageID: userMsg.id,
+ sessionID: input.sessionID,
type: "text",
synthetic: true,
text: result.output,
@@ -422,11 +469,17 @@ export namespace Session {
FileTime.read(input.sessionID, filePath)
return [
{
+ id: Identifier.ascending("part"),
+ messageID: userMsg.id,
+ sessionID: input.sessionID,
type: "text",
text: `Called the Read tool with the following input: {\"filePath\":\"${pathname}\"}`,
synthetic: true,
},
{
+ id: Identifier.ascending("part"),
+ messageID: userMsg.id,
+ sessionID: input.sessionID,
type: "file",
url: `data:${part.mime};base64,` + Buffer.from(await file.bytes()).toString("base64"),
mime: part.mime,
@@ -440,7 +493,10 @@ export namespace Session {
).then((x) => x.flat())
if (input.mode === "plan")
- input.parts.push({
+ userParts.push({
+ id: Identifier.ascending("part"),
+ messageID: userMsg.id,
+ sessionID: input.sessionID,
type: "text",
text: PROMPT_PLAN,
synthetic: true,
@@ -459,13 +515,15 @@ export namespace Session {
),
...MessageV2.toModelMessage([
{
- id: Identifier.ascending("message"),
- role: "user",
- sessionID: input.sessionID,
- parts: input.parts,
- time: {
- created: Date.now(),
+ info: {
+ id: Identifier.ascending("message"),
+ role: "user",
+ sessionID: input.sessionID,
+ time: {
+ created: Date.now(),
+ },
},
+ parts: userParts,
},
]),
],
@@ -479,17 +537,11 @@ export namespace Session {
})
.catch(() => {})
}
- const msg: MessageV2.Info = {
- id: Identifier.ascending("message"),
- role: "user",
- sessionID: input.sessionID,
- parts: input.parts,
- time: {
- created: Date.now(),
- },
+ await updateMessage(userMsg)
+ for (const part of userParts) {
+ await updatePart(part)
}
- await updateMessage(msg)
- msgs.push(msg)
+ msgs.push({ info: userMsg, parts: userParts })
const mode = await Mode.get(input.mode ?? "build")
let system = mode.prompt ? [mode.prompt] : SystemPrompt.provider(input.providerID, input.modelID)
@@ -499,10 +551,9 @@ export namespace Session {
const [first, ...rest] = system
system = [first, rest.join("\n")]
- const next: MessageV2.Info = {
+ const assistantMsg: MessageV2.Info = {
id: Identifier.ascending("message"),
role: "assistant",
- parts: [],
system,
path: {
cwd: app.path.cwd,
@@ -522,7 +573,7 @@ export namespace Session {
},
sessionID: input.sessionID,
}
- await updateMessage(next)
+ await updateMessage(assistantMsg)
const tools: Record<string, AITool> = {}
for (const item of await Provider.tools(input.providerID)) {
@@ -531,20 +582,29 @@ export namespace Session {
id: item.id as any,
description: item.description,
inputSchema: item.parameters as ZodSchema,
- async execute(args, opts) {
+ async execute(args) {
const result = await item.execute(args, {
sessionID: input.sessionID,
abort: abort.signal,
- messageID: next.id,
- metadata: async (val) => {
- const match = next.parts.find(
- (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === opts.toolCallId,
- )
+ messageID: assistantMsg.id,
+ metadata: async () => {
+ /*
+ const match = toolCalls[opts.toolCallId]
if (match && match.state.status === "running") {
- match.state.title = val.title
- match.state.metadata = val.metadata
+ await updatePart({
+ ...match,
+ state: {
+ title: val.title,
+ metadata: val.metadata,
+ status: "running",
+ input: args.input,
+ time: {
+ start: Date.now(),
+ },
+ },
+ })
}
- await updateMessage(next)
+ */
},
})
return result
@@ -582,10 +642,6 @@ export namespace Session {
tools[key] = item
}
- let text: MessageV2.TextPart = {
- type: "text",
- text: "",
- }
const result = streamText({
onError() {},
maxRetries: 10,
@@ -619,9 +675,20 @@ export namespace Session {
],
}),
})
+ return processStream(assistantMsg, model.info, result)
+ }
+
+ async function processStream(
+ assistantMsg: MessageV2.Assistant,
+ model: ModelsDev.Model,
+ stream: StreamTextResult<Record<string, AITool>, never>,
+ ) {
try {
- for await (const value of result.fullStream) {
- l.info("part", {
+ let currentText: MessageV2.TextPart | undefined
+ const toolCalls: Record<string, MessageV2.ToolPart> = {}
+
+ for await (const value of stream.fullStream) {
+ log.info("part", {
type: value.type,
})
switch (value.type) {
@@ -629,88 +696,78 @@ export namespace Session {
break
case "tool-input-start":
- next.parts.push({
+ const part = await updatePart({
+ id: Identifier.ascending("part"),
+ messageID: assistantMsg.id,
+ sessionID: assistantMsg.sessionID,
type: "tool",
tool: value.toolName,
- id: value.id,
+ callID: value.id,
state: {
status: "pending",
},
})
- Bus.publish(MessageV2.Event.PartUpdated, {
- part: next.parts[next.parts.length - 1],
- sessionID: next.sessionID,
- messageID: next.id,
- })
+ toolCalls[value.id] = part as MessageV2.ToolPart
break
case "tool-input-delta":
break
case "tool-call": {
- const match = next.parts.find(
- (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId,
- )
+ const match = toolCalls[value.toolCallId]
if (match) {
- match.state = {
- status: "running",
- input: value.input,
- time: {
- start: Date.now(),
+ const part = await updatePart({
+ ...match,
+ state: {
+ status: "running",
+ input: value.input,
+ time: {
+ start: Date.now(),
+ },
},
- }
- Bus.publish(MessageV2.Event.PartUpdated, {
- part: match,
- sessionID: next.sessionID,
- messageID: next.id,
})
+ toolCalls[value.toolCallId] = part as MessageV2.ToolPart
}
break
}
case "tool-result": {
- const match = next.parts.find(
- (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId,
- )
+ const match = toolCalls[value.toolCallId]
if (match && match.state.status === "running") {
- match.state = {
- status: "completed",
- input: value.input,
- output: value.output.output,
- metadata: value.output.metadata,
- title: value.output.title,
- time: {
- start: match.state.time.start,
- end: Date.now(),
+ await updatePart({
+ ...match,
+ state: {
+ status: "completed",
+ input: value.input,
+ output: value.output.output,
+ metadata: value.output.metadata,
+ title: value.output.title,
+ time: {
+ start: match.state.time.start,
+ end: Date.now(),
+ },
},
- }
- Bus.publish(MessageV2.Event.PartUpdated, {
- part: match,
- sessionID: next.sessionID,
- messageID: next.id,
})
+ delete toolCalls[value.toolCallId]
}
break
}
case "tool-error": {
- const match = next.parts.find(
- (p): p is MessageV2.ToolPart => p.type === "tool" && p.id === value.toolCallId,
- )
+ const match = toolCalls[value.toolCallId]
if (match && match.state.status === "running") {
- match.state = {
- status: "error",
- input: value.input,
- error: (value.error as any).toString(),
- time: {
- start: match.state.time.start,
- end: Date.now(),
+ await updatePart({
+ ...match,
+ state: {
+ status: "error",
+ input: value.input,
+ error: (value.error as any).toString(),
+ time: {
+ start: match.state.time.start,
+ end: Date.now(),
+ },
},
- }
- Bus.publish(MessageV2.Event.PartUpdated, {
- part: match,
- sessionID: next.sessionID,
- messageID: next.id,
})
+ delete toolCalls[value.toolCallId]
}
break
}
@@ -719,53 +776,71 @@ export namespace Session {
throw value.error
case "start-step":
- next.parts.push({
+ await updatePart({
+ id: Identifier.ascending("part"),
+ messageID: assistantMsg.id,
+ sessionID: assistantMsg.sessionID,
type: "step-start",
})
break
case "finish-step":
- const usage = getUsage(model.info, value.usage, value.providerMetadata)
- next.cost += usage.cost
- next.tokens = usage.tokens
- next.parts.push({
+ const usage = getUsage(model, value.usage, value.providerMetadata)
+ assistantMsg.cost += usage.cost
+ assistantMsg.tokens = usage.tokens
+ await updatePart({
+ id: Identifier.ascending("part"),
+ messageID: assistantMsg.id,
+ sessionID: assistantMsg.sessionID,
type: "step-finish",
tokens: usage.tokens,
cost: usage.cost,
})
+ await updateMessage(assistantMsg)
break
case "text-start":
- text = {
+ currentText = {
+ id: Identifier.ascending("part"),
+ messageID: assistantMsg.id,
+ sessionID: assistantMsg.sessionID,
type: "text",
text: "",
+ time: {
+ start: Date.now(),
+ },
}
break
case "text":
- if (text.text === "") next.parts.push(text)
- text.text += value.text
+ if (currentText) {
+ currentText.text += value.text
+ await updatePart(currentText)
+ }
break
case "text-end":
- Bus.publish(MessageV2.Event.PartUpdated, {
- part: text,
- sessionID: next.sessionID,
- messageID: next.id,
- })
+ if (currentText && currentText.text) {
+ currentText.time = {
+ start: Date.now(),
+ end: Date.now(),
+ }
+ await updatePart(currentText)
+ }
+ currentText = undefined
break
case "finish":
- next.time.completed = Date.now()
+ assistantMsg.time.completed = Date.now()
+ await updateMessage(assistantMsg)
break
default:
- l.info("unhandled", {
+ log.info("unhandled", {
...value,
})
continue
}
- await updateMessage(next)
}
} catch (e) {
log.error("", {
@@ -773,7 +848,7 @@ export namespace Session {
})
switch (true) {
case e instanceof DOMException && e.name === "AbortError":
- next.error = new MessageV2.AbortedError(
+ assistantMsg.error = new MessageV2.AbortedError(
{ message: e.message },
{
cause: e,
@@ -781,44 +856,48 @@ export namespace Session {
).toObject()
break
case MessageV2.OutputLengthError.isInstance(e):
- next.error = e
+ assistantMsg.error = e
break
case LoadAPIKeyError.isInstance(e):
- next.error = new Provider.AuthError(
+ assistantMsg.error = new Provider.AuthError(
{
- providerID: input.providerID,
+ providerID: model.id,
message: e.message,
},
{ cause: e },
).toObject()
break
case e instanceof Error:
- next.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
+ assistantMsg.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
break
default:
- next.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
+ assistantMsg.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e })
}
Bus.publish(Event.Error, {
- sessionID: next.sessionID,
- error: next.error,
+ sessionID: assistantMsg.sessionID,
+ error: assistantMsg.error,
})
}
- for (const part of next.parts) {
+ const p = await parts(assistantMsg.sessionID, assistantMsg.id)
+ for (const part of p) {
if (part.type === "tool" && part.state.status !== "completed") {
- part.state = {
- status: "error",
- error: "Tool execution aborted",
- time: {
- start: Date.now(),
- end: Date.now(),
+ updatePart({
+ ...part,
+ state: {
+ status: "error",
+ error: "Tool execution aborted",
+ time: {
+ start: Date.now(),
+ end: Date.now(),
+ },
+ input: {},
},
- input: {},
- }
+ })
}
}
- next.time.completed = Date.now()
- await updateMessage(next)
- return next
+ assistantMsg.time.completed = Date.now()
+ await updateMessage(assistantMsg)
+ return { info: assistantMsg, parts: p }
}
export async function revert(_input: { sessionID: string; messageID: string; part: number }) {
@@ -867,8 +946,8 @@ export namespace Session {
export async function summarize(input: { sessionID: string; providerID: string; modelID: string }) {
using abort = lock(input.sessionID)
const msgs = await messages(input.sessionID)
- const lastSummary = msgs.findLast((msg) => msg.role === "assistant" && msg.summary === true)?.id
- const filtered = msgs.filter((msg) => !lastSummary || msg.id >= lastSummary)
+ const lastSummary = msgs.findLast((msg) => msg.info.role === "assistant" && msg.info.summary === true)
+ const filtered = msgs.filter((msg) => !lastSummary || msg.info.id >= lastSummary.info.id)
const model = await Provider.getModel(input.providerID, input.modelID)
const app = App.info()
const system = SystemPrompt.summarize(input.providerID)
@@ -876,7 +955,6 @@ export namespace Session {
const next: MessageV2.Info = {
id: Identifier.ascending("message"),
role: "assistant",
- parts: [],
sessionID: input.sessionID,
system,
path: {
@@ -899,7 +977,6 @@ export namespace Session {
}
await updateMessage(next)
- let text: MessageV2.TextPart | undefined
const result = streamText({
abortSignal: abort.signal,
model: model.language,
@@ -921,81 +998,9 @@ export namespace Session {
],
},
],
- onStepFinish: async (step) => {
- const usage = getUsage(model.info, step.usage, step.providerMetadata)
- next.cost += usage.cost
- next.tokens = usage.tokens
- await updateMessage(next)
- if (text) {
- Bus.publish(MessageV2.Event.PartUpdated, {
- part: text,
- messageID: next.id,
- sessionID: next.sessionID,
- })
- }
- text = undefined
- },
- async onFinish(input) {
- const usage = getUsage(model.info, input.usage, input.providerMetadata)
- next.cost += usage.cost
- next.tokens = usage.tokens
- next.time.completed = Date.now()
- await updateMessage(next)
- },
})
- try {
- for await (const value of result.fullStream) {
- switch (value.type) {
- case "text":
- if (!text) {
- text = {
- type: "text",
- text: value.text,
- }
- next.parts.push(text)
- } else text.text += value.text
- await updateMessage(next)
- break
- }
- }
- } catch (e: any) {
- log.error("summarize stream error", {
- error: e,
- })
- switch (true) {
- case e instanceof DOMException && e.name === "AbortError":
- next.error = new MessageV2.AbortedError(
- { message: e.message },
- {
- cause: e,
- },
- ).toObject()
- break
- case MessageV2.OutputLengthError.isInstance(e):
- next.error = e
- break
- case LoadAPIKeyError.isInstance(e):
- next.error = new Provider.AuthError(
- {
- providerID: input.providerID,
- message: e.message,
- },
- { cause: e },
- ).toObject()
- break
- case e instanceof Error:
- next.error = new NamedError.Unknown({ message: e.toString() }, { cause: e }).toObject()
- break
- default:
- next.error = new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject()
- }
- Bus.publish(Event.Error, {
- error: next.error,
- })
- }
- next.time.completed = Date.now()
- await updateMessage(next)
+ return processStream(next, model.info, result)
}
function lock(sessionID: string) {
@@ -1045,14 +1050,23 @@ export namespace Session {
}
}
- export async function initialize(input: { sessionID: string; modelID: string; providerID: string }) {
+ export async function initialize(input: {
+ sessionID: string
+ modelID: string
+ providerID: string
+ messageID: string
+ }) {
const app = App.info()
await Session.chat({
sessionID: input.sessionID,
+ messageID: input.messageID,
providerID: input.providerID,
modelID: input.modelID,
parts: [
{
+ id: Identifier.ascending("part"),
+ sessionID: input.sessionID,
+ messageID: input.messageID,
type: "text",
text: PROMPT_INITIALIZE.replace("${path}", app.path.root),
},
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index aadc1a5e2..353fdcec3 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -4,6 +4,7 @@ import { Provider } from "../provider/provider"
import { NamedError } from "../util/error"
import { Message } from "./message"
import { convertToModelMessages, type ModelMessage, type UIMessage } from "ai"
+import { Identifier } from "../id/id"
export namespace MessageV2 {
export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({}))
@@ -72,67 +73,69 @@ export namespace MessageV2 {
ref: "ToolState",
})
- export const TextPart = z
- .object({
- type: z.literal("text"),
- text: z.string(),
- synthetic: z.boolean().optional(),
- })
- .openapi({
- ref: "TextPart",
- })
+ const PartBase = z.object({
+ id: z.string(),
+ sessionID: z.string(),
+ messageID: z.string(),
+ })
+
+ export const TextPart = PartBase.extend({
+ type: z.literal("text"),
+ text: z.string(),
+ synthetic: z.boolean().optional(),
+ time: z
+ .object({
+ start: z.number(),
+ end: z.number().optional(),
+ })
+ .optional(),
+ }).openapi({
+ ref: "TextPart",
+ })
export type TextPart = z.infer<typeof TextPart>
- export const ToolPart = z
- .object({
- type: z.literal("tool"),
- id: z.string(),
- tool: z.string(),
- state: ToolState,
- })
- .openapi({
- ref: "ToolPart",
- })
+ export const ToolPart = PartBase.extend({
+ type: z.literal("tool"),
+ callID: z.string(),
+ tool: z.string(),
+ state: ToolState,
+ }).openapi({
+ ref: "ToolPart",
+ })
export type ToolPart = z.infer<typeof ToolPart>
- export const FilePart = z
- .object({
- type: z.literal("file"),
- mime: z.string(),
- filename: z.string().optional(),
- url: z.string(),
- })
- .openapi({
- ref: "FilePart",
- })
+ export const FilePart = PartBase.extend({
+ type: z.literal("file"),
+ mime: z.string(),
+ filename: z.string().optional(),
+ url: z.string(),
+ }).openapi({
+ ref: "FilePart",
+ })
export type FilePart = z.infer<typeof FilePart>
- export const StepStartPart = z
- .object({
- type: z.literal("step-start"),
- })
- .openapi({
- ref: "StepStartPart",
- })
+ export const StepStartPart = PartBase.extend({
+ type: z.literal("step-start"),
+ }).openapi({
+ ref: "StepStartPart",
+ })
export type StepStartPart = z.infer<typeof StepStartPart>
- export const StepFinishPart = z
- .object({
- type: z.literal("step-finish"),
- cost: z.number(),
- tokens: z.object({
- input: z.number(),
- output: z.number(),
- reasoning: z.number(),
- cache: z.object({
- read: z.number(),
- write: z.number(),
- }),
+ export const StepFinishPart = PartBase.extend({
+ type: z.literal("step-finish"),
+ cost: z.number(),
+ tokens: z.object({
+ input: z.number(),
+ output: z.number(),
+ reasoning: z.number(),
+ cache: z.object({
+ read: z.number(),
+ write: z.number(),
}),
- })
- .openapi({
- ref: "StepFinishPart",
- })
+ }),
+ }).openapi({
+ ref: "StepFinishPart",
+ })
export type StepFinishPart = z.infer<typeof StepFinishPart>
const Base = z.object({
@@ -140,14 +143,8 @@ export namespace MessageV2 {
sessionID: z.string(),
})
- export const UserPart = z.discriminatedUnion("type", [TextPart, FilePart]).openapi({
- ref: "UserMessagePart",
- })
- export type UserPart = z.infer<typeof UserPart>
-
export const User = Base.extend({
role: z.literal("user"),
- parts: z.array(UserPart),
time: z.object({
created: z.number(),
}),
@@ -156,16 +153,15 @@ export namespace MessageV2 {
})
export type User = z.infer<typeof User>
- export const AssistantPart = z
- .discriminatedUnion("type", [TextPart, ToolPart, StepStartPart, StepFinishPart])
+ export const Part = z
+ .discriminatedUnion("type", [TextPart, FilePart, ToolPart, StepStartPart, StepFinishPart])
.openapi({
- ref: "AssistantMessagePart",
+ ref: "Part",
})
- export type AssistantPart = z.infer<typeof AssistantPart>
+ export type Part = z.infer<typeof Part>
export const Assistant = Base.extend({
role: z.literal("assistant"),
- parts: z.array(AssistantPart),
time: z.object({
created: z.number(),
completed: z.number().optional(),
@@ -223,16 +219,14 @@ export namespace MessageV2 {
PartUpdated: Bus.event(
"message.part.updated",
z.object({
- part: AssistantPart,
- sessionID: z.string(),
- messageID: z.string(),
+ part: Part,
}),
),
}
export function fromV1(v1: Message.Info) {
if (v1.role === "assistant") {
- const result: Assistant = {
+ const info: Assistant = {
id: v1.id,
sessionID: v1.metadata.sessionID,
role: "assistant",
@@ -248,109 +242,135 @@ export namespace MessageV2 {
providerID: v1.metadata.assistant!.providerID,
system: v1.metadata.assistant!.system,
error: v1.metadata.error,
- parts: v1.parts.flatMap((part): AssistantPart[] => {
- if (part.type === "text") {
- return [
- {
- type: "text",
- text: part.text,
- },
- ]
- }
- if (part.type === "step-start") {
- return [
- {
- type: "step-start",
- },
- ]
- }
- if (part.type === "tool-invocation") {
- return [
- {
- type: "tool",
- id: part.toolInvocation.toolCallId,
- tool: part.toolInvocation.toolName,
- state: (() => {
- if (part.toolInvocation.state === "partial-call") {
- return {
- status: "pending",
- }
+ }
+ const parts = v1.parts.flatMap((part): Part[] => {
+ const base = {
+ id: Identifier.ascending("part"),
+ messageID: v1.id,
+ sessionID: v1.metadata.sessionID,
+ }
+ if (part.type === "text") {
+ return [
+ {
+ ...base,
+ type: "text",
+ text: part.text,
+ },
+ ]
+ }
+ if (part.type === "step-start") {
+ return [
+ {
+ ...base,
+ type: "step-start",
+ },
+ ]
+ }
+ if (part.type === "tool-invocation") {
+ return [
+ {
+ ...base,
+ type: "tool",
+ callID: part.toolInvocation.toolCallId,
+ tool: part.toolInvocation.toolName,
+ state: (() => {
+ if (part.toolInvocation.state === "partial-call") {
+ return {
+ status: "pending",
}
+ }
- const { title, time, ...metadata } = v1.metadata.tool[part.toolInvocation.toolCallId] ?? {}
- if (part.toolInvocation.state === "call") {
- return {
- status: "running",
- input: part.toolInvocation.args,
- time: {
- start: time?.start,
- },
- }
+ const { title, time, ...metadata } = v1.metadata.tool[part.toolInvocation.toolCallId] ?? {}
+ if (part.toolInvocation.state === "call") {
+ return {
+ status: "running",
+ input: part.toolInvocation.args,
+ time: {
+ start: time?.start,
+ },
}
+ }
- if (part.toolInvocation.state === "result") {
- return {
- status: "completed",
- input: part.toolInvocation.args,
- output: part.toolInvocation.result,
- title,
- time,
- metadata,
- }
+ if (part.toolInvocation.state === "result") {
+ return {
+ status: "completed",
+ input: part.toolInvocation.args,
+ output: part.toolInvocation.result,
+ title,
+ time,
+ metadata,
}
- throw new Error("unknown tool invocation state")
- })(),
- },
- ]
- }
- return []
- }),
+ }
+ throw new Error("unknown tool invocation state")
+ })(),
+ },
+ ]
+ }
+ return []
+ })
+ return {
+ info,
+ parts,
}
- return result
}
if (v1.role === "user") {
- const result: User = {
+ const info: User = {
id: v1.id,
sessionID: v1.metadata.sessionID,
role: "user",
time: {
created: v1.metadata.time.created,
},
- parts: v1.parts.flatMap((part): UserPart[] => {
- if (part.type === "text") {
- return [
- {
- type: "text",
- text: part.text,
- },
- ]
- }
- if (part.type === "file") {
- return [
- {
- type: "file",
- mime: part.mediaType,
- filename: part.filename,
- url: part.url,
- },
- ]
- }
- return []
- }),
}
- return result
+ const parts = v1.parts.flatMap((part): Part[] => {
+ const base = {
+ id: Identifier.ascending("part"),
+ messageID: v1.id,
+ sessionID: v1.metadata.sessionID,
+ }
+ if (part.type === "text") {
+ return [
+ {
+ ...base,
+ type: "text",
+ text: part.text,
+ },
+ ]
+ }
+ if (part.type === "file") {
+ return [
+ {
+ ...base,
+ type: "file",
+ mime: part.mediaType,
+ filename: part.filename,
+ url: part.url,
+ },
+ ]
+ }
+ return []
+ })
+ return { info, parts }
}
+
+ throw new Error("unknown message type")
}
- export function toModelMessage(input: Info[]): ModelMessage[] {
+ export function toModelMessage(
+ input: {
+ info: Info
+ parts: Part[]
+ }[],
+ ): ModelMessage[] {
const result: UIMessage[] = []
for (const msg of input) {
if (msg.parts.length === 0) continue
- if (msg.role === "user") {
+
+ if (msg.info.role === "user") {
result.push({
- id: msg.id,
+ id: msg.info.id,
role: "user",
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
if (part.type === "text")
@@ -374,9 +394,9 @@ export namespace MessageV2 {
})
}
- if (msg.role === "assistant") {
+ if (msg.info.role === "assistant") {
result.push({
- id: msg.id,
+ id: msg.info.id,
role: "assistant",
parts: msg.parts.flatMap((part): UIMessage["parts"] => {
if (part.type === "text")
@@ -398,7 +418,7 @@ export namespace MessageV2 {
{
type: ("tool-" + part.tool) as `tool-${string}`,
state: "output-available",
- toolCallId: part.id,
+ toolCallId: part.callID,
input: part.state.input,
output: part.state.output,
},
@@ -408,7 +428,7 @@ export namespace MessageV2 {
{
type: ("tool-" + part.tool) as `tool-${string}`,
state: "output-error",
- toolCallId: part.id,
+ toolCallId: part.callID,
input: part.state.input,
errorText: part.state.error,
},
diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts
index 001eee0a1..22876ee40 100644
--- a/packages/opencode/src/storage/storage.ts
+++ b/packages/opencode/src/storage/storage.ts
@@ -5,6 +5,7 @@ import path from "path"
import z from "zod"
import fs from "fs/promises"
import { MessageV2 } from "../session/message-v2"
+import { Identifier } from "../id/id"
export namespace Storage {
const log = Log.create({ service: "storage" })
@@ -28,13 +29,49 @@ export namespace Storage {
log.info("migrating to v2 message", { file })
try {
const result = MessageV2.fromV1(content)
- await Bun.write(file, JSON.stringify(result, null, 2))
+ await Bun.write(
+ file,
+ JSON.stringify(
+ {
+ ...result.info,
+ parts: result.parts,
+ },
+ null,
+ 2,
+ ),
+ )
} catch (e) {
await fs.rename(file, file.replace("storage", "broken"))
}
}
} catch {}
},
+ async (dir: string) => {
+ const files = new Bun.Glob("session/message/*/*.json").scanSync({
+ cwd: dir,
+ absolute: true,
+ })
+ for (const file of files) {
+ try {
+ const { parts, ...info } = await Bun.file(file).json()
+ if (!parts) continue
+ for (const part of parts) {
+ const id = Identifier.ascending("part")
+ await Bun.write(
+ [dir, "session", "part", info.sessionID, info.id, id + ".json"].join("/"),
+ JSON.stringify({
+ ...part,
+ id,
+ sessionID: info.sessionID,
+ messageID: info.id,
+ ...(part.type === "tool" ? { callID: part.id } : {}),
+ }),
+ )
+ }
+ await Bun.write(file, JSON.stringify(info, null, 2))
+ } catch (e) {}
+ }
+ },
]
const state = App.state("storage", async () => {
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index 757398337..0d7808a3a 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -4,6 +4,7 @@ import { z } from "zod"
import { Session } from "../session"
import { Bus } from "../bus"
import { MessageV2 } from "../session/message-v2"
+import { Identifier } from "../id/id"
export const TaskTool = Tool.define({
id: "task",
@@ -16,9 +17,10 @@ export const TaskTool = Tool.define({
const session = await Session.create(ctx.sessionID)
const msg = (await Session.getMessage(ctx.sessionID, ctx.messageID)) as MessageV2.Assistant
- function summary(input: MessageV2.Info) {
+ const parts: Record<string, MessageV2.Part> = {}
+ function summary(input: MessageV2.Part[]) {
const result = []
- for (const part of input.parts) {
+ for (const part of input) {
if (part.type === "tool" && part.state.status === "completed") {
result.push(part)
}
@@ -26,12 +28,13 @@ export const TaskTool = Tool.define({
return result
}
- const unsub = Bus.subscribe(MessageV2.Event.Updated, async (evt) => {
- if (evt.properties.info.sessionID !== session.id) return
+ const unsub = Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
+ if (evt.properties.part.sessionID !== session.id) return
+ parts[evt.properties.part.id] = evt.properties.part
ctx.metadata({
title: params.description,
metadata: {
- summary: summary(evt.properties.info),
+ summary: Object.values(parts).sort((a, b) => a.id?.localeCompare(b.id)),
},
})
})
@@ -39,12 +42,17 @@ export const TaskTool = Tool.define({
ctx.abort.addEventListener("abort", () => {
Session.abort(session.id)
})
+ const messageID = Identifier.ascending("message")
const result = await Session.chat({
+ messageID,
sessionID: session.id,
modelID: msg.modelID,
providerID: msg.providerID,
parts: [
{
+ id: Identifier.ascending("part"),
+ messageID,
+ sessionID: session.id,
type: "text",
text: params.prompt,
},
@@ -54,7 +62,7 @@ export const TaskTool = Tool.define({
return {
title: params.description,
metadata: {
- summary: summary(result),
+ summary: summary(result.parts),
},
output: result.parts.findLast((x) => x.type === "text")!.text,
}
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index 03eb361b7..fb8358d8c 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -16,11 +16,17 @@ import (
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/toast"
"github.com/sst/opencode/internal/config"
+ "github.com/sst/opencode/internal/id"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
)
+type Message struct {
+ Info opencode.MessageUnion
+ Parts []opencode.PartUnion
+}
+
type App struct {
Info opencode.App
Modes []opencode.Mode
@@ -35,7 +41,7 @@ type App struct {
Provider *opencode.Provider
Model *opencode.Model
Session *opencode.Session
- Messages []opencode.MessageUnion
+ Messages []Message
Commands commands.CommandRegistry
InitialModel *string
InitialPrompt *string
@@ -158,7 +164,7 @@ func New(
ModeIndex: modeIndex,
Mode: mode,
Session: &opencode.Session{},
- Messages: []opencode.MessageUnion{},
+ Messages: []Message{},
Commands: commands.LoadFromConfig(configInfo),
InitialModel: initialModel,
InitialPrompt: initialPrompt,
@@ -351,7 +357,7 @@ func (a *App) IsBusy() bool {
}
lastMessage := a.Messages[len(a.Messages)-1]
- if casted, ok := lastMessage.(opencode.AssistantMessage); ok {
+ if casted, ok := lastMessage.Info.(opencode.AssistantMessage); ok {
return casted.Time.Completed == 0
}
return false
@@ -452,54 +458,67 @@ func (a *App) SendChatMessage(
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
}
- optimisticParts := []opencode.UserMessagePart{{
- Type: opencode.UserMessagePartTypeText,
- Text: text,
+ message := opencode.UserMessage{
+ ID: id.Ascending(id.Message),
+ SessionID: a.Session.ID,
+ Role: opencode.UserMessageRoleUser,
+ Time: opencode.UserMessageTime{
+ Created: float64(time.Now().UnixMilli()),
+ },
+ }
+
+ parts := []opencode.PartUnion{opencode.TextPart{
+ ID: id.Ascending(id.Part),
+ MessageID: message.ID,
+ SessionID: a.Session.ID,
+ Type: opencode.TextPartTypeText,
+ Text: text,
}}
if len(attachments) > 0 {
for _, attachment := range attachments {
- optimisticParts = append(optimisticParts, opencode.UserMessagePart{
- Type: opencode.UserMessagePartTypeFile,
- Filename: attachment.Filename.Value,
- Mime: attachment.Mime.Value,
- URL: attachment.URL.Value,
+ parts = append(parts, opencode.FilePart{
+ ID: id.Ascending(id.Part),
+ MessageID: message.ID,
+ SessionID: a.Session.ID,
+ Type: opencode.FilePartTypeFile,
+ Filename: attachment.Filename.Value,
+ Mime: attachment.Mime.Value,
+ URL: attachment.URL.Value,
})
}
}
- optimisticMessage := opencode.UserMessage{
- ID: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
- Role: opencode.UserMessageRoleUser,
- Parts: optimisticParts,
- SessionID: a.Session.ID,
- Time: opencode.UserMessageTime{
- Created: float64(time.Now().Unix()),
- },
- }
-
- a.Messages = append(a.Messages, optimisticMessage)
- cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
+ a.Messages = append(a.Messages, Message{Info: message, Parts: parts})
+ cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: message}))
cmds = append(cmds, func() tea.Msg {
- parts := []opencode.UserMessagePartUnionParam{
- opencode.TextPartParam{
- Type: opencode.F(opencode.TextPartTypeText),
- Text: opencode.F(text),
- },
- }
- if len(attachments) > 0 {
- for _, attachment := range attachments {
- parts = append(parts, opencode.FilePartParam{
- Mime: attachment.Mime,
- Type: attachment.Type,
- URL: attachment.URL,
- Filename: attachment.Filename,
+ partsParam := []opencode.SessionChatParamsPartUnion{}
+ for _, part := range parts {
+ switch casted := part.(type) {
+ case opencode.TextPart:
+ partsParam = append(partsParam, opencode.TextPartParam{
+ ID: opencode.F(casted.ID),
+ MessageID: opencode.F(casted.MessageID),
+ SessionID: opencode.F(casted.SessionID),
+ Type: opencode.F(casted.Type),
+ Text: opencode.F(casted.Text),
+ })
+ case opencode.FilePart:
+ partsParam = append(partsParam, opencode.FilePartParam{
+ ID: opencode.F(casted.ID),
+ Mime: opencode.F(casted.Mime),
+ MessageID: opencode.F(casted.MessageID),
+ SessionID: opencode.F(casted.SessionID),
+ Type: opencode.F(casted.Type),
+ URL: opencode.F(casted.URL),
+ Filename: opencode.F(casted.Filename),
})
}
}
_, err := a.Client.Session.Chat(ctx, a.Session.ID, opencode.SessionChatParams{
- Parts: opencode.F(parts),
+ Parts: opencode.F(partsParam),
+ MessageID: opencode.F(message.ID),
ProviderID: opencode.F(a.Provider.ID),
ModelID: opencode.F(a.Model.ID),
Mode: opencode.F(a.Mode.Name),
@@ -557,15 +576,25 @@ func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
return nil
}
-func (a *App) ListMessages(ctx context.Context, sessionId string) ([]opencode.Message, error) {
+func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, error) {
response, err := a.Client.Session.Messages(ctx, sessionId)
if err != nil {
return nil, err
}
if response == nil {
- return []opencode.Message{}, nil
+ return []Message{}, nil
+ }
+ messages := []Message{}
+ for _, message := range *response {
+ msg := Message{
+ Info: message.Info.AsUnion(),
+ Parts: []opencode.PartUnion{},
+ }
+ for _, part := range message.Parts {
+ msg.Parts = append(msg.Parts, part.AsUnion())
+ }
+ messages = append(messages, msg)
}
- messages := *response
return messages, nil
}
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index 7ecd9b21f..191432cc3 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -106,6 +106,13 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.GotoBottom()
}
}
+ case opencode.EventListResponseEventMessagePartUpdated:
+ if msg.Properties.Part.SessionID == m.app.Session.ID {
+ m.renderView(m.width)
+ if m.tail {
+ m.viewport.GotoBottom()
+ }
+ }
}
viewport, cmd := m.viewport.Update(msg)
@@ -131,16 +138,16 @@ func (m *messagesComponent) renderView(width int) {
var content string
var cached bool
- switch casted := message.(type) {
+ switch casted := message.Info.(type) {
case opencode.UserMessage:
userLoop:
- for partIndex, part := range casted.Parts {
- switch part := part.AsUnion().(type) {
+ for partIndex, part := range message.Parts {
+ switch part := part.(type) {
case opencode.TextPart:
- remainingParts := casted.Parts[partIndex+1:]
+ remainingParts := message.Parts[partIndex+1:]
fileParts := make([]opencode.FilePart, 0)
for _, part := range remainingParts {
- switch part := part.AsUnion().(type) {
+ switch part := part.(type) {
case opencode.FilePart:
fileParts = append(fileParts, part)
}
@@ -181,7 +188,7 @@ func (m *messagesComponent) renderView(width int) {
if !cached {
content = renderText(
m.app,
- message,
+ message.Info,
part.Text,
m.app.Info.User,
m.showToolDetails,
@@ -202,12 +209,12 @@ func (m *messagesComponent) renderView(width int) {
case opencode.AssistantMessage:
hasTextPart := false
- for partIndex, p := range casted.Parts {
- switch part := p.AsUnion().(type) {
+ for partIndex, p := range message.Parts {
+ switch part := p.(type) {
case opencode.TextPart:
hasTextPart = true
finished := casted.Time.Completed > 0
- remainingParts := casted.Parts[partIndex+1:]
+ remainingParts := message.Parts[partIndex+1:]
toolCallParts := make([]opencode.ToolPart, 0)
// sometimes tool calls happen without an assistant message
@@ -222,7 +229,7 @@ func (m *messagesComponent) renderView(width int) {
if !remaining {
break
}
- switch part := part.AsUnion().(type) {
+ switch part := part.(type) {
case opencode.TextPart:
// we only want tool calls associated with the current text part.
// if we hit another text part, we're done.
@@ -238,13 +245,13 @@ func (m *messagesComponent) renderView(width int) {
}
if finished {
- key := m.cache.GenerateKey(casted.ID, p.Text, width, m.showToolDetails, m.selectedPart == m.partCount)
+ key := m.cache.GenerateKey(casted.ID, part.Text, width, m.showToolDetails, m.selectedPart == m.partCount)
content, cached = m.cache.Get(key)
if !cached {
content = renderText(
m.app,
- message,
- p.Text,
+ message.Info,
+ part.Text,
casted.ModelID,
m.showToolDetails,
m.partCount == m.selectedPart,
@@ -257,8 +264,8 @@ func (m *messagesComponent) renderView(width int) {
} else {
content = renderText(
m.app,
- message,
- p.Text,
+ message.Info,
+ part.Text,
casted.ModelID,
m.showToolDetails,
m.partCount == m.selectedPart,
@@ -268,7 +275,7 @@ func (m *messagesComponent) renderView(width int) {
)
}
if content != "" {
- m = m.updateSelected(content, p.Text)
+ m = m.updateSelected(content, part.Text)
blocks = append(blocks, content)
}
case opencode.ToolPart:
@@ -314,7 +321,7 @@ func (m *messagesComponent) renderView(width int) {
}
error := ""
- if assistant, ok := message.(opencode.AssistantMessage); ok {
+ if assistant, ok := message.Info.(opencode.AssistantMessage); ok {
switch err := assistant.Error.AsUnion().(type) {
case nil:
case opencode.AssistantMessageErrorMessageOutputLengthError:
@@ -386,7 +393,7 @@ func (m *messagesComponent) header(width int) string {
contextWindow := m.app.Model.Limit.Context
for _, message := range m.app.Messages {
- if assistant, ok := message.(opencode.AssistantMessage); ok {
+ if assistant, ok := message.Info.(opencode.AssistantMessage); ok {
cost += assistant.Cost
usage := assistant.Tokens
if usage.Output > 0 {
diff --git a/packages/tui/internal/id/id.go b/packages/tui/internal/id/id.go
new file mode 100644
index 000000000..0490b8f20
--- /dev/null
+++ b/packages/tui/internal/id/id.go
@@ -0,0 +1,96 @@
+package id
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+)
+
+const (
+ PrefixSession = "ses"
+ PrefixMessage = "msg"
+ PrefixUser = "usr"
+ PrefixPart = "prt"
+)
+
+const length = 26
+
+var (
+ lastTimestamp int64
+ counter int64
+ mu sync.Mutex
+)
+
+type Prefix string
+
+const (
+ Session Prefix = PrefixSession
+ Message Prefix = PrefixMessage
+ User Prefix = PrefixUser
+ Part Prefix = PrefixPart
+)
+
+func ValidatePrefix(id string, prefix Prefix) bool {
+ return strings.HasPrefix(id, string(prefix))
+}
+
+func Ascending(prefix Prefix, given ...string) string {
+ return generateID(prefix, false, given...)
+}
+
+func Descending(prefix Prefix, given ...string) string {
+ return generateID(prefix, true, given...)
+}
+
+func generateID(prefix Prefix, descending bool, given ...string) string {
+ if len(given) > 0 && given[0] != "" {
+ if !strings.HasPrefix(given[0], string(prefix)) {
+ panic(fmt.Sprintf("ID %s does not start with %s", given[0], string(prefix)))
+ }
+ return given[0]
+ }
+
+ return generateNewID(prefix, descending)
+}
+
+func randomBase62(length int) string {
+ const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ result := make([]byte, length)
+ bytes := make([]byte, length)
+ rand.Read(bytes)
+
+ for i := 0; i < length; i++ {
+ result[i] = chars[bytes[i]%62]
+ }
+
+ return string(result)
+}
+
+func generateNewID(prefix Prefix, descending bool) string {
+ mu.Lock()
+ defer mu.Unlock()
+
+ currentTimestamp := time.Now().UnixMilli()
+
+ if currentTimestamp != lastTimestamp {
+ lastTimestamp = currentTimestamp
+ counter = 0
+ }
+ counter++
+
+ now := uint64(currentTimestamp)*0x1000 + uint64(counter)
+
+ if descending {
+ now = ^now
+ }
+
+ timeBytes := make([]byte, 6)
+ for i := 0; i < 6; i++ {
+ timeBytes[i] = byte((now >> (40 - 8*i)) & 0xff)
+ }
+
+ return string(prefix) + "_" + hex.EncodeToString(timeBytes) + randomBase62(length-12)
+} \ No newline at end of file
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 389dd64f1..0ebdd35ab 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -5,6 +5,7 @@ import (
"log/slog"
"os"
"os/exec"
+ "slices"
"strings"
"time"
@@ -364,55 +365,76 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case opencode.EventListResponseEventSessionDeleted:
if a.app.Session != nil && msg.Properties.Info.ID == a.app.Session.ID {
a.app.Session = &opencode.Session{}
- a.app.Messages = []opencode.MessageUnion{}
+ a.app.Messages = []app.Message{}
}
return a, toast.NewSuccessToast("Session deleted successfully")
case opencode.EventListResponseEventSessionUpdated:
if msg.Properties.Info.ID == a.app.Session.ID {
a.app.Session = &msg.Properties.Info
}
- case opencode.EventListResponseEventMessageUpdated:
- if msg.Properties.Info.SessionID == a.app.Session.ID {
- exists := false
- optimisticReplaced := false
-
- // First check if this is replacing an optimistic message
- if msg.Properties.Info.Role == opencode.MessageRoleUser {
- // Look for optimistic messages to replace
- for i, m := range a.app.Messages {
- switch m := m.(type) {
- case opencode.UserMessage:
- if strings.HasPrefix(m.ID, "optimistic-") && m.Role == opencode.UserMessageRoleUser {
- // Replace the optimistic message with the real one
- a.app.Messages[i] = msg.Properties.Info.AsUnion()
- exists = true
- optimisticReplaced = true
- break
- }
+ case opencode.EventListResponseEventMessagePartUpdated:
+ slog.Info("message part updated", "message", msg.Properties.Part.MessageID, "part", msg.Properties.Part.ID)
+ if msg.Properties.Part.SessionID == a.app.Session.ID {
+ messageIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool {
+ switch casted := m.Info.(type) {
+ case opencode.UserMessage:
+ return casted.ID == msg.Properties.Part.MessageID
+ case opencode.AssistantMessage:
+ return casted.ID == msg.Properties.Part.MessageID
+ }
+ return false
+ })
+ if messageIndex > -1 {
+ message := a.app.Messages[messageIndex]
+ partIndex := slices.IndexFunc(message.Parts, func(p opencode.PartUnion) bool {
+ switch casted := p.(type) {
+ case opencode.TextPart:
+ return casted.ID == msg.Properties.Part.ID
+ case opencode.FilePart:
+ return casted.ID == msg.Properties.Part.ID
+ case opencode.ToolPart:
+ return casted.ID == msg.Properties.Part.ID
+ case opencode.StepStartPart:
+ return casted.ID == msg.Properties.Part.ID
+ case opencode.StepFinishPart:
+ return casted.ID == msg.Properties.Part.ID
}
+ return false
+ })
+ if partIndex > -1 {
+ message.Parts[partIndex] = msg.Properties.Part.AsUnion()
}
+ if partIndex == -1 {
+ message.Parts = append(message.Parts, msg.Properties.Part.AsUnion())
+ }
+ a.app.Messages[messageIndex] = message
}
-
- // If not replacing optimistic, check for existing message with same ID
- if !optimisticReplaced {
- for i, m := range a.app.Messages {
- var id string
- switch m := m.(type) {
- case opencode.UserMessage:
- id = m.ID
- case opencode.AssistantMessage:
- id = m.ID
- }
- if id == msg.Properties.Info.ID {
- a.app.Messages[i] = msg.Properties.Info.AsUnion()
- exists = true
- break
- }
+ }
+ case opencode.EventListResponseEventMessageUpdated:
+ if msg.Properties.Info.SessionID == a.app.Session.ID {
+ matchIndex := slices.IndexFunc(a.app.Messages, func(m app.Message) bool {
+ switch casted := m.Info.(type) {
+ case opencode.UserMessage:
+ return casted.ID == msg.Properties.Info.ID
+ case opencode.AssistantMessage:
+ return casted.ID == msg.Properties.Info.ID
+ }
+ return false
+ })
+
+ if matchIndex > -1 {
+ match := a.app.Messages[matchIndex]
+ a.app.Messages[matchIndex] = app.Message{
+ Info: msg.Properties.Info.AsUnion(),
+ Parts: match.Parts,
}
}
- if !exists {
- a.app.Messages = append(a.app.Messages, msg.Properties.Info.AsUnion())
+ if matchIndex == -1 {
+ a.app.Messages = append(a.app.Messages, app.Message{
+ Info: msg.Properties.Info.AsUnion(),
+ Parts: []opencode.PartUnion{},
+ })
}
}
case opencode.EventListResponseEventSessionError:
@@ -473,10 +495,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, toast.NewErrorToast("Failed to open session")
}
a.app.Session = msg
- a.app.Messages = make([]opencode.MessageUnion, 0)
- for _, message := range messages {
- a.app.Messages = append(a.app.Messages, message.AsUnion())
- }
+ a.app.Messages = messages
return a, util.CmdHandler(app.SessionLoadedMsg{})
case app.ModelSelectedMsg:
a.app.Provider = &msg.Provider
@@ -837,7 +856,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
return a, nil
}
a.app.Session = &opencode.Session{}
- a.app.Messages = []opencode.MessageUnion{}
+ a.app.Messages = []app.Message{}
cmds = append(cmds, util.CmdHandler(app.SessionClearedMsg{}))
case commands.SessionListCommand:
sessionDialog := dialog.NewSessionDialog(a.app)
diff --git a/packages/tui/sdk/.stats.yml b/packages/tui/sdk/.stats.yml
index 4449c4d95..61d2fa3b7 100644
--- a/packages/tui/sdk/.stats.yml
+++ b/packages/tui/sdk/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 22
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-eb25bb3673f94d0e98a7036e2a2b0ed7ad63d1598665f2d5e091ec0835273798.yml
-openapi_spec_hash: 62f6a8a06aaa4f4ae13e85d56652724f
-config_hash: 589ec6a935a43a3c49a325ece86cbda2
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-352994eb17f76d9472b0f0176efacf77a200a6fab2db28d1cfcd29451b211d7a.yml
+openapi_spec_hash: f01cd3de8c7cf0c9fd513896e81986de
+config_hash: 3695cfc829cfaae14490850b4a1ed282
diff --git a/packages/tui/sdk/README.md b/packages/tui/sdk/README.md
index 2b5782347..38840b28a 100644
--- a/packages/tui/sdk/README.md
+++ b/packages/tui/sdk/README.md
@@ -49,11 +49,14 @@ import (
func main() {
client := opencode.NewClient()
- events, err := client.Event.List(context.TODO())
+ stream := client.Event.ListStreaming(context.TODO())
+ for stream.Next() {
+ fmt.Printf("%+v\n", stream.Current())
+ }
+ err := stream.Err()
if err != nil {
panic(err.Error())
}
- fmt.Printf("%+v\n", events)
}
```
@@ -171,14 +174,14 @@ When the API returns a non-success status code, we return an error with type
To handle errors, we recommend that you use the `errors.As` pattern:
```go
-_, err := client.Event.List(context.TODO())
-if err != nil {
+stream := client.Event.ListStreaming(context.TODO())
+if stream.Err() != nil {
var apierr *opencode.Error
- if errors.As(err, &apierr) {
+ if errors.As(stream.Err(), &apierr) {
println(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request
println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response
}
- panic(err.Error()) // GET "/event": 400 Bad Request { ... }
+ panic(stream.Err().Error()) // GET "/event": 400 Bad Request { ... }
}
```
@@ -196,7 +199,7 @@ To set a per-retry timeout, use `option.WithRequestTimeout()`.
// This sets the timeout for the request, including all the retries.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
-client.Event.List(
+client.Event.ListStreaming(
ctx,
// This sets the per-retry timeout
option.WithRequestTimeout(20*time.Second),
@@ -231,7 +234,7 @@ client := opencode.NewClient(
)
// Override per-request:
-client.Event.List(context.TODO(), option.WithMaxRetries(5))
+client.Event.ListStreaming(context.TODO(), option.WithMaxRetries(5))
```
### Accessing raw response data (e.g. response headers)
@@ -242,8 +245,8 @@ you need to examine response headers, status codes, or other details.
```go
// Create a variable to store the HTTP response
var response *http.Response
-events, err := client.Event.List(context.TODO(), option.WithResponseInto(&response))
-if err != nil {
+stream := client.Event.ListStreaming(context.TODO(), option.WithResponseInto(&response))
+if stream.Err() != nil {
// handle error
}
fmt.Printf("%+v\n", events)
diff --git a/packages/tui/sdk/api.md b/packages/tui/sdk/api.md
index 15bdcfa93..a48e6d7f1 100644
--- a/packages/tui/sdk/api.md
+++ b/packages/tui/sdk/api.md
@@ -77,15 +77,15 @@ Params Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartParam">FilePartParam</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPartParam">TextPartParam</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessagePartUnionParam">UserMessagePartUnionParam</a>
Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessagePart">AssistantMessagePart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePart">FilePart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Part">Part</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepFinishPart">StepFinishPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepStartPart">StepStartPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPart">TextPart</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolPart">ToolPart</a>
@@ -94,7 +94,7 @@ Response Types:
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStatePending">ToolStatePending</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunning">ToolStateRunning</a>
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessage">UserMessage</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessagePart">UserMessagePart</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>
Methods:
@@ -104,7 +104,7 @@ Methods:
- <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AssistantMessage">AssistantMessage</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
-- <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Message">Message</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="get /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="post /session/{id}/summarize">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Summarize">Summarize</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionSummarizeParams">SessionSummarizeParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
- <code title="delete /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unshare">Unshare</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
diff --git a/packages/tui/sdk/client_test.go b/packages/tui/sdk/client_test.go
index e75d64925..62222a465 100644
--- a/packages/tui/sdk/client_test.go
+++ b/packages/tui/sdk/client_test.go
@@ -38,7 +38,7 @@ func TestUserAgentHeader(t *testing.T) {
},
}),
)
- client.Event.List(context.Background())
+ client.Event.ListStreaming(context.Background())
if userAgent != fmt.Sprintf("Opencode/Go %s", internal.PackageVersion) {
t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent)
}
@@ -61,7 +61,11 @@ func TestRetryAfter(t *testing.T) {
},
}),
)
- _, err := client.Event.List(context.Background())
+ stream := client.Event.ListStreaming(context.Background())
+ for stream.Next() {
+ // ...
+ }
+ err := stream.Err()
if err == nil {
t.Error("Expected there to be a cancel error")
}
@@ -95,7 +99,11 @@ func TestDeleteRetryCountHeader(t *testing.T) {
}),
option.WithHeaderDel("X-Stainless-Retry-Count"),
)
- _, err := client.Event.List(context.Background())
+ stream := client.Event.ListStreaming(context.Background())
+ for stream.Next() {
+ // ...
+ }
+ err := stream.Err()
if err == nil {
t.Error("Expected there to be a cancel error")
}
@@ -124,7 +132,11 @@ func TestOverwriteRetryCountHeader(t *testing.T) {
}),
option.WithHeader("X-Stainless-Retry-Count", "42"),
)
- _, err := client.Event.List(context.Background())
+ stream := client.Event.ListStreaming(context.Background())
+ for stream.Next() {
+ // ...
+ }
+ err := stream.Err()
if err == nil {
t.Error("Expected there to be a cancel error")
}
@@ -152,7 +164,11 @@ func TestRetryAfterMs(t *testing.T) {
},
}),
)
- _, err := client.Event.List(context.Background())
+ stream := client.Event.ListStreaming(context.Background())
+ for stream.Next() {
+ // ...
+ }
+ err := stream.Err()
if err == nil {
t.Error("Expected there to be a cancel error")
}
@@ -174,7 +190,11 @@ func TestContextCancel(t *testing.T) {
)
cancelCtx, cancel := context.WithCancel(context.Background())
cancel()
- _, err := client.Event.List(cancelCtx)
+ stream := client.Event.ListStreaming(cancelCtx)
+ for stream.Next() {
+ // ...
+ }
+ err := stream.Err()
if err == nil {
t.Error("Expected there to be a cancel error")
}
@@ -193,7 +213,11 @@ func TestContextCancelDelay(t *testing.T) {
)
cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
defer cancel()
- _, err := client.Event.List(cancelCtx)
+ stream := client.Event.ListStreaming(cancelCtx)
+ for stream.Next() {
+ // ...
+ }
+ err := stream.Err()
if err == nil {
t.Error("expected there to be a cancel error")
}
@@ -218,7 +242,11 @@ func TestContextDeadline(t *testing.T) {
},
}),
)
- _, err := client.Event.List(deadlineCtx)
+ stream := client.Event.ListStreaming(deadlineCtx)
+ for stream.Next() {
+ // ...
+ }
+ err := stream.Err()
if err == nil {
t.Error("expected there to be a deadline error")
}
diff --git a/packages/tui/sdk/event.go b/packages/tui/sdk/event.go
index 8bbf636c3..9002d2aac 100644
--- a/packages/tui/sdk/event.go
+++ b/packages/tui/sdk/event.go
@@ -610,18 +610,14 @@ func (r eventListResponseEventMessagePartUpdatedJSON) RawJSON() string {
func (r EventListResponseEventMessagePartUpdated) implementsEventListResponse() {}
type EventListResponseEventMessagePartUpdatedProperties struct {
- MessageID string `json:"messageID,required"`
- Part AssistantMessagePart `json:"part,required"`
- SessionID string `json:"sessionID,required"`
- JSON eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"`
+ Part Part `json:"part,required"`
+ JSON eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"`
}
// eventListResponseEventMessagePartUpdatedPropertiesJSON contains the JSON
// metadata for the struct [EventListResponseEventMessagePartUpdatedProperties]
type eventListResponseEventMessagePartUpdatedPropertiesJSON struct {
- MessageID apijson.Field
Part apijson.Field
- SessionID apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
diff --git a/packages/tui/sdk/scripts/lint b/packages/tui/sdk/scripts/lint
index 9f37abf28..7e03a7beb 100755
--- a/packages/tui/sdk/scripts/lint
+++ b/packages/tui/sdk/scripts/lint
@@ -5,7 +5,7 @@ set -e
cd "$(dirname "$0")/.."
echo "==> Running Go build"
-go build .
+go build ./...
echo "==> Checking tests compile"
-go test -run=^$ .
+go test -run=^$ ./...
diff --git a/packages/tui/sdk/session.go b/packages/tui/sdk/session.go
index 6321d1ff6..e76ab7f90 100644
--- a/packages/tui/sdk/session.go
+++ b/packages/tui/sdk/session.go
@@ -101,7 +101,7 @@ func (r *SessionService) Init(ctx context.Context, id string, body SessionInitPa
}
// List messages for a session
-func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]Message, err error) {
+func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]SessionMessagesResponse, err error) {
opts = append(r.Options[:], opts...)
if id == "" {
err = errors.New("missing required id parameter")
@@ -152,7 +152,6 @@ type AssistantMessage struct {
ID string `json:"id,required"`
Cost float64 `json:"cost,required"`
ModelID string `json:"modelID,required"`
- Parts []AssistantMessagePart `json:"parts,required"`
Path AssistantMessagePath `json:"path,required"`
ProviderID string `json:"providerID,required"`
Role AssistantMessageRole `json:"role,required"`
@@ -171,7 +170,6 @@ type assistantMessageJSON struct {
ID apijson.Field
Cost apijson.Field
ModelID apijson.Field
- Parts apijson.Field
Path apijson.Field
ProviderID apijson.Field
Role apijson.Field
@@ -435,211 +433,23 @@ func (r AssistantMessageErrorName) IsKnown() bool {
return false
}
-type AssistantMessagePart struct {
- Type AssistantMessagePartType `json:"type,required"`
- ID string `json:"id"`
- Cost float64 `json:"cost"`
- // This field can have the runtime type of [ToolPartState].
- State interface{} `json:"state"`
- Synthetic bool `json:"synthetic"`
- Text string `json:"text"`
- // This field can have the runtime type of
- // [AssistantMessagePartStepFinishPartTokens].
- Tokens interface{} `json:"tokens"`
- Tool string `json:"tool"`
- JSON assistantMessagePartJSON `json:"-"`
- union AssistantMessagePartUnion
-}
-
-// assistantMessagePartJSON contains the JSON metadata for the struct
-// [AssistantMessagePart]
-type assistantMessagePartJSON struct {
- Type apijson.Field
- ID apijson.Field
- Cost apijson.Field
- State apijson.Field
- Synthetic apijson.Field
- Text apijson.Field
- Tokens apijson.Field
- Tool apijson.Field
- raw string
- ExtraFields map[string]apijson.Field
-}
-
-func (r assistantMessagePartJSON) RawJSON() string {
- return r.raw
-}
-
-func (r *AssistantMessagePart) UnmarshalJSON(data []byte) (err error) {
- *r = AssistantMessagePart{}
- err = apijson.UnmarshalRoot(data, &r.union)
- if err != nil {
- return err
- }
- return apijson.Port(r.union, &r)
-}
-
-// AsUnion returns a [AssistantMessagePartUnion] interface which you can cast to
-// the specific types for more type safety.
-//
-// Possible runtime types of the union are [TextPart], [ToolPart], [StepStartPart],
-// [AssistantMessagePartStepFinishPart].
-func (r AssistantMessagePart) AsUnion() AssistantMessagePartUnion {
- return r.union
-}
-
-// Union satisfied by [TextPart], [ToolPart], [StepStartPart] or
-// [AssistantMessagePartStepFinishPart].
-type AssistantMessagePartUnion interface {
- implementsAssistantMessagePart()
-}
-
-func init() {
- apijson.RegisterUnion(
- reflect.TypeOf((*AssistantMessagePartUnion)(nil)).Elem(),
- "type",
- apijson.UnionVariant{
- TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(TextPart{}),
- DiscriminatorValue: "text",
- },
- apijson.UnionVariant{
- TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(ToolPart{}),
- DiscriminatorValue: "tool",
- },
- apijson.UnionVariant{
- TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(StepStartPart{}),
- DiscriminatorValue: "step-start",
- },
- apijson.UnionVariant{
- TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(AssistantMessagePartStepFinishPart{}),
- DiscriminatorValue: "step-finish",
- },
- )
-}
-
-type AssistantMessagePartStepFinishPart struct {
- Cost float64 `json:"cost,required"`
- Tokens AssistantMessagePartStepFinishPartTokens `json:"tokens,required"`
- Type AssistantMessagePartStepFinishPartType `json:"type,required"`
- JSON assistantMessagePartStepFinishPartJSON `json:"-"`
-}
-
-// assistantMessagePartStepFinishPartJSON contains the JSON metadata for the struct
-// [AssistantMessagePartStepFinishPart]
-type assistantMessagePartStepFinishPartJSON struct {
- Cost apijson.Field
- Tokens apijson.Field
- Type apijson.Field
- raw string
- ExtraFields map[string]apijson.Field
-}
-
-func (r *AssistantMessagePartStepFinishPart) UnmarshalJSON(data []byte) (err error) {
- return apijson.UnmarshalRoot(data, r)
-}
-
-func (r assistantMessagePartStepFinishPartJSON) RawJSON() string {
- return r.raw
-}
-
-func (r AssistantMessagePartStepFinishPart) implementsAssistantMessagePart() {}
-
-type AssistantMessagePartStepFinishPartTokens struct {
- Cache AssistantMessagePartStepFinishPartTokensCache `json:"cache,required"`
- Input float64 `json:"input,required"`
- Output float64 `json:"output,required"`
- Reasoning float64 `json:"reasoning,required"`
- JSON assistantMessagePartStepFinishPartTokensJSON `json:"-"`
-}
-
-// assistantMessagePartStepFinishPartTokensJSON contains the JSON metadata for the
-// struct [AssistantMessagePartStepFinishPartTokens]
-type assistantMessagePartStepFinishPartTokensJSON struct {
- Cache apijson.Field
- Input apijson.Field
- Output apijson.Field
- Reasoning apijson.Field
- raw string
- ExtraFields map[string]apijson.Field
-}
-
-func (r *AssistantMessagePartStepFinishPartTokens) UnmarshalJSON(data []byte) (err error) {
- return apijson.UnmarshalRoot(data, r)
-}
-
-func (r assistantMessagePartStepFinishPartTokensJSON) RawJSON() string {
- return r.raw
-}
-
-type AssistantMessagePartStepFinishPartTokensCache struct {
- Read float64 `json:"read,required"`
- Write float64 `json:"write,required"`
- JSON assistantMessagePartStepFinishPartTokensCacheJSON `json:"-"`
-}
-
-// assistantMessagePartStepFinishPartTokensCacheJSON contains the JSON metadata for
-// the struct [AssistantMessagePartStepFinishPartTokensCache]
-type assistantMessagePartStepFinishPartTokensCacheJSON struct {
- Read apijson.Field
- Write apijson.Field
- raw string
- ExtraFields map[string]apijson.Field
-}
-
-func (r *AssistantMessagePartStepFinishPartTokensCache) UnmarshalJSON(data []byte) (err error) {
- return apijson.UnmarshalRoot(data, r)
-}
-
-func (r assistantMessagePartStepFinishPartTokensCacheJSON) RawJSON() string {
- return r.raw
-}
-
-type AssistantMessagePartStepFinishPartType string
-
-const (
- AssistantMessagePartStepFinishPartTypeStepFinish AssistantMessagePartStepFinishPartType = "step-finish"
-)
-
-func (r AssistantMessagePartStepFinishPartType) IsKnown() bool {
- switch r {
- case AssistantMessagePartStepFinishPartTypeStepFinish:
- return true
- }
- return false
-}
-
-type AssistantMessagePartType string
-
-const (
- AssistantMessagePartTypeText AssistantMessagePartType = "text"
- AssistantMessagePartTypeTool AssistantMessagePartType = "tool"
- AssistantMessagePartTypeStepStart AssistantMessagePartType = "step-start"
- AssistantMessagePartTypeStepFinish AssistantMessagePartType = "step-finish"
-)
-
-func (r AssistantMessagePartType) IsKnown() bool {
- switch r {
- case AssistantMessagePartTypeText, AssistantMessagePartTypeTool, AssistantMessagePartTypeStepStart, AssistantMessagePartTypeStepFinish:
- return true
- }
- return false
-}
-
type FilePart struct {
- Mime string `json:"mime,required"`
- Type FilePartType `json:"type,required"`
- URL string `json:"url,required"`
- Filename string `json:"filename"`
- JSON filePartJSON `json:"-"`
+ ID string `json:"id,required"`
+ MessageID string `json:"messageID,required"`
+ Mime string `json:"mime,required"`
+ SessionID string `json:"sessionID,required"`
+ Type FilePartType `json:"type,required"`
+ URL string `json:"url,required"`
+ Filename string `json:"filename"`
+ JSON filePartJSON `json:"-"`
}
// filePartJSON contains the JSON metadata for the struct [FilePart]
type filePartJSON struct {
+ ID apijson.Field
+ MessageID apijson.Field
Mime apijson.Field
+ SessionID apijson.Field
Type apijson.Field
URL apijson.Field
Filename apijson.Field
@@ -655,7 +465,7 @@ func (r filePartJSON) RawJSON() string {
return r.raw
}
-func (r FilePart) implementsUserMessagePart() {}
+func (r FilePart) implementsPart() {}
type FilePartType string
@@ -672,23 +482,23 @@ func (r FilePartType) IsKnown() bool {
}
type FilePartParam struct {
- Mime param.Field[string] `json:"mime,required"`
- Type param.Field[FilePartType] `json:"type,required"`
- URL param.Field[string] `json:"url,required"`
- Filename param.Field[string] `json:"filename"`
+ ID param.Field[string] `json:"id,required"`
+ MessageID param.Field[string] `json:"messageID,required"`
+ Mime param.Field[string] `json:"mime,required"`
+ SessionID param.Field[string] `json:"sessionID,required"`
+ Type param.Field[FilePartType] `json:"type,required"`
+ URL param.Field[string] `json:"url,required"`
+ Filename param.Field[string] `json:"filename"`
}
func (r FilePartParam) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
-func (r FilePartParam) implementsUserMessagePartUnionParam() {}
+func (r FilePartParam) implementsSessionChatParamsPartUnion() {}
type Message struct {
- ID string `json:"id,required"`
- // This field can have the runtime type of [[]UserMessagePart],
- // [[]AssistantMessagePart].
- Parts interface{} `json:"parts,required"`
+ ID string `json:"id,required"`
Role MessageRole `json:"role,required"`
SessionID string `json:"sessionID,required"`
// This field can have the runtime type of [UserMessageTime],
@@ -713,7 +523,6 @@ type Message struct {
// messageJSON contains the JSON metadata for the struct [Message]
type messageJSON struct {
ID apijson.Field
- Parts apijson.Field
Role apijson.Field
SessionID apijson.Field
Time apijson.Field
@@ -787,6 +596,128 @@ func (r MessageRole) IsKnown() bool {
return false
}
+type Part struct {
+ ID string `json:"id,required"`
+ MessageID string `json:"messageID,required"`
+ SessionID string `json:"sessionID,required"`
+ Type PartType `json:"type,required"`
+ CallID string `json:"callID"`
+ Cost float64 `json:"cost"`
+ Filename string `json:"filename"`
+ Mime string `json:"mime"`
+ // This field can have the runtime type of [ToolPartState].
+ State interface{} `json:"state"`
+ Synthetic bool `json:"synthetic"`
+ Text string `json:"text"`
+ // This field can have the runtime type of [TextPartTime].
+ Time interface{} `json:"time"`
+ // This field can have the runtime type of [StepFinishPartTokens].
+ Tokens interface{} `json:"tokens"`
+ Tool string `json:"tool"`
+ URL string `json:"url"`
+ JSON partJSON `json:"-"`
+ union PartUnion
+}
+
+// partJSON contains the JSON metadata for the struct [Part]
+type partJSON struct {
+ ID apijson.Field
+ MessageID apijson.Field
+ SessionID apijson.Field
+ Type apijson.Field
+ CallID apijson.Field
+ Cost apijson.Field
+ Filename apijson.Field
+ Mime apijson.Field
+ State apijson.Field
+ Synthetic apijson.Field
+ Text apijson.Field
+ Time apijson.Field
+ Tokens apijson.Field
+ Tool apijson.Field
+ URL apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r partJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r *Part) UnmarshalJSON(data []byte) (err error) {
+ *r = Part{}
+ err = apijson.UnmarshalRoot(data, &r.union)
+ if err != nil {
+ return err
+ }
+ return apijson.Port(r.union, &r)
+}
+
+// AsUnion returns a [PartUnion] interface which you can cast to the specific types
+// for more type safety.
+//
+// Possible runtime types of the union are [TextPart], [FilePart], [ToolPart],
+// [StepStartPart], [StepFinishPart].
+func (r Part) AsUnion() PartUnion {
+ return r.union
+}
+
+// Union satisfied by [TextPart], [FilePart], [ToolPart], [StepStartPart] or
+// [StepFinishPart].
+type PartUnion interface {
+ implementsPart()
+}
+
+func init() {
+ apijson.RegisterUnion(
+ reflect.TypeOf((*PartUnion)(nil)).Elem(),
+ "type",
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(TextPart{}),
+ DiscriminatorValue: "text",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(FilePart{}),
+ DiscriminatorValue: "file",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ToolPart{}),
+ DiscriminatorValue: "tool",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(StepStartPart{}),
+ DiscriminatorValue: "step-start",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(StepFinishPart{}),
+ DiscriminatorValue: "step-finish",
+ },
+ )
+}
+
+type PartType string
+
+const (
+ PartTypeText PartType = "text"
+ PartTypeFile PartType = "file"
+ PartTypeTool PartType = "tool"
+ PartTypeStepStart PartType = "step-start"
+ PartTypeStepFinish PartType = "step-finish"
+)
+
+func (r PartType) IsKnown() bool {
+ switch r {
+ case PartTypeText, PartTypeFile, PartTypeTool, PartTypeStepStart, PartTypeStepFinish:
+ return true
+ }
+ return false
+}
+
type Session struct {
ID string `json:"id,required"`
Time SessionTime `json:"time,required"`
@@ -885,13 +816,115 @@ func (r sessionShareJSON) RawJSON() string {
return r.raw
}
+type StepFinishPart struct {
+ ID string `json:"id,required"`
+ Cost float64 `json:"cost,required"`
+ MessageID string `json:"messageID,required"`
+ SessionID string `json:"sessionID,required"`
+ Tokens StepFinishPartTokens `json:"tokens,required"`
+ Type StepFinishPartType `json:"type,required"`
+ JSON stepFinishPartJSON `json:"-"`
+}
+
+// stepFinishPartJSON contains the JSON metadata for the struct [StepFinishPart]
+type stepFinishPartJSON struct {
+ ID apijson.Field
+ Cost apijson.Field
+ MessageID apijson.Field
+ SessionID apijson.Field
+ Tokens apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *StepFinishPart) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r stepFinishPartJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r StepFinishPart) implementsPart() {}
+
+type StepFinishPartTokens struct {
+ Cache StepFinishPartTokensCache `json:"cache,required"`
+ Input float64 `json:"input,required"`
+ Output float64 `json:"output,required"`
+ Reasoning float64 `json:"reasoning,required"`
+ JSON stepFinishPartTokensJSON `json:"-"`
+}
+
+// stepFinishPartTokensJSON contains the JSON metadata for the struct
+// [StepFinishPartTokens]
+type stepFinishPartTokensJSON struct {
+ Cache apijson.Field
+ Input apijson.Field
+ Output apijson.Field
+ Reasoning apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *StepFinishPartTokens) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r stepFinishPartTokensJSON) RawJSON() string {
+ return r.raw
+}
+
+type StepFinishPartTokensCache struct {
+ Read float64 `json:"read,required"`
+ Write float64 `json:"write,required"`
+ JSON stepFinishPartTokensCacheJSON `json:"-"`
+}
+
+// stepFinishPartTokensCacheJSON contains the JSON metadata for the struct
+// [StepFinishPartTokensCache]
+type stepFinishPartTokensCacheJSON struct {
+ Read apijson.Field
+ Write apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *StepFinishPartTokensCache) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r stepFinishPartTokensCacheJSON) RawJSON() string {
+ return r.raw
+}
+
+type StepFinishPartType string
+
+const (
+ StepFinishPartTypeStepFinish StepFinishPartType = "step-finish"
+)
+
+func (r StepFinishPartType) IsKnown() bool {
+ switch r {
+ case StepFinishPartTypeStepFinish:
+ return true
+ }
+ return false
+}
+
type StepStartPart struct {
- Type StepStartPartType `json:"type,required"`
- JSON stepStartPartJSON `json:"-"`
+ ID string `json:"id,required"`
+ MessageID string `json:"messageID,required"`
+ SessionID string `json:"sessionID,required"`
+ Type StepStartPartType `json:"type,required"`
+ JSON stepStartPartJSON `json:"-"`
}
// stepStartPartJSON contains the JSON metadata for the struct [StepStartPart]
type stepStartPartJSON struct {
+ ID apijson.Field
+ MessageID apijson.Field
+ SessionID apijson.Field
Type apijson.Field
raw string
ExtraFields map[string]apijson.Field
@@ -905,7 +938,7 @@ func (r stepStartPartJSON) RawJSON() string {
return r.raw
}
-func (r StepStartPart) implementsAssistantMessagePart() {}
+func (r StepStartPart) implementsPart() {}
type StepStartPartType string
@@ -922,17 +955,25 @@ func (r StepStartPartType) IsKnown() bool {
}
type TextPart struct {
+ ID string `json:"id,required"`
+ MessageID string `json:"messageID,required"`
+ SessionID string `json:"sessionID,required"`
Text string `json:"text,required"`
Type TextPartType `json:"type,required"`
Synthetic bool `json:"synthetic"`
+ Time TextPartTime `json:"time"`
JSON textPartJSON `json:"-"`
}
// textPartJSON contains the JSON metadata for the struct [TextPart]
type textPartJSON struct {
+ ID apijson.Field
+ MessageID apijson.Field
+ SessionID apijson.Field
Text apijson.Field
Type apijson.Field
Synthetic apijson.Field
+ Time apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
@@ -945,9 +986,7 @@ func (r textPartJSON) RawJSON() string {
return r.raw
}
-func (r TextPart) implementsAssistantMessagePart() {}
-
-func (r TextPart) implementsUserMessagePart() {}
+func (r TextPart) implementsPart() {}
type TextPartType string
@@ -963,29 +1002,70 @@ func (r TextPartType) IsKnown() bool {
return false
}
+type TextPartTime struct {
+ Start float64 `json:"start,required"`
+ End float64 `json:"end"`
+ JSON textPartTimeJSON `json:"-"`
+}
+
+// textPartTimeJSON contains the JSON metadata for the struct [TextPartTime]
+type textPartTimeJSON struct {
+ Start apijson.Field
+ End apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *TextPartTime) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r textPartTimeJSON) RawJSON() string {
+ return r.raw
+}
+
type TextPartParam struct {
- Text param.Field[string] `json:"text,required"`
- Type param.Field[TextPartType] `json:"type,required"`
- Synthetic param.Field[bool] `json:"synthetic"`
+ ID param.Field[string] `json:"id,required"`
+ MessageID param.Field[string] `json:"messageID,required"`
+ SessionID param.Field[string] `json:"sessionID,required"`
+ Text param.Field[string] `json:"text,required"`
+ Type param.Field[TextPartType] `json:"type,required"`
+ Synthetic param.Field[bool] `json:"synthetic"`
+ Time param.Field[TextPartTimeParam] `json:"time"`
}
func (r TextPartParam) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
-func (r TextPartParam) implementsUserMessagePartUnionParam() {}
+func (r TextPartParam) implementsSessionChatParamsPartUnion() {}
+
+type TextPartTimeParam struct {
+ Start param.Field[float64] `json:"start,required"`
+ End param.Field[float64] `json:"end"`
+}
+
+func (r TextPartTimeParam) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
type ToolPart struct {
- ID string `json:"id,required"`
- State ToolPartState `json:"state,required"`
- Tool string `json:"tool,required"`
- Type ToolPartType `json:"type,required"`
- JSON toolPartJSON `json:"-"`
+ ID string `json:"id,required"`
+ CallID string `json:"callID,required"`
+ MessageID string `json:"messageID,required"`
+ SessionID string `json:"sessionID,required"`
+ State ToolPartState `json:"state,required"`
+ Tool string `json:"tool,required"`
+ Type ToolPartType `json:"type,required"`
+ JSON toolPartJSON `json:"-"`
}
// toolPartJSON contains the JSON metadata for the struct [ToolPart]
type toolPartJSON struct {
ID apijson.Field
+ CallID apijson.Field
+ MessageID apijson.Field
+ SessionID apijson.Field
State apijson.Field
Tool apijson.Field
Type apijson.Field
@@ -1001,7 +1081,7 @@ func (r toolPartJSON) RawJSON() string {
return r.raw
}
-func (r ToolPart) implementsAssistantMessagePart() {}
+func (r ToolPart) implementsPart() {}
type ToolPartState struct {
Status ToolPartStateStatus `json:"status,required"`
@@ -1357,18 +1437,16 @@ func (r toolStateRunningTimeJSON) RawJSON() string {
}
type UserMessage struct {
- ID string `json:"id,required"`
- Parts []UserMessagePart `json:"parts,required"`
- Role UserMessageRole `json:"role,required"`
- SessionID string `json:"sessionID,required"`
- Time UserMessageTime `json:"time,required"`
- JSON userMessageJSON `json:"-"`
+ ID string `json:"id,required"`
+ Role UserMessageRole `json:"role,required"`
+ SessionID string `json:"sessionID,required"`
+ Time UserMessageTime `json:"time,required"`
+ JSON userMessageJSON `json:"-"`
}
// userMessageJSON contains the JSON metadata for the struct [UserMessage]
type userMessageJSON struct {
ID apijson.Field
- Parts apijson.Field
Role apijson.Field
SessionID apijson.Field
Time apijson.Field
@@ -1420,119 +1498,82 @@ func (r userMessageTimeJSON) RawJSON() string {
return r.raw
}
-type UserMessagePart struct {
- Type UserMessagePartType `json:"type,required"`
- Filename string `json:"filename"`
- Mime string `json:"mime"`
- Synthetic bool `json:"synthetic"`
- Text string `json:"text"`
- URL string `json:"url"`
- JSON userMessagePartJSON `json:"-"`
- union UserMessagePartUnion
+type SessionMessagesResponse struct {
+ Info Message `json:"info,required"`
+ Parts []Part `json:"parts,required"`
+ JSON sessionMessagesResponseJSON `json:"-"`
}
-// userMessagePartJSON contains the JSON metadata for the struct [UserMessagePart]
-type userMessagePartJSON struct {
- Type apijson.Field
- Filename apijson.Field
- Mime apijson.Field
- Synthetic apijson.Field
- Text apijson.Field
- URL apijson.Field
+// sessionMessagesResponseJSON contains the JSON metadata for the struct
+// [SessionMessagesResponse]
+type sessionMessagesResponseJSON struct {
+ Info apijson.Field
+ Parts apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
-func (r userMessagePartJSON) RawJSON() string {
+func (r *SessionMessagesResponse) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r sessionMessagesResponseJSON) RawJSON() string {
return r.raw
}
-func (r *UserMessagePart) UnmarshalJSON(data []byte) (err error) {
- *r = UserMessagePart{}
- err = apijson.UnmarshalRoot(data, &r.union)
- if err != nil {
- return err
- }
- return apijson.Port(r.union, &r)
+type SessionChatParams struct {
+ MessageID param.Field[string] `json:"messageID,required"`
+ Mode param.Field[string] `json:"mode,required"`
+ ModelID param.Field[string] `json:"modelID,required"`
+ Parts param.Field[[]SessionChatParamsPartUnion] `json:"parts,required"`
+ ProviderID param.Field[string] `json:"providerID,required"`
}
-// AsUnion returns a [UserMessagePartUnion] interface which you can cast to the
-// specific types for more type safety.
-//
-// Possible runtime types of the union are [TextPart], [FilePart].
-func (r UserMessagePart) AsUnion() UserMessagePartUnion {
- return r.union
+func (r SessionChatParams) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
}
-// Union satisfied by [TextPart] or [FilePart].
-type UserMessagePartUnion interface {
- implementsUserMessagePart()
+type SessionChatParamsPart struct {
+ ID param.Field[string] `json:"id,required"`
+ MessageID param.Field[string] `json:"messageID,required"`
+ SessionID param.Field[string] `json:"sessionID,required"`
+ Type param.Field[SessionChatParamsPartsType] `json:"type,required"`
+ Filename param.Field[string] `json:"filename"`
+ Mime param.Field[string] `json:"mime"`
+ Synthetic param.Field[bool] `json:"synthetic"`
+ Text param.Field[string] `json:"text"`
+ Time param.Field[interface{}] `json:"time"`
+ URL param.Field[string] `json:"url"`
}
-func init() {
- apijson.RegisterUnion(
- reflect.TypeOf((*UserMessagePartUnion)(nil)).Elem(),
- "type",
- apijson.UnionVariant{
- TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(TextPart{}),
- DiscriminatorValue: "text",
- },
- apijson.UnionVariant{
- TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(FilePart{}),
- DiscriminatorValue: "file",
- },
- )
+func (r SessionChatParamsPart) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
}
-type UserMessagePartType string
+func (r SessionChatParamsPart) implementsSessionChatParamsPartUnion() {}
+
+// Satisfied by [FilePartParam], [TextPartParam], [SessionChatParamsPart].
+type SessionChatParamsPartUnion interface {
+ implementsSessionChatParamsPartUnion()
+}
+
+type SessionChatParamsPartsType string
const (
- UserMessagePartTypeText UserMessagePartType = "text"
- UserMessagePartTypeFile UserMessagePartType = "file"
+ SessionChatParamsPartsTypeFile SessionChatParamsPartsType = "file"
+ SessionChatParamsPartsTypeText SessionChatParamsPartsType = "text"
)
-func (r UserMessagePartType) IsKnown() bool {
+func (r SessionChatParamsPartsType) IsKnown() bool {
switch r {
- case UserMessagePartTypeText, UserMessagePartTypeFile:
+ case SessionChatParamsPartsTypeFile, SessionChatParamsPartsTypeText:
return true
}
return false
}
-type UserMessagePartParam struct {
- Type param.Field[UserMessagePartType] `json:"type,required"`
- Filename param.Field[string] `json:"filename"`
- Mime param.Field[string] `json:"mime"`
- Synthetic param.Field[bool] `json:"synthetic"`
- Text param.Field[string] `json:"text"`
- URL param.Field[string] `json:"url"`
-}
-
-func (r UserMessagePartParam) MarshalJSON() (data []byte, err error) {
- return apijson.MarshalRoot(r)
-}
-
-func (r UserMessagePartParam) implementsUserMessagePartUnionParam() {}
-
-// Satisfied by [TextPartParam], [FilePartParam], [UserMessagePartParam].
-type UserMessagePartUnionParam interface {
- implementsUserMessagePartUnionParam()
-}
-
-type SessionChatParams struct {
- Mode param.Field[string] `json:"mode,required"`
- ModelID param.Field[string] `json:"modelID,required"`
- Parts param.Field[[]UserMessagePartUnionParam] `json:"parts,required"`
- ProviderID param.Field[string] `json:"providerID,required"`
-}
-
-func (r SessionChatParams) MarshalJSON() (data []byte, err error) {
- return apijson.MarshalRoot(r)
-}
-
type SessionInitParams struct {
+ MessageID param.Field[string] `json:"messageID,required"`
ModelID param.Field[string] `json:"modelID,required"`
ProviderID param.Field[string] `json:"providerID,required"`
}
diff --git a/packages/tui/sdk/session_test.go b/packages/tui/sdk/session_test.go
index 4ff2818c5..c74a4a385 100644
--- a/packages/tui/sdk/session_test.go
+++ b/packages/tui/sdk/session_test.go
@@ -117,12 +117,17 @@ func TestSessionChat(t *testing.T) {
context.TODO(),
"id",
opencode.SessionChatParams{
- Mode: opencode.F("mode"),
- ModelID: opencode.F("modelID"),
- Parts: opencode.F([]opencode.UserMessagePartUnionParam{opencode.TextPartParam{
- Text: opencode.F("text"),
- Type: opencode.F(opencode.TextPartTypeText),
- Synthetic: opencode.F(true),
+ MessageID: opencode.F("messageID"),
+ Mode: opencode.F("mode"),
+ ModelID: opencode.F("modelID"),
+ Parts: opencode.F([]opencode.SessionChatParamsPartUnion{opencode.FilePartParam{
+ ID: opencode.F("id"),
+ MessageID: opencode.F("messageID"),
+ Mime: opencode.F("mime"),
+ SessionID: opencode.F("sessionID"),
+ Type: opencode.F(opencode.FilePartTypeFile),
+ URL: opencode.F("url"),
+ Filename: opencode.F("filename"),
}}),
ProviderID: opencode.F("providerID"),
},
@@ -152,6 +157,7 @@ func TestSessionInit(t *testing.T) {
context.TODO(),
"id",
opencode.SessionInitParams{
+ MessageID: opencode.F("messageID"),
ModelID: opencode.F("modelID"),
ProviderID: opencode.F("providerID"),
},
diff --git a/packages/tui/sdk/usage_test.go b/packages/tui/sdk/usage_test.go
index 0e261a7aa..5e8f44c7c 100644
--- a/packages/tui/sdk/usage_test.go
+++ b/packages/tui/sdk/usage_test.go
@@ -23,10 +23,13 @@ func TestUsage(t *testing.T) {
client := opencode.NewClient(
option.WithBaseURL(baseURL),
)
- events, err := client.Event.List(context.TODO())
+ stream := client.Event.ListStreaming(context.TODO())
+ for stream.Next() {
+ t.Logf("%+v\n", stream.Current())
+ }
+ err := stream.Err()
if err != nil {
t.Error(err)
return
}
- t.Logf("%+v\n", events)
}
diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx
index ad23f188b..56f218fd8 100644
--- a/packages/web/src/components/Share.tsx
+++ b/packages/web/src/components/Share.tsx
@@ -1,6 +1,6 @@
-import { For, Show, onMount, Suspense, onCleanup, createMemo, createSignal, SuspenseList } from "solid-js"
+import { For, Show, onMount, Suspense, onCleanup, createMemo, createSignal, SuspenseList, createEffect } from "solid-js"
import { DateTime } from "luxon"
-import { createStore, reconcile } from "solid-js/store"
+import { createStore, reconcile, unwrap } from "solid-js/store"
import { IconArrowDown } from "./icons"
import { IconOpencode } from "./icons/custom"
import styles from "./share.module.css"
@@ -9,6 +9,8 @@ import type { Message } from "opencode/session/message"
import type { Session } from "opencode/session/index"
import { Part, ProviderIcon } from "./share/part"
+type MessageWithParts = MessageV2.Info & { parts: MessageV2.Part[] }
+
type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting"
function scrollToAnchor(id: string) {
@@ -39,7 +41,7 @@ export default function Share(props: {
id: string
api: string
info: Session.Info
- messages: Record<string, MessageV2.Info>
+ messages: Record<string, MessageWithParts>
}) {
let lastScrollY = 0
let hasScrolledToAnchor = false
@@ -57,10 +59,13 @@ export default function Share(props: {
const [store, setStore] = createStore<{
info?: Session.Info
- messages: Record<string, MessageV2.Info | Message.Info>
+ messages: Record<string, MessageWithParts>
}>({ info: props.info, messages: props.messages })
const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)))
const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"])
+ createEffect(() => {
+ console.log(unwrap(store))
+ })
onMount(() => {
const apiUrl = props.api
@@ -115,8 +120,22 @@ export default function Share(props: {
}
if (type === "message") {
const [, messageID] = splits
+ if ("metadata" in d.content) {
+ d.content = fromV1(d.content)
+ }
+ d.content.parts = d.content.parts ?? store.messages[messageID]?.parts ?? []
setStore("messages", messageID, reconcile(d.content))
}
+ if (type === "part") {
+ setStore("messages", d.content.messageID, "parts", arr => {
+ const index = arr.findIndex((x) => x.id === d.content.id)
+ if (index === -1)
+ arr.push(d.content)
+ if (index > -1)
+ arr[index] = d.content
+ return [...arr]
+ })
+ }
} catch (error) {
console.error("Error parsing WebSocket message:", error)
}
@@ -233,7 +252,7 @@ export default function Share(props: {
rootDir: undefined as string | undefined,
created: undefined as number | undefined,
completed: undefined as number | undefined,
- messages: [] as MessageV2.Info[],
+ messages: [] as MessageWithParts[],
models: {} as Record<string, string[]>,
cost: 0,
tokens: {
@@ -247,7 +266,7 @@ export default function Share(props: {
const msgs = messages()
for (let i = 0; i < msgs.length; i++) {
- const msg = "metadata" in msgs[i] ? fromV1(msgs[i] as Message.Info) : (msgs[i] as MessageV2.Info)
+ const msg = msgs[i]
result.messages.push(msg)
@@ -464,9 +483,9 @@ export default function Share(props: {
)
}
-export function fromV1(v1: Message.Info): MessageV2.Info {
+export function fromV1(v1: Message.Info): MessageWithParts {
if (v1.role === "assistant") {
- const result: MessageV2.Assistant = {
+ return {
id: v1.id,
sessionID: v1.metadata.sessionID,
role: "assistant",
@@ -482,10 +501,16 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
providerID: v1.metadata.assistant!.providerID,
system: v1.metadata.assistant!.system,
error: v1.metadata.error,
- parts: v1.parts.flatMap((part): MessageV2.AssistantPart[] => {
+ parts: v1.parts.flatMap((part, index): MessageV2.Part[] => {
+ const base = {
+ id: index.toString(),
+ messageID: v1.id,
+ sessionID: v1.metadata.sessionID,
+ }
if (part.type === "text") {
return [
{
+ ...base,
type: "text",
text: part.text,
},
@@ -494,6 +519,7 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
if (part.type === "step-start") {
return [
{
+ ...base,
type: "step-start",
},
]
@@ -501,8 +527,9 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
if (part.type === "tool-invocation") {
return [
{
+ ...base,
type: "tool",
- id: part.toolInvocation.toolCallId,
+ callID: part.toolInvocation.toolCallId,
tool: part.toolInvocation.toolName,
state: (() => {
if (part.toolInvocation.state === "partial-call") {
@@ -540,21 +567,26 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
return []
}),
}
- return result
}
if (v1.role === "user") {
- const result: MessageV2.User = {
+ return {
id: v1.id,
sessionID: v1.metadata.sessionID,
role: "user",
time: {
created: v1.metadata.time.created,
},
- parts: v1.parts.flatMap((part): MessageV2.UserPart[] => {
+ parts: v1.parts.flatMap((part, index): MessageV2.Part[] => {
+ const base = {
+ id: index.toString(),
+ messageID: v1.id,
+ sessionID: v1.metadata.sessionID,
+ }
if (part.type === "text") {
return [
{
+ ...base,
type: "text",
text: part.text,
},
@@ -563,6 +595,7 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
if (part.type === "file") {
return [
{
+ ...base,
type: "file",
mime: part.mediaType,
filename: part.filename,
@@ -573,7 +606,6 @@ export function fromV1(v1: Message.Info): MessageV2.Info {
return []
}),
}
- return result
}
throw new Error("unknown message type")