feat(web+nomos): fix chat streaming reactivity, unified activity timeline, tool cards in chat
- fix(web): Svelte 5 identity-based reactivity broke text_delta streaming — immutable message objects in all three chat handlers so text streams live - feat(web): streaming cursor + inline status indicator merged into message flow - feat(web): expandable inline tool call cards in chat thread - feat(web): merge Plan + Event log into one backbone Activity timeline — filled status nodes, branch stubs, auto-scroll follow mode, per-session activityLog, compact for the rail - fix(nomos): add X-Accel-Buffering:no to /chat SSE (proxy buffering) - fix(nomos): plan step auto-close SQL param bug (store.go) - polish: timestamps, role labels, code copy button, table overflow, min window size, delete AgentIndicator/ActivityTimeline dead code
This commit is contained in:
@@ -20,6 +20,7 @@ export interface ChatMessage {
|
||||
text: string
|
||||
tools: ToolCallResult[]
|
||||
pendingApprovals: PendingApproval[]
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
const APPROVAL_RE = /execution\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
||||
@@ -122,7 +123,8 @@ function toChatMessages(msgs: Message[]): ChatMessage[] {
|
||||
role: m.role as 'user' | 'assistant',
|
||||
text: content?.text ?? '',
|
||||
tools,
|
||||
pendingApprovals: extractApprovals(tools)
|
||||
pendingApprovals: extractApprovals(tools),
|
||||
created_at: m.created_at
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -209,7 +211,7 @@ export function sendMessage(text: string) {
|
||||
}
|
||||
messages.update((ms) => [...ms, assistantMsg])
|
||||
|
||||
let activeTools: Map<string, ToolCallResult> = new Map()
|
||||
const activeTools: Map<string, ToolCallResult> = new Map()
|
||||
|
||||
// Multiple tasks can stream concurrently (the backend runs each turn as its
|
||||
// own goroutine — nothing serializes them), but `messages`/`currentSession`
|
||||
@@ -262,7 +264,7 @@ export function sendMessage(text: string) {
|
||||
messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') {
|
||||
last.tools = [...last.tools, tr]
|
||||
ms[ms.length - 1] = { ...last, tools: [...last.tools, tr] }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
@@ -279,10 +281,10 @@ export function sendMessage(text: string) {
|
||||
messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') {
|
||||
last.tools = last.tools.map((t) =>
|
||||
const tools = last.tools.map((t) =>
|
||||
t.id === ev.data.id ? updated : t
|
||||
)
|
||||
last.pendingApprovals = extractApprovals(last.tools)
|
||||
ms[ms.length - 1] = { ...last, tools, pendingApprovals: extractApprovals(tools) }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
@@ -291,7 +293,7 @@ export function sendMessage(text: string) {
|
||||
messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') {
|
||||
last.text += ev.data
|
||||
ms[ms.length - 1] = { ...last, text: last.text + ev.data }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
@@ -300,7 +302,7 @@ export function sendMessage(text: string) {
|
||||
messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') {
|
||||
last.text = ev.data
|
||||
ms[ms.length - 1] = { ...last, text: ev.data }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
@@ -310,7 +312,7 @@ export function sendMessage(text: string) {
|
||||
messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') {
|
||||
last.pendingApprovals = extractApprovals(last.tools)
|
||||
ms[ms.length - 1] = { ...last, pendingApprovals: extractApprovals(last.tools) }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
@@ -580,7 +582,7 @@ export function sendSessionMessage(sessionId: string, text: string) {
|
||||
const assistantMsg: ChatMessage = { id: mid(), role: 'assistant', text: '', tools: [], pendingApprovals: [] }
|
||||
chat.messages.update((ms) => [...ms, assistantMsg])
|
||||
|
||||
let activeTools: Map<string, ToolCallResult> = new Map()
|
||||
const activeTools: Map<string, ToolCallResult> = new Map()
|
||||
let receivedDone = false
|
||||
|
||||
const controller = streamChat(
|
||||
@@ -593,7 +595,9 @@ export function sendSessionMessage(sessionId: string, text: string) {
|
||||
activeTools.set(ev.data.id, tr)
|
||||
chat.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') last.tools = [...last.tools, tr]
|
||||
if (last && last.role === 'assistant') {
|
||||
ms[ms.length - 1] = { ...last, tools: [...last.tools, tr] }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
} else if (ev.type === 'tool_result') {
|
||||
@@ -604,8 +608,8 @@ export function sendSessionMessage(sessionId: string, text: string) {
|
||||
chat.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') {
|
||||
last.tools = last.tools.map((t) => (t.id === ev.data.id ? updated : t))
|
||||
last.pendingApprovals = extractApprovals(last.tools)
|
||||
const tools = last.tools.map((t) => (t.id === ev.data.id ? updated : t))
|
||||
ms[ms.length - 1] = { ...last, tools, pendingApprovals: extractApprovals(tools) }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
@@ -613,13 +617,17 @@ export function sendSessionMessage(sessionId: string, text: string) {
|
||||
} else if (ev.type === 'text_delta') {
|
||||
chat.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') last.text += ev.data
|
||||
if (last && last.role === 'assistant') {
|
||||
ms[ms.length - 1] = { ...last, text: last.text + ev.data }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
} else if (ev.type === 'text') {
|
||||
chat.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') last.text = ev.data
|
||||
if (last && last.role === 'assistant') {
|
||||
ms[ms.length - 1] = { ...last, text: ev.data }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
} else if (ev.type === 'done') {
|
||||
@@ -627,7 +635,9 @@ export function sendSessionMessage(sessionId: string, text: string) {
|
||||
chat.connectionState.set('connected')
|
||||
chat.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') last.pendingApprovals = extractApprovals(last.tools)
|
||||
if (last && last.role === 'assistant') {
|
||||
ms[ms.length - 1] = { ...last, pendingApprovals: extractApprovals(last.tools) }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
startSessionPolling(sessionId)
|
||||
@@ -694,7 +704,9 @@ export function startTask(text: string, onSession: (sessionId: string) => void):
|
||||
activeTools.set(ev.data.id, tr)
|
||||
c.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') last.tools = [...last.tools, tr]
|
||||
if (last && last.role === 'assistant') {
|
||||
ms[ms.length - 1] = { ...last, tools: [...last.tools, tr] }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
} else if (ev.type === 'tool_result') {
|
||||
@@ -705,8 +717,8 @@ export function startTask(text: string, onSession: (sessionId: string) => void):
|
||||
c.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') {
|
||||
last.tools = last.tools.map((t) => (t.id === ev.data.id ? updated : t))
|
||||
last.pendingApprovals = extractApprovals(last.tools)
|
||||
const tools = last.tools.map((t) => (t.id === ev.data.id ? updated : t))
|
||||
ms[ms.length - 1] = { ...last, tools, pendingApprovals: extractApprovals(tools) }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
@@ -714,13 +726,17 @@ export function startTask(text: string, onSession: (sessionId: string) => void):
|
||||
} else if (ev.type === 'text_delta') {
|
||||
c.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') last.text += ev.data
|
||||
if (last && last.role === 'assistant') {
|
||||
ms[ms.length - 1] = { ...last, text: last.text + ev.data }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
} else if (ev.type === 'text') {
|
||||
c.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') last.text = ev.data
|
||||
if (last && last.role === 'assistant') {
|
||||
ms[ms.length - 1] = { ...last, text: ev.data }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
} else if (ev.type === 'done') {
|
||||
@@ -728,7 +744,9 @@ export function startTask(text: string, onSession: (sessionId: string) => void):
|
||||
c.connectionState.set('connected')
|
||||
c.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') last.pendingApprovals = extractApprovals(last.tools)
|
||||
if (last && last.role === 'assistant') {
|
||||
ms[ms.length - 1] = { ...last, pendingApprovals: extractApprovals(last.tools) }
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
startSessionPolling(sessionId)
|
||||
|
||||
@@ -122,5 +122,5 @@ export function openTaskWindow(sessionId: string | null, title: string): void {
|
||||
wm.focus(id)
|
||||
return
|
||||
}
|
||||
wm.open({ id, title, width: 900, height: 640 })
|
||||
wm.open({ id, title, width: 900, height: 640, minWidth: 600, minHeight: 400 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user