Fix assistant composer shell height adaptation

This commit is contained in:
Haitao Pan 2026-03-22 14:12:34 +08:00
parent 3798e2f45b
commit eb79f302ee
2 changed files with 122 additions and 15 deletions

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
@ -22,6 +23,8 @@ import '../../widgets/desktop_workspace_scaffold.dart';
import '../../widgets/pane_resize_handle.dart';
import '../../widgets/surface_card.dart';
const double _assistantComposerDefaultInputHeight = 78;
class AssistantPage extends StatefulWidget {
const AssistantPage({
super.key,
@ -68,6 +71,7 @@ class _AssistantPageState extends State<AssistantPage> {
String? _lastSubmittedSessionKey;
String? _lastAutoAgentLabel;
List<String> _lastSubmittedAttachments = const <String>[];
double _composerContentHeight = 0;
@override
void initState() {
@ -361,28 +365,36 @@ class _AssistantPageState extends State<AssistantPage> {
}) {
return LayoutBuilder(
builder: (context, constraints) {
final composerHeight = constraints.maxHeight >= 900 ? 180.0 : 152.0;
final baseComposerHeight = constraints.maxHeight >= 900 ? 180.0 : 152.0;
final composerHeight = math.min(
math.max(0.0, constraints.maxHeight - 2),
math.max(baseComposerHeight, _composerContentHeight),
);
return Column(
children: [
Expanded(
child: _ConversationArea(
controller: controller,
currentTask: currentTask,
items: timelineItems,
messageViewMode: controller.currentAssistantMessageViewMode,
scrollController: _conversationController,
onOpenDetail: widget.onOpenDetail,
onFocusComposer: _focusComposer,
onOpenGateway: _showConnectDialog,
onOpenAiGatewaySettings: _openAiGatewaySettings,
onReconnectGateway: _connectFromSavedSettingsOrShowDialog,
onMessageViewModeChanged:
controller.setAssistantMessageViewMode,
child: KeyedSubtree(
key: const Key('assistant-conversation-shell'),
child: _ConversationArea(
controller: controller,
currentTask: currentTask,
items: timelineItems,
messageViewMode: controller.currentAssistantMessageViewMode,
scrollController: _conversationController,
onOpenDetail: widget.onOpenDetail,
onFocusComposer: _focusComposer,
onOpenGateway: _showConnectDialog,
onOpenAiGatewaySettings: _openAiGatewaySettings,
onReconnectGateway: _connectFromSavedSettingsOrShowDialog,
onMessageViewModeChanged:
controller.setAssistantMessageViewMode,
),
),
),
const SizedBox(height: 2),
SizedBox(
key: const Key('assistant-composer-shell'),
height: composerHeight,
child: _AssistantLowerPane(
inputController: _inputController,
@ -438,6 +450,8 @@ class _AssistantPageState extends State<AssistantPage> {
onOpenAiGatewaySettings: _openAiGatewaySettings,
onReconnectGateway: _connectFromSavedSettingsOrShowDialog,
onPickAttachments: _pickAttachments,
onComposerContentHeightChanged:
_handleComposerContentHeightChanged,
onSend: _submitPrompt,
),
),
@ -447,6 +461,15 @@ class _AssistantPageState extends State<AssistantPage> {
);
}
void _handleComposerContentHeightChanged(double value) {
if (!mounted || value == _composerContentHeight) {
return;
}
setState(() {
_composerContentHeight = value;
});
}
List<_TimelineItem> _buildTimelineItems(
AppController controller,
List<GatewayChatMessage> messages,
@ -1556,6 +1579,7 @@ class _AssistantLowerPane extends StatelessWidget {
required this.onOpenAiGatewaySettings,
required this.onReconnectGateway,
required this.onPickAttachments,
required this.onComposerContentHeightChanged,
required this.onSend,
});
@ -1579,6 +1603,7 @@ class _AssistantLowerPane extends StatelessWidget {
final VoidCallback onOpenAiGatewaySettings;
final Future<void> Function() onReconnectGateway;
final VoidCallback onPickAttachments;
final ValueChanged<double> onComposerContentHeightChanged;
final Future<void> Function() onSend;
@override
@ -1608,6 +1633,7 @@ class _AssistantLowerPane extends StatelessWidget {
onOpenAiGatewaySettings: onOpenAiGatewaySettings,
onReconnectGateway: onReconnectGateway,
onPickAttachments: onPickAttachments,
onContentHeightChanged: onComposerContentHeightChanged,
onSend: onSend,
),
),
@ -2381,6 +2407,7 @@ class _ComposerBar extends StatefulWidget {
required this.onOpenAiGatewaySettings,
required this.onReconnectGateway,
required this.onPickAttachments,
required this.onContentHeightChanged,
required this.onSend,
});
@ -2404,6 +2431,7 @@ class _ComposerBar extends StatefulWidget {
final VoidCallback onOpenAiGatewaySettings;
final Future<void> Function() onReconnectGateway;
final VoidCallback onPickAttachments;
final ValueChanged<double> onContentHeightChanged;
final Future<void> Function() onSend;
@override
@ -2412,10 +2440,12 @@ class _ComposerBar extends StatefulWidget {
class _ComposerBarState extends State<_ComposerBar> {
static const double _minInputHeight = 68;
static const double _defaultInputHeight = 78;
static const double _defaultInputHeight =
_assistantComposerDefaultInputHeight;
static const double _maxInputHeight = 220;
late double _inputHeight;
double? _reportedContentHeight;
@override
void initState() {
@ -2436,8 +2466,23 @@ class _ComposerBarState extends State<_ComposerBar> {
});
}
void _reportContentHeight() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
final height = context.size?.height;
if (height == null || height == _reportedContentHeight) {
return;
}
_reportedContentHeight = height;
widget.onContentHeightChanged(height);
});
}
@override
Widget build(BuildContext context) {
_reportContentHeight();
final palette = context.palette;
final controller = widget.controller;
final uiFeatures = controller.featuresFor(

View File

@ -463,20 +463,82 @@ void main() {
final resizeHandle = find.byKey(
const Key('assistant-composer-resize-handle'),
);
final conversationShell = find.byKey(
const Key('assistant-conversation-shell'),
);
final composerShell = find.byKey(const Key('assistant-composer-shell'));
expect(inputArea, findsOneWidget);
expect(resizeHandle, findsOneWidget);
expect(conversationShell, findsOneWidget);
expect(composerShell, findsOneWidget);
final initialHeight = tester.getSize(inputArea).height;
final initialComposerHeight = tester.getRect(composerShell).height;
final initialConversationHeight = tester.getRect(conversationShell).height;
await tester.drag(resizeHandle, const Offset(0, 40));
await tester.pumpAndSettle();
final expandedHeight = tester.getSize(inputArea).height;
final expandedComposerHeight = tester.getRect(composerShell).height;
final expandedConversationHeight = tester.getRect(conversationShell).height;
expect(expandedHeight, greaterThan(initialHeight));
expect(expandedComposerHeight, greaterThan(initialComposerHeight));
expect(expandedConversationHeight, lessThan(initialConversationHeight));
});
testWidgets(
'AssistantPage grows the composer shell for selected skills in short windows',
(WidgetTester tester) async {
final controller = await createTestController(tester);
await pumpPage(
tester,
size: const Size(1600, 620),
child: AssistantPage(controller: controller, onOpenDetail: (_) {}),
);
final conversationShell = find.byKey(
const Key('assistant-conversation-shell'),
);
final composerShell = find.byKey(const Key('assistant-composer-shell'));
final skillPickerButton = find.byKey(
const Key('assistant-skill-picker-button'),
);
expect(conversationShell, findsOneWidget);
expect(composerShell, findsOneWidget);
final initialComposerHeight = tester.getRect(composerShell).height;
final initialConversationBottom = tester
.getRect(conversationShell)
.bottom;
await tester.tap(skillPickerButton);
await tester.pumpAndSettle();
await tester.tap(
find.byKey(const ValueKey<String>('assistant-skill-option-xlsx')),
);
await tester.pumpAndSettle();
final expandedComposerHeight = tester.getRect(composerShell).height;
final expandedConversationBottom = tester
.getRect(conversationShell)
.bottom;
expect(expandedComposerHeight, greaterThan(initialComposerHeight));
expect(
expandedConversationBottom,
lessThanOrEqualTo(tester.getRect(composerShell).top),
);
expect(expandedConversationBottom, lessThan(initialConversationBottom));
expect(tester.takeException(), isNull);
},
);
// Known flutter_tester host-exit hang in this widget scenario.
testWidgets(
'AssistantPage syncs task selection with execution target menu and connection chip',