Skip to content

Commit 3fc0367

Browse files
authored
refactor(session): effectify SessionRevert service (#20143)
1 parent 954a6ca commit 3fc0367

2 files changed

Lines changed: 287 additions & 105 deletions

File tree

Lines changed: 142 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import 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"
35
import { Snapshot } from "../snapshot"
4-
import { MessageV2 } from "./message-v2"
5-
import { Session } from "."
6-
import { Log } from "../util/log"
7-
import { SyncEvent } from "../sync"
86
import { 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"
1012
import { SessionPrompt } from "./prompt"
1113
import { 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

Comments
 (0)