mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Compare commits
22 Commits
v0.2.0-bet
...
v0.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b589599a8f | ||
|
|
9821729c75 | ||
|
|
7e599db6fe | ||
|
|
b3bfec027f | ||
|
|
6610dbdffe | ||
|
|
a27a1a07dc | ||
|
|
dcaed8008b | ||
|
|
36f38245ca | ||
|
|
782b341b1a | ||
|
|
7ef3376b62 | ||
|
|
d98c5e77f7 | ||
|
|
fbbdbdc571 | ||
|
|
7e97ab4a9c | ||
|
|
0cb9ecf44b | ||
|
|
afcd972dd0 | ||
|
|
48b14d27a5 | ||
|
|
609f81bc11 | ||
|
|
779a56ed36 | ||
|
|
7bd4afe848 | ||
|
|
fc10c79e26 | ||
|
|
08c522c986 | ||
|
|
4b5551d61c |
18
.mise.toml
18
.mise.toml
@@ -1,10 +1,10 @@
|
||||
[env]
|
||||
ANDROID_HOME = "{{env.HOME}}/.local/share/mise/installs/android-sdk/21.0"
|
||||
_.path = [
|
||||
"{{env.HOME}}/.local/share/mise/installs/android-sdk/21.0/cmdline-tools/21.0/bin",
|
||||
"{{env.HOME}}/.local/share/mise/installs/android-sdk/21.0/platform-tools",
|
||||
"{{env.HOME}}/.local/share/mise/installs/android-sdk/21.0/emulator",
|
||||
]
|
||||
[vars]
|
||||
android_sdk_version = '{{ read_file(path=".tool-versions") | split(pat="android-sdk") | last | trim | split(pat="\n") | first | trim }}'
|
||||
|
||||
[tools]
|
||||
java = "21"
|
||||
[env]
|
||||
ANDROID_HOME = "{{ env.HOME }}/.local/share/mise/installs/android-sdk/{{ vars.android_sdk_version }}"
|
||||
_.path = [
|
||||
"{{ env.HOME }}/.local/share/mise/installs/android-sdk/{{ vars.android_sdk_version }}/cmdline-tools/{{ vars.android_sdk_version }}/bin",
|
||||
"{{ env.HOME }}/.local/share/mise/installs/android-sdk/{{ vars.android_sdk_version }}/platform-tools",
|
||||
"{{ env.HOME }}/.local/share/mise/installs/android-sdk/{{ vars.android_sdk_version }}/emulator",
|
||||
]
|
||||
|
||||
124
CHANGELOG.md
124
CHANGELOG.md
@@ -1,96 +1,48 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.0-beta.4 - 2026-07-23
|
||||
|
||||
### Improved
|
||||
|
||||
- Improved model selection on mobile ([#2361](https://github.com/getpaseo/paseo/pull/2361))
|
||||
- Selector popovers stay readable on iPad ([#2360](https://github.com/getpaseo/paseo/pull/2360) by [@yzim](https://github.com/yzim))
|
||||
## 0.2.2 - 2026-07-25
|
||||
|
||||
### Fixed
|
||||
|
||||
- Workspace creation stays responsive even with many active or archived workspaces ([#2355](https://github.com/getpaseo/paseo/pull/2355), [#2379](https://github.com/getpaseo/paseo/pull/2379))
|
||||
- Failed agent starts no longer leave provider processes running ([#2348](https://github.com/getpaseo/paseo/pull/2348) by [@dwyanewang](https://github.com/dwyanewang))
|
||||
- Completed OpenCode turns stay idle when late metadata updates arrive ([#2336](https://github.com/getpaseo/paseo/pull/2336) by [@mcowger](https://github.com/mcowger))
|
||||
- ACP image prompts no longer appear twice ([#2363](https://github.com/getpaseo/paseo/pull/2363))
|
||||
- File edits preserve CRLF line endings and UTF-8 BOMs ([#2277](https://github.com/getpaseo/paseo/pull/2277) by [@dwyanewang](https://github.com/dwyanewang))
|
||||
- Oh My Pi custom messages marked hidden stay hidden in live and restored chats ([#2280](https://github.com/getpaseo/paseo/pull/2280) by [@isac322](https://github.com/isac322))
|
||||
- Web chats stay pinned to the latest message at non-default browser zoom ([#2368](https://github.com/getpaseo/paseo/pull/2368))
|
||||
- Grouped tool-call loading animations display correctly ([#2369](https://github.com/getpaseo/paseo/pull/2369))
|
||||
- Claude 5 models now use the correct context windows.
|
||||
|
||||
## 0.2.0-beta.3 - 2026-07-22
|
||||
## 0.2.1 - 2026-07-24
|
||||
|
||||
### Added
|
||||
|
||||
- Open the complete Changes view as a workspace tab ([#2298](https://github.com/getpaseo/paseo/pull/2298) by [@nikuscs](https://github.com/nikuscs))
|
||||
- Add files to chat directly from Files and Changes ([#2275](https://github.com/getpaseo/paseo/pull/2275) by [@nikuscs](https://github.com/nikuscs))
|
||||
- Open existing agents from Paseo links or the CLI ([#2324](https://github.com/getpaseo/paseo/pull/2324))
|
||||
- Use Oh My Pi's Write Approval mode to allow reads while requiring approval for changes ([#2228](https://github.com/getpaseo/paseo/pull/2228) by [@theslava](https://github.com/theslava))
|
||||
- Claude Opus 5 is available
|
||||
|
||||
### Improved
|
||||
|
||||
- Usage bars now warn as provider limits approach ([#2322](https://github.com/getpaseo/paseo/pull/2322) by [@cleiter](https://github.com/cleiter))
|
||||
- Oh My Pi advisor results now retain their structure and severity ([#2219](https://github.com/getpaseo/paseo/pull/2219) by [@ebg1223](https://github.com/ebg1223))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Notifications now open the correct workspace and agent ([#2331](https://github.com/getpaseo/paseo/pull/2331))
|
||||
- Archived agents can be restored directly from History ([#2316](https://github.com/getpaseo/paseo/pull/2316))
|
||||
- CLI agent runs stay in the current workspace unless a new workspace is requested ([#2315](https://github.com/getpaseo/paseo/pull/2315))
|
||||
- Oh My Pi slash commands now include commands from every supported source ([#2175](https://github.com/getpaseo/paseo/pull/2175) by [@bendavid](https://github.com/bendavid))
|
||||
- Oh My Pi chats no longer stay stuck as running after delayed or incomplete completion events ([#2261](https://github.com/getpaseo/paseo/pull/2261), [#2282](https://github.com/getpaseo/paseo/pull/2282) by [@isac322](https://github.com/isac322))
|
||||
|
||||
## 0.2.0-beta.2 - 2026-07-22
|
||||
|
||||
### Added
|
||||
|
||||
- Edit files directly in the web and desktop apps ([#2270](https://github.com/getpaseo/paseo/pull/2270), [#2309](https://github.com/getpaseo/paseo/pull/2309))
|
||||
- Switch models from the Command Center for active agents and new drafts ([#2147](https://github.com/getpaseo/paseo/pull/2147) by [@kedrzu](https://github.com/kedrzu))
|
||||
- Configure workspace service ports with a fixed range or external allocator ([#2165](https://github.com/getpaseo/paseo/pull/2165) by [@mcowger](https://github.com/mcowger))
|
||||
- Search keyboard shortcuts by action, note, or key combination ([#2160](https://github.com/getpaseo/paseo/pull/2160))
|
||||
- Turn thinking off for supported Claude models ([#2257](https://github.com/getpaseo/paseo/pull/2257))
|
||||
- Use Pi's Max thinking level ([#2267](https://github.com/getpaseo/paseo/pull/2267) by [@ByteTrue](https://github.com/ByteTrue))
|
||||
|
||||
### Improved
|
||||
|
||||
- Improved parity of CLI and MCP tools for workspace, agent and schedule management ([#2186](https://github.com/getpaseo/paseo/pull/2186))
|
||||
- Pasted PR/MR links in the composer become auto-selected as a checkout option ([#2290](https://github.com/getpaseo/paseo/pull/2290))
|
||||
- Projects, workspaces and chat syncing is more efficient ([#2028](https://github.com/getpaseo/paseo/pull/2028), [#2185](https://github.com/getpaseo/paseo/pull/2185), [#2196](https://github.com/getpaseo/paseo/pull/2196), [#2206](https://github.com/getpaseo/paseo/pull/2206), [#2259](https://github.com/getpaseo/paseo/pull/2259), [#2263](https://github.com/getpaseo/paseo/pull/2263))
|
||||
- Make project creation more explicit ([#2098](https://github.com/getpaseo/paseo/pull/2098), [#2187](https://github.com/getpaseo/paseo/pull/2187))
|
||||
- Idle agents release processes automatically and resume when needed ([#2203](https://github.com/getpaseo/paseo/pull/2203), [#2209](https://github.com/getpaseo/paseo/pull/2209))
|
||||
- New Claude and Codex agents default to safer automatic approval modes when supported ([#2213](https://github.com/getpaseo/paseo/pull/2213))
|
||||
- Oh My Pi now supports Max thinking in imported and new sessions ([#2191](https://github.com/getpaseo/paseo/pull/2191) by [@mvanhorn](https://github.com/mvanhorn))
|
||||
- Commit history now includes recent pushed and base-branch commits ([#2312](https://github.com/getpaseo/paseo/pull/2312))
|
||||
- Permission and thinking changes made during a turn now show when they take effect ([#2201](https://github.com/getpaseo/paseo/pull/2201))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reused branches no longer attach an unrelated merged or closed pull request ([#2172](https://github.com/getpaseo/paseo/pull/2172) by [@nllptrx](https://github.com/nllptrx))
|
||||
- Pi compaction waits for long summaries instead of reporting a false timeout ([#2181](https://github.com/getpaseo/paseo/pull/2181) by [@jasonhnd](https://github.com/jasonhnd))
|
||||
- Pi chats keep new messages aligned with the correct history after an idle agent resumes ([#2313](https://github.com/getpaseo/paseo/pull/2313))
|
||||
- OpenCode follow-ups triggered by completed background work now remain visible ([#2258](https://github.com/getpaseo/paseo/pull/2258))
|
||||
- Codex no longer shows the parent agent as a phantom subagent ([#2214](https://github.com/getpaseo/paseo/pull/2214))
|
||||
- Local dictation now works in Nix-packaged installations ([#1587](https://github.com/getpaseo/paseo/pull/1587) by [@yhori991](https://github.com/yhori991))
|
||||
- The composer remains visible after submitting dictated text and returning to the app ([#2194](https://github.com/getpaseo/paseo/pull/2194))
|
||||
- Desktop's dictation shortcut remains responsive after finishing a recording ([#2268](https://github.com/getpaseo/paseo/pull/2268))
|
||||
- Projects can be renamed before their first workspace ([#2252](https://github.com/getpaseo/paseo/pull/2252) by [@albertodeago](https://github.com/albertodeago))
|
||||
- Settings keep showing a connected remote host when the local daemon is stopped ([#1749](https://github.com/getpaseo/paseo/pull/1749) by [@dwyanewang](https://github.com/dwyanewang))
|
||||
- Pinned workspaces no longer disappear briefly when reopening the compact sidebar ([#2210](https://github.com/getpaseo/paseo/pull/2210))
|
||||
- Session imports find older matches from the current workspace ([#2265](https://github.com/getpaseo/paseo/pull/2265) by [@nikuscs](https://github.com/nikuscs))
|
||||
|
||||
## 0.2.0-beta.1 - 2026-07-17
|
||||
## 0.2.0 - 2026-07-24
|
||||
|
||||
### Added
|
||||
|
||||
- Work with pull requests and merge requests from GitLab, Gitea, Forgejo, and Codeberg ([#1913](https://github.com/getpaseo/paseo/pull/1913) by [@nllptrx](https://github.com/nllptrx))
|
||||
- Use Oh My Pi as a native agent provider ([#2067](https://github.com/getpaseo/paseo/pull/2067) by [@ebg1223](https://github.com/ebg1223))
|
||||
- Browse branch commit history and open individual commit diffs from Changes ([#1534](https://github.com/getpaseo/paseo/pull/1534), [#2146](https://github.com/getpaseo/paseo/pull/2146) by [@adradr](https://github.com/adradr))
|
||||
- Edit files directly in the web and desktop apps ([#2270](https://github.com/getpaseo/paseo/pull/2270), [#2309](https://github.com/getpaseo/paseo/pull/2309), [#2277](https://github.com/getpaseo/paseo/pull/2277), [#2382](https://github.com/getpaseo/paseo/pull/2382) by [@dwyanewang](https://github.com/dwyanewang))
|
||||
- Oh My Pi (OMP) as a native agent provider ([#2067](https://github.com/getpaseo/paseo/pull/2067) by [@ebg1223](https://github.com/ebg1223))
|
||||
- Open the complete Changes view as a workspace tab ([#2298](https://github.com/getpaseo/paseo/pull/2298) by [@nikuscs](https://github.com/nikuscs))
|
||||
- Add files to chat directly from Files and Changes ([#2275](https://github.com/getpaseo/paseo/pull/2275) by [@nikuscs](https://github.com/nikuscs))
|
||||
- Browse workspace commit history and open individual commit diffs from Changes ([#1534](https://github.com/getpaseo/paseo/pull/1534), [#2146](https://github.com/getpaseo/paseo/pull/2146), [#2312](https://github.com/getpaseo/paseo/pull/2312) by [@adradr](https://github.com/adradr))
|
||||
- Switch models from the Command Center for active agents and new drafts ([#2147](https://github.com/getpaseo/paseo/pull/2147) by [@kedrzu](https://github.com/kedrzu))
|
||||
- Open existing agents from Paseo links or the CLI ([#2324](https://github.com/getpaseo/paseo/pull/2324))
|
||||
- Configure workspace service ports with a fixed range or external allocator ([#2165](https://github.com/getpaseo/paseo/pull/2165) by [@mcowger](https://github.com/mcowger))
|
||||
- Search keyboard shortcuts by action, note, or key combination ([#2160](https://github.com/getpaseo/paseo/pull/2160))
|
||||
- Turn thinking off for supported Claude models ([#2257](https://github.com/getpaseo/paseo/pull/2257))
|
||||
- Allow Pi's Max thinking level ([#2267](https://github.com/getpaseo/paseo/pull/2267) by [@ByteTrue](https://github.com/ByteTrue))
|
||||
- Open workspace files in more installed editors and file managers ([#2119](https://github.com/getpaseo/paseo/pull/2119))
|
||||
- Remove individual custom providers from Settings ([#1951](https://github.com/getpaseo/paseo/pull/1951))
|
||||
|
||||
### Improved
|
||||
|
||||
- ACP provider catalog updated to the latest registry versions
|
||||
- Improved model selection on mobile ([#2361](https://github.com/getpaseo/paseo/pull/2361))
|
||||
- Selector popovers stay readable on iPad ([#2360](https://github.com/getpaseo/paseo/pull/2360) by [@yzim](https://github.com/yzim))
|
||||
- Projects, workspaces and chat syncing is more efficient ([#2028](https://github.com/getpaseo/paseo/pull/2028), [#2185](https://github.com/getpaseo/paseo/pull/2185), [#2196](https://github.com/getpaseo/paseo/pull/2196), [#2206](https://github.com/getpaseo/paseo/pull/2206), [#2259](https://github.com/getpaseo/paseo/pull/2259), [#2263](https://github.com/getpaseo/paseo/pull/2263))
|
||||
- CLI and MCP tools manage workspaces, agents, and schedules more consistently ([#2186](https://github.com/getpaseo/paseo/pull/2186))
|
||||
- Pasted PR/MR links in the composer become auto-selected as a checkout option ([#2290](https://github.com/getpaseo/paseo/pull/2290))
|
||||
- Make project creation more explicit ([#2098](https://github.com/getpaseo/paseo/pull/2098), [#2187](https://github.com/getpaseo/paseo/pull/2187))
|
||||
- Idle agents release processes automatically and resume when needed ([#2203](https://github.com/getpaseo/paseo/pull/2203), [#2209](https://github.com/getpaseo/paseo/pull/2209))
|
||||
- New Claude and Codex agents default to safer automatic approval modes when supported ([#2213](https://github.com/getpaseo/paseo/pull/2213))
|
||||
- Permission and thinking changes made during a turn now show when they take effect ([#2201](https://github.com/getpaseo/paseo/pull/2201))
|
||||
- Usage bars now warn as provider limits approach ([#2322](https://github.com/getpaseo/paseo/pull/2322) by [@cleiter](https://github.com/cleiter))
|
||||
- Workspace focus mode stays confined to the active workspace with a visible exit control ([#2151](https://github.com/getpaseo/paseo/pull/2151))
|
||||
- Desktop installs the newest available update instead of a cached older release ([#2149](https://github.com/getpaseo/paseo/pull/2149))
|
||||
- Remote daemon update failures now show specific recovery steps ([#2120](https://github.com/getpaseo/paseo/pull/2120))
|
||||
@@ -98,12 +50,34 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Terminal pairing QR codes remain scannable in narrow terminals ([#2381](https://github.com/getpaseo/paseo/pull/2381))
|
||||
- Workspace creation stays responsive even with many active or archived workspaces ([#2355](https://github.com/getpaseo/paseo/pull/2355), [#2379](https://github.com/getpaseo/paseo/pull/2379))
|
||||
- Failed agent starts no longer leave provider processes running ([#2348](https://github.com/getpaseo/paseo/pull/2348) by [@dwyanewang](https://github.com/dwyanewang))
|
||||
- Completed OpenCode turns stay idle when late metadata updates arrive ([#2336](https://github.com/getpaseo/paseo/pull/2336) by [@mcowger](https://github.com/mcowger))
|
||||
- ACP image prompts no longer appear twice ([#2363](https://github.com/getpaseo/paseo/pull/2363))
|
||||
- Web chats stay pinned to the latest message at non-default browser zoom ([#2368](https://github.com/getpaseo/paseo/pull/2368))
|
||||
- Grouped tool-call loading animations display correctly ([#2369](https://github.com/getpaseo/paseo/pull/2369))
|
||||
- Notifications now open the correct workspace and agent ([#2331](https://github.com/getpaseo/paseo/pull/2331))
|
||||
- Archived agents can be restored directly from History ([#2316](https://github.com/getpaseo/paseo/pull/2316))
|
||||
- CLI agent runs stay in the current workspace unless a new workspace is requested ([#2315](https://github.com/getpaseo/paseo/pull/2315))
|
||||
- Reused branches no longer attach an unrelated merged or closed pull request ([#2172](https://github.com/getpaseo/paseo/pull/2172) by [@nllptrx](https://github.com/nllptrx))
|
||||
- Pi compaction waits for long summaries instead of reporting a false timeout ([#2181](https://github.com/getpaseo/paseo/pull/2181) by [@jasonhnd](https://github.com/jasonhnd))
|
||||
- Pi chats keep new messages aligned with the correct history after an idle agent resumes ([#2313](https://github.com/getpaseo/paseo/pull/2313))
|
||||
- OpenCode follow-ups triggered by completed background work now remain visible ([#2258](https://github.com/getpaseo/paseo/pull/2258))
|
||||
- Codex no longer shows the parent agent as a phantom subagent ([#2214](https://github.com/getpaseo/paseo/pull/2214))
|
||||
- Oh My Pi background notices appear as task notifications instead of raw system text ([#2218](https://github.com/getpaseo/paseo/pull/2218) by [@ebg1223](https://github.com/ebg1223))
|
||||
- Local dictation now works in Nix-packaged installations ([#1587](https://github.com/getpaseo/paseo/pull/1587) by [@yhori991](https://github.com/yhori991))
|
||||
- The composer remains visible after submitting dictated text and returning to the app ([#2194](https://github.com/getpaseo/paseo/pull/2194))
|
||||
- Desktop's dictation shortcut remains responsive after finishing a recording ([#2268](https://github.com/getpaseo/paseo/pull/2268))
|
||||
- Projects can be renamed before their first workspace ([#2252](https://github.com/getpaseo/paseo/pull/2252) by [@albertodeago](https://github.com/albertodeago))
|
||||
- Settings keep showing a connected remote host when the local daemon is stopped ([#1749](https://github.com/getpaseo/paseo/pull/1749) by [@dwyanewang](https://github.com/dwyanewang))
|
||||
- Pinned workspaces no longer disappear briefly when reopening the compact sidebar ([#2210](https://github.com/getpaseo/paseo/pull/2210))
|
||||
- Terminal panes no longer remain at 80x24 after focus or visibility changes ([#2059](https://github.com/getpaseo/paseo/pull/2059), [#2154](https://github.com/getpaseo/paseo/pull/2154) by [@cleiter](https://github.com/cleiter))
|
||||
- Sign-in popups in the desktop browser now complete successfully ([#2137](https://github.com/getpaseo/paseo/pull/2137))
|
||||
- Browser typing and shortcuts no longer submit the active Paseo prompt ([#1982](https://github.com/getpaseo/paseo/pull/1982))
|
||||
- Agent browser tabs remain controllable after switching workspaces ([#2156](https://github.com/getpaseo/paseo/pull/2156))
|
||||
- Archived workspaces now show the correct Unarchive or Restore action ([#2002](https://github.com/getpaseo/paseo/pull/2002))
|
||||
- Archived sessions can be reimported into the current workspace ([#2123](https://github.com/getpaseo/paseo/pull/2123))
|
||||
- Archived sessions can be reimported into the current workspace ([#2123](https://github.com/getpaseo/paseo/pull/2123), [#2265](https://github.com/getpaseo/paseo/pull/2265) by [@nikuscs](https://github.com/nikuscs))
|
||||
- Browser shortcuts no longer appear where browser tabs are unavailable ([#2116](https://github.com/getpaseo/paseo/pull/2116) by [@jasonhnd](https://github.com/jasonhnd))
|
||||
|
||||
## 0.1.110 - 2026-07-16
|
||||
|
||||
@@ -27,13 +27,13 @@ The formula reserves three digits each for minor and patch. If either reaches `1
|
||||
|
||||
## Prerequisites (local dev)
|
||||
|
||||
Local Android builds run on macOS (or Linux) and need the Android toolchain, pinned in `.tool-versions` (`java 21`, `android-sdk 21.0`) and wired up by `.mise.toml` (which sets `ANDROID_HOME` and puts `cmdline-tools/21.0/bin`, `platform-tools`, and `emulator` on `PATH`). With [mise](https://mise.jdx.dev):
|
||||
Local Android builds run on macOS (or Linux) and need the Android toolchain, pinned in `.tool-versions` (`java 21`, `android-sdk 21.0`) and wired up by `.mise.toml` (which derives `ANDROID_HOME` and the command-line tool paths from the `android-sdk` entry). With [mise](https://mise.jdx.dev):
|
||||
|
||||
```bash
|
||||
mise install # java 21 + android-sdk 21.0 command-line tools
|
||||
```
|
||||
|
||||
> **Pin a real `android-sdk` version, not `latest`.** The mise `android-sdk` plugin's `latest` resolved to the ancient `1.0` bundle, whose `sdkmanager` (3.6.0) predates the `emulator` package and fails with `Failed to find package emulator`. `21.0` ships a current `sdkmanager`. If you bump it, update the version in `.tool-versions` and in all four paths in `.mise.toml`.
|
||||
> **Pin a real `android-sdk` version, not `latest`.** The mise `android-sdk` plugin's `latest` resolved to the ancient `1.0` bundle, whose `sdkmanager` (3.6.0) predates the `emulator` package and fails with `Failed to find package emulator`. `21.0` ships a current `sdkmanager`. If you bump it, update only the version in `.tool-versions`; `.mise.toml` derives its paths from that tool entry.
|
||||
|
||||
`mise install` only lays down the command-line tools. Install the rest and create an emulator. On Apple Silicon:
|
||||
|
||||
|
||||
@@ -129,6 +129,13 @@ lockstep, no re-measurement needed. Do not call
|
||||
can briefly report a stale nonzero height with closed progress, and the shared
|
||||
provider is where that is normalized.
|
||||
|
||||
The provider also reconciles iOS from the controller's native `onEnd` event.
|
||||
The controller's stock iOS shared values update at move start and during an
|
||||
interactive move, but not at the terminal event, so JS contention can otherwise
|
||||
leave the last height/progress pair stuck in either the open or closed state.
|
||||
Keep that terminal reconciliation on the UI thread; a later focus or blur must
|
||||
not be required to repair the offset.
|
||||
|
||||
Re-measure on `Keyboard.addListener('keyboardDidShow'|'keyboardDidHide')` only
|
||||
to refresh the snapshot if the keyboard was mid-transition when the popover
|
||||
opened.
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-x6jiaw7zkCuBRlddLHD0tXQxolXwr7tzIQBfWu5xnUU=
|
||||
sha256-t7FqvPj7U5YidGH9OZ1kFnD2dPTYUyjcxBlzAJU7Mak=
|
||||
|
||||
114
package-lock.json
generated
114
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paseo",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -35211,7 +35211,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "6.10.4",
|
||||
"@codemirror/language": "6.12.4",
|
||||
@@ -36236,12 +36236,12 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/client": "0.2.0-beta.4",
|
||||
"@getpaseo/protocol": "0.2.0-beta.4",
|
||||
"@getpaseo/server": "0.2.0-beta.4",
|
||||
"@getpaseo/client": "0.2.2",
|
||||
"@getpaseo/protocol": "0.2.2",
|
||||
"@getpaseo/server": "0.2.2",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
@@ -36487,10 +36487,10 @@
|
||||
},
|
||||
"packages/client": {
|
||||
"name": "@getpaseo/client",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.2.0-beta.4",
|
||||
"@getpaseo/relay": "0.2.0-beta.4",
|
||||
"@getpaseo/protocol": "0.2.2",
|
||||
"@getpaseo/relay": "0.2.2",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -36501,7 +36501,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@getpaseo/cli": "*",
|
||||
@@ -36744,7 +36744,7 @@
|
||||
},
|
||||
"packages/expo-two-way-audio": {
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
@@ -37640,7 +37640,7 @@
|
||||
},
|
||||
"packages/highlight": {
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "6.12.4",
|
||||
"@codemirror/legacy-modes": "^6.5.3",
|
||||
@@ -37872,7 +37872,7 @@
|
||||
},
|
||||
"packages/protocol": {
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"dependencies": {
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
@@ -37885,7 +37885,7 @@
|
||||
},
|
||||
"packages/relay": {
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
@@ -38103,15 +38103,15 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.3.214",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|
||||
"@anthropic-ai/sdk": "^0.104.2",
|
||||
"@getpaseo/client": "0.2.0-beta.4",
|
||||
"@getpaseo/highlight": "0.2.0-beta.4",
|
||||
"@getpaseo/protocol": "0.2.0-beta.4",
|
||||
"@getpaseo/relay": "0.2.0-beta.4",
|
||||
"@getpaseo/client": "0.2.2",
|
||||
"@getpaseo/highlight": "0.2.2",
|
||||
"@getpaseo/protocol": "0.2.2",
|
||||
"@getpaseo/relay": "0.2.2",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
@@ -38155,22 +38155,22 @@
|
||||
}
|
||||
},
|
||||
"packages/server/node_modules/@anthropic-ai/claude-agent-sdk": {
|
||||
"version": "0.3.214",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.214.tgz",
|
||||
"integrity": "sha512-wt5ImwhU+p259Zt4K/Q9v5xVi6ruxYO5+KFICyJxnjs/QFEClAeSRqhcXx1J8jgGfatPW1faw09hkm66680UjA==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.220.tgz",
|
||||
"integrity": "sha512-glc7SdwPkOkLw8oxwLo9PKTdLJGqW/PIR4urWXFoRtX9YllwozsEVc5Tc1+EvLSkfrsxPJqQWqOgpjUOQXf1oA==",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.214",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.214",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.214",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.214",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.214",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.214",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.214",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.214"
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.220",
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.220"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@anthropic-ai/sdk": ">=0.93.0",
|
||||
@@ -38179,9 +38179,9 @@
|
||||
}
|
||||
},
|
||||
"packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": {
|
||||
"version": "0.3.214",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.214.tgz",
|
||||
"integrity": "sha512-vAAOeVtlXs3p7MpFVfvfu9ja32eaKtB+MDZnPxCbdzxJk5nofCHlNsHa1UJwXHOsP9lOnFYNIccYztsZ4DNaJA==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.220.tgz",
|
||||
"integrity": "sha512-7VxlbEosK7DODiOnsjoVd0DSJzbnaPrM2jelMHI0y8zx1UnLS3WC6EFUXbvy74F2sXqEznh2tzn7EKWInaRN6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -38192,9 +38192,9 @@
|
||||
]
|
||||
},
|
||||
"packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": {
|
||||
"version": "0.3.214",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.214.tgz",
|
||||
"integrity": "sha512-NTbV8U2yucxCWqEiDC7L0MUehNmd8x3Op8OGzLZRhMF3xmQBy2DxpXiNZgSwwKWNDC3HdgKmJM5ZBbT7XrF/0g==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.220.tgz",
|
||||
"integrity": "sha512-X9RwDsSmbF6ultKZroaip+DL8WRgC64gHbrAwrRlAFSPNZV7zmJyP2ur8rW7KrxqmtuehdMMkw8+SAC/6hD2PA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -38205,9 +38205,9 @@
|
||||
]
|
||||
},
|
||||
"packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": {
|
||||
"version": "0.3.214",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.214.tgz",
|
||||
"integrity": "sha512-KBCf+BlusG0ZcgvpjjHwv1kh+6WiR8vJbPjpR2udwkrtcQgKLN+24l+FeaQEzO2TL+ExCmM1KC4tNPvnPpy+tw==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.220.tgz",
|
||||
"integrity": "sha512-WkROPwWskqhKR9XgnmseHQ6rLi9zM9qt57IWoToIjL/eXOqDWipp7JXZ1L5ud+LrA42dunHPZfBwD/vXZ+A7LA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -38218,9 +38218,9 @@
|
||||
]
|
||||
},
|
||||
"packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": {
|
||||
"version": "0.3.214",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.214.tgz",
|
||||
"integrity": "sha512-i06wQRsmevE7spY1ryYfs+NP+xdZ1FwAyTjDaF0k/xG+cgtzZYghTFUemdYF3GVwgWgpcava9GiCFDT3DoHI6w==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.220.tgz",
|
||||
"integrity": "sha512-OHoZOZ8Cf2TBr6oXIXPwyvUxj9jrq2w8E4poA8dMpacXszcPSPiCQCMuuOh4aWJzfeJE1+TtWxhKMVb2csXyZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -38231,9 +38231,9 @@
|
||||
]
|
||||
},
|
||||
"packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": {
|
||||
"version": "0.3.214",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.214.tgz",
|
||||
"integrity": "sha512-vqadSceJkBKHaTUszxI35uTuECGWtsHWK/mOwIJ4b9DUtYYySz6EJEk4gHg4ccutisJ/oRVCpFXHIKFb+osrKw==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.220.tgz",
|
||||
"integrity": "sha512-tkTJFnpR9VifvWX2fmkCAPkT6+8Wk/gVu8B5jsVekKZPiZoWRHmMXO30BnZn+f0TZhgYP+82PSX3S8crH1kn+w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -38244,9 +38244,9 @@
|
||||
]
|
||||
},
|
||||
"packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": {
|
||||
"version": "0.3.214",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.214.tgz",
|
||||
"integrity": "sha512-948RstHDhs0E69h+2dKYWIAL1kcwEme4zvtgNUwP8XpKXzWOhrTKmd6MAokHn25zwfuSx5d+HE0XHfFH6R4hLg==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.220.tgz",
|
||||
"integrity": "sha512-K+FWj+LcGhC1Z7wqeWoLxm1iemcba5xKpLLFVwYm4V6HyMx3ruYd/2r2TiQtjT+JWeNFWIys0ScHiItR6vWAiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -38257,9 +38257,9 @@
|
||||
]
|
||||
},
|
||||
"packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": {
|
||||
"version": "0.3.214",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.214.tgz",
|
||||
"integrity": "sha512-CEKlPFCPv+ee79utQMEDSsgeo2f27ulhNHpjrKIt1jXz5G04J7lAWN/QwvPDv9XaLBkWpyfqZWiPXlRcwuaO/w==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.220.tgz",
|
||||
"integrity": "sha512-rIwgq0UwQExWl6KrHUyC4w5KwpL9l6nd95aUTx6RitexaAuEw//xtfTVLnuE4hDDQZFkzEwpdKc3nxDWoGcUbA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -38270,9 +38270,9 @@
|
||||
]
|
||||
},
|
||||
"packages/server/node_modules/@anthropic-ai/claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": {
|
||||
"version": "0.3.214",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.214.tgz",
|
||||
"integrity": "sha512-cJMJfFoR9IWBZWnTtt9PnEm89hGOsZjoDNjOCEOF3+x+g/ANIXAjMJTcaQYv2HKh6MylWZ0AkxBASI+twkukaQ==",
|
||||
"version": "0.3.220",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.220.tgz",
|
||||
"integrity": "sha512-MuOuXhbr66HlGaWXD2f3w0k2PsvmnbkwcUZ0dAe2poFLdl72GC2dapwwOBefxm9QmoNqk9+jmv/dSKGOVWyvLw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -38648,7 +38648,7 @@
|
||||
},
|
||||
"packages/website": {
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.29.1",
|
||||
"@cloudflare/workers-types": "^4.20260317.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"description": "Paseo: voice-controlled development environment for local AI coding agents",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -876,7 +876,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
[pendingPermissions, agentId],
|
||||
);
|
||||
|
||||
const showRunningTurnFooter = context.status === "running";
|
||||
const showRunningTurnFooter = baseRenderModel.turnTiming.isActive;
|
||||
const pendingPermissionsNode = useMemo(
|
||||
() =>
|
||||
renderPendingPermissionsNode({
|
||||
|
||||
@@ -333,6 +333,26 @@ describe("pickAndPersistImages", () => {
|
||||
});
|
||||
|
||||
describe("dispatchComposerAgentMessage", () => {
|
||||
it("removes the optimistic prompt when the host rejects it", async () => {
|
||||
const rejection = new Error("Host rejected prompt");
|
||||
const client = createFakeSendClient({ rejection });
|
||||
const stream = createFakeStream();
|
||||
|
||||
await expect(
|
||||
dispatchComposerAgentMessage({
|
||||
client,
|
||||
agentId: "agent",
|
||||
text: "rejected prompt",
|
||||
attachments: [],
|
||||
encodeImages: passthroughEncodeImages,
|
||||
stream,
|
||||
}),
|
||||
).rejects.toBe(rejection);
|
||||
|
||||
expect(stream.head.get("agent")).toBeUndefined();
|
||||
expect(stream.tail.get("agent") ?? []).toEqual([]);
|
||||
});
|
||||
|
||||
it("sends text + image data + structured attachments and appends user_message to the tail when head is empty", async () => {
|
||||
const client = createFakeSendClient();
|
||||
const stream = createFakeStream();
|
||||
|
||||
@@ -186,40 +186,50 @@ export async function dispatchComposerAgentMessage(
|
||||
images: wirePayload.images,
|
||||
attachments: wirePayload.attachments,
|
||||
});
|
||||
appendUserMessageToStream(input.agentId, userMessage, input.stream);
|
||||
const imagesData = await input.encodeImages(wirePayload.images);
|
||||
await input.client.sendAgentMessage(input.agentId, input.text, {
|
||||
messageId,
|
||||
images: imagesData ?? [],
|
||||
attachments: wirePayload.attachments,
|
||||
});
|
||||
const rollbackOptimisticMessage = appendUserMessageToStream(
|
||||
input.agentId,
|
||||
userMessage,
|
||||
input.stream,
|
||||
);
|
||||
try {
|
||||
const imagesData = await input.encodeImages(wirePayload.images);
|
||||
await input.client.sendAgentMessage(input.agentId, input.text, {
|
||||
messageId,
|
||||
images: imagesData ?? [],
|
||||
attachments: wirePayload.attachments,
|
||||
});
|
||||
} catch (error) {
|
||||
rollbackOptimisticMessage();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function appendUserMessageToStream(
|
||||
agentId: string,
|
||||
userMessage: UserMessageItem,
|
||||
stream: AgentStreamWriter,
|
||||
): void {
|
||||
): () => void {
|
||||
const result = appendOptimisticUserMessageToStream({
|
||||
tail: stream.getTail(agentId) ?? [],
|
||||
head: stream.getHead(agentId) ?? [],
|
||||
message: userMessage,
|
||||
placement: "active-head",
|
||||
});
|
||||
if (result.changedHead) {
|
||||
stream.setHead((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(agentId, result.head);
|
||||
return next;
|
||||
const write = result.changedHead ? stream.setHead : stream.setTail;
|
||||
const items = result.changedHead ? result.head : result.tail;
|
||||
write((prev) => new Map(prev).set(agentId, items));
|
||||
|
||||
return () => {
|
||||
write((prev) => {
|
||||
const current = prev.get(agentId);
|
||||
if (!current) return prev;
|
||||
const nextItems = current.filter(
|
||||
(item) => item.id !== userMessage.id || item.kind !== "user_message" || !item.optimistic,
|
||||
);
|
||||
if (nextItems.length === current.length) return prev;
|
||||
return new Map(prev).set(agentId, nextItems);
|
||||
});
|
||||
}
|
||||
if (result.changedTail) {
|
||||
stream.setTail((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(agentId, result.tail);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface QueueComposerMessageInput {
|
||||
|
||||
@@ -45,6 +45,7 @@ interface ComposerWorkspaceAttachmentBinding {
|
||||
buildOutgoingAttachments: (normalAttachments: UserComposerAttachment[]) => ComposerAttachment[];
|
||||
removeAttachment: (input: RemoveWorkspaceAttachmentInput) => boolean;
|
||||
openAttachment: (input: OpenWorkspaceAttachmentInput) => boolean;
|
||||
beginSubmit: (attachments: readonly ComposerAttachment[]) => void;
|
||||
clearSentAttachments: (attachments: readonly ComposerAttachment[]) => void;
|
||||
completeSubmit: (input: CompleteSubmitInput) => void;
|
||||
resetSuppression: () => void;
|
||||
@@ -196,12 +197,22 @@ function useWorkspaceAttachmentBinding({
|
||||
setSuppressedKeys([]);
|
||||
}, []);
|
||||
|
||||
const beginSubmit = useCallback((attachments: readonly ComposerAttachment[]) => {
|
||||
const keys = attachments.filter(isWorkspaceAttachment).map(getAttachmentKey);
|
||||
if (keys.length === 0) return;
|
||||
setSuppressedKeys((current) => {
|
||||
const next = new Set(current);
|
||||
for (const key of keys) next.add(key);
|
||||
return next.size === current.length ? current : Array.from(next);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const completeSubmit = useCallback(
|
||||
({ result, outgoingAttachments }: CompleteSubmitInput) => {
|
||||
if (result === "submitted") {
|
||||
clearSentAttachments(outgoingAttachments);
|
||||
}
|
||||
if (result === "queued" || result === "submitted") {
|
||||
if (result === "queued" || result === "submitted" || result === "failed") {
|
||||
resetSuppression();
|
||||
}
|
||||
},
|
||||
@@ -213,6 +224,7 @@ function useWorkspaceAttachmentBinding({
|
||||
buildOutgoingAttachments,
|
||||
removeAttachment,
|
||||
openAttachment,
|
||||
beginSubmit,
|
||||
clearSentAttachments,
|
||||
completeSubmit,
|
||||
resetSuppression,
|
||||
|
||||
@@ -1095,6 +1095,7 @@ export function Composer({
|
||||
buildOutgoingAttachments,
|
||||
removeAttachment,
|
||||
openAttachment,
|
||||
beginSubmit,
|
||||
clearSentAttachments,
|
||||
completeSubmit,
|
||||
resetSuppression,
|
||||
@@ -1372,6 +1373,9 @@ export function Composer({
|
||||
queueMessage(queuedText, queuedAttachments);
|
||||
},
|
||||
submitMessage: async ({ message: submitText, attachments: submitAttachments }) => {
|
||||
if (submitBehavior !== "preserve-and-lock") {
|
||||
beginSubmit(submitAttachments);
|
||||
}
|
||||
await submitMessage(submitText, submitAttachments);
|
||||
},
|
||||
clearDraft,
|
||||
@@ -1393,6 +1397,7 @@ export function Composer({
|
||||
},
|
||||
[
|
||||
allowEmptySubmit,
|
||||
beginSubmit,
|
||||
clearDraft,
|
||||
completeSubmit,
|
||||
hasExternalContent,
|
||||
|
||||
@@ -159,10 +159,10 @@ const CATALOG_DATA = [
|
||||
id: "factory-droid",
|
||||
title: "Factory Droid",
|
||||
description: "Factory Droid - AI coding agent powered by Factory AI",
|
||||
version: "0.178.0",
|
||||
version: "0.179.0",
|
||||
iconId: "factory-droid",
|
||||
installLink: "https://factory.ai/product/cli",
|
||||
command: ["npx", "-y", "droid@0.178.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",
|
||||
|
||||
@@ -459,6 +459,7 @@ export function FilePane({
|
||||
preview,
|
||||
supportsEditing,
|
||||
});
|
||||
const canToggleMarkdownMode = isMarkdown && editable;
|
||||
const lineCount =
|
||||
preview?.kind === "text" ? (preview.content ?? "").split("\n").length : undefined;
|
||||
const errorMessage = getFileErrorMessage(query.error, t("panels.file.failedToLoad"));
|
||||
@@ -471,8 +472,8 @@ export function FilePane({
|
||||
preview={preview}
|
||||
version={version}
|
||||
filename={getFileNameFromPath(location.path) ?? location.path}
|
||||
markdownMode={isMarkdown ? markdownMode : undefined}
|
||||
onMarkdownModeChange={isMarkdown ? setMarkdownMode : undefined}
|
||||
markdownMode={canToggleMarkdownMode ? markdownMode : undefined}
|
||||
onMarkdownModeChange={canToggleMarkdownMode ? setMarkdownMode : undefined}
|
||||
lineCount={lineCount}
|
||||
editable={editable}
|
||||
disconnectedMessage={t("workspace.terminal.hostDisconnected")}
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
import { Platform } from "react-native";
|
||||
import type { ViewStyle } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
||||
import {
|
||||
useGenericKeyboardHandler,
|
||||
useReanimatedKeyboardAnimation,
|
||||
} from "react-native-keyboard-controller";
|
||||
import {
|
||||
useAnimatedStyle,
|
||||
useDerivedValue,
|
||||
@@ -40,6 +43,19 @@ export function KeyboardShiftProvider({ children }: { children: ReactNode }) {
|
||||
bottomInset.value = insets.bottom;
|
||||
}, [bottomInset, insets.bottom]);
|
||||
|
||||
useGenericKeyboardHandler(
|
||||
{
|
||||
onEnd: (event) => {
|
||||
"worklet";
|
||||
if (isIos) {
|
||||
keyboardHeight.value = -event.height;
|
||||
keyboardProgress.value = event.progress;
|
||||
}
|
||||
},
|
||||
},
|
||||
[isIos, keyboardHeight, keyboardProgress],
|
||||
);
|
||||
|
||||
const shift = useDerivedValue(() => {
|
||||
"worklet";
|
||||
return resolveKeyboardShift({
|
||||
|
||||
@@ -44,6 +44,7 @@ const { theme, snapshotState, configState, patchConfigMock, openProviderSettings
|
||||
);
|
||||
|
||||
vi.mock("react-native", () => ({
|
||||
Platform: { OS: "web" },
|
||||
View: ({ children, testID }: { children?: React.ReactNode; testID?: string }) =>
|
||||
React.createElement("div", { "data-testid": testID }, children),
|
||||
Text: ({ children }: { children?: React.ReactNode }) =>
|
||||
@@ -90,7 +91,7 @@ vi.mock("react-native-unistyles", () => ({
|
||||
create: (factory: unknown) =>
|
||||
typeof factory === "function" ? (factory as (t: typeof theme) => unknown)(theme) : factory,
|
||||
},
|
||||
useUnistyles: () => ({ theme }),
|
||||
useUnistyles: () => ({ theme, rt: { breakpoint: "md" } }),
|
||||
}));
|
||||
|
||||
vi.mock("lucide-react-native", () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type PressableStateCallbackType,
|
||||
} from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
import { useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { useHostFeature } from "@/runtime/host-features";
|
||||
@@ -178,6 +179,7 @@ function ProviderRow({
|
||||
}: ProviderRowProps) {
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useUnistyles();
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const ProviderIcon = getProviderIcon(def.id);
|
||||
const providerError =
|
||||
enabled &&
|
||||
@@ -229,10 +231,10 @@ function ProviderRow({
|
||||
<Text style={settingsStyles.rowTitle} numberOfLines={1}>
|
||||
{def.label}
|
||||
</Text>
|
||||
<Text style={styles.separator}>·</Text>
|
||||
<StatusIndicator status={providerStatus} />
|
||||
{!isCompact ? <Text style={styles.separator}>·</Text> : null}
|
||||
<StatusIndicator status={providerStatus} compact={isCompact} />
|
||||
</View>
|
||||
{providerError ? (
|
||||
{providerError && !isCompact ? (
|
||||
<Text style={styles.errorText} numberOfLines={3}>
|
||||
{providerError}
|
||||
</Text>
|
||||
@@ -246,18 +248,20 @@ function ProviderRow({
|
||||
disabled={isToggling || isRemoving}
|
||||
accessibilityLabel={t("settings.providers.enableProvider", { name: def.label })}
|
||||
/>
|
||||
{canRemove ? (
|
||||
<ProviderActionsMenu
|
||||
providerId={def.id}
|
||||
providerLabel={def.label}
|
||||
isRemoving={isRemoving}
|
||||
iconSize={theme.iconSize.sm}
|
||||
foregroundColor={theme.colors.foreground}
|
||||
foregroundMutedColor={theme.colors.foregroundMuted}
|
||||
dangerColor={theme.colors.statusDanger}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
) : null}
|
||||
<View style={styles.menuSlot}>
|
||||
{canRemove ? (
|
||||
<ProviderActionsMenu
|
||||
providerId={def.id}
|
||||
providerLabel={def.label}
|
||||
isRemoving={isRemoving}
|
||||
iconSize={theme.iconSize.sm}
|
||||
foregroundColor={theme.colors.foreground}
|
||||
foregroundMutedColor={theme.colors.foregroundMuted}
|
||||
dangerColor={theme.colors.statusDanger}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
@@ -278,7 +282,7 @@ function getDotColor(tone: StatusTone, theme: ReturnType<typeof useUnistyles>["t
|
||||
}
|
||||
}
|
||||
|
||||
function StatusIndicator({ status }: { status: ProviderStatus }) {
|
||||
function StatusIndicator({ status, compact }: { status: ProviderStatus; compact: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useUnistyles();
|
||||
const dotStyle = useMemo(
|
||||
@@ -293,15 +297,19 @@ function StatusIndicator({ status }: { status: ProviderStatus }) {
|
||||
) : (
|
||||
<View style={dotStyle} />
|
||||
)}
|
||||
<Text style={styles.statusLabel}>{status.label}</Text>
|
||||
{status.modelCount !== null ? (
|
||||
{!compact ? (
|
||||
<>
|
||||
<Text style={styles.separator}>·</Text>
|
||||
<Text style={styles.statusLabel}>
|
||||
{status.modelCount === 1
|
||||
? t("settings.providers.models.one")
|
||||
: t("settings.providers.models.many", { count: status.modelCount })}
|
||||
</Text>
|
||||
<Text style={styles.statusLabel}>{status.label}</Text>
|
||||
{status.modelCount !== null ? (
|
||||
<>
|
||||
<Text style={styles.separator}>·</Text>
|
||||
<Text style={styles.statusLabel}>
|
||||
{status.modelCount === 1
|
||||
? t("settings.providers.models.one")
|
||||
: t("settings.providers.models.many", { count: status.modelCount })}
|
||||
</Text>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -536,6 +544,10 @@ const styles = StyleSheet.create((theme) => ({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
menuSlot: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
},
|
||||
menuButtonHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
|
||||
@@ -22,6 +22,36 @@ function assistant(id: string, timestamp: Date): StreamItem {
|
||||
}
|
||||
|
||||
describe("deriveStreamTurnTiming", () => {
|
||||
it("reserves a running footer for an optimistic prompt before the host starts the turn", () => {
|
||||
const optimisticPrompt = {
|
||||
...user("optimistic", new Date("2026-05-15T00:00:00.000Z")),
|
||||
optimistic: true as const,
|
||||
};
|
||||
|
||||
const timing = deriveStreamTurnTiming({
|
||||
agentStatus: "idle",
|
||||
tail: [],
|
||||
head: [optimisticPrompt],
|
||||
});
|
||||
|
||||
assert.equal(timing.isActive, true);
|
||||
});
|
||||
|
||||
it("does not start elapsed time from an optimistic prompt", () => {
|
||||
const optimisticPrompt = {
|
||||
...user("optimistic", new Date("2026-05-15T00:00:00.000Z")),
|
||||
optimistic: true as const,
|
||||
};
|
||||
|
||||
const timing = deriveStreamTurnTiming({
|
||||
agentStatus: "running",
|
||||
tail: [],
|
||||
head: [optimisticPrompt],
|
||||
});
|
||||
|
||||
assert.equal(timing.runningStartedAt, null);
|
||||
});
|
||||
|
||||
it("uses the last user message as the running turn start", () => {
|
||||
const firstUserAt = new Date("2026-05-15T00:00:00.000Z");
|
||||
const secondUserAt = new Date("2026-05-15T00:01:00.000Z");
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface TurnTiming {
|
||||
export interface StreamTurnTiming {
|
||||
byAssistantId: Map<string, TurnTiming>;
|
||||
runningStartedAt: Date | null;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export function deriveStreamTurnTiming(params: {
|
||||
@@ -18,6 +19,8 @@ export function deriveStreamTurnTiming(params: {
|
||||
}): StreamTurnTiming {
|
||||
const byAssistantId = new Map<string, TurnTiming>();
|
||||
let currentUserAt: Date | null = null;
|
||||
let currentAuthoritativeUserAt: Date | null = null;
|
||||
let currentUserIsOptimistic = false;
|
||||
let currentLastItemAt: Date | null = null;
|
||||
let currentAssistantIds: string[] = [];
|
||||
|
||||
@@ -39,6 +42,8 @@ export function deriveStreamTurnTiming(params: {
|
||||
if (item.kind === "user_message") {
|
||||
flushCompletedTurn();
|
||||
currentUserAt = item.timestamp;
|
||||
currentAuthoritativeUserAt = item.optimistic ? null : item.timestamp;
|
||||
currentUserIsOptimistic = item.optimistic === true;
|
||||
currentLastItemAt = null;
|
||||
currentAssistantIds = [];
|
||||
return;
|
||||
@@ -59,10 +64,8 @@ export function deriveStreamTurnTiming(params: {
|
||||
visitItem(item);
|
||||
}
|
||||
|
||||
const runningStartedAt =
|
||||
params.agentStatus === "running"
|
||||
? (findLastUserMessageTimestamp(params.head) ?? currentUserAt)
|
||||
: null;
|
||||
const isRunning = params.agentStatus === "running";
|
||||
const runningStartedAt = isRunning ? currentAuthoritativeUserAt : null;
|
||||
if (params.agentStatus !== "running") {
|
||||
flushCompletedTurn();
|
||||
}
|
||||
@@ -70,15 +73,6 @@ export function deriveStreamTurnTiming(params: {
|
||||
return {
|
||||
byAssistantId,
|
||||
runningStartedAt,
|
||||
isActive: isRunning || currentUserIsOptimistic,
|
||||
};
|
||||
}
|
||||
|
||||
function findLastUserMessageTimestamp(items: StreamItem[]): Date | null {
|
||||
for (let i = items.length - 1; i >= 0; i -= 1) {
|
||||
const item = items[i];
|
||||
if (item?.kind === "user_message") {
|
||||
return item.timestamp;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"description": "Paseo CLI - control your AI coding agents from the command line",
|
||||
"bin": {
|
||||
"paseo": "bin/paseo"
|
||||
@@ -27,9 +27,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/client": "0.2.0-beta.4",
|
||||
"@getpaseo/protocol": "0.2.0-beta.4",
|
||||
"@getpaseo/server": "0.2.0-beta.4",
|
||||
"@getpaseo/client": "0.2.2",
|
||||
"@getpaseo/protocol": "0.2.2",
|
||||
"@getpaseo/server": "0.2.2",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { generateLocalPairingOffer, loadConfig, resolvePaseoHome } from "@getpas
|
||||
import { tryConnectToDaemon } from "../../utils/client.js";
|
||||
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
|
||||
import { addJsonOption } from "../../utils/command-options.js";
|
||||
import { formatPairingInstructions } from "../../output/pairing.js";
|
||||
|
||||
interface PairOptions {
|
||||
home?: string;
|
||||
@@ -97,6 +98,11 @@ function outputPairingResult(
|
||||
return;
|
||||
}
|
||||
|
||||
const qrBlock = pairing.qr ? `${pairing.qr}\n` : "";
|
||||
process.stdout.write(`\nScan to pair:\n${qrBlock}${pairing.url}\n`);
|
||||
process.stdout.write(
|
||||
formatPairingInstructions({
|
||||
url: pairing.url,
|
||||
qr: pairing.qr,
|
||||
columns: process.stdout.columns,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type DaemonStartOptions,
|
||||
} from "./daemon/local-daemon.js";
|
||||
import { tryConnectToDaemon } from "../utils/client.js";
|
||||
import { formatPairingInstructions } from "../output/pairing.js";
|
||||
|
||||
interface OnboardOptions extends DaemonStartOptions {
|
||||
timeout?: string;
|
||||
@@ -520,11 +521,13 @@ export async function runOnboard(options: OnboardOptions): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
renderNote(
|
||||
pairing.qr ?? "QR is unavailable in this terminal. Use the pairing link below.",
|
||||
"Scan to pair",
|
||||
process.stdout.write(
|
||||
formatPairingInstructions({
|
||||
url: pairing.url,
|
||||
qr: pairing.qr,
|
||||
columns: process.stdout.columns,
|
||||
}),
|
||||
);
|
||||
renderNote(pairing.url, "Pairing link");
|
||||
printNextSteps(pairing.url, paseoHome, richUi);
|
||||
if (richUi) {
|
||||
outro("Paseo is ready!");
|
||||
|
||||
30
packages/cli/src/output/pairing.test.ts
Normal file
30
packages/cli/src/output/pairing.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatPairingInstructions } from "./pairing.js";
|
||||
|
||||
const QR = "\u001b[47m\u001b[30m \n ████ \n \u001b[0m";
|
||||
const URL = "https://app.paseo.sh/#offer=pairing-offer";
|
||||
|
||||
describe("formatPairingInstructions", () => {
|
||||
it("prints the QR and an unmodified pairing-link line when the terminal is wide enough", () => {
|
||||
const output = formatPairingInstructions({ qr: QR, url: URL, columns: 7 });
|
||||
|
||||
expect(output).toContain(QR);
|
||||
expect(output.split("\n")).toContain(URL);
|
||||
});
|
||||
|
||||
it("does not print a QR that would reach the terminal edge", () => {
|
||||
const output = formatPairingInstructions({ qr: QR, url: URL, columns: 6 });
|
||||
|
||||
expect(output).not.toContain(QR);
|
||||
expect(output).toContain("Resize the terminal to at least 7 columns");
|
||||
expect(output.split("\n")).toContain(URL);
|
||||
});
|
||||
|
||||
it("does not risk printing a QR when terminal width is unknown", () => {
|
||||
const output = formatPairingInstructions({ qr: QR, url: URL });
|
||||
|
||||
expect(output).not.toContain(QR);
|
||||
expect(output).toContain("terminal width could not be detected");
|
||||
expect(output.split("\n")).toContain(URL);
|
||||
});
|
||||
});
|
||||
37
packages/cli/src/output/pairing.ts
Normal file
37
packages/cli/src/output/pairing.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
const ANSI_PATTERN = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-9;]*m`, "g");
|
||||
|
||||
interface PairingInstructions {
|
||||
url: string;
|
||||
qr: string | null;
|
||||
columns?: number;
|
||||
}
|
||||
|
||||
function visibleWidth(value: string): number {
|
||||
return Math.max(
|
||||
...value
|
||||
.replace(ANSI_PATTERN, "")
|
||||
.split("\n")
|
||||
.map((line) => line.length),
|
||||
);
|
||||
}
|
||||
|
||||
function formatQr(qr: string | null, columns: number | undefined): string {
|
||||
if (!qr) {
|
||||
return "QR code is unavailable. Use the pairing link below.";
|
||||
}
|
||||
|
||||
if (columns === undefined) {
|
||||
return "QR code not shown because terminal width could not be detected.";
|
||||
}
|
||||
|
||||
const width = visibleWidth(qr);
|
||||
if (columns <= width) {
|
||||
return `QR code not shown. Resize the terminal to at least ${width + 1} columns, then run this command again.`;
|
||||
}
|
||||
|
||||
return qr;
|
||||
}
|
||||
|
||||
export function formatPairingInstructions({ url, qr, columns }: PairingInstructions): string {
|
||||
return `\nScan to pair:\n${formatQr(qr, columns)}\n\nPairing link:\n${url}\n`;
|
||||
}
|
||||
@@ -102,6 +102,38 @@ const EXPECTED_CLAUDE_MODELS = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
const EXPECTED_CLAUDE_CONTEXT_MODELS = [
|
||||
{
|
||||
id: "claude-opus-5[1m]",
|
||||
model: "Opus 5 1M",
|
||||
descriptionFragment: "1M context window",
|
||||
},
|
||||
{
|
||||
id: "claude-opus-5",
|
||||
model: "Opus 5",
|
||||
descriptionFragment: "200K context window",
|
||||
},
|
||||
{
|
||||
id: "claude-fable-5[1m]",
|
||||
model: "Fable 5 1M",
|
||||
descriptionFragment: "1M context window",
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-5[1m]",
|
||||
model: "Sonnet 5 1M",
|
||||
descriptionFragment: "1M context window",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const EXPECTED_CLAUDE_CATALOG_MODELS = [
|
||||
...new Map(
|
||||
[...EXPECTED_CLAUDE_MODELS, ...EXPECTED_CLAUDE_CONTEXT_MODELS].map((model) => [
|
||||
model.id,
|
||||
model,
|
||||
]),
|
||||
).values(),
|
||||
];
|
||||
|
||||
let claudeModelIdsFromJson: string[] = [];
|
||||
let claudeModelsFromJson: ProviderModel[] = [];
|
||||
|
||||
@@ -134,18 +166,18 @@ async function runProviderModelsJson(provider: string): Promise<ProviderModel[]>
|
||||
function assertClaudeModels(data: ProviderModel[]): void {
|
||||
assert.strictEqual(
|
||||
data.length,
|
||||
EXPECTED_CLAUDE_MODELS.length,
|
||||
EXPECTED_CLAUDE_CATALOG_MODELS.length,
|
||||
"claude output should match the current catalog size",
|
||||
);
|
||||
|
||||
const byId = new Map(data.map((model) => [model.id, model]));
|
||||
const ids = [...byId.keys()].sort();
|
||||
const expectedIds = EXPECTED_CLAUDE_MODELS.map((model) => model.id).sort();
|
||||
const expectedIds = EXPECTED_CLAUDE_CATALOG_MODELS.map((model) => model.id).sort();
|
||||
|
||||
assert.strictEqual(byId.size, data.length, "claude model IDs should be unique");
|
||||
assert.deepStrictEqual(ids, expectedIds, "claude IDs should match the current catalog");
|
||||
|
||||
for (const expectedModel of EXPECTED_CLAUDE_MODELS) {
|
||||
for (const expectedModel of EXPECTED_CLAUDE_CATALOG_MODELS) {
|
||||
const actualModel = byId.get(expectedModel.id);
|
||||
assert(actualModel, `claude output should include ${expectedModel.id}`);
|
||||
assert.strictEqual(
|
||||
@@ -383,7 +415,7 @@ try {
|
||||
const lines = result.stdout.trim().split("\n").filter(Boolean);
|
||||
assert.strictEqual(
|
||||
lines.length,
|
||||
EXPECTED_CLAUDE_MODELS.length,
|
||||
EXPECTED_CLAUDE_CATALOG_MODELS.length,
|
||||
"should have one line per Claude catalog model",
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
@@ -393,7 +425,7 @@ try {
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
[...lines].sort(),
|
||||
EXPECTED_CLAUDE_MODELS.map((model) => model.id).sort(),
|
||||
EXPECTED_CLAUDE_CATALOG_MODELS.map((model) => model.id).sort(),
|
||||
"--quiet should print the current Claude catalog IDs",
|
||||
);
|
||||
assert(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/client",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"description": "Paseo client SDK package",
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -35,8 +35,8 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getpaseo/protocol": "0.2.0-beta.4",
|
||||
"@getpaseo/relay": "0.2.0-beta.4",
|
||||
"@getpaseo/protocol": "0.2.2",
|
||||
"@getpaseo/relay": "0.2.2",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.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.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"description": "Native module for two way audio streaming",
|
||||
"keywords": [
|
||||
"ExpoTwoWayAudio",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.map"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/protocol",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"description": "Paseo shared protocol schemas and wire types",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"description": "Paseo relay for bridging daemon and client connections",
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"description": "Paseo backend server",
|
||||
"files": [
|
||||
"dist/server",
|
||||
@@ -65,12 +65,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.3.214",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.3.220",
|
||||
"@anthropic-ai/sdk": "^0.104.2",
|
||||
"@getpaseo/client": "0.2.0-beta.4",
|
||||
"@getpaseo/highlight": "0.2.0-beta.4",
|
||||
"@getpaseo/protocol": "0.2.0-beta.4",
|
||||
"@getpaseo/relay": "0.2.0-beta.4",
|
||||
"@getpaseo/client": "0.2.2",
|
||||
"@getpaseo/highlight": "0.2.2",
|
||||
"@getpaseo/protocol": "0.2.2",
|
||||
"@getpaseo/relay": "0.2.2",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
convertClaudeHistoryEntry,
|
||||
normalizeClaudeAskUserQuestionRequestInput,
|
||||
normalizeClaudeAskUserQuestionUpdatedInput,
|
||||
resolveClaudeCodeVersion,
|
||||
toClaudeSdkMcpConfig,
|
||||
} from "./agent.js";
|
||||
import { claudeProjectDirSync } from "./project-dir.js";
|
||||
@@ -405,6 +406,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
const client = new ClaudeAgentClient({
|
||||
logger,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
resolveVersion: async () => "2.1.219",
|
||||
configDir: emptyConfigDir,
|
||||
});
|
||||
const { models } = await client.fetchCatalog({
|
||||
@@ -414,10 +416,14 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
});
|
||||
|
||||
expect(models.map((m) => m.id)).toEqual([
|
||||
"claude-opus-5[1m]",
|
||||
"claude-opus-5",
|
||||
"claude-fable-5[1m]",
|
||||
"claude-fable-5",
|
||||
"claude-opus-4-8[1m]",
|
||||
"claude-opus-4-8",
|
||||
"claude-sonnet-5",
|
||||
"claude-sonnet-5[1m]",
|
||||
"claude-opus-4-7[1m]",
|
||||
"claude-opus-4-7",
|
||||
"claude-opus-4-6[1m]",
|
||||
@@ -433,7 +439,30 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
}
|
||||
|
||||
const defaultModel = models.find((m) => m.isDefault);
|
||||
expect(defaultModel?.id).toBe("claude-opus-4-8");
|
||||
expect(defaultModel?.id).toBe("claude-opus-5[1m]");
|
||||
} finally {
|
||||
await fs.rm(emptyConfigDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("preserves the catalog when Claude Code version detection fails", async () => {
|
||||
const emptyConfigDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-models-empty-"));
|
||||
try {
|
||||
const client = new ClaudeAgentClient({
|
||||
logger,
|
||||
resolveVersion: async () => {
|
||||
throw new Error("unrecognized version output");
|
||||
},
|
||||
configDir: emptyConfigDir,
|
||||
});
|
||||
const { models } = await client.fetchCatalog({
|
||||
scope: "workspace",
|
||||
cwd: "/tmp/claude-models",
|
||||
force: false,
|
||||
});
|
||||
|
||||
expect(models.find((model) => model.isDefault)?.id).toBe("claude-opus-5[1m]");
|
||||
expect(models.map((model) => model.id)).toContain("claude-fable-5[1m]");
|
||||
} finally {
|
||||
await fs.rm(emptyConfigDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -445,6 +474,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
const client = new ClaudeAgentClient({
|
||||
logger,
|
||||
resolveBinary: async () => "/test/claude/bin",
|
||||
resolveVersion: async () => "2.1.219",
|
||||
configDir: emptyConfigDir,
|
||||
});
|
||||
const { models } = await client.fetchCatalog({
|
||||
@@ -456,6 +486,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
return models.find((model) => model.id === modelId)?.thinkingOptions?.map(({ id }) => id);
|
||||
};
|
||||
|
||||
expect(getThinkingIds("claude-opus-5")).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");
|
||||
@@ -473,6 +504,10 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
describe("ClaudeAgentClient binary resolution", () => {
|
||||
const logger = createTestLogger();
|
||||
|
||||
test("resolves the installed Claude Code version", async () => {
|
||||
await expect(resolveClaudeCodeVersion()).resolves.toMatch(/^\d+\.\d+\.\d+$/);
|
||||
});
|
||||
|
||||
test("loads user, project, and local Claude settings", async () => {
|
||||
const queryReturn = vi.fn();
|
||||
queryReturn.mockResolvedValue(undefined);
|
||||
@@ -2081,10 +2116,10 @@ describe("ClaudeAgentSession context window usage", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("native 1M Claude models seed active context window usage from the catalog", async () => {
|
||||
test("selected 1M Claude models seed active context window usage from the catalog", async () => {
|
||||
const session = await createSessionForTurns(
|
||||
[[createInitMessage(), createMessageStartEvent(), createSuccessResult()]],
|
||||
{ model: "claude-sonnet-5" },
|
||||
{ model: "claude-sonnet-5[1m]" },
|
||||
);
|
||||
|
||||
try {
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
import {
|
||||
CLAUDE_DISABLED_THINKING_OPTION_ID,
|
||||
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
|
||||
parseClaudeCodeVersion,
|
||||
resolveClaudeDisabledThinkingForModel,
|
||||
} from "./model-manifest.js";
|
||||
import { parsePartialJsonObject } from "./partial-json.js";
|
||||
@@ -363,6 +364,7 @@ interface ClaudeAgentClientOptions {
|
||||
runtimeSettings?: ProviderRuntimeSettings;
|
||||
queryFactory?: ClaudeQueryFactory;
|
||||
resolveBinary?: () => Promise<string>;
|
||||
resolveVersion?: () => Promise<string>;
|
||||
configDir?: string;
|
||||
}
|
||||
|
||||
@@ -1439,6 +1441,7 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
private readonly runtimeSettings?: ProviderRuntimeSettings;
|
||||
private readonly queryFactory?: ClaudeQueryFactory;
|
||||
private readonly resolveBinary: () => Promise<string>;
|
||||
private readonly resolveVersion: () => Promise<string>;
|
||||
private readonly configDir?: string;
|
||||
|
||||
constructor(options: ClaudeAgentClientOptions) {
|
||||
@@ -1447,6 +1450,8 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
this.runtimeSettings = options.runtimeSettings;
|
||||
this.queryFactory = options.queryFactory;
|
||||
this.resolveBinary = options.resolveBinary ?? (() => resolveClaudeBinary(this.runtimeSettings));
|
||||
this.resolveVersion =
|
||||
options.resolveVersion ?? (() => resolveClaudeCodeVersion(this.runtimeSettings));
|
||||
this.configDir = options.configDir;
|
||||
}
|
||||
|
||||
@@ -1498,7 +1503,17 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
|
||||
async fetchCatalog(_options: FetchCatalogOptions): Promise<ProviderCatalog> {
|
||||
// Claude exposes a global catalog here; cwd/force are intentionally irrelevant.
|
||||
const models = await getClaudeModelsWithSettings(this.logger, this.configDir);
|
||||
let claudeCodeVersion: string | undefined;
|
||||
try {
|
||||
claudeCodeVersion = await this.resolveVersion();
|
||||
} catch (error) {
|
||||
this.logger.warn({ err: error }, "Failed to resolve Claude Code version for model catalog");
|
||||
}
|
||||
const models = await getClaudeModelsWithSettings(
|
||||
this.logger,
|
||||
this.configDir,
|
||||
claudeCodeVersion,
|
||||
);
|
||||
const modes = detectIneligibleAutoModeTransport(
|
||||
createProviderEnv({ baseEnv: process.env, runtimeSettings: this.runtimeSettings }),
|
||||
)
|
||||
@@ -1620,6 +1635,29 @@ async function resolveClaudeBinary(runtimeSettings?: ProviderRuntimeSettings): P
|
||||
);
|
||||
}
|
||||
|
||||
export async function resolveClaudeCodeVersion(
|
||||
runtimeSettings?: ProviderRuntimeSettings,
|
||||
): Promise<string> {
|
||||
const launch = await resolveProviderLaunch({
|
||||
commandConfig: runtimeSettings?.command,
|
||||
defaultBinary: "claude",
|
||||
});
|
||||
const availability = await checkProviderLaunchAvailable(launch);
|
||||
if (!availability.available) {
|
||||
throw new Error("Claude binary not found while resolving Claude Code version");
|
||||
}
|
||||
const executable = availability.resolvedPath ?? launch.command;
|
||||
const { stdout, stderr } = await execCommand(executable, [...launch.args, "--version"], {
|
||||
...createProviderEnvSpec({ runtimeSettings }),
|
||||
timeout: 5_000,
|
||||
});
|
||||
const version = parseClaudeCodeVersion(`${stdout}\n${stderr}`);
|
||||
if (!version) {
|
||||
throw new Error("Unable to parse Claude Code version from --version output");
|
||||
}
|
||||
return version.join(".");
|
||||
}
|
||||
|
||||
async function resolveClaudeAuth(
|
||||
launch: ResolvedProviderLaunch,
|
||||
availability: { resolvedPath: string | null },
|
||||
|
||||
@@ -6,7 +6,8 @@ interface ClaudeModelManifestEntry {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
isDefault?: boolean;
|
||||
defaultPriority?: number;
|
||||
minimumClaudeCodeVersion?: string;
|
||||
contextWindowMaxTokens?: number;
|
||||
effortLevels?: readonly ClaudeEffortLevel[];
|
||||
supportsThinkingDisabled?: boolean;
|
||||
@@ -30,11 +31,39 @@ export const CLAUDE_DISABLED_THINKING_OPTION_ID = "off";
|
||||
export const CLAUDE_ULTRACODE_THINKING_OPTION_ID = "ultracode";
|
||||
|
||||
export const CLAUDE_MODEL_MANIFEST = [
|
||||
{
|
||||
id: "claude-opus-5[1m]",
|
||||
label: "Opus 5 1M",
|
||||
description: "Opus 5 with 1M context window",
|
||||
defaultPriority: 2,
|
||||
minimumClaudeCodeVersion: "2.1.219",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsThinkingDisabled: true,
|
||||
},
|
||||
{
|
||||
id: "claude-opus-5",
|
||||
label: "Opus 5",
|
||||
description: "Opus 5 · 200K context window",
|
||||
minimumClaudeCodeVersion: "2.1.219",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsThinkingDisabled: true,
|
||||
},
|
||||
{
|
||||
id: "claude-fable-5[1m]",
|
||||
label: "Fable 5 1M",
|
||||
description: "Fable 5 with 1M context window",
|
||||
minimumClaudeCodeVersion: "2.1.169",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
},
|
||||
{
|
||||
id: "claude-fable-5",
|
||||
label: "Fable 5",
|
||||
description: "Fable 5 · Most powerful model",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
minimumClaudeCodeVersion: "2.1.169",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
},
|
||||
{
|
||||
@@ -49,8 +78,8 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
{
|
||||
id: "claude-opus-4-8",
|
||||
label: "Opus 4.8",
|
||||
description: "Opus 4.8 · Latest release",
|
||||
isDefault: true,
|
||||
description: "Opus 4.8 · Previous release",
|
||||
defaultPriority: 1,
|
||||
contextWindowMaxTokens: 200_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsThinkingDisabled: true,
|
||||
@@ -60,6 +89,14 @@ export const CLAUDE_MODEL_MANIFEST = [
|
||||
id: "claude-sonnet-5",
|
||||
label: "Sonnet 5",
|
||||
description: "Sonnet 5 · Best for everyday tasks",
|
||||
contextWindowMaxTokens: 200_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsThinkingDisabled: true,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-5[1m]",
|
||||
label: "Sonnet 5 1M",
|
||||
description: "Sonnet 5 with 1M context window",
|
||||
contextWindowMaxTokens: 1_000_000,
|
||||
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
|
||||
supportsThinkingDisabled: true,
|
||||
@@ -147,31 +184,73 @@ function buildThinkingOptions(
|
||||
return options;
|
||||
}
|
||||
|
||||
export function getClaudeManifestModels(): AgentModelDefinition[] {
|
||||
return CLAUDE_MODEL_MANIFEST.map((model) => {
|
||||
export function getClaudeManifestModels(claudeCodeVersion?: string): AgentModelDefinition[] {
|
||||
const availableModels: readonly ClaudeModelManifestEntry[] = CLAUDE_MODEL_MANIFEST.filter(
|
||||
(model) => isModelAvailableInClaudeCode(model, claudeCodeVersion),
|
||||
);
|
||||
const defaultModel = availableModels.reduce<ClaudeModelManifestEntry | undefined>(
|
||||
(selected, candidate) =>
|
||||
(candidate.defaultPriority ?? 0) > (selected?.defaultPriority ?? 0) ? candidate : selected,
|
||||
undefined,
|
||||
);
|
||||
|
||||
return availableModels.map((model) => {
|
||||
const thinkingOptions = buildThinkingOptions(
|
||||
"effortLevels" in model ? model.effortLevels : undefined,
|
||||
"supportsThinkingDisabled" in model && model.supportsThinkingDisabled,
|
||||
model.effortLevels,
|
||||
model.supportsThinkingDisabled === true,
|
||||
);
|
||||
return {
|
||||
const definition: AgentModelDefinition = {
|
||||
provider: "claude",
|
||||
id: model.id,
|
||||
label: model.label,
|
||||
description: model.description,
|
||||
...("isDefault" in model && model.isDefault ? { isDefault: true } : {}),
|
||||
...(model.contextWindowMaxTokens !== undefined
|
||||
? { contextWindowMaxTokens: model.contextWindowMaxTokens }
|
||||
: {}),
|
||||
...(thinkingOptions
|
||||
? {
|
||||
thinkingOptions,
|
||||
defaultThinkingOptionId: "effortLevels" in model ? model.effortLevels?.[0] : undefined,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
if (model === defaultModel) {
|
||||
definition.isDefault = true;
|
||||
}
|
||||
if (model.contextWindowMaxTokens !== undefined) {
|
||||
definition.contextWindowMaxTokens = model.contextWindowMaxTokens;
|
||||
}
|
||||
if (thinkingOptions) {
|
||||
definition.thinkingOptions = thinkingOptions;
|
||||
definition.defaultThinkingOptionId = model.effortLevels?.[0];
|
||||
}
|
||||
return definition;
|
||||
});
|
||||
}
|
||||
|
||||
function isModelAvailableInClaudeCode(
|
||||
model: ClaudeModelManifestEntry,
|
||||
claudeCodeVersion: string | undefined,
|
||||
): boolean {
|
||||
if (!model.minimumClaudeCodeVersion || claudeCodeVersion === undefined) {
|
||||
return true;
|
||||
}
|
||||
return compareVersions(claudeCodeVersion, model.minimumClaudeCodeVersion) >= 0;
|
||||
}
|
||||
|
||||
function compareVersions(left: string, right: string): number {
|
||||
const leftParts = parseClaudeCodeVersion(left);
|
||||
const rightParts = parseClaudeCodeVersion(right);
|
||||
if (!leftParts || !rightParts) {
|
||||
return -1;
|
||||
}
|
||||
for (let index = 0; index < leftParts.length; index += 1) {
|
||||
const difference = leftParts[index] - rightParts[index];
|
||||
if (difference !== 0) {
|
||||
return difference;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function parseClaudeCodeVersion(value: string): [number, number, number] | null {
|
||||
const match =
|
||||
value.match(/\b(\d+)\.(\d+)\.(\d+)\s+\(Claude Code\)/i) ??
|
||||
value.match(/\b(\d+)\.(\d+)\.(\d+)\b/);
|
||||
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
||||
}
|
||||
|
||||
export interface ClaudeDisabledThinkingResolution {
|
||||
supported: boolean;
|
||||
fallbackThinkingOptionId: string | undefined;
|
||||
@@ -228,7 +307,7 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined)
|
||||
}
|
||||
|
||||
const singleSegmentMatch = trimmed.match(
|
||||
/^(?:claude[-_ ])?(fable|opus|sonnet|haiku)[-_ ]+(\d+)(\[1m\])?(?:[-_ ]+\d{8})?$/i,
|
||||
/^(?:claude[-_ ])?(fable|opus|sonnet|haiku)[-_ ]+(\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
|
||||
);
|
||||
if (singleSegmentMatch) {
|
||||
return normalizeSingleSegmentClaudeModelId(
|
||||
@@ -239,7 +318,7 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined)
|
||||
}
|
||||
|
||||
const runtimeMatch = trimmed.match(
|
||||
/^(?:claude[-_ ])?(opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?(?:[-_ ]+\d{8})?$/i,
|
||||
/^(?:claude[-_ ])?(opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
|
||||
);
|
||||
if (!runtimeMatch) {
|
||||
return null;
|
||||
@@ -249,7 +328,7 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined)
|
||||
runtimeMatch[1],
|
||||
runtimeMatch[2],
|
||||
runtimeMatch[3],
|
||||
Boolean(runtimeMatch[4]),
|
||||
trimmed.toLowerCase().includes("[1m]"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -276,7 +355,7 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined):
|
||||
const normalizedModelId = normalizeSingleSegmentClaudeModelId(
|
||||
singleSegmentMatch[1],
|
||||
singleSegmentMatch[2],
|
||||
Boolean(singleSegmentMatch[3]),
|
||||
trimmed.toLowerCase().includes("[1m]"),
|
||||
);
|
||||
if (normalizedModelId) {
|
||||
return normalizedModelId;
|
||||
@@ -294,7 +373,7 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined):
|
||||
runtimeMatch[1],
|
||||
runtimeMatch[2],
|
||||
runtimeMatch[3],
|
||||
Boolean(runtimeMatch[4]),
|
||||
trimmed.toLowerCase().includes("[1m]"),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
|
||||
claudeManifestModelSupportsFastMode,
|
||||
normalizeClaudeManifestModelId,
|
||||
parseClaudeCodeVersion,
|
||||
resolveClaudeDisabledThinkingForModel,
|
||||
} from "./model-manifest.js";
|
||||
import { findClaudeModel, getClaudeModels, normalizeClaudeRuntimeModelId } from "./models.js";
|
||||
@@ -38,14 +39,25 @@ async function createClaudeConfigDirWithRawSettings(settings: string): Promise<s
|
||||
return configDir;
|
||||
}
|
||||
|
||||
function createCatalogClient(claudeCodeVersion = "2.1.219"): ClaudeAgentClient {
|
||||
return new ClaudeAgentClient({
|
||||
logger: createTestLogger(),
|
||||
resolveVersion: async () => claudeCodeVersion,
|
||||
});
|
||||
}
|
||||
|
||||
describe("getClaudeModels", () => {
|
||||
it("returns all claude models", () => {
|
||||
const models = getClaudeModels();
|
||||
expect(models.map((m) => m.id)).toEqual([
|
||||
"claude-opus-5[1m]",
|
||||
"claude-opus-5",
|
||||
"claude-fable-5[1m]",
|
||||
"claude-fable-5",
|
||||
"claude-opus-4-8[1m]",
|
||||
"claude-opus-4-8",
|
||||
"claude-sonnet-5",
|
||||
"claude-sonnet-5[1m]",
|
||||
"claude-opus-4-7[1m]",
|
||||
"claude-opus-4-7",
|
||||
"claude-opus-4-6[1m]",
|
||||
@@ -60,7 +72,7 @@ describe("getClaudeModels", () => {
|
||||
const models = getClaudeModels();
|
||||
const defaults = models.filter((m) => m.isDefault);
|
||||
expect(defaults).toHaveLength(1);
|
||||
expect(defaults[0].id).toBe("claude-opus-4-8");
|
||||
expect(defaults[0].id).toBe("claude-opus-5[1m]");
|
||||
});
|
||||
|
||||
it("defines context window sizes in the catalog", () => {
|
||||
@@ -70,10 +82,14 @@ describe("getClaudeModels", () => {
|
||||
|
||||
expect(contextWindows).toEqual(
|
||||
new Map([
|
||||
["claude-fable-5", 1_000_000],
|
||||
["claude-opus-5[1m]", 1_000_000],
|
||||
["claude-opus-5", 200_000],
|
||||
["claude-fable-5[1m]", 1_000_000],
|
||||
["claude-fable-5", 200_000],
|
||||
["claude-opus-4-8[1m]", 1_000_000],
|
||||
["claude-opus-4-8", 200_000],
|
||||
["claude-sonnet-5", 1_000_000],
|
||||
["claude-sonnet-5", 200_000],
|
||||
["claude-sonnet-5[1m]", 1_000_000],
|
||||
["claude-opus-4-7[1m]", 1_000_000],
|
||||
["claude-opus-4-7", 200_000],
|
||||
["claude-opus-4-6[1m]", 1_000_000],
|
||||
@@ -85,9 +101,32 @@ describe("getClaudeModels", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("filters models by their minimum Claude Code version", () => {
|
||||
const oldVersionModels = getClaudeModels("2.1.218");
|
||||
expect(oldVersionModels.map((model) => model.id)).not.toContain("claude-opus-5[1m]");
|
||||
expect(oldVersionModels.map((model) => model.id)).not.toContain("claude-opus-5");
|
||||
expect(oldVersionModels.find((model) => model.isDefault)?.id).toBe("claude-opus-4-8");
|
||||
expect(getClaudeModels("2.1.219").map((model) => model.id)).toContain("claude-opus-5[1m]");
|
||||
expect(getClaudeModels("2.1.219").map((model) => model.id)).toContain("claude-opus-5");
|
||||
|
||||
expect(getClaudeModels("2.1.168").map((model) => model.id)).not.toContain("claude-fable-5[1m]");
|
||||
expect(getClaudeModels("2.1.168").map((model) => model.id)).not.toContain("claude-fable-5");
|
||||
expect(getClaudeModels("2.1.169").map((model) => model.id)).toContain("claude-fable-5[1m]");
|
||||
expect(getClaudeModels("2.1.169").map((model) => model.id)).toContain("claude-fable-5");
|
||||
});
|
||||
|
||||
it("derives thinking options from model effort capabilities", () => {
|
||||
const models = new Map(getClaudeModels().map((model) => [model.id, model]));
|
||||
|
||||
expect(models.get("claude-opus-5")?.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
CLAUDE_DISABLED_THINKING_OPTION_ID,
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
|
||||
]);
|
||||
expect(models.get("claude-sonnet-5")?.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
CLAUDE_DISABLED_THINKING_OPTION_ID,
|
||||
"low",
|
||||
@@ -104,6 +143,9 @@ describe("getClaudeModels", () => {
|
||||
?.label,
|
||||
).toBe("Ultra Code");
|
||||
expect(models.get("claude-sonnet-5")?.defaultThinkingOptionId).toBe("low");
|
||||
expect(models.get("claude-sonnet-5[1m]")?.thinkingOptions).toEqual(
|
||||
models.get("claude-sonnet-5")?.thinkingOptions,
|
||||
);
|
||||
|
||||
expect(models.get("claude-opus-4-7")?.thinkingOptions?.map((option) => option.id)).toEqual([
|
||||
CLAUDE_DISABLED_THINKING_OPTION_ID,
|
||||
@@ -124,11 +166,17 @@ 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[1m]")?.thinkingOptions).toEqual(
|
||||
models.get("claude-fable-5")?.thinkingOptions,
|
||||
);
|
||||
expect(models.get("claude-haiku-4-5")?.thinkingOptions).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["claude-opus-5", true, "low"],
|
||||
["claude-opus-5-20260724", true, "low"],
|
||||
["claude-sonnet-5", true, "low"],
|
||||
["claude-sonnet-5[1m]", true, "low"],
|
||||
["claude-sonnet-5-20260101", true, "low"],
|
||||
["claude-fable-5", false, "low"],
|
||||
["claude-haiku-4-5", false, undefined],
|
||||
@@ -162,7 +210,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
},
|
||||
});
|
||||
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
|
||||
const client = new ClaudeAgentClient({ logger: createTestLogger() });
|
||||
const client = createCatalogClient();
|
||||
|
||||
const { models } = await client.fetchCatalog({
|
||||
scope: "workspace",
|
||||
@@ -215,7 +263,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
const configDir = await fs.mkdtemp(path.join(os.tmpdir(), "paseo-claude-models-"));
|
||||
createdClaudeConfigDirs.push(configDir);
|
||||
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
|
||||
const client = new ClaudeAgentClient({ logger: createTestLogger() });
|
||||
const client = createCatalogClient();
|
||||
|
||||
const { models } = await client.fetchCatalog({
|
||||
scope: "workspace",
|
||||
@@ -229,7 +277,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
it("falls back to hardcoded models when settings.json is malformed", async () => {
|
||||
const configDir = await createClaudeConfigDirWithRawSettings("{ nope");
|
||||
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
|
||||
const client = new ClaudeAgentClient({ logger: createTestLogger() });
|
||||
const client = createCatalogClient();
|
||||
|
||||
const { models } = await client.fetchCatalog({
|
||||
scope: "workspace",
|
||||
@@ -249,7 +297,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
},
|
||||
});
|
||||
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
|
||||
const client = new ClaudeAgentClient({ logger: createTestLogger() });
|
||||
const client = createCatalogClient();
|
||||
|
||||
const { models } = await client.fetchCatalog({
|
||||
scope: "workspace",
|
||||
@@ -269,7 +317,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
},
|
||||
});
|
||||
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
|
||||
const client = new ClaudeAgentClient({ logger: createTestLogger() });
|
||||
const client = createCatalogClient();
|
||||
|
||||
const { models } = await client.fetchCatalog({
|
||||
scope: "workspace",
|
||||
@@ -282,12 +330,29 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
|
||||
"glm-5.1",
|
||||
]);
|
||||
});
|
||||
|
||||
it("omits models that require a newer Claude Code version", async () => {
|
||||
const client = createCatalogClient("2.1.218");
|
||||
|
||||
const { models } = await client.fetchCatalog({
|
||||
scope: "workspace",
|
||||
cwd: os.tmpdir(),
|
||||
force: true,
|
||||
});
|
||||
|
||||
expect(models.map((model) => model.id)).not.toContain("claude-opus-5[1m]");
|
||||
expect(models.map((model) => model.id)).not.toContain("claude-opus-5");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeClaudeRuntimeModelId", () => {
|
||||
it("returns exact match for known model IDs", () => {
|
||||
expect(normalizeClaudeRuntimeModelId("claude-opus-5[1m]")).toBe("claude-opus-5[1m]");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-opus-5")).toBe("claude-opus-5");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-fable-5")).toBe("claude-fable-5");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-fable-5[1m]")).toBe("claude-fable-5[1m]");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-sonnet-5")).toBe("claude-sonnet-5");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-sonnet-5[1m]")).toBe("claude-sonnet-5[1m]");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-opus-4-6")).toBe("claude-opus-4-6");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-opus-4-6[1m]")).toBe("claude-opus-4-6[1m]");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-sonnet-4-6")).toBe("claude-sonnet-4-6");
|
||||
@@ -295,14 +360,23 @@ describe("normalizeClaudeRuntimeModelId", () => {
|
||||
});
|
||||
|
||||
it("normalizes dated model IDs to base model", () => {
|
||||
expect(normalizeClaudeRuntimeModelId("claude-opus-5-20260724")).toBe("claude-opus-5");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-fable-5-20260301")).toBe("claude-fable-5");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-sonnet-5-20260101")).toBe("claude-sonnet-5");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-opus-4-6-20260101")).toBe("claude-opus-4-6");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-sonnet-4-6-20260101")).toBe("claude-sonnet-4-6");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-haiku-4-5-20251001")).toBe("claude-haiku-4-5");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-opus-5-20260724[1m]")).toBe("claude-opus-5[1m]");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-fable-5-20260301[1m]")).toBe("claude-fable-5[1m]");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-sonnet-5-20260101[1m]")).toBe(
|
||||
"claude-sonnet-5[1m]",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves [1m] suffix from runtime model strings", () => {
|
||||
expect(normalizeClaudeRuntimeModelId("claude-opus-5[1m]")).toBe("claude-opus-5[1m]");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-fable-5[1m]")).toBe("claude-fable-5[1m]");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-sonnet-5[1m]")).toBe("claude-sonnet-5[1m]");
|
||||
expect(normalizeClaudeRuntimeModelId("claude-opus-4-6[1m]")).toBe("claude-opus-4-6[1m]");
|
||||
});
|
||||
|
||||
@@ -331,6 +405,12 @@ describe("normalizeClaudeRuntimeModelId", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseClaudeCodeVersion", () => {
|
||||
it("prefers the Claude Code version over a wrapper banner", () => {
|
||||
expect(parseClaudeCodeVersion("wrapper 1.0.0\n2.1.219 (Claude Code)")).toEqual([2, 1, 219]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findClaudeModel", () => {
|
||||
it("resolves runtime model IDs to catalog entries", () => {
|
||||
expect(findClaudeModel("claude-sonnet-5-20260101")?.id).toBe("claude-sonnet-5");
|
||||
|
||||
@@ -17,8 +17,8 @@ const CLAUDE_SETTINGS_MODEL_ENV_KEYS = [
|
||||
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
||||
] as const;
|
||||
|
||||
export function getClaudeModels(): AgentModelDefinition[] {
|
||||
return getClaudeManifestModels();
|
||||
export function getClaudeModels(claudeCodeVersion?: string): AgentModelDefinition[] {
|
||||
return getClaudeManifestModels(claudeCodeVersion);
|
||||
}
|
||||
|
||||
export function findClaudeModel(
|
||||
@@ -34,8 +34,9 @@ export function findClaudeModel(
|
||||
export async function getClaudeModelsWithSettings(
|
||||
logger: Logger,
|
||||
configDir?: string,
|
||||
claudeCodeVersion?: string,
|
||||
): Promise<AgentModelDefinition[]> {
|
||||
const hardcodedModels = getClaudeModels();
|
||||
const hardcodedModels = getClaudeModels(claudeCodeVersion);
|
||||
const settingsModels = await readClaudeSettingsModels(logger, configDir);
|
||||
if (settingsModels.length === 0) {
|
||||
return hardcodedModels;
|
||||
|
||||
@@ -277,6 +277,35 @@ describe("OMP agent client and session", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("renders a live system-notice custom message as a synthetic tool call", async () => {
|
||||
const omp = new OmpHarness();
|
||||
await omp.start();
|
||||
|
||||
await omp.runPrompt("hello OMP", "done");
|
||||
omp
|
||||
.runtime()
|
||||
.acceptCustomMessage(
|
||||
[
|
||||
"<system-notice>",
|
||||
"Background job DocsSmokeTwo has completed.",
|
||||
'<task-result id="DocsSmokeTwo" agent="explore" status="completed" duration="21.6s">',
|
||||
"<output>done</output>",
|
||||
"</task-result>",
|
||||
"</system-notice>",
|
||||
].join("\n"),
|
||||
);
|
||||
omp.runtime().acceptCustomMessage("plain custom status text");
|
||||
|
||||
expect(omp.timeline().filter((item) => item.type === "tool_call")).toMatchObject([
|
||||
{ callId: "omp-notice:DocsSmokeTwo", name: "task_notification", status: "completed" },
|
||||
]);
|
||||
// Non-notice custom messages still fall through as assistant messages.
|
||||
expect(omp.timeline().filter((item) => item.type === "assistant_message")).toMatchObject([
|
||||
{ text: "done" },
|
||||
{ text: "plain custom status text" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("does not complete a queued model turn from OMP's local-only hint", async () => {
|
||||
const omp = new OmpHarness();
|
||||
await omp.start();
|
||||
|
||||
@@ -68,6 +68,7 @@ export { formatOmpVersionSupport, resolveOmpDiagnosticPaths } from "./provider-c
|
||||
import { OmpSubagentCardTracker, type OmpSubagentCardScheduler } from "./subagent-card-tracker.js";
|
||||
import { shouldDisplayOmpCustomMessage } from "./custom-message.js";
|
||||
import { getUserMessageText } from "./message-history.js";
|
||||
import { mapOmpSystemNoticeToToolCall } from "./system-notice.js";
|
||||
import { materializeProviderImage } from "../provider-image-output.js";
|
||||
import { OmpCliRuntime } from "./cli-runtime.js";
|
||||
import { listOmpImportableSessions, readOmpImportSessionConfig } from "./session-descriptor.js";
|
||||
@@ -2068,12 +2069,14 @@ export class OmpAgentSession implements AgentSession {
|
||||
if (shouldDisplayOmpCustomMessage(event.message)) {
|
||||
const text = getUserMessageText(event.message.content);
|
||||
if (text) {
|
||||
const advisorItem = mapOmpAdvisorMessageToToolCall(event.message, text);
|
||||
const item =
|
||||
mapOmpAdvisorMessageToToolCall(event.message, text) ??
|
||||
mapOmpSystemNoticeToToolCall(text);
|
||||
this.emit({
|
||||
type: "timeline",
|
||||
provider: this.provider,
|
||||
turnId,
|
||||
item: advisorItem ?? { type: "assistant_message", text },
|
||||
item: item ?? { type: "assistant_message", text },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
28
packages/server/src/server/pairing-qr.test.ts
Normal file
28
packages/server/src/server/pairing-qr.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import * as QRCode from "qrcode";
|
||||
import { renderPairingQr } from "./pairing-qr.js";
|
||||
|
||||
const ESCAPE = String.fromCharCode(0x1b);
|
||||
const ANSI_PATTERN = new RegExp(`${ESCAPE}\\[[0-9;]*m`, "g");
|
||||
|
||||
describe("renderPairingQr", () => {
|
||||
it("renders a theme-independent QR code with a four-module quiet zone", async () => {
|
||||
const url = "https://app.paseo.sh/#offer=test-pairing-offer";
|
||||
const qr = await renderPairingQr(url);
|
||||
const styledLines = qr.split("\n");
|
||||
const visibleLines = qr.replace(ANSI_PATTERN, "").split("\n");
|
||||
const moduleCount = QRCode.create(url).modules.size;
|
||||
|
||||
expect(
|
||||
styledLines.every(
|
||||
(line) => line.startsWith(`${ESCAPE}[47m${ESCAPE}[30m`) && line.endsWith(`${ESCAPE}[0m`),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(visibleLines.every((line) => line.length === moduleCount + 8)).toBe(true);
|
||||
expect(visibleLines.slice(0, 2).every((line) => line.trim() === "")).toBe(true);
|
||||
expect(visibleLines.slice(-2).every((line) => line.trim() === "")).toBe(true);
|
||||
expect(visibleLines.every((line) => line.startsWith(" ") && line.endsWith(" "))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,50 +1,18 @@
|
||||
import * as QRCode from "qrcode";
|
||||
import type { QRCodeToStringOptionsTerminal, QRCodeToStringOptionsOther } from "qrcode";
|
||||
import type { Logger } from "pino";
|
||||
import type { QRCodeToStringOptionsOther } from "qrcode";
|
||||
|
||||
function parseBooleanEnv(value: string | undefined): boolean | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
|
||||
if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function shouldPrintPairingQr(): boolean {
|
||||
const env = parseBooleanEnv(process.env.PASEO_PAIRING_QR);
|
||||
if (env !== undefined) return env;
|
||||
return process.stdout.isTTY ?? false;
|
||||
}
|
||||
const BLACK_ON_WHITE = "\u001b[47m\u001b[30m";
|
||||
const RESET_COLORS = "\u001b[0m";
|
||||
|
||||
export async function renderPairingQr(url: string): Promise<string> {
|
||||
const terminalOptions: QRCodeToStringOptionsTerminal = {
|
||||
type: "terminal",
|
||||
small: true,
|
||||
};
|
||||
|
||||
const utf8Options: QRCodeToStringOptionsOther = {
|
||||
type: "utf8",
|
||||
margin: 4,
|
||||
};
|
||||
|
||||
try {
|
||||
return await QRCode.toString(url, terminalOptions);
|
||||
} catch {
|
||||
return await QRCode.toString(url, utf8Options);
|
||||
}
|
||||
}
|
||||
|
||||
export async function printPairingQrIfEnabled(args: {
|
||||
url: string;
|
||||
logger?: Logger;
|
||||
}): Promise<void> {
|
||||
if (!shouldPrintPairingQr()) return;
|
||||
|
||||
const qr = await renderPairingQr(args.url);
|
||||
const out = `\nScan to pair:\n${qr}\n${args.url}\n`;
|
||||
|
||||
try {
|
||||
process.stdout.write(out);
|
||||
} catch (error) {
|
||||
args.logger?.debug({ error }, "Failed to print pairing QR");
|
||||
}
|
||||
const qr = await QRCode.toString(url, utf8Options);
|
||||
return qr
|
||||
.split("\n")
|
||||
.map((line) => `${BLACK_ON_WHITE}${line}${RESET_COLORS}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.2.0-beta.4",
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
84
public-docs/codex.md
Normal file
84
public-docs/codex.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: Codex
|
||||
description: Run Codex in Paseo using the official Codex CLI and your existing OpenAI account.
|
||||
nav: Codex
|
||||
order: 24
|
||||
category: Providers
|
||||
---
|
||||
|
||||
# Codex
|
||||
|
||||
Paseo runs Codex through the official `codex` CLI and its app-server interface.
|
||||
|
||||
## Does Codex cost extra in Paseo?
|
||||
|
||||
No. Paseo does not add a charge for Codex. Sign in to the Codex CLI with ChatGPT to use the access included with your ChatGPT plan, or sign in with an API key for usage billed through your OpenAI Platform account.
|
||||
|
||||
Your plan's normal Codex limits and OpenAI's standard API pricing still apply.
|
||||
|
||||
## Getting started
|
||||
|
||||
Install the [Codex CLI](https://learn.chatgpt.com/docs/codex/cli) on the machine running Paseo:
|
||||
|
||||
```bash
|
||||
npm install -g @openai/codex
|
||||
```
|
||||
|
||||
Sign in with ChatGPT for subscription access:
|
||||
|
||||
```bash
|
||||
codex login
|
||||
```
|
||||
|
||||
Or sign in with an API key for usage billed through your OpenAI Platform account:
|
||||
|
||||
```bash
|
||||
# macOS or Linux
|
||||
printenv OPENAI_API_KEY | codex login --with-api-key
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell
|
||||
$env:OPENAI_API_KEY | codex login --with-api-key
|
||||
```
|
||||
|
||||
Then confirm the CLI starts:
|
||||
|
||||
```bash
|
||||
codex
|
||||
```
|
||||
|
||||
Paseo uses this installation and its existing authentication when you start a Codex agent.
|
||||
|
||||
## Codex is missing in Paseo
|
||||
|
||||
The ChatGPT desktop app and the Codex CLI are separate installs. Installing the desktop app does not make the `codex` command available to Paseo.
|
||||
|
||||
Check whether the CLI is on your `PATH`:
|
||||
|
||||
```bash
|
||||
# macOS or Linux
|
||||
which -a codex
|
||||
|
||||
# Windows
|
||||
where.exe codex
|
||||
```
|
||||
|
||||
If the command is not found:
|
||||
|
||||
1. Install the [Codex CLI](https://learn.chatgpt.com/docs/codex/cli).
|
||||
2. Sign in with ChatGPT or an API key using the commands above. See [Codex authentication](https://learn.chatgpt.com/docs/auth).
|
||||
3. Restart Paseo if its daemon was already running when you installed the CLI.
|
||||
4. In Paseo, open **Settings → Providers → Codex** and select **Refresh**.
|
||||
|
||||
The provider should become available once Paseo can find and start the `codex` command.
|
||||
|
||||
## Use Codex in the Paseo terminal
|
||||
|
||||
Codex also works inside the Paseo terminal. Open a terminal in your workspace and run `codex` for the standard CLI experience while keeping access to your workspace, git changes, and other Paseo tools.
|
||||
|
||||
## See also
|
||||
|
||||
- [Supported providers](/docs/supported-providers), for other agents you can run alongside Codex.
|
||||
- [Custom providers](/docs/custom-providers), for custom binaries, third-party endpoints, or multiple Codex profiles.
|
||||
- [Paseo vs Codex app](/alternatives/codex-app), for a feature comparison.
|
||||
@@ -15,7 +15,7 @@ For the concept and how Paseo manages providers, see [Providers](/docs/providers
|
||||
Work out of the box once the underlying CLI is installed and authenticated.
|
||||
|
||||
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code). Anthropic's coding agent with MCP support, streaming, and deep reasoning.
|
||||
- [Codex CLI](https://github.com/openai/codex). OpenAI's workspace agent with sandbox controls and optional network access.
|
||||
- [Codex](/docs/codex). OpenAI's workspace agent with sandbox controls and optional network access.
|
||||
- [OpenCode](https://opencode.ai/). Open-source coding assistant with multi-provider model support.
|
||||
- [pi](https://github.com/svkozak/pi-acp). Minimal terminal-based coding agent with multi-provider LLM support.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user