Fix agent input focus scoping
This commit is contained in:
@@ -4,7 +4,6 @@ import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyl
|
||||
import { ArrowUp, Square, Pencil, AudioLines } from "lucide-react-native";
|
||||
import Animated from "react-native-reanimated";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useIsFocused } from "@react-navigation/native";
|
||||
import { FOOTER_HEIGHT, MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
import { generateMessageId, type StreamItem } from "@/types/stream";
|
||||
import {
|
||||
@@ -59,6 +58,7 @@ type ImageListUpdater = ImageAttachment[] | ((prev: ImageAttachment[]) => ImageA
|
||||
interface AgentInputAreaProps {
|
||||
agentId: string;
|
||||
serverId: string;
|
||||
isInputActive: boolean;
|
||||
onSubmitMessage?: (payload: MessagePayload) => Promise<void>;
|
||||
/** Externally controlled loading state. When true, disables the submit button. */
|
||||
isSubmitLoading?: boolean;
|
||||
@@ -91,6 +91,7 @@ const MOBILE_MESSAGE_PLACEHOLDER = "Message, @files, /commands";
|
||||
export function AgentInputArea({
|
||||
agentId,
|
||||
serverId,
|
||||
isInputActive,
|
||||
onSubmitMessage,
|
||||
isSubmitLoading = false,
|
||||
blurOnSubmit = false,
|
||||
@@ -112,8 +113,6 @@ export function AgentInputArea({
|
||||
const { theme } = useUnistyles();
|
||||
const buttonIconSize = Platform.OS === "web" ? theme.iconSize.md : theme.iconSize.lg;
|
||||
const insets = useSafeAreaInsets();
|
||||
const isScreenFocused = useIsFocused();
|
||||
|
||||
const client = useHostRuntimeClient(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
const agentDirectoryStatus = useHostRuntimeAgentDirectoryStatus(serverId);
|
||||
@@ -423,7 +422,7 @@ export function AgentInputArea({
|
||||
|
||||
const handleKeyboardAction = useCallback(
|
||||
(action: KeyboardActionDefinition): boolean => {
|
||||
if (!isScreenFocused) {
|
||||
if (!isInputActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -459,7 +458,7 @@ export function AgentInputArea({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[isScreenFocused],
|
||||
[isInputActive],
|
||||
);
|
||||
|
||||
useKeyboardActionHandler({
|
||||
@@ -471,9 +470,9 @@ export function AgentInputArea({
|
||||
"message-input.voice-toggle",
|
||||
"message-input.voice-mute-toggle",
|
||||
],
|
||||
enabled: isScreenFocused,
|
||||
enabled: isInputActive,
|
||||
priority: isMessageInputFocused ? 200 : 100,
|
||||
isActive: () => isScreenFocused,
|
||||
isActive: () => isInputActive,
|
||||
handle: handleKeyboardAction,
|
||||
});
|
||||
|
||||
@@ -715,7 +714,7 @@ export function AgentInputArea({
|
||||
autoFocus={autoFocus && isDesktopWebBreakpoint}
|
||||
autoFocusKey={`${serverId}:${agentId}`}
|
||||
disabled={isSubmitLoading}
|
||||
isScreenFocused={isScreenFocused}
|
||||
isInputActive={isInputActive}
|
||||
leftContent={leftContent}
|
||||
rightContent={rightContent}
|
||||
voiceServerId={serverId}
|
||||
|
||||
@@ -66,8 +66,8 @@ export interface MessageInputProps {
|
||||
autoFocus?: boolean;
|
||||
autoFocusKey?: string;
|
||||
disabled?: boolean;
|
||||
/** True when the containing screen is focused (React Navigation). Used to disable global hotkeys and cancel dictation when unfocused. */
|
||||
isScreenFocused?: boolean;
|
||||
/** True when this input is the active composer. Used to gate global hotkeys and stop dictation when hidden. */
|
||||
isInputActive?: boolean;
|
||||
/** Content to render on the left side of the button row (e.g., AgentStatusBar) */
|
||||
leftContent?: React.ReactNode;
|
||||
/** Content to render on the right side after voice button (e.g., realtime button, cancel button) */
|
||||
@@ -190,7 +190,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
||||
autoFocus = false,
|
||||
autoFocusKey,
|
||||
disabled = false,
|
||||
isScreenFocused = true,
|
||||
isInputActive = true,
|
||||
leftContent,
|
||||
rightContent,
|
||||
voiceServerId,
|
||||
@@ -387,7 +387,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
||||
onError: handleDictationError,
|
||||
canStart: canStartDictation,
|
||||
canConfirm: canConfirmDictation,
|
||||
autoStopWhenHidden: { isVisible: isScreenFocused },
|
||||
autoStopWhenHidden: { isVisible: isInputActive },
|
||||
enableDuration: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -89,6 +89,43 @@ describe("keyboard-action-dispatcher", () => {
|
||||
expect(handle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("dispatches to the active mounted tab when a newer hidden tab is inactive", () => {
|
||||
const calls: string[] = [];
|
||||
const action: KeyboardActionDefinition = {
|
||||
id: "message-input.dictation-toggle",
|
||||
scope: "message-input",
|
||||
};
|
||||
|
||||
dispatcher.registerHandler({
|
||||
handlerId: "visible-tab",
|
||||
actions: [action.id],
|
||||
enabled: true,
|
||||
priority: 100,
|
||||
isActive: () => true,
|
||||
handle: () => {
|
||||
calls.push("visible-tab");
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
dispatcher.registerHandler({
|
||||
handlerId: "hidden-tab",
|
||||
actions: [action.id],
|
||||
enabled: true,
|
||||
priority: 100,
|
||||
isActive: () => false,
|
||||
handle: () => {
|
||||
calls.push("hidden-tab");
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const handled = dispatcher.dispatch(action);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(calls).toEqual(["visible-tab"]);
|
||||
});
|
||||
|
||||
it("tries lower-priority handlers when a higher one does not consume the action", () => {
|
||||
const calls: string[] = [];
|
||||
const action: KeyboardActionDefinition = {
|
||||
|
||||
@@ -807,6 +807,7 @@ function AgentPanelBody({
|
||||
<AgentInputArea
|
||||
agentId={agentId}
|
||||
serverId={serverId}
|
||||
isInputActive={isPaneFocused}
|
||||
value={agentInputDraft.text}
|
||||
onChangeText={agentInputDraft.setText}
|
||||
images={agentInputDraft.images}
|
||||
|
||||
@@ -1219,6 +1219,7 @@ function DraftAgentScreenContent({
|
||||
<AgentInputArea
|
||||
agentId={draftAgentIdRef.current}
|
||||
serverId={selectedServerId ?? ""}
|
||||
isInputActive={isFocused}
|
||||
onSubmitMessage={handleCreateFromInput}
|
||||
isSubmitLoading={isSubmitting}
|
||||
blurOnSubmit={true}
|
||||
|
||||
@@ -268,6 +268,7 @@ export function WorkspaceDraftAgentTab({
|
||||
<AgentInputArea
|
||||
agentId={tabId}
|
||||
serverId={serverId}
|
||||
isInputActive={isPaneFocused}
|
||||
onSubmitMessage={handleCreateFromInput}
|
||||
isSubmitLoading={isSubmitting}
|
||||
blurOnSubmit={true}
|
||||
|
||||
Reference in New Issue
Block a user