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:
@@ -433,3 +433,148 @@ export function toolActivityLabel(t: ToolCallResult): string {
|
||||
return t.name.replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase())
|
||||
}
|
||||
}
|
||||
|
||||
// ── toolResultSummary ────────────────────────────────────────────────────
|
||||
// A one-line, humanized summary of a tool's RESULT (the Claude-Code-style
|
||||
// "exit 0 · <line>" / "host:hubris (healthy)" affordance) so each tool line
|
||||
// in the inline trace reads as an outcome instead of a raw JSON blob. Empty
|
||||
// for a still-running call (no result yet) or an errored one (the error is
|
||||
// surfaced separately). Best-effort by tool name; the fallback is the first
|
||||
// non-empty line of the stringified result, truncated — never blank (the
|
||||
// expandable raw detail is always one click away).
|
||||
function resultAsString(r: unknown): string {
|
||||
if (r == null) return ''
|
||||
if (typeof r === 'string') return r
|
||||
try {
|
||||
return JSON.stringify(r)
|
||||
} catch {
|
||||
return String(r)
|
||||
}
|
||||
}
|
||||
function firstLine(s: string, max = 80): string {
|
||||
const line = s
|
||||
.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.find((l) => l.length > 0) ?? ''
|
||||
return line.length > max ? `${line.slice(0, max - 1)}…` : line
|
||||
}
|
||||
function resultArray(r: unknown): unknown[] | null {
|
||||
if (Array.isArray(r)) return r
|
||||
if (r && typeof r === 'object') {
|
||||
const o = r as Record<string, unknown>
|
||||
for (const k of [
|
||||
'entities',
|
||||
'results',
|
||||
'relations',
|
||||
'steps',
|
||||
'items',
|
||||
'containers',
|
||||
'docs',
|
||||
'questions',
|
||||
'signals',
|
||||
'events',
|
||||
'patterns',
|
||||
'skills'
|
||||
]) {
|
||||
if (Array.isArray(o[k])) return o[k] as unknown[]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
function num(v: unknown): number | null {
|
||||
return typeof v === 'number' && Number.isFinite(v) ? v : null
|
||||
}
|
||||
function plural(n: number, singular: string, pluralForm = `${singular}s`): string {
|
||||
return `${n} ${n === 1 ? singular : pluralForm}`
|
||||
}
|
||||
export function toolResultSummary(t: ToolCallResult): string {
|
||||
if (t.type === 'tool_use') return '' // still running
|
||||
if (t.error) return '' // error surfaced separately
|
||||
const args = t.args ?? {}
|
||||
const str = (v: unknown): string => (typeof v === 'string' ? v : '')
|
||||
const obj = (r: unknown): Record<string, unknown> | null =>
|
||||
r && typeof r === 'object' && !Array.isArray(r) ? (r as Record<string, unknown>) : null
|
||||
switch (t.name) {
|
||||
case 'run': {
|
||||
const s = resultAsString(t.result)
|
||||
const m = s.match(/exit (?:status )?(\d+)/i)
|
||||
const tag = m ? `exit ${m[1]}` : /error/i.test(s) ? 'error' : 'ok'
|
||||
const rest = firstLine(s.replace(/[\s\S]*exit (?:status )?\d+/i, ''), 60)
|
||||
return rest ? `${tag} · ${rest}` : tag
|
||||
}
|
||||
case 'get_entity': {
|
||||
const o = obj(t.result)
|
||||
const slug = str(o?.slug) || str(args.slug_or_id)
|
||||
const health = str(o?.health) || str(o?.state)
|
||||
return [slug, health && `(${health})`].filter(Boolean).join(' ') || 'found'
|
||||
}
|
||||
case 'get_relations': {
|
||||
const a = resultArray(t.result)
|
||||
return a ? plural(a.length, 'relation') : 'done'
|
||||
}
|
||||
case 'list_entities':
|
||||
case 'list_lxcs': {
|
||||
const a = resultArray(t.result)
|
||||
if (!a) return 'done'
|
||||
return t.name === 'list_lxcs'
|
||||
? plural(a.length, 'container')
|
||||
: plural(a.length, 'entity', 'entities')
|
||||
}
|
||||
case 'get_health_summary': {
|
||||
const o = obj(t.result)
|
||||
const h = (o?.health && obj(o.health)) || o
|
||||
if (h) {
|
||||
const parts = ['healthy', 'degraded', 'down', 'unknown']
|
||||
.map((k) => {
|
||||
const n = num((h as Record<string, unknown>)[k])
|
||||
return n != null ? `${k} ${n}` : null
|
||||
})
|
||||
.filter((p): p is string => p != null)
|
||||
if (parts.length) return parts.join(' · ')
|
||||
}
|
||||
return 'done'
|
||||
}
|
||||
case 'get_state_snapshot': {
|
||||
const o = obj(t.result)
|
||||
const drift = num(o?.drift ?? o?.drift_count)
|
||||
return drift != null ? plural(drift, 'drift') : 'done'
|
||||
}
|
||||
case 'search_knowledge':
|
||||
case 'get_entity_knowledge':
|
||||
case 'get_patterns':
|
||||
case 'get_skills': {
|
||||
const a = resultArray(t.result)
|
||||
return a ? plural(a.length, 'result') : firstLine(resultAsString(t.result)) || 'done'
|
||||
}
|
||||
case 'upsert_knowledge': {
|
||||
const m = resultAsString(t.result).match(/[a-z]+:nomos\/[a-z0-9-]+/)
|
||||
return m ? `recorded ${m[0]}` : 'recorded'
|
||||
}
|
||||
case 'update_plan_step': {
|
||||
const seq = num(args.seq)
|
||||
const status = str(args.status)
|
||||
if (seq != null && status) return `step ${seq} → ${status}`
|
||||
return status || 'updated'
|
||||
}
|
||||
case 'propose_plan': {
|
||||
const a = resultArray(t.result) ?? resultArray(args.steps)
|
||||
return a ? plural(a.length, 'step') : 'planned'
|
||||
}
|
||||
case 'set_goal':
|
||||
return 'goal set'
|
||||
case 'complete_task':
|
||||
return str(args.outcome) || 'complete'
|
||||
case 'ask_operator':
|
||||
return 'asked'
|
||||
case 'ping_service': {
|
||||
const s = resultAsString(t.result).toLowerCase()
|
||||
return /ok|reachable|up|healthy/.test(s) ? 'reachable' : firstLine(s, 40) || 'done'
|
||||
}
|
||||
case 'get_execution_status': {
|
||||
const o = obj(t.result)
|
||||
return str(o?.state) || firstLine(resultAsString(t.result), 40) || 'done'
|
||||
}
|
||||
default:
|
||||
return firstLine(resultAsString(t.result)) || 'done'
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user