Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 10 additions & 7 deletions packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -251,13 +252,15 @@ export async function startInteractiveUI(
<SessionStatsProvider sessionId={config.getSessionId()}>
<VimModeProvider settings={settings}>
<AgentViewProvider config={config}>
<AppContainer
config={config}
settings={settings}
startupWarnings={startupWarnings}
version={version}
initializationResult={initializationResult}
/>
<BackgroundTaskViewProvider config={config}>
<AppContainer
config={config}
settings={settings}
startupWarnings={startupWarnings}
version={version}
initializationResult={initializationResult}
/>
</BackgroundTaskViewProvider>
</AgentViewProvider>
</VimModeProvider>
</SessionStatsProvider>
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/nonInteractiveCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}),
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/nonInteractiveCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import type {
BackgroundAgentStatus,
BackgroundTaskStatus,
Config,
ToolCallRequestInfo,
} from '@qwen-code/qwen-code-core';
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string | null>(null);
Expand Down Expand Up @@ -1593,7 +1599,8 @@ export const AppContainer = (props: AppContainerProps) => {
isResumeDialogOpen ||
isDeleteDialogOpen ||
isExtensionsManagerDialogOpen ||
isRewindSelectorOpen;
isRewindSelectorOpen ||
bgTasksDialogOpen;
dialogsVisibleRef.current = dialogsVisible;
const shouldShowStickyTodos =
stickyTodos !== null &&
Expand Down Expand Up @@ -1918,6 +1925,8 @@ export const AppContainer = (props: AppContainerProps) => {
isFolderTrustDialogOpen,
showWelcomeBackDialog,
handleWelcomeBackClose,
isBackgroundTasksDialogOpen: bgTasksDialogOpen,
closeBackgroundTasksDialog: closeBgTasksDialog,
});

const handleExit = useCallback(
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/src/ui/components/DialogManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -64,6 +66,7 @@ export const DialogManager = ({

const uiState = useUIState();
const uiActions = useUIActions();
const { dialogOpen: bgTasksDialogOpen } = useBackgroundTaskViewState();
const { constrainHeight, terminalHeight, staticExtraHeight, mainAreaWidth } =
uiState;

Expand Down Expand Up @@ -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 (
<BackgroundTasksDialog
availableTerminalHeight={terminalHeight - staticExtraHeight}
terminalWidth={mainAreaWidth}
/>
);
}

return null;
};
13 changes: 8 additions & 5 deletions packages/cli/src/ui/components/Footer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -97,11 +98,13 @@ const renderWithWidth = (width: number, uiState: UIState) => {
return render(
<SettingsContext.Provider value={mockSettings}>
<ConfigContext.Provider value={createMockConfig() as never}>
<VimModeProvider settings={mockSettings}>
<UIStateContext.Provider value={uiState}>
<Footer />
</UIStateContext.Provider>
</VimModeProvider>
<KeypressProvider kittyProtocolEnabled={false}>
<VimModeProvider settings={mockSettings}>
<UIStateContext.Provider value={uiState}>
<Footer />
</UIStateContext.Provider>
</VimModeProvider>
</KeypressProvider>
</ConfigContext.Provider>
</SettingsContext.Provider>,
);
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/ui/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ContextUsageDisplay } from './ContextUsageDisplay.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
import { ShellModeIndicator } from './ShellModeIndicator.js';
import { BackgroundTasksPill } from './background-view/BackgroundTasksPill.js';
import { isNarrowWidth } from '../utils/isNarrowWidth.js';

import { useStatusLine } from '../hooks/useStatusLine.js';
Expand Down Expand Up @@ -173,7 +174,10 @@ export const Footer: React.FC = () => {
{line}
</Text>
))}
<Text wrap="truncate">{leftBottomContent}</Text>
<Box flexDirection="row" flexShrink={1}>
<Text wrap="truncate">{leftBottomContent}</Text>
<BackgroundTasksPill />
</Box>
</Box>

{/* Right Section — never compressed, aligns to top so multi-line
Expand Down
49 changes: 43 additions & 6 deletions packages/cli/src/ui/components/InputPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ import {
useAgentViewState,
useAgentViewActions,
} from '../contexts/AgentViewContext.js';
import {
useBackgroundTaskViewState,
useBackgroundTaskViewActions,
} from '../contexts/BackgroundTaskViewContext.js';
import { FEEDBACK_DIALOG_KEYS } from '../FeedbackDialog.js';
import { BaseTextInput } from './BaseTextInput.js';
import type { RenderLineOptions } from './BaseTextInput.js';
Expand Down Expand Up @@ -124,7 +128,16 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const { pasteWorkaround } = useKeypressContext();
const { agents, agentTabBarFocused } = useAgentViewState();
const { setAgentTabBarFocused } = useAgentViewActions();
const {
entries: bgEntries,
dialogOpen: bgDialogOpen,
pillFocused: bgPillFocused,
} = useBackgroundTaskViewState();
const { setPillFocused: setBgPillFocused } = useBackgroundTaskViewActions();
const hasAgents = agents.size > 0;
// Includes terminal entries — the pill stays open so users can reopen
// the dialog to inspect final state after the last agent finishes.
const hasBgAgents = bgEntries.length > 0;
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
const [escPressCount, setEscPressCount] = useState(0);
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
Expand Down Expand Up @@ -445,12 +458,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({

const handleInput = useCallback(
(key: Key): boolean => {
// When the tab bar has focus, block all non-printable keys so arrow
// keys and shortcuts don't interfere. Printable characters fall
// through to BaseTextInput's default handler so the first keystroke
// appears in the input immediately (the tab bar handler releases
// focus on the same event).
if (agentTabBarFocused) {
// When the Arena tab bar or background pill has focus, block
// non-printable keys so arrow keys and shortcuts don't interfere.
// Printable characters fall through to BaseTextInput's default
// handler so the first keystroke appears in the input immediately
// (each surface's own handler releases focus on the same event).
if (agentTabBarFocused || bgPillFocused) {
if (
key.sequence &&
key.sequence.length === 1 &&
Expand All @@ -462,6 +475,16 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
return true; // consume non-printable keys
}

// When the Background tasks dialog is open, swallow every key so
// nothing reaches the composer buffer — the dialog's own keypress
// handler owns selection, open/close, and stop actions. Unlike
// the tab bar we do NOT let printable chars type through, because
// the dialog doesn't auto-close on printable input and users
// would leak text into the hidden composer.
if (bgDialogOpen) {
return true;
}

// TODO(jacobr): this special case is likely not needed anymore.
// We should probably stop supporting paste if the InputPrompt is not
// focused.
Expand Down Expand Up @@ -929,10 +952,20 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
if (inputHistory.navigateDown()) {
return true;
}
// Focus order on Down from an empty composer:
// team tab bar (if any Arena agents) → Background tasks pill
// (if any bg agents) → otherwise stay put. The pill itself
// opens the dialog on Enter; the tab bar re-routes Down into
// the pill once it has focus, so both surfaces remain reachable
// in sequence.
if (hasAgents) {
setAgentTabBarFocused(true);
return true;
}
if (hasBgAgents) {
setBgPillFocused(true);
return true;
}
return true;
}
} else {
Expand Down Expand Up @@ -1096,8 +1129,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
parsePlaceholder,
freePlaceholderId,
agentTabBarFocused,
bgDialogOpen,
bgPillFocused,
hasAgents,
hasBgAgents,
setAgentTabBarFocused,
setBgPillFocused,
followup,
onPromptSuggestionDismiss,
],
Expand Down
Loading
Loading