diff --git a/src/App.tsx b/src/App.tsx index 68414182..dcca4cb2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -93,6 +93,7 @@ const SessionView: Component<{ sessionId={s().id} messages={s().messages || []} messagesInfo={s().messagesInfo} + revert={s().revert} /> { 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) } diff --git a/src/components/message-stream.tsx b/src/components/message-stream.tsx index 0f60a1a1..bc958da4 100644 --- a/src/components/message-stream.tsx +++ b/src/components/message-stream.tsx @@ -10,6 +10,12 @@ interface MessageStreamProps { sessionId: string messages: Message[] messagesInfo?: Map + 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") diff --git a/src/lib/sse-manager.ts b/src/lib/sse-manager.ts index 297f2b75..52053d12 100644 --- a/src/lib/sse-manager.ts +++ b/src/lib/sse-manager.ts @@ -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 diff --git a/src/stores/sessions.ts b/src/stores/sessions.ts index 89d2c6f6..ca96305a 100644 --- a/src/stores/sessions.ts +++ b/src/stores/sessions.ts @@ -52,6 +52,14 @@ async function fetchSessions(instanceId: string): Promise { 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 { +async function loadMessages(instanceId: string, sessionId: string, force = false): Promise { + // 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, diff --git a/src/types/session.ts b/src/types/session.ts index 6c9dd8ee..553dc0cd 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -14,6 +14,12 @@ export interface Session { created: number updated: number } + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } messages: Message[] messagesInfo: Map }