@@ -28,7 +28,7 @@ import { Config } from "@/config/config"
2828import { Todo } from "@/session/todo"
2929import { z } from "zod"
3030import { LoadAPIKeyError } from "ai"
31- import type { OpencodeClient } from "@opencode-ai/sdk/v2"
31+ import type { OpencodeClient , SessionMessageResponse } from "@opencode-ai/sdk/v2"
3232
3333export namespace ACP {
3434 const log = Log . create ( { service : "acp-agent" } )
@@ -386,7 +386,7 @@ export namespace ACP {
386386
387387 log . info ( "creating_session" , { sessionId, mcpServers : params . mcpServers . length } )
388388
389- const load = await this . loadSession ( {
389+ const load = await this . loadSessionMode ( {
390390 cwd : directory ,
391391 mcpServers : params . mcpServers ,
392392 sessionId,
@@ -412,6 +412,242 @@ export namespace ACP {
412412 }
413413
414414 async loadSession ( params : LoadSessionRequest ) {
415+ const directory = params . cwd
416+ const sessionId = params . sessionId
417+
418+ try {
419+ const model = await defaultModel ( this . config , directory )
420+
421+ // Store ACP session state
422+ const state = await this . sessionManager . load ( sessionId , params . cwd , params . mcpServers , model )
423+
424+ log . info ( "load_session" , { sessionId, mcpServers : params . mcpServers . length } )
425+
426+ const mode = await this . loadSessionMode ( {
427+ cwd : directory ,
428+ mcpServers : params . mcpServers ,
429+ sessionId,
430+ } )
431+
432+ this . setupEventSubscriptions ( state )
433+
434+ // Replay session history
435+ const messages = await this . sdk . session
436+ . messages (
437+ {
438+ sessionID : sessionId ,
439+ directory,
440+ } ,
441+ { throwOnError : true } ,
442+ )
443+ . then ( ( x ) => x . data )
444+ . catch ( ( err ) => {
445+ log . error ( "unexpected error when fetching message" , { error : err } )
446+ return undefined
447+ } )
448+
449+ for ( const msg of messages ?? [ ] ) {
450+ log . debug ( "replay message" , msg )
451+ await this . processMessage ( msg )
452+ }
453+
454+ return mode
455+ } catch ( e ) {
456+ const error = MessageV2 . fromError ( e , {
457+ providerID : this . config . defaultModel ?. providerID ?? "unknown" ,
458+ } )
459+ if ( LoadAPIKeyError . isInstance ( error ) ) {
460+ throw RequestError . authRequired ( )
461+ }
462+ throw e
463+ }
464+ }
465+
466+ private async processMessage ( message : SessionMessageResponse ) {
467+ log . debug ( "process message" , message )
468+ if ( message . info . role !== "assistant" && message . info . role !== "user" ) return
469+ const sessionId = message . info . sessionID
470+
471+ for ( const part of message . parts ) {
472+ if ( part . type === "tool" ) {
473+ switch ( part . state . status ) {
474+ case "pending" :
475+ await this . connection
476+ . sessionUpdate ( {
477+ sessionId,
478+ update : {
479+ sessionUpdate : "tool_call" ,
480+ toolCallId : part . callID ,
481+ title : part . tool ,
482+ kind : toToolKind ( part . tool ) ,
483+ status : "pending" ,
484+ locations : [ ] ,
485+ rawInput : { } ,
486+ } ,
487+ } )
488+ . catch ( ( err ) => {
489+ log . error ( "failed to send tool pending to ACP" , { error : err } )
490+ } )
491+ break
492+ case "running" :
493+ await this . connection
494+ . sessionUpdate ( {
495+ sessionId,
496+ update : {
497+ sessionUpdate : "tool_call_update" ,
498+ toolCallId : part . callID ,
499+ status : "in_progress" ,
500+ locations : toLocations ( part . tool , part . state . input ) ,
501+ rawInput : part . state . input ,
502+ } ,
503+ } )
504+ . catch ( ( err ) => {
505+ log . error ( "failed to send tool in_progress to ACP" , { error : err } )
506+ } )
507+ break
508+ case "completed" :
509+ const kind = toToolKind ( part . tool )
510+ const content : ToolCallContent [ ] = [
511+ {
512+ type : "content" ,
513+ content : {
514+ type : "text" ,
515+ text : part . state . output ,
516+ } ,
517+ } ,
518+ ]
519+
520+ if ( kind === "edit" ) {
521+ const input = part . state . input
522+ const filePath = typeof input [ "filePath" ] === "string" ? input [ "filePath" ] : ""
523+ const oldText = typeof input [ "oldString" ] === "string" ? input [ "oldString" ] : ""
524+ const newText =
525+ typeof input [ "newString" ] === "string"
526+ ? input [ "newString" ]
527+ : typeof input [ "content" ] === "string"
528+ ? input [ "content" ]
529+ : ""
530+ content . push ( {
531+ type : "diff" ,
532+ path : filePath ,
533+ oldText,
534+ newText,
535+ } )
536+ }
537+
538+ if ( part . tool === "todowrite" ) {
539+ const parsedTodos = z . array ( Todo . Info ) . safeParse ( JSON . parse ( part . state . output ) )
540+ if ( parsedTodos . success ) {
541+ await this . connection
542+ . sessionUpdate ( {
543+ sessionId,
544+ update : {
545+ sessionUpdate : "plan" ,
546+ entries : parsedTodos . data . map ( ( todo ) => {
547+ const status : PlanEntry [ "status" ] =
548+ todo . status === "cancelled" ? "completed" : ( todo . status as PlanEntry [ "status" ] )
549+ return {
550+ priority : "medium" ,
551+ status,
552+ content : todo . content ,
553+ }
554+ } ) ,
555+ } ,
556+ } )
557+ . catch ( ( err ) => {
558+ log . error ( "failed to send session update for todo" , { error : err } )
559+ } )
560+ } else {
561+ log . error ( "failed to parse todo output" , { error : parsedTodos . error } )
562+ }
563+ }
564+
565+ await this . connection
566+ . sessionUpdate ( {
567+ sessionId,
568+ update : {
569+ sessionUpdate : "tool_call_update" ,
570+ toolCallId : part . callID ,
571+ status : "completed" ,
572+ kind,
573+ content,
574+ title : part . state . title ,
575+ rawOutput : {
576+ output : part . state . output ,
577+ metadata : part . state . metadata ,
578+ } ,
579+ } ,
580+ } )
581+ . catch ( ( err ) => {
582+ log . error ( "failed to send tool completed to ACP" , { error : err } )
583+ } )
584+ break
585+ case "error" :
586+ await this . connection
587+ . sessionUpdate ( {
588+ sessionId,
589+ update : {
590+ sessionUpdate : "tool_call_update" ,
591+ toolCallId : part . callID ,
592+ status : "failed" ,
593+ content : [
594+ {
595+ type : "content" ,
596+ content : {
597+ type : "text" ,
598+ text : part . state . error ,
599+ } ,
600+ } ,
601+ ] ,
602+ rawOutput : {
603+ error : part . state . error ,
604+ } ,
605+ } ,
606+ } )
607+ . catch ( ( err ) => {
608+ log . error ( "failed to send tool error to ACP" , { error : err } )
609+ } )
610+ break
611+ }
612+ } else if ( part . type === "text" ) {
613+ if ( part . text ) {
614+ await this . connection
615+ . sessionUpdate ( {
616+ sessionId,
617+ update : {
618+ sessionUpdate : message . info . role === "user" ? "user_message_chunk" : "agent_message_chunk" ,
619+ content : {
620+ type : "text" ,
621+ text : part . text ,
622+ } ,
623+ } ,
624+ } )
625+ . catch ( ( err ) => {
626+ log . error ( "failed to send text to ACP" , { error : err } )
627+ } )
628+ }
629+ } else if ( part . type === "reasoning" ) {
630+ if ( part . text ) {
631+ await this . connection
632+ . sessionUpdate ( {
633+ sessionId,
634+ update : {
635+ sessionUpdate : "agent_thought_chunk" ,
636+ content : {
637+ type : "text" ,
638+ text : part . text ,
639+ } ,
640+ } ,
641+ } )
642+ . catch ( ( err ) => {
643+ log . error ( "failed to send reasoning to ACP" , { error : err } )
644+ } )
645+ }
646+ }
647+ }
648+ }
649+
650+ private async loadSessionMode ( params : LoadSessionRequest ) {
415651 const directory = params . cwd
416652 const model = await defaultModel ( this . config , directory )
417653 const sessionId = params . sessionId
0 commit comments