From 319aabbb805332a8fb96ea4258b099a5b28c50db Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 16 Apr 2026 20:49:41 -0500 Subject: [PATCH] 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 ``, 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. --- ui-tui/src/components/appLayout.tsx | 92 ++++++++++++++++++++--------- ui-tui/src/components/thinking.tsx | 3 +- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index 4f5c772761c..545817f23c8 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -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 ( + + {progress.showProgressArea && ( + + + + )} + + {progress.showStreamingArea && ( + + )} + + ) +}) + const TranscriptPane = memo(function TranscriptPane({ actions, composer, @@ -55,35 +109,15 @@ const TranscriptPane = memo(function TranscriptPane({ {transcript.virtualHistory.bottomSpacer > 0 ? : null} - {progress.showProgressArea && ( - - )} + - {progress.showStreamingArea && ( - - - - )} diff --git a/ui-tui/src/components/thinking.tsx b/ui-tui/src/components/thinking.tsx index 8c57eeaad04..25f20808184 100644 --- a/ui-tui/src/components/thinking.tsx +++ b/ui-tui/src/components/thinking.tsx @@ -695,7 +695,8 @@ export const ToolTrail = memo(function ToolTrail({ const hasThinking = !!cot || reasoningActive || busy const thinkingLive = reasoningActive || reasoningStreaming - const tokenCount = reasoningTokens !== undefined ? reasoningTokens : reasoning ? estimateTokensRough(reasoning) : 0 + const tokenCount = + reasoningTokens && reasoningTokens > 0 ? reasoningTokens : reasoning ? estimateTokensRough(reasoning) : 0 const toolTokenCount = toolTokens ?? 0 const totalTokenCount = tokenCount + toolTokenCount