Merge remote-tracking branch 'origin/main' into dev
# Conflicts: # nix/package.nix
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.42 - 2026-04-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed Claude Code failing to launch on Windows when installed to a path with spaces (e.g. `C:\Program Files\...`).
|
||||||
|
|
||||||
## 0.1.41 - 2026-04-01
|
## 0.1.41 - 2026-04-01
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ buildNpmPackage rec {
|
|||||||
|
|
||||||
# To update: run `nix build` with lib.fakeHash, copy the `got:` hash.
|
# To update: run `nix build` with lib.fakeHash, copy the `got:` hash.
|
||||||
# CI auto-updates this when package-lock.json changes (see .github/workflows/).
|
# CI auto-updates this when package-lock.json changes (see .github/workflows/).
|
||||||
npmDepsHash = "sha256-r9y8rUyT/56wHFUp8D/yA7mjy715jjezSYaEuj1D4TQ=";
|
npmDepsHash = "sha256-UtE4rf5CbPCtp86swvZ0IbU+DhGxZlEfDubAiWz2RKE=";
|
||||||
|
|
||||||
# Prevent onnxruntime-node's install script from running during automatic
|
# Prevent onnxruntime-node's install script from running during automatic
|
||||||
# npm rebuild (it tries to download from api.nuget.org, which fails in the sandbox).
|
# npm rebuild (it tries to download from api.nuget.org, which fails in the sandbox).
|
||||||
|
|||||||
Generated
+19
-19
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "paseo",
|
"name": "paseo",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "paseo",
|
"name": "paseo",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
@@ -36285,16 +36285,16 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@getpaseo/app",
|
"name": "@getpaseo/app",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@expo/vector-icons": "^15.0.2",
|
"@expo/vector-icons": "^15.0.2",
|
||||||
"@floating-ui/react-native": "^0.10.7",
|
"@floating-ui/react-native": "^0.10.7",
|
||||||
"@getpaseo/expo-two-way-audio": "0.1.41",
|
"@getpaseo/expo-two-way-audio": "0.1.43-rc.1",
|
||||||
"@getpaseo/highlight": "0.1.41",
|
"@getpaseo/highlight": "0.1.43-rc.1",
|
||||||
"@getpaseo/server": "0.1.41",
|
"@getpaseo/server": "0.1.43-rc.1",
|
||||||
"@gorhom/bottom-sheet": "^5.2.6",
|
"@gorhom/bottom-sheet": "^5.2.6",
|
||||||
"@gorhom/portal": "^1.0.14",
|
"@gorhom/portal": "^1.0.14",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
@@ -36411,11 +36411,11 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@getpaseo/cli",
|
"name": "@getpaseo/cli",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^1.0.0",
|
"@clack/prompts": "^1.0.0",
|
||||||
"@getpaseo/relay": "0.1.41",
|
"@getpaseo/relay": "0.1.43-rc.1",
|
||||||
"@getpaseo/server": "0.1.41",
|
"@getpaseo/server": "0.1.43-rc.1",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"commander": "^12.0.0",
|
"commander": "^12.0.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
@@ -36456,11 +36456,11 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@getpaseo/desktop",
|
"name": "@getpaseo/desktop",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getpaseo/cli": "0.1.41",
|
"@getpaseo/cli": "0.1.43-rc.1",
|
||||||
"@getpaseo/server": "0.1.41",
|
"@getpaseo/server": "0.1.43-rc.1",
|
||||||
"electron-log": "^5.4.3",
|
"electron-log": "^5.4.3",
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
"ws": "^8.14.2"
|
"ws": "^8.14.2"
|
||||||
@@ -36494,7 +36494,7 @@
|
|||||||
},
|
},
|
||||||
"packages/expo-two-way-audio": {
|
"packages/expo-two-way-audio": {
|
||||||
"name": "@getpaseo/expo-two-way-audio",
|
"name": "@getpaseo/expo-two-way-audio",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
@@ -36695,7 +36695,7 @@
|
|||||||
},
|
},
|
||||||
"packages/highlight": {
|
"packages/highlight": {
|
||||||
"name": "@getpaseo/highlight",
|
"name": "@getpaseo/highlight",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lezer/common": "^1.5.0",
|
"@lezer/common": "^1.5.0",
|
||||||
"@lezer/cpp": "^1.1.5",
|
"@lezer/cpp": "^1.1.5",
|
||||||
@@ -36721,7 +36721,7 @@
|
|||||||
},
|
},
|
||||||
"packages/relay": {
|
"packages/relay": {
|
||||||
"name": "@getpaseo/relay",
|
"name": "@getpaseo/relay",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
@@ -36737,13 +36737,13 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@getpaseo/server",
|
"name": "@getpaseo/server",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "2.0.52",
|
"@ai-sdk/openai": "2.0.52",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
||||||
"@deepgram/sdk": "^3.4.0",
|
"@deepgram/sdk": "^3.4.0",
|
||||||
"@getpaseo/highlight": "0.1.41",
|
"@getpaseo/highlight": "0.1.43-rc.1",
|
||||||
"@getpaseo/relay": "0.1.41",
|
"@getpaseo/relay": "0.1.43-rc.1",
|
||||||
"@isaacs/ttlcache": "^2.1.4",
|
"@isaacs/ttlcache": "^2.1.4",
|
||||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||||
"@opencode-ai/sdk": "1.2.6",
|
"@opencode-ai/sdk": "1.2.6",
|
||||||
@@ -37145,7 +37145,7 @@
|
|||||||
},
|
},
|
||||||
"packages/website": {
|
"packages/website": {
|
||||||
"name": "@getpaseo/website",
|
"name": "@getpaseo/website",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "^1.20.3",
|
"@cloudflare/vite-plugin": "^1.20.3",
|
||||||
"@cloudflare/workers-types": "^4.20260114.0",
|
"@cloudflare/workers-types": "^4.20260114.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paseo",
|
"name": "paseo",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/expo-two-way-audio",
|
"packages/expo-two-way-audio",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/app",
|
"name": "@getpaseo/app",
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
@@ -31,9 +31,9 @@
|
|||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@expo/vector-icons": "^15.0.2",
|
"@expo/vector-icons": "^15.0.2",
|
||||||
"@floating-ui/react-native": "^0.10.7",
|
"@floating-ui/react-native": "^0.10.7",
|
||||||
"@getpaseo/expo-two-way-audio": "0.1.41",
|
"@getpaseo/expo-two-way-audio": "0.1.43-rc.1",
|
||||||
"@getpaseo/highlight": "0.1.41",
|
"@getpaseo/highlight": "0.1.43-rc.1",
|
||||||
"@getpaseo/server": "0.1.41",
|
"@getpaseo/server": "0.1.43-rc.1",
|
||||||
"@gorhom/bottom-sheet": "^5.2.6",
|
"@gorhom/bottom-sheet": "^5.2.6",
|
||||||
"@gorhom/portal": "^1.0.14",
|
"@gorhom/portal": "^1.0.14",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
} from "@dnd-kit/sortable";
|
} from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import type { DraggableListProps, DraggableRenderItemInfo } from "./draggable-list.types";
|
import type { DraggableListProps, DraggableRenderItemInfo } from "./draggable-list.types";
|
||||||
import { WebDesktopScrollbarOverlay, useWebDesktopScrollbarMetrics } from "./web-desktop-scrollbar";
|
import { useWebScrollViewScrollbar } from "./use-web-scrollbar";
|
||||||
|
|
||||||
export type { DraggableListProps, DraggableRenderItemInfo };
|
export type { DraggableListProps, DraggableRenderItemInfo };
|
||||||
|
|
||||||
@@ -133,8 +133,11 @@ export function DraggableList<T>({
|
|||||||
const [activeId, setActiveId] = useState<string | null>(null);
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
const [dragItems, setDragItems] = useState<T[] | null>(null);
|
const [dragItems, setDragItems] = useState<T[] | null>(null);
|
||||||
const items = dragItems ?? data;
|
const items = dragItems ?? data;
|
||||||
|
const showCustomScrollbar = enableDesktopWebScrollbar && scrollEnabled;
|
||||||
const scrollViewRef = useRef<ScrollView>(null);
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
const scrollbarMetrics = useWebDesktopScrollbarMetrics();
|
const scrollbar = useWebScrollViewScrollbar(scrollViewRef, {
|
||||||
|
enabled: showCustomScrollbar,
|
||||||
|
});
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
@@ -177,7 +180,6 @@ export function DraggableList<T>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const ids = items.map((item, index) => keyExtractor(item, index));
|
const ids = items.map((item, index) => keyExtractor(item, index));
|
||||||
const showCustomScrollbar = enableDesktopWebScrollbar && scrollEnabled;
|
|
||||||
const wrapperStyle = [
|
const wrapperStyle = [
|
||||||
{ position: "relative" as const },
|
{ position: "relative" as const },
|
||||||
scrollEnabled ? { flex: 1, minHeight: 0 } : null,
|
scrollEnabled ? { flex: 1, minHeight: 0 } : null,
|
||||||
@@ -193,12 +195,10 @@ export function DraggableList<T>({
|
|||||||
style={style}
|
style={style}
|
||||||
contentContainerStyle={contentContainerStyle}
|
contentContainerStyle={contentContainerStyle}
|
||||||
showsVerticalScrollIndicator={showCustomScrollbar ? false : showsVerticalScrollIndicator}
|
showsVerticalScrollIndicator={showCustomScrollbar ? false : showsVerticalScrollIndicator}
|
||||||
onLayout={showCustomScrollbar ? scrollbarMetrics.onLayout : undefined}
|
onLayout={scrollbar.onLayout}
|
||||||
onContentSizeChange={
|
onContentSizeChange={scrollbar.onContentSizeChange}
|
||||||
showCustomScrollbar ? scrollbarMetrics.onContentSizeChange : undefined
|
onScroll={scrollbar.onScroll}
|
||||||
}
|
scrollEventThrottle={16}
|
||||||
onScroll={showCustomScrollbar ? scrollbarMetrics.onScroll : undefined}
|
|
||||||
scrollEventThrottle={showCustomScrollbar ? 16 : undefined}
|
|
||||||
>
|
>
|
||||||
{ListHeaderComponent}
|
{ListHeaderComponent}
|
||||||
{items.length === 0 && ListEmptyComponent}
|
{items.length === 0 && ListEmptyComponent}
|
||||||
@@ -259,13 +259,7 @@ export function DraggableList<T>({
|
|||||||
{ListFooterComponent}
|
{ListFooterComponent}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<WebDesktopScrollbarOverlay
|
{scrollbar.overlay}
|
||||||
enabled={showCustomScrollbar}
|
|
||||||
metrics={scrollbarMetrics}
|
|
||||||
onScrollToOffset={(nextOffset) => {
|
|
||||||
scrollViewRef.current?.scrollTo({ y: nextOffset, animated: false });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import {
|
|||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
FlatList,
|
FlatList,
|
||||||
ListRenderItemInfo,
|
ListRenderItemInfo,
|
||||||
type LayoutChangeEvent,
|
|
||||||
type NativeScrollEvent,
|
|
||||||
type NativeSyntheticEvent,
|
|
||||||
Pressable,
|
Pressable,
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
@@ -53,10 +50,7 @@ import { buildWorkspaceExplorerStateKey } from "@/hooks/use-file-explorer-action
|
|||||||
import { usePanelStore, type SortOption } from "@/stores/panel-store";
|
import { usePanelStore, type SortOption } from "@/stores/panel-store";
|
||||||
import { formatTimeAgo } from "@/utils/time";
|
import { formatTimeAgo } from "@/utils/time";
|
||||||
import { buildAbsoluteExplorerPath } from "@/utils/explorer-paths";
|
import { buildAbsoluteExplorerPath } from "@/utils/explorer-paths";
|
||||||
import {
|
import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar";
|
||||||
WebDesktopScrollbarOverlay,
|
|
||||||
useWebDesktopScrollbarMetrics,
|
|
||||||
} from "@/components/web-desktop-scrollbar";
|
|
||||||
|
|
||||||
const SORT_OPTIONS: { value: SortOption; label: string }[] = [
|
const SORT_OPTIONS: { value: SortOption; label: string }[] = [
|
||||||
{ value: "name", label: "Name" },
|
{ value: "name", label: "Name" },
|
||||||
@@ -152,7 +146,9 @@ export function FileExplorerPane({
|
|||||||
|
|
||||||
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set(["."]));
|
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set(["."]));
|
||||||
const treeListRef = useRef<FlatList<TreeRow>>(null);
|
const treeListRef = useRef<FlatList<TreeRow>>(null);
|
||||||
const treeScrollbarMetrics = useWebDesktopScrollbarMetrics();
|
const scrollbar = useWebScrollViewScrollbar(treeListRef, {
|
||||||
|
enabled: showDesktopWebScrollbar,
|
||||||
|
});
|
||||||
|
|
||||||
const hasInitializedRef = useRef(false);
|
const hasInitializedRef = useRef(false);
|
||||||
|
|
||||||
@@ -502,24 +498,6 @@ export function FileExplorerPane({
|
|||||||
});
|
});
|
||||||
}, [errorRecoveryPath, hasWorkspaceScope, requestDirectoryListing, selectExplorerEntry]);
|
}, [errorRecoveryPath, hasWorkspaceScope, requestDirectoryListing, selectExplorerEntry]);
|
||||||
|
|
||||||
const handleTreeListScroll = useCallback(
|
|
||||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
||||||
if (showDesktopWebScrollbar) {
|
|
||||||
treeScrollbarMetrics.onScroll(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[showDesktopWebScrollbar, treeScrollbarMetrics],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTreeListLayout = useCallback(
|
|
||||||
(event: LayoutChangeEvent) => {
|
|
||||||
if (showDesktopWebScrollbar) {
|
|
||||||
treeScrollbarMetrics.onLayout(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[showDesktopWebScrollbar, treeScrollbarMetrics],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasWorkspaceScope) {
|
if (!hasWorkspaceScope) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centerState}>
|
<View style={styles.centerState}>
|
||||||
@@ -598,27 +576,16 @@ export function FileExplorerPane({
|
|||||||
keyExtractor={(row) => row.entry.path}
|
keyExtractor={(row) => row.entry.path}
|
||||||
testID="file-explorer-tree-scroll"
|
testID="file-explorer-tree-scroll"
|
||||||
contentContainerStyle={styles.entriesContent}
|
contentContainerStyle={styles.entriesContent}
|
||||||
onLayout={showDesktopWebScrollbar ? handleTreeListLayout : undefined}
|
onLayout={scrollbar.onLayout}
|
||||||
onScroll={showDesktopWebScrollbar ? handleTreeListScroll : undefined}
|
onScroll={scrollbar.onScroll}
|
||||||
onContentSizeChange={
|
onContentSizeChange={scrollbar.onContentSizeChange}
|
||||||
showDesktopWebScrollbar ? treeScrollbarMetrics.onContentSizeChange : undefined
|
scrollEventThrottle={16}
|
||||||
}
|
|
||||||
scrollEventThrottle={showDesktopWebScrollbar ? 16 : undefined}
|
|
||||||
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
|
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
|
||||||
initialNumToRender={24}
|
initialNumToRender={24}
|
||||||
maxToRenderPerBatch={40}
|
maxToRenderPerBatch={40}
|
||||||
windowSize={12}
|
windowSize={12}
|
||||||
/>
|
/>
|
||||||
<WebDesktopScrollbarOverlay
|
{scrollbar.overlay}
|
||||||
enabled={showDesktopWebScrollbar}
|
|
||||||
metrics={treeScrollbarMetrics}
|
|
||||||
onScrollToOffset={(nextOffset) => {
|
|
||||||
treeListRef.current?.scrollToOffset({
|
|
||||||
offset: nextOffset,
|
|
||||||
animated: false,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo, useRef } from "react";
|
import React, { useMemo, useRef } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@@ -7,17 +7,11 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
Platform,
|
Platform,
|
||||||
type LayoutChangeEvent,
|
|
||||||
type NativeScrollEvent,
|
|
||||||
type NativeSyntheticEvent,
|
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { Fonts } from "@/constants/theme";
|
import { Fonts } from "@/constants/theme";
|
||||||
import { useSessionStore, type ExplorerFile } from "@/stores/session-store";
|
import { useSessionStore, type ExplorerFile } from "@/stores/session-store";
|
||||||
import {
|
import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar";
|
||||||
WebDesktopScrollbarOverlay,
|
|
||||||
useWebDesktopScrollbarMetrics,
|
|
||||||
} from "@/components/web-desktop-scrollbar";
|
|
||||||
import {
|
import {
|
||||||
highlightCode,
|
highlightCode,
|
||||||
darkHighlightColors,
|
darkHighlightColors,
|
||||||
@@ -123,9 +117,10 @@ function FilePreviewBody({
|
|||||||
const colorMap = isDark ? darkHighlightColors : lightHighlightColors;
|
const colorMap = isDark ? darkHighlightColors : lightHighlightColors;
|
||||||
const baseColor = isDark ? "#c9d1d9" : "#24292f";
|
const baseColor = isDark ? "#c9d1d9" : "#24292f";
|
||||||
|
|
||||||
const enablePreviewDesktopScrollbar = showDesktopWebScrollbar;
|
|
||||||
const previewScrollRef = useRef<RNScrollView>(null);
|
const previewScrollRef = useRef<RNScrollView>(null);
|
||||||
const previewScrollbarMetrics = useWebDesktopScrollbarMetrics();
|
const scrollbar = useWebScrollViewScrollbar(previewScrollRef, {
|
||||||
|
enabled: showDesktopWebScrollbar,
|
||||||
|
});
|
||||||
|
|
||||||
const highlightedLines = useMemo(() => {
|
const highlightedLines = useMemo(() => {
|
||||||
if (!preview || preview.kind !== "text") {
|
if (!preview || preview.kind !== "text") {
|
||||||
@@ -140,24 +135,6 @@ function FilePreviewBody({
|
|||||||
return lineNumberGutterWidth(highlightedLines.length);
|
return lineNumberGutterWidth(highlightedLines.length);
|
||||||
}, [highlightedLines]);
|
}, [highlightedLines]);
|
||||||
|
|
||||||
const handlePreviewScroll = useCallback(
|
|
||||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
||||||
if (enablePreviewDesktopScrollbar) {
|
|
||||||
previewScrollbarMetrics.onScroll(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[enablePreviewDesktopScrollbar, previewScrollbarMetrics],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePreviewLayout = useCallback(
|
|
||||||
(event: LayoutChangeEvent) => {
|
|
||||||
if (enablePreviewDesktopScrollbar) {
|
|
||||||
previewScrollbarMetrics.onLayout(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[enablePreviewDesktopScrollbar, previewScrollbarMetrics],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading && !preview) {
|
if (isLoading && !preview) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centerState}>
|
<View style={styles.centerState}>
|
||||||
@@ -197,13 +174,11 @@ function FilePreviewBody({
|
|||||||
<RNScrollView
|
<RNScrollView
|
||||||
ref={previewScrollRef}
|
ref={previewScrollRef}
|
||||||
style={styles.previewContent}
|
style={styles.previewContent}
|
||||||
onLayout={enablePreviewDesktopScrollbar ? handlePreviewLayout : undefined}
|
onLayout={scrollbar.onLayout}
|
||||||
onScroll={enablePreviewDesktopScrollbar ? handlePreviewScroll : undefined}
|
onScroll={scrollbar.onScroll}
|
||||||
onContentSizeChange={
|
onContentSizeChange={scrollbar.onContentSizeChange}
|
||||||
enablePreviewDesktopScrollbar ? previewScrollbarMetrics.onContentSizeChange : undefined
|
scrollEventThrottle={16}
|
||||||
}
|
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
|
||||||
scrollEventThrottle={enablePreviewDesktopScrollbar ? 16 : undefined}
|
|
||||||
showsVerticalScrollIndicator={!enablePreviewDesktopScrollbar}
|
|
||||||
>
|
>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<View style={styles.previewCodeScrollContent}>{codeLines}</View>
|
<View style={styles.previewCodeScrollContent}>{codeLines}</View>
|
||||||
@@ -218,13 +193,7 @@ function FilePreviewBody({
|
|||||||
</RNScrollView>
|
</RNScrollView>
|
||||||
)}
|
)}
|
||||||
</RNScrollView>
|
</RNScrollView>
|
||||||
<WebDesktopScrollbarOverlay
|
{scrollbar.overlay}
|
||||||
enabled={enablePreviewDesktopScrollbar}
|
|
||||||
metrics={previewScrollbarMetrics}
|
|
||||||
onScrollToOffset={(nextOffset) => {
|
|
||||||
previewScrollRef.current?.scrollTo({ y: nextOffset, animated: false });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -236,13 +205,11 @@ function FilePreviewBody({
|
|||||||
ref={previewScrollRef}
|
ref={previewScrollRef}
|
||||||
style={styles.previewContent}
|
style={styles.previewContent}
|
||||||
contentContainerStyle={styles.previewImageScrollContent}
|
contentContainerStyle={styles.previewImageScrollContent}
|
||||||
onLayout={enablePreviewDesktopScrollbar ? handlePreviewLayout : undefined}
|
onLayout={scrollbar.onLayout}
|
||||||
onScroll={enablePreviewDesktopScrollbar ? handlePreviewScroll : undefined}
|
onScroll={scrollbar.onScroll}
|
||||||
onContentSizeChange={
|
onContentSizeChange={scrollbar.onContentSizeChange}
|
||||||
enablePreviewDesktopScrollbar ? previewScrollbarMetrics.onContentSizeChange : undefined
|
scrollEventThrottle={16}
|
||||||
}
|
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
|
||||||
scrollEventThrottle={enablePreviewDesktopScrollbar ? 16 : undefined}
|
|
||||||
showsVerticalScrollIndicator={!enablePreviewDesktopScrollbar}
|
|
||||||
>
|
>
|
||||||
<RNImage
|
<RNImage
|
||||||
source={{
|
source={{
|
||||||
@@ -252,13 +219,7 @@ function FilePreviewBody({
|
|||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
</RNScrollView>
|
</RNScrollView>
|
||||||
<WebDesktopScrollbarOverlay
|
{scrollbar.overlay}
|
||||||
enabled={enablePreviewDesktopScrollbar}
|
|
||||||
metrics={previewScrollbarMetrics}
|
|
||||||
onScrollToOffset={(nextOffset) => {
|
|
||||||
previewScrollRef.current?.scrollTo({ y: nextOffset, animated: false });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,10 +62,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip
|
|||||||
import { GitHubIcon } from "@/components/icons/github-icon";
|
import { GitHubIcon } from "@/components/icons/github-icon";
|
||||||
import { buildGitActions, type GitActions } from "@/components/git-actions-policy";
|
import { buildGitActions, type GitActions } from "@/components/git-actions-policy";
|
||||||
import { lineNumberGutterWidth } from "@/components/code-insets";
|
import { lineNumberGutterWidth } from "@/components/code-insets";
|
||||||
import {
|
import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar";
|
||||||
WebDesktopScrollbarOverlay,
|
|
||||||
useWebDesktopScrollbarMetrics,
|
|
||||||
} from "@/components/web-desktop-scrollbar";
|
|
||||||
import { buildNewAgentRoute, resolveNewAgentWorkingDir } from "@/utils/new-agent-routing";
|
import { buildNewAgentRoute, resolveNewAgentWorkingDir } from "@/utils/new-agent-routing";
|
||||||
import { openExternalUrl } from "@/utils/open-external-url";
|
import { openExternalUrl } from "@/utils/open-external-url";
|
||||||
import { GitActionsSplitButton } from "@/components/git-actions-split-button";
|
import { GitActionsSplitButton } from "@/components/git-actions-split-button";
|
||||||
@@ -429,7 +426,9 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
|
|||||||
const [isManualRefresh, setIsManualRefresh] = useState(false);
|
const [isManualRefresh, setIsManualRefresh] = useState(false);
|
||||||
const [expandedByPath, setExpandedByPath] = useState<Record<string, boolean>>({});
|
const [expandedByPath, setExpandedByPath] = useState<Record<string, boolean>>({});
|
||||||
const diffListRef = useRef<FlatList<DiffFlatItem>>(null);
|
const diffListRef = useRef<FlatList<DiffFlatItem>>(null);
|
||||||
const diffScrollbarMetrics = useWebDesktopScrollbarMetrics();
|
const scrollbar = useWebScrollViewScrollbar(diffListRef, {
|
||||||
|
enabled: showDesktopWebScrollbar,
|
||||||
|
});
|
||||||
const diffListScrollOffsetRef = useRef(0);
|
const diffListScrollOffsetRef = useRef(0);
|
||||||
const diffListViewportHeightRef = useRef(0);
|
const diffListViewportHeightRef = useRef(0);
|
||||||
const headerHeightByPathRef = useRef<Record<string, number>>({});
|
const headerHeightByPathRef = useRef<Record<string, number>>({});
|
||||||
@@ -515,11 +514,9 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
|
|||||||
const handleDiffListScroll = useCallback(
|
const handleDiffListScroll = useCallback(
|
||||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
diffListScrollOffsetRef.current = event.nativeEvent.contentOffset.y;
|
diffListScrollOffsetRef.current = event.nativeEvent.contentOffset.y;
|
||||||
if (showDesktopWebScrollbar) {
|
scrollbar.onScroll(event);
|
||||||
diffScrollbarMetrics.onScroll(event);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[diffScrollbarMetrics, showDesktopWebScrollbar],
|
[scrollbar.onScroll],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDiffListLayout = useCallback(
|
const handleDiffListLayout = useCallback(
|
||||||
@@ -529,11 +526,9 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
diffListViewportHeightRef.current = height;
|
diffListViewportHeightRef.current = height;
|
||||||
if (showDesktopWebScrollbar) {
|
scrollbar.onLayout(event);
|
||||||
diffScrollbarMetrics.onLayout(event);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[diffScrollbarMetrics, showDesktopWebScrollbar],
|
[scrollbar.onLayout],
|
||||||
);
|
);
|
||||||
|
|
||||||
const computeHeaderOffset = useCallback(
|
const computeHeaderOffset = useCallback(
|
||||||
@@ -844,9 +839,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
|
|||||||
testID="git-diff-scroll"
|
testID="git-diff-scroll"
|
||||||
onLayout={handleDiffListLayout}
|
onLayout={handleDiffListLayout}
|
||||||
onScroll={handleDiffListScroll}
|
onScroll={handleDiffListScroll}
|
||||||
onContentSizeChange={
|
onContentSizeChange={scrollbar.onContentSizeChange}
|
||||||
showDesktopWebScrollbar ? diffScrollbarMetrics.onContentSizeChange : undefined
|
|
||||||
}
|
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
|
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
@@ -1085,16 +1078,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi
|
|||||||
|
|
||||||
<View style={styles.diffContainer}>
|
<View style={styles.diffContainer}>
|
||||||
{bodyContent}
|
{bodyContent}
|
||||||
<WebDesktopScrollbarOverlay
|
{hasChanges ? scrollbar.overlay : null}
|
||||||
enabled={showDesktopWebScrollbar && hasChanges}
|
|
||||||
metrics={diffScrollbarMetrics}
|
|
||||||
onScrollToOffset={(nextOffset) => {
|
|
||||||
diffListRef.current?.scrollToOffset({
|
|
||||||
offset: nextOffset,
|
|
||||||
animated: false,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,15 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useState, useRef, useCallback, useEffect, useImperativeHandle, forwardRef } from "react";
|
import {
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
forwardRef,
|
||||||
|
} from "react";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { Mic, MicOff, ArrowUp, Paperclip, Plus, X, Square } from "lucide-react-native";
|
import { Mic, MicOff, ArrowUp, Paperclip, Plus, X, Square } from "lucide-react-native";
|
||||||
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated";
|
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated";
|
||||||
@@ -33,6 +41,7 @@ import { useAttachmentPreviewUrl } from "@/attachments/use-attachment-preview-ur
|
|||||||
import { focusWithRetries } from "@/utils/web-focus";
|
import { focusWithRetries } from "@/utils/web-focus";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { Shortcut } from "@/components/ui/shortcut";
|
import { Shortcut } from "@/components/ui/shortcut";
|
||||||
|
import { useWebElementScrollbar } from "@/components/use-web-scrollbar";
|
||||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||||
import type { MessageInputKeyboardActionKind } from "@/keyboard/actions";
|
import type { MessageInputKeyboardActionKind } from "@/keyboard/actions";
|
||||||
import {
|
import {
|
||||||
@@ -572,6 +581,18 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const webTextareaRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (IS_WEB) {
|
||||||
|
webTextareaRef.current = getWebTextArea() as HTMLElement | null;
|
||||||
|
}
|
||||||
|
}, [getWebTextArea]);
|
||||||
|
|
||||||
|
const inputScrollbar = useWebElementScrollbar(webTextareaRef, {
|
||||||
|
enabled: IS_WEB && inputHeight >= MAX_INPUT_HEIGHT,
|
||||||
|
});
|
||||||
|
|
||||||
const getWebElement = useCallback((target: "root" | "wrapper"): HTMLElement | null => {
|
const getWebElement = useCallback((target: "root" | "wrapper"): HTMLElement | null => {
|
||||||
const ref = target === "root" ? rootRef.current : inputWrapperRef.current;
|
const ref = target === "root" ? rootRef.current : inputWrapperRef.current;
|
||||||
if (!ref) return null;
|
if (!ref) return null;
|
||||||
@@ -913,42 +934,45 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Text input */}
|
{/* Text input */}
|
||||||
<TextInput
|
<View style={styles.textInputScrollWrapper}>
|
||||||
ref={textInputRef}
|
<TextInput
|
||||||
value={value}
|
ref={textInputRef}
|
||||||
onChangeText={handleInputChange}
|
value={value}
|
||||||
placeholder={placeholder}
|
onChangeText={handleInputChange}
|
||||||
placeholderTextColor={theme.colors.surface4}
|
placeholder={placeholder}
|
||||||
accessibilityLabel="Message agent..."
|
placeholderTextColor={theme.colors.surface4}
|
||||||
onFocus={() => {
|
accessibilityLabel="Message agent..."
|
||||||
isInputFocusedRef.current = true;
|
onFocus={() => {
|
||||||
onFocusChange?.(true);
|
isInputFocusedRef.current = true;
|
||||||
}}
|
onFocusChange?.(true);
|
||||||
onBlur={() => {
|
}}
|
||||||
isInputFocusedRef.current = false;
|
onBlur={() => {
|
||||||
onFocusChange?.(false);
|
isInputFocusedRef.current = false;
|
||||||
}}
|
onFocusChange?.(false);
|
||||||
style={[
|
}}
|
||||||
styles.textInput,
|
style={[
|
||||||
IS_WEB
|
styles.textInput,
|
||||||
? {
|
IS_WEB
|
||||||
height: inputHeight,
|
? {
|
||||||
minHeight: MIN_INPUT_HEIGHT,
|
height: inputHeight,
|
||||||
maxHeight: MAX_INPUT_HEIGHT,
|
minHeight: MIN_INPUT_HEIGHT,
|
||||||
}
|
maxHeight: MAX_INPUT_HEIGHT,
|
||||||
: {
|
}
|
||||||
minHeight: MIN_INPUT_HEIGHT,
|
: {
|
||||||
maxHeight: MAX_INPUT_HEIGHT,
|
minHeight: MIN_INPUT_HEIGHT,
|
||||||
},
|
maxHeight: MAX_INPUT_HEIGHT,
|
||||||
]}
|
},
|
||||||
multiline
|
]}
|
||||||
scrollEnabled={IS_WEB ? inputHeight >= MAX_INPUT_HEIGHT : true}
|
multiline
|
||||||
onContentSizeChange={handleContentSizeChange}
|
scrollEnabled={IS_WEB ? inputHeight >= MAX_INPUT_HEIGHT : true}
|
||||||
editable={!isDictating && !isRealtimeVoiceForCurrentAgent && !disabled}
|
onContentSizeChange={handleContentSizeChange}
|
||||||
onKeyPress={shouldHandleDesktopSubmit ? handleDesktopKeyPress : undefined}
|
editable={!isDictating && !isRealtimeVoiceForCurrentAgent && !disabled}
|
||||||
onSelectionChange={handleSelectionChange}
|
onKeyPress={shouldHandleDesktopSubmit ? handleDesktopKeyPress : undefined}
|
||||||
autoFocus={IS_WEB && autoFocus}
|
onSelectionChange={handleSelectionChange}
|
||||||
/>
|
autoFocus={IS_WEB && autoFocus}
|
||||||
|
/>
|
||||||
|
{inputScrollbar}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Button row */}
|
{/* Button row */}
|
||||||
<View style={styles.buttonRow}>
|
<View style={styles.buttonRow}>
|
||||||
@@ -1189,6 +1213,9 @@ const styles = StyleSheet.create(((theme: any) => ({
|
|||||||
removeImageButtonVisible: {
|
removeImageButtonVisible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
|
textInputScrollWrapper: {
|
||||||
|
position: "relative",
|
||||||
|
},
|
||||||
textInput: {
|
textInput: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
|
|||||||
@@ -23,21 +23,7 @@ const WEB_BOTTOM_SETTLE_TIMEOUT_MS = 200;
|
|||||||
const USER_SCROLL_DELTA_EPSILON = 1;
|
const USER_SCROLL_DELTA_EPSILON = 1;
|
||||||
const AUTO_SCROLL_BOTTOM_THRESHOLD_PX = 64;
|
const AUTO_SCROLL_BOTTOM_THRESHOLD_PX = 64;
|
||||||
const AUTO_SCROLL_RESUME_THRESHOLD_PX = 1;
|
const AUTO_SCROLL_RESUME_THRESHOLD_PX = 1;
|
||||||
const WEB_STREAM_SCROLLBAR_STYLE_ID = "web-stream-viewport-scrollbar-style";
|
import { useWebElementScrollbar } from "./use-web-scrollbar";
|
||||||
const WEB_STREAM_SCROLLBAR_STYLE = `
|
|
||||||
#agent-chat-scroll-web-dom-scroll,
|
|
||||||
#agent-chat-scroll-web-dom-virtualized {
|
|
||||||
scrollbar-width: none;
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#agent-chat-scroll-web-dom-scroll::-webkit-scrollbar,
|
|
||||||
#agent-chat-scroll-web-dom-virtualized::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function logWebStickyBottom(_event: string, _details: Record<string, unknown>): void {
|
function logWebStickyBottom(_event: string, _details: Record<string, unknown>): void {
|
||||||
// Intentionally disabled: this path is too noisy during voice debugging.
|
// Intentionally disabled: this path is too noisy during voice debugging.
|
||||||
@@ -119,8 +105,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
scrollEnabled,
|
scrollEnabled,
|
||||||
isMobileBreakpoint,
|
isMobileBreakpoint,
|
||||||
} = props;
|
} = props;
|
||||||
const { WebDesktopScrollbarOverlay, useWebDesktopScrollbarMetrics } =
|
|
||||||
require("./web-desktop-scrollbar") as typeof import("./web-desktop-scrollbar");
|
|
||||||
const scrollContainerRef = useRef<HTMLElement | null>(null);
|
const scrollContainerRef = useRef<HTMLElement | null>(null);
|
||||||
const contentRef = useRef<HTMLElement | null>(null);
|
const contentRef = useRef<HTMLElement | null>(null);
|
||||||
const [followOutput, setFollowOutputr] = useState(true);
|
const [followOutput, setFollowOutputr] = useState(true);
|
||||||
@@ -142,8 +126,11 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
const lastTouchClientYRef = useRef<number | null>(null);
|
const lastTouchClientYRef = useRef<number | null>(null);
|
||||||
const pendingAutoScrollFrameRef = useRef<number | null>(null);
|
const pendingAutoScrollFrameRef = useRef<number | null>(null);
|
||||||
const pendingAutoScrollTimeoutRef = useRef<number | null>(null);
|
const pendingAutoScrollTimeoutRef = useRef<number | null>(null);
|
||||||
const streamScrollbarMetrics = useWebDesktopScrollbarMetrics();
|
|
||||||
const showDesktopWebScrollbar = !isMobileBreakpoint;
|
const showDesktopWebScrollbar = !isMobileBreakpoint;
|
||||||
|
const scrollbarOverlay = useWebElementScrollbar(scrollContainerRef, {
|
||||||
|
enabled: showDesktopWebScrollbar,
|
||||||
|
contentRef,
|
||||||
|
});
|
||||||
const shouldUseVirtualizer = segments.historyVirtualized.length > 0;
|
const shouldUseVirtualizer = segments.historyVirtualized.length > 0;
|
||||||
const {
|
const {
|
||||||
renderHistoryVirtualizedRow,
|
renderHistoryVirtualizedRow,
|
||||||
@@ -271,33 +258,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
onNearBottomChange(true);
|
onNearBottomChange(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
streamScrollbarMetrics.onContentSizeChange(
|
|
||||||
scrollContainer.clientWidth,
|
|
||||||
scrollContainer.scrollHeight,
|
|
||||||
);
|
|
||||||
streamScrollbarMetrics.onLayout({
|
|
||||||
nativeEvent: {
|
|
||||||
layout: {
|
|
||||||
width: scrollContainer.clientWidth,
|
|
||||||
height: scrollContainer.clientHeight,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as never);
|
|
||||||
streamScrollbarMetrics.onScroll({
|
|
||||||
nativeEvent: {
|
|
||||||
contentOffset: { x: 0, y: scrollContainer.scrollTop },
|
|
||||||
contentSize: {
|
|
||||||
width: scrollContainer.clientWidth,
|
|
||||||
height: scrollContainer.scrollHeight,
|
|
||||||
},
|
|
||||||
layoutMeasurement: {
|
|
||||||
width: scrollContainer.clientWidth,
|
|
||||||
height: scrollContainer.clientHeight,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as never);
|
|
||||||
syncNearBottom(scrollContainer, onNearBottomChange);
|
syncNearBottom(scrollContainer, onNearBottomChange);
|
||||||
const currentMetrics = {
|
const currentMetrics = {
|
||||||
scrollTop: scrollContainer.scrollTop,
|
scrollTop: scrollContainer.scrollTop,
|
||||||
@@ -323,7 +283,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
...currentMetrics,
|
...currentMetrics,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [onNearBottomChange, props.agentId, streamScrollbarMetrics]);
|
}, [onNearBottomChange, props.agentId]);
|
||||||
|
|
||||||
const handleDomScroll = useCallback(() => {
|
const handleDomScroll = useCallback(() => {
|
||||||
const scrollContainer = scrollContainerRef.current;
|
const scrollContainer = scrollContainerRef.current;
|
||||||
@@ -711,7 +671,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style id={WEB_STREAM_SCROLLBAR_STYLE_ID}>{WEB_STREAM_SCROLLBAR_STYLE}</style>
|
|
||||||
<div
|
<div
|
||||||
ref={(node) => {
|
ref={(node) => {
|
||||||
scrollContainerRef.current = node;
|
scrollContainerRef.current = node;
|
||||||
@@ -759,20 +718,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
|||||||
{shouldRenderEmpty ? listEmptyComponent : null}
|
{shouldRenderEmpty ? listEmptyComponent : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<WebDesktopScrollbarOverlay
|
{scrollbarOverlay}
|
||||||
enabled={showDesktopWebScrollbar}
|
|
||||||
metrics={streamScrollbarMetrics}
|
|
||||||
inverted={false}
|
|
||||||
onScrollToOffset={(nextOffset) => {
|
|
||||||
const scrollContainer = scrollContainerRef.current;
|
|
||||||
if (!scrollContainer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
scrollContainer.scrollTo({ top: nextOffset, behavior: "auto" });
|
|
||||||
lastKnownScrollTopRef.current = scrollContainer.scrollTop;
|
|
||||||
updateScrollMetrics();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { useCallback, useEffect, useState, type ReactNode, type RefObject } from "react";
|
||||||
|
import {
|
||||||
|
Platform,
|
||||||
|
type FlatList,
|
||||||
|
type LayoutChangeEvent,
|
||||||
|
type NativeScrollEvent,
|
||||||
|
type NativeSyntheticEvent,
|
||||||
|
type ScrollView,
|
||||||
|
} from "react-native";
|
||||||
|
import {
|
||||||
|
WebDesktopScrollbarOverlay,
|
||||||
|
useWebDesktopScrollbarMetrics,
|
||||||
|
type ScrollbarMetrics,
|
||||||
|
} from "./web-desktop-scrollbar";
|
||||||
|
|
||||||
|
const METRICS_EPSILON = 0.5;
|
||||||
|
const HIDE_SCROLLBAR_STYLE_ID = "paseo-hide-scrollbar";
|
||||||
|
|
||||||
|
function ensureHideScrollbarStyle(): void {
|
||||||
|
if (typeof document === "undefined") return;
|
||||||
|
if (document.getElementById(HIDE_SCROLLBAR_STYLE_ID)) return;
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.id = HIDE_SCROLLBAR_STYLE_ID;
|
||||||
|
style.textContent =
|
||||||
|
"[data-hide-scrollbar]::-webkit-scrollbar { display: none; width: 0; height: 0; }";
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
function metricsChanged(a: ScrollbarMetrics, b: ScrollbarMetrics): boolean {
|
||||||
|
return (
|
||||||
|
Math.abs(a.offset - b.offset) > METRICS_EPSILON ||
|
||||||
|
Math.abs(a.viewportSize - b.viewportSize) > METRICS_EPSILON ||
|
||||||
|
Math.abs(a.contentSize - b.contentSize) > METRICS_EPSILON
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DOM element scrollbar ────────────────────────────────────────────
|
||||||
|
// Fully automatic: listens to scroll/input/resize events on the element,
|
||||||
|
// hides the native scrollbar, and returns a themed overlay or null.
|
||||||
|
|
||||||
|
export function useWebElementScrollbar(
|
||||||
|
elementRef: RefObject<HTMLElement | null>,
|
||||||
|
options?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
contentRef?: RefObject<HTMLElement | null>;
|
||||||
|
},
|
||||||
|
): ReactNode {
|
||||||
|
const isWeb = Platform.OS === "web";
|
||||||
|
const enabled = (options?.enabled ?? true) && isWeb;
|
||||||
|
const contentRef = options?.contentRef;
|
||||||
|
|
||||||
|
const [metrics, setMetrics] = useState<ScrollbarMetrics>({
|
||||||
|
offset: 0,
|
||||||
|
viewportSize: 0,
|
||||||
|
contentSize: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return;
|
||||||
|
const element = elementRef.current;
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
element.setAttribute("data-hide-scrollbar", "");
|
||||||
|
(element.style as any).scrollbarWidth = "none";
|
||||||
|
(element.style as any).msOverflowStyle = "none";
|
||||||
|
ensureHideScrollbarStyle();
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
const el = elementRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const next: ScrollbarMetrics = {
|
||||||
|
offset: el.scrollTop,
|
||||||
|
viewportSize: el.clientHeight,
|
||||||
|
contentSize: el.scrollHeight,
|
||||||
|
};
|
||||||
|
setMetrics((prev) => (metricsChanged(prev, next) ? next : prev));
|
||||||
|
}
|
||||||
|
|
||||||
|
element.addEventListener("scroll", update, { passive: true });
|
||||||
|
element.addEventListener("input", update, { passive: true });
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(update);
|
||||||
|
resizeObserver.observe(element);
|
||||||
|
const contentElement = contentRef?.current;
|
||||||
|
if (contentElement) {
|
||||||
|
resizeObserver.observe(contentElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener("scroll", update);
|
||||||
|
element.removeEventListener("input", update);
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
element.removeAttribute("data-hide-scrollbar");
|
||||||
|
(element.style as any).scrollbarWidth = "";
|
||||||
|
(element.style as any).msOverflowStyle = "";
|
||||||
|
};
|
||||||
|
}, [contentRef, elementRef, enabled]);
|
||||||
|
|
||||||
|
const onScrollToOffset = useCallback(
|
||||||
|
(offset: number) => {
|
||||||
|
elementRef.current?.scrollTo({ top: offset, behavior: "auto" });
|
||||||
|
},
|
||||||
|
[elementRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!enabled) return null;
|
||||||
|
|
||||||
|
return <WebDesktopScrollbarOverlay enabled metrics={metrics} onScrollToOffset={onScrollToOffset} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RN ScrollView / FlatList scrollbar ───────────────────────────────
|
||||||
|
// Returns event handlers to wire onto your ScrollView/FlatList plus
|
||||||
|
// a renderable overlay. The overlay is null when disabled.
|
||||||
|
|
||||||
|
interface WebScrollViewScrollbar {
|
||||||
|
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||||
|
onLayout: (event: LayoutChangeEvent) => void;
|
||||||
|
onContentSizeChange: (width: number, height: number) => void;
|
||||||
|
overlay: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWebScrollViewScrollbar(
|
||||||
|
scrollableRef: RefObject<ScrollView | FlatList | null>,
|
||||||
|
options?: { enabled?: boolean },
|
||||||
|
): WebScrollViewScrollbar {
|
||||||
|
const isWeb = Platform.OS === "web";
|
||||||
|
const enabled = (options?.enabled ?? true) && isWeb;
|
||||||
|
const metricsHook = useWebDesktopScrollbarMetrics();
|
||||||
|
|
||||||
|
const onScrollToOffset = useCallback(
|
||||||
|
(offset: number) => {
|
||||||
|
const scrollable = scrollableRef.current;
|
||||||
|
if (!scrollable) return;
|
||||||
|
if ("scrollToOffset" in scrollable) {
|
||||||
|
(scrollable as FlatList).scrollToOffset({ offset, animated: false });
|
||||||
|
} else {
|
||||||
|
(scrollable as ScrollView).scrollTo({ y: offset, animated: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrollableRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
const overlay: ReactNode = enabled ? (
|
||||||
|
<WebDesktopScrollbarOverlay enabled metrics={metricsHook} onScrollToOffset={onScrollToOffset} />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onScroll: metricsHook.onScroll,
|
||||||
|
onLayout: metricsHook.onLayout,
|
||||||
|
onContentSizeChange: metricsHook.onContentSizeChange,
|
||||||
|
overlay,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ function clamp(value: number, min: number, max: number): number {
|
|||||||
return Math.min(max, Math.max(min, value));
|
return Math.min(max, Math.max(min, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrollbarMetrics = {
|
export type ScrollbarMetrics = {
|
||||||
offset: number;
|
offset: number;
|
||||||
viewportSize: number;
|
viewportSize: number;
|
||||||
contentSize: number;
|
contentSize: number;
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ const darkSemanticColors = {
|
|||||||
foregroundMuted: "#A1A5A4",
|
foregroundMuted: "#A1A5A4",
|
||||||
|
|
||||||
// Controls
|
// Controls
|
||||||
scrollbarHandle: "#71717a", // zinc-500
|
scrollbarHandle: "#717574", // zinc-500 w/ teal tint
|
||||||
|
|
||||||
// Borders
|
// Borders
|
||||||
border: "#252B2A",
|
border: "#252B2A",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/cli",
|
"name": "@getpaseo/cli",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"description": "Paseo CLI - control your AI coding agents from the command line",
|
"description": "Paseo CLI - control your AI coding agents from the command line",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^1.0.0",
|
"@clack/prompts": "^1.0.0",
|
||||||
"@getpaseo/relay": "0.1.41",
|
"@getpaseo/relay": "0.1.43-rc.1",
|
||||||
"@getpaseo/server": "0.1.41",
|
"@getpaseo/server": "0.1.43-rc.1",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"commander": "^12.0.0",
|
"commander": "^12.0.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
import { getOrCreateServerId, findExecutable, applyProviderEnv } from "@getpaseo/server";
|
import {
|
||||||
|
getOrCreateServerId,
|
||||||
|
findExecutable,
|
||||||
|
quoteWindowsCommand,
|
||||||
|
applyProviderEnv,
|
||||||
|
} from "@getpaseo/server";
|
||||||
import { tryConnectToDaemon } from "../../utils/client.js";
|
import { tryConnectToDaemon } from "../../utils/client.js";
|
||||||
import type { CommandOptions, ListResult, OutputSchema } from "../../output/index.js";
|
import type { CommandOptions, ListResult, OutputSchema } from "../../output/index.js";
|
||||||
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
|
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
|
||||||
@@ -170,7 +175,7 @@ function checkProviderBinary(binary: string): { path: string | null; version: st
|
|||||||
}
|
}
|
||||||
const env = applyProviderEnv(process.env);
|
const env = applyProviderEnv(process.env);
|
||||||
try {
|
try {
|
||||||
const output = execFileSync(binaryPath, ["--version"], {
|
const output = execFileSync(quoteWindowsCommand(binaryPath), ["--version"], {
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/desktop",
|
"name": "@getpaseo/desktop",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Paseo desktop app (Electron wrapper)",
|
"description": "Paseo desktop app (Electron wrapper)",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getpaseo/cli": "0.1.41",
|
"@getpaseo/cli": "0.1.43-rc.1",
|
||||||
"@getpaseo/server": "0.1.41",
|
"@getpaseo/server": "0.1.43-rc.1",
|
||||||
"electron-log": "^5.4.3",
|
"electron-log": "^5.4.3",
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
"ws": "^8.14.2"
|
"ws": "^8.14.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/expo-two-way-audio",
|
"name": "@getpaseo/expo-two-way-audio",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"description": "Native module for two way audio streaming",
|
"description": "Native module for two way audio streaming",
|
||||||
"main": "build/index.js",
|
"main": "build/index.js",
|
||||||
"types": "build/index.d.ts",
|
"types": "build/index.d.ts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/highlight",
|
"name": "@getpaseo/highlight",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/relay",
|
"name": "@getpaseo/relay",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"description": "Paseo relay for bridging daemon and client connections",
|
"description": "Paseo relay for bridging daemon and client connections",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/server",
|
"name": "@getpaseo/server",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"description": "Paseo backend server",
|
"description": "Paseo backend server",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -64,8 +64,8 @@
|
|||||||
"@ai-sdk/openai": "2.0.52",
|
"@ai-sdk/openai": "2.0.52",
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
|
||||||
"@deepgram/sdk": "^3.4.0",
|
"@deepgram/sdk": "^3.4.0",
|
||||||
"@getpaseo/highlight": "0.1.41",
|
"@getpaseo/highlight": "0.1.43-rc.1",
|
||||||
"@getpaseo/relay": "0.1.41",
|
"@getpaseo/relay": "0.1.43-rc.1",
|
||||||
"@isaacs/ttlcache": "^2.1.4",
|
"@isaacs/ttlcache": "^2.1.4",
|
||||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||||
"@opencode-ai/sdk": "1.2.6",
|
"@opencode-ai/sdk": "1.2.6",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
findExecutable,
|
findExecutable,
|
||||||
|
quoteWindowsArgument,
|
||||||
|
quoteWindowsCommand,
|
||||||
resolveProviderCommandPrefix,
|
resolveProviderCommandPrefix,
|
||||||
applyProviderEnv,
|
applyProviderEnv,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
@@ -218,3 +220,75 @@ describe("findExecutable", () => {
|
|||||||
expect(findExecutableDependencies.existsSync).toHaveBeenCalledWith("/usr/local/bin/codex");
|
expect(findExecutableDependencies.existsSync).toHaveBeenCalledWith("/usr/local/bin/codex");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("quoteWindowsCommand", () => {
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
|
||||||
|
function setPlatform(value: string) {
|
||||||
|
Object.defineProperty(process, "platform", { value, writable: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setPlatform(originalPlatform);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("quotes a Windows path with spaces", () => {
|
||||||
|
setPlatform("win32");
|
||||||
|
expect(quoteWindowsCommand("C:\\Program Files\\Anthropic\\claude.exe")).toBe(
|
||||||
|
'"C:\\Program Files\\Anthropic\\claude.exe"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not double-quote an already-quoted path", () => {
|
||||||
|
setPlatform("win32");
|
||||||
|
expect(quoteWindowsCommand('"C:\\Program Files\\Anthropic\\claude.exe"')).toBe(
|
||||||
|
'"C:\\Program Files\\Anthropic\\claude.exe"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns the command unchanged when there are no spaces", () => {
|
||||||
|
setPlatform("win32");
|
||||||
|
expect(quoteWindowsCommand("C:\\nvm4w\\nodejs\\codex")).toBe("C:\\nvm4w\\nodejs\\codex");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns the command unchanged on non-Windows platforms", () => {
|
||||||
|
setPlatform("darwin");
|
||||||
|
expect(quoteWindowsCommand("/usr/local/bin/claude code")).toBe("/usr/local/bin/claude code");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("quoteWindowsArgument", () => {
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
|
||||||
|
function setPlatform(value: string) {
|
||||||
|
Object.defineProperty(process, "platform", { value, writable: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
setPlatform(originalPlatform);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("quotes a Windows argument with spaces", () => {
|
||||||
|
setPlatform("win32");
|
||||||
|
expect(quoteWindowsArgument("C:\\Program Files\\Anthropic\\cli.js")).toBe(
|
||||||
|
'"C:\\Program Files\\Anthropic\\cli.js"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not double-quote an already-quoted argument", () => {
|
||||||
|
setPlatform("win32");
|
||||||
|
expect(quoteWindowsArgument('"C:\\Program Files\\Anthropic\\cli.js"')).toBe(
|
||||||
|
'"C:\\Program Files\\Anthropic\\cli.js"',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns the argument unchanged when there are no spaces", () => {
|
||||||
|
setPlatform("win32");
|
||||||
|
expect(quoteWindowsArgument("--version")).toBe("--version");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns the argument unchanged on non-Windows platforms", () => {
|
||||||
|
setPlatform("darwin");
|
||||||
|
expect(quoteWindowsArgument("/usr/local/bin/claude code")).toBe("/usr/local/bin/claude code");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -281,6 +281,31 @@ export function findExecutable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When spawning with `shell: true` on Windows, the command is passed to
|
||||||
|
* `cmd.exe /d /s /c "command args"`. The `/s` strips outer quotes, so a
|
||||||
|
* command path with spaces (e.g. `C:\Program Files\...`) is split at the
|
||||||
|
* space. Wrapping it in quotes produces the correct `"C:\Program Files\..." args`.
|
||||||
|
*/
|
||||||
|
export function quoteWindowsCommand(command: string): string {
|
||||||
|
if (process.platform !== "win32") return command;
|
||||||
|
if (!command.includes(" ")) return command;
|
||||||
|
if (command.startsWith('"') && command.endsWith('"')) return command;
|
||||||
|
return `"${command}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `spawn(..., { shell: true })` on Windows also passes argv through `cmd.exe`.
|
||||||
|
* Any argument containing spaces must be quoted or it will be split before the
|
||||||
|
* child process sees it.
|
||||||
|
*/
|
||||||
|
export function quoteWindowsArgument(argument: string): string {
|
||||||
|
if (process.platform !== "win32") return argument;
|
||||||
|
if (!argument.includes(" ")) return argument;
|
||||||
|
if (argument.startsWith('"') && argument.endsWith('"')) return argument;
|
||||||
|
return `"${argument}"`;
|
||||||
|
}
|
||||||
|
|
||||||
export function isCommandAvailable(command: string): boolean {
|
export function isCommandAvailable(command: string): boolean {
|
||||||
return findExecutable(command) !== null;
|
return findExecutable(command) !== null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,25 @@ describe("ClaudeAgentSession integration", () => {
|
|||||||
}
|
}
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
|
|
||||||
|
test.runIf(canRunClaudeIntegration)(
|
||||||
|
"keeps bypassPermissions available after a thinking-option restart",
|
||||||
|
async () => {
|
||||||
|
const handle = await createSession({
|
||||||
|
cwdPrefix: "claude-agent-bypass-restart-",
|
||||||
|
modeId: "bypassPermissions",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handle.session.setMode("acceptEdits");
|
||||||
|
await handle.session.setThinkingOption("high");
|
||||||
|
await expect(handle.session.setMode("bypassPermissions")).resolves.toBeUndefined();
|
||||||
|
} finally {
|
||||||
|
await cleanupSession(handle);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
60_000,
|
||||||
|
);
|
||||||
|
|
||||||
test.runIf(canRunClaudeIntegration)(
|
test.runIf(canRunClaudeIntegration)(
|
||||||
"supportedModels returns the current abstract Claude SDK model shape",
|
"supportedModels returns the current abstract Claude SDK model shape",
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
@@ -783,6 +783,58 @@ describe("ClaudeAgentSession redesign invariants", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("preserves bypass capability across query restarts triggered by thinking changes", async () => {
|
||||||
|
const capturedOptions: Array<{
|
||||||
|
permissionMode?: string;
|
||||||
|
allowDangerouslySkipPermissions?: boolean;
|
||||||
|
effort?: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
sdkQueryFactory.mockImplementation(
|
||||||
|
({
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
options: {
|
||||||
|
permissionMode?: string;
|
||||||
|
allowDangerouslySkipPermissions?: boolean;
|
||||||
|
effort?: string;
|
||||||
|
};
|
||||||
|
}) => {
|
||||||
|
capturedOptions.push({
|
||||||
|
permissionMode: options.permissionMode,
|
||||||
|
allowDangerouslySkipPermissions: options.allowDangerouslySkipPermissions,
|
||||||
|
effort: options.effort,
|
||||||
|
});
|
||||||
|
|
||||||
|
return createBaseQueryMock(
|
||||||
|
vi.fn(async () => ({ done: true, value: undefined })),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const session = await createSession();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await session.setMode("bypassPermissions");
|
||||||
|
await session.setMode("acceptEdits");
|
||||||
|
await session.setThinkingOption("high");
|
||||||
|
await session.setMode("bypassPermissions");
|
||||||
|
|
||||||
|
expect(capturedOptions).toHaveLength(2);
|
||||||
|
expect(capturedOptions[0]).toMatchObject({
|
||||||
|
permissionMode: "default",
|
||||||
|
allowDangerouslySkipPermissions: true,
|
||||||
|
});
|
||||||
|
expect(capturedOptions[1]).toMatchObject({
|
||||||
|
permissionMode: "acceptEdits",
|
||||||
|
allowDangerouslySkipPermissions: true,
|
||||||
|
effort: "high",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await session.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("reuses one autonomous run for unbound stream_event bursts with no foreground run", async () => {
|
test("reuses one autonomous run for unbound stream_event bursts with no foreground run", async () => {
|
||||||
const session = await createSession();
|
const session = await createSession();
|
||||||
const internal = session as unknown as {
|
const internal = session as unknown as {
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ import type {
|
|||||||
import {
|
import {
|
||||||
applyProviderEnv,
|
applyProviderEnv,
|
||||||
findExecutable,
|
findExecutable,
|
||||||
|
quoteWindowsArgument,
|
||||||
|
quoteWindowsCommand,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
} from "../provider-launch-config.js";
|
} from "../provider-launch-config.js";
|
||||||
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
||||||
@@ -213,7 +215,10 @@ function applyRuntimeSettingsToClaudeOptions(
|
|||||||
const isDefaultRuntime =
|
const isDefaultRuntime =
|
||||||
resolved.command === "node" || resolved.command === "bun";
|
resolved.command === "node" || resolved.command === "bun";
|
||||||
const command = isDefaultRuntime ? process.execPath : resolved.command;
|
const command = isDefaultRuntime ? process.execPath : resolved.command;
|
||||||
const child = spawn(command, resolved.args, {
|
const child = spawn(
|
||||||
|
quoteWindowsCommand(command),
|
||||||
|
resolved.args.map((argument) => quoteWindowsArgument(argument)),
|
||||||
|
{
|
||||||
cwd: spawnOptions.cwd,
|
cwd: spawnOptions.cwd,
|
||||||
env: {
|
env: {
|
||||||
...applyProviderEnv(spawnOptions.env, runtimeSettings),
|
...applyProviderEnv(spawnOptions.env, runtimeSettings),
|
||||||
@@ -222,7 +227,8 @@ function applyRuntimeSettingsToClaudeOptions(
|
|||||||
shell: process.platform === "win32",
|
shell: process.platform === "win32",
|
||||||
signal: spawnOptions.signal,
|
signal: spawnOptions.signal,
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (typeof options.stderr === "function") {
|
if (typeof options.stderr === "function") {
|
||||||
child.stderr?.on("data", (chunk: Buffer | string) => {
|
child.stderr?.on("data", (chunk: Buffer | string) => {
|
||||||
options.stderr?.(chunk.toString());
|
options.stderr?.(chunk.toString());
|
||||||
@@ -1886,6 +1892,10 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
cwd: this.config.cwd,
|
cwd: this.config.cwd,
|
||||||
includePartialMessages: true,
|
includePartialMessages: true,
|
||||||
permissionMode: this.currentMode,
|
permissionMode: this.currentMode,
|
||||||
|
// Dynamic mode switching can recreate the underlying Claude query. Keep the
|
||||||
|
// bypass launch capability available so later setPermissionMode("bypassPermissions")
|
||||||
|
// calls do not fail after a model/thinking/rewind-driven restart.
|
||||||
|
allowDangerouslySkipPermissions: true,
|
||||||
agents: this.defaults?.agents,
|
agents: this.defaults?.agents,
|
||||||
canUseTool: this.handlePermissionRequest,
|
canUseTool: this.handlePermissionRequest,
|
||||||
...(claudeBinary ? { pathToClaudeCodeExecutable: claudeBinary } : {}),
|
...(claudeBinary ? { pathToClaudeCodeExecutable: claudeBinary } : {}),
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
applyProviderEnv,
|
applyProviderEnv,
|
||||||
findExecutable,
|
findExecutable,
|
||||||
|
quoteWindowsArgument,
|
||||||
|
quoteWindowsCommand,
|
||||||
resolveProviderCommandPrefix,
|
resolveProviderCommandPrefix,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
} from "../provider-launch-config.js";
|
} from "../provider-launch-config.js";
|
||||||
@@ -3419,12 +3421,16 @@ export class CodexAppServerAgentClient implements AgentClient {
|
|||||||
},
|
},
|
||||||
"Spawning Codex app server",
|
"Spawning Codex app server",
|
||||||
);
|
);
|
||||||
return spawn(launchPrefix.command, [...launchPrefix.args, "app-server"], {
|
return spawn(
|
||||||
detached: process.platform !== "win32",
|
quoteWindowsCommand(launchPrefix.command),
|
||||||
shell: process.platform === "win32",
|
[...launchPrefix.args, "app-server"].map((argument) => quoteWindowsArgument(argument)),
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
{
|
||||||
env: buildCodexAppServerEnv(this.runtimeSettings, launchEnv),
|
detached: process.platform !== "win32",
|
||||||
});
|
shell: process.platform === "win32",
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
env: buildCodexAppServerEnv(this.runtimeSettings, launchEnv),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createSession(
|
async createSession(
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import type {
|
|||||||
import {
|
import {
|
||||||
applyProviderEnv,
|
applyProviderEnv,
|
||||||
findExecutable,
|
findExecutable,
|
||||||
|
quoteWindowsArgument,
|
||||||
|
quoteWindowsCommand,
|
||||||
resolveProviderCommandPrefix,
|
resolveProviderCommandPrefix,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
} from "../provider-launch-config.js";
|
} from "../provider-launch-config.js";
|
||||||
@@ -333,8 +335,10 @@ export class OpenCodeServerManager {
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.server = spawn(
|
this.server = spawn(
|
||||||
launchPrefix.command,
|
quoteWindowsCommand(launchPrefix.command),
|
||||||
[...launchPrefix.args, "serve", "--port", String(this.port)],
|
[...launchPrefix.args, "serve", "--port", String(this.port)].map((argument) =>
|
||||||
|
quoteWindowsArgument(argument),
|
||||||
|
),
|
||||||
{
|
{
|
||||||
shell: process.platform === "win32",
|
shell: process.platform === "win32",
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
|||||||
@@ -30,7 +30,12 @@ export {
|
|||||||
} from "./speech/providers/local/sherpa/sherpa-runtime-env.js";
|
} from "./speech/providers/local/sherpa/sherpa-runtime-env.js";
|
||||||
|
|
||||||
// Provider binary resolution
|
// Provider binary resolution
|
||||||
export { findExecutable, applyProviderEnv } from "./agent/provider-launch-config.js";
|
export {
|
||||||
|
findExecutable,
|
||||||
|
applyProviderEnv,
|
||||||
|
quoteWindowsArgument,
|
||||||
|
quoteWindowsCommand,
|
||||||
|
} from "./agent/provider-launch-config.js";
|
||||||
|
|
||||||
// Agent SDK types for CLI commands
|
// Agent SDK types for CLI commands
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/website",
|
"name": "@getpaseo/website",
|
||||||
"version": "0.1.41",
|
"version": "0.1.43-rc.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user