Compare commits

...
Author SHA1 Message Date
Mohamed Boudra 58d1767a64 Fix mobile sidebar reset after theme switch 2026-03-28 09:59:50 +07:00
Mohamed Boudra 0ab346e5a2 fix(desktop): restore windows daemon bootstrap logging 2026-03-27 22:48:27 +07:00
Mohamed Boudra a8c04c26b9 fix: hardcode paseo://app as allowed CORS origin in daemon bootstrap
The desktop app uses the paseo:// custom protocol scheme, but the
origin was only passed via PASEO_CORS_ORIGINS when the desktop started
the daemon itself. If the daemon was started by the CLI, the origin
was missing and the Electron renderer's WebSocket connection was
rejected.

Also removes file:// and null from allowed origins — any page loaded
via file:// could connect to the daemon, which is a security gap.
2026-03-27 21:40:02 +07:00
Mohamed Boudra e2b2b7c701 chore(release): cut 0.1.37 2026-03-27 21:11:53 +07:00
Mohamed Boudra af35cccb5f fix(release): skip release notes sync when no changelog entry exists
Draft releases don't have changelog entries, so the script should log
and continue rather than throwing.
2026-03-27 21:10:03 +07:00
github-actions[bot] 6342057a6f fix: update lockfile signatures and Nix hash 2026-03-27 14:00:20 +00:00
Mohamed Boudra 89923b5f76 fix(ci): default to draft release when no GitHub release exists
The fallback was "release", so pushing a tag without pre-creating a
draft release on GitHub caused electron-builder to create a published
release instead of a draft.
2026-03-27 20:59:07 +07:00
Mohamed Boudra 54b5a22688 fix(windows): broken PATH propagation, ps usage, and claude binary resolution
- sherpa-runtime-env: use case-insensitive key lookup when modifying PATH
  on plain env objects. On Windows, `{...process.env}` stores PATH as
  `Path` but `applySherpaLoaderEnv` used hardcoded `"PATH"`, creating a
  duplicate key that could shadow the real system PATH in child processes.

- runtime-toolchain: use `wmic` on Windows instead of Unix-only `ps -o`
  to resolve the node executable path from a PID.

- claude-agent: pass `findExecutable("claude")` as
  `pathToClaudeCodeExecutable` so the SDK uses the user's installed
  binary when available. Update spawn hook to only replace bare
  "node"/"bun" with process.execPath, preserving native binary paths.

- desktop/paseo.cmd: use ELECTRON_RUN_AS_NODE with node-entrypoint-runner
  for the Windows CLI wrapper.
2026-03-27 20:50:18 +07:00
Mohamed Boudra ab3443fd52 feat(desktop): hide native titlebar on Windows/Linux with overlay window controls
Use titleBarStyle: 'hidden' on all platforms. On Windows/Linux, enable
titleBarOverlay with dark theme colors for native min/max/close buttons.
Extend useTrafficLightPadding to return side ('left'|'right'|null) so
consumers apply padding on the correct side per platform.
2026-03-27 19:59:17 +07:00
Mohamed Boudra 95ad879f82 fix: show toast on dictation errors instead of silent console log 2026-03-27 19:27:29 +07:00
Mohamed Boudra 80e153f861 feat(desktop): add file logging with electron-log
Logs from both main and renderer processes now persist to
~/Library/Logs/Paseo/main.log (macOS) with automatic rotation.
Removes the unused webview_log IPC handler.
2026-03-27 19:22:40 +07:00
Mohamed Boudra dfa376f5ca docs: add 0.1.36 changelog entry 2026-03-27 18:48:06 +07:00
Mohamed Boudra 76c357c88c fix: run release:check before version bump to prevent dirty state on failure 2026-03-27 18:44:18 +07:00
github-actions[bot] 5adde123e3 fix: update lockfile signatures and Nix hash 2026-03-27 11:32:43 +00:00
36 changed files with 370 additions and 154 deletions
+3 -3
View File
@@ -124,7 +124,7 @@ jobs:
release_type="release"
fi
else
release_type="release"
release_type="draft"
fi
echo "RELEASE_TYPE=$release_type" >> "$GITHUB_ENV"
@@ -230,7 +230,7 @@ jobs:
release_type="release"
fi
else
release_type="release"
release_type="draft"
fi
echo "RELEASE_TYPE=$release_type" >> "$GITHUB_ENV"
@@ -339,7 +339,7 @@ jobs:
release_type="release"
fi
else
release_type="release"
release_type="draft"
fi
echo "RELEASE_TYPE=$release_type" >> "$GITHUB_ENV"
+9
View File
@@ -1,5 +1,14 @@
# Changelog
## 0.1.36 - 2026-03-27
### Fixed
- Fixed Windows drive-letter path handling across the codebase.
- Fixed stale Nix hash with automatic lockfile-change detection.
### Added
- Added metrics collection and terminal performance tests.
## 0.1.35 - 2026-03-26
### Improved
+1 -1
View File
@@ -15,8 +15,8 @@ If asked to "release paseo" without specifying major/minor, treat it as a patch
## Manual step-by-step
```bash
npm run release:check # Typecheck, build, dry-run pack
npm run version:all:patch # Bump version, create commit + tag
npm run release:check # Validate release
npm run release:publish # Publish to npm
npm run release:push # Push HEAD + tag (triggers CI workflows)
```
+1 -1
View File
@@ -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-xj5pSRE/F8zvrOcVgc22bSHG4GTFzVHgB6eFSs+eI8Y=";
npmDepsHash = "sha256-jBl1E7cwEf5XKXFQip/ZlEa/zNQoz0Hq5t0hqjp8Qe4=";
# 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).
+29 -19
View File
@@ -1,12 +1,12 @@
{
"name": "paseo",
"version": "0.1.36",
"version": "0.1.37",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "paseo",
"version": "0.1.36",
"version": "0.1.37",
"hasInstallScript": true,
"license": "AGPL-3.0-or-later",
"workspaces": [
@@ -16718,6 +16718,15 @@
"node": ">=8"
}
},
"node_modules/electron-log": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.3.tgz",
"integrity": "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/electron-publish": {
"version": "26.8.1",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz",
@@ -34842,16 +34851,16 @@
},
"packages/app": {
"name": "@getpaseo/app",
"version": "0.1.36",
"version": "0.1.37",
"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.36",
"@getpaseo/highlight": "0.1.36",
"@getpaseo/server": "0.1.36",
"@getpaseo/expo-two-way-audio": "0.1.37",
"@getpaseo/highlight": "0.1.37",
"@getpaseo/server": "0.1.37",
"@gorhom/bottom-sheet": "^5.2.6",
"@gorhom/portal": "^1.0.14",
"@react-native-async-storage/async-storage": "2.2.0",
@@ -34967,11 +34976,11 @@
},
"packages/cli": {
"name": "@getpaseo/cli",
"version": "0.1.36",
"version": "0.1.37",
"dependencies": {
"@clack/prompts": "^1.0.0",
"@getpaseo/relay": "0.1.36",
"@getpaseo/server": "0.1.36",
"@getpaseo/relay": "0.1.37",
"@getpaseo/server": "0.1.37",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",
@@ -35012,11 +35021,12 @@
},
"packages/desktop": {
"name": "@getpaseo/desktop",
"version": "0.1.36",
"version": "0.1.37",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@getpaseo/cli": "0.1.36",
"@getpaseo/server": "0.1.36",
"@getpaseo/cli": "0.1.37",
"@getpaseo/server": "0.1.37",
"electron-log": "^5.4.3",
"electron-updater": "^6.6.2",
"ws": "^8.14.2"
},
@@ -35049,7 +35059,7 @@
},
"packages/expo-two-way-audio": {
"name": "@getpaseo/expo-two-way-audio",
"version": "0.1.36",
"version": "0.1.37",
"license": "MIT",
"devDependencies": {
"@biomejs/biome": "1.9.4",
@@ -35250,7 +35260,7 @@
},
"packages/highlight": {
"name": "@getpaseo/highlight",
"version": "0.1.36",
"version": "0.1.37",
"dependencies": {
"@lezer/common": "^1.5.0",
"@lezer/cpp": "^1.1.5",
@@ -35276,7 +35286,7 @@
},
"packages/relay": {
"name": "@getpaseo/relay",
"version": "0.1.36",
"version": "0.1.37",
"dependencies": {
"base64-js": "^1.5.1",
"tweetnacl": "^1.0.3",
@@ -35292,13 +35302,13 @@
},
"packages/server": {
"name": "@getpaseo/server",
"version": "0.1.36",
"version": "0.1.37",
"dependencies": {
"@ai-sdk/openai": "2.0.52",
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
"@deepgram/sdk": "^3.4.0",
"@getpaseo/highlight": "0.1.36",
"@getpaseo/relay": "0.1.36",
"@getpaseo/highlight": "0.1.37",
"@getpaseo/relay": "0.1.37",
"@modelcontextprotocol/sdk": "^1.20.1",
"@opencode-ai/sdk": "1.2.6",
"@sctg/sentencepiece-js": "^1.1.0",
@@ -35686,7 +35696,7 @@
},
"packages/website": {
"name": "@getpaseo/website",
"version": "0.1.36",
"version": "0.1.37",
"dependencies": {
"@cloudflare/vite-plugin": "^1.20.3",
"@cloudflare/workers-types": "^4.20260114.0",
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "paseo",
"version": "0.1.36",
"version": "0.1.37",
"private": true,
"workspaces": [
"packages/expo-two-way-audio",
@@ -49,13 +49,13 @@
"release:publish": "npm publish --workspace=@getpaseo/highlight --access public && npm publish --workspace=@getpaseo/relay --access public && npm publish --workspace=@getpaseo/server --access public && npm publish --workspace=@getpaseo/cli --access public",
"release:push": "node scripts/push-current-release-tag.mjs",
"draft-release:push": "node scripts/push-current-release-tag.mjs --draft-release",
"draft-release:patch": "npm run version:all:patch && npm run release:check && npm run draft-release:push",
"draft-release:minor": "npm run version:all:minor && npm run release:check && npm run draft-release:push",
"draft-release:major": "npm run version:all:major && npm run release:check && npm run draft-release:push",
"draft-release:patch": "npm run release:check && npm run version:all:patch && npm run draft-release:push",
"draft-release:minor": "npm run release:check && npm run version:all:minor && npm run draft-release:push",
"draft-release:major": "npm run release:check && npm run version:all:major && npm run draft-release:push",
"release:finalize": "node scripts/finalize-current-release.mjs",
"release:patch": "npm run version:all:patch && npm run release:check && npm run release:publish && npm run release:push",
"release:minor": "npm run version:all:minor && npm run release:check && npm run release:publish && npm run release:push",
"release:major": "npm run version:all:major && npm run release:check && npm run release:publish && npm run release:push"
"release:patch": "npm run release:check && npm run version:all:patch && npm run release:publish && npm run release:push",
"release:minor": "npm run release:check && npm run version:all:minor && npm run release:publish && npm run release:push",
"release:major": "npm run release:check && npm run version:all:major && npm run release:publish && npm run release:push"
},
"devDependencies": {
"@biomejs/biome": "^2.4.8",
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@getpaseo/app",
"main": "index.ts",
"version": "0.1.36",
"version": "0.1.37",
"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.36",
"@getpaseo/highlight": "0.1.36",
"@getpaseo/server": "0.1.36",
"@getpaseo/expo-two-way-audio": "0.1.37",
"@getpaseo/highlight": "0.1.37",
"@getpaseo/server": "0.1.37",
"@gorhom/bottom-sheet": "^5.2.6",
"@gorhom/portal": "^1.0.14",
"@react-native-async-storage/async-storage": "2.2.0",
@@ -6,7 +6,6 @@ import {
HEADER_INNER_HEIGHT,
HEADER_INNER_HEIGHT_MOBILE,
HEADER_TOP_PADDING_MOBILE,
getIsDesktopMac,
} from "@/constants/layout";
import { useDesktopDragHandlers, useTrafficLightPadding } from "@/utils/desktop-window";
import { usePanelStore } from "@/stores/panel-store";
@@ -31,8 +30,10 @@ export function ScreenHeader({ left, right, leftStyle, rightStyle }: ScreenHeade
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets
const topPadding = isMobile ? HEADER_TOP_PADDING_MOBILE : 0;
const baseHorizontalPadding = theme.spacing[2];
const collapsedSidebarTrafficLightInset =
!isMobile && !desktopAgentListOpen && getIsDesktopMac() ? trafficLightPadding.left : 0;
const collapsedSidebarInset =
!isMobile && !desktopAgentListOpen && trafficLightPadding.side
? trafficLightPadding
: { left: 0, right: 0 };
const dragHandlers = useDesktopDragHandlers();
@@ -42,7 +43,10 @@ export function ScreenHeader({ left, right, leftStyle, rightStyle }: ScreenHeade
<View
style={[
styles.row,
{ paddingLeft: baseHorizontalPadding + collapsedSidebarTrafficLightInset },
{
paddingLeft: baseHorizontalPadding + collapsedSidebarInset.left,
paddingRight: baseHorizontalPadding + collapsedSidebarInset.right,
},
]}
{...dragHandlers}
>
+1 -1
View File
@@ -681,7 +681,7 @@ function DesktopSidebar({
return (
<Animated.View style={[styles.desktopSidebar, resizeAnimatedStyle]}>
{trafficLightPadding.top > 0 ? (
{trafficLightPadding.side === 'left' ? (
<View style={{ height: trafficLightPadding.top }} {...dragHandlers} />
) : null}
<View style={styles.sidebarHeader} {...dragHandlers}>
@@ -346,9 +346,13 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
[onChangeText, onSubmit, images, isAgentRunning],
);
const handleDictationError = useCallback((error: Error) => {
console.error("[MessageInput] Dictation error:", error);
}, []);
const handleDictationError = useCallback(
(error: Error) => {
console.error("[MessageInput] Dictation error:", error);
toast.error(error.message);
},
[toast],
);
const dictationUnavailableMessage = resolveVoiceUnavailableMessage({
serverInfo,
@@ -886,7 +886,10 @@ function SplitPaneView({
style={[
styles.paneTabs,
isFocusModeEnabled &&
trafficLightPadding.left > 0 && { paddingLeft: trafficLightPadding.left },
trafficLightPadding.side && {
paddingLeft: trafficLightPadding.left,
paddingRight: trafficLightPadding.right,
},
]}
>
<WorkspaceDesktopTabsRow
+4
View File
@@ -19,6 +19,10 @@ export const MAX_CONTENT_WIDTH = 820;
export const DESKTOP_TRAFFIC_LIGHT_WIDTH = 78;
export const DESKTOP_TRAFFIC_LIGHT_HEIGHT = 45;
// Windows/Linux window controls (minimize/maximize/close) — top-right
export const DESKTOP_WINDOW_CONTROLS_WIDTH = 140;
export const DESKTOP_WINDOW_CONTROLS_HEIGHT = 48;
// Check if running in desktop app (any OS)
function isDesktopEnvironment(): boolean {
if (Platform.OS !== "web") return false;
@@ -4,6 +4,10 @@ import { useSharedValue, withTiming, Easing, type SharedValue } from "react-nati
import { type GestureType } from "react-native-gesture-handler";
import { UnistylesRuntime } from "react-native-unistyles";
import { usePanelStore } from "@/stores/panel-store";
import {
getRightSidebarAnimationTargets,
shouldSyncSidebarAnimation,
} from "@/utils/sidebar-animation-state";
const ANIMATION_DURATION = 220;
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
@@ -31,55 +35,59 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
const isOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
// Right sidebar: closed = +windowWidth (off-screen right), open = 0
const translateX = useSharedValue(isOpen ? 0 : windowWidth);
const backdropOpacity = useSharedValue(isOpen ? 1 : 0);
const initialTargets = getRightSidebarAnimationTargets({ isOpen, windowWidth });
const translateX = useSharedValue(initialTargets.translateX);
const backdropOpacity = useSharedValue(initialTargets.backdropOpacity);
const isGesturing = useSharedValue(false);
const closeGestureRef = useRef<GestureType | undefined>(undefined);
// Track previous isOpen to detect changes
const prevIsOpen = useRef(isOpen);
const prevWindowWidth = useRef(windowWidth);
// Sync animation with store state changes (e.g., backdrop tap, programmatic open/close)
useEffect(() => {
// Skip if this is initial render or if we're mid-gesture
if (prevIsOpen.current === isOpen) {
return;
}
const didStateChange = shouldSyncSidebarAnimation({
previousIsOpen: prevIsOpen.current,
nextIsOpen: isOpen,
previousWindowWidth: prevWindowWidth.current,
nextWindowWidth: windowWidth,
});
const previousIsOpen = prevIsOpen.current;
prevIsOpen.current = isOpen;
prevWindowWidth.current = windowWidth;
if (!didStateChange) {
return;
}
// Don't animate if we're in the middle of a gesture - the gesture handler will handle it
if (isGesturing.value) {
return;
}
if (isOpen) {
translateX.value = withTiming(0, {
const targets = getRightSidebarAnimationTargets({ isOpen, windowWidth });
if (previousIsOpen !== isOpen) {
translateX.value = withTiming(targets.translateX, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
backdropOpacity.value = withTiming(1, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
} else {
translateX.value = withTiming(windowWidth, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
backdropOpacity.value = withTiming(0, {
backdropOpacity.value = withTiming(targets.backdropOpacity, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
return;
}
translateX.value = targets.translateX;
backdropOpacity.value = targets.backdropOpacity;
}, [
isOpen,
translateX,
backdropOpacity,
windowWidth,
isGesturing,
mobileView,
desktopFileExplorerOpen,
]);
const animateToOpen = () => {
@@ -12,6 +12,10 @@ import { useSharedValue, withTiming, Easing, type SharedValue } from "react-nati
import { type GestureType } from "react-native-gesture-handler";
import { UnistylesRuntime } from "react-native-unistyles";
import { usePanelStore } from "@/stores/panel-store";
import {
getLeftSidebarAnimationTargets,
shouldSyncSidebarAnimation,
} from "@/utils/sidebar-animation-state";
const ANIMATION_DURATION = 220;
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
@@ -38,46 +42,53 @@ export function SidebarAnimationProvider({ children }: { children: ReactNode })
const isOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen;
// Initialize based on current state
const translateX = useSharedValue(isOpen ? 0 : -windowWidth);
const backdropOpacity = useSharedValue(isOpen ? 1 : 0);
const initialTargets = getLeftSidebarAnimationTargets({ isOpen, windowWidth });
const translateX = useSharedValue(initialTargets.translateX);
const backdropOpacity = useSharedValue(initialTargets.backdropOpacity);
const isGesturing = useSharedValue(false);
const closeGestureRef = useRef<GestureType | undefined>(undefined);
// Track previous isOpen to detect changes
const prevIsOpen = useRef(isOpen);
const prevWindowWidth = useRef(windowWidth);
// Sync animation with store state changes (e.g., backdrop tap, programmatic open/close)
useEffect(() => {
// Skip if this is initial render or if we're mid-gesture
if (prevIsOpen.current === isOpen) {
const didStateChange = shouldSyncSidebarAnimation({
previousIsOpen: prevIsOpen.current,
nextIsOpen: isOpen,
previousWindowWidth: prevWindowWidth.current,
nextWindowWidth: windowWidth,
});
const previousIsOpen = prevIsOpen.current;
prevIsOpen.current = isOpen;
prevWindowWidth.current = windowWidth;
if (!didStateChange) {
return;
}
prevIsOpen.current = isOpen;
// Don't animate if we're in the middle of a gesture - the gesture handler will handle it
if (isGesturing.value) {
return;
}
if (isOpen) {
translateX.value = withTiming(0, {
const targets = getLeftSidebarAnimationTargets({ isOpen, windowWidth });
if (previousIsOpen !== isOpen) {
translateX.value = withTiming(targets.translateX, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
backdropOpacity.value = withTiming(1, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
} else {
translateX.value = withTiming(-windowWidth, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
backdropOpacity.value = withTiming(0, {
backdropOpacity.value = withTiming(targets.backdropOpacity, {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
return;
}
translateX.value = targets.translateX;
backdropOpacity.value = targets.backdropOpacity;
}, [isOpen, translateX, backdropOpacity, windowWidth, isGesturing]);
const animateToOpen = useCallback(() => {
+26 -11
View File
@@ -1162,19 +1162,34 @@ export class HostRuntimeStore {
}
private async bootstrapDesktop(): Promise<void> {
try {
const daemon = await startDesktopDaemon();
const connection = connectionFromListen(daemon.listen);
if (!connection || !daemon.serverId) {
let lastError: unknown = null;
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
const daemon = await startDesktopDaemon();
const connection = connectionFromListen(daemon.listen);
if (!connection || !daemon.serverId) {
return;
}
await this.upsertHostConnection({
serverId: daemon.serverId,
label: daemon.hostname ?? undefined,
connection,
});
return;
} catch (error) {
lastError = error;
console.warn(`[HostRuntime] Failed to bootstrap desktop daemon (attempt ${attempt}/3)`, error);
if (attempt < 3) {
await new Promise((resolve) => {
setTimeout(resolve, attempt * 500);
});
}
}
await this.upsertHostConnection({
serverId: daemon.serverId,
label: daemon.hostname ?? undefined,
connection,
});
} catch (error) {
console.warn("[HostRuntime] Failed to bootstrap desktop daemon", error);
}
if (lastError) {
console.warn("[HostRuntime] Desktop daemon bootstrap exhausted retries", lastError);
}
}
@@ -7,7 +7,6 @@ import { PaseoLogo } from "@/components/icons/paseo-logo";
import { SidebarMenuToggle } from "@/components/headers/menu-header";
import { useOpenProjectPicker } from "@/hooks/use-open-project-picker";
import { usePanelStore } from "@/stores/panel-store";
import { getIsDesktopMac } from "@/constants/layout";
import { useDesktopDragHandlers, useTrafficLightPadding } from "@/utils/desktop-window";
export function OpenProjectScreen({ serverId }: { serverId: string }) {
@@ -19,8 +18,10 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) {
const openProjectPicker = useOpenProjectPicker(serverId);
const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const needsTrafficLightInset = !isMobile && !desktopAgentListOpen && getIsDesktopMac();
const trafficLightInset = needsTrafficLightInset ? trafficLightPadding.left : 0;
const collapsedSidebarInset =
!isMobile && !desktopAgentListOpen && trafficLightPadding.side
? trafficLightPadding
: { left: 0, right: 0 };
const dragHandlers = useDesktopDragHandlers();
useEffect(() => {
@@ -31,7 +32,7 @@ export function OpenProjectScreen({ serverId }: { serverId: string }) {
return (
<View style={styles.container} {...dragHandlers}>
<View style={[styles.menuToggle, { paddingTop: insets.top, paddingLeft: trafficLightInset }]}>
<View style={[styles.menuToggle, { paddingTop: insets.top, paddingLeft: collapsedSidebarInset.left, paddingRight: collapsedSidebarInset.right }]}>
<SidebarMenuToggle />
</View>
<View style={styles.content}>
@@ -896,12 +896,6 @@ export default function SettingsScreen() {
const handleThemeChange = useCallback(
(newTheme: AppSettings["theme"]) => {
void updateSettings({ theme: newTheme });
if (newTheme === "auto") {
UnistylesRuntime.setAdaptiveThemes(true);
} else {
UnistylesRuntime.setAdaptiveThemes(false);
UnistylesRuntime.setTheme(newTheme);
}
},
[updateSettings],
);
+20 -6
View File
@@ -2,8 +2,11 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { Platform, type PointerEvent as RNPointerEvent, type ViewProps } from "react-native";
import {
getIsDesktopMac,
getIsDesktop,
DESKTOP_TRAFFIC_LIGHT_WIDTH,
DESKTOP_TRAFFIC_LIGHT_HEIGHT,
DESKTOP_WINDOW_CONTROLS_WIDTH,
DESKTOP_WINDOW_CONTROLS_HEIGHT,
} from "@/constants/layout";
import { getDesktopWindow } from "@/desktop/electron/window";
import { isDesktop } from "@/desktop/host";
@@ -120,11 +123,11 @@ export function useDesktopDragHandlers(): DesktopDragViewProps {
}, [isActive]);
}
export function useTrafficLightPadding(): { left: number; top: number } {
export function useTrafficLightPadding(): { left: number; right: number; top: number; side: 'left' | 'right' | null } {
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
if (Platform.OS !== "web" || !getIsDesktopMac()) return;
if (Platform.OS !== "web" || !getIsDesktop()) return;
let disposed = false;
let cleanup: (() => void) | undefined;
@@ -175,12 +178,23 @@ export function useTrafficLightPadding(): { left: number; top: number } {
};
}, []);
if (!getIsDesktopMac() || isFullscreen) {
return { left: 0, top: 0 };
if (!getIsDesktop() || isFullscreen) {
return { left: 0, right: 0, top: 0, side: null };
}
if (getIsDesktopMac()) {
return {
left: DESKTOP_TRAFFIC_LIGHT_WIDTH,
right: 0,
top: DESKTOP_TRAFFIC_LIGHT_HEIGHT,
side: 'left',
};
}
return {
left: DESKTOP_TRAFFIC_LIGHT_WIDTH,
top: DESKTOP_TRAFFIC_LIGHT_HEIGHT,
left: 0,
right: DESKTOP_WINDOW_CONTROLS_WIDTH,
top: DESKTOP_WINDOW_CONTROLS_HEIGHT,
side: 'right',
};
}
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import {
getLeftSidebarAnimationTargets,
getRightSidebarAnimationTargets,
shouldSyncSidebarAnimation,
} from "./sidebar-animation-state";
describe("sidebar-animation-state", () => {
it("requests a sync when the open state changes", () => {
expect(
shouldSyncSidebarAnimation({
previousIsOpen: false,
nextIsOpen: true,
previousWindowWidth: 390,
nextWindowWidth: 390,
}),
).toBe(true);
});
it("requests a sync when the viewport width changes", () => {
expect(
shouldSyncSidebarAnimation({
previousIsOpen: false,
nextIsOpen: false,
previousWindowWidth: 390,
nextWindowWidth: 430,
}),
).toBe(true);
});
it("keeps the left sidebar fully off-screen when closed", () => {
expect(getLeftSidebarAnimationTargets({ isOpen: false, windowWidth: 430 })).toEqual({
translateX: -430,
backdropOpacity: 0,
});
});
it("keeps the right sidebar fully off-screen when closed", () => {
expect(getRightSidebarAnimationTargets({ isOpen: false, windowWidth: 430 })).toEqual({
translateX: 430,
backdropOpacity: 0,
});
});
});
@@ -0,0 +1,41 @@
interface SidebarAnimationSyncInput {
previousIsOpen: boolean;
nextIsOpen: boolean;
previousWindowWidth: number;
nextWindowWidth: number;
}
interface SidebarAnimationTargetInput {
isOpen: boolean;
windowWidth: number;
}
interface SidebarAnimationTargets {
translateX: number;
backdropOpacity: number;
}
export function shouldSyncSidebarAnimation(input: SidebarAnimationSyncInput): boolean {
return (
input.previousIsOpen !== input.nextIsOpen ||
input.previousWindowWidth !== input.nextWindowWidth
);
}
export function getLeftSidebarAnimationTargets(
input: SidebarAnimationTargetInput,
): SidebarAnimationTargets {
return {
translateX: input.isOpen ? 0 : -input.windowWidth,
backdropOpacity: input.isOpen ? 1 : 0,
};
}
export function getRightSidebarAnimationTargets(
input: SidebarAnimationTargetInput,
): SidebarAnimationTargets {
return {
translateX: input.isOpen ? 0 : input.windowWidth,
backdropOpacity: input.isOpen ? 1 : 0,
};
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/cli",
"version": "0.1.36",
"version": "0.1.37",
"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.36",
"@getpaseo/server": "0.1.36",
"@getpaseo/relay": "0.1.37",
"@getpaseo/server": "0.1.37",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",
@@ -1,4 +1,5 @@
import { spawnSync } from "node:child_process";
import { platform } from "node:os";
export interface NodePathFromPidResult {
nodePath: string | null;
@@ -12,17 +13,14 @@ function normalizeError(error: unknown): string {
return String(error);
}
export function resolveNodePathFromPid(pid: number): NodePathFromPidResult {
function resolveNodePathFromPidUnix(pid: number): NodePathFromPidResult {
const result = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
if (result.error) {
return {
nodePath: null,
error: `ps failed: ${normalizeError(result.error)}`,
};
return { nodePath: null, error: `ps failed: ${normalizeError(result.error)}` };
}
if ((result.status ?? 1) !== 0) {
@@ -34,12 +32,36 @@ export function resolveNodePathFromPid(pid: number): NodePathFromPidResult {
}
const resolved = result.stdout.trim();
if (!resolved) {
return resolved ? { nodePath: resolved } : { nodePath: null, error: "ps returned an empty command path" };
}
function resolveNodePathFromPidWindows(pid: number): NodePathFromPidResult {
const result = spawnSync(
"wmic",
["process", "where", `ProcessId=${pid}`, "get", "ExecutablePath", "/VALUE"],
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
);
if (result.error) {
return { nodePath: null, error: `wmic failed: ${normalizeError(result.error)}` };
}
if ((result.status ?? 1) !== 0) {
const details = result.stderr?.trim();
return {
nodePath: null,
error: "ps returned an empty command path",
error: details ? `wmic failed: ${details}` : `wmic exited with code ${result.status ?? 1}`,
};
}
return { nodePath: resolved };
// wmic output format: "ExecutablePath=C:\path\to\node.exe\r\n"
const match = result.stdout.match(/ExecutablePath=(.+)/);
const resolved = match?.[1]?.trim();
return resolved ? { nodePath: resolved } : { nodePath: null, error: "wmic returned no executable path" };
}
export function resolveNodePathFromPid(pid: number): NodePathFromPidResult {
return platform() === "win32"
? resolveNodePathFromPidWindows(pid)
: resolveNodePathFromPidUnix(pid);
}
+3 -2
View File
@@ -9,5 +9,6 @@ if not exist "%APP_EXECUTABLE%" (
exit /b 1
)
set "PASEO_DESKTOP_CLI=1"
"%APP_EXECUTABLE%" %*
set "ELECTRON_RUN_AS_NODE=1"
"%APP_EXECUTABLE%" "%RESOURCES_DIR%\app.asar\dist\daemon\node-entrypoint-runner.js" bare "%RESOURCES_DIR%\app.asar\node_modules\@getpaseo\cli\dist\index.js" %*
exit /b %errorlevel%
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/desktop",
"version": "0.1.36",
"version": "0.1.37",
"private": true,
"description": "Paseo desktop app (Electron wrapper)",
"main": "dist/main.js",
@@ -12,8 +12,9 @@
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@getpaseo/cli": "0.1.36",
"@getpaseo/server": "0.1.36",
"@getpaseo/cli": "0.1.37",
"@getpaseo/server": "0.1.37",
"electron-log": "^5.4.3",
"electron-updater": "^6.6.2",
"ws": "^8.14.2"
},
@@ -148,8 +148,6 @@ function buildDesktopDaemonCorsOriginsEnv(): string | undefined {
.filter((value) => value.length > 0),
);
origins.add("paseo://app");
const devServerUrl = process.env.EXPO_DEV_URL ?? DEFAULT_ELECTRON_DEV_SERVER_URL;
try {
const parsed = new URL(devServerUrl);
@@ -545,12 +543,6 @@ export function createDaemonCommandHandlers(): Record<string, DesktopCommandHand
return downloadAndInstallUpdate(currentVersion);
},
get_local_daemon_version: () => getLocalDaemonVersion(),
webview_log: (args) => {
const level = typeof args?.level === "number" ? args.level : 1;
const message = typeof args?.message === "string" ? args.message : "";
const method = level === 0 ? "debug" : level === 2 ? "warn" : level >= 3 ? "error" : "info";
console[method]("[webview]", message);
},
};
}
+14 -2
View File
@@ -1,3 +1,6 @@
import log from "electron-log/main";
log.initialize({ spyRendererConsole: true });
import path from "node:path";
import { pathToFileURL } from "node:url";
import { existsSync } from "node:fs";
@@ -89,8 +92,17 @@ async function createMainWindow(): Promise<void> {
height: 800,
show: false,
...(iconPath ? { icon: iconPath } : {}),
titleBarStyle: isMac ? "hidden" : "default",
trafficLightPosition: isMac ? { x: 16, y: 14 } : undefined,
titleBarStyle: "hidden",
...(isMac
? { trafficLightPosition: { x: 16, y: 14 } }
: {
titleBarOverlay: {
color: "#18181c",
symbolColor: "#e4e4e7",
height: 48,
},
autoHideMenuBar: true,
}),
webPreferences: {
preload: getPreloadPath(),
contextIsolation: true,
+1
View File
@@ -1,3 +1,4 @@
import "electron-log/preload";
import { contextBridge, ipcRenderer } from "electron";
type EventHandler = (payload: unknown) => void;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/expo-two-way-audio",
"version": "0.1.36",
"version": "0.1.37",
"description": "Native module for two way audio streaming",
"main": "build/index.js",
"types": "build/index.d.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/highlight",
"version": "0.1.36",
"version": "0.1.37",
"type": "module",
"publishConfig": {
"access": "public"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/relay",
"version": "0.1.36",
"version": "0.1.37",
"description": "Paseo relay for bridging daemon and client connections",
"type": "module",
"publishConfig": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/server",
"version": "0.1.36",
"version": "0.1.37",
"description": "Paseo backend server",
"type": "module",
"publishConfig": {
@@ -63,8 +63,8 @@
"@ai-sdk/openai": "2.0.52",
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
"@deepgram/sdk": "^3.4.0",
"@getpaseo/highlight": "0.1.36",
"@getpaseo/relay": "0.1.36",
"@getpaseo/highlight": "0.1.37",
"@getpaseo/relay": "0.1.37",
"@modelcontextprotocol/sdk": "^1.20.1",
"@opencode-ai/sdk": "1.2.6",
"@sctg/sentencepiece-js": "^1.1.0",
@@ -67,7 +67,11 @@ import type {
McpServerConfig,
PersistedAgentDescriptor,
} from "../agent-sdk-types.js";
import { applyProviderEnv, type ProviderRuntimeSettings } from "../provider-launch-config.js";
import {
applyProviderEnv,
findExecutable,
type ProviderRuntimeSettings,
} from "../provider-launch-config.js";
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
const fsPromises = promises;
@@ -201,11 +205,14 @@ function applyRuntimeSettingsToClaudeOptions(
...options,
spawnClaudeCodeProcess: (spawnOptions) => {
const resolved = resolveClaudeSpawnCommand(spawnOptions, runtimeSettings);
// The SDK defaults to spawning "node" via PATH lookup, which fails when
// running from the managed runtime bundle where node isn't in PATH.
// Always use process.execPath — the actual node binary running the daemon.
const command =
resolved.command === spawnOptions.command ? process.execPath : resolved.command;
// When the SDK passes a default JS runtime ("node"/"bun"), replace it with
// process.execPath — the actual node binary running the daemon. This avoids
// PATH lookup failures in the managed runtime bundle.
// When the SDK passes a native binary path (from pathToClaudeCodeExecutable)
// or the user overrides the command via runtime settings, use that directly.
const isDefaultRuntime =
resolved.command === "node" || resolved.command === "bun";
const command = isDefaultRuntime ? process.execPath : resolved.command;
const child = spawn(command, resolved.args, {
cwd: spawnOptions.cwd,
env: {
@@ -1855,12 +1862,14 @@ class ClaudeAgentSession implements AgentSession {
.filter((entry): entry is string => typeof entry === "string" && entry.length > 0)
.join("\n\n");
const claudeBinary = findExecutable("claude");
const base: ClaudeOptions = {
cwd: this.config.cwd,
includePartialMessages: true,
permissionMode: this.currentMode,
agents: this.defaults?.agents,
canUseTool: this.handlePermissionRequest,
...(claudeBinary ? { pathToClaudeCodeExecutable: claudeBinary } : {}),
// Use Claude Code preset system prompt and load CLAUDE.md files
// Append provider-agnostic system prompt and orchestrator instructions for agents.
systemPrompt: {
+2 -3
View File
@@ -246,9 +246,8 @@ export async function createPaseoDaemon(
// CORS - allow same-origin + configured origins
const allowedOrigins = new Set([
...config.corsAllowedOrigins,
// Packaged desktop renderers may send `file://` or `null` as the origin.
"file://",
"null",
// Packaged desktop renderers use the custom paseo:// protocol scheme.
"paseo://app",
// For TCP, add localhost variants
...(listenTarget.type === "tcp"
? [
@@ -70,6 +70,21 @@ export function resolveSherpaLoaderEnv(
}
}
/**
* Find the actual case-sensitive key in a plain object that matches the given
* key case-insensitively. On Windows, `{...process.env}` produces a plain
* (case-sensitive) object where PATH is typically stored as `Path`. Using a
* hardcoded `"PATH"` would miss the existing key and create a duplicate,
* breaking the child process's PATH.
*/
function findEnvKey(env: NodeJS.ProcessEnv, key: string): string {
const lower = key.toLowerCase();
for (const k of Object.keys(env)) {
if (k.toLowerCase() === lower) return k;
}
return key;
}
export function applySherpaLoaderEnv(
env: NodeJS.ProcessEnv,
platform: NodeJS.Platform = process.platform,
@@ -90,9 +105,10 @@ export function applySherpaLoaderEnv(
};
}
const next = prependEnvPath(env[resolved.key], resolved.libDir);
const changed = next !== (env[resolved.key] ?? "");
env[resolved.key] = next;
const actualKey = findEnvKey(env, resolved.key);
const next = prependEnvPath(env[actualKey], resolved.libDir);
const changed = next !== (env[actualKey] ?? "");
env[actualKey] = next;
return {
changed,
key: resolved.key,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/website",
"version": "0.1.36",
"version": "0.1.37",
"private": true,
"type": "module",
"scripts": {
@@ -150,7 +150,8 @@ function main() {
const targetEntry = entries.find((entry) => entry.tag === targetTag);
if (!targetEntry) {
throw new Error(`No matching changelog section found for ${targetTag}.`);
console.log(`No matching changelog section found for ${targetTag}. Skipping.`);
return;
}
const tempDir = mkdtempSync(path.join(tmpdir(), "paseo-release-notes-"));