Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion packages/app/src/components/agent-input-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
import { submitAgentInput } from "@/components/agent-input-submit";
import {
markActiveChatComposer,
registerActiveChatComposer,
} from "@/utils/active-chat-composer";
import { buildComposerInsertResult } from "@/utils/composer-text-insert";

type QueuedMessage = {
id: string;
Expand Down Expand Up @@ -77,6 +82,8 @@ interface AgentInputAreaProps {
onAddImages?: (addImages: (images: ImageAttachment[]) => void) => void;
/** Callback to expose a focus function to parent components (desktop only). */
onFocusInput?: (focus: () => void) => void;
/** Called when external insertion should activate the owning workspace tab. */
onActivateTab?: () => void;
/** Optional draft context for listing commands before an agent exists. */
commandDraftConfig?: DraftCommandConfig;
/** Called when a message is about to be sent (any path: keyboard, dictation, queued). */
Expand Down Expand Up @@ -107,6 +114,7 @@ export function AgentInputArea({
autoFocus = false,
onAddImages,
onFocusInput,
onActivateTab,
commandDraftConfig,
onMessageSent,
onComposerHeightChange,
Expand Down Expand Up @@ -168,9 +176,14 @@ export function AgentInputArea({
const [sendError, setSendError] = useState<string | null>(null);
const [isMessageInputFocused, setIsMessageInputFocused] = useState(false);
const messageInputRef = useRef<MessageInputRef>(null);
const composerSelectionRef = useRef({ start: 0, end: 0 });
const hasKnownComposerSelectionRef = useRef(false);
const isMessageInputFocusedRef = useRef(false);
const userInputRef = useRef(userInput);
const keyboardHandlerIdRef = useRef(
`message-input:${serverId}:${agentId}:${Math.random().toString(36).slice(2)}`,
);
const composerId = `${serverId}:${agentId}`;

const autocomplete = useAgentAutocomplete({
userInput,
Expand All @@ -195,6 +208,10 @@ export function AgentInputArea({
setCursorIndex((current) => Math.min(current, userInput.length));
}, [userInput.length]);

useEffect(() => {
userInputRef.current = userInput;
}, [userInput]);

const { pickImages } = useImageAttachmentPicker();
const agentIdRef = useRef(agentId);
const sendAgentMessageRef = useRef<
Expand All @@ -215,7 +232,10 @@ export function AgentInputArea({
}, [addImages, onAddImages]);

const focusInput = useCallback(() => {
if (Platform.OS !== "web") return;
if (Platform.OS !== "web") {
messageInputRef.current?.focus();
return;
}
focusWithRetries({
focus: () => messageInputRef.current?.focus(),
isFocused: () => {
Expand All @@ -229,6 +249,59 @@ export function AgentInputArea({
onFocusInput?.(focusInput);
}, [focusInput, onFocusInput]);

const insertComposerText = useCallback(
(text: string): boolean => {
const token = text.trim();
if (!token) {
return false;
}

const result = buildComposerInsertResult({
value: userInputRef.current,
token,
selection: composerSelectionRef.current,
hasKnownSelection: hasKnownComposerSelectionRef.current,
});
if (result.text === userInputRef.current) {
return false;
}

composerSelectionRef.current = result.selection;
hasKnownComposerSelectionRef.current = true;
userInputRef.current = result.text;
setCursorIndex(result.selection.start);
setUserInput(result.text);

const applySelection = () => {
messageInputRef.current?.setSelection(result.selection);
};
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(applySelection);
} else {
setTimeout(applySelection, 0);
}
return true;
},
[],
);

useEffect(() => {
return registerActiveChatComposer({
id: composerId,
handle: {
insertText: insertComposerText,
activateTab: onActivateTab,
},
});
}, [composerId, insertComposerText, onActivateTab]);

useEffect(() => {
if (!isInputActive) {
return;
}
markActiveChatComposer(composerId);
}, [composerId, isInputActive]);

const submitMessage = useCallback(
async (text: string, images?: ImageAttachment[]) => {
onMessageSent?.();
Expand Down Expand Up @@ -746,11 +819,15 @@ export function AgentInputArea({
onSubmitLoadingPress={isAgentRunning ? handleCancelAgent : undefined}
onKeyPress={handleCommandKeyPress}
onSelectionChange={(selection) => {
composerSelectionRef.current = selection;
hasKnownComposerSelectionRef.current = true;
setCursorIndex(selection.start);
}}
onFocusChange={(focused) => {
isMessageInputFocusedRef.current = focused;
setIsMessageInputFocused(focused);
if (focused) {
markActiveChatComposer(composerId);
onAttentionInputFocus?.();
}
}}
Expand Down
5 changes: 4 additions & 1 deletion packages/app/src/components/diff-scroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface DiffScrollProps {
children: React.ReactNode;
scrollViewWidth: number;
onScrollViewWidthChange: (width: number) => void;
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
style?: StyleProp<ViewStyle>;
contentContainerStyle?: StyleProp<ViewStyle>;
}
Expand All @@ -22,6 +23,7 @@ export function DiffScroll({
children,
scrollViewWidth,
onScrollViewWidthChange,
onScroll,
style,
contentContainerStyle,
}: DiffScrollProps) {
Expand Down Expand Up @@ -57,8 +59,9 @@ export function DiffScroll({
if (horizontalScroll) {
horizontalScroll.registerScrollOffset(scrollId, offsetX);
}
onScroll?.(event);
},
[horizontalScroll, scrollId],
[horizontalScroll, onScroll, scrollId],
);

return (
Expand Down
12 changes: 11 additions & 1 deletion packages/app/src/components/diff-scroll.web.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { ScrollView, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native";
import {
ScrollView,
type LayoutChangeEvent,
type NativeSyntheticEvent,
type NativeScrollEvent,
type StyleProp,
type ViewStyle,
} from "react-native";

interface DiffScrollProps {
children: React.ReactNode;
scrollViewWidth: number;
onScrollViewWidthChange: (width: number) => void;
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
style?: StyleProp<ViewStyle>;
contentContainerStyle?: StyleProp<ViewStyle>;
}

export function DiffScroll({
children,
onScrollViewWidthChange,
onScroll,
style,
contentContainerStyle,
}: DiffScrollProps) {
Expand All @@ -21,6 +30,7 @@ export function DiffScroll({
showsHorizontalScrollIndicator
style={style}
contentContainerStyle={contentContainerStyle}
onScroll={onScroll}
onLayout={(e: LayoutChangeEvent) => onScrollViewWidthChange(e.nativeEvent.layout.width)}
>
{children}
Expand Down
Loading