diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx
index 6102c5b81..a02985926 100644
--- a/packages/cli/src/ui/components/DialogManager.tsx
+++ b/packages/cli/src/ui/components/DialogManager.tsx
@@ -392,7 +392,12 @@ export const DialogManager = ({
// AppContainer) so its visibility mutes the composer and the global
// Ctrl+C / Esc handlers route through `closeAnyOpenDialog`.
if (bgTasksDialogOpen) {
- return ;
+ return (
+
+ );
}
return null;
diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx
index 585f47ece..fe8ddadab 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');
@@ -96,11 +97,13 @@ const renderWithWidth = (width: number, uiState: UIState) => {
return render(
-
-
-
-
-
+
+
+
+
+
+
+
,
);
diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index 626cb03f6..08b1fa6f3 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -128,9 +128,12 @@ export const InputPrompt: React.FC = ({
const { pasteWorkaround } = useKeypressContext();
const { agents, agentTabBarFocused } = useAgentViewState();
const { setAgentTabBarFocused } = useAgentViewActions();
- const { entries: bgEntries, dialogOpen: bgDialogOpen } =
- useBackgroundAgentViewState();
- const { openDialog: openBgDialog } = useBackgroundAgentViewActions();
+ const {
+ entries: bgEntries,
+ dialogOpen: bgDialogOpen,
+ pillFocused: bgPillFocused,
+ } = useBackgroundAgentViewState();
+ const { setPillFocused: setBgPillFocused } = useBackgroundAgentViewActions();
const hasAgents = agents.size > 0;
const hasBgAgents = bgEntries.length > 0;
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
@@ -433,12 +436,12 @@ export const InputPrompt: React.FC = ({
const handleInput = useCallback(
(key: Key): boolean => {
- // When the Arena tab bar 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 (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 &&
@@ -916,16 +919,17 @@ export const InputPrompt: React.FC = ({
return true;
}
// Focus order on Down from an empty composer:
- // team tab bar (if any Arena agents) → Background tasks dialog
- // (if any bg agents) → otherwise stay put. The tab bar itself
- // re-routes Down into the bg dialog once it has focus, so both
- // surfaces remain reachable in sequence.
+ // 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) {
- openBgDialog();
+ setBgPillFocused(true);
return true;
}
return true;
@@ -1092,10 +1096,11 @@ export const InputPrompt: React.FC = ({
freePlaceholderId,
agentTabBarFocused,
bgDialogOpen,
+ bgPillFocused,
hasAgents,
hasBgAgents,
setAgentTabBarFocused,
- openBgDialog,
+ setBgPillFocused,
followup,
onPromptSuggestionDismiss,
],
diff --git a/packages/cli/src/ui/components/agent-view/AgentTabBar.tsx b/packages/cli/src/ui/components/agent-view/AgentTabBar.tsx
index efc0215ae..43a02e3d7 100644
--- a/packages/cli/src/ui/components/agent-view/AgentTabBar.tsx
+++ b/packages/cli/src/ui/components/agent-view/AgentTabBar.tsx
@@ -70,7 +70,7 @@ export const AgentTabBar: React.FC = () => {
setAgentTabBarFocused,
} = useAgentViewActions();
const { entries: bgEntries } = useBackgroundAgentViewState();
- const { openDialog: openBgDialog } = useBackgroundAgentViewActions();
+ const { setPillFocused: setBgPillFocused } = useBackgroundAgentViewActions();
const { embeddedShellFocused } = useUIState();
const hasBgAgents = bgEntries.length > 0;
@@ -86,15 +86,14 @@ export const AgentTabBar: React.FC = () => {
} else if (key.name === 'up') {
setAgentTabBarFocused(false);
} else if (key.name === 'down') {
- // Down cascades to the Background tasks dialog if any background
- // agents exist. Switch to main first — DialogManager only mounts
- // in the main-view branch of DefaultAppLayout, so opening the
- // dialog while an agent tab is active would leave the user in a
- // hidden-modal state.
+ // Down cascades to the Background tasks pill if any background
+ // agents exist. Switch to main first — the footer pill only
+ // renders under the main view, so focusing it from an agent tab
+ // would strand focus on an offscreen surface.
if (hasBgAgents) {
setAgentTabBarFocused(false);
switchToMain();
- openBgDialog();
+ setBgPillFocused(true);
}
} else if (
key.sequence &&
diff --git a/packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx b/packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx
index 5dfe7e5f0..ae53b95e5 100644
--- a/packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx
+++ b/packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx
@@ -11,13 +11,14 @@
*/
import type React from 'react';
-import { useEffect, useMemo, useState } from 'react';
+import { Fragment, useEffect, useMemo, useState } from 'react';
import { Box, Text } from 'ink';
import {
useBackgroundAgentViewState,
useBackgroundAgentViewActions,
} from '../../contexts/BackgroundAgentViewContext.js';
import { useKeypress } from '../../hooks/useKeypress.js';
+import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { theme } from '../../semantic-colors.js';
import { useConfig } from '../../contexts/ConfigContext.js';
import {
@@ -109,7 +110,11 @@ const ListBody: React.FC<{
// ─── Detail mode ───────────────────────────────────────────
-const DetailBody: React.FC<{ entry: BackgroundAgentEntry }> = ({ entry }) => {
+const DetailBody: React.FC<{
+ entry: BackgroundAgentEntry;
+ maxHeight: number;
+ maxWidth: number;
+}> = ({ entry, maxHeight, maxWidth }) => {
const title = `${entry.subagentType ?? 'Agent'} \u203A ${rowLabel(entry)}`;
const subtitleParts: string[] = [];
@@ -132,59 +137,88 @@ const DetailBody: React.FC<{ entry: BackgroundAgentEntry }> = ({ entry }) => {
);
}
- const activities = entry.recentActivities ?? [];
+ const activities = (entry.recentActivities ?? []).slice().reverse();
+ const hasError = entry.status === 'failed' && Boolean(entry.error);
return (
-
-
- {title}
-
- {subtitleParts.join(' \u00B7 ')}
+
+
+
+ {title}
+
+
+
+
+ {subtitleParts.join(' \u00B7 ')}
+
+
{activities.length > 0 && (
-
- Progress
- {activities
- .slice()
- .reverse()
- .map((a, i) => (
-
-
- {i === 0 ? '\u203A ' : ' '}
-
- {a.name}
- {a.description ? (
- {a.description}
- ) : null}
+
+
+
+ Progress
+
+ {activities.map((a, i) => (
+
+
+ {i === 0 ? '\u203A ' : ' '}
- ))}
-
+ {a.name}
+ {a.description ? (
+ {a.description}
+ ) : null}
+
+ ))}
+
)}
{entry.prompt && (
-
- Prompt
- {entry.prompt}
-
+
+
+
+ Prompt
+
+
+ {entry.prompt}
+
+
)}
- {entry.status === 'failed' && entry.error && (
-
-
- Error
-
-
- {entry.error}
-
-
+ {hasError && (
+
+
+
+
+ Error
+
+
+
+
+ {entry.error}
+
+
+
)}
-
+
);
};
// ─── Dialog shell ──────────────────────────────────────────
-export const BackgroundTasksDialog: React.FC = () => {
+interface BackgroundTasksDialogProps {
+ availableTerminalHeight: number;
+ terminalWidth: number;
+}
+
+export const BackgroundTasksDialog: React.FC = ({
+ availableTerminalHeight,
+ terminalWidth,
+}) => {
const { entries, selectedIndex, dialogOpen, dialogMode } =
useBackgroundAgentViewState();
const {
@@ -197,6 +231,17 @@ export const BackgroundTasksDialog: React.FC = () => {
} = useBackgroundAgentViewActions();
const config = useConfig();
+ // Detail view is capped at ~50% of the available area. Chrome (border,
+ // title line, two marginTops, hint line) eats 6 rows; MaxSizedBox
+ // handles row-level truncation for the rest.
+ const detailMaxHeight = Math.max(
+ 6,
+ Math.floor(availableTerminalHeight * 0.5),
+ );
+ const detailContentHeight = Math.max(2, detailMaxHeight - 6);
+ // Rounded border + paddingX=1 on the outer Box ≈ 4 horizontal cells.
+ const detailContentWidth = Math.max(10, terminalWidth - 4);
+
const selectedEntry = useMemo(
() => entries[selectedIndex] ?? null,
[entries, selectedIndex],
@@ -300,7 +345,11 @@ export const BackgroundTasksDialog: React.FC = () => {
{dialogMode === 'list' ? (
) : selectedEntry ? (
-
+
) : (
No entry to show.
diff --git a/packages/cli/src/ui/components/background-view/BackgroundTasksPill.tsx b/packages/cli/src/ui/components/background-view/BackgroundTasksPill.tsx
index 889cb2f7b..3866bed48 100644
--- a/packages/cli/src/ui/components/background-view/BackgroundTasksPill.tsx
+++ b/packages/cli/src/ui/components/background-view/BackgroundTasksPill.tsx
@@ -5,8 +5,13 @@
*/
import type React from 'react';
+import { useCallback } from 'react';
import { Box, Text } from 'ink';
-import { useBackgroundAgentViewState } from '../../contexts/BackgroundAgentViewContext.js';
+import {
+ useBackgroundAgentViewState,
+ useBackgroundAgentViewActions,
+} from '../../contexts/BackgroundAgentViewContext.js';
+import { useKeypress, type Key } from '../../hooks/useKeypress.js';
import { theme } from '../../semantic-colors.js';
import type { BackgroundAgentEntry } from '@qwen-code/qwen-code-core';
@@ -18,8 +23,31 @@ export function getPillLabel(running: readonly BackgroundAgentEntry[]): string {
}
export const BackgroundTasksPill: React.FC = () => {
- const { entries } = useBackgroundAgentViewState();
+ const { entries, pillFocused } = useBackgroundAgentViewState();
+ const { openDialog, setPillFocused } = useBackgroundAgentViewActions();
const running = entries.filter((e) => e.status === 'running');
+
+ const onKeypress = useCallback(
+ (key: Key) => {
+ if (!pillFocused) return;
+ if (key.name === 'return') {
+ openDialog();
+ } else if (key.name === 'up' || key.name === 'escape') {
+ setPillFocused(false);
+ } else if (
+ key.sequence &&
+ key.sequence.length === 1 &&
+ !key.ctrl &&
+ !key.meta
+ ) {
+ setPillFocused(false);
+ }
+ },
+ [pillFocused, openDialog, setPillFocused],
+ );
+
+ useKeypress(onKeypress, { isActive: true });
+
if (running.length === 0) return null;
const label = getPillLabel(running);
@@ -27,10 +55,16 @@ export const BackgroundTasksPill: React.FC = () => {
return (
·
-
- {label}
+
+ {pillFocused ? ` ${label} ` : label}
- · ↓ to view
+ {!pillFocused && (
+ {' · ↓ to view'}
+ )}
);
};
diff --git a/packages/cli/src/ui/contexts/BackgroundAgentViewContext.tsx b/packages/cli/src/ui/contexts/BackgroundAgentViewContext.tsx
index 298f4a569..1d5173aed 100644
--- a/packages/cli/src/ui/contexts/BackgroundAgentViewContext.tsx
+++ b/packages/cli/src/ui/contexts/BackgroundAgentViewContext.tsx
@@ -15,6 +15,7 @@ import {
createContext,
useContext,
useCallback,
+ useEffect,
useMemo,
useState,
} from 'react';
@@ -37,6 +38,11 @@ export interface BackgroundAgentViewState {
dialogMode: BackgroundDialogMode;
/** Convenience boolean: `dialogMode !== 'closed'`. */
dialogOpen: boolean;
+ /**
+ * True when the footer pill owns keyboard focus (highlighted, awaiting
+ * Enter to open the dialog). Mirrors the Arena tab-bar focus pattern.
+ */
+ pillFocused: boolean;
}
export interface BackgroundAgentViewActions {
@@ -49,6 +55,7 @@ export interface BackgroundAgentViewActions {
exitDetail(): void;
/** Cancel the currently selected entry (no-op if not running). */
cancelSelected(): void;
+ setPillFocused(focused: boolean): void;
}
// ─── Context ────────────────────────────────────────────────
@@ -65,6 +72,7 @@ const DEFAULT_STATE: BackgroundAgentViewState = {
selectedIndex: 0,
dialogMode: 'closed',
dialogOpen: false,
+ pillFocused: false,
};
const noop = () => {};
@@ -79,6 +87,7 @@ const DEFAULT_ACTIONS: BackgroundAgentViewActions = {
enterDetail: noop,
exitDetail: noop,
cancelSelected: noop,
+ setPillFocused: noop,
};
// ─── Hooks ──────────────────────────────────────────────────
@@ -106,7 +115,16 @@ export function BackgroundAgentViewProvider({
const [rawSelectedIndex, setRawSelectedIndex] = useState(0);
const [dialogMode, setDialogMode] = useState('closed');
+ const [pillFocused, setPillFocused] = useState(false);
const dialogOpen = dialogMode !== 'closed';
+ const hasRunning = entries.some((e) => e.status === 'running');
+
+ // Drop stale pill focus as soon as the pill loses its reason to exist —
+ // without this, InputPrompt's input-blocking branch would stay on after
+ // the last running agent finishes.
+ useEffect(() => {
+ if (pillFocused && !hasRunning) setPillFocused(false);
+ }, [pillFocused, hasRunning]);
// Single clamp on read — `rawSelectedIndex` can fall out of range when
// entries shrink between renders.
@@ -138,6 +156,8 @@ export function BackgroundAgentViewProvider({
const openDialog = useCallback(() => {
setDialogMode('list');
+ // Opening the dialog supersedes pill focus — the dialog now owns input.
+ setPillFocused(false);
}, []);
const closeDialog = useCallback(() => {
@@ -171,8 +191,9 @@ export function BackgroundAgentViewProvider({
selectedIndex,
dialogMode,
dialogOpen,
+ pillFocused,
}),
- [entries, selectedIndex, dialogMode, dialogOpen],
+ [entries, selectedIndex, dialogMode, dialogOpen, pillFocused],
);
const actions: BackgroundAgentViewActions = useMemo(
@@ -185,6 +206,7 @@ export function BackgroundAgentViewProvider({
enterDetail,
exitDetail,
cancelSelected,
+ setPillFocused,
}),
[
setSelectedIndex,
@@ -195,6 +217,7 @@ export function BackgroundAgentViewProvider({
enterDetail,
exitDetail,
cancelSelected,
+ setPillFocused,
],
);