feat(tui): wire up undo/redo and revert for V2 sessions (#34263)

This commit is contained in:
Dax 2026-06-27 20:04:53 -04:00 committed by GitHub
commit d1d7ebc2c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 478 additions and 87 deletions

View file

@ -102,6 +102,9 @@ export class PromptConflictError extends Schema.TaggedErrorClass<PromptConflictE
sessionID: SessionSchema.ID,
messageID: SessionMessage.ID,
}) {}
export class BusyError extends Schema.TaggedErrorClass<BusyError>()("Session.BusyError", {
sessionID: SessionSchema.ID,
}) {}
export const MessageNotFoundError = SessionRevert.MessageNotFoundError
export type MessageNotFoundError = SessionRevert.MessageNotFoundError
@ -136,6 +139,10 @@ export interface Interface {
sessionID: SessionSchema.ID
model: ModelV2.Ref
}) => Effect.Effect<void, NotFoundError>
readonly rename: (input: {
sessionID: SessionSchema.ID
title: string
}) => Effect.Effect<void, NotFoundError>
readonly prompt: (input: {
id?: SessionMessage.ID
sessionID: SessionSchema.ID
@ -165,9 +172,9 @@ export interface Interface {
sessionID: SessionSchema.ID
messageID: SessionMessage.ID
files?: boolean
}) => Effect.Effect<Revert.State, NotFoundError | MessageNotFoundError | Snapshot.Error>
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | Snapshot.Error>
readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
}) => Effect.Effect<Revert.State, NotFoundError | MessageNotFoundError | BusyError | Snapshot.Error>
readonly clear: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError | Snapshot.Error>
readonly commit: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError | BusyError>
}
}
@ -404,6 +411,14 @@ export const layer = Layer.unwrap(
model: input.model,
})
}),
rename: Effect.fn("V2Session.rename")(function* (input) {
yield* result.get(input.sessionID)
yield* events.publish(SessionEvent.Renamed, {
sessionID: input.sessionID,
timestamp: yield* DateTime.now,
title: input.title,
})
}),
compact: Effect.fn("V2Session.compact")(function* (input) {
yield* result.get(input.sessionID)
return yield* new OperationUnavailableError({ operation: "compact" })
@ -423,6 +438,8 @@ export const layer = Layer.unwrap(
revert: {
stage: Effect.fn("V2Session.revert.stage")(function* (input) {
const session = yield* result.get(input.sessionID)
if ((yield* execution.active).has(input.sessionID))
return yield* new BusyError({ sessionID: input.sessionID })
return yield* SessionRevert.stage({ session, messageID: input.messageID, files: input.files }).pipe(
Effect.provideService(Database.Service, database),
Effect.provideService(EventV2.Service, events),
@ -431,6 +448,8 @@ export const layer = Layer.unwrap(
}),
clear: Effect.fn("V2Session.revert.clear")(function* (sessionID) {
const session = yield* result.get(sessionID)
if ((yield* execution.active).has(sessionID))
return yield* new BusyError({ sessionID })
yield* SessionRevert.clear(session).pipe(
Effect.provideService(EventV2.Service, events),
Effect.provide(locations.get(session.location)),
@ -438,6 +457,8 @@ export const layer = Layer.unwrap(
}),
commit: Effect.fn("V2Session.revert.commit")(function* (sessionID) {
const session = yield* result.get(sessionID)
if ((yield* execution.active).has(sessionID))
return yield* new BusyError({ sessionID })
yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events))
}),
},

View file

@ -123,6 +123,7 @@ export function update(adapter: Adapter, event: SessionEvent.Event) {
)
},
"session.next.moved": () => Effect.void,
"session.next.renamed": () => Effect.void,
"session.next.prompted": (event) => {
return adapter.appendMessage(
SessionMessage.User.make({

View file

@ -347,6 +347,14 @@ export const layer = Layer.effectDiscard(
yield* run(db, event)
}),
)
yield* events.project(SessionEvent.Renamed, (event) =>
db
.update(SessionTable)
.set({ title: event.data.title, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
.where(eq(SessionTable.id, event.data.sessionID))
.run()
.pipe(Effect.orDie),
)
yield* events.project(SessionEvent.Prompted, (event) =>
Effect.gen(function* () {
if (event.durable === undefined) return yield* Effect.die("Durable Session event is missing aggregate sequence")