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;