fix(tui): inline todo in transcript, group across thinking

This commit is contained in:
Brooklyn Nicholson 2026-04-26 16:09:28 -05:00
parent 4943ea2a7c
commit 319c1c1691
6 changed files with 104 additions and 30 deletions

View file

@ -10,7 +10,7 @@ import {
} from '../app/delegationStore.js'
import { patchOverlayState } from '../app/overlayStore.js'
import { $spawnDiff, $spawnHistory, clearDiffPair, type SpawnSnapshot } from '../app/spawnHistoryStore.js'
import { $turnState } from '../app/turnStore.js'
import { useTurnSelector } from '../app/turnStore.js'
import type { GatewayClient } from '../gatewayClient.js'
import type { DelegationPauseResponse, DelegationStatusResponse, SubagentInterruptResponse } from '../gatewayTypes.js'
import { asRpcResult } from '../lib/rpc.js'
@ -683,7 +683,7 @@ function DiffView({
// ── Main overlay ─────────────────────────────────────────────────────
export function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }: AgentsOverlayProps) {
const turn = useStore($turnState)
const liveSubagents = useTurnSelector(state => state.subagents)
const delegation = useStore($delegationState)
const history = useStore($spawnHistory)
const diffPair = useStore($spawnDiff)
@ -705,17 +705,17 @@ export function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }: Agent
const [mode, setMode] = useState<'detail' | 'list'>('list')
const detailScrollRef = useRef<null | ScrollBoxHandle>(null)
const prevLiveCountRef = useRef(turn.subagents.length)
const prevLiveCountRef = useRef(liveSubagents.length)
// ── Derived state ──────────────────────────────────────────────────
const activeSnapshot = historyIndex > 0 ? history[historyIndex - 1] : null
// Instant fallback to history[0] the moment the live list clears — avoids
// a one-frame "no subagents" flash while the auto-follow effect fires.
const justFinishedSnapshot = historyIndex === 0 && turn.subagents.length === 0 ? (history[0] ?? null) : null
const justFinishedSnapshot = historyIndex === 0 && liveSubagents.length === 0 ? (history[0] ?? null) : null
const effectiveSnapshot = activeSnapshot ?? justFinishedSnapshot
const replayMode = effectiveSnapshot != null
const subagents = replayMode ? effectiveSnapshot.subagents : turn.subagents
const subagents = replayMode ? effectiveSnapshot.subagents : liveSubagents
const tree = useMemo(() => buildSubagentTree(subagents), [subagents])
const totals = useMemo(() => treeTotals(tree), [tree])
@ -753,14 +753,14 @@ export function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }: Agent
// dropped into an empty live view. Fires only when transitioning from
// "had live subagents" → "live empty" while in live mode.
const prev = prevLiveCountRef.current
prevLiveCountRef.current = turn.subagents.length
prevLiveCountRef.current = liveSubagents.length
if (historyIndex === 0 && prev > 0 && turn.subagents.length === 0 && history.length > 0) {
if (historyIndex === 0 && prev > 0 && liveSubagents.length === 0 && history.length > 0) {
setHistoryIndex(1)
setCursor(0)
setFlash('turn finished · inspect freely · q to close')
}
}, [history.length, historyIndex, turn.subagents.length])
}, [history.length, historyIndex, liveSubagents.length])
useEffect(() => {
// Reset detail scroll on navigation so the top of the new node shows.

View file

@ -3,7 +3,7 @@ import { useStore } from '@nanostores/react'
import { type ReactNode, type RefObject, useEffect, useMemo, useState } from 'react'
import { $delegationState } from '../app/delegationStore.js'
import { $turnState } from '../app/turnStore.js'
import { useTurnSelector } from '../app/turnStore.js'
import { FACES } from '../content/faces.js'
import { VERBS } from '../content/verbs.js'
import { fmtDuration } from '../domain/messages.js'
@ -69,9 +69,9 @@ function SpawnHud({ t }: { t: Theme }) {
// Tight HUD that only appears when the session is actually fanning out.
// Colour escalates to warn/error as depth or concurrency approaches the cap.
const delegation = useStore($delegationState)
const turn = useStore($turnState)
const subagents = useTurnSelector(state => state.subagents)
const tree = useMemo(() => buildSubagentTree(turn.subagents), [turn.subagents])
const tree = useMemo(() => buildSubagentTree(subagents), [subagents])
const totals = useMemo(() => treeTotals(tree), [tree])
if (!totals.descendantCount && !delegation.paused) {

View file

@ -66,13 +66,11 @@ const TranscriptPane = memo(function TranscriptPane({
progress={progress}
sections={ui.sections}
/>
<LiveTodoPanel />
</Box>
</ScrollBox>
<Box flexDirection="column" flexShrink={0} paddingX={1}>
<LiveTodoPanel />
</Box>
<NoSelect flexShrink={0} marginLeft={1}>
<TranscriptScrollbar scrollRef={transcript.scrollRef} t={ui.theme} />
</NoSelect>

View file

@ -4,24 +4,14 @@ import { memo } from 'react'
import type { AppLayoutProgressProps } from '../app/interfaces.js'
import { toggleTodoCollapsed, useTurnSelector } from '../app/turnStore.js'
import { $uiState } from '../app/uiStore.js'
import { appendToolShelfMessage } from '../lib/liveProgress.js'
import type { DetailsMode, Msg, SectionVisibility } from '../types.js'
import { MessageLine } from './messageLine.js'
import { TodoPanel } from './todoPanel.js'
const isToolOnly = (msg: Msg | undefined) =>
Boolean(msg && msg.kind === 'trail' && !msg.thinking?.trim() && !msg.text && msg.tools?.length)
const groupedSegments = (segments: Msg[]) =>
segments.reduce<Msg[]>((acc, msg) => {
if (isToolOnly(msg) && isToolOnly(acc.at(-1))) {
const prev = acc.at(-1)!
return [...acc.slice(0, -1), { ...prev, tools: [...(prev.tools ?? []), ...(msg.tools ?? [])] }]
}
return [...acc, msg]
}, [])
const groupedSegments = (segments: Msg[]): Msg[] =>
segments.reduce<Msg[]>((acc, msg) => appendToolShelfMessage(acc, msg), [])
export const StreamingAssistant = memo(function StreamingAssistant({
cols,