feat(ui): implement sticky topic header below the prompt (checkpoint)

This commit saves the current state of the sticky topic header implementation.

- Introduced 'TopicUpdated' event to CoreEventEmitter to track topic changes.
- Updated TopicState to store topic summaries and emit update events.
- Refactored TopicMessage into a shared TopicDisplay component for UI consistency.
- Implemented TopicStickyHeader to persistently show the current topic and summary.
- Positioned the sticky header immediately below the prompt (Composer) in both Default and ScreenReader layouts.
- Removed redundant topic messages from the chat history to prevent duplicate display.
- Adjusted terminal height calculations to account for the new persistent UI element.
- Added unit tests for the new sticky header component and updated core tests.
- Addressed some React Hook dependency warnings in AppContainer and MainContent.
This commit is contained in:
Abhijit Balaji
2026-04-01 18:46:21 -04:00
parent 0d7e778e08
commit 0d50b16f69
13 changed files with 307 additions and 64 deletions
@@ -15,7 +15,7 @@ import type {
import { ToolCallStatus, mapCoreStatusToDisplayStatus } from '../../types.js';
import { ToolMessage } from './ToolMessage.js';
import { ShellToolMessage } from './ShellToolMessage.js';
import { TopicMessage, isTopicTool } from './TopicMessage.js';
import { isTopicTool } from './TopicMessage.js';
import { SubagentGroupDisplay } from './SubagentGroupDisplay.js';
import { DenseToolMessage } from './DenseToolMessage.js';
import { theme } from '../../semantic-colors.js';
@@ -143,7 +143,14 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
) {
return false;
}
// Hide topic tools from history as they are now sticky headers
if (isTopicTool(t.name)) {
return false;
}
// Standard hiding logic (e.g. Plan Mode internal edits)
if (
shouldHideToolCall({
displayName: t.name,
@@ -458,8 +465,6 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
>
{isCompact ? (
<DenseToolMessage {...commonProps} />
) : isTopicToolCall ? (
<TopicMessage {...commonProps} />
) : isShellToolCall ? (
<ShellToolMessage {...commonProps} config={config} />
) : (
@@ -11,11 +11,36 @@ import {
UPDATE_TOPIC_DISPLAY_NAME,
TOPIC_PARAM_TITLE,
TOPIC_PARAM_SUMMARY,
TOPIC_PARAM_STRATEGIC_INTENT,
} from '@google/gemini-cli-core';
import type { IndividualToolCallDisplay } from '../../types.js';
import { theme } from '../../semantic-colors.js';
interface TopicDisplayProps {
title?: string;
summary?: string;
marginLeft?: number;
}
export const TopicDisplay: React.FC<TopicDisplayProps> = ({
title,
summary,
marginLeft = 2,
}) => {
return (
<Box flexDirection="row" marginLeft={marginLeft} flexWrap="wrap">
<Text color={theme.text.primary} bold wrap="wrap">
{title || 'Topic'}
{summary && <Text>: </Text>}
</Text>
{summary && (
<Text color={theme.text.secondary} wrap="wrap">
{summary}
</Text>
)}
</Box>
);
};
interface TopicMessageProps extends IndividualToolCallDisplay {
terminalWidth: number;
}
@@ -26,21 +51,8 @@ export const isTopicTool = (name: string): boolean =>
export const TopicMessage: React.FC<TopicMessageProps> = ({ args }) => {
const rawTitle = args?.[TOPIC_PARAM_TITLE];
const title = typeof rawTitle === 'string' ? rawTitle : undefined;
const rawIntent =
args?.[TOPIC_PARAM_STRATEGIC_INTENT] || args?.[TOPIC_PARAM_SUMMARY];
const intent = typeof rawIntent === 'string' ? rawIntent : undefined;
const rawSummary = args?.[TOPIC_PARAM_SUMMARY];
const summary = typeof rawSummary === 'string' ? rawSummary : undefined;
return (
<Box flexDirection="row" marginLeft={2} flexWrap="wrap">
<Text color={theme.text.primary} bold wrap="truncate-end">
{title || 'Topic'}
{intent && <Text>: </Text>}
</Text>
{intent && (
<Text color={theme.text.secondary} wrap="wrap">
{intent}
</Text>
)}
</Box>
);
return <TopicDisplay title={title} summary={summary} />;
};