diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx
index d8883e673..159d59cd4 100644
--- a/packages/cli/src/ui/App.test.tsx
+++ b/packages/cli/src/ui/App.test.tsx
@@ -49,6 +49,7 @@ describe('App', () => {
quittingMessages: null,
dialogsVisible: false,
mainControlsRef: { current: null },
+ subagentFullscreenPanel: null,
historyManager: {
addItem: vi.fn(),
history: [],
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index ea8482a16..25838d78d 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { useIsScreenReaderEnabled } from 'ink';
+import { Box, useIsScreenReaderEnabled } from 'ink';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import { lerp } from '../utils/math.js';
import { useUIState } from './contexts/UIStateContext.js';
@@ -12,6 +12,7 @@ import { StreamingContext } from './contexts/StreamingContext.js';
import { QuittingDisplay } from './components/QuittingDisplay.js';
import { ScreenReaderAppLayout } from './layouts/ScreenReaderAppLayout.js';
import { DefaultAppLayout } from './layouts/DefaultAppLayout.js';
+import { SubagentFullscreenPanel } from './components/subagents/runtime/SubagentFullscreenPanel.js';
const getContainerWidth = (terminalWidth: number): string => {
if (terminalWidth <= 80) {
@@ -38,12 +39,18 @@ export const App = () => {
return ;
}
+ const showFullscreen = Boolean(uiState.subagentFullscreenPanel);
+ const mainLayout = isScreenReaderEnabled ? (
+
+ ) : (
+
+ );
+
return (
- {isScreenReaderEnabled ? (
-
- ) : (
-
+ {mainLayout}
+ {showFullscreen && uiState.subagentFullscreenPanel && (
+
)}
);
diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx
index 0abb960c0..bbfc40b33 100644
--- a/packages/cli/src/ui/AppContainer.test.tsx
+++ b/packages/cli/src/ui/AppContainer.test.tsx
@@ -634,6 +634,34 @@ describe('AppContainer State Management', () => {
capturedUIActions.handleProQuotaChoice('auth');
expect(mockHandler).toHaveBeenCalledWith('auth');
});
+
+ it('disables the input prompt when subagent fullscreen panel is open', async () => {
+ render(
+ ,
+ );
+
+ expect(capturedUIState.isInputActive).toBe(true);
+
+ capturedUIActions.openSubagentFullscreenPanel({
+ panelId: 'test-panel',
+ subagentName: 'Agent',
+ status: 'running',
+ content: [],
+ getSnapshot: () => [],
+ });
+
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ expect(capturedUIState.isInputActive).toBe(false);
+
+ capturedUIActions.closeSubagentFullscreenPanel('test-panel');
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ expect(capturedUIState.isInputActive).toBe(true);
+ });
});
describe('Terminal Title Update Feature', () => {
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index 5e76bc194..f8bed85b4 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -26,6 +26,7 @@ import {
ToolCallStatus,
type HistoryItemWithoutId,
AuthState,
+ type SubagentFullscreenPanelState,
} from './types.js';
import { MessageType, StreamingState } from './types.js';
import {
@@ -149,6 +150,8 @@ export const AppContainer = (props: AppContainerProps) => {
initializationResult.geminiMdFileCount,
);
const [shellModeActive, setShellModeActive] = useState(false);
+ const [subagentFullscreenPanel, setSubagentFullscreenPanel] =
+ useState(null);
const [modelSwitchedFromQuotaError, setModelSwitchedFromQuotaError] =
useState(false);
const [historyRemountKey, setHistoryRemountKey] = useState(0);
@@ -450,6 +453,46 @@ export const AppContainer = (props: AppContainerProps) => {
closeAgentsManagerDialog,
} = useAgentsManagerDialog();
+ const openSubagentFullscreenPanel = useCallback(
+ (panel: SubagentFullscreenPanelState) => {
+ setSubagentFullscreenPanel(panel);
+ },
+ [],
+ );
+
+ const closeSubagentFullscreenPanel = useCallback((panelId: string) => {
+ setSubagentFullscreenPanel((current) => {
+ if (!current || current.panelId !== panelId) {
+ return current;
+ }
+ current.onClose?.();
+ return null;
+ });
+ }, []);
+
+ const updateSubagentFullscreenPanel = useCallback(
+ (
+ panelId: string,
+ updates: Partial<
+ Pick<
+ SubagentFullscreenPanelState,
+ 'content' | 'status' | 'subagentName'
+ >
+ >,
+ ) => {
+ setSubagentFullscreenPanel((current) => {
+ if (!current || current.panelId !== panelId) {
+ return current;
+ }
+ return {
+ ...current,
+ ...updates,
+ };
+ });
+ },
+ [],
+ );
+
// Vision model auto-switch dialog state (must be before slashCommandActions)
const [isVisionSwitchDialogOpen, setIsVisionSwitchDialogOpen] =
useState(false);
@@ -730,7 +773,8 @@ export const AppContainer = (props: AppContainerProps) => {
!isProcessing &&
(streamingState === StreamingState.Idle ||
streamingState === StreamingState.Responding) &&
- !proQuotaRequest;
+ !proQuotaRequest &&
+ !subagentFullscreenPanel;
const [controlsHeight, setControlsHeight] = useState(0);
@@ -1294,6 +1338,7 @@ export const AppContainer = (props: AppContainerProps) => {
// Subagent dialogs
isSubagentCreateDialogOpen,
isAgentsManagerDialogOpen,
+ subagentFullscreenPanel,
}),
[
isThemeDialogOpen,
@@ -1389,6 +1434,7 @@ export const AppContainer = (props: AppContainerProps) => {
// Subagent dialogs
isSubagentCreateDialogOpen,
isAgentsManagerDialogOpen,
+ subagentFullscreenPanel,
],
);
@@ -1427,6 +1473,9 @@ export const AppContainer = (props: AppContainerProps) => {
// Subagent dialogs
closeSubagentCreateDialog,
closeAgentsManagerDialog,
+ openSubagentFullscreenPanel,
+ closeSubagentFullscreenPanel,
+ updateSubagentFullscreenPanel,
}),
[
handleThemeSelect,
@@ -1460,6 +1509,9 @@ export const AppContainer = (props: AppContainerProps) => {
// Subagent dialogs
closeSubagentCreateDialog,
closeAgentsManagerDialog,
+ openSubagentFullscreenPanel,
+ closeSubagentFullscreenPanel,
+ updateSubagentFullscreenPanel,
],
);
diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx
index 084cd7465..c33e150c4 100644
--- a/packages/cli/src/ui/components/Composer.test.tsx
+++ b/packages/cli/src/ui/components/Composer.test.tsx
@@ -124,6 +124,7 @@ const createMockUIState = (overrides: Partial = {}): UIState =>
errorCount: 0,
nightly: false,
isTrustedFolder: true,
+ subagentFullscreenPanel: null,
...overrides,
}) as UIState;
@@ -134,6 +135,9 @@ const createMockUIActions = (): UIActions =>
setShellModeActive: vi.fn(),
onEscapePromptChange: vi.fn(),
vimHandleInput: vi.fn(),
+ openSubagentFullscreenPanel: vi.fn(),
+ closeSubagentFullscreenPanel: vi.fn(),
+ updateSubagentFullscreenPanel: vi.fn(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any;
diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx
index 3d9b91b8a..838e59275 100644
--- a/packages/cli/src/ui/components/Footer.test.tsx
+++ b/packages/cli/src/ui/components/Footer.test.tsx
@@ -52,6 +52,7 @@ const createMockUIState = (overrides: Partial = {}): UIState =>
lastPromptTokenCount: 100,
},
branchName: defaultProps.branchName,
+ subagentFullscreenPanel: null,
...overrides,
}) as UIState;
diff --git a/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx b/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx
index 9bd9a812f..98805436b 100644
--- a/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx
+++ b/packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx
@@ -16,6 +16,11 @@ import { useKeypress } from '../../../hooks/useKeypress.js';
import { COLOR_OPTIONS } from '../constants.js';
import { fmtDuration } from '../utils.js';
import { ToolConfirmationMessage } from '../../messages/ToolConfirmationMessage.js';
+import { useUIActions } from '../../../contexts/UIActionsContext.js';
+import { useUIState } from '../../../contexts/UIStateContext.js';
+import type { SubagentFullscreenPanelState } from '../../../types.js';
+import { CTRL_ALT_E_SEQUENCE } from './fullscreenKeys.js';
+import { getStatusColor, getStatusText } from './status.js';
export type DisplayMode = 'compact' | 'default' | 'verbose';
@@ -26,47 +31,23 @@ export interface AgentExecutionDisplayProps {
config: Config;
}
-const getStatusColor = (
- status:
- | TaskResultDisplay['status']
- | 'executing'
- | 'success'
- | 'awaiting_approval',
-) => {
- switch (status) {
- case 'running':
- case 'executing':
- case 'awaiting_approval':
- return theme.status.warning;
- case 'completed':
- case 'success':
- return theme.status.success;
- case 'cancelled':
- return theme.status.warning;
- case 'failed':
- return theme.status.error;
- default:
- return theme.text.secondary;
- }
-};
-
-const getStatusText = (status: TaskResultDisplay['status']) => {
- switch (status) {
- case 'running':
- return 'Running';
- case 'completed':
- return 'Completed';
- case 'cancelled':
- return 'User Cancelled';
- case 'failed':
- return 'Failed';
- default:
- return 'Unknown';
- }
-};
-
const MAX_TOOL_CALLS = 5;
const MAX_TASK_PROMPT_LINES = 5;
+const MAX_FULLSCREEN_RESULT_LINES = 12;
+
+const TOOL_CALL_STATUS_ICONS = {
+ executing: '⊷',
+ awaiting_approval: '?',
+ success: '✓',
+ failed: '✖',
+} as const;
+
+const TOOL_CALL_STATUS_TEXT = {
+ executing: 'Executing',
+ awaiting_approval: 'Awaiting approval',
+ success: 'Success',
+ failed: 'Failed',
+} as const;
/**
* Component to display subagent execution progress and results.
@@ -79,6 +60,16 @@ export const AgentExecutionDisplay: React.FC = ({
childWidth,
config,
}) => {
+ const uiActions = useUIActions();
+ const uiState = useUIState();
+ const panelIdRef = React.useRef(
+ `subagent-${Math.random().toString(36).slice(2)}`,
+ );
+ const panelId = panelIdRef.current;
+ const panelOpen = uiState.subagentFullscreenPanel?.panelId === panelId;
+ const renderDataRef = React.useRef(data);
+ renderDataRef.current = data;
+
const [displayMode, setDisplayMode] = React.useState('compact');
const agentColor = useMemo(() => {
@@ -88,6 +79,12 @@ export const AgentExecutionDisplay: React.FC = ({
return colorOption?.value || theme.text.accent;
}, [data.subagentColor]);
+ const fullscreenHint = ' Press ctrl+alt+e for fullscreen.';
+ const fullscreenContent = React.useMemo(
+ () => buildFullscreenContent(data),
+ [data],
+ );
+
const footerText = React.useMemo(() => {
// This component only listens to keyboard shortcut events when the subagent is running
if (data.status !== 'running') return '';
@@ -99,21 +96,58 @@ export const AgentExecutionDisplay: React.FC = ({
data.toolCalls && data.toolCalls.length > MAX_TOOL_CALLS;
if (hasMoreToolCalls || hasMoreLines) {
- return 'Press ctrl+r to show less, ctrl+e to show more.';
+ return `Press ctrl+r to show less, ctrl+e to show more.${fullscreenHint}`;
}
- return 'Press ctrl+r to show less.';
+ return `Press ctrl+r to show less.${fullscreenHint}`;
}
if (displayMode === 'verbose') {
- return 'Press ctrl+e to show less.';
+ return `Press ctrl+e to show less.${fullscreenHint}`;
+ }
+
+ if (displayMode === 'compact') {
+ return `Press ctrl+r to expand.${fullscreenHint}`;
}
- return '';
+ return fullscreenHint.trim();
}, [displayMode, data]);
- // Handle keyboard shortcuts to control display mode
+ const openFullscreen = React.useCallback(() => {
+ const panel: SubagentFullscreenPanelState = {
+ panelId,
+ subagentName: data.subagentName,
+ status: data.status,
+ content: fullscreenContent,
+ getSnapshot: () => buildFullscreenContent(renderDataRef.current),
+ };
+ uiActions.openSubagentFullscreenPanel(panel);
+ }, [data, fullscreenContent, panelId, uiActions]);
+
+ const closeFullscreen = React.useCallback(() => {
+ uiActions.closeSubagentFullscreenPanel(panelId);
+ }, [panelId, uiActions]);
+
+ // Handle keyboard shortcuts to control display mode and fullscreen toggle
useKeypress(
(key) => {
+ const sequence = key.sequence ?? '';
+ const isCtrlAltToggle =
+ (key.ctrl && key.meta && key.name === 'e') ||
+ sequence === CTRL_ALT_E_SEQUENCE;
+
+ if (isCtrlAltToggle) {
+ if (panelOpen) {
+ closeFullscreen();
+ } else {
+ openFullscreen();
+ }
+ return;
+ }
+
+ if (panelOpen) {
+ return;
+ }
+
if (key.ctrl && key.name === 'r') {
// ctrl+r toggles between compact and default
setDisplayMode((current) =>
@@ -129,6 +163,24 @@ export const AgentExecutionDisplay: React.FC = ({
{ isActive: true },
);
+ React.useEffect(() => {
+ if (!panelOpen) {
+ return;
+ }
+ uiActions.updateSubagentFullscreenPanel(panelId, {
+ content: fullscreenContent,
+ status: data.status,
+ subagentName: data.subagentName,
+ });
+ }, [
+ panelOpen,
+ data.status,
+ data.subagentName,
+ fullscreenContent,
+ panelId,
+ uiActions,
+ ]);
+
if (displayMode === 'compact') {
return (
@@ -200,6 +252,12 @@ export const AgentExecutionDisplay: React.FC = ({
)}
+
+
+
+ Press ctrl+alt+e for fullscreen.
+
+
);
}
@@ -220,6 +278,7 @@ export const AgentExecutionDisplay: React.FC = ({
{/* Progress section for running tasks */}
@@ -271,7 +330,8 @@ export const AgentExecutionDisplay: React.FC = ({
const TaskPromptSection: React.FC<{
taskPrompt: string;
displayMode: DisplayMode;
-}> = ({ taskPrompt, displayMode }) => {
+ showFullscreenHint?: boolean;
+}> = ({ taskPrompt, displayMode, showFullscreenHint = false }) => {
const lines = taskPrompt.split('\n');
const shouldTruncate = lines.length > 10;
const showFull = displayMode === 'verbose';
@@ -293,6 +353,13 @@ const TaskPromptSection: React.FC<{
{displayLines.join('\n') + (shouldTruncate && !showFull ? '...' : '')}
+ {showFullscreenHint && (
+
+
+ Press ctrl+alt+e for fullscreen.
+
+
+ )}
);
};
@@ -548,3 +615,149 @@ const ResultsSection: React.FC<{
)}
);
+
+function appendMultiline(
+ lines: string[],
+ text: string,
+ indent: string,
+ maxLines?: number,
+) {
+ if (!text) {
+ return;
+ }
+
+ const rawLines = text.split('\n');
+ const limitedLines =
+ maxLines !== undefined ? rawLines.slice(0, maxLines) : rawLines;
+
+ for (const rawLine of limitedLines) {
+ lines.push(`${indent}${rawLine}`);
+ }
+
+ if (maxLines !== undefined && rawLines.length > maxLines) {
+ lines.push(`${indent}…`);
+ }
+}
+
+function trimTrailingBlankLines(lines: string[]) {
+ while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
+ lines.pop();
+ }
+}
+
+function buildFullscreenContent(data: TaskResultDisplay): string[] {
+ const lines: string[] = [];
+
+ lines.push(`Agent: ${data.subagentName}`);
+ lines.push(`Status: ${getStatusText(data.status)}`);
+ if (data.taskDescription) {
+ lines.push(`Description: ${data.taskDescription}`);
+ }
+ lines.push('');
+
+ lines.push('Task Prompt:');
+ if (data.taskPrompt.trim().length === 0) {
+ lines.push(' ');
+ } else {
+ appendMultiline(lines, data.taskPrompt, ' ');
+ }
+ lines.push('');
+
+ if (data.pendingConfirmation) {
+ lines.push('Pending Confirmation:');
+ lines.push(` ${data.pendingConfirmation.title}`);
+ if (
+ 'command' in data.pendingConfirmation &&
+ data.pendingConfirmation.command
+ ) {
+ lines.push(` Command: ${data.pendingConfirmation.command}`);
+ }
+ if (
+ 'toolName' in data.pendingConfirmation &&
+ data.pendingConfirmation.toolName
+ ) {
+ lines.push(` Tool: ${data.pendingConfirmation.toolName}`);
+ }
+ if ('plan' in data.pendingConfirmation && data.pendingConfirmation.plan) {
+ appendMultiline(lines, data.pendingConfirmation.plan, ' ');
+ }
+ if (
+ 'prompt' in data.pendingConfirmation &&
+ data.pendingConfirmation.prompt
+ ) {
+ appendMultiline(lines, data.pendingConfirmation.prompt, ' ');
+ }
+ lines.push('');
+ }
+
+ if (data.toolCalls && data.toolCalls.length > 0) {
+ lines.push('Tool Calls:');
+ data.toolCalls.forEach((toolCall, index) => {
+ const icon = TOOL_CALL_STATUS_ICONS[toolCall.status];
+ const statusText = TOOL_CALL_STATUS_TEXT[toolCall.status];
+ lines.push(` ${index + 1}. ${icon} ${toolCall.name} — ${statusText}`);
+ if (toolCall.description) {
+ appendMultiline(lines, toolCall.description, ' ');
+ }
+ if (toolCall.error) {
+ lines.push(` Error: ${toolCall.error}`);
+ }
+ if (toolCall.resultDisplay) {
+ lines.push(' Result:');
+ appendMultiline(
+ lines,
+ toolCall.resultDisplay,
+ ' ',
+ MAX_FULLSCREEN_RESULT_LINES,
+ );
+ } else if (toolCall.result) {
+ lines.push(' Result:');
+ appendMultiline(
+ lines,
+ toolCall.result,
+ ' ',
+ MAX_FULLSCREEN_RESULT_LINES,
+ );
+ }
+ });
+ lines.push('');
+ }
+
+ if (data.status === 'failed' && data.terminateReason) {
+ lines.push(`Failure reason: ${data.terminateReason}`);
+ lines.push('');
+ }
+
+ if (data.status === 'cancelled' && data.terminateReason) {
+ lines.push(`Cancelled: ${data.terminateReason}`);
+ lines.push('');
+ }
+
+ if (data.status === 'completed' && data.executionSummary) {
+ const summary = data.executionSummary;
+ lines.push('Execution Summary:');
+ lines.push(
+ ` Duration: ${fmtDuration(summary.totalDurationMs)} · Rounds: ${summary.rounds}`,
+ );
+ lines.push(
+ ` Tool Calls: ${summary.totalToolCalls} (${summary.successfulToolCalls} success, ${summary.failedToolCalls} failed)`,
+ );
+ lines.push(` Success Rate: ${summary.successRate.toFixed(1)}%`);
+ lines.push(
+ ` Tokens: ${summary.totalTokens.toLocaleString()} (in ${summary.inputTokens.toLocaleString()}, out ${summary.outputTokens.toLocaleString()})`,
+ );
+ if (summary.estimatedCost) {
+ lines.push(` Estimated Cost: $${summary.estimatedCost.toFixed(4)}`);
+ }
+ lines.push('');
+ }
+
+ if (data.result) {
+ lines.push('Result:');
+ appendMultiline(lines, data.result, ' ', MAX_FULLSCREEN_RESULT_LINES);
+ lines.push('');
+ }
+
+ trimTrailingBlankLines(lines);
+ return lines;
+}
diff --git a/packages/cli/src/ui/components/subagents/runtime/SubagentFullscreenPanel.tsx b/packages/cli/src/ui/components/subagents/runtime/SubagentFullscreenPanel.tsx
new file mode 100644
index 000000000..aae6337fc
--- /dev/null
+++ b/packages/cli/src/ui/components/subagents/runtime/SubagentFullscreenPanel.tsx
@@ -0,0 +1,171 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { useTerminalSize } from '../../../hooks/useTerminalSize.js';
+import { useKeypress } from '../../../hooks/useKeypress.js';
+import { theme } from '../../../semantic-colors.js';
+import { useUIActions } from '../../../contexts/UIActionsContext.js';
+import type { SubagentFullscreenPanelState } from '../../../types.js';
+import { getStatusColor, getStatusText } from './status.js';
+import { CTRL_ALT_E_SEQUENCE } from './fullscreenKeys.js';
+
+const FULLSCREEN_INSTRUCTION_TEXT =
+ '↑/↓ scroll PgUp/PgDn page Home/End jump q quit ctrl+alt+e close';
+
+interface SubagentFullscreenPanelProps {
+ panel: SubagentFullscreenPanelState;
+}
+
+export const SubagentFullscreenPanel: React.FC<
+ SubagentFullscreenPanelProps
+> = ({ panel }) => {
+ const { closeSubagentFullscreenPanel } = useUIActions();
+ const { rows, columns } = useTerminalSize();
+ const viewportHeight = Math.max(1, rows - 10); // header/footer padding
+ const panelWidth = Math.max(6, columns - 2); // keep space for borders + padding at minimum
+ const content = React.useMemo(
+ () => panel.content ?? panel.getSnapshot?.() ?? [],
+ [panel],
+ );
+ const previousContentLengthRef = React.useRef(content.length);
+ const [scrollOffset, setScrollOffset] = React.useState(() =>
+ Math.max(0, content.length - viewportHeight),
+ );
+ const [lastRefreshedAt, setLastRefreshedAt] = React.useState(
+ new Date(),
+ );
+
+ const panelId = panel.panelId;
+
+ const closePanel = React.useCallback(() => {
+ closeSubagentFullscreenPanel(panelId);
+ }, [closeSubagentFullscreenPanel, panelId]);
+
+ React.useEffect(() => {
+ const maxOffset = Math.max(0, content.length - viewportHeight);
+ setScrollOffset((previous) => {
+ const previousLength = previousContentLengthRef.current;
+ const previousMaxOffset = Math.max(0, previousLength - viewportHeight);
+ const wasPinnedToBottom = previous >= previousMaxOffset;
+ previousContentLengthRef.current = content.length;
+ return wasPinnedToBottom ? maxOffset : Math.min(previous, maxOffset);
+ });
+ setLastRefreshedAt(new Date());
+ }, [content, viewportHeight]);
+
+ useKeypress(
+ (key) => {
+ const sequence = key.sequence ?? '';
+ const isCtrlAltE =
+ (key.ctrl && key.meta && key.name === 'e') ||
+ sequence === CTRL_ALT_E_SEQUENCE;
+
+ if (isCtrlAltE) {
+ closePanel();
+ return;
+ }
+
+ if (key.ctrl || key.meta) {
+ return;
+ }
+
+ const maxOffset = Math.max(0, content.length - viewportHeight);
+
+ switch (key.name) {
+ case 'q':
+ case 'escape':
+ closePanel();
+ return;
+ case 'up':
+ setScrollOffset((previous) => Math.max(0, previous - 1));
+ return;
+ case 'down':
+ setScrollOffset((previous) => Math.min(maxOffset, previous + 1));
+ return;
+ case 'pageup':
+ setScrollOffset((previous) => Math.max(0, previous - viewportHeight));
+ return;
+ case 'pagedown':
+ setScrollOffset((previous) =>
+ Math.min(maxOffset, previous + viewportHeight),
+ );
+ return;
+ case 'home':
+ setScrollOffset(0);
+ return;
+ case 'end':
+ setScrollOffset(maxOffset);
+ return;
+ default:
+ break;
+ }
+ },
+ { isActive: true },
+ );
+
+ const visibleLines = content.slice(
+ scrollOffset,
+ scrollOffset + viewportHeight,
+ );
+ const paddingLineCount = Math.max(0, viewportHeight - visibleLines.length);
+ const rangeText =
+ content.length > 0
+ ? `Lines ${Math.min(scrollOffset + 1, content.length)}-${Math.min(
+ scrollOffset + viewportHeight,
+ content.length,
+ )} / ${content.length}`
+ : 'Waiting for execution data';
+ const lastRefreshText = lastRefreshedAt
+ ? `Last updated at ${lastRefreshedAt.toLocaleTimeString()}`
+ : 'Waiting for execution data';
+
+ return (
+
+
+
+ {panel.subagentName}
+
+
+ {lastRefreshText}
+
+
+
+
+
+ {getStatusText(panel.status)}
+
+ {rangeText}
+
+
+
+ {visibleLines.map((line, index) => (
+
+ {line}
+
+ ))}
+ {Array.from({ length: paddingLineCount }).map((_, index) => (
+
+ ))}
+
+
+
+ {FULLSCREEN_INSTRUCTION_TEXT}
+
+
+ );
+};
diff --git a/packages/cli/src/ui/components/subagents/runtime/fullscreenKeys.ts b/packages/cli/src/ui/components/subagents/runtime/fullscreenKeys.ts
new file mode 100644
index 000000000..8805a1de4
--- /dev/null
+++ b/packages/cli/src/ui/components/subagents/runtime/fullscreenKeys.ts
@@ -0,0 +1,7 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const CTRL_ALT_E_SEQUENCE = '\u001b\u0005';
diff --git a/packages/cli/src/ui/components/subagents/runtime/status.ts b/packages/cli/src/ui/components/subagents/runtime/status.ts
new file mode 100644
index 000000000..c354abdf7
--- /dev/null
+++ b/packages/cli/src/ui/components/subagents/runtime/status.ts
@@ -0,0 +1,47 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type { TaskResultDisplay } from '@qwen-code/qwen-code-core';
+import { theme } from '../../../semantic-colors.js';
+
+export const getStatusColor = (
+ status:
+ | TaskResultDisplay['status']
+ | 'executing'
+ | 'success'
+ | 'awaiting_approval',
+) => {
+ switch (status) {
+ case 'running':
+ case 'executing':
+ case 'awaiting_approval':
+ return theme.status.warning;
+ case 'completed':
+ case 'success':
+ return theme.status.success;
+ case 'cancelled':
+ return theme.status.warning;
+ case 'failed':
+ return theme.status.error;
+ default:
+ return theme.text.secondary;
+ }
+};
+
+export const getStatusText = (status: TaskResultDisplay['status']) => {
+ switch (status) {
+ case 'running':
+ return 'Running';
+ case 'completed':
+ return 'Completed';
+ case 'cancelled':
+ return 'User Cancelled';
+ case 'failed':
+ return 'Failed';
+ default:
+ return 'Unknown';
+ }
+};
diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx
index e68029654..357060ffa 100644
--- a/packages/cli/src/ui/contexts/UIActionsContext.tsx
+++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx
@@ -10,7 +10,7 @@ import { type IdeIntegrationNudgeResult } from '../IdeIntegrationNudge.js';
import { type FolderTrustChoice } from '../components/FolderTrustDialog.js';
import { type AuthType, type EditorType } from '@qwen-code/qwen-code-core';
import { type SettingScope } from '../../config/settings.js';
-import type { AuthState } from '../types.js';
+import type { AuthState, SubagentFullscreenPanelState } from '../types.js';
import { type VisionSwitchOutcome } from '../components/ModelSwitchDialog.js';
export interface UIActions {
@@ -56,6 +56,14 @@ export interface UIActions {
// Subagent dialogs
closeSubagentCreateDialog: () => void;
closeAgentsManagerDialog: () => void;
+ openSubagentFullscreenPanel: (panel: SubagentFullscreenPanelState) => void;
+ closeSubagentFullscreenPanel: (panelId: string) => void;
+ updateSubagentFullscreenPanel: (
+ panelId: string,
+ updates: Partial<
+ Pick
+ >,
+ ) => void;
}
export const UIActionsContext = createContext(null);
diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx
index e2fd4cf50..00a297a73 100644
--- a/packages/cli/src/ui/contexts/UIStateContext.tsx
+++ b/packages/cli/src/ui/contexts/UIStateContext.tsx
@@ -15,7 +15,7 @@ import type {
QuitConfirmationRequest,
HistoryItemWithoutId,
StreamingState,
-} from '../types.js';
+ SubagentFullscreenPanelState } from '../types.js';
import type { DeviceAuthorizationInfo } from '../hooks/useQwenAuth.js';
import type { CommandContext, SlashCommand } from '../commands/types.js';
import type { TextBuffer } from '../components/shared/text-buffer.js';
@@ -145,6 +145,7 @@ export interface UIState {
// Subagent dialogs
isSubagentCreateDialogOpen: boolean;
isAgentsManagerDialogOpen: boolean;
+ subagentFullscreenPanel: SubagentFullscreenPanelState | null;
}
export const UIStateContext = createContext(null);
diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts
index 1d2fa7822..888f51cbc 100644
--- a/packages/cli/src/ui/types.ts
+++ b/packages/cli/src/ui/types.ts
@@ -11,6 +11,7 @@ import type {
ToolCallConfirmationDetails,
ToolConfirmationOutcome,
ToolResultDisplay,
+ TaskResultDisplay,
} from '@qwen-code/qwen-code-core';
import type { PartListUnion } from '@google/genai';
import { type ReactNode } from 'react';
@@ -162,6 +163,15 @@ export type HistoryItemToolGroup = HistoryItemBase & {
tools: IndividualToolCallDisplay[];
};
+export interface SubagentFullscreenPanelState {
+ panelId: string;
+ subagentName: string;
+ status: TaskResultDisplay['status'];
+ content: string[];
+ getSnapshot?: () => string[];
+ onClose?: () => void;
+}
+
export type HistoryItemUserShell = HistoryItemBase & {
type: 'user_shell';
text: string;