v0.20.0: thinking blocks, chat windows overhaul, scroll fix
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

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:
2026-08-04 22:42:53 +02:00
parent 20adb89650
commit 1aaedf498a
25 changed files with 1852 additions and 1211 deletions

View File

@@ -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]
})