Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
640 changes: 640 additions & 0 deletions integration-tests/terminal-capture/subagent-flicker-regression.ts

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions packages/cli/src/ui/AppContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ describe('AppContainer State Management', () => {
const mockedUseTextBuffer = useTextBuffer as Mock;
const mockedUseLogger = useLogger as Mock;
const mockedUseLoadingIndicator = useLoadingIndicator as Mock;
const mockedUseTerminalSize = useTerminalSize as Mock;

beforeEach(() => {
vi.clearAllMocks();
Expand Down Expand Up @@ -276,6 +277,7 @@ describe('AppContainer State Management', () => {
elapsedTime: '0.0s',
currentLoadingPhrase: '',
});
mockedUseTerminalSize.mockReturnValue({ columns: 80, rows: 24 });

// Mock Config
mockConfig = makeFakeConfig();
Expand Down Expand Up @@ -452,6 +454,34 @@ describe('AppContainer State Management', () => {
expect(mockStdout.write).toHaveBeenCalledWith(ansiEscapes.clearTerminal);
});

it('does not clear the terminal just because width changed', () => {
vi.spyOn(mockConfig, 'initialize').mockResolvedValue(undefined);
mockedUseTerminalSize.mockReturnValue({ columns: 80, rows: 24 });
const { rerender } = render(
<AppContainer
config={mockConfig}
settings={mockSettings}
version="1.0.0"
initializationResult={mockInitResult}
/>,
);
mockStdout.write.mockClear();

mockedUseTerminalSize.mockReturnValue({ columns: 100, rows: 24 });
rerender(
<AppContainer
config={mockConfig}
settings={mockSettings}
version="1.0.0"
initializationResult={mockInitResult}
/>,
);

expect(mockStdout.write).not.toHaveBeenCalledWith(
ansiEscapes.clearTerminal,
);
});

it('handleClearScreen avoids a second clearTerminal write', () => {
const clearSpy = vi.spyOn(console, 'clear').mockImplementation(() => {});

Expand Down
17 changes: 0 additions & 17 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1643,8 +1643,6 @@ export const AppContainer = (props: AppContainerProps) => {
pager: settings.merged.tools?.shell?.pager,
showColor: settings.merged.tools?.shell?.showColor,
});
const isInitialMount = useRef(true);

useEffect(() => {
if (activePtyId) {
ShellExecutionService.resizePty(
Expand All @@ -1662,21 +1660,6 @@ export const AppContainer = (props: AppContainerProps) => {
}
}, [ideNeedsRestart]);

useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
return;
}

const handler = setTimeout(() => {
refreshStatic();
}, 300);

return () => {
clearTimeout(handler);
};
}, [terminalWidth, refreshStatic]);

useEffect(() => {
const unsubscribe = ideContextStore.subscribe(setIdeContextState);
setIdeContextState(ideContextStore.get());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/

import { act } from 'react';
import { render } from 'ink-testing-library';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { AgentResultDisplay } from '@qwen-code/qwen-code-core';
import { makeFakeConfig } from '@qwen-code/qwen-code-core';
import { AgentExecutionDisplay } from './AgentExecutionDisplay.js';

let keypressHandler:
| ((key: { ctrl?: boolean; name?: string }) => void)
| undefined;

vi.mock('../../../hooks/useKeypress.js', () => ({
// The mock honours { isActive } so historical/completed displays don't
// capture the keypress handler — same scoping the production hook does.
useKeypress: vi.fn(
(
handler: (key: { ctrl?: boolean; name?: string }) => void,
options?: { isActive?: boolean },
) => {
keypressHandler = options?.isActive === false ? undefined : handler;
},
),
}));

function makeRunningData(toolCount: number): AgentResultDisplay {
return {
type: 'task_execution',
subagentName: 'reviewer',
subagentColor: 'blue',
status: 'running',
taskDescription: 'Review large output stability',
taskPrompt: `${'very-long-task-prompt '.repeat(20)}\nsecond\nthird`,
toolCalls: Array.from({ length: toolCount }, (_, index) => ({
callId: `call-${index}`,
name: `tool-${index}`,
status: 'success',
description: `description-${index} ${'wide '.repeat(20)}`,
resultDisplay: `result-${index} ${'payload '.repeat(20)}`,
})),
};
}

function makeCompletedData(toolCount: number): AgentResultDisplay {
return {
...makeRunningData(toolCount),
status: 'completed',
executionSummary: {
rounds: 3,
totalDurationMs: 12_345,
totalToolCalls: toolCount,
successfulToolCalls: toolCount,
failedToolCalls: 0,
successRate: 100,
inputTokens: 100,
outputTokens: 200,
thoughtTokens: 0,
cachedTokens: 0,
totalTokens: 4_321,
toolUsage: [],
},
};
}

function visualRowCount(frame: string): number {
if (!frame) return 0;
return frame.split('\n').length;
}

describe('<AgentExecutionDisplay />', () => {
beforeEach(() => {
keypressHandler = undefined;
});

it('bounds expanded detail by the assigned visual height', () => {
const { lastFrame } = render(
<AgentExecutionDisplay
data={makeRunningData(8)}
availableHeight={8}
childWidth={40}
config={makeFakeConfig()}
/>,
);

act(() => {
keypressHandler?.({ ctrl: true, name: 'e' });
});

const frame = lastFrame() ?? '';
expect(frame).toContain('Showing the first 1 visual lines');
expect(frame).toContain('Showing the last 1 of 8 tools');
expect(frame).toContain('tool-7');
expect(frame).not.toContain('tool-0');
});

it('keeps the rendered running frame within availableHeight', () => {
const availableHeight = 26;
const { lastFrame } = render(
<AgentExecutionDisplay
data={makeRunningData(8)}
availableHeight={availableHeight}
childWidth={80}
config={makeFakeConfig()}
/>,
);

act(() => {
keypressHandler?.({ ctrl: true, name: 'e' });
});

expect(visualRowCount(lastFrame() ?? '')).toBeLessThanOrEqual(
availableHeight,
);
});

it('keeps the rendered completed frame within availableHeight', () => {
Comment thread
wenshao marked this conversation as resolved.
Outdated
const availableHeight = 30;
const { lastFrame } = render(
<AgentExecutionDisplay
data={makeCompletedData(8)}
availableHeight={availableHeight}
childWidth={80}
config={makeFakeConfig()}
/>,
);

act(() => {
keypressHandler?.({ ctrl: true, name: 'e' });
});

expect(visualRowCount(lastFrame() ?? '')).toBeLessThanOrEqual(
availableHeight,
);
});

it('does not respond to ctrl+e once the subagent has completed', () => {
const { lastFrame } = render(
<AgentExecutionDisplay
data={makeCompletedData(2)}
availableHeight={20}
childWidth={80}
config={makeFakeConfig()}
/>,
);

const before = lastFrame() ?? '';
act(() => {
keypressHandler?.({ ctrl: true, name: 'e' });
});
const after = lastFrame() ?? '';

expect(after).toBe(before);
});
});
Loading
Loading