diff --git a/docs/architecture/agent-memory-system/spec.md b/docs/architecture/agent-memory-system/spec.md index 9d22c9b0f..685178c17 100644 --- a/docs/architecture/agent-memory-system/spec.md +++ b/docs/architecture/agent-memory-system/spec.md @@ -697,8 +697,8 @@ flowchart TD - **`tape_context`** expands specific entry ids with a bounded window (before/after default 2, clamp ≤ 20), an entry cap (default 50), and byte budgets (per-entry default 2048 / max 8192; total default 16384 / max 65536), UTF-8-safe truncated. Tape tools are DeepChat-agent-only and `tape_context` is advertised only when - the tool-runtime port exposes `getTapeContext` (wired to `AgentSessionPresenter`, not the memory kernel; in - practice always present). + the tool-runtime port exposes `getTapeContext` (wired directly to `SessionProjectionCoordinator`, not the + memory kernel; in practice always present). - The whole projection/search layer is fail-open: any error degrades to a coarser search over the effective tape rather than throwing (the one exception is an unparseable time boundary, which is reported). diff --git a/docs/architecture/agent-system-layered-runtime/README.md b/docs/architecture/agent-system-layered-runtime/README.md index 943399e81..253e9a6f4 100644 --- a/docs/architecture/agent-system-layered-runtime/README.md +++ b/docs/architecture/agent-system-layered-runtime/README.md @@ -38,11 +38,12 @@ DeepChat loop + `AcpProvider` compatibility。 Memory runtime orchestration 已收敛到唯一 `MemoryRuntimeCoordinator`,通过 awaited `MemoryPromptContributor` 与 background `MemoryIngestionObserver` 接入;Tape tool facts 已迁到 stable -per-fact `TapeRecorder` path,causal observation 只读联结现有 Tape/message/trace。`AgentSessionPresenter` -仅保留 compatibility forwarding;core lifecycle、turn、assignment 与 projection 已由四个 -composition-owned session application coordinators 承担。typed routes 直接组合 history、translation、 -export、usage、RTK 与 catalog owner,startup hooks 直接调用 migration/maintenance owner。 -`AgentRuntimePresenter` 保留 DeepChat state/delegate 与 adapter wiring;两者不再构成 generic agent runtime。 +per-fact `TapeRecorder` path,causal observation 只读联结现有 Tape/message/trace。core lifecycle、turn、 +assignment 与 projection 已由四个 composition-owned session application coordinators 承担; +`AgentSessionPresenter` 与 main-process `IAgentSessionPresenter` 已退休。typed routes、Tool、MCP、Floating +与 hooks 直接使用分离的 coordinator ports;history、translation、export、usage、RTK 与 catalog owner +继续独立。startup hooks 直接调用 migration/maintenance owner。`AgentRuntimePresenter` 保留 DeepChat +state/delegate 与 adapter wiring,不再构成 generic agent runtime。 current docs、architecture guards 与 baseline generator 已在 `ASLR-091` 收敛;`ASLR-092` 已完成 canonical baseline write、全量 main/renderer/Memory/native/build/E2E gates 与最终契约 diff。 @@ -173,7 +174,7 @@ accept input / claim pending item typed routes -> SessionService / ChatService -- narrow ports ─┐ remote -> RemoteConversationRunner -- four narrow ports ─────┤ cron -> Cron session starter -- Lifecycle / Turn ports ──────┤ -AgentSessionPresenter compatibility façade -- forwarding ─────┘ +Tool / MCP / Floating / hooks -- owner-specific ports ────────┘ ▼ Lifecycle / Turn / AgentAssignment / Projection ├─ AppSessionService ─ new_sessions / window binding / shared CRUD @@ -275,7 +276,6 @@ src/main/agent/ src/main/presenter/ ├── sessionApplication/ # lifecycle/turn/assignment/projection coordinators -├── agentSessionPresenter/ # retained compatibility façade └── agentRuntimePresenter/ # retained DeepChat state/delegate + message/Tape/resource adapters ``` diff --git a/docs/architecture/agent-system-layered-runtime/modules/shared-data-and-io.md b/docs/architecture/agent-system-layered-runtime/modules/shared-data-and-io.md index 0fe96c680..020b2d0b9 100644 --- a/docs/architecture/agent-system-layered-runtime/modules/shared-data-and-io.md +++ b/docs/architecture/agent-system-layered-runtime/modules/shared-data-and-io.md @@ -103,8 +103,8 @@ backend kind 每次由 `agent_id -> AgentDescriptor` 解析。backend 不能复 ### ASLR-072 shared data ports -production switch 没有把 shared data methods 塞进 `AgentManager`。`AgentSessionPresenter` 与 direct ACP -backend 通过四个独立 facet 访问现有 owner: +production switch 没有把 shared data methods 塞进 `AgentManager`。session application coordinators 与 +direct ACP backend 通过四个独立 facet 访问现有 owner: ```ts interface AgentSharedDataPorts { diff --git a/docs/architecture/agent-system.md b/docs/architecture/agent-system.md index c4ea5be8c..66892ecea 100644 --- a/docs/architecture/agent-system.md +++ b/docs/architecture/agent-system.md @@ -34,7 +34,8 @@ flowchart TD UI["Renderer / typed routes"] --> RouteOwners["Session route owners
search / translation / export / usage / catalog"] UI --> Services["SessionService / ChatService"] Services --> App["Session application coordinators"] - Compat["AgentSessionPresenter
compatibility forwarding"] --> App + UI --> RoutePorts["Main route runtime
four separate session ports"] + RoutePorts --> App App --> Sessions["AppSessionService"] App --> Manager["AgentManager"] Manager --> Catalog["strict executable catalog"] @@ -58,8 +59,8 @@ flowchart TD - `SessionLifecycleCoordinator`、`SessionTurnCoordinator`、`SessionAgentAssignmentCoordinator` 和 `SessionProjectionCoordinator` 分别拥有 core session application invariants;typed Session/Chat、Remote 和 Cron 通过 consumer-owned narrow ports 调用它们。 -- `AgentSessionPresenter` 仅保留 compatibility public surface;core session methods 只转发到 - composition-owned coordinators,不拥有 application policy、state 或 session-boundary capabilities。 +- `AgentSessionPresenter` 与 main-process `IAgentSessionPresenter` 已退休;main route runtime、Tool、MCP、 + Floating 与 hooks 直接调用 composition-owned coordinators,不经过 aggregate façade。 - typed session routes 直接组合 `SessionHistorySearch`、`SessionTranslation`、 `AgentSessionExportService`、`UsageStatsService`、RTK runtime service 与 available-agent catalog policy; lifecycle hooks 直接调用 startup migration/maintenance owner。 @@ -173,7 +174,7 @@ DeepChat app projection;`acp_turns` 只是 protocol metadata。 仍保留: -- `AgentSessionPresenter` compatibility façade 和 `AgentRuntimePresenter` state/delegate façade; +- `AgentRuntimePresenter` state/delegate façade; - `AcpProvider` 的 DeepChat + ACP-provider compatibility; - `startupMigrations/LegacyChatImportService`、旧 conversations/messages、`SessionPresenter` export/thread/data compatibility;current agent-session export 由 `AgentSessionExportService` 拥有; @@ -181,6 +182,7 @@ DeepChat app projection;`acp_turns` 只是 protocol metadata。 已经退休并由 guard 阻止回流: +- `AgentSessionPresenter` compatibility façade 与 main-process `IAgentSessionPresenter`; - fake `AgentRegistry`; - unified optional implementation interface; - reflection-based legacy backend; @@ -193,7 +195,7 @@ DeepChat app projection;`acp_turns` 只是 protocol metadata。 1. kind/session routing:`src/main/agent/manager/agentManager.ts` 2. core session application behavior:`src/main/presenter/sessionApplication/` -3. compatibility forwarding:`src/main/presenter/agentSessionPresenter/index.ts` +3. route composition:`src/main/routes/index.ts` 4. session search/translation:`src/main/routes/sessions/` 5. current session export:`src/main/presenter/exporter/agentSessionExporter.ts` 6. usage/startup/catalog:`src/main/presenter/{usageStatsService.ts,startupMigrations/}` 与 diff --git a/docs/architecture/baselines/agent-system-layered-runtime-baseline.json b/docs/architecture/baselines/agent-system-layered-runtime-baseline.json index 05a62c13a..0be9916f4 100644 --- a/docs/architecture/baselines/agent-system-layered-runtime-baseline.json +++ b/docs/architecture/baselines/agent-system-layered-runtime-baseline.json @@ -1,7 +1,7 @@ { "schemaVersion": 2, "goal": "agent-system-layered-runtime", - "headCommit": "1122b240619afae1bd97c2395f0c820c41ecf048", + "headCommit": "30d57c3873cec94e342ce616d8ccafe9f71676b3", "relevantWorkingTree": { "dirty": false, "files": [] @@ -95,9 +95,12 @@ "src/main/presenter/agentRuntimePresenter/messageStore.ts", "src/main/presenter/agentRuntimePresenter/process.ts", "src/main/presenter/agentRuntimePresenter/tapeService.ts", - "src/main/presenter/agentSessionPresenter/index.ts", "src/main/presenter/index.ts", - "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts" + "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts", + "src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts", + "src/main/presenter/sessionApplication/lifecycleCoordinator.ts", + "src/main/presenter/sessionApplication/projectionCoordinator.ts", + "src/main/presenter/sessionApplication/turnCoordinator.ts" ], "expectedFiles": { "src/main/agent/acp/instance/acpAgentInstance.ts": true, @@ -121,9 +124,12 @@ "src/main/presenter/agentRuntimePresenter/messageStore.ts": true, "src/main/presenter/agentRuntimePresenter/process.ts": true, "src/main/presenter/agentRuntimePresenter/tapeService.ts": true, - "src/main/presenter/agentSessionPresenter/index.ts": true, "src/main/presenter/index.ts": true, - "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts": true + "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts": true, + "src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts": true, + "src/main/presenter/sessionApplication/lifecycleCoordinator.ts": true, + "src/main/presenter/sessionApplication/projectionCoordinator.ts": true, + "src/main/presenter/sessionApplication/turnCoordinator.ts": true }, "ownerEvidence": { "agentManager": { @@ -186,8 +192,23 @@ "exists": true, "declarationCount": 1 }, - "retainedAgentSessionFacade": { - "file": "src/main/presenter/agentSessionPresenter/index.ts", + "sessionProjectionCoordinator": { + "file": "src/main/presenter/sessionApplication/projectionCoordinator.ts", + "exists": true, + "declarationCount": 1 + }, + "sessionAgentAssignmentCoordinator": { + "file": "src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts", + "exists": true, + "declarationCount": 1 + }, + "sessionTurnCoordinator": { + "file": "src/main/presenter/sessionApplication/turnCoordinator.ts", + "exists": true, + "declarationCount": 1 + }, + "sessionLifecycleCoordinator": { + "file": "src/main/presenter/sessionApplication/lifecycleCoordinator.ts", "exists": true, "declarationCount": 1 }, @@ -201,10 +222,13 @@ "paths": { "src/main/agent/manager/legacyAgentBackends.ts": 0, "src/main/lib/agentRuntime": 0, - "src/main/presenter/agentSessionPresenter/agentRegistry.ts": 0 + "src/main/presenter/agentSessionPresenter": 0, + "src/shared/types/presenters/agent-session.presenter.d.ts": 0 }, "symbols": { "AgentRegistry": 0, + "AgentSessionPresenter": 0, + "IAgentSessionPresenter": 0, "IAgentImplementation": 0, "createLegacyAgentBackend": 0, "LegacyDeepChatSessionBackend": 0, @@ -238,9 +262,12 @@ "src/main/presenter/agentRuntimePresenter/messageStore.ts", "src/main/presenter/agentRuntimePresenter/process.ts", "src/main/presenter/agentRuntimePresenter/tapeService.ts", - "src/main/presenter/agentSessionPresenter/index.ts", "src/main/presenter/index.ts", - "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts" + "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts", + "src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts", + "src/main/presenter/sessionApplication/lifecycleCoordinator.ts", + "src/main/presenter/sessionApplication/projectionCoordinator.ts", + "src/main/presenter/sessionApplication/turnCoordinator.ts" ] }, "contracts": { @@ -407,7 +434,7 @@ "providers", "settings_activity" ], - "sha256": "ca9306d19cb9066d6447268aac2e9485ca867fc65f89abda5a1e4b54d4f21974" + "sha256": "16882d6b4c22ec84e0e28c4eb7f5aeb7a867503648e67eeb42461f4f838fe198" }, "memoryDuckDbSidecar": { "files": [ @@ -428,7 +455,7 @@ "src/main/presenter/lifecyclePresenter/hooks/beforeQuit/presenterDestroyHook.ts", "src/main/presenter/lifecyclePresenter/index.ts" ], - "sha256": "90836b4d0ef4078293ac7d001ef2992f572809a10c67d35fdd8a972e8a66fb86" + "sha256": "d42c0862ef9e41fa77cb9296dcb625f5559b5ae594bd1fbba3ba248b2a4e5203" }, "dependencyMetrics": { "loopFiles": [ diff --git a/docs/architecture/baselines/archive-reference-report.md b/docs/architecture/baselines/archive-reference-report.md index ec38a8e70..e84a257fc 100644 --- a/docs/architecture/baselines/archive-reference-report.md +++ b/docs/architecture/baselines/archive-reference-report.md @@ -1,5 +1,5 @@ # Archive Reference Baseline -Generated on 2026-07-13. +Generated on 2026-07-14. - Total references: 0 diff --git a/docs/architecture/baselines/dependency-report.md b/docs/architecture/baselines/dependency-report.md index 22dc3f6b8..c0092a7b9 100644 --- a/docs/architecture/baselines/dependency-report.md +++ b/docs/architecture/baselines/dependency-report.md @@ -1,20 +1,20 @@ # Dependency Baseline -Generated on 2026-07-13. +Generated on 2026-07-14. ## main -- Total files: 563 -- Internal dependency edges: 1526 +- Total files: 565 +- Internal dependency edges: 1537 - Cycles detected: 36 ### Top outgoing dependencies -- `presenter/index.ts`: 70 -- `presenter/agentRuntimePresenter/index.ts`: 45 +- `presenter/index.ts`: 69 +- `presenter/agentRuntimePresenter/index.ts`: 48 - `presenter/sqlitePresenter/index.ts`: 40 - `presenter/sqlitePresenter/schemaCatalog.ts`: 37 -- `routes/index.ts`: 34 +- `routes/index.ts`: 36 - `presenter/configPresenter/index.ts`: 27 - `presenter/lifecyclePresenter/hooks/index.ts`: 23 - `presenter/toolPresenter/agentTools/agentToolManager.ts`: 22 @@ -40,7 +40,7 @@ Generated on 2026-07-13. - `presenter/sqlitePresenter/index.ts`: 21 - `presenter/memoryPresenter/ports.ts`: 20 - `presenter/remoteControlPresenter/services/remoteConversationRunner.ts`: 16 -- `presenter/memoryPresenter/domain/types.ts`: 14 +- `presenter/memoryPresenter/domain/types.ts`: 15 - `presenter/filePresenter/BaseFileAdapter.ts`: 13 - `presenter/memoryPresenter/context.ts`: 12 diff --git a/docs/architecture/baselines/main-kernel-boundary-baseline.md b/docs/architecture/baselines/main-kernel-boundary-baseline.md index 36bf710cd..46e38738d 100644 --- a/docs/architecture/baselines/main-kernel-boundary-baseline.md +++ b/docs/architecture/baselines/main-kernel-boundary-baseline.md @@ -1,6 +1,6 @@ # Main Kernel Boundary Baseline -Generated on 2026-07-13. +Generated on 2026-07-14. Current phase: P5. ## Metric Snapshot @@ -17,8 +17,8 @@ Current phase: P5. | `renderer.business.windowApi.count` | 0 | | `renderer.quarantine.windowApi.count` | 0 | | `renderer.quarantine.sourceFile.count` | 0 | -| `hotpath.presenterEdge.count` | 9 | -| `runtime.rawTimer.count` | 206 | +| `hotpath.presenterEdge.count` | 8 | +| `runtime.rawTimer.count` | 211 | | `migrated.rawChannel.count` | 0 | | `bridge.active.count` | 0 | | `bridge.expired.count` | 0 | @@ -55,12 +55,11 @@ Current phase: P5. ## Hot Path Direct Dependencies -- Direct edge count: 9 +- Direct edge count: 8 - `src/main/presenter/agentRuntimePresenter/index.ts -> src/main/eventbus.ts` - `src/main/presenter/index.ts -> src/main/eventbus.ts` - `src/main/presenter/index.ts -> src/main/presenter/agentRuntimePresenter/index.ts` -- `src/main/presenter/index.ts -> src/main/presenter/agentSessionPresenter/index.ts` - `src/main/presenter/index.ts -> src/main/presenter/llmProviderPresenter/index.ts` - `src/main/presenter/index.ts -> src/main/presenter/sessionPresenter/index.ts` - `src/main/presenter/llmProviderPresenter/index.ts -> src/main/eventbus.ts` @@ -87,7 +86,7 @@ Current phase: P5. ## Raw Timers -- Total count: 206 +- Total count: 211 - `src/main/presenter/githubCopilotDeviceFlow.ts`: 6 - `src/main/presenter/browser/BrowserTab.ts`: 5 @@ -95,12 +94,12 @@ Current phase: P5. - `src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts`: 5 - `src/main/presenter/remoteControlPresenter/index.ts`: 5 - `src/renderer/src/pages/ChatPage.vue`: 5 +- `src/main/presenter/memoryPresenter/infra/vectorStoreManager.ts`: 4 - `src/main/presenter/memoryPresenter/services/maintenanceService.ts`: 4 - `src/renderer/src/components/message/MessageToolbar.vue`: 4 - `src/renderer/src/composables/message/useMessageScroll.ts`: 4 - `src/main/agent/acp/launch/acpInitHelper.ts`: 3 - `src/main/agent/shared/process/backgroundExecSessionManager.ts`: 3 -- `src/main/lib/fileWatcher/watcherHost.ts`: 3 ## Migrated Path Raw Channel Literals diff --git a/docs/architecture/baselines/main-kernel-migration-scoreboard.json b/docs/architecture/baselines/main-kernel-migration-scoreboard.json index 237af9d2b..abb756d14 100644 --- a/docs/architecture/baselines/main-kernel-migration-scoreboard.json +++ b/docs/architecture/baselines/main-kernel-migration-scoreboard.json @@ -1,6 +1,6 @@ { "program": "main-kernel-refactor", - "generatedOn": "2026-07-13", + "generatedOn": "2026-07-14", "currentPhase": "P5", "metrics": { "renderer.usePresenter.count": 0, @@ -13,8 +13,8 @@ "renderer.business.windowApi.count": 0, "renderer.quarantine.windowApi.count": 0, "renderer.quarantine.sourceFile.count": 0, - "hotpath.presenterEdge.count": 9, - "runtime.rawTimer.count": 206, + "hotpath.presenterEdge.count": 8, + "runtime.rawTimer.count": 211, "migrated.rawChannel.count": 0, "bridge.active.count": 0, "bridge.expired.count": 0 @@ -61,7 +61,6 @@ "src/main/presenter/agentRuntimePresenter/index.ts -> src/main/eventbus.ts", "src/main/presenter/index.ts -> src/main/eventbus.ts", "src/main/presenter/index.ts -> src/main/presenter/agentRuntimePresenter/index.ts", - "src/main/presenter/index.ts -> src/main/presenter/agentSessionPresenter/index.ts", "src/main/presenter/index.ts -> src/main/presenter/llmProviderPresenter/index.ts", "src/main/presenter/index.ts -> src/main/presenter/sessionPresenter/index.ts", "src/main/presenter/llmProviderPresenter/index.ts -> src/main/eventbus.ts", diff --git a/docs/architecture/baselines/main-kernel-migration-scoreboard.md b/docs/architecture/baselines/main-kernel-migration-scoreboard.md index 5e409c2de..25b0a98e3 100644 --- a/docs/architecture/baselines/main-kernel-migration-scoreboard.md +++ b/docs/architecture/baselines/main-kernel-migration-scoreboard.md @@ -1,6 +1,6 @@ # Main Kernel Migration Scoreboard -Generated on 2026-07-13. +Generated on 2026-07-14. Current phase: P5. Phase 0 establishes the comparison baseline. Later phases should update this report and compare against this checkpoint. @@ -17,8 +17,8 @@ Phase 0 establishes the comparison baseline. Later phases should update this rep | `renderer.business.windowApi.count` | 0 | baseline | | `renderer.quarantine.windowApi.count` | 0 | baseline | | `renderer.quarantine.sourceFile.count` | 0 | baseline | -| `hotpath.presenterEdge.count` | 9 | baseline | -| `runtime.rawTimer.count` | 206 | baseline | +| `hotpath.presenterEdge.count` | 8 | baseline | +| `runtime.rawTimer.count` | 211 | baseline | | `migrated.rawChannel.count` | 0 | baseline | | `bridge.active.count` | 0 | baseline | | `bridge.expired.count` | 0 | baseline | diff --git a/docs/architecture/baselines/zero-inbound-candidates.md b/docs/architecture/baselines/zero-inbound-candidates.md index 8cfdd19d8..899b7bf73 100644 --- a/docs/architecture/baselines/zero-inbound-candidates.md +++ b/docs/architecture/baselines/zero-inbound-candidates.md @@ -1,6 +1,6 @@ # Zero Inbound Candidates -Generated on 2026-07-13. +Generated on 2026-07-14. These files have no in-repo importers inside their scope and need manual classification before deletion. diff --git a/docs/architecture/deepchat-vs-acp-agents/spec.md b/docs/architecture/deepchat-vs-acp-agents/spec.md index 68e4fe111..1c7007cf0 100644 --- a/docs/architecture/deepchat-vs-acp-agents/spec.md +++ b/docs/architecture/deepchat-vs-acp-agents/spec.md @@ -56,7 +56,7 @@ Flow: ```text Renderer send - -> AgentSessionPresenter + -> SessionService / ChatService -> Lifecycle / Turn coordinator -> AgentManager -> typed DeepChat handle -> DeepChatAgentInstance preparation -> create/register LoopRun @@ -88,7 +88,7 @@ Flow: ```text Renderer send - -> AgentSessionPresenter + -> SessionService / ChatService -> Lifecycle / Turn coordinator -> AgentManager -> direct ACP handle -> validate descriptor/config/workdir identity -> AcpAgentRuntime hydrate/prepare AcpAgentInstance diff --git a/docs/architecture/event-system.md b/docs/architecture/event-system.md index a003a92d6..a3dda8e1b 100644 --- a/docs/architecture/event-system.md +++ b/docs/architecture/event-system.md @@ -36,7 +36,7 @@ Current event families include: | Family | Examples | Publisher owner | | --- | --- | --- | | `chat.*` | `chat.stream.updated`, `chat.stream.completed`, `chat.stream.failed`, `chat.plan.updated` | `agentRuntimePresenter`, `dispatch` | -| `sessions.*` | `sessions.updated`, `sessions.status.changed`, `sessions.pendingInputs.changed` | `agentSessionPresenter`, runtime services | +| `sessions.*` | `sessions.updated`, `sessions.status.changed`, `sessions.pendingInputs.changed` | session application coordinators, runtime services | | `settings.*` | `settings.changed`, `settings.navigateRequested`, `settings.checkForUpdatesRequested` | config/settings/window flows | | `config.*` | language, theme, system prompts, agents, shortcut keys | `configPresenter` helpers | | `providers.*` and `models.*` | provider/model/rate-limit updates | provider runtime | diff --git a/docs/architecture/history-read-hot-path/spec.md b/docs/architecture/history-read-hot-path/spec.md index 63c84d7e9..189d9bb49 100644 --- a/docs/architecture/history-read-hot-path/spec.md +++ b/docs/architecture/history-read-hot-path/spec.md @@ -4,6 +4,9 @@ 已实现。 +> Historical implementation note: the boolean history checks now belong to the Lifecycle and Turn +> coordinators. `AgentSessionPresenter` and its main-process interface were retired in stage 3. + ## 问题 发送消息前,`AgentSessionPresenter` 只需要知道会话里“有没有消息”,但当前会读取并组装 diff --git a/docs/architecture/retire-agent-session-presenter/plan.md b/docs/architecture/retire-agent-session-presenter/plan.md new file mode 100644 index 000000000..d85bff39a --- /dev/null +++ b/docs/architecture/retire-agent-session-presenter/plan.md @@ -0,0 +1,70 @@ +# Retire Agent Session Presenter - Implementation Plan + +## Approach + +This is a deletion migration, not another extraction: + +1. replace each facade call with the already-existing owner; +2. remove the facade from the composition root and route runtime; +3. delete the class and shared interface after repository-wide production references reach zero; +4. redirect integration tests to the same coordinator instances; +5. convert facade-preservation guards into retirement guards and regenerate architecture baselines. + +## Wiring Plan + +### Composition root + +- Remove the `IAgentSessionPresenter` import, `AgentSessionPresenter` import, public property, and + construction. +- Use Projection for lookup, message, Tape, hook, floating-widget, and MCP consumers. +- Use Lifecycle for subagent creation. +- Use Turn for send and cancel. +- Use AgentAssignment for permission/settings and subagent Tape operations. +- Keep permission cleanup on the existing `SessionPermissionPort`. + +### Route runtime + +- Replace `agentSessionPresenter` with separate lifecycle, turn, assignment, and projection + dependencies. +- Continue passing lifecycle/projection into SessionService and turn/projection into ChatService. +- Dispatch direct route cases to the same four dependencies by the ownership table in `spec.md`. +- Update route tests to provide four focused doubles and assert the owning port. + +### Remaining consumers + +- Floating widget: Projection list and activate. +- MCP conversation search: Projection session/message reads. +- MCP tool manager: Projection session lookup. +- Hooks notification adapter: Projection session/message lookup. +- Agent tool runtime and skill state adapter: direct coordinator properties captured by closures. + +### Tests + +- Delete tests whose only subject is forwarding. +- Move retained cross-owner and runtime integration tests under `test/main/presenter/sessionApplication/`. +- Replace aggregate test variables with explicit Lifecycle, Turn, AgentAssignment, and Projection + variables. +- Preserve renderer tests because their local `agentSessionPresenter` names represent route clients, + not the retired main-process interface. + +### Guards and docs + +- Mark the facade directory and interface file as retired paths. +- Reject retired symbols in `src/main`, `src/shared`, and `test/main`. +- Remove facade ownership evidence and deleted files from architecture-baseline generation. +- Update current session-management and stage-2 documentation to state that stage 3 is complete. +- Verify baseline generation in an isolated output directory; regenerate canonical reports only + from a clean committed tree. + +## Validation + +Run in this order: + +1. repository-wide retired-symbol/path search; +2. focused route, sessionApplication, floating, MCP, and composition tests; +3. `pnpm run lint:architecture`; +4. `pnpm run typecheck`; +5. `pnpm run test:main` and `pnpm run test:renderer` (rerun any isolated timeout once to distinguish + contention from a functional failure); +6. `pnpm run format`, `pnpm run i18n`, and `pnpm run lint`; +7. final clean-worktree and diff review. diff --git a/docs/architecture/retire-agent-session-presenter/spec.md b/docs/architecture/retire-agent-session-presenter/spec.md new file mode 100644 index 000000000..64cdf4fa3 --- /dev/null +++ b/docs/architecture/retire-agent-session-presenter/spec.md @@ -0,0 +1,140 @@ +# Retire Agent Session Presenter + +> Status: implemented and validated +> Base: `dev@750f229d7` +> Branch: `task/retire-agent-session-presenter` + +## Context + +The session-boundary cleanup is complete through two stages: + +1. foreign capabilities moved to explicit history, import, migration, usage, RTK, export, + translation, and catalog owners; +2. Lifecycle, Turn, AgentAssignment, and Projection behavior moved to four composition-owned + coordinators, while SessionService, ChatService, Remote, and Cron adopted narrow ports. + +`AgentSessionPresenter` now contains forwarding only. Its shared `IAgentSessionPresenter` contract is +used only by the main process, but still makes the forwarding surface appear to be a domain owner. + +## Problem + +The compatibility facade creates four remaining architecture failures: + +1. the composition root constructs a fifth session object that owns no behavior; +2. typed route dispatch retains one aggregate dependency alongside its existing narrow ports; +3. Tool, MCP, floating-widget, hooks, and agent-runtime adapters reach session behavior through the + facade instead of the actual coordinator; +4. `IPresenter` exports the main-only aggregate contract through shared types, allowing new callers + to couple to it again. + +## Goals + +1. Remove `src/main/presenter/agentSessionPresenter/` and the `AgentSessionPresenter` class. +2. Remove the main-process `IAgentSessionPresenter` declaration, export, and `IPresenter` property. +3. Wire the composition root directly to the existing Lifecycle, Turn, AgentAssignment, and + Projection coordinator instances. +4. Give route runtime four separate session dependencies and route every command/read to its owner. +5. Rewire remaining main-process consumers to the narrow coordinator they actually use. +6. Preserve all route, event, IPC, renderer-client, persistence, and runtime behavior. +7. Add architecture enforcement that keeps the retired facade, interface, path, and aggregate + dependency from returning. + +## Non-goals + +- Changing coordinator behavior, transaction ordering, DTOs, route names, preload APIs, or renderer + clients. +- Moving state between AppSessionService, AgentManager, backend instances, transcript, Tape, + permission, skill, or UI owners. +- Combining the four coordinators into a replacement `SessionApplicationServices`, facade, registry, + service container, or command bus. +- Renaming renderer-side session clients or test doubles that do not reference the retired + main-process presenter contract. +- GitHub issue synchronization. + +## Target Dependency Shape + +```text +Presenter composition root + |- SessionLifecycleCoordinator + |- SessionTurnCoordinator + |- SessionAgentAssignmentCoordinator + `- SessionProjectionCoordinator + +main route runtime + |- lifecycle -> lifecycle routes + SessionService + |- turn -> turn routes + ChatService + |- assignment -> assignment/settings routes + `- projection -> read/window/Tape routes + SessionService + ChatService + +other main consumers + |- agent tool runtime -> lifecycle/turn/assignment/projection + |- hooks -> projection + |- floating widget -> projection + `- MCP tools -> projection +``` + +No object may re-export all four capability groups. + +## Ownership Mapping + +| Retired facade calls | Direct owner | +| --- | --- | +| create, detached create, subagent create, draft ensure, fork, delete | Lifecycle | +| send, steer, pending input, message mutation, compaction, cancel, interaction | Turn | +| transfer, runtime settings, ACP config/commands, subagent Tape merge/discard | AgentAssignment | +| session/message/Tape reads, window activation, rename/pin, title/status events | Projection | +| permission cleanup | existing `SessionPermissionPort` | + +## Boundary Rules + +1. Production and main-process tests must not import, construct, type, or access + `AgentSessionPresenter`, `IAgentSessionPresenter`, or `agentSessionPresenter`. +2. The composition root constructs exactly one instance of each existing session coordinator. +3. Route runtime exposes four separate session dependencies; it must not expose a combined session + application object. +4. Required dependencies remain required. No optional method probes, fallback no-ops, setters, or + `as unknown as` wiring may replace the facade. +5. Projection remains a singleton because it owns active-window bindings and status snapshots. +6. Existing stage-1 owners remain independent and are not absorbed by any coordinator. +7. Renderer-side route clients remain unchanged because the retired interface is main-process-only. + +## Compatibility Invariants + +- Every typed route keeps the same input parsing, output parsing, scheduler, timeout, and error + behavior. +- SessionService and ChatService continue using their existing narrow ports and shared coordinator + instances. +- Tool runtime session lookup, subagent creation, Tape operations, send, and cancellation preserve + their current return and failure behavior. +- Hooks continue reading sessions/messages from the same Projection singleton. +- Floating-widget list and activation behavior remains optional only with respect to app lifecycle, + not coordinator capabilities. +- MCP conversation history and ACP conversation lookup retain existing missing-session and error + handling. +- Remote and Cron wiring remains unchanged apart from removal of obsolete facade references. + +## Acceptance Criteria + +1. The `agentSessionPresenter` production and main-test directories are deleted. +2. `AgentSessionPresenter`, `IAgentSessionPresenter`, and main-process `agentSessionPresenter` + references are zero. +3. The shared presenter barrel and `IPresenter` no longer export the retired contract/property. +4. The composition root constructs no forwarding facade and wires every former caller to one of the + four existing coordinators or `SessionPermissionPort`. +5. Route runtime contains four separate session dependencies and no aggregate replacement. +6. Existing integration coverage calls coordinators directly; forwarding-only tests are removed. +7. Architecture guards reject retired paths/symbols and continue rejecting combined facades and + duplicate coordinator construction. +8. Current architecture docs and baseline generation describe the post-retirement graph; isolated + generation succeeds, while canonical reports remain restricted to a clean committed tree. +9. Formatting, i18n validation, lint, typecheck, main tests, renderer tests, and architecture guards + pass. + +## Risks + +- A mechanical route remap can send a method to the wrong coordinator while still type-checking if + test doubles are overly broad. +- Reconstructing Projection per consumer would split active-window and status state. +- Deleting facade tests without redirecting integration coverage would hide cross-owner regressions. +- Generated architecture baselines can retain deleted edges unless their source configuration is + updated before regeneration. diff --git a/docs/architecture/retire-agent-session-presenter/tasks.md b/docs/architecture/retire-agent-session-presenter/tasks.md new file mode 100644 index 000000000..3e33df3bc --- /dev/null +++ b/docs/architecture/retire-agent-session-presenter/tasks.md @@ -0,0 +1,42 @@ +# Retire Agent Session Presenter - Tasks + +## Specification + +- [x] Confirm the facade contains forwarding only. +- [x] Inventory production, shared-type, test, guard, and baseline references. +- [x] Define direct owner mapping and compatibility invariants. +- [x] Resolve all clarification items. + +## Composition and consumers + +- [x] Remove facade construction/property/imports from the composition root. +- [x] Rewire agent tool runtime, skill state, hooks, floating widget, and MCP consumers. +- [x] Replace the route-runtime facade dependency with four separate owner dependencies. +- [x] Keep SessionService, ChatService, Remote, and Cron on their existing narrow ports. + +## Retirement + +- [x] Delete `AgentSessionPresenter` and its production directory. +- [x] Delete `IAgentSessionPresenter`, its barrel export, and the shared `IPresenter` property. +- [x] Remove all main-process and main-test facade symbols. + +## Tests and enforcement + +- [x] Remove forwarding-only tests and relocate retained integration coverage. +- [x] Update route/composition consumer tests to use explicit owners. +- [x] Convert architecture checks and fixtures to enforce retirement. +- [x] Update baseline generation and verify reports in an isolated output directory. + +## Documentation and validation + +- [x] Update maintained session architecture documentation. +- [x] Run focused tests and architecture guards. +- [x] Run full typecheck, main tests, and renderer tests. +- [x] Run format, i18n validation, and lint. +- [x] Review final diff and mark the SDD implemented. + +## Baseline note + +Canonical reports are generated only from a clean committed tree. The updated generator completed +successfully against an isolated output directory; canonical regeneration belongs to the later +commit/PR workflow. diff --git a/docs/architecture/session-application-coordinators/plan.md b/docs/architecture/session-application-coordinators/plan.md index 058c94049..10d38d012 100644 --- a/docs/architecture/session-application-coordinators/plan.md +++ b/docs/architecture/session-application-coordinators/plan.md @@ -1,5 +1,8 @@ # Session Application Coordinators — Implementation Plan +> Historical stage-2 plan. Stage 3 subsequently removed the compatibility façade and connected the +> remaining consumers directly to the four coordinators. + ## Approach The migration uses a strangler sequence inside the existing compatibility façade: diff --git a/docs/architecture/session-application-coordinators/spec.md b/docs/architecture/session-application-coordinators/spec.md index 3430e450f..819a0df22 100644 --- a/docs/architecture/session-application-coordinators/spec.md +++ b/docs/architecture/session-application-coordinators/spec.md @@ -4,6 +4,8 @@ > Original base: `dev@28e2a0e92` > Integrated base: `dev@135779210` via merge commit `1122b2406` > Branch: `task/session-application-coordinators` +> Follow-up: stage 3 retired `AgentSessionPresenter` and `IAgentSessionPresenter`; current callers use +> the four coordinator ports directly. ## Context diff --git a/docs/architecture/session-application-coordinators/tasks.md b/docs/architecture/session-application-coordinators/tasks.md index 19918c568..7ca130729 100644 --- a/docs/architecture/session-application-coordinators/tasks.md +++ b/docs/architecture/session-application-coordinators/tasks.md @@ -1,5 +1,8 @@ # Session Application Coordinators — Tasks +> Historical stage-2 checklist. Presenter retirement is tracked in +> `docs/architecture/retire-agent-session-presenter/`. + ## SDD and Inventory - [x] Audit Lifecycle, Turn, AgentAssignment, and Projection methods, state, dependencies, and tests. diff --git a/docs/architecture/session-boundary-cleanup/spec.md b/docs/architecture/session-boundary-cleanup/spec.md index 213b386a6..d78a01a51 100644 --- a/docs/architecture/session-boundary-cleanup/spec.md +++ b/docs/architecture/session-boundary-cleanup/spec.md @@ -1,5 +1,8 @@ # Session Boundary Cleanup +> Historical stage-1 specification. Stages 2 and 3 later extracted the four session application +> coordinators and retired `AgentSessionPresenter` / `IAgentSessionPresenter`. + > Status: implemented and merged in PR #1957 > Base: `dev@28e2a0e92` > Branch: `codex/session-boundary-cleanup` diff --git a/docs/architecture/session-management.md b/docs/architecture/session-management.md index 60f9dc15e..81ee3f718 100644 --- a/docs/architecture/session-management.md +++ b/docs/architecture/session-management.md @@ -1,6 +1,6 @@ # 会话管理架构详解 -当前会话管理分成五个明确边界: +当前会话管理分成四个明确边界: - app-session shell:`AppSessionService` 管理 `new_sessions`、window binding 和 shared CRUD; - application coordination:四个 `sessionApplication` coordinator 分别拥有 Lifecycle、Turn、 @@ -8,8 +8,9 @@ - agent execution routing:`AgentManager` 按 executable descriptor kind 返回 typed handle; - route/application owners:typed session routes 直接组合 search、translation、export、usage、RTK 与 agent catalog owner; -- compatibility forwarding:`AgentSessionPresenter` 保留尚未退休的 public surface,并把 core session 方法 - 转发到四个 coordinator,不拥有 application behavior。 + +`AgentSessionPresenter` 和 main-process `IAgentSessionPresenter` 已退休。Composition root 与 main route +runtime 直接暴露四个分离的 coordinator port,不存在 aggregate session façade。 `SessionPresenter` 是旧 conversations/messages 的 compatibility/data façade,不在当前 agent execution 链路中。 @@ -19,7 +20,6 @@ | 组件 | 位置 | 当前职责 | | --- | --- | --- | | Session application coordinators | `src/main/presenter/sessionApplication/` | Lifecycle、Turn、AgentAssignment、Projection application invariants | -| `AgentSessionPresenter` | `src/main/presenter/agentSessionPresenter/index.ts` | core session public compatibility forwarding;不拥有 application behavior | | Session route owners | `src/main/routes/sessions/` | history search、translation 与 typed route orchestration | | Session export | `src/main/presenter/exporter/agentSessionExporter.ts` | current agent-session export mapping and format dispatch | | Startup/maintenance owners | `src/main/presenter/startupMigrations/`, `usageStatsService.ts` | legacy import、session-data migrations、usage backfill/dashboard;RTK 由其 runtime service 自有 | @@ -127,9 +127,9 @@ ACP-provider source 只清 compatibility binding,不被误判成 direct ACP。 - exporter 的旧消息格式化。 当前 session create/send/cancel/tool interaction 从 `SessionService/ChatService -> sessionApplication -coordinator -> AgentManager -> typed backend` 开始追踪。兼容调用可能先进入 `AgentSessionPresenter`,但其 -core session methods 只转发到同一 coordinator 实例。DeepChat state/loop 看 `agent/deepchat`,direct ACP -看 `agent/acp/instance`。 +coordinator -> AgentManager -> typed backend` 开始追踪。Main route runtime、Tool、MCP、Floating 与 hooks +也直接接到同一组 composition-owned coordinator。DeepChat state/loop 看 `agent/deepchat`,direct ACP 看 +`agent/acp/instance`。 旧聊天数据导入由 `presenter/startupMigrations/legacyChatImportService.ts` 拥有;当前 agent-session export -由 `presenter/exporter/agentSessionExporter.ts` 拥有。两者都不属于 `AgentSessionPresenter`。 +由 `presenter/exporter/agentSessionExporter.ts` 拥有。两者都不属于 session application coordinator。 diff --git a/docs/architecture/tool-system.md b/docs/architecture/tool-system.md index cf480fc3f..9eaf136d2 100644 --- a/docs/architecture/tool-system.md +++ b/docs/architecture/tool-system.md @@ -100,7 +100,7 @@ port 负责提供: - conversation workdir 解析 - 已批准路径查询 - settings approval 消费 -- `agentSessionPresenter` 会话上下文桥接 +- Lifecycle / Turn / AgentAssignment / Projection session ports ## FFF Search diff --git a/scripts/agent-cleanup-guard.mjs b/scripts/agent-cleanup-guard.mjs index 078fdb6f2..a77daca21 100644 --- a/scripts/agent-cleanup-guard.mjs +++ b/scripts/agent-cleanup-guard.mjs @@ -23,7 +23,6 @@ const LEGACY_MAIN_DIRS = [ const PRIMARY_MAIN_GUARD_PATHS = [ path.join(ROOT, 'src/main/agent'), - path.join(ROOT, 'src/main/presenter/agentSessionPresenter'), path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'), path.join(ROOT, 'src/main/presenter/skillPresenter'), path.join(ROOT, 'src/main/presenter/mcpPresenter/toolManager.ts'), @@ -156,7 +155,6 @@ function buildViolation(kind, filePath, specifier) { async function findViolations() { const scanRoots = [ path.join(ROOT, 'src/main/agent'), - path.join(ROOT, 'src/main/presenter/agentSessionPresenter'), path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'), path.join(ROOT, 'src/main/presenter/skillPresenter'), path.join(ROOT, 'src/main/presenter/mcpPresenter/toolManager.ts'), diff --git a/scripts/architecture-guard.mjs b/scripts/architecture-guard.mjs index 64a852ae6..c48bbae2e 100644 --- a/scripts/architecture-guard.mjs +++ b/scripts/architecture-guard.mjs @@ -19,17 +19,14 @@ const SOURCE_EXTENSIONS = new Set([ ]) const MAIN_GUARD_PATHS = [ - path.join(ROOT, 'src/main/presenter/agentSessionPresenter'), path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'), path.join(ROOT, 'src/main/agent') ] const REGULAR_MAIN_TEST_ROOT = path.join(ROOT, 'test/main') const INTERNAL_AGENT_KIND_ROOTS = [ path.join(ROOT, 'src/main/agent'), - path.join(ROOT, 'src/main/presenter/agentSessionPresenter'), path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'), path.join(ROOT, 'test/main/agent'), - path.join(ROOT, 'test/main/presenter/agentSessionPresenter'), path.join(ROOT, 'test/main/presenter/agentRuntimePresenter') ] const AGENT_HANDLE_BACKEND_RUNTIME_KIND_ROOTS = [ @@ -49,8 +46,16 @@ const RETIRED_RENDERER_LEGACY_ENTRY_PATHS = [ ] const RETIRED_MAIN_PATHS = [ path.join(ROOT, 'src/main/lib/agentRuntime'), - path.join(ROOT, 'src/main/agent/manager/legacyAgentBackends.ts') + path.join(ROOT, 'src/main/agent/manager/legacyAgentBackends.ts'), + path.join(ROOT, 'src/main/presenter/agentSessionPresenter'), + path.join(ROOT, 'src/shared/types/presenters/agent-session.presenter.d.ts'), + path.join(ROOT, 'test/main/presenter/agentSessionPresenter') ] +const RETIRED_SESSION_FACADE_NAMES = new Set([ + 'AgentSessionPresenter', + 'IAgentSessionPresenter', + 'agentSessionPresenter' +]) const RENDERER_TYPED_BOUNDARY_WINDOW_API_ALLOWLIST = [ path.join(ROOT, 'src/renderer/api/runtime.ts') ] @@ -127,14 +132,6 @@ const SESSION_FACADE_CAPABILITY_CATEGORIES = new Map([ ]) const SESSION_PHASE_ONE_FOREIGN_IMPORT_PATTERN = /(?:^|[/_.-])(?:history|export(?:er)?|usage(?:[-_.]?stats?)?|rtk|legacy[-_.]?import(?:er|s)?|import(?:er|s)?|migrations?|translation|catalog)(?:$|[/_.-])/ -const AGENT_SESSION_PRESENTER_PATH = path.join( - ROOT, - 'src/main/presenter/agentSessionPresenter/index.ts' -) -const AGENT_SESSION_PRESENTER_INTERFACE_PATH = path.join( - ROOT, - 'src/shared/types/presenters/agent-session.presenter.d.ts' -) const SESSION_BOUNDARY_STARTUP_HOOK_PATHS = new Set( [ 'disabledSearchToolCleanupHook.ts', @@ -146,37 +143,6 @@ const SESSION_BOUNDARY_STARTUP_HOOK_PATHS = new Set( path.join(ROOT, 'src/main/presenter/lifecyclePresenter/hooks/after-start', fileName) ) ) -const REMOVED_AGENT_SESSION_CAPABILITY_METHODS = new Set([ - 'searchHistory', - 'getLegacyImportStatus', - 'retryLegacyImport', - 'startLegacyImport', - 'startLegacyImportTask', - 'startUsageStatsBackfill', - 'startUsageStatsBackfillTask', - 'startMainlineNormalizationBackfill', - 'startMainlineNormalizationBackfillTask', - 'startDisabledSearchToolCleanupBackfill', - 'startDisabledSearchToolCleanupBackfillTask', - 'startRtkHealthCheck', - 'startRtkHealthCheckTask', - 'retryRtkHealthCheck', - 'getUsageDashboard', - 'repairImportedLegacySessionSkills', - 'translateText', - 'getAgents', - 'exportSession' -]) -const AGENT_SESSION_FORBIDDEN_IMPORTS = [ - ['legacy import', /(?:^|\/)legacy(?:Chat)?ImportService$/], - ['startup migrations', /(?:^|\/)startupMigrations(?:\/|$)/], - ['usage owner or policy', /(?:^|\/)usageStats(?:Service)?$/], - ['RTK runtime', /(?:^|\/)rtkRuntimeService$/], - ['exporter formats', /(?:^|\/)exporter\/formats(?:\/|$)/], - ['history search', /(?:^|\/)sessionHistorySearch$/], - ['session translation', /(?:^|\/)sessionTranslation$/], - ['agent catalog', /(?:^|\/)availableAgentCatalog$/] -] const PHASE_ORDER = new Map([ ['P0', 0], ['P1', 1], @@ -210,7 +176,6 @@ const MIGRATED_RAW_CHANNEL_GUARD_PATHS = [ path.join(ROOT, 'src/renderer/settings'), path.join(ROOT, 'src/main/presenter/windowPresenter'), path.join(ROOT, 'src/main/presenter/configPresenter'), - path.join(ROOT, 'src/main/presenter/agentSessionPresenter'), path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'), path.join(ROOT, 'src/main/presenter/sessionPresenter'), path.join(ROOT, 'src/main/presenter/llmProviderPresenter'), @@ -227,7 +192,6 @@ const MIGRATED_RAW_CHANNEL_BASELINE = new Map() const HOT_PATH_FILES = [ path.join(ROOT, 'src/main/presenter/index.ts'), path.join(ROOT, 'src/main/eventbus.ts'), - path.join(ROOT, 'src/main/presenter/agentSessionPresenter/index.ts'), path.join(ROOT, 'src/main/presenter/agentRuntimePresenter/index.ts'), path.join(ROOT, 'src/main/presenter/llmProviderPresenter/index.ts'), path.join(ROOT, 'src/main/presenter/sessionPresenter/index.ts') @@ -832,25 +796,6 @@ function findNamedClassDeclarations(sourceFile, className) { return declarations } -function findNamedDeclarationMembers(source, filePath, declarationName) { - const sourceFile = sourceFileForAst(source, filePath) - const names = [] - const visit = (node) => { - if ( - (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && - node.name?.text === declarationName - ) { - for (const member of node.members) { - const name = member.name ? propertyNameText(member.name) : null - if (name) names.push(name) - } - } - ts.forEachChild(node, visit) - } - visit(sourceFile) - return names -} - function analyzeSessionBoundaryStartupHook(source, filePath) { const sourceFile = sourceFileForAst(source, filePath) let agentSessionPresenterDependencies = 0 @@ -1604,7 +1549,10 @@ export async function runArchitectureGuard({ virtualFiles = new Map(), memoryCom } for (const retiredPath of RETIRED_MAIN_PATHS) { - if (await pathExists(retiredPath)) { + const virtualPathExists = [...normalizedVirtualFiles.keys()].some((filePath) => + isUnder(filePath, retiredPath) + ) + if ((await pathExists(retiredPath)) || virtualPathExists) { violations.push(`[main-retired-path] ${relativePath(retiredPath)} must remain deleted`) } } @@ -1613,9 +1561,25 @@ export async function runArchitectureGuard({ virtualFiles = new Map(), memoryCom const source = await readSource(filePath) const specifiers = extractModuleSpecifiers(source) const isMainSource = isUnder(filePath, MAIN_SOURCE_ROOT) + const scansRetiredSessionFacade = + isMainSource || + isUnder(filePath, SHARED_SOURCE_ROOT) || + isUnder(filePath, REGULAR_MAIN_TEST_ROOT) + const sourceFile = scansRetiredSessionFacade ? sourceFileForAst(source, filePath) : null + + if (scansRetiredSessionFacade) { + const retiredFacadeNames = findIdentifierNames( + sourceFile, + RETIRED_SESSION_FACADE_NAMES + ) + for (const name of retiredFacadeNames) { + violations.push( + `[session-retired-facade-symbol] ${relativePath(filePath)} must not reference ${name}` + ) + } + } if (isMainSource) { - const sourceFile = sourceFileForAst(source, filePath) const importRecords = importRecordsFromSourceFile(sourceFile) if (isSessionMigratedConsumerPath(filePath)) { @@ -1711,42 +1675,6 @@ export async function runArchitectureGuard({ virtualFiles = new Map(), memoryCom } } - if (path.resolve(filePath) === path.resolve(AGENT_SESSION_PRESENTER_PATH)) { - const removedMethods = findNamedDeclarationMembers( - source, - filePath, - 'AgentSessionPresenter' - ).filter((name) => REMOVED_AGENT_SESSION_CAPABILITY_METHODS.has(name)) - if (removedMethods.length > 0) { - violations.push( - `[session-boundary-presenter-method] ${relativePath(filePath)} must not declare moved capabilities: ${removedMethods.join(', ')}` - ) - } - - for (const specifier of specifiers) { - for (const [owner, pattern] of AGENT_SESSION_FORBIDDEN_IMPORTS) { - if (pattern.test(specifier)) { - violations.push( - `[session-boundary-presenter-import] ${relativePath(filePath)} must not import ${owner}: ${specifier}` - ) - } - } - } - } - - if (path.resolve(filePath) === path.resolve(AGENT_SESSION_PRESENTER_INTERFACE_PATH)) { - const removedMethods = findNamedDeclarationMembers( - source, - filePath, - 'IAgentSessionPresenter' - ).filter((name) => REMOVED_AGENT_SESSION_CAPABILITY_METHODS.has(name)) - if (removedMethods.length > 0) { - violations.push( - `[session-boundary-interface-method] ${relativePath(filePath)} must not declare moved capabilities: ${removedMethods.join(', ')}` - ) - } - } - if (SESSION_BOUNDARY_STARTUP_HOOK_PATHS.has(path.resolve(filePath))) { const hook = analyzeSessionBoundaryStartupHook(source, filePath) if (hook.agentSessionPresenterDependencies > 0) { @@ -1803,7 +1731,7 @@ export async function runArchitectureGuard({ virtualFiles = new Map(), memoryCom if (isUnder(filePath, MAIN_SOURCE_ROOT)) { if (source.includes('MemoryRuntimeCoordinator')) { const coordinatorClasses = findNamedClassDeclarations( - sourceFileForAst(source, filePath), + sourceFile, 'MemoryRuntimeCoordinator' ) memoryCoordinatorOwners.push( diff --git a/scripts/generate-architecture-baseline.mjs b/scripts/generate-architecture-baseline.mjs index f6f4a85fc..a300a807b 100644 --- a/scripts/generate-architecture-baseline.mjs +++ b/scripts/generate-architecture-baseline.mjs @@ -17,7 +17,10 @@ const AGENT_SYSTEM_SOURCE_ROOTS = [ ] const AGENT_SYSTEM_PRESENTER_BOUNDARY_FILES = [ 'src/main/presenter/index.ts', - 'src/main/presenter/agentSessionPresenter/index.ts', + 'src/main/presenter/sessionApplication/projectionCoordinator.ts', + 'src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts', + 'src/main/presenter/sessionApplication/turnCoordinator.ts', + 'src/main/presenter/sessionApplication/lifecycleCoordinator.ts', 'src/main/presenter/agentRuntimePresenter/index.ts', 'src/main/presenter/agentRuntimePresenter/process.ts', 'src/main/presenter/agentRuntimePresenter/dispatch.ts', @@ -98,9 +101,24 @@ const AGENT_SYSTEM_OWNER_EVIDENCE = [ /\bclass AcpAgentInstance\b/g ], [ - 'retainedAgentSessionFacade', - 'src/main/presenter/agentSessionPresenter/index.ts', - /\bclass AgentSessionPresenter\b/g + 'sessionProjectionCoordinator', + 'src/main/presenter/sessionApplication/projectionCoordinator.ts', + /\bclass SessionProjectionCoordinator\b/g + ], + [ + 'sessionAgentAssignmentCoordinator', + 'src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts', + /\bclass SessionAgentAssignmentCoordinator\b/g + ], + [ + 'sessionTurnCoordinator', + 'src/main/presenter/sessionApplication/turnCoordinator.ts', + /\bclass SessionTurnCoordinator\b/g + ], + [ + 'sessionLifecycleCoordinator', + 'src/main/presenter/sessionApplication/lifecycleCoordinator.ts', + /\bclass SessionLifecycleCoordinator\b/g ], [ 'retainedDeepChatStateDelegateFacade', @@ -111,10 +129,13 @@ const AGENT_SYSTEM_OWNER_EVIDENCE = [ const AGENT_SYSTEM_RETIRED_PATHS = [ 'src/main/agent/manager/legacyAgentBackends.ts', 'src/main/lib/agentRuntime', - 'src/main/presenter/agentSessionPresenter/agentRegistry.ts' + 'src/main/presenter/agentSessionPresenter', + 'src/shared/types/presenters/agent-session.presenter.d.ts' ] const AGENT_SYSTEM_RETIRED_SYMBOL_PATTERNS = [ ['AgentRegistry', /\bAgentRegistry\b/g], + ['AgentSessionPresenter', /\bAgentSessionPresenter\b/g], + ['IAgentSessionPresenter', /\bIAgentSessionPresenter\b/g], ['IAgentImplementation', /\bIAgentImplementation\b/g], ['createLegacyAgentBackend', /\bcreateLegacyAgentBackend\b/g], ['LegacyDeepChatSessionBackend', /\bLegacyDeepChatSessionBackend\b/g], @@ -185,7 +206,6 @@ const BRIDGE_REGISTER_PATH = path.join( const HOT_PATH_FILES = [ path.join(ROOT, 'src/main/presenter/index.ts'), path.join(ROOT, 'src/main/eventbus.ts'), - path.join(ROOT, 'src/main/presenter/agentSessionPresenter/index.ts'), path.join(ROOT, 'src/main/presenter/agentRuntimePresenter/index.ts'), path.join(ROOT, 'src/main/presenter/llmProviderPresenter/index.ts'), path.join(ROOT, 'src/main/presenter/sessionPresenter/index.ts') @@ -203,7 +223,6 @@ const MIGRATED_RAW_CHANNEL_GUARD_PATHS = [ path.join(ROOT, 'src/renderer/src/pages/NewThreadPage.vue'), path.join(ROOT, 'src/main/presenter/windowPresenter'), path.join(ROOT, 'src/main/presenter/configPresenter'), - path.join(ROOT, 'src/main/presenter/agentSessionPresenter'), path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'), path.join(ROOT, 'src/main/presenter/sessionPresenter'), path.join(ROOT, 'src/main/presenter/llmProviderPresenter'), diff --git a/src/main/presenter/agentSessionPresenter/index.ts b/src/main/presenter/agentSessionPresenter/index.ts deleted file mode 100644 index 7ef479931..000000000 --- a/src/main/presenter/agentSessionPresenter/index.ts +++ /dev/null @@ -1,427 +0,0 @@ -import type { - AgentTapeAnchorResult, - AgentTapeAnchorsOptions, - AgentTapeContextOptions, - AgentTapeContextResult, - AgentTapeInfo, - AgentTapeSearchOptions, - AgentTapeSearchResult, - AgentTransferImpact, - ChatMessagePageResult, - ChatMessageRecord, - CreateDetachedSessionInput, - CreateSessionInput, - MessagePageCursor, - MessageStartResult, - MessageTraceRecord, - PermissionMode, - SendMessageInput, - SessionCompactionState, - SessionGenerationSettings, - SessionLightweightListResult, - SessionListItem, - SessionPageCursor, - SessionWithState, - ToolInteractionResponse, - ToolInteractionResult -} from '@shared/types/agent-interface' -import type { SearchResult } from '@shared/types/core/search' -import type { DeepChatTapeViewManifestRecord } from '@shared/types/tape-view-manifest' -import type { - DeepChatTapeReplayExportOptions, - DeepChatTapeReplaySlice -} from '@shared/types/tape-replay' -import type { AcpConfigState } from '@shared/presenter' -import { SessionProjectionCoordinator } from '../sessionApplication/projectionCoordinator' -import type { - SessionAgentAssignmentPort, - SessionLifecyclePort, - SessionLifecycleSubagentInput, - SessionTurnPort -} from '../sessionApplication/ports' -import type { SessionPermissionPort } from '../runtimePorts' - -export class AgentSessionPresenter { - private sessionProjection: SessionProjectionCoordinator - private sessionLifecycle: SessionLifecyclePort - private sessionAssignment: SessionAgentAssignmentPort - private sessionTurn: SessionTurnPort - private sessionPermissionPort?: SessionPermissionPort - - constructor( - sessionProjection: SessionProjectionCoordinator, - sessionLifecycle: SessionLifecyclePort, - sessionAssignment: SessionAgentAssignmentPort, - sessionTurn: SessionTurnPort, - runtimePorts?: { - sessionPermissionPort?: SessionPermissionPort - } - ) { - this.sessionProjection = sessionProjection - this.sessionLifecycle = sessionLifecycle - this.sessionAssignment = sessionAssignment - this.sessionTurn = sessionTurn - this.sessionPermissionPort = runtimePorts?.sessionPermissionPort - } - - // ---- IPC-facing methods ---- - - async createSession(input: CreateSessionInput, webContentsId: number): Promise { - return await this.sessionLifecycle.createSession(input, webContentsId) - } - - async createDetachedSession(input: CreateDetachedSessionInput): Promise { - return await this.sessionLifecycle.createDetachedSession(input) - } - - async createSubagentSession(input: SessionLifecycleSubagentInput): Promise { - return await this.sessionLifecycle.createSubagentSession(input) - } - - async ensureAcpDraftSession(input: { - agentId: string - projectDir: string - permissionMode?: PermissionMode - }): Promise { - return await this.sessionLifecycle.ensureAcpDraftSession(input) - } - - async sendMessage( - sessionId: string, - content: string | SendMessageInput, - options?: { maxProviderRounds?: number } - ): Promise { - return await this.sessionTurn.sendMessage(sessionId, content, options) - } - - async steerActiveTurn(sessionId: string, content: string | SendMessageInput): Promise { - await this.sessionTurn.steerActiveTurn(sessionId, content) - } - - async listPendingInputs(sessionId: string) { - return await this.sessionTurn.listPendingInputs(sessionId) - } - - async queuePendingInput(sessionId: string, content: string | SendMessageInput) { - return await this.sessionTurn.queuePendingInput(sessionId, content) - } - - async updateQueuedInput(sessionId: string, itemId: string, content: string | SendMessageInput) { - return await this.sessionTurn.updateQueuedInput(sessionId, itemId, content) - } - - async moveQueuedInput(sessionId: string, itemId: string, toIndex: number) { - return await this.sessionTurn.moveQueuedInput(sessionId, itemId, toIndex) - } - - async convertPendingInputToSteer(sessionId: string, itemId: string) { - return await this.sessionTurn.convertPendingInputToSteer(sessionId, itemId) - } - - async steerPendingInput(sessionId: string, itemId: string) { - return await this.sessionTurn.steerPendingInput(sessionId, itemId) - } - - async deletePendingInput(sessionId: string, itemId: string): Promise { - await this.sessionTurn.deletePendingInput(sessionId, itemId) - } - - async retryMessage(sessionId: string, messageId: string): Promise { - await this.sessionTurn.retryMessage(sessionId, messageId) - } - - async deleteMessage(sessionId: string, messageId: string): Promise { - await this.sessionTurn.deleteMessage(sessionId, messageId) - } - - async editUserMessage( - sessionId: string, - messageId: string, - text: string - ): Promise { - return await this.sessionTurn.editUserMessage(sessionId, messageId, text) - } - - async forkSession( - sourceSessionId: string, - targetMessageId: string, - newTitle?: string - ): Promise { - return await this.sessionLifecycle.forkSession(sourceSessionId, targetMessageId, newTitle) - } - - async getSessionList(filters?: { - agentId?: string - projectDir?: string - includeSubagents?: boolean - parentSessionId?: string - }): Promise { - return await this.sessionProjection.listSessions(filters) - } - - async getLightweightSessionList(options?: { - limit?: number - cursor?: SessionPageCursor | null - includeSubagents?: boolean - agentId?: string - prioritizeSessionId?: string - }): Promise { - return await this.sessionProjection.listLightweight(options) - } - - async getLightweightSessionsByIds(sessionIds: string[]): Promise { - return await this.sessionProjection.getLightweightByIds(sessionIds) - } - - async getSession(sessionId: string): Promise { - return await this.sessionProjection.getSession(sessionId) - } - - async getMessages(sessionId: string): Promise { - return await this.sessionProjection.getMessages(sessionId) - } - - async listMessagesPage( - sessionId: string, - options?: { - limit?: number - cursor?: MessagePageCursor | null - } - ): Promise { - return await this.sessionProjection.listMessagesPage(sessionId, options) - } - - async getSessionCompactionState(sessionId: string): Promise { - return await this.sessionTurn.getSessionCompactionState(sessionId) - } - - async compactSession( - sessionId: string - ): Promise<{ compacted: boolean; state: SessionCompactionState }> { - return await this.sessionTurn.compactSession(sessionId) - } - - async getTapeInfo(sessionId: string): Promise { - return await this.sessionProjection.getTapeInfo(sessionId) - } - - async searchTape( - sessionId: string, - query: string, - options?: AgentTapeSearchOptions - ): Promise { - return await this.sessionProjection.searchTape(sessionId, query, options) - } - - async getTapeContext( - sessionId: string, - entryIds: number[], - options?: AgentTapeContextOptions - ): Promise { - return await this.sessionProjection.getTapeContext(sessionId, entryIds, options) - } - - async listTapeAnchors( - sessionId: string, - options?: AgentTapeAnchorsOptions - ): Promise { - return await this.sessionProjection.listTapeAnchors(sessionId, options) - } - - async handoffTape( - sessionId: string, - name: string, - state: Record = {} - ): Promise { - return await this.sessionProjection.handoffTape(sessionId, name, state) - } - - async listMessageViewManifests(messageId: string): Promise { - return await this.sessionProjection.listMessageViewManifests(messageId) - } - - async exportMessageTapeReplaySlice( - messageId: string, - options?: DeepChatTapeReplayExportOptions - ): Promise { - return await this.sessionProjection.exportMessageTapeReplaySlice(messageId, options) - } - - async mergeSubagentTape( - parentSessionId: string, - childSessionId: string, - meta: Record = {} - ): Promise { - await this.sessionAssignment.mergeSubagentTape(parentSessionId, childSessionId, meta) - } - - async discardSubagentTape( - parentSessionId: string, - childSessionId: string, - meta: Record = {} - ): Promise { - await this.sessionAssignment.discardSubagentTape(parentSessionId, childSessionId, meta) - } - - async getSearchResults(messageId: string, searchId?: string): Promise { - return await this.sessionProjection.getSearchResults(messageId, searchId) - } - - async listMessageTraces(messageId: string): Promise { - return await this.sessionProjection.listMessageTraces(messageId) - } - - async getMessageTraceCount(messageId: string): Promise { - return await this.sessionProjection.getMessageTraceCount(messageId) - } - - async getMessageIds(sessionId: string): Promise { - return await this.sessionProjection.getMessageIds(sessionId) - } - - async getMessage(messageId: string): Promise { - return await this.sessionProjection.getMessage(messageId) - } - - async activateSession(webContentsId: number, sessionId: string): Promise { - await this.sessionProjection.activate(webContentsId, sessionId) - } - - async deactivateSession(webContentsId: number): Promise { - await this.sessionProjection.deactivate(webContentsId) - } - - async getActiveSession(webContentsId: number): Promise { - return await this.sessionProjection.getActive(webContentsId) - } - - getActiveSessionId(webContentsId: number): string | null { - return this.sessionProjection.getActiveId(webContentsId) - } - - async renameSession(sessionId: string, title: string): Promise { - await this.sessionProjection.renameSession(sessionId, title) - } - - async toggleSessionPinned(sessionId: string, pinned: boolean): Promise { - await this.sessionProjection.toggleSessionPinned(sessionId, pinned) - } - - async clearSessionMessages(sessionId: string): Promise { - await this.sessionTurn.clearSessionMessages(sessionId) - } - - async deleteSession(sessionId: string): Promise { - await this.sessionLifecycle.deleteSession(sessionId) - } - - async getAgentTransferImpact(agentId: string): Promise { - return await this.sessionAssignment.getAgentTransferImpact(agentId) - } - - async moveAgentSessions( - fromAgentId: string, - toAgentId: string - ): Promise<{ movedSessionIds: string[]; deletedSessionIds: string[] }> { - return await this.sessionAssignment.moveAgentSessions(fromAgentId, toAgentId) - } - - async deleteAgentSessions(agentId: string): Promise { - return await this.sessionAssignment.deleteAgentSessions(agentId) - } - - async moveSessionToAgent(sessionId: string, toAgentId: string): Promise { - return await this.sessionAssignment.moveSessionToAgent(sessionId, toAgentId) - } - - async cancelGeneration(sessionId: string): Promise { - await this.sessionTurn.cancelGeneration(sessionId) - } - - clearSessionPermissions(sessionId: string): void { - this.sessionPermissionPort?.clearSessionPermissions(sessionId) - } - - async respondToolInteraction( - sessionId: string, - messageId: string, - toolCallId: string, - response: ToolInteractionResponse - ): Promise { - return await this.sessionTurn.respondToolInteraction(sessionId, messageId, toolCallId, response) - } - - async getAcpSessionCommands(sessionId: string): Promise< - Array<{ - name: string - description: string - input?: { hint: string } | null - }> - > { - return await this.sessionAssignment.getAcpSessionCommands(sessionId) - } - - async getAcpSessionConfigOptions(sessionId: string): Promise { - return await this.sessionAssignment.getAcpSessionConfigOptions(sessionId) - } - - async setAcpSessionConfigOption( - sessionId: string, - configId: string, - value: string | boolean - ): Promise { - return await this.sessionAssignment.setAcpSessionConfigOption(sessionId, configId, value) - } - - async getPermissionMode(sessionId: string): Promise { - return await this.sessionAssignment.getPermissionMode(sessionId) - } - - async setPermissionMode(sessionId: string, mode: PermissionMode): Promise { - await this.sessionAssignment.setPermissionMode(sessionId, mode) - } - - async setSessionSubagentEnabled(sessionId: string, enabled: boolean): Promise { - return await this.sessionAssignment.setSessionSubagentEnabled(sessionId, enabled) - } - - async setSessionModel( - sessionId: string, - providerId: string, - modelId: string - ): Promise { - return await this.sessionAssignment.setSessionModel(sessionId, providerId, modelId) - } - - async setSessionProjectDir( - sessionId: string, - projectDir: string | null - ): Promise { - return await this.sessionAssignment.setSessionProjectDir(sessionId, projectDir) - } - - async getSessionGenerationSettings(sessionId: string): Promise { - return await this.sessionAssignment.getSessionGenerationSettings(sessionId) - } - - async getSessionDisabledAgentTools(sessionId: string): Promise { - return await this.sessionAssignment.getSessionDisabledAgentTools(sessionId) - } - - async updateSessionDisabledAgentTools( - sessionId: string, - disabledAgentTools: string[] - ): Promise { - return await this.sessionAssignment.updateSessionDisabledAgentTools( - sessionId, - disabledAgentTools - ) - } - - async updateSessionGenerationSettings( - sessionId: string, - settings: Partial - ): Promise { - return await this.sessionAssignment.updateSessionGenerationSettings(sessionId, settings) - } -} diff --git a/src/main/presenter/floatingButtonPresenter/index.ts b/src/main/presenter/floatingButtonPresenter/index.ts index 4c69dff7a..1c2fca8aa 100644 --- a/src/main/presenter/floatingButtonPresenter/index.ts +++ b/src/main/presenter/floatingButtonPresenter/index.ts @@ -612,17 +612,12 @@ export class FloatingButtonPresenter { } private async loadSessions(): Promise { - const agentSessionPresenter = presenter.agentSessionPresenter as - | { - getSessionList?: (filters?: { agentId?: string }) => Promise - } - | undefined - - if (!agentSessionPresenter?.getSessionList) { + const projection = presenter?.sessionProjectionCoordinator + if (!projection) { return [] } - return await agentSessionPresenter.getSessionList() + return await projection.listSessions() } private async loadAgents(): Promise { @@ -631,13 +626,8 @@ export class FloatingButtonPresenter { private async openSession(sessionId: string): Promise { try { - const agentSessionPresenter = presenter.agentSessionPresenter as - | { - activateSession?: (webContentsId: number, sessionId: string) => Promise - } - | undefined - - if (!agentSessionPresenter?.activateSession) { + const projection = presenter?.sessionProjectionCoordinator + if (!projection) { return } @@ -646,7 +636,7 @@ export class FloatingButtonPresenter { return } - await agentSessionPresenter.activateSession(targetWindow.webContents.id, sessionId) + await projection.activate(targetWindow.webContents.id, sessionId) presenter.windowPresenter.show(targetWindow.id, true) this.setExpanded(false) } catch (error) { diff --git a/src/main/presenter/index.ts b/src/main/presenter/index.ts index 178175c49..6d59b6b86 100644 --- a/src/main/presenter/index.ts +++ b/src/main/presenter/index.ts @@ -29,7 +29,6 @@ import { IYoBrowserPresenter, ISkillPresenter, ISkillSyncPresenter, - IAgentSessionPresenter, IProjectPresenter, IRemoteControlPresenter } from '@shared/presenter' @@ -76,7 +75,6 @@ import { toAppSessionId } from '@/agent/shared/agentSessionIds' import { resolveAssistantModelSelection } from '@/agent/shared/assistantModelSelection' import { AgentUnavailableError } from '@/agent/shared/agentCatalogCodec' import { resolveAcpAgentAlias } from '@shared/utils/acpAgentAlias' -import { AgentSessionPresenter } from './agentSessionPresenter' import { SessionProjectionCoordinator } from './sessionApplication/projectionCoordinator' import { SessionAgentAssignmentPolicy } from './sessionApplication/agentAssignmentPolicy' import { SessionAgentAssignmentCoordinator } from './sessionApplication/agentAssignmentCoordinator' @@ -173,7 +171,6 @@ export class Presenter implements IPresenter { lifecycleManager: ILifecycleManager skillPresenter: ISkillPresenter skillSyncPresenter: ISkillSyncPresenter - agentSessionPresenter: IAgentSessionPresenter sessionProjectionCoordinator: SessionProjectionCoordinator sessionAgentAssignmentPolicy: SessionAgentAssignmentPolicy sessionAgentAssignmentCoordinator: SessionAgentAssignmentCoordinator @@ -301,7 +298,7 @@ export class Presenter implements IPresenter { const agentToolRuntime: AgentToolRuntimePort = { resolveConversationWorkdir: async (conversationId) => { try { - const session = await this.agentSessionPresenter?.getSession(conversationId) + const session = await this.sessionProjectionCoordinator.getSession(conversationId) const normalized = session?.projectDir?.trim() if (normalized) { return normalized @@ -316,25 +313,20 @@ export class Presenter implements IPresenter { return null }, resolveConversationSessionInfo: async (conversationId) => { - const session = await this.agentSessionPresenter?.getSession(conversationId) + const session = await this.sessionProjectionCoordinator.getSession(conversationId) if (!session) { return null } const agent = await this.configPresenter.getAgent(session.agentId) const agentType = await this.configPresenter.getAgentType(session.agentId) - const permissionMode = - typeof this.agentSessionPresenter?.getPermissionMode === 'function' - ? await this.agentSessionPresenter.getPermissionMode(session.id) - : 'full_access' + const permissionMode = await this.sessionAgentAssignmentCoordinator.getPermissionMode( + session.id + ) const generationSettings = - typeof this.agentSessionPresenter?.getSessionGenerationSettings === 'function' - ? await this.agentSessionPresenter.getSessionGenerationSettings(session.id) - : null + await this.sessionAgentAssignmentCoordinator.getSessionGenerationSettings(session.id) const disabledAgentTools = - typeof this.agentSessionPresenter?.getSessionDisabledAgentTools === 'function' - ? await this.agentSessionPresenter.getSessionDisabledAgentTools(session.id) - : [] + await this.sessionAgentAssignmentCoordinator.getSessionDisabledAgentTools(session.id) const activeSkills = await this.skillPresenter.getActiveSkills(session.id) const availableSubagentSlots = agentType === 'deepchat' && session.sessionKind === 'regular' @@ -363,19 +355,23 @@ export class Presenter implements IPresenter { } }, getTapeInfo: async (conversationId) => { - return await this.agentSessionPresenter.getTapeInfo(conversationId) + return await this.sessionProjectionCoordinator.getTapeInfo(conversationId) }, searchTape: async (conversationId, query, options) => { - return await this.agentSessionPresenter.searchTape(conversationId, query, options) + return await this.sessionProjectionCoordinator.searchTape(conversationId, query, options) }, getTapeContext: async (conversationId, entryIds, options) => { - return await this.agentSessionPresenter.getTapeContext(conversationId, entryIds, options) + return await this.sessionProjectionCoordinator.getTapeContext( + conversationId, + entryIds, + options + ) }, listTapeAnchors: async (conversationId, options) => { - return await this.agentSessionPresenter.listTapeAnchors(conversationId, options) + return await this.sessionProjectionCoordinator.listTapeAnchors(conversationId, options) }, handoffTape: async (conversationId, name, state) => { - return await this.agentSessionPresenter.handoffTape(conversationId, name, state) + return await this.sessionProjectionCoordinator.handoffTape(conversationId, name, state) }, isMemoryEnabled: (agentId) => this.memoryPresenter.isEnabled(agentId), rememberMemory: async (agentId, input, sourceSession, model) => @@ -409,29 +405,28 @@ export class Presenter implements IPresenter { listCronJobRuns: async (jobId, limit) => this.cronJobs.listRuns(jobId, limit), previewCronSchedule: async (input) => this.cronJobs.previewSchedule(input), createSubagentSession: async (input) => { - const agentSessionPresenter = this.agentSessionPresenter as IAgentSessionPresenter & { - createSubagentSession?: (createInput: typeof input) => Promise<{ - id: string - } | null> - } - const created = await agentSessionPresenter.createSubagentSession?.(input) - if (!created?.id) { - return null - } - + const created = await this.sessionLifecycleCoordinator.createSubagentSession(input) return await agentToolRuntime.resolveConversationSessionInfo(created.id) }, mergeSubagentTape: async (parentSessionId, childSessionId, meta) => { - await this.agentSessionPresenter.mergeSubagentTape(parentSessionId, childSessionId, meta) + await this.sessionAgentAssignmentCoordinator.mergeSubagentTape( + parentSessionId, + childSessionId, + meta + ) }, discardSubagentTape: async (parentSessionId, childSessionId, meta) => { - await this.agentSessionPresenter.discardSubagentTape(parentSessionId, childSessionId, meta) + await this.sessionAgentAssignmentCoordinator.discardSubagentTape( + parentSessionId, + childSessionId, + meta + ) }, sendConversationMessage: async (conversationId, content) => { - await this.agentSessionPresenter.sendMessage(conversationId, content) + await this.sessionTurnCoordinator.sendMessage(conversationId, content) }, cancelConversation: async (conversationId) => { - await this.agentSessionPresenter.cancelGeneration(conversationId) + await this.sessionTurnCoordinator.cancelGeneration(conversationId) }, subscribeDeepChatSessionUpdates: (listener) => subscribeDeepChatInternalSessionUpdates(listener), @@ -501,7 +496,7 @@ export class Presenter implements IPresenter { const skillSessionStatePort: SkillSessionStatePort = { hasNewSession: async (conversationId) => { try { - return Boolean(await this.agentSessionPresenter?.getSession(conversationId)) + return Boolean(await this.sessionProjectionCoordinator.getSession(conversationId)) } catch { return false } @@ -537,8 +532,8 @@ export class Presenter implements IPresenter { // Initialize new agent architecture presenters first (needed by hooksNotifications) this.hooksNotifications = new HooksNotificationsService(this.configPresenter, { - getSession: async () => null, - getMessage: async () => null + getSession: (sessionId) => this.sessionProjectionCoordinator.getSession(sessionId), + getMessage: (messageId) => this.sessionProjectionCoordinator.getMessage(messageId) }) this.cronJobs = new CronJobsService({ sqlitePresenter: this.sqlitePresenter as unknown as SQLitePresenter, @@ -944,15 +939,6 @@ export class Presenter implements IPresenter { configPresenter: this.configPresenter, llmProviderPresenter: this.llmproviderPresenter }) - this.agentSessionPresenter = new AgentSessionPresenter( - this.sessionProjectionCoordinator, - this.sessionLifecycleCoordinator, - this.sessionAgentAssignmentCoordinator, - this.sessionTurnCoordinator, - { - sessionPermissionPort - } - ) this.projectPresenter = new ProjectPresenter( this.sqlitePresenter as unknown as import('./sqlitePresenter').SQLitePresenter, this.devicePresenter, @@ -972,12 +958,6 @@ export class Presenter implements IPresenter { this.remoteControlPresenter = this.#remoteControlPresenter this.cronJobs.setRemoteDeliveryPort(this.#remoteControlPresenter) - // Update hooksNotifications with actual dependencies now that agentSessionPresenter is ready - this.hooksNotifications = new HooksNotificationsService(this.configPresenter, { - getSession: this.agentSessionPresenter.getSession.bind(this.agentSessionPresenter), - getMessage: this.agentSessionPresenter.getMessage.bind(this.agentSessionPresenter) - }) - this.setupEventBus() } @@ -1367,10 +1347,10 @@ const buildMainKernelRouteRuntime = () => configPresenter: presenter.configPresenter, llmProviderPresenter: presenter.llmproviderPresenter, acpProviderAdminPort: presenter.acpProviderAdminPort, - agentSessionPresenter: presenter.agentSessionPresenter, sessionLifecyclePort: presenter.sessionLifecycleCoordinator, sessionProjectionPort: presenter.sessionProjectionCoordinator, sessionTurnPort: presenter.sessionTurnCoordinator, + sessionAssignmentPort: presenter.sessionAgentAssignmentCoordinator, sessionPermissionPort: presenter.sessionPermissionPort, skillPresenter: presenter.skillPresenter, skillSyncPresenter: presenter.skillSyncPresenter, diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts b/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts index 7a31b1c4f..0de9edc64 100644 --- a/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts +++ b/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts @@ -266,11 +266,11 @@ export class ConversationSearchServer { // 获取对话历史 private async getConversationHistory(conversationId: string, includeSystem: boolean = false) { try { - const session = await presenter.agentSessionPresenter.getSession(conversationId) + const session = await presenter.sessionProjectionCoordinator.getSession(conversationId) if (!session) { throw new Error(`Session not found: ${conversationId}`) } - const records = await presenter.agentSessionPresenter.getMessages(conversationId) + const records = await presenter.sessionProjectionCoordinator.getMessages(conversationId) const filteredMessages = includeSystem ? records diff --git a/src/main/presenter/mcpPresenter/toolManager.ts b/src/main/presenter/mcpPresenter/toolManager.ts index 4aa87b565..e9cc56126 100644 --- a/src/main/presenter/mcpPresenter/toolManager.ts +++ b/src/main/presenter/mcpPresenter/toolManager.ts @@ -436,7 +436,7 @@ export class ToolManager { } try { - const session = await presenter.agentSessionPresenter.getSession(sessionId) + const session = await presenter.sessionProjectionCoordinator.getSession(sessionId) const agentId = session?.agentId?.trim() const providerId = session?.providerId?.trim() if (session && providerId === 'acp' && agentId) { diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts index 62ce50514..8d842ccb7 100644 --- a/src/main/routes/index.ts +++ b/src/main/routes/index.ts @@ -1,6 +1,5 @@ import { BrowserWindow, app, type IpcMain, type IpcMainInvokeEvent } from 'electron' import type { - IAgentSessionPresenter, IConfigPresenter, IConversationExporter, IDevicePresenter, @@ -403,11 +402,7 @@ import { } from '@shared/contracts/routes/memory.routes' import type { ChatMessageRecord } from '@shared/types/agent-interface' import { buildEffectiveTapeView } from '../presenter/agentRuntimePresenter/tapeEffectiveView' -import { - ChatService, - type ChatServiceProjectionPort, - type ChatServiceTurnPort -} from './chat/chatService' +import { ChatService, type ChatServiceProjectionPort } from './chat/chatService' import { dispatchConfigRoute } from './config/configRouteHandler' import { createPresenterHotPathPorts } from './hotPathPorts' import { dispatchModelRoute } from './models/modelRouteHandler' @@ -424,11 +419,7 @@ import { ProviderImportService } from './providers/providerImportService' import { ProviderService } from './providers/providerService' import { createSettingsRouteAdapter } from './settings/settingsAdapter' import { createSettingsRouteHandler } from './settings/settingsHandler' -import { - SessionService, - type SessionServiceLifecyclePort, - type SessionServiceProjectionPort -} from './sessions/sessionService' +import { SessionService, type SessionServiceProjectionPort } from './sessions/sessionService' import type { StartupWorkloadCoordinator } from '@/presenter/startupWorkloadCoordinator' import type { PluginPresenter } from '@/presenter/pluginPresenter' import type { DatabaseSecurityPresenter } from '@/presenter/databaseSecurityPresenter' @@ -447,6 +438,12 @@ import type { SessionHistorySearch } from './sessions/sessionHistorySearch' import type { SessionTranslation } from './sessions/sessionTranslation' import type { AgentSessionExportService } from '@/presenter/exporter/agentSessionExporter' import { listAvailableAgents } from '@/agent/shared/availableAgentCatalog' +import type { + SessionAgentAssignmentPort, + SessionLifecyclePort, + SessionTurnPort +} from '@/presenter/sessionApplication/ports' +import type { SessionProjectionCoordinator } from '@/presenter/sessionApplication/projectionCoordinator' const MEMORY_PERSONA_STATES = ['draft', 'active', 'superseded', 'rejected'] as const type MemoryPersonaState = (typeof MEMORY_PERSONA_STATES)[number] @@ -456,7 +453,10 @@ export type MainKernelRouteRuntime = { configPresenter: IConfigPresenter llmProviderPresenter: ILlmProviderPresenter acpProviderAdminPort: AcpProviderAdminPort - agentSessionPresenter: IAgentSessionPresenter + sessionLifecyclePort: SessionLifecyclePort + sessionProjectionPort: MainKernelSessionProjectionPort + sessionTurnPort: SessionTurnPort + sessionAssignmentPort: SessionAgentAssignmentPort skillPresenter: ISkillPresenter skillSyncPresenter: ISkillSyncPresenter exporter: IConversationExporter @@ -494,6 +494,22 @@ export type MainKernelRouteRuntime = { sessionTranslation: Pick } +export type MainKernelSessionProjectionPort = SessionServiceProjectionPort & + ChatServiceProjectionPort & + Pick< + SessionProjectionCoordinator, + | 'getActiveId' + | 'listLightweight' + | 'getLightweightByIds' + | 'getSearchResults' + | 'getTapeContext' + | 'listMessageTraces' + | 'listMessageViewManifests' + | 'exportMessageTapeReplaySlice' + | 'renameSession' + | 'toggleSessionPinned' + > + export function formatMemorySourceRecordContent(record: ChatMessageRecord): string { try { const parsed = JSON.parse(record.content) as unknown @@ -739,10 +755,10 @@ export function createMainKernelRouteRuntime(deps: { configPresenter: IConfigPresenter llmProviderPresenter: ILlmProviderPresenter acpProviderAdminPort: AcpProviderAdminPort - agentSessionPresenter: IAgentSessionPresenter - sessionLifecyclePort: SessionServiceLifecyclePort - sessionProjectionPort: SessionServiceProjectionPort & ChatServiceProjectionPort - sessionTurnPort: ChatServiceTurnPort + sessionLifecyclePort: SessionLifecyclePort + sessionProjectionPort: MainKernelSessionProjectionPort + sessionTurnPort: SessionTurnPort + sessionAssignmentPort: SessionAgentAssignmentPort sessionPermissionPort: Pick skillPresenter: ISkillPresenter skillSyncPresenter: ISkillSyncPresenter @@ -798,7 +814,10 @@ export function createMainKernelRouteRuntime(deps: { configPresenter: deps.configPresenter, llmProviderPresenter: deps.llmProviderPresenter, acpProviderAdminPort: deps.acpProviderAdminPort, - agentSessionPresenter: deps.agentSessionPresenter, + sessionLifecyclePort: deps.sessionLifecyclePort, + sessionProjectionPort: deps.sessionProjectionPort, + sessionTurnPort: deps.sessionTurnPort, + sessionAssignmentPort: deps.sessionAssignmentPort, skillPresenter: deps.skillPresenter, skillSyncPresenter: deps.skillSyncPresenter, exporter: deps.exporter, @@ -2743,13 +2762,10 @@ export async function dispatchDeepchatRoute( const coordinator = (runtime as Partial).startupWorkloadCoordinator if (!coordinator) { - const activeSessionId = runtime.agentSessionPresenter.getActiveSessionId( - context.webContentsId - ) + const activeSessionId = runtime.sessionProjectionPort.getActiveId(context.webContentsId) const activeSession = activeSessionId - ? (( - await runtime.agentSessionPresenter.getLightweightSessionsByIds([activeSessionId]) - )[0] ?? null) + ? ((await runtime.sessionProjectionPort.getLightweightByIds([activeSessionId]))[0] ?? + null) : null const [agents, acpEnabled, defaultChatWorkspacePath] = await Promise.all([ runtime.configPresenter.listAgents(), @@ -2793,13 +2809,10 @@ export async function dispatchDeepchatRoute( runId: coordinator.getRunId('main'), run: async () => { const startupRunId = coordinator.getRunId('main') - const activeSessionId = runtime.agentSessionPresenter.getActiveSessionId( - context.webContentsId - ) + const activeSessionId = runtime.sessionProjectionPort.getActiveId(context.webContentsId) const activeSession = activeSessionId - ? (( - await runtime.agentSessionPresenter.getLightweightSessionsByIds([activeSessionId]) - )[0] ?? null) + ? ((await runtime.sessionProjectionPort.getLightweightByIds([activeSessionId]))[0] ?? + null) : null const [agents, acpEnabled, defaultChatWorkspacePath] = await Promise.all([ runtime.configPresenter.listAgents(), @@ -2865,16 +2878,14 @@ export async function dispatchDeepchatRoute( case sessionsListLightweightRoute.name: { return await runTrackedRouteTask(runtime, routeName, context, async () => { const input = sessionsListLightweightRoute.input.parse(rawInput) - const page = await runtime.agentSessionPresenter.getLightweightSessionList(input) + const page = await runtime.sessionProjectionPort.listLightweight(input) return sessionsListLightweightRoute.output.parse(page) }) } case sessionsGetLightweightByIdsRoute.name: { const input = sessionsGetLightweightByIdsRoute.input.parse(rawInput) - const items = await runtime.agentSessionPresenter.getLightweightSessionsByIds( - input.sessionIds - ) + const items = await runtime.sessionProjectionPort.getLightweightByIds(input.sessionIds) return sessionsGetLightweightByIdsRoute.output.parse({ items }) } @@ -2898,28 +2909,25 @@ export async function dispatchDeepchatRoute( case sessionsEnsureAcpDraftRoute.name: { const input = sessionsEnsureAcpDraftRoute.input.parse(rawInput) - const session = await runtime.agentSessionPresenter.ensureAcpDraftSession(input) + const session = await runtime.sessionLifecyclePort.ensureAcpDraftSession(input) return sessionsEnsureAcpDraftRoute.output.parse({ session }) } case sessionsListPendingInputsRoute.name: { const input = sessionsListPendingInputsRoute.input.parse(rawInput) - const items = await runtime.agentSessionPresenter.listPendingInputs(input.sessionId) + const items = await runtime.sessionTurnPort.listPendingInputs(input.sessionId) return sessionsListPendingInputsRoute.output.parse({ items }) } case sessionsQueuePendingInputRoute.name: { const input = sessionsQueuePendingInputRoute.input.parse(rawInput) - const item = await runtime.agentSessionPresenter.queuePendingInput( - input.sessionId, - input.content - ) + const item = await runtime.sessionTurnPort.queuePendingInput(input.sessionId, input.content) return sessionsQueuePendingInputRoute.output.parse({ item }) } case sessionsUpdateQueuedInputRoute.name: { const input = sessionsUpdateQueuedInputRoute.input.parse(rawInput) - const item = await runtime.agentSessionPresenter.updateQueuedInput( + const item = await runtime.sessionTurnPort.updateQueuedInput( input.sessionId, input.itemId, input.content @@ -2929,7 +2937,7 @@ export async function dispatchDeepchatRoute( case sessionsMoveQueuedInputRoute.name: { const input = sessionsMoveQueuedInputRoute.input.parse(rawInput) - const items = await runtime.agentSessionPresenter.moveQueuedInput( + const items = await runtime.sessionTurnPort.moveQueuedInput( input.sessionId, input.itemId, input.toIndex @@ -2939,7 +2947,7 @@ export async function dispatchDeepchatRoute( case sessionsConvertPendingInputToSteerRoute.name: { const input = sessionsConvertPendingInputToSteerRoute.input.parse(rawInput) - const item = await runtime.agentSessionPresenter.convertPendingInputToSteer( + const item = await runtime.sessionTurnPort.convertPendingInputToSteer( input.sessionId, input.itemId ) @@ -2948,34 +2956,31 @@ export async function dispatchDeepchatRoute( case sessionsSteerPendingInputRoute.name: { const input = sessionsSteerPendingInputRoute.input.parse(rawInput) - const item = await runtime.agentSessionPresenter.steerPendingInput( - input.sessionId, - input.itemId - ) + const item = await runtime.sessionTurnPort.steerPendingInput(input.sessionId, input.itemId) return sessionsSteerPendingInputRoute.output.parse({ item }) } case sessionsDeletePendingInputRoute.name: { const input = sessionsDeletePendingInputRoute.input.parse(rawInput) - await runtime.agentSessionPresenter.deletePendingInput(input.sessionId, input.itemId) + await runtime.sessionTurnPort.deletePendingInput(input.sessionId, input.itemId) return sessionsDeletePendingInputRoute.output.parse({ deleted: true }) } case sessionsRetryMessageRoute.name: { const input = sessionsRetryMessageRoute.input.parse(rawInput) - await runtime.agentSessionPresenter.retryMessage(input.sessionId, input.messageId) + await runtime.sessionTurnPort.retryMessage(input.sessionId, input.messageId) return sessionsRetryMessageRoute.output.parse({ retried: true }) } case sessionsDeleteMessageRoute.name: { const input = sessionsDeleteMessageRoute.input.parse(rawInput) - await runtime.agentSessionPresenter.deleteMessage(input.sessionId, input.messageId) + await runtime.sessionTurnPort.deleteMessage(input.sessionId, input.messageId) return sessionsDeleteMessageRoute.output.parse({ deleted: true }) } case sessionsEditUserMessageRoute.name: { const input = sessionsEditUserMessageRoute.input.parse(rawInput) - const message = await runtime.agentSessionPresenter.editUserMessage( + const message = await runtime.sessionTurnPort.editUserMessage( input.sessionId, input.messageId, input.text @@ -2985,7 +2990,7 @@ export async function dispatchDeepchatRoute( case sessionsForkRoute.name: { const input = sessionsForkRoute.input.parse(rawInput) - const session = await runtime.agentSessionPresenter.forkSession( + const session = await runtime.sessionLifecyclePort.forkSession( input.sourceSessionId, input.targetMessageId, input.newTitle @@ -3001,7 +3006,7 @@ export async function dispatchDeepchatRoute( case sessionsGetSearchResultsRoute.name: { const input = sessionsGetSearchResultsRoute.input.parse(rawInput) - const results = await runtime.agentSessionPresenter.getSearchResults( + const results = await runtime.sessionProjectionPort.getSearchResults( input.messageId, input.searchId ) @@ -3010,7 +3015,7 @@ export async function dispatchDeepchatRoute( case sessionsGetTapeContextRoute.name: { const input = sessionsGetTapeContextRoute.input.parse(rawInput) - const context = await runtime.agentSessionPresenter.getTapeContext( + const context = await runtime.sessionProjectionPort.getTapeContext( input.sessionId, input.entryIds, input.options @@ -3020,8 +3025,8 @@ export async function dispatchDeepchatRoute( case sessionsListMessageTracesRoute.name: { const input = sessionsListMessageTracesRoute.input.parse(rawInput) - const traces = await runtime.agentSessionPresenter.listMessageTraces(input.messageId) - const manifests = await runtime.agentSessionPresenter.listMessageViewManifests( + const traces = await runtime.sessionProjectionPort.listMessageTraces(input.messageId) + const manifests = await runtime.sessionProjectionPort.listMessageViewManifests( input.messageId ) return sessionsListMessageTracesRoute.output.parse({ traces, manifests }) @@ -3029,7 +3034,7 @@ export async function dispatchDeepchatRoute( case sessionsExportMessageTapeReplaySliceRoute.name: { const input = sessionsExportMessageTapeReplaySliceRoute.input.parse(rawInput) - const slice = await runtime.agentSessionPresenter.exportMessageTapeReplaySlice( + const slice = await runtime.sessionProjectionPort.exportMessageTapeReplaySlice( input.messageId, input.options ) @@ -3066,25 +3071,25 @@ export async function dispatchDeepchatRoute( case sessionsRenameRoute.name: { const input = sessionsRenameRoute.input.parse(rawInput) - await runtime.agentSessionPresenter.renameSession(input.sessionId, input.title) + await runtime.sessionProjectionPort.renameSession(input.sessionId, input.title) return sessionsRenameRoute.output.parse({ updated: true }) } case sessionsTogglePinnedRoute.name: { const input = sessionsTogglePinnedRoute.input.parse(rawInput) - await runtime.agentSessionPresenter.toggleSessionPinned(input.sessionId, input.pinned) + await runtime.sessionProjectionPort.toggleSessionPinned(input.sessionId, input.pinned) return sessionsTogglePinnedRoute.output.parse({ updated: true }) } case sessionsClearMessagesRoute.name: { const input = sessionsClearMessagesRoute.input.parse(rawInput) - await runtime.agentSessionPresenter.clearSessionMessages(input.sessionId) + await runtime.sessionTurnPort.clearSessionMessages(input.sessionId) return sessionsClearMessagesRoute.output.parse({ cleared: true }) } case sessionsCompactRoute.name: { const input = sessionsCompactRoute.input.parse(rawInput) - const result = await runtime.agentSessionPresenter.compactSession(input.sessionId) + const result = await runtime.sessionTurnPort.compactSession(input.sessionId) return sessionsCompactRoute.output.parse(result) } @@ -3096,19 +3101,19 @@ export async function dispatchDeepchatRoute( case sessionsDeleteRoute.name: { const input = sessionsDeleteRoute.input.parse(rawInput) - await runtime.agentSessionPresenter.deleteSession(input.sessionId) + await runtime.sessionLifecyclePort.deleteSession(input.sessionId) return sessionsDeleteRoute.output.parse({ deleted: true }) } case sessionsGetAgentTransferImpactRoute.name: { const input = sessionsGetAgentTransferImpactRoute.input.parse(rawInput) - const impact = await runtime.agentSessionPresenter.getAgentTransferImpact(input.agentId) + const impact = await runtime.sessionAssignmentPort.getAgentTransferImpact(input.agentId) return sessionsGetAgentTransferImpactRoute.output.parse({ impact }) } case sessionsMoveAgentSessionsRoute.name: { const input = sessionsMoveAgentSessionsRoute.input.parse(rawInput) - const result = await runtime.agentSessionPresenter.moveAgentSessions( + const result = await runtime.sessionAssignmentPort.moveAgentSessions( input.fromAgentId, input.toAgentId ) @@ -3117,7 +3122,7 @@ export async function dispatchDeepchatRoute( case sessionsDeleteAgentSessionsRoute.name: { const input = sessionsDeleteAgentSessionsRoute.input.parse(rawInput) - const deletedSessionIds = await runtime.agentSessionPresenter.deleteAgentSessions( + const deletedSessionIds = await runtime.sessionAssignmentPort.deleteAgentSessions( input.agentId ) return sessionsDeleteAgentSessionsRoute.output.parse({ deletedSessionIds }) @@ -3125,7 +3130,7 @@ export async function dispatchDeepchatRoute( case sessionsMoveToAgentRoute.name: { const input = sessionsMoveToAgentRoute.input.parse(rawInput) - const session = await runtime.agentSessionPresenter.moveSessionToAgent( + const session = await runtime.sessionAssignmentPort.moveSessionToAgent( input.sessionId, input.toAgentId ) @@ -3134,19 +3139,19 @@ export async function dispatchDeepchatRoute( case sessionsGetAcpSessionCommandsRoute.name: { const input = sessionsGetAcpSessionCommandsRoute.input.parse(rawInput) - const commands = await runtime.agentSessionPresenter.getAcpSessionCommands(input.sessionId) + const commands = await runtime.sessionAssignmentPort.getAcpSessionCommands(input.sessionId) return sessionsGetAcpSessionCommandsRoute.output.parse({ commands }) } case sessionsGetAcpSessionConfigOptionsRoute.name: { const input = sessionsGetAcpSessionConfigOptionsRoute.input.parse(rawInput) - const state = await runtime.agentSessionPresenter.getAcpSessionConfigOptions(input.sessionId) + const state = await runtime.sessionAssignmentPort.getAcpSessionConfigOptions(input.sessionId) return sessionsGetAcpSessionConfigOptionsRoute.output.parse({ state }) } case sessionsSetAcpSessionConfigOptionRoute.name: { const input = sessionsSetAcpSessionConfigOptionRoute.input.parse(rawInput) - const state = await runtime.agentSessionPresenter.setAcpSessionConfigOption( + const state = await runtime.sessionAssignmentPort.setAcpSessionConfigOption( input.sessionId, input.configId, input.value @@ -3156,19 +3161,19 @@ export async function dispatchDeepchatRoute( case sessionsGetPermissionModeRoute.name: { const input = sessionsGetPermissionModeRoute.input.parse(rawInput) - const mode = await runtime.agentSessionPresenter.getPermissionMode(input.sessionId) + const mode = await runtime.sessionAssignmentPort.getPermissionMode(input.sessionId) return sessionsGetPermissionModeRoute.output.parse({ mode }) } case sessionsSetPermissionModeRoute.name: { const input = sessionsSetPermissionModeRoute.input.parse(rawInput) - await runtime.agentSessionPresenter.setPermissionMode(input.sessionId, input.mode) + await runtime.sessionAssignmentPort.setPermissionMode(input.sessionId, input.mode) return sessionsSetPermissionModeRoute.output.parse({ updated: true }) } case sessionsSetSubagentEnabledRoute.name: { const input = sessionsSetSubagentEnabledRoute.input.parse(rawInput) - const session = await runtime.agentSessionPresenter.setSessionSubagentEnabled( + const session = await runtime.sessionAssignmentPort.setSessionSubagentEnabled( input.sessionId, input.enabled ) @@ -3177,7 +3182,7 @@ export async function dispatchDeepchatRoute( case sessionsSetModelRoute.name: { const input = sessionsSetModelRoute.input.parse(rawInput) - const session = await runtime.agentSessionPresenter.setSessionModel( + const session = await runtime.sessionAssignmentPort.setSessionModel( input.sessionId, input.providerId, input.modelId @@ -3187,7 +3192,7 @@ export async function dispatchDeepchatRoute( case sessionsSetProjectDirRoute.name: { const input = sessionsSetProjectDirRoute.input.parse(rawInput) - const session = await runtime.agentSessionPresenter.setSessionProjectDir( + const session = await runtime.sessionAssignmentPort.setSessionProjectDir( input.sessionId, input.projectDir ) @@ -3196,7 +3201,7 @@ export async function dispatchDeepchatRoute( case sessionsGetGenerationSettingsRoute.name: { const input = sessionsGetGenerationSettingsRoute.input.parse(rawInput) - const settings = await runtime.agentSessionPresenter.getSessionGenerationSettings( + const settings = await runtime.sessionAssignmentPort.getSessionGenerationSettings( input.sessionId ) return sessionsGetGenerationSettingsRoute.output.parse({ settings }) @@ -3204,7 +3209,7 @@ export async function dispatchDeepchatRoute( case sessionsGetDisabledAgentToolsRoute.name: { const input = sessionsGetDisabledAgentToolsRoute.input.parse(rawInput) - const disabledAgentTools = await runtime.agentSessionPresenter.getSessionDisabledAgentTools( + const disabledAgentTools = await runtime.sessionAssignmentPort.getSessionDisabledAgentTools( input.sessionId ) return sessionsGetDisabledAgentToolsRoute.output.parse({ disabledAgentTools }) @@ -3213,7 +3218,7 @@ export async function dispatchDeepchatRoute( case sessionsUpdateDisabledAgentToolsRoute.name: { const input = sessionsUpdateDisabledAgentToolsRoute.input.parse(rawInput) const disabledAgentTools = - await runtime.agentSessionPresenter.updateSessionDisabledAgentTools( + await runtime.sessionAssignmentPort.updateSessionDisabledAgentTools( input.sessionId, input.disabledAgentTools ) @@ -3222,7 +3227,7 @@ export async function dispatchDeepchatRoute( case sessionsUpdateGenerationSettingsRoute.name: { const input = sessionsUpdateGenerationSettingsRoute.input.parse(rawInput) - const settings = await runtime.agentSessionPresenter.updateSessionGenerationSettings( + const settings = await runtime.sessionAssignmentPort.updateSessionGenerationSettings( input.sessionId, input.settings ) diff --git a/src/shared/types/presenters/agent-session.presenter.d.ts b/src/shared/types/presenters/agent-session.presenter.d.ts deleted file mode 100644 index 418e6cfc2..000000000 --- a/src/shared/types/presenters/agent-session.presenter.d.ts +++ /dev/null @@ -1,189 +0,0 @@ -import type { - SessionListItem, - SessionLightweightListResult, - SessionPageCursor, - MessagePageCursor, - ChatMessagePageResult, - CreateSessionInput, - CreateDetachedSessionInput, - SessionWithState, - ChatMessageRecord, - MessageTraceRecord, - PermissionMode, - SessionGenerationSettings, - SessionCompactionState, - PendingSessionInputRecord, - SendMessageInput, - MessageStartResult, - ToolInteractionResponse, - ToolInteractionResult, - AgentTapeInfo, - AgentTapeAnchorsOptions, - AgentTapeContextOptions, - AgentTapeContextResult, - AgentTapeSearchOptions, - AgentTapeSearchResult, - AgentTapeAnchorResult, - AgentTransferImpact -} from '../agent-interface' -import type { DeepChatTapeViewManifestRecord } from '../tape-view-manifest' -import type { DeepChatTapeReplayExportOptions, DeepChatTapeReplaySlice } from '../tape-replay' -import type { AcpConfigState } from './llmprovider.presenter' -import type { SearchResult } from './thread.presenter' - -export interface IAgentSessionPresenter { - createSession(input: CreateSessionInput, webContentsId: number): Promise - createDetachedSession(input: CreateDetachedSessionInput): Promise - ensureAcpDraftSession(input: { - agentId: string - projectDir: string - permissionMode?: PermissionMode - }): Promise - listPendingInputs(sessionId: string): Promise - queuePendingInput( - sessionId: string, - content: string | SendMessageInput - ): Promise - updateQueuedInput( - sessionId: string, - itemId: string, - content: string | SendMessageInput - ): Promise - moveQueuedInput( - sessionId: string, - itemId: string, - toIndex: number - ): Promise - convertPendingInputToSteer(sessionId: string, itemId: string): Promise - steerPendingInput(sessionId: string, itemId: string): Promise - deletePendingInput(sessionId: string, itemId: string): Promise - sendMessage( - sessionId: string, - content: string | SendMessageInput, - options?: { maxProviderRounds?: number } - ): Promise - steerActiveTurn(sessionId: string, content: string | SendMessageInput): Promise - retryMessage(sessionId: string, messageId: string): Promise - deleteMessage(sessionId: string, messageId: string): Promise - editUserMessage(sessionId: string, messageId: string, text: string): Promise - forkSession( - sourceSessionId: string, - targetMessageId: string, - newTitle?: string - ): Promise - getSessionList(filters?: { - agentId?: string - projectDir?: string - includeSubagents?: boolean - parentSessionId?: string - }): Promise - getSession(sessionId: string): Promise - getMessages(sessionId: string): Promise - listMessagesPage( - sessionId: string, - options?: { - limit?: number - cursor?: MessagePageCursor | null - } - ): Promise - getSessionCompactionState(sessionId: string): Promise - compactSession(sessionId: string): Promise<{ compacted: boolean; state: SessionCompactionState }> - getTapeInfo(sessionId: string): Promise - searchTape( - sessionId: string, - query: string, - options?: AgentTapeSearchOptions - ): Promise - getTapeContext( - sessionId: string, - entryIds: number[], - options?: AgentTapeContextOptions - ): Promise - listTapeAnchors( - sessionId: string, - options?: AgentTapeAnchorsOptions - ): Promise - handoffTape( - sessionId: string, - name: string, - state?: Record - ): Promise - mergeSubagentTape( - parentSessionId: string, - childSessionId: string, - meta?: Record - ): Promise - discardSubagentTape( - parentSessionId: string, - childSessionId: string, - meta?: Record - ): Promise - getSearchResults(messageId: string, searchId?: string): Promise - listMessageTraces(messageId: string): Promise - listMessageViewManifests(messageId: string): Promise - exportMessageTapeReplaySlice( - messageId: string, - options?: DeepChatTapeReplayExportOptions - ): Promise - getMessageTraceCount(messageId: string): Promise - getMessageIds(sessionId: string): Promise - getMessage(messageId: string): Promise - activateSession(webContentsId: number, sessionId: string): Promise - deactivateSession(webContentsId: number): Promise - getActiveSession(webContentsId: number): Promise - getActiveSessionId(webContentsId: number): string | null - getLightweightSessionList(options?: { - limit?: number - cursor?: SessionPageCursor | null - includeSubagents?: boolean - agentId?: string - prioritizeSessionId?: string - }): Promise - getLightweightSessionsByIds(sessionIds: string[]): Promise - renameSession(sessionId: string, title: string): Promise - toggleSessionPinned(sessionId: string, pinned: boolean): Promise - clearSessionMessages(sessionId: string): Promise - deleteSession(sessionId: string): Promise - getAgentTransferImpact(agentId: string): Promise - moveAgentSessions( - fromAgentId: string, - toAgentId: string - ): Promise<{ movedSessionIds: string[]; deletedSessionIds: string[] }> - deleteAgentSessions(agentId: string): Promise - moveSessionToAgent(sessionId: string, toAgentId: string): Promise - cancelGeneration(sessionId: string): Promise - respondToolInteraction( - sessionId: string, - messageId: string, - toolCallId: string, - response: ToolInteractionResponse - ): Promise - getAcpSessionCommands(sessionId: string): Promise< - Array<{ - name: string - description: string - input?: { hint: string } | null - }> - > - getAcpSessionConfigOptions(sessionId: string): Promise - setAcpSessionConfigOption( - sessionId: string, - configId: string, - value: string | boolean - ): Promise - getPermissionMode(sessionId: string): Promise - setPermissionMode(sessionId: string, mode: PermissionMode): Promise - setSessionSubagentEnabled(sessionId: string, enabled: boolean): Promise - setSessionModel(sessionId: string, providerId: string, modelId: string): Promise - setSessionProjectDir(sessionId: string, projectDir: string | null): Promise - getSessionGenerationSettings(sessionId: string): Promise - getSessionDisabledAgentTools(sessionId: string): Promise - updateSessionDisabledAgentTools( - sessionId: string, - disabledAgentTools: string[] - ): Promise - updateSessionGenerationSettings( - sessionId: string, - settings: Partial - ): Promise -} diff --git a/src/shared/types/presenters/core.presenter.d.ts b/src/shared/types/presenters/core.presenter.d.ts index a24ec249e..18e48ae03 100644 --- a/src/shared/types/presenters/core.presenter.d.ts +++ b/src/shared/types/presenters/core.presenter.d.ts @@ -20,7 +20,6 @@ import type { IWorkspacePresenter } from './workspace' import type { IToolPresenter } from './tool.presenter' import type { ISkillPresenter } from '../skill' import type { ISkillSyncPresenter } from '../skillSync' -import type { IAgentSessionPresenter } from './agent-session.presenter' import type { IProjectPresenter } from './project.presenter' import type { BrowserPageInfo, DownloadInfo, ScreenshotOptions, YoBrowserStatus } from '../browser' import type { IWindowPresenter, TabData } from './window.presenter' @@ -446,7 +445,6 @@ export interface IPresenter { toolPresenter: IToolPresenter skillPresenter: ISkillPresenter skillSyncPresenter: ISkillSyncPresenter - agentSessionPresenter: IAgentSessionPresenter projectPresenter: IProjectPresenter init(): void destroy(): Promise diff --git a/src/shared/types/presenters/index.d.ts b/src/shared/types/presenters/index.d.ts index 8ad8f3de3..637c5d81a 100644 --- a/src/shared/types/presenters/index.d.ts +++ b/src/shared/types/presenters/index.d.ts @@ -100,7 +100,6 @@ export type { } from './acp.presenter' // New agent architecture types -export type { IAgentSessionPresenter } from './agent-session.presenter' export type { IProjectPresenter } from './project.presenter' export type { ChannelSettingsMap, diff --git a/test/main/presenter/floatingButtonPresenter/index.test.ts b/test/main/presenter/floatingButtonPresenter/index.test.ts index b8e4da961..af6fe3a1e 100644 --- a/test/main/presenter/floatingButtonPresenter/index.test.ts +++ b/test/main/presenter/floatingButtonPresenter/index.test.ts @@ -166,9 +166,9 @@ vi.mock('../../../../src/main/presenter/floatingButtonPresenter/FloatingButtonWi vi.mock('../../../../src/main/presenter/index', () => ({ presenter: { - agentSessionPresenter: { - getSessionList: getSessionListMock, - activateSession: vi.fn() + sessionProjectionCoordinator: { + listSessions: getSessionListMock, + activate: vi.fn() }, windowPresenter: { mainWindow: null, diff --git a/test/main/presenter/mcpPresenter/toolManager.test.ts b/test/main/presenter/mcpPresenter/toolManager.test.ts index 375a2eaa4..663e4b6a7 100644 --- a/test/main/presenter/mcpPresenter/toolManager.test.ts +++ b/test/main/presenter/mcpPresenter/toolManager.test.ts @@ -7,7 +7,7 @@ const eventBusMocks = vi.hoisted(() => ({ })) const presenterMocks = vi.hoisted(() => ({ - agentSessionPresenter: { + sessionProjectionCoordinator: { getSession: vi.fn() } })) @@ -192,7 +192,7 @@ describe('ToolManager', () => { configPresenter.getAcpAgents.mockResolvedValue([{ id: 'agent-1', name: 'Agent 1' }]) configPresenter.getAgentMcpSelections.mockResolvedValue([]) - presenterMocks.agentSessionPresenter.getSession.mockResolvedValue({ + presenterMocks.sessionProjectionCoordinator.getSession.mockResolvedValue({ id: 'session-1', agentId: 'agent-1', title: 'New Chat', @@ -346,7 +346,7 @@ describe('ToolManager', () => { it('skips ACP session resolution when provider hint is non-ACP', async () => { const client = createClient('open-server') const configPresenter = createConfigPresenter('open-server') - presenterMocks.agentSessionPresenter.getSession.mockResolvedValue(null) + presenterMocks.sessionProjectionCoordinator.getSession.mockResolvedValue(null) const manager = new ToolManager( configPresenter as never, @@ -367,7 +367,7 @@ describe('ToolManager', () => { expect(result.isError).toBe(false) expect(result.content).toBe('ok') expect(client.callTool).toHaveBeenCalledWith('echo', {}) - expect(presenterMocks.agentSessionPresenter.getSession).not.toHaveBeenCalled() + expect(presenterMocks.sessionProjectionCoordinator.getSession).not.toHaveBeenCalled() expect(configPresenter.getAgentMcpSelections).not.toHaveBeenCalled() expect( warnSpy.mock.calls.some((call) => @@ -682,7 +682,7 @@ describe('ToolManager', () => { const client = createClient('open-server') const configPresenter = createConfigPresenter('open-server') - presenterMocks.agentSessionPresenter.getSession.mockResolvedValue({ + presenterMocks.sessionProjectionCoordinator.getSession.mockResolvedValue({ id: 'session-2', agentId: 'deepchat', title: 'Normal Chat', @@ -813,7 +813,7 @@ describe('ToolManager', () => { it('treats missing provider hint as a fallback to new session resolution', async () => { const client = createClient('open-server') const configPresenter = createConfigPresenter('open-server') - presenterMocks.agentSessionPresenter.getSession.mockResolvedValue(null) + presenterMocks.sessionProjectionCoordinator.getSession.mockResolvedValue(null) const manager = new ToolManager( configPresenter as never, @@ -833,7 +833,9 @@ describe('ToolManager', () => { expect(result.isError).toBe(false) expect(result.content).toBe('ok') expect(client.callTool).toHaveBeenCalledWith('echo', {}) - expect(presenterMocks.agentSessionPresenter.getSession).toHaveBeenCalledWith('conv-fallback') + expect(presenterMocks.sessionProjectionCoordinator.getSession).toHaveBeenCalledWith( + 'conv-fallback' + ) expect(configPresenter.getAgentMcpSelections).not.toHaveBeenCalled() }) }) diff --git a/test/main/presenter/presenterCallErrorHandler.test.ts b/test/main/presenter/presenterCallErrorHandler.test.ts index 43f6ea757..e24f78408 100644 --- a/test/main/presenter/presenterCallErrorHandler.test.ts +++ b/test/main/presenter/presenterCallErrorHandler.test.ts @@ -31,7 +31,7 @@ describe('presenterCallErrorHandler', () => { await expect( handlePresenterCallResult(Promise.reject(error), { webContentsId: 7, - presenterName: 'agentSessionPresenter', + presenterName: 'sessionLifecycleCoordinator', methodName: 'createSession' }) ).rejects.toThrow(error) @@ -56,7 +56,7 @@ describe('presenterCallErrorHandler', () => { await expect( handlePresenterCallResult(Promise.reject(new Error(errorMessage)), { webContentsId: 9, - presenterName: 'agentSessionPresenter', + presenterName: 'sessionLifecycleCoordinator', methodName: 'createSession' }) ).rejects.toThrow(errorMessage) @@ -80,7 +80,7 @@ describe('presenterCallErrorHandler', () => { await expect( handlePresenterCallResult(Promise.reject(new Error(errorMessage)), { webContentsId: 11, - presenterName: 'agentSessionPresenter', + presenterName: 'sessionLifecycleCoordinator', methodName: 'createSession' }) ).rejects.toThrow(errorMessage) diff --git a/test/main/presenter/agentSessionPresenter/assignmentCoordinatorFixture.ts b/test/main/presenter/sessionApplication/assignmentCoordinatorFixture.ts similarity index 100% rename from test/main/presenter/agentSessionPresenter/assignmentCoordinatorFixture.ts rename to test/main/presenter/sessionApplication/assignmentCoordinatorFixture.ts diff --git a/test/main/presenter/agentSessionPresenter/projectionCoordinatorFixture.ts b/test/main/presenter/sessionApplication/projectionCoordinatorFixture.ts similarity index 100% rename from test/main/presenter/agentSessionPresenter/projectionCoordinatorFixture.ts rename to test/main/presenter/sessionApplication/projectionCoordinatorFixture.ts diff --git a/test/main/presenter/agentSessionPresenter/integration.test.ts b/test/main/presenter/sessionApplication/runtimeIntegration.test.ts similarity index 93% rename from test/main/presenter/agentSessionPresenter/integration.test.ts rename to test/main/presenter/sessionApplication/runtimeIntegration.test.ts index 5ba4e5d79..d28c1b88b 100644 --- a/test/main/presenter/agentSessionPresenter/integration.test.ts +++ b/test/main/presenter/sessionApplication/runtimeIntegration.test.ts @@ -1,6 +1,5 @@ import { AppSessionService } from '@/agent/shared/appSessionService' import { describe, it, expect, vi, beforeEach } from 'vitest' -import { AgentSessionPresenter } from '@/presenter/agentSessionPresenter/index' import { AgentRuntimePresenter } from '@/presenter/agentRuntimePresenter/index' import { estimateMessagesTokens } from '@/presenter/agentRuntimePresenter/contextBuilder' import { NewSessionHooksBridge } from '@/presenter/hooksNotifications/newSessionBridge' @@ -676,7 +675,9 @@ describe('Integration: createSession end-to-end', () => { let sqlitePresenter: ReturnType let llmProvider: ReturnType let configPresenter: ReturnType - let agentPresenter: AgentSessionPresenter + let lifecycle: ReturnType['lifecycle'] + let turn: ReturnType['turn'] + let projection: ReturnType beforeEach(() => { vi.clearAllMocks() @@ -703,7 +704,7 @@ describe('Integration: createSession end-to-end', () => { transcriptMutation: deepchatAgent, tape: deepchatAgent } - const projection = createProjectionCoordinatorFixture({ + projection = createProjectionCoordinatorFixture({ agentManager, appSessionService, llmProviderPresenter: llmProvider, @@ -720,16 +721,12 @@ describe('Integration: createSession end-to-end', () => { projection, acp: llmProvider }) - agentPresenter = new AgentSessionPresenter( - projection, - sessionApplications.lifecycle, - sessionApplications.assignment, - sessionApplications.turn - ) + lifecycle = sessionApplications.lifecycle + turn = sessionApplications.turn }) it('createSession → new_sessions row + deepchat_sessions row + messages + events', async () => { - const session = await agentPresenter.createSession( + const session = await lifecycle.createSession( { agentId: 'deepchat', message: 'Tell me a joke', @@ -805,26 +802,23 @@ describe('Integration: createSession end-to-end', () => { }) it('session list returns enriched sessions', async () => { - await agentPresenter.createSession({ agentId: 'deepchat', message: 'Hello' }, 1) + await lifecycle.createSession({ agentId: 'deepchat', message: 'Hello' }, 1) // Wait for processMessage to complete await new Promise((r) => setTimeout(r, 50)) - const sessions = await agentPresenter.getSessionList() + const sessions = await projection.listSessions() expect(sessions).toHaveLength(1) expect(sessions[0].status).toBe('idle') expect(sessions[0].providerId).toBe('openai') }) it('deleteSession cleans up all data', async () => { - const session = await agentPresenter.createSession( - { agentId: 'deepchat', message: 'To delete' }, - 1 - ) + const session = await lifecycle.createSession({ agentId: 'deepchat', message: 'To delete' }, 1) await new Promise((r) => setTimeout(r, 50)) - await agentPresenter.deleteSession(session.id) + await lifecycle.deleteSession(session.id) expect(sqlitePresenter.deepchatMessagesTable.deleteBySession).toHaveBeenCalledWith(session.id) expect(sqlitePresenter.deepchatSessionsTable.delete).toHaveBeenCalledWith(session.id) @@ -832,14 +826,11 @@ describe('Integration: createSession end-to-end', () => { }) it('clearSessionMessages clears messages but keeps session row', async () => { - const session = await agentPresenter.createSession( - { agentId: 'deepchat', message: 'To clear' }, - 1 - ) + const session = await lifecycle.createSession({ agentId: 'deepchat', message: 'To clear' }, 1) await new Promise((r) => setTimeout(r, 50)) - await agentPresenter.clearSessionMessages(session.id) + await turn.clearSessionMessages(session.id) expect(sqlitePresenter.deepchatMessagesTable.deleteBySession).toHaveBeenCalledWith(session.id) expect(sqlitePresenter.newSessionsTable.delete).not.toHaveBeenCalledWith(session.id) @@ -853,7 +844,7 @@ describe('Integration: ACP hooks bridge', () => { let sqlitePresenter: ReturnType let llmProvider: ReturnType let configPresenter: ReturnType - let agentPresenter: AgentSessionPresenter + let lifecycle: ReturnType['lifecycle'] let hookDispatcher: { dispatchEvent: ReturnType } beforeEach(() => { @@ -901,16 +892,11 @@ describe('Integration: ACP hooks bridge', () => { projection, acp: llmProvider }) - agentPresenter = new AgentSessionPresenter( - projection, - sessionApplications.lifecycle, - sessionApplications.assignment, - sessionApplications.turn - ) + lifecycle = sessionApplications.lifecycle }) it('dispatches lifecycle hooks for ACP sessions through the new bridge', async () => { - const session = await agentPresenter.createSession( + const session = await lifecycle.createSession( { agentId: 'coder', providerId: 'acp', @@ -966,7 +952,10 @@ describe('Integration: multi-turn context', () => { let llmProvider: ReturnType let configPresenter: ReturnType let deepchatAgent: AgentRuntimePresenter - let agentPresenter: AgentSessionPresenter + let lifecycle: ReturnType['lifecycle'] + let turn: ReturnType['turn'] + let assignment: ReturnType['assignment'] + let projection: ReturnType beforeEach(() => { vi.clearAllMocks() @@ -993,7 +982,7 @@ describe('Integration: multi-turn context', () => { transcriptMutation: deepchatAgent, tape: deepchatAgent } - const projection = createProjectionCoordinatorFixture({ + projection = createProjectionCoordinatorFixture({ agentManager, appSessionService, llmProviderPresenter: llmProvider, @@ -1010,17 +999,14 @@ describe('Integration: multi-turn context', () => { projection, acp: llmProvider }) - agentPresenter = new AgentSessionPresenter( - projection, - sessionApplications.lifecycle, - sessionApplications.assignment, - sessionApplications.turn - ) + lifecycle = sessionApplications.lifecycle + turn = sessionApplications.turn + assignment = sessionApplications.assignment }) it('second message includes first exchange in LLM context', async () => { // Send first message - const session = await agentPresenter.createSession( + const session = await lifecycle.createSession( { agentId: 'deepchat', message: 'Hello', projectDir: null }, 1 ) @@ -1029,7 +1015,7 @@ describe('Integration: multi-turn context', () => { await new Promise((r) => setTimeout(r, 50)) // Send second message - await agentPresenter.sendMessage(session.id, 'Follow up question') + await turn.sendMessage(session.id, 'Follow up question') // Wait for second processMessage to complete await new Promise((r) => setTimeout(r, 50)) @@ -1093,16 +1079,16 @@ describe('Integration: multi-turn context', () => { }) it('supports both string and object sendMessage input', async () => { - const session = await agentPresenter.createSession( + const session = await lifecycle.createSession( { agentId: 'deepchat', message: 'Hello', projectDir: null }, 1 ) await new Promise((r) => setTimeout(r, 50)) - await agentPresenter.sendMessage(session.id, 'Follow up (string)') + await turn.sendMessage(session.id, 'Follow up (string)') await new Promise((r) => setTimeout(r, 50)) - await agentPresenter.sendMessage(session.id, { + await turn.sendMessage(session.id, { text: 'Follow up (object)', files: [{ name: 'a.md', path: '/tmp/a.md', mimeType: 'text/markdown', content: '# a' } as any] }) @@ -1130,7 +1116,7 @@ describe('Integration: multi-turn context', () => { } llmProvider.getProviderInstance.mockReturnValue(providerInstance) - const session = await agentPresenter.createSession( + const session = await lifecycle.createSession( { agentId: 'deepchat', message: 'First turn', projectDir: null }, 1 ) @@ -1166,16 +1152,16 @@ describe('Integration: multi-turn context', () => { } llmProvider.getProviderInstance.mockReturnValue(providerInstance) - const session = await agentPresenter.createSession( + const session = await lifecycle.createSession( { agentId: 'deepchat', message: 'First turn', projectDir: null }, 1 ) await new Promise((r) => setTimeout(r, 80)) - await agentPresenter.sendMessage(session.id, 'Immediate follow up') + await turn.sendMessage(session.id, 'Immediate follow up') await new Promise((r) => setTimeout(r, 20)) - await expect(agentPresenter.listPendingInputs(session.id)).resolves.toEqual([]) + await expect(turn.listPendingInputs(session.id)).resolves.toEqual([]) const messagesDuringSecondTurn = sqlitePresenter.deepchatMessagesTable.getBySession(session.id) const userMessagesDuringSecondTurn = messagesDuringSecondTurn.filter( @@ -1207,15 +1193,15 @@ describe('Integration: multi-turn context', () => { } llmProvider.getProviderInstance.mockReturnValue(providerInstance) - const session = await agentPresenter.createSession( + const session = await lifecycle.createSession( { agentId: 'deepchat', message: 'First turn', projectDir: null }, 1 ) await new Promise((r) => setTimeout(r, 20)) - await agentPresenter.queuePendingInput(session.id, 'Queued follow up') + await turn.queuePendingInput(session.id, 'Queued follow up') - const pendingBeforeRelease = await agentPresenter.listPendingInputs(session.id) + const pendingBeforeRelease = await turn.listPendingInputs(session.id) expect(pendingBeforeRelease).toHaveLength(1) expect(pendingBeforeRelease[0].mode).toBe('queue') @@ -1231,7 +1217,7 @@ describe('Integration: multi-turn context', () => { const afterUserMessages = afterMessages.filter((message: any) => message.role === 'user') expect(afterUserMessages).toHaveLength(2) expect(JSON.parse(afterUserMessages[1].content).text).toBe('Queued follow up') - await expect(agentPresenter.listPendingInputs(session.id)).resolves.toEqual([]) + await expect(turn.listPendingInputs(session.id)).resolves.toEqual([]) }) it('drains converted steer inputs as visible user messages before queued messages', async () => { @@ -1253,18 +1239,18 @@ describe('Integration: multi-turn context', () => { } llmProvider.getProviderInstance.mockReturnValue(providerInstance) - const session = await agentPresenter.createSession( + const session = await lifecycle.createSession( { agentId: 'deepchat', message: 'Turn one', projectDir: null }, 1 ) await new Promise((r) => setTimeout(r, 20)) - await agentPresenter.queuePendingInput(session.id, 'Steer instruction') - await agentPresenter.queuePendingInput(session.id, 'Queued target') + await turn.queuePendingInput(session.id, 'Steer instruction') + await turn.queuePendingInput(session.id, 'Queued target') - const pendingInputs = await agentPresenter.listPendingInputs(session.id) + const pendingInputs = await turn.listPendingInputs(session.id) expect(pendingInputs).toHaveLength(2) - await agentPresenter.convertPendingInputToSteer(session.id, pendingInputs[0].id) + await turn.convertPendingInputToSteer(session.id, pendingInputs[0].id) releaseFirstTurn?.() await vi.waitFor(() => { @@ -1297,7 +1283,7 @@ describe('Integration: multi-turn context', () => { 'Steer instruction', 'Queued target' ]) - await expect(agentPresenter.listPendingInputs(session.id)).resolves.toEqual([]) + await expect(turn.listPendingInputs(session.id)).resolves.toEqual([]) }) it('rebudgets long converted steer inputs as their own visible turn', async () => { @@ -1322,18 +1308,18 @@ describe('Integration: multi-turn context', () => { } llmProvider.getProviderInstance.mockReturnValue(providerInstance) - const session = await agentPresenter.createSession( + const session = await lifecycle.createSession( { agentId: 'deepchat', message: firstPrompt, projectDir: null }, 1 ) await new Promise((r) => setTimeout(r, 20)) - await agentPresenter.updateSessionGenerationSettings(session.id, { + await assignment.updateSessionGenerationSettings(session.id, { contextLength: 2048, maxTokens: 128 }) - await agentPresenter.queuePendingInput(session.id, { + await turn.queuePendingInput(session.id, { text: steerUserText, files: [ { @@ -1343,11 +1329,11 @@ describe('Integration: multi-turn context', () => { } as any ] }) - await agentPresenter.queuePendingInput(session.id, 'Queued target') + await turn.queuePendingInput(session.id, 'Queued target') - const pendingInputs = await agentPresenter.listPendingInputs(session.id) + const pendingInputs = await turn.listPendingInputs(session.id) expect(pendingInputs).toHaveLength(2) - await agentPresenter.convertPendingInputToSteer(session.id, pendingInputs[0].id) + await turn.convertPendingInputToSteer(session.id, pendingInputs[0].id) releaseFirstTurn?.() await vi.waitFor(() => { @@ -1483,19 +1469,19 @@ describe('Integration: multi-turn context', () => { } llmProvider.getProviderInstance.mockReturnValue(providerInstance) - const session = await agentPresenter.createSession( + const session = await lifecycle.createSession( { agentId: 'deepchat', message: 'Fail first', projectDir: null }, 1 ) await new Promise((r) => setTimeout(r, 50)) - const failedSession = await agentPresenter.getSession(session.id) + const failedSession = await projection.getSession(session.id) expect(failedSession?.status).toBe('error') - await agentPresenter.sendMessage(session.id, 'Recover after error') + await turn.sendMessage(session.id, 'Recover after error') await new Promise((r) => setTimeout(r, 80)) - const recoveredSession = await agentPresenter.getSession(session.id) + const recoveredSession = await projection.getSession(session.id) expect(recoveredSession?.status).toBe('idle') expect(providerInstance.coreStream).toHaveBeenCalledTimes(2) @@ -1503,7 +1489,7 @@ describe('Integration: multi-turn context', () => { const userMessages = messages.filter((message: any) => message.role === 'user') expect(userMessages).toHaveLength(2) expect(JSON.parse(userMessages[1].content).text).toBe('Recover after error') - await expect(agentPresenter.listPendingInputs(session.id)).resolves.toEqual([]) + await expect(turn.listPendingInputs(session.id)).resolves.toEqual([]) }) it('drains queued turns when a new message is enqueued after a session error', async () => { @@ -1524,30 +1510,30 @@ describe('Integration: multi-turn context', () => { } llmProvider.getProviderInstance.mockReturnValue(providerInstance) - const session = await agentPresenter.createSession( + const session = await lifecycle.createSession( { agentId: 'deepchat', message: 'Turn that errors', projectDir: null }, 1 ) await new Promise((r) => setTimeout(r, 20)) - await agentPresenter.queuePendingInput(session.id, 'Queued while failing') + await turn.queuePendingInput(session.id, 'Queued while failing') releaseFirstTurn?.() await new Promise((r) => setTimeout(r, 80)) - const failedSession = await agentPresenter.getSession(session.id) + const failedSession = await projection.getSession(session.id) expect(failedSession?.status).toBe('error') - const pendingAfterError = await agentPresenter.listPendingInputs(session.id) + const pendingAfterError = await turn.listPendingInputs(session.id) expect(pendingAfterError).toHaveLength(1) expect(pendingAfterError[0].mode).toBe('queue') // Enqueuing from an errored session drains the backlog (no manual resume step). - await agentPresenter.queuePendingInput(session.id, 'New message after error') + await turn.queuePendingInput(session.id, 'New message after error') await vi.waitFor(() => { expect(providerInstance.coreStream).toHaveBeenCalledTimes(3) }) - const recoveredSession = await agentPresenter.getSession(session.id) + const recoveredSession = await projection.getSession(session.id) expect(recoveredSession?.status).toBe('idle') expect(providerInstance.coreStream).toHaveBeenCalledTimes(3) @@ -1556,7 +1542,7 @@ describe('Integration: multi-turn context', () => { expect(userMessages).toHaveLength(3) expect(JSON.parse(userMessages[1].content).text).toBe('Queued while failing') expect(JSON.parse(userMessages[2].content).text).toBe('New message after error') - await expect(agentPresenter.listPendingInputs(session.id)).resolves.toEqual([]) + await expect(turn.listPendingInputs(session.id)).resolves.toEqual([]) }) }) diff --git a/test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts b/test/main/presenter/sessionApplication/sessionApplication.integration.test.ts similarity index 92% rename from test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts rename to test/main/presenter/sessionApplication/sessionApplication.integration.test.ts index 047db1e41..e9eaec676 100644 --- a/test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts +++ b/test/main/presenter/sessionApplication/sessionApplication.integration.test.ts @@ -7,7 +7,6 @@ import { AgentUnavailableError } from '@/agent/shared/agentCatalogCodec' import type { AcpAgentDescriptor } from '@/agent/shared/agentDescriptors' import type { AppSessionId } from '@/agent/shared/agentSessionIds' import { AgentRepository } from '@/presenter/agentRepository' -import { AgentSessionPresenter } from '@/presenter/agentSessionPresenter/index' import { resolveAcpAgentAlias } from '@shared/utils/acpAgentAlias' import { createDeepChatAgentBackendFixture } from '../../agent/manager/deepChatAgentBackendFixture' import { createProjectionCoordinatorFixture } from './projectionCoordinatorFixture' @@ -598,16 +597,11 @@ function createDescriptorIndependentDeleteHarness(options: { skillPresenter, sessionPermissionPort }) - const presenter = new AgentSessionPresenter( - projection, - sessionApplications.lifecycle, - sessionApplications.assignment, - sessionApplications.turn, - { sessionPermissionPort } - ) - return { - presenter, + lifecycle: sessionApplications.lifecycle, + turn: sessionApplications.turn, + assignment: sessionApplications.assignment, + projection, manager, sessions, deleteSessionRow, @@ -621,7 +615,7 @@ function createDescriptorIndependentDeleteHarness(options: { } } -describe('AgentSessionPresenter', () => { +describe('Session application coordinators', () => { let deepChatAgent: ReturnType let llmProviderPresenter: ReturnType let configPresenter: ReturnType @@ -640,7 +634,10 @@ describe('AgentSessionPresenter', () => { resolveSubagentFacet: ReturnType cleanupSessionBackends: ReturnType } - let presenter: AgentSessionPresenter + let lifecycle: ReturnType['lifecycle'] + let turn: ReturnType['turn'] + let assignment: ReturnType['assignment'] + let projection: ReturnType beforeEach(() => { vi.clearAllMocks() @@ -730,7 +727,7 @@ describe('AgentSessionPresenter', () => { transcriptMutation: deepChatAgent, tape: deepChatAgent } as any - const projection = createProjectionCoordinatorFixture({ + projection = createProjectionCoordinatorFixture({ agentManager: agentManager as any, appSessionService, llmProviderPresenter, @@ -749,12 +746,9 @@ describe('AgentSessionPresenter', () => { acp: llmProviderPresenter, skillPresenter }) - presenter = new AgentSessionPresenter( - projection, - sessionApplications.lifecycle, - sessionApplications.assignment, - sessionApplications.turn - ) + lifecycle = sessionApplications.lifecycle + turn = sessionApplications.turn + assignment = sessionApplications.assignment }) it('routes public session operations through the real catalog and manager chain', async () => { @@ -1001,23 +995,18 @@ describe('AgentSessionPresenter', () => { acp: llmProviderPresenter, skillPresenter }) - const integratedPresenter = new AgentSessionPresenter( - projection, - sessionApplications.lifecycle, - sessionApplications.assignment, - sessionApplications.turn - ) - - await expect(integratedPresenter.sendMessage('deepchat-session', 'Hello')).resolves.toEqual({ + await expect( + sessionApplications.turn.sendMessage('deepchat-session', 'Hello') + ).resolves.toEqual({ requestId: null, messageId: null }) - await expect(integratedPresenter.sendMessage('acp-session', 'Hello')).resolves.toEqual({ + await expect(sessionApplications.turn.sendMessage('acp-session', 'Hello')).resolves.toEqual({ requestId: null, messageId: null }) - await integratedPresenter.queuePendingInput('acp-session', 'Later') - await integratedPresenter.steerActiveTurn('acp-session', 'Now') + await sessionApplications.turn.queuePendingInput('acp-session', 'Later') + await sessionApplications.turn.steerActiveTurn('acp-session', 'Now') await new Promise((resolve) => setTimeout(resolve, 0)) expect(deepchatImplementation.queuePendingInput).toHaveBeenCalledWith( 'deepchat-session', @@ -1047,21 +1036,20 @@ describe('AgentSessionPresenter', () => { 'claude-acp' ) await expect( - integratedPresenter.sendMessage('unconfigured-session', 'Hello') + sessionApplications.turn.sendMessage('unconfigured-session', 'Hello') ).rejects.toMatchObject({ code: 'AGENT_UNAVAILABLE', reason: 'invalid-config' }) - await expect(integratedPresenter.deleteSession('unconfigured-session')).resolves.toBeUndefined() + await expect( + sessionApplications.lifecycle.deleteSession('unconfigured-session') + ).resolves.toBeUndefined() expect(directAcpRuntime.cleanupSession).toHaveBeenCalledWith('unconfigured-session') expect(deepchatImplementation.queuePendingInput).toHaveBeenCalledTimes(1) expect(llmProviderPresenter.setAcpWorkdir).not.toHaveBeenCalled() - await expect(integratedPresenter.sendMessage('missing-session', 'Hello')).rejects.toMatchObject( - { - code: 'AGENT_NOT_FOUND' - } - ) - await expect(integratedPresenter.sendMessage('broken-session', 'Hello')).rejects.toMatchObject({ - code: 'AGENT_UNAVAILABLE', - reason: 'missing-manual-command' - }) + await expect( + sessionApplications.turn.sendMessage('missing-session', 'Hello') + ).rejects.toMatchObject({ code: 'AGENT_NOT_FOUND' }) + await expect( + sessionApplications.turn.sendMessage('broken-session', 'Hello') + ).rejects.toMatchObject({ code: 'AGENT_UNAVAILABLE', reason: 'missing-manual-command' }) }) it.each([ @@ -1130,7 +1118,7 @@ describe('AgentSessionPresenter', () => { agents: agent ? [agent] : [] }) - await expect(harness.presenter.deleteSession('delete-target')).resolves.toBeUndefined() + await expect(harness.lifecycle.deleteSession('delete-target')).resolves.toBeUndefined() expect(harness.resolveExecutableDescriptor).not.toHaveBeenCalled() expect(harness.resolveInput).not.toHaveBeenCalled() @@ -1194,7 +1182,7 @@ describe('AgentSessionPresenter', () => { ] }) - await expect(harness.presenter.deleteSession('parent-session')).resolves.toBeUndefined() + await expect(harness.lifecycle.deleteSession('parent-session')).resolves.toBeUndefined() expect(harness.resolveExecutableDescriptor).not.toHaveBeenCalled() expect(harness.resolveInput).not.toHaveBeenCalled() @@ -1230,7 +1218,7 @@ describe('AgentSessionPresenter', () => { await resolved.handle.snapshot() harness.resolveExecutableDescriptor.mockClear() - await expect(harness.presenter.deleteSession('deepchat-session')).resolves.toBeUndefined() + await expect(harness.lifecycle.deleteSession('deepchat-session')).resolves.toBeUndefined() expect(harness.resolveExecutableDescriptor).not.toHaveBeenCalled() expect(harness.deepchatImplementation.cancelGeneration).toHaveBeenCalledExactlyOnceWith( @@ -1260,7 +1248,7 @@ describe('AgentSessionPresenter', () => { }) harness.resolveExecutableDescriptor.mockClear() - await expect(harness.presenter.deleteSession('direct-session')).resolves.toBeUndefined() + await expect(harness.lifecycle.deleteSession('direct-session')).resolves.toBeUndefined() expect(harness.resolveExecutableDescriptor).not.toHaveBeenCalled() expect(harness.resolveInput).not.toHaveBeenCalled() @@ -1274,7 +1262,7 @@ describe('AgentSessionPresenter', () => { describe('createSession', () => { it('creates session with correct parameters', async () => { - const result = await presenter.createSession( + const result = await lifecycle.createSession( { agentId: 'deepchat', message: 'Hello world', projectDir: '/tmp/proj' }, 1 ) @@ -1307,20 +1295,20 @@ describe('AgentSessionPresenter', () => { it('derives title from first 50 chars of message', async () => { const longMessage = 'A'.repeat(100) - const result = await presenter.createSession({ agentId: 'deepchat', message: longMessage }, 1) + const result = await lifecycle.createSession({ agentId: 'deepchat', message: longMessage }, 1) expect(result.title).toBe('A'.repeat(50)) }) it('defaults to "New Chat" when message is empty', async () => { - const result = await presenter.createSession({ agentId: 'deepchat', message: '' }, 1) + const result = await lifecycle.createSession({ agentId: 'deepchat', message: '' }, 1) expect(result.title).toBe('New Chat') expect(llmProviderPresenter.summaryTitles).not.toHaveBeenCalled() }) it('calls agent.initSession and queues the first message', async () => { - await presenter.createSession({ agentId: 'deepchat', message: 'Hello' }, 1) + await lifecycle.createSession({ agentId: 'deepchat', message: 'Hello' }, 1) expect(deepChatAgent.initSession).toHaveBeenCalledWith( 'mock-session-id', @@ -1359,7 +1347,7 @@ describe('AgentSessionPresenter', () => { }) deepChatAgent.queuePendingInput.mockImplementation(queuePendingInput) - await presenter.createSession( + await lifecycle.createSession( { agentId: 'deepchat', message: 'Hello', projectDir: '/tmp/proj' }, 1 ) @@ -1373,7 +1361,7 @@ describe('AgentSessionPresenter', () => { }) it('publishes typed created session update', async () => { - await presenter.createSession({ agentId: 'deepchat', message: 'Hello' }, 42) + await lifecycle.createSession({ agentId: 'deepchat', message: 'Hello' }, 42) expectSessionsUpdated({ sessionIds: ['mock-session-id'], @@ -1384,7 +1372,7 @@ describe('AgentSessionPresenter', () => { }) it('uses default provider/model from config when not specified', async () => { - await presenter.createSession({ agentId: 'deepchat', message: 'Hi' }, 1) + await lifecycle.createSession({ agentId: 'deepchat', message: 'Hi' }, 1) expect(deepChatAgent.initSession).toHaveBeenCalledWith( expect.any(String), @@ -1403,7 +1391,7 @@ describe('AgentSessionPresenter', () => { defaultProjectPath: '/workspaces/agent-default' }) - await presenter.createSession({ agentId: 'deepchat', message: 'Hi' }, 1) + await lifecycle.createSession({ agentId: 'deepchat', message: 'Hi' }, 1) expect(deepChatAgent.initSession).toHaveBeenCalledWith( expect.any(String), @@ -1424,7 +1412,7 @@ describe('AgentSessionPresenter', () => { configPresenter.resolveDeepChatAgentConfig.mockResolvedValue({}) configPresenter.getDefaultProjectPath.mockReturnValue('/workspaces/global-default') - await presenter.createSession({ agentId: 'deepchat', message: 'Hi' }, 1) + await lifecycle.createSession({ agentId: 'deepchat', message: 'Hi' }, 1) expect(deepChatAgent.initSession).toHaveBeenCalledWith( expect.any(String), @@ -1447,7 +1435,7 @@ describe('AgentSessionPresenter', () => { }) configPresenter.getDefaultProjectPath.mockReturnValue('/workspaces/global-default') - await presenter.createSession({ agentId: 'deepchat', message: 'Hi', projectDir: null }, 1) + await lifecycle.createSession({ agentId: 'deepchat', message: 'Hi', projectDir: null }, 1) expect(deepChatAgent.initSession).toHaveBeenCalledWith( expect.any(String), @@ -1465,7 +1453,7 @@ describe('AgentSessionPresenter', () => { }) it('uses input provider/model when specified', async () => { - await presenter.createSession( + await lifecycle.createSession( { agentId: 'deepchat', message: 'Hi', providerId: 'anthropic', modelId: 'claude-3' }, 1 ) @@ -1483,7 +1471,7 @@ describe('AgentSessionPresenter', () => { }) it('uses input permission mode when specified', async () => { - await presenter.createSession( + await lifecycle.createSession( { agentId: 'deepchat', message: 'Hi', @@ -1507,7 +1495,7 @@ describe('AgentSessionPresenter', () => { }) it('passes generationSettings to agent.initSession', async () => { - await presenter.createSession( + await lifecycle.createSession( { agentId: 'deepchat', message: 'Hi', @@ -1542,7 +1530,7 @@ describe('AgentSessionPresenter', () => { }) it('persists disabled agent tools for deepchat sessions', async () => { - await presenter.createSession( + await lifecycle.createSession( { agentId: 'deepchat', message: 'Hi', @@ -1567,12 +1555,12 @@ describe('AgentSessionPresenter', () => { configPresenter.getDefaultModel.mockReturnValue(null) await expect( - presenter.createSession({ agentId: 'deepchat', message: 'Hi' }, 1) + lifecycle.createSession({ agentId: 'deepchat', message: 'Hi' }, 1) ).rejects.toThrow('No provider or model configured') }) it('passes active skills as initial message-scoped skills without pinning the session', async () => { - await presenter.createSession( + await lifecycle.createSession( { agentId: 'deepchat', message: 'Hello', @@ -1648,7 +1636,7 @@ describe('AgentSessionPresenter', () => { } ]) - await presenter.createSession({ agentId: 'deepchat', message: 'Please summarize' }, 1) + await lifecycle.createSession({ agentId: 'deepchat', message: 'Please summarize' }, 1) await new Promise((r) => setTimeout(r, 20)) expect(llmProviderPresenter.summaryTitles).toHaveBeenCalled() @@ -1705,7 +1693,7 @@ describe('AgentSessionPresenter', () => { vi.useFakeTimers() try { - await presenter.createSession({ agentId: 'deepchat', message: 'Please summarize' }, 1) + await lifecycle.createSession({ agentId: 'deepchat', message: 'Please summarize' }, 1) await vi.advanceTimersByTimeAsync(20) expect(llmProviderPresenter.summaryTitles).not.toHaveBeenCalled() @@ -1789,7 +1777,7 @@ describe('AgentSessionPresenter', () => { vi.useFakeTimers() try { - await presenter.createSession({ agentId: 'deepchat', message: 'Please summarize' }, 1) + await lifecycle.createSession({ agentId: 'deepchat', message: 'Please summarize' }, 1) await vi.advanceTimersByTimeAsync(20) expect(llmProviderPresenter.summaryTitles).not.toHaveBeenCalled() @@ -1816,8 +1804,8 @@ describe('AgentSessionPresenter', () => { }) ) - await presenter.createSession({ agentId: 'deepchat', message: 'Original prompt' }, 1) - await presenter.renameSession('mock-session-id', 'Manual title') + await lifecycle.createSession({ agentId: 'deepchat', message: 'Original prompt' }, 1) + await projection.renameSession('mock-session-id', 'Manual title') resolveMessages([ { id: 'u1', @@ -1854,9 +1842,9 @@ describe('AgentSessionPresenter', () => { }) ) - await presenter.createSession({ agentId: 'deepchat', message: 'Original prompt' }, 1) + await lifecycle.createSession({ agentId: 'deepchat', message: 'Original prompt' }, 1) await vi.waitFor(() => expect(llmProviderPresenter.summaryTitles).toHaveBeenCalledOnce()) - await presenter.renameSession('mock-session-id', 'Manual title') + await projection.renameSession('mock-session-id', 'Manual title') resolveTitle('Generated title') await new Promise((resolve) => setTimeout(resolve, 0)) @@ -1885,7 +1873,7 @@ describe('AgentSessionPresenter', () => { .mockRejectedValueOnce(new Error('assistant unavailable')) .mockResolvedValueOnce('Fallback title') - await presenter.createSession( + await lifecycle.createSession( { agentId: 'deepchat', message: 'Original prompt', @@ -1925,7 +1913,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - await presenter.createSession( + await lifecycle.createSession( { agentId: 'acp-coder', message: 'Hello ACP', @@ -1966,7 +1954,7 @@ describe('AgentSessionPresenter', () => { llmProviderPresenter.setAcpWorkdir.mockRejectedValueOnce(new Error('sync failed')) await expect( - presenter.createSession( + lifecycle.createSession( { agentId: 'acp-coder', message: 'Hello ACP', @@ -1994,7 +1982,7 @@ describe('AgentSessionPresenter', () => { deepChatAgent.destroySession.mockRejectedValueOnce(closeError) await expect( - presenter.createSession( + lifecycle.createSession( { agentId: 'deepchat', message: 'Hello ACP', @@ -2015,7 +2003,7 @@ describe('AgentSessionPresenter', () => { expect(deepChatAgent.destroySession.mock.invocationCallOrder[0]).toBeLessThan( sqlitePresenter.newSessionsTable.delete.mock.invocationCallOrder[0] ) - expect(presenter.getActiveSessionId(1)).toBeNull() + expect(projection.getActiveId(1)).toBeNull() expect(publishDeepchatEvent).not.toHaveBeenCalled() expect(sessionUiPort.refreshSessionUi).not.toHaveBeenCalled() expect(warnSpy).toHaveBeenCalledWith( @@ -2032,7 +2020,7 @@ describe('AgentSessionPresenter', () => { describe('createDetachedSession', () => { it('creates a detached session without window activation', async () => { - const result = await presenter.createDetachedSession({ + const result = await lifecycle.createDetachedSession({ title: 'Remote Session', agentId: 'deepchat' }) @@ -2065,7 +2053,7 @@ describe('AgentSessionPresenter', () => { }) configPresenter.getAgentType.mockResolvedValue('deepchat') - await presenter.createDetachedSession({ + await lifecycle.createDetachedSession({ title: 'Remote Agent Session', agentId: 'deepchat-remote' }) @@ -2102,9 +2090,9 @@ describe('AgentSessionPresenter', () => { describe('draft turn promotion failures', () => { it.each([ - ['send', () => presenter.sendMessage('s-draft', 'New prompt')], - ['steer', () => presenter.steerActiveTurn('s-draft', 'New prompt')], - ['queue', () => presenter.queuePendingInput('s-draft', 'New prompt')] + ['send', () => turn.sendMessage('s-draft', 'New prompt')], + ['steer', () => turn.steerActiveTurn('s-draft', 'New prompt')], + ['queue', () => turn.queuePendingInput('s-draft', 'New prompt')] ])('does not roll back draft promotion when %s fails later', async (_name, invoke) => { const row = { id: 's-draft', @@ -2170,7 +2158,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - await presenter.sendMessage('s-draft', 'Hello ACP') + await turn.sendMessage('s-draft', 'Hello ACP') expect(sqlitePresenter.newSessionsTable.update).toHaveBeenCalledWith('s-draft', { is_draft: 0, @@ -2202,7 +2190,7 @@ describe('AgentSessionPresenter', () => { }) deepChatAgent.hasMessages.mockResolvedValue(true) - await presenter.sendMessage('s1', 'Follow-up') + await turn.sendMessage('s1', 'Follow-up') expect(agentManager.resolveSessionHandle).toHaveBeenCalledWith('s1') expect(deepChatAgent.queuePendingInput).toHaveBeenCalledWith( 's1', @@ -2242,7 +2230,7 @@ describe('AgentSessionPresenter', () => { } }) - await presenter.sendMessage('s1', 'Scheduled prompt', { maxProviderRounds: 4 }) + await turn.sendMessage('s1', 'Scheduled prompt', { maxProviderRounds: 4 }) expect(send).toHaveBeenCalledWith({ content: { text: 'Scheduled prompt', files: [] }, @@ -2269,7 +2257,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - await presenter.sendMessage('s1', 'Refine this') + await turn.sendMessage('s1', 'Refine this') expect(deepChatAgent.queuePendingInput).toHaveBeenCalledWith( 's1', @@ -2284,9 +2272,7 @@ describe('AgentSessionPresenter', () => { it('throws for unknown session', async () => { sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined) - await expect(presenter.sendMessage('unknown', 'hi')).rejects.toThrow( - 'Session not found: unknown' - ) + await expect(turn.sendMessage('unknown', 'hi')).rejects.toThrow('Session not found: unknown') }) }) @@ -2316,7 +2302,7 @@ describe('AgentSessionPresenter', () => { updated_at: 1000 }) - await presenter.queuePendingInput('s1', 'Later') + await turn.queuePendingInput('s1', 'Later') expect(queuePendingInput).toHaveBeenCalledWith( 's1', @@ -2350,11 +2336,11 @@ describe('AgentSessionPresenter', () => { deepChatAgent.convertPendingInputToSteer.mockResolvedValueOnce(converted) deepChatAgent.steerPendingInput.mockResolvedValueOnce(steered) - await expect(presenter.updateQueuedInput('s1', 'pending-1', 'Updated')).resolves.toBe(updated) - await expect(presenter.moveQueuedInput('s1', 'pending-1', 2)).resolves.toBe(moved) - await expect(presenter.convertPendingInputToSteer('s1', 'pending-1')).resolves.toBe(converted) - await expect(presenter.steerPendingInput('s1', 'pending-1')).resolves.toBe(steered) - await expect(presenter.deletePendingInput('s1', 'pending-1')).resolves.toBeUndefined() + await expect(turn.updateQueuedInput('s1', 'pending-1', 'Updated')).resolves.toBe(updated) + await expect(turn.moveQueuedInput('s1', 'pending-1', 2)).resolves.toBe(moved) + await expect(turn.convertPendingInputToSteer('s1', 'pending-1')).resolves.toBe(converted) + await expect(turn.steerPendingInput('s1', 'pending-1')).resolves.toBe(steered) + await expect(turn.deletePendingInput('s1', 'pending-1')).resolves.toBeUndefined() expect(deepChatAgent.updateQueuedInput).toHaveBeenCalledWith('s1', 'pending-1', { text: 'Updated', @@ -2367,11 +2353,11 @@ describe('AgentSessionPresenter', () => { }) it.each([ - ['update', () => presenter.updateQueuedInput('missing', 'pending-1', 'Updated')], - ['move', () => presenter.moveQueuedInput('missing', 'pending-1', 1)], - ['convert', () => presenter.convertPendingInputToSteer('missing', 'pending-1')], - ['steer', () => presenter.steerPendingInput('missing', 'pending-1')], - ['delete', () => presenter.deletePendingInput('missing', 'pending-1')] + ['update', () => turn.updateQueuedInput('missing', 'pending-1', 'Updated')], + ['move', () => turn.moveQueuedInput('missing', 'pending-1', 1)], + ['convert', () => turn.convertPendingInputToSteer('missing', 'pending-1')], + ['steer', () => turn.steerPendingInput('missing', 'pending-1')], + ['delete', () => turn.deletePendingInput('missing', 'pending-1')] ])('rejects %s before resolving a handle for a missing session', async (_name, invoke) => { sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined) @@ -2383,7 +2369,7 @@ describe('AgentSessionPresenter', () => { it('returns an empty pending list for a missing session without resolving a handle', async () => { sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined) - await expect(presenter.listPendingInputs('missing')).resolves.toEqual([]) + await expect(turn.listPendingInputs('missing')).resolves.toEqual([]) expect(agentManager.resolveSessionHandle).not.toHaveBeenCalled() }) @@ -2409,7 +2395,7 @@ describe('AgentSessionPresenter', () => { projectDir: '/retry/project' }) - await presenter.retryMessage('s1', 'message-1') + await turn.retryMessage('s1', 'message-1') expect(deepChatAgent.prepareRetryMessage).toHaveBeenCalledWith('s1', 'message-1') expect(deepChatAgent.processMessage).toHaveBeenCalledWith( @@ -2427,12 +2413,12 @@ describe('AgentSessionPresenter', () => { const cancelError = new Error('cancel failed') deepChatAgent.cancelGeneration.mockRejectedValueOnce(cancelError) - await expect(presenter.deleteMessage('s1', 'message-1')).rejects.toBe(cancelError) + await expect(turn.deleteMessage('s1', 'message-1')).rejects.toBe(cancelError) expect(deepChatAgent.deleteMessage).not.toHaveBeenCalled() deepChatAgent.cancelGeneration.mockResolvedValueOnce(undefined) - await presenter.deleteMessage('s1', 'message-1') + await turn.deleteMessage('s1', 'message-1') expect(deepChatAgent.deleteMessage).toHaveBeenCalledWith('s1', 'message-1') expect(deepChatAgent.cancelGeneration.mock.invocationCallOrder.at(-1)).toBeLessThan( @@ -2444,7 +2430,7 @@ describe('AgentSessionPresenter', () => { const edited = { id: 'message-1', sessionId: 's1', role: 'user', content: 'Edited' } deepChatAgent.editUserMessage.mockResolvedValueOnce(edited) - await expect(presenter.editUserMessage('s1', 'message-1', 'Edited')).resolves.toBe(edited) + await expect(turn.editUserMessage('s1', 'message-1', 'Edited')).resolves.toBe(edited) expect(deepChatAgent.editUserMessage).toHaveBeenCalledWith('s1', 'message-1', 'Edited') expect(deepChatAgent.cancelGeneration).not.toHaveBeenCalled() @@ -2470,7 +2456,7 @@ describe('AgentSessionPresenter', () => { } }) - await presenter.setSessionProjectDir('s1', '/tmp/workspace') + await assignment.setSessionProjectDir('s1', '/tmp/workspace') expect(sqlitePresenter.newSessionsTable.update).toHaveBeenCalledWith('s1', { project_dir: '/tmp/workspace' @@ -2497,7 +2483,7 @@ describe('AgentSessionPresenter', () => { const runtimeError = new Error('runtime project update failed') deepChatAgent.setSessionProjectDir.mockRejectedValueOnce(runtimeError) - await expect(presenter.setSessionProjectDir('s1', '/tmp/workspace')).rejects.toBe( + await expect(assignment.setSessionProjectDir('s1', '/tmp/workspace')).rejects.toBe( runtimeError ) @@ -2538,7 +2524,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - const session = await presenter.ensureAcpDraftSession({ + const session = await lifecycle.ensureAcpDraftSession({ agentId: 'acp-coder', projectDir: '/tmp/workspace' }) @@ -2584,7 +2570,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - const session = await presenter.ensureAcpDraftSession({ + const session = await lifecycle.ensureAcpDraftSession({ agentId: 'acp-coder', projectDir: '/tmp/workspace' }) @@ -2641,7 +2627,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - const session = await presenter.createSubagentSession({ + const session = await lifecycle.createSubagentSession({ parentSessionId: 'parent-1', agentId: ' kimi-cli ', slotId: 'reviewer', @@ -2735,7 +2721,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - const session = await presenter.createSubagentSession({ + const session = await lifecycle.createSubagentSession({ parentSessionId: 'parent-1', agentId: 'acp-reviewer', slotId: 'reviewer', @@ -2813,7 +2799,7 @@ describe('AgentSessionPresenter', () => { } }) - const session = await presenter.createSubagentSession({ + const session = await lifecycle.createSubagentSession({ parentSessionId: 'parent-1', agentId: 'deepchat', slotId: 'reviewer', @@ -2854,7 +2840,7 @@ describe('AgentSessionPresenter', () => { return undefined }) - await presenter[action]('parent', 'child', { source: 'test' }) + await assignment[action]('parent', 'child', { source: 'test' }) expect(deepChatAgent[method]).toHaveBeenCalledWith('parent', 'child', { source: 'test' }) } @@ -2867,7 +2853,7 @@ describe('AgentSessionPresenter', () => { return undefined }) - await expect(presenter.mergeSubagentTape('parent', 'child')).rejects.toThrow( + await expect(assignment.mergeSubagentTape('parent', 'child')).rejects.toThrow( 'Session child is not a child of parent.' ) expect(agentManager.resolveSubagentFacet).not.toHaveBeenCalled() @@ -2911,7 +2897,7 @@ describe('AgentSessionPresenter', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) await expect( - presenter.forkSession('source-session', 'message-1', 'Forked title') + lifecycle.forkSession('source-session', 'message-1', 'Forked title') ).rejects.toBe(forkError) expect(deepChatAgent.initSession).toHaveBeenCalledWith( @@ -2965,7 +2951,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - const sessions = await presenter.getSessionList() + const sessions = await projection.listSessions() expect(sessions).toHaveLength(1) expect(sessions[0].providerId).toBe('summary-provider') @@ -2987,7 +2973,7 @@ describe('AgentSessionPresenter', () => { } ]) - const sessions = await presenter.getSessionList() + const sessions = await projection.listSessions() expect(sessions).toHaveLength(1) expect(sessions[0].status).toBe('idle') expect(sessions[0].providerId).toBe('openai') @@ -3017,7 +3003,7 @@ describe('AgentSessionPresenter', () => { } ]) - const sessions = await presenter.getSessionList() + const sessions = await projection.listSessions() expect(sessions).toHaveLength(1) expect(sessions[0].id).toBe('s1') @@ -3062,7 +3048,7 @@ describe('AgentSessionPresenter', () => { } }) - const sessions = await presenter.getSessionList() + const sessions = await projection.listSessions() expect(sessions).toHaveLength(1) expect(sessions[0].id).toBe('healthy-state') @@ -3099,10 +3085,10 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - await expect(presenter.getSession('s1')).resolves.toMatchObject({ status: 'generating' }) + await expect(projection.getSession('s1')).resolves.toMatchObject({ status: 'generating' }) agentManager.resolveSessionHandle.mockClear() - await expect(presenter.getLightweightSessionList()).resolves.toMatchObject({ + await expect(projection.listLightweight()).resolves.toMatchObject({ items: [expect.objectContaining({ id: 's1', status: 'generating' })], nextCursor: null, hasMore: false @@ -3129,7 +3115,7 @@ describe('AgentSessionPresenter', () => { } sqlitePresenter.newSessionsTable.listPage.mockReturnValue({ rows: [row], hasMore: false }) - const result = await presenter.getLightweightSessionList() + const result = await projection.listLightweight() expect(result.items).toEqual([expect.objectContaining({ id: 's1', status: 'idle' })]) expect(agentManager.resolveSessionHandle).not.toHaveBeenCalled() @@ -3148,14 +3134,14 @@ describe('AgentSessionPresenter', () => { updated_at: 2000 }) - const session = await presenter.getSession('s1') + const session = await projection.getSession('s1') expect(session).not.toBeNull() expect(session!.status).toBe('idle') }) it('returns null for unknown session', async () => { sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined) - expect(await presenter.getSession('unknown')).toBeNull() + expect(await projection.getSession('unknown')).toBeNull() }) it('returns null when session agent is unavailable', async () => { @@ -3170,7 +3156,7 @@ describe('AgentSessionPresenter', () => { updated_at: 2000 }) - expect(await presenter.getSession('s-disabled')).toBeNull() + expect(await projection.getSession('s-disabled')).toBeNull() expect(warnSpy).toHaveBeenCalledWith( '[SessionProjectionCoordinator] Skipping unavailable session id=s-disabled agent=disabled-agent:', expect.any(Error) @@ -3196,7 +3182,7 @@ describe('AgentSessionPresenter', () => { summaryUpdatedAt: 123 }) - const state = await presenter.getSessionCompactionState('s1') + const state = await turn.getSessionCompactionState('s1') expect(deepChatAgent.getSessionCompactionState).toHaveBeenCalledWith('s1') expect(state).toEqual({ @@ -3217,7 +3203,7 @@ describe('AgentSessionPresenter', () => { updated_at: 2000 }) - await expect(presenter.getSessionCompactionState('s-acp')).resolves.toEqual({ + await expect(turn.getSessionCompactionState('s-acp')).resolves.toEqual({ status: 'idle', cursorOrderSeq: 1, summaryUpdatedAt: null @@ -3237,7 +3223,7 @@ describe('AgentSessionPresenter', () => { updated_at: 2000 }) - await expect(presenter.compactSession('s-acp')).rejects.toThrow( + await expect(turn.compactSession('s-acp')).rejects.toThrow( 'Agent acp-coder does not support manual compaction.' ) @@ -3257,7 +3243,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - await expect(presenter.compactSession('s-compat')).rejects.toThrow( + await expect(turn.compactSession('s-compat')).rejects.toThrow( 'Manual compaction is only available for DeepChat agent sessions.' ) expect(deepChatAgent.compactSession).not.toHaveBeenCalled() @@ -3276,7 +3262,7 @@ describe('AgentSessionPresenter', () => { }) deepChatAgent.compactSession.mockRejectedValueOnce(compactError) - await expect(presenter.compactSession('s1')).rejects.toBe(compactError) + await expect(turn.compactSession('s1')).rejects.toBe(compactError) expect(deepChatAgent.compactSession).toHaveBeenCalledWith('s1') }) @@ -3300,7 +3286,7 @@ describe('AgentSessionPresenter', () => { } ]) - const traces = await presenter.listMessageTraces('m1') + const traces = await projection.listMessageTraces('m1') expect(traces).toEqual([ { id: 't2', @@ -3319,14 +3305,14 @@ describe('AgentSessionPresenter', () => { }) it('returns empty list for blank message id', async () => { - const traces = await presenter.listMessageTraces(' ') + const traces = await projection.listMessageTraces(' ') expect(traces).toEqual([]) expect(sqlitePresenter.deepchatMessageTracesTable.listByMessageId).not.toHaveBeenCalled() }) it('returns trace count by message id', async () => { sqlitePresenter.deepchatMessageTracesTable.countByMessageId.mockReturnValue(3) - await expect(presenter.getMessageTraceCount('m1')).resolves.toBe(3) + await expect(projection.getMessageTraceCount('m1')).resolves.toBe(3) expect(sqlitePresenter.deepchatMessageTracesTable.countByMessageId).toHaveBeenCalledWith('m1') }) }) @@ -3336,7 +3322,7 @@ describe('AgentSessionPresenter', () => { const message = { id: 'm1', sessionId: 's1', role: 'assistant' } deepChatAgent.getMessage.mockResolvedValue(message as any) - await expect(presenter.getMessage('m1')).resolves.toBe(message) + await expect(projection.getMessage('m1')).resolves.toBe(message) expect(deepChatAgent.getMessage).toHaveBeenCalledWith('m1') }) @@ -3354,14 +3340,16 @@ describe('AgentSessionPresenter', () => { } ]) - await expect(presenter.getSearchResults(' message-1 ', 'missing-search')).resolves.toEqual([ - expect.objectContaining({ - title: 'Legacy result', - url: 'https://example.com', - rank: 2, - searchId: undefined - }) - ]) + await expect(projection.getSearchResults(' message-1 ', 'missing-search')).resolves.toEqual( + [ + expect.objectContaining({ + title: 'Legacy result', + url: 'https://example.com', + rank: 2, + searchId: undefined + }) + ] + ) expect( sqlitePresenter.deepchatMessageSearchResultsTable.listByMessageId @@ -3395,8 +3383,8 @@ describe('AgentSessionPresenter', () => { deepChatAgent.listMessageViewManifests.mockRejectedValueOnce(new Error('manifest failed')) deepChatAgent.exportMessageTapeReplaySlice.mockRejectedValueOnce(new Error('replay failed')) - await expect(presenter.listMessageViewManifests('message-1')).resolves.toEqual([]) - await expect(presenter.exportMessageTapeReplaySlice('message-1')).resolves.toBeNull() + await expect(projection.listMessageViewManifests('message-1')).resolves.toEqual([]) + await expect(projection.exportMessageTapeReplaySlice('message-1')).resolves.toBeNull() expect(deepChatAgent.listMessageViewManifests).toHaveBeenCalledWith('s1', 'message-1') expect(deepChatAgent.exportMessageTapeReplaySlice).toHaveBeenCalledWith( @@ -3409,9 +3397,6 @@ describe('AgentSessionPresenter', () => { describe('session update publication', () => { it('trims and dedupes ids, applies reason defaults, and refreshes session UI', () => { - const projection = Reflect.get(presenter, 'sessionProjection') as { - notify(options?: { sessionIds?: string[] }): void - } const emit = projection.notify.bind(projection) as (options?: { sessionIds?: string[] }) => void @@ -3442,7 +3427,7 @@ describe('AgentSessionPresenter', () => { describe('activateSession', () => { it('binds window and publishes typed activated update', async () => { - await presenter.activateSession(42, 's1') + await projection.activate(42, 's1') expectSessionsUpdated({ webContentsId: 42, sessionIds: ['s1'], @@ -3457,7 +3442,7 @@ describe('AgentSessionPresenter', () => { describe('deactivateSession', () => { it('unbinds window and publishes typed deactivated update', async () => { - await presenter.deactivateSession(42) + await projection.deactivate(42) expectSessionsUpdated({ sessionIds: [], reason: 'deactivated', @@ -3480,7 +3465,7 @@ describe('AgentSessionPresenter', () => { updated_at: 2000 }) - await presenter.deleteSession('s1') + await lifecycle.deleteSession('s1') expect(deepChatAgent.destroySession).toHaveBeenCalledWith('s1') expect(sqlitePresenter.newSessionsTable.delete).toHaveBeenCalledWith('s1') expectSessionsUpdated({ reason: 'deleted', sessionIds: ['s1'] }) @@ -3488,7 +3473,7 @@ describe('AgentSessionPresenter', () => { it('no-ops for unknown session', async () => { sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined) - await presenter.deleteSession('unknown') // should not throw + await lifecycle.deleteSession('unknown') // should not throw expect(deepChatAgent.destroySession).not.toHaveBeenCalled() }) @@ -3512,7 +3497,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - await presenter.deleteSession('s-acp') + await lifecycle.deleteSession('s-acp') expect(llmProviderPresenter.clearAcpSession).not.toHaveBeenCalled() expect(deepChatAgent.destroySession).toHaveBeenCalledWith('s-acp') @@ -3535,7 +3520,7 @@ describe('AgentSessionPresenter', () => { }) agentManager.cleanupSessionBackends.mockRejectedValueOnce(cleanupError) - await expect(presenter.deleteSession('s1')).rejects.toBe(cleanupError) + await expect(lifecycle.deleteSession('s1')).rejects.toBe(cleanupError) expect(deepChatAgent.destroySession).toHaveBeenCalledExactlyOnceWith('s1') expect(skillPresenter.clearNewAgentSessionSkills).not.toHaveBeenCalled() @@ -3557,7 +3542,7 @@ describe('AgentSessionPresenter', () => { agentManager.cleanupSessionBackends.mockRejectedValueOnce(backendError) deepChatAgent.destroySession.mockRejectedValueOnce(sharedError) - await expect(presenter.deleteSession('s1')).rejects.toBe(backendError) + await expect(lifecycle.deleteSession('s1')).rejects.toBe(backendError) expect(deepChatAgent.destroySession).toHaveBeenCalledExactlyOnceWith('s1') expect(skillPresenter.clearNewAgentSessionSkills).not.toHaveBeenCalled() @@ -3578,7 +3563,7 @@ describe('AgentSessionPresenter', () => { }) deepChatAgent.destroySession.mockRejectedValueOnce(sharedError) - await expect(presenter.deleteSession('s1')).rejects.toBe(sharedError) + await expect(lifecycle.deleteSession('s1')).rejects.toBe(sharedError) expect(agentManager.cleanupSessionBackends).toHaveBeenCalledExactlyOnceWith('s1') expect(skillPresenter.clearNewAgentSessionSkills).not.toHaveBeenCalled() @@ -3599,14 +3584,14 @@ describe('AgentSessionPresenter', () => { updated_at: 1000 }) - await presenter.cancelGeneration('s1') + await turn.cancelGeneration('s1') expect(deepChatAgent.cancelGeneration).toHaveBeenCalledWith('s1') }) it('is a no-op for a missing session', async () => { sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined) - await expect(presenter.cancelGeneration('missing')).resolves.toBeUndefined() + await expect(turn.cancelGeneration('missing')).resolves.toBeUndefined() expect(agentManager.resolveSessionHandle).not.toHaveBeenCalled() expect(deepChatAgent.cancelGeneration).not.toHaveBeenCalled() @@ -3628,7 +3613,7 @@ describe('AgentSessionPresenter', () => { deepChatAgent.respondToolInteraction.mockResolvedValueOnce({ resumed: true }) await expect( - presenter.respondToolInteraction('s1', 'message-1', 'tool-1', response) + turn.respondToolInteraction('s1', 'message-1', 'tool-1', response) ).resolves.toEqual({ resumed: true }) expect(deepChatAgent.respondToolInteraction).toHaveBeenCalledWith( @@ -3643,7 +3628,7 @@ describe('AgentSessionPresenter', () => { sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined) await expect( - presenter.respondToolInteraction('missing', 'message-1', 'tool-1', { + turn.respondToolInteraction('missing', 'message-1', 'tool-1', { kind: 'permission', granted: true }) @@ -3666,7 +3651,7 @@ describe('AgentSessionPresenter', () => { updated_at: 1000 }) - const settings = await presenter.getSessionGenerationSettings('s1') + const settings = await assignment.getSessionGenerationSettings('s1') expect(deepChatAgent.getGenerationSettings).toHaveBeenCalledWith('s1') expect(settings).toEqual({ @@ -3688,7 +3673,7 @@ describe('AgentSessionPresenter', () => { updated_at: 1000 }) - const updated = await presenter.updateSessionGenerationSettings('s1', { + const updated = await assignment.updateSessionGenerationSettings('s1', { temperature: 1.4, reasoningEffort: 'high' }) @@ -3704,11 +3689,11 @@ describe('AgentSessionPresenter', () => { it('throws when generation settings methods target unknown session', async () => { sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined) - await expect(presenter.getSessionGenerationSettings('unknown')).rejects.toThrow( + await expect(assignment.getSessionGenerationSettings('unknown')).rejects.toThrow( 'Session not found: unknown' ) await expect( - presenter.updateSessionGenerationSettings('unknown', { temperature: 1 }) + assignment.updateSessionGenerationSettings('unknown', { temperature: 1 }) ).rejects.toThrow('Session not found: unknown') }) }) @@ -3726,7 +3711,7 @@ describe('AgentSessionPresenter', () => { }) sqlitePresenter.newSessionsTable.getDisabledAgentTools.mockReturnValue(['exec', 'cdp_send']) - const disabledTools = await presenter.getSessionDisabledAgentTools('s1') + const disabledTools = await assignment.getSessionDisabledAgentTools('s1') expect(disabledTools).toEqual(['exec', 'cdp_send']) }) @@ -3742,7 +3727,7 @@ describe('AgentSessionPresenter', () => { updated_at: 1000 }) - const disabledTools = await presenter.updateSessionDisabledAgentTools('s1', [ + const disabledTools = await assignment.updateSessionDisabledAgentTools('s1', [ 'grep', 'ls', 'cdp_send', @@ -3777,7 +3762,7 @@ describe('AgentSessionPresenter', () => { updated_at: 1000 }) - await expect(presenter.setSessionSubagentEnabled('s-acp', true)).rejects.toThrow( + await expect(assignment.setSessionSubagentEnabled('s-acp', true)).rejects.toThrow( 'Only DeepChat sessions can change subagent state.' ) @@ -3807,7 +3792,7 @@ describe('AgentSessionPresenter', () => { }) deepChatAgent.getSessionState.mockRejectedValueOnce(new Error('state unavailable')) - await expect(presenter.setSessionSubagentEnabled('s1', true)).rejects.toThrow( + await expect(assignment.setSessionSubagentEnabled('s1', true)).rejects.toThrow( 'Failed to build session state for sessionId: s1' ) @@ -3837,7 +3822,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - const updated = await presenter.setSessionModel('s1', 'anthropic', 'claude-3-5-sonnet') + const updated = await assignment.setSessionModel('s1', 'anthropic', 'claude-3-5-sonnet') expect(deepChatAgent.setSessionModel).toHaveBeenCalledWith( 's1', @@ -3863,7 +3848,7 @@ describe('AgentSessionPresenter', () => { { id: 'acp-coder', name: 'ACP Coder', command: 'acp-coder' } ]) - await expect(presenter.setSessionModel('s-acp', 'openai', 'gpt-4')).rejects.toThrow( + await expect(assignment.setSessionModel('s-acp', 'openai', 'gpt-4')).rejects.toThrow( 'ACP session model is locked.' ) expect(deepChatAgent.setSessionModel).not.toHaveBeenCalled() @@ -3910,7 +3895,7 @@ describe('AgentSessionPresenter', () => { async (sessionId: string) => sessionId === 's-ready' ) - const impact = await presenter.getAgentTransferImpact('deepchat-writer') + const impact = await assignment.getAgentTransferImpact('deepchat-writer') expect(impact.totalSessions).toBe(2) expect(impact.movableSessions).toBe(1) @@ -3943,7 +3928,7 @@ describe('AgentSessionPresenter', () => { configPresenter.getAgentType.mockResolvedValue('deepchat') deepChatAgent.hasMessages.mockRejectedValue(new Error('query failed')) - const impact = await presenter.getAgentTransferImpact('deepchat-writer') + const impact = await assignment.getAgentTransferImpact('deepchat-writer') expect(impact.movableSessions).toBe(1) expect(impact.emptyDrafts).toBe(0) @@ -3970,7 +3955,7 @@ describe('AgentSessionPresenter', () => { deepChatAgent.hasMessages.mockResolvedValue(true) deepChatAgent.listPendingInputs.mockRejectedValue(new Error('pending query failed')) - await expect(presenter.moveSessionToAgent('s1', 'deepchat-coder')).rejects.toThrow( + await expect(assignment.moveSessionToAgent('s1', 'deepchat-coder')).rejects.toThrow( 'Session s1 cannot be moved: pending-input' ) @@ -3981,7 +3966,7 @@ describe('AgentSessionPresenter', () => { }) it('rejects blank agent ids for destructive agent-session deletion', async () => { - await expect(presenter.deleteAgentSessions(' ')).rejects.toThrow('Agent id is required.') + await expect(assignment.deleteAgentSessions(' ')).rejects.toThrow('Agent id is required.') expect(sqlitePresenter.newSessionsTable.list).not.toHaveBeenCalled() expect(sqlitePresenter.newSessionsTable.delete).not.toHaveBeenCalled() }) @@ -4037,7 +4022,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'default' }) - const updated = await presenter.moveSessionToAgent('s1', 'deepchat-coder') + const updated = await assignment.moveSessionToAgent('s1', 'deepchat-coder') expect(deepChatAgent.setSessionAgentContext).toHaveBeenCalledWith( 's1', @@ -4122,7 +4107,7 @@ describe('AgentSessionPresenter', () => { })) await expect( - presenter.moveAgentSessions('deepchat-writer', 'deepchat-coder') + assignment.moveAgentSessions('deepchat-writer', 'deepchat-coder') ).rejects.toThrow('Session s-active cannot be moved: active') expect(deepChatAgent.setSessionAgentContext).not.toHaveBeenCalled() @@ -4188,7 +4173,7 @@ describe('AgentSessionPresenter', () => { } }) - const updated = await presenter.moveSessionToAgent('s-acp', 'deepchat-coder') + const updated = await assignment.moveSessionToAgent('s-acp', 'deepchat-coder') expect(deepChatAgent.setSessionAgentContext).toHaveBeenCalledWith( 's-acp', @@ -4254,7 +4239,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - await expect(presenter.moveSessionToAgent('s-acp', 'deepchat-coder')).rejects.toThrow( + await expect(assignment.moveSessionToAgent('s-acp', 'deepchat-coder')).rejects.toThrow( 'ownership update failed' ) @@ -4348,7 +4333,7 @@ describe('AgentSessionPresenter', () => { }) await expect( - presenter.moveAgentSessions('deepchat-writer', 'deepchat-coder') + assignment.moveAgentSessions('deepchat-writer', 'deepchat-coder') ).rejects.toThrow('Partial transfer completed: 1 moved.') expect(rows.get('s-ready-1').agent_id).toBe('deepchat-coder') @@ -4385,7 +4370,7 @@ describe('AgentSessionPresenter', () => { }) deepChatAgent.hasMessages.mockResolvedValue(true) - await expect(presenter.moveSessionToAgent('s-deepchat', 'acp-coder')).rejects.toThrow( + await expect(assignment.moveSessionToAgent('s-deepchat', 'acp-coder')).rejects.toThrow( 'Conversation history cannot be moved to ACP agents.' ) expect(deepChatAgent.setSessionAgentContext).not.toHaveBeenCalled() @@ -4405,7 +4390,7 @@ describe('AgentSessionPresenter', () => { return null }) - await expect(presenter.moveAgentSessions('deepchat-writer', 'acp-coder')).rejects.toThrow( + await expect(assignment.moveAgentSessions('deepchat-writer', 'acp-coder')).rejects.toThrow( 'Conversation history cannot be moved to ACP agents.' ) expect(sqlitePresenter.newSessionsTable.updateAgentId).not.toHaveBeenCalled() @@ -4445,7 +4430,7 @@ describe('AgentSessionPresenter', () => { deepChatAgent.hasMessages.mockResolvedValue(true) await expect( - presenter.moveSessionToAgent('s-deepchat', 'deepchat-acp-default') + assignment.moveSessionToAgent('s-deepchat', 'deepchat-acp-default') ).rejects.toThrow('Conversation history cannot be moved to ACP agents.') expect(deepChatAgent.setSessionAgentContext).not.toHaveBeenCalled() expect(sqlitePresenter.newSessionsTable.updateAgentId).not.toHaveBeenCalled() @@ -4481,7 +4466,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - await expect(presenter.moveSessionToAgent('s-acp', 'acp-reviewer')).rejects.toThrow( + await expect(assignment.moveSessionToAgent('s-acp', 'acp-reviewer')).rejects.toThrow( 'Conversation history cannot be moved to ACP agents.' ) expect(deepChatAgent.setSessionAgentContext).not.toHaveBeenCalled() @@ -4502,7 +4487,7 @@ describe('AgentSessionPresenter', () => { updated_at: 1000 }) - await presenter.deleteSession('s1') + await lifecycle.deleteSession('s1') expect(skillPresenter.clearNewAgentSessionSkills).toHaveBeenCalledWith('s1') }) }) @@ -4519,7 +4504,7 @@ describe('AgentSessionPresenter', () => { updated_at: 1000 }) - await presenter.renameSession('s1', ' New Title ') + await projection.renameSession('s1', ' New Title ') expect(sqlitePresenter.newSessionsTable.update).toHaveBeenCalledWith('s1', { title: 'New Title' @@ -4538,7 +4523,7 @@ describe('AgentSessionPresenter', () => { updated_at: 1000 }) - await presenter.toggleSessionPinned('s1', true) + await projection.toggleSessionPinned('s1', true) expect(sqlitePresenter.newSessionsTable.update).toHaveBeenCalledWith('s1', { is_pinned: 1 @@ -4557,7 +4542,7 @@ describe('AgentSessionPresenter', () => { updated_at: 1000 }) - await presenter.clearSessionMessages('s1') + await turn.clearSessionMessages('s1') expect(deepChatAgent.clearMessages).toHaveBeenCalledWith('s1') expect(deepChatAgent.cancelGeneration.mock.invocationCallOrder[0]).toBeLessThan( @@ -4580,7 +4565,7 @@ describe('AgentSessionPresenter', () => { const cancelError = new Error('cancel failed') deepChatAgent.cancelGeneration.mockRejectedValueOnce(cancelError) - await expect(presenter.clearSessionMessages('s1')).rejects.toBe(cancelError) + await expect(turn.clearSessionMessages('s1')).rejects.toBe(cancelError) expect(deepChatAgent.clearMessages).not.toHaveBeenCalled() expect(publishDeepchatEvent).not.toHaveBeenCalled() @@ -4600,7 +4585,7 @@ describe('AgentSessionPresenter', () => { updated_at: 1000 }) - const commands = await presenter.getAcpSessionCommands('s1') + const commands = await assignment.getAcpSessionCommands('s1') expect(commands).toEqual([]) expect(llmProviderPresenter.getAcpSessionCommands).not.toHaveBeenCalled() }) @@ -4625,7 +4610,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - const commands = await presenter.getAcpSessionCommands('s-acp') + const commands = await assignment.getAcpSessionCommands('s-acp') expect(directAcpControl.getCommands).toHaveBeenCalledOnce() expect(llmProviderPresenter.getAcpSessionCommands).not.toHaveBeenCalled() @@ -4650,7 +4635,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - const commands = await presenter.getAcpSessionCommands('s-compat') + const commands = await assignment.getAcpSessionCommands('s-compat') expect(llmProviderPresenter.getAcpSessionCommands).toHaveBeenCalledWith('s-compat') expect(directAcpControl.getCommands).not.toHaveBeenCalled() @@ -4670,7 +4655,7 @@ describe('AgentSessionPresenter', () => { updated_at: 1000 }) - const result = await presenter.getAcpSessionConfigOptions('s1') + const result = await assignment.getAcpSessionConfigOptions('s1') expect(result).toBeNull() expect(llmProviderPresenter.getAcpSessionConfigOptions).not.toHaveBeenCalled() @@ -4696,7 +4681,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - const result = await presenter.getAcpSessionConfigOptions('s-acp') + const result = await assignment.getAcpSessionConfigOptions('s-acp') expect(directAcpControl.getConfigOptions).toHaveBeenCalledOnce() expect(llmProviderPresenter.getAcpSessionConfigOptions).not.toHaveBeenCalled() @@ -4723,7 +4708,7 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - const result = await presenter.setAcpSessionConfigOption('s-acp', 'model', 'gpt-5-mini') + const result = await assignment.setAcpSessionConfigOption('s-acp', 'model', 'gpt-5-mini') expect(directAcpControl.setConfigOption).toHaveBeenCalledWith('model', 'gpt-5-mini') expect(llmProviderPresenter.setAcpSessionConfigOption).not.toHaveBeenCalled() @@ -4747,8 +4732,8 @@ describe('AgentSessionPresenter', () => { permissionMode: 'full_access' }) - const state = await presenter.getAcpSessionConfigOptions('s-compat') - const updated = await presenter.setAcpSessionConfigOption('s-compat', 'model', 'gpt-5-mini') + const state = await assignment.getAcpSessionConfigOptions('s-compat') + const updated = await assignment.setAcpSessionConfigOption('s-compat', 'model', 'gpt-5-mini') expect(llmProviderPresenter.getAcpSessionConfigOptions).toHaveBeenCalledWith('s-compat') expect(llmProviderPresenter.setAcpSessionConfigOption).toHaveBeenCalledWith( @@ -4765,7 +4750,7 @@ describe('AgentSessionPresenter', () => { describe('getActiveSession', () => { it('returns null when no session bound', async () => { - expect(await presenter.getActiveSession(99)).toBeNull() + expect(await projection.getActive(99)).toBeNull() }) it('returns session when bound', async () => { @@ -4779,25 +4764,25 @@ describe('AgentSessionPresenter', () => { updated_at: 2000 }) - await presenter.activateSession(1, 's1') - const session = await presenter.getActiveSession(1) + await projection.activate(1, 's1') + const session = await projection.getActive(1) expect(session).not.toBeNull() expect(session!.id).toBe('s1') }) it('keeps bindings isolated by window', async () => { - await presenter.activateSession(1, 's1') - await presenter.activateSession(2, 's2') - await presenter.deactivateSession(1) + await projection.activate(1, 's1') + await projection.activate(2, 's2') + await projection.deactivate(1) - expect(presenter.getActiveSessionId(1)).toBeNull() - expect(presenter.getActiveSessionId(2)).toBe('s2') + expect(projection.getActiveId(1)).toBeNull() + expect(projection.getActiveId(2)).toBe('s2') }) it('returns null and clears binding when bound session becomes unavailable', async () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - await presenter.activateSession(1, 's-disabled') + await projection.activate(1, 's-disabled') vi.mocked(publishDeepchatEvent).mockClear() sqlitePresenter.newSessionsTable.get.mockReturnValueOnce({ id: 's-disabled', @@ -4809,7 +4794,7 @@ describe('AgentSessionPresenter', () => { updated_at: 2000 }) - await expect(presenter.getActiveSession(1)).resolves.toBeNull() + await expect(projection.getActive(1)).resolves.toBeNull() expect(publishDeepchatEvent).not.toHaveBeenCalled() sqlitePresenter.newSessionsTable.get.mockReturnValue({ @@ -4821,7 +4806,7 @@ describe('AgentSessionPresenter', () => { created_at: 1000, updated_at: 2000 }) - await expect(presenter.getActiveSession(1)).resolves.toBeNull() + await expect(projection.getActive(1)).resolves.toBeNull() expect(warnSpy).toHaveBeenCalledWith( '[SessionProjectionCoordinator] Skipping unavailable session id=s-disabled agent=disabled-agent:', expect.any(Error) diff --git a/test/main/presenter/sessionBoundaryComposition.test.ts b/test/main/presenter/sessionBoundaryComposition.test.ts index f9aa75d56..73ec89097 100644 --- a/test/main/presenter/sessionBoundaryComposition.test.ts +++ b/test/main/presenter/sessionBoundaryComposition.test.ts @@ -22,4 +22,20 @@ describe('session boundary composition', () => { ) expect(legacyHookSource).toContain('presenter.legacyChatImportService.start(false)') }) + + it('keeps hooks notifications on one instance with lazy projection dependencies', async () => { + const { readFileSync } = await vi.importActual('node:fs') + const presenterSource = readFileSync( + path.resolve(process.cwd(), 'src/main/presenter/index.ts'), + 'utf8' + ) + + expect(presenterSource.match(/new HooksNotificationsService\(/g)).toHaveLength(1) + expect(presenterSource).toContain( + 'getSession: (sessionId) => this.sessionProjectionCoordinator.getSession(sessionId)' + ) + expect(presenterSource).toContain( + 'getMessage: (messageId) => this.sessionProjectionCoordinator.getMessage(messageId)' + ) + }) }) diff --git a/test/main/routes/dispatcher.test.ts b/test/main/routes/dispatcher.test.ts index 8eaa339b8..7eb079155 100644 --- a/test/main/routes/dispatcher.test.ts +++ b/test/main/routes/dispatcher.test.ts @@ -1,5 +1,4 @@ import type { - IAgentSessionPresenter, IConfigPresenter, IConversationExporter, IDevicePresenter, @@ -420,118 +419,6 @@ function createRuntime() { }) } as unknown as IConfigPresenter - const agentSessionPresenter = { - getActiveSessionId: vi.fn(() => null), - getLightweightSessionsByIds: vi.fn().mockResolvedValue([]), - createDetachedSession: vi.fn().mockResolvedValue({ - id: 'session-1', - agentId: 'deepchat', - title: 'New Chat', - projectDir: '/workspace', - isPinned: false, - isDraft: false, - sessionKind: 'regular', - parentSessionId: null, - subagentEnabled: false, - subagentMeta: null, - createdAt: 1, - updatedAt: 2, - status: 'idle', - providerId: 'openai', - modelId: 'gpt-5.4' - }), - createSession: vi.fn().mockResolvedValue({ - id: 'session-1', - agentId: 'deepchat', - title: 'New Chat', - projectDir: '/workspace', - isPinned: false, - isDraft: false, - sessionKind: 'regular', - parentSessionId: null, - subagentEnabled: false, - subagentMeta: null, - createdAt: 1, - updatedAt: 2, - status: 'idle', - providerId: 'openai', - modelId: 'gpt-5.4' - }), - getSession: vi.fn().mockResolvedValue({ - id: 'session-1', - agentId: 'deepchat', - title: 'Restored', - projectDir: '/workspace', - isPinned: false, - isDraft: false, - sessionKind: 'regular', - parentSessionId: null, - subagentEnabled: false, - subagentMeta: null, - createdAt: 1, - updatedAt: 2, - status: 'idle', - providerId: 'openai', - modelId: 'gpt-5.4' - }), - getMessages: vi.fn().mockResolvedValue([ - { - id: 'message-1', - sessionId: 'session-1', - orderSeq: 1, - role: 'user', - content: '{"text":"hello"}', - status: 'sent', - isContextEdge: 0, - metadata: '{}', - createdAt: 1, - updatedAt: 1 - } - ]), - getSessionList: vi.fn().mockResolvedValue([]), - getActiveSession: vi.fn().mockResolvedValue(null), - activateSession: vi.fn().mockResolvedValue(undefined), - deactivateSession: vi.fn().mockResolvedValue(undefined), - getSessionGenerationSettings: vi.fn().mockResolvedValue({ - systemPrompt: '', - temperature: 0.7, - contextLength: 32000, - maxTokens: 4096, - timeout: 5000 - }), - updateSessionGenerationSettings: vi - .fn() - .mockImplementation(async (_sessionId: string, settings: { timeout?: number }) => ({ - systemPrompt: '', - temperature: 0.7, - contextLength: 32000, - maxTokens: 4096, - timeout: settings.timeout ?? 5000 - })), - sendMessage: vi.fn().mockResolvedValue({ - requestId: 'message-2', - messageId: 'message-2' - }), - steerActiveTurn: vi.fn().mockResolvedValue(undefined), - compactSession: vi.fn().mockResolvedValue({ - compacted: true, - state: { - status: 'compacted', - cursorOrderSeq: 5, - summaryUpdatedAt: 123 - } - }), - cancelGeneration: vi.fn().mockResolvedValue(undefined), - getMessage: vi.fn().mockResolvedValue({ - id: 'message-1', - sessionId: 'session-1' - }), - respondToolInteraction: vi.fn().mockResolvedValue({ - resumed: true - }), - clearSessionPermissions: vi.fn() - } as unknown as IAgentSessionPresenter - const sessionSnapshot = { id: 'session-1', agentId: 'deepchat', @@ -550,7 +437,12 @@ function createRuntime() { modelId: 'gpt-5.4' } const sessionLifecyclePort = { - createSession: vi.fn().mockResolvedValue({ ...sessionSnapshot, title: 'New Chat' }) + createSession: vi.fn().mockResolvedValue({ ...sessionSnapshot, title: 'New Chat' }), + createDetachedSession: vi.fn().mockResolvedValue(sessionSnapshot), + createSubagentSession: vi.fn().mockResolvedValue(sessionSnapshot), + ensureAcpDraftSession: vi.fn().mockResolvedValue(sessionSnapshot), + forkSession: vi.fn().mockResolvedValue(sessionSnapshot), + deleteSession: vi.fn().mockResolvedValue(undefined) } const sessionProjectionPort = { getSession: vi.fn().mockResolvedValue(sessionSnapshot), @@ -576,6 +468,20 @@ function createRuntime() { activate: vi.fn().mockResolvedValue(undefined), deactivate: vi.fn().mockResolvedValue(undefined), getActive: vi.fn().mockResolvedValue(null), + getActiveId: vi.fn(() => null), + listLightweight: vi.fn().mockResolvedValue({ + sessions: [], + nextCursor: null, + hasMore: false + }), + getLightweightByIds: vi.fn().mockResolvedValue([]), + getSearchResults: vi.fn().mockResolvedValue([]), + getTapeContext: vi.fn().mockResolvedValue({ entries: [] }), + listMessageTraces: vi.fn().mockResolvedValue([]), + listMessageViewManifests: vi.fn().mockResolvedValue([]), + exportMessageTapeReplaySlice: vi.fn().mockResolvedValue(null), + renameSession: vi.fn().mockResolvedValue(undefined), + toggleSessionPinned: vi.fn().mockResolvedValue(undefined), getMessage: vi.fn().mockResolvedValue({ id: 'message-1', sessionId: 'session-1', @@ -595,9 +501,61 @@ function createRuntime() { messageId: 'message-2' }), steerActiveTurn: vi.fn().mockResolvedValue(undefined), + listPendingInputs: vi.fn().mockResolvedValue([]), + queuePendingInput: vi.fn().mockResolvedValue({}), + updateQueuedInput: vi.fn().mockResolvedValue({}), + moveQueuedInput: vi.fn().mockResolvedValue([]), + convertPendingInputToSteer: vi.fn().mockResolvedValue({}), + steerPendingInput: vi.fn().mockResolvedValue({}), + deletePendingInput: vi.fn().mockResolvedValue(undefined), + retryMessage: vi.fn().mockResolvedValue(undefined), + deleteMessage: vi.fn().mockResolvedValue(undefined), + editUserMessage: vi.fn().mockResolvedValue({}), + getSessionCompactionState: vi.fn().mockResolvedValue({ status: 'idle' }), + compactSession: vi.fn().mockResolvedValue({ + compacted: true, + state: { + status: 'compacted', + cursorOrderSeq: 5, + summaryUpdatedAt: 123 + } + }), + clearSessionMessages: vi.fn().mockResolvedValue(undefined), cancelGeneration: vi.fn().mockResolvedValue(undefined), respondToolInteraction: vi.fn().mockResolvedValue({ resumed: true }) } + const sessionAssignmentPort = { + getAgentTransferImpact: vi.fn().mockResolvedValue({}), + moveAgentSessions: vi.fn().mockResolvedValue({ movedSessionIds: [], deletedSessionIds: [] }), + deleteAgentSessions: vi.fn().mockResolvedValue([]), + moveSessionToAgent: vi.fn().mockResolvedValue(sessionSnapshot), + getAcpSessionCommands: vi.fn().mockResolvedValue([]), + getAcpSessionConfigOptions: vi.fn().mockResolvedValue(null), + setAcpSessionConfigOption: vi.fn().mockResolvedValue(null), + getPermissionMode: vi.fn().mockResolvedValue('full_access'), + setPermissionMode: vi.fn().mockResolvedValue(undefined), + setSessionSubagentEnabled: vi.fn().mockResolvedValue(sessionSnapshot), + setSessionModel: vi.fn().mockResolvedValue(sessionSnapshot), + setSessionProjectDir: vi.fn().mockResolvedValue(sessionSnapshot), + getSessionGenerationSettings: vi.fn().mockResolvedValue({ + systemPrompt: '', + temperature: 0.7, + contextLength: 32000, + maxTokens: 4096, + timeout: 5000 + }), + getSessionDisabledAgentTools: vi.fn().mockResolvedValue([]), + updateSessionDisabledAgentTools: vi.fn().mockResolvedValue([]), + updateSessionGenerationSettings: vi + .fn() + .mockImplementation(async (_sessionId: string, settings: { timeout?: number }) => ({ + systemPrompt: '', + temperature: 0.7, + contextLength: 32000, + maxTokens: 4096, + timeout: settings.timeout ?? 5000 + })) + } const sessionPermissionPort = { clearSessionPermissions: vi.fn() } @@ -1344,10 +1302,10 @@ function createRuntime() { configPresenter, llmProviderPresenter, acpProviderAdminPort, - agentSessionPresenter, sessionLifecyclePort, sessionProjectionPort, sessionTurnPort, + sessionAssignmentPort, sessionPermissionPort, skillPresenter, skillSyncPresenter, @@ -1375,10 +1333,10 @@ function createRuntime() { configPresenter, llmProviderPresenter, acpProviderAdminPort, - agentSessionPresenter, sessionLifecyclePort, sessionProjectionPort, sessionTurnPort, + sessionAssignmentPort, sessionPermissionPort, skillPresenter, skillSyncPresenter, @@ -3635,8 +3593,7 @@ describe('dispatchDeepchatRoute', () => { }) it('dispatches session and chat routes with renderer context', async () => { - const { runtime, agentSessionPresenter, sessionLifecyclePort, sessionTurnPort } = - createRuntime() + const { runtime, sessionLifecyclePort, sessionTurnPort } = createRuntime() const createResult = await dispatchDeepchatRoute( runtime, @@ -3709,7 +3666,7 @@ describe('dispatchDeepchatRoute', () => { } ) - expect(agentSessionPresenter.compactSession).toHaveBeenCalledWith('session-1') + expect(sessionTurnPort.compactSession).toHaveBeenCalledWith('session-1') expect(compactResult).toEqual({ compacted: true, state: { @@ -3721,7 +3678,7 @@ describe('dispatchDeepchatRoute', () => { }) it('dispatches session generation settings routes without dropping timeout', async () => { - const { runtime, agentSessionPresenter } = createRuntime() + const { runtime, sessionAssignmentPort } = createRuntime() const updateResult = await dispatchDeepchatRoute( runtime, @@ -3750,7 +3707,7 @@ describe('dispatchDeepchatRoute', () => { } ) - expect(agentSessionPresenter.updateSessionGenerationSettings).toHaveBeenCalledWith( + expect(sessionAssignmentPort.updateSessionGenerationSettings).toHaveBeenCalledWith( 'session-1', { timeout: 5000 @@ -3765,7 +3722,7 @@ describe('dispatchDeepchatRoute', () => { timeout: 5000 } }) - expect(agentSessionPresenter.getSessionGenerationSettings).toHaveBeenCalledWith('session-1') + expect(sessionAssignmentPort.getSessionGenerationSettings).toHaveBeenCalledWith('session-1') expect(getResult).toEqual({ settings: { systemPrompt: '', diff --git a/test/main/scripts/architectureGuard.test.ts b/test/main/scripts/architectureGuard.test.ts index fff2f8ef4..a1b5145ed 100644 --- a/test/main/scripts/architectureGuard.test.ts +++ b/test/main/scripts/architectureGuard.test.ts @@ -112,7 +112,6 @@ const CAUSAL_OBSERVATION_ARROW_FIXTURE = path.join( ) const PRESENTER_ROOT_ENTRY = path.join(ROOT, 'src/main/presenter/index.ts') const SESSION_APPLICATION_ROOT = path.join(ROOT, 'src/main/presenter/sessionApplication') -const SESSION_APPLICATION_PORTS_PATH = path.join(SESSION_APPLICATION_ROOT, 'ports.ts') const REMOTE_CONTROL_PRESENTER_PATH = path.join( ROOT, 'src/main/presenter/remoteControlPresenter/index.ts' @@ -264,12 +263,6 @@ const SESSION_WHOLE_DEPENDENCY_FIXTURES = [ specifier: '../index', localName: 'AppPresenter' }, - { - dependency: 'IAgentSessionPresenter', - specifier: '@shared/presenter', - localName: 'SessionFacade', - filePath: SESSION_APPLICATION_PORTS_PATH - }, { dependency: 'AgentSharedDataPorts', specifier: '@/agent/shared/agentSharedData', @@ -938,15 +931,14 @@ describe('architecture guard', () => { expect(result.stdout).toContain('Architecture guard passed.') }) - it('guards the production Remote presenter path from broad session presenter access', () => { + it('guards the production Remote presenter path from retired session facade access', () => { const fixtureViolations = sessionViolationsForFile( violations, SESSION_PRODUCTION_CONSUMER_FIXTURE.filePath ) - expect(fixtureViolations).toEqual([ - expect.stringContaining('[session-consumer-presenter-type]'), - expect.stringContaining('[session-consumer-presenter-facade]') - ]) + expect(fixtureViolations.join('\n')).toContain('[session-consumer-presenter-type]') + expect(fixtureViolations.join('\n')).toContain('[session-consumer-presenter-facade]') + expect(fixtureViolations.join('\n')).toContain('[session-retired-facade-symbol]') }) it.each(SESSION_DUPLICATE_CONSTRUCTION_FIXTURES)( @@ -1021,27 +1013,21 @@ describe('architecture guard', () => { expect(fixtureViolations).toContain('[memory-legacy-list-caller]') }) - it('keeps moved capabilities and owners out of AgentSessionPresenter', () => { + it('keeps the AgentSessionPresenter path and symbol retired', () => { const fixtureViolations = forFile(violations, AGENT_SESSION_PRESENTER_PATH).join('\n') - expect(fixtureViolations).toContain('[session-boundary-presenter-method]') - for (const owner of [ - 'legacy import', - 'startup migrations', - 'usage owner or policy', - 'RTK runtime', - 'exporter formats', - 'history search', - 'session translation', - 'agent catalog' - ]) { - expect(fixtureViolations).toContain(`must not import ${owner}`) - } + expect(violations.join('\n')).toContain( + '[main-retired-path] src/main/presenter/agentSessionPresenter' + ) + expect(fixtureViolations).toContain('[session-retired-facade-symbol]') }) - it('keeps moved capabilities out of IAgentSessionPresenter', () => { - expect(forFile(violations, AGENT_SESSION_PRESENTER_INTERFACE_PATH).join('\n')).toContain( - '[session-boundary-interface-method]' - ) + it('keeps the IAgentSessionPresenter path and symbol retired', () => { + const fixtureViolations = forFile( + violations, + AGENT_SESSION_PRESENTER_INTERFACE_PATH + ).join('\n') + expect(fixtureViolations).toContain('[main-retired-path]') + expect(fixtureViolations).toContain('[session-retired-facade-symbol]') }) it( @@ -1104,7 +1090,7 @@ describe('architecture guard', () => { expect(unsafeCastResult.join('\n')).toContain('[session-boundary-hook-type-cast]') expect(unsafeCastResult.join('\n')).not.toContain('[session-boundary-hook-optional-task]') }, - 30_000 + 60_000 ) it('keeps Memory orchestration and injection callbacks out of the runtime presenter', () => {