# Home Composer Redesign Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 重做 Home 输入组件,统一为单胶囊容器并稳定语音长按交互,消除布局漂移,同时保持 `+` 与发送等既有业务逻辑不变。 **Architecture:** 采用“单容器双模式”方案:`HomeScreen` 继续持有业务状态与动作,新增 `HomeComposer` 专注 UI 与手势分发;中间区域用受控状态在文本/按住说话/录音中/识别中之间切换,外层高度固定。录音提示与声波动画都内聚在主容器内部渲染,避免额外布局占位。 **Tech Stack:** Flutter, flutter_bloc, lucide_icons, design tokens (`AppColors`/`AppSpacing`/`AppRadius`), widget test --- ### Task 1: 建立 HomeComposer 组件骨架与参数契约 **Files:** - Create: `apps/lib/features/home/ui/widgets/home_composer.dart` - Modify: `apps/lib/features/home/ui/screens/home_screen.dart` - Test: `apps/test/features/home/ui/widgets/home_composer_test.dart` **Step 1: 写失败测试(渲染结构)** ```dart testWidgets('renders one unified rounded composer container', (tester) async { // pump HomeComposer with minimum required callbacks/states // expect: one root container, plus button, center content slot, right action slot }); ``` **Step 2: 运行测试确认失败** Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "renders one unified rounded composer container"` Expected: FAIL(`HomeComposer` 未定义或结构不匹配) **Step 3: 写最小实现** ```dart class HomeComposer extends StatelessWidget { const HomeComposer({ super.key, required this.isHoldToSpeakMode, required this.isRecording, required this.isTranscribing, required this.hasMessage, required this.isWaitingAgent, required this.onTapPlus, required this.onTapRightAction, required this.centerChild, }); // unified capsule container with left/center/right slots } ``` **Step 4: 再跑测试确认通过** Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "renders one unified rounded composer container"` Expected: PASS **Step 5: 小步提交(仅用户明确要求时)** ```bash git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_composer_test.dart git commit -m "refactor: extract unified home composer container" ``` ### Task 2: 完成右侧图标状态映射(activity/keyboard/send/stop) **Files:** - Modify: `apps/lib/features/home/ui/widgets/home_composer.dart` - Modify: `apps/lib/features/home/ui/screens/home_screen.dart` - Test: `apps/test/features/home/ui/widgets/home_composer_test.dart` **Step 1: 写失败测试(图标状态机)** ```dart testWidgets('right action icon follows state priority', (tester) async { // waiting > hasMessage > holdToSpeakMode > textMode // expect LucideIcons.square/send/keyboard/activity respectively }); ``` **Step 2: 运行测试确认失败** Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "right action icon follows state priority"` Expected: FAIL(图标选择逻辑尚未完整实现) **Step 3: 最小实现图标决策** ```dart IconData resolveRightIcon(...) { if (isWaitingAgent) return LucideIcons.square; if (hasMessage) return LucideIcons.send; return isHoldToSpeakMode ? LucideIcons.keyboard : LucideIcons.activity; } ``` **Step 4: 再跑测试确认通过** Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "right action icon follows state priority"` Expected: PASS **Step 5: 小步提交(仅用户明确要求时)** ```bash git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_composer_test.dart git commit -m "refactor: stabilize composer right action icon mapping" ``` ### Task 3: 实现中间区域双模式替换并固定高度 **Files:** - Modify: `apps/lib/features/home/ui/widgets/home_composer.dart` - Modify: `apps/lib/features/home/ui/screens/home_screen.dart` - Test: `apps/test/features/home/ui/widgets/home_composer_test.dart` **Step 1: 写失败测试(模式切换不改变高度)** ```dart testWidgets('composer height remains stable across mode switches', (tester) async { // measure size in text mode and hold-to-speak mode // expect equal heights }); ``` **Step 2: 运行测试确认失败** Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "composer height remains stable across mode switches"` Expected: FAIL(当前结构切换时高度波动) **Step 3: 最小实现(AnimatedSwitcher + fixed constraints)** ```dart SizedBox( height: composerHeight, child: AnimatedSwitcher( duration: switchDuration, child: isHoldToSpeakMode ? holdToSpeakChild : textInputChild, ), ) ``` **Step 4: 再跑测试确认通过** Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "composer height remains stable across mode switches"` Expected: PASS **Step 5: 小步提交(仅用户明确要求时)** ```bash git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_composer_test.dart git commit -m "refactor: keep composer layout stable during mode switch" ``` ### Task 4: 实现长按录音交互(开始/上滑取消/松开发送) **Files:** - Modify: `apps/lib/features/home/ui/screens/home_screen.dart` - Modify: `apps/lib/features/home/ui/widgets/home_composer.dart` - Test: `apps/test/features/home/ui/widgets/home_composer_test.dart` **Step 1: 写失败测试(录音提示只在 recording)** ```dart testWidgets('recording hint appears only while recording', (tester) async { // idle hold-to-speak: no hint // recording: show "松开发送,上滑取消" }); ``` **Step 2: 运行测试确认失败** Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "recording hint appears only while recording"` Expected: FAIL **Step 3: 最小实现录音流程映射** ```dart onLongPressStart => HapticFeedback.lightImpact() + onHoldStart(); onLongPressMoveUpdate => if (dy < threshold) onHoldCancel(); onLongPressEnd => onHoldEnd(autoSend: true); ``` **Step 4: 再跑测试确认通过** Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "recording hint appears only while recording"` Expected: PASS **Step 5: 小步提交(仅用户明确要求时)** ```bash git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_composer_test.dart git commit -m "feat: rework hold-to-speak interaction with stable recording state" ``` ### Task 5: 视觉重构为轻拟物胶囊(仅 tokens) **Files:** - Modify: `apps/lib/features/home/ui/widgets/home_composer.dart` - Modify: `apps/lib/core/theme/design_tokens.dart`(仅当现有 token 不足时新增) - Test: `apps/test/features/home/ui/widgets/home_composer_test.dart` **Step 1: 写失败测试(主容器统一性)** ```dart testWidgets('plus, center and right action are inside same capsule', (tester) async { // find one capsule host and verify children are descendants }); ``` **Step 2: 运行测试确认失败** Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "plus, center and right action are inside same capsule"` Expected: FAIL **Step 3: 最小视觉实现(不硬编码)** ```dart // use AppColors/AppSpacing/AppRadius and existing shadow tokens // no hardcoded color/spacing/radius/size ``` **Step 4: 再跑测试确认通过** Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "plus, center and right action are inside same capsule"` Expected: PASS **Step 5: 小步提交(仅用户明确要求时)** ```bash git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/core/theme/design_tokens.dart apps/test/features/home/ui/widgets/home_composer_test.dart git commit -m "refactor: redesign home composer with neumorphic capsule style" ``` ### Task 6: 集成回归与文档同步 **Files:** - Modify: `apps/lib/features/home/ui/screens/home_screen.dart` - Modify: `docs/runtime/runtime-route.md`(若交互说明有变化) **Step 1: 运行目标测试文件** Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart` Expected: PASS **Step 2: 运行 Home 相关回归测试(若新增)** Run: `flutter test test/features/home` Expected: PASS(若目录存在) **Step 3: 运行应用侧基础回归** Run: `flutter test` Expected: PASS 或仅存在与本改动无关的已知失败 **Step 4: 记录验证结论** ```text - 输入组件统一容器:通过 - 模式切换稳定:通过 - 录音提示条件:通过 - + 按钮/发送逻辑:通过 ``` **Step 5: 小步提交(仅用户明确要求时)** ```bash git add apps/lib/features/home/ui/screens/home_screen.dart apps/lib/features/home/ui/widgets/home_composer.dart apps/test/features/home/ui/widgets/home_composer_test.dart docs/runtime/runtime-route.md git commit -m "test: add regression coverage for home composer redesign" ``` ## 实施注意事项 - 保持 `HomeScreen` 作为业务状态单一来源,避免在 `HomeComposer` 内部复制业务状态。 - 录音中 (`recording`) 禁止触发模式切换,防止并发手势引发错位。 - 严格遵守 `apps/AGENTS.md`:不硬编码视觉值,必须使用 design tokens。 - 用户反馈统一使用 `Toast.show(...)`,不得引入 `SnackBar`。