qwen-code/packages/cli/src/ui/components/messages/CompactToolGroupDisplay.tsx
Shaojin Wen 69da115dcf
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
feat(cli): combine elapsed + timeout in shell time indicator (#3512)
* feat(cli): combine elapsed + timeout in shell time indicator

Render shell tools that have an explicit timeout as
`(elapsed · timeout N)` inline with the Running… status from t=0,
instead of splitting the information across the right-aligned elapsed
indicator and the ShellStatsBar row.

- formatters: add a `hideTrailingZeros` option so whole seconds render
  as `5s` rather than `5.0s` while fractional values like `5.5s` stay
  intact
- ToolElapsedTime: accept optional `timeoutMs`; when set, skip the 3s
  quiet threshold and render the combined `(elapsed · timeout N)` label
- ToolMessage: extract `timeoutMs` from AnsiOutputDisplay and feed it
  to ToolElapsedTime
- ShellStatsBar: drop its `timeoutMs` field (now inline); keeps
  `+N lines` and memory usage only
- Unify both modes on `formatDuration` so hour-range output is
  consistent (`1h 2m 6s` across timeout and no-timeout paths)

* feat(cli): thread shell timeoutMs through compact tool group display

The combined `(elapsed · timeout N)` format introduced in the previous
commit was only wired through the expanded ToolMessage path. Compact
tool groups kept rendering ToolElapsedTime without timeoutMs, so shell
tools displayed in compact mode silently dropped the timeout budget.

- CompactToolGroupDisplay: add getShellTimeoutMs() to pull timeoutMs
  off the active tool's AnsiOutputDisplay result (same shape used by
  ToolMessage) and feed it to ToolElapsedTime
- add CompactToolGroupDisplay.test.tsx covering the three paths:
  ansi display with timeoutMs, ansi display without timeoutMs, and
  non-ansi resultDisplay (string)
2026-04-23 08:52:37 +08:00

139 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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 type { AnsiOutputDisplay } from '@qwen-code/qwen-code-core';
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';
import { ToolElapsedTime } from '../shared/ToolElapsedTime.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]
);
}
// Pull the configured shell timeout off an AnsiOutputDisplay result so
// ToolElapsedTime can surface it inline (matches the expanded
// ToolMessage path). Non-ansi resultDisplay → undefined → legacy
// quiet-then-elapsed behavior.
function getShellTimeoutMs(
tool: IndividualToolCallDisplay,
): number | undefined {
const display = tool.resultDisplay;
if (
typeof display === 'object' &&
display !== null &&
'ansiOutput' in display
) {
return (display as AnsiOutputDisplay).timeoutMs;
}
return undefined;
}
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 + count + description + elapsed */}
<Box flexDirection="row">
<ToolStatusIndicator status={overallStatus} name={activeTool.name} />
<Box flexGrow={1}>
<Text wrap="truncate-end">
<Text bold>{activeTool.name}</Text>
{toolCalls.length > 1 ? (
<Text color={theme.text.secondary}>
{' × '}
{toolCalls.length}
</Text>
) : null}
{activeToolDescription ? (
<Text color={theme.text.secondary}>
{' '}
{activeToolDescription}
</Text>
) : null}
</Text>
</Box>
<ToolElapsedTime
status={overallStatus}
executionStartTime={activeTool.executionStartTime}
timeoutMs={getShellTimeoutMs(activeTool)}
/>
</Box>
{/* Hint line */}
<Text color={theme.text.secondary}>
{t('Press Ctrl+O to show full tool output')}
</Text>
</Box>
);
};