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>
133 lines
4.7 KiB
Dart
133 lines
4.7 KiB
Dart
/// Icon-only permission-mode control trailing the Claude composer (T-275),
|
|
/// built on the ClideAnchoredOverlay + ClideMenu primitive (D-88).
|
|
///
|
|
/// Shows the current mode as a per-mode coloured glyph; clicking opens a menu
|
|
/// of the safe trio (default / accept-edits / plan) with the active one marked,
|
|
/// plus a divided, disabled `bypass` row — that footgun stays behind the
|
|
/// cockpit's explicit confirmed path (T-181), never one click away here. The
|
|
/// label lives in the tooltip, the menu rows, and the status-bar indicator —
|
|
/// the resting button is the glyph alone.
|
|
library;
|
|
|
|
import 'package:clide/builtin/claude/src/claude_status.dart' show kSafePermissionCycle, permissionModeLabel;
|
|
import 'package:clide/kernel/kernel.dart';
|
|
import 'package:clide/widgets/widgets.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
/// Per-mode glyph. `bypass` reuses a warning shield; the safe trio gets a
|
|
/// check-shield (default), pencil (accept-edits), and checklist (plan).
|
|
ClideIconPainter permissionModeIcon(String mode) {
|
|
switch (mode) {
|
|
case 'acceptEdits':
|
|
return PhosphorIcons.pencilSimple;
|
|
case 'plan':
|
|
return PhosphorIcons.listChecks;
|
|
case 'bypassPermissions':
|
|
return PhosphorIcons.shieldWarning;
|
|
default:
|
|
return PhosphorIcons.shieldCheck;
|
|
}
|
|
}
|
|
|
|
/// Per-mode accent — used by the control glyph and the passive status indicator
|
|
/// so both read the same colour. All theme tokens (no hardcoded hex).
|
|
Color permissionModeColor(String mode, SurfaceTokens tokens) {
|
|
switch (mode) {
|
|
case 'acceptEdits':
|
|
return tokens.statusWarning;
|
|
case 'plan':
|
|
return tokens.globalFocus;
|
|
case 'bypassPermissions':
|
|
return tokens.statusError;
|
|
default:
|
|
return tokens.globalTextMuted;
|
|
}
|
|
}
|
|
|
|
class PermissionModeControl extends StatefulWidget {
|
|
const PermissionModeControl({super.key, required this.mode, required this.onSelect});
|
|
|
|
/// Current permission mode (e.g. `default`, `acceptEdits`, `plan`).
|
|
final String mode;
|
|
|
|
/// Set a specific safe mode (the disabled `bypass` row never calls this).
|
|
final ValueChanged<String> onSelect;
|
|
|
|
@override
|
|
State<PermissionModeControl> createState() => _PermissionModeControlState();
|
|
}
|
|
|
|
class _PermissionModeControlState extends State<PermissionModeControl> {
|
|
final ClideOverlayController _overlay = ClideOverlayController();
|
|
|
|
@override
|
|
void dispose() {
|
|
_overlay.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
List<ClideMenuEntry> _entries(SurfaceTokens tokens) => [
|
|
for (final m in kSafePermissionCycle)
|
|
ClideMenuItem(
|
|
leading: permissionModeIcon(m),
|
|
color: permissionModeColor(m, tokens),
|
|
label: permissionModeLabel(m),
|
|
active: m == widget.mode,
|
|
onSelect: () => widget.onSelect(m),
|
|
),
|
|
const ClideMenuSeparator(),
|
|
ClideMenuItem(
|
|
leading: permissionModeIcon('bypassPermissions'),
|
|
color: permissionModeColor('bypassPermissions', tokens),
|
|
label: permissionModeLabel('bypassPermissions'),
|
|
enabled: false,
|
|
active: widget.mode == 'bypassPermissions',
|
|
onSelect: () {},
|
|
),
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tokens = ClideTheme.of(context).surface;
|
|
return ClideAnchoredOverlay(
|
|
controller: _overlay,
|
|
side: ClideAnchorSide.above,
|
|
align: ClideAnchorAlign.end,
|
|
offset: const Offset(0, -6),
|
|
overlayBuilder: (ctx, ctrl) => ClideMenu(
|
|
onClose: ctrl.close,
|
|
minWidth: 180,
|
|
entries: _entries(ClideTheme.of(ctx).surface),
|
|
),
|
|
anchor: ListenableBuilder(
|
|
listenable: _overlay,
|
|
builder: (ctx, _) {
|
|
final open = _overlay.isOpen;
|
|
return Semantics(
|
|
button: true,
|
|
label: 'permission mode: ${permissionModeLabel(widget.mode)}. Activate to change.',
|
|
excludeSemantics: true,
|
|
child: ClideTappable(
|
|
onTap: _overlay.toggle,
|
|
tooltip: 'Permission mode: ${permissionModeLabel(widget.mode)} — change (Ctrl/Cmd+M cycles)',
|
|
builder: (ctx, hovered, _) => Container(
|
|
width: 28,
|
|
height: 28,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: hovered ? tokens.listItemHoverBackground : null,
|
|
border: Border.all(
|
|
color: open ? tokens.globalFocus : (hovered ? tokens.panelActiveBorder : tokens.globalBorder),
|
|
),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: ClideIcon(permissionModeIcon(widget.mode), size: 16, color: permissionModeColor(widget.mode, tokens)),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|