# 右侧栏边路任务面板技术方案 > 版本: v1.0 | 更新时间: 2026-07-15 --- ## 1. 需求背景 ### 1.1 问题 当前边路任务的生命周期完全绑定在中央 Tab 栏上:当用户关闭边路任务 Tab 后,该边路任务便从界面上消失,无法再次找回或回显。即使用户只是误关 Tab,也无法恢复查看该边路任务的执行状态和内容。 具体表现为: - 边路任务关闭后,`sideSessionId` 被写入 `localStorage` 标记为「已关闭」 - 页面刷新时 `restoreSideTaskTabs()` 会跳过已关闭的边路任务 - 没有其他 UI 入口能浏览、重新打开历史上创建过的边路任务 ### 1.2 目标 在右侧栏(TaskInspector)的「进度」模块下方新增「边路任务」模块,展示当前主任务下的所有边路任务列表,支持点击重新打开。 ### 1.3 边界说明 | 要做的 | 不做的 | |--------|--------| | 在右侧栏「进度」下方新增「边路任务」模块 | 在左侧任务列表(TaskIndexPanel)中展示边路任务 | | 展示主任务下全部边路任务(含已关闭的) | 仅展示当前 Tab 栏中打开的边路任务 | | 每条展示 phase 状态圆点 + 标题 + 创建时间 | 展示边路任务的对话内容或消息预览 | | hover 时展示操作按钮(编辑标题/删除) | 展示边路任务的对话内容或消息预览 | | 点击条目 → 打开/激活对应的边路任务 Tab | 点击后跳转到独立页面替换主任务视图 | | 被动刷新:切回主任务时拉取 + 事件驱动局部更新 | 定时轮询后端接口 | | 支持删除边路任务(同时清理 Tab) | 归档边路任务 | --- ## 2. 需求描述 ### 2.1 用户故事 > 作为开发者,我在主任务中创建了多个边路任务(如「部署到测试环境」「提交代码」),期间关闭了一些 Tab。后来我想回顾之前某个边路任务的执行结果,却找不到入口了。我希望能在右侧栏的边路任务列表中看到所有历史边路任务,点击即可重新打开查看。 ### 2.2 交互流程 ``` 用户在任务视图(TaskView) │ ├── 切到某个主任务 │ │ │ └── 右侧栏(TaskInspector) │ ├── 工作区信息 │ ├── 进度(TodoChecklist) │ └── 边路任务(新增) ← 本需求 │ │ │ ├── 列表展示:phase 圆点 + 标题 + 创建时间 │ │ ├── 空态:「暂无边路任务」 │ │ └── 每条 hover 显示操作按钮 │ │ │ ├── 点击条目 → 若已在 Tab 栏打开则激活,否则新建 Tab │ │ │ └── hover 操作按钮 │ ├── 编辑标题 │ └── 删除 │ ├── 新建边路任务(现有流程不变) │ └── 边路任务列表自动新增一条(事件驱动) │ └── 边路任务执行完成 └── phase 状态自动更新(事件驱动) ``` ### 2.3 视觉效果参考 参考左侧任务列表(TaskIndexPanel)中会话条目的展示方式: - **phase 状态圆点**:运行中(蓝色脉冲)/ 待审批(橙色)/ 已完成(绿色)/ 失败(红色)/ 空闲(灰色) - **标题**:单行省略 - **创建时间**:相对时间(如「3分」「2小时」「1天」) - **hover 操作按钮**:编辑标题 / 删除 --- ## 3. 技术选型 ### 3.1 整体方案:纯前端改动 本次需求**不涉及后端改动**。后端已有 `GET /sessions/{id}/side-tasks` 接口(`SessionController.listSideTasks`),返回结构为 `SideTaskVO`: ```java @Data public static class SideTaskVO { private Long id; private String title; private Long modelId; private String phase; private String createdAt; } ``` 该接口: - 按 `parent_session_id` + `user_id` + `session_type = 'SIDE_TASK'` + `status != 'ARCHIVED'` 查询 - 按 `created_at DESC` 排序 - 已过滤掉已归档的边路任务 ### 3.2 前端改动范围 | 改动项 | 文件 | 类型 | |--------|------|------| | 新增边路任务列表组件 | `desktop/src/components/task/SideTaskList.vue` | 新增 | | 右侧面板增加边路任务模块 | `desktop/src/components/task/TaskInspector.vue` | 修改 | | 边路任务数据管理 | `desktop/src/stores/session.ts` | 修改 | | 事件驱动的列表更新 | `desktop/src/views/task/TaskView.vue` | 修改 | | WebSocket 事件集成 phase 同步 | `desktop/src/composables/useStreamWS.ts` | 修改 | 不涉及后端、管理后台、数据库迁移。 ### 3.3 数据流设计 ``` ┌─────────────────────────────────────────────────────────┐ │ TaskView.vue │ │ ┌─────────────────────────────────────────────────────┐│ │ │ loadSession(sid) ││ │ │ ├── GET /sessions/{sid} → 主任务详情 ││ │ │ ├── GET /sessions/{sid}/side-tasks → 边路任务列表 ││ │ │ │ └── sessionStore.setSideTasks(sid, list) ││ │ │ └── restoreSideTaskTabs(sid, list) ││ │ └─────────────────────────────────────────────────────┘│ │ │ │ ┌─────────────────────────────────────────────────────┐│ │ │ handleSideSessionCreated(event) ││ │ │ ├── updateSideTaskTab(...) ← 现有逻辑 ││ │ │ └── sessionStore.addSideTask(sid, newTask) ← 新增││ │ └─────────────────────────────────────────────────────┘│ └─────────────────────────────────────────────────────────┘ │ ▼ props: sideTasks[] ┌─────────────────────────────────────────────────────────┐ │ TaskInspector.vue │ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ │ │ 工作区 │ │ 进度 │ │ 边路任务(新增)│ │ │ │ │ │ TodoChecklist│ │ SideTaskList │ │ │ └──────────────┘ └──────────────┘ └───────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` --- ## 4. 实现步骤 ### 4.1 第一步:session store 增加边路任务数据管理 **文件**:`desktop/src/stores/session.ts` 新增方法和状态: ```typescript // 边路任务列表缓存,key = parentSessionId const sideTaskCache = ref>(new Map()) export interface SideTaskItem { id: number title: string modelId?: number phase: TaskPhase createdAt?: string } // 设置某个主任务的边路任务列表 function setSideTasks(parentSessionId: string, tasks: SideTaskItem[]) { sideTaskCache.value.set(parentSessionId, tasks) } // 新增一个边路任务(side_session_created 事件触发) function addSideTask(parentSessionId: string, task: SideTaskItem) { const list = sideTaskCache.value.get(parentSessionId) if (list) { // 去重后插入头部 const filtered = list.filter(t => t.id !== task.id) sideTaskCache.value.set(parentSessionId, [task, ...filtered]) } } // 更新边路任务 phase(session_status 事件触发) function updateSideTaskPhase(sideSessionId: number, phase: TaskPhase) { for (const [, list] of sideTaskCache.value) { const item = list.find(t => t.id === sideSessionId) if (item) { item.phase = phase // 触发响应式更新 sideTaskCache.value = new Map(sideTaskCache.value) break } } } // 更新边路任务标题(编辑后) function updateSideTaskTitle(parentSessionId: string, sideSessionId: number, title: string) { const list = sideTaskCache.value.get(parentSessionId) if (list) { const item = list.find(t => t.id === sideSessionId) if (item) { item.title = title sideTaskCache.value = new Map(sideTaskCache.value) } } } // 删除边路任务 function removeSideTask(parentSessionId: string, sideSessionId: number) { const list = sideTaskCache.value.get(parentSessionId) if (list) { sideTaskCache.value.set(parentSessionId, list.filter(t => t.id !== sideSessionId)) } } // 获取边路任务列表(computed) function getSideTasks(parentSessionId: string): SideTaskItem[] { return sideTaskCache.value.get(parentSessionId) ?? [] } ``` 需在 store 的 return 中暴露:`setSideTasks`、`addSideTask`、`updateSideTaskPhase`、`updateSideTaskTitle`、`removeSideTask`、`getSideTasks`。在 `$reset` 中清空 `sideTaskCache`。 ### 4.2 第二步:新增 SideTaskList 组件 **文件**:`desktop/src/components/task/SideTaskList.vue`(新建) ```vue ``` **逻辑要点**: - `phaseClass()` 参照 `TaskIndexPanel.vue` 的 `phaseClass()`,返回 `running` / `waiting` / `completed` / `failed` / `idle` - `formatElapsed()` 参照 `TaskIndexPanel.vue` 的 `formatElapsed()`(刚刚 / N分 / N小时 / N天 / N月) - `handleClick(task)`:emit `open-side-task` 事件,携带 `sideSessionId` 和 `title` - **内联编辑标题**:`editingId` / `editingTitle` 管理编辑状态,`startEdit(task)` 进入编辑、`confirmEdit(task)` 调用后端 `PATCH /sessions/{id}` 并 emit `edit-title` 事件、`cancelEdit` 退出编辑 - `handleDelete(task)`:emit `delete-side-task` 事件 - hover 时通过 CSS 显示 `.side-task-item-actions` **样式**: - 整体风格与 `TodoChecklist` 保持一致(间距、字体大小、颜色变量) - `.side-task-item` 结构与 `TaskIndexPanel` 的 `.session-item` 类似(flex 布局、圆角、hover 背景) - Dark mode 支持 ### 4.3 第三步:TaskInspector 集成边路任务模块 **文件**:`desktop/src/components/task/TaskInspector.vue` **改动**:在「进度」模块的 `
` 之后,新增: ```vue

边路任务

``` **新增 props**: ```typescript sideTasks?: SideTaskItem[] ``` **新增 emits**: ```typescript 'open-side-task': [payload: { sideSessionId: number; title: string }] 'edit-title': [payload: { sideSessionId: number; title: string }] 'delete-side-task': [sideSessionId: number] ``` **事件处理**:直接向上透传给 `TaskView`: ```typescript function handleOpenSideTask(payload: { sideSessionId: number; title: string }) { emit('open-side-task', payload) } function handleEditSideTaskTitle(payload: { sideSessionId: number; title: string }) { emit('edit-title', payload) } function handleDeleteSideTask(sideSessionId: number) { emit('delete-side-task', sideSessionId) } ``` ### 4.4 第四步:TaskView 数据集成与事件处理 **文件**:`desktop/src/views/task/TaskView.vue` #### 4.4.1 数据获取 在 `loadSession(sid)` 中,加载边路任务列表并存入 store: ```typescript // 现有代码(保留): const res = await api.get(`/sessions/${sid}/side-tasks`) const sideTasks = res?.data // 改为:同时写入 store if (Array.isArray(sideTasks) && sideTasks.length > 0) { const items = sideTasks.map((st: any) => ({ id: st.id, title: st.title || '任务', modelId: st.modelId, phase: st.phase || 'IDLE', createdAt: st.createdAt, })) sessionStore.setSideTasks(sid, items) // ... 后续 restoreSideTaskTabs 逻辑不变 } ``` #### 4.4.2 事件驱动更新 **新建边路任务时**(`handleSideSessionCreated` 中追加): ```typescript sessionStore.addSideTask(sid, { id: detail.sideSessionId, title: title || detail.title || '任务', phase: 'RUNNING', createdAt: new Date().toISOString(), }) ``` **边路任务 phase 变化时**:在 `useStreamWS.ts` 的 `session_status` 事件处理中,已有 `sessionStore.updateSessionPhase(sessionId, phase)`。需要额外调用: ```typescript // 边路任务的 phase 也需要更新到 sideTaskCache sessionStore.updateSideTaskPhase(Number(sessionId), phase) ``` > 注意:`updateSessionPhase` 只更新 `sessions` 数组中的主会话,对边路任务(不在 `sessions` 数组中)需要走 `sessionPhases` 缓存,`updateSideTaskPhase` 则更新 `sideTaskCache`。 #### 4.4.3 事件处理函数 在 `TaskView.vue` 中新增: ```typescript // 打开边路任务:如果已在 Tab 中则激活,否则创建新 Tab function handleOpenSideTask(payload: { sideSessionId: number; title: string }) { openSideTaskTab(payload.sideSessionId, payload.title) } // 编辑边路任务标题:调后端 rename + 更新 store + 同步 Tab 标题 async function handleEditSideTaskTitle(payload: { sideSessionId: number; title: string }) { try { const { data } = await api.patch(`/sessions/${payload.sideSessionId}`, { title: payload.title }) if (data) { const newTitle = data.summary || data.title sessionStore.updateSideTaskTitle(activeSessionIdRef.value || '', payload.sideSessionId, newTitle) // 同步更新 Tab 栏中的标题(如果该边路任务已打开) updateSideTaskTab('side:' + payload.sideSessionId, payload.sideSessionId, newTitle) } } catch (e) { console.warn('[side-task] Failed to rename side task:', e) } } // 删除边路任务:调后端删除 + 清理 Tab + 清理 store async function handleDeleteSideTask(sideSessionId: number) { try { await api.delete(`/sessions/${sideSessionId}`) } catch (e) { console.warn('[side-task] Failed to delete side task:', e) } // 清理 Tab(如果打开了的话) const tabId = 'side:' + sideSessionId const tab = tabs.value.find(t => t.id === tabId) if (tab) closeTab(tabId) // 清理 store sessionStore.removeSideTask(activeSessionIdRef.value || '', sideSessionId) } ``` ### 4.5 第五步:useStreamWS 集成 phase 更新 **文件**:`desktop/src/composables/useStreamWS.ts` 在 `session_status` 事件处理中(约 410 行),`sessionStore.updateSessionPhase(sessionId, phase)` 之后追加: ```typescript // 同步更新边路任务的 phase(如果该 sessionId 是边路任务) sessionStore.updateSideTaskPhase(Number(sessionId), phase) ``` ### 4.6 第六步:清理逻辑补充 在 `sessionStore.$reset()` 中增加: ```typescript sideTaskCache.value = new Map() ``` 在 `useCenterTabs.removeSessionTabs()` 调用时(目前无调用点,未来如果删除主任务时需要),同时清理对应主任务的 `sideTaskCache` 条目。 --- ## 5. 落地清单 ### 5.1 新增文件 | 文件 | 说明 | |------|------| | `desktop/src/components/task/SideTaskList.vue` | 边路任务列表组件 | ### 5.2 修改文件 | 文件 | 改动内容 | |------|----------| | `desktop/src/stores/session.ts` | 新增 `SideTaskItem` 类型、`sideTaskCache` 状态、`setSideTasks`/`addSideTask`/`updateSideTaskPhase`/`updateSideTaskTitle`/`removeSideTask`/`getSideTasks` 方法;`$reset` 清空缓存 | | `desktop/src/components/task/TaskInspector.vue` | 新增 `sideTasks` prop、引入 `SideTaskList` 组件、在「进度」下方渲染「边路任务」模块、透传 `open-side-task`/`edit-title`/`delete-side-task` 三个事件 | | `desktop/src/views/task/TaskView.vue` | `loadSession` 中写入 `setSideTasks`;`handleSideSessionCreated` 中调用 `addSideTask`;新增 `handleOpenSideTask`/`handleEditSideTaskTitle`/`handleDeleteSideTask` 三个事件处理函数;传递 `sideTasks` prop 给 `TaskInspector` | | `desktop/src/composables/useStreamWS.ts` | `session_status` 事件处理中追加 `updateSideTaskPhase` 调用 | ### 5.3 不涉及的文件 | 文件 | 原因 | |------|------| | `backend/**` | 后端 API 已完备,无需改动 | | `admin/**` | 边路任务不在管理后台展示 | | `desktop/src/composables/useCenterTabs.ts` | 现有 `openSideTaskTab` / `restoreSideTaskTabs` / `closeTab` 逻辑已满足需求 | | `desktop/src/utils/side-task-tabs.ts` | `markSideTaskClosed` / `getClosedSideTaskIds` / `normalizeSideTaskTitle` 逻辑不变,不涉及本需求 | | `desktop/src/components/chat/SideChatPanel.vue` | 边路任务聊天面板无需改动 | | `desktop/electron/**` | 不涉及 Electron 主进程 | ### 5.4 测试要点 | 场景 | 预期结果 | |------|----------| | 主任务无边路任务 | 右侧栏显示「暂无边路任务」空态 | | 主任务有边路任务 | 列表按创建时间倒序展示,每条显示 phase 圆点 + 标题 + 相对时间 | | 点击边路任务条目 | 若 Tab 已打开则激活,否则新建 Tab | | 关闭边路任务 Tab 后 | 列表中该条目仍可见(不受关闭影响) | | 点击「编辑标题」按钮 | 标题变为输入框,修改后 Enter 确认调用后端 API,列表和 Tab 标题同步更新 | | 点击「删除」按钮 | 调后端删除会话、清理 Tab(如打开)、从列表移除 | | 新建边路任务 | 列表头部自动新增一条,phase 为 RUNNING | | 边路任务执行完成 | phase 圆点从蓝色变为绿色 | | 刷新页面 | 边路任务列表重新从后端拉取,状态正确恢复 | | Dark mode | 样式正确适配暗色主题 | | 右侧面板宽度变化 | 列表正常响应宽度变化,标题截断正常 | --- ## 6. 补充说明 ### 6.1 与现有逻辑的关系 - **Tab 管理**:现有 `useCenterTabs` 的 `openSideTaskTab` / `closeTab` / `restoreSideTaskTabs` 逻辑不变。边路任务面板是**只读浏览+跳转入口**,不替代 Tab 管理。 - **localStorage closed 标记**:`markSideTaskClosed` 和 `getClosedSideTaskIds` 逻辑保留,用于 `restoreSideTaskTabs` 判断刷新后是否恢复 Tab,不与边路任务面板交互。 - **数据一致性**:边路任务列表数据源始终是后端 `GET /sessions/{id}/side-tasks`,localStorage 仅用于 closed 状态(Tab 恢复行为),非列表数据源。 ### 6.2 性能考量 - 边路任务数量通常有限(一个主任务下不会超过几十个),列表渲染无性能瓶颈 - 数据获取在 `loadSession` 时一次性拉取,不额外增加请求 - 事件驱动的增量更新(`addSideTask` / `updateSideTaskPhase`)确保列表在新建和状态变化时及时反映 ### 6.3 未来的扩展方向(不在本次范围) - 边路任务列表支持搜索/过滤 - 支持批量删除边路任务 - 边路任务执行结果摘要预览