@@ -2,9 +2,9 @@ import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/p
22import { SessionMessage } from "@opencode-ai/schema/session-message"
33import { EOL } from "node:os"
44import { readFile } from "node:fs/promises"
5- import { toolOutputText } from "./tool"
5+ import { toolOutputText } from "../mini/tool"
6+ import type { MiniToolPart } from "../mini/types"
67import { UI } from "./ui"
7- import type { MiniToolPart } from "./types"
88
99type Model = {
1010 providerID : string
@@ -30,6 +30,7 @@ type Input = {
3030 auto : boolean
3131 /** True when the client is attached to a shared server rather than an exclusive in-process one. */
3232 attached : boolean
33+ compatibility ?: "v1"
3334 renderTool : ( part : MiniToolPart ) => Promise < void >
3435 renderToolError : ( part : MiniToolPart ) => Promise < void >
3536}
@@ -71,7 +72,9 @@ export async function runNonInteractivePrompt(input: Input) {
7172 let permissionRejected = false
7273 let formCancelled = false
7374 let interrupted = false
75+ let v1InvalidOutput = false
7476 let admission : AbortController | undefined
77+ let pendingStep : { timestamp : number ; part : Record < string , unknown > ; label : string } | undefined
7578
7679 const emit = ( type : string , timestamp : number , data : Record < string , unknown > ) => {
7780 if ( input . format !== "json" ) return false
@@ -92,6 +95,17 @@ export async function runNonInteractivePrompt(input: Input) {
9295 UI . empty ( )
9396 }
9497
98+ const flushStep = ( ) => {
99+ if ( ! pendingStep ) return
100+ const value = pendingStep
101+ pendingStep = undefined
102+ if ( ! emit ( "step_start" , value . timestamp , { part : value . part } ) && input . format !== "json" ) {
103+ UI . empty ( )
104+ UI . println ( value . label )
105+ UI . empty ( )
106+ }
107+ }
108+
95109 const replyPermission = async ( request : { id : string ; action : string ; resources : ReadonlyArray < string > } ) => {
96110 if ( ! input . auto ) {
97111 permissionRejected = true
@@ -178,6 +192,14 @@ export async function runNonInteractivePrompt(input: Input) {
178192 type : "step-start" ,
179193 snapshot : event . data . snapshot ,
180194 }
195+ if ( input . compatibility === "v1" ) {
196+ pendingStep = {
197+ timestamp : time ,
198+ part,
199+ label : `> ${ event . data . agent } · ${ event . data . model . id } ` ,
200+ }
201+ continue
202+ }
181203 if ( ! emit ( "step_start" , time , { part } ) && input . format !== "json" ) {
182204 UI . empty ( )
183205 UI . println ( `> ${ event . data . agent } · ${ event . data . model . id } ` )
@@ -187,6 +209,7 @@ export async function runNonInteractivePrompt(input: Input) {
187209 }
188210
189211 if ( event . type === "session.text.started" ) {
212+ flushStep ( )
190213 starts . set ( "text" , { id : partID ( event . id ) , timestamp : time } )
191214 continue
192215 }
@@ -206,6 +229,7 @@ export async function runNonInteractivePrompt(input: Input) {
206229 }
207230
208231 if ( event . type === "session.reasoning.started" ) {
232+ flushStep ( )
209233 starts . set ( "reasoning" , { id : partID ( event . id ) , timestamp : time } )
210234 continue
211235 }
@@ -236,6 +260,7 @@ export async function runNonInteractivePrompt(input: Input) {
236260 }
237261
238262 if ( event . type === "session.tool.input.started" ) {
263+ flushStep ( )
239264 tools . set ( event . data . callID , {
240265 id : partID ( event . id ) ,
241266 timestamp : time ,
@@ -251,6 +276,7 @@ export async function runNonInteractivePrompt(input: Input) {
251276 continue
252277 }
253278 if ( event . type === "session.tool.called" ) {
279+ flushStep ( )
254280 const current = tools . get ( event . data . callID )
255281 tools . set ( event . data . callID , {
256282 id : current ?. id ?? partID ( event . id ) ,
@@ -316,6 +342,7 @@ export async function runNonInteractivePrompt(input: Input) {
316342 } ,
317343 }
318344 tools . delete ( event . data . callID )
345+ if ( input . compatibility === "v1" && ( permissionRejected || questionRejected || formCancelled ) ) continue
319346 if ( ! emit ( "tool_use" , time , { part } ) ) {
320347 await input . renderToolError ( part )
321348 UI . error ( error )
@@ -324,6 +351,7 @@ export async function runNonInteractivePrompt(input: Input) {
324351 }
325352
326353 if ( event . type === "session.step.ended" ) {
354+ flushStep ( )
327355 const part = {
328356 id : partID ( event . id ) ,
329357 sessionID : input . sessionID ,
@@ -338,13 +366,28 @@ export async function runNonInteractivePrompt(input: Input) {
338366 continue
339367 }
340368 if ( event . type === "session.step.failed" ) {
369+ if (
370+ input . compatibility === "v1" &&
371+ event . data . error . message === "Provider stream ended without a terminal finish event"
372+ ) {
373+ pendingStep = undefined
374+ v1InvalidOutput = true
375+ continue
376+ }
341377 if ( interrupted || permissionRejected || questionRejected || formCancelled ) continue
378+ flushStep ( )
342379 emittedError = true
343380 process . exitCode = 1
344381 if ( ! emit ( "error" , time , { error : event . data . error } ) ) UI . error ( event . data . error . message )
345382 continue
346383 }
347384 if ( event . type === "session.execution.failed" ) {
385+ if (
386+ input . compatibility === "v1" &&
387+ ( v1InvalidOutput || permissionRejected || questionRejected || formCancelled )
388+ )
389+ return
390+ flushStep ( )
348391 if ( ! emittedError && ! questionRejected && ! formCancelled ) {
349392 emittedError = true
350393 process . exitCode = 1
@@ -353,6 +396,7 @@ export async function runNonInteractivePrompt(input: Input) {
353396 return
354397 }
355398 if ( event . type === "session.execution.interrupted" ) {
399+ if ( input . compatibility === "v1" && ( permissionRejected || questionRejected || formCancelled ) ) return
356400 if ( event . data . reason === "user" && interrupted ) process . exitCode = 130
357401 if ( event . data . reason !== "user" && ! emittedError ) {
358402 emittedError = true
@@ -478,7 +522,7 @@ async function prepareFile(file: File) {
478522 const uri = file . url . startsWith ( "data:" )
479523 ? file . url
480524 : `data:${ file . mime } ;base64,${ ( await readFile ( new URL ( file . url ) ) ) . toString ( "base64" ) } `
481- return { attachment : { uri, mime : file . mime , name : file . filename } }
525+ return { attachment : { uri, name : file . filename } }
482526 }
483527 const content = file . url . startsWith ( "data:" )
484528 ? Buffer . from ( file . url . slice ( file . url . indexOf ( "," ) + 1 ) , "base64" ) . toString ( "utf8" )
0 commit comments