mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
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 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)
139 lines
4.4 KiB
TypeScript
139 lines
4.4 KiB
TypeScript
/**
|
||
* @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>
|
||
);
|
||
};
|