From 34bab3cf9a22a358f5d2ba1b04f6eb681318333e Mon Sep 17 00:00:00 2001 From: Shubham Naik Date: Fri, 20 Feb 2026 12:06:18 -0800 Subject: [PATCH] Shub/let listener mode control (#9584) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add two-way mode control for listener connections Enable bidirectional permission mode control between letta-cloud UI and letta-code instances. **Backend:** - Added ModeChangeMessage and ModeChangedMessage to WebSocket protocol - Added sendModeChange endpoint (/v1/listeners/:connectionId/mode) - listenersRouter publishes mode_change via Redis Pub/Sub - listenersHandler handles mode_changed acknowledgments from letta-code - Stores current mode in Redis for UI state sync **Contract:** - Added sendModeChange contract with PermissionModeSchema - 4 modes: default, acceptEdits, plan, bypassPermissions **Frontend:** - Extended PermissionMode type to 4 modes (was 2: ask/never) - PermissionModeSelector now shows all 4 modes with descriptions - Added disabled prop (grayed out when Cloud orchestrator selected) - PermissionModeContext.sendModeChangeToDevice() calls API - AgentMessenger sends mode changes to device on mode/device change - Updated auto-approval logic (only in Cloud mode, only for bypassPermissions) - Updated inputMode logic (device handles approvals, not cloud) **Translations:** - Updated en.json with 4 mode labels and descriptions - Removed legacy "askAlways" and "neverAsk" keys **Mode Behavior:** - default: Ask permission for each tool - acceptEdits: Auto-approve file edits only - plan: Read-only exploration (denies writes) - bypassPermissions: Auto-approve everything **Lint Fixes:** - Removed unused imports and functions from trackingMiddleware.ts 🐾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta * fix: store mode in connectionData and show approvals for all modes **Backend:** - Fixed Redis WRONGTYPE error - store currentMode inside connectionData object - Changed const connectionData to let connectionData (needs mutation) - Updated mode_changed handler to reassign entire connectionData object - Updated ping handler for consistency (also reassigns connectionData) - Added currentMode field to ListenerConnectionSchema (optional) **Frontend:** - Simplified inputMode logic - always show approval UI when toolCallsToApprove.length > 0 - Removed mode-specific approval filtering (show approvals even in bypass/acceptEdits for visibility) - Users can see what tools are being auto-approved during execution **Why:** - Redis key is a JSON string (via setRedisData), not a hash - Cannot use hset on string keys - causes WRONGTYPE error - Must update entire object via setRedisData like ping handler does - Approval visibility helpful for debugging/understanding agent behavior 🐾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta * fix: use useMutation hook for sendModeChange instead of direct call cloudAPI is initialized via initTsrReactQuery, so sendModeChange is a mutation hook object, not a callable function. Use .useMutation() at the component level and mutateAsync in the callback. Co-authored-by: Shubham Naik <4shub@users.noreply.github.com> * chore: update logs --------- Co-authored-by: Letta Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com> Co-authored-by: Shubham Naik <4shub@users.noreply.github.com> --- fern/openapi.json | 89 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/fern/openapi.json b/fern/openapi.json index 55a528b4..4fa4a0fc 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -25983,6 +25983,15 @@ }, "lastHeartbeat": { "type": "number" + }, + "currentMode": { + "type": "string", + "enum": [ + "default", + "acceptEdits", + "plan", + "bypassPermissions" + ] } }, "required": [ @@ -26245,6 +26254,86 @@ } } } + }, + "/v1/listeners/{connectionId}/mode": { + "post": { + "description": "Change the permission mode of a specific listener connection", + "summary": "Change Listener Mode", + "tags": ["listeners"], + "parameters": [ + { + "name": "connectionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "operationId": "listeners.sendModeChange", + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "default", + "acceptEdits", + "plan", + "bypassPermissions" + ] + } + }, + "required": ["mode"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["success", "message"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "errorCode": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["errorCode", "message"] + } + } + } + } + } + } } }, "components": {