mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-26 11:12:53 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2941157a8 | ||
|
|
870c969fdb | ||
|
|
3b67e20d89 | ||
|
|
4cbbac563d | ||
|
|
20665a18cd | ||
|
|
7225c0fe12 | ||
|
|
f9ac58b181 |
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## 0.13.2 - 2026-10-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed repeated Android animation exceptions for views that have already unmounted ([#7907](https://github.com/getpaseo/paseo/pull/7907))
|
||||
- Fixed the Android animation run loop continuing while the app is in the background ([#7914](https://github.com/getpaseo/paseo/pull/7914))
|
||||
- Fixed finished Android chat messages being remeasured during streaming updates ([#7914](https://github.com/getpaseo/paseo/pull/7914))
|
||||
- Fixed bottom sheets retaining window-size listeners after closing ([#7914](https://github.com/getpaseo/paseo/pull/7914))
|
||||
|
||||
## 0.13.1 - 2026-10-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a startup crash with Show background enabled and eight or more workspaces in a project ([#7896](https://github.com/getpaseo/paseo/pull/7896) by [@budivoogt](https://github.com/budivoogt))
|
||||
|
||||
## 0.13.0 - 2026-10-24
|
||||
|
||||
Foldable phone layouts, agents that delegate work to other machines, plugin file panels, and plugin translations.
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
# 更新日志
|
||||
|
||||
## 0.13.2 - 2026-10-26
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复 Android 对已卸载视图反复抛出动画异常的问题 ([#7907](https://github.com/getpaseo/paseo/pull/7907))
|
||||
- 修复 Android 应用进入后台后动画运行循环仍持续执行的问题 ([#7914](https://github.com/getpaseo/paseo/pull/7914))
|
||||
- 修复 Android 在流式更新时反复测量已完成聊天消息的问题 ([#7914](https://github.com/getpaseo/paseo/pull/7914))
|
||||
- 修复底部面板关闭后仍保留窗口尺寸监听器的问题 ([#7914](https://github.com/getpaseo/paseo/pull/7914))
|
||||
|
||||
## 0.13.1 - 2026-10-26
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复开启“显示后台工作区”且项目包含至少八个工作区时的启动崩溃([#7896](https://github.com/getpaseo/paseo/pull/7896),由 [@budivoogt](https://github.com/budivoogt) 贡献)
|
||||
|
||||
## 0.13.0 - 2026-10-24
|
||||
|
||||
折叠屏手机布局、可将工作委派给其他机器的 Agent、插件文件面板,以及插件翻译。
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Fixed
|
||||
- Fixed a startup crash with Show background enabled and eight or more workspaces in a project
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -0,0 +1,4 @@
|
||||
Fixed
|
||||
- Fixed a startup crash with Show background enabled and eight or more workspaces in a project
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -0,0 +1,4 @@
|
||||
Fixed
|
||||
- Fixed a startup crash with Show background enabled and eight or more workspaces in a project
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -0,0 +1,4 @@
|
||||
Fixed
|
||||
- Fixed a startup crash with Show background enabled and eight or more workspaces in a project
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -0,0 +1,7 @@
|
||||
Fixed
|
||||
- Fixed repeated Android animation exceptions for views that have already unmounted
|
||||
- Fixed the Android animation run loop continuing while the app is in the background
|
||||
- Fixed finished Android chat messages being remeasured during streaming updates
|
||||
- Fixed bottom sheets retaining window-size listeners after closing
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -0,0 +1,7 @@
|
||||
Fixed
|
||||
- Fixed repeated Android animation exceptions for views that have already unmounted
|
||||
- Fixed the Android animation run loop continuing while the app is in the background
|
||||
- Fixed finished Android chat messages being remeasured during streaming updates
|
||||
- Fixed bottom sheets retaining window-size listeners after closing
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -0,0 +1,7 @@
|
||||
Fixed
|
||||
- Fixed repeated Android animation exceptions for views that have already unmounted
|
||||
- Fixed the Android animation run loop continuing while the app is in the background
|
||||
- Fixed finished Android chat messages being remeasured during streaming updates
|
||||
- Fixed bottom sheets retaining window-size listeners after closing
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -0,0 +1,7 @@
|
||||
Fixed
|
||||
- Fixed repeated Android animation exceptions for views that have already unmounted
|
||||
- Fixed the Android animation run loop continuing while the app is in the background
|
||||
- Fixed finished Android chat messages being remeasured during streaming updates
|
||||
- Fixed bottom sheets retaining window-size listeners after closing
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
+1
-1
@@ -1 +1 @@
|
||||
sha256-DZOPOf81PkUxdD65BrfkTZhs6/DkfDWCFZ/SU+QrZbg=
|
||||
sha256-n6m3m0541/mBbMN1xrh/2uBCRQc62yicK1cdvyyUmmU=
|
||||
|
||||
Generated
+37
-70
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paseo",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
@@ -30269,9 +30269,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-reanimated": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.1.tgz",
|
||||
"integrity": "sha512-RnMvtDuR+68ig864gAvZCOdZehqhC5rFmMo0kn+ARfgVSTvFeF6IFLBVgMPUu0KwihaapEyW24WRi6nEyy1kSA==",
|
||||
"version": "4.5.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.5.tgz",
|
||||
"integrity": "sha512-xtJXZRZ1vkec1AIUVG02St2sbyZS5jc7227TlDu/HonqUYPeZAfiC4RI2jqL1dVMNBu1+7oBx/c9QkjF2kg6Lg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-native-is-edge-to-edge": "^1.3.1",
|
||||
@@ -30280,7 +30280,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "0.83 - 0.86",
|
||||
"react-native-worklets": "0.10.x"
|
||||
"react-native-worklets": "0.10.x - 0.11.x"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-safe-area-context": {
|
||||
@@ -30406,9 +30406,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-worklets": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.1.tgz",
|
||||
"integrity": "sha512-62mRM19bDpfpdI8HLkEErcdOsrAPDtE9lA/sw+5lLRpzBHNhxaoj9QyY2KjXqUmirelxkX4zuPGTC3VdA0feJA==",
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.4.tgz",
|
||||
"integrity": "sha512-gbk0CxMrGywEp3SJAHO+2JVaCWtl4WyAYBIxLZy6Y113+mHHuq3FuT6iV+VcaBuJzuuN34mtdZ4j5JIlBDzmeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-arrow-functions": "^7.27.1",
|
||||
@@ -35695,7 +35695,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "6.10.4",
|
||||
"@codemirror/language": "6.12.4",
|
||||
@@ -35780,14 +35780,14 @@
|
||||
"react-native-gesture-handler": "~2.32.0",
|
||||
"react-native-keyboard-controller": "1.21.12",
|
||||
"react-native-nitro-modules": "0.35.5",
|
||||
"react-native-reanimated": "4.5.1",
|
||||
"react-native-reanimated": "4.5.5",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "~4.26.0",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-unistyles": "^3.2.4",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-webview": "13.16.1",
|
||||
"react-native-worklets": "0.10.1",
|
||||
"react-native-worklets": "0.10.4",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"turndown": "^7.2.4",
|
||||
"turndown-plugin-gfm": "^1.0.2",
|
||||
@@ -37961,12 +37961,12 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/client": "0.13.0",
|
||||
"@getpaseo/protocol": "0.13.0",
|
||||
"@getpaseo/server": "0.13.0",
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/server": "0.13.2",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
@@ -38213,10 +38213,10 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@getpaseo/client",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.13.0",
|
||||
"@getpaseo/relay": "0.13.0",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/relay": "0.13.2",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
@@ -38228,7 +38228,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@getpaseo/cli": "*",
|
||||
@@ -38472,7 +38472,7 @@
|
||||
},
|
||||
"packages/expo-two-way-audio": {
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
@@ -41012,39 +41012,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/expo-two-way-audio/node_modules/expo-module-scripts/node_modules/react-native-worklets": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.13.0.tgz",
|
||||
"integrity": "sha512-1OrhAeAKaZF4XrBa4YbPQbKzNOpjh52HDlId8e6ete1OCHNdAlrQHzG4gznJ225EAj3dgQXHViJvK51M9B52jQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.27.1",
|
||||
"@babel/plugin-transform-arrow-functions": "^7.27.1",
|
||||
"@babel/plugin-transform-class-properties": "^7.28.6",
|
||||
"@babel/plugin-transform-classes": "^7.28.6",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.28.6",
|
||||
"@babel/plugin-transform-shorthand-properties": "^7.27.1",
|
||||
"@babel/plugin-transform-template-literals": "^7.27.1",
|
||||
"@babel/plugin-transform-unicode-regex": "^7.27.1",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"semver": "^7.7.4",
|
||||
"whatwg-fetch": "^3.6.20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "*",
|
||||
"@react-native/metro-config": "*",
|
||||
"react": "*",
|
||||
"react-native": "0.86 - 0.88"
|
||||
}
|
||||
},
|
||||
"packages/expo-two-way-audio/node_modules/expo-module-scripts/node_modules/react-native/node_modules/pretty-format": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
||||
@@ -42124,7 +42091,7 @@
|
||||
},
|
||||
"packages/highlight": {
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "6.12.4",
|
||||
"@codemirror/legacy-modes": "^6.5.3",
|
||||
@@ -42153,15 +42120,15 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@getpaseo/plugin",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^1.4.0",
|
||||
"i18next": "^26.3.0",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@getpaseo/client": "0.13.0",
|
||||
"@getpaseo/protocol": "0.13.0",
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/react": "~19.2.0",
|
||||
"react": "19.2.3",
|
||||
@@ -42171,8 +42138,8 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@getpaseo/client": "0.13.0",
|
||||
"@getpaseo/protocol": "0.13.0",
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"react": ">=19.1.0 <20",
|
||||
"react-native": ">=0.81.5",
|
||||
"zod": "^4.4.3"
|
||||
@@ -42194,7 +42161,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"dependencies": {
|
||||
"semver": "^7.8.5",
|
||||
"zod": "^4.4.3"
|
||||
@@ -42221,7 +42188,7 @@
|
||||
},
|
||||
"packages/relay": {
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^2.4.0",
|
||||
"@noble/curves": "^2.4.0",
|
||||
@@ -42442,16 +42409,16 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.246",
|
||||
"@anthropic-ai/sdk": "^0.104.2",
|
||||
"@getpaseo/client": "0.13.0",
|
||||
"@getpaseo/highlight": "0.13.0",
|
||||
"@getpaseo/plugin": "0.13.0",
|
||||
"@getpaseo/protocol": "0.13.0",
|
||||
"@getpaseo/relay": "0.13.0",
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/highlight": "0.13.2",
|
||||
"@getpaseo/plugin": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/relay": "0.13.2",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
@@ -43372,7 +43339,7 @@
|
||||
},
|
||||
"packages/website": {
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.29.1",
|
||||
"@cloudflare/workers-types": "^4.20260317.1",
|
||||
@@ -44982,7 +44949,7 @@
|
||||
},
|
||||
"plugins": {
|
||||
"name": "@getpaseo/builtin-plugins",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"dependencies": {
|
||||
"@getpaseo/plugin": "*",
|
||||
"zod": "^4.4.3"
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"private": true,
|
||||
"description": "Paseo: voice-controlled development environment for local AI coding agents",
|
||||
"keywords": [
|
||||
@@ -154,7 +154,7 @@
|
||||
"react-dom": "19.2.3",
|
||||
"react-native": "0.86.3",
|
||||
"react-native-gesture-handler": "~2.32.0",
|
||||
"react-native-reanimated": "4.5.1",
|
||||
"react-native-worklets": "0.10.1"
|
||||
"react-native-reanimated": "4.5.5",
|
||||
"react-native-worklets": "0.10.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,6 @@ import {
|
||||
} from "../support/helpers/settings";
|
||||
import { getServerId } from "../support/helpers/server-id";
|
||||
import { expectAppRoute } from "../support/helpers/route-assertions";
|
||||
import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher";
|
||||
import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent";
|
||||
|
||||
async function openWorkspace(
|
||||
page: import("@playwright/test").Page,
|
||||
@@ -55,74 +53,6 @@ async function openWorkspace(
|
||||
}
|
||||
|
||||
test.describe("Settings sidebar navigation", () => {
|
||||
test("opening Settings closes the composer model selector until it is opened again", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}, testInfo) => {
|
||||
const workspace = await withWorkspace({ prefix: "settings-model-selector-" });
|
||||
await gotoWorkspace(page, workspace.workspaceId);
|
||||
await clickNewChat(page);
|
||||
|
||||
const selector = page.getByTestId("combined-model-selector").filter({ visible: true });
|
||||
const popover = page.getByTestId("combobox-desktop-container");
|
||||
await selector.click();
|
||||
await expect(popover).toBeVisible();
|
||||
|
||||
const modifier = process.platform === "darwin" ? "Meta" : "Control";
|
||||
await page.keyboard.press(`${modifier}+,`);
|
||||
await expect(page).toHaveURL(/\/settings\/general$/);
|
||||
await page.screenshot({ path: testInfo.outputPath("settings-after-model-selector.png") });
|
||||
await expect(popover).toHaveCount(0);
|
||||
|
||||
await clickSettingsBackToWorkspace(page);
|
||||
await expect(selector).toBeVisible();
|
||||
await expect(popover).toHaveCount(0);
|
||||
|
||||
await selector.click();
|
||||
await expect(popover).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(popover).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("opening Settings closes an existing chat's model selector until it is opened again", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const workspace = await seedMockAgentWorkspace({
|
||||
repoPrefix: "settings-existing-model-",
|
||||
title: "Settings model selector",
|
||||
});
|
||||
try {
|
||||
await openAgentRoute(page, {
|
||||
workspaceId: workspace.workspaceId,
|
||||
agentId: workspace.agentId,
|
||||
});
|
||||
|
||||
const selector = page.getByTestId("combined-model-selector").filter({ visible: true });
|
||||
const popover = page.getByTestId("combobox-desktop-container");
|
||||
await selector.click();
|
||||
await expect(popover).toBeVisible();
|
||||
|
||||
const modifier = process.platform === "darwin" ? "Meta" : "Control";
|
||||
await page.keyboard.press(`${modifier}+,`);
|
||||
await expect(page).toHaveURL(/\/settings\/general$/);
|
||||
await page.screenshot({
|
||||
path: testInfo.outputPath("settings-after-existing-model-selector.png"),
|
||||
});
|
||||
await expect(popover).toHaveCount(0);
|
||||
|
||||
await clickSettingsBackToWorkspace(page);
|
||||
await expect(selector).toBeVisible();
|
||||
await expect(popover).toHaveCount(0);
|
||||
|
||||
await selector.click();
|
||||
await expect(popover).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(popover).toHaveCount(0);
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("clicking a sidebar section updates the URL and renders the section", async ({ page }) => {
|
||||
await gotoAppShell(page);
|
||||
await openSettings(page);
|
||||
@@ -217,42 +147,6 @@ test.describe("Settings sidebar navigation", () => {
|
||||
test.describe("Settings — compact master-detail", () => {
|
||||
test.use({ viewport: { width: 390, height: 844 } });
|
||||
|
||||
test("the compact tab switcher reopens after Settings hides its workspace", async ({ page }) => {
|
||||
const workspace = await seedMockAgentWorkspace({
|
||||
repoPrefix: "settings-tab-switcher-",
|
||||
title: "Compact tab switcher",
|
||||
});
|
||||
try {
|
||||
await openAgentRoute(page, {
|
||||
workspaceId: workspace.workspaceId,
|
||||
agentId: workspace.agentId,
|
||||
});
|
||||
|
||||
const switcher = page.getByTestId("workspace-tab-switcher-trigger").filter({ visible: true });
|
||||
const newAgent = page.getByTestId("workspace-tab-switcher-new-agent");
|
||||
await switcher.click();
|
||||
await expect(newAgent).toBeInViewport();
|
||||
|
||||
// Navigate without dismissing the sheet through its backdrop first.
|
||||
await page
|
||||
.getByTestId("menu-button")
|
||||
.filter({ visible: true })
|
||||
.first()
|
||||
.dispatchEvent("click");
|
||||
await page.getByTestId("sidebar-settings").first().dispatchEvent("click");
|
||||
await expectAppRoute(page, buildSettingsRoute());
|
||||
await expect(newAgent).toHaveCount(0);
|
||||
|
||||
await closeCompactSettings(page);
|
||||
await expect(switcher).toBeVisible();
|
||||
await expect(newAgent).toHaveCount(0);
|
||||
await switcher.click();
|
||||
await expect(newAgent).toBeInViewport();
|
||||
} finally {
|
||||
await workspace.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("opens compact app and host details and returns through the settings list", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
@@ -126,14 +126,14 @@
|
||||
"react-native-gesture-handler": "~2.32.0",
|
||||
"react-native-keyboard-controller": "1.21.12",
|
||||
"react-native-nitro-modules": "0.35.5",
|
||||
"react-native-reanimated": "4.5.1",
|
||||
"react-native-reanimated": "4.5.5",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "~4.26.0",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-unistyles": "^3.2.4",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-webview": "13.16.1",
|
||||
"react-native-worklets": "0.10.1",
|
||||
"react-native-worklets": "0.10.4",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"turndown": "^7.2.4",
|
||||
"turndown-plugin-gfm": "^1.0.2",
|
||||
@@ -198,6 +198,14 @@
|
||||
],
|
||||
"enableVideo": false
|
||||
},
|
||||
"expo": {
|
||||
"install": {
|
||||
"exclude": [
|
||||
"react-native-reanimated",
|
||||
"react-native-worklets"
|
||||
]
|
||||
}
|
||||
},
|
||||
"reanimated": {
|
||||
"staticFeatureFlags": {
|
||||
"FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS": false
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { createWorkspaceVisibilityPolicy } from "@/components/sidebar/sidebar-group-visibility";
|
||||
import { useLimitedSidebarGroup } from "@/components/sidebar/use-limited-sidebar-group";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { WorkspaceScriptPayload } from "@getpaseo/protocol/messages";
|
||||
@@ -486,6 +487,20 @@ describe("sidebar group visibility", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("treats placements without a hydrated entry as Done", () => {
|
||||
const items = entries([...Array(8).fill("done"), "running", "done"]);
|
||||
const hydrated = new Map(items.slice(0, 9).map((item) => [item.workspaceKey, item]));
|
||||
for (const entriesByKey of [new Map(), hydrated]) {
|
||||
const { result } = renderHook(() =>
|
||||
useLimitedSidebarGroup(items, createWorkspaceVisibilityPolicy(entriesByKey)),
|
||||
);
|
||||
expect(result.current.canToggle).toBe(true);
|
||||
expect(names(result.current.visibleItems)).toEqual(
|
||||
entriesByKey.size === 0 ? names(items.slice(0, 7)) : [...names(items.slice(0, 7)), "row-9"],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("shows eight active rows once with no redundant toggle", () => {
|
||||
const items = entries([
|
||||
"running",
|
||||
|
||||
@@ -99,6 +99,7 @@ import {
|
||||
import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction";
|
||||
import { SidebarSectionHeader } from "@/components/sidebar/sidebar-section-header";
|
||||
import { SidebarGroupToggleRow } from "@/components/sidebar/sidebar-group-toggle-row";
|
||||
import { createWorkspaceVisibilityPolicy } from "@/components/sidebar/sidebar-group-visibility";
|
||||
import { useLimitedSidebarGroup } from "@/components/sidebar/use-limited-sidebar-group";
|
||||
import {
|
||||
SidebarWorkspaceRowFrame,
|
||||
@@ -1593,11 +1594,7 @@ function ProjectBlock({
|
||||
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
||||
}) {
|
||||
const visibilityPolicy = useMemo(
|
||||
() => ({
|
||||
kind: "workspaces" as const,
|
||||
getStatusBucket: (workspace: SidebarWorkspacePlacement) =>
|
||||
workspaceEntriesByKey.get(workspace.workspaceKey)!.statusBucket,
|
||||
}),
|
||||
() => createWorkspaceVisibilityPolicy<SidebarWorkspacePlacement>(workspaceEntriesByKey),
|
||||
[workspaceEntriesByKey],
|
||||
);
|
||||
const {
|
||||
|
||||
@@ -4,6 +4,18 @@ export type SidebarGroupVisibilityPolicy<T> =
|
||||
| { kind: "pinned" }
|
||||
| { kind: "workspaces"; getStatusBucket: (item: T) => SidebarStateBucket };
|
||||
|
||||
// A placement can outrun its entry: a retained sidebar that is inactive (compact layout with the
|
||||
// sidebar closed) or still waiting for sessions keeps an empty entry map. A missing entry renders
|
||||
// no row, so it counts as Done and claims no slot past the limit.
|
||||
export function createWorkspaceVisibilityPolicy<T extends { workspaceKey: string }>(
|
||||
entriesByKey: ReadonlyMap<string, { statusBucket: SidebarStateBucket }>,
|
||||
): SidebarGroupVisibilityPolicy<T> {
|
||||
return {
|
||||
kind: "workspaces",
|
||||
getStatusBucket: (item) => entriesByKey.get(item.workspaceKey)?.statusBucket ?? "done",
|
||||
};
|
||||
}
|
||||
|
||||
export function selectSidebarGroupVisibility<T>(
|
||||
items: readonly T[],
|
||||
expanded: boolean,
|
||||
|
||||
@@ -28,7 +28,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { EditingTextInputHandle } from "@/components/ui/text-input";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||
import { BottomSheetScrollView, type BottomSheetBackgroundProps } from "@gorhom/bottom-sheet";
|
||||
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
@@ -911,9 +910,13 @@ function resolveInitialActiveIndex(
|
||||
return selectedIndex >= 0 ? selectedIndex : fallbackIndex;
|
||||
}
|
||||
|
||||
type BottomSheetVisibility = ReturnType<typeof useIsolatedBottomSheetVisibility>;
|
||||
|
||||
interface MobileBodyProps {
|
||||
isOpen: boolean;
|
||||
bottomSheetRef: BottomSheetVisibility["sheetRef"];
|
||||
snapPoints: string[];
|
||||
handleSheetChange: BottomSheetVisibility["handleSheetChange"];
|
||||
handleSheetDismiss: BottomSheetVisibility["handleSheetDismiss"];
|
||||
handleIndicatorStyle: { backgroundColor: string };
|
||||
titleColor: string;
|
||||
title: string;
|
||||
@@ -941,22 +944,7 @@ interface MobileBodyProps {
|
||||
safeAreaBottom: number;
|
||||
}
|
||||
|
||||
function MobileComboboxBody(props: MobileBodyProps): ReactElement | null {
|
||||
const active = useRetainedPanelActive();
|
||||
if (!active) return null;
|
||||
// The visibility tracker must leave the tree with its sheet.
|
||||
return <MobileComboboxSheet {...props} />;
|
||||
}
|
||||
|
||||
function MobileComboboxSheet(props: MobileBodyProps): ReactElement {
|
||||
const {
|
||||
sheetRef: bottomSheetRef,
|
||||
handleSheetChange,
|
||||
handleSheetDismiss,
|
||||
} = useIsolatedBottomSheetVisibility({
|
||||
visible: props.isOpen,
|
||||
onClose: props.onClose,
|
||||
});
|
||||
function MobileComboboxBody(props: MobileBodyProps): ReactElement {
|
||||
const comboboxTitleStyle = useMemo(
|
||||
() => [styles.comboboxTitle, { color: props.titleColor }],
|
||||
[props.titleColor],
|
||||
@@ -991,13 +979,13 @@ function MobileComboboxSheet(props: MobileBodyProps): ReactElement {
|
||||
|
||||
return (
|
||||
<IsolatedBottomSheetModal
|
||||
ref={bottomSheetRef}
|
||||
ref={props.bottomSheetRef}
|
||||
contextBridge={null}
|
||||
snapPoints={props.snapPoints}
|
||||
index={0}
|
||||
enableDynamicSizing={false}
|
||||
onChange={handleSheetChange}
|
||||
onDismiss={handleSheetDismiss}
|
||||
onChange={props.handleSheetChange}
|
||||
onDismiss={props.handleSheetDismiss}
|
||||
backdropOpacity={0.45}
|
||||
enablePanDownToClose
|
||||
backgroundComponent={ComboboxSheetBackground}
|
||||
@@ -1180,8 +1168,7 @@ function DesktopComboboxOptionsBody(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function DesktopComboboxBody(props: DesktopBodyProps): ReactElement | null {
|
||||
const active = useRetainedPanelActive();
|
||||
function DesktopComboboxBody(props: DesktopBodyProps): ReactElement {
|
||||
const handleDesktopKey = props.handleDesktopKey;
|
||||
const handleWebOverlayKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
@@ -1203,9 +1190,6 @@ function DesktopComboboxBody(props: DesktopBodyProps): ReactElement | null {
|
||||
[props.refs, setWebOverlayScope],
|
||||
);
|
||||
|
||||
// Remove the portal before a retained chat can freeze its closing update.
|
||||
if (!active) return null;
|
||||
|
||||
const overlay = (
|
||||
<OverlayLayerProvider layer={props.overlayLayer}>
|
||||
<View
|
||||
@@ -1325,7 +1309,6 @@ export function Combobox({
|
||||
const resolvedEmptyText = emptyText ?? t("common.empty.noOptionsMatchSearch");
|
||||
const resolvedTitle = title ?? t("common.actions.select");
|
||||
const isMobile = useIsCompactFormFactor();
|
||||
const active = useRetainedPanelActive();
|
||||
const floatingLayer = useOverlayLayer("floating");
|
||||
const safeAreaInsets = useSafeAreaInsets();
|
||||
const titleColor = theme.colors.foreground;
|
||||
@@ -1365,10 +1348,6 @@ export function Combobox({
|
||||
bumpSearchResetKey();
|
||||
}, [setOpen, setSearchQueryWithCallback]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!active && isOpen) handleClose();
|
||||
}, [active, isOpen, handleClose]);
|
||||
|
||||
useResetSearchOnOpen(isOpen, setSearchQueryWithCallback, bumpSearchResetKey);
|
||||
|
||||
const collisionPadding = useMemo(computeCollisionPadding, []);
|
||||
@@ -1434,6 +1413,16 @@ export function Combobox({
|
||||
referenceWidth,
|
||||
});
|
||||
|
||||
const {
|
||||
sheetRef: bottomSheetRef,
|
||||
handleSheetChange,
|
||||
handleSheetDismiss,
|
||||
} = useIsolatedBottomSheetVisibility({
|
||||
visible: isOpen,
|
||||
isEnabled: isMobile,
|
||||
onClose: handleClose,
|
||||
});
|
||||
|
||||
const normalizedSearch = searchable ? searchQuery.trim().toLowerCase() : "";
|
||||
const sanitizedSearchValue = searchQuery.trim();
|
||||
const showCustomOption = useMemo(
|
||||
@@ -1583,8 +1572,10 @@ export function Combobox({
|
||||
if (isMobile) {
|
||||
return (
|
||||
<MobileComboboxBody
|
||||
isOpen={isOpen}
|
||||
bottomSheetRef={bottomSheetRef}
|
||||
snapPoints={snapPoints}
|
||||
handleSheetChange={handleSheetChange}
|
||||
handleSheetDismiss={handleSheetDismiss}
|
||||
handleIndicatorStyle={handleIndicatorStyle}
|
||||
titleColor={titleColor}
|
||||
title={resolvedTitle}
|
||||
|
||||
@@ -58,6 +58,7 @@ vi.mock("@gorhom/bottom-sheet", async () => {
|
||||
});
|
||||
|
||||
import { IsolatedBottomSheetModal, type ContextBridge } from ".";
|
||||
import { useAnimatedLayout } from "@gorhom/bottom-sheet/src/hooks/useAnimatedLayout";
|
||||
import { useIsInsideBottomSheet } from "@/components/ui/bottom-sheet-scope";
|
||||
|
||||
/** Renders whatever the portal is currently holding, from its own place in the tree. */
|
||||
@@ -200,3 +201,35 @@ describe("IsolatedBottomSheetModal context bridging", () => {
|
||||
).toEqual(["false", "true"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bottom sheet layout subscription", () => {
|
||||
it("stops updating a dismissed sheet when the window resizes", () => {
|
||||
const container = document.createElement("div");
|
||||
const root = createRoot(container);
|
||||
let layout: ReturnType<typeof useAnimatedLayout> | undefined;
|
||||
const viewport = document.documentElement;
|
||||
const originalWidth = Object.getOwnPropertyDescriptor(viewport, "clientWidth");
|
||||
function resize(width: number) {
|
||||
Object.defineProperty(viewport, "clientWidth", { configurable: true, value: width });
|
||||
act(() => window.dispatchEvent(new Event("resize")));
|
||||
}
|
||||
function LayoutProbe() {
|
||||
layout = useAnimatedLayout(undefined, 0, 0);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
act(() => root.render(<LayoutProbe />));
|
||||
if (!layout) throw new Error("Sheet layout did not mount");
|
||||
const mountedLayout = layout;
|
||||
resize(640);
|
||||
expect(mountedLayout.get().window.width).toBe(640);
|
||||
act(() => root.render(null));
|
||||
resize(960);
|
||||
expect(mountedLayout.get().window.width).toBe(640);
|
||||
} finally {
|
||||
act(() => root.unmount());
|
||||
if (originalWidth) Object.defineProperty(viewport, "clientWidth", originalWidth);
|
||||
else Reflect.deleteProperty(viewport, "clientWidth");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,7 +107,7 @@ function createScreenRestorer(
|
||||
|
||||
return {
|
||||
async restore(snapshot: Awaited<ReturnType<typeof benchmarkSnapshot>>) {
|
||||
drain.restoreText(renderTerminalSnapshotToAnsi(snapshot));
|
||||
drain.restoreSnapshot(snapshot);
|
||||
await drain.flush();
|
||||
frameRecorder.frames[0]?.();
|
||||
return screen;
|
||||
@@ -116,38 +116,6 @@ function createScreenRestorer(
|
||||
}
|
||||
|
||||
describe("native terminal output drain", () => {
|
||||
test.each([41, 35])(
|
||||
"keeps a restored shell cursor beside its prompt after replay at %i rows",
|
||||
async (rows) => {
|
||||
const source = createNativeHeadlessTerminal({ rows: 41, cols: 49 });
|
||||
await source.write(Array.from({ length: 16 }, (_, i) => `probe ${i}\r\n`).join("") + "$ ");
|
||||
const snapshot = source.getState();
|
||||
const terminal = createNativeHeadlessTerminal({ rows, cols: 49 });
|
||||
const frameRecorder = createFrameRecorder();
|
||||
const drain = createNativeTerminalOutputDrain({
|
||||
write: (chunk) => terminal.write(chunk),
|
||||
reset: () => terminal.reset(),
|
||||
getViewportState: () => terminal.getViewportState(),
|
||||
onPaint: () => {},
|
||||
scheduleFrame: frameRecorder.scheduleFrame,
|
||||
cancelFrame: frameRecorder.cancelFrame,
|
||||
});
|
||||
try {
|
||||
drain.restoreText(renderTerminalSnapshotToAnsi(snapshot));
|
||||
await drain.flush();
|
||||
terminal.resize({ rows: 41, cols: 49 });
|
||||
const restored = terminal.getState();
|
||||
const promptRow = restored.grid.findIndex((row) => rowText(row) === "$ ".trimEnd());
|
||||
expect(promptRow).toBeGreaterThanOrEqual(0);
|
||||
expect(restored.cursor).toEqual({ row: promptRow, col: 2 });
|
||||
} finally {
|
||||
drain.dispose();
|
||||
terminal.dispose();
|
||||
source.dispose();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test("drains large bursts without waiting one frame per 2KB parse slice", async () => {
|
||||
const writes: string[] = [];
|
||||
const parseSamples: Array<{ parsedChars: number; queuedChars: number }> = [];
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { expect, test } from "vitest";
|
||||
import { TerminalGridView } from "./terminal-grid-view.native";
|
||||
import type { TerminalViewportState } from "./headless-terminal-state";
|
||||
import type { TerminalGridCellMetrics } from "./terminal-grid-metrics";
|
||||
|
||||
test("measures a single font cell before the terminal has its viewport columns", async () => {
|
||||
const container = document.createElement("div");
|
||||
container.style.cssText = "width: 400px; height: 700px; display: flex";
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
const measurements: TerminalGridCellMetrics[] = [];
|
||||
const state: TerminalViewportState = {
|
||||
rows: 1,
|
||||
cols: 1,
|
||||
firstRow: 0,
|
||||
oldestRow: 0,
|
||||
newestRow: 0,
|
||||
grid: [[{ char: " " }]],
|
||||
cursor: { row: 0, col: 0 },
|
||||
};
|
||||
const style = { flex: 1 };
|
||||
const recordMeasurement = (metrics: TerminalGridCellMetrics) => measurements.push(metrics);
|
||||
try {
|
||||
act(() =>
|
||||
root.render(
|
||||
React.createElement(TerminalGridView, {
|
||||
state,
|
||||
fontSize: 12,
|
||||
style,
|
||||
onCellMetricsChange: recordMeasurement,
|
||||
}),
|
||||
),
|
||||
);
|
||||
await expect.poll(() => measurements.length).toBeGreaterThan(0);
|
||||
const sample = [...container.querySelectorAll("div")].find(
|
||||
(element) => element.textContent === "mmmmmmmmmm" && element.childElementCount === 0,
|
||||
);
|
||||
expect(sample).toBeDefined();
|
||||
// Native Text wraps to its containing block. Font measurement must fit even
|
||||
// when the terminal starts with one column, before layout determines its size.
|
||||
expect(sample!.parentElement!.getBoundingClientRect().width).toBeGreaterThanOrEqual(
|
||||
sample!.getBoundingClientRect().width,
|
||||
);
|
||||
// The first measurement is used to size the emulator before restore data arrives.
|
||||
// A wrapped ten-character sample reports a fraction of a cell and multiple lines.
|
||||
expect(measurements[0].cellWidth).toBeGreaterThan(5);
|
||||
expect(measurements[0].cellHeight).toBeLessThan(24);
|
||||
} finally {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
}
|
||||
});
|
||||
@@ -407,11 +407,10 @@ export function TerminalGridView({
|
||||
|
||||
return (
|
||||
<View onLayout={handleContainerLayout} pointerEvents="none" style={containerStyle}>
|
||||
{/* Measure against the viewport, independently of the grid width derived from these metrics. */}
|
||||
<Text onLayout={handleMeasure} pointerEvents="none" style={measureStyle}>
|
||||
{MEASURE_TEXT}
|
||||
</Text>
|
||||
<View style={gridStyle}>
|
||||
<Text onLayout={handleMeasure} pointerEvents="none" style={measureStyle}>
|
||||
{MEASURE_TEXT}
|
||||
</Text>
|
||||
{rows.map((row) => (
|
||||
<MemoTerminalGridRow
|
||||
key={row.index}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.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.13.0",
|
||||
"@getpaseo/protocol": "0.13.0",
|
||||
"@getpaseo/server": "0.13.0",
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/server": "0.13.2",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/client",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"description": "Paseo client SDK package",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -47,8 +47,8 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.13.0",
|
||||
"@getpaseo/relay": "0.13.0",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/relay": "0.13.2",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.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.13.0",
|
||||
"version": "0.13.2",
|
||||
"description": "Native module for two way audio streaming",
|
||||
"keywords": [
|
||||
"ExpoTwoWayAudio",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/plugin",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"description": "Paseo plugin SDK package",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -72,8 +72,8 @@
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@getpaseo/client": "0.13.0",
|
||||
"@getpaseo/protocol": "0.13.0",
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/react": "~19.2.0",
|
||||
"react": "19.2.3",
|
||||
@@ -83,8 +83,8 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@getpaseo/client": "0.13.0",
|
||||
"@getpaseo/protocol": "0.13.0",
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"react": ">=19.1.0 <20",
|
||||
"react-native": ">=0.81.5",
|
||||
"zod": "^4.4.3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"description": "Paseo shared protocol schemas and wire types",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"description": "Paseo relay for bridging daemon and client connections",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"description": "Paseo backend server",
|
||||
"files": [
|
||||
"dist/server",
|
||||
@@ -107,11 +107,11 @@
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.246",
|
||||
"@anthropic-ai/sdk": "^0.104.2",
|
||||
"@getpaseo/client": "0.13.0",
|
||||
"@getpaseo/highlight": "0.13.0",
|
||||
"@getpaseo/plugin": "0.13.0",
|
||||
"@getpaseo/protocol": "0.13.0",
|
||||
"@getpaseo/relay": "0.13.0",
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/highlight": "0.13.2",
|
||||
"@getpaseo/plugin": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/relay": "0.13.2",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import fs, { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import pino from "pino";
|
||||
@@ -7,9 +7,6 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { ClaudeAgentClient } from "./agent.js";
|
||||
import { claudeProjectDirSync } from "./project-dir.js";
|
||||
import { withClaudeReplayHistory } from "./transcript-history.js";
|
||||
import type { AgentSession, AgentStreamEvent } from "../../agent-sdk-types.js";
|
||||
import type { ClaudeQueryFactory } from "./query.js";
|
||||
import { collectSessionTurnEvents } from "../test-utils/session-stream-adapter.js";
|
||||
|
||||
/**
|
||||
* A resumed Claude agent whose transcript is missing or unreadable opens with an empty timeline.
|
||||
@@ -32,18 +29,19 @@ describe("ClaudeAgentSession persisted history load", () => {
|
||||
let configDir: string;
|
||||
let records: LogRecord[];
|
||||
|
||||
async function resume(queryFactory?: ClaudeQueryFactory): Promise<AgentSession> {
|
||||
async function resume(): Promise<void> {
|
||||
const client = new ClaudeAgentClient({
|
||||
logger: pino({ level: "info" }, { write: (line: string) => records.push(JSON.parse(line)) }),
|
||||
queryFactory:
|
||||
queryFactory ??
|
||||
(() => {
|
||||
throw new Error("history load must not start a query");
|
||||
}),
|
||||
resolveBinary: async () => process.execPath,
|
||||
queryFactory: vi.fn(() => {
|
||||
throw new Error("history load must not start a query");
|
||||
}),
|
||||
resolveVersion: async () => "2.1.220",
|
||||
});
|
||||
return client.resumeSession({ provider: "claude", sessionId: SESSION_ID }, { cwd });
|
||||
const session = await client.resumeSession(
|
||||
{ provider: "claude", sessionId: SESSION_ID },
|
||||
{ cwd },
|
||||
);
|
||||
await session.close();
|
||||
}
|
||||
|
||||
function transcriptPath(): string {
|
||||
@@ -60,218 +58,12 @@ describe("ClaudeAgentSession persisted history load", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test.each(["file_path", "files"])(
|
||||
"replays %s without probing targets and preserves live existence checks",
|
||||
async (field) => {
|
||||
const existingPath = path.join(cwd, "existing.txt");
|
||||
const missingPath = path.join(cwd, "missing.txt");
|
||||
writeFileSync(existingPath, "existing");
|
||||
const targets = [existingPath, missingPath];
|
||||
const inputs = targets.map((target) => ({
|
||||
[field]: field === "files" ? [target] : target,
|
||||
}));
|
||||
const toolBlocks = inputs.map((input, index) => ({
|
||||
type: "tool_use",
|
||||
id: `change-${index}`,
|
||||
name: "FileChange",
|
||||
input,
|
||||
}));
|
||||
const resultBlocks = toolBlocks.map((block) => ({
|
||||
type: "tool_result",
|
||||
tool_use_id: block.id,
|
||||
content: "done",
|
||||
}));
|
||||
const transcript = [
|
||||
{ type: "user", message: { content: "Change files" } },
|
||||
{ type: "assistant", message: { content: toolBlocks } },
|
||||
{ type: "user", message: { content: resultBlocks } },
|
||||
{ type: "assistant", message: { content: "Finished" } },
|
||||
];
|
||||
mkdirSync(path.dirname(transcriptPath()), { recursive: true });
|
||||
writeFileSync(transcriptPath(), transcript.map((entry) => JSON.stringify(entry)).join("\n"));
|
||||
|
||||
// Observe the real filesystem calls; neither existing nor missing targets are mocked.
|
||||
const exists = vi.spyOn(fs, "existsSync");
|
||||
const liveMessages = [
|
||||
{ type: "assistant", message: { content: toolBlocks } },
|
||||
{ type: "user", message: { content: resultBlocks } },
|
||||
{ type: "result", subtype: "success", total_cost_usd: 0 },
|
||||
];
|
||||
const queryFactory = vi.fn();
|
||||
queryFactory.mockReturnValue({
|
||||
async next() {
|
||||
const value = liveMessages.shift();
|
||||
return value ? { done: false, value } : { done: true, value: undefined };
|
||||
},
|
||||
close() {},
|
||||
async return() {
|
||||
return { done: true, value: undefined };
|
||||
},
|
||||
async supportedCommands() {
|
||||
return [];
|
||||
},
|
||||
[Symbol.asyncIterator]() {
|
||||
return this;
|
||||
},
|
||||
});
|
||||
const session = await resume(queryFactory);
|
||||
try {
|
||||
const history: AgentStreamEvent[] = [];
|
||||
for await (const event of session.streamHistory()) history.push(event);
|
||||
expect(exists.mock.calls.filter(([target]) => targets.includes(String(target)))).toEqual(
|
||||
[],
|
||||
);
|
||||
const running = inputs.map((input, index) => ({
|
||||
type: "tool_call",
|
||||
callId: `change-${index}`,
|
||||
name: "FileChange",
|
||||
status: "running",
|
||||
error: null,
|
||||
detail: { type: "unknown", input, output: null },
|
||||
}));
|
||||
const completed = inputs.map((input, index) => ({
|
||||
type: "tool_call",
|
||||
callId: `change-${index}`,
|
||||
name: "FileChange",
|
||||
status: "completed",
|
||||
error: null,
|
||||
detail: {
|
||||
type: "unknown",
|
||||
input,
|
||||
output: {
|
||||
output: "done",
|
||||
files: [{ path: index === 0 ? "existing.txt" : "missing.txt", kind: "update" }],
|
||||
},
|
||||
},
|
||||
}));
|
||||
expect(history).toEqual(
|
||||
[
|
||||
{ type: "user_message", text: "Change files" },
|
||||
...running,
|
||||
...completed,
|
||||
{ type: "assistant_message", text: "Finished" },
|
||||
].map((item) => ({ type: "timeline", provider: "claude", timestamp: undefined, item })),
|
||||
);
|
||||
|
||||
exists.mockClear();
|
||||
const live = await collectSessionTurnEvents(session, "Change files again");
|
||||
expect(exists.mock.calls.filter(([target]) => targets.includes(String(target)))).toEqual([
|
||||
[existingPath],
|
||||
[missingPath],
|
||||
]);
|
||||
const liveCompleted = live.flatMap((event) =>
|
||||
event.type === "timeline" &&
|
||||
event.item.type === "tool_call" &&
|
||||
event.item.status === "completed"
|
||||
? [event.item]
|
||||
: [],
|
||||
);
|
||||
const missing = completed[1];
|
||||
expect(liveCompleted).toEqual([
|
||||
completed[0],
|
||||
{
|
||||
...missing,
|
||||
detail: {
|
||||
...missing.detail,
|
||||
output: { output: "done", files: [{ path: "missing.txt", kind: "add" }] },
|
||||
},
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test.each(["parent", "subagent"])(
|
||||
"replays %s Write and Edit calls without probing their targets",
|
||||
async (scope) => {
|
||||
const filePath = path.join(cwd, "unavailable", "file.txt");
|
||||
const content = ["Write", "Edit"].map((name) => ({
|
||||
type: "tool_use",
|
||||
id: name,
|
||||
name,
|
||||
input: { file_path: filePath },
|
||||
}));
|
||||
const entry = { type: "assistant", message: { content } };
|
||||
mkdirSync(path.dirname(transcriptPath()), { recursive: true });
|
||||
if (scope === "subagent") {
|
||||
const directory = path.join(path.dirname(transcriptPath()), SESSION_ID, "subagents");
|
||||
mkdirSync(directory, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(directory, "agent-child.meta.json"),
|
||||
JSON.stringify({ toolUseId: "task" }),
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(directory, "agent-child.jsonl"),
|
||||
JSON.stringify({
|
||||
...entry,
|
||||
isSidechain: true,
|
||||
agentId: "child",
|
||||
}),
|
||||
);
|
||||
writeFileSync(
|
||||
transcriptPath(),
|
||||
JSON.stringify({
|
||||
type: "assistant",
|
||||
message: {
|
||||
content: [
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "task",
|
||||
name: "Task",
|
||||
input: { description: "Change files" },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
writeFileSync(transcriptPath(), JSON.stringify(entry));
|
||||
}
|
||||
|
||||
const exists = vi.spyOn(fs, "existsSync");
|
||||
const session = await resume();
|
||||
try {
|
||||
const items = [];
|
||||
for await (const event of session.streamHistory()) {
|
||||
if (scope === "parent" && event.type === "timeline") items.push(event.item);
|
||||
if (event.type === "provider_subagent" && event.event.type === "timeline") {
|
||||
items.push(event.event.item);
|
||||
}
|
||||
}
|
||||
expect(exists.mock.calls.filter(([target]) => target === filePath)).toEqual([]);
|
||||
expect(items).toEqual([
|
||||
{
|
||||
type: "tool_call",
|
||||
callId: "Write",
|
||||
name: "Write",
|
||||
status: "running",
|
||||
error: null,
|
||||
detail: { type: "write", filePath },
|
||||
},
|
||||
{
|
||||
type: "tool_call",
|
||||
callId: "Edit",
|
||||
name: "Edit",
|
||||
status: "running",
|
||||
error: null,
|
||||
detail: { type: "edit", filePath },
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test("logs the resolved transcript path when the transcript does not exist", async () => {
|
||||
await (await resume()).close();
|
||||
await resume();
|
||||
|
||||
expect(records).toContainEqual(
|
||||
expect.objectContaining({
|
||||
@@ -287,7 +79,7 @@ describe("ClaudeAgentSession persisted history load", () => {
|
||||
// A directory at the transcript path exists but cannot be read as a file.
|
||||
mkdirSync(transcriptPath(), { recursive: true });
|
||||
|
||||
await (await resume()).close();
|
||||
await resume();
|
||||
|
||||
expect(records).toContainEqual(
|
||||
expect.objectContaining({
|
||||
@@ -356,7 +148,7 @@ describe("ClaudeAgentSession persisted history load", () => {
|
||||
chmodSync(path.join(configDir, "projects"), 0o000);
|
||||
|
||||
try {
|
||||
await (await resume()).close();
|
||||
await resume();
|
||||
} finally {
|
||||
chmodSync(path.join(configDir, "projects"), 0o755);
|
||||
}
|
||||
|
||||
@@ -2326,7 +2326,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
{ type: "provider_subagent" }
|
||||
>[] = [];
|
||||
private historyPending = false;
|
||||
private replayingHistory = false;
|
||||
private turnState: TurnState = "idle";
|
||||
private nextTurnOrdinal = 1;
|
||||
private cancelCurrentTurn: (() => void) | null = null;
|
||||
@@ -5254,7 +5253,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
|
||||
private loadPersistedHistory(sessionId: string): void {
|
||||
let historyPath: string | null = null;
|
||||
this.replayingHistory = true;
|
||||
try {
|
||||
this.taskState.reset();
|
||||
historyPath = this.resolveHistoryPath(sessionId);
|
||||
@@ -5274,8 +5272,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
{ err: error, sessionId, historyPath },
|
||||
"Failed to load Claude history from transcript",
|
||||
);
|
||||
} finally {
|
||||
this.replayingHistory = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6043,11 +6039,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
private detectFileKind(filePath: string): string {
|
||||
// A historical target may be on an unreachable mount; its current state says nothing
|
||||
// about whether the past operation created or updated it.
|
||||
if (this.replayingHistory) {
|
||||
return "update";
|
||||
}
|
||||
try {
|
||||
return fs.existsSync(filePath) ? "update" : "add";
|
||||
} catch {
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import type { SessionInfo } from "@opencode/client";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import type { AgentStreamEvent } from "../../../agent-sdk-types.js";
|
||||
import { V2Harness } from "../test-utils/v2-harness.js";
|
||||
import { SessionChildren } from "./children.js";
|
||||
|
||||
function setup() {
|
||||
const harness = new V2Harness();
|
||||
const child: SessionInfo = {
|
||||
...harness.info,
|
||||
id: "child",
|
||||
parentID: "session",
|
||||
title: "Check the worker model",
|
||||
agent: "worker",
|
||||
model: { providerID: "test", id: "worker-model", variant: "xhigh" },
|
||||
tokens: { input: 10000, output: 3000, reasoning: 2000, cache: { read: 3000, write: 800 } },
|
||||
};
|
||||
harness.api.session.list = async (input) => ({
|
||||
data: input?.parentID === "session" ? [child] : [],
|
||||
cursor: {},
|
||||
});
|
||||
harness.api.session.get = async () => child;
|
||||
const events: AgentStreamEvent[] = [];
|
||||
const children = new SessionChildren({
|
||||
client: () => harness.api,
|
||||
id: "session",
|
||||
emit: (event) => events.push(event),
|
||||
reconcilePermissions: async () => {},
|
||||
});
|
||||
const upserts = () =>
|
||||
events.flatMap((event) =>
|
||||
event.type === "provider_subagent" && event.event.type === "upsert" ? [event.event] : [],
|
||||
);
|
||||
return { children, child, upserts };
|
||||
}
|
||||
|
||||
describe("OpenCode v2 child presentation", () => {
|
||||
test("restores agent, model, variant and all token categories in the child subtitle", async () => {
|
||||
const { children, upserts } = setup();
|
||||
await children.reconcile("session");
|
||||
expect(upserts()).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "child",
|
||||
title: "Check the worker model",
|
||||
parentSubagentId: null,
|
||||
status: "completed",
|
||||
subtitle: "worker · worker-model · Extra High · 18.8k tokens",
|
||||
}),
|
||||
]);
|
||||
await children.reconcile("session");
|
||||
expect(upserts()).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("publishes changed child facts on live events even when its timestamp is unchanged", async () => {
|
||||
const { children, child, upserts } = setup();
|
||||
await children.observe({ id: "created", created: 1, type: "session.created", data: child });
|
||||
child.model = { providerID: "test", id: "next-model", variant: "low" };
|
||||
child.tokens.output += 1200;
|
||||
await children.observe({
|
||||
id: "finished",
|
||||
created: 2,
|
||||
type: "session.execution.succeeded",
|
||||
durable: { aggregateID: "child", seq: 1, version: 1 },
|
||||
data: { sessionID: "child" },
|
||||
});
|
||||
expect(upserts().map((event) => event.subtitle)).toEqual([
|
||||
"worker · worker-model · Extra High · 18.8k tokens",
|
||||
"worker · next-model · Low · 20k tokens",
|
||||
]);
|
||||
});
|
||||
|
||||
test("keeps available agent information before a child has a model or token usage", async () => {
|
||||
const { children, child, upserts } = setup();
|
||||
delete child.model;
|
||||
child.tokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } };
|
||||
await children.reconcile("session");
|
||||
expect(upserts()[0]).toMatchObject({ title: "Check the worker model", subtitle: "worker" });
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,6 @@ import type { AgentStreamEvent } from "../../../agent-sdk-types.js";
|
||||
import type { ProviderSubagentInputEvent } from "../../../provider-subagents/store.js";
|
||||
|
||||
import { messages } from "./history.js";
|
||||
import { buildOpenCodeSubagentSubtitle } from "../subagent-presentation.js";
|
||||
|
||||
import type { V2Api } from "./api.js";
|
||||
interface ChildrenOptions {
|
||||
@@ -66,23 +65,11 @@ export class SessionChildren {
|
||||
if (active[info.id]) status = "running";
|
||||
else if (info.outcome === "failed") status = "failed";
|
||||
else if (info.outcome === "interrupted") status = "canceled";
|
||||
const subtitle = buildOpenCodeSubagentSubtitle({
|
||||
agentName: info.agent,
|
||||
modelId: info.model?.id,
|
||||
variant: info.model?.variant,
|
||||
totalTokens:
|
||||
info.tokens.input +
|
||||
info.tokens.output +
|
||||
info.tokens.reasoning +
|
||||
info.tokens.cache.read +
|
||||
info.tokens.cache.write,
|
||||
});
|
||||
const presentation: ProviderSubagentInputEvent = {
|
||||
type: "upsert",
|
||||
id: info.id,
|
||||
parentSubagentId: info.parentID === this.options.id ? null : info.parentID,
|
||||
title: info.title ?? null,
|
||||
...(subtitle ? { subtitle } : {}),
|
||||
status,
|
||||
cwd: info.location.directory,
|
||||
timestamp: new Date(info.time.updated).toISOString(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -13,6 +13,27 @@ index ee51e76..dc1bc17 100644
|
||||
if (_providedOnChange) {
|
||||
_providedOnChange(_index, _position, _type);
|
||||
}
|
||||
diff --git a/node_modules/@gorhom/bottom-sheet/lib/commonjs/hooks/useAnimatedLayout.js b/node_modules/@gorhom/bottom-sheet/lib/commonjs/hooks/useAnimatedLayout.js
|
||||
index 973d05c..b484b5f 100644
|
||||
--- a/node_modules/@gorhom/bottom-sheet/lib/commonjs/hooks/useAnimatedLayout.js
|
||||
+++ b/node_modules/@gorhom/bottom-sheet/lib/commonjs/hooks/useAnimatedLayout.js
|
||||
@@ -86,7 +86,7 @@ function useAnimatedLayout(containerLayoutState, topInset, bottomInset, modal, s
|
||||
});
|
||||
}, [state, verticalInset, modal]);
|
||||
(0, _react.useEffect)(() => {
|
||||
- _reactNative.Dimensions.addEventListener('change', ({
|
||||
+ const subscription = _reactNative.Dimensions.addEventListener('change', ({
|
||||
window
|
||||
}) => {
|
||||
state.modify(_state => {
|
||||
@@ -96,6 +96,7 @@ function useAnimatedLayout(containerLayoutState, topInset, bottomInset, modal, s
|
||||
return _state;
|
||||
});
|
||||
});
|
||||
+ return () => subscription.remove();
|
||||
}, [state]);
|
||||
//#endregion
|
||||
|
||||
diff --git a/node_modules/@gorhom/bottom-sheet/lib/commonjs/index.js b/node_modules/@gorhom/bottom-sheet/lib/commonjs/index.js
|
||||
index e44ad86..96555ef 100644
|
||||
--- a/node_modules/@gorhom/bottom-sheet/lib/commonjs/index.js
|
||||
@@ -54,6 +75,27 @@ index 94fabdb..60ca66a 100644
|
||||
if (_providedOnChange) {
|
||||
_providedOnChange(_index, _position, _type);
|
||||
}
|
||||
diff --git a/node_modules/@gorhom/bottom-sheet/lib/module/hooks/useAnimatedLayout.js b/node_modules/@gorhom/bottom-sheet/lib/module/hooks/useAnimatedLayout.js
|
||||
index b0eec93..67713a2 100644
|
||||
--- a/node_modules/@gorhom/bottom-sheet/lib/module/hooks/useAnimatedLayout.js
|
||||
+++ b/node_modules/@gorhom/bottom-sheet/lib/module/hooks/useAnimatedLayout.js
|
||||
@@ -82,7 +82,7 @@ export function useAnimatedLayout(containerLayoutState, topInset, bottomInset, m
|
||||
});
|
||||
}, [state, verticalInset, modal]);
|
||||
useEffect(() => {
|
||||
- Dimensions.addEventListener('change', ({
|
||||
+ const subscription = Dimensions.addEventListener('change', ({
|
||||
window
|
||||
}) => {
|
||||
state.modify(_state => {
|
||||
@@ -92,6 +92,7 @@ export function useAnimatedLayout(containerLayoutState, topInset, bottomInset, m
|
||||
return _state;
|
||||
});
|
||||
});
|
||||
+ return () => subscription.remove();
|
||||
}, [state]);
|
||||
//#endregion
|
||||
|
||||
diff --git a/node_modules/@gorhom/bottom-sheet/lib/module/index.js b/node_modules/@gorhom/bottom-sheet/lib/module/index.js
|
||||
index 8259637..eb057fe 100644
|
||||
--- a/node_modules/@gorhom/bottom-sheet/lib/module/index.js
|
||||
@@ -101,6 +143,26 @@ index 2954b3f..7260656 100644
|
||||
|
||||
if (_providedOnChange) {
|
||||
_providedOnChange(_index, _position, _type);
|
||||
diff --git a/node_modules/@gorhom/bottom-sheet/src/hooks/useAnimatedLayout.ts b/node_modules/@gorhom/bottom-sheet/src/hooks/useAnimatedLayout.ts
|
||||
index 68c674d..564833c 100644
|
||||
--- a/node_modules/@gorhom/bottom-sheet/src/hooks/useAnimatedLayout.ts
|
||||
+++ b/node_modules/@gorhom/bottom-sheet/src/hooks/useAnimatedLayout.ts
|
||||
@@ -106,13 +106,14 @@ export function useAnimatedLayout(
|
||||
[state, verticalInset, modal]
|
||||
);
|
||||
useEffect(() => {
|
||||
- Dimensions.addEventListener('change', ({ window }) => {
|
||||
+ const subscription = Dimensions.addEventListener('change', ({ window }) => {
|
||||
state.modify(_state => {
|
||||
'worklet';
|
||||
_state.window = window;
|
||||
return _state;
|
||||
});
|
||||
});
|
||||
+ return () => subscription.remove();
|
||||
}, [state]);
|
||||
//#endregion
|
||||
|
||||
diff --git a/node_modules/@gorhom/bottom-sheet/src/index.ts b/node_modules/@gorhom/bottom-sheet/src/index.ts
|
||||
index 7aaea57..6a5f5a4 100644
|
||||
--- a/node_modules/@gorhom/bottom-sheet/src/index.ts
|
||||
|
||||
@@ -40,9 +40,10 @@ index 0a1c6c2..5935112 100644
|
||||
|
||||
struct OnLinkLongPress {
|
||||
diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt
|
||||
index c12937c..8105833 100644
|
||||
--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt
|
||||
+++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt
|
||||
@@ -95,7 +95,7 @@
|
||||
@@ -95,7 +95,7 @@ class EnrichedMarkdown(
|
||||
private var accessibilityLabels = AccessibilityLabels()
|
||||
private var textBreakStrategy: String = BreakStrategyUtils.DEFAULT_STRATEGY
|
||||
|
||||
@@ -51,7 +52,7 @@ diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/c
|
||||
private var onLinkLongPressCallback: ((String) -> Unit)? = null
|
||||
private var onTaskListItemPressCallback: ((Int, Boolean, String) -> Unit)? = null
|
||||
private var onLatexErrorCallback: LatexErrorReporter? = null
|
||||
@@ -296,7 +296,7 @@
|
||||
@@ -296,7 +296,7 @@ class EnrichedMarkdown(
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +61,7 @@ diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/c
|
||||
onLinkPressCallback = callback
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@
|
||||
@@ -405,7 +405,7 @@ class EnrichedMarkdown(
|
||||
val codeLinkRegex = inlineCodeLinkRegex
|
||||
val renderId = ++currentRenderId
|
||||
|
||||
@@ -69,7 +70,7 @@ diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/c
|
||||
try {
|
||||
val filtered =
|
||||
if (isStreaming) {
|
||||
@@ -418,13 +418,13 @@
|
||||
@@ -418,13 +418,13 @@ class EnrichedMarkdown(
|
||||
|
||||
if (renderableMarkdown.isEmpty()) {
|
||||
postToMain(renderId) { applyRenderedSegments(emptyList(), false) }
|
||||
@@ -85,7 +86,19 @@ diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/c
|
||||
}
|
||||
|
||||
val segments = splitASTIntoSegments(ast)
|
||||
@@ -646,8 +646,12 @@
|
||||
@@ -478,6 +478,11 @@ class EnrichedMarkdown(
|
||||
if (forceHeight || topologyChanged || heightBefore != heightAfter) {
|
||||
MeasurementStore.invalidate(id)
|
||||
requestLayout()
|
||||
+ stateWrapper?.let {
|
||||
+ val state = Arguments.createMap()
|
||||
+ state.putInt("forceHeightRecalculationCounter", ++forceHeightRecalculationCounter)
|
||||
+ it.updateState(state)
|
||||
+ }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -646,8 +651,12 @@ class EnrichedMarkdown(
|
||||
renderId: Long,
|
||||
action: () -> Unit,
|
||||
) {
|
||||
@@ -3138,3 +3151,89 @@ new file mode 100644
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
diff --git a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.cpp b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.cpp
|
||||
index a430d1a..b30a017 100644
|
||||
--- a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.cpp
|
||||
+++ b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.cpp
|
||||
@@ -22,6 +22,14 @@ void MarkdownContainerShadowNode::dirtyLayoutIfNeeded() {
|
||||
}
|
||||
}
|
||||
|
||||
+// Only new props change the rendered markdown. When a parent is cloned, React Native's adoptYogaChild
|
||||
+// re-clones its children without new props, and dirtying those clones re-measures every settled message
|
||||
+// whenever the list above grows.
|
||||
+bool MarkdownContainerShadowNode::shouldNewRevisionDirtyMeasurement(const ShadowNode & /*sourceShadowNode*/,
|
||||
+ const ShadowNodeFragment &fragment) const {
|
||||
+ return fragment.props != nullptr;
|
||||
+}
|
||||
+
|
||||
Size MarkdownContainerShadowNode::measureContent(const LayoutContext &layoutContext,
|
||||
const LayoutConstraints &layoutConstraints) const {
|
||||
return measurementsManager_->measure(getSurfaceId(), getTag(), getConcreteProps(), layoutConstraints);
|
||||
diff --git a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.h b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.h
|
||||
index dea8bb2..6c4bd20 100644
|
||||
--- a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.h
|
||||
+++ b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.h
|
||||
@@ -19,6 +19,8 @@ public:
|
||||
|
||||
MarkdownContainerShadowNode(ShadowNode const &sourceShadowNode, ShadowNodeFragment const &fragment)
|
||||
: ConcreteViewShadowNode(sourceShadowNode, fragment) {
|
||||
+ forceHeightRecalculationCounter_ =
|
||||
+ static_cast<const MarkdownContainerShadowNode &>(sourceShadowNode).forceHeightRecalculationCounter_;
|
||||
dirtyLayoutIfNeeded();
|
||||
}
|
||||
|
||||
@@ -35,6 +37,10 @@ public:
|
||||
|
||||
Size measureContent(const LayoutContext &layoutContext, const LayoutConstraints &layoutConstraints) const override;
|
||||
|
||||
+protected:
|
||||
+ bool shouldNewRevisionDirtyMeasurement(const ShadowNode &sourceShadowNode,
|
||||
+ const ShadowNodeFragment &fragment) const override;
|
||||
+
|
||||
private:
|
||||
int forceHeightRecalculationCounter_{0};
|
||||
std::shared_ptr<MarkdownContainerMeasurementManager> measurementsManager_;
|
||||
diff --git a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp
|
||||
index 3ca259b..ab51055 100644
|
||||
--- a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp
|
||||
+++ b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp
|
||||
@@ -22,6 +22,14 @@ void MarkdownTextShadowNode::dirtyLayoutIfNeeded() {
|
||||
}
|
||||
}
|
||||
|
||||
+// Only new props change the rendered markdown. When a parent is cloned, React Native's adoptYogaChild
|
||||
+// re-clones its children without new props, and dirtying those clones re-measures every settled message
|
||||
+// whenever the list above grows.
|
||||
+bool MarkdownTextShadowNode::shouldNewRevisionDirtyMeasurement(const ShadowNode & /*sourceShadowNode*/,
|
||||
+ const ShadowNodeFragment &fragment) const {
|
||||
+ return fragment.props != nullptr;
|
||||
+}
|
||||
+
|
||||
Size MarkdownTextShadowNode::measureContent(const LayoutContext &layoutContext,
|
||||
const LayoutConstraints &layoutConstraints) const {
|
||||
return measurementsManager_->measure(getSurfaceId(), getTag(), getConcreteProps(), layoutConstraints);
|
||||
diff --git a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h
|
||||
index eaa3279..05a02eb 100644
|
||||
--- a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h
|
||||
+++ b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h
|
||||
@@ -22,6 +22,8 @@ public:
|
||||
|
||||
MarkdownTextShadowNode(ShadowNode const &sourceShadowNode, ShadowNodeFragment const &fragment)
|
||||
: ConcreteViewShadowNode(sourceShadowNode, fragment) {
|
||||
+ forceHeightRecalculationCounter_ =
|
||||
+ static_cast<const MarkdownTextShadowNode &>(sourceShadowNode).forceHeightRecalculationCounter_;
|
||||
dirtyLayoutIfNeeded();
|
||||
}
|
||||
|
||||
@@ -39,6 +41,10 @@ public:
|
||||
|
||||
Size measureContent(const LayoutContext &layoutContext, const LayoutConstraints &layoutConstraints) const override;
|
||||
|
||||
+protected:
|
||||
+ bool shouldNewRevisionDirtyMeasurement(const ShadowNode &sourceShadowNode,
|
||||
+ const ShadowNodeFragment &fragment) const override;
|
||||
+
|
||||
private:
|
||||
int forceHeightRecalculationCounter_{0};
|
||||
std::shared_ptr<MarkdownTextMeasurementManager> measurementsManager_;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
diff --git a/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.kt b/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.kt
|
||||
index 48e3771..0fba1af 100644
|
||||
--- a/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.kt
|
||||
+++ b/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.kt
|
||||
@@ -245,6 +245,15 @@ open class NativeProxy {
|
||||
}.apply { isAccessible = true }
|
||||
}
|
||||
|
||||
+ // Backport of https://github.com/software-mansion/react-native-reanimated/pull/10435
|
||||
+ // (shipped in Reanimated 4.7.1). The reflective updatePropsSynchronously path skips RN's
|
||||
+ // view-existence guard, so a tag that unmounted with buffered updates throws
|
||||
+ // RetryableMountingLayerException on every event dispatch and Log.w printed the full stack
|
||||
+ // on the UI thread inside the draw pass. Remove once the installed Reanimated includes the fix.
|
||||
+ private val getViewExistsMethod by lazy {
|
||||
+ runCatching { mountingManager.javaClass.getMethod("getViewExists", Int::class.javaPrimitiveType) }.getOrNull()
|
||||
+ }
|
||||
+
|
||||
@DoNotStrip
|
||||
fun synchronouslyUpdateUIProps(
|
||||
intBuffer: IntArray,
|
||||
@@ -253,9 +262,13 @@ open class NativeProxy {
|
||||
SynchronousPropsBufferParser.parse(intBuffer, doubleBuffer) { viewTag, props ->
|
||||
if (BuildConfig.IS_REACT_NATIVE_86_OR_NEWER) {
|
||||
try {
|
||||
+ // The props stay in the registry, and the commit hook applies them to the shadow tree.
|
||||
+ if (getViewExistsMethod?.invoke(mountingManager, viewTag) == false) {
|
||||
+ return@parse
|
||||
+ }
|
||||
updatePropsSynchronouslyMethod.invoke(mountingManager, viewTag, props)
|
||||
} catch (e: Exception) {
|
||||
- Log.w("Reanimated", "synchronouslyUpdateUIProps failed for tag $viewTag", e)
|
||||
+ Log.w("Reanimated", "synchronouslyUpdateUIProps failed for tag $viewTag: ${e.cause ?: e}")
|
||||
}
|
||||
} else {
|
||||
mFabricUIManager.synchronouslyUpdateViewOnUIThread(viewTag, props)
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/builtin-plugins",
|
||||
"version": "0.13.0",
|
||||
"version": "0.13.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -14,6 +14,9 @@ const patchedPackages = [
|
||||
nodeModulesPath: "node_modules/expo-modules-core",
|
||||
patchPrefix: "expo-modules-core+",
|
||||
},
|
||||
// Backport https://github.com/software-mansion/enriched-markdown/pull/924: preserve settled
|
||||
// Android measurements across parent clones. Remove those hunks when upgrading past
|
||||
// 1.2.0-nightly-20261019-926ef06f0; reconcile the other local patch hunks separately.
|
||||
{
|
||||
nodeModulesPath: "packages/app/node_modules/react-native-enriched-markdown",
|
||||
patchPrefix: "react-native-enriched-markdown+",
|
||||
@@ -25,6 +28,14 @@ const patchedPackages = [
|
||||
nodeModulesPath: "node_modules/react-native-unistyles",
|
||||
patchPrefix: "react-native-unistyles+",
|
||||
},
|
||||
// Backport of https://github.com/software-mansion/react-native-reanimated/pull/10435
|
||||
// (Reanimated 4.7.1): skip synchronous Android prop updates for unmounted view tags
|
||||
// instead of throwing and printing a full stack trace on the UI thread per tag per event.
|
||||
// Remove when the installed Reanimated is >= 4.7.1.
|
||||
{
|
||||
nodeModulesPath: "node_modules/react-native-reanimated",
|
||||
patchPrefix: "react-native-reanimated+",
|
||||
},
|
||||
{
|
||||
nodeModulesPath: "node_modules/react-native-draggable-flatlist",
|
||||
patchPrefix: "react-native-draggable-flatlist+",
|
||||
@@ -37,6 +48,9 @@ const patchedPackages = [
|
||||
// modal stack (see packages/app/src/components/ui/isolated-bottom-sheet-modal/host-scope.tsx), and
|
||||
// keeps a dismissing modal's status through its final onChange(-1), so a modal with
|
||||
// enableDismissOnClose=false that unmounts while presented still leaves the portal.
|
||||
// Dimensions subscription cleanup: https://github.com/gorhom/react-native-bottom-sheet/issues/2734
|
||||
// and https://github.com/gorhom/react-native-bottom-sheet/pull/2751. Remove the useAnimatedLayout
|
||||
// hunks once an installed release includes the cleanup; retain the other local hunks.
|
||||
{
|
||||
nodeModulesPath: "node_modules/@gorhom/bottom-sheet",
|
||||
patchPrefix: "@gorhom+bottom-sheet+",
|
||||
|
||||
Reference in New Issue
Block a user