-
-
diff --git a/web/src/lib/components/UnifiedTimeline.svelte b/web/src/lib/components/UnifiedTimeline.svelte
index 793d080..07099bd 100644
--- a/web/src/lib/components/UnifiedTimeline.svelte
+++ b/web/src/lib/components/UnifiedTimeline.svelte
@@ -16,6 +16,9 @@
import FlagIcon from '@lucide/svelte/icons/flag'
// Merged plan + activity timeline, designed for the narrow rail:
+ // - ordered newest-first: what the agent is doing right now is at the top,
+ // history flows downward, and the goal sits at the bottom where the task
+ // began (see the sort in `items`)
// - one continuous vertical "backbone"; every item owns a segment of it,
// colored by state (done = filled primary, running = faint primary,
// pending/future = muted) so the line visibly fills in as work completes
@@ -25,7 +28,8 @@
// markers on the same backbone
// - the running step auto-expands and the view auto-scrolls to keep the
// current step visible while the agent works (follow mode disengages if
- // the operator scrolls up, re-engages when streaming starts again)
+ // the operator scrolls down into history, re-engages when streaming
+ // starts again)
let { entries, planSteps: steps, streaming = false }: {
entries: ActivityEntry[]
planSteps: PlanStep[]
@@ -63,9 +67,10 @@
for (const s of steps) {
if (s.status === 'pending' && !entries.some((e) => e.stepSeq === s.seq)) {
// Pending steps with no activity yet still show on the timeline so
- // the operator sees what's coming — but only if a plan exists.
+ // the operator sees what's coming — but only if a plan exists. ts 0
+ // parks them at the tail of the newest-first sort below (see there).
if (steps.length > 0) {
- out.push({ kind: 'step', step: s, tools: [], ts: Number.MAX_SAFE_INTEGER - s.seq })
+ out.push({ kind: 'step', step: s, tools: [], ts: 0 })
}
continue
}
@@ -73,8 +78,11 @@
(e) => e.stepSeq === s.seq && (e.type === 'tool_running' || e.type === 'tool_done' || e.type === 'tool_error')
)
const stepEntry = entries.find((e) => e.id === s.id)
+ // Timed from the step's own entry, else its earliest tool — so a step
+ // is placed by when it started, not by its latest activity.
const ts = stepEntry?.timestamp ?? tools[0]?.timestamp ?? Date.now()
- out.push({ kind: 'step', step: s, tools, ts })
+ // Tools inside a step run newest-first too, matching the outer order.
+ out.push({ kind: 'step', step: s, tools: [...tools].reverse(), ts })
}
for (const e of entries) {
@@ -84,7 +92,15 @@
out.push({ kind: 'entry', entry: e, ts: e.timestamp })
}
- out.sort((a, b) => a.ts - b.ts)
+ // Newest first: whatever the agent is doing right now sits at the top of
+ // the rail, with history flowing downward. The two ts-0 groups fall to
+ // the bottom for free, which is where both belong in this order: the goal
+ // (timestamp 0 — where the task started) and not-yet-run plan steps.
+ // Sorting the latter by their future position would put them *above* the
+ // running step and push it off the top, which is exactly what this
+ // ordering exists to prevent. Array.sort is stable, so each group keeps
+ // its insertion order (plan steps in seq order).
+ out.sort((a, b) => b.ts - a.ts)
return out
})
@@ -100,9 +116,11 @@
let container = $state(null)
let follow = $state(true)
+ // Newest-first, so "following the agent" means being parked at the top —
+ // the mirror of the bottom-anchored follow this had when it ran oldest-first.
function onScroll() {
if (!container) return
- follow = container.scrollHeight - container.scrollTop - container.clientHeight < 80
+ follow = container.scrollTop < 80
}
// A new turn re-engages follow mode even if the operator had scrolled up.
@@ -129,7 +147,7 @@
? container.querySelector(`[data-tl-id="${CSS.escape(currentId)}"]`)
: null
if (target) target.scrollIntoView({ behavior: 'auto', block: 'nearest' })
- else container.scrollTop = container.scrollHeight
+ else container.scrollTop = 0
})
// ── Presentation helpers ──────────────────────────────────────────────────
diff --git a/web/src/lib/stores/activity.ts b/web/src/lib/stores/activity.ts
index 73c78ed..f8af4b7 100644
--- a/web/src/lib/stores/activity.ts
+++ b/web/src/lib/stores/activity.ts
@@ -185,7 +185,10 @@ export function activityLogFor(sessionId: string): Readable {
return derived([chat.messages, ws.planSteps, task], ([$msgs, $steps, $task]) => computeActivityLog($msgs, $steps, $task))
}
-function toolActivityLabel(t: ToolCallResult): string {
+// Humanized, past/present-tense description of what a tool call is doing
+// ("Check execution", "Research: …") rather than its raw wire name. Exported
+// so the chat's agent trace can read as a thinking log instead of an API log.
+export function toolActivityLabel(t: ToolCallResult): string {
const args = t.args ?? {}
const str = (v: unknown): string => typeof v === 'string' ? v : ''
switch (t.name) {