Ctrl K
ComunidadGuías…

docs/plan/2026-06-08-new-task-flow-redesign.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 新建任务交互流程优化 — 技术方案

## 1. 背景与目标

### 1.1 现状

当前新建任务流程:

```
点击"+"按钮 → 弹出 NewTaskDialog 弹窗 → 选择智能体 → 选择执行模式 → 点击"开始"
→ 调用 POST /sessions 创建任务 → 关闭弹窗 → 导航到 /tasks/:id → 用户输入消息
```

问题:
- 弹窗打断了用户的操作流
- 必须先创建任务才能输入消息,步骤冗余
- 智能体选择和模式选择的 UI 占用弹窗空间,体验不够沉浸

### 1.2 目标

```
点击"+"按钮 → 直接进入对话页面(空状态)→ 在页面内选择智能体、模式、工作区
→ 输入消息 → 点击发送 → 先创建任务,再发送消息
```

核心变化:
1. **去掉弹窗**:不再使用 `NewTaskDialog`
2. **延迟创建**:任务创建从"选择完点开始"推迟到"第一次发送消息时"
3. **内联选择**:智能体选择以卡片列表形式内嵌在输入框上方,模式/工作区内联在输入区域
4. **发送前可改**:只要还没发送消息,智能体/模式/工作区都可以更改
5. **发送后锁定**:发送第一条消息后,选择区域隐藏,进入正常对话流程

### 1.3 适用范围

仅影响**新建任务**流程。继续已有对话、从分组新建等流程不变。

---

## 2. 现状分析

### 2.1 关键文件清单

| 文件 | 当前职责 | 本次影响 |
|------|---------|---------|
| `views/task/TaskView.vue` | 核心视图,组合所有子组件 | **主要修改** |
| `components/task/NewTaskDialog.vue` | 新建任务弹窗 | **废弃,不再使用** |
| `components/chat/ChatInput.vue` | 聊天输入框 | **重构,扩展为新任务配置区** |
| `composables/useChat.ts` | 聊天核心逻辑 | **小改**,适配延迟创建 |
| `stores/session.ts` | 会话 store | 不变 |
| `stores/agent.ts` | 智能体 store | 不变 |

### 2.2 当前数据流

#### 新建任务弹窗流程

```
TaskView.handleNewTask()
  → newSession()                    // 重置 sessionId、清空队列
  → showNewTaskDialog = true        // 打开弹窗

NewTaskDialog.onOpen()
  → agentStore.fetchAgents()        // 加载智能体列表
  → 用户选择 agent + mode + workspace
  → 点击"开始"
    → sessionStore.createSession()  // POST /sessions,立即创建
    → emit('created', session)
    → TaskView.onSessionCreated()
      → router.push(`/tasks/${id}`)
```

#### 发送消息流程(useChat.sendMessage)

```
sendMessage(text, files)
  → uploadImages(files)
  → connect()
  → if (!sessionId):
      → sessionStore.createSession(agentId, mode, workspace)  // 懒创建
      → sessionId = sessionData.id
  → addUserMessage / addAssistantMessage
  → subscribe(sid)
  → wsSendMessage(sid, text, eventId, imageUrls)
  → await pendingCallbacks
```

关键发现:`useChat.sendMessage()` 已经具备懒创建 session 的能力(第 156-172 行)。当 `sessionId` 为空时,会自动调用 `sessionStore.createSession()`。这意味着**延迟创建的基础设施已经存在**,只需确保 `agentId` 和 `executionMode` 在发送前已被正确设置。

---

## 3. 详细设计

### 3.1 新增组件:`AgentSelector.vue`

**路径**:`components/task/AgentSelector.vue`

**职责**:在输入框上方展示智能体卡片列表,支持折叠/展开。

**Props**:
```typescript
interface Props {
  agents: Agent[]                    // 智能体列表
  selectedAgentId: string | null     // 当前选中的智能体 ID
  collapsed?: boolean                // 是否折叠(默认 false)
}

interface Emits {
  'update:selectedAgentId': [id: string | null]
  'update:collapsed': [collapsed: boolean]
}
```

**UI 结构**:
```
┌─────────────────────────────────────────────┐
│ ▼ 选择智能体                          [收起] │  ← 标题栏,可点击折叠
├─────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐     │
│ │ [A]      │ │ [B]      │ │ [C]      │     │  ← 卡片网格
│ │ Agent A  │ │ Agent B  │ │ Agent C  │     │
│ │ 描述...  │ │ 描述...  │ │ 描述...  │     │
│ └──────────┘ └──────────┘ └──────────┘     │
│ ┌──────────┐ ┌──────────┐                   │
│ │ [D]      │ │ [E]      │                   │
│ │ ...      │ │ ...      │                   │
│ └──────────┘ └──────────┘                   │
└─────────────────────────────────────────────┘
```

折叠后只显示一行:
```
┌─────────────────────────────────────────────┐
│ ▶ [A] Agent A                        [更换] │
└─────────────────────────────────────────────┘
```

**行为**:
- 初始状态:展开,未选中任何智能体
- 点击卡片:选中该智能体,自动折叠
- 已选中状态下点击"更换":展开列表
- 选中状态用高亮边框标识

### 3.2 修改组件:`ChatInput.vue`

**职责扩展**:在输入框上方增加模式选择和工作区选择的内联区域。

**新增 Props**:
```typescript
// 新增
isNewTask?: boolean              // 是否为新建任务模式(默认 false)
selectedAgentId?: string | null  // 当前选中的智能体 ID(新建任务模式下使用)
agents?: Agent[]                 // 智能体列表(新建任务模式下使用)
```

**新增/修改的 Emits**:
```typescript
// 新增
'update:executionMode': [mode: string]
'update:workspace': [workspace: string]
```

**UI 变化**:

新建任务模式下,输入框上方增加配置栏:

```
┌─────────────────────────────────────────────┐
│ AgentSelector(智能体卡片列表,可折叠)       │
├─────────────────────────────────────────────┤
│ ┌─ 模式 ──────┐  ┌─ 工作区 ─────────────┐  │
│ │ CLOUD  ▼    │  │ 📁 选择工作目录       │  │  ← 仅 LOCAL 模式显示工作区
│ └─────────────┘  └──────────────────────┘  │
├─────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────┐ │
│ │ textarea: 描述你希望 Agent 完成的任务    │ │
│ │                                         │ │
│ └─────────────────────────────────────────┘ │
│ [📎]                        [模型名] [发送] │
└─────────────────────────────────────────────┘
```

已有对话模式下,UI 保持不变。

**新增 computed**:
```typescript
const canSendInNewTask = computed(() => {
  if (!props.isNewTask) return true
  // 新建任务模式下,必须选择智能体才能发送
  return !!props.selectedAgentId
})
```

发送按钮的 `disabled` 状态需要同时考虑 `canSend` 和 `canSendInNewTask`。

### 3.3 修改:`TaskView.vue`

#### 3.3.1 新增状态

```typescript
// 新建任务的配置状态(仅在无 sessionId 时使用)
const isNewTaskMode = computed(() => !sessionId.value)
const newTaskAgentId = ref<string | null>(null)
const newTaskMode = ref<'CLOUD' | 'LOCAL'>('CLOUD')
const newTaskWorkspace = ref('')
```

#### 3.3.2 去掉 NewTaskDialog

- 移除 `import NewTaskDialog`
- 移除模板中的 `<NewTaskDialog>` 组件
- 移除 `showNewTaskDialog`、`defaultAgentId`、`defaultMode`、`defaultWorkspace` 等 ref
- 移除 `onSessionCreated` 函数

#### 3.3.3 修改 handleNewTask

```typescript
function handleNewTask() {
  newSession()                       // 重置 useChat 状态
  newTaskAgentId.value = null        // 重置新建任务配置
  newTaskMode.value = 'CLOUD'
  newTaskWorkspace.value = ''
  router.push('/')                   // 导航到无 sessionId 的路径
}
```

#### 3.3.4 修改 handleNewTaskFromGroup

从分组新建时,预填配置:

```typescript
function handleNewTaskFromGroup(payload: { agentId: string; executionMode: string; workspace?: string }) {
  newSession()
  newTaskAgentId.value = payload.agentId
  newTaskMode.value = payload.executionMode as 'CLOUD' | 'LOCAL'
  newTaskWorkspace.value = payload.workspace || ''
  router.push('/')
}
```

#### 3.3.5 修改 navigateToLatestSession

当没有会话时,不再弹出弹窗,而是进入空状态的新建任务页面:

```typescript
function navigateToLatestSession() {
  const latest = sessionStore.sessions[0]
  if (latest) {
    router.replace(`/tasks/${latest.id}`)
  } else {
    // 不再调用 handleNewTask() 打开弹窗
    // 保持在当前路由(/),显示空状态 + 内联选择器
    newSession()
  }
}
```

#### 3.3.6 修改 handleSend

发送时处理新建任务逻辑:

```typescript
function handleSend(text: string, files: File[]) {
  if (isNewTaskMode.value) {
    // 新建任务模式:先创建 session
    if (!newTaskAgentId.value) {
      ElMessage.warning('请先选择智能体')
      return
    }
    // 将新建任务的配置同步到 useChat 的响应式变量
    agentId.value = newTaskAgentId.value
    executionMode.value = newTaskMode.value
    workspace.value = newTaskWorkspace.value
  }
  sendMessageWithQueue(text, files)
  nextTick(scrollToBottomSmooth)
}
```

#### 3.3.7 修改 ChatInput 的 props 绑定

```html
<ChatInput
  :loading="sending && !cancelling"
  :cancelling="cancelling"
  :workspace="isNewTaskMode ? newTaskWorkspace : workspace"
  :execution-mode="isNewTaskMode ? newTaskMode : executionMode"
  :model-name="agentStore.activeAgent?.modelName || ''"
  :permission-level="permissionLevel"
  :is-new-task="isNewTaskMode"
  :selected-agent-id="newTaskAgentId"
  :agents="agentStore.agents"
  @send="handleSend"
  @stop="handleStop"
  @update:permission-level="handlePermissionLevelChange"
  @update:execution-mode="handleNewTaskModeChange"
  @update:workspace="handleNewTaskWorkspaceChange"
/>
```

新增处理函数:

```typescript
function handleNewTaskModeChange(mode: string) {
  newTaskMode.value = mode as 'CLOUD' | 'LOCAL'
  if (mode === 'CLOUD') {
    newTaskWorkspace.value = ''
  }
}

function handleNewTaskWorkspaceChange(ws: string) {
  newTaskWorkspace.value = ws
}

function handleNewTaskAgentChange(agentId: string | null) {
  newTaskAgentId.value = agentId
  if (agentId) {
    agentStore.fetchAgent(agentId)  // 加载智能体详情(获取 modelName 等)
  }
}
```

#### 3.3.8 修改空状态模板

```html
<div v-if="messages.length === 0 && !sending" class="empty-state">
  <template v-if="!sessionId">
    <!-- 不再显示"选择一个 Agent 开始新任务"和按钮 -->
    <!-- 改为引导文案 -->
    <el-icon :size="48" class="empty-icon"><ChatDotRound /></el-icon>
    <p>选择一个智能体,输入任务描述开始对话</p>
  </template>
  <template v-else>
    <p class="guidance-text">在下方输入框描述你的任务,我会帮你完成</p>
  </template>
</div>
```

### 3.4 模板布局调整

TaskView 的 `.task-container` 内部结构调整:

```html
<div class="task-container">
  <div class="messages" ref="messagesContainer">
    <!-- 空状态引导(仅无 sessionId 时) -->
    <div v-if="isNewTaskMode && messages.length === 0 && !sending" class="empty-state">
      <el-icon :size="48" class="empty-icon"><ChatDotRound /></el-icon>
      <p>选择一个智能体,输入任务描述开始对话</p>
    </div>

    <!-- 已有消息时的渲染逻辑(不变) -->
    <template v-if="messageRounds.length > 0">
      <!-- ... 轮次渲染 ... -->
    </template>
    <template v-else>
      <!-- ... 直接渲染 ... -->
    </template>
  </div>

  <QueuePanel ... />

  <ChatInput
    :is-new-task="isNewTaskMode"
    :selected-agent-id="newTaskAgentId"
    :agents="agentStore.agents"
    :workspace="isNewTaskMode ? newTaskWorkspace : workspace"
    :execution-mode="isNewTaskMode ? newTaskMode : executionMode"
    ...
    @send="handleSend"
    @update:execution-mode="handleNewTaskModeChange"
    @update:workspace="handleNewTaskWorkspaceChange"
  />
</div>
```

---

## 4. useChat.ts 适配

### 4.1 现有能力分析

`useChat.sendMessage()` 已经支持懒创建(第 156-172 行):

```typescript
if (!sessionId.value) {
  // LOCAL 模式下如果没有 workspace,弹出选择器
  if (executionMode.value === 'LOCAL' && isElectron && !workspace.value) {
    const dir = await (window as any).electronAPI.selectDirectory()
    if (dir) workspace.value = dir
    else { sending.value = false; return }
  }
  // 创建 session
  const sessionData = await sessionStore.createSession(
    agentId.value, executionMode.value, workspace.value || undefined
  )
  sessionId.value = sessionData.id
}
```

这意味着只要在调用 `sendMessage` 之前,`agentId`、`executionMode`、`workspace` 这三个 ref 已经被正确赋值,现有的懒创建逻辑就能正常工作。

### 4.2 所需改动

**最小改动**:`useChat` 本身不需要修改。`TaskView.handleSend()` 在调用 `sendMessageWithQueue()` 之前,将新建任务的配置同步到 `agentId`/`executionMode`/`workspace` 这三个 ref 即可。

**可选优化**:在 `sendMessage` 开头增加 `agentId` 的校验,避免创建一个没有 agent 的 session:

```typescript
async function sendMessage(text: string, files?: File[]) {
  if ((!text && (!files || files.length === 0)) || sending.value) return
  if (!agentId.value) {
    ElMessage.warning('请先选择智能体')
    return
  }
  // ... 其余不变
}
```

### 4.3 newSession() 行为确认

`newSession()` 当前会重置:
- `sessionId = null`
- `workspace = ''`
- `agentName = 'Agent'`
- `sending = false`
- 清空队列和审批

它**不会**重置 `agentId` 和 `executionMode`(这两个是外部传入的 ref)。这是正确的行为——`TaskView` 中的 `newTaskAgentId` 和 `newTaskMode` 是独立管理的。

---

## 5. 路由与状态管理

### 5.1 路由行为

| 场景 | 路由 | 行为 |
|------|------|------|
| 点击"+"新建任务 | `/`(无 sessionId) | 显示空状态 + 内联选择器 |
| 从分组新建 | `/`(无 sessionId) | 同上,但预填配置 |
| 用户无任何会话时进入 | `/` | `navigateToLatestSession()` → 保持在 `/`,显示新建任务 UI |
| 发送第一条消息后 | `/` → `router.push(`/tasks/${newId}`)` | 导航到新创建的任务 |

### 5.2 sessionIdParam watch 行为调整

当从 `/` 发送消息创建了新 session 后,需要导航到 `/tasks/:id`。这个导航由 `sendMessage` 完成后触发:

```typescript
// TaskView 中监听 sessionId 变化
watch(() => sessionId.value, (newSid) => {
  if (newSid && !route.params.sessionId) {
    // 从新建任务模式创建了 session,导航到任务详情
    router.push(`/tasks/${newSid}`)
    // 发送完成后隐藏选择区域(isNewTaskMode 会自动变为 false,因为 sessionId 已有值)
  }
})
```

注意:`sessionId` 是 `useChat` 返回的 ref,在 `sendMessage` 内部被赋值。导航后 `sessionIdParam` watch 会触发 `loadSession`,但由于 session 刚刚创建,消息已经在 store 中了,`loadSession` 会正常工作。

### 5.3 侧边栏点击"+"按钮的行为

`TaskIndexPanel` emit `new-task` 事件 → `TaskView.handleNewTask()`:
1. 调用 `newSession()` 重置状态
2. 重置 `newTaskAgentId`/`newTaskMode`/`newTaskWorkspace`
3. `router.push('/')` 导航到无 sessionId 的路径
4. 页面显示空状态 + 内联选择器

---

## 6. UI/UX 详细设计

### 6.1 智能体卡片列表(AgentSelector)

**展开状态**:
- 标题行:"选择智能体",右侧有收起按钮
- 卡片网格:`grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))`
- 每张卡片:左侧 avatar(首字母),右侧 name + description
- 选中态:蓝色边框 + 浅蓝背景
- hover 态:边框变色 + 微阴影
- 最大高度 240px,超出滚动

**折叠状态**:
- 单行显示:avatar + 智能体名称 + "更换"按钮
- 无选中时显示 "请选择智能体" + 展开箭头

**交互流程**:
1. 页面初始 → 展开,无选中
2. 用户点击卡片 → 选中,自动折叠
3. 用户点击"更换" → 展开
4. 用户再次点击卡片 → 更新选中,自动折叠

### 6.2 模式选择(内联在 ChatInput)

**位置**:ChatInput 输入框上方,智能体选择器下方

**UI**:
```
┌─────────────────────────────────────────┐
│ [☁️ CLOUD ▼]  [📁 /path/to/workspace]  │
└─────────────────────────────────────────┘
```

- 模式选择:`el-dropdown` 下拉菜单,选项为 CLOUD / LOCAL
- 工作区选择:点击弹出 Electron 文件夹选择器,显示路径
- CLOUD 模式下不显示工作区选择
- LOCAL 模式下如果未选择工作区,工作区按钮显示警告色

### 6.3 发送按钮状态

| 智能体 | 模式 | 工作区(LOCAL) | 输入内容 | 发送按钮 |
|--------|------|--------------|---------|---------|
| 未选 | 任意 | 任意 | 任意 | disabled |
| 已选 | CLOUD | - | 空 | disabled |
| 已选 | CLOUD | - | 有内容 | active |
| 已选 | LOCAL | 未选 | 任意 | disabled |
| 已选 | LOCAL | 已选 | 空 | disabled |
| 已选 | LOCAL | 已选 | 有内容 | active |

### 6.4 发送后的状态转换

```
发送前(isNewTaskMode = true):
  ┌─────────────────────────────────────┐
  │ AgentSelector(展开/折叠)           │
  │ [模式选择] [工作区选择]              │
  │ [输入框]                            │
  │ [工具栏]                            │
  └─────────────────────────────────────┘

发送后(isNewTaskMode = false):
  ┌─────────────────────────────────────┐
  │ [输入框]                            │  ← AgentSelector 和模式选择区域消失
  │ [工具栏]                            │
  └─────────────────────────────────────┘
```

这个切换是自动的——`isNewTaskMode` 是 `computed(() => !sessionId.value)`,当 `sendMessage` 创建了 session 并赋值 `sessionId` 后,`isNewTaskMode` 自动变为 `false`,选择区域自动隐藏。

---

## 7. 边界情况与处理

### 7.1 用户选了智能体但没输入内容就切换会话

- 侧边栏点击其他会话 → `router.push('/tasks/:id')` → `sessionIdParam` watch 触发 `loadSession`
- `newTaskAgentId`/`newTaskMode`/`newTaskWorkspace` 保留但不影响(因为 `isNewTaskMode` 变为 `false`)
- 如果用户再次点击"+"新建任务,`handleNewTask()` 会重置这些状态

### 7.2 用户在 LOCAL 模式下未选工作区就发送

- `useChat.sendMessage()` 第 157-163 行已有处理:弹出 `electronAPI.selectDirectory()` 选择器
- 如果用户取消选择,`sending.value = false` 并 return
- 这个逻辑保持不变

### 7.3 智能体列表加载失败

- `agentStore.fetchAgents()` 失败时,卡片列表为空
- 显示空状态提示:"暂无可用智能体"
- 发送按钮因未选择智能体而 disabled

### 7.4 从分组新建时预填了智能体但该智能体已被删除

- `handleNewTaskFromGroup` 中预填 `agentId`
- `AgentSelector` 渲染时找不到匹配的 agent,不选中任何卡片
- 用户需要重新选择

### 7.5 快速连续点击"+"按钮

- `handleNewTask()` 会调用 `newSession()` 重置状态 + `router.push('/')`
- 如果已经在 `/` 路由,`router.push('/')` 不会触发 `sessionIdParam` watch(值未变)
- `newSession()` 的重置操作是幂等的,多次调用无副作用

---

## 8. 实现步骤

### Step 1:新建 `AgentSelector.vue` 组件

- 实现智能体卡片列表的展示、选择、折叠/展开
- 复用 `NewTaskDialog.vue` 中的卡片样式(`.agent-card`、`.agent-grid` 等)

### Step 2:修改 `ChatInput.vue`

- 新增 `isNewTask`、`selectedAgentId`、`agents` props
- 新增模式选择下拉和工作区选择按钮(输入框上方)
- 修改发送按钮的 disabled 逻辑
- 新增 `update:executionMode`、`update:workspace` emits

### Step 3:修改 `TaskView.vue`

- 新增 `newTaskAgentId`、`newTaskMode`、`newTaskWorkspace` 状态
- 修改 `handleNewTask` / `handleNewTaskFromGroup`:不再打开弹窗
- 修改 `handleSend`:同步新建任务配置到 useChat
- 修改 `navigateToLatestSession`:无会话时不弹窗
- 修改模板:移除 NewTaskDialog,集成 AgentSelector 到 ChatInput 区域
- 新增 `sessionId` watch:创建 session 后导航到 `/tasks/:id`

### Step 4:清理

- 移除 `NewTaskDialog.vue` 的 import 和引用
- 确认 `NewTaskDialog.vue` 文件是否还有其他使用者,如无则可删除

### Step 5:验证

- 新建任务:点击"+"→ 选择智能体 → 选择模式 → 输入消息 → 发送 → 确认任务创建成功并导航
- 从分组新建:点击分组"+"→ 确认预填配置正确
- 继续对话:点击已有会话 → 确认不受影响
- 无会话首次进入:确认显示新建任务 UI
- LOCAL 模式:确认工作区选择和发送后行为正确
- 折叠/展开:确认 AgentSelector 交互流畅

---

## 9. 影响范围总结

| 维度 | 影响 |
|------|------|
| 后端 API | 无变化 |
| 路由 | 无新增路由,现有行为微调 |
| 新增文件 | `components/task/AgentSelector.vue` |
| 修改文件 | `views/task/TaskView.vue`、`components/chat/ChatInput.vue` |
| 废弃文件 | `components/task/NewTaskDialog.vue`(不再引用,可删除) |
| Composables | `useChat.ts` 可选微调(增加 agentId 校验) |
| Stores | 无变化 |