mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-26 11:12:53 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96020ab773 |
+4
-26
@@ -1,21 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## 0.13.2 - 2026-10-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed repeated Android animation exceptions for views that have already unmounted ([#7907](https://github.com/getpaseo/paseo/pull/7907))
|
||||
- Fixed the Android animation run loop continuing while the app is in the background ([#7914](https://github.com/getpaseo/paseo/pull/7914))
|
||||
- Fixed finished Android chat messages being remeasured during streaming updates ([#7914](https://github.com/getpaseo/paseo/pull/7914))
|
||||
- Fixed bottom sheets retaining window-size listeners after closing ([#7914](https://github.com/getpaseo/paseo/pull/7914))
|
||||
|
||||
## 0.13.1 - 2026-10-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a startup crash with Show background enabled and eight or more workspaces in a project ([#7896](https://github.com/getpaseo/paseo/pull/7896) by [@budivoogt](https://github.com/budivoogt))
|
||||
|
||||
## 0.13.0 - 2026-10-24
|
||||
## 0.13.0-beta.2 - 2026-10-23
|
||||
|
||||
Foldable phone layouts, agents that delegate work to other machines, plugin file panels, and plugin translations.
|
||||
|
||||
@@ -38,7 +23,7 @@ paseo host add 'relay://...' --label laptop
|
||||
|
||||
### Improved
|
||||
|
||||
- Changed sidebar workspaces to sort by Created, with Last prompt and Manual under Display → Sort ([#7635](https://github.com/getpaseo/paseo/pull/7635), [#7771](https://github.com/getpaseo/paseo/pull/7771))
|
||||
- Changed sidebar workspaces to sort by Created, with Last prompt under Display → Sort; workspaces no longer drag to reorder ([#7635](https://github.com/getpaseo/paseo/pull/7635))
|
||||
- Changed long project groups in the sidebar to show seven workspaces plus any that need attention or are working, with Show more for the rest ([#7635](https://github.com/getpaseo/paseo/pull/7635))
|
||||
- Changed Command Center agent and workspace results to show the same icons and status as tabs and the sidebar ([#7690](https://github.com/getpaseo/paseo/pull/7690))
|
||||
- Changed Manual metadata generation to use only the models you chose, without falling back to the defaults ([#7611](https://github.com/getpaseo/paseo/pull/7611))
|
||||
@@ -47,7 +32,6 @@ paseo host add 'relay://...' --label laptop
|
||||
- Changed Explorer tabs on phones to keep full labels and scroll, with Close moved to the long-press menu ([#7589](https://github.com/getpaseo/paseo/pull/7589))
|
||||
- Changed relay connections to require the daemon password when the daemon has one; re-add relay hosts saved without it ([#7668](https://github.com/getpaseo/paseo/pull/7668) by [@nickmaglowsch](https://github.com/nickmaglowsch))
|
||||
- Kept the sidebar open on phones when you cancel Search, Import session or Add project ([#7693](https://github.com/getpaseo/paseo/pull/7693))
|
||||
- Updated pinned ACP agents to their latest releases ([#7781](https://github.com/getpaseo/paseo/pull/7781) by [@JunWeiUp](https://github.com/JunWeiUp), [@LouisDeconinck](https://github.com/LouisDeconinck))
|
||||
- Removed the legacy terminal renderer; iOS and Android always use the native terminal renderer ([#7639](https://github.com/getpaseo/paseo/pull/7639))
|
||||
- Changed usage rows to say whether a percentage is used or remaining, in the app's language ([#7376](https://github.com/getpaseo/paseo/pull/7376) by [@michaelmwu](https://github.com/michaelmwu))
|
||||
- Changed an agent's report and its finish notification to reach the parent agent as one message ([#7561](https://github.com/getpaseo/paseo/pull/7561))
|
||||
@@ -64,11 +48,10 @@ paseo host add 'relay://...' --label laptop
|
||||
- Added translated plugin listings through `translations` in `paseo-plugin.json` and `OVERVIEW.zh-CN.md` ([#7373](https://github.com/getpaseo/paseo/pull/7373))
|
||||
- Added `Markdown` to `@getpaseo/plugin/client/ui` to render Markdown with Paseo's renderer ([#7579](https://github.com/getpaseo/paseo/pull/7579))
|
||||
- Added inline SVG and component icons to every plugin `icon` ([#7368](https://github.com/getpaseo/paseo/pull/7368))
|
||||
- Changed every plugin UI import to `@getpaseo/plugin/client/ui`; the previous entry point, including `useRevealedText`, still works and is deprecated ([#7474](https://github.com/getpaseo/paseo/pull/7474), [#7780](https://github.com/getpaseo/paseo/pull/7780) by [@luoph](https://github.com/luoph), [@RRD-dev](https://github.com/RRD-dev), [@mcowger](https://github.com/mcowger))
|
||||
- Changed every plugin UI import to `@getpaseo/plugin/client/ui`; the previous entry point still works and is deprecated ([#7474](https://github.com/getpaseo/paseo/pull/7474))
|
||||
- Deprecated the panel `context` and `agentId` props and `addWorkspacePanel`'s `context` field; use `params.agentId` and `addAgentPanel` ([#7685](https://github.com/getpaseo/paseo/pull/7685))
|
||||
- Changed turn-ended hooks to send an empty `timeline`; fetch recent history with `timeline.refetch` instead ([#7477](https://github.com/getpaseo/paseo/pull/7477))
|
||||
- Fixed plugin header buttons missing on phones and narrow windows; they now appear in the workspace ··· menu ([#7630](https://github.com/getpaseo/paseo/pull/7630))
|
||||
- Fixed reopened ACP plugin agents showing an empty chat ([#7717](https://github.com/getpaseo/paseo/pull/7717) by [@nistream](https://github.com/nistream))
|
||||
- Fixed notification timeline transformers failing to load with "invalid item type: notification" ([#7582](https://github.com/getpaseo/paseo/pull/7582) by [@Gavinchen92](https://github.com/Gavinchen92))
|
||||
|
||||
### Fixed
|
||||
@@ -83,11 +66,6 @@ paseo host add 'relay://...' --label laptop
|
||||
- Fixed workspace diff stats and merged-PR auto-archive missing updates while no app was showing the workspace ([#7681](https://github.com/getpaseo/paseo/pull/7681))
|
||||
- Fixed the `/` and `@` picker on iPhone covering the composer and hiding its best match behind the keyboard ([#7679](https://github.com/getpaseo/paseo/pull/7679) by [@masz77](https://github.com/masz77))
|
||||
- Fixed reopening a workspace marked unread on a phone leaving its agent stuck loading ([#7608](https://github.com/getpaseo/paseo/pull/7608))
|
||||
- Fixed a new workspace's pin or title disappearing from the sidebar until the next update ([#7758](https://github.com/getpaseo/paseo/pull/7758) by [@stv1024](https://github.com/stv1024))
|
||||
- Fixed Claude background command notifications appearing twice after a daemon restart ([#7737](https://github.com/getpaseo/paseo/pull/7737))
|
||||
- Fixed voice replies skipping later sentences with OpenAI-compatible speech servers ([#7756](https://github.com/getpaseo/paseo/pull/7756) by [@grueneisfabian](https://github.com/grueneisfabian))
|
||||
- Fixed Cursor usage showing bonus spend as overage instead of the quota percentages ([#7778](https://github.com/getpaseo/paseo/pull/7778) by [@kbitz](https://github.com/kbitz), [@stackia](https://github.com/stackia))
|
||||
- Fixed the Android tablet taskbar covering the sidebar's Add project, Hosts, Help and Settings ([#7768](https://github.com/getpaseo/paseo/pull/7768))
|
||||
- Fixed Command Center jumping back to the selected result while you scroll ([#7688](https://github.com/getpaseo/paseo/pull/7688))
|
||||
- Fixed Claude system context appearing as an assistant message in imported or restored chats ([#7590](https://github.com/getpaseo/paseo/pull/7590) by [@khevamann](https://github.com/khevamann), [@rogerluan](https://github.com/rogerluan))
|
||||
- Fixed saved workspace tabs being lost when leaving or reloading the app right after it loads ([#7547](https://github.com/getpaseo/paseo/pull/7547))
|
||||
@@ -95,7 +73,7 @@ paseo host add 'relay://...' --label laptop
|
||||
- Fixed Add image failing with "Failed to select image" on Android after changing the font size ([#7546](https://github.com/getpaseo/paseo/pull/7546) by [@RRD-dev](https://github.com/RRD-dev), [@idoyana](https://github.com/idoyana), [@luoph](https://github.com/luoph))
|
||||
- Fixed popovers, tooltips and context menus on Android opening one status bar below their trigger ([#7589](https://github.com/getpaseo/paseo/pull/7589))
|
||||
- Fixed the active Explorer tab being invisible in the light theme ([#7589](https://github.com/getpaseo/paseo/pull/7589))
|
||||
- Fixed Android showing "Host not found" for untrusted certificates, and TLS advice for hostnames that don't resolve ([#7429](https://github.com/getpaseo/paseo/pull/7429), [#7701](https://github.com/getpaseo/paseo/pull/7701))
|
||||
- Fixed Android showing "Host not found" instead of a TLS error for untrusted certificates ([#7429](https://github.com/getpaseo/paseo/pull/7429))
|
||||
- Fixed the keyboard covering focused Settings fields on Android ([#7362](https://github.com/getpaseo/paseo/pull/7362))
|
||||
- Fixed Pull or Push appearing twice in the compact Changes menu ([#7355](https://github.com/getpaseo/paseo/pull/7355) by [@az33zy](https://github.com/az33zy))
|
||||
- Fixed Jump to tab and Jump to workspace saving shortcuts that don't end in a number ([#7365](https://github.com/getpaseo/paseo/pull/7365))
|
||||
|
||||
+4
-26
@@ -1,21 +1,6 @@
|
||||
# 更新日志
|
||||
|
||||
## 0.13.2 - 2026-10-26
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复 Android 对已卸载视图反复抛出动画异常的问题 ([#7907](https://github.com/getpaseo/paseo/pull/7907))
|
||||
- 修复 Android 应用进入后台后动画运行循环仍持续执行的问题 ([#7914](https://github.com/getpaseo/paseo/pull/7914))
|
||||
- 修复 Android 在流式更新时反复测量已完成聊天消息的问题 ([#7914](https://github.com/getpaseo/paseo/pull/7914))
|
||||
- 修复底部面板关闭后仍保留窗口尺寸监听器的问题 ([#7914](https://github.com/getpaseo/paseo/pull/7914))
|
||||
|
||||
## 0.13.1 - 2026-10-26
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复开启“显示后台工作区”且项目包含至少八个工作区时的启动崩溃([#7896](https://github.com/getpaseo/paseo/pull/7896),由 [@budivoogt](https://github.com/budivoogt) 贡献)
|
||||
|
||||
## 0.13.0 - 2026-10-24
|
||||
## 0.13.0-beta.2 - 2026-10-23
|
||||
|
||||
折叠屏手机布局、可将工作委派给其他机器的 Agent、插件文件面板,以及插件翻译。
|
||||
|
||||
@@ -38,7 +23,7 @@ paseo host add 'relay://...' --label laptop
|
||||
|
||||
### 改进
|
||||
|
||||
- 侧边栏工作区改为按“创建时间”排序,可在“显示”→“排序”中选择“最近提示词”或“手动” ([#7635](https://github.com/getpaseo/paseo/pull/7635), [#7771](https://github.com/getpaseo/paseo/pull/7771))
|
||||
- 侧边栏工作区改为按“创建时间”排序,可在“显示”→“排序”中选择“最近提示词”;工作区不再支持拖动排序 ([#7635](https://github.com/getpaseo/paseo/pull/7635))
|
||||
- 侧边栏中较长的项目分组现在显示七个工作区,以及所有需要处理或正在运行的工作区,其余的可通过“显示更多”查看 ([#7635](https://github.com/getpaseo/paseo/pull/7635))
|
||||
- Command Center 中的 Agent 和工作区结果现在显示与标签页和侧边栏相同的图标和状态 ([#7690](https://github.com/getpaseo/paseo/pull/7690))
|
||||
- 手动元数据生成现在只使用你选择的模型,不再回退到默认模型 ([#7611](https://github.com/getpaseo/paseo/pull/7611))
|
||||
@@ -47,7 +32,6 @@ paseo host add 'relay://...' --label laptop
|
||||
- 手机上的 Explorer 标签页保留完整标签并可滚动,关闭操作移到了长按菜单中 ([#7589](https://github.com/getpaseo/paseo/pull/7589))
|
||||
- 如果 daemon 设置了密码,relay 连接现在需要该密码;请重新添加保存时未带密码的 relay 主机 ([#7668](https://github.com/getpaseo/paseo/pull/7668) by [@nickmaglowsch](https://github.com/nickmaglowsch))
|
||||
- 在手机上取消搜索、导入会话或添加项目时,侧边栏保持打开 ([#7693](https://github.com/getpaseo/paseo/pull/7693))
|
||||
- 已固定版本的 ACP Agent 更新到最新版本 ([#7781](https://github.com/getpaseo/paseo/pull/7781) by [@JunWeiUp](https://github.com/JunWeiUp), [@LouisDeconinck](https://github.com/LouisDeconinck))
|
||||
- 移除了旧版终端渲染器;iOS 和 Android 始终使用原生终端渲染器 ([#7639](https://github.com/getpaseo/paseo/pull/7639))
|
||||
- 用量行现在用应用的语言说明百分比是已用还是剩余 ([#7376](https://github.com/getpaseo/paseo/pull/7376) by [@michaelmwu](https://github.com/michaelmwu))
|
||||
- Agent 的报告和完成通知现在合并为一条消息发送给父 Agent ([#7561](https://github.com/getpaseo/paseo/pull/7561))
|
||||
@@ -64,11 +48,10 @@ paseo host add 'relay://...' --label laptop
|
||||
- 新增插件列表翻译,可通过 `paseo-plugin.json` 中的 `translations` 和 `OVERVIEW.zh-CN.md` 提供 ([#7373](https://github.com/getpaseo/paseo/pull/7373))
|
||||
- `@getpaseo/plugin/client/ui` 新增 `Markdown`,可使用 Paseo 的渲染器渲染 Markdown ([#7579](https://github.com/getpaseo/paseo/pull/7579))
|
||||
- 所有插件 `icon` 新增对内联 SVG 和组件图标的支持 ([#7368](https://github.com/getpaseo/paseo/pull/7368))
|
||||
- 所有插件 UI 导入改为 `@getpaseo/plugin/client/ui`;原入口(包括 `useRevealedText`)仍可使用,但已弃用 ([#7474](https://github.com/getpaseo/paseo/pull/7474), [#7780](https://github.com/getpaseo/paseo/pull/7780) by [@luoph](https://github.com/luoph), [@RRD-dev](https://github.com/RRD-dev), [@mcowger](https://github.com/mcowger))
|
||||
- 所有插件 UI 导入改为 `@getpaseo/plugin/client/ui`;原入口仍可使用,但已弃用 ([#7474](https://github.com/getpaseo/paseo/pull/7474))
|
||||
- 弃用面板的 `context` 和 `agentId` 属性,以及 `addWorkspacePanel` 的 `context` 字段;请改用 `params.agentId` 和 `addAgentPanel` ([#7685](https://github.com/getpaseo/paseo/pull/7685))
|
||||
- turn-ended hook 现在发送空的 `timeline`;请改用 `timeline.refetch` 获取最近的历史记录 ([#7477](https://github.com/getpaseo/paseo/pull/7477))
|
||||
- 修复了插件标题栏按钮在手机和窄窗口中不显示的问题;它们现在显示在工作区的 ··· 菜单中 ([#7630](https://github.com/getpaseo/paseo/pull/7630))
|
||||
- 修复了重新打开的 ACP 插件 Agent 聊天为空的问题 ([#7717](https://github.com/getpaseo/paseo/pull/7717) by [@nistream](https://github.com/nistream))
|
||||
- 修复了 notification 时间线转换器加载失败并报错 "invalid item type: notification" 的问题 ([#7582](https://github.com/getpaseo/paseo/pull/7582) by [@Gavinchen92](https://github.com/Gavinchen92))
|
||||
|
||||
### 修复
|
||||
@@ -83,11 +66,6 @@ paseo host add 'relay://...' --label laptop
|
||||
- 修复了没有应用显示工作区时,工作区 diff 统计和已合并 PR 的自动归档不更新的问题 ([#7681](https://github.com/getpaseo/paseo/pull/7681))
|
||||
- 修复了 iPhone 上 `/` 和 `@` 选择器遮挡输入框、并把最佳匹配项藏在键盘后面的问题 ([#7679](https://github.com/getpaseo/paseo/pull/7679) by [@masz77](https://github.com/masz77))
|
||||
- 修复了在手机上重新打开标记为未读的工作区后 Agent 一直加载的问题 ([#7608](https://github.com/getpaseo/paseo/pull/7608))
|
||||
- 修复了新工作区的置顶或标题在下次更新前从侧边栏消失的问题 ([#7758](https://github.com/getpaseo/paseo/pull/7758) by [@stv1024](https://github.com/stv1024))
|
||||
- 修复了 daemon 重启后 Claude 后台命令通知出现两次的问题 ([#7737](https://github.com/getpaseo/paseo/pull/7737))
|
||||
- 修复了使用 OpenAI 兼容语音服务时语音回复跳过后续句子的问题 ([#7756](https://github.com/getpaseo/paseo/pull/7756) by [@grueneisfabian](https://github.com/grueneisfabian))
|
||||
- 修复了 Cursor 用量将赠送额度消费显示为超额、而不是配额百分比的问题 ([#7778](https://github.com/getpaseo/paseo/pull/7778) by [@kbitz](https://github.com/kbitz), [@stackia](https://github.com/stackia))
|
||||
- 修复了 Android 平板任务栏遮挡侧边栏中添加项目、主机、帮助和设置的问题 ([#7768](https://github.com/getpaseo/paseo/pull/7768))
|
||||
- 修复了滚动时 Command Center 跳回所选结果的问题 ([#7688](https://github.com/getpaseo/paseo/pull/7688))
|
||||
- 修复了导入或恢复的聊天中 Claude 系统上下文显示为助手消息的问题 ([#7590](https://github.com/getpaseo/paseo/pull/7590) by [@khevamann](https://github.com/khevamann), [@rogerluan](https://github.com/rogerluan))
|
||||
- 修复了应用加载后立即离开或重新加载时已保存的工作区标签页丢失的问题 ([#7547](https://github.com/getpaseo/paseo/pull/7547))
|
||||
@@ -95,7 +73,7 @@ paseo host add 'relay://...' --label laptop
|
||||
- 修复了 Android 上更改字体大小后添加图片失败并提示“选择图片失败”的问题 ([#7546](https://github.com/getpaseo/paseo/pull/7546) by [@RRD-dev](https://github.com/RRD-dev), [@idoyana](https://github.com/idoyana), [@luoph](https://github.com/luoph))
|
||||
- 修复了 Android 上弹出框、提示和上下文菜单在触发元素下方偏移一个状态栏高度打开的问题 ([#7589](https://github.com/getpaseo/paseo/pull/7589))
|
||||
- 修复了浅色主题中当前 Explorer 标签页不可见的问题 ([#7589](https://github.com/getpaseo/paseo/pull/7589))
|
||||
- 修复了 Android 上不受信任的证书显示 "Host not found"、无法解析的主机名显示 TLS 建议的问题 ([#7429](https://github.com/getpaseo/paseo/pull/7429), [#7701](https://github.com/getpaseo/paseo/pull/7701))
|
||||
- 修复了 Android 上不受信任的证书显示 "Host not found" 而不是 TLS 错误的问题 ([#7429](https://github.com/getpaseo/paseo/pull/7429))
|
||||
- 修复了 Android 上键盘遮挡已聚焦的设置字段的问题 ([#7362](https://github.com/getpaseo/paseo/pull/7362))
|
||||
- 修复了紧凑 Changes 菜单中 Pull 或 Push 出现两次的问题 ([#7355](https://github.com/getpaseo/paseo/pull/7355) by [@az33zy](https://github.com/az33zy))
|
||||
- 修复了“跳转到标签”和“跳转到 workspace”可以保存不以数字结尾的快捷键的问题 ([#7365](https://github.com/getpaseo/paseo/pull/7365))
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
Foldable phone layouts, agents that delegate work to other machines, plugin file panels, and plugin translations.
|
||||
|
||||
Connect your daemon to another machine, then ask an agent to delegate work there:
|
||||
|
||||
`sh
|
||||
|
||||
paseo host add 192.168.1.20:6767 --label studio
|
||||
|
||||
paseo host add 'ssh://me@build-box' --label build-box
|
||||
|
||||
paseo host add 'relay://...' --label laptop
|
||||
|
||||
`
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -1,15 +0,0 @@
|
||||
Foldable phone layouts, agents that delegate work to other machines, plugin file panels, and plugin translations.
|
||||
|
||||
Connect your daemon to another machine, then ask an agent to delegate work there:
|
||||
|
||||
`sh
|
||||
|
||||
paseo host add 192.168.1.20:6767 --label studio
|
||||
|
||||
paseo host add 'ssh://me@build-box' --label build-box
|
||||
|
||||
paseo host add 'relay://...' --label laptop
|
||||
|
||||
`
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -1,15 +0,0 @@
|
||||
Foldable phone layouts, agents that delegate work to other machines, plugin file panels, and plugin translations.
|
||||
|
||||
Connect your daemon to another machine, then ask an agent to delegate work there:
|
||||
|
||||
`sh
|
||||
|
||||
paseo host add 192.168.1.20:6767 --label studio
|
||||
|
||||
paseo host add 'ssh://me@build-box' --label build-box
|
||||
|
||||
paseo host add 'relay://...' --label laptop
|
||||
|
||||
`
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -1,15 +0,0 @@
|
||||
Foldable phone layouts, agents that delegate work to other machines, plugin file panels, and plugin translations.
|
||||
|
||||
Connect your daemon to another machine, then ask an agent to delegate work there:
|
||||
|
||||
`sh
|
||||
|
||||
paseo host add 192.168.1.20:6767 --label studio
|
||||
|
||||
paseo host add 'ssh://me@build-box' --label build-box
|
||||
|
||||
paseo host add 'relay://...' --label laptop
|
||||
|
||||
`
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -1,4 +0,0 @@
|
||||
Fixed
|
||||
- Fixed a startup crash with Show background enabled and eight or more workspaces in a project
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -1,4 +0,0 @@
|
||||
Fixed
|
||||
- Fixed a startup crash with Show background enabled and eight or more workspaces in a project
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -1,4 +0,0 @@
|
||||
Fixed
|
||||
- Fixed a startup crash with Show background enabled and eight or more workspaces in a project
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -1,4 +0,0 @@
|
||||
Fixed
|
||||
- Fixed a startup crash with Show background enabled and eight or more workspaces in a project
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -1,7 +0,0 @@
|
||||
Fixed
|
||||
- Fixed repeated Android animation exceptions for views that have already unmounted
|
||||
- Fixed the Android animation run loop continuing while the app is in the background
|
||||
- Fixed finished Android chat messages being remeasured during streaming updates
|
||||
- Fixed bottom sheets retaining window-size listeners after closing
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -1,7 +0,0 @@
|
||||
Fixed
|
||||
- Fixed repeated Android animation exceptions for views that have already unmounted
|
||||
- Fixed the Android animation run loop continuing while the app is in the background
|
||||
- Fixed finished Android chat messages being remeasured during streaming updates
|
||||
- Fixed bottom sheets retaining window-size listeners after closing
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -1,7 +0,0 @@
|
||||
Fixed
|
||||
- Fixed repeated Android animation exceptions for views that have already unmounted
|
||||
- Fixed the Android animation run loop continuing while the app is in the background
|
||||
- Fixed finished Android chat messages being remeasured during streaming updates
|
||||
- Fixed bottom sheets retaining window-size listeners after closing
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
@@ -1,7 +0,0 @@
|
||||
Fixed
|
||||
- Fixed repeated Android animation exceptions for views that have already unmounted
|
||||
- Fixed the Android animation run loop continuing while the app is in the background
|
||||
- Fixed finished Android chat messages being remeasured during streaming updates
|
||||
- Fixed bottom sheets retaining window-size listeners after closing
|
||||
|
||||
Full notes: https://paseo.sh/changelog
|
||||
Generated
+70
-37
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paseo",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
@@ -30269,9 +30269,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-reanimated": {
|
||||
"version": "4.5.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.5.tgz",
|
||||
"integrity": "sha512-xtJXZRZ1vkec1AIUVG02St2sbyZS5jc7227TlDu/HonqUYPeZAfiC4RI2jqL1dVMNBu1+7oBx/c9QkjF2kg6Lg==",
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.5.1.tgz",
|
||||
"integrity": "sha512-RnMvtDuR+68ig864gAvZCOdZehqhC5rFmMo0kn+ARfgVSTvFeF6IFLBVgMPUu0KwihaapEyW24WRi6nEyy1kSA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-native-is-edge-to-edge": "^1.3.1",
|
||||
@@ -30280,7 +30280,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "0.83 - 0.86",
|
||||
"react-native-worklets": "0.10.x - 0.11.x"
|
||||
"react-native-worklets": "0.10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-safe-area-context": {
|
||||
@@ -30406,9 +30406,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-worklets": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.4.tgz",
|
||||
"integrity": "sha512-gbk0CxMrGywEp3SJAHO+2JVaCWtl4WyAYBIxLZy6Y113+mHHuq3FuT6iV+VcaBuJzuuN34mtdZ4j5JIlBDzmeA==",
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.10.1.tgz",
|
||||
"integrity": "sha512-62mRM19bDpfpdI8HLkEErcdOsrAPDtE9lA/sw+5lLRpzBHNhxaoj9QyY2KjXqUmirelxkX4zuPGTC3VdA0feJA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-arrow-functions": "^7.27.1",
|
||||
@@ -35695,7 +35695,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "6.10.4",
|
||||
"@codemirror/language": "6.12.4",
|
||||
@@ -35780,14 +35780,14 @@
|
||||
"react-native-gesture-handler": "~2.32.0",
|
||||
"react-native-keyboard-controller": "1.21.12",
|
||||
"react-native-nitro-modules": "0.35.5",
|
||||
"react-native-reanimated": "4.5.5",
|
||||
"react-native-reanimated": "4.5.1",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "~4.26.0",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-unistyles": "^3.2.4",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-webview": "13.16.1",
|
||||
"react-native-worklets": "0.10.4",
|
||||
"react-native-worklets": "0.10.1",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"turndown": "^7.2.4",
|
||||
"turndown-plugin-gfm": "^1.0.2",
|
||||
@@ -37961,12 +37961,12 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/server": "0.13.2",
|
||||
"@getpaseo/client": "0.13.0-beta.2",
|
||||
"@getpaseo/protocol": "0.13.0-beta.2",
|
||||
"@getpaseo/server": "0.13.0-beta.2",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
@@ -38213,10 +38213,10 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@getpaseo/client",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/relay": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.0-beta.2",
|
||||
"@getpaseo/relay": "0.13.0-beta.2",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
@@ -38228,7 +38228,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@getpaseo/cli": "*",
|
||||
@@ -38472,7 +38472,7 @@
|
||||
},
|
||||
"packages/expo-two-way-audio": {
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
@@ -41012,6 +41012,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/expo-two-way-audio/node_modules/expo-module-scripts/node_modules/react-native-worklets": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.13.0.tgz",
|
||||
"integrity": "sha512-1OrhAeAKaZF4XrBa4YbPQbKzNOpjh52HDlId8e6ete1OCHNdAlrQHzG4gznJ225EAj3dgQXHViJvK51M9B52jQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.27.1",
|
||||
"@babel/plugin-transform-arrow-functions": "^7.27.1",
|
||||
"@babel/plugin-transform-class-properties": "^7.28.6",
|
||||
"@babel/plugin-transform-classes": "^7.28.6",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
|
||||
"@babel/plugin-transform-optional-chaining": "^7.28.6",
|
||||
"@babel/plugin-transform-shorthand-properties": "^7.27.1",
|
||||
"@babel/plugin-transform-template-literals": "^7.27.1",
|
||||
"@babel/plugin-transform-unicode-regex": "^7.27.1",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"semver": "^7.7.4",
|
||||
"whatwg-fetch": "^3.6.20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "*",
|
||||
"@react-native/metro-config": "*",
|
||||
"react": "*",
|
||||
"react-native": "0.86 - 0.88"
|
||||
}
|
||||
},
|
||||
"packages/expo-two-way-audio/node_modules/expo-module-scripts/node_modules/react-native/node_modules/pretty-format": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
||||
@@ -42091,7 +42124,7 @@
|
||||
},
|
||||
"packages/highlight": {
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "6.12.4",
|
||||
"@codemirror/legacy-modes": "^6.5.3",
|
||||
@@ -42120,15 +42153,15 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@getpaseo/plugin",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^1.4.0",
|
||||
"i18next": "^26.3.0",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/client": "0.13.0-beta.2",
|
||||
"@getpaseo/protocol": "0.13.0-beta.2",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/react": "~19.2.0",
|
||||
"react": "19.2.3",
|
||||
@@ -42138,8 +42171,8 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/client": "0.13.0-beta.2",
|
||||
"@getpaseo/protocol": "0.13.0-beta.2",
|
||||
"react": ">=19.1.0 <20",
|
||||
"react-native": ">=0.81.5",
|
||||
"zod": "^4.4.3"
|
||||
@@ -42161,7 +42194,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"dependencies": {
|
||||
"semver": "^7.8.5",
|
||||
"zod": "^4.4.3"
|
||||
@@ -42188,7 +42221,7 @@
|
||||
},
|
||||
"packages/relay": {
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"dependencies": {
|
||||
"@noble/ciphers": "^2.4.0",
|
||||
"@noble/curves": "^2.4.0",
|
||||
@@ -42409,16 +42442,16 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.246",
|
||||
"@anthropic-ai/sdk": "^0.104.2",
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/highlight": "0.13.2",
|
||||
"@getpaseo/plugin": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/relay": "0.13.2",
|
||||
"@getpaseo/client": "0.13.0-beta.2",
|
||||
"@getpaseo/highlight": "0.13.0-beta.2",
|
||||
"@getpaseo/plugin": "0.13.0-beta.2",
|
||||
"@getpaseo/protocol": "0.13.0-beta.2",
|
||||
"@getpaseo/relay": "0.13.0-beta.2",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
@@ -43339,7 +43372,7 @@
|
||||
},
|
||||
"packages/website": {
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.29.1",
|
||||
"@cloudflare/workers-types": "^4.20260317.1",
|
||||
@@ -44949,7 +44982,7 @@
|
||||
},
|
||||
"plugins": {
|
||||
"name": "@getpaseo/builtin-plugins",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"dependencies": {
|
||||
"@getpaseo/plugin": "*",
|
||||
"zod": "^4.4.3"
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"private": true,
|
||||
"description": "Paseo: voice-controlled development environment for local AI coding agents",
|
||||
"keywords": [
|
||||
@@ -154,7 +154,7 @@
|
||||
"react-dom": "19.2.3",
|
||||
"react-native": "0.86.3",
|
||||
"react-native-gesture-handler": "~2.32.0",
|
||||
"react-native-reanimated": "4.5.5",
|
||||
"react-native-worklets": "0.10.4"
|
||||
"react-native-reanimated": "4.5.1",
|
||||
"react-native-worklets": "0.10.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,6 @@ async function pressSettingsToggleShortcut(page: import("@playwright/test").Page
|
||||
await page.keyboard.press(`${modifier}+Comma`);
|
||||
}
|
||||
|
||||
async function openComposerTab(page: import("@playwright/test").Page, agentId: string) {
|
||||
await page
|
||||
.getByTestId(`workspace-tab-agent_${agentId}`)
|
||||
.filter({ visible: true })
|
||||
.first()
|
||||
.click();
|
||||
await expectAgentTabActive(page, agentId);
|
||||
await expect(page.getByRole("textbox", { name: "Message agent..." })).toBeVisible();
|
||||
}
|
||||
|
||||
interface ComposerFrame {
|
||||
at: number;
|
||||
visible: boolean;
|
||||
@@ -37,9 +27,7 @@ async function startComposerFrameCapture(page: import("@playwright/test").Page)
|
||||
|
||||
const sample = () => {
|
||||
if (capture.stopped) return;
|
||||
const root = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('[data-testid="message-input-root"]'),
|
||||
).find((candidate) => candidate.offsetParent !== null);
|
||||
const root = document.querySelector<HTMLElement>('[data-testid="message-input-root"]');
|
||||
const input = root?.querySelector<HTMLTextAreaElement>("textarea");
|
||||
if (root && input) {
|
||||
const style = getComputedStyle(input);
|
||||
@@ -190,8 +178,6 @@ test.describe("Settings toggle tab regression", () => {
|
||||
await openWorkspaceWithAgents(page, [firstAgent, secondAgent]);
|
||||
await waitForTabBar(page);
|
||||
|
||||
await openComposerTab(page, firstAgent.id);
|
||||
await openComposerTab(page, secondAgent.id);
|
||||
const input = page.getByRole("textbox", { name: "Message agent..." });
|
||||
await input.fill("Keep this short draft in the composer");
|
||||
const root = page.getByTestId("message-input-root").filter({ visible: true }).first();
|
||||
@@ -208,7 +194,6 @@ test.describe("Settings toggle tab regression", () => {
|
||||
|
||||
const frames = await stopComposerFrameCapture(page);
|
||||
const visibleFrames = frames.filter((frame) => frame.visible);
|
||||
expect(visibleFrames.length, "Capture must observe the visible composer").toBeGreaterThan(0);
|
||||
const tallestIndex = visibleFrames.reduce(
|
||||
(index, frame, candidateIndex) =>
|
||||
frame.rootHeight > visibleFrames[index].rootHeight ? candidateIndex : index,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
@@ -126,14 +126,14 @@
|
||||
"react-native-gesture-handler": "~2.32.0",
|
||||
"react-native-keyboard-controller": "1.21.12",
|
||||
"react-native-nitro-modules": "0.35.5",
|
||||
"react-native-reanimated": "4.5.5",
|
||||
"react-native-reanimated": "4.5.1",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "~4.26.0",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-unistyles": "^3.2.4",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-webview": "13.16.1",
|
||||
"react-native-worklets": "0.10.4",
|
||||
"react-native-worklets": "0.10.1",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"turndown": "^7.2.4",
|
||||
"turndown-plugin-gfm": "^1.0.2",
|
||||
@@ -198,14 +198,6 @@
|
||||
],
|
||||
"enableVideo": false
|
||||
},
|
||||
"expo": {
|
||||
"install": {
|
||||
"exclude": [
|
||||
"react-native-reanimated",
|
||||
"react-native-worklets"
|
||||
]
|
||||
}
|
||||
},
|
||||
"reanimated": {
|
||||
"staticFeatureFlags": {
|
||||
"FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS": false
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { createWorkspaceVisibilityPolicy } from "@/components/sidebar/sidebar-group-visibility";
|
||||
import { useLimitedSidebarGroup } from "@/components/sidebar/use-limited-sidebar-group";
|
||||
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
||||
import type { WorkspaceScriptPayload } from "@getpaseo/protocol/messages";
|
||||
@@ -487,20 +486,6 @@ describe("sidebar group visibility", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("treats placements without a hydrated entry as Done", () => {
|
||||
const items = entries([...Array(8).fill("done"), "running", "done"]);
|
||||
const hydrated = new Map(items.slice(0, 9).map((item) => [item.workspaceKey, item]));
|
||||
for (const entriesByKey of [new Map(), hydrated]) {
|
||||
const { result } = renderHook(() =>
|
||||
useLimitedSidebarGroup(items, createWorkspaceVisibilityPolicy(entriesByKey)),
|
||||
);
|
||||
expect(result.current.canToggle).toBe(true);
|
||||
expect(names(result.current.visibleItems)).toEqual(
|
||||
entriesByKey.size === 0 ? names(items.slice(0, 7)) : [...names(items.slice(0, 7)), "row-9"],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("shows eight active rows once with no redundant toggle", () => {
|
||||
const items = entries([
|
||||
"running",
|
||||
|
||||
@@ -99,7 +99,6 @@ import {
|
||||
import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction";
|
||||
import { SidebarSectionHeader } from "@/components/sidebar/sidebar-section-header";
|
||||
import { SidebarGroupToggleRow } from "@/components/sidebar/sidebar-group-toggle-row";
|
||||
import { createWorkspaceVisibilityPolicy } from "@/components/sidebar/sidebar-group-visibility";
|
||||
import { useLimitedSidebarGroup } from "@/components/sidebar/use-limited-sidebar-group";
|
||||
import {
|
||||
SidebarWorkspaceRowFrame,
|
||||
@@ -1594,7 +1593,11 @@ function ProjectBlock({
|
||||
onToggleWorkspacePin: ToggleSidebarWorkspacePin;
|
||||
}) {
|
||||
const visibilityPolicy = useMemo(
|
||||
() => createWorkspaceVisibilityPolicy<SidebarWorkspacePlacement>(workspaceEntriesByKey),
|
||||
() => ({
|
||||
kind: "workspaces" as const,
|
||||
getStatusBucket: (workspace: SidebarWorkspacePlacement) =>
|
||||
workspaceEntriesByKey.get(workspace.workspaceKey)!.statusBucket,
|
||||
}),
|
||||
[workspaceEntriesByKey],
|
||||
);
|
||||
const {
|
||||
|
||||
@@ -4,18 +4,6 @@ export type SidebarGroupVisibilityPolicy<T> =
|
||||
| { kind: "pinned" }
|
||||
| { kind: "workspaces"; getStatusBucket: (item: T) => SidebarStateBucket };
|
||||
|
||||
// A placement can outrun its entry: a retained sidebar that is inactive (compact layout with the
|
||||
// sidebar closed) or still waiting for sessions keeps an empty entry map. A missing entry renders
|
||||
// no row, so it counts as Done and claims no slot past the limit.
|
||||
export function createWorkspaceVisibilityPolicy<T extends { workspaceKey: string }>(
|
||||
entriesByKey: ReadonlyMap<string, { statusBucket: SidebarStateBucket }>,
|
||||
): SidebarGroupVisibilityPolicy<T> {
|
||||
return {
|
||||
kind: "workspaces",
|
||||
getStatusBucket: (item) => entriesByKey.get(item.workspaceKey)?.statusBucket ?? "done",
|
||||
};
|
||||
}
|
||||
|
||||
export function selectSidebarGroupVisibility<T>(
|
||||
items: readonly T[],
|
||||
expanded: boolean,
|
||||
|
||||
@@ -50,9 +50,9 @@ import {
|
||||
import {
|
||||
TERMINAL_GESTURE_LONG_PRESS_MS,
|
||||
TERMINAL_GESTURE_TAP_TOLERANCE_PX,
|
||||
advanceTerminalScrollGesture,
|
||||
classifyTerminalGestureIntent,
|
||||
resolveTerminalGestureReleaseAction,
|
||||
routeTerminalGestureRelease,
|
||||
routeTerminalScrollGesture,
|
||||
type TerminalGestureIntent,
|
||||
} from "../terminal/native-renderer/terminal-selection-gesture";
|
||||
import { resolveNativeTerminalMountAction } from "../terminal/native-renderer/terminal-mount-policy";
|
||||
@@ -95,6 +95,7 @@ interface NativeTerminalPanState {
|
||||
didNavigate: boolean;
|
||||
movedBeyondTapTolerance: boolean;
|
||||
intent: TerminalGestureIntent | null;
|
||||
origin: { x: number; y: number };
|
||||
}
|
||||
|
||||
interface NativeTerminalSelectionPress {
|
||||
@@ -214,6 +215,7 @@ function NativeTerminalEmulator({
|
||||
didNavigate: false,
|
||||
movedBeyondTapTolerance: false,
|
||||
intent: null,
|
||||
origin: { x: 0, y: 0 },
|
||||
});
|
||||
const inputModeTrackerRef = useRef(new TerminalInputModeTracker());
|
||||
const lastInputModeStateRef = useRef<TerminalInputModeState>({
|
||||
@@ -775,31 +777,33 @@ function NativeTerminalEmulator({
|
||||
);
|
||||
|
||||
const handlePanResponderScroll = useCallback(
|
||||
(currentDy: number) => {
|
||||
const cellHeight = metricsRef.current?.cellHeight;
|
||||
if (!cellHeight || cellHeight <= 0) {
|
||||
(gesture: PanResponderGestureState) => {
|
||||
const metrics = metricsRef.current;
|
||||
const viewport = resolveSelectionViewport();
|
||||
if (!metrics || metrics.cellHeight <= 0 || !viewport) {
|
||||
return;
|
||||
}
|
||||
const panState = panStateRef.current;
|
||||
const update = advanceTerminalScrollGesture({
|
||||
const { state, route } = routeTerminalScrollGesture({
|
||||
state: panState,
|
||||
currentDy,
|
||||
cellHeight,
|
||||
currentDy: gesture.dy,
|
||||
point: { x: panState.origin.x + gesture.dx, y: panState.origin.y + gesture.dy },
|
||||
metrics,
|
||||
viewport,
|
||||
inputMode: inputModeTrackerRef.current,
|
||||
});
|
||||
panState.lastDy = update.state.lastDy;
|
||||
panState.rowRemainder = update.state.rowRemainder;
|
||||
panState.didScroll = update.state.didScroll;
|
||||
const { rowDelta } = update;
|
||||
if (rowDelta === 0) {
|
||||
panState.lastDy = state.lastDy;
|
||||
panState.rowRemainder = state.rowRemainder;
|
||||
panState.didScroll = state.didScroll;
|
||||
if (route.kind === "scrollback") {
|
||||
applyScroll(route.direction, route.rows);
|
||||
return;
|
||||
}
|
||||
if (rowDelta > 0) {
|
||||
applyScroll("up", rowDelta);
|
||||
return;
|
||||
if (route.kind === "input") {
|
||||
callbacksRef.current.onInput?.(route.data);
|
||||
}
|
||||
applyScroll("down", Math.abs(rowDelta));
|
||||
},
|
||||
[applyScroll],
|
||||
[applyScroll, resolveSelectionViewport],
|
||||
);
|
||||
|
||||
const panResponder = useMemo(
|
||||
@@ -828,6 +832,10 @@ function NativeTerminalEmulator({
|
||||
return intent === "scroll" || intent === "select";
|
||||
},
|
||||
onPanResponderGrant: (event) => {
|
||||
const point = {
|
||||
x: event.nativeEvent.locationX,
|
||||
y: event.nativeEvent.locationY,
|
||||
};
|
||||
panStateRef.current = {
|
||||
lastDy: 0,
|
||||
rowRemainder: 0,
|
||||
@@ -835,10 +843,7 @@ function NativeTerminalEmulator({
|
||||
didNavigate: false,
|
||||
movedBeyondTapTolerance: false,
|
||||
intent: null,
|
||||
};
|
||||
const point = {
|
||||
x: event.nativeEvent.locationX,
|
||||
y: event.nativeEvent.locationY,
|
||||
origin: point,
|
||||
};
|
||||
const pressStatus = selectionRangeRef.current ? "selecting" : "pressing";
|
||||
selectionPressRef.current = {
|
||||
@@ -875,7 +880,7 @@ function NativeTerminalEmulator({
|
||||
}
|
||||
|
||||
if (panState.intent === "scroll") {
|
||||
handlePanResponderScroll(gesture.dy);
|
||||
handlePanResponderScroll(gesture);
|
||||
return;
|
||||
}
|
||||
if (panState.didNavigate) {
|
||||
@@ -903,7 +908,7 @@ function NativeTerminalEmulator({
|
||||
|
||||
if (intent === "scroll") {
|
||||
selectionPressRef.current = null;
|
||||
handlePanResponderScroll(gesture.dy);
|
||||
handlePanResponderScroll(gesture);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -932,8 +937,9 @@ function NativeTerminalEmulator({
|
||||
didNavigate: false,
|
||||
movedBeyondTapTolerance: false,
|
||||
intent: null,
|
||||
origin: { x: 0, y: 0 },
|
||||
};
|
||||
const releaseAction = resolveTerminalGestureReleaseAction({
|
||||
const { action: releaseAction, mouseInput } = routeTerminalGestureRelease({
|
||||
status: selectionPress?.status ?? "idle",
|
||||
startedWithSelection: selectionPress?.startedWithSelection,
|
||||
didScroll,
|
||||
@@ -942,11 +948,18 @@ function NativeTerminalEmulator({
|
||||
pressDurationMs: selectionPress ? Date.now() - selectionPress.startedAt : 0,
|
||||
longPressMs: TERMINAL_GESTURE_LONG_PRESS_MS,
|
||||
scrollMode: terminalScreenRef.current?.scroll.mode ?? "following",
|
||||
point: { x: selectionPress?.startX ?? 0, y: selectionPress?.startY ?? 0 },
|
||||
metrics: metricsRef.current,
|
||||
viewport: resolveSelectionViewport(),
|
||||
inputMode: inputModeTrackerRef.current,
|
||||
});
|
||||
if (releaseAction === "select" && selectionPress) {
|
||||
beginSelectionAtPoint({ x: selectionPress.startX, y: selectionPress.startY });
|
||||
return;
|
||||
}
|
||||
if (mouseInput !== null) {
|
||||
callbacksRef.current.onInput?.(mouseInput);
|
||||
}
|
||||
if (releaseAction === "focus" || releaseAction === "clear") {
|
||||
focusTerminalFromTap();
|
||||
}
|
||||
@@ -961,6 +974,7 @@ function NativeTerminalEmulator({
|
||||
didNavigate: false,
|
||||
movedBeyondTapTolerance: false,
|
||||
intent: null,
|
||||
origin: { x: 0, y: 0 },
|
||||
};
|
||||
},
|
||||
}),
|
||||
@@ -969,6 +983,7 @@ function NativeTerminalEmulator({
|
||||
clearSelectionLongPressTimeout,
|
||||
focusTerminalFromTap,
|
||||
handlePanResponderScroll,
|
||||
resolveSelectionViewport,
|
||||
updateSelectionAtPoint,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -58,7 +58,6 @@ vi.mock("@gorhom/bottom-sheet", async () => {
|
||||
});
|
||||
|
||||
import { IsolatedBottomSheetModal, type ContextBridge } from ".";
|
||||
import { useAnimatedLayout } from "@gorhom/bottom-sheet/src/hooks/useAnimatedLayout";
|
||||
import { useIsInsideBottomSheet } from "@/components/ui/bottom-sheet-scope";
|
||||
|
||||
/** Renders whatever the portal is currently holding, from its own place in the tree. */
|
||||
@@ -201,35 +200,3 @@ describe("IsolatedBottomSheetModal context bridging", () => {
|
||||
).toEqual(["false", "true"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bottom sheet layout subscription", () => {
|
||||
it("stops updating a dismissed sheet when the window resizes", () => {
|
||||
const container = document.createElement("div");
|
||||
const root = createRoot(container);
|
||||
let layout: ReturnType<typeof useAnimatedLayout> | undefined;
|
||||
const viewport = document.documentElement;
|
||||
const originalWidth = Object.getOwnPropertyDescriptor(viewport, "clientWidth");
|
||||
function resize(width: number) {
|
||||
Object.defineProperty(viewport, "clientWidth", { configurable: true, value: width });
|
||||
act(() => window.dispatchEvent(new Event("resize")));
|
||||
}
|
||||
function LayoutProbe() {
|
||||
layout = useAnimatedLayout(undefined, 0, 0);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
act(() => root.render(<LayoutProbe />));
|
||||
if (!layout) throw new Error("Sheet layout did not mount");
|
||||
const mountedLayout = layout;
|
||||
resize(640);
|
||||
expect(mountedLayout.get().window.width).toBe(640);
|
||||
act(() => root.render(null));
|
||||
resize(960);
|
||||
expect(mountedLayout.get().window.width).toBe(640);
|
||||
} finally {
|
||||
act(() => root.unmount());
|
||||
if (originalWidth) Object.defineProperty(viewport, "clientWidth", originalWidth);
|
||||
else Reflect.deleteProperty(viewport, "clientWidth");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,10 +38,10 @@ const CATALOG_DATA = [
|
||||
title: "Auggie CLI",
|
||||
description:
|
||||
"Augment Code's powerful software agent, backed by industry-leading context engine",
|
||||
version: "0.36.0",
|
||||
version: "0.33.0",
|
||||
iconId: "auggie",
|
||||
installLink: "https://www.augmentcode.com/",
|
||||
command: ["npx", "-y", "@augmentcode/auggie@0.36.0", "--acp"],
|
||||
command: ["npx", "-y", "@augmentcode/auggie@0.33.0", "--acp"],
|
||||
env: {
|
||||
AUGMENT_DISABLE_AUTO_UPDATE: "1",
|
||||
},
|
||||
@@ -60,10 +60,10 @@ const CATALOG_DATA = [
|
||||
title: "Cline",
|
||||
description:
|
||||
"Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
version: "3.0.73",
|
||||
version: "3.0.46",
|
||||
iconId: "cline",
|
||||
installLink: "https://cline.bot/cli",
|
||||
command: ["npx", "-y", "cline@3.0.73", "--acp"],
|
||||
command: ["npx", "-y", "cline@3.0.46", "--acp"],
|
||||
},
|
||||
{
|
||||
id: "codebuddy-code",
|
||||
@@ -123,10 +123,10 @@ const CATALOG_DATA = [
|
||||
id: "deepagents",
|
||||
title: "DeepAgents",
|
||||
description: "Batteries-included AI coding and general purpose agent powered by LangChain.",
|
||||
version: "0.1.36",
|
||||
version: "0.1.20",
|
||||
iconId: "deepagents",
|
||||
installLink: "https://docs.langchain.com/oss/javascript/deepagents/overview",
|
||||
command: ["npx", "-y", "deepagents-acp@0.1.36"],
|
||||
command: ["npx", "-y", "deepagents-acp@0.1.20"],
|
||||
},
|
||||
{
|
||||
id: "devin",
|
||||
@@ -141,29 +141,29 @@ const CATALOG_DATA = [
|
||||
id: "dimcode",
|
||||
title: "DimCode",
|
||||
description: "A coding agent that puts leading models at your command.",
|
||||
version: "0.5.21",
|
||||
version: "0.2.36",
|
||||
iconId: "dimcode",
|
||||
installLink: "https://dimcode.dev/docs/acp.html",
|
||||
command: ["npx", "-y", "dimcode@0.5.21", "acp"],
|
||||
command: ["npx", "-y", "dimcode@0.2.36", "acp"],
|
||||
},
|
||||
{
|
||||
id: "dirac",
|
||||
title: "Dirac",
|
||||
description:
|
||||
"Reduces API costs by more than 50%, produces better and faster work. Uses Hash anchored parallel edits, AST manipulation and a whole lot of neat optimizations. Fully Open Source.",
|
||||
version: "0.5.23",
|
||||
version: "0.4.22",
|
||||
iconId: "dirac",
|
||||
installLink: "https://dirac.run",
|
||||
command: ["npx", "-y", "dirac-cli@0.5.23", "--acp"],
|
||||
command: ["npx", "-y", "dirac-cli@0.4.22", "--acp"],
|
||||
},
|
||||
{
|
||||
id: "factory-droid",
|
||||
title: "Factory Droid",
|
||||
description: "Factory Droid - AI coding agent powered by Factory AI",
|
||||
version: "0.249.0",
|
||||
version: "0.179.0",
|
||||
iconId: "factory-droid",
|
||||
installLink: "https://factory.ai/product/cli",
|
||||
command: ["npx", "-y", "droid@0.249.0", "exec", "--output-format", "acp-daemon"],
|
||||
command: ["npx", "-y", "droid@0.179.0", "exec", "--output-format", "acp-daemon"],
|
||||
env: {
|
||||
DROID_DISABLE_AUTO_UPDATE: "true",
|
||||
FACTORY_DROID_AUTO_UPDATE_ENABLED: "false",
|
||||
@@ -174,19 +174,19 @@ const CATALOG_DATA = [
|
||||
id: "fast-agent",
|
||||
title: "fast-agent",
|
||||
description: "Code and build agents with comprehensive multi-provider support",
|
||||
version: "0.10.47",
|
||||
version: "0.9.22",
|
||||
iconId: "fast-agent",
|
||||
installLink: "https://fast-agent.ai/acp/",
|
||||
command: ["uvx", "--from", "fast-agent-acp==0.10.47", "fast-agent-acp", "-x"],
|
||||
command: ["uvx", "--from", "fast-agent-acp==0.9.22", "fast-agent-acp", "-x"],
|
||||
},
|
||||
{
|
||||
id: "gemini",
|
||||
title: "Gemini CLI",
|
||||
description: "Google's official CLI for Gemini",
|
||||
version: "0.65.0",
|
||||
version: "0.52.0",
|
||||
iconId: "gemini",
|
||||
installLink: "https://geminicli.com",
|
||||
command: ["npx", "-y", "@google/gemini-cli@0.65.0", "--acp"],
|
||||
command: ["npx", "-y", "@google/gemini-cli@0.52.0", "--acp"],
|
||||
},
|
||||
{
|
||||
id: "gjc",
|
||||
@@ -205,11 +205,11 @@ const CATALOG_DATA = [
|
||||
id: "glm-acp-agent",
|
||||
title: "GLM Agent",
|
||||
description:
|
||||
"ACP agent powered by Zhipu AI's GLM Coding Plan models (glm-5.3, glm-5.3-flash, glm-5-turbo, glm-4.7). Supports streaming, tool calls, mid-session model switching, image input via Z.AI Coding Plan Vision MCP, and session load/fork/resume with on-disk persistence.",
|
||||
version: "1.15.0",
|
||||
"ACP agent powered by Zhipu AI's GLM Coding Plan models (glm-5.1, glm-5-turbo, glm-4.7, glm-4.5-air). Supports streaming, tool calls, mid-session model switching, image input via Z.AI Coding Plan Vision MCP, and session load/fork/resume with on-disk persistence.",
|
||||
version: "1.3.0",
|
||||
iconId: "glm-acp-agent",
|
||||
installLink: "https://github.com/stefandevo/glm-acp-agent",
|
||||
command: ["npx", "-y", "glm-acp-agent@1.15.0"],
|
||||
command: ["npx", "-y", "glm-acp-agent@1.3.0"],
|
||||
},
|
||||
{
|
||||
id: "goose",
|
||||
@@ -289,20 +289,20 @@ const CATALOG_DATA = [
|
||||
id: "minimax-code",
|
||||
title: "MiniMax Code",
|
||||
description: "MiniMax's coding agent for the terminal",
|
||||
version: "0.6.13",
|
||||
version: "0.1.2",
|
||||
iconId: "minimax-code",
|
||||
installLink: "https://agent.minimax.io",
|
||||
command: ["npx", "-y", "@minimax-ai/code@0.6.13", "acp"],
|
||||
command: ["npx", "-y", "@minimax-ai/code@0.1.2", "acp"],
|
||||
},
|
||||
{
|
||||
id: "minion-code",
|
||||
title: "Minion Code",
|
||||
description:
|
||||
"An enhanced AI code assistant built on the Minion framework with rich development tools",
|
||||
version: "0.1.45",
|
||||
version: "0.1.44",
|
||||
iconId: "minion-code",
|
||||
installLink: "https://github.com/femto/minion-code",
|
||||
command: ["uvx", "--from", "minion-code==0.1.45", "minion-code", "acp"],
|
||||
command: ["uvx", "--from", "minion-code==0.1.44", "minion-code", "acp"],
|
||||
},
|
||||
{
|
||||
id: "mistral-vibe",
|
||||
@@ -317,10 +317,10 @@ const CATALOG_DATA = [
|
||||
id: "nova",
|
||||
title: "Nova",
|
||||
description: "Nova by Compass AI - a fully-fledged software engineer at your command",
|
||||
version: "1.1.59",
|
||||
version: "1.1.29",
|
||||
iconId: "nova",
|
||||
installLink: "https://www.compassap.ai/portfolio/nova.html",
|
||||
command: ["npx", "-y", "@compass-ai/nova@1.1.59", "acp"],
|
||||
command: ["npx", "-y", "@compass-ai/nova@1.1.29", "acp"],
|
||||
},
|
||||
{
|
||||
id: "poolside",
|
||||
@@ -335,19 +335,19 @@ const CATALOG_DATA = [
|
||||
id: "qoder",
|
||||
title: "Qoder CLI",
|
||||
description: "AI coding assistant with agentic capabilities",
|
||||
version: "1.1.78",
|
||||
version: "1.1.4",
|
||||
iconId: "qoder",
|
||||
installLink: "https://qoder.com",
|
||||
command: ["npx", "-y", "@qoder-ai/qodercli@1.1.78", "--acp"],
|
||||
command: ["npx", "-y", "@qoder-ai/qodercli@1.1.4", "--acp"],
|
||||
},
|
||||
{
|
||||
id: "qwen-code",
|
||||
title: "Qwen Code",
|
||||
description: "Alibaba's Qwen coding assistant",
|
||||
version: "0.26.0",
|
||||
version: "0.20.1",
|
||||
iconId: "qwen-code",
|
||||
installLink: "https://qwenlm.github.io/qwen-code-docs/en/users/overview",
|
||||
command: ["npx", "-y", "@qwen-code/qwen-code@0.26.0", "--acp", "--experimental-skills"],
|
||||
command: ["npx", "-y", "@qwen-code/qwen-code@0.20.1", "--acp", "--experimental-skills"],
|
||||
},
|
||||
{
|
||||
id: "sigit",
|
||||
|
||||
@@ -61,8 +61,8 @@ describe("ACP provider catalog", () => {
|
||||
it("offers MiniMax Code through its pinned public ACP package", () => {
|
||||
expect(findProvider("minimax-code")).toMatchObject({
|
||||
title: "MiniMax Code",
|
||||
version: "0.6.13",
|
||||
command: ["npx", "-y", "@minimax-ai/code@0.6.13", "acp"],
|
||||
version: "0.1.2",
|
||||
command: ["npx", "-y", "@minimax-ai/code@0.1.2", "acp"],
|
||||
});
|
||||
expect(findProvider("minimax-code").iconSvg).toContain("<svg");
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { useRevealedText } from "@/hooks/use-revealed-text";
|
||||
import { runPluginClientBundle, type PluginClientRuntime } from "./evaluate";
|
||||
|
||||
const runtime = {
|
||||
@@ -706,21 +705,6 @@ describe("evaluatePluginClientBundle", () => {
|
||||
expect(plugin.surfaces.map((surface) => surface.id)).toEqual(["main"]);
|
||||
});
|
||||
|
||||
it("keeps the host reveal hook on the deprecated client/react-native entry", () => {
|
||||
const plugin = evaluatePluginClientBundle(
|
||||
"legacy-reveal",
|
||||
bundle(`
|
||||
const legacy = require("@getpaseo/plugin/client/react-native");
|
||||
const ui = require("@getpaseo/plugin/client/ui");
|
||||
if (typeof legacy.useRevealedText !== "function") throw new Error("Missing legacy reveal hook");
|
||||
if (legacy.StreamedText !== ui.StreamedText) throw new Error("Missing current UI exports");
|
||||
if ("useRevealedText" in ui) throw new Error("Legacy hook leaked into current UI");
|
||||
plugin.addScreen({ id: "reveal", title: "Reveal", Component: legacy.useRevealedText });
|
||||
`),
|
||||
);
|
||||
expect(plugin.surfaces[0]?.Component).toBe(useRevealedText);
|
||||
});
|
||||
|
||||
it("keeps shared and client runtime exports separate", () => {
|
||||
expect(() =>
|
||||
evaluatePluginClientBundle(
|
||||
|
||||
@@ -2,7 +2,6 @@ import { requirePluginText } from "@getpaseo/plugin/client/host";
|
||||
import type { createPluginHosts } from "./hosts";
|
||||
import { openExternalUrl } from "@/utils/open-external-url";
|
||||
import * as pluginUiRuntime from "./react-native/ui";
|
||||
import { useRevealedText } from "@/hooks/use-revealed-text";
|
||||
import { useSettings } from "./settings/use-settings";
|
||||
import * as pluginSharedRuntime from "@getpaseo/plugin";
|
||||
import * as pluginClientRuntime from "@getpaseo/plugin/client";
|
||||
@@ -115,10 +114,6 @@ export type PluginClientRuntime = Pick<
|
||||
| "addHeaderButton"
|
||||
> & { hosts: ReturnType<typeof createPluginHosts> };
|
||||
|
||||
// COMPAT(pluginReactNativeEntrypoint): added in v0.13.0, remove after 2027-04-20
|
||||
// The 0.12 entry exposed the reveal hook as well as UI components.
|
||||
const pluginReactNativeCompatRuntime = { ...pluginUiRuntime, useRevealedText };
|
||||
|
||||
export function runPluginClientBundle(
|
||||
id: string,
|
||||
bundle: string,
|
||||
@@ -508,7 +503,7 @@ export function runPluginClientBundle(
|
||||
};
|
||||
const runtimeRequire = (name: string): unknown => {
|
||||
// COMPAT(pluginReactNativeEntrypoint): added in v0.13.0, remove after 2027-04-20
|
||||
if (name === "@getpaseo/plugin/client/react-native") return pluginReactNativeCompatRuntime;
|
||||
if (name === "@getpaseo/plugin/client/react-native") return pluginUiRuntime;
|
||||
if (name === "@getpaseo/plugin/client/ui") return pluginUiRuntime;
|
||||
if (name === "react") return React;
|
||||
if (name === "react/jsx-runtime") return ReactJsxRuntime;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { TerminalInputModeTracker } from "@getpaseo/protocol/terminal-input-mode";
|
||||
|
||||
import {
|
||||
TERMINAL_GESTURE_LONG_PRESS_MS,
|
||||
@@ -7,8 +8,64 @@ import {
|
||||
advanceTerminalScrollGesture,
|
||||
classifyTerminalGestureIntent,
|
||||
resolveTerminalGestureReleaseAction,
|
||||
routeTerminalGestureRelease,
|
||||
routeTerminalScrollGesture,
|
||||
type TerminalGestureReleaseRouteInput,
|
||||
type TerminalScrollGestureRoute,
|
||||
} from "./terminal-selection-gesture";
|
||||
|
||||
const PI_FULLSCREEN_MOUSE_ENABLE =
|
||||
"\x1b[?1049h\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1004h\x1b[?1006h";
|
||||
const PI_FULLSCREEN_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l";
|
||||
|
||||
function trackerFromOutput(output: string): TerminalInputModeTracker {
|
||||
const tracker = new TerminalInputModeTracker();
|
||||
tracker.feed(output);
|
||||
return tracker;
|
||||
}
|
||||
|
||||
function dragRoutes(input: {
|
||||
inputMode: TerminalInputModeTracker;
|
||||
dys: number[];
|
||||
point?: { x: number; y: number };
|
||||
}): TerminalScrollGestureRoute[] {
|
||||
let state = { lastDy: 0, rowRemainder: 0, didScroll: false };
|
||||
const routes: TerminalScrollGestureRoute[] = [];
|
||||
for (const currentDy of input.dys) {
|
||||
const update = routeTerminalScrollGesture({
|
||||
state,
|
||||
currentDy,
|
||||
point: input.point ?? { x: 25, y: 50 },
|
||||
metrics: { cellWidth: 8, cellHeight: 14 },
|
||||
viewport: { rows: 30, cols: 40 },
|
||||
inputMode: input.inputMode,
|
||||
});
|
||||
state = update.state;
|
||||
routes.push(update.route);
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
function tapRelease(
|
||||
overrides: Partial<TerminalGestureReleaseRouteInput> = {},
|
||||
): ReturnType<typeof routeTerminalGestureRelease> {
|
||||
return routeTerminalGestureRelease({
|
||||
status: "pressing",
|
||||
startedWithSelection: false,
|
||||
didScroll: false,
|
||||
didNavigate: false,
|
||||
movedBeyondTapTolerance: false,
|
||||
pressDurationMs: 80,
|
||||
longPressMs: TERMINAL_GESTURE_LONG_PRESS_MS,
|
||||
scrollMode: "following",
|
||||
point: { x: 25, y: 50 },
|
||||
metrics: { cellWidth: 8, cellHeight: 14 },
|
||||
viewport: { rows: 30, cols: 40 },
|
||||
inputMode: trackerFromOutput(PI_FULLSCREEN_MOUSE_ENABLE),
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
describe("native terminal gesture policy", () => {
|
||||
describe("advanceTerminalScrollGesture", () => {
|
||||
it("keeps consuming a slow drag after scroll intent arms below one row", () => {
|
||||
@@ -351,4 +408,143 @@ describe("native terminal gesture policy", () => {
|
||||
).toEqual("none");
|
||||
});
|
||||
});
|
||||
describe("routeTerminalScrollGesture", () => {
|
||||
it("sends SGR wheel reports at the touched cell while a fullscreen app tracks the mouse", () => {
|
||||
const inputMode = trackerFromOutput(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||
|
||||
expect(dragRoutes({ inputMode, dys: [13, 28, 42, 14] })).toEqual([
|
||||
{ kind: "none" },
|
||||
{ kind: "input", data: "\x1b[<64;4;4M\x1b[<64;4;4M" },
|
||||
{ kind: "input", data: "\x1b[<64;4;4M" },
|
||||
{ kind: "input", data: "\x1b[<65;4;4M\x1b[<65;4;4M" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps ordinary shell drags in local scrollback", () => {
|
||||
const inputMode = trackerFromOutput("\x1b[?2004h$ ls\r\n");
|
||||
|
||||
expect(dragRoutes({ inputMode, dys: [13, 28, 14] })).toEqual([
|
||||
{ kind: "none" },
|
||||
{ kind: "scrollback", direction: "up", rows: 2 },
|
||||
{ kind: "scrollback", direction: "down", rows: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns drags to local scrollback after the app disables mouse tracking", () => {
|
||||
const inputMode = trackerFromOutput(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||
inputMode.feed(PI_FULLSCREEN_MOUSE_DISABLE);
|
||||
|
||||
expect(dragRoutes({ inputMode, dys: [28] })).toEqual([
|
||||
{ kind: "scrollback", direction: "up", rows: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps forwarding wheel input after a reconnect replays the daemon preamble", () => {
|
||||
const daemon = trackerFromOutput(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||
const client = trackerFromOutput(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||
client.reset();
|
||||
client.feed(daemon.getPreamble());
|
||||
|
||||
expect(dragRoutes({ inputMode: client, dys: [14] })).toEqual([
|
||||
{ kind: "input", data: "\x1b[<64;4;4M" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps pixel reports integral at fractional cell boundaries", () => {
|
||||
const inputMode = trackerFromOutput("\x1b[?1003h\x1b[?1016h");
|
||||
const { route } = routeTerminalScrollGesture({
|
||||
state: { lastDy: 0, rowRemainder: 0, didScroll: false },
|
||||
currentDy: 15,
|
||||
point: { x: 999, y: 999 },
|
||||
metrics: { cellWidth: 8.25, cellHeight: 14.25 },
|
||||
viewport: { rows: 3, cols: 3 },
|
||||
inputMode,
|
||||
});
|
||||
expect(route).toEqual({ kind: "input", data: "\x1b[<64;25;43M" });
|
||||
});
|
||||
|
||||
it("clamps wheel reports to the visible grid", () => {
|
||||
const inputMode = trackerFromOutput(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||
|
||||
expect(dragRoutes({ inputMode, dys: [-14], point: { x: 999, y: -5 } })).toEqual([
|
||||
{ kind: "input", data: "\x1b[<65;40;1M" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("routeTerminalGestureRelease", () => {
|
||||
it("clicks the tapped cell and still focuses while a fullscreen app tracks the mouse", () => {
|
||||
expect(tapRelease()).toEqual({
|
||||
action: "focus",
|
||||
mouseInput: "\x1b[<0;4;4M\x1b[<0;4;4m",
|
||||
});
|
||||
});
|
||||
|
||||
it("only focuses on an ordinary shell tap", () => {
|
||||
expect(tapRelease({ inputMode: trackerFromOutput("\x1b[?2004h$ ls\r\n") })).toEqual({
|
||||
action: "focus",
|
||||
mouseInput: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("sends only the press for X10 tracking", () => {
|
||||
expect(tapRelease({ inputMode: trackerFromOutput("\x1b[?9h") })).toEqual({
|
||||
action: "focus",
|
||||
mouseInput: "\x1b[M $$",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a long press as local selection without clicking", () => {
|
||||
expect(tapRelease({ pressDurationMs: TERMINAL_GESTURE_LONG_PRESS_MS + 50 })).toEqual({
|
||||
action: "select",
|
||||
mouseInput: null,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["moved beyond tap tolerance", { movedBeyondTapTolerance: true }],
|
||||
["scrolled", { didScroll: true }],
|
||||
])("never clicks after a drag that %s", (_name, overrides) => {
|
||||
expect(tapRelease(overrides)).toEqual({ action: "none", mouseInput: null });
|
||||
});
|
||||
|
||||
it("clears an existing selection without clicking", () => {
|
||||
expect(tapRelease({ status: "selecting", startedWithSelection: true })).toEqual({
|
||||
action: "clear",
|
||||
mouseInput: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not send live mouse input from a scrolled-back viewport", () => {
|
||||
expect(tapRelease({ scrollMode: "scrolled" })).toEqual({
|
||||
action: "focus",
|
||||
mouseInput: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not click before the grid is measured", () => {
|
||||
expect(tapRelease({ metrics: null, viewport: null })).toEqual({
|
||||
action: "focus",
|
||||
mouseInput: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("clamps clicks to the visible grid", () => {
|
||||
expect(tapRelease({ point: { x: 999, y: -5 } })).toEqual({
|
||||
action: "focus",
|
||||
mouseInput: "\x1b[<0;40;1M\x1b[<0;40;1m",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps pixel clicks integral at fractional cell boundaries", () => {
|
||||
expect(
|
||||
tapRelease({
|
||||
inputMode: trackerFromOutput("\x1b[?1000h\x1b[?1016h"),
|
||||
point: { x: 999, y: 999 },
|
||||
metrics: { cellWidth: 8.25, cellHeight: 14.25 },
|
||||
viewport: { rows: 3, cols: 3 },
|
||||
}),
|
||||
).toEqual({ action: "focus", mouseInput: "\x1b[<0;25;43M\x1b[<0;25;43m" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import type {
|
||||
TerminalInputModeTracker,
|
||||
TerminalMousePosition,
|
||||
} from "@getpaseo/protocol/terminal-input-mode";
|
||||
import type { TerminalGridCellMetrics } from "./terminal-grid-metrics";
|
||||
|
||||
export type TerminalGestureStatus = "idle" | "pressing" | "selecting";
|
||||
export type TerminalGestureIntent = "tap" | "pending" | "select" | "scroll";
|
||||
export type TerminalGestureScrollMode = "following" | "scrolled";
|
||||
@@ -33,8 +39,39 @@ export interface TerminalScrollGestureUpdate {
|
||||
rowDelta: number;
|
||||
}
|
||||
|
||||
export type TerminalScrollGestureRoute =
|
||||
| { kind: "none" }
|
||||
| { kind: "scrollback"; direction: "up" | "down"; rows: number }
|
||||
| { kind: "input"; data: string };
|
||||
|
||||
export interface TerminalScrollGestureRouteInput {
|
||||
state: TerminalScrollGestureState;
|
||||
currentDy: number;
|
||||
point: { x: number; y: number };
|
||||
metrics: TerminalGridCellMetrics;
|
||||
viewport: { rows: number; cols: number };
|
||||
inputMode: Pick<TerminalInputModeTracker, "encodeWheel">;
|
||||
}
|
||||
|
||||
export interface TerminalScrollGestureRouteUpdate {
|
||||
state: TerminalScrollGestureState;
|
||||
route: TerminalScrollGestureRoute;
|
||||
}
|
||||
|
||||
export type TerminalGestureAction = "none" | "focus" | "select" | "clear";
|
||||
|
||||
export interface TerminalGestureReleaseRouteInput extends TerminalGestureReleaseInput {
|
||||
point: { x: number; y: number };
|
||||
metrics: TerminalGridCellMetrics | null;
|
||||
viewport: { rows: number; cols: number } | null;
|
||||
inputMode: Pick<TerminalInputModeTracker, "encodeLeftClick">;
|
||||
}
|
||||
|
||||
export interface TerminalGestureReleaseRoute {
|
||||
action: TerminalGestureAction;
|
||||
mouseInput: string | null;
|
||||
}
|
||||
|
||||
export const TERMINAL_GESTURE_TAP_TOLERANCE_PX = 8;
|
||||
export const TERMINAL_GESTURE_LONG_PRESS_MS = 450;
|
||||
export const TERMINAL_GESTURE_VERTICAL_SCROLL_THRESHOLD_PX = 12;
|
||||
@@ -78,6 +115,60 @@ export function advanceTerminalScrollGesture(input: {
|
||||
};
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
/** Clamps a touch point to the visible grid; pixel reports stay integral at fractional cell sizes. */
|
||||
function resolveTerminalMousePosition(input: {
|
||||
point: { x: number; y: number };
|
||||
metrics: TerminalGridCellMetrics;
|
||||
viewport: { rows: number; cols: number };
|
||||
}): TerminalMousePosition {
|
||||
const { cellWidth, cellHeight } = input.metrics;
|
||||
const pixelX = clamp(
|
||||
Math.floor(input.point.x),
|
||||
0,
|
||||
Math.ceil(input.viewport.cols * cellWidth) - 1,
|
||||
);
|
||||
const pixelY = clamp(
|
||||
Math.floor(input.point.y),
|
||||
0,
|
||||
Math.ceil(input.viewport.rows * cellHeight) - 1,
|
||||
);
|
||||
return {
|
||||
col: Math.floor(pixelX / cellWidth) + 1,
|
||||
row: Math.floor(pixelY / cellHeight) + 1,
|
||||
pixelX: pixelX + 1,
|
||||
pixelY: pixelY + 1,
|
||||
};
|
||||
}
|
||||
|
||||
/** A drag scrolls local scrollback unless the foreground app asked for wheel reports. */
|
||||
export function routeTerminalScrollGesture(
|
||||
input: TerminalScrollGestureRouteInput,
|
||||
): TerminalScrollGestureRouteUpdate {
|
||||
const { state, rowDelta } = advanceTerminalScrollGesture({
|
||||
state: input.state,
|
||||
currentDy: input.currentDy,
|
||||
cellHeight: input.metrics.cellHeight,
|
||||
});
|
||||
if (rowDelta === 0) {
|
||||
return { state, route: { kind: "none" } };
|
||||
}
|
||||
|
||||
const direction = rowDelta > 0 ? "up" : "down";
|
||||
const rows = Math.abs(rowDelta);
|
||||
const report = input.inputMode.encodeWheel({
|
||||
direction,
|
||||
...resolveTerminalMousePosition(input),
|
||||
});
|
||||
if (report === null) {
|
||||
return { state, route: { kind: "scrollback", direction, rows } };
|
||||
}
|
||||
return { state, route: { kind: "input", data: report.repeat(rows) } };
|
||||
}
|
||||
|
||||
export function resolveTerminalGestureReleaseAction(
|
||||
input: TerminalGestureReleaseInput,
|
||||
): TerminalGestureAction {
|
||||
@@ -106,3 +197,19 @@ export function resolveTerminalGestureReleaseAction(
|
||||
|
||||
return "focus";
|
||||
}
|
||||
|
||||
/**
|
||||
* Only a short tap on the live screen clicks; long press, drags and selection clears stay local.
|
||||
* The caller sends the click before focusing, so the keyboard resize cannot move the target cell.
|
||||
*/
|
||||
export function routeTerminalGestureRelease(
|
||||
input: TerminalGestureReleaseRouteInput,
|
||||
): TerminalGestureReleaseRoute {
|
||||
const action = resolveTerminalGestureReleaseAction(input);
|
||||
const { metrics, viewport } = input;
|
||||
if (action !== "focus" || input.scrollMode !== "following" || !metrics || !viewport) {
|
||||
return { action, mouseInput: null };
|
||||
}
|
||||
const position = resolveTerminalMousePosition({ point: input.point, metrics, viewport });
|
||||
return { action, mouseInput: input.inputMode.encodeLeftClick(position) };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"description": "Paseo CLI - control your AI coding agents from the command line",
|
||||
"bin": {
|
||||
"paseo": "bin/paseo"
|
||||
@@ -28,9 +28,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/server": "0.13.2",
|
||||
"@getpaseo/client": "0.13.0-beta.2",
|
||||
"@getpaseo/protocol": "0.13.0-beta.2",
|
||||
"@getpaseo/server": "0.13.0-beta.2",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/client",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"description": "Paseo client SDK package",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -47,8 +47,8 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/relay": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.0-beta.2",
|
||||
"@getpaseo/relay": "0.13.0-beta.2",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"private": true,
|
||||
"description": "Paseo desktop app (Electron wrapper)",
|
||||
"homepage": "https://paseo.sh",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"description": "Native module for two way audio streaming",
|
||||
"keywords": [
|
||||
"ExpoTwoWayAudio",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/plugin",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"description": "Paseo plugin SDK package",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -72,8 +72,8 @@
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/client": "0.13.0-beta.2",
|
||||
"@getpaseo/protocol": "0.13.0-beta.2",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/react": "~19.2.0",
|
||||
"react": "19.2.3",
|
||||
@@ -83,8 +83,8 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/client": "0.13.0-beta.2",
|
||||
"@getpaseo/protocol": "0.13.0-beta.2",
|
||||
"react": ">=19.1.0 <20",
|
||||
"react-native": ">=0.81.5",
|
||||
"zod": "^4.4.3"
|
||||
|
||||
@@ -101,29 +101,12 @@ describe("plugin SDK import boundaries", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each(compatibilityEntries)("%s respects the client boundary", (entry) => {
|
||||
expect(
|
||||
boundaryViolations(
|
||||
path.resolve(sourceDirectory, "..", manifest.exports[entry].source),
|
||||
"client",
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("typechecks a 0.12 reveal-hook consumer without exposing the hook on current UI", () => {
|
||||
expect(
|
||||
typeErrors(`
|
||||
import { useRevealedText, Icon } from "@getpaseo/plugin/client/react-native";
|
||||
import { StreamedText } from "@getpaseo/plugin/client/ui";
|
||||
// @ts-expect-error the legacy hook is not part of the current UI API
|
||||
import { useRevealedText as currentHook } from "@getpaseo/plugin/client/ui";
|
||||
import { Text } from "react-native";
|
||||
function LegacyRow({ text, phase }: { text: string; phase: "streaming" | "complete" }) {
|
||||
const revealed: string = useRevealedText(text, phase);
|
||||
return <Text>{revealed}</Text>;
|
||||
}
|
||||
`),
|
||||
).toEqual([]);
|
||||
it.each(compatibilityEntries)("%s only re-exports the UI entry", (entry) => {
|
||||
const source = readFileSync(
|
||||
path.resolve(sourceDirectory, "..", manifest.exports[entry].source),
|
||||
"utf8",
|
||||
);
|
||||
expect(source.replace(/\/\/[^\n]*|\/\*[^]*?\*\//g, "").trim()).toBe('export * from "./ui.js";');
|
||||
});
|
||||
|
||||
it.each(Object.entries(entries))(
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// COMPAT(pluginReactNativeEntrypoint): added in v0.13.0, remove after 2027-04-20
|
||||
/** @deprecated Import Paseo UI from @getpaseo/plugin/client/ui. */
|
||||
export * from "./ui.js";
|
||||
|
||||
/** @deprecated Render StreamedText from @getpaseo/plugin/client/ui. Removed after 2027-04-20 with this entry. */
|
||||
export declare function useRevealedText(text: string, phase: "streaming" | "complete"): string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"description": "Paseo shared protocol schemas and wire types",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -118,6 +118,111 @@ describe("TerminalInputModeTracker", () => {
|
||||
expect(tracker.getPreamble()).toBe("");
|
||||
});
|
||||
|
||||
const PI_FULLSCREEN_MOUSE_ENABLE =
|
||||
"\x1b[?1049h\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1004h\x1b[?1006h";
|
||||
const PI_FULLSCREEN_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l";
|
||||
const wheelUpAt3x4 = { direction: "up" as const, col: 3, row: 4, pixelX: 30, pixelY: 80 };
|
||||
|
||||
it("encodes SGR wheel reports while a fullscreen app tracks the mouse", () => {
|
||||
const tracker = new TerminalInputModeTracker();
|
||||
expect(tracker.encodeWheel(wheelUpAt3x4)).toBeNull();
|
||||
|
||||
expect(tracker.feed(PI_FULLSCREEN_MOUSE_ENABLE).changed).toBe(true);
|
||||
|
||||
expect(tracker.encodeWheel(wheelUpAt3x4)).toBe("\x1b[<64;3;4M");
|
||||
expect(tracker.encodeWheel({ ...wheelUpAt3x4, direction: "down" })).toBe("\x1b[<65;3;4M");
|
||||
});
|
||||
|
||||
it("stops encoding wheel reports once the app disables mouse tracking", () => {
|
||||
const tracker = new TerminalInputModeTracker();
|
||||
tracker.feed(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||
|
||||
expect(tracker.feed(PI_FULLSCREEN_MOUSE_DISABLE).changed).toBe(true);
|
||||
|
||||
expect(tracker.encodeWheel(wheelUpAt3x4)).toBeNull();
|
||||
expect(tracker.getPreamble()).toBe("");
|
||||
});
|
||||
|
||||
it("replays mouse tracking so a reconnected client keeps forwarding wheel input", () => {
|
||||
const daemon = new TerminalInputModeTracker();
|
||||
daemon.feed(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||
const client = new TerminalInputModeTracker();
|
||||
client.feed(PI_FULLSCREEN_MOUSE_ENABLE);
|
||||
|
||||
client.reset();
|
||||
expect(client.encodeWheel(wheelUpAt3x4)).toBeNull();
|
||||
expect(daemon.getPreamble()).toBe("\x1b[?1003h\x1b[?1006h");
|
||||
client.feed(daemon.getPreamble());
|
||||
|
||||
expect(client.encodeWheel(wheelUpAt3x4)).toBe("\x1b[<64;3;4M");
|
||||
});
|
||||
|
||||
it("encodes SGR pixel wheel reports with pixel coordinates", () => {
|
||||
const tracker = new TerminalInputModeTracker();
|
||||
tracker.feed("\x1b[?1000h\x1b[?1016h");
|
||||
|
||||
expect(tracker.encodeWheel(wheelUpAt3x4)).toBe("\x1b[<64;30;80M");
|
||||
expect(tracker.getPreamble()).toBe("\x1b[?1000h\x1b[?1016h");
|
||||
});
|
||||
|
||||
it("encodes default wheel reports only while every byte stays ASCII", () => {
|
||||
const tracker = new TerminalInputModeTracker();
|
||||
tracker.feed("\x1b[?1000h");
|
||||
|
||||
expect(tracker.encodeWheel(wheelUpAt3x4)).toBe("\x1b[M`#$");
|
||||
expect(tracker.encodeWheel({ ...wheelUpAt3x4, col: 96 })).toBeNull();
|
||||
expect(tracker.encodeWheel({ ...wheelUpAt3x4, row: 96 })).toBeNull();
|
||||
});
|
||||
|
||||
it("does not encode wheel reports for X10 press-only tracking", () => {
|
||||
const tracker = new TerminalInputModeTracker();
|
||||
tracker.feed("\x1b[?9h\x1b[?1006h");
|
||||
|
||||
expect(tracker.encodeWheel(wheelUpAt3x4)).toBeNull();
|
||||
});
|
||||
|
||||
const tapAt3x4 = { col: 3, row: 4, pixelX: 30, pixelY: 80 };
|
||||
|
||||
it.each([
|
||||
["no tracking", "", null],
|
||||
["SGR press/release", PI_FULLSCREEN_MOUSE_ENABLE, "\x1b[<0;3;4M\x1b[<0;3;4m"],
|
||||
["SGR pixel press/release", "\x1b[?1000h\x1b[?1016h", "\x1b[<0;30;80M\x1b[<0;30;80m"],
|
||||
["default press and button-3 release", "\x1b[?1000h", "\x1b[M #$\x1b[M##$"],
|
||||
["X10 default press only", "\x1b[?9h", "\x1b[M #$"],
|
||||
["X10 SGR press only", "\x1b[?9h\x1b[?1006h", "\x1b[<0;3;4M"],
|
||||
[
|
||||
"tracking disabled again",
|
||||
`${PI_FULLSCREEN_MOUSE_ENABLE}${PI_FULLSCREEN_MOUSE_DISABLE}`,
|
||||
null,
|
||||
],
|
||||
])("encodes a left click with %s", (_name, output, expected) => {
|
||||
const tracker = new TerminalInputModeTracker();
|
||||
tracker.feed(output);
|
||||
|
||||
expect(tracker.encodeLeftClick(tapAt3x4)).toBe(expected);
|
||||
});
|
||||
|
||||
it("does not encode a default left click that leaves ASCII", () => {
|
||||
const tracker = new TerminalInputModeTracker();
|
||||
tracker.feed("\x1b[?1000h");
|
||||
|
||||
expect(tracker.encodeLeftClick({ ...tapAt3x4, col: 96 })).toBeNull();
|
||||
});
|
||||
|
||||
it.each([["\x1bc"], ["\x1b", "c"]])(
|
||||
"clears every tracked mode on a full reset split as %j",
|
||||
(...chunks) => {
|
||||
const tracker = new TerminalInputModeTracker();
|
||||
tracker.feed(`\x1b[>7u\x1b[?2004h${PI_FULLSCREEN_MOUSE_ENABLE}`);
|
||||
|
||||
const results = chunks.map((chunk) => tracker.feed(chunk));
|
||||
|
||||
expect(results.at(-1)).toEqual({ changed: true, responses: [] });
|
||||
expect(tracker.getPreamble()).toBe("");
|
||||
expect(tracker.encodeWheel(wheelUpAt3x4)).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it("ignores encoded key input sequences", () => {
|
||||
const tracker = new TerminalInputModeTracker();
|
||||
|
||||
|
||||
@@ -10,6 +10,17 @@ export interface TerminalInputModeState {
|
||||
bracketedPaste?: boolean;
|
||||
}
|
||||
|
||||
export interface TerminalMousePosition {
|
||||
col: number;
|
||||
row: number;
|
||||
pixelX: number;
|
||||
pixelY: number;
|
||||
}
|
||||
|
||||
export interface TerminalWheelInput extends TerminalMousePosition {
|
||||
direction: "up" | "down";
|
||||
}
|
||||
|
||||
export const DEFAULT_TERMINAL_INPUT_MODE_STATE: TerminalInputModeState = {
|
||||
kittyKeyboardFlags: 0,
|
||||
win32InputMode: false,
|
||||
@@ -21,8 +32,20 @@ const ESC = String.fromCharCode(0x1b);
|
||||
const APPLICATION_CURSOR_KEYS_MODE = 1;
|
||||
const WIN32_INPUT_MODE = 9001;
|
||||
const BRACKETED_PASTE_MODE = 2004;
|
||||
const CSI_INPUT_MODE_SEQUENCE = new RegExp(
|
||||
`${ESC}\\[(?:([<>=?]?)([0-9;]*)u|\\?([0-9;]*)([hl]))`,
|
||||
const X10_MOUSE_TRACKING_MODE = 9;
|
||||
const MOUSE_TRACKING_MODES = new Set([X10_MOUSE_TRACKING_MODE, 1000, 1002, 1003]);
|
||||
const SGR_MOUSE_ENCODING_MODE = 1006;
|
||||
const SGR_PIXELS_MOUSE_ENCODING_MODE = 1016;
|
||||
const MOUSE_ENCODING_MODES = new Set([SGR_MOUSE_ENCODING_MODE, SGR_PIXELS_MOUSE_ENCODING_MODE]);
|
||||
const LEFT_BUTTON = 0;
|
||||
const DEFAULT_RELEASE_BUTTON = 3;
|
||||
const WHEEL_UP_BUTTON = 64;
|
||||
const WHEEL_DOWN_BUTTON = 65;
|
||||
const DEFAULT_MOUSE_ENCODING_OFFSET = 32;
|
||||
const MAX_ASCII_CODE = 127;
|
||||
const FULL_RESET = `${ESC}c`;
|
||||
const INPUT_MODE_SEQUENCE = new RegExp(
|
||||
`${ESC}(?:c|\\[(?:([<>=?]?)([0-9;]*)u|\\?([0-9;]*)([hl])))`,
|
||||
"g",
|
||||
);
|
||||
const INCOMPLETE_CSI_INPUT_MODE_SEQUENCE = new RegExp(`${ESC}\\[[<>=?]?[0-9;]*$`);
|
||||
@@ -74,6 +97,8 @@ export class TerminalInputModeTracker {
|
||||
private win32InputMode = false;
|
||||
private applicationCursorKeys = false;
|
||||
private bracketedPaste = false;
|
||||
private mouseTrackingMode = 0;
|
||||
private mouseEncodingMode = 0;
|
||||
private readonly kittyKeyboardStack: number[] = [];
|
||||
private pending = "";
|
||||
|
||||
@@ -89,13 +114,19 @@ export class TerminalInputModeTracker {
|
||||
const responses: string[] = [];
|
||||
let consumedUntil = 0;
|
||||
|
||||
CSI_INPUT_MODE_SEQUENCE.lastIndex = 0;
|
||||
INPUT_MODE_SEQUENCE.lastIndex = 0;
|
||||
for (;;) {
|
||||
const match = CSI_INPUT_MODE_SEQUENCE.exec(text);
|
||||
const match = INPUT_MODE_SEQUENCE.exec(text);
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
consumedUntil = CSI_INPUT_MODE_SEQUENCE.lastIndex;
|
||||
consumedUntil = INPUT_MODE_SEQUENCE.lastIndex;
|
||||
|
||||
if (match[0] === FULL_RESET) {
|
||||
changed = this.getPreamble().length > 0 || changed;
|
||||
this.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (match[4]) {
|
||||
changed = this.applyPrivateModeSequence(match[3] ?? "", match[4]) || changed;
|
||||
@@ -108,10 +139,10 @@ export class TerminalInputModeTracker {
|
||||
}
|
||||
|
||||
const tail = text.slice(consumedUntil);
|
||||
const pendingStart = tail.lastIndexOf(`${ESC}[`);
|
||||
const pendingStart = tail.lastIndexOf(ESC);
|
||||
if (pendingStart >= 0) {
|
||||
const pending = tail.slice(pendingStart);
|
||||
if (INCOMPLETE_CSI_INPUT_MODE_SEQUENCE.test(pending)) {
|
||||
if (pending === ESC || INCOMPLETE_CSI_INPUT_MODE_SEQUENCE.test(pending)) {
|
||||
this.pending = pending;
|
||||
}
|
||||
}
|
||||
@@ -124,6 +155,8 @@ export class TerminalInputModeTracker {
|
||||
this.win32InputMode = false;
|
||||
this.applicationCursorKeys = false;
|
||||
this.bracketedPaste = false;
|
||||
this.mouseTrackingMode = 0;
|
||||
this.mouseEncodingMode = 0;
|
||||
this.kittyKeyboardStack.length = 0;
|
||||
this.pending = "";
|
||||
}
|
||||
@@ -159,9 +192,64 @@ export class TerminalInputModeTracker {
|
||||
if (this.bracketedPaste) {
|
||||
parts.push("\x1b[?2004h");
|
||||
}
|
||||
// The alternate screen is never replayed here: re-entering it would clear the restored screen.
|
||||
if (this.mouseTrackingMode > 0) {
|
||||
parts.push(`\x1b[?${this.mouseTrackingMode}h`);
|
||||
}
|
||||
if (this.mouseEncodingMode > 0) {
|
||||
parts.push(`\x1b[?${this.mouseEncodingMode}h`);
|
||||
}
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
/** Returns the wheel report the foreground app requested, or null when it does not track the wheel. */
|
||||
encodeWheel(input: TerminalWheelInput): string | null {
|
||||
const isWheelReported =
|
||||
this.mouseTrackingMode > 0 && this.mouseTrackingMode !== X10_MOUSE_TRACKING_MODE;
|
||||
if (!isWheelReported) {
|
||||
return null;
|
||||
}
|
||||
const button = input.direction === "up" ? WHEEL_UP_BUTTON : WHEEL_DOWN_BUTTON;
|
||||
return this.encodeMouseReport(button, input, "M");
|
||||
}
|
||||
|
||||
/** Returns a left press and release, press only under X10, or null when the app does not track the mouse. */
|
||||
encodeLeftClick(position: TerminalMousePosition): string | null {
|
||||
if (this.mouseTrackingMode === 0) {
|
||||
return null;
|
||||
}
|
||||
const press = this.encodeMouseReport(LEFT_BUTTON, position, "M");
|
||||
if (press === null || this.mouseTrackingMode === X10_MOUSE_TRACKING_MODE) {
|
||||
return press;
|
||||
}
|
||||
const isSgr = this.mouseEncodingMode !== 0;
|
||||
const release = isSgr
|
||||
? this.encodeMouseReport(LEFT_BUTTON, position, "m")
|
||||
: this.encodeMouseReport(DEFAULT_RELEASE_BUTTON, position, "M");
|
||||
return release === null ? null : `${press}${release}`;
|
||||
}
|
||||
|
||||
private encodeMouseReport(
|
||||
button: number,
|
||||
position: TerminalMousePosition,
|
||||
sgrFinal: "M" | "m",
|
||||
): string | null {
|
||||
if (this.mouseEncodingMode === SGR_PIXELS_MOUSE_ENCODING_MODE) {
|
||||
return `\x1b[<${button};${position.pixelX};${position.pixelY}${sgrFinal}`;
|
||||
}
|
||||
if (this.mouseEncodingMode === SGR_MOUSE_ENCODING_MODE) {
|
||||
return `\x1b[<${button};${position.col};${position.row}${sgrFinal}`;
|
||||
}
|
||||
// Input travels as UTF-8 text, so a default-encoded byte above ASCII would be re-encoded.
|
||||
const codes = [button, position.col, position.row].map(
|
||||
(value) => value + DEFAULT_MOUSE_ENCODING_OFFSET,
|
||||
);
|
||||
if (codes.some((code) => code > MAX_ASCII_CODE)) {
|
||||
return null;
|
||||
}
|
||||
return `\x1b[M${String.fromCharCode(...codes)}`;
|
||||
}
|
||||
|
||||
private applyKittyKeyboardSequence(
|
||||
prefix: string,
|
||||
params: string,
|
||||
@@ -205,6 +293,18 @@ export class TerminalInputModeTracker {
|
||||
const modes = parsePrivateModeParams(params);
|
||||
let changed = false;
|
||||
|
||||
for (const mode of modes) {
|
||||
const next = final === "h" ? mode : 0;
|
||||
if (MOUSE_TRACKING_MODES.has(mode)) {
|
||||
changed = this.mouseTrackingMode !== next || changed;
|
||||
this.mouseTrackingMode = next;
|
||||
}
|
||||
if (MOUSE_ENCODING_MODES.has(mode)) {
|
||||
changed = this.mouseEncodingMode !== next || changed;
|
||||
this.mouseEncodingMode = next;
|
||||
}
|
||||
}
|
||||
|
||||
if (modes.has(WIN32_INPUT_MODE)) {
|
||||
const previous = this.win32InputMode;
|
||||
this.win32InputMode = final === "h";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"description": "Paseo relay for bridging daemon and client connections",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"description": "Paseo backend server",
|
||||
"files": [
|
||||
"dist/server",
|
||||
@@ -107,11 +107,11 @@
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.3.246",
|
||||
"@anthropic-ai/sdk": "^0.104.2",
|
||||
"@getpaseo/client": "0.13.2",
|
||||
"@getpaseo/highlight": "0.13.2",
|
||||
"@getpaseo/plugin": "0.13.2",
|
||||
"@getpaseo/protocol": "0.13.2",
|
||||
"@getpaseo/relay": "0.13.2",
|
||||
"@getpaseo/client": "0.13.0-beta.2",
|
||||
"@getpaseo/highlight": "0.13.0-beta.2",
|
||||
"@getpaseo/plugin": "0.13.0-beta.2",
|
||||
"@getpaseo/protocol": "0.13.0-beta.2",
|
||||
"@getpaseo/relay": "0.13.0-beta.2",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -13,27 +13,6 @@ index ee51e76..dc1bc17 100644
|
||||
if (_providedOnChange) {
|
||||
_providedOnChange(_index, _position, _type);
|
||||
}
|
||||
diff --git a/node_modules/@gorhom/bottom-sheet/lib/commonjs/hooks/useAnimatedLayout.js b/node_modules/@gorhom/bottom-sheet/lib/commonjs/hooks/useAnimatedLayout.js
|
||||
index 973d05c..b484b5f 100644
|
||||
--- a/node_modules/@gorhom/bottom-sheet/lib/commonjs/hooks/useAnimatedLayout.js
|
||||
+++ b/node_modules/@gorhom/bottom-sheet/lib/commonjs/hooks/useAnimatedLayout.js
|
||||
@@ -86,7 +86,7 @@ function useAnimatedLayout(containerLayoutState, topInset, bottomInset, modal, s
|
||||
});
|
||||
}, [state, verticalInset, modal]);
|
||||
(0, _react.useEffect)(() => {
|
||||
- _reactNative.Dimensions.addEventListener('change', ({
|
||||
+ const subscription = _reactNative.Dimensions.addEventListener('change', ({
|
||||
window
|
||||
}) => {
|
||||
state.modify(_state => {
|
||||
@@ -96,6 +96,7 @@ function useAnimatedLayout(containerLayoutState, topInset, bottomInset, modal, s
|
||||
return _state;
|
||||
});
|
||||
});
|
||||
+ return () => subscription.remove();
|
||||
}, [state]);
|
||||
//#endregion
|
||||
|
||||
diff --git a/node_modules/@gorhom/bottom-sheet/lib/commonjs/index.js b/node_modules/@gorhom/bottom-sheet/lib/commonjs/index.js
|
||||
index e44ad86..96555ef 100644
|
||||
--- a/node_modules/@gorhom/bottom-sheet/lib/commonjs/index.js
|
||||
@@ -75,27 +54,6 @@ index 94fabdb..60ca66a 100644
|
||||
if (_providedOnChange) {
|
||||
_providedOnChange(_index, _position, _type);
|
||||
}
|
||||
diff --git a/node_modules/@gorhom/bottom-sheet/lib/module/hooks/useAnimatedLayout.js b/node_modules/@gorhom/bottom-sheet/lib/module/hooks/useAnimatedLayout.js
|
||||
index b0eec93..67713a2 100644
|
||||
--- a/node_modules/@gorhom/bottom-sheet/lib/module/hooks/useAnimatedLayout.js
|
||||
+++ b/node_modules/@gorhom/bottom-sheet/lib/module/hooks/useAnimatedLayout.js
|
||||
@@ -82,7 +82,7 @@ export function useAnimatedLayout(containerLayoutState, topInset, bottomInset, m
|
||||
});
|
||||
}, [state, verticalInset, modal]);
|
||||
useEffect(() => {
|
||||
- Dimensions.addEventListener('change', ({
|
||||
+ const subscription = Dimensions.addEventListener('change', ({
|
||||
window
|
||||
}) => {
|
||||
state.modify(_state => {
|
||||
@@ -92,6 +92,7 @@ export function useAnimatedLayout(containerLayoutState, topInset, bottomInset, m
|
||||
return _state;
|
||||
});
|
||||
});
|
||||
+ return () => subscription.remove();
|
||||
}, [state]);
|
||||
//#endregion
|
||||
|
||||
diff --git a/node_modules/@gorhom/bottom-sheet/lib/module/index.js b/node_modules/@gorhom/bottom-sheet/lib/module/index.js
|
||||
index 8259637..eb057fe 100644
|
||||
--- a/node_modules/@gorhom/bottom-sheet/lib/module/index.js
|
||||
@@ -143,26 +101,6 @@ index 2954b3f..7260656 100644
|
||||
|
||||
if (_providedOnChange) {
|
||||
_providedOnChange(_index, _position, _type);
|
||||
diff --git a/node_modules/@gorhom/bottom-sheet/src/hooks/useAnimatedLayout.ts b/node_modules/@gorhom/bottom-sheet/src/hooks/useAnimatedLayout.ts
|
||||
index 68c674d..564833c 100644
|
||||
--- a/node_modules/@gorhom/bottom-sheet/src/hooks/useAnimatedLayout.ts
|
||||
+++ b/node_modules/@gorhom/bottom-sheet/src/hooks/useAnimatedLayout.ts
|
||||
@@ -106,13 +106,14 @@ export function useAnimatedLayout(
|
||||
[state, verticalInset, modal]
|
||||
);
|
||||
useEffect(() => {
|
||||
- Dimensions.addEventListener('change', ({ window }) => {
|
||||
+ const subscription = Dimensions.addEventListener('change', ({ window }) => {
|
||||
state.modify(_state => {
|
||||
'worklet';
|
||||
_state.window = window;
|
||||
return _state;
|
||||
});
|
||||
});
|
||||
+ return () => subscription.remove();
|
||||
}, [state]);
|
||||
//#endregion
|
||||
|
||||
diff --git a/node_modules/@gorhom/bottom-sheet/src/index.ts b/node_modules/@gorhom/bottom-sheet/src/index.ts
|
||||
index 7aaea57..6a5f5a4 100644
|
||||
--- a/node_modules/@gorhom/bottom-sheet/src/index.ts
|
||||
|
||||
@@ -40,10 +40,9 @@ index 0a1c6c2..5935112 100644
|
||||
|
||||
struct OnLinkLongPress {
|
||||
diff --git a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt
|
||||
index c12937c..8105833 100644
|
||||
--- a/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt
|
||||
+++ b/node_modules/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt
|
||||
@@ -95,7 +95,7 @@ class EnrichedMarkdown(
|
||||
@@ -95,7 +95,7 @@
|
||||
private var accessibilityLabels = AccessibilityLabels()
|
||||
private var textBreakStrategy: String = BreakStrategyUtils.DEFAULT_STRATEGY
|
||||
|
||||
@@ -52,7 +51,7 @@ index c12937c..8105833 100644
|
||||
private var onLinkLongPressCallback: ((String) -> Unit)? = null
|
||||
private var onTaskListItemPressCallback: ((Int, Boolean, String) -> Unit)? = null
|
||||
private var onLatexErrorCallback: LatexErrorReporter? = null
|
||||
@@ -296,7 +296,7 @@ class EnrichedMarkdown(
|
||||
@@ -296,7 +296,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +60,7 @@ index c12937c..8105833 100644
|
||||
onLinkPressCallback = callback
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ class EnrichedMarkdown(
|
||||
@@ -405,7 +405,7 @@
|
||||
val codeLinkRegex = inlineCodeLinkRegex
|
||||
val renderId = ++currentRenderId
|
||||
|
||||
@@ -70,7 +69,7 @@ index c12937c..8105833 100644
|
||||
try {
|
||||
val filtered =
|
||||
if (isStreaming) {
|
||||
@@ -418,13 +418,13 @@ class EnrichedMarkdown(
|
||||
@@ -418,13 +418,13 @@
|
||||
|
||||
if (renderableMarkdown.isEmpty()) {
|
||||
postToMain(renderId) { applyRenderedSegments(emptyList(), false) }
|
||||
@@ -86,19 +85,7 @@ index c12937c..8105833 100644
|
||||
}
|
||||
|
||||
val segments = splitASTIntoSegments(ast)
|
||||
@@ -478,6 +478,11 @@ class EnrichedMarkdown(
|
||||
if (forceHeight || topologyChanged || heightBefore != heightAfter) {
|
||||
MeasurementStore.invalidate(id)
|
||||
requestLayout()
|
||||
+ stateWrapper?.let {
|
||||
+ val state = Arguments.createMap()
|
||||
+ state.putInt("forceHeightRecalculationCounter", ++forceHeightRecalculationCounter)
|
||||
+ it.updateState(state)
|
||||
+ }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -646,8 +651,12 @@ class EnrichedMarkdown(
|
||||
@@ -646,8 +646,12 @@
|
||||
renderId: Long,
|
||||
action: () -> Unit,
|
||||
) {
|
||||
@@ -3151,89 +3138,3 @@ new file mode 100644
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
diff --git a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.cpp b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.cpp
|
||||
index a430d1a..b30a017 100644
|
||||
--- a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.cpp
|
||||
+++ b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.cpp
|
||||
@@ -22,6 +22,14 @@ void MarkdownContainerShadowNode::dirtyLayoutIfNeeded() {
|
||||
}
|
||||
}
|
||||
|
||||
+// Only new props change the rendered markdown. When a parent is cloned, React Native's adoptYogaChild
|
||||
+// re-clones its children without new props, and dirtying those clones re-measures every settled message
|
||||
+// whenever the list above grows.
|
||||
+bool MarkdownContainerShadowNode::shouldNewRevisionDirtyMeasurement(const ShadowNode & /*sourceShadowNode*/,
|
||||
+ const ShadowNodeFragment &fragment) const {
|
||||
+ return fragment.props != nullptr;
|
||||
+}
|
||||
+
|
||||
Size MarkdownContainerShadowNode::measureContent(const LayoutContext &layoutContext,
|
||||
const LayoutConstraints &layoutConstraints) const {
|
||||
return measurementsManager_->measure(getSurfaceId(), getTag(), getConcreteProps(), layoutConstraints);
|
||||
diff --git a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.h b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.h
|
||||
index dea8bb2..6c4bd20 100644
|
||||
--- a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.h
|
||||
+++ b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownContainerShadowNode.h
|
||||
@@ -19,6 +19,8 @@ public:
|
||||
|
||||
MarkdownContainerShadowNode(ShadowNode const &sourceShadowNode, ShadowNodeFragment const &fragment)
|
||||
: ConcreteViewShadowNode(sourceShadowNode, fragment) {
|
||||
+ forceHeightRecalculationCounter_ =
|
||||
+ static_cast<const MarkdownContainerShadowNode &>(sourceShadowNode).forceHeightRecalculationCounter_;
|
||||
dirtyLayoutIfNeeded();
|
||||
}
|
||||
|
||||
@@ -35,6 +37,10 @@ public:
|
||||
|
||||
Size measureContent(const LayoutContext &layoutContext, const LayoutConstraints &layoutConstraints) const override;
|
||||
|
||||
+protected:
|
||||
+ bool shouldNewRevisionDirtyMeasurement(const ShadowNode &sourceShadowNode,
|
||||
+ const ShadowNodeFragment &fragment) const override;
|
||||
+
|
||||
private:
|
||||
int forceHeightRecalculationCounter_{0};
|
||||
std::shared_ptr<MarkdownContainerMeasurementManager> measurementsManager_;
|
||||
diff --git a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp
|
||||
index 3ca259b..ab51055 100644
|
||||
--- a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp
|
||||
+++ b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp
|
||||
@@ -22,6 +22,14 @@ void MarkdownTextShadowNode::dirtyLayoutIfNeeded() {
|
||||
}
|
||||
}
|
||||
|
||||
+// Only new props change the rendered markdown. When a parent is cloned, React Native's adoptYogaChild
|
||||
+// re-clones its children without new props, and dirtying those clones re-measures every settled message
|
||||
+// whenever the list above grows.
|
||||
+bool MarkdownTextShadowNode::shouldNewRevisionDirtyMeasurement(const ShadowNode & /*sourceShadowNode*/,
|
||||
+ const ShadowNodeFragment &fragment) const {
|
||||
+ return fragment.props != nullptr;
|
||||
+}
|
||||
+
|
||||
Size MarkdownTextShadowNode::measureContent(const LayoutContext &layoutContext,
|
||||
const LayoutConstraints &layoutConstraints) const {
|
||||
return measurementsManager_->measure(getSurfaceId(), getTag(), getConcreteProps(), layoutConstraints);
|
||||
diff --git a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h
|
||||
index eaa3279..05a02eb 100644
|
||||
--- a/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h
|
||||
+++ b/node_modules/react-native-enriched-markdown/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h
|
||||
@@ -22,6 +22,8 @@ public:
|
||||
|
||||
MarkdownTextShadowNode(ShadowNode const &sourceShadowNode, ShadowNodeFragment const &fragment)
|
||||
: ConcreteViewShadowNode(sourceShadowNode, fragment) {
|
||||
+ forceHeightRecalculationCounter_ =
|
||||
+ static_cast<const MarkdownTextShadowNode &>(sourceShadowNode).forceHeightRecalculationCounter_;
|
||||
dirtyLayoutIfNeeded();
|
||||
}
|
||||
|
||||
@@ -39,6 +41,10 @@ public:
|
||||
|
||||
Size measureContent(const LayoutContext &layoutContext, const LayoutConstraints &layoutConstraints) const override;
|
||||
|
||||
+protected:
|
||||
+ bool shouldNewRevisionDirtyMeasurement(const ShadowNode &sourceShadowNode,
|
||||
+ const ShadowNodeFragment &fragment) const override;
|
||||
+
|
||||
private:
|
||||
int forceHeightRecalculationCounter_{0};
|
||||
std::shared_ptr<MarkdownTextMeasurementManager> measurementsManager_;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
diff --git a/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.kt b/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.kt
|
||||
index 48e3771..0fba1af 100644
|
||||
--- a/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.kt
|
||||
+++ b/node_modules/react-native-reanimated/android/src/main/java/com/swmansion/reanimated/NativeProxy.kt
|
||||
@@ -245,6 +245,15 @@ open class NativeProxy {
|
||||
}.apply { isAccessible = true }
|
||||
}
|
||||
|
||||
+ // Backport of https://github.com/software-mansion/react-native-reanimated/pull/10435
|
||||
+ // (shipped in Reanimated 4.7.1). The reflective updatePropsSynchronously path skips RN's
|
||||
+ // view-existence guard, so a tag that unmounted with buffered updates throws
|
||||
+ // RetryableMountingLayerException on every event dispatch and Log.w printed the full stack
|
||||
+ // on the UI thread inside the draw pass. Remove once the installed Reanimated includes the fix.
|
||||
+ private val getViewExistsMethod by lazy {
|
||||
+ runCatching { mountingManager.javaClass.getMethod("getViewExists", Int::class.javaPrimitiveType) }.getOrNull()
|
||||
+ }
|
||||
+
|
||||
@DoNotStrip
|
||||
fun synchronouslyUpdateUIProps(
|
||||
intBuffer: IntArray,
|
||||
@@ -253,9 +262,13 @@ open class NativeProxy {
|
||||
SynchronousPropsBufferParser.parse(intBuffer, doubleBuffer) { viewTag, props ->
|
||||
if (BuildConfig.IS_REACT_NATIVE_86_OR_NEWER) {
|
||||
try {
|
||||
+ // The props stay in the registry, and the commit hook applies them to the shadow tree.
|
||||
+ if (getViewExistsMethod?.invoke(mountingManager, viewTag) == false) {
|
||||
+ return@parse
|
||||
+ }
|
||||
updatePropsSynchronouslyMethod.invoke(mountingManager, viewTag, props)
|
||||
} catch (e: Exception) {
|
||||
- Log.w("Reanimated", "synchronouslyUpdateUIProps failed for tag $viewTag", e)
|
||||
+ Log.w("Reanimated", "synchronouslyUpdateUIProps failed for tag $viewTag: ${e.cause ?: e}")
|
||||
}
|
||||
} else {
|
||||
mFabricUIManager.synchronouslyUpdateViewOnUIThread(viewTag, props)
|
||||
@@ -1,92 +0,0 @@
|
||||
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { fetchUsage } from "./usage.js";
|
||||
|
||||
it("shows the reported Pro percentage pools without treating bonus spend as an overage", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "cursor-reported-pools-"));
|
||||
const locator = join(directory, "auth.json");
|
||||
try {
|
||||
await writeFile(locator, JSON.stringify({ accessToken: "fixture-token" }));
|
||||
const report = await fetchUsage({ store: "file", locator }, async () =>
|
||||
Response.json({
|
||||
billingCycleStart: "2026-09-25T07:10:08.000Z",
|
||||
billingCycleEnd: "2026-10-25T07:10:08.000Z",
|
||||
planUsage: {
|
||||
totalSpend: 5717,
|
||||
includedSpend: 2000,
|
||||
bonusSpend: 3717,
|
||||
limit: 2000,
|
||||
autoPercentUsed: 9.157777777777778,
|
||||
apiPercentUsed: 35.46666666666667,
|
||||
totalPercentUsed: 11.54949494949495,
|
||||
},
|
||||
spendLimitUsage: { individualLimit: 100, individualRemaining: 100, limitType: "user" },
|
||||
}),
|
||||
);
|
||||
expect(report).toMatchObject({
|
||||
status: "available",
|
||||
windows: [
|
||||
{ id: "plan_usage", usedPct: 11.54949494949495 },
|
||||
{ id: "cursor_usage", usedPct: 9.157777777777778 },
|
||||
{ id: "third_party_usage", usedPct: 35.46666666666667 },
|
||||
],
|
||||
balances: [],
|
||||
});
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
["Pro+", 42015, 7000, 34.583333333333336, 4.6818181818181825, 32.07251908396947],
|
||||
["Teams", 77145, 2000, 61.8352380952381, 61.09, 61.716],
|
||||
] as const)(
|
||||
"preserves the reported %s pools separately from bonus-inclusive dollars",
|
||||
async (_plan, totalSpend, limit, autoPercentUsed, apiPercentUsed, totalPercentUsed) => {
|
||||
const report = await reportFor({
|
||||
totalSpend,
|
||||
limit,
|
||||
autoPercentUsed,
|
||||
apiPercentUsed,
|
||||
totalPercentUsed,
|
||||
});
|
||||
expect(report).toMatchObject({
|
||||
status: "available",
|
||||
windows: [
|
||||
{ id: "plan_usage", usedPct: totalPercentUsed, summary: true },
|
||||
{ id: "cursor_usage", usedPct: autoPercentUsed },
|
||||
{ id: "third_party_usage", usedPct: apiPercentUsed },
|
||||
],
|
||||
balances: [],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("retains a zero percentage without inventing absent breakdowns", async () => {
|
||||
expect(await reportFor({ totalPercentUsed: "0", autoPercentUsed: null })).toMatchObject({
|
||||
status: "available",
|
||||
windows: [{ id: "plan_usage", usedPct: 0, remainingPct: 100 }],
|
||||
balances: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps dollar-only reports when the response has no percentages", async () => {
|
||||
expect(await reportFor({ totalSpend: 1500, remaining: 2500, limit: 4000 })).toMatchObject({
|
||||
status: "available",
|
||||
windows: [],
|
||||
balances: [{ id: "plan_usage", used: 15, remaining: 25, limit: 40, unit: "usd" }],
|
||||
});
|
||||
});
|
||||
|
||||
async function reportFor(planUsage: Record<string, unknown>) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "cursor-usage-pools-"));
|
||||
const locator = join(directory, "auth.json");
|
||||
try {
|
||||
await writeFile(locator, JSON.stringify({ accessToken: "fixture-token" }));
|
||||
return await fetchUsage({ store: "file", locator }, async () => Response.json({ planUsage }));
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,10 @@ import {
|
||||
toneFromUsedPct,
|
||||
usedPctOf,
|
||||
unavailable,
|
||||
windowFromUsedPct,
|
||||
type UsageAccount,
|
||||
type UsageReport,
|
||||
type UsageBalance,
|
||||
type UsageScope,
|
||||
type UsageWindow,
|
||||
} from "@getpaseo/plugin/server/usage";
|
||||
|
||||
const ApiNumberSchema = z.coerce.number().finite();
|
||||
@@ -62,9 +60,6 @@ const CursorUsageResponseSchema = z.object({
|
||||
bonusSpend: ApiNullableNumberSchema,
|
||||
remaining: ApiNullableNumberSchema,
|
||||
limit: ApiNullableNumberSchema,
|
||||
totalPercentUsed: ApiNullableNumberSchema,
|
||||
autoPercentUsed: ApiNullableNumberSchema,
|
||||
apiPercentUsed: ApiNullableNumberSchema,
|
||||
})
|
||||
.nullish(),
|
||||
billingCycleStart: CursorBillingCycleTimestampSchema,
|
||||
@@ -182,29 +177,8 @@ export async function fetchUsage(
|
||||
|
||||
const resp = CursorUsageResponseSchema.parse(await res.json());
|
||||
const billingCycleEnd = parseCursorBillingCycleTimestamp(resp.billingCycleEnd);
|
||||
const windows: UsageWindow[] = [];
|
||||
const balances: UsageBalance[] = [];
|
||||
if (resp.planUsage) {
|
||||
// Total spend includes bonus credits; returned percentages measure quota usage.
|
||||
for (const [id, label, usedPct] of [
|
||||
["plan_usage", "Total", resp.planUsage.totalPercentUsed],
|
||||
["cursor_usage", "Cursor", resp.planUsage.autoPercentUsed],
|
||||
["third_party_usage", "Third Party", resp.planUsage.apiPercentUsed],
|
||||
] as const) {
|
||||
if (usedPct === null) continue;
|
||||
windows.push(
|
||||
windowFromUsedPct({
|
||||
id,
|
||||
label,
|
||||
utilizationPct: usedPct,
|
||||
resetsAt: billingCycleEnd,
|
||||
summary: id === "plan_usage",
|
||||
tone: toneFromUsedPct(usedPct),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (resp.planUsage && windows.length === 0) {
|
||||
const totalSpend = centsToDollars(resp.planUsage.totalSpend);
|
||||
const remaining = centsToDollars(resp.planUsage.remaining);
|
||||
const limit = centsToDollars(resp.planUsage.limit);
|
||||
@@ -223,7 +197,7 @@ export async function fetchUsage(
|
||||
return {
|
||||
status: "available",
|
||||
planLabel: undefined,
|
||||
windows,
|
||||
windows: [],
|
||||
balances,
|
||||
details: [],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/builtin-plugins",
|
||||
"version": "0.13.2",
|
||||
"version": "0.13.0-beta.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -176,9 +176,8 @@ its registration; that registration belongs in the client entry.
|
||||
|
||||
### UI imports in v0.13
|
||||
|
||||
Use `/client/ui` for all Paseo UI components and UI hooks. The deprecated
|
||||
`/client/react-native` entry retains `useRevealedText(text, phase): string` for 0.12 plugins
|
||||
until removal after 2027-04-20. The hook is deprecated and is not exported from `/client/ui`.
|
||||
Use `/client/ui` for all Paseo UI components and UI hooks. The previous entry remains an
|
||||
undocumented compatibility alias until 2027-04-20.
|
||||
|
||||
For paced timeline text, replace `useRevealedText(text, phase)` and the surrounding `Text` with
|
||||
`<StreamedText text={text} phase={phase} style={style} />` from `/client/ui`.
|
||||
|
||||
@@ -14,9 +14,6 @@ const patchedPackages = [
|
||||
nodeModulesPath: "node_modules/expo-modules-core",
|
||||
patchPrefix: "expo-modules-core+",
|
||||
},
|
||||
// Backport https://github.com/software-mansion/enriched-markdown/pull/924: preserve settled
|
||||
// Android measurements across parent clones. Remove those hunks when upgrading past
|
||||
// 1.2.0-nightly-20261019-926ef06f0; reconcile the other local patch hunks separately.
|
||||
{
|
||||
nodeModulesPath: "packages/app/node_modules/react-native-enriched-markdown",
|
||||
patchPrefix: "react-native-enriched-markdown+",
|
||||
@@ -28,14 +25,6 @@ const patchedPackages = [
|
||||
nodeModulesPath: "node_modules/react-native-unistyles",
|
||||
patchPrefix: "react-native-unistyles+",
|
||||
},
|
||||
// Backport of https://github.com/software-mansion/react-native-reanimated/pull/10435
|
||||
// (Reanimated 4.7.1): skip synchronous Android prop updates for unmounted view tags
|
||||
// instead of throwing and printing a full stack trace on the UI thread per tag per event.
|
||||
// Remove when the installed Reanimated is >= 4.7.1.
|
||||
{
|
||||
nodeModulesPath: "node_modules/react-native-reanimated",
|
||||
patchPrefix: "react-native-reanimated+",
|
||||
},
|
||||
{
|
||||
nodeModulesPath: "node_modules/react-native-draggable-flatlist",
|
||||
patchPrefix: "react-native-draggable-flatlist+",
|
||||
@@ -48,9 +37,6 @@ const patchedPackages = [
|
||||
// modal stack (see packages/app/src/components/ui/isolated-bottom-sheet-modal/host-scope.tsx), and
|
||||
// keeps a dismissing modal's status through its final onChange(-1), so a modal with
|
||||
// enableDismissOnClose=false that unmounts while presented still leaves the portal.
|
||||
// Dimensions subscription cleanup: https://github.com/gorhom/react-native-bottom-sheet/issues/2734
|
||||
// and https://github.com/gorhom/react-native-bottom-sheet/pull/2751. Remove the useAnimatedLayout
|
||||
// hunks once an installed release includes the cleanup; retain the other local hunks.
|
||||
{
|
||||
nodeModulesPath: "node_modules/@gorhom/bottom-sheet",
|
||||
patchPrefix: "@gorhom+bottom-sheet+",
|
||||
|
||||
Reference in New Issue
Block a user