v0.20.0: thinking blocks, chat windows overhaul, scroll fix
Backend: - Add isThinking flag to agentEvent for text before tool calls - Separate thinking from response text in runChatTurn and continue.go - Persist thinking in a dedicated field in message content Frontend: - Add thinking field to MessageContent, ChatMessage, ChatTextEvent types - Create ThinkingBlock.svelte — collapsible block with brain icon - SSE handler moves text_delta content to thinking on isThinking flag - Render thinking block between tools and response in ChatThread - Fix chat window scroll reset on focus change (stable windowKeys order) - Remove redundant #key id wrapper in WindowLayer - Enlarge sidebar rail (24→32 default, 40→60 max) - Remove glyph from sidebar, square graph at top - Replace AgentTrace/ToolCallCard/UnifiedTimeline with TurnTrace/ToolLine
This commit is contained in:
@@ -25,6 +25,7 @@ export interface ChatMessage {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
text: string
|
||||
thinking?: string
|
||||
tools: ToolCallResult[]
|
||||
pendingApprovals: PendingApproval[]
|
||||
created_at?: string
|
||||
@@ -223,6 +224,7 @@ function toChatMessages(msgs: Message[]): ChatMessage[] {
|
||||
id: m.id,
|
||||
role: m.role as 'user' | 'assistant',
|
||||
text: content?.text ?? '',
|
||||
thinking: content?.thinking ?? undefined,
|
||||
tools,
|
||||
pendingApprovals: extractApprovals(tools),
|
||||
created_at: m.created_at
|
||||
@@ -230,6 +232,16 @@ function toChatMessages(msgs: Message[]): ChatMessage[] {
|
||||
})
|
||||
}
|
||||
|
||||
function chatMessagesChanged(a: ChatMessage[], b: ChatMessage[]): boolean {
|
||||
if (a.length !== b.length) return true
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i].id !== b[i].id || a[i].role !== b[i].role || a[i].text !== b[i].text || a[i].tools.length !== b[i].tools.length) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function loadSessionMessages(sessionId: string) {
|
||||
currentSession.set(sessionId)
|
||||
// This is a fresh view of sessionId's current (REST-loaded) state — reset
|
||||
@@ -270,15 +282,11 @@ function startPolling(sessionId: string) {
|
||||
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
|
||||
// No cheap "anything new?" check: the auto-continuation worker updates a
|
||||
// placeholder message IN PLACE as each tool call lands (see
|
||||
// cmd/nomos/continue.go), so the message COUNT stays the same while the
|
||||
// content changes — a length-only diff (the previous version of this
|
||||
// code) never detected those updates and progress looked frozen even
|
||||
// though the backend was actively working. Just re-set every tick;
|
||||
// Svelte's own diffing keeps the actual re-render cheap.
|
||||
sessionMessages.set(msgs)
|
||||
messages.set(toChatMessages(msgs))
|
||||
const incoming = toChatMessages(msgs)
|
||||
if (chatMessagesChanged(get(messages), incoming)) {
|
||||
sessionMessages.set(msgs)
|
||||
messages.set(incoming)
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
@@ -401,7 +409,15 @@ export function sendMessage(text: string) {
|
||||
messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') {
|
||||
ms[ms.length - 1] = { ...last, text: ev.data }
|
||||
if (ev.is_thinking) {
|
||||
ms[ms.length - 1] = {
|
||||
...last,
|
||||
thinking: (last.thinking || '') + ev.data,
|
||||
text: ''
|
||||
}
|
||||
} else {
|
||||
ms[ms.length - 1] = { ...last, text: ev.data }
|
||||
}
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
@@ -596,7 +612,10 @@ function startSessionPolling(sessionId: string) {
|
||||
if (get(chat.streaming) && get(chat.connectionState) === 'connected') return
|
||||
const msgs = await fetchMessages(sessionId)
|
||||
if (get(chat.streaming)) return // re-check: the fetch itself takes time
|
||||
chat.messages.set(toChatMessages(msgs))
|
||||
const incoming = toChatMessages(msgs)
|
||||
if (chatMessagesChanged(get(chat.messages), incoming)) {
|
||||
chat.messages.set(incoming)
|
||||
}
|
||||
// F3 safety net: if we're recovering from a dropped SSE but the
|
||||
// session's task has already reached a turn-ended status, clear the
|
||||
// stuck disconnected/streaming flags. Catches the edge where the
|
||||
@@ -728,7 +747,15 @@ export function sendSessionMessage(sessionId: string, text: string) {
|
||||
chat.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') {
|
||||
ms[ms.length - 1] = { ...last, text: ev.data }
|
||||
if (ev.is_thinking) {
|
||||
ms[ms.length - 1] = {
|
||||
...last,
|
||||
thinking: (last.thinking || '') + ev.data,
|
||||
text: ''
|
||||
}
|
||||
} else {
|
||||
ms[ms.length - 1] = { ...last, text: ev.data }
|
||||
}
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
@@ -869,7 +896,15 @@ export function startTask(text: string, onSession: (sessionId: string) => void):
|
||||
c.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') {
|
||||
ms[ms.length - 1] = { ...last, text: ev.data }
|
||||
if (ev.is_thinking) {
|
||||
ms[ms.length - 1] = {
|
||||
...last,
|
||||
thinking: (last.thinking || '') + ev.data,
|
||||
text: ''
|
||||
}
|
||||
} else {
|
||||
ms[ms.length - 1] = { ...last, text: ev.data }
|
||||
}
|
||||
}
|
||||
return [...ms]
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user