feat: 实现 AgentScope ReAct Runner 两阶段执行并重构事件处理

This commit is contained in:
zl-q
2026-03-16 09:01:01 +08:00
parent 072c09d99d
commit dcceb48d84
51 changed files with 5015 additions and 5663 deletions
@@ -1,122 +0,0 @@
# Home 输入组件重做设计(HomeComposer Redesign
## 1. 目标与范围
### 1.1 目标
- 解决当前输入组件“质感弱、结构割裂、录音时布局漂移”的问题。
- 统一 `+` 按钮、输入区、右侧动作图标到一个圆角矩形主容器内。
- 保留并复用现有录音、转写、自动发送、停止生成、Toast 错误处理逻辑。
### 1.2 非目标
- 不改动聊天流、消息发送后端协议、语音识别接口。
- 不改动 `+` 按钮业务行为。
- 不新增独立的页面级浮层录音面板。
## 2. 问题诊断(现状)
- 当前输入区由多个分离容器拼接,视觉上像“纸片贴上去”。
- 输入框本体和右侧图标视觉上未合为一个整体容器。
- “按住说话”提示与录音动画在主布局外追加,录音时造成结构上下跳动。
## 3. 方案对比
### 方案 A:单容器双模式(推荐)
- 单一胶囊主容器承载三段:左操作、中间主内容、右操作。
- 中间区域在文本模式与按住说话模式之间替换(`AnimatedSwitcher`)。
- 录音动画仅在中间区域内部切换显示,不改变主容器高度。
优点:结构稳定、状态清晰、维护成本低。
缺点:视觉表达自由度略低于 Overlay 方案。
### 方案 B:双容器交叉切换
- 文本容器和语音容器完整分离,做交叉淡入。
优点:切换动效可做得更明显。
缺点:状态同步复杂,容易再次出现错位与边界问题。
### 方案 COverlay 浮层
- 保持输入容器不变,录音时叠加浮层。
优点:动画自由度高。
缺点:与“整块替换”诉求不一致,事件命中与无障碍处理更复杂。
结论:采用方案 A。
## 4. 信息架构与组件边界
## 4.1 新组件
- 新建 `HomeComposer`(从 `home_screen.dart` 抽离输入区渲染职责)。
- `HomeScreen` 继续持有业务状态与行为方法,`HomeComposer` 负责展示与手势分发。
## 4.2 主容器结构
- 一个圆角矩形主容器(轻拟物胶囊风格)。
- 左侧:`+` 按钮(行为不变)。
- 中间:
- 文本模式:无边框输入区(文字垂直居中)。
- 语音模式:按住说话按钮区。
- 录音中/识别中:在语音模式内部替换状态内容。
- 右侧:动作图标(声波/键盘/发送/停止)。
## 5. 状态机设计
## 5.1 状态定义
- 模式层:`text` / `holdToSpeak`
- 过程层:`idle` / `recording` / `transcribing`
## 5.2 核心约束
- 主容器高度固定,状态变化不得引发外层布局高度变化。
- `recording` 时禁止模式切换,避免状态错位。
## 5.3 右侧图标决策
- Agent 等待中:停止图标。
- 非等待:
- 有文本:发送图标。
- 无文本且 `text``LucideIcons.activity`
- 无文本且 `holdToSpeak``LucideIcons.keyboard`
## 6. 交互与动画
## 6.1 长按语音流程
1. `onLongPressStart`:触发 `HapticFeedback.lightImpact()`,开始录音。
2. `onLongPressMoveUpdate`:上滑超过阈值,取消录音。
3. `onLongPressEnd`:未取消则停止录音,转写并自动发送。
## 6.2 提示文案显示策略
- “松开发送,上滑取消”仅在 `recording` 显示。
- 空闲按住说话模式不显示该提示。
## 6.3 动画策略
- 模式切换:短时 `AnimatedSwitcher`(淡入/轻位移)。
- 录音波形:仅在 `recording` 驱动;停止后立刻回收。
- 动画渲染在中间区域内部,不新增外部占位。
## 7. 视觉规范(Design Tokens
- 严格使用 `apps/lib/core/theme/design_tokens.dart` 中的 `AppColors``AppSpacing``AppRadius`
- 禁止硬编码颜色、间距、圆角、尺寸、阴影。
- 主容器采用白色底 + 细边 + 柔和阴影,形成轻拟物层次。
- 输入区内部无额外边框,确保文字垂直居中和图标视觉对齐。
## 8. 验收标准
- 图标与输入区在同一圆角矩形中,不再分离。
- 录音全流程不出现输入组件上移、下坠或高度抖动。
- 提示文案仅在实际录音中显示。
- 文本/语音模式切换平滑;`+` 与发送逻辑行为保持一致。
## 9. 风险与缓解
- 风险:重构过程中影响现有发送/停止生成分支。
- 缓解:优先复用原有行为方法,仅调整 UI 结构与状态映射。
- 风险:手势与模式切换并发导致状态错乱。
- 缓解:录音期间加切换锁,结束后释放。
## 10. 验证计划
- 手工验证:
- 文本发送、停止生成、`+` 弹层、语音长按录音、上滑取消、自动发送。
- 文本模式与语音模式往返切换稳定性。
- Widget 测试(建议新增):
- 右侧图标状态映射测试。
- 录音中提示文案显示条件测试。
- 模式切换时主容器高度恒定测试。
@@ -1,275 +0,0 @@
# 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`
@@ -1,261 +0,0 @@
# Agent Runs Multimodal Refactor Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 让 runs/resume 使用真实多模态图片输入,并将 worker/tool 按新结构化 metadata 规范落库。
**Architecture:** 保持现有 event pipeline,不引入旁路写库。请求入口完成 URL 安全边界校验;runtime 将 `binary` 转模型可识别 `image_url` blockevent store 统一校验 `WorkerAgentOutput` / `ToolAgentOutput` 并完成 `content` 映射。
**Tech Stack:** FastAPI, Pydantic v2, SQLAlchemy AsyncSession, AgentScope, LiteLLM, Redis Stream
---
### Task 1: Runs 输入安全边界
**Files:**
- Modify: `backend/src/core/agentscope/schemas/agui_input.py`
- Modify: `backend/src/v1/agent/router.py`
- Modify: `backend/src/v1/agent/service.py`
- Test: `backend/tests/unit/v1/agent/test_agent_router.py`
**Step 1: Write the failing test**
```python
def test_runs_rejects_non_project_signed_url(...) -> None:
payload = build_run_payload_with_binary_url("https://evil.example.com/storage/v1/object/sign/..." )
resp = client.post("/api/v1/agent/runs", json=payload, headers=auth_headers)
assert resp.status_code == 422
```
**Step 2: Run test to verify it fails**
Run: `pytest backend/tests/unit/v1/agent/test_agent_router.py::test_runs_rejects_non_project_signed_url -v`
Expected: FAIL(当前不会拦截该 URL
**Step 3: Write minimal implementation**
```python
def validate_binary_signed_url_scope(*, url: str, user_id: UUID, thread_id: UUID) -> tuple[str, str]:
bucket, path = supabase_service.parse_signed_url(url)
# check host, bucket, path prefix agent-inputs/{user_id}/{thread_id}/uploads/
return bucket, path
```
`runs/resume` 请求入口调用校验;若请求含 binary 且当前模型不支持视觉,抛 `HTTPException(status_code=422, ...)`
**Step 4: Run test to verify it passes**
Run: `pytest backend/tests/unit/v1/agent/test_agent_router.py::test_runs_rejects_non_project_signed_url -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agentscope/schemas/agui_input.py backend/src/v1/agent/router.py backend/src/v1/agent/service.py backend/tests/unit/v1/agent/test_agent_router.py
git commit -m "fix: enforce signed image url scope on runs"
```
### Task 2: Runtime 多模态直传(移除文本化图片)
**Files:**
- Modify: `backend/src/core/agentscope/runtime/orchestrator.py`
- Modify: `backend/src/core/agentscope/prompts/agent_prompt.py`
- Test: `backend/tests/unit/core/agentscope/runtime/test_orchestrator.py`
**Step 1: Write the failing test**
```python
async def test_orchestrator_passes_image_url_block_to_runner() -> None:
command = build_run_input_with_binary("https://project.supabase.co/storage/v1/object/sign/...")
await orchestrator.run(..., command=command, ...)
assert fake_runner.user_input[1]["type"] == "image_url"
```
**Step 2: Run test to verify it fails**
Run: `pytest backend/tests/unit/core/agentscope/runtime/test_orchestrator.py::test_orchestrator_passes_image_url_block_to_runner -v`
Expected: FAIL(当前路径仍可能文本化)
**Step 3: Write minimal implementation**
```python
def _to_model_multimodal_blocks(content_blocks: list[dict[str, Any]]) -> list[dict[str, Any]]:
# text -> {type:"text", text:...}
# binary -> {type:"image_url", image_url:{url:...}}
```
将 runner 输入改为上述多模态块;禁止把图片块拼进普通字符串。
**Step 4: Run test to verify it passes**
Run: `pytest backend/tests/unit/core/agentscope/runtime/test_orchestrator.py::test_orchestrator_passes_image_url_block_to_runner -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agentscope/runtime/orchestrator.py backend/src/core/agentscope/prompts/agent_prompt.py backend/tests/unit/core/agentscope/runtime/test_orchestrator.py
git commit -m "feat: pass image blocks as multimodal payload to model"
```
### Task 3: Worker 结构化落库(content=answer
**Files:**
- Modify: `backend/src/core/agentscope/events/store.py`
- Modify: `backend/src/core/agentscope/runtime/orchestrator.py`
- Test: `backend/tests/unit/core/agentscope/events/test_store.py`
**Step 1: Write the failing test**
```python
async def test_text_message_end_persists_worker_output_and_answer_content() -> None:
event = build_text_end_event(worker_agent_output={"answer": "ok", ...})
await store.persist(event)
assert saved.content == "ok"
assert saved.metadata_json["worker_agent_output"]["answer"] == "ok"
```
**Step 2: Run test to verify it fails**
Run: `pytest backend/tests/unit/core/agentscope/events/test_store.py::test_text_message_end_persists_worker_output_and_answer_content -v`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
worker = WorkerAgentOutput.model_validate(event.get("workerAgentOutput") or {})
content = worker.answer
metadata["worker_agent_output"] = worker.model_dump(mode="json")
```
orchestrator 在 `text.end` 事件 data 写入 `workerAgentOutput`
**Step 4: Run test to verify it passes**
Run: `pytest backend/tests/unit/core/agentscope/events/test_store.py::test_text_message_end_persists_worker_output_and_answer_content -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agentscope/events/store.py backend/src/core/agentscope/runtime/orchestrator.py backend/tests/unit/core/agentscope/events/test_store.py
git commit -m "refactor: persist worker output schema with answer as message content"
```
### Task 4: Tool 结构化落库(content=result_summary)并删除旧摘要逻辑
**Files:**
- Modify: `backend/src/core/agentscope/events/store.py`
- Modify: `backend/src/core/agentscope/runtime/orchestrator.py`
- Delete: `backend/src/core/agentscope/events/tool_result_summary.py`
- Test: `backend/tests/unit/core/agentscope/events/test_store.py`
**Step 1: Write the failing test**
```python
async def test_tool_result_persists_tool_output_and_summary_content() -> None:
event = build_tool_result_event(tool_agent_output={"result_summary": "done", ...})
await store.persist(event)
assert saved.content == "done"
assert saved.metadata_json["tool_agent_output"]["result_summary"] == "done"
```
**Step 2: Run test to verify it fails**
Run: `pytest backend/tests/unit/core/agentscope/events/test_store.py::test_tool_result_persists_tool_output_and_summary_content -v`
Expected: FAIL
**Step 3: Write minimal implementation**
```python
tool = ToolAgentOutput.model_validate(event.get("toolAgentOutput") or {})
content = tool.result_summary
metadata["tool_agent_output"] = tool.model_dump(mode="json")
```
移除 `build_tool_content_summary` 相关 import/调用。
**Step 4: Run test to verify it passes**
Run: `pytest backend/tests/unit/core/agentscope/events/test_store.py::test_tool_result_persists_tool_output_and_summary_content -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/core/agentscope/events/store.py backend/src/core/agentscope/runtime/orchestrator.py backend/tests/unit/core/agentscope/events/test_store.py backend/src/core/agentscope/events/tool_result_summary.py
git commit -m "refactor: persist tool output schema and remove legacy summary builder"
```
### Task 5: Worker output 模型别名收敛(可选第二阶段)
**Files:**
- Modify: `backend/src/schemas/agent/runtime_models.py`
- Modify: `backend/src/schemas/messages/chat_message.py`
- Test: `backend/tests/unit/schemas/agent/test_runtime_models.py`
**Step 1: Write the failing test**
```python
def test_worker_output_lite_disallows_ui_hints() -> None:
with pytest.raises(ValidationError):
WorkerAgentOutputLite.model_validate({... , "ui_hints": {...}})
```
**Step 2: Run test to verify it fails**
Run: `pytest backend/tests/unit/schemas/agent/test_runtime_models.py::test_worker_output_lite_disallows_ui_hints -v`
Expected: 根据现状决定(若已 fail 则作为守护测试)
**Step 3: Write minimal implementation**
```python
WorkerAgentOutput = WorkerAgentOutputLite | WorkerAgentOutputRich
```
如不想扩大变更,可保留现状并仅补充注释说明由 `resolve_worker_output_model` 决定运行时约束。
**Step 4: Run test to verify it passes**
Run: `pytest backend/tests/unit/schemas/agent/test_runtime_models.py -v`
Expected: PASS
**Step 5: Commit**
```bash
git add backend/src/schemas/agent/runtime_models.py backend/src/schemas/messages/chat_message.py backend/tests/unit/schemas/agent/test_runtime_models.py
git commit -m "refactor: clarify worker output model contract for lite and rich modes"
```
### Task 6: 端到端回归与文档同步
**Files:**
- Modify: `docs/protocols/agent-chat-messages.md`
- Modify: `docs/runtime/runtime-route.md`
**Step 1: Run targeted backend tests**
Run: `pytest backend/tests/unit/v1/agent/test_agent_router.py backend/tests/unit/core/agentscope/runtime/test_orchestrator.py backend/tests/unit/core/agentscope/events/test_store.py -v`
Expected: PASS
**Step 2: Run lint/type checks**
Run: `cd backend && ruff check src tests && mypy src`
Expected: PASS
**Step 3: Update docs for new contracts**
- 明确 `runs` 的 URL 安全边界与 422 错误码。
- 明确 `worker_agent_output`/`tool_agent_output` 的落库契约及 `content` 映射规则。
**Step 4: Final verification**
Run: `pytest backend/tests -q`
Expected: PASS
**Step 5: Commit**
```bash
git add docs/protocols/agent-chat-messages.md docs/runtime/runtime-route.md
git commit -m "docs: align runs multimodal and structured persistence contracts"
```
@@ -1,87 +0,0 @@
# Agent Runs Multimodal 与落库重构设计
**目标**:让 `POST /agent/runs` 支持真实多模态直传到模型(非文本化),并将 worker/tool 结果按新 metadata 协议结构化落库。
**范围**:后端 `runs/resume` 请求校验、runtime 输入转换、事件落库、history 回放一致性。
---
## 1. 背景与问题
- 当前 `binary` 内容在运行链路中被当作普通 JSON 文本拼接进入 prompt,模型拿不到原生图像输入。
- tool 落库仍依赖旧摘要逻辑 `build_tool_content_summary`,与最新 `ToolAgentOutput` 元数据规范不一致。
- worker 落库当前只落文本内容,未确保 `WorkerAgentOutput` 结构化对象与 `content=answer` 的一致关系。
---
## 2. 设计原则
- 协议单一信源:严格遵循 `docs/protocols/agent-chat-messages.md`,只接受 `binary` 形态,不兼容旧形态。
- 最小安全边界:仅允许本项目 Supabase 私有桶签名 URL,拒绝任意外部 URL。
- 事件驱动持久化:以 event store 作为唯一落库入口,避免双轨逻辑。
- 数据可回放:history 始终可按 metadata 重新签名并回填 user 附件。
---
## 3. 目标数据流
1. `runs` 入参校验通过后,user message 入库(附件仅存 bucket/path/mime)。
2. runtime 执行时,将 `binary` 转为模型多模态 `image_url` content block 直传。
3. orchestrator 产出结构化事件:
- worker 主响应通过 `TEXT_MESSAGE_*` 事件发送,`TEXT_MESSAGE_END` 携带 `workerAgentOutput`
- tool 执行结果通过 `TOOL_CALL_RESULT` 事件发送,携带 `toolAgentOutput`
4. event store 统一校验并落库:
- worker`content = answer`metadata 写 `worker_agent_output`
- tool`content = result_summary`metadata 写 `tool_agent_output`
5. history 读取 user metadata 重新签名 URL,返回 `binary` block 给前端。
---
## 4. 安全与错误策略
### 4.1 URL 安全边界
- `binary.url` 必须满足:
- host 为当前 Supabase 项目域名。
- path 为 `/storage/v1/object/sign/{bucket}/{path}`
- `{bucket}` 等于 `config.storage.bucket`
- `{path}` 前缀匹配 `agent-inputs/{user_id}/{thread_id}/uploads/`
### 4.2 运行失败
- 保持 AG-UI 生命周期完整:`RUN_STARTED` 后只能 `RUN_FINISHED``RUN_ERROR` 结束。
- 运行错误时不落半结构化消息,避免脏元数据。
---
## 5. 落库契约
### 5.1 Worker
- 入库角色:`assistant`
- `messages.content = worker_agent_output.answer`
- `messages.metadata.worker_agent_output = WorkerAgentOutput`(完整、schema 校验后)
### 5.2 Tool
- 入库角色:`tool`
- `messages.content = tool_agent_output.result_summary`
- `messages.metadata.tool_agent_output = ToolAgentOutput`(完整、schema 校验后)
- 删除旧摘要逻辑:`build_tool_content_summary`
---
## 6. 兼容性策略
- 不兼容旧输入块形态(如 `image_url` 作为 runs 输入)。
- 历史接口输出协议保持不变,前端无需修改消费协议。
- 原有 user 附件回放路径保留,只强化入站 URL 校验。
---
## 7. 验收标准
- runs 包含合法 `binary` 时,模型收到多模态消息(非文本化 JSON)。
- 非本项目签名 URL 返回 `422`
- worker/tool 落库满足 `content` 与结构化 metadata 一一对应。
- history 仍能正确回放 user 附件(临时签名 URL)。
@@ -1,318 +0,0 @@
# Auth Pages And Feedback System Redesign
## Goal
Redesign the mobile authentication experience so the login, register, and reset-password flows feel like a polished assistant product instead of a flat form flow, while preserving existing business logic, routing, validation, and feedback behavior semantics.
## Scope
- Rebuild the UI for `login`, `register`, and `reset-password`
- Preserve existing auth logic, cubits, navigation, and API calls
- Redesign the fixed-length code input experience for verification code and invite code
- Redesign the feedback system into two coordinated layers:
- global floating toast messages
- in-component contextual messages
- Keep all work inside the existing Flutter design system and shared widget architecture
## Constraints
- Must follow `apps/AGENTS.md`
- Must follow `apps/rules/visual_design_language.md`
- Must use tokens from `apps/lib/core/theme/design_tokens.dart`
- Must not introduce a parallel feedback system outside the approved Toast and inline message architecture
- Must not change auth protocols, routing semantics, or submission logic
## Current Problems
### Visual Problems
- The screens read as flat white pages with blue buttons, which matches a prohibited anti-pattern in the visual design language.
- The main CTA button uses color as its only emphasis mechanism, so it feels plastic and low-fidelity.
- The three auth screens share only superficial consistency, not a strong surface system.
- The reset-password screen presents all steps at once, causing poor rhythm and weak hierarchy.
- The verification code layout feels cramped and improvised because the code cells and send button compete on the same row.
### Feedback Problems
- Global toast messages are visually generic and lack product identity.
- Component-level messaging is under-specified and inconsistently expressed.
- Result messages and contextual validation messages are not clearly separated in responsibility.
## Design Direction
The approved direction is a floating-card auth system with a soft blue atmospheric background, a clear brand anchor, and a calm layered surface hierarchy.
The target feeling is:
- premium
- calm
- trustworthy
- assistant-oriented
- softly tactile
- mobile-native
The redesign should feel like a cohesive product surface, not a stack of form containers.
## Surface Model
Each auth screen uses the same four-layer structure.
### 1. Background Surface
The screen background is not a plain blank fill. It should feel like a soft spatial field with subtle blue-gray atmosphere. This establishes the assistant mood before the user interacts with any form element.
### 2. Brand Anchor Surface
The top area contains the logo and brand title. On login and register, this remains the visual identity anchor. On reset password, the title becomes more task-driven, but the page still inherits the same spatial language and product mood.
### 3. Primary Floating Card
The form lives in a single, elevated primary card with softened corners, restrained shadows, and calm separation from the background. This card should feel intentional and product-grade rather than like a white panel.
### 4. Secondary Assistive Layer
Links, helper text, step hints, resend actions, and supplemental explanations belong to a lower-emphasis support layer. These elements should feel connected to the primary card without visually competing with it.
## Shared Auth Composition
All three auth screens should use a unified composition pattern.
- Top brand or task heading area
- Main floating card for the active task
- Internal grouped sections inside the card
- Lightweight transition area for secondary actions
This creates cross-screen consistency while allowing each flow to have different emphasis.
## Screen Designs
### Login Screen
The login screen should be the most restrained and focused of the three.
Structure:
- logo and brand title at top
- one compact floating form card
- email field
- password field
- primary login CTA
- low-emphasis forgot-password action inside the card
- lightweight bottom switch action to register
Intent:
- reduce visual noise
- make the login action feel confident and central
- keep account switching and recovery available without stealing focus
### Register Screen
The register screen should feel richer than login, but still composed.
Structure:
- same brand anchor as login
- taller primary card
- grouped section for core account information
- grouped section for invite code as an optional enhancement
- subtle progress indicator treated as part of the card rhythm, not as a crude progress bar
- primary CTA for moving to verification
- lightweight switch action to login
Invite code treatment:
- remains a fixed-length segmented input
- visually grouped as an optional section, not mixed into the main required inputs
- supported by a nearby low-emphasis explanation block
### Reset Password Screen
The reset-password screen should shift from a flat all-fields form into a guided two-stage flow.
Structure:
- task heading at top
- primary floating card
- stage one: email input plus send-code action
- after successful code send, reveal stage two inside the same card
- stage two: segmented verification code input, resend action, new password, confirm password, submit CTA
- lightweight return-to-login switch action below
Intent:
- create procedural clarity
- avoid forcing all fields onto the screen at once
- make the verification code area feel deliberate and product-grade
## Fixed-Length Segmented Input Design
The user explicitly prefers fixed-length segmented input for both verification code and invite code. This input pattern will be preserved.
However, it will be redesigned to feel like a premium grouped input rather than a row of raw boxes.
Design principles:
- the individual cells must read as one continuous input group
- current focus should be visible at both cell level and group level
- spacing should be balanced and calm
- filled state should feel confident and readable
- disabled and error states should be obvious without becoming harsh
Required states:
- default
- focused group
- active cell
- filled
- error
- disabled
Usage rules:
- verification code remains segmented
- invite code remains segmented
- resend action is no longer visually fused to the segmented input row
## Button Design
The CTA button must stop reading as a flat blue block.
New button intent:
- deeper and calmer brand blue
- stronger tactile weight
- premium capsule shape
- light depth through tonal layering and restrained shadow
- pressed and disabled states that clearly change material weight
Button hierarchy:
- primary button: for submission and key forward progress
- secondary button: for bounded alternative actions
- text-link action: for lightweight transitions and low-risk actions
Color strategy:
- blue remains the anchor, but is used with restraint
- the strongest emphasis goes to the main CTA only
- supporting actions should not look equally loud
## Input Field Design
Text fields should move from generic bordered rectangles to soft embedded surfaces.
Desired qualities:
- calmer default appearance
- stronger focus clarity
- reduced raw border noise
- consistent radius and spacing rhythm
- visually integrated label, input, and helper message states
## Feedback System Redesign
The feedback system is split into two coordinated layers.
### 1. Global Floating Toast
Global toast is a lightweight floating feedback card presented in the safe area near the top of the screen.
Use global toast for:
- cross-component success feedback
- async result notifications
- cross-step flow results
- errors that are not tied to a single visible field
Do not use global toast for:
- simple inline validation
- field-level format guidance
- contextual explanations that belong near the active input area
Visual direction:
- floating product card, not a system notification strip
- soft surface and rounded shape
- restrained state tinting
- icon plus title/message hierarchy if needed
- gentle slide/fade motion
### 2. In-Component Contextual Message
Contextual messages live inside the component or form group they explain.
Use contextual messages for:
- validation near fields
- warnings tied to current form state
- helper guidance for invite code or verification flow
- inline explanation of what the user should fix next
Visual direction:
- integrated into the card hierarchy
- lighter than a toast
- close to the related field or group
- consistent state styling across info, warning, and error
### Responsibility Boundary
- global toast = result-oriented, temporary, cross-context feedback
- inline message = contextual, explanatory, local feedback
This separation prevents toast overuse and makes forms feel calmer.
## Motion Language
Motion should be soft and minimal.
Use motion for:
- toast entrance and dismissal
- reset-password stage reveal
- button press feedback
- segmented input focus continuity
Avoid:
- springy or playful motion
- overlapping dramatic transitions
- flashy state animations
## Shared Component Impact
The redesign likely requires updates or additions to shared widgets rather than one-off page styling.
Expected shared component work:
- refine `AppButton`
- refine or replace current toast visuals
- refine `AppBanner` or introduce a shared inline message presentation built on the same semantics
- redesign `FixedLengthCodeInput`
- add a reusable auth surface wrapper if needed
## Verification Strategy
Because this work is UI-heavy, verification should focus on correctness, consistency, and safe reuse.
Primary verification targets:
- `flutter analyze`
- impacted auth tests
- targeted widget tests only where reusable interactive widgets become materially more complex
- manual visual review of the three auth screens and feedback states
## Success Criteria
The redesign is successful when all of the following are true:
- the three auth screens feel like one coherent product system
- the UI no longer resembles a plain white form page with blue buttons
- the main CTA has better perceived material quality
- reset password has a clearer and more attractive two-stage flow
- segmented code input remains intact but feels premium
- global toasts and inline messages have clear responsibility boundaries
- the feedback system feels native to the product rather than bolted on
- the result plausibly matches the visual language standard for a polished assistant app
@@ -1,337 +0,0 @@
# Auth Pages Redesign Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Redesign the Flutter auth pages and feedback system so login, register, reset-password, segmented code input, and toast/inline feedback all match the project visual design language while preserving existing logic.
**Architecture:** Keep auth business logic, cubits, navigation, and repository behavior unchanged. Move the redesign into shared design tokens and shared widgets first, then rebuild each auth screen on top of those primitives so the result is reusable and consistent.
**Tech Stack:** Flutter, flutter_bloc, go_router, existing design tokens and shared widgets under `apps/lib/`
---
### Task 1: Audit reusable auth and feedback primitives
**Files:**
- Modify: `apps/lib/core/theme/design_tokens.dart`
- Modify: `apps/lib/shared/widgets/app_button.dart`
- Modify: `apps/lib/shared/widgets/fixed_length_code_input.dart`
- Modify: `apps/lib/shared/widgets/toast/toast.dart`
- Modify: `apps/lib/shared/widgets/banner/app_banner.dart`
- Test: `apps/test/` relevant existing widget or feature tests if impacted
**Step 1: Identify missing token semantics before UI changes**
Review current colors, spacing, radius, and surface semantics. List any missing token roles needed for:
- atmospheric auth background
- floating card border/shadow layering
- premium CTA surface states
- segmented input states
- toast and inline message states
**Step 2: Add only the minimal new tokens needed**
Update `apps/lib/core/theme/design_tokens.dart` with shared semantic tokens rather than page-local constants.
Expected areas:
- auth background surface tones
- auth card border/highlight tones
- stronger button tonal roles
- inline message backgrounds/borders/text roles
- toast surface roles if existing status tokens are insufficient
**Step 3: Run a quick compile-oriented check mentally against all planned components**
Confirm the new tokens are generic enough for shared reuse and not named after individual screens.
**Step 4: Commit checkpoint note**
Do not create a git commit unless explicitly requested by the user.
### Task 2: Redesign shared CTA and link interaction surfaces
**Files:**
- Modify: `apps/lib/shared/widgets/app_button.dart`
- Modify: `apps/lib/shared/widgets/link_button.dart`
- Test: existing auth tests if button usage affects behavior
**Step 1: Write the failing test if widget behavior changes materially**
If the redesign introduces new behavior beyond styling, add a targeted widget test. If changes remain visual-only, document that no new test is added per lightweight UI testing policy.
**Step 2: Refactor `AppButton` into a stronger material hierarchy**
Implement:
- calmer premium CTA surface
- better disabled state separation
- consistent capsule-like shape
- optional secondary/outlined appearance if already used
Keep public API stable unless a small safe extension is clearly needed.
**Step 3: Refine `LinkButton` hit area and visual tone**
Implement:
- clearer touch target
- lighter emphasis than CTA
- better pressed and disabled feel
**Step 4: Run impacted checks**
Run: `flutter analyze`
Expected: no new analyzer issues from button or link widget changes.
### Task 3: Redesign segmented input as a premium grouped control
**Files:**
- Modify: `apps/lib/shared/widgets/fixed_length_code_input.dart`
- Test: add or update a widget test only if interaction logic changes materially
**Step 1: Write the failing test for any changed interaction behavior**
If focus progression, formatting, or semantic behavior changes, add a widget test that captures the intended interaction. If only visuals change, document why no new test is added.
**Step 2: Rebuild the visual structure of the segmented input**
Implement:
- clearer grouped container feel
- balanced cell rhythm
- group-level focus cue plus active-cell cue
- stronger filled state
- polished error and disabled states
**Step 3: Preserve existing logical behavior**
Keep:
- fixed length handling
- allowed character filtering
- uppercase support
- autofill compatibility
**Step 4: Run focused verification**
Run: `flutter analyze`
Expected: no new issues from segmented input refactor.
### Task 4: Rebuild global toast and inline message visuals
**Files:**
- Modify: `apps/lib/shared/widgets/toast/toast.dart`
- Modify: `apps/lib/shared/widgets/banner/app_banner.dart`
- Modify: `apps/lib/shared/widgets/toast/toast_type.dart` if needed
- Modify: `apps/lib/shared/widgets/toast/toast_type_config.dart`
- Test: add widget tests only if feedback behavior semantics change
**Step 1: Inspect current toast config implementation**
Review `toast_type_config.dart` and align the redesign with shared semantic tokens.
**Step 2: Redesign global toast as a floating product card**
Implement:
- safe-area aware floating card
- refined tint, border, icon, and text hierarchy
- restrained shadow and motion
- stable dismissal behavior
**Step 3: Redesign inline message presentation**
Implement:
- lighter component-level message styling
- consistent relationship to current form group
- clear differentiation from toast while sharing status semantics
**Step 4: Preserve system rules**
Keep:
- `Toast.show(...)` for transient global feedback
- `AppBanner` for persistent inline feedback
- no raw `ScaffoldMessenger`
**Step 5: Run focused verification**
Run: `flutter analyze`
Expected: no analyzer issues.
### Task 5: Add a reusable auth surface composition primitive
**Files:**
- Modify: `apps/lib/features/auth/ui/widgets/auth_page_scaffold.dart`
- Create or Modify: `apps/lib/features/auth/ui/widgets/` shared auth surface widgets as needed
**Step 1: Evaluate whether the existing scaffold can express the new surface hierarchy**
Check whether `AuthPageScaffold` can support:
- atmospheric background treatment
- safe-area balanced centering
- top brand anchor spacing
- floating card composition
**Step 2: Implement the minimal reusable auth layout primitives**
Possible additions:
- auth hero header
- auth floating card shell
- grouped section wrapper
Only create what is reused by at least two auth screens.
**Step 3: Keep layout semantics explicit**
Ensure every `Row` and `Column` has explicit `crossAxisAlignment` and layout intent remains traceable.
### Task 6: Rebuild the login page UI on top of the shared primitives
**Files:**
- Modify: `apps/lib/features/auth/ui/screens/login_screen.dart`
**Step 1: Preserve all existing logic paths**
Do not change:
- cubit usage
- submit flow
- auth bloc event dispatch
- navigation targets
**Step 2: Replace the existing layout with the new design**
Implement:
- brand anchor header
- compact floating login card
- refined email and password groups
- CTA-first hierarchy
- lightweight forgot-password action
- low-emphasis register switch
**Step 3: Reconnect inline feedback to the new grouping**
Place validation and error banners where they support the form rhythm rather than interrupt it.
**Step 4: Run impacted checks**
Run: `flutter analyze`
Expected: login screen compiles cleanly.
### Task 7: Rebuild the register page UI with grouped invite code treatment
**Files:**
- Modify: `apps/lib/features/auth/ui/screens/register_screen.dart`
**Step 1: Preserve all existing logic paths**
Do not change:
- cubit usage
- invite code normalization
- validation behavior
- route transition to verification page
- silent code send behavior
**Step 2: Rebuild required and optional sections**
Implement:
- shared brand anchor header
- taller floating card
- grouped account info section
- separate optional invite code section
- refined progress indicator treatment
- improved footer switch action
**Step 3: Keep segmented invite code input**
Use the redesigned shared segmented input without changing its fixed-length semantics.
**Step 4: Run impacted checks**
Run: `flutter analyze`
Expected: register screen compiles cleanly.
### Task 8: Rebuild the reset-password page as a guided two-stage flow
**Files:**
- Modify: `apps/lib/features/auth/ui/screens/reset_password_screen.dart`
**Step 1: Preserve all existing logic and feedback semantics**
Do not change:
- cubit interactions
- send code and resend behavior
- submit behavior
- success redirect
- toast semantics
**Step 2: Recompose the screen into staged groups**
Implement:
- task-focused header
- stage one email + send code group
- stage two reveal for code and password reset controls after send success
- separate resend action from the code row while preserving usability
**Step 3: Reconnect segmented verification code input and inline messaging**
Make the code group feel premium and central without overcrowding the card.
**Step 4: Run impacted checks**
Run: `flutter analyze`
Expected: reset-password screen compiles cleanly.
### Task 9: Run verification and inspect affected tests
**Files:**
- Test: impacted auth tests and any new widget tests added
**Step 1: Run analyzer**
Run: `flutter analyze`
Expected: PASS with no new issues.
**Step 2: Run impacted auth tests**
Run an auth-focused test subset appropriate to the changed files, for example:
```bash
flutter test apps/test/features/auth
```
Adjust the exact command to the repository's Flutter test layout if needed.
Expected: existing auth tests remain green; any new targeted widget tests pass.
**Step 3: Manual visual review checklist**
Verify:
- login looks calm and premium
- register invite code section feels optional but intentional
- reset-password stage flow is clear
- segmented code input feels grouped and polished
- toast feels like a product surface
- inline messages feel local and non-intrusive
### Task 10: Final review and handoff
**Files:**
- Modify: any touched files from previous tasks if final polish is needed
**Step 1: Check for consistency drift**
Ensure the three auth pages, toast, inline messages, buttons, and segmented input all feel like one system.
**Step 2: Confirm no scope creep changed logic unexpectedly**
Re-check that routing, auth behavior, and validation rules remain intact.
**Step 3: Prepare concise handoff summary**
Include:
- files changed
- verification commands run
- test results
- any follow-up visual refinements still worth considering
@@ -1,484 +0,0 @@
# Home Screen Visual Refresh Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Rebuild the post-login home screen into a calm, premium assistant homepage with a layered background field, a clearer conversation stage, and a floating input island that carries text, voice, and attachment states without relying on helper copy.
**Architecture:** Keep the existing chat data flow and AG-UI behavior unchanged. Refactor the screen into a surface-based composition: background field at the bottom, content layer for header and conversation stage in the middle, and a floating input layer on top. Move visual semantics into shared design tokens first, then rebuild `MessageComposer` and `HomeScreen` around those tokens.
**Tech Stack:** Flutter, Material, flutter_bloc, existing design token system, existing widget tests in `apps/test/features/home/ui/widgets/`.
---
### Task 1: Add home surface tokens
**Files:**
- Modify: `apps/lib/core/theme/design_tokens.dart`
- Reference: `apps/rules/visual_design_language.md`
**Step 1: Write the failing test**
Add a widget test assertion that depends on a new home token being used by `MessageComposer`, for example:
```dart
testWidgets('composer uses refreshed home surface token', (tester) async {
await tester.pumpWidget(_buildTestApp(
mode: MessageComposerMode.text,
process: MessageComposerProcess.idle,
hasMessage: false,
isWaitingAgent: false,
));
final container = tester.widget<Container>(
find.byKey(messageComposerContainerKey),
);
final decoration = container.decoration! as BoxDecoration;
expect(decoration.color, AppColors.homeComposerShell);
});
```
**Step 2: Run test to verify it fails**
Run: `flutter test apps/test/features/home/ui/widgets/home_composer_test.dart`
Expected: FAIL because `homeComposerShell` does not exist yet.
**Step 3: Write minimal implementation**
Add the first batch of home-specific tokens in `apps/lib/core/theme/design_tokens.dart`, keeping names semantic instead of layout-specific:
```dart
static const homeBackgroundTop = Color(0xFFF5F9FF);
static const homeBackgroundBottom = Color(0xFFF7FAFE);
static const homeBackgroundGlow = Color(0xFFDCEBFF);
static const homeBackgroundGlowSoft = Color(0xFFF1F6FF);
static const homeToolbarSurface = Color(0xF2FFFFFF);
static const homeToolbarBorder = Color(0xD9E6F7);
static const homeConversationSurface = Color(0xBFFFFFFF);
static const homeConversationBorder = Color(0xDDE8F6);
static const homeComposerShell = Color(0xFDFCFEFF);
static const homeComposerInner = Color(0xFFF7FAFE);
static const homeComposerBorder = Color(0xD7E3F3);
static const homeComposerAccent = Color(0xFFEAF3FF);
static const homeAttachmentSurface = Color(0xFFF3F7FD);
```
**Step 4: Run test to verify it passes**
Run: `flutter test apps/test/features/home/ui/widgets/home_composer_test.dart`
Expected: PASS, with the new token available to downstream widgets.
**Step 5: Commit**
```bash
git add apps/lib/core/theme/design_tokens.dart apps/test/features/home/ui/widgets/home_composer_test.dart
git commit -m "feat: add home screen surface tokens"
```
### Task 2: Extract background field and floating header primitives
**Files:**
- Create: `apps/lib/features/home/ui/widgets/home_background_field.dart`
- Create: `apps/lib/features/home/ui/widgets/home_floating_header.dart`
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
**Step 1: Write the failing test**
Create a focused widget test for the new background field composition:
```dart
testWidgets('home background field renders layered glow surfaces', (tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(body: HomeBackgroundField()),
));
expect(find.byKey(homeBackgroundFieldKey), findsOneWidget);
expect(find.byKey(homeTopGlowKey), findsOneWidget);
expect(find.byKey(homeBottomGlowKey), findsOneWidget);
});
```
**Step 2: Run test to verify it fails**
Run: `flutter test apps/test/features/home/ui/widgets/home_background_field_test.dart`
Expected: FAIL because the widget file and keys do not exist.
**Step 3: Write minimal implementation**
Build a reusable background widget using only tokens and soft gradients:
```dart
class HomeBackgroundField extends StatelessWidget {
const HomeBackgroundField({super.key});
@override
Widget build(BuildContext context) {
return DecoratedBox(
key: homeBackgroundFieldKey,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppColors.homeBackgroundTop,
AppColors.homeBackgroundBottom,
],
),
),
child: Stack(
children: const [
_TopGlow(),
_BottomGlow(),
],
),
);
}
}
```
Add a matching lightweight floating header widget so `HomeScreen` stops inlining all visual treatment in one file.
**Step 4: Run test to verify it passes**
Run: `flutter test apps/test/features/home/ui/widgets/home_background_field_test.dart`
Expected: PASS.
**Step 5: Commit**
```bash
git add apps/lib/features/home/ui/widgets/home_background_field.dart apps/lib/features/home/ui/widgets/home_floating_header.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_background_field_test.dart
git commit -m "feat: add layered home screen background primitives"
```
### Task 3: Rebuild `MessageComposer` as a floating input island
**Files:**
- Modify: `apps/lib/shared/widgets/message_composer.dart`
- Modify: `apps/test/features/home/ui/widgets/home_composer_test.dart`
**Step 1: Write the failing test**
Extend the existing composer tests to enforce the new structure:
```dart
testWidgets('composer exposes shell and inner surface', (tester) async {
await tester.pumpWidget(_buildTestApp(
mode: MessageComposerMode.text,
process: MessageComposerProcess.idle,
hasMessage: false,
isWaitingAgent: false,
));
expect(find.byKey(messageComposerShellKey), findsOneWidget);
expect(find.byKey(messageComposerInnerKey), findsOneWidget);
});
```
Add another test to ensure the hold-to-speak state stays within the same shell:
```dart
testWidgets('recording state keeps unified floating shell', (tester) async {
await tester.pumpWidget(_buildTestApp(
mode: MessageComposerMode.holdToSpeak,
process: MessageComposerProcess.recording,
hasMessage: false,
isWaitingAgent: false,
));
expect(find.byKey(messageComposerShellKey), findsOneWidget);
expect(find.text('松开发送'), findsOneWidget);
});
```
**Step 2: Run test to verify it fails**
Run: `flutter test apps/test/features/home/ui/widgets/home_composer_test.dart`
Expected: FAIL because the new structure keys do not exist.
**Step 3: Write minimal implementation**
Refactor `MessageComposer` from a single decorated `Container` into a layered shell:
```dart
return Container(
key: messageComposerShellKey,
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: AppColors.homeComposerShell,
borderRadius: BorderRadius.circular(AppRadius.xxl),
border: Border.all(color: AppColors.homeComposerBorder),
boxShadow: const [
BoxShadow(...),
],
),
child: Container(
key: messageComposerInnerKey,
decoration: BoxDecoration(
color: AppColors.homeComposerInner,
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Row(...),
),
);
```
Keep all current callbacks and AG-UI-adjacent behavior intact. Do not change event names or chat flow.
**Step 4: Run test to verify it passes**
Run: `flutter test apps/test/features/home/ui/widgets/home_composer_test.dart`
Expected: PASS, including the existing callback and state-priority assertions.
**Step 5: Commit**
```bash
git add apps/lib/shared/widgets/message_composer.dart apps/test/features/home/ui/widgets/home_composer_test.dart
git commit -m "feat: rebuild composer as floating input island"
```
### Task 4: Integrate attachment strip into the input island stack
**Files:**
- Create: `apps/lib/features/home/ui/widgets/home_attachment_strip.dart`
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
**Step 1: Write the failing test**
Add a widget test that verifies selected images render in a unified strip above the composer shell:
```dart
testWidgets('selected images render in attachment strip above composer', (tester) async {
// Pump HomeScreen with seeded selected images via a test-only constructor hook.
expect(find.byKey(homeAttachmentStripKey), findsOneWidget);
});
```
**Step 2: Run test to verify it fails**
Run: `flutter test apps/test/features/home/ui/widgets/home_screen_input_stack_test.dart`
Expected: FAIL because the strip widget and key do not exist.
**Step 3: Write minimal implementation**
Create a dedicated strip widget that uses the same home surface language:
```dart
class HomeAttachmentStrip extends StatelessWidget {
const HomeAttachmentStrip({
super.key,
required this.images,
required this.onRemove,
});
final List<XFile> images;
final ValueChanged<int> onRemove;
@override
Widget build(BuildContext context) {
if (images.isEmpty) return const SizedBox.shrink();
return Container(
key: homeAttachmentStripKey,
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: AppColors.homeAttachmentSurface,
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Wrap(...),
);
}
}
```
Mount it in the same bottom stack as the composer, not in the main scroll column.
**Step 4: Run test to verify it passes**
Run: `flutter test apps/test/features/home/ui/widgets/home_screen_input_stack_test.dart`
Expected: PASS.
**Step 5: Commit**
```bash
git add apps/lib/features/home/ui/widgets/home_attachment_strip.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_screen_input_stack_test.dart
git commit -m "feat: unify home attachments with composer stack"
```
### Task 5: Recompose `HomeScreen` around stage + floating bottom stack
**Files:**
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
- Reference: `apps/lib/features/home/ui/screens/home_sheet.dart`
**Step 1: Write the failing test**
Add a focused home screen layout test:
```dart
testWidgets('home screen shows floating header, conversation stage, and bottom input stack', (tester) async {
await tester.pumpWidget(buildHomeScreenForTest());
expect(find.byKey(homeFloatingHeaderKey), findsOneWidget);
expect(find.byKey(homeConversationStageKey), findsOneWidget);
expect(find.byKey(homeBottomInputStackKey), findsOneWidget);
});
```
**Step 2: Run test to verify it fails**
Run: `flutter test apps/test/features/home/ui/widgets/home_screen_layout_test.dart`
Expected: FAIL because the layout keys are not present.
**Step 3: Write minimal implementation**
Refactor the page into a single `Stack` with explicit layers:
```dart
return Scaffold(
body: SafeArea(
child: Stack(
children: [
const Positioned.fill(child: HomeBackgroundField()),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const HomeFloatingHeader(),
Expanded(child: _buildConversationStage(context, state)),
],
),
_buildBottomInputStack(context, state),
if (_isRecording) _buildRecordingGestureOverlay(),
],
),
),
);
```
Inside `_buildConversationStage`, keep existing history/message rendering logic but place it inside a calmer stage container with stable bottom padding so the floating composer never overlaps message content.
**Step 4: Run test to verify it passes**
Run: `flutter test apps/test/features/home/ui/widgets/home_screen_layout_test.dart`
Expected: PASS.
**Step 5: Commit**
```bash
git add apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_screen_layout_test.dart
git commit -m "feat: recompose home screen into layered assistant stage"
```
### Task 6: Tune empty-state and waiting-state presentation without adding helper copy
**Files:**
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
**Step 1: Write the failing test**
Add a test that verifies the empty state uses a dedicated stage surface instead of only centered text:
```dart
testWidgets('empty state renders stage surface without relying on helper copy', (tester) async {
await tester.pumpWidget(buildEmptyHomeScreenForTest());
expect(find.byKey(homeConversationStageKey), findsOneWidget);
expect(find.byKey(homeEmptyStateOrbKey), findsOneWidget);
});
```
**Step 2: Run test to verify it fails**
Run: `flutter test apps/test/features/home/ui/widgets/home_screen_layout_test.dart`
Expected: FAIL because the empty-state focal surface does not exist.
**Step 3: Write minimal implementation**
Replace the current `Center(Text('开始对话吧'))` style empty state with a focal surface that uses shape, spacing, and a soft orb layer instead of explanatory copy:
```dart
Widget _buildEmptyConversationStage() {
return Center(
child: Container(
key: homeEmptyStateOrbKey,
width: 220,
height: 220,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(...),
),
),
);
}
```
Waiting state should sit at the lower edge of the stage, visually connected to the composer instead of appearing as a detached loading row.
**Step 4: Run test to verify it passes**
Run: `flutter test apps/test/features/home/ui/widgets/home_screen_layout_test.dart`
Expected: PASS.
**Step 5: Commit**
```bash
git add apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_screen_layout_test.dart
git commit -m "feat: refine home empty and waiting states"
```
### Task 7: Verify visual refresh and document manual QA
**Files:**
- Modify: `docs/plans/2026-03-13-home-screen-visual-refresh.md`
**Step 1: Run automated verification**
Run:
```bash
flutter test apps/test/features/home/ui/widgets/home_composer_test.dart apps/test/features/home/ui/widgets/home_background_field_test.dart apps/test/features/home/ui/widgets/home_screen_layout_test.dart apps/test/features/home/ui/widgets/home_screen_input_stack_test.dart
```
Expected: PASS.
**Step 2: Run static verification**
Run:
```bash
dart format apps/lib/core/theme/design_tokens.dart apps/lib/features/home/ui/screens/home_screen.dart apps/lib/features/home/ui/widgets/home_background_field.dart apps/lib/features/home/ui/widgets/home_floating_header.dart apps/lib/features/home/ui/widgets/home_attachment_strip.dart apps/lib/shared/widgets/message_composer.dart apps/test/features/home/ui/widgets/home_composer_test.dart apps/test/features/home/ui/widgets/home_background_field_test.dart apps/test/features/home/ui/widgets/home_screen_layout_test.dart apps/test/features/home/ui/widgets/home_screen_input_stack_test.dart
flutter analyze
```
Expected: PASS.
**Step 3: Run manual QA**
Verify on a phone-sized simulator or device:
```text
1. 首页空态 first impression 不依赖提示文案,仍然有清晰主场感
2. 顶部浮层不抢焦点,底部输入岛是最稳定视觉锚点
3. 文本/语音/转写/等待态切换时,输入岛壳体保持连续
4. 附件预览与输入区属于同一层级,不再像临时插块
5. 消息列表滚动到底部时,不会被悬浮输入岛遮挡
```
**Step 4: Update plan status note**
Append a short verification note to this plan with pass/fail status and any follow-up token work.
**Step 5: Commit**
```bash
git add docs/plans/2026-03-13-home-screen-visual-refresh.md
git commit -m "docs: record home screen visual refresh verification"
```