mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 12:40:44 +00:00
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:
parent
eaef9efe90
commit
89f8751233
19 changed files with 1273 additions and 337 deletions
|
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue