feat(core): agnostic background task UI with CompletionBehavior (#22740)

Co-authored-by: mkorwel <matt.korwel@gmail.com>
This commit is contained in:
Adam Weidman
2026-03-28 17:27:51 -04:00
committed by GitHub
parent 07ab16dbbe
commit 3eebb75b7a
54 changed files with 1467 additions and 875 deletions
@@ -10,7 +10,7 @@ import { DefaultAppLayout } from './DefaultAppLayout.js';
import { StreamingState } from '../types.js';
import { Text } from 'ink';
import type { UIState } from '../contexts/UIStateContext.js';
import type { BackgroundShell } from '../hooks/shellCommandProcessor.js';
import type { BackgroundTask } from '../hooks/useExecutionLifecycle.js';
// Mock dependencies
const mockUIState = {
@@ -18,13 +18,13 @@ const mockUIState = {
terminalHeight: 24,
terminalWidth: 80,
mainAreaWidth: 80,
backgroundShells: new Map<number, BackgroundShell>(),
activeBackgroundShellPid: null as number | null,
backgroundShellHeight: 10,
backgroundTasks: new Map<number, BackgroundTask>(),
activeBackgroundTaskPid: null as number | null,
backgroundTaskHeight: 10,
embeddedShellFocused: false,
dialogsVisible: false,
streamingState: StreamingState.Idle,
isBackgroundShellListOpen: false,
isBackgroundTaskListOpen: false,
mainControlsRef: vi.fn(),
customDialog: null,
historyManager: { addItem: vi.fn() },
@@ -34,7 +34,7 @@ const mockUIState = {
constrainHeight: false,
availableTerminalHeight: 20,
activePtyId: null,
isBackgroundShellVisible: true,
isBackgroundTaskVisible: true,
} as unknown as UIState;
vi.mock('../contexts/UIStateContext.js', () => ({
@@ -79,11 +79,11 @@ vi.mock('../components/ExitWarning.js', () => ({
vi.mock('../components/CopyModeWarning.js', () => ({
CopyModeWarning: () => <Text>CopyModeWarning</Text>,
}));
vi.mock('../components/BackgroundShellDisplay.js', () => ({
BackgroundShellDisplay: () => <Text>BackgroundShellDisplay</Text>,
vi.mock('../components/BackgroundTaskDisplay.js', () => ({
BackgroundTaskDisplay: () => <Text>BackgroundTaskDisplay</Text>,
}));
const createMockShell = (pid: number): BackgroundShell => ({
const createMockShell = (pid: number): BackgroundTask => ({
pid,
command: 'test command',
output: 'test output',
@@ -96,25 +96,25 @@ describe('<DefaultAppLayout />', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset mock state defaults
mockUIState.backgroundShells = new Map();
mockUIState.activeBackgroundShellPid = null;
mockUIState.backgroundTasks = new Map();
mockUIState.activeBackgroundTaskPid = null;
mockUIState.streamingState = StreamingState.Idle;
});
it('renders BackgroundShellDisplay when shells exist and active', async () => {
mockUIState.backgroundShells.set(123, createMockShell(123));
mockUIState.activeBackgroundShellPid = 123;
mockUIState.backgroundShellHeight = 5;
it('renders BackgroundTaskDisplay when shells exist and active', async () => {
mockUIState.backgroundTasks.set(123, createMockShell(123));
mockUIState.activeBackgroundTaskPid = 123;
mockUIState.backgroundTaskHeight = 5;
const { lastFrame, unmount } = await render(<DefaultAppLayout />);
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('hides BackgroundShellDisplay when StreamingState is WaitingForConfirmation', async () => {
mockUIState.backgroundShells.set(123, createMockShell(123));
mockUIState.activeBackgroundShellPid = 123;
mockUIState.backgroundShellHeight = 5;
it('hides BackgroundTaskDisplay when StreamingState is WaitingForConfirmation', async () => {
mockUIState.backgroundTasks.set(123, createMockShell(123));
mockUIState.activeBackgroundTaskPid = 123;
mockUIState.backgroundTaskHeight = 5;
mockUIState.streamingState = StreamingState.WaitingForConfirmation;
const { lastFrame, unmount } = await render(<DefaultAppLayout />);
@@ -122,10 +122,10 @@ describe('<DefaultAppLayout />', () => {
unmount();
});
it('shows BackgroundShellDisplay when StreamingState is NOT WaitingForConfirmation', async () => {
mockUIState.backgroundShells.set(123, createMockShell(123));
mockUIState.activeBackgroundShellPid = 123;
mockUIState.backgroundShellHeight = 5;
it('shows BackgroundTaskDisplay when StreamingState is NOT WaitingForConfirmation', async () => {
mockUIState.backgroundTasks.set(123, createMockShell(123));
mockUIState.activeBackgroundTaskPid = 123;
mockUIState.backgroundTaskHeight = 5;
mockUIState.streamingState = StreamingState.Responding;
const { lastFrame, unmount } = await render(<DefaultAppLayout />);
@@ -15,7 +15,7 @@ import { useUIState } from '../contexts/UIStateContext.js';
import { useFlickerDetector } from '../hooks/useFlickerDetector.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import { CopyModeWarning } from '../components/CopyModeWarning.js';
import { BackgroundShellDisplay } from '../components/BackgroundShellDisplay.js';
import { BackgroundTaskDisplay } from '../components/BackgroundTaskDisplay.js';
import { StreamingState } from '../types.js';
export const DefaultAppLayout: React.FC = () => {
@@ -39,21 +39,21 @@ export const DefaultAppLayout: React.FC = () => {
>
<MainContent />
{uiState.isBackgroundShellVisible &&
uiState.backgroundShells.size > 0 &&
uiState.activeBackgroundShellPid &&
uiState.backgroundShellHeight > 0 &&
{uiState.isBackgroundTaskVisible &&
uiState.backgroundTasks.size > 0 &&
uiState.activeBackgroundTaskPid &&
uiState.backgroundTaskHeight > 0 &&
uiState.streamingState !== StreamingState.WaitingForConfirmation && (
<Box height={uiState.backgroundShellHeight} flexShrink={0}>
<BackgroundShellDisplay
shells={uiState.backgroundShells}
activePid={uiState.activeBackgroundShellPid}
<Box height={uiState.backgroundTaskHeight} flexShrink={0}>
<BackgroundTaskDisplay
shells={uiState.backgroundTasks}
activePid={uiState.activeBackgroundTaskPid}
width={uiState.terminalWidth}
height={uiState.backgroundShellHeight}
height={uiState.backgroundTaskHeight}
isFocused={
uiState.embeddedShellFocused && !uiState.dialogsVisible
}
isListOpenProp={uiState.isBackgroundShellListOpen}
isListOpenProp={uiState.isBackgroundTaskListOpen}
/>
</Box>
)}
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<DefaultAppLayout /> > hides BackgroundShellDisplay when StreamingState is WaitingForConfirmation 1`] = `
exports[`<DefaultAppLayout /> > hides BackgroundTaskDisplay when StreamingState is WaitingForConfirmation 1`] = `
"MainContent
Notifications
CopyModeWarning
@@ -9,9 +9,9 @@ ExitWarning
"
`;
exports[`<DefaultAppLayout /> > renders BackgroundShellDisplay when shells exist and active 1`] = `
exports[`<DefaultAppLayout /> > renders BackgroundTaskDisplay when shells exist and active 1`] = `
"MainContent
BackgroundShellDisplay
BackgroundTaskDisplay
@@ -23,9 +23,9 @@ ExitWarning
"
`;
exports[`<DefaultAppLayout /> > shows BackgroundShellDisplay when StreamingState is NOT WaitingForConfirmation 1`] = `
exports[`<DefaultAppLayout /> > shows BackgroundTaskDisplay when StreamingState is NOT WaitingForConfirmation 1`] = `
"MainContent
BackgroundShellDisplay
BackgroundTaskDisplay