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:
109
src/App.tsx
109
src/App.tsx
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user