diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index f5a7f6f6ca49..b7ff2ff3e449 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1101,6 +1101,7 @@ export function Session() { { - if (!final()) return 0 - if (!props.message.time.completed) return 0 - const user = messages().find((x) => x.role === "user" && x.id === props.message.parentID) - if (!user || !user.time) return 0 - return props.message.time.completed - user.time.created + const stats = createMemo(() => { + // if (!final() || !props.message.time.completed) return null + + const list = messages() + const stats = { + duration: 0, + tps: [] as number[], + } + + for (let i = props.index; i >= 0; i--) { + const msg = list[i] + + if (msg.id === props.message.parentID && msg.role === "user") { + stats.duration = (props.message.time.completed ?? Date.now()) - msg.time.created + return { + duration: (props.message.time.completed ?? Date.now()) - msg.time.created, + tps: stats.tps.reduce((sum, x) => sum + x, 0) / stats.tps.length, + } + } + if (msg.role === "assistant") { + if (msg.tokens.output && msg.time.started && msg.time.streamed) { + const duration = msg.time.streamed - msg.time.started + const tps = msg.tokens.output / (duration / 1000) + stats.tps.push(tps) + } + } + } + + return null }) return ( @@ -1334,8 +1358,14 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las {" "} {Locale.titlecase(props.message.mode)} · {props.message.modelID} - - · {Locale.duration(duration())} + + {(s) => ( + + {" "} + · {Locale.duration(s().duration)} + 0}> · {s().tps.toFixed(0)} tok/s + + )} · interrupted diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 178751a2227a..dc7acdc7b316 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -392,6 +392,8 @@ export namespace MessageV2 { role: z.literal("assistant"), time: z.object({ created: z.number(), + started: z.number().optional(), + streamed: z.number().optional(), completed: z.number().optional(), }), error: z diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index e7532d20073b..3d1bf6aeb4aa 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -57,6 +57,8 @@ export namespace SessionProcessor { switch (value.type) { case "start": SessionStatus.set(input.sessionID, { type: "busy" }) + input.assistantMessage.time.started = Date.now() + await Session.updateMessage(input.assistantMessage) break case "reasoning-start": @@ -334,6 +336,8 @@ export namespace SessionProcessor { await Session.updatePart(currentText) } currentText = undefined + input.assistantMessage.time.streamed = Date.now() + await Session.updateMessage(input.assistantMessage) break case "finish": diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 4050ef15738c..b7e8ddc57fa6 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -207,6 +207,8 @@ export type AssistantMessage = { role: "assistant" time: { created: number + started?: number + streamed?: number completed?: number } error?: