Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/app/src/pages/session/use-session-commands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,8 @@ export const useSessionCommands = (actions: SessionCommandContext) => {
return
}

await sdk.client.session.revert({ sessionID, messageID: next.id })
const mode = info()?.revert?.mode
await sdk.client.session.revert({ sessionID, messageID: next.id, mode })
const prev = findLast(userMessages(), (x) => x.id < next.id)
setActiveMessage(prev)
}
Expand Down
69 changes: 40 additions & 29 deletions packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,51 @@ export function DialogMessage(props: {
const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID))
const route = useRoute()

function revert(mode: "conversation" | "conversation_and_code") {
return (dialog: { clear: () => void }) => {
const msg = message()
if (!msg) return

sdk.client.session.revert({
sessionID: props.sessionID,
messageID: msg.id,
mode,
})

if (props.setPrompt) {
const parts = sync.data.part[msg.id]
const promptInfo = parts.reduce(
(agg, part) => {
if (part.type === "text") {
if (!part.synthetic) agg.input += part.text
}
if (part.type === "file") agg.parts.push(part)
return agg
},
{ input: "", parts: [] as PromptInfo["parts"] },
)
props.setPrompt(promptInfo)
}

dialog.clear()
}
}

return (
<DialogSelect
title="Message Actions"
options={[
{
title: "Revert",
value: "session.revert",
title: "Revert conversation",
value: "session.revert.conversation",
description: "undo messages only",
onSelect: revert("conversation"),
},
{
title: "Revert conversation and code",
value: "session.revert.conversation_and_code",
description: "undo messages and file changes",
onSelect: (dialog) => {
const msg = message()
if (!msg) return

void sdk.client.session.revert({
sessionID: props.sessionID,
messageID: msg.id,
})

if (props.setPrompt) {
const parts = sync.data.part[msg.id]
const promptInfo = parts.reduce(
(agg, part) => {
if (part.type === "text") {
if (!part.synthetic) agg.input += part.text
}
if (part.type === "file") agg.parts.push(strip(part))
return agg
},
{ input: "", parts: [] as PromptInfo["parts"] },
)
props.setPrompt(promptInfo)
}

dialog.clear()
},
onSelect: revert("conversation_and_code"),
},
{
title: "Copy",
Expand Down Expand Up @@ -88,7 +99,7 @@ export function DialogMessage(props: {
if (part.type === "text") {
if (!part.synthetic) agg.input += part.text
}
if (part.type === "file") agg.parts.push(part)
if (part.type === "file") agg.parts.push(strip(part))
return agg
},
{ input: "", parts: [] as PromptInfo["parts"] },
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -596,9 +596,11 @@ export function Session() {
prompt?.set({ input: "", parts: [] })
return
}
const mode = session()?.revert?.mode
void sdk.client.session.revert({
sessionID: route.sessionID,
messageID: message.id,
mode,
})
},
},
Expand Down
79 changes: 79 additions & 0 deletions packages/opencode/src/session/message-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,85 @@ export const toModelMessagesEffect = Effect.fnUntraced(function* (
tools,
},
),
}

export const stream = fn(Identifier.schema("session"), async function* (sessionID) {
const size = 50
let offset = 0
while (true) {
const rows = Database.use((db) =>
db
.select()
.from(MessageTable)
.where(eq(MessageTable.session_id, sessionID))
.orderBy(desc(MessageTable.time_created), desc(MessageTable.id))
.limit(size)
.offset(offset)
.all(),
)
if (rows.length === 0) break

const ids = rows.map((row) => row.id)
const partsByMessage = new Map<string, MessageV2.Part[]>()
if (ids.length > 0) {
const partRows = Database.use((db) =>
db
.select()
.from(PartTable)
.where(inArray(PartTable.message_id, ids))
.orderBy(PartTable.message_id, PartTable.id)
.all(),
)
for (const row of partRows) {
const part = {
...row.data,
id: row.id,
sessionID: row.session_id,
messageID: row.message_id,
} as MessageV2.Part
const list = partsByMessage.get(row.message_id)
if (list) list.push(part)
else partsByMessage.set(row.message_id, [part])
}
}

for (const row of rows) {
const info = { ...row.data, id: row.id, sessionID: row.session_id } as MessageV2.Info
yield {
info,
parts: partsByMessage.get(row.id) ?? [],
}
}

offset += rows.length
if (rows.length < size) break
}
})

export const parts = fn(Identifier.schema("message"), async (message_id) => {
const rows = Database.use((db) =>
db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(),
)
return rows.map(
(row) => ({ ...row.data, id: row.id, sessionID: row.session_id, messageID: row.message_id }) as MessageV2.Part,
)
})

export const get = fn(
z.object({
sessionID: Identifier.schema("session"),
messageID: Identifier.schema("message"),
}),
async (input): Promise<WithParts> => {
const row = Database.use((db) => db.select().from(MessageTable).where(eq(MessageTable.id, input.messageID)).get())
if (!row) throw new Error(`Message not found: ${input.messageID}`)
const info = { ...row.data, id: row.id, sessionID: row.session_id } as MessageV2.Info
return {
info,
parts: await parts(input.messageID),
}
},
>>>>>>> 01d5f3f (fix(session): deterministic message ordering to fix revert flakiness)
)
})

Expand Down
22 changes: 19 additions & 3 deletions packages/opencode/src/session/revert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const RevertInput = Schema.Struct({
sessionID: SessionID,
messageID: MessageID,
partID: Schema.optional(PartID),
mode: Schema.optional(Schema.Literal("conversation", "conversation_and_code")),
}).pipe(withStatics((s) => ({ zod: zod(s) })))
export type RevertInput = Schema.Schema.Type<typeof RevertInput>

Expand Down Expand Up @@ -45,6 +46,8 @@ export const layer = Layer.effect(
let lastUser: MessageV2.User | undefined
const session = yield* sessions.get(input.sessionID)

const mode = input.mode ?? session.revert?.mode ?? "conversation_and_code"

let rev: Session.Info["revert"]
const patches: Snapshot.Patch[] = []
for (const msg of all) {
Expand All @@ -71,17 +74,30 @@ export const layer = Layer.effect(

if (!rev) return session

const range = all.filter((msg) => msg.info.id >= rev!.messageID)
const diffs = yield* summary.computeDiff({ messages: range })

if (mode === "conversation") {
return yield* sessions.setRevert({
sessionID: input.sessionID,
revert: { ...rev, mode },
summary: {
additions: diffs.reduce((sum, x) => sum + x.additions, 0),
deletions: diffs.reduce((sum, x) => sum + x.deletions, 0),
files: diffs.length,
},
})
}

rev.snapshot = session.revert?.snapshot ?? (yield* snap.track())
if (session.revert?.snapshot) yield* snap.restore(session.revert.snapshot)
yield* snap.revert(patches)
if (rev.snapshot) rev.diff = yield* snap.diff(rev.snapshot as string)
const range = all.filter((msg) => msg.info.id >= rev!.messageID)
const diffs = yield* summary.computeDiff({ messages: range })
yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore)
yield* bus.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs })
yield* sessions.setRevert({
sessionID: input.sessionID,
revert: rev,
revert: { ...rev, mode },
summary: {
additions: diffs.reduce((sum, x) => sum + x.additions, 0),
deletions: diffs.reduce((sum, x) => sum + x.deletions, 0),
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/session/session.sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const SessionTable = sqliteTable(
summary_deletions: integer(),
summary_files: integer(),
summary_diffs: text({ mode: "json" }).$type<Snapshot.FileDiff[]>(),
revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: PartID; snapshot?: string; diff?: string }>(),
revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: PartID; snapshot?: string; diff?: string; mode?: "conversation" | "conversation_and_code" }>(),
permission: text({ mode: "json" }).$type<Permission.Ruleset>(),
...Timestamps,
time_compacting: integer(),
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ const Revert = Schema.Struct({
partID: optionalOmitUndefined(PartID),
snapshot: optionalOmitUndefined(Schema.String),
diff: optionalOmitUndefined(Schema.String),
mode: optionalOmitUndefined(Schema.Literal("conversation", "conversation_and_code")),
})

export const Info = Schema.Struct({
Expand Down
Loading
Loading