feat(cli): background-agent UI — pill, combined dialog, detail view

Surfaces background agents in the TUI with a combined pill/dialog pattern
and a responsive detail view that splits or full-swaps based on terminal
height. Transcript state (messages, pending approvals, live outputs,
shell PIDs) moves from AgentInteractive into AgentCore so both Arena and
background agents render through a shared AgentChatContent component
without duplicating renderer logic.
This commit is contained in:
tanzhenxin 2026-04-21 06:31:08 +00:00
parent 5ac51f353d
commit fcf62011c3
25 changed files with 1748 additions and 366 deletions

View file

@ -47,6 +47,10 @@ import {
useAgentViewState,
useAgentViewActions,
} from '../contexts/AgentViewContext.js';
import {
useBackgroundAgentViewState,
useBackgroundAgentViewActions,
} from '../contexts/BackgroundAgentViewContext.js';
import { FEEDBACK_DIALOG_KEYS } from '../FeedbackDialog.js';
import { BaseTextInput } from './BaseTextInput.js';
import type { RenderLineOptions } from './BaseTextInput.js';
@ -124,7 +128,11 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
const { pasteWorkaround } = useKeypressContext();
const { agents, agentTabBarFocused } = useAgentViewState();
const { setAgentTabBarFocused } = useAgentViewActions();
const { entries: bgEntries, dialogOpen: bgDialogOpen } =
useBackgroundAgentViewState();
const { openDialog: openBgDialog } = useBackgroundAgentViewActions();
const hasAgents = agents.size > 0;
const hasBgAgents = bgEntries.length > 0;
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
const [escPressCount, setEscPressCount] = useState(0);
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
@ -425,11 +433,11 @@ 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).
// 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) {
if (
key.sequence &&
@ -442,6 +450,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.
@ -897,10 +915,19 @@ 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 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.
if (hasAgents) {
setAgentTabBarFocused(true);
return true;
}
if (hasBgAgents) {
openBgDialog();
return true;
}
return true;
}
} else {
@ -1064,8 +1091,11 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
parsePlaceholder,
freePlaceholderId,
agentTabBarFocused,
bgDialogOpen,
hasAgents,
hasBgAgents,
setAgentTabBarFocused,
openBgDialog,
followup,
onPromptSuggestionDismiss,
],