11import z from "zod"
2- import { SessionID , MessageID , PartID } from "./schema"
2+ import { Effect , Layer , ServiceMap } from "effect"
3+ import { makeRuntime } from "@/effect/run-service"
4+ import { Bus } from "../bus"
35import { Snapshot } from "../snapshot"
4- import { MessageV2 } from "./message-v2"
5- import { Session } from "."
6- import { Log } from "../util/log"
7- import { SyncEvent } from "../sync"
86import { Storage } from "@/storage/storage"
9- import { Bus } from "../bus"
7+ import { SyncEvent } from "../sync"
8+ import { Log } from "../util/log"
9+ import { Session } from "."
10+ import { MessageV2 } from "./message-v2"
11+ import { SessionID , MessageID , PartID } from "./schema"
1012import { SessionPrompt } from "./prompt"
1113import { SessionSummary } from "./summary"
1214
@@ -20,116 +22,152 @@ export namespace SessionRevert {
2022 } )
2123 export type RevertInput = z . infer < typeof RevertInput >
2224
23- export async function revert ( input : RevertInput ) {
24- await SessionPrompt . assertNotBusy ( input . sessionID )
25- const all = await Session . messages ( { sessionID : input . sessionID } )
26- let lastUser : MessageV2 . User | undefined
27- const session = await Session . get ( input . sessionID )
28-
29- let revert : Session . Info [ "revert" ]
30- const patches : Snapshot . Patch [ ] = [ ]
31- for ( const msg of all ) {
32- if ( msg . info . role === "user" ) lastUser = msg . info
33- const remaining = [ ]
34- for ( const part of msg . parts ) {
35- if ( revert ) {
36- if ( part . type === "patch" ) {
37- patches . push ( part )
25+ export interface Interface {
26+ readonly revert : ( input : RevertInput ) => Effect . Effect < Session . Info >
27+ readonly unrevert : ( input : { sessionID : SessionID } ) => Effect . Effect < Session . Info >
28+ readonly cleanup : ( session : Session . Info ) => Effect . Effect < void >
29+ }
30+
31+ export class Service extends ServiceMap . Service < Service , Interface > ( ) ( "@opencode/SessionRevert" ) { }
32+
33+ export const layer = Layer . effect (
34+ Service ,
35+ Effect . gen ( function * ( ) {
36+ const sessions = yield * Session . Service
37+ const snap = yield * Snapshot . Service
38+ const storage = yield * Storage . Service
39+ const bus = yield * Bus . Service
40+
41+ const revert = Effect . fn ( "SessionRevert.revert" ) ( function * ( input : RevertInput ) {
42+ yield * Effect . promise ( ( ) => SessionPrompt . assertNotBusy ( input . sessionID ) )
43+ const all = yield * sessions . messages ( { sessionID : input . sessionID } )
44+ let lastUser : MessageV2 . User | undefined
45+ const session = yield * sessions . get ( input . sessionID )
46+
47+ let rev : Session . Info [ "revert" ]
48+ const patches : Snapshot . Patch [ ] = [ ]
49+ for ( const msg of all ) {
50+ if ( msg . info . role === "user" ) lastUser = msg . info
51+ const remaining = [ ]
52+ for ( const part of msg . parts ) {
53+ if ( rev ) {
54+ if ( part . type === "patch" ) patches . push ( part )
55+ continue
56+ }
57+
58+ if ( ! rev ) {
59+ if ( ( msg . info . id === input . messageID && ! input . partID ) || part . id === input . partID ) {
60+ const partID = remaining . some ( ( item ) => [ "text" , "tool" ] . includes ( item . type ) ) ? input . partID : undefined
61+ rev = {
62+ messageID : ! partID && lastUser ? lastUser . id : msg . info . id ,
63+ partID,
64+ }
65+ }
66+ remaining . push ( part )
67+ }
3868 }
39- continue
4069 }
4170
42- if ( ! revert ) {
43- if ( ( msg . info . id === input . messageID && ! input . partID ) || part . id === input . partID ) {
44- // if no useful parts left in message, same as reverting whole message
45- const partID = remaining . some ( ( item ) => [ "text" , "tool" ] . includes ( item . type ) ) ? input . partID : undefined
46- revert = {
47- messageID : ! partID && lastUser ? lastUser . id : msg . info . id ,
48- partID,
71+ if ( ! rev ) return session
72+
73+ rev . snapshot = session . revert ?. snapshot ?? ( yield * snap . track ( ) )
74+ yield * snap . revert ( patches )
75+ if ( rev . snapshot ) rev . diff = yield * snap . diff ( rev . snapshot as string )
76+ const range = all . filter ( ( msg ) => msg . info . id >= rev ! . messageID )
77+ const diffs = yield * Effect . promise ( ( ) => SessionSummary . computeDiff ( { messages : range } ) )
78+ yield * storage . write ( [ "session_diff" , input . sessionID ] , diffs ) . pipe ( Effect . ignore )
79+ yield * bus . publish ( Session . Event . Diff , { sessionID : input . sessionID , diff : diffs } )
80+ yield * sessions . setRevert ( {
81+ sessionID : input . sessionID ,
82+ revert : rev ,
83+ summary : {
84+ additions : diffs . reduce ( ( sum , x ) => sum + x . additions , 0 ) ,
85+ deletions : diffs . reduce ( ( sum , x ) => sum + x . deletions , 0 ) ,
86+ files : diffs . length ,
87+ } ,
88+ } )
89+ return yield * sessions . get ( input . sessionID )
90+ } )
91+
92+ const unrevert = Effect . fn ( "SessionRevert.unrevert" ) ( function * ( input : { sessionID : SessionID } ) {
93+ log . info ( "unreverting" , input )
94+ yield * Effect . promise ( ( ) => SessionPrompt . assertNotBusy ( input . sessionID ) )
95+ const session = yield * sessions . get ( input . sessionID )
96+ if ( ! session . revert ) return session
97+ if ( session . revert . snapshot ) yield * snap . restore ( session . revert ! . snapshot ! )
98+ yield * sessions . clearRevert ( input . sessionID )
99+ return yield * sessions . get ( input . sessionID )
100+ } )
101+
102+ const cleanup = Effect . fn ( "SessionRevert.cleanup" ) ( function * ( session : Session . Info ) {
103+ if ( ! session . revert ) return
104+ const sessionID = session . id
105+ const msgs = yield * sessions . messages ( { sessionID } )
106+ const messageID = session . revert . messageID
107+ const remove = [ ] as MessageV2 . WithParts [ ]
108+ let target : MessageV2 . WithParts | undefined
109+ for ( const msg of msgs ) {
110+ if ( msg . info . id < messageID ) continue
111+ if ( msg . info . id > messageID ) {
112+ remove . push ( msg )
113+ continue
114+ }
115+ if ( session . revert . partID ) {
116+ target = msg
117+ continue
118+ }
119+ remove . push ( msg )
120+ }
121+ for ( const msg of remove ) {
122+ SyncEvent . run ( MessageV2 . Event . Removed , {
123+ sessionID,
124+ messageID : msg . info . id ,
125+ } )
126+ }
127+ if ( session . revert . partID && target ) {
128+ const partID = session . revert . partID
129+ const idx = target . parts . findIndex ( ( part ) => part . id === partID )
130+ if ( idx >= 0 ) {
131+ const removeParts = target . parts . slice ( idx )
132+ target . parts = target . parts . slice ( 0 , idx )
133+ for ( const part of removeParts ) {
134+ SyncEvent . run ( MessageV2 . Event . PartRemoved , {
135+ sessionID,
136+ messageID : target . info . id ,
137+ partID : part . id ,
138+ } )
49139 }
50140 }
51- remaining . push ( part )
52141 }
53- }
54- }
55-
56- if ( revert ) {
57- const session = await Session . get ( input . sessionID )
58- revert . snapshot = session . revert ?. snapshot ?? ( await Snapshot . track ( ) )
59- await Snapshot . revert ( patches )
60- if ( revert . snapshot ) revert . diff = await Snapshot . diff ( revert . snapshot )
61- const rangeMessages = all . filter ( ( msg ) => msg . info . id >= revert ! . messageID )
62- const diffs = await SessionSummary . computeDiff ( { messages : rangeMessages } )
63- await Storage . write ( [ "session_diff" , input . sessionID ] , diffs )
64- Bus . publish ( Session . Event . Diff , {
65- sessionID : input . sessionID ,
66- diff : diffs ,
67- } )
68- return Session . setRevert ( {
69- sessionID : input . sessionID ,
70- revert,
71- summary : {
72- additions : diffs . reduce ( ( sum , x ) => sum + x . additions , 0 ) ,
73- deletions : diffs . reduce ( ( sum , x ) => sum + x . deletions , 0 ) ,
74- files : diffs . length ,
75- } ,
142+ yield * sessions . clearRevert ( sessionID )
76143 } )
77- }
78- return session
144+
145+ return Service . of ( { revert, unrevert, cleanup } )
146+ } ) ,
147+ )
148+
149+ export const defaultLayer = Layer . unwrap (
150+ Effect . sync ( ( ) =>
151+ layer . pipe (
152+ Layer . provide ( Session . defaultLayer ) ,
153+ Layer . provide ( Snapshot . defaultLayer ) ,
154+ Layer . provide ( Storage . defaultLayer ) ,
155+ Layer . provide ( Bus . layer ) ,
156+ ) ,
157+ ) ,
158+ )
159+
160+ const { runPromise } = makeRuntime ( Service , defaultLayer )
161+
162+ export async function revert ( input : RevertInput ) {
163+ return runPromise ( ( svc ) => svc . revert ( input ) )
79164 }
80165
81166 export async function unrevert ( input : { sessionID : SessionID } ) {
82- log . info ( "unreverting" , input )
83- await SessionPrompt . assertNotBusy ( input . sessionID )
84- const session = await Session . get ( input . sessionID )
85- if ( ! session . revert ) return session
86- if ( session . revert . snapshot ) await Snapshot . restore ( session . revert . snapshot )
87- return Session . clearRevert ( input . sessionID )
167+ return runPromise ( ( svc ) => svc . unrevert ( input ) )
88168 }
89169
90170 export async function cleanup ( session : Session . Info ) {
91- if ( ! session . revert ) return
92- const sessionID = session . id
93- const msgs = await Session . messages ( { sessionID } )
94- const messageID = session . revert . messageID
95- const remove = [ ] as MessageV2 . WithParts [ ]
96- let target : MessageV2 . WithParts | undefined
97- for ( const msg of msgs ) {
98- if ( msg . info . id < messageID ) {
99- continue
100- }
101- if ( msg . info . id > messageID ) {
102- remove . push ( msg )
103- continue
104- }
105- if ( session . revert . partID ) {
106- target = msg
107- continue
108- }
109- remove . push ( msg )
110- }
111- for ( const msg of remove ) {
112- SyncEvent . run ( MessageV2 . Event . Removed , {
113- sessionID : sessionID ,
114- messageID : msg . info . id ,
115- } )
116- }
117- if ( session . revert . partID && target ) {
118- const partID = session . revert . partID
119- const removeStart = target . parts . findIndex ( ( part ) => part . id === partID )
120- if ( removeStart >= 0 ) {
121- const preserveParts = target . parts . slice ( 0 , removeStart )
122- const removeParts = target . parts . slice ( removeStart )
123- target . parts = preserveParts
124- for ( const part of removeParts ) {
125- SyncEvent . run ( MessageV2 . Event . PartRemoved , {
126- sessionID : sessionID ,
127- messageID : target . info . id ,
128- partID : part . id ,
129- } )
130- }
131- }
132- }
133- await Session . clearRevert ( sessionID )
171+ return runPromise ( ( svc ) => svc . cleanup ( session ) )
134172 }
135173}
0 commit comments