mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-31 05:32:58 +00:00
fix(app): expose terminal controls on native tablets (#5712)
Wide native panes previously followed the compact-layout gate, hiding terminal keys and skipping software-keyboard clearance. Keep bar eligibility tied to native touch while sizing rows from the available pane width. Co-authored-by: Leandro Ardissone <lardissone@gmail.com>
This commit is contained in:
co-authored by
Leandro Ardissone
parent
ad56659a89
commit
2553ab4d82
@@ -0,0 +1,24 @@
|
||||
context platform=android timeout=90000 retries=1
|
||||
env APP_ID=sh.paseo.debug
|
||||
|
||||
open "${APP_ID}"
|
||||
wait "id=\"workspace-new-tab-button\"" 45000
|
||||
press "id=\"workspace-new-tab-button\""
|
||||
wait "id=\"workspace-new-tab-menu-terminal\"" 10000
|
||||
press "id=\"workspace-new-tab-menu-terminal\""
|
||||
wait "id=\"terminal-key-esc\"" 10000
|
||||
is visible "id=\"terminal-virtual-keyboard-native-wide\""
|
||||
is visible "id=\"terminal-key-esc\""
|
||||
is visible "id=\"terminal-key-ctrl\""
|
||||
is visible "id=\"terminal-key-enter\""
|
||||
is visible "id=\"terminal-paste\""
|
||||
press "id=\"terminal-keyboard-toggle\""
|
||||
type "printf '__AD_%s__\\n' NATIVE_WIDE_KEY_OK" --delay-ms 0
|
||||
press "id=\"terminal-key-enter\""
|
||||
wait "text" "__AD_NATIVE_WIDE_KEY_OK__" 10000
|
||||
press "id=\"terminal-key-ctrl\""
|
||||
type "c" --delay-ms 0
|
||||
type "printf '__AD_%s__\\n' MODIFIER_RESET_OK" --delay-ms 0
|
||||
press "id=\"terminal-key-enter\""
|
||||
wait "text" "__AD_MODIFIER_RESET_OK__" 10000
|
||||
close
|
||||
@@ -0,0 +1,17 @@
|
||||
context platform=ios timeout=90000 retries=1
|
||||
env APP_ID=sh.paseo.debug
|
||||
|
||||
open "${APP_ID}"
|
||||
wait "id=\"workspace-new-tab-button\"" 30000
|
||||
press "id=\"workspace-new-tab-button\""
|
||||
wait "id=\"workspace-new-tab-menu-terminal\"" 10000
|
||||
press "id=\"workspace-new-tab-menu-terminal\""
|
||||
wait "id=\"terminal-virtual-keyboard-native-wide\"" 10000
|
||||
is visible "id=\"terminal-key-esc\""
|
||||
is visible "id=\"terminal-key-enter\""
|
||||
is visible "id=\"terminal-paste\""
|
||||
press "id=\"terminal-keyboard-toggle\""
|
||||
type "printf '__AD_IOS_%s__\\n' KEY_OK" --delay-ms 0
|
||||
press "id=\"terminal-key-enter\""
|
||||
wait "text" "__AD_IOS_KEY_OK__" 10000
|
||||
close
|
||||
@@ -0,0 +1,40 @@
|
||||
appId: sh.paseo.debug
|
||||
---
|
||||
- launchApp
|
||||
- runFlow:
|
||||
when:
|
||||
visible: ".*getRegistrationInfoAsync.*"
|
||||
commands:
|
||||
- tapOn: ".*getRegistrationInfoAsync.*"
|
||||
- tapOn: Dismiss
|
||||
- assertNotVisible: ".*getRegistrationInfoAsync.*"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: workspace-new-tab-button
|
||||
timeout: 30000
|
||||
- tapOn:
|
||||
id: workspace-new-tab-button
|
||||
- tapOn:
|
||||
id: workspace-new-tab-menu-terminal
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: terminal-virtual-keyboard-native-wide
|
||||
timeout: 10000
|
||||
- assertVisible:
|
||||
id: terminal-key-esc
|
||||
- assertVisible:
|
||||
id: terminal-key-enter
|
||||
- assertVisible:
|
||||
id: terminal-paste
|
||||
- tapOn:
|
||||
id: terminal-paste
|
||||
- runFlow:
|
||||
when:
|
||||
visible: Allow Paste
|
||||
commands:
|
||||
- tapOn: Allow Paste
|
||||
- tapOn:
|
||||
id: terminal-key-enter
|
||||
- extendedWaitUntil:
|
||||
visible: __AD_IOS_PASTE_OK__
|
||||
timeout: 10000
|
||||
@@ -0,0 +1,18 @@
|
||||
# Native wide terminal device flows
|
||||
|
||||
Run these against a connected task-owned tablet with the current development app, Metro bundle, and a workspace whose terminal opens successfully. The flows assert `terminal-virtual-keyboard-native-wide`, which is emitted only for a native noncompact window. That assertion makes a compact phone fail before any PTY success marker. A landscape phone that reaches the noncompact breakpoint is excluded by the explicit tablet `--device` argument; do not rely on the runner's default device.
|
||||
|
||||
Android Pixel Tablet API 36.1:
|
||||
|
||||
```sh
|
||||
agent-device replay packages/app/e2e/mobile/agent-device/native-terminal-wide-keybar.android.ad --platform android --device paseo-tablet-p1-api36
|
||||
```
|
||||
|
||||
iPad Pro 11-inch (M5), iOS 26.5: from the Namespace Mac checkout, prefill the simulator clipboard with the command below, then run the Maestro compatibility flow. Its `runFlow.when.visible` presses **Allow Paste** only if the iOS sheet appears; every other action and output assertion remains strict. The clipboard payload contains `PASTE_OK`, while only executed PTY output contains `__AD_IOS_PASTE_OK__`.
|
||||
|
||||
```sh
|
||||
printf '%s' "printf '__AD_IOS_%s__\\n' PASTE_OK" | xcrun simctl pbcopy E49C88C3-9A60-4715-B63A-881498D1991A
|
||||
agent-device replay packages/app/e2e/mobile/agent-device/native-terminal-wide-paste.ios.yaml --maestro --platform ios --device 'iPad Pro 11-inch (M5)'
|
||||
```
|
||||
|
||||
The unsigned simulator build used for phase 1 raises a known `getRegistrationInfoAsync` entitlement overlay on launch. The flow dismisses only that exact development error and asserts it is gone before touching terminal controls; another overlay still fails the flow. `native-terminal-wide-keybar.ios.ad` separately proves that the wide iPad bar sends Enter to the PTY. It needs no clipboard permission. The Maestro flow above is the paste gate for both first-run and already-granted states.
|
||||
@@ -4,7 +4,13 @@ import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
||||
import {
|
||||
Pressable,
|
||||
Text,
|
||||
View,
|
||||
type LayoutChangeEvent,
|
||||
type PressableStateCallbackType,
|
||||
} from "react-native";
|
||||
import Animated, { runOnJS, useAnimatedReaction } from "react-native-reanimated";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { Keyboard as KeyboardIcon, KeyboardOff as KeyboardOffIcon } from "lucide-react-native";
|
||||
@@ -31,9 +37,10 @@ import {
|
||||
} from "@/terminal/runtime/terminal-key-dispatch";
|
||||
import {
|
||||
getTerminalVirtualKeyboardControlId,
|
||||
resolveTerminalVirtualKeyboardRows,
|
||||
shouldShowTerminalVirtualKeyBar,
|
||||
shouldShowTerminalFloatingCopyAction,
|
||||
shouldShowTerminalPasteAction,
|
||||
TERMINAL_VIRTUAL_KEYBOARD_ROWS,
|
||||
type TerminalVirtualKeyboardControl,
|
||||
} from "@/terminal/runtime/terminal-virtual-keyboard";
|
||||
import { pasteTerminalClipboard } from "@/terminal/runtime/terminal-paste";
|
||||
@@ -93,6 +100,7 @@ interface TerminalPaneProps {
|
||||
|
||||
const TERMINAL_REFIT_DELAYS_MS = [0, 48, 144, 320];
|
||||
const TERMINAL_RESIZE_DEBOUNCE_MS = 100;
|
||||
const TERMINAL_KEY_BAR_MAX_WIDTH = 880;
|
||||
|
||||
const MODIFIER_LABELS = {
|
||||
ctrl: "Ctrl",
|
||||
@@ -138,8 +146,16 @@ function ModifierButton({ modifier, active, onToggle }: ModifierButtonProps) {
|
||||
() => [styles.keyButtonText, active && styles.keyButtonTextActive],
|
||||
[active],
|
||||
);
|
||||
const accessibilityState = useMemo(() => ({ selected: active }), [active]);
|
||||
return (
|
||||
<Pressable testID={`terminal-key-${modifier}`} onPress={handlePress} style={pressableStyle}>
|
||||
<Pressable
|
||||
accessibilityLabel={MODIFIER_LABELS[modifier]}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={accessibilityState}
|
||||
testID={`terminal-key-${modifier}`}
|
||||
onPress={handlePress}
|
||||
style={pressableStyle}
|
||||
>
|
||||
<Text style={textStyle}>{MODIFIER_LABELS[modifier]}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
@@ -162,7 +178,13 @@ function VirtualKeyButton({ id, label, keyValue, onSend }: VirtualKeyButtonProps
|
||||
[],
|
||||
);
|
||||
return (
|
||||
<Pressable testID={`terminal-key-${id}`} onPress={handlePress} style={pressableStyle}>
|
||||
<Pressable
|
||||
accessibilityLabel={label}
|
||||
accessibilityRole="button"
|
||||
testID={`terminal-key-${id}`}
|
||||
onPress={handlePress}
|
||||
style={pressableStyle}
|
||||
>
|
||||
<Text style={styles.keyButtonText}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
@@ -222,12 +244,18 @@ export function TerminalPane({
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}, [settings.monoFontFamily]);
|
||||
const isMobile = useIsCompactFormFactor();
|
||||
const showVirtualKeyBar = shouldShowTerminalVirtualKeyBar({ isNative, isCompact: isMobile });
|
||||
const [keyBarWidth, setKeyBarWidth] = useState(0);
|
||||
const virtualKeyboardRows = resolveTerminalVirtualKeyboardRows({
|
||||
isCompact: isMobile,
|
||||
availableWidth: keyBarWidth,
|
||||
});
|
||||
const mobileView = usePanelStore((state) => state.mobilePanel.target);
|
||||
const showMobileAgentList = usePanelStore((state) => state.showMobileAgentList);
|
||||
const swipeGesturesEnabled = isMobile;
|
||||
const { shift: keyboardShift, style: keyboardPaddingStyle } = useKeyboardShiftStyle({
|
||||
mode: "padding",
|
||||
enabled: isMobile,
|
||||
enabled: isNative || isMobile,
|
||||
});
|
||||
const [keyboardInset, setKeyboardInset] = useState(0);
|
||||
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
|
||||
@@ -292,7 +320,7 @@ export function TerminalPane({
|
||||
}, [terminalId]);
|
||||
|
||||
const refreshClipboardAvailability = useCallback(async () => {
|
||||
if (!isMobile) {
|
||||
if (!showVirtualKeyBar) {
|
||||
setHasClipboardText(false);
|
||||
return;
|
||||
}
|
||||
@@ -302,7 +330,7 @@ export function TerminalPane({
|
||||
} catch {
|
||||
setHasClipboardText(false);
|
||||
}
|
||||
}, [isMobile]);
|
||||
}, [showVirtualKeyBar]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshClipboardAvailability();
|
||||
@@ -436,7 +464,7 @@ export function TerminalPane({
|
||||
|
||||
const handleKeyboardChange = useCallback(
|
||||
(nextShift: number) => {
|
||||
setKeyboardInset(isMobile ? nextShift : 0);
|
||||
setKeyboardInset(isNative || isMobile ? nextShift : 0);
|
||||
setIsKeyboardVisible(nextShift > 0);
|
||||
pulseKeyboardRefits();
|
||||
},
|
||||
@@ -955,6 +983,11 @@ export function TerminalPane({
|
||||
[keyboardPaddingStyle],
|
||||
);
|
||||
|
||||
const handleKeyBarLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
const width = event.nativeEvent.layout.width;
|
||||
if (width > 0) setKeyBarWidth(width);
|
||||
}, []);
|
||||
|
||||
const handleSwipeRight = useCallback(() => {
|
||||
if (!swipeGesturesEnabled) return;
|
||||
emulatorRef.current?.blur();
|
||||
@@ -1099,10 +1132,14 @@ export function TerminalPane({
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{isMobile ? (
|
||||
{showVirtualKeyBar ? (
|
||||
<View style={styles.keyboardContainer} testID="terminal-virtual-keyboard">
|
||||
<View style={styles.keyboardRows}>
|
||||
{TERMINAL_VIRTUAL_KEYBOARD_ROWS.map((row) => (
|
||||
<View
|
||||
style={styles.keyboardRows}
|
||||
onLayout={handleKeyBarLayout}
|
||||
testID={isNative && !isMobile ? "terminal-virtual-keyboard-native-wide" : undefined}
|
||||
>
|
||||
{virtualKeyboardRows.map((row) => (
|
||||
<View
|
||||
key={row.map(getTerminalVirtualKeyboardControlId).join(":")}
|
||||
style={styles.keyboardRow}
|
||||
@@ -1165,6 +1202,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
},
|
||||
keyboardRows: {
|
||||
gap: theme.spacing[1],
|
||||
width: "100%",
|
||||
maxWidth: TERMINAL_KEY_BAR_MAX_WIDTH,
|
||||
alignSelf: "center",
|
||||
},
|
||||
keyboardRow: {
|
||||
flexDirection: "row",
|
||||
|
||||
@@ -2,9 +2,13 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getTerminalVirtualKeyboardControlId,
|
||||
resolveTerminalVirtualKeyboardRows,
|
||||
shouldShowTerminalVirtualKeyBar,
|
||||
shouldShowTerminalFloatingCopyAction,
|
||||
shouldShowTerminalPasteAction,
|
||||
TERMINAL_VIRTUAL_KEYBOARD_ROWS,
|
||||
TERMINAL_VIRTUAL_KEYBOARD_THREE_COLUMN_ROWS,
|
||||
TERMINAL_VIRTUAL_KEYBOARD_WIDE_ROWS,
|
||||
type TerminalVirtualKeyboardControl,
|
||||
} from "./terminal-virtual-keyboard";
|
||||
|
||||
@@ -41,6 +45,38 @@ function controlsByType(
|
||||
}
|
||||
|
||||
describe("terminal virtual keyboard policy", () => {
|
||||
it("keeps terminal controls on wide native touch screens with either IME state", () => {
|
||||
expect(shouldShowTerminalVirtualKeyBar({ isNative: true, isCompact: false })).toBe(true);
|
||||
expect(shouldShowTerminalVirtualKeyBar({ isNative: true, isCompact: true })).toBe(true);
|
||||
expect(shouldShowTerminalVirtualKeyBar({ isNative: false, isCompact: true })).toBe(true);
|
||||
expect(shouldShowTerminalVirtualKeyBar({ isNative: false, isCompact: false })).toBe(false);
|
||||
});
|
||||
|
||||
it("selects the same controls in narrow and wide row arrangements", () => {
|
||||
const ids = (rows: readonly (readonly TerminalVirtualKeyboardControl[])[]) =>
|
||||
rows.flatMap((row) => row.map(getTerminalVirtualKeyboardControlId));
|
||||
expect([...ids(TERMINAL_VIRTUAL_KEYBOARD_WIDE_ROWS)].sort()).toEqual(
|
||||
[...ids(TERMINAL_VIRTUAL_KEYBOARD_ROWS)].sort(),
|
||||
);
|
||||
expect([...ids(TERMINAL_VIRTUAL_KEYBOARD_THREE_COLUMN_ROWS)].sort()).toEqual(
|
||||
[...ids(TERMINAL_VIRTUAL_KEYBOARD_ROWS)].sort(),
|
||||
);
|
||||
expect(resolveTerminalVirtualKeyboardRows({ isCompact: false, availableWidth: 0 })).toBe(
|
||||
TERMINAL_VIRTUAL_KEYBOARD_ROWS,
|
||||
);
|
||||
expect(resolveTerminalVirtualKeyboardRows({ isCompact: false, availableWidth: 515 })).toBe(
|
||||
TERMINAL_VIRTUAL_KEYBOARD_ROWS,
|
||||
);
|
||||
expect(resolveTerminalVirtualKeyboardRows({ isCompact: false, availableWidth: 160 })).toBe(
|
||||
TERMINAL_VIRTUAL_KEYBOARD_THREE_COLUMN_ROWS,
|
||||
);
|
||||
expect(resolveTerminalVirtualKeyboardRows({ isCompact: false, availableWidth: 698 })).toBe(
|
||||
TERMINAL_VIRTUAL_KEYBOARD_WIDE_ROWS,
|
||||
);
|
||||
expect(resolveTerminalVirtualKeyboardRows({ isCompact: true, availableWidth: 700 })).toBe(
|
||||
TERMINAL_VIRTUAL_KEYBOARD_ROWS,
|
||||
);
|
||||
});
|
||||
it("does not expose redundant Space or Backspace controls", () => {
|
||||
expect(controlIds()).toEqual([
|
||||
"terminal-key-esc",
|
||||
|
||||
@@ -41,6 +41,68 @@ export const TERMINAL_VIRTUAL_KEYBOARD_ROWS = [
|
||||
],
|
||||
] as const satisfies readonly (readonly TerminalVirtualKeyboardControl[])[];
|
||||
|
||||
export const TERMINAL_VIRTUAL_KEYBOARD_WIDE_ROWS = [
|
||||
[
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.esc },
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.tab },
|
||||
{ type: "modifier", modifier: "ctrl" },
|
||||
{ type: "modifier", modifier: "alt" },
|
||||
{ type: "modifier", modifier: "shift" },
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.left },
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.down },
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.up },
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.right },
|
||||
{ type: "paste" },
|
||||
{ type: "keyboardToggle" },
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.enter },
|
||||
],
|
||||
] as const satisfies readonly (readonly TerminalVirtualKeyboardControl[])[];
|
||||
|
||||
export const TERMINAL_VIRTUAL_KEYBOARD_THREE_COLUMN_ROWS = [
|
||||
[
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.esc },
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.tab },
|
||||
{ type: "modifier", modifier: "ctrl" },
|
||||
],
|
||||
[
|
||||
{ type: "modifier", modifier: "alt" },
|
||||
{ type: "modifier", modifier: "shift" },
|
||||
{ type: "keyboardToggle" },
|
||||
],
|
||||
[
|
||||
{ type: "paste" },
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.up },
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.enter },
|
||||
],
|
||||
[
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.left },
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.down },
|
||||
{ type: "key", button: TERMINAL_VIRTUAL_KEY_BUTTONS.right },
|
||||
],
|
||||
] as const satisfies readonly (readonly TerminalVirtualKeyboardControl[])[];
|
||||
|
||||
const TERMINAL_KEY_BAR_SINGLE_ROW_MIN_WIDTH = 660;
|
||||
const TERMINAL_KEY_BAR_THREE_COLUMN_MAX_WIDTH = 300;
|
||||
|
||||
export function resolveTerminalVirtualKeyboardRows(input: {
|
||||
isCompact: boolean;
|
||||
availableWidth: number;
|
||||
}) {
|
||||
if (input.availableWidth > 0 && input.availableWidth < TERMINAL_KEY_BAR_THREE_COLUMN_MAX_WIDTH) {
|
||||
return TERMINAL_VIRTUAL_KEYBOARD_THREE_COLUMN_ROWS;
|
||||
}
|
||||
return !input.isCompact && input.availableWidth >= TERMINAL_KEY_BAR_SINGLE_ROW_MIN_WIDTH
|
||||
? TERMINAL_VIRTUAL_KEYBOARD_WIDE_ROWS
|
||||
: TERMINAL_VIRTUAL_KEYBOARD_ROWS;
|
||||
}
|
||||
|
||||
export function shouldShowTerminalVirtualKeyBar(input: {
|
||||
isNative: boolean;
|
||||
isCompact: boolean;
|
||||
}): boolean {
|
||||
return input.isNative || input.isCompact;
|
||||
}
|
||||
|
||||
export function getTerminalVirtualKeyboardControlId(
|
||||
control: TerminalVirtualKeyboardControl,
|
||||
): string {
|
||||
|
||||
Reference in New Issue
Block a user