docs(spec): add long-task phase live/resume UX P2 spec and plan

This commit is contained in:
WeiCHEN
2026-09-13 02:50:20 +00:00
committed by weichen
parent a6864fe7b4
commit c83f53a4fa
2 changed files with 422 additions and 0 deletions
@@ -0,0 +1,240 @@
# Long-Task Phase Live / Resume-Restart UX Implementation Plan (P2)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Wire the Web workbench to the existing `/api/v1/tasks/run/stream` SSE for real long tasks: live `PhaseEventStrip` driven by real `phase` events, typed recovery (`resumable` → 继续 / `fatal` → 重新开始) on failed task bubbles, and a session-level transient-fault integration test (test-report follow-up #2).
**Architecture:** Extend `internal/server/sse.go` `handleRunTaskStream` with `X-Request-Id`, an embedded `taskStreamRequest` (adds `work_dir` / `add_steps` without touching `TaskRunRequestDTO` in the WIP-tainted `handlers.go`), a resume branch calling `ResumeOrModifyWorkspace`, and workspace-aware error classification. Add `DonePayload.WorkspaceID` + `EmitDoneWithWorkspace`. On the web side add `taskStream.ts` (SSE client), `phaseEventMapper.ts` (phase → PhaseEventStrip mapping, icon table aligned with CLI), and extend `App.tsx` with `runTaskAttempt` (same state machine as P1 chat) plus `continue`/`restart` in `handleErrorAction`.
**Tech Stack:** Go 1.22+ (`net/http` SSE), React 19 + TypeScript + Vite, `node:test` for web unit tests, real `pi-agent` + MiniMax for E2E acceptance.
**Spec:** `docs/superpowers/specs/2026-09-09-longtask-phase-live-resume-ux-design.md`
## Global Constraints
- **WIP isolation is mandatory**: every change lands in a clean or newly created file (`sse.go`, `internal/stream/*`, `web/src/App.tsx`, `web/src/components/chat/MessageBubble.tsx`, `web/src/types/workbench.ts`, new files). Never `git add -A` / `git add .`; always stage explicit file lists. `internal/server/handlers.go`, `internal/server/server.go`, `internal/bizdev/service.go`, `internal/coordinator/*`, `web/src/data/storage.ts` and the other ~70 uncommitted WIP files must stay untouched.
- Progress must be truthful: the strip is driven only by real `phase` events; no fake progress spinners.
- Frontend renders only `error_info.actions` (P1 fallback mapping stays); auto-retry remains transient-only (P1 behavior unchanged).
- Long-task attempts use the same bubble state machine as P1: `pending → streaming_status → completed / failed / cancelled`.
- Real-Agent E2E evidence goes to `.doc/test-reports/2026-09-09-longtask-phase-live-resume-ux-test-report.md` (no stub-only acceptance).
- Progressive commits: `feat(stream)`, `feat(api)`, `test(server)`, `feat(web)`, `fix(web)`, `docs(test)`.
## File Structure
| File | Responsibility | WIP-safe |
| :--- | :--- | :--- |
| `internal/stream/types.go` | `DonePayload.WorkspaceID` | clean |
| `internal/stream/stream.go` | `EmitDoneWithWorkspace` | clean |
| `internal/stream/stream_test.go` | Done payload tests | clean |
| `internal/server/sse.go` | request-id, `taskStreamRequest`, resume branch, classified errors | clean |
| `internal/server/taskstream_test.go` (new) | resume / classification / transient-injection integration tests | new |
| `web/src/api/taskStream.ts` (new) | task SSE client + request builders | new |
| `web/src/api/taskStream.test.ts` (new) | client unit tests | new |
| `web/src/engine/phaseEventMapper.ts` (new) | phase → PhaseEvent[] pure mapper | new |
| `web/src/engine/phaseEventMapper.test.ts` (new) | mapper unit tests | new |
| `web/src/types/workbench.ts` | `ChatMessage.taskRequest` | clean |
| `web/src/App.tsx` | template trigger → real stream; `runTaskAttempt`; open continue/restart | clean |
| `web/src/components/chat/MessageBubble.tsx` | restart confirm styling, cancelled copy | clean |
| `.doc/test-reports/2026-09-09-longtask-phase-live-resume-ux-test-report.md` (new) | real E2E report | new |
---
### Task 1: Backend — task stream request-id, resume mode, workspace-aware errors
**Files:**
- Modify: `internal/stream/types.go`
- Modify: `internal/stream/stream.go`
- Modify: `internal/server/sse.go`
- Modify: `internal/stream/stream_test.go`
**Interfaces:**
- Consumes: `bizdev.BizdevService.ResumeOrModifyWorkspace`, `stream.ClassifyError`, `TaskRunRequestDTO`
- Produces:
- `DonePayload.WorkspaceID string` (`json:"workspace_id,omitempty"`)
- `Streamer.EmitDoneWithWorkspace(sessionID, workspaceID string, duration time.Duration)`
- `taskStreamRequest{TaskRunRequestDTO; WorkDir string; AddSteps []squad.StepSpec}` (sse.go)
- resume branch: `work_dir != ""` → `ResumeOrModifyWorkspace(ctx, squad.ModifyWorkflowRequest{WorkDir, AddSteps, Backend, AutoRepair})`
- failure path: `ClassifyError(err, phaseID, requestID, sessionID, workspaceID)` with workspaceID from `result.WorkDir` (soft failure) / `req.WorkDir` (resume) / `""` (hard failure)
- `X-Request-Id` response header + `stream.WithRequestID`
- [ ] **Step 1: Extend DonePayload + EmitDoneWithWorkspace with tests**
`internal/stream/types.go`: add `WorkspaceID string \`json:"workspace_id,omitempty"\`` to `DonePayload`.
`internal/stream/stream.go`: add `EmitDoneWithWorkspace(sessionID, workspaceID string, duration time.Duration)` mirroring `EmitDone` (keep `EmitDone` untouched).
`internal/stream/stream_test.go`: assert `workspace_id` present/omitted in marshaled done events.
- [ ] **Step 2: Rework `handleRunTaskStream` in `internal/server/sse.go`**
1. Set `X-Request-Id` (same pattern as `handleChatStream`) before SSE headers; use `stream.WithRequestID(r.Context(), requestID)`.
2. Decode into `taskStreamRequest` (embedded DTO keeps existing JSON contract).
3. Resume branch when `req.WorkDir != ""`: call `s.svc.ResumeOrModifyWorkspace(ctxWithS, squad.ModifyWorkflowRequest{WorkDir: req.WorkDir, AddSteps: req.AddSteps, Backend: req.Backend, AutoRepair: req.AutoRepair})`; on success `EmitDoneWithWorkspace("task_resume", req.WorkDir, time.Since(start))`; on failure emit `ClassifyError(err, "task_resume", requestID, "", req.WorkDir)` via `EmitErrorInfo`.
4. Existing stage/task branches: capture result; on error choose workspaceID = `result.WorkDir` (soft failure, result != nil) else `""`; emit `EmitErrorInfo(ClassifyError(err, lastPhaseID, requestID, "", wsID))`; on success emit `EmitDoneWithWorkspace("task_run"/"stage_run", result.WorkDir, time.Since(start))`.
5. Build must stay green: `go build ./... && go vet ./internal/server ./internal/stream`.
- [ ] **Step 3: Commit**
```bash
git add internal/stream/types.go internal/stream/stream.go internal/stream/stream_test.go internal/server/sse.go
git commit -m "feat(api): task stream request-id, resume mode and workspace-aware error classification"
```
---
### Task 2: Backend — session-level integration tests (transient injection + resume + classification)
**Files:**
- Create: `internal/server/taskstream_test.go`
**Interfaces:**
- Consumes: `httptest` server with `Server` wired to a stub `BizdevService` (or invalid backend), `handleRunTaskStream`
- Produces: SSE-level assertions on `event: error` / `error_info` / `done.workspace_id` / `X-Request-Id`
- [ ] **Step 1: Transient injection test (test-report follow-up #2)**
Point `Backend` at an unreachable port or a stub service returning a dial error. Assert the SSE body contains `event: error` with `error_info.class` == `transient` and `actions` containing `retry`, plus `X-Request-Id` header non-empty.
- [ ] **Step 2: Soft-failure classification test**
Stub service returns a result with `WorkDir = "/tmp/ws-x"` and `Success=false` plus an error (or `ErrorReason`). Assert `error_info.class == "resumable"`, `workspace_id == "/tmp/ws-x"`, `actions` == `[continue, restart]`.
- [ ] **Step 3: Resume branch test**
POST body with `work_dir` set to a stub workspace dir; assert the stub `ResumeOrModifyWorkspace` receives the request and the stream ends with `done` carrying `workspace_id`.
- [ ] **Step 4: Run + Commit**
Run: `go test ./internal/server ./internal/stream -count=1` (record known env-related failures if any appear).
Commit:
```bash
git add internal/server/taskstream_test.go
git commit -m "test(server): task stream transient injection, classification and resume integration tests"
```
---
### Task 3: Web — task stream client + phase event mapper
**Files:**
- Create: `web/src/api/taskStream.ts`, `web/src/api/taskStream.test.ts`
- Create: `web/src/engine/phaseEventMapper.ts`, `web/src/engine/phaseEventMapper.test.ts`
**Interfaces:**
- Consumes: `parseSSEChunk` / `mapErrorInfo` from `chatStream.ts`; `PhaseEvent` from `types/workbench`
- Produces:
- `streamTask(body: Record<string, unknown>, handlers, signal?)` → POST `/api/v1/tasks/run/stream`
- `buildTaskRequest(task: TaskRunContext): Record<string, unknown>` and `buildResumeRequest(task, workDir)` (adds `work_dir`)
- `applyPhaseEvent(strip: PhaseEvent[], phase?: {phase_id?; message?; step_index?; total_steps?; progress?}): PhaseEvent[]`
- [ ] **Step 1: Mapper with tests (icon table aligned with CLI `bizdev-cli/main.go:516-534`)**
Cover: unknown phase default ⏳; new phase appends `running` and completes predecessors; existing phase updates label in place; `step_completed`/`stage_completed`/`task_failed` close the running entry; label suffix `第x/y步` when step_index/total_steps valid, or `z%` when progress > 0. Run `node --test` per repo convention.
- [ ] **Step 2: Client with tests**
Reuse `parseSSEChunk`; tests assert `buildTaskRequest` includes five project fields + `task_key` + `user_id`, and `buildResumeRequest` adds `work_dir`.
- [ ] **Step 3: Commit**
```bash
git add web/src/api/taskStream.ts web/src/api/taskStream.test.ts web/src/engine/phaseEventMapper.ts web/src/engine/phaseEventMapper.test.ts
git commit -m "feat(web): add task SSE client and live phase-event mapper"
```
---
### Task 4: Web — wire long tasks into App.tsx (trigger, state machine, recovery actions)
**Files:**
- Modify: `web/src/types/workbench.ts` (add `TaskRunContext` + `ChatMessage.taskRequest?` / `taskWorkspaceId?`)
- Modify: `web/src/App.tsx`
**Interfaces:**
- Consumes: `streamTask`, `buildTaskRequest`, `buildResumeRequest`, `applyPhaseEvent`
- Produces: `runTaskAttempt(task, asstId, mode: 'run' | 'resume' | 'restart', attemptIndex)`; template-keyword messages hit real SSE; `handleErrorAction` supports `retry` (chat+task), `continue` (resume via `errorInfo.workspaceId`), `restart` (confirm → fresh run)
- [ ] **Step 1: Types** — `ChatMessage.taskRequest?: TaskRunContext` with `{ taskKey; projectName; customerName; deviceType; intendedUse; targetMarket; userId?; projectId? }`; `ChatMessage.taskWorkspaceId?: string`.
- [ ] **Step 2: `runTaskAttempt`** — mirror `runStreamAttempt`: abort-previous-controller; consume `phase`/`status` → `livePhaseLabel` + `phaseEvents` via `applyPhaseEvent`; `delta` → content; `deliverable` → append `📄 已生成文档:<title>` line; `error` → `failOrRetry(mapErrorInfo(ev))`; `done` → `completed` + persist `done.workspace_id` into `taskWorkspaceId`; network catch → `buildNetworkErrorInfo` (transient).
- [ ] **Step 3: Trigger switch** — in `handleSendMessage`, when `matchedTemplate` is truthy (and not a DocGov shortcut), insert user + placeholder with `taskRequest` and call `runTaskAttempt`; remove the mock template-fill branch body (DocGov shortcuts and `isGenDocShortcut` mock stay).
- [ ] **Step 4: Open continue/restart** — delete the `info.actions.filter((a) => a === 'retry')` line in `failOrRetry`; extend `handleErrorAction`: `retry` → chat `runStreamAttempt` / task `runTaskAttempt(...,'run',0)`; `continue` (require `errorInfo.workspaceId`) → `runTaskAttempt(...,'resume',0)` keeping `phaseEvents`, label「正在继续任务…」; `restart` → `window.confirm` then `runTaskAttempt(...,'restart',0)` with content/phaseEvents reset.
- [ ] **Step 5: Build + Commit** — `cd web && npm run build` → SUCCESS.
```bash
git add web/src/types/workbench.ts web/src/App.tsx
git commit -m "feat(web): drive long tasks from SSE with live phases and continue/restart recovery"
```
---
### Task 5: Web — bubble polish for long-task failure/cancel states
**Files:**
- Modify: `web/src/components/chat/MessageBubble.tsx`
- [ ] **Step 1: Styling + copy** — distinguish `restart` (secondary/danger outline) vs `retry`/`continue` (primary); cancelled task bubbles read「已停止任务(可重新开始)」; keep P1 XSS-safe rendering untouched.
- [ ] **Step 2: Build + Commit** — `cd web && npm run build && npm test` → SUCCESS.
```bash
git add web/src/components/chat/MessageBubble.tsx
git commit -m "fix(web): distinguish recovery action styles for long-task failures"
```
---
### Task 6: Real-Agent E2E + test report + regression
**Files:**
- Create: `.doc/test-reports/2026-09-09-longtask-phase-live-resume-ux-test-report.md`
- [ ] **Step 1: Ensure dev stack up** (agent-dev `:8080`, pi-agent healthy, vite proxy).
- [ ] **Step 2: Case A** — template message (命中 feasibility_reg 等): ≤1s wait bubble, PhaseEventStrip lights up with real phases, real reply; capture SSE event types with timestamps.
- [ ] **Step 3: Case B** — interrupt (stop pi-agent container or CDP block) mid-run: `TASK_INTERRUPTED` resumable bubble with 继续任务/重新开始 + diag line; restore service → 继续任务 resumes the same workspace to completion; restart creates a fresh workspace.
- [ ] **Step 4: Report** per `.cursor/rules/test-reports-standard.mdc` fixed sections 一–七, PASS/FAIL per case.
- [ ] **Step 5: Regression** — `go test ./... -count=1` and `cd web && npm test`; document known unrelated failures.
- [ ] **Step 6: Commit**
```bash
git add .doc/test-reports/2026-09-09-longtask-phase-live-resume-ux-test-report.md
git commit -m "docs(test): archive long-task phase live/resume UX real-agent report"
```
---
## Out of scope (follow-up plans)
- **P3:** token-level `EmitDelta` from pi-agent runner through Go to Web typewriter UX.
- Deliverable → right-rail document panel wiring; DocGov mock shortcut replacement; cross-process resume durability.
## Spec coverage self-check
| Spec requirement | Task |
| :--- | :--- |
| Web uses task SSE | Task 3 + 4 |
| PhaseEventStrip live, no fake progress | Task 3 + 4 |
| `X-Request-Id` on task stream | Task 1 |
| resume mode via `work_dir` | Task 1 + 3 + 4 |
| workspace-aware classification (soft/hard failure) | Task 1 |
| `DonePayload.workspace_id` | Task 1 |
| transient session-level integration test (report follow-up #2) | Task 2 |
| actions from server; drop P1 retry-only filter | Task 4 |
| continue via `errorInfo.workspaceId` | Task 4 |
| restart with confirm → fresh run | Task 4 + 5 |
| stop aborts task stream | Task 4 |
| no silent success template | Task 4 |
| Real Agent report | Task 6 |
| WIP isolation (no `git add -A`) | All commits |
| P3 / deliverable wiring | Explicitly out of scope |
## Placeholder / consistency check
- Action names: `retry` | `continue` | `restart`; classes: `transient` | `resumable` | `fatal` | `cancelled` (unchanged from P1).
- Phase ids covered by mapper: `squad_assembly`, `knowledge_retrieval`, `knowledge_retrieved`, `step_executing`, `step_completed`, `quality_gate`, `advisor_briefing`, `stage_transition`, `stage_started`, `stage_completed`, `task_failed`, default ⏳.
- No TBD steps remaining.
@@ -0,0 +1,182 @@
# 长任务 PhaseEventStrip live 化与分型恢复(继续/重新开始)UX 设计(P2)
## 一、背景与定位
P1 已把 Web 闲聊接到 `POST /api/v1/chat/stream`,实现 ≤1s 等待态、结构化 `error_info`、transient 自动重试与手动重试(见 `2026-09-09-chat-wait-error-retry-ux-design.md` 与 `.doc/test-reports/2026-09-09-chat-wait-error-retry-ux-test-report.md`)。但长任务(智能体小队文档生成、阶段推进)在 Web 上仍完全走本地 mock:既无真实进度,也无分型恢复。
本设计是 P1 的 **P2 增量**,目标:
1. **长任务接入真实 SSE**:Web 通过 `/api/v1/tasks/run/stream` 触发真实 `pi-agent` 长任务。
2. **`PhaseEventStrip` live 化**:由后端真实 `phase` 事件驱动步骤条(进度求真,禁止假进度)。
3. **`resumable` → 「继续任务」**:失败气泡按钮直接续跑失败的工作区。
4. **`fatal` → 「重新开始」**:确认后按原意图发起新 run。
5. **补齐测试报告遗留建议 #2**:真实 transient 故障的会话级集成测试(注入失败 backend → SSE 输出 `event: error`)。
不做(范围外):token 级流式与打字机(P3)、`deliverable` → 右侧文档区完整联动、闲聊路径改动、DocGov 演示捷径替换、test 分支合并。
---
## 二、现状盘点(P2 起点)
### 2.1 后端已就绪
| 能力 | 位置 | 说明 |
| :--- | :--- | :--- |
| task stream handler | `internal/server/sse.go: handleRunTaskStream` | 支持 `stage` / `task_key` 两种模式,SSE 输出 phase/status/detail/deliverable/error/done |
| `ClassifyError` | `internal/stream/classify.go` | `workspaceID != ""` → `TASK_INTERRUPTED`/`resumable`/`[continue, restart]` |
| `ResumeOrModifyWorkspace` | `internal/bizdev/service.go` → `internal/squad/service.go` | 透传 `squad.ModifyWorkflowRequest{WorkDir, AddSteps, RemoveStepIDs, UpdateSteps, Backend, AutoRepair}` |
| phase 事件 | `internal/squad/service.go`(`squad_assembly`/`task_failed`)、`workflow.go`(`step_executing`/`step_completed`)、bizdev/coordinator(`knowledge_retrieval`/`quality_gate` 等) | `PhasePayload{phase_id, message, role, step_index, total_steps, progress}` |
| CLI 图标映射 | `cmd/bizdev-cli/main.go:516-534` | phase_id → 图标对照表,Web 直接复用 |
### 2.2 后端 P2 缺口
1. `handleRunTaskStream` **无 `X-Request-Id`**(P1 只给 chat stream 加了)。
2. 失败时 `streamer.EmitError(err)` 内部调用 `ClassifyError(err, "", "", "", "")` —— **workspaceID 恒为空,长任务失败被错判为 `fatal`**,前端永远看不到「继续任务」。
3. `ResumeOrModifyWorkspace` **未暴露到 HTTP/SSE**。
4. `DonePayload` 无 `workspace_id` —— 成功路径前端拿不到 work_dir(「重新开始后可继续」与后续 P3 续跑都需要)。
### 2.3 前端已就绪
| 组件 | 现状 |
| :--- | :--- |
| `MessageBubble` | 错误动作区已支持 `retry/continue/restart` 三按钮(文案「重试/继续任务/重新开始」已备好);`phaseEvents` → `PhaseEventStrip` 渲染已接 |
| `PhaseEventStrip` | 纯展示组件 `PhaseEvent{step, icon, label, status: pending\|running\|completed}` |
| `App.tsx` | 闲聊 SSE 状态机(`runStreamAttempt`)、停止、`isStreamingActive` 均就绪 |
| `InputArea` | 停止按钮(`onStopStreaming`)已接线 |
### 2.4 前端 P2 缺口
1. 长任务无真实触发入口:「生成文档/测算/推演」等关键词全部走本地 mock 分支。
2. `handleErrorAction` 硬编码 `if (action !== 'retry') return`;`failOrRetry` 把 actions 过滤为仅 `retry`。
3. 无 task stream 客户端与 phase→PhaseEvent 映射器。
---
## 三、契约设计
### 3.1 task stream 请求扩展(向后兼容)
在既有 `TaskRunRequestDTO`(`internal/server/handlers.go`,不修改该文件)基础上,于 `sse.go` 内定义嵌入请求体:
```go
type taskStreamRequest struct {
TaskRunRequestDTO
WorkDir string `json:"work_dir,omitempty"` // 非空 → resume 模式
AddSteps []squad.StepSpec `json:"add_steps,omitempty"` // 可选动态追加步骤
}
```
- `work_dir == ""`:新任务(`ExecuteStage` / `ExecuteTask`,既有行为)。
- `work_dir != ""`:**resume 模式**,调用 `ResumeOrModifyWorkspace(ctx, squad.ModifyWorkflowRequest{WorkDir, AddSteps, Backend, AutoRepair})`;同流输出 phase/detail/error/done。
### 3.2 响应头与错误分类
- 补 `X-Request-Id` 响应头(与 chat stream 一致),注入 `stream.WithRequestID`。
- 失败时显式分类:`ClassifyError(err, phaseID, requestID, sessionID, workDir)`:
- **软失败**(执行器返回 result 且 `result.WorkDir != ""`,如 squad 层 `Success:false`):`workspaceID = result.WorkDir` → `resumable` + `[continue, restart]`;
- **硬失败**(result == nil,工作区未建立或 prepare 失败):`workspaceID = ""` → 按超时/网络/默认规则分类(超时/网络 → transient,其余 fatal);
- resume 模式失败:`workspaceID = req.WorkDir`(工作区仍在)→ `resumable`。
### 3.3 `DonePayload` 扩展
```go
type DonePayload struct {
SessionID string `json:"session_id,omitempty"`
DurationMs int64 `json:"duration_ms"`
TotalEvents int `json:"total_events"`
WorkspaceID string `json:"workspace_id,omitempty"` // P2 新增:任务工作区目录
}
```
配套新增 `Streamer.EmitDoneWithWorkspace(sessionID, workspaceID string, duration time.Duration)`,不改动既有 `EmitDone` 签名与调用点(向后兼容)。前端记录 workspace_id 供「继续任务」使用。
### 3.4 前端 phase → PhaseEventStrip 映射
新建 `web/src/engine/phaseEventMapper.ts` 纯函数(node:test 单测):
```ts
applyPhaseEvent(strip: PhaseEvent[], phase: PhasePayload | undefined): PhaseEvent[]
```
规则(进度求真):
1. phase_id → `{icon, label}` 映射表与 CLI 一致:`squad_assembly`👥 / `knowledge_retrieval`📚 / `step_executing`⚙️ / `step_completed`✅ / `quality_gate`🛡️ / `advisor_briefing`💡 / `stage_transition`🚀 / `stage_started`🚀 / `stage_completed`🎉 / `task_failed`⚠️ / 未知 → ⏳。
2. 收到新 phase_id:条目追加为 `running`;此前所有条目置 `completed`(先行阶段已完成)。
3. 已存在 phase_id:仅更新 label(同阶段刷新文案,不重复追加)。
4. `step_completed` / `stage_completed` / `task_failed`:把当前(或最后)`running` 条目置 `completed`(task_failed 条目本身标 `running` 由错误气泡接棒展示失败态)。
5. label 追加 `step_index/total_steps`(若有效)或 `progress%`(若 >0)。
### 3.5 分型恢复动作(前端)
`handleErrorAction` 放开 `continue` / `restart`:
| 动作 | 触发条件 | 行为 |
| :--- | :--- | :--- |
| `retry` | `transient`(含闲聊) | 原请求重发(新 request_id),P1 行为不变 |
| `continue` | `errorInfo.actions` 含 `continue` 且 `workspaceId` 非空 | 同一气泡恢复:status → `streaming_status`、livePhaseLabel「正在继续任务…」、phaseEvents 保留并续接;调用 task stream resume 模式(`work_dir = workspaceId`) |
| `restart` | `errorInfo.actions` 含 `restart` | `window.confirm` 确认后按原任务参数新 run(不带 work_dir,新工作区);气泡重置(content 清空、phaseEvents 重置、livePhaseLabel「正在重新开始任务…」) |
约束:
- 动作仍只渲染服务端 `error_info.actions`(P1 兜底映射保留);删除 P1 在 `failOrRetry` 中把 actions 过滤为仅 `retry` 的行(闲聊路径天然只有 `retry`,行为不变)。
- 用户停止或发送新消息 → abort 当前流(含 resume/restart 流)。
- `ChatMessage` 新增 `taskRequest` 字段记录任务上下文(taskKey/项目五要素/user_id 等),供 retry/continue/restart 重发。
### 3.6 触发入口(保守增量)
现状消息命中 `matchedTemplate`(模板关键词 → taskKey:需求→intake、调研→research_competitive、可行性→feasibility_reg/feasibility_dev、设计→prelim_design、报价→cdo_quote)时走本地 mock 填报演示。
P2 决策(可调整):**`matchedTemplate` 命中的消息切换到真实 task stream**(taskKey 与项目五要素均明确、无歧义),原 mock 本地填报演示随之下线(真实链路替代其演示价值);DocGov 三捷径(驳回/修改/通过)与「测算/推演/备忘/生成报告」等模糊生成词**暂留 mock**,避免 P2 范围膨胀与演示能力回退。
长任务气泡复用既有等待态/结果态/失败态三态(spec 5.1),`delta` 整段正文照 P1 写入;`deliverable` 事件仅在气泡内轻量提示(`📄 已生成文档:<title>`),右侧文档区联动不做。
---
## 四、组件与文件
| 文件 | 职责 | 是否触碰 WIP |
| :--- | :--- | :--- |
| `internal/server/sse.go` | `X-Request-Id`;`taskStreamRequest`;resume 分支;失败分类带 workDir | 否(干净) |
| `internal/stream/types.go` | `DonePayload.workspace_id` | 否(干净) |
| `internal/stream/stream.go` | `EmitDoneWithWorkspace` | 否(干净) |
| `internal/server/*_test.go`(新建) | resume 模式、错误分类、transient 注入集成测试 | 否(干净) |
| `web/src/api/taskStream.ts`(新建) | task stream 客户端(复用 `parseSSEChunk`) | 否(新建) |
| `web/src/engine/phaseEventMapper.ts`(新建) | phase → PhaseEvent 映射纯函数 | 否(新建) |
| `web/src/types/workbench.ts` | `ChatMessage.taskRequest` | 否(干净) |
| `web/src/App.tsx` | 模板触发真实 stream;runTaskAttempt;动作放开 | 否(干净) |
| `web/src/components/chat/MessageBubble.tsx` | restart 确认/样式微调;cancelled 长任务文案 | 否(干净) |
**WIP 隔离纪律**:P2 所有改动落在上述干净文件与新建文件;提交一律精确 `git add <files>`;绝不触碰 `handlers.go`/`server.go`/`bizdev/service.go`/`storage.ts` 等 74 条 WIP。若实现中发现必须修改 WIP 文件,停下向用户报告。
---
## 五、测试与验收(P2)
### 5.1 分层
- **单元**:`phaseEventMapper`(映射/追加/收口/进度标注);task stream 请求体构造;`DonePayload` 序列化。
- **集成(Go)**:task stream resume 模式(httptest);失败分类带 workspace_id(软失败 → resumable + continue/restart);**transient 注入**(不可达 backend → `event: error` + `error_info.class=transient`,补测试报告遗留建议 #2)。
- **E2E(真实 Agent)**:模板消息 → ≤1s 等待态 + PhaseEventStrip live → 真实正文;中断(停 pi-agent 容器或 CDP 阻断)→ `TASK_INTERRUPTED` 错误气泡(继续任务/重新开始 + 诊断行)→ 恢复服务 → 「继续任务」续跑成功;「重新开始」产生新工作区。
### 5.2 验收标准
1. 模板消息发送后 **≤1s** 出现等待气泡,`PhaseEventStrip` 随真实 `phase` 事件逐步点亮(无假进度轮播)。
2. 长任务失败时气泡含白话 + 一行诊断 + 复制诊断 + 服务端声明的动作(`continue`/`restart`),`class=resumable` 时必现「继续任务」。
3. 「继续任务」基于 `workspace_id` 续跑成功完成;「重新开始」确认后新工作区独立推进;「重试」对 transient 保持 P1 行为。
4. 停止按钮可打断长任务流;无静默成功模板。
5. `go test ./...` 与 `cd web && npm test` 回归通过(或标注已知环境相关失败)。
### 5.3 风险
- 真实长任务耗时数分钟,E2E 选择小任务并记录时长。
- resume 依赖工作区状态持久(磁盘 manifest/blackboard),跨进程重启后的 resume 不在本期验证范围。
- mock 模板填报演示下线属 UX 取舍,如需保留可在 review 时回退为「演示填报」专用触发词。
---
## 六、与既有文档的关系
- 继承:`2026-09-09-chat-wait-error-retry-ux-design.md`(P1)的状态机、错误契约、自动重试策略——本文件仅增量,不推翻。
- 继承:`2026-09-08-streaming-events-and-digestible-ux-design.md` 的三层信息架构与事件类型定义。
- 落地该文档 P2 行:「长任务 `PhaseEventStrip` live;`resumable`→继续;`fatal`→重新开始;对齐 Resume/task stream」。