mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-15 05:42:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4092c29265 | ||
|
|
5cedc210a8 | ||
|
|
4724d1d9b7 | ||
|
|
8fdca94ead | ||
|
|
88f410567d | ||
|
|
f998ee1691 | ||
|
|
7a47ec0ee9 | ||
|
|
4f796181bf | ||
|
|
f517493591 | ||
|
|
fb74f766c8 | ||
|
|
e268d1a894 | ||
|
|
b5f5832214 |
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.2 - 2026-08-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed workspaces crashing on Android tablets and foldables in non-compact layouts ([#3760](https://github.com/getpaseo/paseo/pull/3760) by [@jeid64](https://github.com/jeid64))
|
||||
- Fixed app settings resetting when builds sharing a profile recognize different settings ([#3787](https://github.com/getpaseo/paseo/pull/3787))
|
||||
|
||||
## 0.5.1 - 2026-08-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed multiline composers stopping resizing after workspace and agent transitions on iOS and Android ([#3740](https://github.com/getpaseo/paseo/pull/3740))
|
||||
|
||||
## 0.5.0 - 2026-08-23
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
sha256-umufh5lfrLUsnvRLzOzw+BYaE/DgH/7x6uGnDwdM4Gk=
|
||||
sha256-o8d/nX0FdM6QB7c/RWpo1USsz8UNmwohSqkS7tflayc=
|
||||
|
||||
Generated
+25
-25
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paseo",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -36140,7 +36140,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "6.10.4",
|
||||
"@codemirror/language": "6.12.4",
|
||||
@@ -37192,12 +37192,12 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/client": "0.5.0",
|
||||
"@getpaseo/protocol": "0.5.0",
|
||||
"@getpaseo/server": "0.5.0",
|
||||
"@getpaseo/client": "0.5.2",
|
||||
"@getpaseo/protocol": "0.5.2",
|
||||
"@getpaseo/server": "0.5.2",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
@@ -37444,10 +37444,10 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@getpaseo/client",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.5.0",
|
||||
"@getpaseo/relay": "0.5.0",
|
||||
"@getpaseo/protocol": "0.5.2",
|
||||
"@getpaseo/relay": "0.5.2",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -37458,7 +37458,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@getpaseo/cli": "*",
|
||||
@@ -37701,7 +37701,7 @@
|
||||
},
|
||||
"packages/expo-two-way-audio": {
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
@@ -38597,7 +38597,7 @@
|
||||
},
|
||||
"packages/highlight": {
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "6.12.4",
|
||||
"@codemirror/legacy-modes": "^6.5.3",
|
||||
@@ -38626,12 +38626,12 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@getpaseo/plugin",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@getpaseo/client": "0.5.0",
|
||||
"@getpaseo/client": "0.5.2",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/react": "~19.2.0",
|
||||
"react": "19.1.0",
|
||||
@@ -38640,14 +38640,14 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@getpaseo/client": "0.5.0",
|
||||
"@getpaseo/client": "0.5.2",
|
||||
"react": "19.1.0",
|
||||
"zod": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
@@ -38660,7 +38660,7 @@
|
||||
},
|
||||
"packages/relay": {
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
@@ -38878,17 +38878,17 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.220",
|
||||
"@anthropic-ai/sdk": "^0.104.2",
|
||||
"@babel/parser": "^7.29.8",
|
||||
"@getpaseo/client": "0.5.0",
|
||||
"@getpaseo/highlight": "0.5.0",
|
||||
"@getpaseo/plugin": "0.5.0",
|
||||
"@getpaseo/protocol": "0.5.0",
|
||||
"@getpaseo/relay": "0.5.0",
|
||||
"@getpaseo/client": "0.5.2",
|
||||
"@getpaseo/highlight": "0.5.2",
|
||||
"@getpaseo/plugin": "0.5.2",
|
||||
"@getpaseo/protocol": "0.5.2",
|
||||
"@getpaseo/relay": "0.5.2",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
@@ -39940,7 +39940,7 @@
|
||||
},
|
||||
"packages/website": {
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.29.1",
|
||||
"@cloudflare/workers-types": "^4.20260317.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"private": true,
|
||||
"description": "Paseo: voice-controlled development environment for local AI coding agents",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { reduceNativeComposerHeight } from "./height-state";
|
||||
|
||||
const bounds = { minHeight: 30, maxHeight: 240 };
|
||||
|
||||
describe("native composer height", () => {
|
||||
it("grows a fresh composer from the content size exposed during caret scrolling", () => {
|
||||
expect(
|
||||
reduceNativeComposerHeight(30, bounds, {
|
||||
type: "content-measured",
|
||||
height: 131,
|
||||
}),
|
||||
).toBe(131);
|
||||
});
|
||||
|
||||
it("remeasures a restored multiline draft before applying its intrinsic height", () => {
|
||||
const invalidatedHeight = reduceNativeComposerHeight(30, bounds, { type: "remeasure" });
|
||||
|
||||
expect(invalidatedHeight).toBeGreaterThan(30);
|
||||
expect(
|
||||
reduceNativeComposerHeight(invalidatedHeight, bounds, {
|
||||
type: "content-measured",
|
||||
height: 173,
|
||||
}),
|
||||
).toBe(173);
|
||||
});
|
||||
|
||||
it("shrinks a grown composer after deletion and the resulting native measurement", () => {
|
||||
const invalidatedHeight = reduceNativeComposerHeight(240, bounds, {
|
||||
type: "text-changed",
|
||||
previousText: "one\ntwo\nthree",
|
||||
nextText: "one",
|
||||
});
|
||||
|
||||
expect(invalidatedHeight).toBeLessThan(240);
|
||||
expect(
|
||||
reduceNativeComposerHeight(invalidatedHeight, bounds, {
|
||||
type: "content-measured",
|
||||
height: 50,
|
||||
}),
|
||||
).toBe(50);
|
||||
});
|
||||
|
||||
it("does not disturb height while text grows", () => {
|
||||
expect(
|
||||
reduceNativeComposerHeight(120, bounds, {
|
||||
type: "text-changed",
|
||||
previousText: "one",
|
||||
nextText: "one two",
|
||||
}),
|
||||
).toBe(120);
|
||||
});
|
||||
|
||||
it("clamps measurements and resets to the current bounds", () => {
|
||||
expect(
|
||||
reduceNativeComposerHeight(30, bounds, {
|
||||
type: "content-measured",
|
||||
height: 999,
|
||||
}),
|
||||
).toBe(240);
|
||||
expect(reduceNativeComposerHeight(180, bounds, { type: "reset" })).toBe(30);
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
export interface ComposerHeightBounds {
|
||||
minHeight: number;
|
||||
maxHeight: number;
|
||||
}
|
||||
|
||||
export type NativeComposerHeightEvent =
|
||||
| { type: "content-measured"; height: number }
|
||||
| { type: "text-changed"; previousText: string; nextText: string }
|
||||
| { type: "remeasure" }
|
||||
| { type: "reset" };
|
||||
|
||||
export function clampComposerHeight(height: number, bounds: ComposerHeightBounds): number {
|
||||
return Math.max(bounds.minHeight, Math.min(bounds.maxHeight, height));
|
||||
}
|
||||
|
||||
export function reduceNativeComposerHeight(
|
||||
currentHeight: number,
|
||||
bounds: ComposerHeightBounds,
|
||||
event: NativeComposerHeightEvent,
|
||||
): number {
|
||||
if (event.type === "reset") return bounds.minHeight;
|
||||
|
||||
if (event.type === "content-measured") {
|
||||
if (!Number.isFinite(event.height) || event.height <= 0) return currentHeight;
|
||||
return clampComposerHeight(event.height, bounds);
|
||||
}
|
||||
|
||||
if (event.type === "remeasure") {
|
||||
const direction = currentHeight <= bounds.minHeight ? 1 : -1;
|
||||
return clampComposerHeight(currentHeight + direction, bounds);
|
||||
}
|
||||
|
||||
if (event.nextText.length >= event.previousText.length || currentHeight <= bounds.minHeight) {
|
||||
return currentHeight;
|
||||
}
|
||||
|
||||
// Fabric can retain the old UITextView content size after deletion while the text still fits
|
||||
// inside its frame. Changing that frame invalidates layout; the following content-size event
|
||||
// supplies the real smaller height.
|
||||
return reduceNativeComposerHeight(currentHeight, bounds, { type: "remeasure" });
|
||||
}
|
||||
@@ -1,100 +1,18 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
import type { RefObject } from "react";
|
||||
import type {
|
||||
NativeSyntheticEvent,
|
||||
TextInputContentSizeChangeEventData,
|
||||
TextInputScrollEventData,
|
||||
TextStyle,
|
||||
} from "react-native";
|
||||
import { reduceNativeComposerHeight, type NativeComposerHeightEvent } from "./height-state";
|
||||
import type { ComposerHeightResult } from "./height.types";
|
||||
|
||||
interface ComposerHeightArgs {
|
||||
value: string;
|
||||
textareaRef: RefObject<unknown>;
|
||||
minHeight: number;
|
||||
maxHeight: number;
|
||||
onHeightChange?: (height: number) => void;
|
||||
}
|
||||
|
||||
interface ComposerHeightResult {
|
||||
style: TextStyle;
|
||||
scrollEnabled: boolean;
|
||||
onTextChange: (previousText: string, nextText: string) => void;
|
||||
onContentSizeChange: (event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => void;
|
||||
onScroll: (event: NativeSyntheticEvent<TextInputScrollEventData>) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export function useComposerHeight({
|
||||
value,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
onHeightChange,
|
||||
}: ComposerHeightArgs): ComposerHeightResult {
|
||||
const [height, setHeight] = useState(minHeight);
|
||||
const heightRef = useRef(minHeight);
|
||||
const didRemeasureInitialValueRef = useRef(false);
|
||||
const onHeightChangeRef = useRef(onHeightChange);
|
||||
onHeightChangeRef.current = onHeightChange;
|
||||
|
||||
const apply = useCallback(
|
||||
(event: NativeComposerHeightEvent) => {
|
||||
const nextHeight = reduceNativeComposerHeight(
|
||||
heightRef.current,
|
||||
{ minHeight, maxHeight },
|
||||
event,
|
||||
);
|
||||
if (Math.abs(nextHeight - heightRef.current) < 1) return;
|
||||
heightRef.current = nextHeight;
|
||||
setHeight(nextHeight);
|
||||
onHeightChangeRef.current?.(nextHeight);
|
||||
},
|
||||
[maxHeight, minHeight],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
apply({ type: "content-measured", height: heightRef.current });
|
||||
}, [apply]);
|
||||
|
||||
useEffect(() => {
|
||||
if (didRemeasureInitialValueRef.current || value.length === 0) return;
|
||||
didRemeasureInitialValueRef.current = true;
|
||||
apply({ type: "remeasure" });
|
||||
}, [apply, value]);
|
||||
|
||||
const onTextChange = useCallback(
|
||||
(previousText: string, nextText: string) => {
|
||||
apply({ type: "text-changed", previousText, nextText });
|
||||
},
|
||||
[apply],
|
||||
);
|
||||
|
||||
const onContentSizeChange = useCallback(
|
||||
(event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
|
||||
apply({ type: "content-measured", height: event.nativeEvent.contentSize.height });
|
||||
},
|
||||
[apply],
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
(event: NativeSyntheticEvent<TextInputScrollEventData>) => {
|
||||
// iOS Fabric can omit the first content-size callback when a fixed-height input overflows.
|
||||
// The caret-scroll event still carries the actual size and bootstraps the same state.
|
||||
const nativeEvent = event.nativeEvent as TextInputScrollEventData & {
|
||||
contentSize?: { width: number; height: number };
|
||||
};
|
||||
const contentHeight = nativeEvent.contentSize?.height;
|
||||
if (contentHeight !== undefined) {
|
||||
apply({ type: "content-measured", height: contentHeight });
|
||||
}
|
||||
},
|
||||
[apply],
|
||||
);
|
||||
|
||||
const reset = useCallback(() => apply({ type: "reset" }), [apply]);
|
||||
const style = useMemo(() => ({ height, minHeight, maxHeight }), [height, maxHeight, minHeight]);
|
||||
|
||||
// A non-scrolling UITextView reports its frame as contentSize. Scrolling stays enabled so
|
||||
// caret overflow can expose the intrinsic size through onScroll.
|
||||
return { style, scrollEnabled: true, onTextChange, onContentSizeChange, onScroll, reset };
|
||||
const style = useMemo(() => ({ minHeight, maxHeight }), [maxHeight, minHeight]);
|
||||
return { mode: "intrinsic", style, scrollEnabled: true };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { TextStyle } from "react-native";
|
||||
|
||||
export type ComposerHeightResult =
|
||||
| {
|
||||
mode: "intrinsic";
|
||||
style: TextStyle;
|
||||
scrollEnabled: true;
|
||||
}
|
||||
| {
|
||||
mode: "measured";
|
||||
style: TextStyle;
|
||||
scrollEnabled: boolean;
|
||||
onTextChange: (previousText: string, nextText: string) => void;
|
||||
reset: () => void;
|
||||
};
|
||||
@@ -1,28 +1,12 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import type { RefObject } from "react";
|
||||
import type {
|
||||
NativeSyntheticEvent,
|
||||
TextInputContentSizeChangeEventData,
|
||||
TextInputScrollEventData,
|
||||
TextStyle,
|
||||
} from "react-native";
|
||||
import { clampComposerHeight } from "./height-state";
|
||||
import type { ComposerHeightResult } from "./height.types";
|
||||
|
||||
interface ComposerHeightArgs {
|
||||
value: string;
|
||||
textareaRef: RefObject<HTMLElement | null>;
|
||||
minHeight: number;
|
||||
maxHeight: number;
|
||||
onHeightChange?: (height: number) => void;
|
||||
}
|
||||
|
||||
interface ComposerHeightResult {
|
||||
style: TextStyle;
|
||||
scrollEnabled: boolean;
|
||||
onTextChange: (previousText: string, nextText: string) => void;
|
||||
onContentSizeChange: (event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => void;
|
||||
onScroll: (event: NativeSyntheticEvent<TextInputScrollEventData>) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const COPIED_STYLES = [
|
||||
@@ -52,28 +36,19 @@ export function useComposerHeight({
|
||||
textareaRef,
|
||||
minHeight,
|
||||
maxHeight,
|
||||
onHeightChange,
|
||||
}: ComposerHeightArgs): ComposerHeightResult {
|
||||
const [height, setHeight] = useState(minHeight);
|
||||
const heightRef = useRef(minHeight);
|
||||
const paramsRef = useRef({ value, minHeight, maxHeight, onHeightChange });
|
||||
paramsRef.current = { value, minHeight, maxHeight, onHeightChange };
|
||||
const paramsRef = useRef({ value, minHeight, maxHeight });
|
||||
paramsRef.current = { value, minHeight, maxHeight };
|
||||
const mirrorRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const setBoundedHeight = useCallback((nextHeight: number) => {
|
||||
const {
|
||||
minHeight: currentMin,
|
||||
maxHeight: currentMax,
|
||||
onHeightChange: onChange,
|
||||
} = paramsRef.current;
|
||||
const bounded = clampComposerHeight(nextHeight, {
|
||||
minHeight: currentMin,
|
||||
maxHeight: currentMax,
|
||||
});
|
||||
const { minHeight: currentMin, maxHeight: currentMax } = paramsRef.current;
|
||||
const bounded = Math.max(currentMin, Math.min(currentMax, nextHeight));
|
||||
if (Math.abs(heightRef.current - bounded) < 1) return;
|
||||
heightRef.current = bounded;
|
||||
setHeight(bounded);
|
||||
onChange?.(bounded);
|
||||
}, []);
|
||||
|
||||
const measure = useCallback(
|
||||
@@ -145,22 +120,13 @@ export function useComposerHeight({
|
||||
[measure],
|
||||
);
|
||||
const reset = useCallback(() => setBoundedHeight(minHeight), [minHeight, setBoundedHeight]);
|
||||
const onContentSizeChange = useCallback(
|
||||
(_event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => undefined,
|
||||
[],
|
||||
);
|
||||
const onScroll = useCallback(
|
||||
(_event: NativeSyntheticEvent<TextInputScrollEventData>) => undefined,
|
||||
[],
|
||||
);
|
||||
const style = useMemo(() => ({ height, minHeight, maxHeight }), [height, maxHeight, minHeight]);
|
||||
|
||||
return {
|
||||
mode: "measured",
|
||||
style,
|
||||
scrollEnabled: height >= maxHeight,
|
||||
onTextChange,
|
||||
onContentSizeChange,
|
||||
onScroll,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ import {
|
||||
Text,
|
||||
useWindowDimensions,
|
||||
NativeSyntheticEvent,
|
||||
TextInputContentSizeChangeEventData,
|
||||
TextInputKeyPressEventData,
|
||||
TextInputScrollEventData,
|
||||
TextInputSelectionChangeEventData,
|
||||
type LayoutChangeEvent,
|
||||
} from "react-native";
|
||||
import {
|
||||
useState,
|
||||
@@ -640,8 +639,6 @@ interface ComposerTextSurfaceProps {
|
||||
editable: boolean;
|
||||
scrollEnabled: boolean;
|
||||
autoFocus: boolean;
|
||||
onContentSizeChange: (event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => void;
|
||||
onScroll: (event: NativeSyntheticEvent<TextInputScrollEventData>) => void;
|
||||
onKeyPress: ((event: WebTextInputKeyPressEvent) => void) | undefined;
|
||||
onSelectionChange: (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => void;
|
||||
onPasteImages: ((files: readonly NativePastedFile[]) => void) | undefined;
|
||||
@@ -680,8 +677,6 @@ function ComposerTextSurface(props: ComposerTextSurfaceProps): React.ReactElemen
|
||||
style={props.textInputStyle}
|
||||
multiline
|
||||
scrollEnabled={props.scrollEnabled}
|
||||
onContentSizeChange={props.onContentSizeChange}
|
||||
onScroll={props.onScroll}
|
||||
editable={props.editable}
|
||||
onKeyPress={props.onKeyPress}
|
||||
onSelectionChange={props.onSelectionChange}
|
||||
@@ -1216,20 +1211,21 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
const selectionRef = useRef({ start: value.length, end: value.length });
|
||||
const appliedTextReplacementKeyRef = useRef(textReplacementKey);
|
||||
const webTextareaRef = useRef<HTMLElement | null>(null);
|
||||
const {
|
||||
style: composerHeightStyle,
|
||||
scrollEnabled: isComposerScrollEnabled,
|
||||
onTextChange: updateComposerHeightForText,
|
||||
onContentSizeChange: handleComposerContentSizeChange,
|
||||
onScroll: handleComposerScroll,
|
||||
reset: resetComposerHeight,
|
||||
} = useComposerHeight({
|
||||
const composerHeight = useComposerHeight({
|
||||
value,
|
||||
textareaRef: webTextareaRef,
|
||||
minHeight: MIN_INPUT_HEIGHT,
|
||||
maxHeight: maxInputHeight,
|
||||
onHeightChange,
|
||||
});
|
||||
const { style: composerHeightStyle, scrollEnabled: isComposerScrollEnabled } = composerHeight;
|
||||
const measuredComposerHeight = composerHeight.mode === "measured" ? composerHeight : undefined;
|
||||
const updateComposerHeightForText = measuredComposerHeight?.onTextChange;
|
||||
const resetComposerHeight = measuredComposerHeight?.reset;
|
||||
|
||||
const handleComposerLayout = useCallback(
|
||||
(event: LayoutChangeEvent) => onHeightChange?.(event.nativeEvent.layout.height),
|
||||
[onHeightChange],
|
||||
);
|
||||
|
||||
const updateLiveTextPresence = useCallback((text: string) => {
|
||||
const nextHasLiveText = text.trim().length > 0;
|
||||
@@ -1240,7 +1236,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
|
||||
const replaceText = useCallback(
|
||||
(nextText: string, selection?: { start: number; end: number }) => {
|
||||
updateComposerHeightForText(valueRef.current, nextText);
|
||||
updateComposerHeightForText?.(valueRef.current, nextText);
|
||||
valueRef.current = nextText;
|
||||
updateLiveTextPresence(nextText);
|
||||
selectionRef.current = selection ?? { start: nextText.length, end: nextText.length };
|
||||
@@ -1293,7 +1289,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
useEffect(() => {
|
||||
if (appliedTextReplacementKeyRef.current === textReplacementKey) return;
|
||||
appliedTextReplacementKeyRef.current = textReplacementKey;
|
||||
updateComposerHeightForText(valueRef.current, value);
|
||||
updateComposerHeightForText?.(valueRef.current, value);
|
||||
valueRef.current = value;
|
||||
updateLiveTextPresence(value);
|
||||
textInputRef.current?.replaceText(value);
|
||||
@@ -1494,7 +1490,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
]);
|
||||
|
||||
const minimizeInputHeight = useCallback(() => {
|
||||
resetComposerHeight();
|
||||
resetComposerHeight?.();
|
||||
}, [resetComposerHeight]);
|
||||
|
||||
const handleSendMessage = useCallback(() => {
|
||||
@@ -1664,7 +1660,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(nextValue: string) => {
|
||||
updateComposerHeightForText(valueRef.current, nextValue);
|
||||
updateComposerHeightForText?.(valueRef.current, nextValue);
|
||||
valueRef.current = nextValue;
|
||||
updateLiveTextPresence(nextValue);
|
||||
onChangeText(nextValue);
|
||||
@@ -1776,7 +1772,12 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
);
|
||||
|
||||
return (
|
||||
<View ref={rootRef} style={styles.container} testID="message-input-root">
|
||||
<View
|
||||
ref={rootRef}
|
||||
style={styles.container}
|
||||
testID="message-input-root"
|
||||
onLayout={handleComposerLayout}
|
||||
>
|
||||
<MessageInputAutoFocus
|
||||
enabled={autoFocus}
|
||||
autoFocusKey={autoFocusKey}
|
||||
@@ -1805,8 +1806,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
editable={!isDictating && !isRealtimeVoiceForCurrentAgent && !disabled}
|
||||
scrollEnabled={isComposerScrollEnabled}
|
||||
autoFocus={false}
|
||||
onContentSizeChange={handleComposerContentSizeChange}
|
||||
onScroll={handleComposerScroll}
|
||||
onKeyPress={shouldHandleWebKeyPress ? handleDesktopKeyPress : undefined}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
onPasteImages={onPasteImages}
|
||||
|
||||
@@ -5,9 +5,9 @@ export const LEGACY_SETTINGS_KEY = "@paseo:settings";
|
||||
|
||||
/**
|
||||
* The applied-migration marker deliberately sits outside the settings blob:
|
||||
* `StoredAppSettingsSchema` is a strict object and `readValidatedJson` clears the value when
|
||||
* parsing fails, so a new field there would wipe every setting for anyone who downgrades to a
|
||||
* client that predates it. Keeping the marker as a list of ids also means the next migration is
|
||||
* a new entry rather than a schema change.
|
||||
* App settings preserve fields newer builds understand, but this marker remains separate so
|
||||
* applying a migration never becomes an app-settings field that older builds must interpret.
|
||||
* Keeping the marker as a list of ids also means the next migration is a new entry rather than
|
||||
* a schema change.
|
||||
*/
|
||||
export const SETTINGS_MIGRATIONS_KEY = "@paseo:settings-migrations";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { readValidatedJson } from "@/storage/validated-storage";
|
||||
import { APP_SETTINGS_KEY, SETTINGS_MIGRATIONS_KEY } from "./keys";
|
||||
import type { AppSettings, KeyValueStorage } from "./storage";
|
||||
import type { AppSettings, KeyValueStorage, PersistedAppSettings } from "./storage";
|
||||
|
||||
const AppliedMigrationsSchema = z.strictObject({ applied: z.array(z.string()) });
|
||||
|
||||
@@ -22,9 +22,14 @@ const STEER_DEFAULT_MIGRATION = "steer-default";
|
||||
export async function migrateAppSettings(
|
||||
settings: AppSettings,
|
||||
storage: KeyValueStorage,
|
||||
stored?: PersistedAppSettings,
|
||||
): Promise<AppSettings> {
|
||||
const stored = await readValidatedJson(storage, SETTINGS_MIGRATIONS_KEY, AppliedMigrationsSchema);
|
||||
const applied = new Set(stored?.applied ?? []);
|
||||
const migrationMarker = await readValidatedJson(
|
||||
storage,
|
||||
SETTINGS_MIGRATIONS_KEY,
|
||||
AppliedMigrationsSchema,
|
||||
);
|
||||
const applied = new Set(migrationMarker?.applied ?? []);
|
||||
if (applied.has(STEER_DEFAULT_MIGRATION)) {
|
||||
return settings;
|
||||
}
|
||||
@@ -32,7 +37,15 @@ export async function migrateAppSettings(
|
||||
const migrated: AppSettings =
|
||||
settings.sendBehavior === "interrupt" ? { ...settings, sendBehavior: "steer" } : settings;
|
||||
if (migrated !== settings) {
|
||||
await storage.setItem(APP_SETTINGS_KEY, JSON.stringify(migrated));
|
||||
const storedSidebarRowItems = stored?.sidebarRowItems ?? {};
|
||||
await storage.setItem(
|
||||
APP_SETTINGS_KEY,
|
||||
JSON.stringify({
|
||||
...stored,
|
||||
...migrated,
|
||||
sidebarRowItems: { ...storedSidebarRowItems, ...migrated.sidebarRowItems },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
applied.add(STEER_DEFAULT_MIGRATION);
|
||||
|
||||
@@ -50,6 +50,28 @@ describe("loadAppSettingsFromStorage", () => {
|
||||
});
|
||||
expect((await loadAppSettingsFromStorage(deps)).sendBehavior).toBe("steer");
|
||||
});
|
||||
|
||||
it("keeps valid settings when another build wrote unknown fields or enum values", async () => {
|
||||
const stored = {
|
||||
theme: "dark",
|
||||
contentFontSize: 16,
|
||||
sendBehavior: "future-mode",
|
||||
futureSetting: { enabled: true },
|
||||
sidebarRowItems: { host: false, futureRowItem: true },
|
||||
};
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
[APP_SETTINGS_KEY]: JSON.stringify(stored),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await loadAppSettingsFromStorage(deps);
|
||||
|
||||
expect(result.theme).toBe("dark");
|
||||
expect(result.sendBehavior).toBe(DEFAULT_CLIENT_SETTINGS.sendBehavior);
|
||||
expect(result.sidebarRowItems.host).toBe(false);
|
||||
expect(JSON.parse(deps.storage.entries.get(APP_SETTINGS_KEY) ?? "null")).toEqual(stored);
|
||||
});
|
||||
it("migrates a stored interrupt to steer and persists it", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
@@ -63,6 +85,32 @@ describe("loadAppSettingsFromStorage", () => {
|
||||
expect(JSON.parse(deps.storage.entries.get(APP_SETTINGS_KEY) ?? "{}").sendBehavior).toBe(
|
||||
"steer",
|
||||
);
|
||||
expect(JSON.parse(deps.storage.entries.get(APP_SETTINGS_KEY) ?? "{}")).not.toHaveProperty(
|
||||
"needsWrite",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps an explicit services choice over the legacy scripts fallback", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
[APP_SETTINGS_KEY]: JSON.stringify({
|
||||
contentFontSize: 16,
|
||||
sidebarRowItems: { scripts: false, services: true },
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
expect((await loadAppSettingsFromStorage(deps)).sidebarRowItems.services).toBe(true);
|
||||
|
||||
await saveAppSettings({
|
||||
queryClient: new QueryClient(),
|
||||
updates: {
|
||||
sidebarRowItems: { ...DEFAULT_SIDEBAR_ROW_ITEMS, services: true },
|
||||
},
|
||||
deps,
|
||||
});
|
||||
|
||||
expect((await loadAppSettingsFromStorage(deps)).sidebarRowItems.services).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps an interrupt the user picked after the migration ran", async () => {
|
||||
@@ -108,8 +156,8 @@ describe("loadAppSettingsFromStorage", () => {
|
||||
|
||||
expect(result).toEqual(DEFAULT_CLIENT_SETTINGS);
|
||||
expect(DEFAULT_CLIENT_SETTINGS.language).toBe("system");
|
||||
expect(deps.storage.entries.get(APP_SETTINGS_KEY)).toBe(
|
||||
JSON.stringify(DEFAULT_CLIENT_SETTINGS),
|
||||
expect(JSON.parse(deps.storage.entries.get(APP_SETTINGS_KEY) ?? "null")).toEqual(
|
||||
DEFAULT_CLIENT_SETTINGS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -235,7 +283,11 @@ describe("loadAppSettingsFromStorage", () => {
|
||||
theme: "dark",
|
||||
contentFontSize: DEFAULT_UI_BASE_FONT_SIZE,
|
||||
});
|
||||
expect(deps.storage.entries.get(APP_SETTINGS_KEY)).toBe(JSON.stringify(result));
|
||||
expect(JSON.parse(deps.storage.entries.get(APP_SETTINGS_KEY) ?? "null")).toEqual({
|
||||
manageBuiltInDaemon: false,
|
||||
releaseChannel: "beta",
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the legacy key's explicit interface size as content size", async () => {
|
||||
@@ -400,6 +452,36 @@ describe("loadSettingsFromStorage", () => {
|
||||
});
|
||||
|
||||
describe("saveAppSettings", () => {
|
||||
it("round-trips fields written by a newer build", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
[APP_SETTINGS_KEY]: JSON.stringify({
|
||||
theme: "dark",
|
||||
contentFontSize: 16,
|
||||
sendBehavior: "future-mode",
|
||||
futureSetting: { enabled: true },
|
||||
sidebarRowItems: { host: false, futureRowItem: true },
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
await loadAppSettingsFromStorage(deps);
|
||||
await saveAppSettings({
|
||||
queryClient: new QueryClient(),
|
||||
updates: { theme: "light" },
|
||||
deps,
|
||||
});
|
||||
|
||||
expect(JSON.parse(deps.storage.entries.get(APP_SETTINGS_KEY) ?? "null")).toMatchObject({
|
||||
theme: "light",
|
||||
futureSetting: { enabled: true },
|
||||
sidebarRowItems: {
|
||||
host: false,
|
||||
futureRowItem: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("saves terminal scrollback through app settings persistence", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
@@ -609,7 +691,7 @@ describe("appearance settings", () => {
|
||||
|
||||
expect(result.uiBaseFontSize).toBe(baseSize);
|
||||
expect(persisted).toMatchObject({ uiBaseFontSize: baseSize });
|
||||
expect(persisted).not.toHaveProperty("uiFontSize");
|
||||
expect(persisted.uiFontSize).toBe(legacySize);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -623,6 +705,20 @@ describe("appearance settings", () => {
|
||||
expect((await loadAppSettingsFromStorage(deps)).uiBaseFontSize).toBe(16);
|
||||
});
|
||||
|
||||
it("falls back to a valid legacy interface scale when the explicit base size is invalid", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
[APP_SETTINGS_KEY]: JSON.stringify({ uiBaseFontSize: "abc", uiFontSize: 17 }),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await loadAppSettingsFromStorage(deps);
|
||||
const persisted = JSON.parse(deps.storage.entries.get(APP_SETTINGS_KEY) ?? "null");
|
||||
|
||||
expect(result.uiBaseFontSize).toBe(15);
|
||||
expect(persisted).toMatchObject({ uiBaseFontSize: 15, uiFontSize: 17 });
|
||||
});
|
||||
|
||||
it("clamps the UI base font size into range and rejects non-numeric values", async () => {
|
||||
const deps = makeDeps({
|
||||
storage: createInMemoryKeyValueStorage({
|
||||
|
||||
@@ -2,16 +2,14 @@ import { isSyntaxThemeId, type SyntaxThemeId } from "@getpaseo/highlight";
|
||||
import type { ActiveTurnBehavior } from "@getpaseo/protocol/messages";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import type { DesktopSettings } from "@/desktop/settings/desktop-settings";
|
||||
import { parseAppLanguage, type AppLanguage } from "@/i18n/locales";
|
||||
import type { AppLanguage } from "@/i18n/locales";
|
||||
import {
|
||||
DEFAULT_SIDEBAR_CHECKS_DISPLAY,
|
||||
parseSidebarChecksDisplay,
|
||||
type SidebarChecksDisplay,
|
||||
} from "@/components/sidebar/display-preferences/checks-display";
|
||||
import {
|
||||
DEFAULT_SIDEBAR_ROW_ITEMS,
|
||||
isChecksHiddenByLegacyRowItem,
|
||||
parseSidebarRowItems,
|
||||
type SidebarRowItems,
|
||||
} from "@/components/sidebar/display-preferences/row-items";
|
||||
import { isNative } from "@/constants/platform";
|
||||
@@ -22,7 +20,6 @@ import {
|
||||
type ThemePreference,
|
||||
} from "@/styles/theme";
|
||||
import { z } from "zod";
|
||||
import { readValidatedJson } from "@/storage/validated-storage";
|
||||
import { APP_SETTINGS_KEY, LEGACY_SETTINGS_KEY } from "./keys";
|
||||
import { migrateAppSettings } from "./migrations";
|
||||
|
||||
@@ -41,17 +38,8 @@ const ThemePreferenceSchema = z.enum([
|
||||
...THEME_OPTIONS.map((option) => option.name),
|
||||
PLUGIN_THEME_PREFERENCE,
|
||||
]);
|
||||
const VALID_THEMES = new Set<string>(ThemePreferenceSchema.options);
|
||||
/** Where the theme picker lands when the persisted preference cannot be honoured. */
|
||||
export const DEFAULT_THEME_PREFERENCE = "auto" satisfies ThemePreference;
|
||||
const VALID_SERVICE_URL_BEHAVIORS = new Set<ServiceUrlBehavior>(["ask", "in-app", "external"]);
|
||||
const VALID_WORKSPACE_TITLE_SOURCES = new Set<WorkspaceTitleSource>(["title", "branch"]);
|
||||
const VALID_SIDEBAR_WORKSPACE_TRAILINGS = new Set<SidebarWorkspaceTrailing>([
|
||||
"diff",
|
||||
"timestamp",
|
||||
"none",
|
||||
]);
|
||||
const VALID_TOOL_CALL_DETAIL_LEVELS = new Set<ToolCallDetailLevel>(["overview", "detailed"]);
|
||||
export const DEFAULT_TERMINAL_SCROLLBACK_LINES = 10_000;
|
||||
export const MIN_TERMINAL_SCROLLBACK_LINES = 0;
|
||||
export const MAX_TERMINAL_SCROLLBACK_LINES = 1_000_000;
|
||||
@@ -106,58 +94,6 @@ export interface Settings extends AppSettings {
|
||||
releaseChannel: ReleaseChannel;
|
||||
}
|
||||
|
||||
// Strict, so every item in SIDEBAR_ROW_ITEMS needs a key here the day it is added: the whole
|
||||
// settings write is one validation, and one unknown key silently loses every other toggle in it.
|
||||
// `checks` and `scripts` are gone from the item list and stay here for the COMPAT reads in
|
||||
// row-items.ts.
|
||||
const SidebarRowItemsSchema = z.strictObject({
|
||||
branch: z.boolean().optional(),
|
||||
project: z.boolean().optional(),
|
||||
host: z.boolean().optional(),
|
||||
changeRequest: z.boolean().optional(),
|
||||
services: z.boolean().optional(),
|
||||
labels: z.boolean().optional(),
|
||||
checks: z.boolean().optional(),
|
||||
scripts: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const StoredAppSettingsSchema = z.strictObject({
|
||||
theme: ThemePreferenceSchema.optional(),
|
||||
pluginThemeId: z.string().nullish(),
|
||||
language: z
|
||||
.enum(["system", "ar", "en", "es", "fr", "ja", "ko", "pt-BR", "ru", "zh-CN"])
|
||||
.optional(),
|
||||
sendBehavior: z.enum(["interrupt", "steer", "queue"]).optional(),
|
||||
serviceUrlBehavior: z.enum(["ask", "in-app", "external"]).optional(),
|
||||
terminalScrollbackLines: z.union([z.number(), z.string()]).optional(),
|
||||
useLegacyTerminalRenderer: z.boolean().optional(),
|
||||
uiFontFamily: z.string().optional(),
|
||||
monoFontFamily: z.string().optional(),
|
||||
uiBaseFontSize: z.union([z.number(), z.string()]).optional(),
|
||||
contentFontSize: z.union([z.number(), z.string()]).optional(),
|
||||
// COMPAT(uiFontSizeScale): replaced by the literal base size in v0.4, remove after 2027-08-17.
|
||||
uiFontSize: z.union([z.number(), z.string()]).optional(),
|
||||
codeFontSize: z.union([z.number(), z.string()]).optional(),
|
||||
syntaxTheme: z.string().refine(isSyntaxThemeId).optional(),
|
||||
workspaceTitleSource: z.enum(["title", "branch"]).optional(),
|
||||
sidebarWorkspaceTrailing: z.enum(["diff", "timestamp", "none"]).optional(),
|
||||
sidebarRowItems: SidebarRowItemsSchema.optional(),
|
||||
sidebarChecksDisplay: z.enum(["iconAndText", "icon", "none"]).optional(),
|
||||
autoExpandReasoning: z.boolean().optional(),
|
||||
toolCallDetailLevel: z.enum(["overview", "detailed"]).optional(),
|
||||
compactToolCalls: z.boolean().optional(),
|
||||
chatOutlineEnabled: z.boolean().optional(),
|
||||
vimKeybindings: z.boolean().optional(),
|
||||
openSupportingTabsInSidePanel: z.boolean().optional(),
|
||||
// COMPAT(rendererDesktopSettings): these fields used to share this renderer-owned key.
|
||||
manageBuiltInDaemon: z.boolean().optional(),
|
||||
releaseChannel: z.enum(["stable", "beta"]).optional(),
|
||||
});
|
||||
|
||||
const LegacyRendererSettingsSchema = StoredAppSettingsSchema;
|
||||
|
||||
type StoredAppSettings = z.infer<typeof StoredAppSettingsSchema>;
|
||||
|
||||
export const DEFAULT_CLIENT_SETTINGS: AppSettings = {
|
||||
theme: DEFAULT_THEME_PREFERENCE,
|
||||
pluginThemeId: null,
|
||||
@@ -189,6 +125,131 @@ export const DEFAULT_APP_SETTINGS: Settings = {
|
||||
releaseChannel: "stable",
|
||||
};
|
||||
|
||||
function clampedNumber(min: number, max: number) {
|
||||
return z
|
||||
.unknown()
|
||||
.transform((value) => parseClampedFontSize(value, { min, max }))
|
||||
.pipe(z.number());
|
||||
}
|
||||
|
||||
function sanitizedFontFamily() {
|
||||
return z.unknown().transform(sanitizeFontFamily).pipe(z.string());
|
||||
}
|
||||
|
||||
const SidebarRowItemsSchema = z
|
||||
.looseObject({
|
||||
branch: z.boolean().catch(DEFAULT_SIDEBAR_ROW_ITEMS.branch),
|
||||
project: z.boolean().catch(DEFAULT_SIDEBAR_ROW_ITEMS.project),
|
||||
host: z.boolean().catch(DEFAULT_SIDEBAR_ROW_ITEMS.host),
|
||||
changeRequest: z.boolean().catch(DEFAULT_SIDEBAR_ROW_ITEMS.changeRequest),
|
||||
services: z.boolean().optional().catch(undefined),
|
||||
labels: z.boolean().catch(DEFAULT_SIDEBAR_ROW_ITEMS.labels),
|
||||
// COMPAT(sidebarRowItemsChecks): migrated in v0.3.0, remove after 2027-08-05.
|
||||
checks: z.boolean().optional().catch(undefined),
|
||||
// COMPAT(sidebarRowItemsScripts): migrated in v0.3.0, remove after 2027-08-05.
|
||||
scripts: z.boolean().optional().catch(undefined),
|
||||
})
|
||||
.catch(DEFAULT_SIDEBAR_ROW_ITEMS);
|
||||
|
||||
type StoredAppSettingsFallback = AppSettings & {
|
||||
uiFontSize?: number;
|
||||
compactToolCalls?: boolean;
|
||||
manageBuiltInDaemon?: boolean;
|
||||
releaseChannel?: ReleaseChannel;
|
||||
needsWrite: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_STORED_APP_SETTINGS = {
|
||||
...DEFAULT_CLIENT_SETTINGS,
|
||||
needsWrite: false,
|
||||
} satisfies StoredAppSettingsFallback;
|
||||
|
||||
const StoredAppSettingsSchema = z
|
||||
.looseObject({
|
||||
theme: ThemePreferenceSchema.catch(DEFAULT_THEME_PREFERENCE),
|
||||
pluginThemeId: z.string().nullable().catch(null),
|
||||
language: z
|
||||
.enum(["system", "ar", "en", "es", "fr", "ja", "ko", "pt-BR", "ru", "zh-CN"])
|
||||
.catch("system"),
|
||||
sendBehavior: z.enum(["interrupt", "steer", "queue"]).catch("steer"),
|
||||
serviceUrlBehavior: z.enum(["ask", "in-app", "external"]).catch("ask"),
|
||||
terminalScrollbackLines: clampedNumber(
|
||||
MIN_TERMINAL_SCROLLBACK_LINES,
|
||||
MAX_TERMINAL_SCROLLBACK_LINES,
|
||||
).catch(DEFAULT_TERMINAL_SCROLLBACK_LINES),
|
||||
useLegacyTerminalRenderer: z.boolean().catch(false),
|
||||
uiFontFamily: sanitizedFontFamily().catch(""),
|
||||
monoFontFamily: sanitizedFontFamily().catch(""),
|
||||
uiBaseFontSize: clampedNumber(MIN_UI_BASE_FONT_SIZE, MAX_UI_BASE_FONT_SIZE)
|
||||
.optional()
|
||||
.catch(undefined),
|
||||
contentFontSize: clampedNumber(MIN_CONTENT_FONT_SIZE, MAX_CONTENT_FONT_SIZE)
|
||||
.optional()
|
||||
.catch(DEFAULT_CONTENT_FONT_SIZE),
|
||||
// COMPAT(uiFontSizeScale): replaced by the literal base size in v0.4, remove after 2027-08-17.
|
||||
uiFontSize: clampedNumber(11, 24).optional().catch(undefined),
|
||||
codeFontSize: clampedNumber(MIN_CODE_FONT_SIZE, MAX_CODE_FONT_SIZE).catch(
|
||||
DEFAULT_CODE_FONT_SIZE,
|
||||
),
|
||||
syntaxTheme: z.string().refine(isSyntaxThemeId).catch("one"),
|
||||
workspaceTitleSource: z.enum(["title", "branch"]).catch("title"),
|
||||
sidebarWorkspaceTrailing: z.enum(["diff", "timestamp", "none"]).catch("diff"),
|
||||
sidebarRowItems: SidebarRowItemsSchema,
|
||||
sidebarChecksDisplay: z
|
||||
.enum(["iconAndText", "icon", "none"])
|
||||
.optional()
|
||||
.catch(DEFAULT_SIDEBAR_CHECKS_DISPLAY),
|
||||
autoExpandReasoning: z.boolean().catch(false),
|
||||
toolCallDetailLevel: z
|
||||
.enum(["overview", "detailed"])
|
||||
.or(z.literal("concise").transform(() => "overview" as const))
|
||||
.optional()
|
||||
.catch("detailed"),
|
||||
// COMPAT(compactToolCalls): migrated in v0.1.105, remove after 2027-01-12.
|
||||
compactToolCalls: z.boolean().optional().catch(undefined),
|
||||
chatOutlineEnabled: z.boolean().catch(true),
|
||||
vimKeybindings: z.boolean().catch(false),
|
||||
openSupportingTabsInSidePanel: z.boolean().catch(true),
|
||||
// COMPAT(rendererDesktopSettings): these fields used to share this renderer-owned key.
|
||||
manageBuiltInDaemon: z.boolean().optional().catch(undefined),
|
||||
releaseChannel: z.enum(["stable", "beta"]).optional().catch(undefined),
|
||||
})
|
||||
.transform((stored) => {
|
||||
const needsWrite =
|
||||
(stored.uiBaseFontSize === undefined && stored.uiFontSize !== undefined) ||
|
||||
stored.contentFontSize === undefined;
|
||||
const uiBaseFontSize =
|
||||
stored.uiBaseFontSize ??
|
||||
(stored.uiFontSize === undefined
|
||||
? DEFAULT_UI_BASE_FONT_SIZE
|
||||
: Math.round((FONT_SIZE.base * stored.uiFontSize) / 16));
|
||||
const sidebarChecksDisplay =
|
||||
stored.sidebarChecksDisplay ??
|
||||
(isChecksHiddenByLegacyRowItem(stored.sidebarRowItems)
|
||||
? "none"
|
||||
: DEFAULT_SIDEBAR_CHECKS_DISPLAY);
|
||||
const toolCallDetailLevel =
|
||||
stored.toolCallDetailLevel ?? (stored.compactToolCalls ? "overview" : "detailed");
|
||||
return {
|
||||
...stored,
|
||||
uiBaseFontSize,
|
||||
contentFontSize: stored.contentFontSize ?? uiBaseFontSize,
|
||||
sidebarChecksDisplay,
|
||||
sidebarRowItems: {
|
||||
...stored.sidebarRowItems,
|
||||
services:
|
||||
stored.sidebarRowItems.services ??
|
||||
(stored.sidebarRowItems.scripts === false ? false : DEFAULT_SIDEBAR_ROW_ITEMS.services),
|
||||
},
|
||||
toolCallDetailLevel,
|
||||
needsWrite,
|
||||
};
|
||||
})
|
||||
.catch(DEFAULT_STORED_APP_SETTINGS);
|
||||
|
||||
type StoredAppSettings = z.output<typeof StoredAppSettingsSchema>;
|
||||
export type PersistedAppSettings = Omit<StoredAppSettings, "needsWrite">;
|
||||
|
||||
export interface KeyValueStorage {
|
||||
getItem(key: string): Promise<string | null>;
|
||||
setItem(key: string, value: string): Promise<void>;
|
||||
@@ -220,16 +281,22 @@ export async function saveAppSettings(input: {
|
||||
const current = normalizeAppSettings(storedCurrent);
|
||||
const next = { ...current, ...input.updates };
|
||||
input.queryClient.setQueryData<AppSettings>(APP_SETTINGS_QUERY_KEY, next);
|
||||
await input.deps.storage.setItem(APP_SETTINGS_KEY, JSON.stringify(next));
|
||||
await writeAppSettings(
|
||||
input.deps.storage,
|
||||
(await readSettingsObject(input.deps.storage, APP_SETTINGS_KEY)) ??
|
||||
StoredAppSettingsSchema.parse({}),
|
||||
next,
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadAppSettingsFromStorage(deps: SettingsDeps): Promise<AppSettings> {
|
||||
try {
|
||||
const read = await readAppSettings(deps);
|
||||
if (read.needsWrite) {
|
||||
await deps.storage.setItem(APP_SETTINGS_KEY, JSON.stringify(read.settings));
|
||||
await writeAppSettings(deps.storage, read.stored, read.settings);
|
||||
}
|
||||
return await migrateAppSettings(read.settings, deps.storage);
|
||||
const { needsWrite: _needsWrite, ...stored } = read.stored;
|
||||
return await migrateAppSettings(read.settings, deps.storage, stored);
|
||||
} catch (error) {
|
||||
console.error("[AppSettings] Failed to load settings:", error);
|
||||
throw error;
|
||||
@@ -242,23 +309,18 @@ export async function loadAppSettingsFromStorage(deps: SettingsDeps): Promise<Ap
|
||||
*/
|
||||
async function readAppSettings(
|
||||
deps: SettingsDeps,
|
||||
): Promise<{ settings: AppSettings; needsWrite: boolean }> {
|
||||
const stored = await readValidatedJson(deps.storage, APP_SETTINGS_KEY, StoredAppSettingsSchema);
|
||||
): Promise<{ settings: AppSettings; needsWrite: boolean; stored: StoredAppSettings }> {
|
||||
const stored = await readSettingsObject(deps.storage, APP_SETTINGS_KEY);
|
||||
if (stored) {
|
||||
return {
|
||||
settings: normalizeAppSettings(stored),
|
||||
// COMPAT(uiFontSizeScale): persist the converted base size, remove after 2027-08-17.
|
||||
needsWrite:
|
||||
(stored.uiBaseFontSize === undefined && stored.uiFontSize !== undefined) ||
|
||||
stored.contentFontSize === undefined,
|
||||
needsWrite: stored.needsWrite,
|
||||
stored,
|
||||
};
|
||||
}
|
||||
|
||||
const legacyStored = await readValidatedJson(
|
||||
deps.storage,
|
||||
LEGACY_SETTINGS_KEY,
|
||||
LegacyRendererSettingsSchema,
|
||||
);
|
||||
const legacyStored = await readSettingsObject(deps.storage, LEGACY_SETTINGS_KEY);
|
||||
if (legacyStored) {
|
||||
return {
|
||||
settings: {
|
||||
@@ -266,10 +328,12 @@ async function readAppSettings(
|
||||
...pickAppSettingsFromLegacy(legacyStored),
|
||||
} satisfies AppSettings,
|
||||
needsWrite: true,
|
||||
stored: legacyStored,
|
||||
};
|
||||
}
|
||||
|
||||
return { settings: DEFAULT_CLIENT_SETTINGS, needsWrite: true };
|
||||
const defaultStored = StoredAppSettingsSchema.parse({});
|
||||
return { settings: DEFAULT_CLIENT_SETTINGS, needsWrite: true, stored: defaultStored };
|
||||
}
|
||||
|
||||
export async function loadSettingsFromStorage(deps: SettingsDeps): Promise<Settings> {
|
||||
@@ -299,189 +363,27 @@ export async function loadSettingsFromStorage(deps: SettingsDeps): Promise<Setti
|
||||
}
|
||||
|
||||
export function normalizeAppSettings(value: unknown): AppSettings {
|
||||
const result = StoredAppSettingsSchema.safeParse(value);
|
||||
const {
|
||||
needsWrite: _needsWrite,
|
||||
manageBuiltInDaemon: _manageBuiltInDaemon,
|
||||
releaseChannel: _releaseChannel,
|
||||
compactToolCalls: _compactToolCalls,
|
||||
uiFontSize: _uiFontSize,
|
||||
...settings
|
||||
} = StoredAppSettingsSchema.parse(value);
|
||||
return settings;
|
||||
}
|
||||
|
||||
function pickAppSettingsFromLegacy(legacy: StoredAppSettings): AppSettings {
|
||||
const settings = normalizeAppSettings(legacy);
|
||||
return {
|
||||
...DEFAULT_CLIENT_SETTINGS,
|
||||
...pickAppSettings(result.success ? result.data : {}),
|
||||
...settings,
|
||||
// The legacy key rendered content on the interface ramp. Freeze that
|
||||
// rendered value into the new independent preference during migration.
|
||||
contentFontSize: legacy.uiBaseFontSize,
|
||||
};
|
||||
}
|
||||
|
||||
function parseToolCallDetailLevel(stored: StoredAppSettings): ToolCallDetailLevel | null {
|
||||
if (stored.toolCallDetailLevel !== undefined) {
|
||||
if (
|
||||
typeof stored.toolCallDetailLevel === "string" &&
|
||||
VALID_TOOL_CALL_DETAIL_LEVELS.has(stored.toolCallDetailLevel)
|
||||
) {
|
||||
return stored.toolCallDetailLevel;
|
||||
}
|
||||
// COMPAT(toolCallDetailLevelConcise): removed in v0.1.107; legacy "concise" values
|
||||
// deliberately follow the unknown-value fallback. Remove after 2027-01-14.
|
||||
return "overview";
|
||||
}
|
||||
if (typeof stored.compactToolCalls === "boolean") {
|
||||
// COMPAT(compactToolCalls): migrated in v0.1.105, remove after 2027-01-12.
|
||||
return stored.compactToolCalls ? "overview" : "detailed";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseStoredSidebarChecksDisplay(stored: StoredAppSettings): SidebarChecksDisplay | null {
|
||||
const display = parseSidebarChecksDisplay(stored.sidebarChecksDisplay);
|
||||
if (display !== null) {
|
||||
return display;
|
||||
}
|
||||
// COMPAT(sidebarRowItemsChecks): migrated in v0.3.0, remove after 2027-08-05.
|
||||
return isChecksHiddenByLegacyRowItem(stored.sidebarRowItems) ? "none" : null;
|
||||
}
|
||||
|
||||
function pickBooleanAppSettings(stored: StoredAppSettings): Partial<AppSettings> {
|
||||
const result: Partial<AppSettings> = {};
|
||||
if (typeof stored.useLegacyTerminalRenderer === "boolean") {
|
||||
result.useLegacyTerminalRenderer = stored.useLegacyTerminalRenderer;
|
||||
}
|
||||
if (typeof stored.vimKeybindings === "boolean") {
|
||||
result.vimKeybindings = stored.vimKeybindings;
|
||||
}
|
||||
if (typeof stored.chatOutlineEnabled === "boolean") {
|
||||
result.chatOutlineEnabled = stored.chatOutlineEnabled;
|
||||
}
|
||||
if (typeof stored.openSupportingTabsInSidePanel === "boolean") {
|
||||
result.openSupportingTabsInSidePanel = stored.openSupportingTabsInSidePanel;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The settings whose stored value only has to be a member of a fixed set. Grouped like the
|
||||
* boolean settings are: the numeric and font settings need real parsing and clamping, these
|
||||
* need a membership check and nothing else.
|
||||
*/
|
||||
function pickEnumAppSettings(stored: StoredAppSettings): Partial<AppSettings> {
|
||||
const result: Partial<AppSettings> = {};
|
||||
if (typeof stored.theme === "string" && VALID_THEMES.has(stored.theme)) {
|
||||
result.theme = stored.theme;
|
||||
}
|
||||
if (
|
||||
stored.sendBehavior === "interrupt" ||
|
||||
stored.sendBehavior === "steer" ||
|
||||
stored.sendBehavior === "queue"
|
||||
) {
|
||||
result.sendBehavior = stored.sendBehavior;
|
||||
}
|
||||
if (
|
||||
typeof stored.serviceUrlBehavior === "string" &&
|
||||
VALID_SERVICE_URL_BEHAVIORS.has(stored.serviceUrlBehavior)
|
||||
) {
|
||||
result.serviceUrlBehavior = stored.serviceUrlBehavior;
|
||||
}
|
||||
if (typeof stored.syntaxTheme === "string" && isSyntaxThemeId(stored.syntaxTheme)) {
|
||||
result.syntaxTheme = stored.syntaxTheme;
|
||||
}
|
||||
if (
|
||||
typeof stored.workspaceTitleSource === "string" &&
|
||||
VALID_WORKSPACE_TITLE_SOURCES.has(stored.workspaceTitleSource)
|
||||
) {
|
||||
result.workspaceTitleSource = stored.workspaceTitleSource;
|
||||
}
|
||||
if (
|
||||
typeof stored.sidebarWorkspaceTrailing === "string" &&
|
||||
VALID_SIDEBAR_WORKSPACE_TRAILINGS.has(stored.sidebarWorkspaceTrailing)
|
||||
) {
|
||||
result.sidebarWorkspaceTrailing = stored.sidebarWorkspaceTrailing;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function pickAppSettings(stored: StoredAppSettings): Partial<AppSettings> {
|
||||
const result: Partial<AppSettings> = {};
|
||||
Object.assign(result, pickEnumAppSettings(stored));
|
||||
if (typeof stored.pluginThemeId === "string") {
|
||||
result.pluginThemeId = stored.pluginThemeId;
|
||||
}
|
||||
if (stored.sidebarRowItems !== undefined) {
|
||||
result.sidebarRowItems = parseSidebarRowItems(stored.sidebarRowItems);
|
||||
}
|
||||
const sidebarChecksDisplay = parseStoredSidebarChecksDisplay(stored);
|
||||
if (sidebarChecksDisplay !== null) {
|
||||
result.sidebarChecksDisplay = sidebarChecksDisplay;
|
||||
}
|
||||
const language = parseAppLanguage(stored.language);
|
||||
if (language !== null) {
|
||||
result.language = language;
|
||||
}
|
||||
const terminalScrollbackLines = parseTerminalScrollbackLines(stored.terminalScrollbackLines);
|
||||
if (terminalScrollbackLines !== null) {
|
||||
result.terminalScrollbackLines = terminalScrollbackLines;
|
||||
}
|
||||
const uiFontFamily = sanitizeFontFamily(stored.uiFontFamily);
|
||||
if (uiFontFamily !== null) {
|
||||
result.uiFontFamily = uiFontFamily;
|
||||
}
|
||||
const monoFontFamily = sanitizeFontFamily(stored.monoFontFamily);
|
||||
if (monoFontFamily !== null) {
|
||||
result.monoFontFamily = monoFontFamily;
|
||||
}
|
||||
const uiBaseFontSize = parseClampedFontSize(stored.uiBaseFontSize, {
|
||||
min: MIN_UI_BASE_FONT_SIZE,
|
||||
max: MAX_UI_BASE_FONT_SIZE,
|
||||
});
|
||||
if (uiBaseFontSize !== null) {
|
||||
result.uiBaseFontSize = uiBaseFontSize;
|
||||
} else {
|
||||
const legacyUiFontSize = parseClampedFontSize(stored.uiFontSize, {
|
||||
min: 11,
|
||||
max: 24,
|
||||
});
|
||||
if (legacyUiFontSize !== null) {
|
||||
result.uiBaseFontSize = Math.round((FONT_SIZE.base * legacyUiFontSize) / 16);
|
||||
}
|
||||
}
|
||||
const contentFontSize = parseClampedFontSize(stored.contentFontSize, {
|
||||
min: MIN_CONTENT_FONT_SIZE,
|
||||
max: MAX_CONTENT_FONT_SIZE,
|
||||
});
|
||||
if (contentFontSize !== null) {
|
||||
result.contentFontSize = contentFontSize;
|
||||
} else if (stored.contentFontSize === undefined) {
|
||||
// Existing content followed the interface ramp. Preserve that rendered size
|
||||
// once, then persist the independent setting during the read migration.
|
||||
result.contentFontSize = result.uiBaseFontSize ?? DEFAULT_UI_BASE_FONT_SIZE;
|
||||
}
|
||||
const codeFontSize = parseClampedFontSize(stored.codeFontSize, {
|
||||
min: MIN_CODE_FONT_SIZE,
|
||||
max: MAX_CODE_FONT_SIZE,
|
||||
});
|
||||
if (codeFontSize !== null) {
|
||||
result.codeFontSize = codeFontSize;
|
||||
}
|
||||
Object.assign(result, pickBooleanAppSettings(stored));
|
||||
if (typeof stored.autoExpandReasoning === "boolean") {
|
||||
result.autoExpandReasoning = stored.autoExpandReasoning;
|
||||
}
|
||||
const toolCallDetailLevel = parseToolCallDetailLevel(stored);
|
||||
if (toolCallDetailLevel !== null) {
|
||||
result.toolCallDetailLevel = toolCallDetailLevel;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function pickAppSettingsFromLegacy(
|
||||
legacy: z.infer<typeof LegacyRendererSettingsSchema>,
|
||||
): Partial<AppSettings> {
|
||||
const result: Partial<AppSettings> = {};
|
||||
if (legacy.theme === "dark" || legacy.theme === "light" || legacy.theme === "auto") {
|
||||
result.theme = legacy.theme;
|
||||
}
|
||||
const legacyInterfaceSize = pickAppSettings(legacy).uiBaseFontSize;
|
||||
if (legacyInterfaceSize !== undefined) {
|
||||
result.uiBaseFontSize = legacyInterfaceSize;
|
||||
}
|
||||
// The legacy key rendered content on the interface ramp. Freeze that
|
||||
// rendered value into the new independent preference during migration.
|
||||
result.contentFontSize = legacyInterfaceSize ?? DEFAULT_UI_BASE_FONT_SIZE;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseTerminalScrollbackLines(value: unknown): number | null {
|
||||
let numericValue = NaN;
|
||||
if (typeof value === "number") {
|
||||
@@ -548,10 +450,10 @@ async function loadLegacyDesktopSettingsFromStorage(storage: KeyValueStorage): P
|
||||
releaseChannel?: ReleaseChannel;
|
||||
} = {};
|
||||
|
||||
if (typeof stored.manageBuiltInDaemon === "boolean") {
|
||||
if (stored.manageBuiltInDaemon !== undefined) {
|
||||
result.manageBuiltInDaemon = stored.manageBuiltInDaemon;
|
||||
}
|
||||
if (stored.releaseChannel === "stable" || stored.releaseChannel === "beta") {
|
||||
if (stored.releaseChannel !== undefined) {
|
||||
result.releaseChannel = stored.releaseChannel;
|
||||
}
|
||||
|
||||
@@ -560,11 +462,48 @@ async function loadLegacyDesktopSettingsFromStorage(storage: KeyValueStorage): P
|
||||
|
||||
async function loadRendererSettingsPayload(
|
||||
storage: KeyValueStorage,
|
||||
): Promise<z.infer<typeof LegacyRendererSettingsSchema> | null> {
|
||||
const current = await readValidatedJson(storage, APP_SETTINGS_KEY, LegacyRendererSettingsSchema);
|
||||
): Promise<StoredAppSettings | null> {
|
||||
const current = await readSettingsObject(storage, APP_SETTINGS_KEY);
|
||||
if (current) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return readValidatedJson(storage, LEGACY_SETTINGS_KEY, LegacyRendererSettingsSchema);
|
||||
return readSettingsObject(storage, LEGACY_SETTINGS_KEY);
|
||||
}
|
||||
|
||||
async function readSettingsObject(
|
||||
storage: KeyValueStorage,
|
||||
key: string,
|
||||
): Promise<StoredAppSettings | null> {
|
||||
const raw = await storage.getItem(key);
|
||||
if (raw === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let decoded: unknown;
|
||||
try {
|
||||
decoded = JSON.parse(raw);
|
||||
} catch {
|
||||
console.warn(`[AppSettings] Removing corrupt ${key}: invalid JSON.`);
|
||||
await storage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
return StoredAppSettingsSchema.parse(decoded);
|
||||
}
|
||||
|
||||
async function writeAppSettings(
|
||||
storage: KeyValueStorage,
|
||||
stored: StoredAppSettings,
|
||||
settings: AppSettings,
|
||||
): Promise<void> {
|
||||
const { needsWrite: _needsWrite, ...persistedStored } = stored;
|
||||
const storedSidebarRowItems = persistedStored.sidebarRowItems;
|
||||
await storage.setItem(
|
||||
APP_SETTINGS_KEY,
|
||||
JSON.stringify({
|
||||
...persistedStored,
|
||||
...settings,
|
||||
sidebarRowItems: { ...storedSidebarRowItems, ...settings.sidebarRowItems },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3854,29 +3854,36 @@ function WorkspaceScreenContent({
|
||||
) : null}
|
||||
|
||||
{shouldRenderDesktopPaneFallback ? (
|
||||
<WorkspaceDesktopTabsRow
|
||||
paneId={focusedPaneIdOrUndefined}
|
||||
isFocused={isRouteFocused}
|
||||
tabs={desktopTabRowItems}
|
||||
normalizedServerId={normalizedServerId}
|
||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
||||
onNavigateTab={navigateToTabId}
|
||||
onCloseTab={handleCloseTabById}
|
||||
onCopyResumeCommand={handleCopyResumeCommand}
|
||||
onCopyAgentId={handleCopyAgentId}
|
||||
onCopyTerminalId={handleCopyTerminalId}
|
||||
onCopyFilePath={handleCopyFilePath}
|
||||
onReloadAgent={handleReloadAgent}
|
||||
onRenameTab={handleRenameTab}
|
||||
onCloseTabsToLeft={handleCloseTabsToLeft}
|
||||
onCloseTabsToRight={handleCloseTabsToRight}
|
||||
onCloseOtherTabs={handleCloseOtherTabs}
|
||||
onCreateNewTab={handleCreateNewTab}
|
||||
onReorderTabs={handleReorderTabsInFocusedPane}
|
||||
focusModeEnabled={desktopFocusModeEnabled}
|
||||
onExitFocusMode={toggleFocusMode}
|
||||
/>
|
||||
// The splits path renders tab rows inside WorkspacePanelContent, which is what
|
||||
// provides NewTabLauncherProvider. This fallback row sits outside it, and its
|
||||
// new-tab menu content mounts eagerly, so without its own provider every
|
||||
// non-compact native layout (tablets, foldables in landscape) crashed on mount
|
||||
// with "NewTabLauncherProvider is required" (#3750).
|
||||
<NewTabLauncherProvider value={newTabLauncher}>
|
||||
<WorkspaceDesktopTabsRow
|
||||
paneId={focusedPaneIdOrUndefined}
|
||||
isFocused={isRouteFocused}
|
||||
tabs={desktopTabRowItems}
|
||||
normalizedServerId={normalizedServerId}
|
||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||
setHoveredCloseTabKey={setHoveredCloseTabKey}
|
||||
onNavigateTab={navigateToTabId}
|
||||
onCloseTab={handleCloseTabById}
|
||||
onCopyResumeCommand={handleCopyResumeCommand}
|
||||
onCopyAgentId={handleCopyAgentId}
|
||||
onCopyTerminalId={handleCopyTerminalId}
|
||||
onCopyFilePath={handleCopyFilePath}
|
||||
onReloadAgent={handleReloadAgent}
|
||||
onRenameTab={handleRenameTab}
|
||||
onCloseTabsToLeft={handleCloseTabsToLeft}
|
||||
onCloseTabsToRight={handleCloseTabsToRight}
|
||||
onCloseOtherTabs={handleCloseOtherTabs}
|
||||
onCreateNewTab={handleCreateNewTab}
|
||||
onReorderTabs={handleReorderTabsInFocusedPane}
|
||||
focusModeEnabled={desktopFocusModeEnabled}
|
||||
onExitFocusMode={toggleFocusMode}
|
||||
/>
|
||||
</NewTabLauncherProvider>
|
||||
) : null}
|
||||
|
||||
<View style={styles.centerContent}>{workspacePanelContent}</View>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"description": "Paseo CLI - control your AI coding agents from the command line",
|
||||
"bin": {
|
||||
"paseo": "bin/paseo"
|
||||
@@ -28,9 +28,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/client": "0.5.0",
|
||||
"@getpaseo/protocol": "0.5.0",
|
||||
"@getpaseo/server": "0.5.0",
|
||||
"@getpaseo/client": "0.5.2",
|
||||
"@getpaseo/protocol": "0.5.2",
|
||||
"@getpaseo/server": "0.5.2",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/client",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"description": "Paseo client SDK package",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -39,8 +39,8 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.5.0",
|
||||
"@getpaseo/relay": "0.5.0",
|
||||
"@getpaseo/protocol": "0.5.2",
|
||||
"@getpaseo/relay": "0.5.2",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"private": true,
|
||||
"description": "Paseo desktop app (Electron wrapper)",
|
||||
"homepage": "https://paseo.sh",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"description": "Native module for two way audio streaming",
|
||||
"keywords": [
|
||||
"ExpoTwoWayAudio",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/plugin",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"description": "Paseo plugin SDK package",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -39,7 +39,7 @@
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@getpaseo/client": "0.5.0",
|
||||
"@getpaseo/client": "0.5.2",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/react": "~19.2.0",
|
||||
"react": "19.1.0",
|
||||
@@ -48,7 +48,7 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@getpaseo/client": "0.5.0",
|
||||
"@getpaseo/client": "0.5.2",
|
||||
"react": "19.1.0",
|
||||
"zod": "^4.4.3"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"description": "Paseo shared protocol schemas and wire types",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"description": "Paseo relay for bridging daemon and client connections",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"description": "Paseo backend server",
|
||||
"files": [
|
||||
"dist/server",
|
||||
@@ -72,11 +72,11 @@
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.220",
|
||||
"@anthropic-ai/sdk": "^0.104.2",
|
||||
"@babel/parser": "^7.29.8",
|
||||
"@getpaseo/client": "0.5.0",
|
||||
"@getpaseo/highlight": "0.5.0",
|
||||
"@getpaseo/plugin": "0.5.0",
|
||||
"@getpaseo/protocol": "0.5.0",
|
||||
"@getpaseo/relay": "0.5.0",
|
||||
"@getpaseo/client": "0.5.2",
|
||||
"@getpaseo/highlight": "0.5.2",
|
||||
"@getpaseo/plugin": "0.5.2",
|
||||
"@getpaseo/protocol": "0.5.2",
|
||||
"@getpaseo/relay": "0.5.2",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user