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,7 +25,7 @@ vi.mock('./execstream', () => ({
liveExecutionOutputFor: vi.fn(() => writable(null))
}))
import { computeActivityLog } from './activity'
import { computeActivityLog, toolResultSummary } from './activity'
import type { ChatMessage } from './chat'
import type { PlanStep, Session } from '$lib/api'
@@ -172,3 +172,76 @@ describe('computeActivityLog generation awareness (F4)', () => {
expect(entries.find((e) => e.id === 't1')!.stepSeq).toBeUndefined()
})
})
// toolResultSummary (chat interaction overhaul): a one-line, humanized outcome
// per tool so each inline tool line reads as a result instead of raw JSON.
describe('toolResultSummary', () => {
type TR = NonNullable<ChatMessage['tools']>[number]
const done = (name: string, result: unknown, args?: Record<string, unknown>): TR => ({
type: 'tool_result',
name,
id: name,
result,
args
})
it('is empty for a still-running call and for an errored one', () => {
expect(toolResultSummary({ type: 'tool_use', name: 'run', id: 'r' })).toBe('')
expect(
toolResultSummary({ type: 'tool_result', name: 'run', id: 'r', error: 'boom' })
).toBe('')
})
it('parses run exit status', () => {
expect(
toolResultSummary(done('run', 'run on lxc:caddy: ERROR exit status 1'))
).toContain('exit 1')
})
it('summarizes a clean run with its first line', () => {
const s = toolResultSummary(done('run', 'Active: active (running)'))
expect(s.startsWith('ok')).toBe(true)
expect(s).toContain('active')
})
it('formats get_entity as slug (health)', () => {
expect(
toolResultSummary(done('get_entity', { slug: 'host:hubris', health: 'healthy' }))
).toBe('host:hubris (healthy)')
})
it('counts list results', () => {
expect(
toolResultSummary(done('list_entities', { entities: Array(10).fill({}) }))
).toBe('10 entities')
expect(toolResultSummary(done('list_lxcs', { containers: [1, 2] }))).toBe('2 containers')
})
it('formats fleet health counts', () => {
expect(
toolResultSummary(
done('get_health_summary', { health: { healthy: 5, degraded: 1, down: 0, unknown: 2 } })
)
).toBe('healthy 5 · degraded 1 · down 0 · unknown 2')
})
it('extracts the knowledge slug from upsert_knowledge', () => {
expect(
toolResultSummary(done('upsert_knowledge', 'Saved document:nomos/foo-bar to the DB'))
).toBe('recorded document:nomos/foo-bar')
})
it('formats update_plan_step from args', () => {
expect(
toolResultSummary(done('update_plan_step', 'ok', { seq: 2, status: 'done' }))
).toBe('step 2 → done')
})
it('counts proposed plan steps', () => {
expect(toolResultSummary(done('propose_plan', { steps: [{}, {}, {}] }))).toBe('3 steps')
})
it('falls back to the first line for unmapped tools', () => {
expect(toolResultSummary(done('some_new_tool', 'first line\nsecond line'))).toBe('first line')
})
})