add per-agent permission-mode badge to the cockpit roster

Each roster row shows a D/A/P mode badge reflecting the session's live
permission mode. Click cycles the safe trio default -> acceptEdits ->
plan and sends a set_permission_mode control_request to that session
(mirrors interrupt(); fire-and-forget). bypassPermissions is a footgun,
so it is reachable only on Shift-click and behind an inline confirm. A
clide.agent.set-permission-mode command gives the CLI parity.

T-181.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-30 23:25:07 +02:00
co-authored by Claude
parent 4220d97a49
commit 2888efbe5f
8 changed files with 451 additions and 20 deletions
@@ -491,6 +491,26 @@ void main() {
expect(sent['request_id'], isNotNull);
});
test('setPermissionMode writes a set_permission_mode control_request (T-181)', () async {
session.setPermissionMode('acceptEdits');
expect(proc.writes, hasLength(1));
final sent = jsonDecode(proc.writes.single) as Map<String, dynamic>;
expect(sent['type'], 'control_request');
expect(sent['request_id'], isNotNull);
final req = sent['request'] as Map;
expect(req['subtype'], 'set_permission_mode');
expect(req['mode'], 'acceptEdits');
});
test('setPermissionMode uses a unique request_id each call (T-181)', () async {
session.setPermissionMode('plan');
session.setPermissionMode('default');
expect(proc.writes, hasLength(2));
final id1 = (jsonDecode(proc.writes[0]) as Map)['request_id'] as String;
final id2 = (jsonDecode(proc.writes[1]) as Map)['request_id'] as String;
expect(id1, isNot(equals(id2)));
});
test('busy goes true on send and false on a result event', () async {
final busy = <bool>[];
session.busyStream.listen(busy.add);