Fix assistant composer shell height adaptation
This commit is contained in:
parent
3798e2f45b
commit
eb79f302ee
@ -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(
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user