Compare commits

...
Author SHA1 Message Date
Mohamed Boudra e791ab3df7 test(ios): run the native text probe on the main thread 2026-10-09 22:41:11 +02:00
Mohamed BoudraandOxxx f521abfb24 fix(ios): keep recycled text views at their container width
Apply the minimal candidate from #6364 and cover recycled geometry in the native probe. Document that the probe does not reproduce mounted Fabric clipping.

Co-authored-by: Oxxx <103127477+omcdowell@users.noreply.github.com>
2026-10-09 22:24:28 +02:00
Mohamed Boudra b89ebbede6 fix(ios): restore native text reflow for evaluation 2026-10-09 22:24:28 +02:00
Mohamed Boudra aec01ea605 feat(claude): offer Mythos 5.1 when Claude Code advertises it (#6403)
* feat(claude): add Mythos 5.1 with its model capabilities

* feat(claude): gate Mythos on advertised model identity

* fix(claude): retain capabilities for explicitly configured Mythos

* test(usage): load SQLite before timed store tests

The first Windows OMP store case exceeded its five-second budget while subsequent equivalent cases completed under a second. Load the builtin during test collection so its cold initialization does not consume the first test deadline.

* test(workspaces): publish complete service environment fixtures

Waiting for file existence races with writeFileSync creating the file before writing its contents. Rename completed JSON snapshots into place so the existing readiness check cannot expose an empty file.
2026-10-09 21:05:40 +01:00
paseo-ai[bot] a1da1b8f23 fix: update lockfile signatures and Nix hash [skip ci] 2026-10-09 15:43:26 +00:00
Mohamed Boudra 73390346ca fix(ios): address clipped chat and Markdown text (#6445)
* Revert "fix(app): reflow iPad chat text after pane resize (#5716)"

This reverts commit 5715b5ba62.

* docs(changelog): describe iOS text clipping fix for 0.11.2

* chore(release): cut 0.11.2
2026-10-09 16:35:43 +01:00
33 changed files with 572 additions and 87 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## 0.11.2 - 2026-10-09
### Fixed
- Fixed intermittent text clipping in iOS chats and Markdown previews ([#6445](https://github.com/getpaseo/paseo/pull/6445))
## 0.11.1 - 2026-10-07
### Added
+2
View File
@@ -61,6 +61,8 @@ Core direct providers: `claude` (in `providers/claude/agent.ts`), `codex` (`code
Claude first-party model metadata lives in `packages/server/src/server/agent/providers/claude/model-manifest.ts`. When adding or updating a Claude model, update that manifest only; the model picker thinking options and Claude-specific feature gates are derived from the manifest. Do not add model-specific Claude capability lists in feature code.
Keep the curated Claude catalog: `supportedModels()` has historically omitted older versions and carries less metadata. Discovery is only an opt-in visibility gate, not a replacement catalog or an entitlement guarantee. Claude Code v2.1.197 added `resolvedModel` to disambiguate alias rows; a bare family alias does not establish access to a particular version. Explicit user-configured models must remain selectable when discovery fails or omits them.
Paseo tools are not implemented as MCP tools internally. They live in a shared tool catalog under `packages/server/src/server/agent/tools/`; MCP is only the fallback adapter. The daemon resolves `agents.providers.<provider>.paseoTools` by the exact provider ID. The catalog policy belongs to the caller: it filters the tools exposed to the current agent. When that agent calls `create_agent`, the child receives the policy for the child provider ID; the caller's policy is not inherited.
A provider that can register runtime tools directly should set `supportsNativePaseoTools: true` and consume the already-filtered `launchContext.paseoTools` in `createSession`/`resumeSession`. When native tools are present, `AgentManager` strips the internal Paseo MCP server from the provider launch config so the provider does not receive the same tools twice. Providers that only know MCP should keep `supportsMcpServers: true` and let the daemon inject `/mcp/agents`; the MCP server builds the same policy-filtered catalog for that caller. Filtering is enforced at catalog registration in both paths. Browser tools remain subject to the daemon browser-tools setting and browser-host availability.
@@ -0,0 +1,4 @@
Fixed
- Fixed intermittent text clipping in iOS chats and Markdown previews
Full notes: https://paseo.sh/changelog
@@ -0,0 +1,4 @@
Fixed
- Fixed intermittent text clipping in iOS chats and Markdown previews
Full notes: https://paseo.sh/changelog
@@ -0,0 +1,4 @@
Fixed
- Fixed intermittent text clipping in iOS chats and Markdown previews
Full notes: https://paseo.sh/changelog
@@ -0,0 +1,4 @@
Fixed
- Fixed intermittent text clipping in iOS chats and Markdown previews
Full notes: https://paseo.sh/changelog
+1 -1
View File
@@ -1 +1 @@
sha256-aoYgGgEkqeeZJR7OOcSHuZEhBELkpyOGqHo4oEx7W88=
sha256-CK96G7MWsmd/VDvkuaFJ23U04EGNmeiXdTYbmR8RDDQ=
+28 -28
View File
@@ -1,12 +1,12 @@
{
"name": "paseo",
"version": "0.11.1",
"version": "0.11.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "paseo",
"version": "0.11.1",
"version": "0.11.2",
"hasInstallScript": true,
"license": "Apache-2.0",
"workspaces": [
@@ -36413,7 +36413,7 @@
},
"packages/app": {
"name": "@getpaseo/app",
"version": "0.11.1",
"version": "0.11.2",
"dependencies": {
"@codemirror/commands": "6.10.4",
"@codemirror/language": "6.12.4",
@@ -37496,12 +37496,12 @@
},
"packages/cli": {
"name": "@getpaseo/cli",
"version": "0.11.1",
"version": "0.11.2",
"dependencies": {
"@clack/prompts": "^1.0.0",
"@getpaseo/client": "0.11.1",
"@getpaseo/protocol": "0.11.1",
"@getpaseo/server": "0.11.1",
"@getpaseo/client": "0.11.2",
"@getpaseo/protocol": "0.11.2",
"@getpaseo/server": "0.11.2",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",
@@ -37748,10 +37748,10 @@
},
"packages/client": {
"name": "@getpaseo/client",
"version": "0.11.1",
"version": "0.11.2",
"dependencies": {
"@getpaseo/protocol": "0.11.1",
"@getpaseo/relay": "0.11.1",
"@getpaseo/protocol": "0.11.2",
"@getpaseo/relay": "0.11.2",
"zod": "^4.4.3"
},
"devDependencies": {
@@ -37762,7 +37762,7 @@
},
"packages/desktop": {
"name": "@getpaseo/desktop",
"version": "0.11.1",
"version": "0.11.2",
"license": "Apache-2.0",
"dependencies": {
"@getpaseo/cli": "*",
@@ -38006,7 +38006,7 @@
},
"packages/expo-two-way-audio": {
"name": "@getpaseo/expo-two-way-audio",
"version": "0.11.1",
"version": "0.11.2",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.14",
@@ -38902,7 +38902,7 @@
},
"packages/highlight": {
"name": "@getpaseo/highlight",
"version": "0.11.1",
"version": "0.11.2",
"dependencies": {
"@codemirror/language": "6.12.4",
"@codemirror/legacy-modes": "^6.5.3",
@@ -38931,14 +38931,14 @@
},
"packages/plugin": {
"name": "@getpaseo/plugin",
"version": "0.11.1",
"version": "0.11.2",
"dependencies": {
"@agentclientprotocol/sdk": "^1.4.0",
"use-sync-external-store": "^1.6.0"
},
"devDependencies": {
"@getpaseo/client": "0.11.1",
"@getpaseo/protocol": "0.11.1",
"@getpaseo/client": "0.11.2",
"@getpaseo/protocol": "0.11.2",
"@types/node": "^20.9.0",
"@types/react": "~19.2.0",
"react": "19.1.0",
@@ -38948,8 +38948,8 @@
"zod": "^4.4.3"
},
"peerDependencies": {
"@getpaseo/client": "0.11.1",
"@getpaseo/protocol": "0.11.1",
"@getpaseo/client": "0.11.2",
"@getpaseo/protocol": "0.11.2",
"react": "~19.1.0",
"react-native": ">=0.81.5",
"zod": "^4.4.3"
@@ -38971,7 +38971,7 @@
},
"packages/protocol": {
"name": "@getpaseo/protocol",
"version": "0.11.1",
"version": "0.11.2",
"dependencies": {
"semver": "^7.8.5",
"zod": "^4.4.3"
@@ -38998,7 +38998,7 @@
},
"packages/relay": {
"name": "@getpaseo/relay",
"version": "0.11.1",
"version": "0.11.2",
"dependencies": {
"base64-js": "^1.5.1",
"tweetnacl": "^1.0.3",
@@ -39216,16 +39216,16 @@
},
"packages/server": {
"name": "@getpaseo/server",
"version": "0.11.1",
"version": "0.11.2",
"dependencies": {
"@agentclientprotocol/sdk": "^0.17.1",
"@anthropic-ai/claude-agent-sdk": "0.3.246",
"@anthropic-ai/sdk": "^0.104.2",
"@getpaseo/client": "0.11.1",
"@getpaseo/highlight": "0.11.1",
"@getpaseo/plugin": "0.11.1",
"@getpaseo/protocol": "0.11.1",
"@getpaseo/relay": "0.11.1",
"@getpaseo/client": "0.11.2",
"@getpaseo/highlight": "0.11.2",
"@getpaseo/plugin": "0.11.2",
"@getpaseo/protocol": "0.11.2",
"@getpaseo/relay": "0.11.2",
"@isaacs/ttlcache": "^2.1.4",
"@modelcontextprotocol/sdk": "1.29.0",
"@opencode-ai/sdk": "1.14.46",
@@ -40146,7 +40146,7 @@
},
"packages/website": {
"name": "@getpaseo/website",
"version": "0.11.1",
"version": "0.11.2",
"dependencies": {
"@cloudflare/vite-plugin": "^1.29.1",
"@cloudflare/workers-types": "^4.20260317.1",
@@ -41754,7 +41754,7 @@
},
"plugins": {
"name": "@getpaseo/builtin-plugins",
"version": "0.11.1",
"version": "0.11.2",
"dependencies": {
"@getpaseo/plugin": "*",
"zod": "^4.4.3"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "paseo",
"version": "0.11.1",
"version": "0.11.2",
"private": true,
"description": "Paseo: voice-controlled development environment for local AI coding agents",
"keywords": [
@@ -13,6 +13,11 @@ bash packages/app/e2e/mobile/native-text-reflow/ios.sh
The probe constructs the installed `RNUITextView`, fills its real UIKit text view,
and resizes its content container from 500 to 240 and back to 500 points. It
asserts the inner frame follows both widths and the same selection survives.
It then recycles the view the way Fabric's pool does and remounts it 30 points
wider and at the same size; the inner frame must still match the container.
A zeroed frame on recycle comes back 30 or 0 points wide. This assertion checks
the intermediate recycled geometry; it does not reproduce visible clipping in a
mounted Fabric paragraph or exercise the subsequent `drawRect` call.
It uses public UIView/UITextView operations, without mocking React Native.
LLDB detaches after the probe. An assertion failure or evaluation error fails the
shell command; the log distinguishes them.
@@ -2,10 +2,17 @@ import lldb
def __lldb_init_module(debugger, _internal_dict):
process = debugger.GetSelectedTarget().GetProcess()
main_threads = [thread for thread in process if thread.GetQueueName() == "com.apple.main-thread"]
if len(main_threads) != 1 or not process.SetSelectedThread(main_threads[0]):
raise RuntimeError("Could not select the UIKit main thread")
debugger.HandleCommand("expression -l objc++ -- @import UIKit")
frame = debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame()
frame = process.GetSelectedThread().GetSelectedFrame()
options = lldb.SBExpressionOptions()
options.SetLanguage(lldb.eLanguageTypeObjC_plus_plus)
is_main_thread = frame.EvaluateExpression("(BOOL)[NSThread isMainThread]", options)
if is_main_thread.GetError().Fail() or is_main_thread.GetValueAsUnsigned() != 1:
raise RuntimeError("Refusing to run UIKit probe outside the main thread")
result = frame.EvaluateExpression(
r'''({
UIView *host = (UIView *)[[NSClassFromString(@"RNUITextView") alloc] init];
@@ -39,14 +46,28 @@ def __lldb_init_module(debugger, _internal_dict):
container.frame = frame;
[container layoutIfNeeded];
CGFloat wide = text.frame.size.width;
auto keptSelection = text.selectedRange;
// Fabric recycles the view: the container keeps its size in the pool,
// and the next paragraph mounts it 30 points wider (a list-item paragraph
// reused by a full-width one) or at the same size.
[host performSelector:@selector(prepareForRecycle)];
frame.size.width = 530;
container.frame = frame;
[container layoutIfNeeded];
CGFloat reusedWider = text.frame.size.width;
[host performSelector:@selector(prepareForRecycle)];
container.frame = frame;
[container layoutIfNeeded];
CGFloat reusedSame = text.frame.size.width;
BOOL ok = text != nil && narrow == 240 && wide == 500 &&
selection.location == text.selectedRange.location &&
selection.length == text.selectedRange.length &&
reusedWider == 530 && reusedSame == 530 &&
selection.location == keptSelection.location &&
selection.length == keptSelection.length &&
selection.location == 12 && selection.length == 9;
[NSString stringWithFormat:@"NATIVE_REFLOW_%@ narrow=%.0f wide=%.0f selection=%lu:%lu",
ok ? @"PASS" : @"FAIL", narrow, wide,
(unsigned long)text.selectedRange.location,
(unsigned long)text.selectedRange.length];
[NSString stringWithFormat:@"NATIVE_REFLOW_%@ narrow=%.0f wide=%.0f recycled=%.0f,%.0f selection=%lu:%lu",
ok ? @"PASS" : @"FAIL", narrow, wide, reusedWider, reusedSame,
(unsigned long)keptSelection.location,
(unsigned long)keptSelection.length];
})''',
options,
)
@@ -15,4 +15,4 @@ xcrun lldb --batch -p "$PASEO_NATIVE_TEXT_REFLOW_PID" \
-o "process detach" -o quit > "$PASEO_NATIVE_TEXT_REFLOW_LOG" 2>&1
grep '^NATIVE_REFLOW_' "$PASEO_NATIVE_TEXT_REFLOW_LOG"
grep -q '^NATIVE_REFLOW_PASS narrow=240 wide=500 selection=12:9$' "$PASEO_NATIVE_TEXT_REFLOW_LOG"
grep -q '^NATIVE_REFLOW_PASS narrow=240 wide=500 recycled=530,530 selection=12:9$' "$PASEO_NATIVE_TEXT_REFLOW_LOG"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/app",
"version": "0.11.1",
"version": "0.11.2",
"private": true,
"main": "index.ts",
"scripts": {
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/cli",
"version": "0.11.1",
"version": "0.11.2",
"description": "Paseo CLI - control your AI coding agents from the command line",
"bin": {
"paseo": "bin/paseo"
@@ -28,9 +28,9 @@
},
"dependencies": {
"@clack/prompts": "^1.0.0",
"@getpaseo/client": "0.11.1",
"@getpaseo/protocol": "0.11.1",
"@getpaseo/server": "0.11.1",
"@getpaseo/client": "0.11.2",
"@getpaseo/protocol": "0.11.2",
"@getpaseo/server": "0.11.2",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/client",
"version": "0.11.1",
"version": "0.11.2",
"description": "Paseo client SDK package",
"files": [
"dist",
@@ -39,8 +39,8 @@
"test": "vitest run"
},
"dependencies": {
"@getpaseo/protocol": "0.11.1",
"@getpaseo/relay": "0.11.1",
"@getpaseo/protocol": "0.11.2",
"@getpaseo/relay": "0.11.2",
"zod": "^4.4.3"
},
"devDependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/desktop",
"version": "0.11.1",
"version": "0.11.2",
"private": true,
"description": "Paseo desktop app (Electron wrapper)",
"homepage": "https://paseo.sh",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/expo-two-way-audio",
"version": "0.11.1",
"version": "0.11.2",
"description": "Native module for two way audio streaming",
"keywords": [
"ExpoTwoWayAudio",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/highlight",
"version": "0.11.1",
"version": "0.11.2",
"files": [
"dist",
"!dist/**/*.map",
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/plugin",
"version": "0.11.1",
"version": "0.11.2",
"description": "Paseo plugin SDK package",
"files": [
"dist",
@@ -70,8 +70,8 @@
"use-sync-external-store": "^1.6.0"
},
"devDependencies": {
"@getpaseo/client": "0.11.1",
"@getpaseo/protocol": "0.11.1",
"@getpaseo/client": "0.11.2",
"@getpaseo/protocol": "0.11.2",
"@types/node": "^20.9.0",
"@types/react": "~19.2.0",
"react": "19.1.0",
@@ -81,8 +81,8 @@
"zod": "^4.4.3"
},
"peerDependencies": {
"@getpaseo/client": "0.11.1",
"@getpaseo/protocol": "0.11.1",
"@getpaseo/client": "0.11.2",
"@getpaseo/protocol": "0.11.2",
"react": "~19.1.0",
"react-native": ">=0.81.5",
"zod": "^4.4.3"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/protocol",
"version": "0.11.1",
"version": "0.11.2",
"description": "Paseo shared protocol schemas and wire types",
"files": [
"dist",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/relay",
"version": "0.11.1",
"version": "0.11.2",
"description": "Paseo relay for bridging daemon and client connections",
"files": [
"dist",
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/server",
"version": "0.11.1",
"version": "0.11.2",
"description": "Paseo backend server",
"files": [
"dist/server",
@@ -107,11 +107,11 @@
"@agentclientprotocol/sdk": "^0.17.1",
"@anthropic-ai/claude-agent-sdk": "0.3.246",
"@anthropic-ai/sdk": "^0.104.2",
"@getpaseo/client": "0.11.1",
"@getpaseo/highlight": "0.11.1",
"@getpaseo/plugin": "0.11.1",
"@getpaseo/protocol": "0.11.1",
"@getpaseo/relay": "0.11.1",
"@getpaseo/client": "0.11.2",
"@getpaseo/highlight": "0.11.2",
"@getpaseo/plugin": "0.11.2",
"@getpaseo/protocol": "0.11.2",
"@getpaseo/relay": "0.11.2",
"@isaacs/ttlcache": "^2.1.4",
"@modelcontextprotocol/sdk": "1.29.0",
"@opencode-ai/sdk": "1.14.46",
@@ -426,6 +426,14 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
try {
const client = new ClaudeAgentClient({
logger,
discoverModels: async () => [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos",
description: "",
},
],
resolveBinary: async () => "/test/claude/bin",
resolveVersion: async () => "2.1.219",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: emptyConfigDir } },
@@ -438,6 +446,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
expect(models.map((m) => m.id)).toEqual([
"claude-opus-5",
"claude-mythos-5-1",
"claude-fable-5-1",
"claude-fable-5",
"claude-fable-5[1m]",
@@ -472,6 +481,14 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
try {
const client = new ClaudeAgentClient({
logger,
discoverModels: async () => [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos",
description: "",
},
],
resolveVersion: async () => {
throw new Error("unrecognized version output");
},
@@ -495,6 +512,14 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
try {
const client = new ClaudeAgentClient({
logger,
discoverModels: async () => [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos",
description: "",
},
],
resolveBinary: async () => "/test/claude/bin",
resolveVersion: async () => "2.1.293",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: emptyConfigDir } },
@@ -509,6 +534,14 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
};
expect(getThinkingIds("claude-opus-5")).toContain("ultracode");
expect(getThinkingIds("claude-mythos-5-1")).toEqual([
"low",
"medium",
"high",
"xhigh",
"max",
"ultracode",
]);
expect(getThinkingIds("claude-fable-5-1")).toContain("ultracode");
expect(getThinkingIds("claude-fable-5")).toContain("ultracode");
expect(getThinkingIds("claude-opus-4-8[1m]")).toContain("ultracode");
@@ -1232,6 +1265,7 @@ describe("ClaudeAgentSession features", () => {
test.each([
["supported model", "claude-opus-4-8", { type: "disabled" }, undefined],
["Mythos 5.1", "claude-mythos-5-1", { type: "adaptive", display: "summarized" }, "high"],
["Haiku 5.5", "claude-haiku-5-5", { type: "disabled" }, undefined],
["unsupported model", "claude-fable-5", { type: "adaptive", display: "summarized" }, "high"],
["custom model", "openrouter/anthropic/claude-opus-4-8", undefined, undefined],
@@ -76,6 +76,7 @@ import {
type ClaudeProviderOptions,
} from "./options.js";
import { renderPromptAttachmentAsText } from "../../prompt-attachments.js";
import { discoverClaudeModels } from "./model-discovery.js";
import { claudeQuery, type ClaudeOptions, type ClaudeQueryFactory } from "./query.js";
import {
realClaudeRewindSdk,
@@ -412,6 +413,7 @@ export interface ClaudeContentChunk {
}
interface ClaudeAgentClientOptions {
discoverModels?: typeof discoverClaudeModels;
defaults?: { agents?: Record<string, AgentDefinition> };
logger: Logger;
runtimeSettings?: ProviderRuntimeSettings;
@@ -1514,6 +1516,7 @@ export function readEventIdentifiers(message: SDKMessage): EventIdentifiers {
}
export class ClaudeAgentClient implements AgentClient {
private readonly discoverModels: typeof discoverClaudeModels;
readonly provider = "claude" as const;
readonly capabilities = CLAUDE_CAPABILITIES;
@@ -1526,6 +1529,7 @@ export class ClaudeAgentClient implements AgentClient {
private readonly rewindSdk: ClaudeRewindSdk;
constructor(options: ClaudeAgentClientOptions) {
this.discoverModels = options.discoverModels ?? discoverClaudeModels;
this.defaults = options.defaults;
this.logger = options.logger.child({ module: "agent", provider: "claude" });
this.runtimeSettings = options.runtimeSettings;
@@ -1608,8 +1612,35 @@ export class ClaudeAgentClient implements AgentClient {
this.logger.warn({ err: error }, "Failed to resolve Claude Code version for model catalog");
}
const env = this.buildProviderEnv();
let discoveredModelIds = new Set<string>();
try {
const advertisedModels = await runProviderRefreshActivity(context, "models", () =>
this.discoverModels({
resolveBinary: this.resolveBinary,
runtimeSettings: this.runtimeSettings,
env,
signal: context?.signal,
}),
);
discoveredModelIds = new Set(
advertisedModels
.map((model) => normalizeClaudeRuntimeModelId(model.resolvedModel ?? model.value))
.filter((id): id is string => id !== null),
);
} catch (error) {
context?.signal.throwIfAborted();
this.logger.warn(
{ err: error },
"Claude model discovery failed; omitting discovery-required models",
);
}
const models = await runProviderRefreshActivity(context, "settings", () =>
getClaudeModelsWithSettings(this.logger, claudeConfigDir(env), claudeCodeVersion),
getClaudeModelsWithSettings(
this.logger,
claudeConfigDir(env),
claudeCodeVersion,
discoveredModelIds,
),
);
const modeCatalog = claudeModeCatalog(env);
return {
@@ -0,0 +1,89 @@
import type { ChildProcess } from "node:child_process";
import os from "node:os";
import type { ModelInfo, Query, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
import { raceProviderRefreshAbort } from "../../provider-refresh-deadline.js";
import type { ProviderRuntimeSettings } from "../../provider-launch-config.js";
import { terminateWithTreeKill } from "../../../../utils/tree-kill.js";
import { claudeQuery, type ClaudeQueryInput, type ClaudeQueryContext } from "./query.js";
interface ClaudeModelDiscoveryOptions {
resolveBinary: () => Promise<string>;
runtimeSettings?: ProviderRuntimeSettings;
env: NodeJS.ProcessEnv;
signal?: AbortSignal;
}
type OpenDiscoveryQuery = (
input: ClaudeQueryInput,
context: ClaudeQueryContext,
) => Pick<Query, "supportedModels" | "close">;
// The curated catalog survives a failed or slow probe. This process never receives
// a prompt and must not persist a session or launch user MCP servers/hooks.
export async function discoverClaudeModels(
options: ClaudeModelDiscoveryOptions,
openQuery: OpenDiscoveryQuery = claudeQuery,
): Promise<ModelInfo[]> {
const controller = new AbortController();
const signal = options.signal
? AbortSignal.any([options.signal, controller.signal])
: controller.signal;
const timer = setTimeout(
() => controller.abort(new Error("Claude model discovery timed out")),
5_000,
);
let endInput!: () => void;
const ended = new Promise<void>((resolve) => {
endInput = resolve;
});
const input: AsyncIterable<SDKUserMessage> = {
[Symbol.asyncIterator]() {
return {
next: async () => {
await ended;
return { done: true, value: undefined };
},
};
},
};
let query: ReturnType<OpenDiscoveryQuery> | undefined;
let child: ChildProcess | undefined;
try {
signal.throwIfAborted();
const binary = await raceProviderRefreshAbort(signal, options.resolveBinary());
query = openQuery(
{
prompt: input,
options: {
pathToClaudeCodeExecutable: binary,
cwd: os.homedir(),
env: options.env,
persistSession: false,
tools: [],
mcpServers: {},
strictMcpConfig: true,
settingSources: ["user"],
settings: { disableAllHooks: true },
abortController: controller,
},
},
{
runtimeSettings: options.runtimeSettings,
onChildProcess: (process) => {
child = process;
},
},
);
return await raceProviderRefreshAbort(signal, query.supportedModels());
} finally {
clearTimeout(timer);
endInput();
try {
query?.close();
} finally {
if (child) {
await terminateWithTreeKill(child, { gracefulTimeoutMs: 200, forceTimeoutMs: 200 });
}
}
}
}
@@ -9,6 +9,7 @@ interface ClaudeModelManifestEntry {
description: string;
defaultPriority?: number;
minimumClaudeCodeVersion?: string;
requiresDiscovery?: boolean;
contextWindowMaxTokens?: number;
effortLevels?: readonly ClaudeEffortLevel[];
defaultThinkingOptionId?: ClaudeEffortLevel;
@@ -57,6 +58,14 @@ export const CLAUDE_MODEL_MANIFEST = [
supportsThinkingDisabled: true,
supportsFastMode: true,
},
{
id: "claude-mythos-5-1",
label: "Mythos 5.1",
requiresDiscovery: true,
description: "Mythos 5.1 · Requires verified access",
contextWindowMaxTokens: 1_000_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
},
{
id: "claude-fable-5-1",
label: "Fable 5.1",
@@ -217,9 +226,16 @@ function buildThinkingOptions(
return options;
}
export function getClaudeManifestModels(claudeCodeVersion?: string): AgentModelDefinition[] {
export function getClaudeManifestModels(
claudeCodeVersion?: string,
discoveredModelIds?: ReadonlySet<string>,
): AgentModelDefinition[] {
const availableModels: readonly ClaudeModelManifestEntry[] = CLAUDE_MODEL_MANIFEST.filter(
(model) => isModelAvailableInClaudeCode(model, claudeCodeVersion),
(model) =>
isModelAvailableInClaudeCode(model, claudeCodeVersion) &&
(discoveredModelIds === undefined ||
!("requiresDiscovery" in model && model.requiresDiscovery) ||
discoveredModelIds.has(model.id)),
);
const defaultModel = availableModels.reduce<ClaudeModelManifestEntry | undefined>(
(selected, candidate) =>
@@ -359,7 +375,7 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined)
}
const singleSegmentMatch = trimmed.match(
/^(?:claude[-_ ])?(fable|opus|sonnet|haiku)[-_ ]+(\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
/^(?:claude[-_ ])?(mythos|fable|opus|sonnet|haiku)[-_ ]+(\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
);
if (singleSegmentMatch) {
return normalizeSingleSegmentClaudeModelId(
@@ -370,7 +386,7 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined)
}
const runtimeMatch = trimmed.match(
/^(?:claude[-_ ])?(fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
/^(?:claude[-_ ])?(mythos|fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i,
);
if (!runtimeMatch) {
return null;
@@ -403,7 +419,7 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined):
}
const runtimeMatch = trimmed.match(
/claude[-_ ](fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i,
/claude[-_ ](mythos|fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i,
);
if (runtimeMatch) {
const normalizedModelId = normalizeMajorMinorClaudeModelId(
@@ -418,7 +434,7 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined):
}
const singleSegmentMatch = trimmed.match(
/claude[-_ ](fable|opus|sonnet|haiku)[-_ ]+(\d+)(\[1m\])?/i,
/claude[-_ ](mythos|fable|opus|sonnet|haiku)[-_ ]+(\d+)(\[1m\])?/i,
);
if (!singleSegmentMatch) {
return null;
@@ -13,6 +13,8 @@ import {
parseClaudeCodeVersion,
resolveClaudeDisabledThinkingForModel,
} from "./model-manifest.js";
import { discoverClaudeModels } from "./model-discovery.js";
import type { ModelInfo } from "@anthropic-ai/claude-agent-sdk";
import { findClaudeModel, getClaudeModels, normalizeClaudeRuntimeModelId } from "./models.js";
const createdClaudeConfigDirs: string[] = [];
@@ -43,6 +45,14 @@ function createCatalogClient(claudeCodeVersion = "2.1.293"): ClaudeAgentClient {
return new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => claudeCodeVersion,
discoverModels: async () => [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos",
description: "",
},
],
});
}
@@ -52,6 +62,7 @@ describe("getClaudeModels", () => {
expect(models.map((m) => m.id)).toEqual([
"claude-opus-5-5",
"claude-opus-5",
"claude-mythos-5-1",
"claude-fable-5-1",
"claude-fable-5",
"claude-fable-5[1m]",
@@ -87,6 +98,7 @@ describe("getClaudeModels", () => {
new Map([
["claude-opus-5-5", 1_000_000],
["claude-opus-5", 1_000_000],
["claude-mythos-5-1", 1_000_000],
["claude-fable-5-1", 1_000_000],
["claude-fable-5", 1_000_000],
["claude-fable-5[1m]", 1_000_000],
@@ -221,6 +233,118 @@ describe("getClaudeModels", () => {
});
describe("ClaudeAgentClient.fetchCatalog", () => {
it.each([
{ value: "mythos", resolvedModel: "claude-mythos-5-1", expected: true },
{ value: "claude-mythos-5-1", expected: true },
{ value: "claude-mythos-5-1[1m]", expected: true },
{ value: "global.anthropic.claude-mythos-5-1", expected: true },
{ value: "mythos", expected: false },
{ value: "mythos", resolvedModel: "claude-mythos-5-2", expected: false },
{ value: "claude-mythos-5-1", resolvedModel: "claude-fable-5-1", expected: false },
])(
"uses advertised identity $value / $resolvedModel to gate Mythos",
async ({ value, resolvedModel, expected }) => {
const configDir = await createClaudeConfigDir({});
const client = new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => "2.1.293",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: configDir } },
discoverModels: async () => [
{ value, resolvedModel, displayName: "Untrusted label", description: "" },
],
});
const { models } = await client.fetchCatalog({
scope: "workspace",
cwd: configDir,
force: false,
});
expect(models.find((model) => model.id === "claude-mythos-5-1")).toEqual(
expected ? findClaudeModel("claude-mythos-5-1") : undefined,
);
},
);
it("preserves the curated catalog and explicitly configured models after discovery failure", async () => {
const configDir = await createClaudeConfigDir({ model: "claude-mythos-5-1" });
const client = new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => "2.1.293",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: configDir } },
discoverModels: async () => {
throw new Error("discovery unavailable");
},
});
const { models } = await client.fetchCatalog({
scope: "workspace",
cwd: configDir,
force: false,
});
expect(models.filter((model) => model.id !== "claude-mythos-5-1")).toEqual(
getClaudeModels("2.1.293").filter((model) => model.id !== "claude-mythos-5-1"),
);
expect(models.find((model) => model.id === "claude-mythos-5-1")).toEqual({
...findClaudeModel("claude-mythos-5-1"),
label: "claude-mythos-5-1",
description: "From Claude settings.json model",
});
});
it("omits unconfirmed Mythos after a failed probe and retries on refresh", async () => {
const configDir = await createClaudeConfigDir({});
let fail = true;
const client = new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => "2.1.293",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: configDir } },
discoverModels: async () => {
if (fail) throw new Error("discovery unavailable");
return [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos",
description: "",
},
];
},
});
const options = { scope: "workspace" as const, cwd: configDir, force: true };
expect(
(await client.fetchCatalog(options)).models.some((model) => model.id === "claude-mythos-5-1"),
).toBe(false);
fail = false;
expect(
(await client.fetchCatalog(options)).models.find((model) => model.id === "claude-mythos-5-1"),
).toEqual(findClaudeModel("claude-mythos-5-1"));
});
it("keeps the curated catalog when discovery does not advertise Mythos", async () => {
const configDir = await createClaudeConfigDir({});
const client = new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => "2.1.293",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: configDir } },
discoverModels: async () => [
{
value: "opus",
resolvedModel: "claude-opus-5-5",
displayName: "Opus",
description: "Latest",
},
],
});
const { models } = await client.fetchCatalog({
scope: "workspace",
cwd: configDir,
force: false,
});
expect(models.map((model) => model.id)).toEqual(
getClaudeModels("2.1.293")
.filter((model) => model.id !== "claude-mythos-5-1")
.map((model) => model.id),
);
});
it("appends concrete models from Claude settings.json", async () => {
const configDir = await createClaudeConfigDir({
model: "us.anthropic.claude-opus-4-7[1m]",
@@ -298,6 +422,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
const client = new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => "2.1.280",
discoverModels: async () => [],
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: providerConfigDir } },
});
@@ -724,3 +849,124 @@ describe("claudeManifestModelSupportsFastMode", () => {
expect(claudeManifestModelSupportsFastMode("claude-fable-5-1")).toBe(false);
});
});
describe("Claude Mythos 5.1 catalog", () => {
it("offers one 1M model with high default effort and always-on thinking", () => {
const models = getClaudeModels().filter((model) => model.id.startsWith("claude-mythos"));
expect(models).toHaveLength(1);
expect(models[0]).toMatchObject({
id: "claude-mythos-5-1",
label: "Mythos 5.1",
contextWindowMaxTokens: 1_000_000,
defaultThinkingOptionId: "high",
});
expect(models[0].thinkingOptions?.map((option) => option.id)).toEqual([
"low",
"medium",
"high",
"xhigh",
"max",
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
]);
expect(
models[0].thinkingOptions?.filter((option) => option.isDefault).map((option) => option.id),
).toEqual(["high"]);
expect(resolveClaudeDisabledThinkingForModel("claude-mythos-5-1")).toEqual({
supported: false,
fallbackThinkingOptionId: "high",
});
expect(claudeManifestModelSupportsFastMode("claude-mythos-5-1")).toBe(false);
expect(getClaudeModels().find((model) => model.isDefault)?.id).toBe("claude-opus-5-5");
});
it.each([
"claude-mythos-5-1[1m]",
"claude-mythos-5-1-20260901",
"claude-mythos-5-1-20260901[1m]",
"Mythos 5.1",
])("resolves %s to the single catalog entry", (id) => {
expect(findClaudeModel(id)?.id).toBe("claude-mythos-5-1");
});
it.each(["anthropic/claude-mythos-5-1", "us.anthropic.claude-mythos-5-1-v1:0"])(
"normalizes runtime identity without granting first-party capabilities: %s",
(id) => {
expect(normalizeClaudeRuntimeModelId(id)).toBe("claude-mythos-5-1");
expect(normalizeClaudeManifestModelId(id)).toBeNull();
},
);
});
describe("Claude model discovery process", () => {
it("does not send a prompt or persist a session and closes after discovery", async () => {
let closed = false;
let promptEnded: Promise<IteratorResult<unknown>> | undefined;
const advertised: ModelInfo[] = [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos",
description: "",
},
];
const models = await discoverClaudeModels(
{ resolveBinary: async () => "/claude", env: { CLAUDE_CONFIG_DIR: "/provider-config" } },
(input) => {
expect(input.options).toMatchObject({
pathToClaudeCodeExecutable: "/claude",
env: { CLAUDE_CONFIG_DIR: "/provider-config" },
persistSession: false,
tools: [],
mcpServers: {},
strictMcpConfig: true,
settingSources: ["user"],
settings: { disableAllHooks: true },
});
if (typeof input.prompt === "string") throw new Error("unexpected inference prompt");
promptEnded = input.prompt[Symbol.asyncIterator]().next();
return {
supportedModels: async () => advertised,
close: () => {
closed = true;
},
};
},
);
expect(models).toEqual(advertised);
expect(closed).toBe(true);
await expect(promptEnded).resolves.toEqual({ done: true, value: undefined });
});
it("times out and closes an unresponsive discovery process", async () => {
let closed = false;
await expect(
discoverClaudeModels({ resolveBinary: async () => "/claude", env: {} }, () => ({
supportedModels: () => new Promise(() => {}),
close: () => {
closed = true;
},
})),
).rejects.toThrow("Claude model discovery timed out");
expect(closed).toBe(true);
}, 10_000);
it("closes a stalled probe when its refresh is canceled", async () => {
const controller = new AbortController();
const failure = new Error("refresh canceled");
let closed = false;
const result = discoverClaudeModels(
{ resolveBinary: async () => "/claude", env: {}, signal: controller.signal },
() => ({
supportedModels: () => {
controller.abort(failure);
return new Promise(() => {});
},
close: () => {
closed = true;
},
}),
);
await expect(result).rejects.toBe(failure);
expect(closed).toBe(true);
});
});
@@ -52,8 +52,9 @@ export async function getClaudeModelsWithSettings(
logger: Logger,
configDir: string,
claudeCodeVersion?: string,
discoveredModelIds: ReadonlySet<string> = new Set(),
): Promise<AgentModelDefinition[]> {
const hardcodedModels = getClaudeModels(claudeCodeVersion);
const hardcodedModels = getClaudeManifestModels(claudeCodeVersion, discoveredModelIds);
const settingsModels = await readClaudeSettingsModels(logger, configDir);
if (settingsModels.length === 0) {
return hardcodedModels;
@@ -70,7 +71,10 @@ export async function getClaudeModelsWithSettings(
}
continue;
}
models.push(model);
const manifestModel = getClaudeModels(claudeCodeVersion).find(
(candidate) => candidate.id === model.id,
);
models.push({ ...manifestModel, ...model });
}
return models;
@@ -422,12 +422,12 @@ describe.skipIf(isPlatform("win32"))("worktree-bootstrap POSIX-only", () => {
api: {
type: "service",
command:
"node -e \"const fs=require('fs'); fs.writeFileSync('api-env.json', JSON.stringify(process.env)); setTimeout(()=>{}, 30000)\"",
"node -e \"const fs=require('fs'); fs.writeFileSync('api-env.json.tmp', JSON.stringify(process.env)); fs.renameSync('api-env.json.tmp', 'api-env.json'); setTimeout(()=>{}, 30000)\"",
},
web: {
type: "service",
command:
"node -e \"const fs=require('fs'); fs.writeFileSync('web-env.json', JSON.stringify(process.env)); setTimeout(()=>{}, 30000)\"",
"node -e \"const fs=require('fs'); fs.writeFileSync('web-env.json.tmp', JSON.stringify(process.env)); fs.renameSync('web-env.json.tmp', 'web-env.json'); setTimeout(()=>{}, 30000)\"",
},
},
}),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/website",
"version": "0.11.1",
"version": "0.11.2",
"private": true,
"type": "module",
"scripts": {
+26 -10
View File
@@ -1,5 +1,5 @@
diff --git a/node_modules/react-native-uitextview/ios/RNUITextView.mm b/node_modules/react-native-uitextview/ios/RNUITextView.mm
index d0c31ed..df8a233 100644
index d0c31ed..d1605ea 100644
--- a/node_modules/react-native-uitextview/ios/RNUITextView.mm
+++ b/node_modules/react-native-uitextview/ios/RNUITextView.mm
@@ -22,6 +22,7 @@ using namespace facebook::react;
@@ -20,9 +20,17 @@ index d0c31ed..df8a233 100644
const auto longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:@selector(handleLongPressIfNecessary:)];
@@ -94,16 +96,15 @@ using namespace facebook::react;
// Reset the frame to zero so that when it properly lays out on the next use
_textView.frame = CGRectZero;
@@ -91,19 +93,21 @@ using namespace facebook::react;
[super prepareForRecycle];
_state.reset();
- // Reset the frame to zero so that when it properly lays out on the next use
- _textView.frame = CGRectZero;
+ // Keep the text view on the content container's bounds. The container keeps
+ // its size while the view waits in the recycle pool, and autoresizing only
+ // applies the size delta on reuse, so a zeroed text view would come back as
+ // wide as that delta (zero, or a two-word column).
+ _textView.frame = _view.bounds;
_textView.attributedText = nil;
+ _lastTextLayoutFrame = CGRectZero;
}
@@ -41,7 +49,7 @@ index d0c31ed..df8a233 100644
[self setNeedsDisplay];
}
}
@@ -124,8 +125,9 @@ using namespace facebook::react;
@@ -124,10 +128,16 @@ using namespace facebook::react;
// entirely when nothing actually changed so a JS-side state update made in
// response to onSelectionChange doesn't deselect what the user is selecting.
const BOOL textChanged = ![_textView.attributedText isEqualToAttributedString:convertedAttrString];
@@ -52,17 +60,25 @@ index d0c31ed..df8a233 100644
+ if (!textChanged && !frameChanged && !layoutChanged) {
return;
}
+ // Size the text view before handing it the text, so UIKit lays the text out
+ // at the container width instead of a stale width.
+ if (frameChanged) {
+ _textView.frame = _view.bounds;
+ }
if (textChanged) {
@@ -142,7 +144,7 @@ using namespace facebook::react;
// Reassigning attributedText clears any active selection. Save it and
// restore after, while suppressing the synthetic textViewDidChangeSelection
@@ -141,9 +151,6 @@ using namespace facebook::react;
}
_suppressSelectionChange = NO;
}
if (frameChanged) {
- if (frameChanged) {
- _textView.frame = _view.frame;
+ _textView.frame = _view.bounds;
}
- }
__block std::vector<std::string> lines;
@@ -162,6 +164,7 @@ using namespace facebook::react;
const int maxLines = props.numberOfLines;
@@ -162,6 +169,7 @@ using namespace facebook::react;
}
}];
@@ -1,6 +1,7 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { afterEach, beforeEach, expect, test } from "vitest";
import { discover, fetchUsage } from "./usage.js";
@@ -43,7 +44,6 @@ async function logins(options: Parameters<typeof discover>[1]) {
}
async function database(path: string) {
await mkdir((await import("node:path")).dirname(path), { recursive: true });
const { DatabaseSync } = await import("node:sqlite");
const db = new DatabaseSync(path);
db.exec(
"CREATE TABLE auth_credentials (id INTEGER PRIMARY KEY, provider TEXT, credential_type TEXT, data TEXT, disabled_cause TEXT)",
@@ -248,7 +248,6 @@ test("OMP fetch re-reads its row after rotation and skips a newly disabled login
const path = join(home, ".omp", "agent", "agent.db");
await database(path);
const [input] = await logins(lookup());
const { DatabaseSync } = await import("node:sqlite");
const db = new DatabaseSync(path);
db.prepare("UPDATE auth_credentials SET data = ? WHERE id = 1").run(
JSON.stringify({ access: "fixture-rotated-omp", accountId: "omp-account", expires: 2000 }),
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/builtin-plugins",
"version": "0.11.1",
"version": "0.11.2",
"private": true,
"type": "module",
"scripts": {