mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-29 20:52:51 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aef1ec662 | ||
|
|
946b6e58fd | ||
|
|
ac6e4e9f96 | ||
|
|
c4b4516890 | ||
|
|
75f58c4fe7 | ||
|
|
fb69bf3314 | ||
|
|
f7eef9dd0c | ||
|
|
7d74ae5d6c | ||
|
|
61841d1ed5 |
@@ -3,7 +3,7 @@ name: CI
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
# Every base branch, so stacked PRs and PRs into `next` get the same checks.
|
# Every base branch, so stacked PRs and PRs into any base get the same checks.
|
||||||
pull_request:
|
pull_request:
|
||||||
merge_group:
|
merge_group:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
Not released yet. These changes are on `main` but not in 0.14.0-beta.2; the next release entry includes them.
|
Not released yet. These changes are on `main` but not in 0.14.0; the next release entry includes them.
|
||||||
|
|
||||||
Added:
|
Added:
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ Improved:
|
|||||||
|
|
||||||
- Added `server.addHook` with `subject.operation.position` names and the awaited `worktree.create.after` hook before setup and agents; deprecated the previous plugin hook registration methods without changing their behavior ([#8081](https://github.com/getpaseo/paseo/pull/8081))
|
- Added `server.addHook` with `subject.operation.position` names and the awaited `worktree.create.after` hook before setup and agents; deprecated the previous plugin hook registration methods without changing their behavior ([#8081](https://github.com/getpaseo/paseo/pull/8081))
|
||||||
|
|
||||||
## 0.14.0-beta.2 - 2026-10-28
|
## 0.14.0 - 2026-10-29
|
||||||
|
|
||||||
Plugins can now add tools to individual agents and configure their MCP servers when sessions open.
|
Plugins can now add tools to individual agents and configure their MCP servers when sessions open.
|
||||||
|
|
||||||
@@ -43,7 +43,6 @@ Voice settings now let you configure speech providers and download local models
|
|||||||
- Reduced daemon stalls when opening and paging large Claude and OMP histories ([#7789](https://github.com/getpaseo/paseo/pull/7789), [#7940](https://github.com/getpaseo/paseo/pull/7940), [#7948](https://github.com/getpaseo/paseo/pull/7948), [#7958](https://github.com/getpaseo/paseo/pull/7958) by [@ragokan](https://github.com/ragokan))
|
- Reduced daemon stalls when opening and paging large Claude and OMP histories ([#7789](https://github.com/getpaseo/paseo/pull/7789), [#7940](https://github.com/getpaseo/paseo/pull/7940), [#7948](https://github.com/getpaseo/paseo/pull/7948), [#7958](https://github.com/getpaseo/paseo/pull/7958) by [@ragokan](https://github.com/ragokan))
|
||||||
- Reduced daemon stalls during plugin builds, image processing and config reloads ([#7967](https://github.com/getpaseo/paseo/pull/7967), [#7954](https://github.com/getpaseo/paseo/pull/7954), [#7959](https://github.com/getpaseo/paseo/pull/7959))
|
- Reduced daemon stalls during plugin builds, image processing and config reloads ([#7967](https://github.com/getpaseo/paseo/pull/7967), [#7954](https://github.com/getpaseo/paseo/pull/7954), [#7959](https://github.com/getpaseo/paseo/pull/7959))
|
||||||
- Reduced blocking filesystem work during workspace and project refreshes ([#7941](https://github.com/getpaseo/paseo/pull/7941), [#7943](https://github.com/getpaseo/paseo/pull/7943), [#7944](https://github.com/getpaseo/paseo/pull/7944))
|
- Reduced blocking filesystem work during workspace and project refreshes ([#7941](https://github.com/getpaseo/paseo/pull/7941), [#7943](https://github.com/getpaseo/paseo/pull/7943), [#7944](https://github.com/getpaseo/paseo/pull/7944))
|
||||||
- Added a workaround for Claude Code sessions consuming subscription limits faster in Paseo ([#7976](https://github.com/getpaseo/paseo/pull/7976) by [@david828019-oss](https://github.com/david828019-oss))
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@@ -72,7 +71,8 @@ Voice settings now let you configure speech providers and download local models
|
|||||||
|
|
||||||
- Voice mode now defaults to off; enable it in Voice settings
|
- Voice mode now defaults to off; enable it in Voice settings
|
||||||
- If environment variables select OpenAI speech, move keys and endpoints stored only in legacy `providers.openai` into Voice settings
|
- If environment variables select OpenAI speech, move keys and endpoints stored only in legacy `providers.openai` into Voice settings
|
||||||
- Before downgrading to 0.13.2, restore a backed-up `config.json` or convert speech credentials and idle-cleanup settings saved by the beta
|
- Before downgrading to 0.13.2, restore a backed-up `config.json` or convert speech credentials and idle-cleanup settings saved by 0.14.0
|
||||||
|
- Install the optional [Claude Code entrypoint plugin](https://paseo.sh/plugins/getpaseo/claude-code-entrypoint-env) to launch Claude sessions with `CLAUDE_CODE_ENTRYPOINT=Paseo`, a community workaround for subscription limits draining faster ([#7976](https://github.com/getpaseo/paseo/pull/7976), [#8164](https://github.com/getpaseo/paseo/pull/8164) by [@david828019-oss](https://github.com/david828019-oss))
|
||||||
|
|
||||||
## 0.13.2 - 2026-10-26
|
## 0.13.2 - 2026-10-26
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
尚未发布。以下变更已合入 `main`,但不包含在 0.14.0-beta.2 中;下一个版本的条目会包含它们。
|
尚未发布。以下变更已合入 `main`,但不包含在 0.14.0 中;下一个版本的条目会包含它们。
|
||||||
|
|
||||||
新增:
|
新增:
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
- 新增采用 `subject.operation.position` 命名的 `server.addHook` 和在 setup 及 Agent 之前等待的 `worktree.create.after` 钩子,弃用原有的插件钩子注册方法并保留原有行为 ([#8081](https://github.com/getpaseo/paseo/pull/8081))
|
- 新增采用 `subject.operation.position` 命名的 `server.addHook` 和在 setup 及 Agent 之前等待的 `worktree.create.after` 钩子,弃用原有的插件钩子注册方法并保留原有行为 ([#8081](https://github.com/getpaseo/paseo/pull/8081))
|
||||||
|
|
||||||
## 0.14.0-beta.2 - 2026-10-28
|
## 0.14.0 - 2026-10-29
|
||||||
|
|
||||||
插件现在可以为单个 Agent 添加工具,并在会话打开时配置其 MCP 服务器。
|
插件现在可以为单个 Agent 添加工具,并在会话打开时配置其 MCP 服务器。
|
||||||
|
|
||||||
@@ -43,7 +43,6 @@
|
|||||||
- 减少打开和分页加载大型 Claude 和 OMP 历史时的 daemon 卡顿 ([#7789](https://github.com/getpaseo/paseo/pull/7789), [#7940](https://github.com/getpaseo/paseo/pull/7940), [#7948](https://github.com/getpaseo/paseo/pull/7948), [#7958](https://github.com/getpaseo/paseo/pull/7958) by [@ragokan](https://github.com/ragokan))
|
- 减少打开和分页加载大型 Claude 和 OMP 历史时的 daemon 卡顿 ([#7789](https://github.com/getpaseo/paseo/pull/7789), [#7940](https://github.com/getpaseo/paseo/pull/7940), [#7948](https://github.com/getpaseo/paseo/pull/7948), [#7958](https://github.com/getpaseo/paseo/pull/7958) by [@ragokan](https://github.com/ragokan))
|
||||||
- 减少插件构建、图片处理和重新加载配置时的 daemon 卡顿 ([#7967](https://github.com/getpaseo/paseo/pull/7967), [#7954](https://github.com/getpaseo/paseo/pull/7954), [#7959](https://github.com/getpaseo/paseo/pull/7959))
|
- 减少插件构建、图片处理和重新加载配置时的 daemon 卡顿 ([#7967](https://github.com/getpaseo/paseo/pull/7967), [#7954](https://github.com/getpaseo/paseo/pull/7954), [#7959](https://github.com/getpaseo/paseo/pull/7959))
|
||||||
- 减少刷新工作区和项目时由文件系统操作造成的阻塞 ([#7941](https://github.com/getpaseo/paseo/pull/7941), [#7943](https://github.com/getpaseo/paseo/pull/7943), [#7944](https://github.com/getpaseo/paseo/pull/7944))
|
- 减少刷新工作区和项目时由文件系统操作造成的阻塞 ([#7941](https://github.com/getpaseo/paseo/pull/7941), [#7943](https://github.com/getpaseo/paseo/pull/7943), [#7944](https://github.com/getpaseo/paseo/pull/7944))
|
||||||
- 为 Claude Code 会话在 Paseo 中更快消耗订阅额度的问题添加了变通方案 ([#7976](https://github.com/getpaseo/paseo/pull/7976) by [@david828019-oss](https://github.com/david828019-oss))
|
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
@@ -72,7 +71,8 @@
|
|||||||
|
|
||||||
- 语音模式默认关闭,可在语音设置中启用
|
- 语音模式默认关闭,可在语音设置中启用
|
||||||
- 如果通过环境变量选择 OpenAI 语音,请将仅保存在旧版 `providers.openai` 中的密钥和服务地址移至语音设置
|
- 如果通过环境变量选择 OpenAI 语音,请将仅保存在旧版 `providers.openai` 中的密钥和服务地址移至语音设置
|
||||||
- 降级至 0.13.2 前,请恢复备份的 `config.json` 或转换 beta 保存的语音凭据和空闲清理设置
|
- 降级至 0.13.2 前,请恢复备份的 `config.json` 或转换 0.14.0 保存的语音凭据和空闲清理设置
|
||||||
|
- 安装可选的 [Claude Code entrypoint 插件](https://paseo.sh/plugins/getpaseo/claude-code-entrypoint-env),即可使用 `CLAUDE_CODE_ENTRYPOINT=Paseo` 启动 Claude 会话;这是针对订阅额度消耗过快问题的社区变通方案 ([#7976](https://github.com/getpaseo/paseo/pull/7976), [#8164](https://github.com/getpaseo/paseo/pull/8164) by [@david828019-oss](https://github.com/david828019-oss))
|
||||||
|
|
||||||
## 0.13.2 - 2026-10-26
|
## 0.13.2 - 2026-10-26
|
||||||
|
|
||||||
|
|||||||
@@ -117,10 +117,9 @@ See [docs/development.md](docs/development.md) for full setup, build sync requir
|
|||||||
|
|
||||||
## Release branches
|
## Release branches
|
||||||
|
|
||||||
When the user says "this goes to next", create or
|
Merge normal PRs into `main`, including while a beta soaks or a release is being finalized. Each
|
||||||
retarget the PR to `next` and preserve that destination through delivery. Follow
|
release builds in a short-lived release workspace from `origin/main` or a release tag; see
|
||||||
[release branch discipline](docs/release.md#release-branch-discipline) for creating
|
[release base and source](docs/release.md#release-base-and-source).
|
||||||
and updating `next`, integrating it after a release, and releasing a hotfix from a tag.
|
|
||||||
|
|
||||||
## Critical rules
|
## Critical rules
|
||||||
|
|
||||||
|
|||||||
+9
-7
@@ -200,13 +200,15 @@ its own.
|
|||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- **Released height.** Reanimated's web entering animation leaves an inline height snapshot on
|
- **Web enters without Reanimated.** For a custom keyframe, Reanimated's web path snapshots the
|
||||||
the surface. `AnchoredSurface` clears it, and a `revision` prop re-clears it when content
|
surface's rect when the animation ends and writes it back as inline top/left/width/height 750ms
|
||||||
identity changes — a pushed page taller than the one it replaced is clipped without that.
|
after the start. Whatever moved or resized the surface in between was undone: content that
|
||||||
- **Animate only once placed.** The same snapshot carries top/left, and Reanimated writes it back
|
grew once its data arrived left the menu clipped, or hanging over its anchor, until it moved
|
||||||
750ms after mount. `AnchoredSurface` remounts the surface when its position resolves so the
|
again. `AnchoredSurface` plays the same keyframes through the Web Animations API on web, which
|
||||||
entering animation never ends at the off-screen measuring position; on a slow machine that
|
leaves no inline style; native keeps the Reanimated `Keyframe`.
|
||||||
write moved the open menu back off-screen.
|
- **Animate only once placed.** `AnchoredSurface` remounts the surface when its position resolves,
|
||||||
|
so the entering animation runs at the final position rather than ending unseen at the
|
||||||
|
off-screen measuring position.
|
||||||
- **Sheets size to content.** `enableDynamicSizing`, not fixed snap points. A pushed page is
|
- **Sheets size to content.** `enableDynamicSizing`, not fixed snap points. A pushed page is
|
||||||
rarely the height of the page before it. A fill page is the one exception.
|
rarely the height of the page before it. A fill page is the one exception.
|
||||||
- **The sheet's content is teleported out of the menu's subtree**, so `MenuSheetSurface` rebuilds
|
- **The sheet's content is teleported out of the menu's subtree**, so `MenuSheetSurface` rebuilds
|
||||||
|
|||||||
+135
-73
@@ -6,10 +6,10 @@ All workspaces share one version and release together.
|
|||||||
|
|
||||||
A release has exactly two steps. The agent does the first, the user authorizes the second.
|
A release has exactly two steps. The agent does the first, the user authorizes the second.
|
||||||
|
|
||||||
**Preparation** (local, reversible — agent does this):
|
**Preparation** (reversible — agent does this):
|
||||||
|
|
||||||
- format, lint, typecheck all green
|
- format, lint, typecheck all green
|
||||||
- resolve the release source to one commit and confirm that commit's existing CI is green
|
- resolve the [release base](#release-base-and-source), get the [release inventory](#release-inventory) approved when one applies, and confirm CI for the resulting release source is green
|
||||||
- classify the diff from the previous stable to the release source as patch or minor, then show the
|
- classify the diff from the previous stable to the release source as patch or minor, then show the
|
||||||
target version and rationale to the user
|
target version and rationale to the user
|
||||||
- draft the changelog, show it to the user, wait for review
|
- draft the changelog, show it to the user, wait for review
|
||||||
@@ -17,9 +17,10 @@ A release has exactly two steps. The agent does the first, the user authorizes t
|
|||||||
|
|
||||||
**Go-ahead** (user says "go ahead"):
|
**Go-ahead** (user says "go ahead"):
|
||||||
|
|
||||||
- commit the approved release inputs locally
|
- commit the approved release inputs locally on the release branch
|
||||||
- run the release, which publishes npm and pushes the prepared branch and tag
|
- run the release, which publishes npm and pushes the release branch and tag
|
||||||
- create the release heartbeat immediately and babysit it to completion
|
- create the release heartbeat immediately and babysit it to completion
|
||||||
|
- [reconcile `main`](#reconcile-main) with what shipped
|
||||||
|
|
||||||
Rules that apply to both steps:
|
Rules that apply to both steps:
|
||||||
|
|
||||||
@@ -27,56 +28,128 @@ Rules that apply to both steps:
|
|||||||
- No code changes bundled into the changelog commit or the release commit. Code shims live in their own commit, reviewed on their own merits.
|
- No code changes bundled into the changelog commit or the release commit. Code shims live in their own commit, reviewed on their own merits.
|
||||||
- A sanity-check finding is information, not a directive. The agent surfaces it; the user decides.
|
- A sanity-check finding is information, not a directive. The agent surfaces it; the user decides.
|
||||||
- Invoking a release skill is intent to start the flow, not blanket authorization to publish.
|
- Invoking a release skill is intent to start the flow, not blanket authorization to publish.
|
||||||
|
- An approval the user already gave covers what it names, such as an explicit PR selection or authorization to release autonomously. Do not ask for it again.
|
||||||
- If the user asks for a release preview, show the prospective changelog/release contents and answer questions, but do not commit, tag, publish, or run release commands until they explicitly authorize the release.
|
- If the user asks for a release preview, show the prospective changelog/release contents and answer questions, but do not commit, tag, publish, or run release commands until they explicitly authorize the release.
|
||||||
|
|
||||||
## Release source and CI
|
## Main stays open
|
||||||
|
|
||||||
The default release source is `origin/main`. Fetch `origin`, then record the
|
Merge normal PRs into `main` at any time, including while a beta soaks or a release is being
|
||||||
resolved commit. The default release checkout is a clean local `main` whose
|
finalized. Each release builds in a short-lived release workspace, and its published tag records
|
||||||
`HEAD` equals `origin/main`.
|
the shipped source.
|
||||||
|
|
||||||
An explicit user instruction can select another ref, such as a hotfix commit or
|
## Choosing a path
|
||||||
tag. Resolve that ref once and apply every source, diff, and CI check to that
|
|
||||||
commit instead of `origin/main`.
|
|
||||||
|
|
||||||
Before making release-preparation commits, confirm the existing CI run for the
|
The user decides when the changes on `main` warrant a release and how much risk it takes:
|
||||||
resolved commit is green. Pending CI is watched to completion. Release
|
|
||||||
preparation then stays local through the changelog, any explicitly requested ACP
|
|
||||||
catalog update, lockfile preparation, and the version commit. After approval,
|
|
||||||
commit the prepared inputs locally and run the release command. Its branch and
|
|
||||||
tag push is the one remote release batch and starts CI for the complete release
|
|
||||||
commit.
|
|
||||||
|
|
||||||
## Release branch discipline
|
1. **Direct stable release** for a low-risk batch of polish and bug fixes. Stable desktop updates
|
||||||
|
already ramp over 36 hours (see [Staged rollout](#staged-rollout-stable-channel)), and store
|
||||||
|
review often takes about a day, with no guaranteed timing.
|
||||||
|
2. **Beta flow**, the default for changes that need testing. Release candidates on the `beta`
|
||||||
|
channel. Each beta folds into the series' single changelog entry, publishes npm only on the
|
||||||
|
explicit `beta` dist-tag, and stays behind the Stable/Beta switch on `/download`.
|
||||||
|
|
||||||
While you finalize a release on `main`, use a temporary `next` branch for work intended for the following
|
A beta follows these defaults, which the user can explicitly override:
|
||||||
release. This applies to both beta and stable releases.
|
|
||||||
|
|
||||||
- Create each new `next` from freshly fetched `origin/main`. Reuse it while active.
|
- Soak a beta at least 24 hours before promotion. Soak longer for risky or breaking changes, or
|
||||||
- "This goes to next" means create the PR against `next` or retarget an existing
|
while feedback is open. Changes added in a later beta get their own soak; earlier soak time does
|
||||||
PR, and keep that destination through delivery.
|
not count for them.
|
||||||
- Keep `next` current by merging `origin/main` into it as release fixes land.
|
- A follow-up beta carries regressions, fixes, and polish for what that beta delivers. Unrelated
|
||||||
Avoid rebasing this shared branch because agents and open PRs depend on its history.
|
work on `main` and pre-existing bugs wait for a later release unless the user explicitly selects
|
||||||
- After the release ships, bring `next` up to date and open a `next` → `main` PR.
|
them.
|
||||||
Pass CI and merge without squashing away the individual PR commits needed for
|
- Promotion ships the tested beta's product content unchanged and does not pull from `main`.
|
||||||
the changelog. Retarget remaining PRs based on `next` to `main` and delete the integrated
|
Adding a product change needs the user's explicit selection and a new beta, which soaks again.
|
||||||
`next`. Create it fresh when needed again.
|
- During a soak, ship urgent stable regressions and compatibility or provider model updates as a
|
||||||
|
[stable hotfix](#release-base-and-source) from the latest stable tag. The beta keeps its own
|
||||||
|
pace.
|
||||||
|
|
||||||
**Setup still needed:** PR checks run for every base branch, but CI, Docker, and Nix
|
npm dist-tags are independent pointers:
|
||||||
push runs target only `main`, and GitHub permits only squash merges. Enable
|
|
||||||
required-check protection for `next`, CI on its pushes, and merge commits for the
|
|
||||||
integration PR. Handle PR
|
|
||||||
base changes (`edited` events) so retargeting runs checks against the new base;
|
|
||||||
GitHub's default PR events do not cover this. Deployment triggers stay unchanged.
|
|
||||||
|
|
||||||
### Hotfix from a release tag
|
- A beta release publishes with `--tag beta` and moves only `beta`; `latest` remains on the
|
||||||
|
newest stable.
|
||||||
|
- A stable release publishes to `latest`. Move `beta` to it as well when the stable version is
|
||||||
|
newer than the version `beta` points to, which covers promotions and direct stable releases, so
|
||||||
|
`@getpaseo/cli@beta` users never sit behind stable. A stable hotfix on an older line leaves
|
||||||
|
`beta` on the active newer beta.
|
||||||
|
|
||||||
If `main` contains changes you do not want to release, branch from the affected
|
## Release base and source
|
||||||
release tag and cherry-pick only the required fixes. Run CI on that branch, then
|
|
||||||
use the normal release flow with it as the explicit source, choosing a new patch
|
The release base is the tag or commit a release workspace starts from. The release source is the
|
||||||
or beta version. Ensure the fixes and changelog also reach `main` and any active
|
base plus any approved cherry-picks, and every source, diff, and CI check uses it.
|
||||||
`next`, preserving newer development and version changes there. This is a
|
|
||||||
short-lived hotfix branch, not another maintained release track.
|
| Release | Base | Changes on top |
|
||||||
|
| -------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------- |
|
||||||
|
| Starting release: first beta of a line, or direct stable | `origin/main`, captured once | None |
|
||||||
|
| Follow-up beta | The selected published beta tag, normally the series' latest | Approved commits from `main` |
|
||||||
|
| Promotion | The beta tag being promoted | None by default |
|
||||||
|
| Stable hotfix | The latest appropriate stable tag | Approved commits from `main` |
|
||||||
|
|
||||||
|
An explicit user instruction can select another base.
|
||||||
|
|
||||||
|
1. Fetch `origin` with tags and resolve the base to one commit.
|
||||||
|
2. Create the release workspace through Paseo MCP on a new branch from the base, named
|
||||||
|
`release/vX.Y.Z` or `release/vX.Y.Z-beta.N` for the version being cut. Run the release from
|
||||||
|
this workspace: `release:push` pushes `HEAD` to the current branch.
|
||||||
|
3. Cherry-pick the approved commits in their `main` order with `git cherry-pick -x`, so each
|
||||||
|
commit records the `main` commit it came from. Include the prerequisites the inventory listed.
|
||||||
|
Resolve conflicts within the approved changes and report how. A resolution that needs new
|
||||||
|
behavior is a scope addition: surface it for selection.
|
||||||
|
4. Confirm CI for the release source. When the source is a commit whose CI already passed (the
|
||||||
|
captured `origin/main`, or the beta tag being promoted), use that run and watch pending CI to
|
||||||
|
completion. Otherwise push the release branch and run `gh workflow run ci.yml --ref <branch>`,
|
||||||
|
because CI push runs target only `main`.
|
||||||
|
5. Keep the changelog, any explicitly requested ACP catalog update, lockfile preparation, and the
|
||||||
|
version commit local until the go-ahead. The release command pushes the release branch and tag
|
||||||
|
as one batch, and the tag push starts the release workflows. Then run
|
||||||
|
`gh workflow run ci.yml --ref <branch>` so CI covers the complete release commit.
|
||||||
|
6. [Reconcile `main`](#reconcile-main).
|
||||||
|
|
||||||
|
A stable hotfix can take the version a patch beta line would promote to, such as `0.13.3`
|
||||||
|
shipping while `0.13.3-beta.2` soaks. Never republish or reuse a shipped version. Continue the line
|
||||||
|
as the next unused patch beta, `0.13.4-beta.1`, from the tested beta tag. No `version:all:*` mode
|
||||||
|
computes that from a prerelease, so follow the [manual steps](#manual-step-by-step) with
|
||||||
|
`npm version 0.13.4-beta.1 --include-workspace-root --message "chore(release): cut %s"` and
|
||||||
|
`npm run release:publish:beta`. Record the tested beta tag as its source so its soak still counts
|
||||||
|
for the unchanged content.
|
||||||
|
|
||||||
|
## Release inventory
|
||||||
|
|
||||||
|
When the user asks for a follow-up beta or a promotion, list what `main` has that the base does
|
||||||
|
not, then get the batch approved. There is no release label or queue.
|
||||||
|
|
||||||
|
1. Find the commits on `origin/main` whose patches are not in the base tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log --cherry-pick --right-only --no-merges --format='%h %s' <base-tag>...origin/main
|
||||||
|
```
|
||||||
|
|
||||||
|
Patch equivalence recognizes commits already cherry-picked into the tag, so trust the tag
|
||||||
|
content over PR state or branch names.
|
||||||
|
|
||||||
|
2. Present a short include/defer list. Link each PR and give one plain reason. List separately the
|
||||||
|
prerequisites an included PR depends on and anything that adds scope beyond what the beta
|
||||||
|
delivers. For each fix that shipped in a stable hotfix after the beta's base, confirm the beta
|
||||||
|
preserves its behavior, directly or through a superseding change. List the missing ones for
|
||||||
|
explicit selection; adding one to a promotion needs a new beta.
|
||||||
|
3. Wait for the user to approve the batch, unless an explicit selection or autonomous
|
||||||
|
authorization they already gave covers it.
|
||||||
|
|
||||||
|
For a promotion, every item defaults to defer.
|
||||||
|
|
||||||
|
## Reconcile main
|
||||||
|
|
||||||
|
After the release push, open a PR into `main` that brings in what shipped and keeps newer
|
||||||
|
development on `main`:
|
||||||
|
|
||||||
|
- The shipped `CHANGELOG.md` and `CHANGELOG.zh-CN.md` entries, exactly as tagged. A beta or
|
||||||
|
promotion entry replaces the series entry on `main`. A stable hotfix on an older line goes
|
||||||
|
below any newer entries.
|
||||||
|
- The shipped version in every `package.json` and `package-lock.json`, unless `main` already
|
||||||
|
carries a newer version, plus the F-Droid changelog files a stable release generates.
|
||||||
|
- No claims about unshipped work: a shipped entry describes only the tagged content, and notes
|
||||||
|
`main` holds for changes that have not shipped stay out of it.
|
||||||
|
|
||||||
|
Delete the release branch once the PR merges. The tag is the record of shipped source; Paseo
|
||||||
|
maintains no release track besides `main`.
|
||||||
|
|
||||||
## ACP catalog updates
|
## ACP catalog updates
|
||||||
|
|
||||||
@@ -91,21 +164,6 @@ ACP catalog work enters a release through an explicit user request:
|
|||||||
The release authorization covers the requested ACP commit. It ships in the same
|
The release authorization covers the requested ACP commit. It ships in the same
|
||||||
release push as the changelog and version commit.
|
release push as the changelog and version commit.
|
||||||
|
|
||||||
## Two paths
|
|
||||||
|
|
||||||
There are two supported release paths:
|
|
||||||
|
|
||||||
1. **Direct stable release**: you are ready to ship the resolved release source to everyone immediately (default `origin/main`).
|
|
||||||
2. **Beta flow**: release candidates on the `beta` channel. Each beta folds into the series' single changelog entry, publishes npm only on the explicit `beta` dist-tag, and stays behind the Stable/Beta switch on `/download`.
|
|
||||||
|
|
||||||
Paseo has one linear release track even though npm dist-tags are independent
|
|
||||||
pointers. The npm invariant is:
|
|
||||||
|
|
||||||
- A beta release moves only `beta`; `latest` remains on the newest stable.
|
|
||||||
- A stable release moves both `latest` and `beta` to that stable version. This
|
|
||||||
keeps users who install `@getpaseo/cli@beta` on the newest Paseo release after
|
|
||||||
a beta is promoted or superseded by a direct stable release.
|
|
||||||
|
|
||||||
## Release version decision
|
## Release version decision
|
||||||
|
|
||||||
Every fresh release starts by classifying the full diff from the previous
|
Every fresh release starts by classifying the full diff from the previous
|
||||||
@@ -133,7 +191,7 @@ as described in **Fixing a failed release build**.
|
|||||||
|
|
||||||
Before running any stable release command:
|
Before running any stable release command:
|
||||||
|
|
||||||
- Make sure the resolved release source passed CI, the approved release inputs are committed locally on the intended branch, and the working tree is clean.
|
- Make sure the release source passed CI, the approved release inputs are committed locally on the release branch, and the working tree is clean.
|
||||||
- **Run `npm run format`, `npm run lint`, and `npm run typecheck` and commit any resulting changes BEFORE you start any `release:*` command.** `release:check` runs `npm install --workspaces --include-workspace-root` as part of `release:prepare`, which can mutate `package-lock.json` (e.g. churning `"dev": true` markers on optional deps). The next step, `version:all:*`, runs `npm version` which aborts when the working tree is dirty. If this happens mid-flight you have to commit the lockfile churn before retrying — and the pre-commit format hook will reject a lockfile-only commit because oxfmt internally skips `package-lock.json` while lefthook's glob still matches it. Avoid the whole mess by running format/lint/typecheck first, then `release:prepare` once on its own to absorb any lockfile churn into a normal commit, then start the release.
|
- **Run `npm run format`, `npm run lint`, and `npm run typecheck` and commit any resulting changes BEFORE you start any `release:*` command.** `release:check` runs `npm install --workspaces --include-workspace-root` as part of `release:prepare`, which can mutate `package-lock.json` (e.g. churning `"dev": true` markers on optional deps). The next step, `version:all:*`, runs `npm version` which aborts when the working tree is dirty. If this happens mid-flight you have to commit the lockfile churn before retrying — and the pre-commit format hook will reject a lockfile-only commit because oxfmt internally skips `package-lock.json` while lefthook's glob still matches it. Avoid the whole mess by running format/lint/typecheck first, then `release:prepare` once on its own to absorb any lockfile churn into a normal commit, then start the release.
|
||||||
- Do not use a release command as a substitute for checking whether the current commit is actually ready.
|
- Do not use a release command as a substitute for checking whether the current commit is actually ready.
|
||||||
|
|
||||||
@@ -145,9 +203,9 @@ npm run release:minor
|
|||||||
|
|
||||||
This bumps the version across all workspaces, runs checks, publishes to npm, and pushes the branch + tag. The tag push triggers `Desktop Release`, `Android APK Release`, `Docker`, and `Release Notes Sync` on GitHub Actions. The workflows create the GitHub Release as a draft while builds and release-note sync run. EAS picks up the same tag via the EAS GitHub app and starts the iOS + Android store builds in parallel (see "Mobile builds (EAS)" below) — there is no mobile-release workflow under `.github/workflows`.
|
This bumps the version across all workspaces, runs checks, publishes to npm, and pushes the branch + tag. The tag push triggers `Desktop Release`, `Android APK Release`, `Docker`, and `Release Notes Sync` on GitHub Actions. The workflows create the GitHub Release as a draft while builds and release-note sync run. EAS picks up the same tag via the EAS GitHub app and starts the iOS + Android store builds in parallel (see "Mobile builds (EAS)" below) — there is no mobile-release workflow under `.github/workflows`.
|
||||||
|
|
||||||
After the stable release succeeds, move npm's `beta` pointer to the new stable
|
After the stable release succeeds, check `npm view @getpaseo/cli dist-tags`. When the new stable
|
||||||
version for every published package. This changes dist-tags only; do not
|
is newer than `beta`, move `beta` to it for every published package. This changes dist-tags only;
|
||||||
republish the packages:
|
do not republish the packages:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PASEO_VERSION=$(node -p "require('./package.json').version")
|
PASEO_VERSION=$(node -p "require('./package.json').version")
|
||||||
@@ -156,8 +214,8 @@ for package in highlight relay protocol client plugin server cli; do
|
|||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify both npm tags now resolve to `PASEO_VERSION` before considering the
|
Verify `latest` resolves to `PASEO_VERSION`, and `beta` resolves to `PASEO_VERSION` or the
|
||||||
stable release complete.
|
active newer beta, before considering the stable release complete.
|
||||||
|
|
||||||
The Docker workflow builds images from the checked-out source tree on pull requests and on `main` as non-publishing checks. Stable `vX.Y.Z` tag pushes publish `ghcr.io/getpaseo/paseo:X.Y.Z` and `ghcr.io/getpaseo/paseo:latest`; beta `vX.Y.Z-beta.N` tag pushes publish only `ghcr.io/getpaseo/paseo:X.Y.Z-beta.N` and never move `latest`.
|
The Docker workflow builds images from the checked-out source tree on pull requests and on `main` as non-publishing checks. Stable `vX.Y.Z` tag pushes publish `ghcr.io/getpaseo/paseo:X.Y.Z` and `ghcr.io/getpaseo/paseo:latest`; beta `vX.Y.Z-beta.N` tag pushes publish only `ghcr.io/getpaseo/paseo:X.Y.Z-beta.N` and never move `latest`.
|
||||||
|
|
||||||
@@ -175,7 +233,7 @@ npm run version:all:patch
|
|||||||
npm run version:all:minor
|
npm run version:all:minor
|
||||||
npm run release:publish # Publish to npm
|
npm run release:publish # Publish to npm
|
||||||
npm run release:push # Push HEAD + tag (triggers CI workflows)
|
npm run release:push # Push HEAD + tag (triggers CI workflows)
|
||||||
# Then move npm's beta dist-tag to this stable version using the command above.
|
# Then move npm's beta dist-tag to this stable version when it is newer than beta (see above).
|
||||||
```
|
```
|
||||||
|
|
||||||
## Beta flow
|
## Beta flow
|
||||||
@@ -322,7 +380,8 @@ There is no mobile-release workflow under `.github/workflows`. The EAS GitHub ap
|
|||||||
Each stable release publishes the screenshots committed under `assets/store-screenshots/output/` (see [its README](../assets/store-screenshots/README.md) for how they are made). To change the store screenshots, re-render and commit them before you tag.
|
Each stable release publishes the screenshots committed under `assets/store-screenshots/output/` (see [its README](../assets/store-screenshots/README.md) for how they are made). To change the store screenshots, re-render and commit them before you tag.
|
||||||
|
|
||||||
- **App Store:** `submit_review` uploads `output/apple/` and `output/apple-ipad/` to the version it submits, replacing that version's `en-US` screenshots. Apple ties screenshots to an app version, so a version that is already live keeps the ones it shipped with. `Resubmit iOS for App Store review` uploads them again.
|
- **App Store:** `submit_review` uploads `output/apple/` and `output/apple-ipad/` to the version it submits, replacing that version's `en-US` screenshots. Apple ties screenshots to an app version, so a version that is already live keeps the ones it shipped with. `Resubmit iOS for App Store review` uploads them again.
|
||||||
- **Google Play:** `upload_android_screenshots` runs `fastlane android upload_screenshots` after `submit_android`. It replaces only the phone and 10-inch tablet screenshots, never the listing text or changelogs, and skips images whose checksum already matches.
|
- When `submit_review` fails with `appStoreVersions … is not in valid state`, the reason is in the API error's `meta.associatedErrors`, which fastlane does not print. Read it with a `POST /v1/reviewSubmissionItems` against the version. A required age-rating question that Apple added, such as `socialMedia`, fails this way; answer it under App Information > Age Rating in App Store Connect, then resubmit.
|
||||||
|
- **Google Play:** `upload_android_screenshots` runs `fastlane android upload_screenshots` after `submit_android`. It replaces only the `en-US` phone and 10-inch tablet screenshots, never the listing text or changelogs, and skips images whose checksum already matches. The lane stages only `en-US`: supply uploads every locale folder in one edit, and Play rejects F-Droid locales such as `ja`, which fails the whole edit. `Upload Google Play screenshots` reruns the step for the current release without a rebuild.
|
||||||
- **F-Droid:** reads the same files as Google Play. `fastlane/metadata/android/en-US/images/phoneScreenshots/` and `tenInchScreenshots/` hold symlinks to the Google renders. `render.mjs` rewrites them on every render, so added or renamed panels reach Google Play and F-Droid as well.
|
- **F-Droid:** reads the same files as Google Play. `fastlane/metadata/android/en-US/images/phoneScreenshots/` and `tenInchScreenshots/` hold symlinks to the Google renders. `render.mjs` rewrites them on every render, so added or renamed panels reach Google Play and F-Droid as well.
|
||||||
|
|
||||||
Fastlane cannot use the Play key Expo stores for `eas submit`. The Play lane reads its own copy of the same service-account key from the `GOOGLE_PLAY_JSON_KEY` file secret in the EAS `production` environment, and that service account needs the Play Console permission **Manage store presence**.
|
Fastlane cannot use the Play key Expo stores for `eas submit`. The Play lane reads its own copy of the same service-account key from the `GOOGLE_PLAY_JSON_KEY` file secret in the EAS `production` environment, and that service account needs the Play Console permission **Manage store presence**.
|
||||||
@@ -488,7 +547,7 @@ git tag -f android-v0.1.28 HEAD && git push origin android-v0.1.28 --force
|
|||||||
git tag -f v0.1.29-beta.2 HEAD && git push origin v0.1.29-beta.2 --force
|
git tag -f v0.1.29-beta.2 HEAD && git push origin v0.1.29-beta.2 --force
|
||||||
```
|
```
|
||||||
|
|
||||||
This ensures the checkout ref matches the actual code on `main` with the fix included.
|
This ensures the checkout ref matches the code you want built, with the fix included.
|
||||||
|
|
||||||
- `vX.Y.Z` or `vX.Y.Z-beta.N` rebuilds the full tagged release
|
- `vX.Y.Z` or `vX.Y.Z-beta.N` rebuilds the full tagged release
|
||||||
- `desktop-vX.Y.Z` rebuilds desktop for all desktop platforms only
|
- `desktop-vX.Y.Z` rebuilds desktop for all desktop platforms only
|
||||||
@@ -631,7 +690,7 @@ Rules:
|
|||||||
|
|
||||||
This returns every distinct GitHub login that authored or co-authored a commit in the PR. Use those logins for attribution. Fall back to `gh pr view N --json author` only if the commits command returns nothing (which should not happen for merged PRs).
|
This returns every distinct GitHub login that authored or co-authored a commit in the PR. Use those logins for attribution. Fall back to `gh pr view N --json author` only if the commits command returns nothing (which should not happen for merged PRs).
|
||||||
|
|
||||||
When listing PR numbers, `git log --format='%H %s' v<previous>..<release-source-sha> | grep -E '\(#[0-9]+\)$'` pulls the PR number out of squash commit subjects.
|
When listing PR numbers, `git log --cherry-pick --right-only --no-merges --format='%H %s' v<previous>...<release-source-sha> | grep -E '\(#[0-9]+\)$'` pulls the PR number out of squash commit subjects and skips commits the previous stable already shipped as cherry-picks.
|
||||||
|
|
||||||
## Changelog ordering
|
## Changelog ordering
|
||||||
|
|
||||||
@@ -663,11 +722,11 @@ Every entry covers `previous stable release → release source`: first beta, lat
|
|||||||
|
|
||||||
### Beta release
|
### Beta release
|
||||||
|
|
||||||
- [ ] The resolved release source is the intended commit (default `origin/main`) and its existing CI is green
|
- [ ] The release base and changes match the [release base table](#release-base-and-source) and any approved [inventory](#release-inventory), and CI for the release source is green
|
||||||
- [ ] Every PR in the release range has been opened, and its full description and every linked issue have been read before drafting the changelog
|
- [ ] Every PR in the release range has been opened, and its full description and every linked issue have been read before drafting the changelog
|
||||||
- [ ] Fold this beta into the series' single `CHANGELOG.md` entry (heading `## X.Y.Z-beta.N - YYYY-MM-DD`, previous stable → release source), review it against the changelog policy, get approval, and commit it with its [`CHANGELOG.zh-CN.md` translation](#changelog-translations) before cutting the release
|
- [ ] Fold this beta into the series' single `CHANGELOG.md` entry (heading `## X.Y.Z-beta.N - YYYY-MM-DD`, previous stable → release source), review it against the changelog policy, get approval, and commit it with its [`CHANGELOG.zh-CN.md` translation](#changelog-translations) before cutting the release
|
||||||
- [ ] The diff from the previous stable to the resolved release source is classified as patch or minor, with the target version and rationale approved
|
- [ ] The diff from the previous stable to the resolved release source is classified as patch or minor, with the target version and rationale approved
|
||||||
- [ ] Release preparation stayed local until the approved release command pushed the complete branch and tag
|
- [ ] The changelog, ACP, lockfile, and version commits stayed local until the approved release command pushed the release branch and tag
|
||||||
- [ ] `npm run release:beta:patch`, `npm run release:beta:minor`, or `npm run release:beta:next` completes successfully
|
- [ ] `npm run release:beta:patch`, `npm run release:beta:minor`, or `npm run release:beta:next` completes successfully
|
||||||
- [ ] Every GitHub Actions run for the complete release commit and tag is green
|
- [ ] Every GitHub Actions run for the complete release commit and tag is green
|
||||||
- [ ] npm shows the version under the `beta` dist-tag, not `latest`
|
- [ ] npm shows the version under the `beta` dist-tag, not `latest`
|
||||||
@@ -678,22 +737,24 @@ Every entry covers `previous stable release → release source`: first beta, lat
|
|||||||
- [ ] GitHub `Docker` workflow is green and the versioned beta image is published without moving `latest`
|
- [ ] GitHub `Docker` workflow is green and the versioned beta image is published without moving `latest`
|
||||||
- [ ] GitHub `Release Notes Sync` mirrored the beta entry into the prerelease body
|
- [ ] GitHub `Release Notes Sync` mirrored the beta entry into the prerelease body
|
||||||
- [ ] EAS `Release iOS Beta` completed its build, TestFlight distribution, external beta group, and Beta App Review path
|
- [ ] EAS `Release iOS Beta` completed its build, TestFlight distribution, external beta group, and Beta App Review path
|
||||||
|
- [ ] The [reconcile PR](#reconcile-main) brought the shipped changelog entries and version into `main`
|
||||||
- [ ] The release heartbeat was created after the tag push and deleted only after every item above passed
|
- [ ] The release heartbeat was created after the tag push and deleted only after every item above passed
|
||||||
|
|
||||||
### Stable release (or promotion)
|
### Stable release (or promotion)
|
||||||
|
|
||||||
- [ ] Run the pre-release sanity check (see above) and address any findings
|
- [ ] Run the pre-release sanity check (see above) and address any findings
|
||||||
- [ ] The diff from the previous stable to the resolved release source is classified as patch or minor, with the target version and rationale approved
|
- [ ] The diff from the previous stable to the resolved release source is classified as patch or minor, with the target version and rationale approved
|
||||||
- [ ] The resolved release source is the intended commit (default `origin/main`) and its existing CI is green
|
- [ ] The release base and changes match the [release base table](#release-base-and-source) and any approved [inventory](#release-inventory), and CI for the release source is green
|
||||||
|
- [ ] A promotion ships the tested beta's product content after its soak, unless the user explicitly overrode either default
|
||||||
- [ ] Every PR in the release range has been opened, and its full description and every linked issue have been read before drafting the changelog
|
- [ ] Every PR in the release range has been opened, and its full description and every linked issue have been read before drafting the changelog
|
||||||
- [ ] Ensure the approved release inputs are committed locally and the git worktree is clean before running any release command
|
- [ ] Ensure the approved release inputs are committed locally and the git worktree is clean before running any release command
|
||||||
- [ ] Ensure local `npm run typecheck` passes on that exact commit before running any release command
|
- [ ] Ensure local `npm run typecheck` passes on that exact commit before running any release command
|
||||||
- [ ] Update `CHANGELOG.md` with user-facing release notes (features, fixes — not refactors). Promotion renames the series' `## X.Y.Z-beta.N` entry to `## X.Y.Z - YYYY-MM-DD` and re-checks it covers the full release. Commit the matching [`CHANGELOG.zh-CN.md` entry](#changelog-translations) with it
|
- [ ] Update `CHANGELOG.md` with user-facing release notes (features, fixes — not refactors). Promotion renames the series' `## X.Y.Z-beta.N` entry to `## X.Y.Z - YYYY-MM-DD` and re-checks it covers the full release. Commit the matching [`CHANGELOG.zh-CN.md` entry](#changelog-translations) with it
|
||||||
- [ ] Verify the changelog heading follows strict `## X.Y.Z - YYYY-MM-DD` format
|
- [ ] Verify the changelog heading follows strict `## X.Y.Z - YYYY-MM-DD` format
|
||||||
- [ ] Release preparation stayed local until the approved release command pushed the complete branch and tag
|
- [ ] The changelog, ACP, lockfile, and version commits stayed local until the approved release command pushed the release branch and tag
|
||||||
- [ ] `npm run release:patch`, `npm run release:minor`, or `npm run release:promote` completes successfully
|
- [ ] `npm run release:patch`, `npm run release:minor`, or `npm run release:promote` completes successfully
|
||||||
- [ ] Every GitHub Actions run for the complete release commit and tag is green
|
- [ ] Every GitHub Actions run for the complete release commit and tag is green
|
||||||
- [ ] Move npm's `beta` dist-tag to the new stable version for every published package and verify both `latest` and `beta` resolve to it
|
- [ ] `latest` resolves to the new stable, and `beta` resolves to it too unless an active newer beta holds `beta`
|
||||||
- [ ] The GitHub Release was published only after the three stable manifests were uploaded, and it has the changelog body and every expected macOS, Linux, Windows, and Android APK asset
|
- [ ] The GitHub Release was published only after the three stable manifests were uploaded, and it has the changelog body and every expected macOS, Linux, Windows, and Android APK asset
|
||||||
- [ ] GitHub `Desktop Release` workflow for the `v*` tag is green
|
- [ ] GitHub `Desktop Release` workflow for the `v*` tag is green
|
||||||
- [ ] The GitHub Release contains `latest-mac.yml`, `latest-linux.yml`, and `latest.yml`
|
- [ ] The GitHub Release contains `latest-mac.yml`, `latest-linux.yml`, and `latest.yml`
|
||||||
@@ -707,4 +768,5 @@ Every entry covers `previous stable release → release source`: first beta, lat
|
|||||||
- [ ] EAS iOS `submit_ios_for_review` succeeds, putting the build into App Store review
|
- [ ] EAS iOS `submit_ios_for_review` succeeds, putting the build into App Store review
|
||||||
- [ ] EAS Android `build_android` completes for the same tag
|
- [ ] EAS Android `build_android` completes for the same tag
|
||||||
- [ ] EAS Android `submit_android` succeeds, putting the build on its Play Store track
|
- [ ] EAS Android `submit_android` succeeds, putting the build on its Play Store track
|
||||||
|
- [ ] The [reconcile PR](#reconcile-main) brought the shipped changelog entries and version into `main`
|
||||||
- [ ] The release heartbeat was created after the tag push and deleted only after every item above passed
|
- [ ] The release heartbeat was created after the tag push and deleted only after every item above passed
|
||||||
|
|||||||
@@ -358,6 +358,14 @@ export to `src`, but browser and Electron Metro resolve its `browser` export to
|
|||||||
`lib/module`; CommonJS consumers use `lib/commonjs`. A source-only patch leaves
|
`lib/module`; CommonJS consumers use `lib/commonjs`. A source-only patch leaves
|
||||||
Electron running the old code.
|
Electron running the old code.
|
||||||
|
|
||||||
|
The adaptive-theme patch keeps `colorScheme` reads off the media-query lists
|
||||||
|
owned by Unistyles' change listeners. Chromium defers their events while a window
|
||||||
|
is hidden; reading `matches` on those same lists can consume the pending change.
|
||||||
|
CSS variables then follow the OS theme while `withUnistyles` props retain the old
|
||||||
|
colors. Keep this patch until upstream reads the scheme through separate lists.
|
||||||
|
The desktop browser E2E checks both transition directions in an isolated Electron
|
||||||
|
process, because Playwright's focus emulation prevents real hidden-page behavior.
|
||||||
|
|
||||||
Register every dependency patch in `scripts/postinstall-patches.mjs`. A file in
|
Register every dependency patch in `scripts/postinstall-patches.mjs`. A file in
|
||||||
`patches/` is inert unless that script knows which installed package activates
|
`patches/` is inert unless that script knows which installed package activates
|
||||||
it.
|
it.
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Plugins can now add tools to individual agents and configure their MCP servers when sessions open.
|
||||||
|
|
||||||
|
Completed conversation turns can be collapsed into summaries and expanded to read the full exchange.
|
||||||
|
|
||||||
|
Voice settings now let you configure speech providers and download local models with one tap, including SenseVoice and Whisper for Chinese dictation. Voice mode is now opt-in.
|
||||||
|
|
||||||
|
Added
|
||||||
|
- Let plugins give tools to specific agents
|
||||||
|
|
||||||
|
Full notes: https://paseo.sh/changelog
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Plugins can now add tools to individual agents and configure their MCP servers when sessions open.
|
||||||
|
|
||||||
|
Completed conversation turns can be collapsed into summaries and expanded to read the full exchange.
|
||||||
|
|
||||||
|
Voice settings now let you configure speech providers and download local models with one tap, including SenseVoice and Whisper for Chinese dictation. Voice mode is now opt-in.
|
||||||
|
|
||||||
|
Added
|
||||||
|
- Let plugins give tools to specific agents
|
||||||
|
|
||||||
|
Full notes: https://paseo.sh/changelog
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Plugins can now add tools to individual agents and configure their MCP servers when sessions open.
|
||||||
|
|
||||||
|
Completed conversation turns can be collapsed into summaries and expanded to read the full exchange.
|
||||||
|
|
||||||
|
Voice settings now let you configure speech providers and download local models with one tap, including SenseVoice and Whisper for Chinese dictation. Voice mode is now opt-in.
|
||||||
|
|
||||||
|
Added
|
||||||
|
- Let plugins give tools to specific agents
|
||||||
|
|
||||||
|
Full notes: https://paseo.sh/changelog
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Plugins can now add tools to individual agents and configure their MCP servers when sessions open.
|
||||||
|
|
||||||
|
Completed conversation turns can be collapsed into summaries and expanded to read the full exchange.
|
||||||
|
|
||||||
|
Voice settings now let you configure speech providers and download local models with one tap, including SenseVoice and Whisper for Chinese dictation. Voice mode is now opt-in.
|
||||||
|
|
||||||
|
Added
|
||||||
|
- Let plugins give tools to specific agents
|
||||||
|
|
||||||
|
Full notes: https://paseo.sh/changelog
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
sha256-AmMp/4ugSUKXTL6H0oJaGJMztVGiYCPXeWteQaaaguw=
|
sha256-6C4Rjh0mRVrC9akBVGK0NZaCaLJhwKObT7HrgPmcvyw=
|
||||||
|
|||||||
Generated
+28
-28
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "paseo",
|
"name": "paseo",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "paseo",
|
"name": "paseo",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
@@ -37590,7 +37590,7 @@
|
|||||||
},
|
},
|
||||||
"packages/app": {
|
"packages/app": {
|
||||||
"name": "@getpaseo/app",
|
"name": "@getpaseo/app",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/commands": "6.10.4",
|
"@codemirror/commands": "6.10.4",
|
||||||
"@codemirror/language": "6.12.4",
|
"@codemirror/language": "6.12.4",
|
||||||
@@ -39793,12 +39793,12 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@getpaseo/cli",
|
"name": "@getpaseo/cli",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^1.0.0",
|
"@clack/prompts": "^1.0.0",
|
||||||
"@getpaseo/client": "0.14.0-beta.2",
|
"@getpaseo/client": "0.14.0",
|
||||||
"@getpaseo/protocol": "0.14.0-beta.2",
|
"@getpaseo/protocol": "0.14.0",
|
||||||
"@getpaseo/server": "0.14.0-beta.2",
|
"@getpaseo/server": "0.14.0",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"commander": "^12.0.0",
|
"commander": "^12.0.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
@@ -40045,10 +40045,10 @@
|
|||||||
},
|
},
|
||||||
"packages/client": {
|
"packages/client": {
|
||||||
"name": "@getpaseo/client",
|
"name": "@getpaseo/client",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getpaseo/protocol": "0.14.0-beta.2",
|
"@getpaseo/protocol": "0.14.0",
|
||||||
"@getpaseo/relay": "0.14.0-beta.2",
|
"@getpaseo/relay": "0.14.0",
|
||||||
"ws": "^8.20.0",
|
"ws": "^8.20.0",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
@@ -40060,7 +40060,7 @@
|
|||||||
},
|
},
|
||||||
"packages/desktop": {
|
"packages/desktop": {
|
||||||
"name": "@getpaseo/desktop",
|
"name": "@getpaseo/desktop",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getpaseo/cli": "*",
|
"@getpaseo/cli": "*",
|
||||||
@@ -40304,7 +40304,7 @@
|
|||||||
},
|
},
|
||||||
"packages/expo-two-way-audio": {
|
"packages/expo-two-way-audio": {
|
||||||
"name": "@getpaseo/expo-two-way-audio",
|
"name": "@getpaseo/expo-two-way-audio",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
@@ -40350,7 +40350,7 @@
|
|||||||
},
|
},
|
||||||
"packages/highlight": {
|
"packages/highlight": {
|
||||||
"name": "@getpaseo/highlight",
|
"name": "@getpaseo/highlight",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language": "6.12.4",
|
"@codemirror/language": "6.12.4",
|
||||||
"@codemirror/legacy-modes": "^6.5.3",
|
"@codemirror/legacy-modes": "^6.5.3",
|
||||||
@@ -40379,15 +40379,15 @@
|
|||||||
},
|
},
|
||||||
"packages/plugin": {
|
"packages/plugin": {
|
||||||
"name": "@getpaseo/plugin",
|
"name": "@getpaseo/plugin",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "^1.4.0",
|
"@agentclientprotocol/sdk": "^1.4.0",
|
||||||
"i18next": "^26.3.0",
|
"i18next": "^26.3.0",
|
||||||
"use-sync-external-store": "^1.6.0"
|
"use-sync-external-store": "^1.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@getpaseo/client": "0.14.0-beta.2",
|
"@getpaseo/client": "0.14.0",
|
||||||
"@getpaseo/protocol": "0.14.0-beta.2",
|
"@getpaseo/protocol": "0.14.0",
|
||||||
"@types/node": "^20.9.0",
|
"@types/node": "^20.9.0",
|
||||||
"@types/react": "~19.2.0",
|
"@types/react": "~19.2.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
@@ -40397,8 +40397,8 @@
|
|||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@getpaseo/client": "0.14.0-beta.2",
|
"@getpaseo/client": "0.14.0",
|
||||||
"@getpaseo/protocol": "0.14.0-beta.2",
|
"@getpaseo/protocol": "0.14.0",
|
||||||
"react": ">=19.1.0 <20",
|
"react": ">=19.1.0 <20",
|
||||||
"react-native": ">=0.81.5",
|
"react-native": ">=0.81.5",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
@@ -40420,7 +40420,7 @@
|
|||||||
},
|
},
|
||||||
"packages/protocol": {
|
"packages/protocol": {
|
||||||
"name": "@getpaseo/protocol",
|
"name": "@getpaseo/protocol",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": "^7.8.5",
|
"semver": "^7.8.5",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
@@ -40447,7 +40447,7 @@
|
|||||||
},
|
},
|
||||||
"packages/relay": {
|
"packages/relay": {
|
||||||
"name": "@getpaseo/relay",
|
"name": "@getpaseo/relay",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/ciphers": "^2.4.0",
|
"@noble/ciphers": "^2.4.0",
|
||||||
"@noble/curves": "^2.4.0",
|
"@noble/curves": "^2.4.0",
|
||||||
@@ -40668,16 +40668,16 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@getpaseo/server",
|
"name": "@getpaseo/server",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@agentclientprotocol/sdk": "^0.17.1",
|
"@agentclientprotocol/sdk": "^0.17.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "0.3.246",
|
"@anthropic-ai/claude-agent-sdk": "0.3.246",
|
||||||
"@anthropic-ai/sdk": "^0.104.2",
|
"@anthropic-ai/sdk": "^0.104.2",
|
||||||
"@getpaseo/client": "0.14.0-beta.2",
|
"@getpaseo/client": "0.14.0",
|
||||||
"@getpaseo/highlight": "0.14.0-beta.2",
|
"@getpaseo/highlight": "0.14.0",
|
||||||
"@getpaseo/plugin": "0.14.0-beta.2",
|
"@getpaseo/plugin": "0.14.0",
|
||||||
"@getpaseo/protocol": "0.14.0-beta.2",
|
"@getpaseo/protocol": "0.14.0",
|
||||||
"@getpaseo/relay": "0.14.0-beta.2",
|
"@getpaseo/relay": "0.14.0",
|
||||||
"@isaacs/ttlcache": "^2.1.4",
|
"@isaacs/ttlcache": "^2.1.4",
|
||||||
"@modelcontextprotocol/sdk": "1.29.0",
|
"@modelcontextprotocol/sdk": "1.29.0",
|
||||||
"@opencode-ai/sdk": "1.14.46",
|
"@opencode-ai/sdk": "1.14.46",
|
||||||
@@ -41598,7 +41598,7 @@
|
|||||||
},
|
},
|
||||||
"packages/website": {
|
"packages/website": {
|
||||||
"name": "@getpaseo/website",
|
"name": "@getpaseo/website",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/vite-plugin": "^1.29.1",
|
"@cloudflare/vite-plugin": "^1.29.1",
|
||||||
"@cloudflare/workers-types": "^4.20260317.1",
|
"@cloudflare/workers-types": "^4.20260317.1",
|
||||||
@@ -43208,7 +43208,7 @@
|
|||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"name": "@getpaseo/builtin-plugins",
|
"name": "@getpaseo/builtin-plugins",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getpaseo/plugin": "*",
|
"@getpaseo/plugin": "*",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "paseo",
|
"name": "paseo",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Paseo: voice-controlled development environment for local AI coding agents",
|
"description": "Paseo: voice-controlled development environment for local AI coding agents",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: Upload Google Play screenshots
|
||||||
|
|
||||||
|
# Standalone re-trigger for the release workflow's Google Play screenshot step.
|
||||||
|
# Replaces the en-US phone and tablet screenshots with the committed renders,
|
||||||
|
# without rebuilding or resubmitting the app.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
android_version_code:
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
description: "versionCode of the production release to attach the change to, e.g. 14000. Leave empty when the production track has a single release."
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
upload_android_screenshots:
|
||||||
|
name: Upload Google Play screenshots
|
||||||
|
environment: production
|
||||||
|
runs_on: macos-medium
|
||||||
|
steps:
|
||||||
|
- uses: eas/checkout
|
||||||
|
- name: Install fastlane
|
||||||
|
run: bundle install
|
||||||
|
- name: Upload screenshots
|
||||||
|
run: |
|
||||||
|
export ANDROID_VERSION_CODE="${{ inputs.android_version_code }}"
|
||||||
|
bundle exec fastlane android upload_screenshots
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { expect, type Locator, type Page } from "@playwright/test";
|
||||||
|
import { test } from "../support/fixtures";
|
||||||
|
import { runWorkspaceActionFromCommandCenter } from "../support/helpers/command-center-workspace-actions";
|
||||||
|
import { createCreationScenario } from "../support/helpers/creation";
|
||||||
|
import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher";
|
||||||
|
import { seedWorkspace } from "../support/helpers/seed-client";
|
||||||
|
import { waitForWorkspaceTabsVisible } from "../support/helpers/workspace-tabs";
|
||||||
|
|
||||||
|
const DRAG_DISTANCE = -40;
|
||||||
|
const TOLERANCE_PX = 2;
|
||||||
|
|
||||||
|
function splitDividers(page: Page): Locator {
|
||||||
|
return page.getByTestId("workspace-split-resize-handle").filter({ visible: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dividerX(divider: Locator): Promise<number> {
|
||||||
|
const box = await divider.boundingBox();
|
||||||
|
if (!box) throw new Error("Divider has no bounding box");
|
||||||
|
return box.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dragDivider(divider: Locator, deltaX: number): Promise<void> {
|
||||||
|
const page = divider.page();
|
||||||
|
const box = await divider.boundingBox();
|
||||||
|
if (!box) throw new Error("Divider has no bounding box");
|
||||||
|
const x = box.x + box.width / 2;
|
||||||
|
const y = box.y + box.height / 2;
|
||||||
|
await page.mouse.move(x, y);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(x + deltaX, y, { steps: 8 });
|
||||||
|
await page.mouse.up();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dragDividerWithTouch(divider: Locator, deltaX: number): Promise<void> {
|
||||||
|
const page = divider.page();
|
||||||
|
const box = await divider.boundingBox();
|
||||||
|
if (!box) throw new Error("Divider has no bounding box");
|
||||||
|
const x = box.x + box.width / 2;
|
||||||
|
const y = box.y + box.height / 2;
|
||||||
|
const input = await page.context().newCDPSession(page);
|
||||||
|
try {
|
||||||
|
await input.send("Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [{ x, y }] });
|
||||||
|
for (let step = 1; step <= 8; step++) {
|
||||||
|
await input.send("Input.dispatchTouchEvent", {
|
||||||
|
type: "touchMove",
|
||||||
|
touchPoints: [{ x: x + (deltaX * step) / 8, y }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await input.send("Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] });
|
||||||
|
} finally {
|
||||||
|
await input.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function splitIntoFourPanes(page: Page): Promise<void> {
|
||||||
|
for (const expectedDividers of [1, 2, 3]) {
|
||||||
|
await runWorkspaceActionFromCommandCenter(page, "Split pane right");
|
||||||
|
await expect(splitDividers(page)).toHaveCount(expectedDividers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectDividersTrackPointer(
|
||||||
|
page: Page,
|
||||||
|
distance = DRAG_DISTANCE,
|
||||||
|
drag = dragDivider,
|
||||||
|
): Promise<void> {
|
||||||
|
for (const index of [0, 1, 2]) {
|
||||||
|
const divider = splitDividers(page).nth(index);
|
||||||
|
const before = await dividerX(divider);
|
||||||
|
await drag(divider, distance);
|
||||||
|
await expect
|
||||||
|
.poll(async () => Math.abs((await dividerX(divider)) - before - distance), {
|
||||||
|
message: `divider ${index + 1} should stay under the pointer`,
|
||||||
|
})
|
||||||
|
.toBeLessThanOrEqual(TOLERANCE_PX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const hasTouch of [false, true]) {
|
||||||
|
test.describe(`Split pane drag tracking (${hasTouch ? "coarse" : "fine"} pointer)`, () => {
|
||||||
|
test.use({ hasTouch });
|
||||||
|
|
||||||
|
test("a divider moves as far as the pointer in the main row", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 1600, height: 900 });
|
||||||
|
const workspace = await seedWorkspace({ repoPrefix: "split-drag-tracking-" });
|
||||||
|
try {
|
||||||
|
await gotoWorkspace(page, workspace.workspaceId);
|
||||||
|
await waitForWorkspaceTabsVisible(page);
|
||||||
|
await clickNewChat(page);
|
||||||
|
await splitIntoFourPanes(page);
|
||||||
|
await expectDividersTrackPointer(
|
||||||
|
page,
|
||||||
|
DRAG_DISTANCE,
|
||||||
|
hasTouch ? dragDividerWithTouch : dragDivider,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await workspace.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dividers track the pointer after app creation, Explorer changes and reload", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.setViewportSize({ width: 1600, height: 900 });
|
||||||
|
const creation = await createCreationScenario(page);
|
||||||
|
try {
|
||||||
|
await creation.openWorkspaceForm("local");
|
||||||
|
await creation.submitPrompt("Reply briefly.", "Create");
|
||||||
|
await creation.expectAssistantReply();
|
||||||
|
await waitForWorkspaceTabsVisible(page);
|
||||||
|
await splitIntoFourPanes(page);
|
||||||
|
await expectDividersTrackPointer(page);
|
||||||
|
await page
|
||||||
|
.getByTestId("workspace-explorer-toggle")
|
||||||
|
.filter({ visible: true })
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
await expect(page.getByTestId("workspace-explorer-sidebar")).toBeVisible();
|
||||||
|
await expectDividersTrackPointer(page, -DRAG_DISTANCE);
|
||||||
|
await page
|
||||||
|
.getByTestId("workspace-explorer-toggle")
|
||||||
|
.filter({ visible: true })
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
await expect(page.getByTestId("workspace-explorer-sidebar")).not.toBeVisible();
|
||||||
|
await page.reload();
|
||||||
|
await expect(splitDividers(page)).toHaveCount(3);
|
||||||
|
await expectDividersTrackPointer(page);
|
||||||
|
} finally {
|
||||||
|
await creation.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -26,6 +26,22 @@ def stage_app_store_screenshots
|
|||||||
root
|
root
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Google Play only has en-US screenshots, and some F-Droid locales in
|
||||||
|
# ANDROID_METADATA are not Play languages (Play rejects "ja"). supply uploads
|
||||||
|
# every locale folder in one edit, so give it only the en-US images.
|
||||||
|
def stage_google_play_screenshots
|
||||||
|
root = Dir.mktmpdir("google-play-screenshots")
|
||||||
|
images = File.join(ANDROID_METADATA, "en-US/images")
|
||||||
|
%w[phoneScreenshots tenInchScreenshots].each do |kind|
|
||||||
|
target = File.join(root, "en-US/images", kind)
|
||||||
|
FileUtils.mkdir_p(target)
|
||||||
|
Dir.glob(File.join(images, kind, "*.png")).sort.each do |file|
|
||||||
|
FileUtils.cp(File.realpath(file), File.join(target, File.basename(file)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
root
|
||||||
|
end
|
||||||
|
|
||||||
platform :ios do
|
platform :ios do
|
||||||
desc "Submit the latest TestFlight build to App Store Review"
|
desc "Submit the latest TestFlight build to App Store Review"
|
||||||
lane :submit_review do
|
lane :submit_review do
|
||||||
@@ -132,7 +148,7 @@ platform :android do
|
|||||||
supply(
|
supply(
|
||||||
package_name: APP_IDENTIFIER,
|
package_name: APP_IDENTIFIER,
|
||||||
json_key: ENV.fetch("GOOGLE_PLAY_JSON_KEY"),
|
json_key: ENV.fetch("GOOGLE_PLAY_JSON_KEY"),
|
||||||
metadata_path: ANDROID_METADATA,
|
metadata_path: stage_google_play_screenshots,
|
||||||
version_code: version_code.empty? ? nil : version_code.to_i,
|
version_code: version_code.empty? ? nil : version_code.to_i,
|
||||||
skip_upload_apk: true,
|
skip_upload_apk: true,
|
||||||
skip_upload_aab: true,
|
skip_upload_aab: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/app",
|
"name": "@getpaseo/app",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.ts",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function ResizeHandle({
|
|||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const finePointer = useHasFinePointer();
|
const finePointer = useHasFinePointer();
|
||||||
const pointerStatesRef = useRef(new Map<number, PointerState>());
|
const pointerStatesRef = useRef(new Map<number, PointerState>());
|
||||||
const touchDragRef = useRef<ResizeHandleDrag | null>(null);
|
const touchStateRef = useRef<PointerState | null>(null);
|
||||||
const cursorBeforeDragRef = useRef<string | null>(null);
|
const cursorBeforeDragRef = useRef<string | null>(null);
|
||||||
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const [active, setActive] = useState(false);
|
const [active, setActive] = useState(false);
|
||||||
@@ -151,23 +151,30 @@ export function ResizeHandle({
|
|||||||
const touchGesture = useMemo(() => {
|
const touchGesture = useMemo(() => {
|
||||||
const gesture = Gesture.Pan()
|
const gesture = Gesture.Pan()
|
||||||
.runOnJS(true)
|
.runOnJS(true)
|
||||||
.onBegin(() => setDragging(true))
|
.onBegin((event) => {
|
||||||
.onStart(() => {
|
setDragging(true);
|
||||||
touchDragRef.current = startResizeHandleDrag({
|
touchStateRef.current = {
|
||||||
sizes,
|
containerSize,
|
||||||
index,
|
pointerStart: direction === "horizontal" ? event.absoluteX : event.absoluteY,
|
||||||
preview: (nextSizes) => onPreviewResizeSplit(groupId, nextSizes),
|
drag: startResizeHandleDrag({
|
||||||
commit: (nextSizes) => onResizeSplit(groupId, nextSizes),
|
sizes,
|
||||||
});
|
index,
|
||||||
|
preview: (nextSizes) => onPreviewResizeSplit(groupId, nextSizes),
|
||||||
|
commit: (nextSizes) => onResizeSplit(groupId, nextSizes),
|
||||||
|
}),
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.onUpdate((event) => {
|
.onUpdate((event) => {
|
||||||
if (containerSize <= 0) return;
|
const state = touchStateRef.current;
|
||||||
const translation = direction === "horizontal" ? event.translationX : event.translationY;
|
if (!state || state.containerSize <= 0) return;
|
||||||
touchDragRef.current?.move(translation / containerSize);
|
// Gesture Handler resets web translation when the pan activates. Measure from
|
||||||
|
// the initial press so the activation distance is included on every platform.
|
||||||
|
const pointerCurrent = direction === "horizontal" ? event.absoluteX : event.absoluteY;
|
||||||
|
state.drag.move((pointerCurrent - state.pointerStart) / state.containerSize);
|
||||||
})
|
})
|
||||||
.onEnd(() => touchDragRef.current?.finish())
|
.onEnd(() => touchStateRef.current?.drag.finish())
|
||||||
.onFinalize(() => {
|
.onFinalize(() => {
|
||||||
touchDragRef.current = null;
|
touchStateRef.current = null;
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-
|
|||||||
import {
|
import {
|
||||||
collectAllPanes,
|
collectAllPanes,
|
||||||
createDefaultLayout,
|
createDefaultLayout,
|
||||||
|
selectExplorerSidebarPaneId,
|
||||||
useWorkspaceLayoutStore,
|
useWorkspaceLayoutStore,
|
||||||
type SplitNode,
|
type SplitNode,
|
||||||
type SplitPane,
|
type SplitPane,
|
||||||
@@ -183,8 +184,8 @@ export function SplitContainer({
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const maximizedPaneId =
|
const maximizedPaneId =
|
||||||
maximizedPane?.workspaceKey === workspaceKey ? maximizedPane.paneId : null;
|
maximizedPane?.workspaceKey === workspaceKey ? maximizedPane.paneId : null;
|
||||||
const explorerSidebarPaneId = useWorkspaceLayoutStore(
|
const explorerSidebarPaneId = useWorkspaceLayoutStore((state) =>
|
||||||
(state) => state.explorerSidebarPaneIdByWorkspace[workspaceKey] ?? null,
|
selectExplorerSidebarPaneId(state, workspaceKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
const mainRoot = useMemo(
|
const mainRoot = useMemo(
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import React, { act, useEffect, useState, type ReactNode } from "react";
|
||||||
|
import { createRoot, type Root } from "react-dom/client";
|
||||||
|
import { Text, View } from "react-native";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { page } from "vitest/browser";
|
||||||
|
import { MenuRoot, MenuSurface, MenuTrigger, type Size } from "./index";
|
||||||
|
|
||||||
|
// App sources compile against the classic JSX runtime, which expects React on the global.
|
||||||
|
beforeEach(() => vi.stubGlobal("React", React));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A real browser, because the regression lives in Reanimated's web entering path: it snapshots
|
||||||
|
* the surface's rect when a custom keyframe ends and writes that rect back as inline
|
||||||
|
* top/left/width/height when it cleans the keyframe up, 750ms after the animation starts.
|
||||||
|
* Anything that moved or resized the surface in between is undone by that write.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface Mounted {
|
||||||
|
root: Root;
|
||||||
|
container: HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mounted: Mounted[] = [];
|
||||||
|
|
||||||
|
function mount(node: ReactNode): void {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
const root = createRoot(container);
|
||||||
|
act(() => root.render(node));
|
||||||
|
mounted.push({ root, container });
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const entry of mounted.splice(0)) {
|
||||||
|
act(() => entry.root.unmount());
|
||||||
|
entry.container.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const OFFSET = 12;
|
||||||
|
const PILL_POSITION = { position: "absolute", top: 700, left: 40 } as const;
|
||||||
|
const SHORT: Size = { width: 200, height: 40 };
|
||||||
|
const TALL: Size = { width: 200, height: 240 };
|
||||||
|
const WIDE: Size = { width: 480, height: 40 };
|
||||||
|
/** Inside the window between the entering animation ending and Reanimated's cleanup. */
|
||||||
|
const GROWS_BEFORE_CLEANUP_MS = 400;
|
||||||
|
/** Past Reanimated's cleanup, where the surface used to keep a fixed height. */
|
||||||
|
const GROWS_AFTER_CLEANUP_MS = 1_000;
|
||||||
|
/** Long enough after the content grew for anything that would write to the surface to land. */
|
||||||
|
const SETTLE_MS = 1_200;
|
||||||
|
|
||||||
|
/** Plain timers, not `act`: an async `act` holds every update until it resolves. */
|
||||||
|
function wait(ms: number): Promise<void> {
|
||||||
|
return new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function element(testID: string): HTMLElement {
|
||||||
|
const found = document.querySelector(`[data-testid="${testID}"]`);
|
||||||
|
if (!(found instanceof HTMLElement)) throw new Error(`${testID} did not render`);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stands in for a popover whose data lands after it opened, like a plugin's first RPC. */
|
||||||
|
function GrowsAfterOpen({ afterMs, from, to }: { afterMs: number; from: Size; to: Size }) {
|
||||||
|
const [grown, setGrown] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => setGrown(true), afterMs);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [afterMs]);
|
||||||
|
return <View testID="body" style={grown ? to : from} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountMenuAbovePill(body: ReactNode): void {
|
||||||
|
mount(
|
||||||
|
<View style={PILL_POSITION}>
|
||||||
|
<MenuRoot>
|
||||||
|
<MenuTrigger testID="pill" accessibilityLabel="Open panel">
|
||||||
|
<Text>Open panel</Text>
|
||||||
|
</MenuTrigger>
|
||||||
|
<MenuSurface
|
||||||
|
side="top"
|
||||||
|
align="start"
|
||||||
|
offset={OFFSET}
|
||||||
|
minWidth={280}
|
||||||
|
maxWidth={620}
|
||||||
|
testID="panel"
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</MenuSurface>
|
||||||
|
</MenuRoot>
|
||||||
|
</View>,
|
||||||
|
);
|
||||||
|
act(() => element("pill").click());
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("anchored menu surface on web", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await page.viewport(1280, 900);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([GROWS_BEFORE_CLEANUP_MS, GROWS_AFTER_CLEANUP_MS])(
|
||||||
|
"stays on its anchor when its content grows %ims after opening",
|
||||||
|
async (afterMs) => {
|
||||||
|
mountMenuAbovePill(<GrowsAfterOpen afterMs={afterMs} from={SHORT} to={TALL} />);
|
||||||
|
|
||||||
|
await wait(afterMs + SETTLE_MS);
|
||||||
|
|
||||||
|
const pill = element("pill").getBoundingClientRect();
|
||||||
|
const panel = element("panel").getBoundingClientRect();
|
||||||
|
expect(panel.height).toBeGreaterThan(240);
|
||||||
|
expect(panel.bottom).toBeLessThanOrEqual(pill.top);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("widens when its content widens after the entering animation", async () => {
|
||||||
|
mountMenuAbovePill(<GrowsAfterOpen afterMs={GROWS_BEFORE_CLEANUP_MS} from={SHORT} to={WIDE} />);
|
||||||
|
|
||||||
|
await wait(GROWS_BEFORE_CLEANUP_MS + SETTLE_MS);
|
||||||
|
|
||||||
|
const body = element("body").getBoundingClientRect();
|
||||||
|
const panel = element("panel").getBoundingClientRect();
|
||||||
|
expect(panel.width).toBeGreaterThanOrEqual(body.width);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useId,
|
useId,
|
||||||
|
useLayoutEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
type ReactElement,
|
type ReactElement,
|
||||||
@@ -47,56 +48,41 @@ const contentEntering = new Keyframe({
|
|||||||
100: { opacity: 1, transform: [{ scale: 1 }] },
|
100: { opacity: 1, transform: [{ scale: 1 }] },
|
||||||
}).duration(CONTENT_ENTERING_DURATION_MS);
|
}).duration(CONTENT_ENTERING_DURATION_MS);
|
||||||
|
|
||||||
|
/** `contentEntering` for the browser's own animation engine. Both default to linear easing. */
|
||||||
|
const WEB_CONTENT_ENTERING_KEYFRAMES: PropertyIndexedKeyframes = {
|
||||||
|
opacity: [0, 1],
|
||||||
|
transform: ["scale(0.97)", "scale(1)"],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Web plays the same keyframes itself; see `useWebContentEntering`. */
|
||||||
|
const nativeContentEntering = isWeb ? undefined : contentEntering;
|
||||||
|
|
||||||
const contentExiting = new Keyframe({
|
const contentExiting = new Keyframe({
|
||||||
0: { opacity: 1, transform: [{ scale: 1 }] },
|
0: { opacity: 1, transform: [{ scale: 1 }] },
|
||||||
100: { opacity: 0, transform: [{ scale: 0.97 }] },
|
100: { opacity: 0, transform: [{ scale: 0.97 }] },
|
||||||
}).duration(100);
|
}).duration(100);
|
||||||
|
|
||||||
function releaseFixedMenuHeight(surfaceNativeID: string): void {
|
|
||||||
if (!isWeb) return;
|
|
||||||
document.getElementById(surfaceNativeID)?.style.removeProperty("height");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reanimated's web entering animation leaves an inline height snapshot on the measured surface.
|
* On web the surface enters through the Web Animations API rather than Reanimated. Reanimated's
|
||||||
* Once the menu is open, height must return to content-sized so rows can grow in place — and so
|
* web path snapshots an element's rect when a custom keyframe ends and, when it cleans the keyframe
|
||||||
* a submenu page taller than the page it replaced is not clipped to the old height.
|
* up 750ms after the start, writes that rect back as inline top/left/width/height. A surface that
|
||||||
*
|
* moved or resized in between — content that grew once its data arrived — snapped back to the old
|
||||||
* `revision` is what makes that second case work: bump it whenever the rendered content changes
|
* rect and stayed there, clipped or hanging past its anchor. A Web Animation leaves no inline style.
|
||||||
* identity (a page push, for instance) and the snapshot is released again.
|
|
||||||
*/
|
*/
|
||||||
function useReleaseFixedMenuHeight({
|
function useWebContentEntering({
|
||||||
contentSize,
|
placed,
|
||||||
enabled,
|
|
||||||
surfaceNativeID,
|
surfaceNativeID,
|
||||||
revision,
|
|
||||||
}: {
|
}: {
|
||||||
contentSize: Size | null;
|
placed: boolean;
|
||||||
enabled: boolean;
|
|
||||||
surfaceNativeID: string;
|
surfaceNativeID: string;
|
||||||
revision?: string | number;
|
|
||||||
}): void {
|
}): void {
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!enabled) return undefined;
|
if (!isWeb || !placed || typeof document === "undefined") return undefined;
|
||||||
|
const animation = document
|
||||||
const release = () => {
|
.getElementById(surfaceNativeID)
|
||||||
releaseFixedMenuHeight(surfaceNativeID);
|
?.animate(WEB_CONTENT_ENTERING_KEYFRAMES, CONTENT_ENTERING_DURATION_MS);
|
||||||
if (typeof requestAnimationFrame === "function") {
|
return () => animation?.cancel();
|
||||||
requestAnimationFrame(() => releaseFixedMenuHeight(surfaceNativeID));
|
}, [placed, surfaceNativeID]);
|
||||||
}
|
|
||||||
};
|
|
||||||
const timers: ReturnType<typeof setTimeout>[] = [
|
|
||||||
setTimeout(release, CONTENT_ENTERING_DURATION_MS),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (contentSize) {
|
|
||||||
timers.push(setTimeout(release, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
for (const timer of timers) clearTimeout(timer);
|
|
||||||
};
|
|
||||||
}, [contentSize, enabled, surfaceNativeID, revision]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -224,8 +210,6 @@ export interface AnchoredSurfaceProps {
|
|||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
horizontalPadding?: number;
|
horizontalPadding?: number;
|
||||||
scrollable?: boolean;
|
scrollable?: boolean;
|
||||||
/** Bump when the rendered content changes identity, so the height snapshot is released. */
|
|
||||||
revision?: string | number;
|
|
||||||
/** A submenu sits inside its parent's overlay and must not paint a second backdrop. */
|
/** A submenu sits inside its parent's overlay and must not paint a second backdrop. */
|
||||||
backdrop?: boolean;
|
backdrop?: boolean;
|
||||||
/**
|
/**
|
||||||
@@ -259,7 +243,6 @@ export function AnchoredSurface({
|
|||||||
fullWidth = false,
|
fullWidth = false,
|
||||||
horizontalPadding = 16,
|
horizontalPadding = 16,
|
||||||
scrollable = false,
|
scrollable = false,
|
||||||
revision,
|
|
||||||
backdrop = true,
|
backdrop = true,
|
||||||
onPointerEnter,
|
onPointerEnter,
|
||||||
onPointerLeave,
|
onPointerLeave,
|
||||||
@@ -269,33 +252,25 @@ export function AnchoredSurface({
|
|||||||
}: AnchoredSurfaceProps): ReactElement | null {
|
}: AnchoredSurfaceProps): ReactElement | null {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const surfaceNativeID = useId();
|
const surfaceNativeID = useId();
|
||||||
const { position, actualPlacement, contentSize, visibleContentSize, onContentLayout } =
|
const { position, actualPlacement, visibleContentSize, onContentLayout } = useAnchoredPosition({
|
||||||
useAnchoredPosition({
|
open,
|
||||||
open,
|
anchorRect,
|
||||||
anchorRect,
|
anchorRef,
|
||||||
anchorRef,
|
side,
|
||||||
side,
|
align,
|
||||||
align,
|
offset,
|
||||||
offset,
|
scrollable,
|
||||||
scrollable,
|
maxHeight,
|
||||||
maxHeight,
|
|
||||||
});
|
|
||||||
|
|
||||||
useReleaseFixedMenuHeight({
|
|
||||||
contentSize,
|
|
||||||
enabled: open,
|
|
||||||
surfaceNativeID,
|
|
||||||
revision,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// The surface is placed once both measurements land. Until then it sits off-screen so it can
|
// The surface is placed once both measurements land. Until then it sits off-screen so it can
|
||||||
// be measured, and it must not animate: Reanimated's web entering path snapshots the element's
|
// be measured, and it must not animate: an entering animation that ran there would be over
|
||||||
// rect when the animation ends and writes that rect back as inline top/left when it cleans the
|
// before anyone saw the menu. Remounting on placement starts the native animation from the
|
||||||
// keyframe up 750ms later. If the animation ends before placement, the snapshot is the
|
// final position; web starts its own animation on placement.
|
||||||
// off-screen rect and the menu jumps back off-screen. Remounting on placement starts the
|
|
||||||
// animation from the final position.
|
|
||||||
const placed = position !== null;
|
const placed = position !== null;
|
||||||
|
|
||||||
|
useWebContentEntering({ placed, surfaceNativeID });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isWeb || !open || !placed || typeof document === "undefined") return undefined;
|
if (!isWeb || !open || !placed || typeof document === "undefined") return undefined;
|
||||||
const frame = requestAnimationFrame(() => {
|
const frame = requestAnimationFrame(() => {
|
||||||
@@ -383,7 +358,7 @@ export function AnchoredSurface({
|
|||||||
dataSet={surfaceDataSet}
|
dataSet={surfaceDataSet}
|
||||||
style={styles.content}
|
style={styles.content}
|
||||||
frameStyle={frameStyle}
|
frameStyle={frameStyle}
|
||||||
entering={placed ? contentEntering : undefined}
|
entering={placed ? nativeContentEntering : undefined}
|
||||||
exiting={
|
exiting={
|
||||||
!placed || isWeb || !onExited
|
!placed || isWeb || !onExited
|
||||||
? undefined
|
? undefined
|
||||||
|
|||||||
@@ -344,7 +344,6 @@ function MenuFlyout({
|
|||||||
maxHeight={maxHeight}
|
maxHeight={maxHeight}
|
||||||
scrollable={scrollable}
|
scrollable={scrollable}
|
||||||
backdrop={false}
|
backdrop={false}
|
||||||
revision={page.id}
|
|
||||||
onPointerEnter={cancelHoverClose}
|
onPointerEnter={cancelHoverClose}
|
||||||
onPointerLeave={handleHoverOut}
|
onPointerLeave={handleHoverOut}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/cli",
|
"name": "@getpaseo/cli",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"description": "Paseo CLI - control your AI coding agents from the command line",
|
"description": "Paseo CLI - control your AI coding agents from the command line",
|
||||||
"bin": {
|
"bin": {
|
||||||
"paseo": "bin/paseo"
|
"paseo": "bin/paseo"
|
||||||
@@ -28,9 +28,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^1.0.0",
|
"@clack/prompts": "^1.0.0",
|
||||||
"@getpaseo/client": "0.14.0-beta.2",
|
"@getpaseo/client": "0.14.0",
|
||||||
"@getpaseo/protocol": "0.14.0-beta.2",
|
"@getpaseo/protocol": "0.14.0",
|
||||||
"@getpaseo/server": "0.14.0-beta.2",
|
"@getpaseo/server": "0.14.0",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"commander": "^12.0.0",
|
"commander": "^12.0.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/client",
|
"name": "@getpaseo/client",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"description": "Paseo client SDK package",
|
"description": "Paseo client SDK package",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
@@ -47,8 +47,8 @@
|
|||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getpaseo/protocol": "0.14.0-beta.2",
|
"@getpaseo/protocol": "0.14.0",
|
||||||
"@getpaseo/relay": "0.14.0-beta.2",
|
"@getpaseo/relay": "0.14.0",
|
||||||
"ws": "^8.20.0",
|
"ws": "^8.20.0",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
import electronPath from "electron";
|
||||||
|
import { build } from "esbuild";
|
||||||
|
|
||||||
|
export async function runAdaptiveThemeRegression(artifactDir) {
|
||||||
|
const { outputFiles } = await build({
|
||||||
|
stdin: {
|
||||||
|
resolveDir: fileURLToPath(new URL("../../app", import.meta.url)),
|
||||||
|
contents: `
|
||||||
|
import { createElement } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { StyleSheet, UnistylesRuntime, withUnistyles } from "react-native-unistyles";
|
||||||
|
StyleSheet.configure({
|
||||||
|
themes: {
|
||||||
|
dark: { foreground: "#fafafa", background: "#181b1a" },
|
||||||
|
light: { foreground: "#1a1a1e", background: "#ffffff" },
|
||||||
|
},
|
||||||
|
settings: { adaptiveThemes: true },
|
||||||
|
});
|
||||||
|
const Message = withUnistyles(({ color }) =>
|
||||||
|
createElement("p", { style: { color } }, "Theme-reactive message"));
|
||||||
|
createRoot(document.getElementById("root")).render(createElement(Message, {
|
||||||
|
style: { margin: 0 },
|
||||||
|
uniProps: theme => ({ color: theme.foreground }),
|
||||||
|
}));
|
||||||
|
window.readThemeForTest = () => UnistylesRuntime.themeName;
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
bundle: true,
|
||||||
|
write: false,
|
||||||
|
format: "iife",
|
||||||
|
platform: "browser",
|
||||||
|
alias: { "react-native": "react-native-web" },
|
||||||
|
define: { __DEV__: "false" },
|
||||||
|
resolveExtensions: [".web.js", ".js", ".json"],
|
||||||
|
});
|
||||||
|
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-adaptive-theme-"));
|
||||||
|
try {
|
||||||
|
await fs.writeFile(path.join(directory, "renderer.js"), outputFiles[0].text);
|
||||||
|
await fs.writeFile(path.join(directory, "main.cjs"), `(${electronRegression.toString()})()`);
|
||||||
|
// A separate process avoids Playwright's focus emulation, which keeps hidden pages visible.
|
||||||
|
const env = { ...process.env };
|
||||||
|
delete env.ELECTRON_RUN_AS_NODE;
|
||||||
|
const args = [
|
||||||
|
...(process.platform === "linux" ? ["--no-sandbox"] : []),
|
||||||
|
path.join(directory, "main.cjs"),
|
||||||
|
artifactDir,
|
||||||
|
];
|
||||||
|
const command = process.platform === "linux" ? "xvfb-run" : electronPath;
|
||||||
|
const commandArgs =
|
||||||
|
process.platform === "linux"
|
||||||
|
? ["-a", "--server-args=-screen 0 1280x800x24", electronPath, ...args]
|
||||||
|
: args;
|
||||||
|
await promisify(execFile)(command, commandArgs, {
|
||||||
|
env,
|
||||||
|
timeout: 30_000,
|
||||||
|
});
|
||||||
|
return JSON.parse(await fs.readFile(path.join(artifactDir, "adaptive-theme.json"), "utf8"));
|
||||||
|
} finally {
|
||||||
|
await fs.rm(directory, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function electronRegression() {
|
||||||
|
const { app, BrowserWindow, nativeTheme } = require("electron");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const files = require("node:fs");
|
||||||
|
const paths = require("node:path");
|
||||||
|
const { setTimeout: delay } = require("node:timers/promises");
|
||||||
|
app.setPath("userData", paths.join(__dirname, "profile"));
|
||||||
|
app.on("window-all-closed", () => {});
|
||||||
|
// Electron keeps Linux's --no-sandbox in argv; the artifact directory is always last.
|
||||||
|
const artifactDir = process.argv.at(-1);
|
||||||
|
let window;
|
||||||
|
const observations = [];
|
||||||
|
const colors = {
|
||||||
|
dark: { foreground: "rgb(250, 250, 250)", background: "rgb(24, 27, 26)" },
|
||||||
|
light: { foreground: "rgb(26, 26, 30)", background: "rgb(255, 255, 255)" },
|
||||||
|
};
|
||||||
|
async function read(expression) {
|
||||||
|
return window.webContents.executeJavaScript(expression);
|
||||||
|
}
|
||||||
|
async function expectValue(expression, expected) {
|
||||||
|
let actual;
|
||||||
|
for (let attempt = 0; attempt < 50; attempt++) {
|
||||||
|
actual = await read(expression);
|
||||||
|
if (actual === expected) return;
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
assert.equal(actual, expected, expression);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await app.whenReady();
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
app.setActivationPolicy("accessory");
|
||||||
|
app.dock.hide();
|
||||||
|
}
|
||||||
|
for (const [from, to] of [
|
||||||
|
["dark", "light"],
|
||||||
|
["light", "dark"],
|
||||||
|
]) {
|
||||||
|
nativeTheme.themeSource = from;
|
||||||
|
window = new BrowserWindow({ width: 600, height: 200, show: false, skipTaskbar: true });
|
||||||
|
await window.loadURL(
|
||||||
|
"data:text/html," +
|
||||||
|
encodeURIComponent(`<!doctype html>
|
||||||
|
<style>body { background: var(--background); } p { font: 20px system-ui; }</style>
|
||||||
|
<div id="root"></div>`),
|
||||||
|
);
|
||||||
|
await read(files.readFileSync(paths.join(__dirname, "renderer.js"), "utf8"));
|
||||||
|
window.showInactive();
|
||||||
|
const foreground =
|
||||||
|
"document.querySelector('p') && getComputedStyle(document.querySelector('p')).color";
|
||||||
|
const background = "getComputedStyle(document.body).backgroundColor";
|
||||||
|
await expectValue(foreground, colors[from].foreground);
|
||||||
|
await expectValue(background, colors[from].background);
|
||||||
|
window.hide();
|
||||||
|
await expectValue("document.visibilityState", "hidden");
|
||||||
|
nativeTheme.themeSource = to;
|
||||||
|
await expectValue(`matchMedia('(prefers-color-scheme: ${to})').matches`, true);
|
||||||
|
// Reading the public runtime while hidden must not consume the pending theme notification.
|
||||||
|
assert.equal(await read("window.readThemeForTest()"), to);
|
||||||
|
window.showInactive();
|
||||||
|
await expectValue("document.visibilityState", "visible");
|
||||||
|
await expectValue(background, colors[to].background);
|
||||||
|
try {
|
||||||
|
await expectValue(foreground, colors[to].foreground);
|
||||||
|
} finally {
|
||||||
|
files.writeFileSync(
|
||||||
|
paths.join(artifactDir, `theme-${from}-to-${to}.png`),
|
||||||
|
(await window.webContents.capturePage()).toPNG(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
observations.push({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
foreground: await read(foreground),
|
||||||
|
background: await read(background),
|
||||||
|
});
|
||||||
|
window.destroy();
|
||||||
|
window = undefined;
|
||||||
|
}
|
||||||
|
files.writeFileSync(
|
||||||
|
paths.join(artifactDir, "adaptive-theme.json"),
|
||||||
|
JSON.stringify(observations, null, 2),
|
||||||
|
);
|
||||||
|
app.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
window?.destroy();
|
||||||
|
app.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
|
|||||||
import { chromium } from "playwright";
|
import { chromium } from "playwright";
|
||||||
import { hideDeveloperToasts } from "../../app/e2e/support/developer-ui.mjs";
|
import { hideDeveloperToasts } from "../../app/e2e/support/developer-ui.mjs";
|
||||||
import { runAppearanceFontSizeRegression } from "./appearance-font-size.electron.mjs";
|
import { runAppearanceFontSizeRegression } from "./appearance-font-size.electron.mjs";
|
||||||
|
import { runAdaptiveThemeRegression } from "./appearance-theme.electron.mjs";
|
||||||
import { runSettingsScrollRegression } from "./settings-scroll.electron.mjs";
|
import { runSettingsScrollRegression } from "./settings-scroll.electron.mjs";
|
||||||
import { runSettingsMemoryRegression } from "./settings-memory.electron.mjs";
|
import { runSettingsMemoryRegression } from "./settings-memory.electron.mjs";
|
||||||
|
|
||||||
@@ -1241,6 +1242,7 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adaptiveTheme = await runAdaptiveThemeRegression(artifactDir);
|
||||||
await runAppearanceFontSizeRegression(page);
|
await runAppearanceFontSizeRegression(page);
|
||||||
|
|
||||||
const callerAgentId = await createCallerAgent(daemonPort);
|
const callerAgentId = await createCallerAgent(daemonPort);
|
||||||
@@ -1265,7 +1267,12 @@ async function main() {
|
|||||||
artifactDir,
|
artifactDir,
|
||||||
});
|
});
|
||||||
const pluginLinks = await checkPluginLinks();
|
const pluginLinks = await checkPluginLinks();
|
||||||
writeJson(path.join(artifactDir, "result.json"), { ...report, settingsMemory, pluginLinks });
|
writeJson(path.join(artifactDir, "result.json"), {
|
||||||
|
...report,
|
||||||
|
settingsMemory,
|
||||||
|
pluginLinks,
|
||||||
|
adaptiveTheme,
|
||||||
|
});
|
||||||
console.log(
|
console.log(
|
||||||
`Browser desktop browser E2E passed: WebContents ${report.originalWebContentsId} remained ${report.finalWebContentsId}; viewport, inactive capture, focus continuity, list, snapshot, click, local-page selectors passed.`,
|
`Browser desktop browser E2E passed: WebContents ${report.originalWebContentsId} remained ${report.finalWebContentsId}; viewport, inactive capture, focus continuity, list, snapshot, click, local-page selectors passed.`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/desktop",
|
"name": "@getpaseo/desktop",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Paseo desktop app (Electron wrapper)",
|
"description": "Paseo desktop app (Electron wrapper)",
|
||||||
"homepage": "https://paseo.sh",
|
"homepage": "https://paseo.sh",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/expo-two-way-audio",
|
"name": "@getpaseo/expo-two-way-audio",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"description": "Native module for two way audio streaming",
|
"description": "Native module for two way audio streaming",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ExpoTwoWayAudio",
|
"ExpoTwoWayAudio",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/highlight",
|
"name": "@getpaseo/highlight",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"!dist/**/*.map",
|
"!dist/**/*.map",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/plugin",
|
"name": "@getpaseo/plugin",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"description": "Paseo plugin SDK package",
|
"description": "Paseo plugin SDK package",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
@@ -77,8 +77,8 @@
|
|||||||
"use-sync-external-store": "^1.6.0"
|
"use-sync-external-store": "^1.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@getpaseo/client": "0.14.0-beta.2",
|
"@getpaseo/client": "0.14.0",
|
||||||
"@getpaseo/protocol": "0.14.0-beta.2",
|
"@getpaseo/protocol": "0.14.0",
|
||||||
"@types/node": "^20.9.0",
|
"@types/node": "^20.9.0",
|
||||||
"@types/react": "~19.2.0",
|
"@types/react": "~19.2.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
@@ -88,8 +88,8 @@
|
|||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@getpaseo/client": "0.14.0-beta.2",
|
"@getpaseo/client": "0.14.0",
|
||||||
"@getpaseo/protocol": "0.14.0-beta.2",
|
"@getpaseo/protocol": "0.14.0",
|
||||||
"react": ">=19.1.0 <20",
|
"react": ">=19.1.0 <20",
|
||||||
"react-native": ">=0.81.5",
|
"react-native": ">=0.81.5",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/protocol",
|
"name": "@getpaseo/protocol",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"description": "Paseo shared protocol schemas and wire types",
|
"description": "Paseo shared protocol schemas and wire types",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/relay",
|
"name": "@getpaseo/relay",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"description": "Paseo relay for bridging daemon and client connections",
|
"description": "Paseo relay for bridging daemon and client connections",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/server",
|
"name": "@getpaseo/server",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"description": "Paseo backend server",
|
"description": "Paseo backend server",
|
||||||
"files": [
|
"files": [
|
||||||
"dist/server",
|
"dist/server",
|
||||||
@@ -107,11 +107,11 @@
|
|||||||
"@agentclientprotocol/sdk": "^0.17.1",
|
"@agentclientprotocol/sdk": "^0.17.1",
|
||||||
"@anthropic-ai/claude-agent-sdk": "0.3.246",
|
"@anthropic-ai/claude-agent-sdk": "0.3.246",
|
||||||
"@anthropic-ai/sdk": "^0.104.2",
|
"@anthropic-ai/sdk": "^0.104.2",
|
||||||
"@getpaseo/client": "0.14.0-beta.2",
|
"@getpaseo/client": "0.14.0",
|
||||||
"@getpaseo/highlight": "0.14.0-beta.2",
|
"@getpaseo/highlight": "0.14.0",
|
||||||
"@getpaseo/plugin": "0.14.0-beta.2",
|
"@getpaseo/plugin": "0.14.0",
|
||||||
"@getpaseo/protocol": "0.14.0-beta.2",
|
"@getpaseo/protocol": "0.14.0",
|
||||||
"@getpaseo/relay": "0.14.0-beta.2",
|
"@getpaseo/relay": "0.14.0",
|
||||||
"@isaacs/ttlcache": "^2.1.4",
|
"@isaacs/ttlcache": "^2.1.4",
|
||||||
"@modelcontextprotocol/sdk": "1.29.0",
|
"@modelcontextprotocol/sdk": "1.29.0",
|
||||||
"@opencode-ai/sdk": "1.14.46",
|
"@opencode-ai/sdk": "1.14.46",
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { measureMainThreadWork } from "../../test-utils/main-thread-work.js";
|
import { execFile } from "node:child_process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { selectReferenceProjectedTimelinePage } from "./test-utils/timeline-page-reference.js";
|
import { selectReferenceProjectedTimelinePage } from "./test-utils/timeline-page-reference.js";
|
||||||
import type { AgentTimelineItem } from "./agent-sdk-types.js";
|
import type { AgentTimelineItem } from "./agent-sdk-types.js";
|
||||||
@@ -251,23 +253,52 @@ it("includes transitive tool updates in a contiguous projected tail", () => {
|
|||||||
expect(tail).toMatchObject({ startSeq: 1, endSeq: 6, hasOlder: false });
|
expect(tail).toMatchObject({ startSeq: 1, endSeq: 6, hasOlder: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function measureTimelineTail(wholeTurns: boolean) {
|
||||||
|
const fixture = fileURLToPath(
|
||||||
|
new URL("./test-utils/timeline-responsiveness.ts", import.meta.url),
|
||||||
|
);
|
||||||
|
const { stdout, stderr } = await promisify(execFile)(
|
||||||
|
process.execPath,
|
||||||
|
["--expose-gc", "--import", "tsx", fixture, String(wholeTurns)],
|
||||||
|
{ timeout: 15_000 },
|
||||||
|
);
|
||||||
|
process.stderr.write(stderr);
|
||||||
|
return JSON.parse(stdout) as {
|
||||||
|
result: ReturnType<InMemoryAgentTimelineStore["fetch"]>;
|
||||||
|
blockingMs: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it("excludes unrelated test-runner CPU from the timeline fetch budget", async () => {
|
||||||
|
let unrelatedWork: ReturnType<typeof setImmediate>;
|
||||||
|
const burnCpu = () => {
|
||||||
|
const threadCpuUsage: (previous?: NodeJS.CpuUsage) => NodeJS.CpuUsage = Reflect.get(
|
||||||
|
process,
|
||||||
|
"threadCpuUsage",
|
||||||
|
);
|
||||||
|
const start = threadCpuUsage();
|
||||||
|
let elapsed: NodeJS.CpuUsage;
|
||||||
|
do {
|
||||||
|
elapsed = threadCpuUsage(start);
|
||||||
|
} while (elapsed.user + elapsed.system < 80_000);
|
||||||
|
unrelatedWork = setImmediate(burnCpu);
|
||||||
|
};
|
||||||
|
unrelatedWork = setImmediate(burnCpu);
|
||||||
|
try {
|
||||||
|
const { result: tail, blockingMs } = await measureTimelineTail(false);
|
||||||
|
expect(tail.rows).toHaveLength(200);
|
||||||
|
expect(tail.startSeq).toBe(99_801);
|
||||||
|
expect(tail.endSeq).toBe(100_000);
|
||||||
|
expect(blockingMs).toBeLessThan(50);
|
||||||
|
} finally {
|
||||||
|
clearImmediate(unrelatedWork);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it.each([false, true])(
|
it.each([false, true])(
|
||||||
"serves tail 200 from 100,000 retained rows without stalling the loop (wholeTurns=%s)",
|
"serves tail 200 from 100,000 retained rows without stalling the loop (wholeTurns=%s)",
|
||||||
async (wholeTurns) => {
|
async (wholeTurns) => {
|
||||||
const store = new InMemoryAgentTimelineStore();
|
const { result: tail, blockingMs } = await measureTimelineTail(wholeTurns);
|
||||||
store.initialize("large");
|
|
||||||
for (let index = 0; index < 100_000; index++) {
|
|
||||||
store.append("large", { type: "user_message", text: String(index) });
|
|
||||||
}
|
|
||||||
const { result: tail, blockingMs } = await measureMainThreadWork(
|
|
||||||
`timeline tail main-thread work (wholeTurns=${wholeTurns})`,
|
|
||||||
() => {
|
|
||||||
let page = store.fetch("large", { limit: 200, wholeTurns });
|
|
||||||
for (let repeat = 1; repeat < 8; repeat++)
|
|
||||||
page = store.fetch("large", { limit: 200, wholeTurns });
|
|
||||||
return page;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(tail.rows).toHaveLength(200);
|
expect(tail.rows).toHaveLength(200);
|
||||||
expect(tail.startSeq).toBe(99_801);
|
expect(tail.startSeq).toBe(99_801);
|
||||||
expect(tail.endSeq).toBe(100_000);
|
expect(tail.endSeq).toBe(100_000);
|
||||||
|
|||||||
@@ -184,17 +184,24 @@ test("repeated content is reused without rewriting the attachment", async () =>
|
|||||||
test("64 MiB materialization leaves the event loop responsive and preserves bytes", async () => {
|
test("64 MiB materialization leaves the event loop responsive and preserves bytes", async () => {
|
||||||
const bytes = Buffer.alloc(64 * 1024 * 1024, 37);
|
const bytes = Buffer.alloc(64 * 1024 * 1024, 37);
|
||||||
const data = bytes.toString("base64");
|
const data = bytes.toString("base64");
|
||||||
let previous = performance.now();
|
// Main-thread CPU time between ticks bounds the decode and hash work done on the loop;
|
||||||
|
// wall-clock gaps on shared CI runners also count preemption and timer granularity.
|
||||||
|
// File writes stay on fs/promises, so they never block the loop.
|
||||||
|
const threadCpuMs = () => {
|
||||||
|
const { user, system } = process.threadCpuUsage();
|
||||||
|
return (user + system) / 1000;
|
||||||
|
};
|
||||||
|
let previous = threadCpuMs();
|
||||||
let longest = 0;
|
let longest = 0;
|
||||||
const probe = setInterval(() => {
|
const probe = setInterval(() => {
|
||||||
const now = performance.now();
|
const now = threadCpuMs();
|
||||||
longest = Math.max(longest, now - previous);
|
longest = Math.max(longest, now - previous);
|
||||||
previous = now;
|
previous = now;
|
||||||
}, 1);
|
}, 1);
|
||||||
try {
|
try {
|
||||||
const result = await materializeProviderImage({ data, mimeType: "image/png" });
|
const result = await materializeProviderImage({ data, mimeType: "image/png" });
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||||
console.log(`64 MiB attachment longest loop gap: ${longest.toFixed(2)} ms`);
|
console.log(`64 MiB attachment longest blocking stretch: ${longest.toFixed(2)} ms CPU`);
|
||||||
expect(longest).toBeLessThan(50);
|
expect(longest).toBeLessThan(50);
|
||||||
expect(readFileSync(result.path).equals(bytes)).toBe(true);
|
expect(readFileSync(result.path).equals(bytes)).toBe(true);
|
||||||
rmSync(result.path, { force: true });
|
rmSync(result.path, { force: true });
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { setImmediate as nextTurn } from "node:timers/promises";
|
||||||
|
import { measureMainThreadWork } from "../../../test-utils/main-thread-work.js";
|
||||||
|
import { InMemoryAgentTimelineStore } from "../agent-timeline-store.js";
|
||||||
|
|
||||||
|
// Measure a cold fetch on its own thread, without Vitest reporting or callbacks.
|
||||||
|
const wholeTurns = process.argv[2] === "true";
|
||||||
|
const store = new InMemoryAgentTimelineStore();
|
||||||
|
store.initialize("large");
|
||||||
|
for (let index = 0; index < 100_000; index++) {
|
||||||
|
store.append("large", { type: "user_message", text: String(index) });
|
||||||
|
}
|
||||||
|
// Finish fixture allocation and collect its temporary garbage before measuring.
|
||||||
|
globalThis.gc!();
|
||||||
|
await nextTurn();
|
||||||
|
const measured = await measureMainThreadWork(
|
||||||
|
`timeline tail main-thread work (wholeTurns=${wholeTurns})`,
|
||||||
|
() => {
|
||||||
|
let page = store.fetch("large", { limit: 200, wholeTurns });
|
||||||
|
for (let repeat = 1; repeat < 8; repeat++)
|
||||||
|
page = store.fetch("large", { limit: 200, wholeTurns });
|
||||||
|
return page;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
process.stdout.write(JSON.stringify(measured));
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/website",
|
"name": "@getpaseo/website",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -486,3 +486,74 @@ diff --git a/node_modules/react-native-unistyles/lib/commonjs/core/withUnistyles
|
|||||||
...(narrowedProps.style ? {
|
...(narrowedProps.style ? {
|
||||||
style: styleSecrets.styles
|
style: styleSecrets.styles
|
||||||
} : {}),
|
} : {}),
|
||||||
|
diff --git a/node_modules/react-native-unistyles/lib/commonjs/web/runtime.js b/node_modules/react-native-unistyles/lib/commonjs/web/runtime.js
|
||||||
|
index 305fa49..5957aa5 100644
|
||||||
|
--- a/node_modules/react-native-unistyles/lib/commonjs/web/runtime.js
|
||||||
|
+++ b/node_modules/react-native-unistyles/lib/commonjs/web/runtime.js
|
||||||
|
@@ -35,10 +35,17 @@ class UnistylesRuntime {
|
||||||
|
return this.darkMedia;
|
||||||
|
}
|
||||||
|
get colorScheme() {
|
||||||
|
+ // Read the scheme through fresh lists, never through lightMedia/darkMedia. Chromium defers
|
||||||
|
+ // MediaQueryList change events while the document is hidden and drops the pending event
|
||||||
|
+ // of any list whose `matches` was read before the document became visible again, which
|
||||||
|
+ // would silence the adaptive-theme listeners registered on those two lists.
|
||||||
|
+ if ((0, _utils.isServer)()) {
|
||||||
|
+ return _types.ColorScheme.Unspecified;
|
||||||
|
+ }
|
||||||
|
switch (true) {
|
||||||
|
- case this.getLightMedia()?.matches:
|
||||||
|
+ case window.matchMedia('(prefers-color-scheme: light)').matches:
|
||||||
|
return _types.ColorScheme.Light;
|
||||||
|
- case this.getDarkMedia()?.matches:
|
||||||
|
+ case window.matchMedia('(prefers-color-scheme: dark)').matches:
|
||||||
|
return _types.ColorScheme.Dark;
|
||||||
|
default:
|
||||||
|
return _types.ColorScheme.Unspecified;
|
||||||
|
diff --git a/node_modules/react-native-unistyles/lib/module/web/runtime.js b/node_modules/react-native-unistyles/lib/module/web/runtime.js
|
||||||
|
index 7f13a06..c487bee 100644
|
||||||
|
--- a/node_modules/react-native-unistyles/lib/module/web/runtime.js
|
||||||
|
+++ b/node_modules/react-native-unistyles/lib/module/web/runtime.js
|
||||||
|
@@ -31,10 +31,17 @@ export class UnistylesRuntime {
|
||||||
|
return this.darkMedia;
|
||||||
|
}
|
||||||
|
get colorScheme() {
|
||||||
|
+ // Read the scheme through fresh lists, never through lightMedia/darkMedia. Chromium defers
|
||||||
|
+ // MediaQueryList change events while the document is hidden and drops the pending event
|
||||||
|
+ // of any list whose `matches` was read before the document became visible again, which
|
||||||
|
+ // would silence the adaptive-theme listeners registered on those two lists.
|
||||||
|
+ if (isServer()) {
|
||||||
|
+ return ColorScheme.Unspecified;
|
||||||
|
+ }
|
||||||
|
switch (true) {
|
||||||
|
- case this.getLightMedia()?.matches:
|
||||||
|
+ case window.matchMedia('(prefers-color-scheme: light)').matches:
|
||||||
|
return ColorScheme.Light;
|
||||||
|
- case this.getDarkMedia()?.matches:
|
||||||
|
+ case window.matchMedia('(prefers-color-scheme: dark)').matches:
|
||||||
|
return ColorScheme.Dark;
|
||||||
|
default:
|
||||||
|
return ColorScheme.Unspecified;
|
||||||
|
diff --git a/node_modules/react-native-unistyles/src/web/runtime.ts b/node_modules/react-native-unistyles/src/web/runtime.ts
|
||||||
|
index a16dac0..6519858 100644
|
||||||
|
--- a/node_modules/react-native-unistyles/src/web/runtime.ts
|
||||||
|
+++ b/node_modules/react-native-unistyles/src/web/runtime.ts
|
||||||
|
@@ -41,8 +41,16 @@ export class UnistylesRuntime {
|
||||||
|
get colorScheme() {
|
||||||
|
+ // Read the scheme through fresh lists, never through lightMedia/darkMedia. Chromium defers
|
||||||
|
+ // MediaQueryList change events while the document is hidden and drops the pending event
|
||||||
|
+ // of any list whose `matches` was read before the document became visible again, which
|
||||||
|
+ // would silence the adaptive-theme listeners registered on those two lists.
|
||||||
|
+ if (isServer()) {
|
||||||
|
+ return ColorScheme.Unspecified
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
switch (true) {
|
||||||
|
- case this.getLightMedia()?.matches:
|
||||||
|
+ case window.matchMedia('(prefers-color-scheme: light)').matches:
|
||||||
|
return ColorScheme.Light
|
||||||
|
- case this.getDarkMedia()?.matches:
|
||||||
|
+ case window.matchMedia('(prefers-color-scheme: dark)').matches:
|
||||||
|
return ColorScheme.Dark
|
||||||
|
default:
|
||||||
|
return ColorScheme.Unspecified
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@getpaseo/builtin-plugins",
|
"name": "@getpaseo/builtin-plugins",
|
||||||
"version": "0.14.0-beta.2",
|
"version": "0.14.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const patchedPackages = [
|
|||||||
},
|
},
|
||||||
// Registry cleanup: remove after react-native-unistyles ships
|
// Registry cleanup: remove after react-native-unistyles ships
|
||||||
// https://github.com/jpudysz/react-native-unistyles/pull/1203.
|
// https://github.com/jpudysz/react-native-unistyles/pull/1203.
|
||||||
|
// Adaptive themes: keep until colorScheme reads no longer consume listener-owned media changes.
|
||||||
// withUnistyles: also keep the uniProps filter until upstream consumes its control prop.
|
// withUnistyles: also keep the uniProps filter until upstream consumes its control prop.
|
||||||
{
|
{
|
||||||
nodeModulesPath: "node_modules/react-native-unistyles",
|
nodeModulesPath: "node_modules/react-native-unistyles",
|
||||||
|
|||||||
Reference in New Issue
Block a user