mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 12:40:44 +00:00
Merge pull request #2770 from chiga0/feat/add-verbose-mode-switcher
feat: to #2767, support verbose and compact mode swither with ctrl-o
This commit is contained in:
commit
b632541629
24 changed files with 438 additions and 112 deletions
|
|
@ -16,6 +16,7 @@ import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
|||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||
import { useVerboseMode } from '../contexts/VerboseModeContext.js';
|
||||
import { ApprovalMode } from '@qwen-code/qwen-code-core';
|
||||
import { t } from '../../i18n/index.js';
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ export const Footer: React.FC = () => {
|
|||
const uiState = useUIState();
|
||||
const config = useConfig();
|
||||
const { vimEnabled, vimMode } = useVimMode();
|
||||
const { verboseMode } = useVerboseMode();
|
||||
|
||||
const { promptTokenCount, showAutoAcceptIndicator } = {
|
||||
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
|
||||
|
|
@ -93,6 +95,12 @@ export const Footer: React.FC = () => {
|
|||
),
|
||||
});
|
||||
}
|
||||
if (verboseMode) {
|
||||
rightItems.push({
|
||||
key: 'verbose',
|
||||
node: <Text color={theme.text.accent}>{t('verbose')}</Text>,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
justifyContent="space-between"
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import { ContextUsage } from './views/ContextUsage.js';
|
|||
import { ArenaAgentCard, ArenaSessionCard } from './arena/ArenaCards.js';
|
||||
import { InsightProgressMessage } from './messages/InsightProgressMessage.js';
|
||||
import { BtwMessage } from './messages/BtwMessage.js';
|
||||
import { useVerboseMode } from '../contexts/VerboseModeContext.js';
|
||||
|
||||
interface HistoryItemDisplayProps {
|
||||
item: HistoryItem;
|
||||
|
|
@ -79,6 +80,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
|||
? 0
|
||||
: 1;
|
||||
|
||||
const { verboseMode } = useVerboseMode();
|
||||
const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]);
|
||||
const contentWidth = terminalWidth - 4;
|
||||
const boxWidth = mainAreaWidth || contentWidth;
|
||||
|
|
@ -118,7 +120,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
|||
contentWidth={contentWidth}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'gemini_thought' && (
|
||||
{verboseMode && itemForDisplay.type === 'gemini_thought' && (
|
||||
<ThinkMessage
|
||||
text={itemForDisplay.text}
|
||||
isPending={isPending}
|
||||
|
|
@ -128,7 +130,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
|||
contentWidth={contentWidth}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'gemini_thought_content' && (
|
||||
{verboseMode && itemForDisplay.type === 'gemini_thought_content' && (
|
||||
<ThinkMessageContent
|
||||
text={itemForDisplay.text}
|
||||
isPending={isPending}
|
||||
|
|
@ -183,6 +185,7 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
|
|||
isFocused={isFocused}
|
||||
activeShellPtyId={activeShellPtyId}
|
||||
embeddedShellFocused={embeddedShellFocused}
|
||||
isUserInitiated={itemForDisplay.isUserInitiated}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'compression' && (
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { useUIState } from '../contexts/UIStateContext.js';
|
|||
import { useAppContext } from '../contexts/AppContext.js';
|
||||
import { AppHeader } from './AppHeader.js';
|
||||
import { DebugModeNotification } from './DebugModeNotification.js';
|
||||
import { useVerboseMode } from '../contexts/VerboseModeContext.js';
|
||||
|
||||
// Limit Gemini messages to a very high number of lines to mitigate performance
|
||||
// issues in the worst case if we somehow get an enormous response from Gemini.
|
||||
|
|
@ -23,6 +24,7 @@ const MAX_GEMINI_MESSAGE_LINES = 65536;
|
|||
export const MainContent = () => {
|
||||
const { version } = useAppContext();
|
||||
const uiState = useUIState();
|
||||
const { frozenSnapshot } = useVerboseMode();
|
||||
const {
|
||||
pendingHistoryItems,
|
||||
terminalWidth,
|
||||
|
|
@ -57,21 +59,26 @@ export const MainContent = () => {
|
|||
</Static>
|
||||
<OverflowProvider>
|
||||
<Box flexDirection="column">
|
||||
{pendingHistoryItems.map((item, i) => (
|
||||
<HistoryItemDisplay
|
||||
key={i}
|
||||
availableTerminalHeight={
|
||||
uiState.constrainHeight ? availableTerminalHeight : undefined
|
||||
}
|
||||
terminalWidth={terminalWidth}
|
||||
mainAreaWidth={mainAreaWidth}
|
||||
item={{ ...item, id: 0 }}
|
||||
isPending={true}
|
||||
isFocused={!uiState.isEditorDialogOpen}
|
||||
activeShellPtyId={uiState.activePtyId}
|
||||
embeddedShellFocused={uiState.embeddedShellFocused}
|
||||
/>
|
||||
))}
|
||||
{(frozenSnapshot ?? pendingHistoryItems).map((item, i) => {
|
||||
const isFrozen = frozenSnapshot !== null;
|
||||
return (
|
||||
<HistoryItemDisplay
|
||||
key={i}
|
||||
availableTerminalHeight={
|
||||
uiState.constrainHeight ? availableTerminalHeight : undefined
|
||||
}
|
||||
terminalWidth={terminalWidth}
|
||||
mainAreaWidth={mainAreaWidth}
|
||||
item={{ ...item, id: 0 }}
|
||||
isPending={true}
|
||||
isFocused={isFrozen ? false : !uiState.isEditorDialogOpen}
|
||||
activeShellPtyId={isFrozen ? undefined : uiState.activePtyId}
|
||||
embeddedShellFocused={
|
||||
isFrozen ? false : uiState.embeddedShellFocused
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ShowMoreLines constrainHeight={uiState.constrainHeight} />
|
||||
</Box>
|
||||
</OverflowProvider>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<Footer /> > footer rendering (golden snapshots) > renders complete footer on narrow terminal > complete-footer-narrow 1`] = `" ? for shortcuts 0.1% used"`;
|
||||
exports[`<Footer /> > footer rendering (golden snapshots) > renders complete footer on narrow terminal > complete-footer-narrow 1`] = `" ? for shortcuts 0.1% used | verbose"`;
|
||||
|
||||
exports[`<Footer /> > footer rendering (golden snapshots) > renders complete footer on wide terminal > complete-footer-wide 1`] = `" ? for shortcuts 0.1% context used"`;
|
||||
exports[`<Footer /> > footer rendering (golden snapshots) > renders complete footer on wide terminal > complete-footer-wide 1`] = `" ? for shortcuts 0.1% context used | verbose"`;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||
import { ToolCallStatus } from '../../types.js';
|
||||
import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
import { ToolStatusIndicator } from '../shared/ToolStatusIndicator.js';
|
||||
|
||||
interface CompactToolGroupDisplayProps {
|
||||
toolCalls: IndividualToolCallDisplay[];
|
||||
contentWidth: number;
|
||||
}
|
||||
|
||||
// Priority: Confirming > Executing > Error > Canceled > Pending > Success
|
||||
function getOverallStatus(
|
||||
toolCalls: IndividualToolCallDisplay[],
|
||||
): ToolCallStatus {
|
||||
if (toolCalls.some((t) => t.status === ToolCallStatus.Confirming))
|
||||
return ToolCallStatus.Confirming;
|
||||
if (toolCalls.some((t) => t.status === ToolCallStatus.Executing))
|
||||
return ToolCallStatus.Executing;
|
||||
if (toolCalls.some((t) => t.status === ToolCallStatus.Error))
|
||||
return ToolCallStatus.Error;
|
||||
if (toolCalls.some((t) => t.status === ToolCallStatus.Canceled))
|
||||
return ToolCallStatus.Canceled;
|
||||
if (toolCalls.some((t) => t.status === ToolCallStatus.Pending))
|
||||
return ToolCallStatus.Pending;
|
||||
return ToolCallStatus.Success;
|
||||
}
|
||||
|
||||
// Active tool priority: Confirming > Executing > last in array
|
||||
function getActiveTool(
|
||||
toolCalls: IndividualToolCallDisplay[],
|
||||
): IndividualToolCallDisplay {
|
||||
return (
|
||||
toolCalls.find((t) => t.status === ToolCallStatus.Confirming) ??
|
||||
toolCalls.find((t) => t.status === ToolCallStatus.Executing) ??
|
||||
toolCalls[toolCalls.length - 1]
|
||||
);
|
||||
}
|
||||
|
||||
export const CompactToolGroupDisplay: React.FC<
|
||||
CompactToolGroupDisplayProps
|
||||
> = ({ toolCalls, contentWidth }) => {
|
||||
if (toolCalls.length === 0) return null;
|
||||
|
||||
const overallStatus = getOverallStatus(toolCalls);
|
||||
const activeTool = getActiveTool(toolCalls);
|
||||
|
||||
const isShellCommand = toolCalls.some(
|
||||
(t) => t.name === SHELL_COMMAND_NAME || t.name === SHELL_NAME,
|
||||
);
|
||||
const hasPending = !toolCalls.every(
|
||||
(t) => t.status === ToolCallStatus.Success,
|
||||
);
|
||||
|
||||
const borderColor = isShellCommand
|
||||
? theme.ui.symbol
|
||||
: hasPending
|
||||
? theme.status.warning
|
||||
: theme.border.default;
|
||||
|
||||
// Take only the first line of description to prevent multi-line shell scripts
|
||||
// from expanding the compact view (wrap="truncate-end" only handles width overflow,
|
||||
// not literal \n characters in the content)
|
||||
const activeToolDescription = activeTool.description
|
||||
? activeTool.description.split('\n')[0]
|
||||
: '';
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
width={contentWidth}
|
||||
borderDimColor={hasPending}
|
||||
borderColor={borderColor}
|
||||
gap={0}
|
||||
>
|
||||
{/* Status line: icon + tool name + description */}
|
||||
<Box flexDirection="row">
|
||||
<ToolStatusIndicator status={overallStatus} name={activeTool.name} />
|
||||
<Box flexGrow={1}>
|
||||
<Text wrap="truncate-end">
|
||||
<Text bold>{activeTool.name}</Text>
|
||||
{activeToolDescription ? (
|
||||
<Text color={theme.text.secondary}>
|
||||
{' '}
|
||||
{activeToolDescription}
|
||||
</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Hint line */}
|
||||
<Text color={theme.text.secondary}>
|
||||
{t('Press Ctrl+O to show full tool output')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -11,9 +11,11 @@ import type { IndividualToolCallDisplay } from '../../types.js';
|
|||
import { ToolCallStatus } from '../../types.js';
|
||||
import { ToolMessage } from './ToolMessage.js';
|
||||
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
|
||||
import { CompactToolGroupDisplay } from './CompactToolGroupDisplay.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js';
|
||||
import { useConfig } from '../../contexts/ConfigContext.js';
|
||||
import { useVerboseMode } from '../../contexts/VerboseModeContext.js';
|
||||
|
||||
interface ToolGroupMessageProps {
|
||||
groupId: number;
|
||||
|
|
@ -24,6 +26,7 @@ interface ToolGroupMessageProps {
|
|||
activeShellPtyId?: number | null;
|
||||
embeddedShellFocused?: boolean;
|
||||
onShellInputSubmit?: (input: string) => void;
|
||||
isUserInitiated?: boolean;
|
||||
}
|
||||
|
||||
// Main component renders the border and maps the tools using ToolMessage
|
||||
|
|
@ -34,7 +37,15 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
isFocused = true,
|
||||
activeShellPtyId,
|
||||
embeddedShellFocused,
|
||||
isUserInitiated,
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
const { verboseMode } = useVerboseMode();
|
||||
|
||||
const hasConfirmingTool = toolCalls.some(
|
||||
(t) => t.status === ToolCallStatus.Confirming,
|
||||
);
|
||||
const hasErrorTool = toolCalls.some((t) => t.status === ToolCallStatus.Error);
|
||||
const isEmbeddedShellFocused =
|
||||
embeddedShellFocused &&
|
||||
toolCalls.some(
|
||||
|
|
@ -42,11 +53,36 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
t.ptyId === activeShellPtyId && t.status === ToolCallStatus.Executing,
|
||||
);
|
||||
|
||||
// useMemo must be called unconditionally (Rules of Hooks) — before any early return
|
||||
// only prompt for tool approval on the first 'confirming' tool in the list
|
||||
const toolAwaitingApproval = useMemo(
|
||||
() => toolCalls.find((tc) => tc.status === ToolCallStatus.Confirming),
|
||||
[toolCalls],
|
||||
);
|
||||
|
||||
// Compact mode: entire group → single line summary
|
||||
// Force-expand when: user must interact (Confirming), tool errored,
|
||||
// shell is focused, or user-initiated
|
||||
const showCompact =
|
||||
!verboseMode &&
|
||||
!hasConfirmingTool &&
|
||||
!hasErrorTool &&
|
||||
!isEmbeddedShellFocused &&
|
||||
!isUserInitiated;
|
||||
|
||||
if (showCompact) {
|
||||
return (
|
||||
<CompactToolGroupDisplay
|
||||
toolCalls={toolCalls}
|
||||
contentWidth={contentWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Full expanded view
|
||||
const hasPending = !toolCalls.every(
|
||||
(t) => t.status === ToolCallStatus.Success,
|
||||
);
|
||||
|
||||
const config = useConfig();
|
||||
const isShellCommand = toolCalls.some(
|
||||
(t) => t.name === SHELL_COMMAND_NAME || t.name === SHELL_NAME,
|
||||
);
|
||||
|
|
@ -61,13 +97,6 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
// account for border (2 chars) and padding (2 chars)
|
||||
const innerWidth = contentWidth - 4;
|
||||
|
||||
// only prompt for tool approval on the first 'confirming' tool in the list
|
||||
// note, after the CTA, this automatically moves over to the next 'confirming' tool
|
||||
const toolAwaitingApproval = useMemo(
|
||||
() => toolCalls.find((tc) => tc.status === ToolCallStatus.Confirming),
|
||||
[toolCalls],
|
||||
);
|
||||
|
||||
let countToolCallsWithResults = 0;
|
||||
for (const tool of toolCalls) {
|
||||
if (tool.resultDisplay !== undefined && tool.resultDisplay !== '') {
|
||||
|
|
@ -121,6 +150,11 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||
activeShellPtyId={activeShellPtyId}
|
||||
embeddedShellFocused={embeddedShellFocused}
|
||||
config={config}
|
||||
forceShowResult={
|
||||
isUserInitiated ||
|
||||
tool.status === ToolCallStatus.Confirming ||
|
||||
tool.status === ToolCallStatus.Error
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
{tool.status === ToolCallStatus.Confirming &&
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { StreamingState, ToolCallStatus } from '../../types.js';
|
|||
import { Text } from 'ink';
|
||||
import { StreamingContext } from '../../contexts/StreamingContext.js';
|
||||
import { SettingsContext } from '../../contexts/SettingsContext.js';
|
||||
import { VerboseModeProvider } from '../../contexts/VerboseModeContext.js';
|
||||
import type {
|
||||
AnsiOutput,
|
||||
AnsiOutputDisplay,
|
||||
|
|
@ -101,18 +102,21 @@ const mockSettings: LoadedSettings = {
|
|||
},
|
||||
} as LoadedSettings;
|
||||
|
||||
// Helper to render with context
|
||||
// Helper to render with context (verbose=true by default to show tool output)
|
||||
const renderWithContext = (
|
||||
ui: React.ReactElement,
|
||||
streamingState: StreamingState,
|
||||
verboseMode = true,
|
||||
) => {
|
||||
const contextValue: StreamingState = streamingState;
|
||||
return render(
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<StreamingContext.Provider value={contextValue}>
|
||||
{ui}
|
||||
</StreamingContext.Provider>
|
||||
</SettingsContext.Provider>,
|
||||
<VerboseModeProvider value={{ verboseMode, frozenSnapshot: null }}>
|
||||
<SettingsContext.Provider value={mockSettings}>
|
||||
<StreamingContext.Provider value={contextValue}>
|
||||
{ui}
|
||||
</StreamingContext.Provider>
|
||||
</SettingsContext.Provider>
|
||||
</VerboseModeProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -143,6 +147,18 @@ describe('<ToolMessage />', () => {
|
|||
expect(output).toContain('MockMarkdown:Test result');
|
||||
});
|
||||
|
||||
it('hides result output in compact mode (verboseMode=false)', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
<ToolMessage {...baseProps} />,
|
||||
StreamingState.Idle,
|
||||
false, // compact mode
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('✓'); // status indicator still visible
|
||||
expect(output).toContain('test-tool'); // tool name still visible
|
||||
expect(output).not.toContain('MockMarkdown:Test result'); // result hidden
|
||||
});
|
||||
|
||||
describe('ToolStatusIndicator rendering', () => {
|
||||
it('shows ✓ for Success status', () => {
|
||||
const { lastFrame } = renderWithContext(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { ToolCallStatus } from '../../types.js';
|
|||
import { DiffRenderer } from './DiffRenderer.js';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { AnsiOutputText } from '../AnsiOutput.js';
|
||||
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
|
||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { TodoDisplay } from '../TodoDisplay.js';
|
||||
import type {
|
||||
|
|
@ -25,18 +24,19 @@ import type {
|
|||
import { AgentExecutionDisplay } from '../subagents/index.js';
|
||||
import { PlanSummaryDisplay } from '../PlanSummaryDisplay.js';
|
||||
import { ShellInputPrompt } from '../ShellInputPrompt.js';
|
||||
import {
|
||||
SHELL_COMMAND_NAME,
|
||||
SHELL_NAME,
|
||||
TOOL_STATUS,
|
||||
} from '../../constants.js';
|
||||
import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||
import type { LoadedSettings } from '../../../config/settings.js';
|
||||
import { useVerboseMode } from '../../contexts/VerboseModeContext.js';
|
||||
|
||||
import {
|
||||
ToolStatusIndicator,
|
||||
STATUS_INDICATOR_WIDTH,
|
||||
} from '../shared/ToolStatusIndicator.js';
|
||||
|
||||
const STATIC_HEIGHT = 1;
|
||||
const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
|
||||
const STATUS_INDICATOR_WIDTH = 3;
|
||||
const MIN_LINES_SHOWN = 2; // show at least this many lines
|
||||
|
||||
// Large threshold to ensure we don't cause performance issues for very large
|
||||
|
|
@ -248,6 +248,7 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
|
|||
activeShellPtyId?: number | null;
|
||||
embeddedShellFocused?: boolean;
|
||||
config?: Config;
|
||||
forceShowResult?: boolean;
|
||||
}
|
||||
|
||||
export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
|
|
@ -263,10 +264,11 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||
embeddedShellFocused,
|
||||
ptyId,
|
||||
config,
|
||||
forceShowResult,
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
const isThisShellFocused =
|
||||
(name === SHELL_COMMAND_NAME || name === 'Shell') &&
|
||||
(name === SHELL_COMMAND_NAME || name === SHELL_NAME) &&
|
||||
status === ToolCallStatus.Executing &&
|
||||
ptyId === activeShellPtyId &&
|
||||
embeddedShellFocused;
|
||||
|
|
@ -300,7 +302,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||
}, [isThisShellFocused]);
|
||||
|
||||
const isThisShellFocusable =
|
||||
(name === SHELL_COMMAND_NAME || name === 'Shell') &&
|
||||
(name === SHELL_COMMAND_NAME || name === SHELL_NAME) &&
|
||||
status === ToolCallStatus.Executing &&
|
||||
config?.getShouldUseNodePtyShell();
|
||||
|
||||
|
|
@ -324,6 +326,11 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||
|
||||
// Use the custom hook to determine the display type
|
||||
const displayRenderer = useResultDisplayRenderer(resultDisplay);
|
||||
const { verboseMode } = useVerboseMode();
|
||||
const effectiveDisplayRenderer =
|
||||
verboseMode || forceShowResult
|
||||
? displayRenderer
|
||||
: { type: 'none' as const };
|
||||
|
||||
return (
|
||||
<Box paddingX={1} paddingY={0} flexDirection="column">
|
||||
|
|
@ -344,44 +351,44 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||
)}
|
||||
{emphasis === 'high' && <TrailingIndicator />}
|
||||
</Box>
|
||||
{displayRenderer.type !== 'none' && (
|
||||
{effectiveDisplayRenderer.type !== 'none' && (
|
||||
<Box paddingLeft={STATUS_INDICATOR_WIDTH} width="100%" marginTop={1}>
|
||||
<Box flexDirection="column">
|
||||
{displayRenderer.type === 'todo' && (
|
||||
<TodoResultRenderer data={displayRenderer.data} />
|
||||
{effectiveDisplayRenderer.type === 'todo' && (
|
||||
<TodoResultRenderer data={effectiveDisplayRenderer.data} />
|
||||
)}
|
||||
{displayRenderer.type === 'plan' && (
|
||||
{effectiveDisplayRenderer.type === 'plan' && (
|
||||
<PlanResultRenderer
|
||||
data={displayRenderer.data}
|
||||
data={effectiveDisplayRenderer.data}
|
||||
availableHeight={availableHeight}
|
||||
childWidth={innerWidth}
|
||||
/>
|
||||
)}
|
||||
{displayRenderer.type === 'task' && config && (
|
||||
{effectiveDisplayRenderer.type === 'task' && config && (
|
||||
<SubagentExecutionRenderer
|
||||
data={displayRenderer.data}
|
||||
data={effectiveDisplayRenderer.data}
|
||||
availableHeight={availableHeight}
|
||||
childWidth={innerWidth}
|
||||
config={config}
|
||||
/>
|
||||
)}
|
||||
{displayRenderer.type === 'diff' && (
|
||||
{effectiveDisplayRenderer.type === 'diff' && (
|
||||
<DiffResultRenderer
|
||||
data={displayRenderer.data}
|
||||
data={effectiveDisplayRenderer.data}
|
||||
availableHeight={availableHeight}
|
||||
childWidth={innerWidth}
|
||||
settings={settings}
|
||||
/>
|
||||
)}
|
||||
{displayRenderer.type === 'ansi' && (
|
||||
{effectiveDisplayRenderer.type === 'ansi' && (
|
||||
<AnsiOutputText
|
||||
data={displayRenderer.data}
|
||||
data={effectiveDisplayRenderer.data}
|
||||
availableTerminalHeight={availableHeight}
|
||||
/>
|
||||
)}
|
||||
{displayRenderer.type === 'string' && (
|
||||
{effectiveDisplayRenderer.type === 'string' && (
|
||||
<StringResultRenderer
|
||||
data={displayRenderer.data}
|
||||
data={effectiveDisplayRenderer.data}
|
||||
renderAsMarkdown={renderOutputAsMarkdown}
|
||||
availableHeight={availableHeight}
|
||||
childWidth={innerWidth}
|
||||
|
|
@ -402,53 +409,6 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
type ToolStatusIndicatorProps = {
|
||||
status: ToolCallStatus;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const ToolStatusIndicator: React.FC<ToolStatusIndicatorProps> = ({
|
||||
status,
|
||||
name,
|
||||
}) => {
|
||||
const isShell = name === SHELL_COMMAND_NAME || name === SHELL_NAME;
|
||||
const statusColor = isShell ? theme.ui.symbol : theme.status.warning;
|
||||
|
||||
return (
|
||||
<Box minWidth={STATUS_INDICATOR_WIDTH}>
|
||||
{status === ToolCallStatus.Pending && (
|
||||
<Text color={theme.status.success}>{TOOL_STATUS.PENDING}</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Executing && (
|
||||
<GeminiRespondingSpinner
|
||||
spinnerType="toggle"
|
||||
nonRespondingDisplay={TOOL_STATUS.EXECUTING}
|
||||
/>
|
||||
)}
|
||||
{status === ToolCallStatus.Success && (
|
||||
<Text color={theme.status.success} aria-label={'Success:'}>
|
||||
{TOOL_STATUS.SUCCESS}
|
||||
</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Confirming && (
|
||||
<Text color={statusColor} aria-label={'Confirming:'}>
|
||||
{TOOL_STATUS.CONFIRMING}
|
||||
</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Canceled && (
|
||||
<Text color={statusColor} aria-label={'Canceled:'} bold>
|
||||
{TOOL_STATUS.CANCELED}
|
||||
</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Error && (
|
||||
<Text color={theme.status.error} aria-label={'Error:'} bold>
|
||||
{TOOL_STATUS.ERROR}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
type ToolInfo = {
|
||||
name: string;
|
||||
description: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { ToolCallStatus } from '../../types.js';
|
||||
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
|
||||
import {
|
||||
TOOL_STATUS,
|
||||
SHELL_COMMAND_NAME,
|
||||
SHELL_NAME,
|
||||
} from '../../constants.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
|
||||
export const STATUS_INDICATOR_WIDTH = 3;
|
||||
|
||||
type ToolStatusIndicatorProps = {
|
||||
status: ToolCallStatus;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const ToolStatusIndicator: React.FC<ToolStatusIndicatorProps> = ({
|
||||
status,
|
||||
name,
|
||||
}) => {
|
||||
const isShell = name === SHELL_COMMAND_NAME || name === SHELL_NAME;
|
||||
const statusColor = isShell ? theme.ui.symbol : theme.status.warning;
|
||||
|
||||
return (
|
||||
<Box minWidth={STATUS_INDICATOR_WIDTH}>
|
||||
{status === ToolCallStatus.Pending && (
|
||||
<Text color={theme.status.success}>{TOOL_STATUS.PENDING}</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Executing && (
|
||||
<GeminiRespondingSpinner
|
||||
spinnerType="toggle"
|
||||
nonRespondingDisplay={TOOL_STATUS.EXECUTING}
|
||||
/>
|
||||
)}
|
||||
{status === ToolCallStatus.Success && (
|
||||
<Text color={theme.status.success} aria-label={'Success:'}>
|
||||
{TOOL_STATUS.SUCCESS}
|
||||
</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Confirming && (
|
||||
<Text color={statusColor} aria-label={'Confirming:'}>
|
||||
{TOOL_STATUS.CONFIRMING}
|
||||
</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Canceled && (
|
||||
<Text color={statusColor} aria-label={'Canceled:'} bold>
|
||||
{TOOL_STATUS.CANCELED}
|
||||
</Text>
|
||||
)}
|
||||
{status === ToolCallStatus.Error && (
|
||||
<Text color={theme.status.error} aria-label={'Error:'} bold>
|
||||
{TOOL_STATUS.ERROR}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue