add a permission-mode cycler to the primary Claude pane

T-226. The primary pane showed the permission mode but had no way to
change it (only the cockpit roster did, T-181). Add three affordances,
all cycling the safe trio default -> acceptEdits -> plan over the
stream-json control channel:

- Ctrl/Cmd+M while the composer is focused, intercepted at the composer
  so it targets that pane's session. Shift+Tab (the CLI chord) is
  deliberately NOT used — Tab/Shift+Tab are real a11y focus-traversal
  intents since T-204.
- The status-line mode label is now an interactive badge (ClideTappable):
  click, or focus + Enter/Space, cycles it.
- A "Claude: Cycle permission mode" palette command targeting the primary
  session.

bypassPermissions stays out of every cycle path here — it's reachable
only via the cockpit's explicit confirm (T-181). Shared helpers
(nextSafePermissionMode, statusSegmentsAroundMode) live in claude_status;
the cockpit's existing copy is left untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 13:45:52 +02:00
co-authored by Claude Opus 4.8
parent bda53b3bb1
commit 6c4c48bfc2
9 changed files with 217 additions and 12 deletions
@@ -87,3 +87,12 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by,
Scope addition (2026-06-03, from T-220): clide status must surface the focused file + selection from BOTH the editor (EditorRegistry.active, already populated) AND the read-only markdown/decisions reader''s current file (D-81 ReaderNav), since viewer files rightly do not live in EditorRegistry. So ''focused file'' = whichever of {active editor buffer, active reader doc} the user is currently looking at. Pane field comes from the T-219 view-pane snapshot; git summary from git.status; layout from the arrangement.', NULL, '2026-06-03 10:59:31', '2026-06-03 10:59:31', '2026-06-03 10:59:31', NULL, 'd248f5407a3e698ad641ed9393b71607', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-221', 'status', 'backlog', 'in_progress', NULL, '2026-06-03 11:17:49', '2026-06-03 11:17:49', '2026-06-03 11:17:49', NULL, '243f91496d8b597612063bb3bf1ba5a6', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-226', 'status', 'ready', 'in_progress', NULL, '2026-06-03 11:38:13', '2026-06-03 11:38:13', '2026-06-03 11:38:13', NULL, '183df518819e0e8ab0822ead80d66dc7', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-226', 'description', 'Gap found while dogfooding (2026-06-03): the Claude pane shows the permission mode in its status line (permissionModeLabel, claude_status.dart:44) but has NO quick way to CHANGE it, unlike the CLI where Shift+Tab cycles work modes (plan / read / edit / yolo == plan, default, acceptEdits, bypassPermissions). Today the only interactive setter is the team meta-sidebar per-agent control (claude_meta_sidebar.dart:342); the primary pane has none. There is a command claude.agent.set-permission-mode <sessionId> <mode> (extension.dart:148) but it needs an explicit id + explicit mode, and there is no ''cycle'' verb. setPermissionMode() over the stream-json control channel already works (stream_json_session.dart:671, D-78). Scope: (1) add a mode-cycle action that targets the focused/primary session and advances the safe trio default -> acceptEdits -> plan -> default (bypassPermissions only via a confirmed/explicit path, per the T-181 footgun guard); (2) bind it to Shift+Tab INTERCEPTED at the focused Claude composer -- note shift+tab is globally focus.previous (default.yaml:31), so this needs consumer-level interception like the Vim editor (D-82), falling back to focus traversal elsewhere; (3) make the status-line mode label a clickable badge that cycles on click (the ''cockpit badge'' the stream_json_session comment already promises at line 668-669). Relates to T-181, D-77/D-78, D-82.', 'Gap found while dogfooding (2026-06-03): the Claude pane shows the permission mode in its status line (permissionModeLabel, claude_status.dart:44) but has NO quick way to CHANGE it, unlike the CLI where Shift+Tab cycles work modes (plan / read / edit / yolo == plan, default, acceptEdits, bypassPermissions). Today the only interactive setter is the team meta-sidebar per-agent control (claude_meta_sidebar.dart:342); the primary pane has none. There is a command claude.agent.set-permission-mode <sessionId> <mode> (extension.dart:148) but it needs an explicit id + explicit mode, and there is no ''cycle'' verb. setPermissionMode() over the stream-json control channel already works (stream_json_session.dart:671, D-78). Scope: (1) add a mode-cycle action that targets the focused/primary session and advances the safe trio default -> acceptEdits -> plan -> default (bypassPermissions only via a confirmed/explicit path, per the T-181 footgun guard); (2) bind it to Shift+Tab INTERCEPTED at the focused Claude composer -- note shift+tab is globally focus.previous (default.yaml:31), so this needs consumer-level interception like the Vim editor (D-82), falling back to focus traversal elsewhere; (3) make the status-line mode label a clickable badge that cycles on click (the ''cockpit badge'' the stream_json_session comment already promises at line 668-669). Relates to T-181, D-77/D-78, D-82.
Refinement (2026-06-03): Shift+Tab is REJECTED as the trigger — Tab/Shift+Tab are real a11y focus-traversal intents (focus.next/focus.previous) since T-204, so hijacking Shift+Tab would break keyboard navigation. Instead: (a) Ctrl/Cmd+M cycles, intercepted at the focused Claude composer (so it targets that pane''s session, no global ''find focused session'' needed); (b) the status-line mode label becomes a FOCUSABLE button — cycles on click and on Enter/Space when focused (a11y-native, Tab reaches it); (c) a ''Claude: Cycle permission mode'' palette command. Safe trio default->acceptEdits->plan->default; bypassPermissions only via explicit confirmed path.', NULL, '2026-06-03 11:38:13', '2026-06-03 11:38:13', '2026-06-03 11:38:13', NULL, 'b67641486ea393fd30314a7202e4add7', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-181', 'status', 'done', 'in_progress', NULL, '2026-06-03 11:38:13', '2026-06-03 11:38:13', '2026-06-03 11:38:13', NULL, 'db3f97f062272f92dee5f266006d0c0d', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-181', 'description', 'Per-agent permission-mode control in the cockpit roster (extends T-171). Each roster row shows a mode badge (D default / A acceptEdits / P plan). Click cycles the SAFE trio default -> acceptEdits -> plan and sends set_permission_mode to that agent session (a confirmed stream-json control subtype). bypassPermissions is a footgun, so it is reachable only on SHIFT-click (behind a confirm), and the tooltip documents both behaviours. Acceptance: clicking the badge cycles the safe modes and the session receives set_permission_mode; shift-click can reach bypass behind a confirm; the badge reflects the live mode (T-157 status); tooltip explains click vs shift-click; widget + transport tests. Wireframe: docs/design/wireframes/claude-prompts/05-team-cockpit-sidebar.png. Blocked by T-169 (orchestrator).', 'Per-agent permission-mode control in the cockpit roster (extends T-171). Each roster row shows a mode badge (D default / A acceptEdits / P plan). Click cycles the SAFE trio default -> acceptEdits -> plan and sends set_permission_mode to that agent session (a confirmed stream-json control subtype). bypassPermissions is a footgun, so it is reachable only on SHIFT-click (behind a confirm), and the tooltip documents both behaviours. Acceptance: clicking the badge cycles the safe modes and the session receives set_permission_mode; shift-click can reach bypass behind a confirm; the badge reflects the live mode (T-157 status); tooltip explains click vs shift-click; widget + transport tests. Wireframe: docs/design/wireframes/claude-prompts/05-team-cockpit-sidebar.png. Blocked by T-169 (orchestrator).
Verified done (2026-06-03): the cockpit per-agent permission-mode cycle badge is implemented and wired in claude_meta_sidebar.dart — _PermissionModeBadge (safe cycle default->acceptEdits->plan, Shift-click bypass behind a confirm, live mode from SessionStatus). No further work; closing. The primary-pane equivalent is T-226.', NULL, '2026-06-03 11:41:17', '2026-06-03 11:41:17', '2026-06-03 11:41:17', NULL, '1a0f586aa78d40cb14e96592912176ca', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-181', 'status', 'in_progress', 'done', NULL, '2026-06-03 11:41:17', '2026-06-03 11:41:17', '2026-06-03 11:41:17', NULL, '791d993f7f46e80bbb5a4608f55a6038', 1) ON CONFLICT(hash) DO NOTHING;
+6
View File
@@ -225,3 +225,9 @@ Acceptance:
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-221', 'task', 'T-218', 'Add a clide status one-shot snapshot command', 'Gap 6. Add a `status` command to the dispatcher (currently unknown -> exit 3) returning a single snapshot: active pane, focused file + selection, git summary, layout — the natural first call an agent makes to orient. Acceptance: `clide status` returns a structured snapshot with exit 0. Depends on the pane/editor registries (C1/C2) for its pane and file fields.
Scope addition (2026-06-03, from T-220): clide status must surface the focused file + selection from BOTH the editor (EditorRegistry.active, already populated) AND the read-only markdown/decisions reader''s current file (D-81 ReaderNav), since viewer files rightly do not live in EditorRegistry. So ''focused file'' = whichever of {active editor buffer, active reader doc} the user is currently looking at. Pane field comes from the T-219 view-pane snapshot; git summary from git.status; layout from the arrangement.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-02 18:13:52', '2026-06-03 11:17:49', NULL, 'd14252d59f0932f126f32ee73ff09d69', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-226', 'task', NULL, 'Shift+Tab work-mode cycler on the focused Claude pane', 'Gap found while dogfooding (2026-06-03): the Claude pane shows the permission mode in its status line (permissionModeLabel, claude_status.dart:44) but has NO quick way to CHANGE it, unlike the CLI where Shift+Tab cycles work modes (plan / read / edit / yolo == plan, default, acceptEdits, bypassPermissions). Today the only interactive setter is the team meta-sidebar per-agent control (claude_meta_sidebar.dart:342); the primary pane has none. There is a command claude.agent.set-permission-mode <sessionId> <mode> (extension.dart:148) but it needs an explicit id + explicit mode, and there is no ''cycle'' verb. setPermissionMode() over the stream-json control channel already works (stream_json_session.dart:671, D-78). Scope: (1) add a mode-cycle action that targets the focused/primary session and advances the safe trio default -> acceptEdits -> plan -> default (bypassPermissions only via a confirmed/explicit path, per the T-181 footgun guard); (2) bind it to Shift+Tab INTERCEPTED at the focused Claude composer -- note shift+tab is globally focus.previous (default.yaml:31), so this needs consumer-level interception like the Vim editor (D-82), falling back to focus traversal elsewhere; (3) make the status-line mode label a clickable badge that cycles on click (the ''cockpit badge'' the stream_json_session comment already promises at line 668-669). Relates to T-181, D-77/D-78, D-82.
Refinement (2026-06-03): Shift+Tab is REJECTED as the trigger — Tab/Shift+Tab are real a11y focus-traversal intents (focus.next/focus.previous) since T-204, so hijacking Shift+Tab would break keyboard navigation. Instead: (a) Ctrl/Cmd+M cycles, intercepted at the focused Claude composer (so it targets that pane''s session, no global ''find focused session'' needed); (b) the status-line mode label becomes a FOCUSABLE button — cycles on click and on Enter/Space when focused (a11y-native, Tab reaches it); (c) a ''Claude: Cycle permission mode'' palette command. Safe trio default->acceptEdits->plan->default; bypassPermissions only via explicit confirmed path.', 'in_progress', 'medium', NULL, NULL, 'D-78', '2026-06-03 09:37:54', '2026-06-03 11:38:13', NULL, '5dd59c6cbf6f5586fef7c5f873bdfde4', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-181', 'task', 'T-164', 'Per-agent permission-mode cycle badge in the cockpit roster', 'Per-agent permission-mode control in the cockpit roster (extends T-171). Each roster row shows a mode badge (D default / A acceptEdits / P plan). Click cycles the SAFE trio default -> acceptEdits -> plan and sends set_permission_mode to that agent session (a confirmed stream-json control subtype). bypassPermissions is a footgun, so it is reachable only on SHIFT-click (behind a confirm), and the tooltip documents both behaviours. Acceptance: clicking the badge cycles the safe modes and the session receives set_permission_mode; shift-click can reach bypass behind a confirm; the badge reflects the live mode (T-157 status); tooltip explains click vs shift-click; widget + transport tests. Wireframe: docs/design/wireframes/claude-prompts/05-team-cockpit-sidebar.png. Blocked by T-169 (orchestrator).
Verified done (2026-06-03): the cockpit per-agent permission-mode cycle badge is implemented and wired in claude_meta_sidebar.dart — _PermissionModeBadge (safe cycle default->acceptEdits->plan, Shift-click bypass behind a confirm, live mode from SessionStatus). No further work; closing. The primary-pane equivalent is T-226.', 'done', 'low', NULL, NULL, 'D-77', '2026-05-25 09:16:05', '2026-06-03 11:41:17', NULL, '942902ee2825b610673362e5c0add5f7', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);