Compare commits

...
Author SHA1 Message Date
Mohamed Boudra bbb2b40855 chore(release): cut 0.7.1 2026-09-01 20:24:44 +02:00
Mohamed Boudra c062278595 docs: prepare 0.7.1 changelog 2026-09-01 20:21:34 +02:00
Mohamed Boudra b9cdfb6528 fix(nix): refresh npm dependency hash 2026-09-01 20:21:34 +02:00
Mohamed Boudra 1361da5ef6 fix(claude): group Fable models in catalog 2026-09-01 20:13:58 +02:00
Mohamed Boudra 3377ffed29 feat(claude): add Fable 5.1 model
Expose the new 1M-context model through the curated Claude catalog and normalize its dated and context-suffixed runtime IDs back to one canonical entry.
2026-09-01 20:12:36 +02:00
Mohamed Boudra c56638ea8c chore(release): cut 0.7.0 2026-08-31 19:25:32 +02:00
Mohamed Boudra 185ab3f300 docs: prepare v0.7.0 changelog 2026-08-31 19:20:58 +02:00
Matt Cowger 1694828275 fix(opencode): show child session prompts (#4055)
Show OpenCode child-session user prompts even when the text part has no completion timestamp.
2026-08-31 18:35:28 +02:00
74a377ff6a fix(server): keep daemon alive when JSONL RPC stdin hits EPIPE (#4048)
* fix(server): keep daemon alive when JSONL RPC stdin hits EPIPE

A dead Claude/OMP child currently turns stdin.write() EPIPE into
uncaughtException and kills the daemon. Catch the write, listen for
stdin errors, and close that transport instead.

Closes #4047

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(server): reproduce JSONL RPC stdin EPIPE

* test(server): gate native EPIPE fixture on POSIX

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-31 15:35:34 +02:00
Mohamed Boudra 63923fdd07 Keep Android timeline still when tapping text (#4090)
* fix(android): keep timeline still when tapping text

Selectable Android text takes touch focus. React Native then scrolls focused children using untransformed coordinates, which moves inverted timelines even though the user did not scroll.\n\nSkip that auto-scroll only for touch-focused, selectable, non-editable text in inverted vertical scroll views, preserving normal focus navigation and editable inputs.

* test(e2e): isolate PR pane fixtures from bots
2026-08-31 12:10:40 +02:00
Mohamed Boudra 11e76f6655 fix(server): resume agents cleanly after failed turns
Track each foreground run start as pending, started, or failed so a previous turn error cannot reject a newly accepted prompt. Includes a regression for resuming after a failed turn.
2026-08-31 12:10:34 +02:00
Mohamed Boudra 4a3e6dea63 Increase mobile content text to 16px (#4111)
Keep mobile interface chrome at 15px while defaulting readable content to 16px. Migrate existing mobile 15px content preferences once while preserving later explicit choices.
2026-08-31 11:58:42 +02:00
paseo-ai[bot] f35d860725 fix: update lockfile signatures and Nix hash [skip ci] 2026-08-31 08:17:46 +00:00
44 changed files with 563 additions and 173 deletions
+37 -49
View File
@@ -1,82 +1,70 @@
# Changelog
## 0.7.0-beta.3 - 2026-08-31
## 0.7.1 - 2026-09-01
### Added
- Added readable prompts, details, and results for Paseo tool calls ([#4066](https://github.com/getpaseo/paseo/pull/4066))
- Added zoom and pan to workspace images and assistant timeline previews ([#4032](https://github.com/getpaseo/paseo/pull/4032), [#4049](https://github.com/getpaseo/paseo/pull/4049), [#4064](https://github.com/getpaseo/paseo/pull/4064))
- Added host navigation from plugin surfaces to agents and workspaces ([#3901](https://github.com/getpaseo/paseo/pull/3901) by [@omercnet](https://github.com/omercnet))
- Added Astro syntax highlighting to files, the editor, and diffs ([#3997](https://github.com/getpaseo/paseo/pull/3997))
- Added Claude Fable 5.1 with a 1M context window ([#4179](https://github.com/getpaseo/paseo/pull/4179))
### Improved
- Reused one OpenCode helper across ordinary agent sessions instead of spawning one per agent ([#4009](https://github.com/getpaseo/paseo/pull/4009))
- Kept cold-start pull request status checks on the shared GitHub batch path ([#4025](https://github.com/getpaseo/paseo/pull/4025))
### Fixed
- Fixed Android first-message submissions crashing when the keyboard restarted during composer teardown ([#4044](https://github.com/getpaseo/paseo/pull/4044))
- Fixed long mobile drafts hiding composer controls behind the keyboard ([#4051](https://github.com/getpaseo/paseo/pull/4051))
- Fixed web and Electron composers losing focus after submission ([#4067](https://github.com/getpaseo/paseo/pull/4067))
- Fixed Android dictation leaving Bluetooth audio in call-quality routing after capture stopped ([#4069](https://github.com/getpaseo/paseo/pull/4069))
- Fixed dictation retries timing out after an abandoned partial transcript ([#4065](https://github.com/getpaseo/paseo/pull/4065))
- Fixed the model selector crashing in iPad desktop layouts ([#3992](https://github.com/getpaseo/paseo/pull/3992) by [@yzim](https://github.com/yzim))
- Fixed archived agents disappearing when opened from History ([#4033](https://github.com/getpaseo/paseo/pull/4033))
- Fixed Grok unified-billing accounts showing zero credits instead of weekly usage ([#4029](https://github.com/getpaseo/paseo/pull/4029) by [@Lite-G](https://github.com/Lite-G), [@claude](https://github.com/claude))
- Fixed slow Pi and OMP startup requests timing out after 30 seconds ([#4008](https://github.com/getpaseo/paseo/pull/4008))
- Fixed Android system navigation covering the Commits controls ([#4005](https://github.com/getpaseo/paseo/pull/4005))
- Fixed new agent work appearing as reopened tasks ([#4068](https://github.com/getpaseo/paseo/pull/4068))
## 0.7.0-beta.2 - 2026-08-28
### Added
- Added SSH connections to existing remote daemons from Desktop and CLI ([#3989](https://github.com/getpaseo/paseo/pull/3989) by [@reidlevesque](https://github.com/reidlevesque))
- Added workspace management and layout actions to the Command Center ([#3013](https://github.com/getpaseo/paseo/pull/3013) by [@cleiter](https://github.com/cleiter))
- Added contextual composer-pill contributions for plugins ([#3956](https://github.com/getpaseo/paseo/pull/3956))
- Added host UI primitives for client plugins ([#3967](https://github.com/getpaseo/paseo/pull/3967))
- Added PR and MR number search to the Command Center ([#3008](https://github.com/getpaseo/paseo/pull/3008) by [@cleiter](https://github.com/cleiter))
- Added session commands to agent SDK handles ([#3719](https://github.com/getpaseo/paseo/pull/3719) by [@gpambrozio](https://github.com/gpambrozio), [@marvin-ambrozio](https://github.com/marvin-ambrozio))
### Improved
- Batched GitHub pull request polling within the reserved API budget ([#3825](https://github.com/getpaseo/paseo/pull/3825) by [@dezchai](https://github.com/dezchai))
### Fixed
- Fixed daemon reconnects stalling while Git processes were queued ([#3945](https://github.com/getpaseo/paseo/pull/3945))
- Fixed restored workspace tabs entering reconciliation loops ([#3987](https://github.com/getpaseo/paseo/pull/3987))
- Fixed immediate dictation submissions omitting the newest speech segment ([#3968](https://github.com/getpaseo/paseo/pull/3968))
- Fixed archived workspaces returning from durable cache ([#3975](https://github.com/getpaseo/paseo/pull/3975))
- Fixed commits and squash merges bypassing configured Git signing ([#3976](https://github.com/getpaseo/paseo/pull/3976))
- Fixed Markdown literal characters being replaced by typographic symbols ([#3253](https://github.com/getpaseo/paseo/pull/3253) by [@cleiter](https://github.com/cleiter))
## 0.7.0-beta.1 - 2026-08-27
## 0.7.0 - 2026-08-31
### Changed
- Changed the project license to Apache-2.0 ([#3944](https://github.com/getpaseo/paseo/pull/3944))
- Changed Cmd/Ctrl+E to toggle Explorer visibility without selecting a view ([#3896](https://github.com/getpaseo/paseo/pull/3896))
- Changed the default mobile content text size from 15px to 16px ([#4111](https://github.com/getpaseo/paseo/pull/4111))
### Added
- Added plugin installation and updates directly from Git repositories ([#3920](https://github.com/getpaseo/paseo/pull/3920))
- Added plugin-defined timeline transformations and rendering ([#3940](https://github.com/getpaseo/paseo/pull/3940))
- Added SSH connections to existing remote daemons from Desktop and CLI ([#3989](https://github.com/getpaseo/paseo/pull/3989) by [@reidlevesque](https://github.com/reidlevesque))
- Added workspace management and layout actions to the Command Center ([#3013](https://github.com/getpaseo/paseo/pull/3013) by [@cleiter](https://github.com/cleiter))
- Added contextual composer-pill contributions for plugins ([#3956](https://github.com/getpaseo/paseo/pull/3956))
- Added host UI primitives for client plugins ([#3967](https://github.com/getpaseo/paseo/pull/3967))
- Added readable prompts, details, and results for Paseo tool calls ([#4066](https://github.com/getpaseo/paseo/pull/4066))
- Added zoom and pan to workspace images and assistant timeline previews ([#4032](https://github.com/getpaseo/paseo/pull/4032), [#4049](https://github.com/getpaseo/paseo/pull/4049), [#4064](https://github.com/getpaseo/paseo/pull/4064))
- Added opening child folders directly in the preferred desktop editor ([#3615](https://github.com/getpaseo/paseo/pull/3615) by [@caikovsky](https://github.com/caikovsky))
- Added host navigation from plugin surfaces to agents and workspaces ([#3901](https://github.com/getpaseo/paseo/pull/3901) by [@omercnet](https://github.com/omercnet))
- Added host-rendered icons to plugin client surfaces ([#3903](https://github.com/getpaseo/paseo/pull/3903) by [@omercnet](https://github.com/omercnet))
- Added semantic surface, border, success, and warning colors to plugin themes ([#3898](https://github.com/getpaseo/paseo/pull/3898) by [@omercnet](https://github.com/omercnet))
- Added registered project listing to the public plugin SDK ([#3899](https://github.com/getpaseo/paseo/pull/3899) by [@omercnet](https://github.com/omercnet))
- Added PR and MR number search to the Command Center ([#3008](https://github.com/getpaseo/paseo/pull/3008) by [@cleiter](https://github.com/cleiter))
- Added session commands to agent SDK handles ([#3719](https://github.com/getpaseo/paseo/pull/3719) by [@gpambrozio](https://github.com/gpambrozio), [@marvin-ambrozio](https://github.com/marvin-ambrozio))
- Added F-Droid release metadata and stable changelog generation ([#2501](https://github.com/getpaseo/paseo/pull/2501) by [@antonok-edm](https://github.com/antonok-edm))
- Added Astro syntax highlighting to files, the editor, and diffs ([#3997](https://github.com/getpaseo/paseo/pull/3997))
### Improved
- Streamed assistant text at a steady visual rate instead of in arrival lumps ([#3612](https://github.com/getpaseo/paseo/pull/3612) by [@Tommypop2](https://github.com/Tommypop2))
- Painted cached conversations before unrelated cache restoration and network loading ([#3907](https://github.com/getpaseo/paseo/pull/3907))
- Reused one OpenCode helper across ordinary agent sessions instead of spawning one per agent ([#4009](https://github.com/getpaseo/paseo/pull/4009))
- Batched GitHub pull request polling within the reserved API budget ([#3825](https://github.com/getpaseo/paseo/pull/3825) by [@dezchai](https://github.com/dezchai))
- Added manual, action-required, and warning states to GitLab and Gitea checks ([#2337](https://github.com/getpaseo/paseo/pull/2337) by [@nllptrx](https://github.com/nllptrx))
- Kept cold-start pull request status checks on the shared GitHub batch path ([#4025](https://github.com/getpaseo/paseo/pull/4025))
### Fixed
- Fixed the daemon crashing when a Claude or OMP JSONL process closes during a write ([#4048](https://github.com/getpaseo/paseo/pull/4048))
- Fixed Android first-message submissions crashing when the keyboard restarted during composer teardown ([#4044](https://github.com/getpaseo/paseo/pull/4044))
- Fixed long mobile drafts hiding composer controls behind the keyboard ([#4051](https://github.com/getpaseo/paseo/pull/4051))
- Fixed Android timelines shifting when tapping selectable text ([#4090](https://github.com/getpaseo/paseo/pull/4090))
- Fixed web and Electron composers losing focus after submission ([#4067](https://github.com/getpaseo/paseo/pull/4067))
- Fixed Android dictation leaving Bluetooth audio in call-quality routing after capture stopped ([#4069](https://github.com/getpaseo/paseo/pull/4069))
- Fixed dictation retries timing out after an abandoned partial transcript ([#4065](https://github.com/getpaseo/paseo/pull/4065))
- Fixed immediate dictation submissions omitting the newest speech segment ([#3968](https://github.com/getpaseo/paseo/pull/3968))
- Fixed the model selector crashing in iPad desktop layouts ([#3992](https://github.com/getpaseo/paseo/pull/3992) by [@yzim](https://github.com/yzim))
- Fixed OpenCode child-session prompts missing from provider subagent timelines ([#4055](https://github.com/getpaseo/paseo/pull/4055) by [@mcowger](https://github.com/mcowger))
- Fixed archived agents disappearing when opened from History ([#4033](https://github.com/getpaseo/paseo/pull/4033))
- Fixed restored workspace tabs entering reconciliation loops ([#3987](https://github.com/getpaseo/paseo/pull/3987))
- Fixed archived workspaces returning from durable cache ([#3975](https://github.com/getpaseo/paseo/pull/3975))
- Fixed Grok unified-billing accounts showing zero credits instead of weekly usage ([#4029](https://github.com/getpaseo/paseo/pull/4029) by [@Lite-G](https://github.com/Lite-G), [@claude](https://github.com/claude))
- Fixed daemon reconnects stalling while Git processes were queued ([#3945](https://github.com/getpaseo/paseo/pull/3945))
- Fixed slow Pi and OMP startup requests timing out after 30 seconds ([#4008](https://github.com/getpaseo/paseo/pull/4008))
- Fixed commits and squash merges bypassing configured Git signing ([#3976](https://github.com/getpaseo/paseo/pull/3976))
- Fixed Android system navigation covering the Commits controls ([#4005](https://github.com/getpaseo/paseo/pull/4005))
- Fixed new agent work appearing as reopened tasks ([#4068](https://github.com/getpaseo/paseo/pull/4068))
- Fixed Markdown literal characters being replaced by typographic symbols ([#3253](https://github.com/getpaseo/paseo/pull/3253) by [@cleiter](https://github.com/cleiter))
- Fixed rewinding long conversations replaying the complete rebuilt timeline ([#3642](https://github.com/getpaseo/paseo/pull/3642))
- Fixed rewound prompts remaining blank in the mounted iOS composer ([#3946](https://github.com/getpaseo/paseo/pull/3946))
- Fixed native plugin setup and async client callbacks failing on iOS ([#3942](https://github.com/getpaseo/paseo/pull/3942))
+1 -1
View File
@@ -30,7 +30,7 @@ Before adding a new component, read `components/ui/`. The primitive usually exis
Hierarchy is conveyed through weight and color, not size. Most interface text is `fontSize.base`; compact metadata and hints use `fontSize.sm`. The distinction between a row's primary line and its secondary line is `foreground` versus `foregroundMuted`.
The authored interface ramp uses a 14px base. New native installs default to 15px; web and desktop default to 14px. The Appearance **Interface size** setting is the rendered `fontSize.base` value and scales the rest of the UI ramp proportionally. Primary readable content has its own `fontSize.content`, which defaults to 15px on every platform. It owns message bodies, composer input, Markdown, and PR prose. Controls, navigation, metadata, tool chrome, code, diffs, editors, and terminals stay on their interface or code tokens. **Code size** remains independent.
The authored interface ramp uses a 14px base. New native installs default to 15px; web and desktop default to 14px. The Appearance **Interface size** setting is the rendered `fontSize.base` value and scales the rest of the UI ramp proportionally. Primary readable content has its own `fontSize.content`, which defaults to 16px on native and 15px on web and desktop. It owns message bodies, composer input, Markdown, and PR prose. Controls, navigation, metadata, tool chrome, code, diffs, editors, and terminals stay on their interface or code tokens. **Code size** remains independent.
Weight has three tiers, applied by role:
@@ -0,0 +1,11 @@
Changed
- Changed the project license to Apache-2.0
- Changed Cmd/Ctrl+E to toggle Explorer visibility without selecting a view
- Changed the default mobile content text size from 15px to 16px
Added
- Added plugin installation and updates directly from Git repositories
- Added plugin-defined timeline transformations and rendering
- Added SSH connections to existing remote daemons from Desktop and CLI
Full notes: https://paseo.sh/changelog
@@ -0,0 +1,11 @@
Changed
- Changed the project license to Apache-2.0
- Changed Cmd/Ctrl+E to toggle Explorer visibility without selecting a view
- Changed the default mobile content text size from 15px to 16px
Added
- Added plugin installation and updates directly from Git repositories
- Added plugin-defined timeline transformations and rendering
- Added SSH connections to existing remote daemons from Desktop and CLI
Full notes: https://paseo.sh/changelog
@@ -0,0 +1,11 @@
Changed
- Changed the project license to Apache-2.0
- Changed Cmd/Ctrl+E to toggle Explorer visibility without selecting a view
- Changed the default mobile content text size from 15px to 16px
Added
- Added plugin installation and updates directly from Git repositories
- Added plugin-defined timeline transformations and rendering
- Added SSH connections to existing remote daemons from Desktop and CLI
Full notes: https://paseo.sh/changelog
@@ -0,0 +1,11 @@
Changed
- Changed the project license to Apache-2.0
- Changed Cmd/Ctrl+E to toggle Explorer visibility without selecting a view
- Changed the default mobile content text size from 15px to 16px
Added
- Added plugin installation and updates directly from Git repositories
- Added plugin-defined timeline transformations and rendering
- Added SSH connections to existing remote daemons from Desktop and CLI
Full notes: https://paseo.sh/changelog
@@ -0,0 +1,4 @@
Added
- Added Claude Fable 5.1 with a 1M context window
Full notes: https://paseo.sh/changelog
@@ -0,0 +1,4 @@
Added
- Added Claude Fable 5.1 with a 1M context window
Full notes: https://paseo.sh/changelog
@@ -0,0 +1,4 @@
Added
- Added Claude Fable 5.1 with a 1M context window
Full notes: https://paseo.sh/changelog
@@ -0,0 +1,4 @@
Added
- Added Claude Fable 5.1 with a 1M context window
Full notes: https://paseo.sh/changelog
+1 -1
View File
@@ -1 +1 @@
sha256-zn0UJm7RPR5mlCvKTddcq0bIugrp6VcRBPdwJX8kJIg=
sha256-HFYBrCP62r6ZofazBIGSOnMhk8IIEmv+TFJMpqTIJQ8=
+27 -27
View File
@@ -1,12 +1,12 @@
{
"name": "paseo",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "paseo",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"hasInstallScript": true,
"license": "Apache-2.0",
"workspaces": [
@@ -36298,7 +36298,7 @@
},
"packages/app": {
"name": "@getpaseo/app",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"dependencies": {
"@codemirror/commands": "6.10.4",
"@codemirror/language": "6.12.4",
@@ -37363,12 +37363,12 @@
},
"packages/cli": {
"name": "@getpaseo/cli",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"dependencies": {
"@clack/prompts": "^1.0.0",
"@getpaseo/client": "0.7.0-beta.3",
"@getpaseo/protocol": "0.7.0-beta.3",
"@getpaseo/server": "0.7.0-beta.3",
"@getpaseo/client": "0.7.1",
"@getpaseo/protocol": "0.7.1",
"@getpaseo/server": "0.7.1",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",
@@ -37615,10 +37615,10 @@
},
"packages/client": {
"name": "@getpaseo/client",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"dependencies": {
"@getpaseo/protocol": "0.7.0-beta.3",
"@getpaseo/relay": "0.7.0-beta.3",
"@getpaseo/protocol": "0.7.1",
"@getpaseo/relay": "0.7.1",
"zod": "^4.4.3"
},
"devDependencies": {
@@ -37629,7 +37629,7 @@
},
"packages/desktop": {
"name": "@getpaseo/desktop",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"license": "Apache-2.0",
"dependencies": {
"@getpaseo/cli": "*",
@@ -37872,7 +37872,7 @@
},
"packages/expo-two-way-audio": {
"name": "@getpaseo/expo-two-way-audio",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.14",
@@ -38768,7 +38768,7 @@
},
"packages/highlight": {
"name": "@getpaseo/highlight",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"dependencies": {
"@codemirror/language": "6.12.4",
"@codemirror/legacy-modes": "^6.5.3",
@@ -38797,13 +38797,13 @@
},
"packages/plugin": {
"name": "@getpaseo/plugin",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"dependencies": {
"use-sync-external-store": "^1.6.0"
},
"devDependencies": {
"@getpaseo/client": "0.7.0-beta.3",
"@getpaseo/protocol": "0.7.0-beta.3",
"@getpaseo/client": "0.7.1",
"@getpaseo/protocol": "0.7.1",
"@types/node": "^20.9.0",
"@types/react": "~19.2.0",
"react": "19.1.0",
@@ -38812,15 +38812,15 @@
"zod": "^4.4.3"
},
"peerDependencies": {
"@getpaseo/client": "0.7.0-beta.3",
"@getpaseo/protocol": "0.7.0-beta.3",
"@getpaseo/client": "0.7.1",
"@getpaseo/protocol": "0.7.1",
"react": "19.1.0",
"zod": "^4.4.3"
}
},
"packages/protocol": {
"name": "@getpaseo/protocol",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"dependencies": {
"zod": "^4.4.3"
},
@@ -38833,7 +38833,7 @@
},
"packages/relay": {
"name": "@getpaseo/relay",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"dependencies": {
"base64-js": "^1.5.1",
"tweetnacl": "^1.0.3",
@@ -39051,17 +39051,17 @@
},
"packages/server": {
"name": "@getpaseo/server",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"dependencies": {
"@agentclientprotocol/sdk": "^0.17.1",
"@anthropic-ai/claude-agent-sdk": "0.3.246",
"@anthropic-ai/sdk": "^0.104.2",
"@babel/parser": "^7.29.8",
"@getpaseo/client": "0.7.0-beta.3",
"@getpaseo/highlight": "0.7.0-beta.3",
"@getpaseo/plugin": "0.7.0-beta.3",
"@getpaseo/protocol": "0.7.0-beta.3",
"@getpaseo/relay": "0.7.0-beta.3",
"@getpaseo/client": "0.7.1",
"@getpaseo/highlight": "0.7.1",
"@getpaseo/plugin": "0.7.1",
"@getpaseo/protocol": "0.7.1",
"@getpaseo/relay": "0.7.1",
"@isaacs/ttlcache": "^2.1.4",
"@modelcontextprotocol/sdk": "1.29.0",
"@opencode-ai/sdk": "1.14.46",
@@ -39985,7 +39985,7 @@
},
"packages/website": {
"name": "@getpaseo/website",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"dependencies": {
"@cloudflare/vite-plugin": "^1.29.1",
"@cloudflare/workers-types": "^4.20260317.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "paseo",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"private": true,
"description": "Paseo: voice-controlled development environment for local AI coding agents",
"keywords": [
+10 -15
View File
@@ -3,8 +3,8 @@ import {
openPrPane,
expectPrPaneTitle,
expectPrPaneState,
expectPrPaneCheckSummary,
expectPrPaneActivityCount,
expectPrPaneChecks,
expectPrPaneActivity,
} from "../support/helpers/pr-pane";
import { gotoWorkspace } from "../support/helpers/launcher";
import {
@@ -50,7 +50,6 @@ test.describe("PR pane", () => {
],
},
{ title: "PR with reviews", state: "open", commentCount: 3 },
{ title: "PR with no checks", state: "open" },
],
});
@@ -102,25 +101,21 @@ test.describe("PR pane", () => {
await expectPrPaneTitle(page, "Work in progress");
});
test("renders check pills with correct success/failure/pending counts", async ({ page }) => {
test("renders seeded checks in their success, failure, and pending groups", async ({ page }) => {
await gotoWorkspace(page, workspaceByTitle.get("PR with mixed checks")!);
await openPrPane(page);
await expectPrPaneCheckSummary(page, { success: 2, failure: 1, pending: 1 });
await expectPrPaneChecks(page, {
success: ["build-1", "build-2"],
failure: ["deploy"],
pending: ["security"],
});
});
test("renders activity rows with correct count", async ({ page }) => {
test("renders every seeded activity comment", async ({ page }) => {
await gotoWorkspace(page, workspaceByTitle.get("PR with reviews")!);
await openPrPane(page);
await expectPrPaneActivityCount(page, 3);
});
test("renders gracefully with zero checks", async ({ page }) => {
await gotoWorkspace(page, workspaceByTitle.get("PR with no checks")!);
await openPrPane(page);
await expectPrPaneCheckSummary(page, { success: 0, failure: 0, pending: 0 });
await expectPrPaneTitle(page, "PR with no checks");
await expectPrPaneActivity(page, ["Test comment 1", "Test comment 2", "Test comment 3"]);
});
});
@@ -43,7 +43,6 @@ const cases: ProviderSubagentCase[] = [
sentinel: "OPENCODE_CHILD_SENTINEL",
expectedName: "Verify OpenCode descriptor",
expectedSubtitle: /explore · gpt-5\.4(?: · [^\n·]+)? · \d+(?:\.\d+)?k? tokens/i,
expectsUserMessage: false,
providerConfig: { model: "openai/gpt-5.4" },
prompt:
'Use the task tool exactly once with description "Verify OpenCode descriptor" and the explore subagent. Ask it to reply with exactly OPENCODE_CHILD_SENTINEL and do nothing else. Wait for it, then reply ROOT_DONE.',
+24 -17
View File
@@ -1,5 +1,4 @@
import { expect, type Page } from "@playwright/test";
import type { CheckPresentationCounts } from "@/git/check-presentation";
import { getStateLabel } from "@/git/pull-request-panel/data";
import { openPullRequestPanel } from "./workspace-tabs";
@@ -20,23 +19,31 @@ export async function expectPrPaneState(
});
}
async function assertCheckPill(page: Page, testId: string, count: number): Promise<void> {
const locator = page.getByTestId(testId);
await expect(locator).toHaveCount(count > 0 ? 1 : 0, { timeout: 15_000 });
if (count > 0) {
await expect(locator).toContainText(String(count));
export async function expectPrPaneChecks(
page: Page,
checks: { success: string[]; failure: string[]; pending: string[] },
): Promise<void> {
const section = page.getByTestId("pr-pane-checks");
const groups = [
{ status: "failure", label: /failing checks?$/, names: checks.failure },
{ status: "pending", label: /in progress checks?$/, names: checks.pending },
{ status: "success", label: /successful checks?$/, names: checks.success },
];
for (const group of groups) {
const groupSection = section.getByTestId(`pr-pane-check-group-${group.status}`);
await expect(groupSection.getByRole("button", { name: group.label })).toBeVisible({
timeout: 15_000,
});
for (const name of group.names) {
await expect(groupSection.getByText(name, { exact: true })).toHaveCount(1);
}
}
}
export async function expectPrPaneCheckSummary(
page: Page,
counts: Pick<CheckPresentationCounts, "success" | "failure" | "pending">,
): Promise<void> {
await assertCheckPill(page, "pr-pane-check-success", counts.success);
await assertCheckPill(page, "pr-pane-check-failure", counts.failure);
await assertCheckPill(page, "pr-pane-check-pending", counts.pending);
}
export async function expectPrPaneActivityCount(page: Page, count: number): Promise<void> {
await expect(page.getByTestId("pr-pane-activity-row")).toHaveCount(count, { timeout: 15_000 });
export async function expectPrPaneActivity(page: Page, bodies: string[]): Promise<void> {
const activity = page.getByTestId("pr-pane-activity-row");
for (const body of bodies) {
await expect(activity.getByText(body, { exact: true })).toBeVisible({ timeout: 15_000 });
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/app",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"private": true,
"main": "index.ts",
"scripts": {
@@ -148,7 +148,7 @@ function CheckGroup({
}) {
const handlePress = useCallback(() => onToggle(group.status), [group.status, onToggle]);
return (
<View>
<View testID={`pr-pane-check-group-${group.status}`}>
<Pressable onPress={handlePress} style={styles.groupHeader} accessibilityRole="button">
<Text style={styles.groupLabel}>{group.label}</Text>
{collapsed ? (
@@ -20,6 +20,11 @@ function storedSendBehavior(storage: Storage): SendBehavior | undefined {
return raw === undefined ? undefined : JSON.parse(raw).sendBehavior;
}
function storedContentFontSize(storage: Storage): number | undefined {
const raw = storage.entries.get(APP_SETTINGS_KEY);
return raw === undefined ? undefined : JSON.parse(raw).contentFontSize;
}
/** An in-memory storage whose write to `failingKey` always throws, as a full disk would. */
function createFailingWriteStorage(failingKey: string): Storage {
const storage = createInMemoryKeyValueStorage();
@@ -81,6 +86,47 @@ describe("migrateAppSettings", () => {
expect(appliedIds(storage)).toEqual(["some-later-migration", "steer-default"]);
});
it("migrates every mobile 15px content preference to 16px", async () => {
const storage = createInMemoryKeyValueStorage();
const settings = { ...settingsWith("steer"), contentFontSize: 15 };
const result = await migrateAppSettings(settings, storage, undefined, { native: true });
expect(result.contentFontSize).toBe(16);
expect(storedContentFontSize(storage)).toBe(16);
expect(appliedIds(storage)).toEqual(["steer-default", "mobile-content-16"]);
});
it("leaves a 15px web content preference unchanged", async () => {
const storage = createInMemoryKeyValueStorage();
const settings = { ...settingsWith("steer"), contentFontSize: 15 };
const result = await migrateAppSettings(settings, storage, undefined, { native: false });
expect(result.contentFontSize).toBe(15);
expect(storedContentFontSize(storage)).toBeUndefined();
expect(appliedIds(storage)).toEqual(["steer-default"]);
});
it("lets a mobile user choose 15px after the default migration ran", async () => {
const storage = createInMemoryKeyValueStorage();
await migrateAppSettings(
{ ...settingsWith("steer"), contentFontSize: 15 },
storage,
undefined,
{ native: true },
);
const result = await migrateAppSettings(
{ ...settingsWith("steer"), contentFontSize: 15 },
storage,
undefined,
{ native: true },
);
expect(result.contentFontSize).toBe(15);
});
it("stays unmarked when the settings write fails, so a later launch retries", async () => {
const storage = createFailingWriteStorage(APP_SETTINGS_KEY);
@@ -13,6 +13,9 @@ const AppliedMigrationsSchema = z.strictObject({ applied: z.array(z.string()) })
*/
const STEER_DEFAULT_MIGRATION = "steer-default";
/** Existing mobile installs materialized the old 15px content default in storage. */
const MOBILE_CONTENT_16_MIGRATION = "mobile-content-16";
/**
* Brings stored settings up to date, returning what the caller should use. Owns both writes so
* the marker can only ever be written after the settings it describes: a failed marker write
@@ -23,6 +26,7 @@ export async function migrateAppSettings(
settings: AppSettings,
storage: KeyValueStorage,
stored?: PersistedAppSettings,
options: { native?: boolean } = {},
): Promise<AppSettings> {
const migrationMarker = await readValidatedJson(
storage,
@@ -30,12 +34,26 @@ export async function migrateAppSettings(
AppliedMigrationsSchema,
);
const applied = new Set(migrationMarker?.applied ?? []);
if (applied.has(STEER_DEFAULT_MIGRATION)) {
let addedMigration = false;
let migrated = settings;
if (!applied.has(STEER_DEFAULT_MIGRATION)) {
migrated =
migrated.sendBehavior === "interrupt" ? { ...migrated, sendBehavior: "steer" } : migrated;
applied.add(STEER_DEFAULT_MIGRATION);
addedMigration = true;
}
if (options.native && !applied.has(MOBILE_CONTENT_16_MIGRATION)) {
migrated = migrated.contentFontSize === 15 ? { ...migrated, contentFontSize: 16 } : migrated;
applied.add(MOBILE_CONTENT_16_MIGRATION);
addedMigration = true;
}
if (!addedMigration) {
return settings;
}
const migrated: AppSettings =
settings.sendBehavior === "interrupt" ? { ...settings, sendBehavior: "steer" } : settings;
if (migrated !== settings) {
const storedSidebarRowItems = stored?.sidebarRowItems ?? {};
await storage.setItem(
@@ -48,7 +66,6 @@ export async function migrateAppSettings(
);
}
applied.add(STEER_DEFAULT_MIGRATION);
await storage.setItem(SETTINGS_MIGRATIONS_KEY, JSON.stringify({ applied: [...applied] }));
return migrated;
}
@@ -659,8 +659,8 @@ describe("appearance settings", () => {
expect(defaultUiBaseFontSize(false)).toBe(14);
});
it("uses a 15px content default on mobile and web", () => {
expect(defaultContentFontSize(true)).toBe(15);
it("uses a 16px content default on mobile and a 15px default on web", () => {
expect(defaultContentFontSize(true)).toBe(16);
expect(defaultContentFontSize(false)).toBe(15);
expect(DEFAULT_CONTENT_FONT_SIZE).toBe(defaultContentFontSize(false));
});
@@ -52,7 +52,7 @@ export const DEFAULT_UI_BASE_FONT_SIZE = defaultUiBaseFontSize(isNative);
export const MIN_UI_BASE_FONT_SIZE = 10;
export const MAX_UI_BASE_FONT_SIZE = 21;
export function defaultContentFontSize(native: boolean): number {
return native ? 15 : FONT_SIZE.content;
return native ? 16 : FONT_SIZE.content;
}
export const DEFAULT_CONTENT_FONT_SIZE = defaultContentFontSize(isNative);
@@ -75,7 +75,7 @@ export interface AppSettings {
uiFontFamily: string; // "" = platform default UI stack
monoFontFamily: string; // "" = platform default mono stack
uiBaseFontSize: number; // clamped px, platform default 14 or 15
contentFontSize: number; // clamped px, default 15
contentFontSize: number; // clamped px, platform default 15 or 16
codeFontSize: number; // clamped px, default 12
syntaxTheme: SyntaxThemeId; // default "one"
workspaceTitleSource: WorkspaceTitleSource;
@@ -343,7 +343,7 @@ export async function loadAppSettingsFromStorage(deps: SettingsDeps): Promise<Ap
await writeAppSettings(deps.storage, read.stored, read.settings);
}
const { needsWrite: _needsWrite, ...stored } = read.stored;
return await migrateAppSettings(read.settings, deps.storage, stored);
return await migrateAppSettings(read.settings, deps.storage, stored, { native: isNative });
} catch (error) {
console.error("[AppSettings] Failed to load settings:", error);
throw error;
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/cli",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"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.7.0-beta.3",
"@getpaseo/protocol": "0.7.0-beta.3",
"@getpaseo/server": "0.7.0-beta.3",
"@getpaseo/client": "0.7.1",
"@getpaseo/protocol": "0.7.1",
"@getpaseo/server": "0.7.1",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/client",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"description": "Paseo client SDK package",
"files": [
"dist",
@@ -39,8 +39,8 @@
"test": "vitest run"
},
"dependencies": {
"@getpaseo/protocol": "0.7.0-beta.3",
"@getpaseo/relay": "0.7.0-beta.3",
"@getpaseo/protocol": "0.7.1",
"@getpaseo/relay": "0.7.1",
"zod": "^4.4.3"
},
"devDependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/desktop",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"private": true,
"description": "Paseo desktop app (Electron wrapper)",
"homepage": "https://paseo.sh",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/expo-two-way-audio",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"description": "Native module for two way audio streaming",
"keywords": [
"ExpoTwoWayAudio",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/highlight",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"files": [
"dist",
"!dist/**/*.map",
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/plugin",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"description": "Paseo plugin SDK package",
"files": [
"dist",
@@ -44,8 +44,8 @@
"use-sync-external-store": "^1.6.0"
},
"devDependencies": {
"@getpaseo/client": "0.7.0-beta.3",
"@getpaseo/protocol": "0.7.0-beta.3",
"@getpaseo/client": "0.7.1",
"@getpaseo/protocol": "0.7.1",
"@types/node": "^20.9.0",
"@types/react": "~19.2.0",
"react": "19.1.0",
@@ -54,8 +54,8 @@
"zod": "^4.4.3"
},
"peerDependencies": {
"@getpaseo/client": "0.7.0-beta.3",
"@getpaseo/protocol": "0.7.0-beta.3",
"@getpaseo/client": "0.7.1",
"@getpaseo/protocol": "0.7.1",
"react": "19.1.0",
"zod": "^4.4.3"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/protocol",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"description": "Paseo shared protocol schemas and wire types",
"files": [
"dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/relay",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"description": "Paseo relay for bridging daemon and client connections",
"files": [
"dist",
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/server",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"description": "Paseo backend server",
"files": [
"dist/server",
@@ -72,11 +72,11 @@
"@anthropic-ai/claude-agent-sdk": "0.3.246",
"@anthropic-ai/sdk": "^0.104.2",
"@babel/parser": "^7.29.8",
"@getpaseo/client": "0.7.0-beta.3",
"@getpaseo/highlight": "0.7.0-beta.3",
"@getpaseo/plugin": "0.7.0-beta.3",
"@getpaseo/protocol": "0.7.0-beta.3",
"@getpaseo/relay": "0.7.0-beta.3",
"@getpaseo/client": "0.7.1",
"@getpaseo/highlight": "0.7.1",
"@getpaseo/plugin": "0.7.1",
"@getpaseo/protocol": "0.7.1",
"@getpaseo/relay": "0.7.1",
"@isaacs/ttlcache": "^2.1.4",
"@modelcontextprotocol/sdk": "1.29.0",
"@opencode-ai/sdk": "1.14.46",
@@ -5543,6 +5543,88 @@ test("waitForAgentRunStart resolves while a foreground run is still only pending
expect(manager.getAgent(snapshot.id)?.lifecycle).toBe("idle");
});
test("waitForAgentRunStart ignores a prior turn error while the next run starts", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-error-resume-"));
const storage = new AgentStorage(join(workdir, "agents"), logger);
const retryStartEntered = deferred<void>();
const releaseRetryStart = deferred<void>();
class ErrorThenResumeSession extends TestAgentSession {
override async startTurn(): Promise<{ turnId: string }> {
const turnId = `turn-${++this.turnIdCounter}`;
if (this.turnIdCounter === 1) {
void (async () => {
this.pushEvent({ type: "turn_started", provider: this.provider, turnId });
this.pushEvent({
type: "turn_failed",
provider: this.provider,
turnId,
error: "model at capacity",
});
})();
return { turnId };
}
retryStartEntered.resolve();
await releaseRetryStart.promise;
void (async () => {
this.pushEvent({ type: "turn_started", provider: this.provider, turnId });
this.pushEvent({ type: "turn_completed", provider: this.provider, turnId });
})();
return { turnId };
}
}
class ErrorThenResumeClient extends TestAgentClient {
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
return new ErrorThenResumeSession(config);
}
}
const manager = new AgentManager({
clients: { codex: new ErrorThenResumeClient() },
registry: storage,
logger,
});
let agentId: string | null = null;
try {
const snapshot = await manager.createAgent({ provider: "codex", cwd: workdir }, undefined, {
workspaceId: undefined,
});
agentId = snapshot.id;
await manager.runAgent(agentId, "fail").catch(() => undefined);
expect(manager.getAgent(agentId)?.lifecycle).toBe("error");
const dispatch = await startAgentRun(manager, agentId, "resume", logger);
expect(dispatch.disposition).toBe("turn_started");
const wait = manager.waitForAgentRunStart(agentId);
let earlyResult: "pending" | "resolved" | "rejected" = "pending";
void wait.then(
() => {
earlyResult = "resolved";
return earlyResult;
},
() => {
earlyResult = "rejected";
return earlyResult;
},
);
await retryStartEntered.promise;
await Promise.resolve();
expect(earlyResult).toBe("pending");
releaseRetryStart.resolve();
await expect(wait).resolves.toBeUndefined();
await manager.waitForAgentEvent(agentId);
} finally {
releaseRetryStart.resolve();
if (agentId) await manager.closeAgent(agentId).catch(() => undefined);
rmSync(workdir, { recursive: true, force: true });
}
});
test("replaceAgentRun does not emit idle or resolve waiters between interrupted and replacement runs", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-replace-run-"));
const storagePath = join(workdir, "agents");
@@ -2165,6 +2165,7 @@ export class AgentManager {
}
agent.pendingReplacement = false;
const errorMsg = error instanceof Error ? error.message : "Failed to start turn";
pendingRun.start = { status: "failed", error: errorMsg };
await this.handleStreamEvent(agent, {
type: "turn_failed",
provider: agent.provider,
@@ -2232,8 +2233,7 @@ export class AgentManager {
agent.pendingReplacement = false;
}
const turnStartedAt = new Date();
pendingRun.started = true;
pendingRun.turnId = turnId;
pendingRun.start = { status: "started", turnId };
agent.activeForegroundTurnId = turnId;
this.openActiveTurn(agent, turnId, turnStartedAt);
agent.lifecycle = "running";
@@ -2575,7 +2575,10 @@ export class AgentManager {
}
const pendingRun = this.runs.getPendingRun(agentId);
if ((snapshot.lifecycle === "running" || pendingRun?.started) && !snapshot.pendingReplacement) {
if (
(snapshot.lifecycle === "running" || pendingRun?.start.status === "started") &&
!snapshot.pendingReplacement
) {
return;
}
@@ -2640,14 +2643,19 @@ export class AgentManager {
const currentPendingRun = this.runs.getPendingRun(agentId);
if (
(current.lifecycle === "running" || currentPendingRun?.started) &&
(current.lifecycle === "running" || currentPendingRun?.start.status === "started") &&
!current.pendingReplacement
) {
finishOk();
return true;
}
if (current.lifecycle === "error" && !currentPendingRun?.started) {
if (currentPendingRun?.start.status === "failed") {
finishErr(new Error(currentPendingRun.start.error));
return true;
}
if (current.lifecycle === "error" && !currentPendingRun) {
finishErr(new Error(current.lastError ?? `Agent ${agentId} failed to start`));
return true;
}
@@ -2734,16 +2742,17 @@ export class AgentManager {
return { status: settlement === "completed" ? "settled" : "refused" };
}
if (settlement === "timed_out" && run.turnId) {
const runTurnId = this.runs.getTurnId(agentId);
if (settlement === "timed_out" && runTurnId) {
this.logger.warn(
{ agentId, turnId: run.turnId, kind: run.kind },
{ agentId, turnId: runTurnId, kind: run.kind },
"cancelAgentRun: acknowledged turn still active after timeout, force-canceling",
);
await this.dispatchSessionEvent(agent, {
type: "turn_canceled",
provider: agent.provider,
reason: "interrupted",
turnId: run.turnId,
turnId: runTurnId,
});
await run.settledPromise;
} else if (settlement === "timed_out" && run.kind === "foreground") {
@@ -3023,7 +3032,7 @@ export class AgentManager {
let hasStarted =
isAgentBusy(initialStatus) ||
Boolean(snapshot.activeForegroundTurnId) ||
Boolean(pendingForegroundRun?.started);
pendingForegroundRun?.start.status === "started";
let terminalStatusOverride: AgentLifecycleStatus | null = null;
let finished = false;
@@ -3429,7 +3438,7 @@ export class AgentManager {
return;
}
const pendingRun = this.runs.getPendingRun(agentId);
if (pendingRun && !pendingRun.started) {
if (pendingRun?.start.status === "pending") {
pendingRun.stagedEvents.push(event);
return;
}
@@ -14,8 +14,10 @@ export interface PendingForegroundRun {
token: string;
kind: "foreground";
stagedEvents: AgentStreamEvent[];
turnId: string | null;
started: boolean;
start:
| { status: "pending" }
| { status: "started"; turnId: string }
| { status: "failed"; error: string };
settled: boolean;
settledPromise: Promise<void>;
resolveSettled: () => void;
@@ -64,6 +66,13 @@ export class AgentRunState {
return this.runs.has(agentId);
}
getTurnId(agentId: string): string | null {
const run = this.runs.get(agentId);
if (!run) return null;
if (run.kind === "autonomous") return run.turnId;
return run.start.status === "started" ? run.start.turnId : null;
}
trackAutonomousRun(agentId: string, turnId: string | null): TrackedAgentRun {
const current = this.runs.get(agentId);
if (current) {
@@ -85,7 +94,10 @@ export class AgentRunState {
if (!run) {
return;
}
if (run.kind === "foreground" && (run.turnId === null || run.turnId !== turnId)) {
if (
run.kind === "foreground" &&
(run.start.status !== "started" || run.start.turnId !== turnId)
) {
return;
}
if (
@@ -268,8 +280,7 @@ function createPendingForegroundRun(): PendingForegroundRun {
return {
...createTrackedRunState(),
kind: "foreground",
turnId: null,
started: false,
start: { status: "pending" },
stagedEvents: [],
};
}
@@ -425,6 +425,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
expect(models.map((m) => m.id)).toEqual([
"claude-opus-5",
"claude-fable-5-1",
"claude-fable-5",
"claude-fable-5[1m]",
"claude-opus-4-8[1m]",
@@ -495,6 +496,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
};
expect(getThinkingIds("claude-opus-5")).toContain("ultracode");
expect(getThinkingIds("claude-fable-5-1")).toContain("ultracode");
expect(getThinkingIds("claude-fable-5")).toContain("ultracode");
expect(getThinkingIds("claude-opus-4-8[1m]")).toContain("ultracode");
expect(getThinkingIds("claude-opus-4-8")).toContain("ultracode");
@@ -45,12 +45,19 @@ export const CLAUDE_MODEL_MANIFEST = [
supportsThinkingDisabled: true,
supportsFastMode: true,
},
{
id: "claude-fable-5-1",
label: "Fable 5.1",
description: "Fable 5.1 · Most powerful model",
contextWindowMaxTokens: 1_000_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
},
{
id: "claude-fable-5",
// COMPAT(claudeFable5OneMillionId): added in v0.3.0, remove after 2027-02-06 once pre-v0.3.0 app preferences are outside support.
aliases: ["claude-fable-5[1m]"],
label: "Fable 5",
description: "Fable 5 · Most powerful model",
description: "Fable 5 · Previous release",
minimumClaudeCodeVersion: "2.1.169",
contextWindowMaxTokens: 1_000_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
@@ -326,7 +333,7 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined)
}
const runtimeMatch = trimmed.match(
/^(?:claude[-_ ])?(opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
/^(?:claude[-_ ])?(fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
);
if (!runtimeMatch) {
return null;
@@ -371,7 +378,7 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined):
}
const runtimeMatch = trimmed.match(
/claude[-_ ](opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i,
/claude[-_ ](fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i,
);
if (!runtimeMatch) {
return null;
@@ -417,6 +424,14 @@ function normalizeMajorMinorClaudeModelId(
): string | null {
const family = familyValue.toLowerCase();
const suffix = hasOneMillionContext ? "[1m]" : "";
const candidate = `claude-${family}-${major}-${minor}${suffix}`;
return isClaudeManifestModelId(candidate) ? candidate : null;
const candidates = [
`claude-${family}-${major}-${minor}${suffix}`,
`claude-${family}-${major}-${minor}`,
];
for (const candidate of candidates) {
if (isClaudeManifestModelId(candidate)) {
return candidate;
}
}
return null;
}
@@ -51,6 +51,7 @@ describe("getClaudeModels", () => {
const models = getClaudeModels();
expect(models.map((m) => m.id)).toEqual([
"claude-opus-5",
"claude-fable-5-1",
"claude-fable-5",
"claude-fable-5[1m]",
"claude-opus-4-8[1m]",
@@ -82,6 +83,7 @@ describe("getClaudeModels", () => {
expect(contextWindows).toEqual(
new Map([
["claude-opus-5", 1_000_000],
["claude-fable-5-1", 1_000_000],
["claude-fable-5", 1_000_000],
["claude-fable-5[1m]", 1_000_000],
["claude-opus-4-8[1m]", 1_000_000],
@@ -160,6 +162,9 @@ describe("getClaudeModels", () => {
expect(models.get("claude-fable-5")?.thinkingOptions?.map((option) => option.id)).not.toContain(
CLAUDE_DISABLED_THINKING_OPTION_ID,
);
expect(
models.get("claude-fable-5-1")?.thinkingOptions?.map((option) => option.id),
).not.toContain(CLAUDE_DISABLED_THINKING_OPTION_ID);
expect(models.get("claude-haiku-4-5")?.thinkingOptions).toBeUndefined();
});
@@ -170,6 +175,7 @@ describe("getClaudeModels", () => {
["claude-sonnet-5[1m]", true, "high"],
["claude-sonnet-5-20260101", true, "high"],
["claude-fable-5", false, "high"],
["claude-fable-5-1", false, "high"],
["claude-haiku-4-5", false, undefined],
["openrouter/anthropic/claude-opus-4-8", false, undefined],
[null, false, undefined],
@@ -458,7 +464,7 @@ describe("Claude Opus 5 catalog", () => {
describe("Claude Fable 5 catalog", () => {
it("offers one selectable Fable 5 entry and a compatibility entry for old apps", () => {
const fable5Models = getClaudeModels()
.filter((model) => model.id.startsWith("claude-fable-5"))
.filter((model) => model.id === "claude-fable-5" || model.id === "claude-fable-5[1m]")
.map(({ id, aliases, isSelectable, label, contextWindowMaxTokens }) => ({
id,
aliases,
@@ -491,6 +497,24 @@ describe("Claude Fable 5 catalog", () => {
});
});
describe("Claude Fable 5.1 catalog", () => {
it("offers one Fable 5.1 entry with a 1M context window", () => {
const fable51Models = getClaudeModels()
.filter((model) => model.id.startsWith("claude-fable-5-1"))
.map(({ id, label, contextWindowMaxTokens }) => ({ id, label, contextWindowMaxTokens }));
expect(fable51Models).toEqual([
{ id: "claude-fable-5-1", label: "Fable 5.1", contextWindowMaxTokens: 1_000_000 },
]);
});
it("resolves suffixed and dated Fable 5.1 IDs to the catalog entry", () => {
expect(findClaudeModel("claude-fable-5-1[1m]")?.id).toBe("claude-fable-5-1");
expect(findClaudeModel("claude-fable-5-1-20260901")?.id).toBe("claude-fable-5-1");
expect(findClaudeModel("claude-fable-5-1-20260901[1m]")?.id).toBe("claude-fable-5-1");
});
});
describe("claudeManifestModelSupportsFastMode", () => {
it("keeps fast mode strict to first-party manifest model IDs", () => {
expect(normalizeClaudeManifestModelId("openrouter/anthropic/claude-opus-4-8")).toBeNull();
@@ -502,5 +526,6 @@ describe("claudeManifestModelSupportsFastMode", () => {
expect(claudeManifestModelSupportsFastMode("claude-opus-5")).toBe(true);
expect(claudeManifestModelSupportsFastMode("claude-sonnet-5")).toBe(false);
expect(claudeManifestModelSupportsFastMode("claude-fable-5")).toBe(false);
expect(claudeManifestModelSupportsFastMode("claude-fable-5-1")).toBe(false);
});
});
@@ -54,6 +54,14 @@ readline.createInterface({ input: process.stdin, crlfDelay: Infinity }).on("line
});
`;
const CLOSED_STDIN_CHILD_SOURCE = String.raw`
const fs = require("node:fs");
fs.closeSync(0);
process.stdout.write(JSON.stringify({ type: "stdin_closed" }) + "\n");
setInterval(() => {}, 1_000);
`;
interface InMemoryChildProcess extends ChildProcessWithoutNullStreams {
stdin: PassThrough;
stdout: PassThrough;
@@ -63,6 +71,7 @@ interface InMemoryChildProcess extends ChildProcessWithoutNullStreams {
interface StartProcessOptions {
child?: ChildProcessWithoutNullStreams;
defaultRequestTimeoutMs?: number;
source?: string;
}
function createInMemoryChildProcess(): InMemoryChildProcess {
@@ -85,7 +94,7 @@ function startProcess(options: StartProcessOptions = {}): JsonlRpcProcess {
return new JsonlRpcProcess({
launch: {
command: process.execPath,
args: ["-e", CHILD_SOURCE, "--", "resolved-arg"],
args: ["-e", options.source ?? CHILD_SOURCE, "--", "resolved-arg"],
cwd: process.cwd(),
env: { JSONL_RPC_TEST_VALUE: "resolved-env" },
},
@@ -222,6 +231,70 @@ describe("JsonlRpcProcess", () => {
await rejection;
});
test("closes the transport when a direct send synchronously throws EPIPE", async () => {
const child = createInMemoryChildProcess();
const transport = startProcess({ child });
child.stdin.write = () => {
throw Object.assign(new Error("write EPIPE"), { code: "EPIPE" });
};
expect(() => transport.send({ type: "notice" })).not.toThrow();
await expect(transport.request({ type: "echo", value: "after" })).rejects.toThrow(
"JSONL RPC process is closed",
);
});
// Closing fd 0 does not sever the inherited pipe on Windows, so this fixture cannot produce EPIPE.
test.skipIf(process.platform === "win32")(
"keeps the parent alive when a real child closes its stdin pipe",
async () => {
const transport = startProcess({ source: CLOSED_STDIN_CHILD_SOURCE });
const stdinClosed = new Promise<void>((resolve) => {
const unsubscribe = transport.onMessage((message) => {
if (message.type !== "stdin_closed") return;
unsubscribe();
resolve();
});
});
await stdinClosed;
await expect(transport.request({ type: "echo", value: "after" })).rejects.toThrow(
/EPIPE|stdin is not writable/,
);
await expect(transport.request({ type: "echo", value: "later" })).rejects.toThrow(
"JSONL RPC process is closed",
);
},
);
test("stdin error events close the transport instead of becoming uncaught exceptions", async () => {
const child = createInMemoryChildProcess();
const transport = startProcess({ child });
const request = transport.request({ type: "hang" });
const err = Object.assign(new Error("write EPIPE"), { code: "EPIPE" });
child.stdin.emit("error", err);
await expect(request).rejects.toThrow("write EPIPE");
await expect(transport.request({ type: "echo", value: "after" })).rejects.toThrow(
"JSONL RPC process is closed",
);
});
test("a non-writable stdin closes the transport instead of hanging the request", async () => {
const child = createInMemoryChildProcess();
const transport = startProcess({ child });
child.stdin.end();
await expect(transport.request({ type: "hang" })).rejects.toThrow(
"JSONL RPC stdin is not writable",
);
await expect(transport.request({ type: "echo", value: "after" })).rejects.toThrow(
"JSONL RPC process is closed",
);
});
test("reassembles protocol v2 chunked responses into the logical frame", async () => {
const child = createInMemoryChildProcess();
const transport = startProcess({ child });
@@ -104,6 +104,9 @@ export class JsonlRpcProcess {
this.stderrBuffer = this.stderrBuffer.slice(-STDERR_BUFFER_LIMIT);
}
});
this.child.stdin.on("error", (error) => {
this.handleStdinError(error);
});
this.child.on("error", (error) => {
this.failAll(error instanceof Error ? error : new Error(String(error)));
});
@@ -173,10 +176,18 @@ export class JsonlRpcProcess {
}
send(message: Record<string, unknown>): void {
if (this.disposed || this.child.stdin.destroyed || !this.child.stdin.writable) {
if (this.disposed) {
return;
}
this.child.stdin.write(`${JSON.stringify(message)}\n`);
if (this.child.stdin.destroyed || !this.child.stdin.writable) {
this.handleStdinError(new Error(`${this.diagnosticName} stdin is not writable`));
return;
}
try {
this.child.stdin.write(`${JSON.stringify(message)}\n`);
} catch (error) {
this.handleStdinError(error);
}
}
async close(error = new Error(`${this.diagnosticName} process is closed`)): Promise<void> {
@@ -243,6 +254,15 @@ export class JsonlRpcProcess {
pending.resolve(response.data);
}
private handleStdinError(error: unknown): void {
if (this.disposed) {
return;
}
const err = error instanceof Error ? error : new Error(String(error));
this.options.logger.warn({ err }, `${this.diagnosticName} stdin write failed`);
void this.close(err);
}
private failAll(error: Error): void {
if (this.disposed) {
return;
@@ -6361,7 +6361,7 @@ describe("OpenCode provider subagent contract", () => {
messageID: "msg_child_prompt",
type: "text",
text: "Inspect the auth flow.",
time: { start: 1, end: 2 },
time: { start: 1 },
},
},
});
@@ -2811,7 +2811,7 @@ function appendOpenCodeTextPart(
events: AgentStreamEvent[],
): void {
if (messageRole === "user") {
if (!part.time?.end || !part.text || state.emittedUserMessageIds?.has(part.messageID)) {
if (!part.text || state.emittedUserMessageIds?.has(part.messageID)) {
return;
}
state.emittedUserMessageIds?.add(part.messageID);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/website",
"version": "0.7.0-beta.3",
"version": "0.7.1",
"private": true,
"type": "module",
"scripts": {
+37
View File
@@ -0,0 +1,37 @@
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java
index f708543..26df5e0 100644
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java
@@ -29,6 +29,7 @@ import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.OverScroller;
import android.widget.ScrollView;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
@@ -425,12 +426,23 @@ public class ReactScrollView extends ScrollView
*/
@Override
public void requestChildFocus(View child, View focused) {
- if (focused != null) {
+ if (focused != null && shouldScrollToFocusedChild(focused)) {
scrollToChild(focused);
}
super.requestChildFocus(child, focused);
}
+ private boolean shouldScrollToFocusedChild(View focused) {
+ if (getScaleY() >= 0 || !focused.isInTouchMode() || !(focused instanceof TextView)) {
+ return true;
+ }
+
+ // Selectable text takes focus when tapped. In inverted lists, scrolling to that focus uses
+ // untransformed coordinates and unexpectedly moves the timeline.
+ TextView textView = (TextView) focused;
+ return !textView.isTextSelectable() || textView.onCheckIsTextEditor();
+ }
+
private int getScrollDelta(View descendent) {
descendent.getDrawingRect(mTempRect);
offsetDescendantRectToMyCoords(descendent, mTempRect);
+4
View File
@@ -12,6 +12,10 @@ const patchedPackages = [
nodeModulesPath: "node_modules/react-native-markdown-display",
patchPrefix: "react-native-markdown-display+",
},
{
nodeModulesPath: "node_modules/react-native",
patchPrefix: "react-native+",
},
{
nodeModulesPath: "node_modules/react-native-draggable-flatlist",
patchPrefix: "react-native-draggable-flatlist+",