mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-16 14:22:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4092c29265 | ||
|
|
5cedc210a8 | ||
|
|
4724d1d9b7 | ||
|
|
8fdca94ead | ||
|
|
88f410567d | ||
|
|
f998ee1691 | ||
|
|
7a47ec0ee9 | ||
|
|
4f796181bf |
@@ -1,5 +1,12 @@
|
||||
# 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
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
sha256-mF8N1sBkSt2/ZgtB511lqv1knlQ0lmicZxsoucbWZfE=
|
||||
sha256-o8d/nX0FdM6QB7c/RWpo1USsz8UNmwohSqkS7tflayc=
|
||||
|
||||
Generated
+25
-25
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paseo",
|
||||
"version": "0.5.1",
|
||||
"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.1",
|
||||
"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.1",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/client": "0.5.1",
|
||||
"@getpaseo/protocol": "0.5.1",
|
||||
"@getpaseo/server": "0.5.1",
|
||||
"@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.1",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.5.1",
|
||||
"@getpaseo/relay": "0.5.1",
|
||||
"@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.1",
|
||||
"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.1",
|
||||
"version": "0.5.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
@@ -38597,7 +38597,7 @@
|
||||
},
|
||||
"packages/highlight": {
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.5.1",
|
||||
"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.1",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@getpaseo/client": "0.5.1",
|
||||
"@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.1",
|
||||
"@getpaseo/client": "0.5.2",
|
||||
"react": "19.1.0",
|
||||
"zod": "^4.4.3"
|
||||
}
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"dependencies": {
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
@@ -38660,7 +38660,7 @@
|
||||
},
|
||||
"packages/relay": {
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.5.1",
|
||||
"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.1",
|
||||
"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.1",
|
||||
"@getpaseo/highlight": "0.5.1",
|
||||
"@getpaseo/plugin": "0.5.1",
|
||||
"@getpaseo/protocol": "0.5.1",
|
||||
"@getpaseo/relay": "0.5.1",
|
||||
"@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.1",
|
||||
"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.1",
|
||||
"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.1",
|
||||
"version": "0.5.2",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -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.1",
|
||||
"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.1",
|
||||
"@getpaseo/protocol": "0.5.1",
|
||||
"@getpaseo/server": "0.5.1",
|
||||
"@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.1",
|
||||
"version": "0.5.2",
|
||||
"description": "Paseo client SDK package",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -39,8 +39,8 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.5.1",
|
||||
"@getpaseo/relay": "0.5.1",
|
||||
"@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.1",
|
||||
"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.1",
|
||||
"version": "0.5.2",
|
||||
"description": "Native module for two way audio streaming",
|
||||
"keywords": [
|
||||
"ExpoTwoWayAudio",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/plugin",
|
||||
"version": "0.5.1",
|
||||
"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.1",
|
||||
"@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.1",
|
||||
"@getpaseo/client": "0.5.2",
|
||||
"react": "19.1.0",
|
||||
"zod": "^4.4.3"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"description": "Paseo shared protocol schemas and wire types",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.5.1",
|
||||
"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.1",
|
||||
"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.1",
|
||||
"@getpaseo/highlight": "0.5.1",
|
||||
"@getpaseo/plugin": "0.5.1",
|
||||
"@getpaseo/protocol": "0.5.1",
|
||||
"@getpaseo/relay": "0.5.1",
|
||||
"@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.1",
|
||||
"version": "0.5.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user