permission-mode control beside the Claude composer (T-275)

Build the T-275 picker on the new popover primitive (D-88): an icon-only,
per-mode-coloured button trailing the composer text box opens a ClideMenu of
the safe trio (default/acceptEdits/plan, active marked) plus a divided, disabled
bypass row (the footgun stays behind the cockpit guard, T-181). The label lives
in the tooltip, the menu, and the status bar — the resting button is the glyph
alone. Coexists with the composer's Stop row when busy.

- new permission_mode_control.dart (PermissionModeControl + per-mode
  icon/colour helpers); shieldCheck/shieldWarning glyphs added to PhosphorIcons.
- claude_composer.dart: permissionMode + onSetPermissionMode props; control
  trails the text box (bottom-aligned), shown only when wired.
- claude_pane.dart: pass the current mode + a setter; demote the status-bar
  _ModeBadge to a passive, per-mode-coloured text indicator (no click). Ctrl/Cmd+M
  still cycles (onCycleMode unchanged).

Regenerated the phosphor-glyphs reference (47 defined). Tests: menu opens with
the trio + disabled bypass, select sets the mode, helpers map colours/icons,
control coexists with Stop, hidden when no mode.

Note: claude_pane.dart also carries the earlier T-274 resume diagnostic log line
(uncommitted in the working tree, reviewed as benign) — it rides along here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 18:52:29 +02:00
co-authored by Claude Opus 4.8
parent 3550f10dfc
commit a95b332925
9 changed files with 347 additions and 67 deletions
+69 -41
View File
@@ -13,6 +13,7 @@ import 'dart:io';
import 'package:clide/builtin/claude/src/claude_config.dart';
import 'package:clide/builtin/claude/src/clipboard_paste.dart';
import 'package:clide/builtin/claude/src/permission_mode_control.dart';
import 'package:clide/builtin/claude/src/running_indicator.dart';
import 'package:clide/builtin/claude/src/slash_commands.dart';
import 'package:clide/kernel/src/theme/controller.dart';
@@ -50,6 +51,8 @@ class ClaudeComposer extends StatefulWidget {
this.onInterrupt,
this.busy = false,
this.onCycleMode,
this.permissionMode,
this.onSetPermissionMode,
this.initialValue,
this.onDraftChanged,
this.history = const [],
@@ -85,6 +88,14 @@ class ClaudeComposer extends StatefulWidget {
/// Whether a turn is in flight; shows the Stop affordance.
final bool busy;
/// Current permission mode (T-275). When set together with
/// [onSetPermissionMode], an icon-only mode control trails the text box,
/// opening a menu to switch mode. Null hides the control.
final String? permissionMode;
/// Set a specific permission mode from the trailing control's menu (T-275).
final ValueChanged<String>? onSetPermissionMode;
/// Seed value (text + selection) the composer mounts with — the
/// persisted per-session draft (T-228). The composer restores this on
/// init so an in-progress message survives the composer being torn down
@@ -504,52 +515,69 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
children: [for (final a in _attachments) _chip(theme, a)],
),
),
Semantics(
label: widget.hint,
textField: true,
child: Shortcuts(
shortcuts: const {
SingleActivator(LogicalKeyboardKey.enter): SubmitComposerIntent(),
SingleActivator(LogicalKeyboardKey.numpadEnter): SubmitComposerIntent(),
},
child: Actions(
actions: {
SubmitComposerIntent: CallbackAction<SubmitComposerIntent>(
onInvoke: (_) {
_submit();
return null;
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Semantics(
label: widget.hint,
textField: true,
child: Shortcuts(
shortcuts: const {
SingleActivator(LogicalKeyboardKey.enter): SubmitComposerIntent(),
SingleActivator(LogicalKeyboardKey.numpadEnter): SubmitComposerIntent(),
},
),
PasteTextIntent: CallbackAction<PasteTextIntent>(
onInvoke: (_) {
unawaited(_handlePaste());
return null;
},
),
},
child: Stack(
children: [
if (!hasText)
Positioned(
left: 0,
top: 0,
right: 0,
child: ClideText(widget.hint, muted: true, fontSize: clideFontBody),
child: Actions(
actions: {
SubmitComposerIntent: CallbackAction<SubmitComposerIntent>(
onInvoke: (_) {
_submit();
return null;
},
),
PasteTextIntent: CallbackAction<PasteTextIntent>(
onInvoke: (_) {
unawaited(_handlePaste());
return null;
},
),
},
child: Stack(
children: [
if (!hasText)
Positioned(
left: 0,
top: 0,
right: 0,
child: ClideText(widget.hint, muted: true, fontSize: clideFontBody),
),
EditableText(
controller: _controller,
focusNode: _focus,
readOnly: !widget.enabled,
style: TextStyle(fontSize: clideFontBody, color: fg, height: 1.4),
cursorColor: theme.globalFocus,
backgroundCursorColor: theme.globalTextMuted,
maxLines: 8,
minLines: 1,
),
],
),
EditableText(
controller: _controller,
focusNode: _focus,
readOnly: !widget.enabled,
style: TextStyle(fontSize: clideFontBody, color: fg, height: 1.4),
cursorColor: theme.globalFocus,
backgroundCursorColor: theme.globalTextMuted,
maxLines: 8,
minLines: 1,
),
],
),
),
),
),
if (widget.permissionMode != null && widget.onSetPermissionMode != null) ...[
const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: PermissionModeControl(
mode: widget.permissionMode!,
onSelect: widget.onSetPermissionMode!,
),
),
],
],
),
],
),