Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
949c931
refactor(agent): add presenter lifecycle
zerob13 Jan 1, 2026
3f1df7e
fix(presenter): prioritize conversation chatMode over global config
zerob13 Jan 1, 2026
fa80f01
refactor(agent-presenter): migrate chat flow
zerob13 Jan 1, 2026
ae4ef7f
fix(agent-presenter): sync types and trace loading
zerob13 Jan 1, 2026
e7c33da
fix(agent-presenter): use agent id for trace
zerob13 Jan 1, 2026
304b431
refactor(agent-presenter): centralize session lifecycle
zerob13 Jan 1, 2026
dc11d36
test(tool-presenter): pass command permission handler
zerob13 Jan 1, 2026
c04ee43
refactor(agent-presenter): align naming
zerob13 Jan 1, 2026
fd3652e
refactor: remove llama cpp presenter
zerob13 Jan 1, 2026
417fa00
refactor(session): initialize sessionPresenter architecture with core…
zerob13 Jan 1, 2026
29174a7
refactor(session): create persistence layer with ConversationPersiste…
zerob13 Jan 1, 2026
0910d9c
refactor(session): add TabManager for Session-Tab binding management
zerob13 Jan 1, 2026
8543c70
refactor(session): fix SessionPresenter facade and loop module imports
zerob13 Jan 1, 2026
839e21c
refactor(session): implement getDefaultAgentWorkspacePath in SessionM…
zerob13 Jan 1, 2026
2f23d70
refactor(session): implement message sending and loop control in Sess…
zerob13 Jan 1, 2026
be5e74e
refactor(session): move ACP agent modules from llmProviderPresenter t…
zerob13 Jan 1, 2026
05e45c8
refactor(session): remove and document threadPresenter dependencies
zerob13 Jan 1, 2026
c1e5cfc
refactor(session): add TODO comment for full stream handling integration
zerob13 Jan 1, 2026
19155a3
refactor(session): implement full AsyncGenerator stream handling in s…
zerob13 Jan 1, 2026
96c93f0
refactor(presenter): migrate session modules
zerob13 Jan 1, 2026
8dffdc3
chore(search): remove unused artifacts template
zerob13 Jan 1, 2026
622f1c7
refactor(sessionPresenter): drop unused manager
zerob13 Jan 2, 2026
694c207
refactor(agentPresenter): move acp module
zerob13 Jan 2, 2026
55f5a11
refactor(agentPresenter): move loop module
zerob13 Jan 2, 2026
45e5c39
refactor(agentPresenter): move streaming module
zerob13 Jan 2, 2026
c2f5b6e
refactor(agentPresenter): move permission handler
zerob13 Jan 2, 2026
0ba982f
refactor(agentPresenter): move tool module
zerob13 Jan 2, 2026
a3425e0
refactor(agentPresenter): move message module
zerob13 Jan 2, 2026
b7a71ab
refactor(agentPresenter): move utility module
zerob13 Jan 2, 2026
5315805
refactor(presenter): split agent/session roles
zerob13 Jan 2, 2026
aec0dab
docs(presenter): note session proxy binding
zerob13 Jan 2, 2026
d3b9a67
refactor(presenter): share message manager
zerob13 Jan 2, 2026
c118725
refactor(renderer): use session presenter
zerob13 Jan 2, 2026
a9341b2
refactor(acp): expand barrel exports
zerob13 Jan 2, 2026
53a907a
refactor(acp): update import paths
zerob13 Jan 2, 2026
71e1e68
refactor(acp): remove duplicate agent files
zerob13 Jan 2, 2026
9bf8d58
docs(acp): update ACP path references
zerob13 Jan 2, 2026
45b1689
docs(docs): reorganize documentation structure
zerob13 Jan 2, 2026
23d460c
refactor(agent-loop): consolidate loop handlers
zerob13 Jan 2, 2026
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
79 changes: 79 additions & 0 deletions docs/agent-presenter-refactoring-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# AgentPresenter 重构状态(当前架构与时限)

更新时间:2026-01-01

## 当前架构(已落地部分)

### 已接入的核心模块
- `src/main/presenter/agentPresenter/`
- Facade:`index.ts`(对外 IPC 入口,统一 send/continue/cancel/permission/preview)
- Message:`messageBuilder` / `messageFormatter` / `messageTruncator` / `messageCompressor`
- Tool:`toolCallCenter` / `toolRegistry` / `toolRouter`
- Session:`sessionResolver` / `sessionContext` / `sessionManager`

### 运行链路(当前态)
1. Renderer 通过 `agentPresenter` 发起发送/继续/取消/权限/预览等操作。
2. Main 内部仍由 `ThreadPresenter` 承接生成与事件处理(stream + permission)。
3. Message 构建与 token 预算统一由 `agentPresenter/message/*` 模块提供。

### 仍在 `ThreadPresenter` 中的职责
- loop 编排与 stream 消费(`streamGenerationHandler` / `llmEventHandler`)
- permission 处理(`permissionHandler`)
- conversation/message 的 CRUD 与事件分发

## 完成情况(按 Phase 标记)

### Phase 0:类型与脚手架
- [x] `ChatMessage` 统一定义与引用修复
- [x] `agentPresenter` 目录树与空实现
- [x] 基础单测骨架

### Phase 1:Facade 接入
- [x] `agentPresenter` 初始化与 IPC 暴露
- [x] `send/continue/cancel/permission/preview` 最小接口
- [x] `SessionContext.resolved` 调试输出

### Phase 2:Message 层拆分
- [x] `messageFormatter / messageTruncator / messageCompressor / promptEnhancer`
- [x] `messageBuilder` 拆分并接入旧流程
- [x] 单测覆盖核心分支

### Phase 3:ToolCallCenter 收口
- [x] `ToolCallCenter` 薄封装 `ToolPresenter`
- [x] message 构建时统一使用 tool definitions
- [x] tool 去重与 jsonrepair 回退单测

### Phase 4:SessionManager 与状态收口
- [x] `SessionContext.resolved` 基础规则落地
- [x] `agentWorkspacePath` 默认生成与持久化
- [x] 运行时状态迁入 `SessionManager`
- [x] 替换重复的 mode/workspace 决策

### Phase 5:Loop async/await 重写
- [x] `loopOrchestrator` 事件消费层
- [x] `agentLoopHandler` 自驱动 loop
- [x] 三模式回归与性能对照

### Phase 6:Renderer 迁移与清理
- [x] Chat 主流程切换至 `agentPresenter`
- [x] utilities / search / conversation 管理迁移
- [x] 移除 `ThreadPresenter` 入口(renderer/IPC),旧模块内部保留用于委托

## 当前时限(滚动计划)

### 近期(1-2 周)
- Phase 4:SessionManager 状态收口 + workspace 默认路径持久化(已完成)
- 补齐 Phase 3 测试(tool 去重 / jsonrepair)(已完成)

### 中期(2-4 周)
- Phase 5:loop 重写与回归矩阵(已完成)

### 收尾(1-2 周)
- Phase 6:renderer 迁移剩余模块 + 清理旧入口(已完成)

## 变更记录(本轮)
- Renderer chat 流程迁移到 `agentPresenter`(send/continue/cancel/permission/preview)
- 删除 `threadPresenter/utils` 的旧包装层
- SessionManager 统一 workspace 生成/持久化与运行时状态
- LoopOrchestrator 收口 stream 事件消费
- Renderer 侧 IPC 入口统一到 `agentPresenter`
1 change: 1 addition & 0 deletions src/main/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface ImportMetaEnv {
readonly VITE_GITHUB_CLIENT_SECRET: string
readonly VITE_GITHUB_REDIRECT_URI: string
readonly VITE_LOG_IPC_CALL: string
readonly VITE_AGENT_PRESENTER_DEBUG?: string
readonly VITE_APP_LIFECYCLE_HOOK_DELAY?: string
readonly VITE_PROVIDER_DB_URL?: string
}
Expand Down
1 change: 1 addition & 0 deletions src/main/presenter/agentPresenter/agent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {}
1 change: 1 addition & 0 deletions src/main/presenter/agentPresenter/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {}
128 changes: 128 additions & 0 deletions src/main/presenter/agentPresenter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type { IAgentPresenter, IThreadPresenter } from '@shared/presenter'
import type { AssistantMessage } from '@shared/chat'
import { eventBus, SendTarget } from '@/eventbus'
import { STREAM_EVENTS } from '@/events'
import type { SessionContextResolved } from './session/sessionContext'
import type { SessionManager } from './session/sessionManager'

type AgentPresenterDependencies = {
threadPresenter: IThreadPresenter
sessionManager: SessionManager
}

export class AgentPresenter implements IAgentPresenter {
private threadPresenter: IThreadPresenter
private sessionManager: SessionManager

constructor({ threadPresenter, sessionManager }: AgentPresenterDependencies) {
this.threadPresenter = threadPresenter
this.sessionManager = sessionManager
this.bindThreadPresenterMethods()
}

async sendMessage(
agentId: string,
content: string,
_tabId?: number,
selectedVariantsMap?: Record<string, string>
): Promise<AssistantMessage | null> {
await this.logResolvedIfEnabled(agentId)
const assistantMessage = await this.threadPresenter.sendMessage(agentId, content, 'user')
if (!assistantMessage) {
return null
}

await this.sessionManager.startLoop(agentId, assistantMessage.id)
void this.threadPresenter
.startStreamCompletion(agentId, assistantMessage.id, selectedVariantsMap)
.catch((error) => {
console.error('[AgentPresenter] Failed to start stream completion:', error)
eventBus.sendToRenderer(STREAM_EVENTS.ERROR, SendTarget.ALL_WINDOWS, {
eventId: assistantMessage.id
})
})

return assistantMessage
}

async continueLoop(
agentId: string,
messageId: string,
selectedVariantsMap?: Record<string, string>
): Promise<void> {
await this.logResolvedIfEnabled(agentId)
await this.sessionManager.startLoop(agentId, messageId)
await this.threadPresenter.continueStreamCompletion(agentId, messageId, selectedVariantsMap)
}

async cancelLoop(messageId: string): Promise<void> {
try {
const message = await this.threadPresenter.getMessage(messageId)
if (message) {
this.sessionManager.updateRuntime(message.conversationId, { userStopRequested: true })
this.sessionManager.setStatus(message.conversationId, 'paused')
}
} catch (error) {
console.warn('[AgentPresenter] Failed to update session state for cancel:', error)
}
await this.threadPresenter.stopMessageGeneration(messageId)
}

async handlePermissionResponse(
messageId: string,
toolCallId: string,
granted: boolean,
permissionType: 'read' | 'write' | 'all' | 'command',
remember?: boolean
): Promise<void> {
await this.threadPresenter.handlePermissionResponse(
messageId,
toolCallId,
granted,
permissionType,
remember
)
}

async getMessageRequestPreview(agentId: string, messageId?: string): Promise<unknown> {
if (!messageId) {
return null
}
await this.logResolvedIfEnabled(agentId)
return this.threadPresenter.getMessageRequestPreview(messageId)
}

private shouldLogResolved(): boolean {
return import.meta.env.VITE_AGENT_PRESENTER_DEBUG === '1'
}

private async logResolvedIfEnabled(agentId: string): Promise<void> {
if (!this.shouldLogResolved()) {
return
}
try {
const resolved = await this.resolveSession(agentId)
console.log('[AgentPresenter] SessionContext.resolved', { agentId, resolved })
} catch (error) {
console.warn('[AgentPresenter] Failed to resolve session context', { agentId, error })
}
}

private async resolveSession(agentId: string): Promise<SessionContextResolved> {
const session = await this.sessionManager.getSession(agentId)
return session.resolved
}

private bindThreadPresenterMethods(): void {
const threadPresenter = this.threadPresenter as unknown as Record<string, unknown>
const threadProto = Object.getPrototypeOf(threadPresenter) as Record<string, unknown>
for (const key of Object.getOwnPropertyNames(threadProto)) {
if (key === 'constructor') continue
if (key in this) continue
const value = threadPresenter[key]
if (typeof value === 'function') {
;(this as Record<string, unknown>)[key] = value.bind(this.threadPresenter)
}
}
}
}
1 change: 1 addition & 0 deletions src/main/presenter/agentPresenter/loop/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {}
24 changes: 24 additions & 0 deletions src/main/presenter/agentPresenter/loop/loopOrchestrator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { LLMAgentEvent, LLMAgentEventData } from '@shared/presenter'

type LLMEventConsumer = {
handleLLMAgentResponse: (msg: LLMAgentEventData) => Promise<void>
handleLLMAgentError: (msg: LLMAgentEventData) => Promise<void>
handleLLMAgentEnd: (msg: LLMAgentEventData) => Promise<void>
}

export class LoopOrchestrator {
constructor(private readonly consumer: LLMEventConsumer) {}

async consume(stream: AsyncGenerator<LLMAgentEvent, void, unknown>): Promise<void> {
for await (const event of stream) {
const msg = event.data
if (event.type === 'response') {
await this.consumer.handleLLMAgentResponse(msg)
} else if (event.type === 'error') {
await this.consumer.handleLLMAgentError(msg)
} else if (event.type === 'end') {
await this.consumer.handleLLMAgentEnd(msg)
}
}
}
}
1 change: 1 addition & 0 deletions src/main/presenter/agentPresenter/message/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {}
Loading
Loading