From 4a6e7b168c1ed8ac5bc1ed9151e54122c6826937 Mon Sep 17 00:00:00 2001 From: Dina Berry Date: Thu, 12 Mar 2026 13:30:39 -0700 Subject: [PATCH] feat: conversation-aware thinking phrases in ThinkingIndicator When prior conversation history exists, the ThinkingIndicator cycles through context-aware phrases (e.g. 'Reviewing conversation context', 'Connecting to previous work') instead of generic ones. This gives users immediate feedback that the system recognizes ongoing conversation. - Add CONVERSATION_PHRASES array to ThinkingIndicator - Add hasConversation prop threaded through App -> MessageStream -> ThinkingIndicator - Derive hasConversation from messages array in App.tsx (true when agent responses exist) Closes #360 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/cli/shell/components/App.tsx | 8 ++++- .../cli/shell/components/MessageStream.tsx | 4 +++ .../shell/components/ThinkingIndicator.tsx | 30 +++++++++++++++++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/squad-cli/src/cli/shell/components/App.tsx b/packages/squad-cli/src/cli/shell/components/App.tsx index ab252a507..56ad6a310 100644 --- a/packages/squad-cli/src/cli/shell/components/App.tsx +++ b/packages/squad-cli/src/cli/shell/components/App.tsx @@ -314,6 +314,12 @@ export const App: React.FC = ({ registry, renderer, teamRoot, version, return undefined; }, [messages, processing]); + // True when there is prior conversation history (at least one agent response). + const hasConversation = useMemo( + () => messages.some(m => m.role === 'agent'), + [messages], + ); + // Only archived (overflow) messages go to Static scrollback. // Current messages stay in the live region so the user can always see // the recent conversation without scrolling. This prevents the @@ -467,7 +473,7 @@ export const App: React.FC = ({ registry, renderer, teamRoot, version, count to prevent overflow into the InputPrompt area. */} - + {/* Fixed input box at bottom — Copilot/Claude style */} diff --git a/packages/squad-cli/src/cli/shell/components/MessageStream.tsx b/packages/squad-cli/src/cli/shell/components/MessageStream.tsx index 0ee0731f7..23f95df10 100644 --- a/packages/squad-cli/src/cli/shell/components/MessageStream.tsx +++ b/packages/squad-cli/src/cli/shell/components/MessageStream.tsx @@ -53,6 +53,8 @@ interface MessageStreamProps { agentActivities?: Map; thinkingPhase?: ThinkingPhase; maxVisible?: number; + /** When true, thinking indicator shows conversation-aware phrases. */ + hasConversation?: boolean; } /** Format elapsed seconds for response timestamps. */ @@ -199,6 +201,7 @@ export const MessageStream: React.FC = ({ agentActivities, thinkingPhase, maxVisible = 50, + hasConversation = false, }) => { const visible = messages.slice(-maxVisible); const visibleOffset = Math.max(0, messages.length - maxVisible); @@ -327,6 +330,7 @@ export const MessageStream: React.FC = ({ elapsedMs={elapsedMs} activityHint={activityHint} phase={thinkingPhase} + hasConversation={hasConversation} /> )} diff --git a/packages/squad-cli/src/cli/shell/components/ThinkingIndicator.tsx b/packages/squad-cli/src/cli/shell/components/ThinkingIndicator.tsx index ba98cdcdd..f1b44dbec 100644 --- a/packages/squad-cli/src/cli/shell/components/ThinkingIndicator.tsx +++ b/packages/squad-cli/src/cli/shell/components/ThinkingIndicator.tsx @@ -19,6 +19,8 @@ export interface ThinkingIndicatorProps { elapsedMs: number; activityHint?: string; phase?: ThinkingPhase; + /** When true, cycles conversation-aware phrases instead of generic ones. */ + hasConversation?: boolean; } /** Rotating thinking phrases — cycled every few seconds to keep the UI alive. */ @@ -40,6 +42,25 @@ export const THINKING_PHRASES = [ 'Exploring possibilities', ]; +/** Context-aware phrases shown when conversation history exists. */ +export const CONVERSATION_PHRASES = [ + 'Reviewing conversation context', + 'Connecting to previous work', + 'Analyzing how this relates', + 'Checking conversation thread', + 'Considering prior context', + 'Building on earlier discussion', + 'Mapping to your session', + 'Evaluating options', + 'Consulting the team', + 'Synthesizing a response', + 'Weighing trade-offs', + 'Gathering context', + 'Crafting a plan', + 'Connecting the dots', + 'Reading the codebase', +]; + /** Map phase to its default label. */ function phaseLabel(phase: ThinkingPhase): string { switch (phase) { @@ -72,6 +93,7 @@ export const ThinkingIndicator: React.FC = ({ elapsedMs, activityHint, phase = 'routing', + hasConversation = false, }) => { const noColor = isNoColor(); const [frame, setFrame] = useState(0); @@ -86,14 +108,16 @@ export const ThinkingIndicator: React.FC = ({ return () => clearInterval(timer); }, [isThinking, noColor]); + const phrases = hasConversation ? CONVERSATION_PHRASES : THINKING_PHRASES; + // Rotate thinking phrases every 3 seconds useEffect(() => { if (!isThinking) { setPhraseIndex(0); return; } const timer = setInterval(() => { - setPhraseIndex(i => (i + 1) % THINKING_PHRASES.length); + setPhraseIndex(i => (i + 1) % phrases.length); }, 3000); return () => clearInterval(timer); - }, [isThinking]); + }, [isThinking, phrases]); // Reset frame when thinking starts useEffect(() => { @@ -108,7 +132,7 @@ export const ThinkingIndicator: React.FC = ({ // Resolve the display label: activity hint > rotating phrase > phase label const displayLabel = activityHint ?? ( - phase === 'connecting' ? phaseLabel(phase) : `${THINKING_PHRASES[phraseIndex]}...` + phase === 'connecting' ? phaseLabel(phase) : `${phrases[phraseIndex]}...` ); // NO_COLOR: no color props, use text labels