mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-31 05:32:58 +00:00
fix(app): keep composer layout stable across chat and workspace screens (#4973)
* fix(app): dock the New workspace composer like a chat on phones Only the composer is bounded by the viewport; the setup fields sit above it in a bottom-anchored stack that moves up as the draft grows. The keyboard reservation is retained on every screen, so the composer keeps one height across keyboard transitions. Removes the per-screen reserve policy. * fix(app): share composer docking across mobile hosts Extract chat's retained capacity and platform keyboard motion so chat, workspace drafts, and New workspace share one sizing and dismissal boundary. Keep the native drivers and settled portal reconciliation unchanged. * fix(app): preserve overlays in centered composer docks Honor the optional overlay child in both layouts so changing dock placement does not silently discard part of its content. * test(app): resolve keyboard controller through its web entry The shared keyboard entry point makes stream tests load the provider. Keep its real ESM dependencies inside the Vitest resolver so CommonJS does not bypass the React Native web alias and parse native Flow sources. * fix(app): keep native chat scrolling responsive Place keyboard dismissal behind scrollable content so an idle Android Fabric JS responder cannot intercept native drags. Keep decorative form padding transparent to iOS hit testing and exercise idle native scrolling after submission from every composer host.
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { FOOTER_HEIGHT, MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { KeyboardTranslateView } from "@/components/keyboard-translate-view";
|
||||
import { KeyboardTranslateView } from "@/keyboard/shift";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import { toErrorMessage } from "@/utils/error-messages";
|
||||
@@ -17,17 +16,11 @@ interface ArchivedAgentCalloutProps {
|
||||
|
||||
export function ArchivedAgentCallout({ serverId, agentId }: ArchivedAgentCalloutProps) {
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const client = useHostRuntimeClient(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
const [isUnarchiving, setIsUnarchiving] = useState(false);
|
||||
const [unarchiveError, setUnarchiveError] = useState<string | null>(null);
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() => [styles.container, { paddingBottom: insets.bottom }],
|
||||
[insets.bottom],
|
||||
);
|
||||
|
||||
const handleUnarchive = useCallback(async () => {
|
||||
if (!client || !isConnected || isUnarchiving) return;
|
||||
setIsUnarchiving(true);
|
||||
@@ -41,7 +34,7 @@ export function ArchivedAgentCallout({ serverId, agentId }: ArchivedAgentCallout
|
||||
}, [client, isConnected, isUnarchiving, agentId]);
|
||||
|
||||
return (
|
||||
<KeyboardTranslateView style={containerStyle}>
|
||||
<KeyboardTranslateView style={styles.container}>
|
||||
<View style={styles.inputAreaContainer}>
|
||||
<View style={styles.inputAreaContent}>
|
||||
<View style={styles.calloutStack}>
|
||||
|
||||
@@ -25,8 +25,8 @@ import {
|
||||
import { ChangesSurface } from "@/git/diff-pane";
|
||||
import { changesStateSchema, defaultChangesState, type ChangesState } from "@/panels/changes/state";
|
||||
import { FileExplorerPane } from "./file-explorer-pane";
|
||||
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
||||
import { shouldUseCompactExplorerKeyboardPadding } from "@/hooks/keyboard-shift-policy";
|
||||
import { useKeyboardShiftStyle } from "@/keyboard/shift";
|
||||
import { shouldUseCompactExplorerKeyboardPadding } from "@/keyboard/shift";
|
||||
import { WindowChromeSafeArea } from "@/utils/desktop-window";
|
||||
import { TitlebarDragRegion } from "@/components/desktop/titlebar-drag-region";
|
||||
import { RetainedPanel, RetainedPanelActivity } from "@/components/retained-panel";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ComposerDockBackground } from "@/composer/dock";
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import type { LayoutChangeEvent } from "react-native";
|
||||
import { View, type StyleProp, type ViewStyle } from "react-native";
|
||||
@@ -49,7 +50,7 @@ export function ScreenHeader({
|
||||
const rightCombinedStyle = useMemo(() => [styles.right, rightStyle], [rightStyle]);
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
<ComposerDockBackground style={styles.header} testID="composer-dock-header">
|
||||
<View style={innerStyle}>
|
||||
<WindowChromeSafeArea
|
||||
placement="inline"
|
||||
@@ -62,7 +63,7 @@ export function ScreenHeader({
|
||||
<View style={rightCombinedStyle}>{right}</View>
|
||||
</WindowChromeSafeArea>
|
||||
</View>
|
||||
</View>
|
||||
</ComposerDockBackground>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./keyboard-dock.native";
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { ViewProps } from "react-native";
|
||||
import { KeyboardTranslateView } from "@/components/keyboard-translate-view";
|
||||
|
||||
interface KeyboardDockProps extends ViewProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function KeyboardDock({ children, style, ...props }: KeyboardDockProps) {
|
||||
return (
|
||||
<KeyboardTranslateView style={style} {...props}>
|
||||
{children}
|
||||
</KeyboardTranslateView>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { View, type ViewProps } from "react-native";
|
||||
|
||||
interface KeyboardDockProps extends ViewProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function KeyboardDock(props: KeyboardDockProps) {
|
||||
return <View {...props} />;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { ViewProps } from "react-native";
|
||||
import Animated, { useAnimatedStyle } from "react-native-reanimated";
|
||||
import { useKeyboardShift } from "@/hooks/keyboard-shift-context";
|
||||
|
||||
interface KeyboardTranslateViewProps extends ViewProps {
|
||||
children: ReactNode;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function KeyboardTranslateView({
|
||||
children,
|
||||
enabled = true,
|
||||
style,
|
||||
...props
|
||||
}: KeyboardTranslateViewProps) {
|
||||
const { shift } = useKeyboardShift();
|
||||
const keyboardStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateY: enabled ? -shift.value : 0 }],
|
||||
}));
|
||||
|
||||
return (
|
||||
<Animated.View style={[style, keyboardStyle]} {...props}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import { Animated, type ViewProps } from "react-native";
|
||||
import { useKeyboardAnimation } from "react-native-keyboard-controller";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
interface KeyboardTranslateViewProps extends ViewProps {
|
||||
children: ReactNode;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function KeyboardTranslateView({
|
||||
children,
|
||||
enabled = true,
|
||||
style,
|
||||
...props
|
||||
}: KeyboardTranslateViewProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { height, progress } = useKeyboardAnimation();
|
||||
const translateY = useMemo(
|
||||
() => Animated.add(height, Animated.multiply(progress, insets.bottom)),
|
||||
[height, insets.bottom, progress],
|
||||
);
|
||||
const keyboardStyle = useMemo(
|
||||
() => ({ transform: [{ translateY: enabled ? translateY : 0 }] }),
|
||||
[enabled, translateY],
|
||||
);
|
||||
|
||||
return (
|
||||
<Animated.View style={[style, keyboardStyle]} {...props}>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { View, type ViewProps } from "react-native";
|
||||
|
||||
interface KeyboardTranslateViewProps extends ViewProps {
|
||||
children: ReactNode;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function KeyboardTranslateView({
|
||||
children,
|
||||
enabled: _enabled,
|
||||
...props
|
||||
}: KeyboardTranslateViewProps) {
|
||||
return <View {...props}>{children}</View>;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "@getpaseo/protocol/terminal-input-mode";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
||||
import { useKeyboardShiftStyle } from "@/keyboard/shift";
|
||||
import { useAppActivelyVisible } from "@/hooks/use-app-visible";
|
||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||
import {
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
measureFloatingPanelPortalHost,
|
||||
useFloatingPanelPortalHostName,
|
||||
} from "@/components/ui/floating-panel-portal";
|
||||
import { useKeyboardShift } from "@/hooks/keyboard-shift-context";
|
||||
import { useKeyboardShift } from "@/keyboard/shift";
|
||||
import { SPACING } from "@/styles/theme";
|
||||
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { KeyboardTranslateView } from "@/keyboard/shift";
|
||||
import type { CreateWorkspaceRequestOptions } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { AgentSnapshotPayload } from "@getpaseo/protocol/messages";
|
||||
import { getHostRuntimeStore } from "@/runtime/host-runtime";
|
||||
@@ -445,25 +446,27 @@ export function WorkspaceSetupDialog() {
|
||||
desktopMaxWidth={640}
|
||||
>
|
||||
<FileDropZone style={styles.section}>
|
||||
<Composer
|
||||
agentId={`workspace-setup:${serverId}:${sourceDirectory}`}
|
||||
serverId={serverId}
|
||||
isPaneFocused={true}
|
||||
onSubmitMessage={handleCreateChatAgent}
|
||||
isSubmitLoading={pendingAction === "chat"}
|
||||
blurOnSubmit={true}
|
||||
textSource={chatDraft.textSource}
|
||||
onChangeText={chatDraft.editText}
|
||||
textReplacement={chatDraft.textReplacement}
|
||||
attachments={chatDraft.attachments}
|
||||
onChangeAttachments={chatDraft.setAttachments}
|
||||
cwd={sourceDirectory}
|
||||
clearDraft={chatDraft.clear}
|
||||
autoFocus
|
||||
commandDraftConfig={composerState?.commandDraftConfig}
|
||||
agentControls={agentControlsWithDisabled}
|
||||
inputWrapperStyle={styles.composerInputWrapper}
|
||||
/>
|
||||
<KeyboardTranslateView>
|
||||
<Composer
|
||||
agentId={`workspace-setup:${serverId}:${sourceDirectory}`}
|
||||
serverId={serverId}
|
||||
isPaneFocused={true}
|
||||
onSubmitMessage={handleCreateChatAgent}
|
||||
isSubmitLoading={pendingAction === "chat"}
|
||||
blurOnSubmit={true}
|
||||
textSource={chatDraft.textSource}
|
||||
onChangeText={chatDraft.editText}
|
||||
textReplacement={chatDraft.textReplacement}
|
||||
attachments={chatDraft.attachments}
|
||||
onChangeAttachments={chatDraft.setAttachments}
|
||||
cwd={sourceDirectory}
|
||||
clearDraft={chatDraft.clear}
|
||||
autoFocus
|
||||
commandDraftConfig={composerState?.commandDraftConfig}
|
||||
agentControls={agentControlsWithDisabled}
|
||||
inputWrapperStyle={styles.composerInputWrapper}
|
||||
/>
|
||||
</KeyboardTranslateView>
|
||||
</FileDropZone>
|
||||
|
||||
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
||||
|
||||
Reference in New Issue
Block a user