feat(cli): add agent composer UI and refactor text input handling

- Extract shared BaseTextInput component with readline keyboard handling
- Add AgentComposer and AgentFooter components for agent interaction
- Add useAgentStreamingState hook for managing agent streaming state
- Refactor InputPrompt to use BaseTextInput with agent tab bar focus support
- Move calculatePromptWidths to shared layoutUtils
- Disable auto-accept indicator on agent tabs (agents handle their own)

This enables a dedicated input experience for agent tabs with proper
focus management and keyboard navigation between main input and agent tabs.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-03-10 16:53:10 +08:00
parent eaef9efe90
commit 89f8751233
19 changed files with 1273 additions and 337 deletions

View file

@ -22,7 +22,10 @@ import {
useMemo,
useState,
} from 'react';
import type { AgentInteractive } from '@qwen-code/qwen-code-core';
import {
type AgentInteractive,
type ApprovalMode,
} from '@qwen-code/qwen-code-core';
// ─── Types ──────────────────────────────────────────────────
@ -39,6 +42,12 @@ export interface AgentViewState {
agents: ReadonlyMap<string, RegisteredAgent>;
/** Whether any agent tab's embedded shell currently has input focus. */
agentShellFocused: boolean;
/** Current text in the active agent tab's input buffer (empty when on main). */
agentInputBufferText: string;
/** Whether the tab bar has keyboard focus (vs the agent input). */
agentTabBarFocused: boolean;
/** Per-agent approval modes (keyed by agentId). */
agentApprovalModes: ReadonlyMap<string, ApprovalMode>;
}
export interface AgentViewActions {
@ -55,6 +64,9 @@ export interface AgentViewActions {
unregisterAgent(agentId: string): void;
unregisterAll(): void;
setAgentShellFocused(focused: boolean): void;
setAgentInputBufferText(text: string): void;
setAgentTabBarFocused(focused: boolean): void;
setAgentApprovalMode(agentId: string, mode: ApprovalMode): void;
}
// ─── Context ────────────────────────────────────────────────
@ -62,28 +74,43 @@ export interface AgentViewActions {
const AgentViewStateContext = createContext<AgentViewState | null>(null);
const AgentViewActionsContext = createContext<AgentViewActions | null>(null);
// ─── Defaults (used when no provider is mounted) ────────────
const DEFAULT_STATE: AgentViewState = {
activeView: 'main',
agents: new Map(),
agentShellFocused: false,
agentInputBufferText: '',
agentTabBarFocused: false,
agentApprovalModes: new Map(),
};
const noop = () => {};
const DEFAULT_ACTIONS: AgentViewActions = {
switchToMain: noop,
switchToAgent: noop,
switchToNext: noop,
switchToPrevious: noop,
registerAgent: noop,
unregisterAgent: noop,
unregisterAll: noop,
setAgentShellFocused: noop,
setAgentInputBufferText: noop,
setAgentTabBarFocused: noop,
setAgentApprovalMode: noop,
};
// ─── Hook: useAgentViewState ────────────────────────────────
export function useAgentViewState(): AgentViewState {
const ctx = useContext(AgentViewStateContext);
if (!ctx) {
throw new Error(
'useAgentViewState must be used within an AgentViewProvider',
);
}
return ctx;
return useContext(AgentViewStateContext) ?? DEFAULT_STATE;
}
// ─── Hook: useAgentViewActions ──────────────────────────────
export function useAgentViewActions(): AgentViewActions {
const ctx = useContext(AgentViewActionsContext);
if (!ctx) {
throw new Error(
'useAgentViewActions must be used within an AgentViewProvider',
);
}
return ctx;
return useContext(AgentViewActionsContext) ?? DEFAULT_ACTIONS;
}
// ─── Provider ───────────────────────────────────────────────
@ -98,11 +125,17 @@ export function AgentViewProvider({ children }: AgentViewProviderProps) {
() => new Map(),
);
const [agentShellFocused, setAgentShellFocused] = useState(false);
const [agentInputBufferText, setAgentInputBufferText] = useState('');
const [agentTabBarFocused, setAgentTabBarFocused] = useState(false);
const [agentApprovalModes, setAgentApprovalModes] = useState<
Map<string, ApprovalMode>
>(() => new Map());
// ── Navigation ──
const switchToMain = useCallback(() => {
setActiveView('main');
setAgentTabBarFocused(false);
}, []);
const switchToAgent = useCallback(
@ -142,6 +175,13 @@ export function AgentViewProvider({ children }: AgentViewProviderProps) {
next.set(agentId, { interactiveAgent, displayName, color });
return next;
});
// Seed approval mode from the agent's own config
const mode = interactiveAgent.getCore().runtimeContext.getApprovalMode();
setAgentApprovalModes((prev) => {
const next = new Map(prev);
next.set(agentId, mode);
return next;
});
},
[],
);
@ -153,19 +193,58 @@ export function AgentViewProvider({ children }: AgentViewProviderProps) {
next.delete(agentId);
return next;
});
setAgentApprovalModes((prev) => {
if (!prev.has(agentId)) return prev;
const next = new Map(prev);
next.delete(agentId);
return next;
});
setActiveView((current) => (current === agentId ? 'main' : current));
}, []);
const unregisterAll = useCallback(() => {
setAgents(new Map());
setAgentApprovalModes(new Map());
setActiveView('main');
setAgentTabBarFocused(false);
}, []);
const setAgentApprovalMode = useCallback(
(agentId: string, mode: ApprovalMode) => {
// Update the agent's runtime config so tool scheduling picks it up
const agent = agents.get(agentId);
if (agent) {
agent.interactiveAgent.getCore().runtimeContext.setApprovalMode(mode);
}
// Update UI state
setAgentApprovalModes((prev) => {
const next = new Map(prev);
next.set(agentId, mode);
return next;
});
},
[agents],
);
// ── Memoized values ──
const state: AgentViewState = useMemo(
() => ({ activeView, agents, agentShellFocused }),
[activeView, agents, agentShellFocused],
() => ({
activeView,
agents,
agentShellFocused,
agentInputBufferText,
agentTabBarFocused,
agentApprovalModes,
}),
[
activeView,
agents,
agentShellFocused,
agentInputBufferText,
agentTabBarFocused,
agentApprovalModes,
],
);
const actions: AgentViewActions = useMemo(
@ -178,6 +257,9 @@ export function AgentViewProvider({ children }: AgentViewProviderProps) {
unregisterAgent,
unregisterAll,
setAgentShellFocused,
setAgentInputBufferText,
setAgentTabBarFocused,
setAgentApprovalMode,
}),
[
switchToMain,
@ -188,6 +270,9 @@ export function AgentViewProvider({ children }: AgentViewProviderProps) {
unregisterAgent,
unregisterAll,
setAgentShellFocused,
setAgentInputBufferText,
setAgentTabBarFocused,
setAgentApprovalMode,
],
);