refactor agent route for draft→chat transition

This commit is contained in:
Mohamed Boudra
2026-01-24 19:18:07 +07:00
parent 56399bcb42
commit 6ecf88b502
18 changed files with 1425 additions and 1010 deletions
@@ -127,6 +127,7 @@ interface SelectFieldProps {
warningMessage?: string | null;
helperText?: string | null;
controlRef?: React.RefObject<View | null>;
valueEllipsizeMode?: "head" | "middle" | "tail" | "clip";
testID?: string;
}
@@ -140,6 +141,7 @@ export function SelectField({
warningMessage,
helperText,
controlRef,
valueEllipsizeMode,
testID,
}: SelectFieldProps): ReactElement {
return (
@@ -156,6 +158,7 @@ export function SelectField({
<Text
style={value ? styles.selectFieldValue : styles.selectFieldPlaceholder}
numberOfLines={1}
ellipsizeMode={valueEllipsizeMode ?? "tail"}
>
{value || placeholder || "Select..."}
</Text>
@@ -1045,6 +1048,13 @@ export function WorkingDirectoryDropdown({
[handleClose, onSelectPath]
);
const handleSubmitSearch = useCallback(() => {
if (!showCustomOption) {
return;
}
handleSelect(sanitizedSearchValue);
}, [handleSelect, sanitizedSearchValue, showCustomOption]);
return (
<>
<SelectField
@@ -1054,7 +1064,9 @@ export function WorkingDirectoryDropdown({
onPress={handleOpen}
disabled={disabled}
errorMessage={errorMessage || undefined}
valueEllipsizeMode="middle"
controlRef={anchorRef}
testID="working-directory-select"
/>
<AdaptiveSelect
title="Working Directory"
@@ -1072,6 +1084,7 @@ export function WorkingDirectoryDropdown({
autoCapitalize="none"
autoCorrect={false}
autoFocus
onSubmitEditing={handleSubmitSearch}
/>
) : (
<BottomSheetTextInput
@@ -1083,6 +1096,7 @@ export function WorkingDirectoryDropdown({
autoCapitalize="none"
autoCorrect={false}
autoFocus
onSubmitEditing={handleSubmitSearch}
/>
)}
{!hasSuggestedPaths && !showCustomOption ? (
@@ -1094,6 +1108,7 @@ export function WorkingDirectoryDropdown({
<View style={styles.dropdownSheetList}>
<Pressable
key="working-dir-custom-option"
testID="working-directory-custom-option"
style={styles.dropdownSheetOption}
onPress={() => handleSelect(sanitizedSearchValue)}
>
@@ -46,6 +46,8 @@ interface AgentInputAreaProps {
onSubmitMessage?: (payload: MessagePayload) => Promise<void>;
/** Externally controlled loading state. When true, disables the submit button. */
isSubmitLoading?: boolean;
/** When true, blurs the input immediately when submitting. */
blurOnSubmit?: boolean;
value?: string;
onChangeText?: (text: string) => void;
/** When true, auto-focuses the text input on web. */
@@ -70,6 +72,7 @@ export function AgentInputArea({
serverId,
onSubmitMessage,
isSubmitLoading = false,
blurOnSubmit = false,
value,
onChangeText,
autoFocus = false,
@@ -219,10 +222,13 @@ export function AgentInputArea({
forceSend?: boolean
) {
const socketConnected = isConnected;
if (!message.trim() || !socketConnected) return;
if (!sendAgentMessage && !onSubmitMessageRef.current) return;
const trimmedMessage = message.trim();
if (!trimmedMessage) return;
// When the parent controls submission (e.g. draft agent creation), let it
// decide what to do even if the socket is currently disconnected (so we
// don't no-op and lose deterministic error handling in the UI/tests).
if (!onSubmitMessageRef.current && !socketConnected) return;
if (!sendAgentMessage && !onSubmitMessageRef.current) return;
if (agent?.status === "running" && !forceSend) {
queueMessage(trimmedMessage, imageAttachments);
@@ -257,6 +263,9 @@ export function AgentInputArea({
}
function handleSubmit(payload: MessagePayload) {
if (blurOnSubmit) {
messageInputRef.current?.blur();
}
void sendMessageWithContent(
payload.text,
payload.images,
@@ -569,7 +578,7 @@ export function AgentInputArea({
client={client}
placeholder="Message agent..."
autoFocus={autoFocus}
disabled={isRealtimeMode}
disabled={isRealtimeMode || isSubmitLoading}
isScreenFocused={isScreenFocused}
leftContent={leftContent}
rightContent={rightContent}
+2 -3
View File
@@ -85,10 +85,9 @@ export function AgentList({
onAgentSelect?.();
navigate({
pathname: "/agent/[serverId]/[agentId]",
pathname: "/agent/[...route]",
params: {
serverId,
agentId,
route: [serverId, agentId],
},
});
},
@@ -607,7 +607,7 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) {
predicate: (query) =>
Array.isArray(query.queryKey) && query.queryKey[0] === "paseoWorktreeList",
});
router.replace("/");
router.replace("/agent");
},
onError: (err) => {
const message = err instanceof Error ? err.message : "Failed to archive worktree";
+2 -2
View File
@@ -123,7 +123,7 @@ export function HomeFooter() {
{/* Action menu */}
<View style={styles.actionButtonContainer}>
<Pressable
onPress={() => router.push("/")}
onPress={() => router.push("/agents")}
style={({ pressed }) => [
styles.footerButton,
pressed && styles.buttonPressed,
@@ -142,7 +142,7 @@ export function HomeFooter() {
<Pressable
onPress={() => {
console.log("[HomeFooter] New Agent button pressed");
router.push("/");
router.push("/agent");
}}
style={({ pressed }) => [
styles.footerButton,
+46 -11
View File
@@ -70,6 +70,7 @@ export interface MessageInputProps {
export interface MessageInputRef {
focus: () => void;
blur: () => void;
}
const MIN_INPUT_HEIGHT = 30;
@@ -118,15 +119,19 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
) {
const { theme } = useUnistyles();
const toggleAgentList = usePanelStore((state) => state.toggleAgentList);
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT);
const textInputRef = useRef<
TextInput | (TextInput & { getNativeRef?: () => unknown }) | null
>(null);
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT);
const textInputRef = useRef<
TextInput | (TextInput & { getNativeRef?: () => unknown }) | null
>(null);
const isInputFocusedRef = useRef(false);
useImperativeHandle(ref, () => ({
focus: () => {
textInputRef.current?.focus();
},
blur: () => {
textInputRef.current?.blur?.();
},
}));
const inputHeightRef = useRef(MIN_INPUT_HEIGHT);
const baselineInputHeightRef = useRef<number | null>(null);
@@ -210,6 +215,11 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
enableDuration: true,
});
const isDictatingRef = useRef(isDictating);
useEffect(() => {
isDictatingRef.current = isDictating;
}, [isDictating]);
useEffect(() => {
if (isDictating || isDictationProcessing) {
return;
@@ -219,12 +229,29 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
// Cmd+D to start/submit dictation, Escape to cancel
useEffect(() => {
if (!IS_WEB || !isScreenFocused) return;
if (!IS_WEB) return;
const resolveNativeInput = (): unknown => {
const current = textInputRef.current as any;
if (!current) return null;
if (typeof current.getNativeRef === "function") {
return current.getNativeRef();
}
return current;
};
function handleKeyDown(event: KeyboardEvent) {
const nativeInput = resolveNativeInput();
const isFromInput = Boolean(nativeInput && event.target === nativeInput);
if (!isScreenFocused && !isInputFocusedRef.current && !isFromInput) {
return;
}
const dictating = isDictatingRef.current;
// Cmd+D: start dictation or submit if already dictating
if ((event.metaKey || event.ctrlKey) && event.key === "d") {
if (
(event.metaKey || event.ctrlKey) &&
(event.code === "KeyD" || event.key.toLowerCase() === "d")
) {
event.preventDefault();
if (isDictating) {
if (dictating) {
sendAfterTranscriptRef.current = true;
confirmDictation();
} else {
@@ -233,14 +260,14 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
return;
}
// Escape: cancel dictation
if (event.key === "Escape" && isDictating) {
if (event.key === "Escape" && dictating) {
event.preventDefault();
cancelDictation();
}
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isDictating, cancelDictation, confirmDictation, startDictation]);
}, [cancelDictation, confirmDictation, isScreenFocused, startDictation]);
// Animate overlay
useEffect(() => {
@@ -400,15 +427,17 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
const { shiftKey, metaKey, ctrlKey } = event.nativeEvent;
const key = event.nativeEvent.key.toLowerCase();
// Cmd+B or Ctrl+B: toggle sidebar
if ((metaKey || ctrlKey) && event.nativeEvent.key === "b") {
if ((metaKey || ctrlKey) && key === "b") {
event.preventDefault();
toggleAgentList();
return;
}
// Cmd+D or Ctrl+D: start dictation or submit if already dictating
if ((metaKey || ctrlKey) && event.nativeEvent.key === "d") {
if ((metaKey || ctrlKey) && key === "d") {
event.preventDefault();
if (isDictating) {
sendAfterTranscriptRef.current = true;
@@ -502,6 +531,12 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
onChangeText={onChangeText}
placeholder={placeholder}
placeholderTextColor={theme.colors.mutedForeground}
onFocus={() => {
isInputFocusedRef.current = true;
}}
onBlur={() => {
isInputFocusedRef.current = false;
}}
style={[
styles.textInput,
IS_WEB
@@ -82,7 +82,7 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
}, [closeToAgent]);
const handleCreateAgentClean = useCallback(() => {
router.push("/");
router.push("/agent");
}, []);
// Mobile: close sidebar and navigate