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 f5a7f6f6ca..bca9ae767d 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() + let tokens = 0 + let active = 0 + + for (let i = props.index; i >= 0; i--) { + const msg = list[i] + if (msg.role === "assistant") { + tokens += msg.tokens?.output || 0 + if (msg.time.started && msg.time.streamed) { + const delta = msg.time.streamed - msg.time.started + if (delta > 0) active += delta + } + } + if (msg.role === "user" && msg.id === props.message.parentID) { + if (!msg.time?.created) return null + const total = props.message.time.completed - msg.time.created + if (total <= 0 || active <= 0) return null + const tps = tokens > 0 ? tokens / (active / 1000) : null + return { total, tps } + } + } + + return null }) return ( @@ -1334,8 +1355,14 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las {" "} {Locale.titlecase(props.message.mode)} · {props.message.modelID} - - · {Locale.duration(duration())} + + {(s) => ( + + {" "} + · {Locale.duration(s().total)} + · {s().tps!.toFixed(1)} tok/s + + )} · interrupted diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 178751a222..3bbf9b697f 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -392,7 +392,9 @@ export namespace MessageV2 { role: z.literal("assistant"), time: z.object({ created: z.number(), + started: z.number().optional(), completed: z.number().optional(), + streamed: z.number().optional(), }), error: z .discriminatedUnion("name", [ diff --git a/packages/opencode/src/session/message.ts b/packages/opencode/src/session/message.ts index 5c950d0e40..f87c7e12b5 100644 --- a/packages/opencode/src/session/message.ts +++ b/packages/opencode/src/session/message.ts @@ -137,7 +137,9 @@ export namespace Message { .object({ time: z.object({ created: z.number(), + started: z.number().optional(), completed: z.number().optional(), + streamed: z.number().optional(), }), error: z .discriminatedUnion("name", [AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema]) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index e7532d2007..5fa5ef8b17 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -57,6 +57,10 @@ export namespace SessionProcessor { switch (value.type) { case "start": SessionStatus.set(input.sessionID, { type: "busy" }) + if (!input.assistantMessage.time.started) { + input.assistantMessage.time.started = Date.now() + await Session.updateMessage(input.assistantMessage) + } break case "reasoning-start": @@ -337,6 +341,10 @@ export namespace SessionProcessor { break case "finish": + if (!input.assistantMessage.time.streamed) { + input.assistantMessage.time.streamed = Date.now() + await Session.updateMessage(input.assistantMessage) + } break default: