Implement compact, undo, and init commands

- Add compact command with proper API parameters (providerID, modelID)
- Implement undo command matching TUI behavior:
  - Find previous user message before revert point
  - Restore reverted message to prompt input
  - Filter messages client-side based on session.revert field
- Add init command with proper hex-encoded message ID generation
- Add session.revert field handling:
  - Parse revert field from API responses in fetchSessions and createSession
  - Update revert field via SSE session.updated events
  - Filter messages during render when revert point is set
- Add SSE event handlers for session.compacted and session.error
- Add force reload option to loadMessages for post-compact refresh
- Messages now filter instantly based on revert state without API reload
This commit is contained in:
Shantur Rathore
2025-10-24 17:41:55 +01:00
parent a4968e9eb5
commit 3edd852ee2
5 changed files with 195 additions and 9 deletions

View File

@@ -93,6 +93,7 @@ const SessionView: Component<{
sessionId={s().id}
messages={s().messages || []}
messagesInfo={s().messagesInfo}
revert={s().revert}
/>
<PromptInput
instanceId={props.instanceId}
@@ -337,11 +338,23 @@ const App: Component = () => {
const sessionId = activeSessionIdForInstance()
if (!instance || !instance.client || !sessionId || sessionId === "logs") return
const sessions = getSessions(instance.id)
const session = sessions.find((s) => s.id === sessionId)
if (!session) return
try {
await instance.client.session.summarize({ path: { id: sessionId } })
console.log("Session compacted")
} catch (error) {
console.log("Compacting session...")
await instance.client.session.summarize({
path: { id: sessionId },
body: {
providerID: session.model.providerId,
modelID: session.model.modelId,
},
})
} catch (error: any) {
console.error("Failed to compact session:", error)
const message = error?.message || "Failed to compact session"
alert(`Compact failed: ${message}`)
}
},
})
@@ -357,11 +370,76 @@ const App: Component = () => {
const sessionId = activeSessionIdForInstance()
if (!instance || !instance.client || !sessionId || sessionId === "logs") return
const sessions = getSessions(instance.id)
const session = sessions.find((s) => s.id === sessionId)
if (!session) return
// Find the message to revert to (previous user message before revert point)
let after = 0
const revert = session.revert
if (revert?.messageID) {
// Find the timestamp of the revert point
for (let i = session.messages.length - 1; i >= 0; i--) {
const msg = session.messages[i]
const info = session.messagesInfo.get(msg.id)
if (info?.id === revert.messageID) {
after = info.time?.created || 0
break
}
}
}
// Find the previous user message
let messageID = ""
for (let i = session.messages.length - 1; i >= 0; i--) {
const msg = session.messages[i]
const info = session.messagesInfo.get(msg.id)
if (msg.type === "user" && info?.time?.created) {
if (after > 0 && info.time.created >= after) {
continue
}
messageID = msg.id
break
}
}
if (!messageID) {
alert("Nothing to undo")
return
}
try {
await instance.client.session.revert({ path: { id: sessionId } })
console.log("Last message reverted")
// Find the reverted message to restore to input
const revertedMessage = session.messages.find((m) => m.id === messageID)
const revertedInfo = session.messagesInfo.get(messageID)
console.log("Reverting to message:", messageID)
await instance.client.session.revert({
path: { id: sessionId },
body: { messageID },
})
console.log("Revert API call completed")
// Restore the reverted user message to the prompt input
if (revertedMessage && revertedInfo?.role === "user") {
const textParts = revertedMessage.parts.filter((p: any) => p.type === "text")
if (textParts.length > 0) {
const textarea = document.querySelector(".prompt-input") as HTMLTextAreaElement
if (textarea) {
textarea.value = textParts.map((p: any) => p.text).join("\n")
textarea.focus()
}
}
}
console.log("Last message reverted - UI will update via SSE")
} catch (error) {
console.error("Failed to revert message:", error)
alert("Failed to revert message")
}
},
})
@@ -429,9 +507,26 @@ const App: Component = () => {
const sessionId = activeSessionIdForInstance()
if (!instance || !instance.client || !sessionId || sessionId === "logs") return
const sessions = getSessions(instance.id)
const session = sessions.find((s) => s.id === sessionId)
if (!session) return
try {
await instance.client.session.init({ path: { id: sessionId } })
console.log("Initialized AGENTS.md")
// Generate ID similar to server format: timestamp in hex + random chars
const timestamp = Date.now()
const timePart = (timestamp * 0x1000).toString(16).padStart(12, "0")
const randomPart = Math.random().toString(16).substring(2, 16)
const messageID = `msg_${timePart}${randomPart}`
await instance.client.session.init({
path: { id: sessionId },
body: {
messageID,
providerID: session.model.providerId,
modelID: session.model.modelId,
},
})
console.log("Initializing AGENTS.md...")
} catch (error) {
console.error("Failed to initialize AGENTS.md:", error)
}

View File

@@ -10,6 +10,12 @@ interface MessageStreamProps {
sessionId: string
messages: Message[]
messagesInfo?: Map<string, any>
revert?: {
messageID: string
partID?: string
snapshot?: string
diff?: string
}
loading?: boolean
}
@@ -57,6 +63,12 @@ export default function MessageStream(props: MessageStreamProps) {
for (const message of props.messages) {
const messageInfo = props.messagesInfo?.get(message.id)
// If we hit the revert point, stop rendering messages
if (props.revert?.messageID && message.id === props.revert.messageID) {
break
}
const textParts = message.parts.filter((p) => p.type === "text" && !p.synthetic)
const toolParts = message.parts.filter((p) => p.type === "tool")
const reasoningParts = message.parts.filter((p) => p.type === "reasoning")

View File

@@ -92,6 +92,12 @@ class SSEManager {
case "session.updated":
this.onSessionUpdate?.(instanceId, event)
break
case "session.compacted":
this.onSessionCompacted?.(instanceId, event)
break
case "session.error":
this.onSessionError?.(instanceId, event)
break
case "session.idle":
console.log("[SSE] Session idle")
break
@@ -131,6 +137,8 @@ class SSEManager {
onMessageUpdate?: (instanceId: string, event: MessageUpdateEvent) => void
onSessionUpdate?: (instanceId: string, event: SessionUpdateEvent) => void
onSessionCompacted?: (instanceId: string, event: any) => void
onSessionError?: (instanceId: string, event: any) => void
getStatus(instanceId: string): "connecting" | "connected" | "disconnected" | "error" | null {
return connectionStatus().get(instanceId) ?? null

View File

@@ -52,6 +52,14 @@ async function fetchSessions(instanceId: string): Promise<void> {
created: apiSession.time.created,
updated: apiSession.time.updated,
},
revert: apiSession.revert
? {
messageID: apiSession.revert.messageID,
partID: apiSession.revert.partID,
snapshot: apiSession.revert.snapshot,
diff: apiSession.revert.diff,
}
: undefined,
messages: [],
messagesInfo: new Map(),
})
@@ -153,6 +161,14 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
created: response.data.time.created,
updated: response.data.time.updated,
},
revert: response.data.revert
? {
messageID: response.data.revert.messageID,
partID: response.data.revert.partID,
snapshot: response.data.revert.snapshot,
diff: response.data.revert.diff,
}
: undefined,
messages: [],
messagesInfo: new Map(),
}
@@ -358,9 +374,21 @@ function getSessionFamily(instanceId: string, parentId: string): Session[] {
return [parent, ...children]
}
async function loadMessages(instanceId: string, sessionId: string): Promise<void> {
async function loadMessages(instanceId: string, sessionId: string, force = false): Promise<void> {
// If force reload, clear the loaded cache
if (force) {
setMessagesLoaded((prev) => {
const next = new Map(prev)
const loadedSet = next.get(instanceId)
if (loadedSet) {
loadedSet.delete(sessionId)
}
return next
})
}
const alreadyLoaded = messagesLoaded().get(instanceId)?.has(sessionId)
if (alreadyLoaded) {
if (alreadyLoaded && !force) {
return
}
@@ -631,6 +659,14 @@ function handleSessionUpdate(instanceId: string, event: any): void {
...existingSession.time,
updated: info.time?.updated || Date.now(),
},
revert: info.revert
? {
messageID: info.revert.messageID,
partID: info.revert.partID,
snapshot: info.revert.snapshot,
diff: info.revert.diff,
}
: existingSession.revert,
}
setSessions((prev) => {
@@ -812,8 +848,37 @@ async function updateSessionModel(
})
}
function handleSessionCompacted(instanceId: string, event: any): void {
const sessionID = event.properties?.sessionID
if (!sessionID) return
console.log(`[SSE] Session compacted: ${sessionID}`)
loadMessages(instanceId, sessionID, true).catch(console.error)
}
function handleSessionError(instanceId: string, event: any): void {
const error = event.properties?.error
const sessionID = event.properties?.sessionID
console.error(`[SSE] Session error:`, error)
let message = error?.data?.message || error?.message || "Unknown error"
if (error?.data?.responseBody) {
try {
const body = JSON.parse(error.data.responseBody)
if (body.error) {
message = body.error
}
} catch {}
}
alert(`Error: ${message}`)
}
sseManager.onMessageUpdate = handleMessageUpdate
sseManager.onSessionUpdate = handleSessionUpdate
sseManager.onSessionCompacted = handleSessionCompacted
sseManager.onSessionError = handleSessionError
export {
sessions,

View File

@@ -14,6 +14,12 @@ export interface Session {
created: number
updated: number
}
revert?: {
messageID: string
partID?: string
snapshot?: string
diff?: string
}
messages: Message[]
messagesInfo: Map<string, any>
}