Shub/let listener mode control (#9584)

* 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 <noreply@letta.com>

* 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 <noreply@letta.com>

* 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 <noreply@letta.com>
Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com>
Co-authored-by: Shubham Naik <4shub@users.noreply.github.com>
This commit is contained in:
Shubham Naik
2026-02-20 12:06:18 -08:00
committed by Caren Thomas
parent 73c9b14fa9
commit 34bab3cf9a

View File

@@ -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": {