Refine assistant composer icon-only controls

This commit is contained in:
Haitao Pan 2026-03-25 14:28:40 +08:00
parent 18b7d7212a
commit bcaaaa91ec

View File

@ -2653,6 +2653,7 @@ class _ComposerBarState extends State<_ComposerBar> {
late final TextEditingController _skillPickerSearchController;
late final FocusNode _skillPickerSearchFocusNode;
bool _handlingPasteShortcut = false;
bool _refreshingSingleAgentSkills = false;
String _skillPickerQuery = '';
@override
@ -2661,6 +2662,7 @@ class _ComposerBarState extends State<_ComposerBar> {
_inputHeight = _defaultInputHeight;
_skillPickerSearchController = TextEditingController();
_skillPickerSearchFocusNode = FocusNode();
widget.controller.addListener(_handleControllerChanged);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
@ -2669,8 +2671,18 @@ class _ComposerBarState extends State<_ComposerBar> {
});
}
@override
void didUpdateWidget(covariant _ComposerBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (!identical(oldWidget.controller, widget.controller)) {
oldWidget.controller.removeListener(_handleControllerChanged);
widget.controller.addListener(_handleControllerChanged);
}
}
@override
void dispose() {
widget.controller.removeListener(_handleControllerChanged);
if (_skillPickerPortalController.isShowing) {
_skillPickerPortalController.hide();
}
@ -2679,6 +2691,13 @@ class _ComposerBarState extends State<_ComposerBar> {
super.dispose();
}
void _handleControllerChanged() {
if (!mounted || !_skillPickerPortalController.isShowing) {
return;
}
setState(() {});
}
void _resizeInput(double delta) {
final nextHeight = (_inputHeight + delta).clamp(
_minInputHeight,
@ -2781,11 +2800,27 @@ class _ComposerBarState extends State<_ComposerBar> {
_skillPickerSearchFocusNode.requestFocus();
});
if (widget.controller.isSingleAgentMode) {
unawaited(
widget.controller.refreshSingleAgentLocalSkillsForSession(
widget.controller.currentSessionKey,
),
unawaited(_refreshSingleAgentSkills());
}
}
Future<void> _refreshSingleAgentSkills() async {
if (_refreshingSingleAgentSkills) {
return;
}
setState(() {
_refreshingSingleAgentSkills = true;
});
try {
await widget.controller.refreshSingleAgentLocalSkillsForSession(
widget.controller.currentSessionKey,
);
} finally {
if (mounted) {
setState(() {
_refreshingSingleAgentSkills = false;
});
}
}
}
@ -2863,6 +2898,8 @@ class _ComposerBarState extends State<_ComposerBar> {
searchFocusNode: _skillPickerSearchFocusNode,
selectedSkillKeys: widget.selectedSkillKeys,
filteredSkills: _filteredSkillOptions(),
isLoading: _refreshingSingleAgentSkills,
hasQuery: _skillPickerQuery.trim().isNotEmpty,
onQueryChanged: (value) {
setState(() {
_skillPickerQuery = value;
@ -2964,9 +3001,8 @@ class _ComposerBarState extends State<_ComposerBar> {
.toList(),
child: _ComposerToolbarChip(
icon: executionTarget.icon,
label: executionTarget.label,
tooltip: _executionTargetTooltip(executionTarget),
showChevron: true,
maxLabelWidth: 96,
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
@ -2998,10 +3034,11 @@ class _ComposerBarState extends State<_ComposerBar> {
)
.toList(),
child: _ComposerToolbarChip(
icon: Icons.smart_toy_outlined,
label: controller.currentSingleAgentProvider.label,
icon: controller.currentSingleAgentProvider.icon,
tooltip: _singleAgentProviderTooltip(
controller.currentSingleAgentProvider,
),
showChevron: true,
maxLabelWidth: 92,
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
@ -3015,9 +3052,8 @@ class _ComposerBarState extends State<_ComposerBar> {
? _ComposerToolbarChip(
key: const Key('assistant-model-button'),
icon: Icons.bolt_rounded,
label: widget.modelLabel,
tooltip: _modelTooltip(widget.modelLabel),
showChevron: false,
maxLabelWidth: 132,
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
@ -3043,9 +3079,8 @@ class _ComposerBarState extends State<_ComposerBar> {
.toList(),
child: _ComposerToolbarChip(
icon: Icons.bolt_rounded,
label: widget.modelLabel,
tooltip: _modelTooltip(widget.modelLabel),
showChevron: true,
maxLabelWidth: 132,
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
@ -3099,11 +3134,10 @@ class _ComposerBarState extends State<_ComposerBar> {
padding: const EdgeInsets.only(left: 4),
child: _ComposerToolbarChip(
icon: Icons.hub_rounded,
label: collab.config.usesAris
? appText('ARIS', 'ARIS')
: appText('原生', 'Native'),
tooltip: collab.config.usesAris
? appText('多智能体模式: ARIS', 'Multi-agent mode: ARIS')
: appText('多智能体模式: 原生', 'Multi-agent mode: Native'),
showChevron: false,
maxLabelWidth: 64,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
@ -3222,14 +3256,8 @@ class _ComposerBarState extends State<_ComposerBar> {
onTap: _toggleSkillPicker,
child: _ComposerToolbarChip(
icon: Icons.auto_awesome_rounded,
label: selectedSkills.isEmpty
? appText('技能', 'Skills')
: appText(
'已选技能 ${selectedSkills.length}',
'Skills ${selectedSkills.length}',
),
tooltip: _skillsTooltip(selectedSkills.length),
showChevron: true,
maxLabelWidth: 132,
),
),
),
@ -3264,9 +3292,8 @@ class _ComposerBarState extends State<_ComposerBar> {
.toList(),
child: _ComposerToolbarChip(
icon: permissionLevel.icon,
label: permissionLevel.label,
tooltip: _permissionTooltip(permissionLevel),
showChevron: true,
maxLabelWidth: 120,
),
),
const SizedBox(width: 6),
@ -3298,9 +3325,8 @@ class _ComposerBarState extends State<_ComposerBar> {
.toList(),
child: _ComposerToolbarChip(
icon: Icons.psychology_alt_outlined,
label: _assistantThinkingLabel(widget.thinkingLabel),
tooltip: _thinkingTooltip(widget.thinkingLabel),
showChevron: true,
maxLabelWidth: 96,
),
),
],
@ -3463,9 +3489,8 @@ class _ComposerToolbarChip extends StatefulWidget {
const _ComposerToolbarChip({
super.key,
required this.icon,
required this.label,
required this.tooltip,
required this.showChevron,
this.maxLabelWidth = 220,
this.padding = const EdgeInsets.symmetric(
horizontal: AppSpacing.xs,
vertical: 6,
@ -3473,9 +3498,8 @@ class _ComposerToolbarChip extends StatefulWidget {
});
final IconData icon;
final String label;
final String tooltip;
final bool showChevron;
final double maxLabelWidth;
final EdgeInsetsGeometry padding;
@override
@ -3487,54 +3511,46 @@ class _ComposerToolbarChipState extends State<_ComposerToolbarChip> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final palette = context.palette;
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: Container(
padding: widget.padding,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(alpha: _hovered ? 0.94 : 0.88),
_hovered ? palette.chromeSurfacePressed : palette.chromeSurface,
return Tooltip(
message: widget.tooltip,
child: MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
child: Container(
padding: widget.padding,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
palette.chromeHighlight.withValues(
alpha: _hovered ? 0.94 : 0.88,
),
_hovered ? palette.chromeSurfacePressed : palette.chromeSurface,
],
),
borderRadius: BorderRadius.circular(AppRadius.chip),
border: Border.all(color: palette.chromeStroke),
boxShadow: [
_hovered ? palette.chromeShadowLift : palette.chromeShadowAmbient,
],
),
borderRadius: BorderRadius.circular(AppRadius.chip),
border: Border.all(color: palette.chromeStroke),
boxShadow: [
_hovered ? palette.chromeShadowLift : palette.chromeShadowAmbient,
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(widget.icon, size: 13, color: palette.textMuted),
const SizedBox(width: 4),
ConstrainedBox(
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth),
child: Text(
widget.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurface,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(widget.icon, size: 16, color: palette.textMuted),
if (widget.showChevron) ...[
const SizedBox(width: 1),
Icon(
Icons.keyboard_arrow_down_rounded,
size: 14,
color: palette.textMuted,
),
),
),
if (widget.showChevron) ...[
const SizedBox(width: 2),
Icon(
Icons.keyboard_arrow_down_rounded,
size: 14,
color: palette.textMuted,
),
],
],
],
),
),
),
);
@ -3556,6 +3572,39 @@ extension on AssistantPermissionLevel {
};
}
extension on SingleAgentProvider {
IconData get icon => switch (this) {
SingleAgentProvider.auto => Icons.auto_awesome_outlined,
SingleAgentProvider.codex => Icons.adb_rounded,
SingleAgentProvider.opencode => Icons.code_rounded,
SingleAgentProvider.claude => Icons.psychology_alt_outlined,
SingleAgentProvider.gemini => Icons.diamond_outlined,
};
}
String _executionTargetTooltip(AssistantExecutionTarget target) =>
appText('任务对话模式: ${target.label}', 'Task dialog mode: ${target.label}');
String _singleAgentProviderTooltip(SingleAgentProvider provider) => appText(
'单机智能体执行器: ${provider.label}',
'Single-agent provider: ${provider.label}',
);
String _modelTooltip(String modelLabel) =>
appText('模型: $modelLabel', 'Model: $modelLabel');
String _skillsTooltip(int selectedCount) => selectedCount <= 0
? appText('技能', 'Skills')
: appText('技能: 已选 $selectedCount 个', 'Skills: $selectedCount selected');
String _permissionTooltip(AssistantPermissionLevel level) =>
appText('权限: ${level.label}', 'Permissions: ${level.label}');
String _thinkingTooltip(String level) => appText(
'推理强度: ${_assistantThinkingLabel(level)}',
'Reasoning: ${_assistantThinkingLabel(level)}',
);
class _MessageBubble extends StatelessWidget {
const _MessageBubble({
required this.label,
@ -4904,6 +4953,8 @@ class _SkillPickerPopover extends StatelessWidget {
required this.searchFocusNode,
required this.selectedSkillKeys,
required this.filteredSkills,
required this.isLoading,
required this.hasQuery,
required this.onQueryChanged,
required this.onToggleSkill,
});
@ -4913,6 +4964,8 @@ class _SkillPickerPopover extends StatelessWidget {
final FocusNode searchFocusNode;
final List<String> selectedSkillKeys;
final List<_ComposerSkillOption> filteredSkills;
final bool isLoading;
final bool hasQuery;
final ValueChanged<String> onQueryChanged;
final ValueChanged<String> onToggleSkill;
@ -4974,12 +5027,35 @@ class _SkillPickerPopover extends StatelessWidget {
? Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
appText('没有匹配的技能。', 'No matching skills.'),
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: palette.textSecondary,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (isLoading) ...[
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: palette.textSecondary,
),
),
const SizedBox(height: 12),
],
Text(
isLoading
? appText('正在加载技能…', 'Loading skills…')
: hasQuery
? appText('没有匹配的技能。', 'No matching skills.')
: appText(
'当前没有已加载技能。',
'No skills are loaded yet.',
),
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: palette.textSecondary,
),
),
],
),
),
)