Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-10
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
## Context

当前 `ModelSelector` 和 Agent mode toggle(✨增强 / 🤖Agent)在 `ChatPage.tsx` 的导航栏区域(line 268-324)渲染,`agentMode` 存在全局 `chatStore` 中。

shadcn/ui 首页的 AI 输入风格采用 `InputGroup` 组件族:控制项(model selector、mode selector)作为 addon 附着在输入框周围,形成统一的输入面板。

Agent Worker 生命周期分析结论:切换 mode 不需要重启 Worker,Worker 一次创建即可处理两种 mode 消息。但 `ENHANCE_MESSAGE` 不会更新 `languageModel`,跨会话时可能用错模型。

## Goals / Non-Goals

**Goals:**
- ModelSelector 从导航栏移至输入面板
- Agent mode toggle 从导航栏移至输入面板
- `agentMode` 从全局 chatStore 迁移到 per-conversation(Conversation 类型)
- 新输入面板采用 shadcn 风格:横排双行(控制行 + 输入行)
- AgentActivityPanel 归入输入面板下方
- `ENHANCE_MESSAGE` 也传入 `providerConfig` + `model`,修复跨会话 model 过期

**Non-Goals:**
- 不改动 ModelSelector 的交互逻辑(dropdown 选 model 的交互保持不变)
- 不改动 Agent mode 切换的业务逻辑(只是位置和数据范围变了)
- 不改动 Worker 内部的 ReAct 循环逻辑
- 不改动导航栏其他元素(mindmap toggle、settings button 保留)

## Decisions

### 决策 1: 横排双行布局

借鉴 shadcn InputGroup 风格:

```
┌─────────────────────────────────────────────────────┐
│ [Model: DeepSeek/V3 ▼] [✨增强 | 🤖Agent] │ ← 控制行
├─────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────┐ [Send] │ ← 输入行
│ │ 输入消息开始对话... │ │
│ └───────────────────────────────────────┘ │
│ ┌─ Agent 活动指示器 ───────────────────────────────┐│
│ └─────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────┘
```

控制行:ModelSelector(左侧)+ Agent mode toggle(右侧),flex row,中间自然分隔。
输入行:textarea + send button,与当前 MessageInput 类似但集成到同一容器。
Agent 活动指示器:在输入面板底部,仅在增强模式且非 idle 时显示。

**替代方案考虑**:
- 单行紧凑(控制项嵌入输入框两侧):会限制 ModelSelector 的宽度,不适合模型名称较长的情况
- 控制项垂直堆叠:占用垂直空间太多

### 决策 2: agentMode 存入 Conversation 类型

```typescript
// Conversation 类型增加字段
interface Conversation {
// ... 现有字段
agentMode?: 'enhance' | 'mediate' // 新增,默认 'enhance'
}
```

**理由**:
- 模型已经是 per-conversation(providerId, modelId),模式跟随一致
- 切换会话时模式自动跟随
- 新建对话时默认 enhance,与当前行为一致
- 不需要额外的映射表

**替代方案**:
- 在 chatStore 中维护 `Map<conversationId, agentMode>`:增加了复杂度,数据不同步(会话删除时需要清理)

### 决策 3: Worker 消息增强

`ENHANCE_MESSAGE` payload 增加 `providerConfig` 和 `model` 字段,与 `MEDIATE_MESSAGE` 对齐:

```typescript
// ENHANCE_MESSAGE payload 增加
payload: {
// ... 现有字段
providerConfig: { apiEndpoint: string; apiKey: string } // 新增
model: string // 新增
}
```

Worker 收到 `ENHANCE_MESSAGE` 时重建 `languageModel`(与 `MEDIATE_MESSAGE` 一致)。

### 决策 4: 新建对话默认 agentMode

新建对话时 `agentMode` 默认 `'enhance'`(保持当前全局默认行为)。用户可在输入面板中切换。

## Risks / Trade-offs

| 风险 | 缓解 |
|------|------|
| 输入面板高度增加(双行) | 控制行高度仅 32px,整体增加不大。且输入框 auto-resize 到多行时不额外占用空间 |
| 控制行在窄屏上换行 | 使用 `flex-wrap` + `gap` 在小屏上自然换行,或直接隐藏 model 全称用缩写 |
| agentActivityPanel 位置变化 | 从全局底部移到输入面板底部,语义上更合理——它是"输入的附属信息" |
| ModelSelector 交互不变但 DOM 位置变了 | 纯 DOM 移动,组件自身逻辑和 store 引用不变 |
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
## Why

当前 ModelSelector(模型选择)和 Agent mode 切换(增强 / Agent)都放在导航栏中,看起来像全局控制项,但模型(providerId/modelId)实际上已经是 per-conversation 存储在 Conversation 类型中。而 Agent mode(enhance/mediate)却是全局状态,存在 chatStore 里,切换会话时不会跟随。

这导致两个问题:
1. **用户心智模型错位**——"选模型"和"切模式"是跟当前对话绑定的操作,用户期望它们出现在输入区域,而不是导航栏
2. **模式不跟随会话**——从对话 A(Agent 模式)切到对话 B 时,模式依然是 Agent,这可能不是用户想要的

此变更将控制项移至输入面板,与 shadcn/ui 的 InputGroup 风格对齐,让每个对话拥有完整的独立配置。

## What Changes

- **BREAKING - 导航栏精简**:从导航栏移除 ModelSelector 和 Agent mode toggle,放入输入面板
- **新的会话输入面板**:重构 MessageInput,整合 ModelSelector + Agent mode toggle + textarea + send 按钮,横排双行布局
- **Agent mode per-conversation**:`agentMode` 从全局 chatStore 迁移到 Conversation 类型
- **Agent activity 面板归入输入面板**:AgentActivityPanel 不再独立于输入区域之外,而是附属于输入面板
- **Worker 模型同步修复**:ENHANCE_MESSAGE 时传入最新 providerConfig + model(当前只有 MEDIATE_MESSAGE 做了重建)

## Capabilities

### New Capabilities

- `chat-input-panel`: 集成 ModelSelector、Agent mode toggle 和 textarea 的统一输入面板组件

### Modified Capabilities

- `agent-orchestration`: Agent mode 从全局改为 per-conversation,模式切换的行为跟随会话切换

## Impact

- **UI 组件**:MessageInput → 重构为 ChatInputPanel,ModelSelector 和 mode toggle 嵌入其中
- **类型系统**:`Conversation` 增加 `agentMode?: 'enhance' | 'mediate'`
- **Store**:`chatStore` 移除 `agentMode` 相关字段,`conversationStore` 处理 per-conversation 模式
- **Worker通信**:`ENHANCE_MESSAGE` 增加 `providerConfig` 和 `model` 字段(修复跨会话 model 过期)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
## MODIFIED Requirements

### Requirement: Agent supports dual operation modes

The system SHALL support two agent modes: **enhance** (Agent operates in background) and **mediate** (Agent handles the conversation directly).

Agent mode SHALL be per-conversation, not global. Each conversation independently stores and remembers its agent mode setting.

#### Scenario: User switches agent mode for a conversation
- **WHEN** user clicks the mode toggle in the input panel from "enhance" to "mediate"
- **THEN** the conversation's `agentMode` is updated to `'mediate'`
- **AND** the toggle indicator updates to show the current mode for that conversation

#### Scenario: Conversation remembers its agent mode across switches
- **WHEN** user switches from conversation A to conversation B and back to conversation A
- **THEN** conversation A retains its previously selected agent mode
- **AND** conversation B retains its own previously selected agent mode (which may differ from A)

#### Scenario: New conversation defaults to enhance mode
- **WHEN** a new conversation is created
- **THEN** its `agentMode` SHALL default to `'enhance'`

#### Scenario: User sends message in enhance mode
- **WHEN** user sends a message in enhance mode
- **THEN** the message is sent to the AI provider via `streamChat()`
- **AND** the AI response streams token-by-token to the MessageBubble
- **AND** when streaming completes, the Agent Worker receives an `ENHANCE_MESSAGE` event

#### Scenario: User sends message in mediate mode
- **WHEN** user sends a message in mediate mode
- **THEN** the message is sent to the Agent Worker via `MEDIATE_MESSAGE` event
- **AND** the Agent Worker begins its reasoning loop (analyze → tool calls → synthesize → respond)

### Requirement: Agent activity is visible to user

The system SHALL display Agent status information so the user knows what the Agent is doing.

#### Scenario: Agent activity indicator shows during background enhancement
- **WHEN** the Agent Worker is processing an `ENHANCE_MESSAGE` request
- **THEN** the status indicator appears at the bottom of the input panel
- **AND** the status updates in real-time as the Agent progresses through steps (thinking / reading_mindmap / generating_mindmap / complete)

#### Scenario: Agent activity indicator hides when idle
- **WHEN** the Agent has no pending work and status is `idle`
- **THEN** the status indicator is not rendered

## ADDED Requirements

### Requirement: Agent Worker uses latest conversation configuration

The Agent Worker SHALL use the conversation's latest provider and model configuration when processing messages, to support per-conversation model selection.

#### Scenario: ENHANCE_MESSAGE uses active conversation's provider and model
- **WHEN** the Agent Worker receives an `ENHANCE_MESSAGE`
- **THEN** the message payload SHALL include `providerConfig` and `model` fields
- **AND** the Worker SHALL recreate its `languageModel` with these values before processing
- **AND** this ensures the Worker uses the correct model even when switching between conversations with different providers
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## ADDED Requirements

### Requirement: Integrated chat input panel

The system SHALL provide an integrated chat input panel that combines model selection, agent mode selection, and message input in a single visual block.

#### Scenario: Input panel displays below message list
- **WHEN** a conversation is active and a provider is configured
- **THEN** the input panel SHALL render below the message list
- **AND** the input panel SHALL contain: a control row (model selector + agent mode toggle) and an input row (textarea + send button)
- **AND** the input panel SHALL use a two-row layout as its default presentation

#### Scenario: Input panel displays control row
- **WHEN** the input panel is rendered
- **THEN** the control row SHALL display the ModelSelector on the left side
- **AND** the control row SHALL display the Agent mode toggle on the right side
- **AND** both controls SHALL be visually distinct from the input row below

### Requirement: Input panel is conversation-bound

The input panel's model and agent mode values SHALL reflect the active conversation's configuration, not a global setting.

#### Scenario: Switching conversations updates input panel
- **WHEN** the user switches to a different conversation
- **THEN** the ModelSelector SHALL update to show the target conversation's provider/model
- **AND** the Agent mode toggle SHALL update to show the target conversation's agent mode
- **AND** the message input SHALL clear (waiting for new input)

### Requirement: Agent activity indicator in input panel

The system SHALL display the Agent status indicator as part of the input panel, below the input row.

#### Scenario: Agent activity shows during enhancement
- **WHEN** the Agent Worker is processing an `ENHANCE_MESSAGE` request
- **THEN** a status indicator SHALL appear at the bottom of the input panel
- **AND** it SHALL display the current Agent status (thinking / reading_mindmap / generating_mindmap / complete / error)

#### Scenario: Agent activity hides when idle
- **WHEN** the Agent has no pending work (status is `idle`)
- **THEN** no status indicator SHALL be shown in the input panel
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
## 1. Conversation Type — agentMode 字段

- [x] 1.1 Add `agentMode?: 'enhance' | 'mediate'` field to `Conversation` interface in `src/types/conversation.ts`
- [x] 1.2 Verify no TypeScript errors from the new optional field

## 2. ConversationStore — per-conversation agentMode

- [x] 2.1 In `addConversation()`, set default `agentMode: 'enhance'` for new conversations
- [x] 2.2 Export `setConversationAgentMode` action: `updateConversation(id, { agentMode })` (reuses existing `updateConversation` — just a store slice export if needed)
- [x] 2.3 Verify persisted store handles the new field correctly (no migration needed since it's optional)

## 3. ChatStore — 移除全局 agentMode

- [x] 3.1 Remove `agentMode`, `setAgentMode` from `ChatState` interface
- [x] 3.2 Remove related implementations in `create<ChatState>()`
- [x] 3.3 Keep `agentStatus` and `agentMessage` (these remain global — they're UI state, not conversation config)

## 4. Worker 消息类型扩展

- [x] 4.1 Add `providerConfig` and `model` fields to `ENHANCE_MESSAGE` payload in `src/lib/agent/types.ts`:

```typescript
// ENHANCE_MESSAGE payload 增加
providerConfig: { apiEndpoint: string; apiKey: string }
model: string
```

## 5. useMindmapAgent — enhanceMessage 传入 providerConfig

- [x] 5.1 In `enhanceMessage()`, read active conversation's provider + model from store
- [x] 5.2 Pass `providerConfig` and `model` in the `ENHANCE_MESSAGE` payload

## 6. agent.worker — ENHANCE_MESSAGE 重建 languageModel

- [x] 6.1 In `case 'ENHANCE_MESSAGE'`, before processing, recreate `languageModel` from `msg.payload.providerConfig` + `msg.payload.model` (same pattern as `MEDIATE_MESSAGE` lines 270-275)

## 7. ChatInputPanel — 新的集成输入面板组件

- [x] 7.1 Create `src/features/chat/ChatInputPanel.tsx` — new component that composes:
- Control row: `ModelSelector` (left) + agent mode toggle (right) — horizontal flex
- Input row: `textarea` + send/stop button (adapted from current `MessageInput`)
- Bottom row: `AgentActivityPanel` (only in enhance mode & non-idle)
- [x] 7.2 Adopt shadcn InputGroup styling: border container, rounded corners, subtle shadow
- [x] 7.3 Agent mode toggle: two `<button>` elements ("✨ 增强" / "🤖 Agent") with `stopGeneration()` on switch — same logic as current, just relocated
- [x] 7.4 Input textarea: auto-resize (`field-sizing-content` or JS), Enter to send, Shift+Enter newline
- [x] 7.5 Send/stop button: right-aligned in input row
- [x] 7.6 The component reads `activeConversation.agentMode` instead of `chatStore.agentMode` for toggle state
- [x] 7.7 Toggle calls `updateConversation(id, { agentMode: newMode })` instead of `setAgentMode()`

## 8. ChatPage — 导航栏精简 + 输入面板替换

- [x] 8.1 Remove `<ModelSelector />` from navbar (line 280 in current `ChatPage.tsx`)
- [x] 8.2 Remove agent mode toggle `<button>` elements from navbar (lines 282-294)
- [x] 8.3 Remove `agentMode` and `setAgentMode` imports from chatStore
- [x] 8.4 Replace `<MessageInput>` usage with new `<ChatInputPanel>`
- [x] 8.5 Pass necessary props: `onSend`, `onStop`, `isGenerating`, `disabled`
- [x] 8.6 Move `AgentActivityPanel` rendering inside `ChatInputPanel` (remove from ChatPage)

## 9. ModelSelector — 适配嵌入使用

- [x] 9.1 No functional changes needed (already reads from conversationStore)
- [x] 9.2 Consider visual polish: compact variant for narrower space in control row (optional)

## 10. 清理旧代码

- [x] 10.1 Remove `src/features/chat/MessageInput.tsx` (replaced by `ChatInputPanel`)
- [x] 10.2 Remove `AgentActivityPanel` inline usage from `ChatPage.tsx`
- [x] 10.3 Clean up unused imports in `ChatPage.tsx`

## 11. 验证

- [x] 11.1 `npx tsc --noEmit` — no type errors
- [x] 11.2 `npm test` — all tests pass (73 passed)
- [x] 11.3 `npm run build` — production build succeeds
- [ ] 11.4 Manual test: Create conversation A, switch to Agent mode → switch to conversation B, verify it's in enhance mode → switch back to A, verify it's still Agent mode
- [ ] 11.5 Manual test: Switch model while in enhance mode, send message, verify Agent enhancement uses the new model
Loading
Loading