feat: live visibility into what the agent is running (no more silent waiting)
Operator: "I'd like to be able to see in the chat what the agent is actually running, right now I just wait while nothing happens." Two compounding gaps: 1. The auto-continuation worker (cmd/nomos/continue.go) had zero live push — its result only appeared on a manual page reload, so approving a plan and watching the chat looked completely dead even while the agent was actively working. 2. Even with polling, continueSession only persisted ONE message at the very end of a continuation — a continuation that runs several tool calls before concluding would still show total silence for however long that took. Fixed both: - web/src/lib/stores/chat.ts: polls the current session's messages every 3s between turns (never while a live stream owns the message list) and merges in anything new. Started after a live turn ends and when a session loads; stopped on new-chat/session-switch. - cmd/nomos/store.go: insertMessageReturningID/updateMessage — lets a message be created as a placeholder and updated in place. - cmd/nomos/continue.go: continueSession now inserts a placeholder the instant it starts (renders as the existing "thinking" dots — immediate feedback that something is happening) and updates that SAME row after EVERY tool call, not just at the end. A poll within ~3s of any tool call landing shows it — individual `run` commands appear as the agent issues them, not just the final rolled-up summary. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -95,11 +95,8 @@ function mergeToolCalls(raw: ToolCallResult[] | undefined): ToolCallResult[] {
|
||||
return Array.from(byId.values())
|
||||
}
|
||||
|
||||
export async function loadSessionMessages(sessionId: string) {
|
||||
currentSession.set(sessionId)
|
||||
const msgs = await fetchMessages(sessionId)
|
||||
sessionMessages.set(msgs)
|
||||
const chatMsgs: ChatMessage[] = msgs.map((m) => {
|
||||
function toChatMessages(msgs: Message[]): ChatMessage[] {
|
||||
return msgs.map((m) => {
|
||||
const tools = mergeToolCalls(m.content?.tool_calls)
|
||||
return {
|
||||
id: m.id,
|
||||
@@ -109,7 +106,50 @@ export async function loadSessionMessages(sessionId: string) {
|
||||
pendingApprovals: extractApprovals(tools)
|
||||
}
|
||||
})
|
||||
messages.set(chatMsgs)
|
||||
}
|
||||
|
||||
export async function loadSessionMessages(sessionId: string) {
|
||||
currentSession.set(sessionId)
|
||||
const msgs = await fetchMessages(sessionId)
|
||||
sessionMessages.set(msgs)
|
||||
messages.set(toChatMessages(msgs))
|
||||
startPolling(sessionId)
|
||||
}
|
||||
|
||||
// Live visibility for autonomous work: the auto-continuation worker (see
|
||||
// cmd/nomos/continue.go) runs entirely server-side and has no live push —
|
||||
// previously the only way to see its result was to manually reload the
|
||||
// session, so approving a plan and then waiting felt like nothing was
|
||||
// happening even while the agent was actively working. This polls the
|
||||
// session's persisted messages every few seconds and merges in anything new
|
||||
// (an auto-continuation's result, a fresh pending approval it queued, etc.)
|
||||
// so the transcript updates on its own. Only runs between turns — never
|
||||
// while a live streaming turn owns the message list, to avoid clobbering the
|
||||
// in-progress optimistic UI.
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
let pollingSessionId: string | null = null
|
||||
|
||||
function startPolling(sessionId: string) {
|
||||
stopPolling()
|
||||
pollingSessionId = sessionId
|
||||
pollTimer = setInterval(async () => {
|
||||
if (get(streaming)) return
|
||||
if (pollingSessionId !== sessionId || get(currentSession) !== sessionId) return
|
||||
const msgs = await fetchMessages(sessionId)
|
||||
if (get(streaming) || pollingSessionId !== sessionId) return // re-check: the fetch itself takes time
|
||||
const current = get(messages)
|
||||
if (msgs.length === current.length) return
|
||||
sessionMessages.set(msgs)
|
||||
messages.set(toChatMessages(msgs))
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
export function stopPolling() {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
pollingSessionId = null
|
||||
}
|
||||
|
||||
export function sendMessage(text: string) {
|
||||
@@ -202,7 +242,12 @@ export function sendMessage(text: string) {
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
currentSession.set(ev.data?.session_id ?? ev.session_id)
|
||||
const sid = ev.data?.session_id ?? ev.session_id
|
||||
currentSession.set(sid)
|
||||
// Start polling for auto-continuation results now that the live turn
|
||||
// is over — this is what makes an approved plan's later steps show up
|
||||
// on their own instead of requiring a manual reload.
|
||||
if (sid) startPolling(sid)
|
||||
} else if (ev.type === 'error') {
|
||||
error.set(ev.data)
|
||||
}
|
||||
@@ -220,6 +265,7 @@ export function sendMessage(text: string) {
|
||||
|
||||
export function newChat() {
|
||||
cancelStream()
|
||||
stopPolling()
|
||||
currentSession.set(null)
|
||||
messages.set([])
|
||||
error.set(null)
|
||||
|
||||
Reference in New Issue
Block a user