# Agent 对话页面体验优化 实施计划

> **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:** 优化 agent 对话页面的 6 个体验问题：Skill 视觉跳变、Token 不准、Token UI、缺少任务规划、思考不流畅、思考级别控制。

**Architecture:** 后端扩展 thinking.ts 适配层 + 新增 TodoStore/todo 工具 + WS 协议新增 4 个事件；前端用 skill-card 替代双组件、新增 todo-card/thinking-block/token-stats/thinking-level-selector 5 个组件，store 统一管理新状态。

**Tech Stack:** Midway.js 3.20 + TypeORM + Socket.IO (后端) / Vue 3.5 + Element Plus 2.9 + Pinia (前端)

**设计文档:** `docs/superpowers/specs/2026-04-13-agent-chat-ux-overhaul-design.md`

---

## 文件结构

### 后端 (`packages/backend/src/modules/netaclaw/`)

| 文件 | 操作 | 职责 |
|------|------|------|
| `runtime/todo_store.ts` | 新建 | TodoStore 类：write/read/merge/hydrate/formatForInjection |
| `tools/todo_tool.ts` | 新建 | todo 工具 Schema + handler |
| `runtime/thinking.ts` | 修改 | 扩展：buildThinkingParams、getModelThinkingCapability、预算映射 |
| `plugins/llm_providers/anthropic.ts` | 修改 | 传递 thinking 参数 + 流式 thinking_delta |
| `plugins/llm_providers/openai.ts` | 修改 | 传递 reasoning_effort 参数 |
| `runtime/agent.ts` | 修改 | 会话级 thinkLevel 优先级链 + todo 工具注册 |
| `gateway/server.ts` | 修改 | 新增 WS 事件 + set_thinking_level 处理 |

### 前端 (`packages/frontend/src/modules/agent/`)

| 文件 | 操作 | 职责 |
|------|------|------|
| `components/skill-card.vue` | 新建 | 统一 Skill 卡片（替代 skill-indicator + message-item 内嵌） |
| `components/todo-card.vue` | 新建 | Todo 任务规划卡片 |
| `components/token-stats.vue` | 新建 | 紧凑 Token 统计组件 |
| `components/thinking-block.vue` | 新建 | 思考内容流式展示 |
| `components/thinking-level-selector.vue` | 新建 | 思考级别下拉选择器 |
| `types/index.d.ts` | 修改 | 新增 TodoItem/TodoSummary/ThinkLevel/ModelThinkingCapability 类型 |
| `store/chat.ts` | 修改 | 新增 todo/thinking/thinkLevel 状态 + 事件处理 |
| `views/chat.vue` | 修改 | 布局重构：插入新组件 + token 统计移位 |
| `components/message-item.vue` | 修改 | 删除旧 skill 历史 + 旧 thinking 区块，改用新组件 |
| `components/chat-sidebar.vue` | 修改 | 会话项增加 todo 进度 |
| `hooks/websocket.ts` | 修改 | 类型增加新事件 |
| `components/skill-indicator.vue` | 删除 | 被 skill-card.vue 替代 |

---

## 任务依赖顺序

```
Task 1: 前端类型定义（无依赖）
Task 2: 后端 TodoStore（无依赖）
Task 3: 后端 todo 工具（依赖 Task 2）
Task 4: 后端 thinking.ts 扩展（无依赖）
Task 5: 后端 Anthropic 适配器（依赖 Task 4）
Task 6: 后端 OpenAI 适配器（依赖 Task 4）
Task 7: 后端 agent.ts 改造（依赖 Task 2, 3, 4）
Task 8: 后端 gateway WS 协议（依赖 Task 2, 3, 7）
Task 9: 前端 store/chat.ts 改造（依赖 Task 1）
Task 10: 前端 skill-card.vue（依赖 Task 9）
Task 11: 前端 todo-card.vue（依赖 Task 9）
Task 12: 前端 thinking-block.vue（依赖 Task 9）
Task 13: 前端 token-stats.vue（依赖 Task 9）
Task 14: 前端 thinking-level-selector.vue（依赖 Task 9）
Task 15: 前端 chat.vue 布局重构（依赖 Task 10-14）
Task 16: 前端 message-item.vue 改造（依赖 Task 10, 12）
Task 17: 前端 chat-sidebar.vue 适配（依赖 Task 9）
Task 18: 删除 skill-indicator.vue + 清理（依赖 Task 15, 16）
```

---

## Task 1: 前端类型定义

**Files:**
- Modify: `packages/frontend/src/modules/agent/types/index.d.ts:119-169`

- [ ] **Step 1: 在 types/index.d.ts 中新增类型定义**

在文件末尾追加：

```typescript
// === Todo 系统类型 ===
export interface TodoItem {
  id: string;
  content: string;
  status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
}

export interface TodoSummary {
  total: number;
  pending: number;
  in_progress: number;
  completed: number;
  cancelled: number;
}

// === 思考级别类型 ===
export type ThinkLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'adaptive';

export interface ModelThinkingCapability {
  supported: boolean;
  adaptive: boolean;
  levels: ThinkLevel[];
  defaultLevel: ThinkLevel;
}

// === Token 统计类型 ===
export interface TokenUpdateEvent {
  current: {
    inputTokens: number;
    outputTokens: number;
    totalTokens: number;
    apiCalls: number;
  };
  context: {
    usedTokens: number;
    maxTokens: number;
    percent: number;
  };
}
```

- [ ] **Step 2: 更新 WSServerEvent 类型，增加新事件**

找到 `WSServerEvent` 类型定义（约第119行），在 type 联合类型中追加：

```typescript
// 在现有 type 联合中追加：
| 'todo_update'
| 'thinking_delta'
| 'thinking_done'
```

- [ ] **Step 3: 更新 hooks/websocket.ts 中的 WSClientMessage 类型**

在 `hooks/websocket.ts` 中找到客户端消息类型，追加：

```typescript
| 'set_thinking_level'
```

- [ ] **Step 4: 提交**

```bash
git add packages/frontend/src/modules/agent/types/index.d.ts packages/frontend/src/modules/agent/hooks/websocket.ts
git commit -m "feat(agent): 新增 Todo/Thinking/Token 类型定义和 WS 事件类型"
```

---

## Task 2: 后端 TodoStore

**Files:**
- Create: `packages/backend/src/modules/netaclaw/runtime/todo_store.ts`

- [ ] **Step 1: 创建 todo_store.ts**

```typescript
/**
 * TodoStore - 会话级任务列表管理
 * 参考 hermes-agent-main 的 TodoStore 设计
 */

export interface TodoItem {
  id: string;
  content: string;
  status: 'pending' | 'in_progress' | 'completed' | 'cancelled';
}

export interface TodoSummary {
  total: number;
  pending: number;
  in_progress: number;
  completed: number;
  cancelled: number;
}

const VALID_STATUSES = ['pending', 'in_progress', 'completed', 'cancelled'];

export class TodoStore {
  private items: TodoItem[] = [];

  /**
   * 写入 todo 列表
   * @param todos 任务项数组
   * @param merge false=全量替换，true=按 id 增量更新+追加新项
   */
  write(todos: TodoItem[], merge = false): TodoItem[] {
    if (!merge) {
      this.items = todos.map(t => this.validate(t));
    } else {
      for (const todo of todos) {
        const validated = this.validate(todo);
        const existing = this.items.find(i => i.id === validated.id);
        if (existing) {
          Object.assign(existing, validated);
        } else {
          this.items.push(validated);
        }
      }
    }
    return this.read();
  }

  read(): TodoItem[] {
    return JSON.parse(JSON.stringify(this.items));
  }

  hasItems(): boolean {
    return this.items.length > 0;
  }

  getSummary(): TodoSummary {
    const s: TodoSummary = { total: 0, pending: 0, in_progress: 0, completed: 0, cancelled: 0 };
    for (const item of this.items) {
      s.total++;
      s[item.status]++;
    }
    return s;
  }

  /** 上下文压缩后注入，只保留 pending + in_progress */
  formatForInjection(): string | null {
    const active = this.items.filter(i => i.status === 'pending' || i.status === 'in_progress');
    if (active.length === 0) return null;
    const lines = active.map(i => {
      const marker = i.status === 'in_progress' ? '[>]' : '[ ]';
      return `- ${marker} ${i.id}. ${i.content} (${i.status})`;
    });
    return '[你的活跃任务列表在上下文压缩后被保留]\n' + lines.join('\n');
  }

  /** 从历史消息中恢复 todo 状态 */
  static hydrateFromHistory(messages: any[]): TodoStore {
    const store = new TodoStore();
    for (let i = messages.length - 1; i >= 0; i--) {
      const msg = messages[i];
      if (msg.role === 'tool' && msg.toolName === 'todo') {
        try {
          const data = JSON.parse(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content));
          if (data.todos) {
            store.write(data.todos, false);
          }
        } catch { /* 忽略解析失败 */ }
        break;
      }
    }
    return store;
  }

  private validate(item: Partial<TodoItem>): TodoItem {
    return {
      id: item.id || '?',
      content: item.content || '(无描述)',
      status: VALID_STATUSES.includes(item.status!) ? item.status! as TodoItem['status'] : 'pending',
    };
  }
}
```

- [ ] **Step 2: 提交**

```bash
git add packages/backend/src/modules/netaclaw/runtime/todo_store.ts
git commit -m "feat(netaclaw): 新增 TodoStore 类 - 支持 merge/replace/hydrate/压缩注入"
```

---

## Task 3: 后端 todo 工具

**Files:**
- Create: `packages/backend/src/modules/netaclaw/tools/todo_tool.ts`

- [ ] **Step 1: 创建 todo_tool.ts**

```typescript
import { TodoStore } from '../runtime/todo_store.js';

export const todoToolSchema = {
  name: 'todo',
  description:
    '管理当前会话的任务列表。用于3步以上的复杂任务或用户提供多个任务时。\n'
    + '不传参数=读取当前列表。\n'
    + '传 todos 数组=写入。merge=false(默认)全量替换整个计划，merge=true 按 id 增量更新。\n'
    + '每个项: { id: string, content: string, status: pending|in_progress|completed|cancelled }\n'
    + '列表顺序=优先级。同一时间只能有一个 in_progress。\n'
    + '完成立即标记 completed。失败则 cancel 并添加修正项。\n'
    + '始终返回完整列表。',
  parameters: {
    type: 'object' as const,
    properties: {
      todos: {
        type: 'array' as const,
        description: '任务项数组。省略则读取当前列表。',
        items: {
          type: 'object' as const,
          properties: {
            id: { type: 'string' as const, description: '唯一标识' },
            content: { type: 'string' as const, description: '任务描述' },
            status: {
              type: 'string' as const,
              enum: ['pending', 'in_progress', 'completed', 'cancelled'],
              description: '当前状态',
            },
          },
          required: ['id', 'content', 'status'],
        },
      },
      merge: {
        type: 'boolean' as const,
        description: 'true=按 id 增量更新+追加新项。false(默认)=全量替换整个列表。',
        default: false,
      },
    },
    required: [] as string[],
  },
};

/** 执行 todo 工具 */
export function executeTodo(
  store: TodoStore,
  args: { todos?: any[]; merge?: boolean }
): { todos: any[]; summary: any } {
  if (args.todos && args.todos.length > 0) {
    store.write(args.todos, args.merge ?? false);
  }
  return {
    todos: store.read(),
    summary: store.getSummary(),
  };
}
```

- [ ] **Step 2: 提交**

```bash
git add packages/backend/src/modules/netaclaw/tools/todo_tool.ts
git commit -m "feat(netaclaw): 新增 todo 工具 Schema 和 handler"
```

---

## Task 4: 后端 thinking.ts 扩展

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/runtime/thinking.ts` (当前12行)

- [ ] **Step 1: 扩展 thinking.ts，新增预算映射和多模型适配**

保留现有的 `resolveThinkingDefault` 和 `isThinkingSupported`，在文件末尾追加：

```typescript
import type { ThinkLevel } from '../plugins/plugin_entry.js';

// Anthropic 思考预算映射（参考 hermes THINKING_BUDGET）
export const ANTHROPIC_BUDGET_MAP: Record<string, number> = {
  minimal: 2000,
  low: 4000,
  medium: 8000,
  high: 16000,
};

// Anthropic adaptive effort 映射
export const ANTHROPIC_ADAPTIVE_MAP: Record<string, string> = {
  minimal: 'low',
  low: 'low',
  medium: 'medium',
  high: 'high',
  adaptive: 'medium',
};

// OpenAI reasoning effort 映射
export const OPENAI_EFFORT_MAP: Record<string, string> = {
  minimal: 'low',
  low: 'low',
  medium: 'medium',
  high: 'high',
};

/** 检测模型是否支持 adaptive thinking（Claude 4.6+） */
export function supportsAdaptiveThinking(model: string): boolean {
  return /4[-.]6/.test(model);
}

/** 模型思考能力描述 */
export interface ModelThinkingCapability {
  supported: boolean;
  adaptive: boolean;
  levels: ThinkLevel[];
  defaultLevel: ThinkLevel;
}

/** 获取模型的思考能力 */
export function getModelThinkingCapability(supplier: string, model: string): ModelThinkingCapability {
  if (supplier === 'anthropic' && supportsAdaptiveThinking(model)) {
    return { supported: true, adaptive: true, levels: ['off', 'minimal', 'low', 'medium', 'high', 'adaptive'], defaultLevel: 'adaptive' };
  }
  if (supplier === 'anthropic') {
    return { supported: true, adaptive: false, levels: ['off', 'low', 'medium', 'high'], defaultLevel: 'medium' };
  }
  if (supplier === 'openai' && /^(o1|o3)/.test(model)) {
    return { supported: true, adaptive: false, levels: ['off', 'low', 'medium', 'high'], defaultLevel: 'medium' };
  }
  if (supplier === 'deepseek' && model.includes('r1')) {
    return { supported: true, adaptive: false, levels: ['off', 'low', 'medium', 'high'], defaultLevel: 'medium' };
  }
  return { supported: false, adaptive: false, levels: ['off'], defaultLevel: 'off' };
}

/** 构建 Anthropic 思考参数 */
export function buildAnthropicThinkingParams(model: string, level: ThinkLevel) {
  if (level === 'off') return null;
  if (supportsAdaptiveThinking(model)) {
    return {
      thinking: { type: 'adaptive' as const },
      betas: ['interleaved-thinking-2025-05-14'],
    };
  }
  const budget = ANTHROPIC_BUDGET_MAP[level] || 8000;
  return {
    thinking: { type: 'enabled' as const, budget_tokens: budget },
    temperature: 1.0,
    maxTokensAdjust: budget + 4096,
    betas: ['interleaved-thinking-2025-05-14'],
  };
}

/** 构建 OpenAI/DeepSeek 思考参数 */
export function buildOpenAIThinkingParams(level: ThinkLevel) {
  if (level === 'off') return null;
  return {
    reasoning_effort: OPENAI_EFFORT_MAP[level] || 'medium',
  };
}
```

- [ ] **Step 2: 提交**

```bash
git add packages/backend/src/modules/netaclaw/runtime/thinking.ts
git commit -m "feat(netaclaw): 扩展 thinking.ts - 多模型预算映射和能力检测"
```

---

## Task 5: 后端 Anthropic 适配器改造

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/plugins/llm_providers/anthropic.ts:23-30,66`

- [ ] **Step 1: 导入 buildAnthropicThinkingParams 并传递思考参数**

在文件顶部导入：
```typescript
import { buildAnthropicThinkingParams } from '../../runtime/thinking.js';
```

在 `chat()` 方法中 `client.messages.create()` 调用处（约第23-30行），在构建请求参数时加入思考参数：

```typescript
const thinkParams = buildAnthropicThinkingParams(config.model, config.thinkLevel || 'off');

const createParams: any = {
  model: config.model,
  max_tokens: config.maxTokens || 4096,
  messages,
  tools,
  // ...现有参数
};

if (thinkParams) {
  createParams.thinking = thinkParams.thinking;
  if (thinkParams.temperature !== undefined) {
    createParams.temperature = thinkParams.temperature;
  }
  if (thinkParams.maxTokensAdjust) {
    createParams.max_tokens = Math.max(createParams.max_tokens, thinkParams.maxTokensAdjust);
  }
}

// 添加 beta headers
const headers: Record<string, string> = {};
if (thinkParams?.betas?.length) {
  headers['anthropic-beta'] = thinkParams.betas.join(',');
}
```

- [ ] **Step 2: 提交**

```bash
git add packages/backend/src/modules/netaclaw/plugins/llm_providers/anthropic.ts
git commit -m "feat(netaclaw): Anthropic 适配器传递 thinking 参数和 beta headers"
```

---

## Task 6: 后端 OpenAI 适配器改造

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/plugins/llm_providers/openai.ts:18-24`

- [ ] **Step 1: 导入 buildOpenAIThinkingParams 并传递参数**

在文件顶部导入：
```typescript
import { buildOpenAIThinkingParams } from '../../runtime/thinking.js';
```

在 `chat()` 方法中 `client.chat.completions.create()` 调用处（约第18-24行），加入：

```typescript
const thinkParams = buildOpenAIThinkingParams(config.thinkLevel || 'off');

const createParams: any = {
  model: config.model,
  messages,
  tools,
  // ...现有参数
};

if (thinkParams) {
  createParams.reasoning_effort = thinkParams.reasoning_effort;
}
```

- [ ] **Step 2: 提交**

```bash
git add packages/backend/src/modules/netaclaw/plugins/llm_providers/openai.ts
git commit -m "feat(netaclaw): OpenAI 适配器传递 reasoning_effort 参数"
```

---

## Task 7: 后端 agent.ts 改造

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/runtime/agent.ts:36-45,57-66`

- [ ] **Step 1: 导入 TodoStore 和 todo 工具，修改 thinkLevel 优先级链**

在文件顶部导入：
```typescript
import { TodoStore } from './todo_store.js';
import { todoToolSchema, executeTodo } from '../tools/todo_tool.js';
```

修改 `runAgent()` 函数中 thinkLevel 的获取逻辑（约第36行）：

```typescript
// 优先级链：会话级 > agent 配置 > 模型默认
const thinkLevel = params.sessionThinkLevel
  ?? config.defaultThinkLevel
  ?? resolveThinkingDefault(config.model);
```

在工具列表构建处，将 todo 工具加入：

```typescript
// 在现有工具列表中追加 todo 工具
const todoStore = params.todoStore ?? new TodoStore();
const todoTool = {
  ...todoToolSchema,
  execute: async (args: any) => {
    const result = executeTodo(todoStore, args);
    // 通过回调通知前端
    params.onTodoUpdate?.(result);
    return JSON.stringify(result);
  },
};
```

- [ ] **Step 2: 在 AgentConfig 接口中新增 defaultThinkLevel 字段**

```typescript
interface AgentConfig {
  // ...现有字段（name, systemPrompt, model, apiKey, baseUrl, temperature, maxTokens, maxToolRounds, skills）
  defaultThinkLevel?: ThinkLevel;  // agent 配置的默认思考级别
}
```

- [ ] **Step 3: 在 AgentRunParams 接口中新增字段**

```typescript
interface AgentRunParams {
  // ...现有字段
  sessionThinkLevel?: ThinkLevel;   // 会话级思考级别
  todoStore?: TodoStore;            // TodoStore 实例
  onTodoUpdate?: (data: { todos: any[]; summary: any }) => void;  // todo 更新回调
  onThinkingDelta?: (text: string) => void;  // 思考流式回调（替代 onThinking）
}
```

- [ ] **Step 4: 提交**

```bash
git add packages/backend/src/modules/netaclaw/runtime/agent.ts
git commit -m "feat(netaclaw): agent.ts 集成 TodoStore + 会话级思考级别"
```

---

## Task 8: 后端 gateway WS 协议改造

**Files:**
- Modify: `packages/backend/src/modules/netaclaw/gateway/server.ts:77-251`

- [ ] **Step 1: 处理 set_thinking_level 客户端消息**

在 `@OnWSMessage('message')` 处理函数中（约第77行），增加对 `set_thinking_level` 消息的处理：

```typescript
case 'set_thinking_level': {
  const { level } = parsed.data;
  // 更新当前 session 的思考级别
  session.thinkLevel = level;
  break;
}
```

- [ ] **Step 2: 修改 handleChat 中的 runAgent 调用，新增回调**

在 `handleChat()` 方法中（约第193-213行），修改 `runAgent()` 调用：

```typescript
const todoStore = session.todoStore ?? new TodoStore();
session.todoStore = todoStore;

await runAgent({
  // ...现有参数
  sessionThinkLevel: session.thinkLevel,
  todoStore,
  onTodoUpdate: (data) => {
    this.send({ type: 'todo_update', sessionId: sid, data });
  },
  onThinkingDelta: (text) => {
    this.send({ type: 'thinking_delta', sessionId: sid, data: { text } });
  },
  onThinking: (text) => {
    // 保留完整思考文本用于存储
    thinkingText += text;
    // 不再发送旧的 'thinking' 事件
  },
});

// 思考结束标记
if (thinkingText) {
  this.send({ type: 'thinking_done', sessionId: sid, data: {} });
}
```

- [ ] **Step 3: 新增 token_update 推送逻辑（含 context 字段）**

当前后端没有中间 `token_update` 推送，只在 `done` 事件的 `usage` 字段里带。需要在 `runAgent` 回调中新增推送。

首先在 `thinking.ts` 中新增上下文估算工具函数：

```typescript
/** 模型上下文窗口上限映射 */
const MODEL_MAX_TOKENS: Record<string, number> = {
  'claude-opus-4-6': 200000,
  'claude-sonnet-4-6': 200000,
  'gpt-4o': 128000,
  'gpt-4o-mini': 128000,
  'o1': 200000,
  'o3': 200000,
  'deepseek-r1': 64000,
  'deepseek-chat': 64000,
};

export function getModelMaxTokens(model: string): number {
  for (const [key, val] of Object.entries(MODEL_MAX_TOKENS)) {
    if (model.includes(key)) return val;
  }
  return 128000; // 默认
}
```

然后在 gateway `handleChat()` 中，在 `runAgent` 调用后、`done` 事件发送前，推送 `token_update`：

```typescript
// runAgent 完成后，推送最终 token_update
if (result.usage) {
  const inp = result.usage.inputTokens || 0;
  const out = result.usage.outputTokens || 0;
  // 粗估上下文：历史消息 token + 本轮输入
  const historyTokens = history.reduce((sum, m) => sum + (m.content?.length || 0) / 3, 0);
  const usedTokens = Math.round(historyTokens + inp);
  const maxTokens = getModelMaxTokens(agentConfig.model);
  this.send({
    type: 'token_update',
    sessionId: sid,
    data: {
      current: { inputTokens: inp, outputTokens: out, totalTokens: inp + out, apiCalls: result.usage.apiCalls || 1 },
      context: {
        usedTokens,
        maxTokens,
        percent: Math.min(100, Math.round((usedTokens / maxTokens) * 100)),
      },
    },
  });
}
```

- [ ] **Step 4: 提交**

```bash
git add packages/backend/src/modules/netaclaw/gateway/server.ts
git commit -m "feat(netaclaw): gateway 新增 todo_update/thinking_delta/thinking_done WS 事件"
```

---

## Task 9: 前端 store/chat.ts 改造

**Files:**
- Modify: `packages/frontend/src/modules/agent/store/chat.ts:142-337`

- [ ] **Step 1: 新增状态变量**

在 store 的状态定义区域新增：

```typescript
// Todo 状态
const todoItems = ref<TodoItem[]>([]);
const todoSummary = ref<TodoSummary | null>(null);

// 思考流式状态
const isThinking = ref(false);
const thinkingStream = ref('');

// 思考级别
const thinkLevel = ref<ThinkLevel>('medium');
const modelThinkingCapability = ref<ModelThinkingCapability>({
  supported: true, adaptive: false, levels: ['off', 'low', 'medium', 'high'], defaultLevel: 'medium',
});

// Token（简化，删除本地估算）
const tokenUsage = ref<TokenUpdateEvent | null>(null);
```

- [ ] **Step 2: 删除旧的 `case 'thinking'` 和旧的 `case 'token_update'`**

删除 store/chat.ts 中现有的 `case 'thinking':` 区块（约第153-157行）和现有的 `case 'token_update':` 区块（约第224-230行），替换为新事件处理。

- [ ] **Step 3: 在 handleWSEvent 中新增事件处理**

在 `handleWSEvent()` 函数的 switch 中追加：

```typescript
case 'todo_update': {
  todoItems.value = event.data.todos;
  todoSummary.value = event.data.summary;
  break;
}

case 'thinking_delta': {
  if (!isThinking.value) {
    isThinking.value = true;
    thinkingStream.value = '';
  }
  thinkingStream.value += event.data.text;
  break;
}

case 'thinking_done': {
  isThinking.value = false;
  if (assistantMsg) {
    assistantMsg.thinking = thinkingStream.value;
  }
  break;
}

case 'token_update': {
  tokenUsage.value = event.data;
  break;
}
```

- [ ] **Step 3: 修改 'token' 事件处理，自动结束思考状态**

在现有的 `case 'token'` 处理中，开头加入：

```typescript
case 'token': {
  if (isThinking.value) {
    isThinking.value = false;
    if (assistantMsg) {
      assistantMsg.thinking = thinkingStream.value;
    }
  }
  // ...原有追加 content 逻辑
}
```

- [ ] **Step 4: 修改 'done' 事件处理**

修改约第237-269行的 `case 'done'` 处理：

```typescript
case 'done': {
  if (event.sessionId) {
    sessionId.value = event.sessionId;
  }
  if (assistantMsg) {
    if (!assistantMsg.metadata) assistantMsg.metadata = {};
    // 将 skillProgress 写入 metadata（用于历史恢复），但不清空
    assistantMsg.metadata.skillExecutions = [...skillProgress.value];
    // 写入最终 token 数据
    if (tokenUsage.value) {
      assistantMsg.metadata.tokenUsage = tokenUsage.value.current;
    } else if (event.usage) {
      // 兼容：如果没有 token_update 推送，从 done 事件中取
      const inp = event.usage.inputTokens || 0;
      const out = event.usage.outputTokens || 0;
      assistantMsg.metadata.tokenUsage = { input: inp, output: out, total: inp + out, apiCalls: event.usage.apiCalls };
      sessionTotalTokens.value += inp + out;
    }
  }
  tokenUsage.value = null;
  loading.value = false;
  loadSessions();  // 保留：刷新会话列表
  // 注意：不再执行 setTimeout(() => { skillProgress.value = []; }, 3000)
  // skillProgress 在切换会话时才清空
  break;
}
```

- [ ] **Step 5: 简化 recalcSessionTokens，删除本地 token 估算逻辑**

修改 `recalcSessionTokens()` 函数（约第317-337行），删除中文字符估算代码（`chineseChars`/`otherChars`/`ctxEstimate` 相关行），只保留从 metadata.tokenUsage 累计 `sessionTotalTokens` 的逻辑。同时删除 `contextTokens` ref（上下文数据改由后端 `token_update.context` 推送）。

```typescript
function recalcSessionTokens() {
  let total = 0;
  for (const msg of messages.value) {
    const meta = typeof msg.metadata === 'string'
      ? (() => { try { return JSON.parse(msg.metadata); } catch { return null; } })()
      : msg.metadata;
    if (meta?.tokenUsage?.total) {
      total += typeof meta.tokenUsage.total === 'number' ? meta.tokenUsage.total : (meta.tokenUsage.total.total || 0);
    }
  }
  sessionTotalTokens.value = total;
}
```

- [ ] **Step 6: 新增会话切换恢复函数**

```typescript
function restoreTodoFromHistory(messages: ChatMessage[]) {
  for (let i = messages.length - 1; i >= 0; i--) {
    const msg = messages[i];
    if (msg.role === 'tool' && msg.metadata?.toolName === 'todo') {
      try {
        const data = JSON.parse(msg.content);
        todoItems.value = data.todos;
        todoSummary.value = data.summary;
      } catch {}
      return;
    }
  }
  todoItems.value = [];
  todoSummary.value = null;
}

function restoreSkillProgress(messages: ChatMessage[]) {
  skillProgress.value = [];
  for (const msg of messages) {
    if (msg.metadata?.skillExecutions) {
      skillProgress.value.push(...msg.metadata.skillExecutions);
    }
  }
}

function setThinkLevel(level: ThinkLevel) {
  thinkLevel.value = level;
  sendWSMessage({ type: 'set_thinking_level', data: { level } });
}
```

- [ ] **Step 7: 在 switchSession 中调用恢复函数**

在现有的会话切换逻辑中加入：

```typescript
function switchSession(newSessionId: string) {
  skillProgress.value = [];
  todoItems.value = [];
  todoSummary.value = null;
  thinkingStream.value = '';
  isThinking.value = false;
  tokenUsage.value = null;
  // ...加载新会话消息后
  restoreTodoFromHistory(messages);
  restoreSkillProgress(messages);
  thinkLevel.value = session.thinkLevel ?? 'medium';
}
```

- [ ] **Step 8: 导出新状态和函数**

在 store 的 return 中追加导出：

```typescript
return {
  // ...现有导出
  todoItems, todoSummary,
  isThinking, thinkingStream,
  thinkLevel, modelThinkingCapability, tokenUsage,
  restoreTodoFromHistory, restoreSkillProgress, setThinkLevel,
};
```

- [ ] **Step 9: 提交**

```bash
git add packages/frontend/src/modules/agent/store/chat.ts
git commit -m "feat(agent): store 新增 todo/thinking/token 状态管理和 WS 事件处理"
```

---

## Task 10: 前端 skill-card.vue

**Files:**
- Create: `packages/frontend/src/modules/agent/components/skill-card.vue`

- [ ] **Step 1: 创建 skill-card.vue**

完整组件代码见设计文档第 4.2 节。关键点：
- Props: 接收 `SkillProgress` 的所有字段（name, label, status, percent, step, detail, result, tokens）
- 状态过渡：`<transition name="fade">` 包裹进度条和结果区域
- CSS：`.skill-card--running/done/error` 通过 CSS 变量控制边框颜色
- 内部复用现有的 `skill-result-viewer.vue` 展示结果详情

- [ ] **Step 2: 提交**

```bash
git add packages/frontend/src/modules/agent/components/skill-card.vue
git commit -m "feat(agent): 新增 skill-card.vue 统一 Skill 卡片组件"
```

---

## Task 11: 前端 todo-card.vue

**Files:**
- Create: `packages/frontend/src/modules/agent/components/todo-card.vue`

- [ ] **Step 1: 创建 todo-card.vue**

完整组件代码见设计文档第 6.2 节。关键点：
- Props: `items: TodoItem[]`, `summary: TodoSummary`
- 可折叠头部：📋 图标 + 标题 + 进度摘要 + 微型进度条
- `<transition-group name="todo-item">` 包裹列表项实现增删动画
- 状态图标：✅ completed / spinner in_progress / ❌ cancelled / ⬚ pending
- CSS：`.todo-card__item--in_progress` 高亮背景，`.is-done` 删除线

- [ ] **Step 2: 提交**

```bash
git add packages/frontend/src/modules/agent/components/todo-card.vue
git commit -m "feat(agent): 新增 todo-card.vue 任务规划卡片组件"
```

---

## Task 12: 前端 thinking-block.vue

**Files:**
- Create: `packages/frontend/src/modules/agent/components/thinking-block.vue`

- [ ] **Step 1: 创建 thinking-block.vue**

完整组件代码见设计文档第 7.4 节。关键点：
- Props: `content: string`, `isStreaming: boolean`
- 流式时默认展开 + 呼吸动画（`thinking-pulse` 1.5s）+ 光标闪烁（`blink` 1s）
- 完成后自动折叠（watch isStreaming 变化触发）
- 内容渲染为 Markdown（使用项目现有的 markdown 渲染工具）
- CSS：虚线边框 + 半透明背景 + 12px 小字号 + 柔和色

- [ ] **Step 2: 提交**

```bash
git add packages/frontend/src/modules/agent/components/thinking-block.vue
git commit -m "feat(agent): 新增 thinking-block.vue 思考内容流式展示组件"
```

---

## Task 13: 前端 token-stats.vue

**Files:**
- Create: `packages/frontend/src/modules/agent/components/token-stats.vue`

- [ ] **Step 1: 创建 token-stats.vue**

完整组件代码见设计文档第 5.3 节。关键点：
- Props: `tokenUsage: TokenUpdateEvent | null`, `isRunning: boolean`
- 执行中显示：`↑1.2K ↓0.8K · 3次`
- 始终显示：`上下文 32%` + 颜色圆点（≤50%绿/50-80%黄/>80%红）
- `formatTokens()` 工具函数：1234→1.2K, 12345→12.3K
- hover tooltip 显示详细数据

- [ ] **Step 2: 提交**

```bash
git add packages/frontend/src/modules/agent/components/token-stats.vue
git commit -m "feat(agent): 新增 token-stats.vue 紧凑 Token 统计组件"
```

---

## Task 14: 前端 thinking-level-selector.vue

**Files:**
- Create: `packages/frontend/src/modules/agent/components/thinking-level-selector.vue`

- [ ] **Step 1: 创建 thinking-level-selector.vue**

完整组件代码见设计文档第 9.2 节。关键点：
- Props: `modelValue: ThinkLevel`, `disabled: boolean`, `capability: ModelThinkingCapability`
- `el-select` 下拉，选项根据 `capability.levels` 动态生成
- label 映射：off→关闭, minimal→极简, low→低, medium→中等, high→高, adaptive→自适应
- emit `update:modelValue` 和 `change` 事件

- [ ] **Step 2: 提交**

```bash
git add packages/frontend/src/modules/agent/components/thinking-level-selector.vue
git commit -m "feat(agent): 新增 thinking-level-selector.vue 思考级别选择器"
```

---

## Task 15: 前端 chat.vue 布局重构

**Files:**
- Modify: `packages/frontend/src/modules/agent/views/chat.vue:16-218`

- [ ] **Step 1: 删除顶部 token 统计条**

删除约第16-37行的 `<div class="token-stats-bar">` 整个区块。

- [ ] **Step 2: 重构消息列表区域**

修改消息列表区域（约第40-89行），插入新组件：

```vue
<div class="chat-messages" ref="messagesRef">
  <!-- 历史消息 -->
  <message-item v-for="msg in displayMessages" :key="msg.id" :message="msg" />

  <!-- Todo 卡片 -->
  <todo-card v-if="todoItems.length" :items="todoItems" :summary="todoSummary" />

  <!-- Skill 执行卡片（替代 skill-indicator） -->
  <skill-card v-for="sp in skillProgress" :key="sp.name" v-bind="sp" />

  <!-- 流式 assistant 消息（含 thinking-block） -->
  <message-item v-if="streamingMsg" :message="streamingMsg">
    <template #before-content>
      <thinking-block
        v-if="thinkingStream || streamingMsg?.thinking"
        :content="isThinking ? thinkingStream : (streamingMsg?.thinking || '')"
        :is-streaming="isThinking"
      />
    </template>
  </message-item>
</div>
```

- [ ] **Step 3: 在输入区域添加思考级别选择器和 token 统计**

在输入区域（约第92-218行）的工具栏中加入：

```vue
<div class="chat-input__toolbar">
  <!-- 现有的 Agent 选择器、附件按钮 -->
  
  <!-- 新增：思考级别选择器 -->
  <thinking-level-selector
    v-model="thinkLevel"
    :disabled="loading"
    :capability="modelThinkingCapability"
    @change="setThinkLevel"
  />

  <!-- 右侧：token 统计 -->
  <token-stats
    class="chat-input__token-stats"
    :token-usage="tokenUsage"
    :is-running="loading"
  />
</div>
```

- [ ] **Step 4: 导入新组件**

在 `<script setup>` 中导入：

```typescript
import SkillCard from '../components/skill-card.vue';
import TodoCard from '../components/todo-card.vue';
import ThinkingBlock from '../components/thinking-block.vue';
import TokenStats from '../components/token-stats.vue';
import ThinkingLevelSelector from '../components/thinking-level-selector.vue';
```

- [ ] **Step 5: 删除 skill-indicator 的导入和使用**

删除 `import SkillIndicator` 和模板中的 `<skill-indicator>` 标签。

- [ ] **Step 6: 提交**

```bash
git add packages/frontend/src/modules/agent/views/chat.vue
git commit -m "feat(agent): chat.vue 布局重构 - 集成新组件 + token 统计移位"
```

---

## Task 16: 前端 message-item.vue 改造

**Files:**
- Modify: `packages/frontend/src/modules/agent/components/message-item.vue:16-89`

- [ ] **Step 1: 删除旧的 Skill 执行历史区块**

删除约第31-89行的 `<div class="message-item__skills">` 整个区块（skill 历史现在由 skill-card 在 chat.vue 中渲染）。

- [ ] **Step 2: 替换旧的思考区块**

删除约第16-29行的 `<div class="message-item__thinking">` 区块，替换为：

```vue
<!-- 历史消息中的思考内容 -->
<thinking-block
  v-if="message.thinking && message.role === 'assistant'"
  :content="message.thinking"
  :is-streaming="false"
/>

<!-- 插槽：流式消息的 thinking-block 从 chat.vue 传入 -->
<slot name="before-content" />
```

- [ ] **Step 3: 导入 thinking-block**

```typescript
import ThinkingBlock from './thinking-block.vue';
```

- [ ] **Step 4: 提交**

```bash
git add packages/frontend/src/modules/agent/components/message-item.vue
git commit -m "refactor(agent): message-item 移除旧 skill 历史和 thinking 区块，改用新组件"
```

---

## Task 17: 前端 chat-sidebar.vue 适配

**Files:**
- Modify: `packages/frontend/src/modules/agent/components/chat-sidebar.vue:30-59`

- [ ] **Step 1: 在会话项中增加摘要行**

在每个会话项的标题下方（约第31-59行），追加：

```vue
<div class="session-item__subtitle">
  <template v-if="session.todoSummary">
    <span class="session-item__todo-badge">
      📋 {{ session.todoSummary.completed }}/{{ session.todoSummary.total }}
    </span>
  </template>
  <template v-else-if="session.isRunning">
    <span class="session-item__running-dot" />
    <span>执行中...</span>
  </template>
  <template v-else-if="session.lastMessage">
    <span class="session-item__last-msg">{{ session.lastMessage }}</span>
  </template>
</div>
```

- [ ] **Step 2: 添加 CSS**

```css
.session-item__subtitle {
  font-size: 12px;
  color: var(--el-text-color-secondary);
  margin-top: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.session-item__todo-badge {
  font-size: 11px;
}
.session-item__running-dot {
  display: inline-block;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--el-color-success);
  margin-right: 4px;
  animation: pulse 1.5s infinite;
}
```

- [ ] **Step 3: 提交**

```bash
git add packages/frontend/src/modules/agent/components/chat-sidebar.vue
git commit -m "feat(agent): chat-sidebar 会话项增加 todo 进度和运行状态"
```

---

## Task 18: 删除 skill-indicator.vue + 清理

**Files:**
- Delete: `packages/frontend/src/modules/agent/components/skill-indicator.vue`

- [ ] **Step 1: 确认 skill-indicator 不再被引用**

```bash
cd packages/frontend
grep -r "skill-indicator" src/ --include="*.vue" --include="*.ts"
```

预期：无结果（Task 15 已删除所有引用）。

- [ ] **Step 2: 删除文件**

```bash
rm packages/frontend/src/modules/agent/components/skill-indicator.vue
```

- [ ] **Step 3: 全局搜索确认无遗漏引用**

```bash
grep -r "SkillIndicator\|skill-indicator" packages/frontend/src/ --include="*.vue" --include="*.ts"
```

预期：无结果。

- [ ] **Step 4: 提交**

```bash
git add -A packages/frontend/src/modules/agent/components/skill-indicator.vue
git commit -m "refactor(agent): 删除 skill-indicator.vue，已被 skill-card.vue 替代"
```

---

## 验证清单

完成所有 Task 后，执行以下验证：

- [ ] 启动后端 `pnpm --filter @neta/backend dev`，确认无编译错误
- [ ] 启动前端 `pnpm --filter @neta/frontend dev`，确认无编译错误
- [ ] 打开对话页面，发送消息，验证：
  - [ ] Skill 执行中→完成无视觉跳变（同一个卡片，CSS 过渡）
  - [ ] Token 统计显示在输入区右下角，数据来自后端推送
  - [ ] 思考内容流式展示（虚线边框 + 呼吸动画 + 光标闪烁）
  - [ ] 思考完成后自动折叠
  - [ ] 思考级别选择器可切换
  - [ ] Todo 卡片在对话流中实时更新（需 agent 调用 todo 工具）
  - [ ] 切换会话后 todo/skill 状态正确恢复
  - [ ] 会话列表显示 todo 进度

