refactor(tui): wrap progress panel + streaming body in StreamingAssistant

Two improvements:

1. The progress ToolTrail and the streaming MessageLine were two
   sibling JSX blocks in appLayout with hand-rolled margin glue
   between them. Extracted into `<StreamingAssistant>`, a single
   component that owns both the trail and the streaming body plus
   the 1-row gap between them. appLayout just hands it `progress`
   and theme; the layout logic lives in one place, matching the
   mental model that these two pieces are one live assistant turn.

2. Thinking token label was hidden when `reasoningTokens === 0` even
   if the live reasoning text was already populated (the
   scheduleReasoning timer hadn't ticked, or the model sent no
   reasoning but the text was coming in via reasoning.delta).
   Changed the tokenCount fallback from `reasoningTokens !==
   undefined ? reasoningTokens : estimate` to `reasoningTokens > 0 ?
   ... : estimate` so the label appears the moment text exists.
This commit is contained in:
Brooklyn Nicholson 2026-04-16 20:49:41 -05:00
parent 26f3a05c9c
commit 319aabbb80
2 changed files with 65 additions and 30 deletions

View file

@ -2,10 +2,12 @@ import { AlternateScreen, Box, NoSelect, ScrollBox, Text } from '@hermes/ink'
import { useStore } from '@nanostores/react'
import { memo } from 'react'
import type { AppLayoutProps } from '../app/interfaces.js'
import type { AppLayoutProgressProps, AppLayoutProps } from '../app/interfaces.js'
import { $isBlocked } from '../app/overlayStore.js'
import { $uiState } from '../app/uiStore.js'
import { PLACEHOLDER } from '../content/placeholders.js'
import type { Theme } from '../theme.js'
import type { DetailsMode } from '../types.js'
import { GoodVibesHeart, StatusRule, StickyPromptTracker, TranscriptScrollbar } from './appChrome.js'
import { AppOverlays } from './appOverlays.js'
@ -15,6 +17,58 @@ import { QueuedMessages } from './queuedMessages.js'
import { TextInput } from './textInput.js'
import { ToolTrail } from './thinking.js'
const StreamingAssistant = memo(function StreamingAssistant({
busy,
cols,
compact,
detailsMode,
progress,
t
}: {
busy: boolean
cols: number
compact?: boolean
detailsMode: DetailsMode
progress: AppLayoutProgressProps
t: Theme
}) {
if (!progress.showProgressArea && !progress.showStreamingArea) return null
return (
<Box flexDirection="column">
{progress.showProgressArea && (
<Box flexDirection="column" marginBottom={progress.showStreamingArea ? 1 : 0}>
<ToolTrail
activity={progress.activity}
busy={busy}
detailsMode={detailsMode}
reasoning={progress.reasoning}
reasoningActive={progress.reasoningActive}
reasoningStreaming={progress.reasoningStreaming}
reasoningTokens={progress.reasoningTokens}
subagents={progress.subagents}
t={t}
tools={progress.tools}
toolTokens={progress.toolTokens}
trail={progress.turnTrail}
/>
</Box>
)}
{progress.showStreamingArea && (
<MessageLine
cols={cols}
compact={compact}
detailsMode={detailsMode}
isStreaming
msg={{ role: 'assistant', text: progress.streaming }}
t={t}
/>
)}
</Box>
)
})
const TranscriptPane = memo(function TranscriptPane({
actions,
composer,
@ -55,35 +109,15 @@ const TranscriptPane = memo(function TranscriptPane({
{transcript.virtualHistory.bottomSpacer > 0 ? <Box height={transcript.virtualHistory.bottomSpacer} /> : null}
{progress.showProgressArea && (
<ToolTrail
activity={progress.activity}
busy={ui.busy}
detailsMode={ui.detailsMode}
reasoning={progress.reasoning}
reasoningActive={progress.reasoningActive}
reasoningStreaming={progress.reasoningStreaming}
reasoningTokens={progress.reasoningTokens}
subagents={progress.subagents}
t={ui.theme}
tools={progress.tools}
toolTokens={progress.toolTokens}
trail={progress.turnTrail}
/>
)}
<StreamingAssistant
busy={ui.busy}
cols={composer.cols}
compact={ui.compact}
detailsMode={ui.detailsMode}
progress={progress}
t={ui.theme}
/>
{progress.showStreamingArea && (
<Box flexDirection="column" marginTop={progress.showProgressArea ? 1 : 0}>
<MessageLine
cols={composer.cols}
compact={ui.compact}
detailsMode={ui.detailsMode}
isStreaming
msg={{ role: 'assistant', text: progress.streaming }}
t={ui.theme}
/>
</Box>
)}
</Box>
</ScrollBox>