From 03c88b73080e8856efeefd94d1a631ec24092447 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Tue, 28 Apr 2026 10:57:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(cli):=20background-agent=20UI=20=E2=80=94?= =?UTF-8?q?=20pill,=20combined=20dialog,=20detail=20view=20(#3488)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(cli): background-task UI — pill, combined dialog, detail view Adds the user-facing surface for background tasks on top of the model-facing agent control primitives merged in #3471. A dedicated pill in the footer summarises running tasks, ↓ focuses it, and Enter opens a combined dialog listing every task with a detail view that shows the original prompt, live stats, and a rolling progress feed of recent tool invocations. Also renames BackgroundAgent* to BackgroundTask* for consistency with the user-facing terminology and the task_* tool family. * chore: trigger CI --- packages/cli/src/gemini.tsx | 17 +- packages/cli/src/nonInteractiveCli.test.ts | 2 +- packages/cli/src/nonInteractiveCli.ts | 4 +- packages/cli/src/ui/AppContainer.tsx | 11 +- .../cli/src/ui/components/DialogManager.tsx | 17 + .../cli/src/ui/components/Footer.test.tsx | 13 +- packages/cli/src/ui/components/Footer.tsx | 6 +- .../cli/src/ui/components/InputPrompt.tsx | 49 +- .../agent-view/AgentChatContent.tsx | 274 +++++++++ .../components/agent-view/AgentChatView.tsx | 260 +------- .../ui/components/agent-view/AgentTabBar.tsx | 24 +- .../BackgroundTasksDialog.test.tsx | 199 ++++++ .../background-view/BackgroundTasksDialog.tsx | 572 ++++++++++++++++++ .../BackgroundTasksPill.test.tsx | 61 ++ .../background-view/BackgroundTasksPill.tsx | 67 ++ .../runtime/AgentExecutionDisplay.tsx | 6 + .../ui/contexts/BackgroundTaskViewContext.tsx | 214 +++++++ .../cli/src/ui/hooks/useBackgroundTaskView.ts | 55 ++ packages/cli/src/ui/hooks/useDialogClose.ts | 12 + .../agents/backends/InProcessBackend.test.ts | 78 ++- .../core/src/agents/background-tasks.test.ts | 186 +++++- packages/core/src/agents/background-tasks.ts | 182 +++++- .../core/src/agents/runtime/agent-core.ts | 161 ++++- .../core/src/agents/runtime/agent-headless.ts | 13 +- .../agents/runtime/agent-interactive.test.ts | 102 ++++ .../src/agents/runtime/agent-interactive.ts | 115 +--- packages/core/src/tools/agent/agent.test.ts | 7 + packages/core/src/tools/agent/agent.ts | 43 +- 28 files changed, 2322 insertions(+), 428 deletions(-) create mode 100644 packages/cli/src/ui/components/agent-view/AgentChatContent.tsx create mode 100644 packages/cli/src/ui/components/background-view/BackgroundTasksDialog.test.tsx create mode 100644 packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx create mode 100644 packages/cli/src/ui/components/background-view/BackgroundTasksPill.test.tsx create mode 100644 packages/cli/src/ui/components/background-view/BackgroundTasksPill.tsx create mode 100644 packages/cli/src/ui/contexts/BackgroundTaskViewContext.tsx create mode 100644 packages/cli/src/ui/hooks/useBackgroundTaskView.ts diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index e7da7582a..1add91580 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -44,6 +44,7 @@ import { SessionStatsProvider } from './ui/contexts/SessionContext.js'; import { SettingsContext } from './ui/contexts/SettingsContext.js'; import { VimModeProvider } from './ui/contexts/VimModeContext.js'; import { AgentViewProvider } from './ui/contexts/AgentViewContext.js'; +import { BackgroundTaskViewProvider } from './ui/contexts/BackgroundTaskViewContext.js'; import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js'; import { themeManager, AUTO_THEME_NAME } from './ui/themes/theme-manager.js'; import { @@ -251,13 +252,15 @@ export async function startInteractiveUI( - + + + diff --git a/packages/cli/src/nonInteractiveCli.test.ts b/packages/cli/src/nonInteractiveCli.test.ts index a6c9761e0..b2757d845 100644 --- a/packages/cli/src/nonInteractiveCli.test.ts +++ b/packages/cli/src/nonInteractiveCli.test.ts @@ -166,7 +166,7 @@ describe('runNonInteractive', () => { getBackgroundTaskRegistry: vi.fn().mockReturnValue({ setNotificationCallback: vi.fn(), setRegisterCallback: vi.fn(), - getRunning: vi.fn().mockReturnValue([]), + getAll: vi.fn().mockReturnValue([]), hasUnfinalizedTasks: vi.fn().mockReturnValue(false), abortAll: vi.fn(), }), diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index f13c79f0c..5232f6096 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -5,7 +5,7 @@ */ import type { - BackgroundAgentStatus, + BackgroundTaskStatus, Config, ToolCallRequestInfo, } from '@qwen-code/qwen-code-core'; @@ -302,7 +302,7 @@ export async function runNonInteractive( sdkNotification?: { task_id: string; tool_use_id?: string; - status: BackgroundAgentStatus; + status: BackgroundTaskStatus; usage?: { total_tokens: number; tool_uses: number; diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 8d66b3a14..e33beca8a 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -127,6 +127,10 @@ import { import { useCodingPlanUpdates } from './hooks/useCodingPlanUpdates.js'; import { ShellFocusContext } from './contexts/ShellFocusContext.js'; import { useAgentViewState } from './contexts/AgentViewContext.js'; +import { + useBackgroundTaskViewState, + useBackgroundTaskViewActions, +} from './contexts/BackgroundTaskViewContext.js'; import { t } from '../i18n/index.js'; import { useWelcomeBack } from './hooks/useWelcomeBack.js'; import { useDialogClose } from './hooks/useDialogClose.js'; @@ -900,6 +904,8 @@ export const AppContainer = (props: AppContainerProps) => { const [hasSuggestionsVisible, setHasSuggestionsVisible] = useState(false); const agentViewState = useAgentViewState(); + const { dialogOpen: bgTasksDialogOpen } = useBackgroundTaskViewState(); + const { closeDialog: closeBgTasksDialog } = useBackgroundTaskViewActions(); // Prompt suggestion state const [promptSuggestion, setPromptSuggestion] = useState(null); @@ -1593,7 +1599,8 @@ export const AppContainer = (props: AppContainerProps) => { isResumeDialogOpen || isDeleteDialogOpen || isExtensionsManagerDialogOpen || - isRewindSelectorOpen; + isRewindSelectorOpen || + bgTasksDialogOpen; dialogsVisibleRef.current = dialogsVisible; const shouldShowStickyTodos = stickyTodos !== null && @@ -1918,6 +1925,8 @@ export const AppContainer = (props: AppContainerProps) => { isFolderTrustDialogOpen, showWelcomeBackDialog, handleWelcomeBackClose, + isBackgroundTasksDialogOpen: bgTasksDialogOpen, + closeBackgroundTasksDialog: closeBgTasksDialog, }); const handleExit = useCallback( diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx index f884d089a..e15ad77b4 100644 --- a/packages/cli/src/ui/components/DialogManager.tsx +++ b/packages/cli/src/ui/components/DialogManager.tsx @@ -47,6 +47,8 @@ import { HooksManagementDialog } from './hooks/HooksManagementDialog.js'; import { SessionPicker } from './SessionPicker.js'; import { RewindSelector } from './RewindSelector.js'; import { MemoryDialog } from './MemoryDialog.js'; +import { BackgroundTasksDialog } from './background-view/BackgroundTasksDialog.js'; +import { useBackgroundTaskViewState } from '../contexts/BackgroundTaskViewContext.js'; import { t } from '../../i18n/index.js'; interface DialogManagerProps { @@ -64,6 +66,7 @@ export const DialogManager = ({ const uiState = useUIState(); const uiActions = useUIActions(); + const { dialogOpen: bgTasksDialogOpen } = useBackgroundTaskViewState(); const { constrainHeight, terminalHeight, staticExtraHeight, mainAreaWidth } = uiState; @@ -436,5 +439,19 @@ export const DialogManager = ({ ); } + // Background tasks dialog — lowest priority so other dialogs + // (permissions, trust prompts, auth, etc.) always take precedence. The + // dialog is part of the shared dialogsVisible machinery (see + // AppContainer) so its visibility mutes the composer and the global + // Ctrl+C / Esc handlers route through `closeAnyOpenDialog`. + if (bgTasksDialogOpen) { + return ( + + ); + } + return null; }; diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx index c405dbe3e..064206a92 100644 --- a/packages/cli/src/ui/components/Footer.test.tsx +++ b/packages/cli/src/ui/components/Footer.test.tsx @@ -13,6 +13,7 @@ import { type UIState, UIStateContext } from '../contexts/UIStateContext.js'; import { ConfigContext } from '../contexts/ConfigContext.js'; import { VimModeProvider } from '../contexts/VimModeContext.js'; import { SettingsContext } from '../contexts/SettingsContext.js'; +import { KeypressProvider } from '../contexts/KeypressContext.js'; import type { LoadedSettings } from '../../config/settings.js'; vi.mock('../hooks/useTerminalSize.js'); @@ -97,11 +98,13 @@ const renderWithWidth = (width: number, uiState: UIState) => { return render( - - -