refactor agent route for draft→chat transition
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user