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}
|
sessionId={s().id}
|
||||||
messages={s().messages || []}
|
messages={s().messages || []}
|
||||||
messagesInfo={s().messagesInfo}
|
messagesInfo={s().messagesInfo}
|
||||||
|
revert={s().revert}
|
||||||
/>
|
/>
|
||||||
<PromptInput
|
<PromptInput
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
@@ -337,11 +338,23 @@ const App: Component = () => {
|
|||||||
const sessionId = activeSessionIdForInstance()
|
const sessionId = activeSessionIdForInstance()
|
||||||
if (!instance || !instance.client || !sessionId || sessionId === "logs") return
|
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 {
|
try {
|
||||||
await instance.client.session.summarize({ path: { id: sessionId } })
|
console.log("Compacting session...")
|
||||||
console.log("Session compacted")
|
await instance.client.session.summarize({
|
||||||
} catch (error) {
|
path: { id: sessionId },
|
||||||
|
body: {
|
||||||
|
providerID: session.model.providerId,
|
||||||
|
modelID: session.model.modelId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
console.error("Failed to compact session:", error)
|
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()
|
const sessionId = activeSessionIdForInstance()
|
||||||
if (!instance || !instance.client || !sessionId || sessionId === "logs") return
|
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 {
|
try {
|
||||||
await instance.client.session.revert({ path: { id: sessionId } })
|
// Find the reverted message to restore to input
|
||||||
console.log("Last message reverted")
|
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) {
|
} catch (error) {
|
||||||
console.error("Failed to revert message:", error)
|
console.error("Failed to revert message:", error)
|
||||||
|
alert("Failed to revert message")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -429,9 +507,26 @@ const App: Component = () => {
|
|||||||
const sessionId = activeSessionIdForInstance()
|
const sessionId = activeSessionIdForInstance()
|
||||||
if (!instance || !instance.client || !sessionId || sessionId === "logs") return
|
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 {
|
try {
|
||||||
await instance.client.session.init({ path: { id: sessionId } })
|
// Generate ID similar to server format: timestamp in hex + random chars
|
||||||
console.log("Initialized AGENTS.md")
|
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) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize AGENTS.md:", error)
|
console.error("Failed to initialize AGENTS.md:", error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ interface MessageStreamProps {
|
|||||||
sessionId: string
|
sessionId: string
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
messagesInfo?: Map<string, any>
|
messagesInfo?: Map<string, any>
|
||||||
|
revert?: {
|
||||||
|
messageID: string
|
||||||
|
partID?: string
|
||||||
|
snapshot?: string
|
||||||
|
diff?: string
|
||||||
|
}
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +63,12 @@ export default function MessageStream(props: MessageStreamProps) {
|
|||||||
|
|
||||||
for (const message of props.messages) {
|
for (const message of props.messages) {
|
||||||
const messageInfo = props.messagesInfo?.get(message.id)
|
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 textParts = message.parts.filter((p) => p.type === "text" && !p.synthetic)
|
||||||
const toolParts = message.parts.filter((p) => p.type === "tool")
|
const toolParts = message.parts.filter((p) => p.type === "tool")
|
||||||
const reasoningParts = message.parts.filter((p) => p.type === "reasoning")
|
const reasoningParts = message.parts.filter((p) => p.type === "reasoning")
|
||||||
|
|||||||
@@ -92,6 +92,12 @@ class SSEManager {
|
|||||||
case "session.updated":
|
case "session.updated":
|
||||||
this.onSessionUpdate?.(instanceId, event)
|
this.onSessionUpdate?.(instanceId, event)
|
||||||
break
|
break
|
||||||
|
case "session.compacted":
|
||||||
|
this.onSessionCompacted?.(instanceId, event)
|
||||||
|
break
|
||||||
|
case "session.error":
|
||||||
|
this.onSessionError?.(instanceId, event)
|
||||||
|
break
|
||||||
case "session.idle":
|
case "session.idle":
|
||||||
console.log("[SSE] Session idle")
|
console.log("[SSE] Session idle")
|
||||||
break
|
break
|
||||||
@@ -131,6 +137,8 @@ class SSEManager {
|
|||||||
|
|
||||||
onMessageUpdate?: (instanceId: string, event: MessageUpdateEvent) => void
|
onMessageUpdate?: (instanceId: string, event: MessageUpdateEvent) => void
|
||||||
onSessionUpdate?: (instanceId: string, event: SessionUpdateEvent) => 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 {
|
getStatus(instanceId: string): "connecting" | "connected" | "disconnected" | "error" | null {
|
||||||
return connectionStatus().get(instanceId) ?? null
|
return connectionStatus().get(instanceId) ?? null
|
||||||
|
|||||||
@@ -52,6 +52,14 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||||||
created: apiSession.time.created,
|
created: apiSession.time.created,
|
||||||
updated: apiSession.time.updated,
|
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: [],
|
messages: [],
|
||||||
messagesInfo: new Map(),
|
messagesInfo: new Map(),
|
||||||
})
|
})
|
||||||
@@ -153,6 +161,14 @@ async function createSession(instanceId: string, agent?: string): Promise<Sessio
|
|||||||
created: response.data.time.created,
|
created: response.data.time.created,
|
||||||
updated: response.data.time.updated,
|
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: [],
|
messages: [],
|
||||||
messagesInfo: new Map(),
|
messagesInfo: new Map(),
|
||||||
}
|
}
|
||||||
@@ -358,9 +374,21 @@ function getSessionFamily(instanceId: string, parentId: string): Session[] {
|
|||||||
return [parent, ...children]
|
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)
|
const alreadyLoaded = messagesLoaded().get(instanceId)?.has(sessionId)
|
||||||
if (alreadyLoaded) {
|
if (alreadyLoaded && !force) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,6 +659,14 @@ function handleSessionUpdate(instanceId: string, event: any): void {
|
|||||||
...existingSession.time,
|
...existingSession.time,
|
||||||
updated: info.time?.updated || Date.now(),
|
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) => {
|
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.onMessageUpdate = handleMessageUpdate
|
||||||
sseManager.onSessionUpdate = handleSessionUpdate
|
sseManager.onSessionUpdate = handleSessionUpdate
|
||||||
|
sseManager.onSessionCompacted = handleSessionCompacted
|
||||||
|
sseManager.onSessionError = handleSessionError
|
||||||
|
|
||||||
export {
|
export {
|
||||||
sessions,
|
sessions,
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ export interface Session {
|
|||||||
created: number
|
created: number
|
||||||
updated: number
|
updated: number
|
||||||
}
|
}
|
||||||
|
revert?: {
|
||||||
|
messageID: string
|
||||||
|
partID?: string
|
||||||
|
snapshot?: string
|
||||||
|
diff?: string
|
||||||
|
}
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
messagesInfo: Map<string, any>
|
messagesInfo: Map<string, any>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user