diff --git a/CHANGELOG.md b/CHANGELOG.md index 57abaf94f..4f4f05f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 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 ### Fixed diff --git a/nix/package.nix b/nix/package.nix index 13b6c4151..695c396b8 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -42,7 +42,7 @@ buildNpmPackage rec { # To update: run `nix build` with lib.fakeHash, copy the `got:` hash. # 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 # npm rebuild (it tries to download from api.nuget.org, which fails in the sandbox). diff --git a/package-lock.json b/package-lock.json index dac19fcf8..f2d41da9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "paseo", - "version": "0.1.41", + "version": "0.1.43-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "paseo", - "version": "0.1.41", + "version": "0.1.43-rc.1", "hasInstallScript": true, "license": "AGPL-3.0-or-later", "workspaces": [ @@ -36285,16 +36285,16 @@ }, "packages/app": { "name": "@getpaseo/app", - "version": "0.1.41", + "version": "0.1.43-rc.1", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@expo/vector-icons": "^15.0.2", "@floating-ui/react-native": "^0.10.7", - "@getpaseo/expo-two-way-audio": "0.1.41", - "@getpaseo/highlight": "0.1.41", - "@getpaseo/server": "0.1.41", + "@getpaseo/expo-two-way-audio": "0.1.43-rc.1", + "@getpaseo/highlight": "0.1.43-rc.1", + "@getpaseo/server": "0.1.43-rc.1", "@gorhom/bottom-sheet": "^5.2.6", "@gorhom/portal": "^1.0.14", "@react-native-async-storage/async-storage": "2.2.0", @@ -36411,11 +36411,11 @@ }, "packages/cli": { "name": "@getpaseo/cli", - "version": "0.1.41", + "version": "0.1.43-rc.1", "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/relay": "0.1.41", - "@getpaseo/server": "0.1.41", + "@getpaseo/relay": "0.1.43-rc.1", + "@getpaseo/server": "0.1.43-rc.1", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", @@ -36456,11 +36456,11 @@ }, "packages/desktop": { "name": "@getpaseo/desktop", - "version": "0.1.41", + "version": "0.1.43-rc.1", "license": "AGPL-3.0-or-later", "dependencies": { - "@getpaseo/cli": "0.1.41", - "@getpaseo/server": "0.1.41", + "@getpaseo/cli": "0.1.43-rc.1", + "@getpaseo/server": "0.1.43-rc.1", "electron-log": "^5.4.3", "electron-updater": "^6.6.2", "ws": "^8.14.2" @@ -36494,7 +36494,7 @@ }, "packages/expo-two-way-audio": { "name": "@getpaseo/expo-two-way-audio", - "version": "0.1.41", + "version": "0.1.43-rc.1", "license": "MIT", "devDependencies": { "@biomejs/biome": "1.9.4", @@ -36695,7 +36695,7 @@ }, "packages/highlight": { "name": "@getpaseo/highlight", - "version": "0.1.41", + "version": "0.1.43-rc.1", "dependencies": { "@lezer/common": "^1.5.0", "@lezer/cpp": "^1.1.5", @@ -36721,7 +36721,7 @@ }, "packages/relay": { "name": "@getpaseo/relay", - "version": "0.1.41", + "version": "0.1.43-rc.1", "dependencies": { "base64-js": "^1.5.1", "tweetnacl": "^1.0.3", @@ -36737,13 +36737,13 @@ }, "packages/server": { "name": "@getpaseo/server", - "version": "0.1.41", + "version": "0.1.43-rc.1", "dependencies": { "@ai-sdk/openai": "2.0.52", "@anthropic-ai/claude-agent-sdk": "^0.2.11", "@deepgram/sdk": "^3.4.0", - "@getpaseo/highlight": "0.1.41", - "@getpaseo/relay": "0.1.41", + "@getpaseo/highlight": "0.1.43-rc.1", + "@getpaseo/relay": "0.1.43-rc.1", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.2.6", @@ -37145,7 +37145,7 @@ }, "packages/website": { "name": "@getpaseo/website", - "version": "0.1.41", + "version": "0.1.43-rc.1", "dependencies": { "@cloudflare/vite-plugin": "^1.20.3", "@cloudflare/workers-types": "^4.20260114.0", diff --git a/package.json b/package.json index 571940c13..4a4f5654f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paseo", - "version": "0.1.41", + "version": "0.1.43-rc.1", "private": true, "workspaces": [ "packages/expo-two-way-audio", diff --git a/packages/app/package.json b/packages/app/package.json index c8ef0d999..f73a78665 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,7 +1,7 @@ { "name": "@getpaseo/app", "main": "index.ts", - "version": "0.1.41", + "version": "0.1.43-rc.1", "private": true, "scripts": { "start": "expo start", @@ -31,9 +31,9 @@ "@dnd-kit/utilities": "^3.2.2", "@expo/vector-icons": "^15.0.2", "@floating-ui/react-native": "^0.10.7", - "@getpaseo/expo-two-way-audio": "0.1.41", - "@getpaseo/highlight": "0.1.41", - "@getpaseo/server": "0.1.41", + "@getpaseo/expo-two-way-audio": "0.1.43-rc.1", + "@getpaseo/highlight": "0.1.43-rc.1", + "@getpaseo/server": "0.1.43-rc.1", "@gorhom/bottom-sheet": "^5.2.6", "@gorhom/portal": "^1.0.14", "@react-native-async-storage/async-storage": "2.2.0", diff --git a/packages/app/src/components/draggable-list.web.tsx b/packages/app/src/components/draggable-list.web.tsx index c2e435826..260e9388b 100644 --- a/packages/app/src/components/draggable-list.web.tsx +++ b/packages/app/src/components/draggable-list.web.tsx @@ -20,7 +20,7 @@ import { } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; 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 }; @@ -133,8 +133,11 @@ export function DraggableList({ const [activeId, setActiveId] = useState(null); const [dragItems, setDragItems] = useState(null); const items = dragItems ?? data; + const showCustomScrollbar = enableDesktopWebScrollbar && scrollEnabled; const scrollViewRef = useRef(null); - const scrollbarMetrics = useWebDesktopScrollbarMetrics(); + const scrollbar = useWebScrollViewScrollbar(scrollViewRef, { + enabled: showCustomScrollbar, + }); const sensors = useSensors( useSensor(PointerSensor, { @@ -177,7 +180,6 @@ export function DraggableList({ ); const ids = items.map((item, index) => keyExtractor(item, index)); - const showCustomScrollbar = enableDesktopWebScrollbar && scrollEnabled; const wrapperStyle = [ { position: "relative" as const }, scrollEnabled ? { flex: 1, minHeight: 0 } : null, @@ -193,12 +195,10 @@ export function DraggableList({ style={style} contentContainerStyle={contentContainerStyle} showsVerticalScrollIndicator={showCustomScrollbar ? false : showsVerticalScrollIndicator} - onLayout={showCustomScrollbar ? scrollbarMetrics.onLayout : undefined} - onContentSizeChange={ - showCustomScrollbar ? scrollbarMetrics.onContentSizeChange : undefined - } - onScroll={showCustomScrollbar ? scrollbarMetrics.onScroll : undefined} - scrollEventThrottle={showCustomScrollbar ? 16 : undefined} + onLayout={scrollbar.onLayout} + onContentSizeChange={scrollbar.onContentSizeChange} + onScroll={scrollbar.onScroll} + scrollEventThrottle={16} > {ListHeaderComponent} {items.length === 0 && ListEmptyComponent} @@ -259,13 +259,7 @@ export function DraggableList({ {ListFooterComponent} )} - { - scrollViewRef.current?.scrollTo({ y: nextOffset, animated: false }); - }} - /> + {scrollbar.overlay} ); } diff --git a/packages/app/src/components/file-explorer-pane.tsx b/packages/app/src/components/file-explorer-pane.tsx index 5c0632072..08a6dd284 100644 --- a/packages/app/src/components/file-explorer-pane.tsx +++ b/packages/app/src/components/file-explorer-pane.tsx @@ -4,9 +4,6 @@ import { ActivityIndicator, FlatList, ListRenderItemInfo, - type LayoutChangeEvent, - type NativeScrollEvent, - type NativeSyntheticEvent, Pressable, Text, View, @@ -53,10 +50,7 @@ import { buildWorkspaceExplorerStateKey } from "@/hooks/use-file-explorer-action import { usePanelStore, type SortOption } from "@/stores/panel-store"; import { formatTimeAgo } from "@/utils/time"; import { buildAbsoluteExplorerPath } from "@/utils/explorer-paths"; -import { - WebDesktopScrollbarOverlay, - useWebDesktopScrollbarMetrics, -} from "@/components/web-desktop-scrollbar"; +import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar"; const SORT_OPTIONS: { value: SortOption; label: string }[] = [ { value: "name", label: "Name" }, @@ -152,7 +146,9 @@ export function FileExplorerPane({ const [expandedPaths, setExpandedPaths] = useState>(() => new Set(["."])); const treeListRef = useRef>(null); - const treeScrollbarMetrics = useWebDesktopScrollbarMetrics(); + const scrollbar = useWebScrollViewScrollbar(treeListRef, { + enabled: showDesktopWebScrollbar, + }); const hasInitializedRef = useRef(false); @@ -502,24 +498,6 @@ export function FileExplorerPane({ }); }, [errorRecoveryPath, hasWorkspaceScope, requestDirectoryListing, selectExplorerEntry]); - const handleTreeListScroll = useCallback( - (event: NativeSyntheticEvent) => { - if (showDesktopWebScrollbar) { - treeScrollbarMetrics.onScroll(event); - } - }, - [showDesktopWebScrollbar, treeScrollbarMetrics], - ); - - const handleTreeListLayout = useCallback( - (event: LayoutChangeEvent) => { - if (showDesktopWebScrollbar) { - treeScrollbarMetrics.onLayout(event); - } - }, - [showDesktopWebScrollbar, treeScrollbarMetrics], - ); - if (!hasWorkspaceScope) { return ( @@ -598,27 +576,16 @@ export function FileExplorerPane({ keyExtractor={(row) => row.entry.path} testID="file-explorer-tree-scroll" contentContainerStyle={styles.entriesContent} - onLayout={showDesktopWebScrollbar ? handleTreeListLayout : undefined} - onScroll={showDesktopWebScrollbar ? handleTreeListScroll : undefined} - onContentSizeChange={ - showDesktopWebScrollbar ? treeScrollbarMetrics.onContentSizeChange : undefined - } - scrollEventThrottle={showDesktopWebScrollbar ? 16 : undefined} + onLayout={scrollbar.onLayout} + onScroll={scrollbar.onScroll} + onContentSizeChange={scrollbar.onContentSizeChange} + scrollEventThrottle={16} showsVerticalScrollIndicator={!showDesktopWebScrollbar} initialNumToRender={24} maxToRenderPerBatch={40} windowSize={12} /> - { - treeListRef.current?.scrollToOffset({ - offset: nextOffset, - animated: false, - }); - }} - /> + {scrollbar.overlay} )} diff --git a/packages/app/src/components/file-pane.tsx b/packages/app/src/components/file-pane.tsx index 2f375cf60..b92e204e3 100644 --- a/packages/app/src/components/file-pane.tsx +++ b/packages/app/src/components/file-pane.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useRef } from "react"; +import React, { useMemo, useRef } from "react"; import { useQuery } from "@tanstack/react-query"; import { ActivityIndicator, @@ -7,17 +7,11 @@ import { Text, View, Platform, - type LayoutChangeEvent, - type NativeScrollEvent, - type NativeSyntheticEvent, } from "react-native"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { Fonts } from "@/constants/theme"; import { useSessionStore, type ExplorerFile } from "@/stores/session-store"; -import { - WebDesktopScrollbarOverlay, - useWebDesktopScrollbarMetrics, -} from "@/components/web-desktop-scrollbar"; +import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar"; import { highlightCode, darkHighlightColors, @@ -123,9 +117,10 @@ function FilePreviewBody({ const colorMap = isDark ? darkHighlightColors : lightHighlightColors; const baseColor = isDark ? "#c9d1d9" : "#24292f"; - const enablePreviewDesktopScrollbar = showDesktopWebScrollbar; const previewScrollRef = useRef(null); - const previewScrollbarMetrics = useWebDesktopScrollbarMetrics(); + const scrollbar = useWebScrollViewScrollbar(previewScrollRef, { + enabled: showDesktopWebScrollbar, + }); const highlightedLines = useMemo(() => { if (!preview || preview.kind !== "text") { @@ -140,24 +135,6 @@ function FilePreviewBody({ return lineNumberGutterWidth(highlightedLines.length); }, [highlightedLines]); - const handlePreviewScroll = useCallback( - (event: NativeSyntheticEvent) => { - if (enablePreviewDesktopScrollbar) { - previewScrollbarMetrics.onScroll(event); - } - }, - [enablePreviewDesktopScrollbar, previewScrollbarMetrics], - ); - - const handlePreviewLayout = useCallback( - (event: LayoutChangeEvent) => { - if (enablePreviewDesktopScrollbar) { - previewScrollbarMetrics.onLayout(event); - } - }, - [enablePreviewDesktopScrollbar, previewScrollbarMetrics], - ); - if (isLoading && !preview) { return ( @@ -197,13 +174,11 @@ function FilePreviewBody({ {isMobile ? ( {codeLines} @@ -218,13 +193,7 @@ function FilePreviewBody({ )} - { - previewScrollRef.current?.scrollTo({ y: nextOffset, animated: false }); - }} - /> + {scrollbar.overlay} ); } @@ -236,13 +205,11 @@ function FilePreviewBody({ ref={previewScrollRef} style={styles.previewContent} contentContainerStyle={styles.previewImageScrollContent} - onLayout={enablePreviewDesktopScrollbar ? handlePreviewLayout : undefined} - onScroll={enablePreviewDesktopScrollbar ? handlePreviewScroll : undefined} - onContentSizeChange={ - enablePreviewDesktopScrollbar ? previewScrollbarMetrics.onContentSizeChange : undefined - } - scrollEventThrottle={enablePreviewDesktopScrollbar ? 16 : undefined} - showsVerticalScrollIndicator={!enablePreviewDesktopScrollbar} + onLayout={scrollbar.onLayout} + onScroll={scrollbar.onScroll} + onContentSizeChange={scrollbar.onContentSizeChange} + scrollEventThrottle={16} + showsVerticalScrollIndicator={!showDesktopWebScrollbar} > - { - previewScrollRef.current?.scrollTo({ y: nextOffset, animated: false }); - }} - /> + {scrollbar.overlay} ); } diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx index 12a85fe3a..07968af47 100644 --- a/packages/app/src/components/git-diff-pane.tsx +++ b/packages/app/src/components/git-diff-pane.tsx @@ -62,10 +62,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip import { GitHubIcon } from "@/components/icons/github-icon"; import { buildGitActions, type GitActions } from "@/components/git-actions-policy"; import { lineNumberGutterWidth } from "@/components/code-insets"; -import { - WebDesktopScrollbarOverlay, - useWebDesktopScrollbarMetrics, -} from "@/components/web-desktop-scrollbar"; +import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar"; import { buildNewAgentRoute, resolveNewAgentWorkingDir } from "@/utils/new-agent-routing"; import { openExternalUrl } from "@/utils/open-external-url"; 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 [expandedByPath, setExpandedByPath] = useState>({}); const diffListRef = useRef>(null); - const diffScrollbarMetrics = useWebDesktopScrollbarMetrics(); + const scrollbar = useWebScrollViewScrollbar(diffListRef, { + enabled: showDesktopWebScrollbar, + }); const diffListScrollOffsetRef = useRef(0); const diffListViewportHeightRef = useRef(0); const headerHeightByPathRef = useRef>({}); @@ -515,11 +514,9 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi const handleDiffListScroll = useCallback( (event: NativeSyntheticEvent) => { diffListScrollOffsetRef.current = event.nativeEvent.contentOffset.y; - if (showDesktopWebScrollbar) { - diffScrollbarMetrics.onScroll(event); - } + scrollbar.onScroll(event); }, - [diffScrollbarMetrics, showDesktopWebScrollbar], + [scrollbar.onScroll], ); const handleDiffListLayout = useCallback( @@ -529,11 +526,9 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi return; } diffListViewportHeightRef.current = height; - if (showDesktopWebScrollbar) { - diffScrollbarMetrics.onLayout(event); - } + scrollbar.onLayout(event); }, - [diffScrollbarMetrics, showDesktopWebScrollbar], + [scrollbar.onLayout], ); const computeHeaderOffset = useCallback( @@ -844,9 +839,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi testID="git-diff-scroll" onLayout={handleDiffListLayout} onScroll={handleDiffListScroll} - onContentSizeChange={ - showDesktopWebScrollbar ? diffScrollbarMetrics.onContentSizeChange : undefined - } + onContentSizeChange={scrollbar.onContentSizeChange} scrollEventThrottle={16} showsVerticalScrollIndicator={!showDesktopWebScrollbar} onRefresh={handleRefresh} @@ -1085,16 +1078,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi {bodyContent} - { - diffListRef.current?.scrollToOffset({ - offset: nextOffset, - animated: false, - }); - }} - /> + {hasChanges ? scrollbar.overlay : null} ); diff --git a/packages/app/src/components/message-input.tsx b/packages/app/src/components/message-input.tsx index 7cbaa9e79..8a2127589 100644 --- a/packages/app/src/components/message-input.tsx +++ b/packages/app/src/components/message-input.tsx @@ -12,7 +12,15 @@ import { Platform, BackHandler, } 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 { Mic, MicOff, ArrowUp, Paperclip, Plus, X, Square } from "lucide-react-native"; 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 { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Shortcut } from "@/components/ui/shortcut"; +import { useWebElementScrollbar } from "@/components/use-web-scrollbar"; import { useShortcutKeys } from "@/hooks/use-shortcut-keys"; import type { MessageInputKeyboardActionKind } from "@/keyboard/actions"; import { @@ -572,6 +581,18 @@ export const MessageInput = forwardRef(funct return null; }, []); + const webTextareaRef = useRef(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 ref = target === "root" ? rootRef.current : inputWrapperRef.current; if (!ref) return null; @@ -913,42 +934,45 @@ export const MessageInput = forwardRef(funct )} {/* Text input */} - { - isInputFocusedRef.current = true; - onFocusChange?.(true); - }} - onBlur={() => { - isInputFocusedRef.current = false; - onFocusChange?.(false); - }} - style={[ - styles.textInput, - IS_WEB - ? { - height: inputHeight, - 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} - onContentSizeChange={handleContentSizeChange} - editable={!isDictating && !isRealtimeVoiceForCurrentAgent && !disabled} - onKeyPress={shouldHandleDesktopSubmit ? handleDesktopKeyPress : undefined} - onSelectionChange={handleSelectionChange} - autoFocus={IS_WEB && autoFocus} - /> + + { + isInputFocusedRef.current = true; + onFocusChange?.(true); + }} + onBlur={() => { + isInputFocusedRef.current = false; + onFocusChange?.(false); + }} + style={[ + styles.textInput, + IS_WEB + ? { + height: inputHeight, + 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} + onContentSizeChange={handleContentSizeChange} + editable={!isDictating && !isRealtimeVoiceForCurrentAgent && !disabled} + onKeyPress={shouldHandleDesktopSubmit ? handleDesktopKeyPress : undefined} + onSelectionChange={handleSelectionChange} + autoFocus={IS_WEB && autoFocus} + /> + {inputScrollbar} + {/* Button row */} @@ -1189,6 +1213,9 @@ const styles = StyleSheet.create(((theme: any) => ({ removeImageButtonVisible: { opacity: 1, }, + textInputScrollWrapper: { + position: "relative", + }, textInput: { width: "100%", color: theme.colors.foreground, diff --git a/packages/app/src/components/stream-strategy-web.tsx b/packages/app/src/components/stream-strategy-web.tsx index 3768bb898..2189a67b6 100644 --- a/packages/app/src/components/stream-strategy-web.tsx +++ b/packages/app/src/components/stream-strategy-web.tsx @@ -23,21 +23,7 @@ const WEB_BOTTOM_SETTLE_TIMEOUT_MS = 200; const USER_SCROLL_DELTA_EPSILON = 1; const AUTO_SCROLL_BOTTOM_THRESHOLD_PX = 64; const AUTO_SCROLL_RESUME_THRESHOLD_PX = 1; -const WEB_STREAM_SCROLLBAR_STYLE_ID = "web-stream-viewport-scrollbar-style"; -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; - } -`; +import { useWebElementScrollbar } from "./use-web-scrollbar"; function logWebStickyBottom(_event: string, _details: Record): void { // Intentionally disabled: this path is too noisy during voice debugging. @@ -119,8 +105,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool scrollEnabled, isMobileBreakpoint, } = props; - const { WebDesktopScrollbarOverlay, useWebDesktopScrollbarMetrics } = - require("./web-desktop-scrollbar") as typeof import("./web-desktop-scrollbar"); const scrollContainerRef = useRef(null); const contentRef = useRef(null); const [followOutput, setFollowOutputr] = useState(true); @@ -142,8 +126,11 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool const lastTouchClientYRef = useRef(null); const pendingAutoScrollFrameRef = useRef(null); const pendingAutoScrollTimeoutRef = useRef(null); - const streamScrollbarMetrics = useWebDesktopScrollbarMetrics(); const showDesktopWebScrollbar = !isMobileBreakpoint; + const scrollbarOverlay = useWebElementScrollbar(scrollContainerRef, { + enabled: showDesktopWebScrollbar, + contentRef, + }); const shouldUseVirtualizer = segments.historyVirtualized.length > 0; const { renderHistoryVirtualizedRow, @@ -271,33 +258,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool onNearBottomChange(true); 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); const currentMetrics = { scrollTop: scrollContainer.scrollTop, @@ -323,7 +283,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool ...currentMetrics, }); } - }, [onNearBottomChange, props.agentId, streamScrollbarMetrics]); + }, [onNearBottomChange, props.agentId]); const handleDomScroll = useCallback(() => { const scrollContainer = scrollContainerRef.current; @@ -711,7 +671,6 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool return ( <> -
{ scrollContainerRef.current = node; @@ -759,20 +718,7 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool {shouldRenderEmpty ? listEmptyComponent : null}
- { - const scrollContainer = scrollContainerRef.current; - if (!scrollContainer) { - return; - } - scrollContainer.scrollTo({ top: nextOffset, behavior: "auto" }); - lastKnownScrollTopRef.current = scrollContainer.scrollTop; - updateScrollMetrics(); - }} - /> + {scrollbarOverlay} ); } diff --git a/packages/app/src/components/use-web-scrollbar.tsx b/packages/app/src/components/use-web-scrollbar.tsx new file mode 100644 index 000000000..5b4f95948 --- /dev/null +++ b/packages/app/src/components/use-web-scrollbar.tsx @@ -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, + options?: { + enabled?: boolean; + contentRef?: RefObject; + }, +): ReactNode { + const isWeb = Platform.OS === "web"; + const enabled = (options?.enabled ?? true) && isWeb; + const contentRef = options?.contentRef; + + const [metrics, setMetrics] = useState({ + 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 ; +} + +// ── 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) => void; + onLayout: (event: LayoutChangeEvent) => void; + onContentSizeChange: (width: number, height: number) => void; + overlay: ReactNode; +} + +export function useWebScrollViewScrollbar( + scrollableRef: RefObject, + 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 ? ( + + ) : null; + + return { + onScroll: metricsHook.onScroll, + onLayout: metricsHook.onLayout, + onContentSizeChange: metricsHook.onContentSizeChange, + overlay, + }; +} diff --git a/packages/app/src/components/web-desktop-scrollbar.tsx b/packages/app/src/components/web-desktop-scrollbar.tsx index 62f7b063b..48a2211ef 100644 --- a/packages/app/src/components/web-desktop-scrollbar.tsx +++ b/packages/app/src/components/web-desktop-scrollbar.tsx @@ -37,7 +37,7 @@ function clamp(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)); } -type ScrollbarMetrics = { +export type ScrollbarMetrics = { offset: number; viewportSize: number; contentSize: number; diff --git a/packages/app/src/styles/theme.ts b/packages/app/src/styles/theme.ts index a38b15958..027f0eca9 100644 --- a/packages/app/src/styles/theme.ts +++ b/packages/app/src/styles/theme.ts @@ -193,7 +193,7 @@ const darkSemanticColors = { foregroundMuted: "#A1A5A4", // Controls - scrollbarHandle: "#71717a", // zinc-500 + scrollbarHandle: "#717574", // zinc-500 w/ teal tint // Borders border: "#252B2A", diff --git a/packages/cli/package.json b/packages/cli/package.json index e887042fa..8cc2eef5b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "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", "type": "module", "files": [ @@ -24,8 +24,8 @@ }, "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/relay": "0.1.41", - "@getpaseo/server": "0.1.41", + "@getpaseo/relay": "0.1.43-rc.1", + "@getpaseo/server": "0.1.43-rc.1", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", diff --git a/packages/cli/src/commands/daemon/status.ts b/packages/cli/src/commands/daemon/status.ts index 132fe4876..d37194100 100644 --- a/packages/cli/src/commands/daemon/status.ts +++ b/packages/cli/src/commands/daemon/status.ts @@ -1,7 +1,12 @@ import type { Command } from "commander"; import { execFileSync } from "node:child_process"; 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 type { CommandOptions, ListResult, OutputSchema } from "../../output/index.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); try { - const output = execFileSync(binaryPath, ["--version"], { + const output = execFileSync(quoteWindowsCommand(binaryPath), ["--version"], { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "pipe"], diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 991ff9034..d604ebf93 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/desktop", - "version": "0.1.41", + "version": "0.1.43-rc.1", "private": true, "description": "Paseo desktop app (Electron wrapper)", "main": "dist/main.js", @@ -12,8 +12,8 @@ "typecheck": "tsc --noEmit -p tsconfig.json" }, "dependencies": { - "@getpaseo/cli": "0.1.41", - "@getpaseo/server": "0.1.41", + "@getpaseo/cli": "0.1.43-rc.1", + "@getpaseo/server": "0.1.43-rc.1", "electron-log": "^5.4.3", "electron-updater": "^6.6.2", "ws": "^8.14.2" diff --git a/packages/expo-two-way-audio/package.json b/packages/expo-two-way-audio/package.json index 08db49318..a21e63bbe 100644 --- a/packages/expo-two-way-audio/package.json +++ b/packages/expo-two-way-audio/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/expo-two-way-audio", - "version": "0.1.41", + "version": "0.1.43-rc.1", "description": "Native module for two way audio streaming", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/packages/highlight/package.json b/packages/highlight/package.json index 6d6cf6db1..4330afae6 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/highlight", - "version": "0.1.41", + "version": "0.1.43-rc.1", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/relay/package.json b/packages/relay/package.json index 6cc6a8323..b82960091 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/relay", - "version": "0.1.41", + "version": "0.1.43-rc.1", "description": "Paseo relay for bridging daemon and client connections", "type": "module", "publishConfig": { diff --git a/packages/server/package.json b/packages/server/package.json index 77d96f035..80956df3a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/server", - "version": "0.1.41", + "version": "0.1.43-rc.1", "description": "Paseo backend server", "type": "module", "publishConfig": { @@ -64,8 +64,8 @@ "@ai-sdk/openai": "2.0.52", "@anthropic-ai/claude-agent-sdk": "^0.2.11", "@deepgram/sdk": "^3.4.0", - "@getpaseo/highlight": "0.1.41", - "@getpaseo/relay": "0.1.41", + "@getpaseo/highlight": "0.1.43-rc.1", + "@getpaseo/relay": "0.1.43-rc.1", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.2.6", diff --git a/packages/server/src/server/agent/provider-launch-config.test.ts b/packages/server/src/server/agent/provider-launch-config.test.ts index 6ce4ca983..a2b674558 100644 --- a/packages/server/src/server/agent/provider-launch-config.test.ts +++ b/packages/server/src/server/agent/provider-launch-config.test.ts @@ -1,7 +1,9 @@ -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { findExecutable, + quoteWindowsArgument, + quoteWindowsCommand, resolveProviderCommandPrefix, applyProviderEnv, type ProviderRuntimeSettings, @@ -218,3 +220,75 @@ describe("findExecutable", () => { 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"); + }); +}); diff --git a/packages/server/src/server/agent/provider-launch-config.ts b/packages/server/src/server/agent/provider-launch-config.ts index 393f95965..ae80f1d97 100644 --- a/packages/server/src/server/agent/provider-launch-config.ts +++ b/packages/server/src/server/agent/provider-launch-config.ts @@ -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 { return findExecutable(command) !== null; } diff --git a/packages/server/src/server/agent/providers/claude-agent.integration.test.ts b/packages/server/src/server/agent/providers/claude-agent.integration.test.ts index 10f10f01c..e109d47e4 100644 --- a/packages/server/src/server/agent/providers/claude-agent.integration.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.integration.test.ts @@ -213,6 +213,25 @@ describe("ClaudeAgentSession integration", () => { } }, 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)( "supportedModels returns the current abstract Claude SDK model shape", async () => { diff --git a/packages/server/src/server/agent/providers/claude-agent.redesign.test.ts b/packages/server/src/server/agent/providers/claude-agent.redesign.test.ts index 37fd2a201..74bd40656 100644 --- a/packages/server/src/server/agent/providers/claude-agent.redesign.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.redesign.test.ts @@ -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 () => { const session = await createSession(); const internal = session as unknown as { diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index d15c18ff5..fa8fb3674 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -70,6 +70,8 @@ import type { import { applyProviderEnv, findExecutable, + quoteWindowsArgument, + quoteWindowsCommand, type ProviderRuntimeSettings, } from "../provider-launch-config.js"; import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js"; @@ -213,7 +215,10 @@ function applyRuntimeSettingsToClaudeOptions( const isDefaultRuntime = resolved.command === "node" || resolved.command === "bun"; 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, env: { ...applyProviderEnv(spawnOptions.env, runtimeSettings), @@ -222,7 +227,8 @@ function applyRuntimeSettingsToClaudeOptions( shell: process.platform === "win32", signal: spawnOptions.signal, stdio: ["pipe", "pipe", "pipe"], - }); + }, + ); if (typeof options.stderr === "function") { child.stderr?.on("data", (chunk: Buffer | string) => { options.stderr?.(chunk.toString()); @@ -1886,6 +1892,10 @@ class ClaudeAgentSession implements AgentSession { cwd: this.config.cwd, includePartialMessages: true, 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, canUseTool: this.handlePermissionRequest, ...(claudeBinary ? { pathToClaudeCodeExecutable: claudeBinary } : {}), diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index ccd83cf8c..c0052a14f 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -42,6 +42,8 @@ import { import { applyProviderEnv, findExecutable, + quoteWindowsArgument, + quoteWindowsCommand, resolveProviderCommandPrefix, type ProviderRuntimeSettings, } from "../provider-launch-config.js"; @@ -3419,12 +3421,16 @@ export class CodexAppServerAgentClient implements AgentClient { }, "Spawning Codex app server", ); - return spawn(launchPrefix.command, [...launchPrefix.args, "app-server"], { - detached: process.platform !== "win32", - shell: process.platform === "win32", - stdio: ["pipe", "pipe", "pipe"], - env: buildCodexAppServerEnv(this.runtimeSettings, launchEnv), - }); + return spawn( + quoteWindowsCommand(launchPrefix.command), + [...launchPrefix.args, "app-server"].map((argument) => quoteWindowsArgument(argument)), + { + detached: process.platform !== "win32", + shell: process.platform === "win32", + stdio: ["pipe", "pipe", "pipe"], + env: buildCodexAppServerEnv(this.runtimeSettings, launchEnv), + }, + ); } async createSession( diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index a129675d4..7ac1811dd 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -32,6 +32,8 @@ import type { import { applyProviderEnv, findExecutable, + quoteWindowsArgument, + quoteWindowsCommand, resolveProviderCommandPrefix, type ProviderRuntimeSettings, } from "../provider-launch-config.js"; @@ -333,8 +335,10 @@ export class OpenCodeServerManager { return new Promise((resolve, reject) => { this.server = spawn( - launchPrefix.command, - [...launchPrefix.args, "serve", "--port", String(this.port)], + quoteWindowsCommand(launchPrefix.command), + [...launchPrefix.args, "serve", "--port", String(this.port)].map((argument) => + quoteWindowsArgument(argument), + ), { shell: process.platform === "win32", stdio: ["ignore", "pipe", "pipe"], diff --git a/packages/server/src/server/exports.ts b/packages/server/src/server/exports.ts index a7e2bb8fc..ae059eabd 100644 --- a/packages/server/src/server/exports.ts +++ b/packages/server/src/server/exports.ts @@ -30,7 +30,12 @@ export { } from "./speech/providers/local/sherpa/sherpa-runtime-env.js"; // 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 export type { diff --git a/packages/website/package.json b/packages/website/package.json index c99e517d6..200f49fc8 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/website", - "version": "0.1.41", + "version": "0.1.43-rc.1", "private": true, "type": "module", "scripts": {