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:
@@ -336,6 +336,42 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('ClaudeComposer mode cycle (T-226)', () {
|
||||
late KernelFixture f;
|
||||
setUp(() async => f = await KernelFixture.create());
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
testWidgets('Ctrl+M fires onCycleMode and is consumed', (tester) async {
|
||||
var cycles = 0;
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClaudeComposer(onSubmit: (_) {}, onCycleMode: () => cycles++),
|
||||
));
|
||||
await tester.tap(find.byType(EditableText));
|
||||
await tester.pump();
|
||||
|
||||
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.keyM);
|
||||
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
|
||||
await tester.pump();
|
||||
|
||||
expect(cycles, 1);
|
||||
// The chord didn't type an 'm' into the field.
|
||||
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('plain m types normally (no modifier, no cycle)', (tester) async {
|
||||
var cycles = 0;
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
ClaudeComposer(onSubmit: (_) {}, onCycleMode: () => cycles++),
|
||||
));
|
||||
await tester.enterText(find.byType(EditableText), 'm');
|
||||
await tester.pump();
|
||||
expect(cycles, 0);
|
||||
});
|
||||
});
|
||||
|
||||
group('ClaudeComposer external focus node (T-227)', () {
|
||||
late KernelFixture f;
|
||||
setUp(() async => f = await KernelFixture.create());
|
||||
|
||||
@@ -81,4 +81,40 @@ void main() {
|
||||
expect(line, contains('\$0.05'));
|
||||
});
|
||||
});
|
||||
|
||||
group('nextSafePermissionMode (T-226)', () {
|
||||
test('cycles the safe trio and wraps', () {
|
||||
expect(nextSafePermissionMode('default'), 'acceptEdits');
|
||||
expect(nextSafePermissionMode('acceptEdits'), 'plan');
|
||||
expect(nextSafePermissionMode('plan'), 'default');
|
||||
});
|
||||
|
||||
test('bypassPermissions / unknown restarts at default (never cycles into bypass)', () {
|
||||
expect(nextSafePermissionMode('bypassPermissions'), 'default');
|
||||
expect(nextSafePermissionMode('whatever'), 'default');
|
||||
expect(kSafePermissionCycle, isNot(contains('bypassPermissions')));
|
||||
});
|
||||
});
|
||||
|
||||
group('statusSegmentsAroundMode (T-226)', () {
|
||||
test('splits model (leading) from ctx/cost/rate (trailing), mode excluded', () {
|
||||
const s = SessionStatus(
|
||||
model: 'claude-opus-4-7',
|
||||
permissionMode: 'plan',
|
||||
contextTokens: 21000,
|
||||
cost: 0.05,
|
||||
);
|
||||
final seg = statusSegmentsAroundMode(s);
|
||||
expect(seg.leading, 'opus 4.7');
|
||||
expect(seg.trailing, contains('21k ctx'));
|
||||
expect(seg.trailing, contains('\$0.05'));
|
||||
expect(seg.trailing, isNot(contains('plan'))); // mode is its own badge
|
||||
});
|
||||
|
||||
test('nulls when nothing to show on a side', () {
|
||||
final seg = statusSegmentsAroundMode(const SessionStatus(permissionMode: 'default'));
|
||||
expect(seg.leading, isNull);
|
||||
expect(seg.trailing, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user