diff --git a/openspec/changes/archive/2026-05-10-conversation-bound-controls/.openspec.yaml b/openspec/changes/archive/2026-05-10-conversation-bound-controls/.openspec.yaml new file mode 100644 index 0000000..ac20efa --- /dev/null +++ b/openspec/changes/archive/2026-05-10-conversation-bound-controls/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-10 diff --git a/openspec/changes/archive/2026-05-10-conversation-bound-controls/design.md b/openspec/changes/archive/2026-05-10-conversation-bound-controls/design.md new file mode 100644 index 0000000..4f88f3a --- /dev/null +++ b/openspec/changes/archive/2026-05-10-conversation-bound-controls/design.md @@ -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`:增加了复杂度,数据不同步(会话删除时需要清理) + +### 决策 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 引用不变 | diff --git a/openspec/changes/archive/2026-05-10-conversation-bound-controls/proposal.md b/openspec/changes/archive/2026-05-10-conversation-bound-controls/proposal.md new file mode 100644 index 0000000..dba7958 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-conversation-bound-controls/proposal.md @@ -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 过期) diff --git a/openspec/changes/archive/2026-05-10-conversation-bound-controls/specs/agent-orchestration/spec.md b/openspec/changes/archive/2026-05-10-conversation-bound-controls/specs/agent-orchestration/spec.md new file mode 100644 index 0000000..cd11543 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-conversation-bound-controls/specs/agent-orchestration/spec.md @@ -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 diff --git a/openspec/changes/archive/2026-05-10-conversation-bound-controls/specs/chat-input-panel/spec.md b/openspec/changes/archive/2026-05-10-conversation-bound-controls/specs/chat-input-panel/spec.md new file mode 100644 index 0000000..0beee09 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-conversation-bound-controls/specs/chat-input-panel/spec.md @@ -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 diff --git a/openspec/changes/archive/2026-05-10-conversation-bound-controls/tasks.md b/openspec/changes/archive/2026-05-10-conversation-bound-controls/tasks.md new file mode 100644 index 0000000..105ad14 --- /dev/null +++ b/openspec/changes/archive/2026-05-10-conversation-bound-controls/tasks.md @@ -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()` +- [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 ` + + + + + {/* 输入行 — textarea + send/stop */} +
+