From 2df4dbca48e71b4db6348dbe536e743fedd66137 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:26:29 -0700 Subject: [PATCH 1/5] fix(mobile): start an explicitly chosen agent even when it is hidden in Settings Follows #27766: hiding an agent only hides it from pickers and the automatic pick. The phone no longer refuses a hidden agent on the task-page workspace create, the new-workspace sheet, or a source-control AI button's saved agent, and a chosen agent is no longer cleared when it becomes hidden. An older host that still refuses hidden agents shows its own message. Re-recorded the settings.task-workspace and settings.workspace-submit RPC goldens with rpc:record. --- ....task-workspace-create-settings.get-1.json | 273 +++++++++---- ...ettings.task-workspace-settings.get-1.json | 192 +++------- ...tings.workspace-submit-settings.get-1.json | 360 +++++++++++++----- .../settings-task-workspace-fulfilled.json | 70 ++-- .../settings-task-workspace-refused.json | 67 ++-- ...ttings-task-workspace-transport-error.json | 61 ++- .../settings-workspace-submit-fulfilled.json | 131 +++++-- .../settings-workspace-submit-refused.json | 122 ++++-- ...ings-workspace-submit-transport-error.json | 122 ++++-- mobile/src/components/NewWorktreeModal.tsx | 3 - .../new-worktree-agent-selection.test.ts | 6 +- .../new-worktree-agent-selection.ts | 16 +- .../use-new-workspace-create-submit.ts | 28 +- .../mobile-source-control-launch-agent.ts | 27 +- .../src/session/pr-ai-triage-launch.test.ts | 24 ++ mobile/src/tasks/mobile-tasks-dependencies.ts | 1 - ...-mobile-tasks-workspace-create-actions.tsx | 21 +- ...bile-tasks-workspace-create-projection.tsx | 3 +- mobile/src/tasks/workspace-agent-selection.ts | 12 +- .../adapters/workspace-settings-mounts.ts | 1 - .../mutants/operation-mutations.ts | 4 +- 21 files changed, 959 insertions(+), 585 deletions(-) diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json index 09babd4e9339..e16a557c2a8c 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-create-settings.get-1.json @@ -4,6 +4,91 @@ "family": "settings.task-workspace-create", "namedDeltas": [], "values": { + "04cc6fe04bb7": { + "name": "worktree.create#1", + "ordinal": 5, + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Client closed", + "isRpcDeliveryUnknown": false + } + } + }, + "0b4d9ec099f7": { + "name": "worktree.create#1", + "ordinal": 5, + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "activate": true, + "createdWithAgent": "claude", + "displayName": "ORC-1 Recorded issue", + "displayNameKind": "generated", + "linkedLinearIssue": "ORC-1", + "name": "orc-1", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupDraft": "https://linear.app/orca/issue/ORC-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "worktree": { + "displayName": "ORC-1 Recorded issue", + "id": "wt-1" + } + } + } + } + }, "0de38c54b7c1": { "name": "settings.get#1", "ordinal": 3, @@ -78,6 +163,13 @@ } } }, + "14c25fa180ba": { + "name": "actionItem", + "ordinal": 7, + "value": { + "$rpc": "null" + } + }, "15a350feeedb": { "name": "creatingKey", "ordinal": 12, @@ -85,9 +177,9 @@ "$rpc": "null" } }, - "1a4a4adee14f": { + "1a0c83181ac5": { "name": "creatingKey", - "ordinal": 8, + "ordinal": 11, "value": { "$rpc": "null" } @@ -243,10 +335,10 @@ "startedAt": 0 } }, - "483b4c51c982": { + "4d54c9aa9098": { "name": "error", - "ordinal": 7, - "value": "Selected agent is disabled. Choose an enabled agent before creating." + "ordinal": 6, + "value": "Client closed" }, "563b6d30cbc3": { "name": "runtimeTaskSettings", @@ -320,10 +412,10 @@ } } }, - "626f3cf8fc2d": { - "name": "workspaceAgentOverridden", + "6485d1b0d446": { + "name": "worktree.create#1", "ordinal": 6, - "value": false + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"name\":\"orc-1\",\"displayName\":\"ORC-1 Recorded issue\",\"displayNameKind\":\"generated\",\"linkedLinearIssue\":\"ORC-1\",\"setupDecision\":\"inherit\",\"activate\":true,\"startupDraft\":\"https://linear.app/orca/issue/ORC-1\",\"createdWithAgent\":\"claude\"}}" }, "7abdfe20af50": { "creating": "linear:1", @@ -410,10 +502,26 @@ } } }, + "8cc8802c3592": { + "name": "workspaceCreateDraft", + "ordinal": 8, + "value": { + "$rpc": "null" + } + }, "9270aeb7d9c6": { "status": "pending", "startedAt": 0 }, + "93cc0febafd5": { + "creating": { + "$rpc": "null" + }, + "error": "", + "settings": { + "disabledTuiAgents": ["claude"] + } + }, "951af0d31429": { "name": "settings.get#1", "ordinal": 3, @@ -457,6 +565,11 @@ "error": "", "settings": {} }, + "b16a8e439428": { + "name": "navigation", + "ordinal": 10, + "value": "/h/host-1/session/wt-1?name=ORC-1+Recorded+issue&created=1" + }, "ce455f0c9a54": { "name": "actionItem", "ordinal": 8, @@ -496,20 +609,6 @@ } } }, - "d1777904aa15": { - "name": "workspaceAgent", - "ordinal": 5, - "value": "codex" - }, - "d5df3f6b123a": { - "creating": { - "$rpc": "null" - }, - "error": "Selected agent is disabled. Choose an enabled agent before creating.", - "settings": { - "disabledTuiAgents": ["claude"] - } - }, "d82310a7e5db": { "name": "settings.get#1", "ordinal": 3, @@ -544,6 +643,13 @@ } } }, + "d958e78341fc": { + "name": "creatingKey", + "ordinal": 7, + "value": { + "$rpc": "null" + } + }, "e0423a9e6995": { "name": "worktree.create#1", "ordinal": 7, @@ -616,6 +722,13 @@ "ok": false } } + }, + "fccaadef9a9a": { + "name": "setupPrompt", + "ordinal": 9, + "value": { + "$rpc": "null" + } } }, "recording": { @@ -637,21 +750,14 @@ { "id": "settings-task-workspace-create-linear.prelude:cleanup", "observation": { - "sender": ["d048cafe213d"], + "sender": ["d048cafe213d", "04cc6fe04bb7"], "payloads": ["975f2ff9a730"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, "state": "7abdfe20af50", - "effects": [ - "e0a8ee1f3c00", - "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" - ] + "effects": ["e0a8ee1f3c00", "ed3a7d6bc894", "4d54c9aa9098", "d958e78341fc"] } }, { @@ -679,40 +785,42 @@ { "id": "settings-task-workspace-create-linear.result-absent:created", "observation": { - "sender": ["951af0d31429"], - "payloads": ["975f2ff9a730"], + "sender": ["951af0d31429", "0b4d9ec099f7"], + "payloads": ["975f2ff9a730", "6485d1b0d446"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", + "state": "93cc0febafd5", "effects": [ "e0a8ee1f3c00", "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" + "14c25fa180ba", + "8cc8802c3592", + "fccaadef9a9a", + "b16a8e439428", + "1a0c83181ac5" ] } }, { "id": "settings-task-workspace-create-linear.result-null:created", "observation": { - "sender": ["d82310a7e5db"], - "payloads": ["975f2ff9a730"], + "sender": ["d82310a7e5db", "0b4d9ec099f7"], + "payloads": ["975f2ff9a730", "6485d1b0d446"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", + "state": "93cc0febafd5", "effects": [ "e0a8ee1f3c00", "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" + "14c25fa180ba", + "8cc8802c3592", + "fccaadef9a9a", + "b16a8e439428", + "1a0c83181ac5" ] } }, @@ -785,100 +893,105 @@ { "id": "settings-task-workspace-create-linear.outer-refused:created", "observation": { - "sender": ["5ef461d18660"], - "payloads": ["975f2ff9a730"], + "sender": ["5ef461d18660", "0b4d9ec099f7"], + "payloads": ["975f2ff9a730", "6485d1b0d446"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", + "state": "93cc0febafd5", "effects": [ "e0a8ee1f3c00", "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" + "14c25fa180ba", + "8cc8802c3592", + "fccaadef9a9a", + "b16a8e439428", + "1a0c83181ac5" ] } }, { "id": "settings-task-workspace-create-linear.outer-refused-no-message:created", "observation": { - "sender": ["3d80d04df464"], - "payloads": ["975f2ff9a730"], + "sender": ["3d80d04df464", "0b4d9ec099f7"], + "payloads": ["975f2ff9a730", "6485d1b0d446"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", + "state": "93cc0febafd5", "effects": [ "e0a8ee1f3c00", "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" + "14c25fa180ba", + "8cc8802c3592", + "fccaadef9a9a", + "b16a8e439428", + "1a0c83181ac5" ] } }, { "id": "settings-task-workspace-create-linear.method-not-found:created", "observation": { - "sender": ["fc902c20ffc9"], - "payloads": ["975f2ff9a730"], + "sender": ["fc902c20ffc9", "0b4d9ec099f7"], + "payloads": ["975f2ff9a730", "6485d1b0d446"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", + "state": "93cc0febafd5", "effects": [ "e0a8ee1f3c00", "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" + "14c25fa180ba", + "8cc8802c3592", + "fccaadef9a9a", + "b16a8e439428", + "1a0c83181ac5" ] } }, { "id": "settings-task-workspace-create-linear.transport-rejection:created", "observation": { - "sender": ["0de38c54b7c1"], - "payloads": ["975f2ff9a730"], + "sender": ["0de38c54b7c1", "0b4d9ec099f7"], + "payloads": ["975f2ff9a730", "6485d1b0d446"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", + "state": "93cc0febafd5", "effects": [ "e0a8ee1f3c00", "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" + "14c25fa180ba", + "8cc8802c3592", + "fccaadef9a9a", + "b16a8e439428", + "1a0c83181ac5" ] } }, { "id": "settings-task-workspace-create-linear.transport-rejection-no-message:created", "observation": { - "sender": ["5dfa692a84f8"], - "payloads": ["975f2ff9a730"], + "sender": ["5dfa692a84f8", "0b4d9ec099f7"], + "payloads": ["975f2ff9a730", "6485d1b0d446"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", + "state": "93cc0febafd5", "effects": [ "e0a8ee1f3c00", "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" + "14c25fa180ba", + "8cc8802c3592", + "fccaadef9a9a", + "b16a8e439428", + "1a0c83181ac5" ] } } diff --git a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json index f9a8a259419d..162435ff3ef8 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.task-workspace-settings.get-1.json @@ -73,23 +73,23 @@ } } }, - "1686352a2bbd": { - "name": "error", - "ordinal": 8, - "value": "Selected agent is disabled. Choose an enabled agent before creating." - }, - "1a4a4adee14f": { - "name": "creatingKey", - "ordinal": 8, + "2fbe9dcc1b1f": { + "name": "setupPrompt", + "ordinal": 5, "value": { - "$rpc": "null" + "agentOverride": "claude", + "command": "setup", + "item": { + "key": "linear:1", + "provider": "linear", + "source": { + "id": "issue-1" + } + }, + "repoName": "Repo", + "source": "repo" } }, - "2a351b169759": { - "name": "workspaceAgentOverridden", - "ordinal": 7, - "value": false - }, "377e2658e1c6": { "name": "settings.get#1", "ordinal": 3, @@ -186,11 +186,6 @@ "startedAt": 0 } }, - "483b4c51c982": { - "name": "error", - "ordinal": 7, - "value": "Selected agent is disabled. Choose an enabled agent before creating." - }, "515754ff63c6": { "name": "settings.get#1", "ordinal": 3, @@ -303,10 +298,12 @@ } } }, - "626f3cf8fc2d": { - "name": "workspaceAgentOverridden", + "6aabfe447970": { + "name": "creatingKey", "ordinal": 6, - "value": false + "value": { + "$rpc": "null" + } }, "7abdfe20af50": { "creating": "linear:1", @@ -360,23 +357,19 @@ } } }, - "8b8197eed660": { + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "93cc0febafd5": { "creating": { "$rpc": "null" }, - "error": "Selected agent is disabled. Choose an enabled agent before creating.", + "error": "", "settings": { - "defaultTuiAgent": "codex", - "disabledTuiAgents": ["claude"], - "hostSettingOverrides": {}, - "prBotAuthorOverrides": ["bot-user"], - "visibleTaskProviders": ["github", "linear"] + "disabledTuiAgents": ["claude"] } }, - "9270aeb7d9c6": { - "status": "pending", - "startedAt": 0 - }, "951af0d31429": { "name": "settings.get#1", "ordinal": 3, @@ -437,11 +430,6 @@ "error": "", "settings": {} }, - "c8c640db7dff": { - "name": "workspaceAgent", - "ordinal": 6, - "value": "codex" - }, "d048cafe213d": { "name": "settings.get#1", "ordinal": 3, @@ -474,20 +462,6 @@ } } }, - "d1777904aa15": { - "name": "workspaceAgent", - "ordinal": 5, - "value": "codex" - }, - "d5df3f6b123a": { - "creating": { - "$rpc": "null" - }, - "error": "Selected agent is disabled. Choose an enabled agent before creating.", - "settings": { - "disabledTuiAgents": ["claude"] - } - }, "d82310a7e5db": { "name": "settings.get#1", "ordinal": 3, @@ -547,13 +521,6 @@ "ordinal": 2, "value": "" }, - "f2ebfa81caaf": { - "name": "creatingKey", - "ordinal": 9, - "value": { - "$rpc": "null" - } - }, "fc902c20ffc9": { "name": "settings.get#1", "ordinal": 3, @@ -588,6 +555,19 @@ "ok": false } } + }, + "fddbc1c5f7be": { + "creating": { + "$rpc": "null" + }, + "error": "", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": ["claude"], + "hostSettingOverrides": {}, + "prBotAuthorOverrides": ["bot-user"], + "visibleTaskProviders": ["github", "linear"] + } } }, "recording": { @@ -616,14 +596,7 @@ "submit": "eb79a9b3682a" }, "state": "7abdfe20af50", - "effects": [ - "e0a8ee1f3c00", - "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" - ] + "effects": ["e0a8ee1f3c00", "ed3a7d6bc894", "2fbe9dcc1b1f", "6aabfe447970"] } }, { @@ -635,15 +608,13 @@ "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "8b8197eed660", + "state": "fddbc1c5f7be", "effects": [ "e0a8ee1f3c00", "ed3a7d6bc894", "82b1a94a28f4", - "c8c640db7dff", - "2a351b169759", - "1686352a2bbd", - "f2ebfa81caaf" + "a492527dfec9", + "d958e78341fc" ] } }, @@ -656,15 +627,8 @@ "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", - "effects": [ - "e0a8ee1f3c00", - "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" - ] + "state": "93cc0febafd5", + "effects": ["e0a8ee1f3c00", "ed3a7d6bc894", "2fbe9dcc1b1f", "6aabfe447970"] } }, { @@ -676,15 +640,8 @@ "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", - "effects": [ - "e0a8ee1f3c00", - "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" - ] + "state": "93cc0febafd5", + "effects": ["e0a8ee1f3c00", "ed3a7d6bc894", "2fbe9dcc1b1f", "6aabfe447970"] } }, { @@ -753,15 +710,8 @@ "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", - "effects": [ - "e0a8ee1f3c00", - "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" - ] + "state": "93cc0febafd5", + "effects": ["e0a8ee1f3c00", "ed3a7d6bc894", "2fbe9dcc1b1f", "6aabfe447970"] } }, { @@ -773,15 +723,8 @@ "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", - "effects": [ - "e0a8ee1f3c00", - "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" - ] + "state": "93cc0febafd5", + "effects": ["e0a8ee1f3c00", "ed3a7d6bc894", "2fbe9dcc1b1f", "6aabfe447970"] } }, { @@ -793,15 +736,8 @@ "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", - "effects": [ - "e0a8ee1f3c00", - "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" - ] + "state": "93cc0febafd5", + "effects": ["e0a8ee1f3c00", "ed3a7d6bc894", "2fbe9dcc1b1f", "6aabfe447970"] } }, { @@ -813,15 +749,8 @@ "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", - "effects": [ - "e0a8ee1f3c00", - "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" - ] + "state": "93cc0febafd5", + "effects": ["e0a8ee1f3c00", "ed3a7d6bc894", "2fbe9dcc1b1f", "6aabfe447970"] } }, { @@ -833,15 +762,8 @@ "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", - "effects": [ - "e0a8ee1f3c00", - "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" - ] + "state": "93cc0febafd5", + "effects": ["e0a8ee1f3c00", "ed3a7d6bc894", "2fbe9dcc1b1f", "6aabfe447970"] } } ] diff --git a/mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json b/mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json index 7f1ed092d634..51cda46c1ef7 100644 --- a/mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json +++ b/mobile/rpc-foundation/goldens/matrix-settings.workspace-submit-settings.get-1.json @@ -78,6 +78,44 @@ "disabledTuiAgents": ["claude"] } }, + "188b58f7070a": { + "name": "worktree.create#1", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "createdWithAgent": "claude", + "displayName": "recorded", + "displayNameKind": "user", + "name": "recorded", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupAgent": "claude" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Client closed", + "isRpcDeliveryUnknown": false + } + } + }, "1a9d36fca761": { "name": "settings.get#1", "ordinal": 2, @@ -115,18 +153,6 @@ } } }, - "285bfcc87a38": { - "name": "agentOverridden", - "ordinal": 6, - "value": false - }, - "2e8e352c8dd1": { - "creating": false, - "error": "Selected agent is disabled. Choose an enabled agent before creating.", - "settings": { - "disabledTuiAgents": ["claude"] - } - }, "34ebe202db00": { "name": "worktree.create#1", "ordinal": 5, @@ -165,14 +191,6 @@ } } }, - "37e61a793f91": { - "name": "selectedAgent", - "ordinal": 5, - "value": { - "id": "codex", - "label": "Codex" - } - }, "39f99cf479f0": { "name": "error", "ordinal": 1, @@ -188,11 +206,6 @@ "ordinal": 7, "value": "Connection closed" }, - "483b4c51c982": { - "name": "error", - "ordinal": 7, - "value": "Selected agent is disabled. Choose an enabled agent before creating." - }, "48d1cd313a00": { "name": "settings.get#1", "ordinal": 2, @@ -364,11 +377,6 @@ "startedAt": 0 } }, - "5940cb6d5fdf": { - "name": "error", - "ordinal": 6, - "value": "Selected agent is disabled. Choose an enabled agent before creating." - }, "5c233f0d08af": { "name": "settings.get#1", "ordinal": 2, @@ -403,15 +411,41 @@ } } }, - "5efbd884ea5a": { - "creating": false, - "error": "Selected agent is disabled. Choose an enabled agent before creating.", - "settings": { - "defaultTuiAgent": "codex", - "disabledTuiAgents": ["claude"], - "hostSettingOverrides": {}, - "prBotAuthorOverrides": ["bot-user"], - "visibleTaskProviders": ["github", "linear"] + "6bf09f13160c": { + "name": "error", + "ordinal": 5, + "value": "Client closed" + }, + "72251a62f76b": { + "name": "worktree.create#1", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "createdWithAgent": "claude", + "displayName": "recorded", + "displayNameKind": "user", + "name": "recorded", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupAgent": "claude" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 } }, "778f05cac35e": { @@ -461,18 +495,48 @@ "$rpc": "undefined" } }, + "83237138213c": { + "name": "worktree.create#1", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "createdWithAgent": "claude", + "displayName": "recorded", + "displayNameKind": "user", + "name": "recorded", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupAgent": "claude" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection closed", + "isRpcDeliveryUnknown": true + } + } + }, "9270aeb7d9c6": { "status": "pending", "startedAt": 0 }, - "94ddc6962cd6": { - "name": "selectedAgent", - "ordinal": 4, - "value": { - "id": "codex", - "label": "Codex" - } - }, "a5135e33554f": { "name": "settings.get#1", "ordinal": 2, @@ -507,10 +571,10 @@ } } }, - "bc0a5f0f4956": { - "name": "agentOverridden", - "ordinal": 5, - "value": false + "b441f0ab6ed1": { + "name": "error", + "ordinal": 6, + "value": "Connection closed" }, "c752520a09af": { "name": "settings.get#1", @@ -547,6 +611,17 @@ } } }, + "ca2c96890e77": { + "creating": true, + "error": "", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": ["claude"], + "hostSettingOverrides": {}, + "prBotAuthorOverrides": ["bot-user"], + "visibleTaskProviders": ["github", "linear"] + } + }, "d02636603cfd": { "name": "settings.get#1", "ordinal": 2, @@ -625,6 +700,11 @@ "$rpc": "undefined" } }, + "f3c0fdeccce0": { + "name": "worktree.create#1", + "ordinal": 5, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"setupDecision\":\"inherit\",\"name\":\"recorded\",\"displayName\":\"recorded\",\"displayNameKind\":\"user\",\"startupAgent\":\"claude\",\"createdWithAgent\":\"claude\"}}" + }, "f648b042d810": { "name": "settings.get#1", "ordinal": 3, @@ -650,59 +730,92 @@ { "id": "settings-workspace-submit-fulfilled.prelude:cleanup", "observation": { - "sender": ["dc0e0f9e2e34"], + "sender": ["dc0e0f9e2e34", "188b58f7070a"], "payloads": ["f648b042d810"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, "state": "13c996e4ec2b", - "effects": ["39f99cf479f0", "94ddc6962cd6", "bc0a5f0f4956", "5940cb6d5fdf"] + "effects": ["39f99cf479f0", "6bf09f13160c"] } }, { "id": "settings-workspace-submit-fulfilled.normal:settled", "observation": { - "sender": ["778f05cac35e"], - "payloads": ["f648b042d810"], + "sender": ["778f05cac35e", "58939cb0b8a9"], + "payloads": ["f648b042d810", "43d3776368e0"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "ca2c96890e77", + "effects": ["39f99cf479f0", "58752e3dadee"] + } + }, + { + "id": "settings-workspace-submit-fulfilled.normal:cleanup", + "observation": { + "sender": ["778f05cac35e", "34ebe202db00"], + "payloads": ["f648b042d810", "43d3776368e0"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "5efbd884ea5a", - "effects": [ - "39f99cf479f0", - "58752e3dadee", - "37e61a793f91", - "285bfcc87a38", - "483b4c51c982" - ] + "state": "ca2c96890e77", + "effects": ["39f99cf479f0", "58752e3dadee", "45fcf53c5b68"] } }, { "id": "settings-workspace-submit-fulfilled.result-absent:settled", "observation": { - "sender": ["d02636603cfd"], - "payloads": ["f648b042d810"], + "sender": ["d02636603cfd", "72251a62f76b"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0"] + } + }, + { + "id": "settings-workspace-submit-fulfilled.result-absent:cleanup", + "observation": { + "sender": ["d02636603cfd", "83237138213c"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "2e8e352c8dd1", - "effects": ["39f99cf479f0", "94ddc6962cd6", "bc0a5f0f4956", "5940cb6d5fdf"] + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0", "b441f0ab6ed1"] } }, { "id": "settings-workspace-submit-fulfilled.result-null:settled", "observation": { - "sender": ["a5135e33554f"], - "payloads": ["f648b042d810"], + "sender": ["a5135e33554f", "72251a62f76b"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0"] + } + }, + { + "id": "settings-workspace-submit-fulfilled.result-null:cleanup", + "observation": { + "sender": ["a5135e33554f", "83237138213c"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "2e8e352c8dd1", - "effects": ["39f99cf479f0", "94ddc6962cd6", "bc0a5f0f4956", "5940cb6d5fdf"] + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0", "b441f0ab6ed1"] } }, { @@ -786,66 +899,131 @@ { "id": "settings-workspace-submit-fulfilled.outer-refused:settled", "observation": { - "sender": ["48d1cd313a00"], - "payloads": ["f648b042d810"], + "sender": ["48d1cd313a00", "72251a62f76b"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0"] + } + }, + { + "id": "settings-workspace-submit-fulfilled.outer-refused:cleanup", + "observation": { + "sender": ["48d1cd313a00", "83237138213c"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "2e8e352c8dd1", - "effects": ["39f99cf479f0", "94ddc6962cd6", "bc0a5f0f4956", "5940cb6d5fdf"] + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0", "b441f0ab6ed1"] } }, { "id": "settings-workspace-submit-fulfilled.outer-refused-no-message:settled", "observation": { - "sender": ["003c16e56b1f"], - "payloads": ["f648b042d810"], + "sender": ["003c16e56b1f", "72251a62f76b"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0"] + } + }, + { + "id": "settings-workspace-submit-fulfilled.outer-refused-no-message:cleanup", + "observation": { + "sender": ["003c16e56b1f", "83237138213c"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "2e8e352c8dd1", - "effects": ["39f99cf479f0", "94ddc6962cd6", "bc0a5f0f4956", "5940cb6d5fdf"] + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0", "b441f0ab6ed1"] } }, { "id": "settings-workspace-submit-fulfilled.method-not-found:settled", "observation": { - "sender": ["5368ee038065"], - "payloads": ["f648b042d810"], + "sender": ["5368ee038065", "72251a62f76b"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0"] + } + }, + { + "id": "settings-workspace-submit-fulfilled.method-not-found:cleanup", + "observation": { + "sender": ["5368ee038065", "83237138213c"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "2e8e352c8dd1", - "effects": ["39f99cf479f0", "94ddc6962cd6", "bc0a5f0f4956", "5940cb6d5fdf"] + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0", "b441f0ab6ed1"] } }, { "id": "settings-workspace-submit-fulfilled.transport-rejection:settled", "observation": { - "sender": ["566d8202b582"], - "payloads": ["f648b042d810"], + "sender": ["566d8202b582", "72251a62f76b"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0"] + } + }, + { + "id": "settings-workspace-submit-fulfilled.transport-rejection:cleanup", + "observation": { + "sender": ["566d8202b582", "83237138213c"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "2e8e352c8dd1", - "effects": ["39f99cf479f0", "94ddc6962cd6", "bc0a5f0f4956", "5940cb6d5fdf"] + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0", "b441f0ab6ed1"] } }, { "id": "settings-workspace-submit-fulfilled.transport-rejection-no-message:settled", "observation": { - "sender": ["0ca529648be4"], - "payloads": ["f648b042d810"], + "sender": ["0ca529648be4", "72251a62f76b"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0"] + } + }, + { + "id": "settings-workspace-submit-fulfilled.transport-rejection-no-message:cleanup", + "observation": { + "sender": ["0ca529648be4", "83237138213c"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "2e8e352c8dd1", - "effects": ["39f99cf479f0", "94ddc6962cd6", "bc0a5f0f4956", "5940cb6d5fdf"] + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0", "b441f0ab6ed1"] } } ] diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json b/mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json index 20a9306b76ee..cbbcdbc522f5 100644 --- a/mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-fulfilled.json @@ -4,16 +4,6 @@ "family": "settings.task-workspace", "namedDeltas": [], "values": { - "1686352a2bbd": { - "name": "error", - "ordinal": 8, - "value": "Selected agent is disabled. Choose an enabled agent before creating." - }, - "2a351b169759": { - "name": "workspaceAgentOverridden", - "ordinal": 7, - "value": false - }, "41a2214deb19": { "name": "settings.get#1", "ordinal": 3, @@ -98,19 +88,6 @@ "visibleTaskProviders": ["github", "linear"] } }, - "8b8197eed660": { - "creating": { - "$rpc": "null" - }, - "error": "Selected agent is disabled. Choose an enabled agent before creating.", - "settings": { - "defaultTuiAgent": "codex", - "disabledTuiAgents": ["claude"], - "hostSettingOverrides": {}, - "prBotAuthorOverrides": ["bot-user"], - "visibleTaskProviders": ["github", "linear"] - } - }, "9270aeb7d9c6": { "status": "pending", "startedAt": 0 @@ -120,10 +97,29 @@ "ordinal": 4, "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}" }, - "c8c640db7dff": { - "name": "workspaceAgent", + "a492527dfec9": { + "name": "setupPrompt", "ordinal": 6, - "value": "codex" + "value": { + "agentOverride": "claude", + "command": "setup", + "item": { + "key": "linear:1", + "provider": "linear", + "source": { + "id": "issue-1" + } + }, + "repoName": "Repo", + "source": "repo" + } + }, + "d958e78341fc": { + "name": "creatingKey", + "ordinal": 7, + "value": { + "$rpc": "null" + } }, "e0a8ee1f3c00": { "name": "creatingKey", @@ -143,11 +139,17 @@ "ordinal": 2, "value": "" }, - "f2ebfa81caaf": { - "name": "creatingKey", - "ordinal": 9, - "value": { + "fddbc1c5f7be": { + "creating": { "$rpc": "null" + }, + "error": "", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": ["claude"], + "hostSettingOverrides": {}, + "prBotAuthorOverrides": ["bot-user"], + "visibleTaskProviders": ["github", "linear"] } } }, @@ -176,15 +178,13 @@ "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "8b8197eed660", + "state": "fddbc1c5f7be", "effects": [ "e0a8ee1f3c00", "ed3a7d6bc894", "82b1a94a28f4", - "c8c640db7dff", - "2a351b169759", - "1686352a2bbd", - "f2ebfa81caaf" + "a492527dfec9", + "d958e78341fc" ] } } diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-refused.json b/mobile/rpc-foundation/goldens/settings-task-workspace-refused.json index ee6d6a8339b4..44c4a1e43998 100644 --- a/mobile/rpc-foundation/goldens/settings-task-workspace-refused.json +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-refused.json @@ -4,13 +4,6 @@ "family": "settings.task-workspace", "namedDeltas": [], "values": { - "1a4a4adee14f": { - "name": "creatingKey", - "ordinal": 8, - "value": { - "$rpc": "null" - } - }, "2c83b2000f2a": { "name": "settings.get#1", "ordinal": 3, @@ -46,6 +39,23 @@ } } }, + "2fbe9dcc1b1f": { + "name": "setupPrompt", + "ordinal": 5, + "value": { + "agentOverride": "claude", + "command": "setup", + "item": { + "key": "linear:1", + "provider": "linear", + "source": { + "id": "issue-1" + } + }, + "repoName": "Repo", + "source": "repo" + } + }, "41a2214deb19": { "name": "settings.get#1", "ordinal": 3, @@ -72,15 +82,12 @@ "startedAt": 0 } }, - "483b4c51c982": { - "name": "error", - "ordinal": 7, - "value": "Selected agent is disabled. Choose an enabled agent before creating." - }, - "626f3cf8fc2d": { - "name": "workspaceAgentOverridden", + "6aabfe447970": { + "name": "creatingKey", "ordinal": 6, - "value": false + "value": { + "$rpc": "null" + } }, "7abdfe20af50": { "creating": "linear:1", @@ -93,25 +100,20 @@ "status": "pending", "startedAt": 0 }, - "975f2ff9a730": { - "name": "settings.get#1", - "ordinal": 4, - "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}" - }, - "d1777904aa15": { - "name": "workspaceAgent", - "ordinal": 5, - "value": "codex" - }, - "d5df3f6b123a": { + "93cc0febafd5": { "creating": { "$rpc": "null" }, - "error": "Selected agent is disabled. Choose an enabled agent before creating.", + "error": "", "settings": { "disabledTuiAgents": ["claude"] } }, + "975f2ff9a730": { + "name": "settings.get#1", + "ordinal": 4, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}" + }, "e0a8ee1f3c00": { "name": "creatingKey", "ordinal": 1, @@ -156,15 +158,8 @@ "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", - "effects": [ - "e0a8ee1f3c00", - "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" - ] + "state": "93cc0febafd5", + "effects": ["e0a8ee1f3c00", "ed3a7d6bc894", "2fbe9dcc1b1f", "6aabfe447970"] } } ] diff --git a/mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json b/mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json index 62bceef74bcb..b1139a0e09e6 100644 --- a/mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-task-workspace-transport-error.json @@ -4,11 +4,21 @@ "family": "settings.task-workspace", "namedDeltas": [], "values": { - "1a4a4adee14f": { - "name": "creatingKey", - "ordinal": 8, + "2fbe9dcc1b1f": { + "name": "setupPrompt", + "ordinal": 5, "value": { - "$rpc": "null" + "agentOverride": "claude", + "command": "setup", + "item": { + "key": "linear:1", + "provider": "linear", + "source": { + "id": "issue-1" + } + }, + "repoName": "Repo", + "source": "repo" } }, "41a2214deb19": { @@ -37,11 +47,6 @@ "startedAt": 0 } }, - "483b4c51c982": { - "name": "error", - "ordinal": 7, - "value": "Selected agent is disabled. Choose an enabled agent before creating." - }, "5deea0bbb564": { "name": "settings.get#1", "ordinal": 3, @@ -74,10 +79,12 @@ } } }, - "626f3cf8fc2d": { - "name": "workspaceAgentOverridden", + "6aabfe447970": { + "name": "creatingKey", "ordinal": 6, - "value": false + "value": { + "$rpc": "null" + } }, "7abdfe20af50": { "creating": "linear:1", @@ -90,25 +97,20 @@ "status": "pending", "startedAt": 0 }, - "975f2ff9a730": { - "name": "settings.get#1", - "ordinal": 4, - "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}" - }, - "d1777904aa15": { - "name": "workspaceAgent", - "ordinal": 5, - "value": "codex" - }, - "d5df3f6b123a": { + "93cc0febafd5": { "creating": { "$rpc": "null" }, - "error": "Selected agent is disabled. Choose an enabled agent before creating.", + "error": "", "settings": { "disabledTuiAgents": ["claude"] } }, + "975f2ff9a730": { + "name": "settings.get#1", + "ordinal": 4, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"settings.get\"}" + }, "e0a8ee1f3c00": { "name": "creatingKey", "ordinal": 1, @@ -153,15 +155,8 @@ "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "d5df3f6b123a", - "effects": [ - "e0a8ee1f3c00", - "ed3a7d6bc894", - "d1777904aa15", - "626f3cf8fc2d", - "483b4c51c982", - "1a4a4adee14f" - ] + "state": "93cc0febafd5", + "effects": ["e0a8ee1f3c00", "ed3a7d6bc894", "2fbe9dcc1b1f", "6aabfe447970"] } } ] diff --git a/mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json b/mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json index 6dc3f9490c3a..340c3b402093 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-submit-fulfilled.json @@ -11,17 +11,42 @@ "disabledTuiAgents": ["claude"] } }, - "285bfcc87a38": { - "name": "agentOverridden", - "ordinal": 6, - "value": false - }, - "37e61a793f91": { - "name": "selectedAgent", + "34ebe202db00": { + "name": "worktree.create#1", "ordinal": 5, - "value": { - "id": "codex", - "label": "Codex" + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "createdWithAgent": "claude", + "displayName": "recorded", + "displayNameKind": "user", + "name": "recorded", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupAgent": "claude" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection closed", + "isRpcDeliveryUnknown": true + } } }, "39f99cf479f0": { @@ -29,10 +54,15 @@ "ordinal": 1, "value": "" }, - "483b4c51c982": { + "43d3776368e0": { + "name": "worktree.create#1", + "ordinal": 6, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"setupDecision\":\"inherit\",\"name\":\"recorded\",\"displayName\":\"recorded\",\"displayNameKind\":\"user\",\"startupAgent\":\"claude\",\"createdWithAgent\":\"claude\"}}" + }, + "45fcf53c5b68": { "name": "error", "ordinal": 7, - "value": "Selected agent is disabled. Choose an enabled agent before creating." + "value": "Connection closed" }, "51d4cb56be85": { "name": "settings.get#1", @@ -71,15 +101,36 @@ "visibleTaskProviders": ["github", "linear"] } }, - "5efbd884ea5a": { - "creating": false, - "error": "Selected agent is disabled. Choose an enabled agent before creating.", - "settings": { - "defaultTuiAgent": "codex", - "disabledTuiAgents": ["claude"], - "hostSettingOverrides": {}, - "prBotAuthorOverrides": ["bot-user"], - "visibleTaskProviders": ["github", "linear"] + "58939cb0b8a9": { + "name": "worktree.create#1", + "ordinal": 5, + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "createdWithAgent": "claude", + "displayName": "recorded", + "displayNameKind": "user", + "name": "recorded", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupAgent": "claude" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 } }, "778f05cac35e": { @@ -126,6 +177,17 @@ "status": "pending", "startedAt": 0 }, + "ca2c96890e77": { + "creating": true, + "error": "", + "settings": { + "defaultTuiAgent": "codex", + "disabledTuiAgents": ["claude"], + "hostSettingOverrides": {}, + "prBotAuthorOverrides": ["bot-user"], + "visibleTaskProviders": ["github", "linear"] + } + }, "eb79a9b3682a": { "status": "fulfilled", "startedAt": 0, @@ -159,20 +221,27 @@ { "id": "settled", "observation": { - "sender": ["778f05cac35e"], - "payloads": ["f648b042d810"], + "sender": ["778f05cac35e", "58939cb0b8a9"], + "payloads": ["f648b042d810", "43d3776368e0"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "ca2c96890e77", + "effects": ["39f99cf479f0", "58752e3dadee"] + } + }, + { + "id": "cleanup", + "observation": { + "sender": ["778f05cac35e", "34ebe202db00"], + "payloads": ["f648b042d810", "43d3776368e0"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "5efbd884ea5a", - "effects": [ - "39f99cf479f0", - "58752e3dadee", - "37e61a793f91", - "285bfcc87a38", - "483b4c51c982" - ] + "state": "ca2c96890e77", + "effects": ["39f99cf479f0", "58752e3dadee", "45fcf53c5b68"] } } ] diff --git a/mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json b/mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json index f5459aab8edc..8438ee2f288f 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-submit-refused.json @@ -11,13 +11,6 @@ "disabledTuiAgents": ["claude"] } }, - "2e8e352c8dd1": { - "creating": false, - "error": "Selected agent is disabled. Choose an enabled agent before creating.", - "settings": { - "disabledTuiAgents": ["claude"] - } - }, "39f99cf479f0": { "name": "error", "ordinal": 1, @@ -49,27 +42,84 @@ "startedAt": 0 } }, - "5940cb6d5fdf": { - "name": "error", - "ordinal": 6, - "value": "Selected agent is disabled. Choose an enabled agent before creating." + "72251a62f76b": { + "name": "worktree.create#1", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "createdWithAgent": "claude", + "displayName": "recorded", + "displayNameKind": "user", + "name": "recorded", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupAgent": "claude" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "83237138213c": { + "name": "worktree.create#1", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "createdWithAgent": "claude", + "displayName": "recorded", + "displayNameKind": "user", + "name": "recorded", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupAgent": "claude" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection closed", + "isRpcDeliveryUnknown": true + } + } }, "9270aeb7d9c6": { "status": "pending", "startedAt": 0 }, - "94ddc6962cd6": { - "name": "selectedAgent", - "ordinal": 4, - "value": { - "id": "codex", - "label": "Codex" - } - }, - "bc0a5f0f4956": { - "name": "agentOverridden", - "ordinal": 5, - "value": false + "b441f0ab6ed1": { + "name": "error", + "ordinal": 6, + "value": "Connection closed" }, "d942c8ef3642": { "name": "settings.get#1", @@ -114,6 +164,11 @@ "$rpc": "undefined" } }, + "f3c0fdeccce0": { + "name": "worktree.create#1", + "ordinal": 5, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"setupDecision\":\"inherit\",\"name\":\"recorded\",\"displayName\":\"recorded\",\"displayNameKind\":\"user\",\"startupAgent\":\"claude\",\"createdWithAgent\":\"claude\"}}" + }, "f648b042d810": { "name": "settings.get#1", "ordinal": 3, @@ -139,14 +194,27 @@ { "id": "settled", "observation": { - "sender": ["d942c8ef3642"], - "payloads": ["f648b042d810"], + "sender": ["d942c8ef3642", "72251a62f76b"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0"] + } + }, + { + "id": "cleanup", + "observation": { + "sender": ["d942c8ef3642", "83237138213c"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "2e8e352c8dd1", - "effects": ["39f99cf479f0", "94ddc6962cd6", "bc0a5f0f4956", "5940cb6d5fdf"] + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0", "b441f0ab6ed1"] } } ] diff --git a/mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json b/mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json index 20fec5784e93..4a24c4f4efe2 100644 --- a/mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json +++ b/mobile/rpc-foundation/goldens/settings-workspace-submit-transport-error.json @@ -11,13 +11,6 @@ "disabledTuiAgents": ["claude"] } }, - "2e8e352c8dd1": { - "creating": false, - "error": "Selected agent is disabled. Choose an enabled agent before creating.", - "settings": { - "disabledTuiAgents": ["claude"] - } - }, "39f99cf479f0": { "name": "error", "ordinal": 1, @@ -49,27 +42,84 @@ "startedAt": 0 } }, - "5940cb6d5fdf": { - "name": "error", - "ordinal": 6, - "value": "Selected agent is disabled. Choose an enabled agent before creating." + "72251a62f76b": { + "name": "worktree.create#1", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "createdWithAgent": "claude", + "displayName": "recorded", + "displayNameKind": "user", + "name": "recorded", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupAgent": "claude" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "83237138213c": { + "name": "worktree.create#1", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "worktree.create" + }, + { + "name": "params", + "value": { + "createdWithAgent": "claude", + "displayName": "recorded", + "displayNameKind": "user", + "name": "recorded", + "repo": "id:repo-1", + "setupDecision": "inherit", + "startupAgent": "claude" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 600000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "Connection closed", + "isRpcDeliveryUnknown": true + } + } }, "9270aeb7d9c6": { "status": "pending", "startedAt": 0 }, - "94ddc6962cd6": { - "name": "selectedAgent", - "ordinal": 4, - "value": { - "id": "codex", - "label": "Codex" - } - }, - "bc0a5f0f4956": { - "name": "agentOverridden", - "ordinal": 5, - "value": false + "b441f0ab6ed1": { + "name": "error", + "ordinal": 6, + "value": "Connection closed" }, "eb79a9b3682a": { "status": "fulfilled", @@ -79,6 +129,11 @@ "$rpc": "undefined" } }, + "f3c0fdeccce0": { + "name": "worktree.create#1", + "ordinal": 5, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.create\",\"params\":{\"repo\":\"id:repo-1\",\"setupDecision\":\"inherit\",\"name\":\"recorded\",\"displayName\":\"recorded\",\"displayNameKind\":\"user\",\"startupAgent\":\"claude\",\"createdWithAgent\":\"claude\"}}" + }, "f4c218ff736f": { "name": "settings.get#1", "ordinal": 2, @@ -136,14 +191,27 @@ { "id": "settled", "observation": { - "sender": ["f4c218ff736f"], - "payloads": ["f648b042d810"], + "sender": ["f4c218ff736f", "72251a62f76b"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], + "settlements": { + "mount": "eb79a9b3682a", + "submit": "9270aeb7d9c6" + }, + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0"] + } + }, + { + "id": "cleanup", + "observation": { + "sender": ["f4c218ff736f", "83237138213c"], + "payloads": ["f648b042d810", "f3c0fdeccce0"], "settlements": { "mount": "eb79a9b3682a", "submit": "eb79a9b3682a" }, - "state": "2e8e352c8dd1", - "effects": ["39f99cf479f0", "94ddc6962cd6", "bc0a5f0f4956", "5940cb6d5fdf"] + "state": "13c996e4ec2b", + "effects": ["39f99cf479f0", "b441f0ab6ed1"] } } ] diff --git a/mobile/src/components/NewWorktreeModal.tsx b/mobile/src/components/NewWorktreeModal.tsx index 0e343f589e0c..e102781bc09d 100644 --- a/mobile/src/components/NewWorktreeModal.tsx +++ b/mobile/src/components/NewWorktreeModal.tsx @@ -111,11 +111,8 @@ function NewWorktreeModalContent(props: NewWorktreeModalProps) { client, selectedRepo, selectedAgent: agentSelection.selectedAgent, - setSelectedAgent: agentSelection.setSelectedAgent, - setAgentOverridden: agentSelection.setAgentOverridden, runtimeSettings: runtime.runtimeSettings, setRuntimeSettings: runtime.setRuntimeSettings, - detectedAgentIds: executionTarget.detectedAgentIds, sshGate: executionTarget.sshGate, composer, note, diff --git a/mobile/src/components/new-worktree-agent-selection.test.ts b/mobile/src/components/new-worktree-agent-selection.test.ts index dde450d66b9e..bd2308b8c81b 100644 --- a/mobile/src/components/new-worktree-agent-selection.test.ts +++ b/mobile/src/components/new-worktree-agent-selection.test.ts @@ -52,7 +52,7 @@ describe('new worktree agent selection', () => { }) }) - it('clears a disabled user override after detection completes', () => { + it('keeps a user override that is hidden in Settings', () => { const resolved = resolveNewWorktreeAgentSelection({ visible: true, selectedAgent: newWorktreeAgentOptionFor('codex'), @@ -62,8 +62,8 @@ describe('new worktree agent selection', () => { }) expect(resolved).toEqual({ - selectedAgent: newWorktreeAgentOptionFor('claude'), - agentOverridden: false + selectedAgent: newWorktreeAgentOptionFor('codex'), + agentOverridden: true }) }) diff --git a/mobile/src/components/new-worktree-agent-selection.ts b/mobile/src/components/new-worktree-agent-selection.ts index 1e8836a5fed9..d5fa46e2ddeb 100644 --- a/mobile/src/components/new-worktree-agent-selection.ts +++ b/mobile/src/components/new-worktree-agent-selection.ts @@ -1,6 +1,5 @@ import type { TuiAgent } from '../../../src/shared/tui-agent' import { MOBILE_AGENT_CATALOG } from '../tasks/mobile-agent-catalog' -import { isMobileTuiAgentEnabled } from '../tasks/mobile-tui-agents' import { pickWorkspaceAgent } from '../tasks/workspace-agent-selection' export type NewWorktreeRuntimeSettings = { @@ -43,18 +42,12 @@ export function pickPreferredNewWorktreeAgent( ) } +// A chosen agent stays chosen even when hidden in Settings; only an undetected one is dropped. function isSelectableAgent( agent: NewWorktreeAgentOption, - settings: NewWorktreeRuntimeSettings | null, detectedAgentIds: Set | null ): boolean { - if (agent.id === '__blank__') { - return true - } - if (!isMobileTuiAgentEnabled(agent.id, settings?.disabledTuiAgents)) { - return false - } - return detectedAgentIds === null || detectedAgentIds.has(agent.id) + return agent.id === '__blank__' || detectedAgentIds === null || detectedAgentIds.has(agent.id) } export function resolveNewWorktreeAgentSelection({ @@ -79,10 +72,7 @@ export function resolveNewWorktreeAgentSelection({ return { selectedAgent: preferred, agentOverridden: false } } - if ( - detectedAgentIds !== null && - !isSelectableAgent(selectedAgent, runtimeSettings, detectedAgentIds) - ) { + if (detectedAgentIds !== null && !isSelectableAgent(selectedAgent, detectedAgentIds)) { return { selectedAgent: preferred, agentOverridden: false } } diff --git a/mobile/src/components/use-new-workspace-create-submit.ts b/mobile/src/components/use-new-workspace-create-submit.ts index 5ea89f23efc0..03780fe682cd 100644 --- a/mobile/src/components/use-new-workspace-create-submit.ts +++ b/mobile/src/components/use-new-workspace-create-submit.ts @@ -4,7 +4,6 @@ import type { PersistedTrustedOrcaHooks } from '../../../src/shared/orca-yaml-ho import type { RetiredNameRegistry } from '../../../src/shared/worktree/retired-name-registry' import type { RpcClient } from '../transport/rpc-client' import { createBlankWorkspace } from '../tasks/blank-workspace-create' -import { isMobileTuiAgentEnabled } from '../tasks/mobile-tui-agents' import { isSetupHookTrusted, persistSetupHookTrustApproval, @@ -18,10 +17,9 @@ import type { AgentLaunchSupport } from '../tasks/agent-launch-request' import type { WorkspaceSshGate } from '../tasks/workspace-ssh-gate' import type { useMobileComposerSource } from '../tasks/use-mobile-composer-source' import type { WorktreeCreateIdempotencySupport } from '../tasks/worktree-create-idempotency-policy' -import { - pickPreferredNewWorktreeAgent, - type NewWorktreeAgentOption, - type NewWorktreeRuntimeSettings +import type { + NewWorktreeAgentOption, + NewWorktreeRuntimeSettings } from './new-worktree-agent-selection' import type { MobileWorkspaceRepo, SetupRunPolicy } from './new-worktree-modal-types' import type { SetupTrustPrompt } from './SetupHookTrustDrawer' @@ -39,11 +37,8 @@ export function useNewWorkspaceCreateSubmit(args: { client: RpcClient | null selectedRepo: MobileWorkspaceRepo | null selectedAgent: NewWorktreeAgentOption - setSelectedAgent: (agent: NewWorktreeAgentOption) => void - setAgentOverridden: (overridden: boolean) => void runtimeSettings: NewWorktreeRuntimeSettings | null setRuntimeSettings: (settings: NewWorktreeRuntimeSettings) => void - detectedAgentIds: Set | null sshGate: WorkspaceSshGate composer: Composer note: string @@ -88,28 +83,15 @@ export function useNewWorkspaceCreateSubmit(args: { args.setError(`Connect ${selectedRepo.displayName} before creating a workspace.`) return } - let latestRuntimeSettings = args.runtimeSettings try { const settingsReply = await settingsRead.request(client) const settings = settingsRead.interpret(settingsReply) if (settings.accepted) { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - latestRuntimeSettings = settings.value as NewWorktreeRuntimeSettings - args.setRuntimeSettings(latestRuntimeSettings) + args.setRuntimeSettings(settings.value as NewWorktreeRuntimeSettings) } } catch { - // The runtime validates the same setting before spawning. - } - if ( - args.selectedAgent.id !== '__blank__' && - !isMobileTuiAgentEnabled(args.selectedAgent.id, latestRuntimeSettings?.disabledTuiAgents) - ) { - args.setSelectedAgent( - pickPreferredNewWorktreeAgent(latestRuntimeSettings, args.detectedAgentIds) - ) - args.setAgentOverridden(false) - args.setError('Selected agent is disabled. Choose an enabled agent before creating.') - return + // Best-effort refresh, so the sheet shows the host's current agents if the create fails. } const trimmedName = args.composer.name.trim() diff --git a/mobile/src/session/mobile-source-control-launch-agent.ts b/mobile/src/session/mobile-source-control-launch-agent.ts index e017c48410cb..c7da7007fd15 100644 --- a/mobile/src/session/mobile-source-control-launch-agent.ts +++ b/mobile/src/session/mobile-source-control-launch-agent.ts @@ -1,6 +1,7 @@ /** - * Which agent a source-control AI button starts from the phone: the desktop's own resolution - * (`pickSourceControlLaunchAgent`), fed the same saved recipe, default agent and detected agents. + * Which agent a source-control AI button starts from the phone: the action's saved agent when this + * host has it, else the desktop's own automatic pick (`pickSourceControlLaunchAgent`) from the same + * default agent and detected agents. */ import type { GlobalSettings } from '../../../src/shared/global-settings-types' @@ -14,7 +15,6 @@ import { pickSourceControlLaunchAgent, readSourceControlLaunchRecipeAgentId } from '../../../src/shared/source-control-launch-agent-selection' -import { filterEnabledTuiAgents } from '../../../src/shared/tui-agent-selection' import type { TuiAgent } from '../../../src/shared/tui-agent' import { isMobileTuiAgent } from '../tasks/mobile-tui-agents' import type { MobileAgentLaunchContext } from './mobile-new-tab-agent-loader' @@ -38,19 +38,18 @@ export function resolveMobileSourceControlLaunchAgent( const detectedAgents = context.detectedAgents.filter(isMobileTuiAgent) const recipe = actionId ? readSavedRecipe(settings, context.repo, actionId) : null const savedAgent = readSourceControlLaunchRecipeAgentId(recipe) - // Why: the phone has no pre-launch dialog showing the pick, so a saved agent that can't run here - // is an error, as on the desktop's direct launch, rather than a silent swap to another agent. - if ( - savedAgent && - !filterEnabledTuiAgents(detectedAgents, settings?.disabledTuiAgents).includes(savedAgent) - ) { - return { - kind: 'unavailable', - message: 'The saved agent for this action is not available on this workspace host.' - } + // Why: the phone has no pre-launch dialog showing the pick, so a saved agent that isn't on this + // host is an error, as on the desktop's direct launch, rather than a silent swap to another agent. + // A saved agent hidden in Settings still starts; hiding only skips it in the automatic pick. + if (savedAgent) { + return detectedAgents.includes(savedAgent) + ? { kind: 'agent', agent: savedAgent, recipe } + : { + kind: 'unavailable', + message: 'The saved agent for this action is not available on this workspace host.' + } } const agent = pickSourceControlLaunchAgent({ - savedAgent, defaultAgent: settings?.defaultTuiAgent, detectedAgents, disabledAgents: settings?.disabledTuiAgents diff --git a/mobile/src/session/pr-ai-triage-launch.test.ts b/mobile/src/session/pr-ai-triage-launch.test.ts index 16c8132836f3..2a4ccdbd5b10 100644 --- a/mobile/src/session/pr-ai-triage-launch.test.ts +++ b/mobile/src/session/pr-ai-triage-launch.test.ts @@ -192,6 +192,30 @@ describe('launchAgentWithPrompt', () => { expect(launchParams(sendRequest)).toBeUndefined() }) + it("sends a saved agent hidden in Settings, and shows an older host's refusal of it", async () => { + const { client, sendRequest } = hostClient({ + settings: { + disabledTuiAgents: ['claude'], + sourceControlAi: { actions: { fixChecks: { agentId: 'claude' } } } + }, + // A host from before hidden agents could launch still refuses them. + launch: { + id: 'x', + ok: false, + error: { + code: 'agent_launch_failed', + message: 'Agent claude is disabled. Choose an enabled agent.' + }, + _meta: { runtimeId: 'r' } + } + }) + await expect(run(client)).resolves.toEqual({ + kind: 'not-started', + message: 'Agent claude is disabled. Choose an enabled agent.' + }) + expect(launchParams(sendRequest)).toMatchObject({ agent: 'claude' }) + }) + it('still launches an agent when the default is a blank terminal', async () => { const { client, sendRequest } = hostClient({ settings: { defaultTuiAgent: 'blank' }, diff --git a/mobile/src/tasks/mobile-tasks-dependencies.ts b/mobile/src/tasks/mobile-tasks-dependencies.ts index b0e8a9cd68d1..a039d22b8738 100644 --- a/mobile/src/tasks/mobile-tasks-dependencies.ts +++ b/mobile/src/tasks/mobile-tasks-dependencies.ts @@ -92,7 +92,6 @@ export { buildTaskWorkspaceCreateParams } from './workspace-create-params' export { MOBILE_TASKS_CAPABILITY } from './mobile-tasks-capability' export { filterWorkspaceAgents, - isWorkspaceAgentEnabled, pickWorkspaceAgent, resolveWorkspaceAgentSelection, workspaceAgentLabel diff --git a/mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx b/mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx index 4416feb0d31a..4f605f2b966c 100644 --- a/mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx +++ b/mobile/src/tasks/use-mobile-tasks-workspace-create-actions.tsx @@ -6,7 +6,6 @@ import { type WorkspaceAgentChoice, buildTaskWorkspaceCreateParams, isSetupHookTrusted, - isWorkspaceAgentEnabled, pickWorkspaceAgent, shouldResolveHostedReviewStartPoint, useCallback, @@ -40,8 +39,6 @@ export function useMobileTasksWorkspaceCreateActions(model: WorkspaceSshStateMod setOrcaYamlTrustPrompt, setRuntimeTaskSettings, setSetupPrompt, - setWorkspaceAgent, - setWorkspaceAgentOverridden, setWorkspaceCreateDraft, taskStateHydrated, tasksSupported, @@ -87,23 +84,11 @@ export function useMobileTasksWorkspaceCreateActions(model: WorkspaceSshStateMod setRuntimeTaskSettings(latestRuntimeTaskSettings) } } catch { - // Best-effort refresh; the runtime still validates agent availability before spawning. + // Best-effort refresh; only the automatic pick below reads it. } + // A chosen agent starts even when hidden in Settings; hiding only skips it in the automatic pick. const selectedAgent = - agentOverride && - (agentOverride === 'blank' || - isWorkspaceAgentEnabled(agentOverride, latestRuntimeTaskSettings.disabledTuiAgents)) - ? agentOverride - : pickWorkspaceAgent(latestRuntimeTaskSettings, workspaceDetectedAgentIds) - if ( - agentOverride && - agentOverride !== 'blank' && - !isWorkspaceAgentEnabled(agentOverride, latestRuntimeTaskSettings.disabledTuiAgents) - ) { - setWorkspaceAgent(selectedAgent) - setWorkspaceAgentOverridden(false) - throw new Error('Selected agent is disabled. Choose an enabled agent before creating.') - } + agentOverride ?? pickWorkspaceAgent(latestRuntimeTaskSettings, workspaceDetectedAgentIds) const setupResolution = await resolveCreateSetupDecision(targetRepo, setupOverride) const comment = noteOverride?.trim() if (setupResolution.kind === 'prompt') { diff --git a/mobile/src/tasks/use-mobile-tasks-workspace-create-projection.tsx b/mobile/src/tasks/use-mobile-tasks-workspace-create-projection.tsx index 197bf019d5be..98a20af02edb 100644 --- a/mobile/src/tasks/use-mobile-tasks-workspace-create-projection.tsx +++ b/mobile/src/tasks/use-mobile-tasks-workspace-create-projection.tsx @@ -8,7 +8,6 @@ import { type WorkspaceAgentChoice, deriveWorkspaceSshGate, filterWorkspaceAgents, - isWorkspaceAgentEnabled, parseSparsePresetDirectories, resolveComposerBranchSelection, useCallback, @@ -145,10 +144,10 @@ export function useMobileTasksWorkspaceCreateProjection(model: ProjectMetadataLo workspaceDetectedAgentIds === null ? new Set(enabledAgents) : new Set(enabledAgents.filter((agent) => workspaceDetectedAgentIds.has(agent))) + // The current choice stays listed even when hidden in Settings, so the picker can show it. if ( workspaceAgent && workspaceAgent !== 'blank' && - isWorkspaceAgentEnabled(workspaceAgent, runtimeTaskSettings.disabledTuiAgents) && (workspaceDetectedAgentIds === null || workspaceDetectedAgentIds.has(workspaceAgent)) ) { availableAgents.add(workspaceAgent) diff --git a/mobile/src/tasks/workspace-agent-selection.ts b/mobile/src/tasks/workspace-agent-selection.ts index 24dc376a663a..ba2f8db99d0c 100644 --- a/mobile/src/tasks/workspace-agent-selection.ts +++ b/mobile/src/tasks/workspace-agent-selection.ts @@ -63,10 +63,6 @@ export function filterWorkspaceAgents(agents: readonly TuiAgent[], disabled?: un return filterEnabledMobileTuiAgents(agents, disabled) } -export function isWorkspaceAgentEnabled(agent: TuiAgent, disabled?: unknown): boolean { - return isMobileTuiAgentEnabled(agent, disabled) -} - export function resolveWorkspaceAgentSelection({ selectionActive, settings, @@ -84,12 +80,8 @@ export function resolveWorkspaceAgentSelection({ return agent === pickedAgent ? current : { agent: pickedAgent, overridden: false } } - if ( - detectedAgentIds === null || - !agent || - agent === 'blank' || - (detectedAgentIds.has(agent) && isWorkspaceAgentEnabled(agent, settings.disabledTuiAgents)) - ) { + // A chosen agent stays chosen even when hidden in Settings; only an undetected one is dropped. + if (detectedAgentIds === null || !agent || agent === 'blank' || detectedAgentIds.has(agent)) { return current } diff --git a/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts b/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts index 891f134ec844..50fdacde2cf7 100644 --- a/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts +++ b/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts @@ -85,7 +85,6 @@ export function workspaceSettingsMounts( selectedRepo: { id: 'repo-1', displayName: 'Repo' }, selectedAgent: { id: 'claude', label: 'Claude' }, runtimeSettings: { disabledTuiAgents: ['claude'] }, - detectedAgentIds: new Set(['codex']), sshGate: { requiresConnection: false }, composer: { name: 'recorded', createSelection: null, isNameAutoManaged: false }, note: '', diff --git a/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts index 58b12257cdf4..d144eb3514d4 100644 --- a/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts +++ b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts @@ -172,8 +172,8 @@ export const OPERATION_MUTATIONS = { // Publishes the settings envelope as the refreshed workspace runtime settings. 'workspace-submit-envelope': { file: 'use-new-workspace-create-submit.ts', - before: 'latestRuntimeSettings = settings.value as NewWorktreeRuntimeSettings', - after: 'latestRuntimeSettings = settingsReply.result as NewWorktreeRuntimeSettings' + before: 'args.setRuntimeSettings(settings.value as NewWorktreeRuntimeSettings)', + after: 'args.setRuntimeSettings(settingsReply.result as NewWorktreeRuntimeSettings)' }, // Reads settings eagerly, so a null result throws before the sibling's refusal is checked. 'new-tab-deferred-settings-read': { From c465c4c50f6a9def366918f9377824bf3f328f52 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:34:36 -0700 Subject: [PATCH 2/5] fix(mobile): re-pick an automatic agent at create, and word an unconfirmed create Only an agent the user picked may start while hidden: both create screens now re-run the automatic pick from the settings read at create time unless the user chose the agent. The new-workspace sheet lists a hidden current choice like the task picker does. A launch-route create the host can't settle (agent_session_operation_unknown or _expired, which an older host also sends when it refuses a hidden agent) now reads "Couldn't confirm the workspace was created. Check your workspaces before trying again." instead of the raw code. --- mobile/src/components/NewWorktreeModal.tsx | 2 + .../use-new-workspace-agent-selection.ts | 7 +- .../use-new-workspace-create-submit.test.ts | 82 +++++++++++++++ .../use-new-workspace-create-submit.ts | 25 +++-- .../src/session/pr-ai-triage-launch.test.ts | 2 +- .../tasks/agent-launch-mobile-replay.test.ts | 9 +- .../mobile-agent-launch-architecture.test.ts | 19 ++-- .../mobile-tasks-workspace-create-drawer.tsx | 4 +- ...ile-tasks-workspace-create-actions.test.ts | 99 +++++++++++++++++++ .../tasks/workspace-agent-selection.test.ts | 12 +++ .../worktree-create-launch-operation.test.ts | 60 ++++++----- mobile/src/tasks/worktree-create-retry.ts | 11 ++- .../adapters/workspace-settings-mounts.ts | 3 + .../mutants/operation-mutations.ts | 4 +- 14 files changed, 288 insertions(+), 51 deletions(-) create mode 100644 mobile/src/components/use-new-workspace-create-submit.test.ts create mode 100644 mobile/src/tasks/use-mobile-tasks-workspace-create-actions.test.ts diff --git a/mobile/src/components/NewWorktreeModal.tsx b/mobile/src/components/NewWorktreeModal.tsx index e102781bc09d..033ae5a64f7c 100644 --- a/mobile/src/components/NewWorktreeModal.tsx +++ b/mobile/src/components/NewWorktreeModal.tsx @@ -111,8 +111,10 @@ function NewWorktreeModalContent(props: NewWorktreeModalProps) { client, selectedRepo, selectedAgent: agentSelection.selectedAgent, + agentOverridden: agentSelection.agentOverridden, runtimeSettings: runtime.runtimeSettings, setRuntimeSettings: runtime.setRuntimeSettings, + detectedAgentIds: executionTarget.detectedAgentIds, sshGate: executionTarget.sshGate, composer, note, diff --git a/mobile/src/components/use-new-workspace-agent-selection.ts b/mobile/src/components/use-new-workspace-agent-selection.ts index 3f3a50c993f0..c73e1ecffca7 100644 --- a/mobile/src/components/use-new-workspace-agent-selection.ts +++ b/mobile/src/components/use-new-workspace-agent-selection.ts @@ -14,6 +14,8 @@ export function useNewWorkspaceAgentSelection(args: { detectedAgentIds: Set | null }): { selectedAgent: NewWorktreeAgentOption + /** True when the user picked the agent; otherwise it is the automatic pick. */ + agentOverridden: boolean setSelectedAgent: (agent: NewWorktreeAgentOption) => void setAgentOverridden: (overridden: boolean) => void pickerAgentOptions: NewWorktreeAgentOption[] @@ -36,14 +38,17 @@ export function useNewWorkspaceAgentSelection(args: { setSelectedAgent(resolution.selectedAgent) setAgentOverridden(resolution.agentOverridden) } + // The current choice stays listed even when hidden in Settings, so the picker can show it. const visibleAgentOptions = NEW_WORKTREE_AGENT_OPTIONS.filter( (agent) => agent.id !== '__blank__' && (args.detectedAgentIds === null || args.detectedAgentIds.has(agent.id)) && - isMobileTuiAgentEnabled(agent.id, args.runtimeSettings?.disabledTuiAgents) + (agent.id === resolution.selectedAgent.id || + isMobileTuiAgentEnabled(agent.id, args.runtimeSettings?.disabledTuiAgents)) ) return { selectedAgent: resolution.selectedAgent, + agentOverridden: resolution.agentOverridden, setSelectedAgent, setAgentOverridden, pickerAgentOptions: [...visibleAgentOptions, NEW_WORKTREE_BLANK_AGENT] diff --git a/mobile/src/components/use-new-workspace-create-submit.test.ts b/mobile/src/components/use-new-workspace-create-submit.test.ts new file mode 100644 index 000000000000..611341b310cf --- /dev/null +++ b/mobile/src/components/use-new-workspace-create-submit.test.ts @@ -0,0 +1,82 @@ +import { createElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { EMPTY_RETIRED_NAME_REGISTRY } from '../../../src/shared/worktree/retired-name-registry' +import type { RpcClient } from '../transport/rpc-client' +import { newWorktreeAgentOptionFor } from './new-worktree-agent-selection' +import { useNewWorkspaceCreateSubmit } from './use-new-workspace-create-submit' + +type SubmitArgs = Parameters[0] + +// The host has hidden Claude since the sheet read its settings, when Claude was the default. +function hostClient() { + const sendRequest = vi.fn(async (method: string, _params?: unknown) => { + const result = + method === 'settings.get' + ? { settings: { defaultTuiAgent: 'claude', disabledTuiAgents: ['claude'] } } + : { worktree: { id: 'wt-1', displayName: 'recorded' } } + return { id: 'x', ok: true, result, _meta: { runtimeId: 'r' } } + }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a non-replaying create reaches only `sendRequest` on the client. + return { client: { sendRequest } as unknown as RpcClient, sendRequest } +} + +describe('useNewWorkspaceCreateSubmit agent', () => { + let renderer: ReactTestRenderer | null = null + + afterEach(() => { + act(() => renderer?.unmount()) + renderer = null + }) + + async function createWith(agentOverridden: boolean): Promise { + const { client, sendRequest } = hostClient() + const args: SubmitArgs = { + client, + selectedRepo: { id: 'repo-1', displayName: 'Repo', path: '/repo', kind: 'git' }, + selectedAgent: newWorktreeAgentOptionFor('claude'), + agentOverridden, + runtimeSettings: { defaultTuiAgent: 'claude', disabledTuiAgents: [] }, + setRuntimeSettings: () => {}, + detectedAgentIds: new Set(['claude', 'codex']), + sshGate: { status: null, requiresConnection: false, connectInProgress: false, error: null }, + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: create reads only these three composer members. + composer: { name: 'recorded', createSelection: null, isNameAutoManaged: false } as never, + note: '', + retiredWorktreeNames: EMPTY_RETIRED_NAME_REGISTRY, + setupCommand: null, + setupTrust: null, + setupRunPolicy: 'skip-by-default', + setupDecisionChoice: null, + runSetup: false, + trustedOrcaHooks: {}, + setTrustedOrcaHooks: () => {}, + getWorktreeCreateCutoverSupport: async () => false, + getAgentLaunchSupport: async () => false, + transitionDrawer: () => {}, + setError: () => {}, + onCreated: () => {}, + onClose: () => {} + } + let submit: ReturnType | null = null + function Harness(): null { + submit = useNewWorkspaceCreateSubmit(args) + return null + } + await act(async () => { + renderer = create(createElement(Harness)) + }) + await act(async () => { + await submit!.create() + }) + return sendRequest.mock.calls.find(([method]) => method === 'worktree.create')?.[1] + } + + it('re-picks an automatic agent the host has since hidden', async () => { + await expect(createWith(false)).resolves.toMatchObject({ startupAgent: 'codex' }) + }) + + it('starts an agent the user picked even though it is hidden', async () => { + await expect(createWith(true)).resolves.toMatchObject({ startupAgent: 'claude' }) + }) +}) diff --git a/mobile/src/components/use-new-workspace-create-submit.ts b/mobile/src/components/use-new-workspace-create-submit.ts index 03780fe682cd..c3c74fc235de 100644 --- a/mobile/src/components/use-new-workspace-create-submit.ts +++ b/mobile/src/components/use-new-workspace-create-submit.ts @@ -17,9 +17,10 @@ import type { AgentLaunchSupport } from '../tasks/agent-launch-request' import type { WorkspaceSshGate } from '../tasks/workspace-ssh-gate' import type { useMobileComposerSource } from '../tasks/use-mobile-composer-source' import type { WorktreeCreateIdempotencySupport } from '../tasks/worktree-create-idempotency-policy' -import type { - NewWorktreeAgentOption, - NewWorktreeRuntimeSettings +import { + pickPreferredNewWorktreeAgent, + type NewWorktreeAgentOption, + type NewWorktreeRuntimeSettings } from './new-worktree-agent-selection' import type { MobileWorkspaceRepo, SetupRunPolicy } from './new-worktree-modal-types' import type { SetupTrustPrompt } from './SetupHookTrustDrawer' @@ -37,8 +38,11 @@ export function useNewWorkspaceCreateSubmit(args: { client: RpcClient | null selectedRepo: MobileWorkspaceRepo | null selectedAgent: NewWorktreeAgentOption + /** True when the user picked `selectedAgent`; otherwise create re-picks from fresh Settings. */ + agentOverridden: boolean runtimeSettings: NewWorktreeRuntimeSettings | null setRuntimeSettings: (settings: NewWorktreeRuntimeSettings) => void + detectedAgentIds: Set | null sshGate: WorkspaceSshGate composer: Composer note: string @@ -83,16 +87,22 @@ export function useNewWorkspaceCreateSubmit(args: { args.setError(`Connect ${selectedRepo.displayName} before creating a workspace.`) return } + let latestRuntimeSettings = args.runtimeSettings try { const settingsReply = await settingsRead.request(client) const settings = settingsRead.interpret(settingsReply) if (settings.accepted) { // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Preserve the established response shape at this boundary. - args.setRuntimeSettings(settings.value as NewWorktreeRuntimeSettings) + latestRuntimeSettings = settings.value as NewWorktreeRuntimeSettings + args.setRuntimeSettings(latestRuntimeSettings) } } catch { - // Best-effort refresh, so the sheet shows the host's current agents if the create fails. + // Best-effort refresh; only the automatic pick below reads it. } + // A chosen agent starts even when hidden in Settings; hiding only skips it in the automatic pick. + const agent = args.agentOverridden + ? args.selectedAgent + : pickPreferredNewWorktreeAgent(latestRuntimeSettings, args.detectedAgentIds) const trimmedName = args.composer.name.trim() const baseName = @@ -133,8 +143,7 @@ export function useNewWorkspaceCreateSubmit(args: { return } - const createdWithAgentId = - args.selectedAgent.id !== '__blank__' ? args.selectedAgent.id : undefined + const createdWithAgentId = agent.id !== '__blank__' ? agent.id : undefined const trimmedNote = args.note.trim() || undefined const selection = args.composer.createSelection const result = selection @@ -143,7 +152,7 @@ export function useNewWorkspaceCreateSubmit(args: { selection, targetRepoId: selectedRepo.id, setupDecision, - agent: { choice: normalizeWorkspaceAgent(args.selectedAgent.id) ?? 'blank' }, + agent: { choice: normalizeWorkspaceAgent(agent.id) ?? 'blank' }, workspaceName: trimmedName || undefined, note: trimmedNote, nameIsAutoManaged: args.composer.isNameAutoManaged, diff --git a/mobile/src/session/pr-ai-triage-launch.test.ts b/mobile/src/session/pr-ai-triage-launch.test.ts index 2a4ccdbd5b10..cb88f8583fda 100644 --- a/mobile/src/session/pr-ai-triage-launch.test.ts +++ b/mobile/src/session/pr-ai-triage-launch.test.ts @@ -203,7 +203,7 @@ describe('launchAgentWithPrompt', () => { id: 'x', ok: false, error: { - code: 'agent_launch_failed', + code: 'runtime_error', message: 'Agent claude is disabled. Choose an enabled agent.' }, _meta: { runtimeId: 'r' } diff --git a/mobile/src/tasks/agent-launch-mobile-replay.test.ts b/mobile/src/tasks/agent-launch-mobile-replay.test.ts index 51d790b85dd5..b375119058ec 100644 --- a/mobile/src/tasks/agent-launch-mobile-replay.test.ts +++ b/mobile/src/tasks/agent-launch-mobile-replay.test.ts @@ -2,7 +2,10 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { createWorktreeWithNameRetry } from './worktree-create-retry' +import { + createWorktreeWithNameRetry, + WORKSPACE_CREATE_UNCONFIRMED_MESSAGE +} from './worktree-create-retry' import type { RpcClient } from '../transport/rpc-client' import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity' import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client' @@ -132,7 +135,7 @@ describe('mobile launch retries through the host ledger', () => { createStructuredSession.mockResolvedValue({ ok: false, refusal: { code, message: code } }) const launch = mobileLaunch() - await expect(launch.result).resolves.toEqual({ error: 'agent_session_operation_unknown' }) + await expect(launch.result).resolves.toEqual({ error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE }) expect(launch.runtime.createManagedWorktree).toHaveBeenCalledTimes(1) expect(launch.attempts).toHaveLength(1) expect(store.listOperationRows()[0]?.outcome.status).toBe('unknown') @@ -144,7 +147,7 @@ describe('mobile launch retries through the host ledger', () => { AGENT_SESSION_MAX_NEW_OPERATION_AGE_MS + AGENT_SESSION_OPERATION_FUTURE_SKEW_MS + 1 }) - await expect(launch.result).resolves.toEqual({ error: 'agent_session_operation_expired' }) + await expect(launch.result).resolves.toEqual({ error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE }) expect(launch.runtime.createManagedWorktree).toHaveBeenCalledTimes(1) expect(createStructuredSession).toHaveBeenCalledTimes(1) expect(launch.attempts).toHaveLength(2) diff --git a/mobile/src/tasks/mobile-agent-launch-architecture.test.ts b/mobile/src/tasks/mobile-agent-launch-architecture.test.ts index 2a4e4a82eedb..51fbd9617217 100644 --- a/mobile/src/tasks/mobile-agent-launch-architecture.test.ts +++ b/mobile/src/tasks/mobile-agent-launch-architecture.test.ts @@ -20,7 +20,10 @@ import { createStableLogicalRpcClient } from '../transport/stable-logical-rpc-cl import type { RpcClient } from '../transport/rpc-client' import type { RpcResponse } from '../transport/types' import { WorktreeCreateCollisionError } from '../../../src/shared/new-workspace/worktree-create-collision' -import { createWorktreeWithNameRetry } from './worktree-create-retry' +import { + createWorktreeWithNameRetry, + WORKSPACE_CREATE_UNCONFIRMED_MESSAGE +} from './worktree-create-retry' import { readNewWorktreeRuntimeCapabilities } from './worktree-create-capability' import { AGENT_LAUNCH_RUNTIME_CAPABILITY, @@ -256,9 +259,8 @@ describe('mobile launch retry authority', () => { it('does not duplicate when a replacement host strips operationId', async () => { const launch = scenario({ replacement: 'legacy' }) - await expect(launch.result).resolves.toMatchObject({ - error: expect.stringContaining('Unknown method') - }) + // The first send may have created the workspace, so the user is told to check, not "Unknown method". + await expect(launch.result).resolves.toEqual({ error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE }) expect( launch.runtime.createManagedWorktree.mock.calls.length + launch.replacement.createManagedWorktree.mock.calls.length @@ -312,16 +314,15 @@ describe('mobile launch retry authority', () => { } }) const launch = scenario() - await expect(launch.result).resolves.toEqual({ error: 'agent_session_operation_unknown' }) + await expect(launch.result).resolves.toEqual({ error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE }) expect(launch.runtime.createManagedWorktree).toHaveBeenCalledTimes(1) expect(createStructuredSession).toHaveBeenCalledTimes(1) }) it('refuses an older receiver reached after a connected timeout', async () => { const launch = scenario({ replacement: 'legacy', loss: 'timeout' }) - await expect(launch.result).resolves.toMatchObject({ - error: expect.stringContaining('Unknown method') - }) + // The first send may have created the workspace, so the user is told to check, not "Unknown method". + await expect(launch.result).resolves.toEqual({ error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE }) expect(launch.runtime.createManagedWorktree).toHaveBeenCalledTimes(1) expect(launch.replacement.createManagedWorktree).not.toHaveBeenCalled() }) @@ -358,7 +359,7 @@ describe('mobile launch retry authority', () => { refusal: { code: 'agent_session_operation_unknown', message } }) const launch = scenario() - await expect(launch.result).resolves.toEqual({ error: 'agent_session_operation_unknown' }) + await expect(launch.result).resolves.toEqual({ error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE }) expect(launch.runtime.createManagedWorktree).toHaveBeenCalledTimes(1) expect(createStructuredSession).toHaveBeenCalledTimes(1) }) diff --git a/mobile/src/tasks/mobile-tasks-workspace-create-drawer.tsx b/mobile/src/tasks/mobile-tasks-workspace-create-drawer.tsx index 352958ff0b54..d3cc9ddb40f9 100644 --- a/mobile/src/tasks/mobile-tasks-workspace-create-drawer.tsx +++ b/mobile/src/tasks/mobile-tasks-workspace-create-drawer.tsx @@ -38,6 +38,7 @@ export function renderMobileTasksWorkspaceCreateDrawer(model: ConnectionPresenta showWorkspaceAdvanced, taskUiReady, workspaceAgentDetectionPending, + workspaceAgentOverridden, workspaceBaseBranch, workspaceBranchAutoName, workspaceBranchNameOverride, @@ -253,7 +254,8 @@ export function renderMobileTasksWorkspaceCreateDrawer(model: ConnectionPresenta workspaceCreateDraft.item, workspaceCreateDraft.repoIdOverride, undefined, - resolvedWorkspaceAgent, + // Only the user's own pick is sent; create re-picks from fresh Settings otherwise. + workspaceAgentOverridden ? resolvedWorkspaceAgent : undefined, workspaceNameDraft.trim(), undefined, workspaceBaseBranch?.refName, diff --git a/mobile/src/tasks/use-mobile-tasks-workspace-create-actions.test.ts b/mobile/src/tasks/use-mobile-tasks-workspace-create-actions.test.ts new file mode 100644 index 000000000000..d0a5a33a4f6e --- /dev/null +++ b/mobile/src/tasks/use-mobile-tasks-workspace-create-actions.test.ts @@ -0,0 +1,99 @@ +import { createElement } from 'react' +import { act, create, type ReactTestRenderer } from 'react-test-renderer' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { RpcClient } from '../transport/rpc-client' + +// Why: the tasks barrel pulls the whole react-native screen graph in; re-export only what create +// uses, straight from its leaf modules. +vi.mock('./mobile-tasks-dependencies', async () => ({ + useCallback: (await import('react')).useCallback, + ...(await import('./workspace-create-params')), + ...(await import('./workspace-agent-selection')), + ...(await import('./hosted-review-start-point')), + ...(await import('./workspace-create-timeout')), + ...(await import('./setup-hook-trust')) +})) + +const { useMobileTasksWorkspaceCreateActions } = + await import('./use-mobile-tasks-workspace-create-actions') + +type CreateModel = Parameters[0] + +// The host has hidden Claude since the Tasks screen read its settings, when Claude was the default. +function hostClient() { + const sendRequest = vi.fn(async (method: string, _params?: unknown) => { + const result = + method === 'settings.get' + ? { settings: { defaultTuiAgent: 'claude', disabledTuiAgents: ['claude'] } } + : { worktree: { id: 'wt-1', displayName: 'ORC-1 Recorded issue' } } + return { id: 'x', ok: true, result, _meta: { runtimeId: 'r' } } + }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: settings.get and worktree.create reach only `sendRequest` on the client. + return { client: { sendRequest } as unknown as RpcClient, sendRequest } +} + +describe('useMobileTasksWorkspaceCreateActions agent', () => { + let renderer: ReactTestRenderer | null = null + + afterEach(() => { + act(() => renderer?.unmount()) + renderer = null + }) + + async function createWith(agentOverride: 'claude' | undefined): Promise { + const { client, sendRequest } = hostClient() + const model = { + client, + hostId: 'host-1', + tasksSupported: true, + taskStateHydrated: true, + runtimeTaskSettings: { defaultTuiAgent: 'claude', disabledTuiAgents: [] }, + setRuntimeTaskSettings: () => {}, + trustedOrcaHooks: {}, + workspaceDetectedAgentIds: new Set(['claude', 'codex']), + workspaceLastAutoName: '', + ensureWorkspaceSshReady: async () => {}, + getWorkspaceTargetRepo: () => ({ id: 'repo-1', displayName: 'Repo' }), + resolveCreateSetupDecision: async () => ({ kind: 'decision', decision: 'inherit' }), + setCreatingKey: () => {}, + setError: () => {}, + setActionItem: () => {}, + setWorkspaceCreateDraft: () => {}, + setSetupPrompt: () => {}, + setOrcaYamlTrustPrompt: () => {}, + router: { push: () => {} } + } + let actions: ReturnType | null = null + function Harness(): null { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: createWorkspace reads only the members supplied above. + actions = useMobileTasksWorkspaceCreateActions(model as unknown as CreateModel) + return null + } + await act(async () => { + renderer = create(createElement(Harness)) + }) + await act(async () => { + await actions!.createWorkspace( + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a Linear create reads only the key, title and these source members. + { + key: 'linear:1', + provider: 'linear', + title: 'Recorded issue', + source: { identifier: 'ORC-1', title: 'Recorded issue', url: 'https://linear.app/orc-1' } + } as never, + undefined, + undefined, + agentOverride + ) + }) + return sendRequest.mock.calls.find(([method]) => method === 'worktree.create')?.[1] + } + + it('re-picks an automatic agent the host has since hidden', async () => { + await expect(createWith(undefined)).resolves.toMatchObject({ createdWithAgent: 'codex' }) + }) + + it('starts an agent the user picked even though it is hidden', async () => { + await expect(createWith('claude')).resolves.toMatchObject({ createdWithAgent: 'claude' }) + }) +}) diff --git a/mobile/src/tasks/workspace-agent-selection.test.ts b/mobile/src/tasks/workspace-agent-selection.test.ts index 18202d5dc719..c002b16fc30c 100644 --- a/mobile/src/tasks/workspace-agent-selection.test.ts +++ b/mobile/src/tasks/workspace-agent-selection.test.ts @@ -80,6 +80,18 @@ describe('workspace agent selection', () => { ).toEqual({ agent: 'codex', overridden: true }) }) + it('preserves a user override that is hidden in Settings', () => { + expect( + resolveWorkspaceAgentSelection({ + selectionActive: true, + settings: { defaultTuiAgent: 'claude', disabledTuiAgents: ['codex'] }, + detectedAgentIds: new Set(['claude', 'codex']), + agent: 'codex', + overridden: true + }) + ).toEqual({ agent: 'codex', overridden: true }) + }) + it('falls back when a user override is unavailable after detection settles', () => { expect( resolveWorkspaceAgentSelection({ diff --git a/mobile/src/tasks/worktree-create-launch-operation.test.ts b/mobile/src/tasks/worktree-create-launch-operation.test.ts index 76e4f3be3913..fef0bb05231a 100644 --- a/mobile/src/tasks/worktree-create-launch-operation.test.ts +++ b/mobile/src/tasks/worktree-create-launch-operation.test.ts @@ -5,7 +5,11 @@ import type { RpcClient } from '../transport/rpc-client' import type { ConnectionState } from '../transport/types' import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client' import { WORKTREE_CREATE_DEDUPE_TTL_LEGACY_HOST_MS } from './worktree-create-idempotency-policy' -import { createWorktreeWithNameRetry, type WorktreeCreateResult } from './worktree-create-retry' +import { + createWorktreeWithNameRetry, + WORKSPACE_CREATE_UNCONFIRMED_MESSAGE, + type WorktreeCreateResult +} from './worktree-create-retry' type Attempt = { method: string; params: Record } @@ -231,31 +235,37 @@ describe('agent.launch operation id', () => { }) // These codes also describe a refused nested attach or an expired receipt after creation. - it.each([ - 'agent_session_operation_capacity', - 'agent_session_operation_invalid', - 'agent_session_operation_expired' - ])('preserves the operation identity when the host refuses with %s', async (code) => { - const attempts: Attempt[] = [] - const client = scriptedLaunchClient([{ errorCode: code }, { launched: 'wt-unnamed' }], attempts) - await expect(launchRetry({ client, attempts })).resolves.toEqual({ error: code }) - expect(attempts).toHaveLength(1) - expect(launchOperationIds(attempts)).toEqual(['op-1']) - }) + it.each(['agent_session_operation_capacity', 'agent_session_operation_invalid'])( + 'preserves the operation identity when the host refuses with %s', + async (code) => { + const attempts: Attempt[] = [] + const client = scriptedLaunchClient( + [{ errorCode: code }, { launched: 'wt-unnamed' }], + attempts + ) + await expect(launchRetry({ client, attempts })).resolves.toEqual({ error: code }) + expect(attempts).toHaveLength(1) + expect(launchOperationIds(attempts)).toEqual(['op-1']) + } + ) - // An unsettled claim is evidence that the launch may already have run. - it('surfaces an unknown operation without re-sending and without re-minting', async () => { - const attempts: Attempt[] = [] - const client = scriptedLaunchClient( - [{ errorCode: 'agent_session_operation_unknown' }], - attempts - ) - await expect(launchRetry({ client, attempts })).resolves.toEqual({ - error: 'agent_session_operation_unknown' - }) - expect(attempts).toHaveLength(1) - expect(launchOperationIds(attempts)).toEqual(['op-1']) - }) + // An unsettled claim is evidence that the launch may already have run. An older host also answers + // this way when it refuses a hidden agent, so the user gets words rather than the code. + it.each(['agent_session_operation_unknown', 'agent_session_operation_expired'])( + 'reports %s in plain words without re-sending and without re-minting', + async (code) => { + const attempts: Attempt[] = [] + const client = scriptedLaunchClient( + [{ errorCode: code }, { launched: 'wt-unnamed' }], + attempts + ) + await expect(launchRetry({ client, attempts })).resolves.toEqual({ + error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE + }) + expect(attempts).toHaveLength(1) + expect(launchOperationIds(attempts)).toEqual(['op-1']) + } + ) // A conflict can only come from a client that reused one id across two payloads. Re-sending it // unnamed would hide that bug behind a create that quietly works. diff --git a/mobile/src/tasks/worktree-create-retry.ts b/mobile/src/tasks/worktree-create-retry.ts index 7ba680ba6356..64ddc9e07b68 100644 --- a/mobile/src/tasks/worktree-create-retry.ts +++ b/mobile/src/tasks/worktree-create-retry.ts @@ -19,6 +19,7 @@ import { readAgentLaunchCreateOutcome, type WorktreeCreateAgentLaunch } from './agent-launch-request' +import { classifyAgentLaunchReplayRefusal } from '../../../src/shared/agent-launch-replay-refusal' import { structuredSessionOperationId } from '../session/structured-session-operation-id' import { WORKTREE_CREATE_TIMEOUT_MS } from './workspace-create-timeout' import type { WorkspaceCreateParams } from './workspace-create-params' @@ -40,6 +41,9 @@ export type WorktreeCreateResult = | { worktreeId: string; name: string; warning?: string } | { error: string } +export const WORKSPACE_CREATE_UNCONFIRMED_MESSAGE = + "Couldn't confirm the workspace was created. Check your workspaces before trying again." + export type CreateWorktreeWithNameRetryArgs = { client: RpcClient baseName: string @@ -126,7 +130,12 @@ export async function createWorktreeWithNameRetry( lastError = 'Failed to create workspace' break } - lastError = response.error.message + // An unsettled launch answers with a bare code: the host itself can't say whether it created. + lastError = + launch?.replay && + classifyAgentLaunchReplayRefusal(response.error, sent.replayed) === 'unknown' + ? WORKSPACE_CREATE_UNCONFIRMED_MESSAGE + : response.error.message // The replay-required host already exhausted its candidates; only legacy hosts need this loop. if (launch?.replay || !isRetryableWorktreeCreateConflict(lastError ?? '')) { break diff --git a/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts b/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts index 50fdacde2cf7..487d8e58e78e 100644 --- a/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts +++ b/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts @@ -84,7 +84,10 @@ export function workspaceSettingsMounts( client: context.client, selectedRepo: { id: 'repo-1', displayName: 'Repo' }, selectedAgent: { id: 'claude', label: 'Claude' }, + // The user's own pick of an agent hidden in Settings, which create sends as chosen. + agentOverridden: true, runtimeSettings: { disabledTuiAgents: ['claude'] }, + detectedAgentIds: new Set(['codex']), sshGate: { requiresConnection: false }, composer: { name: 'recorded', createSelection: null, isNameAutoManaged: false }, note: '', diff --git a/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts index d144eb3514d4..58b12257cdf4 100644 --- a/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts +++ b/mobile/src/test-support/rpc-recording/mutants/operation-mutations.ts @@ -172,8 +172,8 @@ export const OPERATION_MUTATIONS = { // Publishes the settings envelope as the refreshed workspace runtime settings. 'workspace-submit-envelope': { file: 'use-new-workspace-create-submit.ts', - before: 'args.setRuntimeSettings(settings.value as NewWorktreeRuntimeSettings)', - after: 'args.setRuntimeSettings(settingsReply.result as NewWorktreeRuntimeSettings)' + before: 'latestRuntimeSettings = settings.value as NewWorktreeRuntimeSettings', + after: 'latestRuntimeSettings = settingsReply.result as NewWorktreeRuntimeSettings' }, // Reads settings eagerly, so a null result throws before the sibling's refusal is checked. 'new-tab-deferred-settings-read': { From d42dd9ea1de61946540e08a8c8ca48ab2af8b803 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:28:19 -0700 Subject: [PATCH 3/5] fix(mobile): name the hidden agent when an older computer refuses it Reverts the generic "Couldn't confirm the workspace was created" mapping in the create retry, so resend, expiry and cutover replies behave as on main. Instead, the new-workspace sheet recognises the one case it can explain: a user-picked agent that the Settings just read hide, refused with agent_session_operation_unknown by a computer from before hidden agents could start. That computer created nothing, so the sheet names the agent and the next step. --- .../use-new-workspace-create-submit.test.ts | 33 +++++++--- .../use-new-workspace-create-submit.ts | 13 +++- .../tasks/agent-launch-mobile-replay.test.ts | 9 +-- .../mobile-agent-launch-architecture.test.ts | 19 +++--- .../worktree-create-launch-operation.test.ts | 60 ++++++++----------- mobile/src/tasks/worktree-create-retry.ts | 11 +--- .../adapters/workspace-settings-mounts.ts | 2 +- 7 files changed, 77 insertions(+), 70 deletions(-) diff --git a/mobile/src/components/use-new-workspace-create-submit.test.ts b/mobile/src/components/use-new-workspace-create-submit.test.ts index 611341b310cf..32f1d496c4a2 100644 --- a/mobile/src/components/use-new-workspace-create-submit.test.ts +++ b/mobile/src/components/use-new-workspace-create-submit.test.ts @@ -4,13 +4,21 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { EMPTY_RETIRED_NAME_REGISTRY } from '../../../src/shared/worktree/retired-name-registry' import type { RpcClient } from '../transport/rpc-client' import { newWorktreeAgentOptionFor } from './new-worktree-agent-selection' -import { useNewWorkspaceCreateSubmit } from './use-new-workspace-create-submit' +import { + hiddenAgentRefusedMessage, + useNewWorkspaceCreateSubmit +} from './use-new-workspace-create-submit' type SubmitArgs = Parameters[0] // The host has hidden Claude since the sheet read its settings, when Claude was the default. function hostClient() { const sendRequest = vi.fn(async (method: string, _params?: unknown) => { + if (method === 'agent.launchReplay') { + // A computer from before hidden agents could start refuses one with only this code. + const code = 'agent_session_operation_unknown' + return { id: 'x', ok: false, error: { code, message: code }, _meta: { runtimeId: 'r' } } + } const result = method === 'settings.get' ? { settings: { defaultTuiAgent: 'claude', disabledTuiAgents: ['claude'] } } @@ -29,8 +37,12 @@ describe('useNewWorkspaceCreateSubmit agent', () => { renderer = null }) - async function createWith(agentOverridden: boolean): Promise { + async function createWith( + agentOverridden: boolean, + launchRoute = false + ): Promise<{ sent: unknown; error: string }> { const { client, sendRequest } = hostClient() + let error = '' const args: SubmitArgs = { client, selectedRepo: { id: 'repo-1', displayName: 'Repo', path: '/repo', kind: 'git' }, @@ -52,9 +64,11 @@ describe('useNewWorkspaceCreateSubmit agent', () => { trustedOrcaHooks: {}, setTrustedOrcaHooks: () => {}, getWorktreeCreateCutoverSupport: async () => false, - getAgentLaunchSupport: async () => false, + getAgentLaunchSupport: async () => (launchRoute ? { replay: true } : false), transitionDrawer: () => {}, - setError: () => {}, + setError: (message) => { + error = typeof message === 'function' ? message(error) : message + }, onCreated: () => {}, onClose: () => {} } @@ -69,14 +83,19 @@ describe('useNewWorkspaceCreateSubmit agent', () => { await act(async () => { await submit!.create() }) - return sendRequest.mock.calls.find(([method]) => method === 'worktree.create')?.[1] + const sent = sendRequest.mock.calls.find(([method]) => method === 'worktree.create')?.[1] + return { sent, error } } it('re-picks an automatic agent the host has since hidden', async () => { - await expect(createWith(false)).resolves.toMatchObject({ startupAgent: 'codex' }) + expect((await createWith(false)).sent).toMatchObject({ startupAgent: 'codex' }) }) it('starts an agent the user picked even though it is hidden', async () => { - await expect(createWith(true)).resolves.toMatchObject({ startupAgent: 'claude' }) + expect((await createWith(true)).sent).toMatchObject({ startupAgent: 'claude' }) + }) + + it('names the hidden agent when an older computer refuses it with only a code', async () => { + expect((await createWith(true, true)).error).toBe(hiddenAgentRefusedMessage('Claude')) }) }) diff --git a/mobile/src/components/use-new-workspace-create-submit.ts b/mobile/src/components/use-new-workspace-create-submit.ts index c3c74fc235de..b3743f4b83ba 100644 --- a/mobile/src/components/use-new-workspace-create-submit.ts +++ b/mobile/src/components/use-new-workspace-create-submit.ts @@ -27,6 +27,10 @@ import type { SetupTrustPrompt } from './SetupHookTrustDrawer' import type { NewWorktreeDrawerView } from './use-new-worktree-drawer-navigation' import { getSuggestedCreatureName } from './worktree-name-suggestion' +export function hiddenAgentRefusedMessage(agentLabel: string): string { + return `Orca on your computer won't start ${agentLabel} while it's hidden in Settings there. Show ${agentLabel} in Settings on your computer, or pick another agent.` +} + type CreateOptions = { setupOverride?: Exclude approvedSetupContentHash?: string @@ -171,7 +175,14 @@ export function useNewWorkspaceCreateSubmit(args: { agentLaunchSupported: args.getAgentLaunchSupport() }) if ('error' in result) { - args.setError(result.error) + // Why: a computer from before hidden agents could start refuses one with only this code, + // having created nothing; the Settings read above say why. + const refusedAsHidden = + result.error === 'agent_session_operation_unknown' && + args.agentOverridden && + agent.id !== '__blank__' && + latestRuntimeSettings?.disabledTuiAgents?.includes(agent.id) === true + args.setError(refusedAsHidden ? hiddenAgentRefusedMessage(agent.label) : result.error) return } args.onClose() diff --git a/mobile/src/tasks/agent-launch-mobile-replay.test.ts b/mobile/src/tasks/agent-launch-mobile-replay.test.ts index b375119058ec..51d790b85dd5 100644 --- a/mobile/src/tasks/agent-launch-mobile-replay.test.ts +++ b/mobile/src/tasks/agent-launch-mobile-replay.test.ts @@ -2,10 +2,7 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { - createWorktreeWithNameRetry, - WORKSPACE_CREATE_UNCONFIRMED_MESSAGE -} from './worktree-create-retry' +import { createWorktreeWithNameRetry } from './worktree-create-retry' import type { RpcClient } from '../transport/rpc-client' import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity' import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client' @@ -135,7 +132,7 @@ describe('mobile launch retries through the host ledger', () => { createStructuredSession.mockResolvedValue({ ok: false, refusal: { code, message: code } }) const launch = mobileLaunch() - await expect(launch.result).resolves.toEqual({ error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE }) + await expect(launch.result).resolves.toEqual({ error: 'agent_session_operation_unknown' }) expect(launch.runtime.createManagedWorktree).toHaveBeenCalledTimes(1) expect(launch.attempts).toHaveLength(1) expect(store.listOperationRows()[0]?.outcome.status).toBe('unknown') @@ -147,7 +144,7 @@ describe('mobile launch retries through the host ledger', () => { AGENT_SESSION_MAX_NEW_OPERATION_AGE_MS + AGENT_SESSION_OPERATION_FUTURE_SKEW_MS + 1 }) - await expect(launch.result).resolves.toEqual({ error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE }) + await expect(launch.result).resolves.toEqual({ error: 'agent_session_operation_expired' }) expect(launch.runtime.createManagedWorktree).toHaveBeenCalledTimes(1) expect(createStructuredSession).toHaveBeenCalledTimes(1) expect(launch.attempts).toHaveLength(2) diff --git a/mobile/src/tasks/mobile-agent-launch-architecture.test.ts b/mobile/src/tasks/mobile-agent-launch-architecture.test.ts index 51fbd9617217..2a4e4a82eedb 100644 --- a/mobile/src/tasks/mobile-agent-launch-architecture.test.ts +++ b/mobile/src/tasks/mobile-agent-launch-architecture.test.ts @@ -20,10 +20,7 @@ import { createStableLogicalRpcClient } from '../transport/stable-logical-rpc-cl import type { RpcClient } from '../transport/rpc-client' import type { RpcResponse } from '../transport/types' import { WorktreeCreateCollisionError } from '../../../src/shared/new-workspace/worktree-create-collision' -import { - createWorktreeWithNameRetry, - WORKSPACE_CREATE_UNCONFIRMED_MESSAGE -} from './worktree-create-retry' +import { createWorktreeWithNameRetry } from './worktree-create-retry' import { readNewWorktreeRuntimeCapabilities } from './worktree-create-capability' import { AGENT_LAUNCH_RUNTIME_CAPABILITY, @@ -259,8 +256,9 @@ describe('mobile launch retry authority', () => { it('does not duplicate when a replacement host strips operationId', async () => { const launch = scenario({ replacement: 'legacy' }) - // The first send may have created the workspace, so the user is told to check, not "Unknown method". - await expect(launch.result).resolves.toEqual({ error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE }) + await expect(launch.result).resolves.toMatchObject({ + error: expect.stringContaining('Unknown method') + }) expect( launch.runtime.createManagedWorktree.mock.calls.length + launch.replacement.createManagedWorktree.mock.calls.length @@ -314,15 +312,16 @@ describe('mobile launch retry authority', () => { } }) const launch = scenario() - await expect(launch.result).resolves.toEqual({ error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE }) + await expect(launch.result).resolves.toEqual({ error: 'agent_session_operation_unknown' }) expect(launch.runtime.createManagedWorktree).toHaveBeenCalledTimes(1) expect(createStructuredSession).toHaveBeenCalledTimes(1) }) it('refuses an older receiver reached after a connected timeout', async () => { const launch = scenario({ replacement: 'legacy', loss: 'timeout' }) - // The first send may have created the workspace, so the user is told to check, not "Unknown method". - await expect(launch.result).resolves.toEqual({ error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE }) + await expect(launch.result).resolves.toMatchObject({ + error: expect.stringContaining('Unknown method') + }) expect(launch.runtime.createManagedWorktree).toHaveBeenCalledTimes(1) expect(launch.replacement.createManagedWorktree).not.toHaveBeenCalled() }) @@ -359,7 +358,7 @@ describe('mobile launch retry authority', () => { refusal: { code: 'agent_session_operation_unknown', message } }) const launch = scenario() - await expect(launch.result).resolves.toEqual({ error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE }) + await expect(launch.result).resolves.toEqual({ error: 'agent_session_operation_unknown' }) expect(launch.runtime.createManagedWorktree).toHaveBeenCalledTimes(1) expect(createStructuredSession).toHaveBeenCalledTimes(1) }) diff --git a/mobile/src/tasks/worktree-create-launch-operation.test.ts b/mobile/src/tasks/worktree-create-launch-operation.test.ts index fef0bb05231a..76e4f3be3913 100644 --- a/mobile/src/tasks/worktree-create-launch-operation.test.ts +++ b/mobile/src/tasks/worktree-create-launch-operation.test.ts @@ -5,11 +5,7 @@ import type { RpcClient } from '../transport/rpc-client' import type { ConnectionState } from '../transport/types' import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client' import { WORKTREE_CREATE_DEDUPE_TTL_LEGACY_HOST_MS } from './worktree-create-idempotency-policy' -import { - createWorktreeWithNameRetry, - WORKSPACE_CREATE_UNCONFIRMED_MESSAGE, - type WorktreeCreateResult -} from './worktree-create-retry' +import { createWorktreeWithNameRetry, type WorktreeCreateResult } from './worktree-create-retry' type Attempt = { method: string; params: Record } @@ -235,37 +231,31 @@ describe('agent.launch operation id', () => { }) // These codes also describe a refused nested attach or an expired receipt after creation. - it.each(['agent_session_operation_capacity', 'agent_session_operation_invalid'])( - 'preserves the operation identity when the host refuses with %s', - async (code) => { - const attempts: Attempt[] = [] - const client = scriptedLaunchClient( - [{ errorCode: code }, { launched: 'wt-unnamed' }], - attempts - ) - await expect(launchRetry({ client, attempts })).resolves.toEqual({ error: code }) - expect(attempts).toHaveLength(1) - expect(launchOperationIds(attempts)).toEqual(['op-1']) - } - ) + it.each([ + 'agent_session_operation_capacity', + 'agent_session_operation_invalid', + 'agent_session_operation_expired' + ])('preserves the operation identity when the host refuses with %s', async (code) => { + const attempts: Attempt[] = [] + const client = scriptedLaunchClient([{ errorCode: code }, { launched: 'wt-unnamed' }], attempts) + await expect(launchRetry({ client, attempts })).resolves.toEqual({ error: code }) + expect(attempts).toHaveLength(1) + expect(launchOperationIds(attempts)).toEqual(['op-1']) + }) - // An unsettled claim is evidence that the launch may already have run. An older host also answers - // this way when it refuses a hidden agent, so the user gets words rather than the code. - it.each(['agent_session_operation_unknown', 'agent_session_operation_expired'])( - 'reports %s in plain words without re-sending and without re-minting', - async (code) => { - const attempts: Attempt[] = [] - const client = scriptedLaunchClient( - [{ errorCode: code }, { launched: 'wt-unnamed' }], - attempts - ) - await expect(launchRetry({ client, attempts })).resolves.toEqual({ - error: WORKSPACE_CREATE_UNCONFIRMED_MESSAGE - }) - expect(attempts).toHaveLength(1) - expect(launchOperationIds(attempts)).toEqual(['op-1']) - } - ) + // An unsettled claim is evidence that the launch may already have run. + it('surfaces an unknown operation without re-sending and without re-minting', async () => { + const attempts: Attempt[] = [] + const client = scriptedLaunchClient( + [{ errorCode: 'agent_session_operation_unknown' }], + attempts + ) + await expect(launchRetry({ client, attempts })).resolves.toEqual({ + error: 'agent_session_operation_unknown' + }) + expect(attempts).toHaveLength(1) + expect(launchOperationIds(attempts)).toEqual(['op-1']) + }) // A conflict can only come from a client that reused one id across two payloads. Re-sending it // unnamed would hide that bug behind a create that quietly works. diff --git a/mobile/src/tasks/worktree-create-retry.ts b/mobile/src/tasks/worktree-create-retry.ts index 64ddc9e07b68..7ba680ba6356 100644 --- a/mobile/src/tasks/worktree-create-retry.ts +++ b/mobile/src/tasks/worktree-create-retry.ts @@ -19,7 +19,6 @@ import { readAgentLaunchCreateOutcome, type WorktreeCreateAgentLaunch } from './agent-launch-request' -import { classifyAgentLaunchReplayRefusal } from '../../../src/shared/agent-launch-replay-refusal' import { structuredSessionOperationId } from '../session/structured-session-operation-id' import { WORKTREE_CREATE_TIMEOUT_MS } from './workspace-create-timeout' import type { WorkspaceCreateParams } from './workspace-create-params' @@ -41,9 +40,6 @@ export type WorktreeCreateResult = | { worktreeId: string; name: string; warning?: string } | { error: string } -export const WORKSPACE_CREATE_UNCONFIRMED_MESSAGE = - "Couldn't confirm the workspace was created. Check your workspaces before trying again." - export type CreateWorktreeWithNameRetryArgs = { client: RpcClient baseName: string @@ -130,12 +126,7 @@ export async function createWorktreeWithNameRetry( lastError = 'Failed to create workspace' break } - // An unsettled launch answers with a bare code: the host itself can't say whether it created. - lastError = - launch?.replay && - classifyAgentLaunchReplayRefusal(response.error, sent.replayed) === 'unknown' - ? WORKSPACE_CREATE_UNCONFIRMED_MESSAGE - : response.error.message + lastError = response.error.message // The replay-required host already exhausted its candidates; only legacy hosts need this loop. if (launch?.replay || !isRetryableWorktreeCreateConflict(lastError ?? '')) { break diff --git a/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts b/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts index 487d8e58e78e..6d025a27bf5a 100644 --- a/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts +++ b/mobile/src/test-support/rpc-recording/adapters/workspace-settings-mounts.ts @@ -87,7 +87,7 @@ export function workspaceSettingsMounts( // The user's own pick of an agent hidden in Settings, which create sends as chosen. agentOverridden: true, runtimeSettings: { disabledTuiAgents: ['claude'] }, - detectedAgentIds: new Set(['codex']), + detectedAgentIds: new Set(['claude', 'codex']), sshGate: { requiresConnection: false }, composer: { name: 'recorded', createSelection: null, isNameAutoManaged: false }, note: '', From 25dbba7d1e5f4e882db82414b7eff10cde7deadd Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 11 Oct 2026 23:36:33 -0700 Subject: [PATCH 4/5] fix(mobile): say hidden, not enabled, where the phone has no agent to offer Matches the desktop Settings switch, now Shown/Hidden: the new-tab menu's empty row and the AI button's no-agent line name hidden agents and the next step. --- mobile/src/session/mobile-source-control-launch-agent.ts | 6 +++++- mobile/src/session/pr-ai-triage-launch.test.ts | 5 +++-- .../src/session/use-mobile-session-panel-route-actions.tsx | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/mobile/src/session/mobile-source-control-launch-agent.ts b/mobile/src/session/mobile-source-control-launch-agent.ts index c7da7007fd15..381d49c6f864 100644 --- a/mobile/src/session/mobile-source-control-launch-agent.ts +++ b/mobile/src/session/mobile-source-control-launch-agent.ts @@ -56,7 +56,11 @@ export function resolveMobileSourceControlLaunchAgent( }) return agent ? { kind: 'agent', agent, recipe } - : { kind: 'unavailable', message: 'No enabled AI agent was detected on this workspace host.' } + : { + kind: 'unavailable', + message: + 'No AI agent on this workspace host is both installed and shown in Settings. Install one there, or show one in Settings on your computer.' + } } function readLaunchAgentSettings(settings: unknown): LaunchAgentSettings | null { diff --git a/mobile/src/session/pr-ai-triage-launch.test.ts b/mobile/src/session/pr-ai-triage-launch.test.ts index cb88f8583fda..7df3d43819d2 100644 --- a/mobile/src/session/pr-ai-triage-launch.test.ts +++ b/mobile/src/session/pr-ai-triage-launch.test.ts @@ -227,14 +227,15 @@ describe('launchAgentWithPrompt', () => { expect(launchParams(sendRequest)).toMatchObject({ agent: 'claude' }) }) - it('reports no agent when none is detected and enabled', async () => { + it('reports no agent when none is detected and shown', async () => { const { client } = hostClient({ settings: { disabledTuiAgents: ['claude'] }, detected: ['claude'] }) await expect(run(client)).resolves.toEqual({ kind: 'not-started', - message: 'No enabled AI agent was detected on this workspace host.' + message: + 'No AI agent on this workspace host is both installed and shown in Settings. Install one there, or show one in Settings on your computer.' }) }) diff --git a/mobile/src/session/use-mobile-session-panel-route-actions.tsx b/mobile/src/session/use-mobile-session-panel-route-actions.tsx index 1c39b0c849ee..bd073f470209 100644 --- a/mobile/src/session/use-mobile-session-panel-route-actions.tsx +++ b/mobile/src/session/use-mobile-session-panel-route-actions.tsx @@ -67,7 +67,7 @@ export function useMobileSessionPanelRouteActions(scope: MobileSessionPresentati : createTabAgentLoadState === 'loaded' ? [ { - label: 'No Enabled Agents', + label: 'All Agents Hidden or Not Installed', icon: Bot, disabled: true, onPress: () => {} @@ -121,7 +121,7 @@ export function useMobileSessionPanelRouteActions(scope: MobileSessionPresentati : createTabAgentLoadState === 'loaded' ? [ { - label: 'No Enabled Agents', + label: 'All Agents Hidden or Not Installed', icon: Bot, disabled: true, onPress: () => {} From 4447ca5415bb33df046d188c0a1169164f597663 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:26:53 -0700 Subject: [PATCH 5/5] fix(mobile): never relabel a resend as a hidden-agent refusal; match the desktop's empty-menu wording The create retry marks a failure that answered a resend (replayed: true), and the new-workspace sheet only names a hidden agent when the refusal answered the first send. The empty new-tab menu now reads "No Agents to Show". --- .../use-new-workspace-create-submit.test.ts | 16 ++++++++++--- .../use-new-workspace-create-submit.ts | 6 +++-- ...use-mobile-session-panel-route-actions.tsx | 4 ++-- .../tasks/agent-launch-mobile-replay.test.ts | 5 +++- .../worktree-create-launch-operation.test.ts | 3 ++- mobile/src/tasks/worktree-create-retry.ts | 24 +++++++++++++++---- 6 files changed, 44 insertions(+), 14 deletions(-) diff --git a/mobile/src/components/use-new-workspace-create-submit.test.ts b/mobile/src/components/use-new-workspace-create-submit.test.ts index 32f1d496c4a2..c54240eb2d6b 100644 --- a/mobile/src/components/use-new-workspace-create-submit.test.ts +++ b/mobile/src/components/use-new-workspace-create-submit.test.ts @@ -25,7 +25,7 @@ function hostClient() { : { worktree: { id: 'wt-1', displayName: 'recorded' } } return { id: 'x', ok: true, result, _meta: { runtimeId: 'r' } } }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a non-replaying create reaches only `sendRequest` on the client. + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every reply here is definite, so no create is resent and only `sendRequest` is reached. return { client: { sendRequest } as unknown as RpcClient, sendRequest } } @@ -39,14 +39,15 @@ describe('useNewWorkspaceCreateSubmit agent', () => { async function createWith( agentOverridden: boolean, - launchRoute = false + launchRoute = false, + selected: 'claude' | 'codex' = 'claude' ): Promise<{ sent: unknown; error: string }> { const { client, sendRequest } = hostClient() let error = '' const args: SubmitArgs = { client, selectedRepo: { id: 'repo-1', displayName: 'Repo', path: '/repo', kind: 'git' }, - selectedAgent: newWorktreeAgentOptionFor('claude'), + selectedAgent: newWorktreeAgentOptionFor(selected), agentOverridden, runtimeSettings: { defaultTuiAgent: 'claude', disabledTuiAgents: [] }, setRuntimeSettings: () => {}, @@ -98,4 +99,13 @@ describe('useNewWorkspaceCreateSubmit agent', () => { it('names the hidden agent when an older computer refuses it with only a code', async () => { expect((await createWith(true, true)).error).toBe(hiddenAgentRefusedMessage('Claude')) }) + + it.each([ + ['an automatic pick', false, 'claude'], + ['a user pick that is shown in Settings', true, 'codex'] + ] as const)('shows the code as on main for %s', async (_case, overridden, selected) => { + expect((await createWith(overridden, true, selected)).error).toBe( + 'agent_session_operation_unknown' + ) + }) }) diff --git a/mobile/src/components/use-new-workspace-create-submit.ts b/mobile/src/components/use-new-workspace-create-submit.ts index b3743f4b83ba..a65e961df782 100644 --- a/mobile/src/components/use-new-workspace-create-submit.ts +++ b/mobile/src/components/use-new-workspace-create-submit.ts @@ -175,10 +175,12 @@ export function useNewWorkspaceCreateSubmit(args: { agentLaunchSupported: args.getAgentLaunchSupport() }) if ('error' in result) { - // Why: a computer from before hidden agents could start refuses one with only this code, - // having created nothing; the Settings read above say why. + // Why: a computer from before hidden agents could start refuses one on the first send with + // only this code; the Settings read above say why. On a newer computer any failure on this + // route sends the same code, so a failed create for a hidden pick also lands here. const refusedAsHidden = result.error === 'agent_session_operation_unknown' && + !result.replayed && args.agentOverridden && agent.id !== '__blank__' && latestRuntimeSettings?.disabledTuiAgents?.includes(agent.id) === true diff --git a/mobile/src/session/use-mobile-session-panel-route-actions.tsx b/mobile/src/session/use-mobile-session-panel-route-actions.tsx index bd073f470209..b3e960ceea49 100644 --- a/mobile/src/session/use-mobile-session-panel-route-actions.tsx +++ b/mobile/src/session/use-mobile-session-panel-route-actions.tsx @@ -67,7 +67,7 @@ export function useMobileSessionPanelRouteActions(scope: MobileSessionPresentati : createTabAgentLoadState === 'loaded' ? [ { - label: 'All Agents Hidden or Not Installed', + label: 'No Agents to Show', icon: Bot, disabled: true, onPress: () => {} @@ -121,7 +121,7 @@ export function useMobileSessionPanelRouteActions(scope: MobileSessionPresentati : createTabAgentLoadState === 'loaded' ? [ { - label: 'All Agents Hidden or Not Installed', + label: 'No Agents to Show', icon: Bot, disabled: true, onPress: () => {} diff --git a/mobile/src/tasks/agent-launch-mobile-replay.test.ts b/mobile/src/tasks/agent-launch-mobile-replay.test.ts index 51d790b85dd5..4768ec019977 100644 --- a/mobile/src/tasks/agent-launch-mobile-replay.test.ts +++ b/mobile/src/tasks/agent-launch-mobile-replay.test.ts @@ -144,7 +144,10 @@ describe('mobile launch retries through the host ledger', () => { AGENT_SESSION_MAX_NEW_OPERATION_AGE_MS + AGENT_SESSION_OPERATION_FUTURE_SKEW_MS + 1 }) - await expect(launch.result).resolves.toEqual({ error: 'agent_session_operation_expired' }) + await expect(launch.result).resolves.toEqual({ + error: 'agent_session_operation_expired', + replayed: true + }) expect(launch.runtime.createManagedWorktree).toHaveBeenCalledTimes(1) expect(createStructuredSession).toHaveBeenCalledTimes(1) expect(launch.attempts).toHaveLength(2) diff --git a/mobile/src/tasks/worktree-create-launch-operation.test.ts b/mobile/src/tasks/worktree-create-launch-operation.test.ts index 76e4f3be3913..df1adb64006d 100644 --- a/mobile/src/tasks/worktree-create-launch-operation.test.ts +++ b/mobile/src/tasks/worktree-create-launch-operation.test.ts @@ -305,7 +305,8 @@ describe('agent.launch operation id', () => { ) await expect(launchRetry({ client, attempts })).resolves.toEqual({ - error: 'agent_launch_unsupported' + error: 'agent_launch_unsupported', + replayed: true }) expect(attempts.map((attempt) => attempt.method)).toEqual([ 'agent.launchReplay', diff --git a/mobile/src/tasks/worktree-create-retry.ts b/mobile/src/tasks/worktree-create-retry.ts index 7ba680ba6356..8526df7f7b50 100644 --- a/mobile/src/tasks/worktree-create-retry.ts +++ b/mobile/src/tasks/worktree-create-retry.ts @@ -38,7 +38,8 @@ import { // loop in src/renderer/src/store/slices/worktrees.ts. export type WorktreeCreateResult = | { worktreeId: string; name: string; warning?: string } - | { error: string } + /** `replayed`: the failing reply answered a resend, so an earlier send may have run. */ + | { error: string; replayed?: true } export type CreateWorktreeWithNameRetryArgs = { client: RpcClient @@ -75,6 +76,7 @@ export async function createWorktreeWithNameRetry( const mintMutationId = args.mintMutationId ?? defaultWorktreeCreateMutationId const mintLaunchOperationId = args.mintLaunchOperationId ?? structuredSessionOperationId let lastError: string | null = null + let lastReplayed = false for (let attempt = 0; attempt < maxAttempts; attempt += 1) { const candidateName = args.nameWasGenerated ? getGeneratedWorktreeCreateRetryCandidate(baseName, attempt) @@ -96,6 +98,7 @@ export async function createWorktreeWithNameRetry( worktreeCreateIdempotency ) let response = sent.response + let replayed = sent.replayed if ( !response.ok && !sent.replayed && @@ -107,9 +110,15 @@ export async function createWorktreeWithNameRetry( // The probe said the host knows `agent.launch` but it refused the call — most likely this // client's capability list had not landed yet. Downgrade for good rather than fail a create. launch = null - response = ( - await sendWorktreeCreateResilient(client, null, null, params, worktreeCreateIdempotency) - ).response + const fallback = await sendWorktreeCreateResilient( + client, + null, + null, + params, + worktreeCreateIdempotency + ) + response = fallback.response + replayed = fallback.replayed } // Ledger refusals can follow workspace creation or an expired receipt; never retry unnamed. // Why the raw refusal: the retry decision below is `isRetryableWorktreeCreateConflict` over the @@ -124,15 +133,20 @@ export async function createWorktreeWithNameRetry( } } lastError = 'Failed to create workspace' + lastReplayed = false break } lastError = response.error.message + lastReplayed = replayed // The replay-required host already exhausted its candidates; only legacy hosts need this loop. if (launch?.replay || !isRetryableWorktreeCreateConflict(lastError ?? '')) { break } } - return { error: lastError ?? 'Failed to create workspace' } + return { + error: lastError ?? 'Failed to create workspace', + ...(lastReplayed ? { replayed: true } : {}) + } } async function resolveAgentLaunchRoute(