mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ef75c0914 | ||
|
|
a9ea3c76a3 | ||
|
|
e0463ad17f | ||
|
|
04a985bf23 |
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.96 - 2026-06-13
|
||||
|
||||
_This release only fixes an Android issue — desktop users don't need to update._
|
||||
|
||||
### Fixed
|
||||
|
||||
- On Android, the sidebar no longer reappears and gets stuck after you open a chat
|
||||
|
||||
## 0.1.95 - 2026-06-13
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-xOrVsNfFlrlBetLE1dpRsAiRwgHiLhw3EB3EqpHDTuw=
|
||||
sha256-Z5t4Ds05M88Dab1xp4sd2Jwd/NeyQNiVJw3nQQoW6Lw=
|
||||
|
||||
42
package-lock.json
generated
42
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paseo",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -37074,7 +37074,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
@@ -37405,12 +37405,12 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/client": "0.1.95",
|
||||
"@getpaseo/protocol": "0.1.95",
|
||||
"@getpaseo/server": "0.1.95",
|
||||
"@getpaseo/client": "0.1.96",
|
||||
"@getpaseo/protocol": "0.1.96",
|
||||
"@getpaseo/server": "0.1.96",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
@@ -37656,10 +37656,10 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@getpaseo/client",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.1.95",
|
||||
"@getpaseo/relay": "0.1.95",
|
||||
"@getpaseo/protocol": "0.1.96",
|
||||
"@getpaseo/relay": "0.1.96",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -37679,7 +37679,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@getpaseo/cli": "*",
|
||||
@@ -37931,7 +37931,7 @@
|
||||
},
|
||||
"packages/expo-two-way-audio": {
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.25",
|
||||
@@ -37967,7 +37967,7 @@
|
||||
},
|
||||
"packages/highlight": {
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.12.3",
|
||||
"@codemirror/legacy-modes": "^6.5.3",
|
||||
@@ -38198,7 +38198,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"dependencies": {
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
@@ -38219,7 +38219,7 @@
|
||||
},
|
||||
"packages/relay": {
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
@@ -38437,14 +38437,14 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.133",
|
||||
"@getpaseo/client": "0.1.95",
|
||||
"@getpaseo/highlight": "0.1.95",
|
||||
"@getpaseo/protocol": "0.1.95",
|
||||
"@getpaseo/relay": "0.1.95",
|
||||
"@getpaseo/client": "0.1.96",
|
||||
"@getpaseo/highlight": "0.1.96",
|
||||
"@getpaseo/protocol": "0.1.96",
|
||||
"@getpaseo/relay": "0.1.96",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
@@ -39216,7 +39216,7 @@
|
||||
},
|
||||
"packages/website": {
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.29.1",
|
||||
"@cloudflare/workers-types": "^4.20260317.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"private": true,
|
||||
"description": "Paseo: voice-controlled development environment with OpenAI Realtime API",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -511,6 +511,7 @@ function MobileGestureWrapper({
|
||||
windowWidth,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
setOverlayPeek,
|
||||
isGesturing,
|
||||
mobilePanelState,
|
||||
gestureAnimatingRef,
|
||||
@@ -579,6 +580,8 @@ function MobileGestureWrapper({
|
||||
})
|
||||
.onStart(() => {
|
||||
isGesturing.value = true;
|
||||
// The overlay is display:none while closed; reveal it for the drag.
|
||||
runOnJS(setOverlayPeek)(true);
|
||||
})
|
||||
.onUpdate((event) => {
|
||||
const newTranslateX = Math.min(0, -windowWidth + event.translationX);
|
||||
@@ -602,6 +605,7 @@ function MobileGestureWrapper({
|
||||
})
|
||||
.onFinalize(() => {
|
||||
isGesturing.value = false;
|
||||
runOnJS(setOverlayPeek)(false);
|
||||
}),
|
||||
[
|
||||
openGestureEnabled,
|
||||
@@ -611,6 +615,7 @@ function MobileGestureWrapper({
|
||||
mobilePanelState,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
setOverlayPeek,
|
||||
handleGestureOpen,
|
||||
isGesturing,
|
||||
openGestureRef,
|
||||
|
||||
@@ -97,7 +97,7 @@ export function ExplorerSidebar({
|
||||
windowWidth,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
settledGeneration,
|
||||
overlayVisible,
|
||||
isGesturing,
|
||||
gestureAnimatingRef,
|
||||
closeGestureRef,
|
||||
@@ -260,25 +260,13 @@ export function ExplorerSidebar({
|
||||
[isMobile, explorerWidth, resizeWidth, setExplorerWidth, viewportWidth],
|
||||
);
|
||||
|
||||
// settledGeneration as deps: after each settle the updater is rebuilt so the
|
||||
// shared values are re-applied to the view, protecting against a heavy Fabric
|
||||
// commit reverting the transform to stale React-committed props (#9635).
|
||||
const sidebarAnimatedStyle = useAnimatedStyle(
|
||||
() => ({
|
||||
transform: [{ translateX: translateX.value }],
|
||||
}),
|
||||
[settledGeneration],
|
||||
);
|
||||
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateX: translateX.value }],
|
||||
}));
|
||||
|
||||
// pointerEvents comes from React state, not the worklet: the Fabric revert
|
||||
// protection above does not cover pointerEvents, so a worklet-driven value
|
||||
// can wedge an invisible tap-eating backdrop after a heavy commit.
|
||||
const backdropAnimatedStyle = useAnimatedStyle(
|
||||
() => ({
|
||||
opacity: backdropOpacity.value,
|
||||
}),
|
||||
[settledGeneration],
|
||||
);
|
||||
const backdropAnimatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: backdropOpacity.value,
|
||||
}));
|
||||
|
||||
const resizeAnimatedStyle = useAnimatedStyle(() => ({
|
||||
width: resizeWidth.value,
|
||||
@@ -288,6 +276,9 @@ export function ExplorerSidebar({
|
||||
() => [
|
||||
explorerStaticStyles.backdrop,
|
||||
backdropAnimatedStyle,
|
||||
// pointerEvents is React-owned, not worklet-owned: Reanimated never
|
||||
// touches it, so a stale animated-prop revert can't wedge an invisible
|
||||
// tap-eating backdrop.
|
||||
{ pointerEvents: isOpen ? ("auto" as const) : ("none" as const) },
|
||||
],
|
||||
[backdropAnimatedStyle, isOpen],
|
||||
@@ -311,6 +302,16 @@ export function ExplorerSidebar({
|
||||
mobileKeyboardInsetStyle,
|
||||
],
|
||||
);
|
||||
// display is React-owned on the plain wrapper View (no animated styles), so
|
||||
// a hidden overlay stays hidden no matter what Reanimated's Fabric overlay
|
||||
// reverts the panel transform to after a heavy commit (reanimated#9635).
|
||||
const overlayStyle = useMemo(
|
||||
() => [
|
||||
StyleSheet.absoluteFillObject,
|
||||
{ display: overlayVisible ? ("flex" as const) : ("none" as const) },
|
||||
],
|
||||
[overlayVisible],
|
||||
);
|
||||
const desktopSidebarStyle = useMemo(
|
||||
() => [explorerStaticStyles.desktopSidebar, resizeAnimatedStyle, { paddingTop: insets.top }],
|
||||
[resizeAnimatedStyle, insets.top],
|
||||
@@ -331,7 +332,7 @@ export function ExplorerSidebar({
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<View style={StyleSheet.absoluteFillObject} pointerEvents={overlayPointerEvents}>
|
||||
<View style={overlayStyle} pointerEvents={overlayPointerEvents}>
|
||||
{/* Backdrop */}
|
||||
<Animated.View style={backdropCombinedStyle} />
|
||||
|
||||
|
||||
@@ -41,10 +41,7 @@ import { Shortcut } from "@/components/ui/shortcut";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import {
|
||||
useSidebarAnimation,
|
||||
useSidebarSettledGeneration,
|
||||
} from "@/contexts/sidebar-animation-context";
|
||||
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
|
||||
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
|
||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||
import { useSidebarShortcutModel } from "@/hooks/use-sidebar-shortcut-model";
|
||||
@@ -618,12 +615,12 @@ function MobileSidebar({
|
||||
windowWidth,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
overlayVisible,
|
||||
isGesturing,
|
||||
mobilePanelState,
|
||||
gestureAnimatingRef,
|
||||
closeGestureRef,
|
||||
} = useSidebarAnimation();
|
||||
const settledGeneration = useSidebarSettledGeneration();
|
||||
const closeTouchStartX = useSharedValue(0);
|
||||
const closeTouchStartY = useSharedValue(0);
|
||||
|
||||
@@ -752,25 +749,13 @@ function MobileSidebar({
|
||||
[activeHostStatusColor],
|
||||
);
|
||||
|
||||
// settledGeneration as deps: after each settle the updater is rebuilt so the
|
||||
// shared values are re-applied to the view, protecting against a heavy Fabric
|
||||
// commit reverting the transform to stale React-committed props (#9635).
|
||||
const sidebarAnimatedStyle = useAnimatedStyle(
|
||||
() => ({
|
||||
transform: [{ translateX: translateX.value }],
|
||||
}),
|
||||
[settledGeneration],
|
||||
);
|
||||
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ translateX: translateX.value }],
|
||||
}));
|
||||
|
||||
// pointerEvents comes from React state, not the worklet: the Fabric revert
|
||||
// protection above does not cover pointerEvents, so a worklet-driven value
|
||||
// can wedge an invisible tap-eating backdrop after a heavy commit.
|
||||
const backdropAnimatedStyle = useAnimatedStyle(
|
||||
() => ({
|
||||
opacity: backdropOpacity.value,
|
||||
}),
|
||||
[settledGeneration],
|
||||
);
|
||||
const backdropAnimatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: backdropOpacity.value,
|
||||
}));
|
||||
|
||||
let overlayPointerEvents: "auto" | "none" | "box-none";
|
||||
if (!isWeb) overlayPointerEvents = "box-none";
|
||||
@@ -781,6 +766,9 @@ function MobileSidebar({
|
||||
() => [
|
||||
staticStyles.backdrop,
|
||||
backdropAnimatedStyle,
|
||||
// pointerEvents is React-owned, not worklet-owned: Reanimated never
|
||||
// touches it, so a stale animated-prop revert can't wedge an invisible
|
||||
// tap-eating backdrop.
|
||||
{ pointerEvents: isOpen ? ("auto" as const) : ("none" as const) },
|
||||
],
|
||||
[backdropAnimatedStyle, isOpen],
|
||||
@@ -794,9 +782,19 @@ function MobileSidebar({
|
||||
],
|
||||
[mobileSidebarInsetStyle, sidebarAnimatedStyle, theme.colors.surfaceSidebar],
|
||||
);
|
||||
// display is React-owned on the plain wrapper View (no animated styles), so
|
||||
// a hidden overlay stays hidden no matter what Reanimated's Fabric overlay
|
||||
// reverts the panel transform to after a heavy commit (reanimated#9635).
|
||||
const overlayStyle = useMemo(
|
||||
() => [
|
||||
StyleSheet.absoluteFillObject,
|
||||
{ display: overlayVisible ? ("flex" as const) : ("none" as const) },
|
||||
],
|
||||
[overlayVisible],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={StyleSheet.absoluteFillObject} pointerEvents={overlayPointerEvents}>
|
||||
<View style={overlayStyle} pointerEvents={overlayPointerEvents}>
|
||||
<Animated.View style={backdropStyle} />
|
||||
|
||||
<GestureDetector gesture={closeGesture} touchAction="pan-y">
|
||||
|
||||
@@ -9,13 +9,7 @@ import {
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { useWindowDimensions } from "react-native";
|
||||
import {
|
||||
runOnJS,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
Easing,
|
||||
type SharedValue,
|
||||
} from "react-native-reanimated";
|
||||
import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated";
|
||||
import { type GestureType } from "react-native-gesture-handler";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
|
||||
@@ -27,13 +21,17 @@ import {
|
||||
|
||||
const ANIMATION_DURATION = 220;
|
||||
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
|
||||
// Keeps the overlay displayed long enough for the close animation to finish
|
||||
// before React hides it.
|
||||
const OVERLAY_CLOSE_GRACE_MS = 300;
|
||||
interface ExplorerSidebarAnimationContextValue {
|
||||
translateX: SharedValue<number>;
|
||||
backdropOpacity: SharedValue<number>;
|
||||
windowWidth: number;
|
||||
animateToOpen: () => void;
|
||||
animateToClose: () => void;
|
||||
settledGeneration: number;
|
||||
overlayVisible: boolean;
|
||||
setOverlayPeek: (peek: boolean) => void;
|
||||
isGesturing: SharedValue<boolean>;
|
||||
gestureAnimatingRef: React.MutableRefObject<boolean>;
|
||||
openGestureRef: React.MutableRefObject<GestureType | undefined>;
|
||||
@@ -62,12 +60,21 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
|
||||
const openGestureRef = useRef<GestureType | undefined>(undefined);
|
||||
const closeGestureRef = useRef<GestureType | undefined>(undefined);
|
||||
|
||||
// Same Fabric stale-props revert protection as in sidebar-animation-context:
|
||||
// bump after every settle so consumers re-commit the settled shared values.
|
||||
const [settledGeneration, setSettledGeneration] = useState(0);
|
||||
const bumpSettledGeneration = useCallback(() => {
|
||||
setSettledGeneration((generation) => generation + 1);
|
||||
}, []);
|
||||
// React owns whether the overlay is displayed at all; the worklet owns its
|
||||
// motion. See the matching block in sidebar-animation-context.tsx for why
|
||||
// (Fabric re-applies stale animated props over committed props,
|
||||
// reanimated#9635).
|
||||
const [overlayPeek, setOverlayPeek] = useState(false);
|
||||
const overlayTarget = isOpen || overlayPeek;
|
||||
const [overlayVisible, setOverlayVisible] = useState(overlayTarget);
|
||||
useEffect(() => {
|
||||
if (overlayTarget) {
|
||||
setOverlayVisible(true);
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => setOverlayVisible(false), OVERLAY_CLOSE_GRACE_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [overlayTarget]);
|
||||
|
||||
// Track previous isOpen to detect changes
|
||||
const prevIsOpen = useRef(isOpen);
|
||||
@@ -123,7 +130,6 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
|
||||
if (isCompactLayout) {
|
||||
settleMobilePanel("file-explorer");
|
||||
}
|
||||
runOnJS(bumpSettledGeneration)();
|
||||
},
|
||||
);
|
||||
backdropOpacity.value = withTiming(targets.backdropOpacity, {
|
||||
@@ -147,7 +153,6 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
|
||||
if (isCompactLayout && mobileView === "agent") {
|
||||
settleMobilePanel("agent");
|
||||
}
|
||||
runOnJS(bumpSettledGeneration)();
|
||||
},
|
||||
);
|
||||
backdropOpacity.value = withTiming(targets.backdropOpacity, {
|
||||
@@ -162,7 +167,6 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
|
||||
if (isCompactLayout && ownsMobileViewChange) {
|
||||
settleMobilePanel(mobileView);
|
||||
}
|
||||
bumpSettledGeneration();
|
||||
}, [
|
||||
isOpen,
|
||||
mobileView,
|
||||
@@ -173,7 +177,6 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
|
||||
isCompactLayout,
|
||||
startMobilePanelTransition,
|
||||
settleMobilePanel,
|
||||
bumpSettledGeneration,
|
||||
]);
|
||||
|
||||
const animateToOpen = useCallback(() => {
|
||||
@@ -188,20 +191,13 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
|
||||
(finished) => {
|
||||
if (!finished) return;
|
||||
settleMobilePanel("file-explorer");
|
||||
runOnJS(bumpSettledGeneration)();
|
||||
},
|
||||
);
|
||||
backdropOpacity.value = withTiming(1, {
|
||||
duration: ANIMATION_DURATION,
|
||||
easing: ANIMATION_EASING,
|
||||
});
|
||||
}, [
|
||||
translateX,
|
||||
backdropOpacity,
|
||||
startMobilePanelTransition,
|
||||
settleMobilePanel,
|
||||
bumpSettledGeneration,
|
||||
]);
|
||||
}, [translateX, backdropOpacity, startMobilePanelTransition, settleMobilePanel]);
|
||||
|
||||
const animateToClose = useCallback(() => {
|
||||
"worklet";
|
||||
@@ -215,21 +211,13 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
|
||||
(finished) => {
|
||||
if (!finished) return;
|
||||
settleMobilePanel("agent");
|
||||
runOnJS(bumpSettledGeneration)();
|
||||
},
|
||||
);
|
||||
backdropOpacity.value = withTiming(0, {
|
||||
duration: ANIMATION_DURATION,
|
||||
easing: ANIMATION_EASING,
|
||||
});
|
||||
}, [
|
||||
translateX,
|
||||
backdropOpacity,
|
||||
windowWidth,
|
||||
startMobilePanelTransition,
|
||||
settleMobilePanel,
|
||||
bumpSettledGeneration,
|
||||
]);
|
||||
}, [translateX, backdropOpacity, windowWidth, startMobilePanelTransition, settleMobilePanel]);
|
||||
|
||||
const value = useMemo<ExplorerSidebarAnimationContextValue>(
|
||||
() => ({
|
||||
@@ -238,7 +226,8 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
|
||||
windowWidth,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
settledGeneration,
|
||||
overlayVisible,
|
||||
setOverlayPeek,
|
||||
isGesturing,
|
||||
gestureAnimatingRef,
|
||||
openGestureRef,
|
||||
@@ -250,7 +239,7 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
|
||||
windowWidth,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
settledGeneration,
|
||||
overlayVisible,
|
||||
isGesturing,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -9,13 +9,7 @@ import {
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { Keyboard, useWindowDimensions } from "react-native";
|
||||
import {
|
||||
runOnJS,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
Easing,
|
||||
type SharedValue,
|
||||
} from "react-native-reanimated";
|
||||
import { useSharedValue, withTiming, Easing, type SharedValue } from "react-native-reanimated";
|
||||
import { type GestureType } from "react-native-gesture-handler";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { isNative } from "@/constants/platform";
|
||||
@@ -38,6 +32,9 @@ import {
|
||||
|
||||
const ANIMATION_DURATION = 220;
|
||||
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
|
||||
// Keeps the overlay displayed long enough for the close animation to finish
|
||||
// before React hides it.
|
||||
const OVERLAY_CLOSE_GRACE_MS = 300;
|
||||
export const MOBILE_VISUAL_PANEL_AGENT = 0;
|
||||
export const MOBILE_VISUAL_PANEL_AGENT_LIST = 1;
|
||||
export const MOBILE_VISUAL_PANEL_FILE_EXPLORER = 2;
|
||||
@@ -50,6 +47,8 @@ interface SidebarAnimationContextValue {
|
||||
animateToClose: () => void;
|
||||
startMobilePanelTransition: (mobileView: "agent" | "agent-list" | "file-explorer") => void;
|
||||
settleMobilePanel: (mobileView: "agent" | "agent-list" | "file-explorer") => void;
|
||||
overlayVisible: boolean;
|
||||
setOverlayPeek: (peek: boolean) => void;
|
||||
isGesturing: SharedValue<boolean>;
|
||||
mobileVisualPanel: SharedValue<number>;
|
||||
mobilePanelState: SharedValue<number>;
|
||||
@@ -60,10 +59,6 @@ interface SidebarAnimationContextValue {
|
||||
|
||||
const SidebarAnimationContext = createContext<SidebarAnimationContextValue | null>(null);
|
||||
|
||||
// Separate context so that settle-driven re-renders only reach MobileSidebar,
|
||||
// not every other useSidebarAnimation() consumer.
|
||||
const SidebarSettledGenerationContext = createContext<number>(0);
|
||||
|
||||
function getMobileVisualPanel(mobileView: "agent" | "agent-list" | "file-explorer"): number {
|
||||
if (mobileView === "agent-list") {
|
||||
return MOBILE_VISUAL_PANEL_AGENT_LIST;
|
||||
@@ -104,16 +99,27 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
||||
const openGestureRef = useRef<GestureType | undefined>(undefined);
|
||||
const closeGestureRef = useRef<GestureType | undefined>(undefined);
|
||||
|
||||
// After an open/close settles, a heavy Fabric commit can re-apply React's
|
||||
// stale committed props onto the native view, reverting the UI-thread
|
||||
// transform (reanimated#9635 — sidebar reappears "ghost-open"). Bumping this
|
||||
// counter after every settle re-renders the consumers so the animated styles
|
||||
// refresh React's committed props from the settled shared values. It must
|
||||
// never write a shared value — it only triggers a React re-commit.
|
||||
const [settledGeneration, setSettledGeneration] = useState(0);
|
||||
const bumpSettledGeneration = useCallback(() => {
|
||||
setSettledGeneration((generation) => generation + 1);
|
||||
}, []);
|
||||
// React owns whether the overlay is displayed at all; the worklet owns its
|
||||
// motion. On Fabric, Reanimated re-applies its own (possibly stale) animated
|
||||
// props over React's committed props on every commit, so a settled-closed
|
||||
// overlay can ghost back on screen after a heavy commit (reanimated#9635) —
|
||||
// no committed transform/opacity value can prevent that. display lives on
|
||||
// the plain wrapper View that Reanimated never touches, so React's value is
|
||||
// authoritative: a hidden overlay stays hidden no matter what the animated
|
||||
// props revert to. overlayPeek shows the overlay during an open gesture
|
||||
// before the store flips; the close grace keeps it displayed while the
|
||||
// close animation plays.
|
||||
const [overlayPeek, setOverlayPeek] = useState(false);
|
||||
const overlayTarget = isOpen || overlayPeek;
|
||||
const [overlayVisible, setOverlayVisible] = useState(overlayTarget);
|
||||
useEffect(() => {
|
||||
if (overlayTarget) {
|
||||
setOverlayVisible(true);
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => setOverlayVisible(false), OVERLAY_CLOSE_GRACE_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [overlayTarget]);
|
||||
|
||||
// Track previous isOpen to detect changes
|
||||
const prevIsOpen = useRef(isOpen);
|
||||
@@ -241,7 +247,6 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
||||
if (isCompactLayout) {
|
||||
settleMobilePanel("agent-list");
|
||||
}
|
||||
runOnJS(bumpSettledGeneration)();
|
||||
},
|
||||
);
|
||||
backdropOpacity.value = withTiming(targets.backdropOpacity, {
|
||||
@@ -265,7 +270,6 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
||||
if (isCompactLayout && mobileView === "agent") {
|
||||
settleMobilePanel("agent");
|
||||
}
|
||||
runOnJS(bumpSettledGeneration)();
|
||||
},
|
||||
);
|
||||
backdropOpacity.value = withTiming(targets.backdropOpacity, {
|
||||
@@ -280,7 +284,6 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
||||
if (isCompactLayout && ownsMobileViewChange) {
|
||||
settleMobilePanel(mobileView);
|
||||
}
|
||||
bumpSettledGeneration();
|
||||
}, [
|
||||
isOpen,
|
||||
mobileView,
|
||||
@@ -293,7 +296,6 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
||||
mobilePanelState,
|
||||
startMobilePanelTransition,
|
||||
settleMobilePanel,
|
||||
bumpSettledGeneration,
|
||||
]);
|
||||
|
||||
const animateToOpen = useCallback(() => {
|
||||
@@ -308,20 +310,13 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
||||
(finished) => {
|
||||
if (!finished) return;
|
||||
settleMobilePanel("agent-list");
|
||||
runOnJS(bumpSettledGeneration)();
|
||||
},
|
||||
);
|
||||
backdropOpacity.value = withTiming(1, {
|
||||
duration: ANIMATION_DURATION,
|
||||
easing: ANIMATION_EASING,
|
||||
});
|
||||
}, [
|
||||
translateX,
|
||||
backdropOpacity,
|
||||
startMobilePanelTransition,
|
||||
settleMobilePanel,
|
||||
bumpSettledGeneration,
|
||||
]);
|
||||
}, [translateX, backdropOpacity, startMobilePanelTransition, settleMobilePanel]);
|
||||
|
||||
const animateToClose = useCallback(() => {
|
||||
"worklet";
|
||||
@@ -335,21 +330,13 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
||||
(finished) => {
|
||||
if (!finished) return;
|
||||
settleMobilePanel("agent");
|
||||
runOnJS(bumpSettledGeneration)();
|
||||
},
|
||||
);
|
||||
backdropOpacity.value = withTiming(0, {
|
||||
duration: ANIMATION_DURATION,
|
||||
easing: ANIMATION_EASING,
|
||||
});
|
||||
}, [
|
||||
translateX,
|
||||
backdropOpacity,
|
||||
windowWidth,
|
||||
startMobilePanelTransition,
|
||||
settleMobilePanel,
|
||||
bumpSettledGeneration,
|
||||
]);
|
||||
}, [translateX, backdropOpacity, windowWidth, startMobilePanelTransition, settleMobilePanel]);
|
||||
|
||||
const value = useMemo<SidebarAnimationContextValue>(
|
||||
() => ({
|
||||
@@ -360,6 +347,8 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
||||
animateToClose,
|
||||
startMobilePanelTransition,
|
||||
settleMobilePanel,
|
||||
overlayVisible,
|
||||
setOverlayPeek,
|
||||
isGesturing,
|
||||
mobileVisualPanel,
|
||||
mobilePanelState,
|
||||
@@ -375,6 +364,7 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
||||
animateToClose,
|
||||
startMobilePanelTransition,
|
||||
settleMobilePanel,
|
||||
overlayVisible,
|
||||
isGesturing,
|
||||
mobileVisualPanel,
|
||||
mobilePanelState,
|
||||
@@ -385,11 +375,7 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarAnimationContext.Provider value={value}>
|
||||
<SidebarSettledGenerationContext.Provider value={settledGeneration}>
|
||||
{children}
|
||||
</SidebarSettledGenerationContext.Provider>
|
||||
</SidebarAnimationContext.Provider>
|
||||
<SidebarAnimationContext.Provider value={value}>{children}</SidebarAnimationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -400,7 +386,3 @@ export function useSidebarAnimation() {
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useSidebarSettledGeneration() {
|
||||
return useContext(SidebarSettledGenerationContext);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export function useExplorerOpenGesture({ enabled, onOpen }: UseExplorerOpenGestu
|
||||
windowWidth,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
setOverlayPeek,
|
||||
isGesturing,
|
||||
gestureAnimatingRef,
|
||||
openGestureRef,
|
||||
@@ -93,6 +94,8 @@ export function useExplorerOpenGesture({ enabled, onOpen }: UseExplorerOpenGestu
|
||||
})
|
||||
.onStart(() => {
|
||||
isGesturing.value = true;
|
||||
// The overlay is display:none while closed; reveal it for the drag.
|
||||
runOnJS(setOverlayPeek)(true);
|
||||
})
|
||||
.onUpdate((event) => {
|
||||
// Right sidebar: start from closed position (+windowWidth) and move towards 0.
|
||||
@@ -122,6 +125,7 @@ export function useExplorerOpenGesture({ enabled, onOpen }: UseExplorerOpenGestu
|
||||
})
|
||||
.onFinalize(() => {
|
||||
isGesturing.value = false;
|
||||
runOnJS(setOverlayPeek)(false);
|
||||
}),
|
||||
[
|
||||
enabled,
|
||||
@@ -131,6 +135,7 @@ export function useExplorerOpenGesture({ enabled, onOpen }: UseExplorerOpenGestu
|
||||
mobilePanelState,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
setOverlayPeek,
|
||||
isGesturing,
|
||||
openGestureRef,
|
||||
leftOpenGestureRef,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"description": "Paseo CLI - control your AI coding agents from the command line",
|
||||
"bin": {
|
||||
"paseo": "bin/paseo"
|
||||
@@ -27,9 +27,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/client": "0.1.95",
|
||||
"@getpaseo/protocol": "0.1.95",
|
||||
"@getpaseo/server": "0.1.95",
|
||||
"@getpaseo/client": "0.1.96",
|
||||
"@getpaseo/protocol": "0.1.96",
|
||||
"@getpaseo/server": "0.1.96",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/client",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"description": "Paseo client SDK package",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -35,8 +35,8 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.1.95",
|
||||
"@getpaseo/relay": "0.1.95",
|
||||
"@getpaseo/protocol": "0.1.96",
|
||||
"@getpaseo/relay": "0.1.96",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"private": true,
|
||||
"description": "Paseo desktop app (Electron wrapper)",
|
||||
"homepage": "https://paseo.sh",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"description": "Native module for two way audio streaming",
|
||||
"keywords": [
|
||||
"ExpoTwoWayAudio",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"description": "Paseo shared protocol schemas and wire types",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"description": "Paseo relay for bridging daemon and client connections",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"description": "Paseo backend server",
|
||||
"files": [
|
||||
"dist/server",
|
||||
@@ -59,10 +59,10 @@
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.133",
|
||||
"@getpaseo/client": "0.1.95",
|
||||
"@getpaseo/highlight": "0.1.95",
|
||||
"@getpaseo/protocol": "0.1.95",
|
||||
"@getpaseo/relay": "0.1.95",
|
||||
"@getpaseo/client": "0.1.96",
|
||||
"@getpaseo/highlight": "0.1.96",
|
||||
"@getpaseo/protocol": "0.1.96",
|
||||
"@getpaseo/relay": "0.1.96",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.1.95",
|
||||
"version": "0.1.96",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user