From 18d4e811e98b96c2f4e45fa20e087671f7eeaf99 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Thu, 26 Mar 2026 19:22:58 +0800 Subject: [PATCH] Tighten assistant center pane layout --- lib/features/assistant/assistant_page.dart | 486 +++++++++++---------- test/features/assistant_page_suite.dart | 10 +- 2 files changed, 250 insertions(+), 246 deletions(-) diff --git a/lib/features/assistant/assistant_page.dart b/lib/features/assistant/assistant_page.dart index d2a5cac3..49349ba1 100644 --- a/lib/features/assistant/assistant_page.dart +++ b/lib/features/assistant/assistant_page.dart @@ -397,6 +397,7 @@ class _AssistantPageState extends State { }) { return LayoutBuilder( builder: (context, constraints) { + final palette = context.palette; final mediaQuery = MediaQuery.of(context); final composerBottomInset = math.max( mediaQuery.viewPadding.bottom, @@ -448,111 +449,120 @@ class _AssistantPageState extends State { .clamp(composerHeightLowerBound, composerHeightUpperBound) .toDouble(); - return Column( - children: [ - Expanded( - child: KeyedSubtree( - key: const Key('assistant-conversation-shell'), - child: _ConversationArea( - controller: controller, - currentTask: currentTask, - items: timelineItems, - messageViewMode: controller.currentAssistantMessageViewMode, + return SurfaceCard( + borderRadius: 0, + padding: EdgeInsets.zero, + tone: SurfaceCardTone.chrome, + child: Column( + children: [ + Expanded( + child: KeyedSubtree( + key: const Key('assistant-conversation-shell'), + child: _ConversationArea( + controller: controller, + currentTask: currentTask, + items: timelineItems, + messageViewMode: controller.currentAssistantMessageViewMode, + bottomContentInset: composerBottomSpacing, + topTrailingInset: _artifactPaneCollapsed + ? _assistantCollapsedArtifactToggleClearance + : 0, + scrollController: _conversationController, + onOpenDetail: widget.onOpenDetail, + onFocusComposer: _focusComposer, + onOpenGateway: _openGatewaySettings, + onOpenAiGatewaySettings: _openAiGatewaySettings, + onReconnectGateway: _connectFromSavedSettingsOrShowDialog, + onMessageViewModeChanged: + controller.setAssistantMessageViewMode, + ), + ), + ), + ColoredBox( + color: palette.canvas, + child: SizedBox( + key: const Key('assistant-workspace-resize-handle'), + height: _assistantVerticalResizeHandleHeight, + child: PaneResizeHandle( + axis: Axis.vertical, + onDelta: (delta) { + setState(() { + final nextComposerHeight = (composerHeight - delta) + .clamp( + composerHeightLowerBound, + composerHeightUpperBound, + ) + .toDouble(); + _workspaceLowerPaneHeightAdjustment = + nextComposerHeight - defaultComposerHeight; + }); + }, + ), + ), + ), + SizedBox( + key: const Key('assistant-composer-shell'), + height: composerHeight, + child: _AssistantLowerPane( bottomContentInset: composerBottomSpacing, - topTrailingInset: _artifactPaneCollapsed - ? _assistantCollapsedArtifactToggleClearance - : 0, - scrollController: _conversationController, - onOpenDetail: widget.onOpenDetail, - onFocusComposer: _focusComposer, + inputController: _inputController, + focusNode: _composerFocusNode, + thinkingLabel: _thinkingLabel, + showModelControl: !controller.isSingleAgentMode + ? true + : controller.currentSingleAgentShouldShowModelControl, + modelLabel: controller.isSingleAgentMode + ? controller.currentSingleAgentModelDisplayLabel + : controller.resolvedAssistantModel.isEmpty + ? appText('未选择模型', 'No model selected') + : controller.resolvedAssistantModel, + modelOptions: controller.assistantModelChoices, + attachments: _attachments, + availableSkills: _availableSkillOptions(controller), + selectedSkillKeys: _selectedSkillKeysFor(controller), + controller: controller, + onRemoveAttachment: (attachment) { + setState(() { + _attachments = _attachments + .where((item) => item.path != attachment.path) + .toList(growable: false); + }); + }, + onToggleSkill: (key) { + unawaited( + controller.toggleAssistantSkillForSession( + controller.currentSessionKey, + key, + ), + ); + _focusComposer(); + }, + onThinkingChanged: (value) { + setState(() => _thinkingLabel = value); + }, + onModelChanged: (modelId) => + controller.selectAssistantModelForSession( + controller.currentSessionKey, + modelId, + ), onOpenGateway: _openGatewaySettings, onOpenAiGatewaySettings: _openAiGatewaySettings, onReconnectGateway: _connectFromSavedSettingsOrShowDialog, - onMessageViewModeChanged: - controller.setAssistantMessageViewMode, + onPickAttachments: _pickAttachments, + onAddAttachment: (attachment) { + setState(() { + _attachments = [..._attachments, attachment]; + }); + }, + onPasteImageAttachment: + widget.clipboardImageReader ?? _readClipboardImageAsXFile, + onComposerInputHeightChanged: + _handleComposerInputHeightChanged, + onSend: _submitPrompt, ), ), - ), - SizedBox( - key: const Key('assistant-workspace-resize-handle'), - height: _assistantVerticalResizeHandleHeight, - child: PaneResizeHandle( - axis: Axis.vertical, - onDelta: (delta) { - setState(() { - final nextComposerHeight = (composerHeight - delta) - .clamp( - composerHeightLowerBound, - composerHeightUpperBound, - ) - .toDouble(); - _workspaceLowerPaneHeightAdjustment = - nextComposerHeight - defaultComposerHeight; - }); - }, - ), - ), - SizedBox( - key: const Key('assistant-composer-shell'), - height: composerHeight, - child: _AssistantLowerPane( - bottomContentInset: composerBottomSpacing, - inputController: _inputController, - focusNode: _composerFocusNode, - thinkingLabel: _thinkingLabel, - showModelControl: !controller.isSingleAgentMode - ? true - : controller.currentSingleAgentShouldShowModelControl, - modelLabel: controller.isSingleAgentMode - ? controller.currentSingleAgentModelDisplayLabel - : controller.resolvedAssistantModel.isEmpty - ? appText('未选择模型', 'No model selected') - : controller.resolvedAssistantModel, - modelOptions: controller.assistantModelChoices, - attachments: _attachments, - availableSkills: _availableSkillOptions(controller), - selectedSkillKeys: _selectedSkillKeysFor(controller), - controller: controller, - onRemoveAttachment: (attachment) { - setState(() { - _attachments = _attachments - .where((item) => item.path != attachment.path) - .toList(growable: false); - }); - }, - onToggleSkill: (key) { - unawaited( - controller.toggleAssistantSkillForSession( - controller.currentSessionKey, - key, - ), - ); - _focusComposer(); - }, - onThinkingChanged: (value) { - setState(() => _thinkingLabel = value); - }, - onModelChanged: (modelId) => - controller.selectAssistantModelForSession( - controller.currentSessionKey, - modelId, - ), - onOpenGateway: _openGatewaySettings, - onOpenAiGatewaySettings: _openAiGatewaySettings, - onReconnectGateway: _connectFromSavedSettingsOrShowDialog, - onPickAttachments: _pickAttachments, - onAddAttachment: (attachment) { - setState(() { - _attachments = [..._attachments, attachment]; - }); - }, - onPasteImageAttachment: - widget.clipboardImageReader ?? _readClipboardImageAsXFile, - onComposerInputHeightChanged: _handleComposerInputHeightChanged, - onSend: _submitPrompt, - ), - ), - ], + ], + ), ); }, ); @@ -1817,36 +1827,36 @@ class _AssistantLowerPane extends StatelessWidget { @override Widget build(BuildContext context) { - return Align( - alignment: Alignment.bottomCenter, + final palette = context.palette; + + return ColoredBox( + color: palette.canvas, child: SingleChildScrollView( physics: const ClampingScrollPhysics(), - child: Padding( - padding: EdgeInsets.only(bottom: bottomContentInset), - child: _ComposerBar( - controller: controller, - inputController: inputController, - focusNode: focusNode, - thinkingLabel: thinkingLabel, - showModelControl: showModelControl, - modelLabel: modelLabel, - modelOptions: modelOptions, - attachments: attachments, - availableSkills: availableSkills, - selectedSkillKeys: selectedSkillKeys, - onRemoveAttachment: onRemoveAttachment, - onToggleSkill: onToggleSkill, - onThinkingChanged: onThinkingChanged, - onModelChanged: onModelChanged, - onOpenGateway: onOpenGateway, - onOpenAiGatewaySettings: onOpenAiGatewaySettings, - onReconnectGateway: onReconnectGateway, - onPickAttachments: onPickAttachments, - onAddAttachment: onAddAttachment, - onPasteImageAttachment: onPasteImageAttachment, - onInputHeightChanged: onComposerInputHeightChanged, - onSend: onSend, - ), + padding: EdgeInsets.only(bottom: bottomContentInset), + child: _ComposerBar( + controller: controller, + inputController: inputController, + focusNode: focusNode, + thinkingLabel: thinkingLabel, + showModelControl: showModelControl, + modelLabel: modelLabel, + modelOptions: modelOptions, + attachments: attachments, + availableSkills: availableSkills, + selectedSkillKeys: selectedSkillKeys, + onRemoveAttachment: onRemoveAttachment, + onToggleSkill: onToggleSkill, + onThinkingChanged: onThinkingChanged, + onModelChanged: onModelChanged, + onOpenGateway: onOpenGateway, + onOpenAiGatewaySettings: onOpenAiGatewaySettings, + onReconnectGateway: onReconnectGateway, + onPickAttachments: onPickAttachments, + onAddAttachment: onAddAttachment, + onPasteImageAttachment: onPasteImageAttachment, + onInputHeightChanged: onComposerInputHeightChanged, + onSend: onSend, ), ), ); @@ -1889,122 +1899,117 @@ class _ConversationArea extends StatelessWidget { Widget build(BuildContext context) { final palette = context.palette; - return SurfaceCard( - borderRadius: 0, - padding: EdgeInsets.zero, - tone: SurfaceCardTone.chrome, - child: Column( - children: [ - Padding( - padding: EdgeInsets.fromLTRB(10, 8, 10 + topTrailingInset, 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Spacer(), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - _MessageViewModeChip( - value: messageViewMode, - onSelected: onMessageViewModeChanged, - ), - const SizedBox(width: 6), - _ConnectionChip(controller: controller), - ], - ), - ], - ), + return Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(10, 8, 10 + topTrailingInset, 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + _MessageViewModeChip( + value: messageViewMode, + onSelected: onMessageViewModeChanged, + ), + const SizedBox(width: 6), + _ConnectionChip(controller: controller), + ], + ), + ], ), - Divider(height: 1, color: palette.strokeSoft), - Expanded( - child: Container( - decoration: BoxDecoration(color: palette.canvas), - child: items.isEmpty - ? _AssistantEmptyState( - controller: controller, - onFocusComposer: onFocusComposer, - onOpenGateway: onOpenGateway, - onOpenAiGatewaySettings: onOpenAiGatewaySettings, - onReconnectGateway: onReconnectGateway, - ) - : ListView.separated( - controller: scrollController, - padding: EdgeInsets.fromLTRB( - 10, - 8, - 10, - 8 + bottomContentInset, - ), - physics: const BouncingScrollPhysics(), - itemCount: items.length, - separatorBuilder: (_, _) => const SizedBox(height: 6), - itemBuilder: (context, index) { - final item = items[index]; - return switch (item.kind) { - _TimelineItemKind.user => _MessageBubble( - label: item.label!, - text: item.text!, - alignRight: true, - tone: _BubbleTone.user, - messageViewMode: messageViewMode, - ), - _TimelineItemKind.assistant => _MessageBubble( - label: item.label!, - text: item.text!, - alignRight: false, - tone: _BubbleTone.assistant, - messageViewMode: messageViewMode, - ), - _TimelineItemKind.agent => _MessageBubble( - label: item.label!, - text: item.text!, - alignRight: false, - tone: _BubbleTone.agent, - messageViewMode: messageViewMode, - ), - _TimelineItemKind.toolCall => _ToolCallTile( - toolName: item.title!, - summary: item.text!, - pending: item.pending, - error: item.error, - onOpenDetail: () => onOpenDetail( - DetailPanelData( - title: item.title!, - subtitle: appText('工具调用', 'Tool Call'), - icon: Icons.build_circle_outlined, - status: StatusInfo( - item.pending - ? appText('运行中', 'Running') - : appText('已完成', 'Completed'), - item.error - ? StatusTone.danger - : StatusTone.accent, - ), - description: item.text ?? '', - meta: [ - controller.currentSessionKey, - controller.activeAgentName, - ], - actions: [appText('复制', 'Copy')], - sections: const [], + ), + Divider(height: 1, color: palette.strokeSoft), + Expanded( + child: Container( + decoration: BoxDecoration(color: palette.canvas), + child: items.isEmpty + ? _AssistantEmptyState( + controller: controller, + onFocusComposer: onFocusComposer, + onOpenGateway: onOpenGateway, + onOpenAiGatewaySettings: onOpenAiGatewaySettings, + onReconnectGateway: onReconnectGateway, + ) + : ListView.separated( + controller: scrollController, + padding: EdgeInsets.fromLTRB( + 10, + 8, + 10, + 8 + bottomContentInset, + ), + physics: const BouncingScrollPhysics(), + itemCount: items.length, + separatorBuilder: (_, _) => const SizedBox(height: 6), + itemBuilder: (context, index) { + final item = items[index]; + return switch (item.kind) { + _TimelineItemKind.user => _MessageBubble( + label: item.label!, + text: item.text!, + alignRight: true, + tone: _BubbleTone.user, + messageViewMode: messageViewMode, + ), + _TimelineItemKind.assistant => _MessageBubble( + label: item.label!, + text: item.text!, + alignRight: false, + tone: _BubbleTone.assistant, + messageViewMode: messageViewMode, + ), + _TimelineItemKind.agent => _MessageBubble( + label: item.label!, + text: item.text!, + alignRight: false, + tone: _BubbleTone.agent, + messageViewMode: messageViewMode, + ), + _TimelineItemKind.toolCall => _ToolCallTile( + toolName: item.title!, + summary: item.text!, + pending: item.pending, + error: item.error, + onOpenDetail: () => onOpenDetail( + DetailPanelData( + title: item.title!, + subtitle: appText('工具调用', 'Tool Call'), + icon: Icons.build_circle_outlined, + status: StatusInfo( + item.pending + ? appText('运行中', 'Running') + : appText('已完成', 'Completed'), + item.error + ? StatusTone.danger + : StatusTone.accent, ), + description: item.text ?? '', + meta: [ + controller.currentSessionKey, + controller.activeAgentName, + ], + actions: [appText('复制', 'Copy')], + sections: const [], ), ), - _TimelineItemKind.taskCard => _TaskStatusCard( - title: item.title!, - status: item.status!, - summary: item.summary!, - detail: item.detail!, - owner: item.owner!, - sessionKey: item.sessionKey!, - ), - }; - }, - ), - ), + ), + _TimelineItemKind.taskCard => _TaskStatusCard( + title: item.title!, + status: item.status!, + summary: item.summary!, + detail: item.detail!, + owner: item.owner!, + sessionKey: item.sessionKey!, + ), + }; + }, + ), ), - ], - ), + ), + ], ); } } @@ -2992,9 +2997,8 @@ class _ComposerBarState extends State<_ComposerBar> { ? appText('重连', 'Reconnect') : appText('连接', 'Connect'); - return SurfaceCard( - borderRadius: 10, - tone: SurfaceCardTone.chrome, + return Padding( + padding: const EdgeInsets.fromLTRB(10, 8, 10, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/test/features/assistant_page_suite.dart b/test/features/assistant_page_suite.dart index 9689f221..d1269540 100644 --- a/test/features/assistant_page_suite.dart +++ b/test/features/assistant_page_suite.dart @@ -531,7 +531,7 @@ void main() { expect(submitButton, findsOneWidget); expect( tester.getRect(composerShell).bottom, - moreOrLessEquals(pageRect.bottom, epsilon: 0.01), + moreOrLessEquals(pageRect.bottom, epsilon: 1.01), ); expect( tester.getRect(submitButton).bottom, @@ -1024,11 +1024,11 @@ void main() { ); expect( horizontalHandleRect.right, - moreOrLessEquals(conversationRect.left, epsilon: 2.01), + moreOrLessEquals(conversationRect.left, epsilon: 4.01), ); expect( conversationRect.top, - moreOrLessEquals(pageRect.top, epsilon: 0.01), + moreOrLessEquals(pageRect.top, epsilon: 1.01), ); expect( conversationRect.bottom, @@ -1040,11 +1040,11 @@ void main() { ); expect( composerRect.bottom, - moreOrLessEquals(pageRect.bottom, epsilon: 0.01), + moreOrLessEquals(pageRect.bottom, epsilon: 1.01), ); expect( composerRect.right, - moreOrLessEquals(pageRect.right, epsilon: 0.01), + moreOrLessEquals(pageRect.right, epsilon: 1.01), ); expect(conversationRect.width, greaterThan(620)); expect(conversationRect.height, greaterThanOrEqualTo(180));