From 34d980a99ff2a90a122c6d3524e21c4f81fd2332 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 14 Mar 2026 12:36:59 +0800 Subject: [PATCH 01/26] docs(agent-cleanup): add phased baseline --- docs/specs/agent-cleanup/plan.md | 123 +++++++++++++++++++++++++++++ docs/specs/agent-cleanup/spec.md | 126 ++++++++++++++++++++++++++++++ docs/specs/agent-cleanup/tasks.md | 94 ++++++++++++++++++++++ 3 files changed, 343 insertions(+) create mode 100644 docs/specs/agent-cleanup/plan.md create mode 100644 docs/specs/agent-cleanup/spec.md create mode 100644 docs/specs/agent-cleanup/tasks.md diff --git a/docs/specs/agent-cleanup/plan.md b/docs/specs/agent-cleanup/plan.md new file mode 100644 index 000000000..90ad64162 --- /dev/null +++ b/docs/specs/agent-cleanup/plan.md @@ -0,0 +1,123 @@ +# Agent Cleanup Plan + +## Immediate Micro-Batches + +As of March 14, 2026, cleanup execution is split into smaller reviewable slices. + +### Batch 0A + +- Inventory only. +- Record current legacy coupling in the new primary flow and compatibility layer. +- Record the current main-to-renderer event contract and freeze its payload/ordering assumptions. +- No runtime, renderer, DB, IPC, or CI wiring changes. + +Rollback: + +- Revert docs only. + +Event safety gate: + +- No cleanup batch may change `STREAM_EVENTS` or `SESSION_EVENTS` names, payload fields, or sender + ordering until the renderer listeners are migrated in a dedicated event batch. +- In particular, `stream:end` must keep its current dual meaning ("stream finished" and "refresh + persisted message from DB") until a separate refresh event exists and the renderer stops relying + on `loadMessages()` for that path. + +### Batch 0B + +- Add the static dependency guard script. +- Wire the guard into `pnpm run lint`. +- Do not combine with helper extraction or renderer protocol work. + +Rollback: + +- Remove the guard script from `package.json`. + +## Batch 0 + +- Add `docs/specs/agent-cleanup/{spec,plan,tasks}.md`. +- Batch 0 is now split into 0A and 0B above. + +Rollback: + +- Remove the guard script from `package.json`. + +## Batch 1 + +Batch 1 is split into independent helper/type moves. Each slice should ship alone. + +### Batch 1A + +- Extract only the question-tool schema/parser to a neutral module. +- Update `deepchatAgentPresenter/dispatch.ts` to use the neutral helper. +- No renderer, DB, or `MCPToolDefinition`/`SearchResult` changes. + +Rollback: + +- Point `deepchatAgentPresenter/dispatch.ts` back to `agentPresenter/tools/questionTool`. + +### Batch 1B + +- Extract only the runtime/system env prompt builder to a neutral module. +- Update `deepchatAgentPresenter/index.ts` to use the neutral helper. +- No session path, renderer, DB, or compatibility-layer changes. + +Rollback: + +- Point `deepchatAgentPresenter/index.ts` back to + `agentPresenter/message/systemEnvPromptBuilder`. + +### Batch 1C + +- Extract only session offload/session path helper ownership. +- Introduce standalone `core/search` and direct `core/mcp` imports where needed. +- Keep legacy files as thin re-export shims if required. + +Rollback: + +- Restore imports to `sessionPresenter/sessionPaths` and shared presenter barrels. + +- Extract shared helpers to neutral modules: + - runtime/system env prompt builder + - question tool schema/parser + - session offload/session path helper +- Keep legacy files as thin re-export shims so old code keeps working. +- Move new primary-flow code to direct `core` type imports for MCP/search instead of legacy + presenter barrels. + +Rollback: + +- Point new-flow imports back to legacy helper modules. + +## Batch 2 + +- Expand `agent-interface` message block/user content types to cover the block variants already + rendered by the new UI. +- Replace new UI `@shared/chat` imports with `agent-interface` + renderer-local display types. +- Remove `ChatPage` runtime adaptation to legacy message protocol. + +Rollback: + +- Restore new UI imports to `@shared/chat`. + +## Batch 3 + +- Persist `activeSkills` in `new_sessions`. +- Make `SkillPresenter` read/write new-session skills from the new session domain. +- Remove new-session skill fallback to legacy `sessionPresenter`. +- Remove new-session ACP gating dependence on global `input_chatMode`. +- Move shared runtime helpers used by `skillExecutionService` out of legacy folders. + +Rollback: + +- Keep DB column and route reads/writes back through legacy fallback. + +## Batch 4 + +- Re-audit remaining runtime references to legacy presenter/session modules. +- Retire legacy runtime wiring only after batches 0-3 are stable. +- Keep only legacy data import compatibility; do not preserve old runtime ownership beyond that. + +Rollback: + +- Restore legacy presenter construction or shared type exposure if runtime regressions are found. diff --git a/docs/specs/agent-cleanup/spec.md b/docs/specs/agent-cleanup/spec.md new file mode 100644 index 000000000..6dc2298cd --- /dev/null +++ b/docs/specs/agent-cleanup/spec.md @@ -0,0 +1,126 @@ +# Agent Cleanup + +## Summary + +This cleanup tracks the phased removal of legacy coupling around the new agent architecture. + +Current primary flow: + +- renderer new stores / `NewThreadPage` / `ChatPage` +- `newAgentPresenter` +- `deepchatAgentPresenter` + +Legacy compatibility remains import-only for old session data. Old `conversations` / `messages` +tables are kept as migration sources during this cleanup and are not removed in this workstream. + +Target end-state: + +- legacy runtime logic is removed from the new primary flow +- only legacy data import compatibility remains +- old runtime folders/helpers stay only as long as they are required by import-only code paths + +## Goals + +1. Prevent new primary-flow code from adding fresh dependencies on legacy presenter modules. +2. Extract shared runtime helpers out of legacy presenter folders. +3. Move new UI chat rendering to `agent-interface` types instead of `@shared/chat`. +4. Persist new-session skill state in the new session domain. +5. Reduce runtime fallbacks to legacy chat-mode/session paths where the new architecture already + owns the behavior. +6. Retire legacy runtime ownership once import-only compatibility is stable. + +## Non-Goals + +1. Removing legacy import support in this phase. +2. Dropping old database tables in this phase. +3. Rewriting `llmProviderPresenter` internal legacy loop as part of the first cleanup batch. +4. Keeping legacy runtime logic around permanently. + +## Batch Boundaries + +### Batch 0 + +- Add spec artifacts. +- Add static dependency guardrails. + +### Batch 1 + +- Extract shared helpers from legacy presenter folders. +- Narrow new primary-flow type imports. + +### Batch 2 + +- Switch new UI chat pages/components/stores to `agent-interface` message types. + +### Batch 3 + +- Persist `activeSkills` in `new_sessions`. +- Remove new-session runtime fallback to legacy conversation settings and global chat mode. + +### Batch 4 + +- Re-audit runtime references and retire legacy-only runtime wiring that is no longer needed. + +## Safety Rules + +1. One cleanup batch per PR. +2. Do not combine runtime decoupling, UI protocol changes, and data migration in the same PR. +3. Keep behavior unchanged while moving helper ownership. +4. Keep `legacyImportHook` and `LegacyChatImportService` available until data compatibility is + intentionally retired in a separate task. + +## Event Contract Baseline + +Cleanup must preserve the current main-to-renderer event contract until a dedicated event-migration +batch exists. + +### Session Events + +- `session:list-updated` + - Sender: `newAgentPresenter` + - Payload: none + - Renderer effect: `useSessionStore.fetchSessions()` +- `session:activated` + - Sender: `newAgentPresenter` + - Payload: `{ webContentsId, sessionId }` + - Renderer effect: activate the matching session only for the current `webContentsId` +- `session:deactivated` + - Sender: `newAgentPresenter` + - Payload: `{ webContentsId }` + - Renderer effect: clear the active session only for the current `webContentsId` +- `session:status-changed` + - Sender: `deepchatAgentPresenter` + - Payload: `{ sessionId, status }` + - Renderer effect: map runtime status to UI status (`generating -> working`, `idle -> none`) +- `session:compaction-updated` + - Sender: `deepchatAgentPresenter` + - Payload: `{ sessionId, status, cursorOrderSeq, summaryUpdatedAt }` + - Note: emitted by main, but there is no direct renderer store listener in the current new UI + +### Stream Events + +- `stream:response` + - Sender: `deepchatAgentPresenter/dispatch.ts` and `deepchatAgentPresenter/echo.ts` + - Payload: `{ conversationId, eventId, messageId, blocks }` + - Renderer effect: `useMessageStore` updates `streamingBlocks`, marks the session as streaming, + and may hydrate/update the in-flight assistant message by `messageId` +- `stream:end` + - Sender: `deepchatAgentPresenter/dispatch.ts` and `deepchatAgentPresenter/index.ts` + - Payload: `{ conversationId, eventId, messageId }` + - Renderer effect: `useMessageStore` clears local streaming state and reloads messages from DB + - Important: this event currently means both "stream finished" and "message refresh needed" +- `stream:error` + - Sender: `deepchatAgentPresenter/dispatch.ts`, `deepchatAgentPresenter/process.ts`, and + `deepchatAgentPresenter/index.ts` + - Payload: `{ conversationId, eventId, messageId, error }` + - Renderer effect: clear local streaming state and reload messages from DB error state + +### Hidden Coupling To Preserve For Now + +1. `useMessageStore` treats `stream:end` as the trigger to reload persisted messages, not just as a + terminal stream signal. +2. `ChatPage` still adapts `ChatMessageRecord` and streaming blocks into legacy `@shared/chat` + message shapes before rendering. +3. Event payload identity currently relies on `conversationId` and `messageId` matching DB records. +4. The order "emit stream update -> persist/finalize -> emit end/error" must remain stable within a + cleanup slice unless the renderer listener is migrated in the same dedicated batch. diff --git a/docs/specs/agent-cleanup/tasks.md b/docs/specs/agent-cleanup/tasks.md new file mode 100644 index 000000000..8a1160f94 --- /dev/null +++ b/docs/specs/agent-cleanup/tasks.md @@ -0,0 +1,94 @@ +# Agent Cleanup Tasks + +## Current Inventory + +Baseline recorded on March 14, 2026. This section is read-only inventory and does not imply that +all items should be addressed in one batch. + +### Event Contract Baseline + +- [x] Document `session:list-updated`, `session:activated`, `session:deactivated` +- [x] Document `session:status-changed` and `session:compaction-updated` +- [x] Document `stream:response`, `stream:end`, and `stream:error` +- [x] Capture the current renderer assumptions: + - `useMessageStore` clears stream state and reloads DB records on `stream:end/error` + - `stream:end` is also used as a message refresh signal today + - `ChatPage` still adapts `ChatMessageRecord` into legacy `@shared/chat` render inputs +- [ ] Keep event payloads and emit ordering unchanged in batches `0B`, `1A`, `1B`, and `1C` + +### Primary Flow Reverse Imports + +- [ ] `src/main/presenter/deepchatAgentPresenter/index.ts` -> `../agentPresenter/message/systemEnvPromptBuilder` +- [ ] `src/main/presenter/deepchatAgentPresenter/dispatch.ts` -> `../agentPresenter/tools/questionTool` +- [ ] `src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts` -> `../sessionPresenter/sessionPaths` + +### Primary Flow Type-Source Coupling + +- [ ] `src/main/presenter/deepchatAgentPresenter/types.ts` -> `MCPToolDefinition` from `@shared/presenter` +- [ ] `src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts` -> `MCPToolDefinition` from `@shared/presenter` +- [ ] `src/main/presenter/deepchatAgentPresenter/dispatch.ts` -> `MCPToolDefinition` and `SearchResult` from `@shared/presenter` +- [ ] `src/main/presenter/deepchatAgentPresenter/messageStore.ts` -> `SearchResult` from `@shared/presenter` +- [ ] `src/main/presenter/newAgentPresenter/index.ts` -> `Message` from `@shared/chat`, `SearchResult` from `@shared/presenter` +- [ ] `src/main/presenter/newAgentPresenter/legacyImportService.ts` -> `SearchResult` from `@shared/presenter` + +### New UI Legacy Message Protocol Imports + +- [ ] Pages -> `src/renderer/src/pages/ChatPage.vue`, `src/renderer/src/pages/NewThreadPage.vue` +- [ ] Stores -> `src/renderer/src/stores/ui/message.ts`, `src/renderer/src/stores/ui/session.ts` +- [ ] Chat components -> `src/renderer/src/components/chat/ChatAttachmentItem.vue`, `src/renderer/src/components/chat/ChatInputBox.vue`, `src/renderer/src/components/chat/MessageList.vue`, `src/renderer/src/components/chat/messageListItems.ts`, `src/renderer/src/components/chat/composables/useChatInputFiles.ts` +- [ ] Message components -> `src/renderer/src/components/message/MessageBlockAction.vue`, `src/renderer/src/components/message/MessageBlockAudio.vue`, `src/renderer/src/components/message/MessageBlockContent.vue`, `src/renderer/src/components/message/MessageBlockError.vue`, `src/renderer/src/components/message/MessageBlockImage.vue`, `src/renderer/src/components/message/MessageBlockMcpUi.vue`, `src/renderer/src/components/message/MessageBlockPlan.vue`, `src/renderer/src/components/message/MessageBlockQuestionRequest.vue`, `src/renderer/src/components/message/MessageBlockThink.vue`, `src/renderer/src/components/message/MessageBlockToolCall.vue`, `src/renderer/src/components/message/MessageContent.vue`, `src/renderer/src/components/message/MessageItemAssistant.vue`, `src/renderer/src/components/message/MessageItemUser.vue`, `src/renderer/src/components/message/MessageMinimap.vue` + +### Secondary Renderer Type Coupling + +- [ ] `src/renderer/src/components/message/MessageBlockContent.vue` -> `SearchResult` from `@shared/presenter` +- [ ] `src/renderer/src/components/message/ReferencePreview.vue` -> `SearchResult` from `@shared/presenter` +- [ ] `src/renderer/src/stores/reference.ts` -> `SearchResult` from `@shared/presenter` + +### Compatibility Layer Runtime Fallbacks + +- [ ] `src/main/presenter/skillPresenter/index.ts` -> `presenter.sessionPresenter.getConversation/updateConversationSettings` +- [ ] `src/main/presenter/skillPresenter/skillExecutionService.ts` -> `../sessionPresenter/sessionPaths`, `../agentPresenter/acp/backgroundExecSessionManager`, `../agentPresenter/acp/shellEnvHelper` +- [ ] `src/main/presenter/mcpPresenter/toolManager.ts` -> global `input_chatMode` and `presenter.sessionPresenter.getConversation` + +### Recommended Next PR Order + +- [ ] `0A` Inventory only, docs only +- [ ] `0B` Add static guardrails only +- [ ] `1A` Extract `questionTool` helper only +- [ ] `1B` Extract system env prompt helper only +- [ ] `1C` Extract session path helper and narrow `MCPToolDefinition` / `SearchResult` imports +- [ ] `4` Remove remaining legacy runtime logic after import-only compatibility is proven stable + +## Batch 0 + +- [x] Add spec artifacts under `docs/specs/agent-cleanup/` +- [ ] Add static dependency guard script +- [ ] Wire guard script into `pnpm run lint` + +## Batch 1 + +- [ ] Extract neutral runtime prompt builder +- [ ] Extract neutral question-tool helper +- [ ] Extract neutral session path helper +- [ ] Update new-flow imports to neutral helpers +- [ ] Add standalone `SearchResult` core type +- [ ] Update new-flow imports to `core/mcp` and `core/search` + +## Batch 2 + +- [ ] Extend `agent-interface` message protocol for currently rendered blocks +- [ ] Introduce renderer-local display message types +- [ ] Remove new UI direct imports from `@shared/chat` + +## Batch 3 + +- [ ] Add `active_skills` persistence to `new_sessions` +- [ ] Make `SkillPresenter` persist new-session active skills +- [ ] Remove new-session fallback to legacy conversation settings +- [ ] Remove new-session ACP runtime gating dependence on `input_chatMode` +- [ ] Move skill runtime helpers out of legacy presenter folders + +## Batch 4 + +- [ ] Audit remaining legacy runtime references +- [ ] Retire safe legacy-only runtime wiring From 766eaf2f73dcccd239eb75fff8e6e624eb24825e Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 14 Mar 2026 12:39:20 +0800 Subject: [PATCH 02/26] chore(agent-cleanup): add static guard --- docs/specs/agent-cleanup/tasks.md | 6 +- package.json | 3 +- scripts/agent-cleanup-guard.mjs | 220 ++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 scripts/agent-cleanup-guard.mjs diff --git a/docs/specs/agent-cleanup/tasks.md b/docs/specs/agent-cleanup/tasks.md index 8a1160f94..ca187f6aa 100644 --- a/docs/specs/agent-cleanup/tasks.md +++ b/docs/specs/agent-cleanup/tasks.md @@ -53,7 +53,7 @@ all items should be addressed in one batch. ### Recommended Next PR Order - [ ] `0A` Inventory only, docs only -- [ ] `0B` Add static guardrails only +- [x] `0B` Add static guardrails only - [ ] `1A` Extract `questionTool` helper only - [ ] `1B` Extract system env prompt helper only - [ ] `1C` Extract session path helper and narrow `MCPToolDefinition` / `SearchResult` imports @@ -62,8 +62,8 @@ all items should be addressed in one batch. ## Batch 0 - [x] Add spec artifacts under `docs/specs/agent-cleanup/` -- [ ] Add static dependency guard script -- [ ] Wire guard script into `pnpm run lint` +- [x] Add static dependency guard script +- [x] Wire guard script into `pnpm run lint` ## Batch 1 diff --git a/package.json b/package.json index 7613b8f9c..b2a63fe56 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "test:ui": "vitest --ui", "format:check": "prettier --check .", "format": "prettier --cache --write .", - "lint": "oxlint .", + "lint": "pnpm run lint:agent-cleanup && oxlint .", + "lint:agent-cleanup": "node scripts/agent-cleanup-guard.mjs", "typecheck:node": "tsgo --noEmit -p tsconfig.node.json --composite false", "typecheck:web": "vue-tsgo --project tsconfig.app.tsgo.json", "typecheck": "pnpm run typecheck:node && pnpm run typecheck:web", diff --git a/scripts/agent-cleanup-guard.mjs b/scripts/agent-cleanup-guard.mjs new file mode 100644 index 000000000..a791ec079 --- /dev/null +++ b/scripts/agent-cleanup-guard.mjs @@ -0,0 +1,220 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' + +const ROOT = process.cwd() + +const SOURCE_EXTENSIONS = new Set([ + '.js', + '.jsx', + '.mjs', + '.cjs', + '.ts', + '.tsx', + '.mts', + '.cts', + '.vue' +]) + +const LEGACY_MAIN_DIRS = [ + path.join(ROOT, 'src/main/presenter/agentPresenter'), + path.join(ROOT, 'src/main/presenter/sessionPresenter') +] + +const MAIN_PROTECTED_DIRS = [ + path.join(ROOT, 'src/main/presenter/newAgentPresenter'), + path.join(ROOT, 'src/main/presenter/deepchatAgentPresenter') +] + +const RENDERER_PROTECTED_DIRS = [ + path.join(ROOT, 'src/renderer/src/pages/ChatPage.vue'), + path.join(ROOT, 'src/renderer/src/pages/NewThreadPage.vue'), + path.join(ROOT, 'src/renderer/src/stores/ui'), + path.join(ROOT, 'src/renderer/src/components/chat'), + path.join(ROOT, 'src/renderer/src/components/message') +] + +const ALLOWED_BASELINE = new Set([ + 'src/main/presenter/deepchatAgentPresenter/dispatch.ts|../agentPresenter/tools/questionTool', + 'src/main/presenter/deepchatAgentPresenter/index.ts|../agentPresenter/message/systemEnvPromptBuilder', + 'src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts|../sessionPresenter/sessionPaths', + 'src/renderer/src/pages/ChatPage.vue|@shared/chat', + 'src/renderer/src/pages/NewThreadPage.vue|@shared/chat', + 'src/renderer/src/stores/ui/message.ts|@shared/chat', + 'src/renderer/src/stores/ui/session.ts|@shared/chat', + 'src/renderer/src/components/chat/ChatAttachmentItem.vue|@shared/chat', + 'src/renderer/src/components/chat/ChatInputBox.vue|@shared/chat', + 'src/renderer/src/components/chat/MessageList.vue|@shared/chat', + 'src/renderer/src/components/chat/messageListItems.ts|@shared/chat', + 'src/renderer/src/components/chat/composables/useChatInputFiles.ts|@shared/chat', + 'src/renderer/src/components/message/MessageBlockAction.vue|@shared/chat', + 'src/renderer/src/components/message/MessageBlockAudio.vue|@shared/chat', + 'src/renderer/src/components/message/MessageBlockContent.vue|@shared/chat', + 'src/renderer/src/components/message/MessageBlockError.vue|@shared/chat', + 'src/renderer/src/components/message/MessageBlockImage.vue|@shared/chat', + 'src/renderer/src/components/message/MessageBlockMcpUi.vue|@shared/chat', + 'src/renderer/src/components/message/MessageBlockPlan.vue|@shared/chat', + 'src/renderer/src/components/message/MessageBlockQuestionRequest.vue|@shared/chat', + 'src/renderer/src/components/message/MessageBlockThink.vue|@shared/chat', + 'src/renderer/src/components/message/MessageBlockToolCall.vue|@shared/chat', + 'src/renderer/src/components/message/MessageContent.vue|@shared/chat', + 'src/renderer/src/components/message/MessageItemAssistant.vue|@shared/chat', + 'src/renderer/src/components/message/MessageItemUser.vue|@shared/chat', + 'src/renderer/src/components/message/MessageMinimap.vue|@shared/chat' +]) + +function toPosix(value) { + return value.split(path.sep).join('/') +} + +function relativePath(filePath) { + return toPosix(path.relative(ROOT, filePath)) +} + +function isSourceFile(filePath) { + return SOURCE_EXTENSIONS.has(path.extname(filePath)) +} + +function isUnder(targetPath, parentPath) { + const normalizedTarget = path.resolve(targetPath) + const normalizedParent = path.resolve(parentPath) + return ( + normalizedTarget === normalizedParent || + normalizedTarget.startsWith(`${normalizedParent}${path.sep}`) + ) +} + +function isProtectedPath(filePath, protectedPaths) { + return protectedPaths.some((entry) => isUnder(filePath, entry)) +} + +function extractModuleSpecifiers(source) { + const specifiers = new Set() + const patterns = [ + /\bimport\s+(?:type\s+)?[\s\S]*?\bfrom\s*['"]([^'"]+)['"]/g, + /\bexport\s+[\s\S]*?\bfrom\s*['"]([^'"]+)['"]/g, + /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g + ] + + for (const pattern of patterns) { + let match + while ((match = pattern.exec(source)) !== null) { + specifiers.add(match[1]) + } + } + + return specifiers +} + +async function collectFiles(entryPath) { + const stats = await fs.stat(entryPath) + if (stats.isFile()) { + return isSourceFile(entryPath) ? [entryPath] : [] + } + + const entries = await fs.readdir(entryPath, { withFileTypes: true }) + const files = [] + for (const entry of entries) { + const nextPath = path.join(entryPath, entry.name) + if (entry.isDirectory()) { + files.push(...(await collectFiles(nextPath))) + continue + } + if (entry.isFile() && isSourceFile(nextPath)) { + files.push(nextPath) + } + } + return files +} + +function classifyViolation(filePath, specifier) { + if (isProtectedPath(filePath, MAIN_PROTECTED_DIRS) && specifier.startsWith('.')) { + const resolved = path.resolve(path.dirname(filePath), specifier) + if (LEGACY_MAIN_DIRS.some((legacyDir) => isUnder(resolved, legacyDir))) { + return 'legacy-main' + } + } + + if ( + isProtectedPath(filePath, RENDERER_PROTECTED_DIRS) && + (specifier === '@shared/chat' || specifier.startsWith('@shared/chat/')) + ) { + return 'legacy-chat' + } + + return null +} + +async function findViolations() { + const files = [ + ...(await collectFiles(path.join(ROOT, 'src/main/presenter/newAgentPresenter'))), + ...(await collectFiles(path.join(ROOT, 'src/main/presenter/deepchatAgentPresenter'))), + ...(await collectFiles(path.join(ROOT, 'src/renderer/src/pages/ChatPage.vue'))), + ...(await collectFiles(path.join(ROOT, 'src/renderer/src/pages/NewThreadPage.vue'))), + ...(await collectFiles(path.join(ROOT, 'src/renderer/src/stores/ui'))), + ...(await collectFiles(path.join(ROOT, 'src/renderer/src/components/chat'))), + ...(await collectFiles(path.join(ROOT, 'src/renderer/src/components/message'))) + ] + + const violations = [] + for (const filePath of files) { + const source = await fs.readFile(filePath, 'utf8') + for (const specifier of extractModuleSpecifiers(source)) { + const kind = classifyViolation(filePath, specifier) + if (!kind) { + continue + } + + const file = relativePath(filePath) + violations.push({ + kind, + file, + specifier, + key: `${file}|${specifier}` + }) + } + } + + violations.sort((left, right) => left.key.localeCompare(right.key)) + return violations +} + +function printViolationList(title, violations) { + if (violations.length === 0) { + return + } + + console.error(title) + for (const violation of violations) { + console.error(`- [${violation.kind}] ${violation.file} -> ${violation.specifier}`) + } +} + +async function main() { + const violations = await findViolations() + const unexpected = violations.filter((violation) => !ALLOWED_BASELINE.has(violation.key)) + const removedFromBaseline = [...ALLOWED_BASELINE] + .filter((key) => !violations.some((violation) => violation.key === key)) + .sort() + + if (unexpected.length > 0) { + console.error('Agent cleanup guard failed. New legacy coupling was introduced.') + printViolationList('Unexpected violations:', unexpected) + process.exit(1) + } + + if (removedFromBaseline.length > 0) { + console.log('Agent cleanup guard note: some baseline violations were removed.') + for (const key of removedFromBaseline) { + console.log(`- ${key}`) + } + console.log('You can shrink the allowlist in scripts/agent-cleanup-guard.mjs in a follow-up.') + } + + console.log(`Agent cleanup guard passed. Baseline violations tracked: ${violations.length}.`) +} + +main().catch((error) => { + console.error('Agent cleanup guard failed to run:', error) + process.exit(1) +}) From 55572c9c45eecbd2f98cd2d4740d24e8c843fe73 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 14 Mar 2026 13:58:02 +0800 Subject: [PATCH 03/26] refactor(agent-cleanup): extract question tool --- scripts/agent-cleanup-guard.mjs | 1 - src/main/lib/agentRuntime/questionTool.ts | 64 +++++++++++++++++ .../agentPresenter/tools/questionTool.ts | 70 ++----------------- .../deepchatAgentPresenter/dispatch.ts | 2 +- 4 files changed, 71 insertions(+), 66 deletions(-) create mode 100644 src/main/lib/agentRuntime/questionTool.ts diff --git a/scripts/agent-cleanup-guard.mjs b/scripts/agent-cleanup-guard.mjs index a791ec079..19a04cc83 100644 --- a/scripts/agent-cleanup-guard.mjs +++ b/scripts/agent-cleanup-guard.mjs @@ -35,7 +35,6 @@ const RENDERER_PROTECTED_DIRS = [ ] const ALLOWED_BASELINE = new Set([ - 'src/main/presenter/deepchatAgentPresenter/dispatch.ts|../agentPresenter/tools/questionTool', 'src/main/presenter/deepchatAgentPresenter/index.ts|../agentPresenter/message/systemEnvPromptBuilder', 'src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts|../sessionPresenter/sessionPaths', 'src/renderer/src/pages/ChatPage.vue|@shared/chat', diff --git a/src/main/lib/agentRuntime/questionTool.ts b/src/main/lib/agentRuntime/questionTool.ts new file mode 100644 index 000000000..1d71f8060 --- /dev/null +++ b/src/main/lib/agentRuntime/questionTool.ts @@ -0,0 +1,64 @@ +import { z } from 'zod' +import { jsonrepair } from 'jsonrepair' +import type { QuestionInfo } from '@shared/types/core/question' + +export const QUESTION_TOOL_NAME = 'deepchat_question' + +const questionOptionSchema = z.object({ + label: z.string().trim().min(1).max(30), + description: z.string().trim().max(200).optional() +}) + +export const questionToolSchema = z.object({ + header: z.string().trim().max(30).optional(), + question: z.string().trim().min(1).max(500), + options: z.array(questionOptionSchema).min(1).max(10), + multiple: z.boolean().optional().default(false), + custom: z.boolean().optional().default(true) +}) + +export type QuestionToolInput = z.infer + +const normalizeQuestionInfo = (input: QuestionToolInput): QuestionInfo => { + const header = input.header?.trim() + const question = input.question.trim() + const options = input.options.map((option) => { + const description = option.description?.trim() + return { + label: option.label.trim(), + ...(description ? { description } : {}) + } + }) + + return { + ...(header ? { header } : {}), + question, + options, + multiple: Boolean(input.multiple), + custom: input.custom !== false + } +} + +export const parseQuestionToolArgs = ( + rawArgs: string +): { success: true; data: QuestionInfo } | { success: false; error: string } => { + let parsed: unknown = {} + if (rawArgs && rawArgs.trim()) { + try { + parsed = JSON.parse(rawArgs) as Record + } catch { + try { + parsed = JSON.parse(jsonrepair(rawArgs)) as Record + } catch { + return { success: false, error: 'Invalid JSON for question tool arguments.' } + } + } + } + + const result = questionToolSchema.safeParse(parsed) + if (!result.success) { + return { success: false, error: result.error.message } + } + + return { success: true, data: normalizeQuestionInfo(result.data) } +} diff --git a/src/main/presenter/agentPresenter/tools/questionTool.ts b/src/main/presenter/agentPresenter/tools/questionTool.ts index 1d71f8060..1979f7342 100644 --- a/src/main/presenter/agentPresenter/tools/questionTool.ts +++ b/src/main/presenter/agentPresenter/tools/questionTool.ts @@ -1,64 +1,6 @@ -import { z } from 'zod' -import { jsonrepair } from 'jsonrepair' -import type { QuestionInfo } from '@shared/types/core/question' - -export const QUESTION_TOOL_NAME = 'deepchat_question' - -const questionOptionSchema = z.object({ - label: z.string().trim().min(1).max(30), - description: z.string().trim().max(200).optional() -}) - -export const questionToolSchema = z.object({ - header: z.string().trim().max(30).optional(), - question: z.string().trim().min(1).max(500), - options: z.array(questionOptionSchema).min(1).max(10), - multiple: z.boolean().optional().default(false), - custom: z.boolean().optional().default(true) -}) - -export type QuestionToolInput = z.infer - -const normalizeQuestionInfo = (input: QuestionToolInput): QuestionInfo => { - const header = input.header?.trim() - const question = input.question.trim() - const options = input.options.map((option) => { - const description = option.description?.trim() - return { - label: option.label.trim(), - ...(description ? { description } : {}) - } - }) - - return { - ...(header ? { header } : {}), - question, - options, - multiple: Boolean(input.multiple), - custom: input.custom !== false - } -} - -export const parseQuestionToolArgs = ( - rawArgs: string -): { success: true; data: QuestionInfo } | { success: false; error: string } => { - let parsed: unknown = {} - if (rawArgs && rawArgs.trim()) { - try { - parsed = JSON.parse(rawArgs) as Record - } catch { - try { - parsed = JSON.parse(jsonrepair(rawArgs)) as Record - } catch { - return { success: false, error: 'Invalid JSON for question tool arguments.' } - } - } - } - - const result = questionToolSchema.safeParse(parsed) - if (!result.success) { - return { success: false, error: result.error.message } - } - - return { success: true, data: normalizeQuestionInfo(result.data) } -} +export { + parseQuestionToolArgs, + QUESTION_TOOL_NAME, + questionToolSchema, + type QuestionToolInput +} from '../../../lib/agentRuntime/questionTool' diff --git a/src/main/presenter/deepchatAgentPresenter/dispatch.ts b/src/main/presenter/deepchatAgentPresenter/dispatch.ts index 79db30b22..5ef5dfbb5 100644 --- a/src/main/presenter/deepchatAgentPresenter/dispatch.ts +++ b/src/main/presenter/deepchatAgentPresenter/dispatch.ts @@ -5,7 +5,7 @@ import type { MCPToolDefinition, SearchResult } from '@shared/presenter' import type { MCPToolCall, MCPContentItem, MCPResourceContent } from '@shared/types/core/mcp' import type { IToolPresenter } from '@shared/types/presenters/tool.presenter' import type { AssistantMessageBlock, PermissionMode } from '@shared/types/agent-interface' -import { parseQuestionToolArgs, QUESTION_TOOL_NAME } from '../agentPresenter/tools/questionTool' +import { parseQuestionToolArgs, QUESTION_TOOL_NAME } from '../../lib/agentRuntime/questionTool' import type { IoParams, PendingToolInteraction, ProcessHooks, StreamState } from './types' import type { ChatMessage } from '@shared/types/core/chat-message' import { nanoid } from 'nanoid' From 5dd6f16f028ac4c64c412e2d62f1b303eba333cd Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 14 Mar 2026 14:33:20 +0800 Subject: [PATCH 04/26] refactor(agent-cleanup): extract env prompt --- scripts/agent-cleanup-guard.mjs | 18 ++- .../agentRuntime/systemEnvPromptBuilder.ts | 126 +++++++++++++++++ .../message/systemEnvPromptBuilder.ts | 131 +----------------- .../presenter/deepchatAgentPresenter/index.ts | 4 +- .../deepchatAgentPresenter.test.ts | 7 +- 5 files changed, 152 insertions(+), 134 deletions(-) create mode 100644 src/main/lib/agentRuntime/systemEnvPromptBuilder.ts diff --git a/scripts/agent-cleanup-guard.mjs b/scripts/agent-cleanup-guard.mjs index 19a04cc83..26031908c 100644 --- a/scripts/agent-cleanup-guard.mjs +++ b/scripts/agent-cleanup-guard.mjs @@ -35,7 +35,6 @@ const RENDERER_PROTECTED_DIRS = [ ] const ALLOWED_BASELINE = new Set([ - 'src/main/presenter/deepchatAgentPresenter/index.ts|../agentPresenter/message/systemEnvPromptBuilder', 'src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts|../sessionPresenter/sessionPaths', 'src/renderer/src/pages/ChatPage.vue|@shared/chat', 'src/renderer/src/pages/NewThreadPage.vue|@shared/chat', @@ -127,9 +126,20 @@ async function collectFiles(entryPath) { } function classifyViolation(filePath, specifier) { - if (isProtectedPath(filePath, MAIN_PROTECTED_DIRS) && specifier.startsWith('.')) { - const resolved = path.resolve(path.dirname(filePath), specifier) - if (LEGACY_MAIN_DIRS.some((legacyDir) => isUnder(resolved, legacyDir))) { + if (isProtectedPath(filePath, MAIN_PROTECTED_DIRS)) { + if (specifier.startsWith('.')) { + const resolved = path.resolve(path.dirname(filePath), specifier) + if (LEGACY_MAIN_DIRS.some((legacyDir) => isUnder(resolved, legacyDir))) { + return 'legacy-main' + } + } + + if ( + specifier === '@/presenter/agentPresenter' || + specifier.startsWith('@/presenter/agentPresenter/') || + specifier === '@/presenter/sessionPresenter' || + specifier.startsWith('@/presenter/sessionPresenter/') + ) { return 'legacy-main' } } diff --git a/src/main/lib/agentRuntime/systemEnvPromptBuilder.ts b/src/main/lib/agentRuntime/systemEnvPromptBuilder.ts new file mode 100644 index 000000000..fdba156b3 --- /dev/null +++ b/src/main/lib/agentRuntime/systemEnvPromptBuilder.ts @@ -0,0 +1,126 @@ +import * as fs from 'node:fs' +import path from 'node:path' +import { presenter } from '@/presenter' +import logger from '@shared/logger' + +export interface BuildSystemEnvPromptOptions { + providerId?: string + modelId?: string + workdir?: string | null + platform?: NodeJS.Platform + now?: Date + agentsFilePath?: string +} + +function resolveModelDisplayName(providerId: string, modelId: string): string | undefined { + try { + const models = presenter.configPresenter?.getProviderModels?.(providerId) || [] + const match = models.find((model) => model.id === modelId) + if (match?.name) { + return match.name + } + + const customModels = presenter.configPresenter?.getCustomModels?.(providerId) || [] + const customMatch = customModels.find((model) => model.id === modelId) + if (customMatch?.name) { + return customMatch.name + } + } catch (error) { + console.warn( + `[SystemEnvPromptBuilder] Failed to resolve model display name for ${providerId}/${modelId}:`, + error + ) + } + + return undefined +} + +function resolveModelIdentity( + providerId?: string, + modelId?: string +): { + modelName: string + exactModelId: string +} { + const trimmedProviderId = providerId?.trim() || 'unknown-provider' + const trimmedModelId = modelId?.trim() || 'unknown-model' + const displayName = resolveModelDisplayName(trimmedProviderId, trimmedModelId) + + return { + modelName: displayName || trimmedModelId, + exactModelId: `${trimmedProviderId}/${trimmedModelId}` + } +} + +function resolveWorkdir(workdir?: string | null): string { + const normalized = workdir?.trim() + if (normalized) { + return path.resolve(normalized) + } + return process.cwd() +} + +function isGitRepository(workdir: string): boolean { + let current = path.resolve(workdir) + while (true) { + if (fs.existsSync(path.join(current, '.git'))) { + return true + } + const parent = path.dirname(current) + if (parent === current) { + return false + } + current = parent + } +} + +async function readAgentsInstructions(sourcePath: string): Promise { + try { + return await fs.promises.readFile(sourcePath, 'utf8') + } catch (error) { + logger.warn('[SystemEnvPromptBuilder] Failed to read AGENTS.md', { + sourcePath, + error + }) + return '' + } +} + +export function buildRuntimeCapabilitiesPrompt(): string { + return [ + '## Runtime Capabilities', + '- YoBrowser tools are available for browser automation when needed.', + '- Use exec(background: true) to start long-running terminal commands.', + '- Use process(list|poll|log|write|kill|remove) to manage background terminal sessions.', + '- Before launching another long-running command, prefer process action "list" to inspect existing sessions.' + ].join('\n') +} + +export async function buildSystemEnvPrompt( + options: BuildSystemEnvPromptOptions = {} +): Promise { + const now = options.now ?? new Date() + const platform = options.platform ?? process.platform + const workdir = resolveWorkdir(options.workdir) + const agentsFilePath = options.agentsFilePath + ? path.resolve(options.agentsFilePath) + : path.join(workdir, 'AGENTS.md') + const agentsContent = await readAgentsInstructions(agentsFilePath) + const { modelName, exactModelId } = resolveModelIdentity(options.providerId, options.modelId) + + const promptLines = [ + `You are powered by the model named ${modelName}.`, + `The exact model ID is ${exactModelId}`, + `## Here is some useful information about the environment you are running in:`, + `Working directory: ${workdir}`, + `Is directory a git repo: ${isGitRepository(workdir) ? 'yes' : 'no'}`, + `Platform: ${platform}`, + `Today's date: ${now.toDateString()}` + ] + + if (agentsContent.trim().length > 0) { + promptLines.push(`Instructions from: ${agentsFilePath}\n`, agentsContent) + } + + return promptLines.join('\n') +} diff --git a/src/main/presenter/agentPresenter/message/systemEnvPromptBuilder.ts b/src/main/presenter/agentPresenter/message/systemEnvPromptBuilder.ts index fdba156b3..c39ec9ffb 100644 --- a/src/main/presenter/agentPresenter/message/systemEnvPromptBuilder.ts +++ b/src/main/presenter/agentPresenter/message/systemEnvPromptBuilder.ts @@ -1,126 +1,5 @@ -import * as fs from 'node:fs' -import path from 'node:path' -import { presenter } from '@/presenter' -import logger from '@shared/logger' - -export interface BuildSystemEnvPromptOptions { - providerId?: string - modelId?: string - workdir?: string | null - platform?: NodeJS.Platform - now?: Date - agentsFilePath?: string -} - -function resolveModelDisplayName(providerId: string, modelId: string): string | undefined { - try { - const models = presenter.configPresenter?.getProviderModels?.(providerId) || [] - const match = models.find((model) => model.id === modelId) - if (match?.name) { - return match.name - } - - const customModels = presenter.configPresenter?.getCustomModels?.(providerId) || [] - const customMatch = customModels.find((model) => model.id === modelId) - if (customMatch?.name) { - return customMatch.name - } - } catch (error) { - console.warn( - `[SystemEnvPromptBuilder] Failed to resolve model display name for ${providerId}/${modelId}:`, - error - ) - } - - return undefined -} - -function resolveModelIdentity( - providerId?: string, - modelId?: string -): { - modelName: string - exactModelId: string -} { - const trimmedProviderId = providerId?.trim() || 'unknown-provider' - const trimmedModelId = modelId?.trim() || 'unknown-model' - const displayName = resolveModelDisplayName(trimmedProviderId, trimmedModelId) - - return { - modelName: displayName || trimmedModelId, - exactModelId: `${trimmedProviderId}/${trimmedModelId}` - } -} - -function resolveWorkdir(workdir?: string | null): string { - const normalized = workdir?.trim() - if (normalized) { - return path.resolve(normalized) - } - return process.cwd() -} - -function isGitRepository(workdir: string): boolean { - let current = path.resolve(workdir) - while (true) { - if (fs.existsSync(path.join(current, '.git'))) { - return true - } - const parent = path.dirname(current) - if (parent === current) { - return false - } - current = parent - } -} - -async function readAgentsInstructions(sourcePath: string): Promise { - try { - return await fs.promises.readFile(sourcePath, 'utf8') - } catch (error) { - logger.warn('[SystemEnvPromptBuilder] Failed to read AGENTS.md', { - sourcePath, - error - }) - return '' - } -} - -export function buildRuntimeCapabilitiesPrompt(): string { - return [ - '## Runtime Capabilities', - '- YoBrowser tools are available for browser automation when needed.', - '- Use exec(background: true) to start long-running terminal commands.', - '- Use process(list|poll|log|write|kill|remove) to manage background terminal sessions.', - '- Before launching another long-running command, prefer process action "list" to inspect existing sessions.' - ].join('\n') -} - -export async function buildSystemEnvPrompt( - options: BuildSystemEnvPromptOptions = {} -): Promise { - const now = options.now ?? new Date() - const platform = options.platform ?? process.platform - const workdir = resolveWorkdir(options.workdir) - const agentsFilePath = options.agentsFilePath - ? path.resolve(options.agentsFilePath) - : path.join(workdir, 'AGENTS.md') - const agentsContent = await readAgentsInstructions(agentsFilePath) - const { modelName, exactModelId } = resolveModelIdentity(options.providerId, options.modelId) - - const promptLines = [ - `You are powered by the model named ${modelName}.`, - `The exact model ID is ${exactModelId}`, - `## Here is some useful information about the environment you are running in:`, - `Working directory: ${workdir}`, - `Is directory a git repo: ${isGitRepository(workdir) ? 'yes' : 'no'}`, - `Platform: ${platform}`, - `Today's date: ${now.toDateString()}` - ] - - if (agentsContent.trim().length > 0) { - promptLines.push(`Instructions from: ${agentsFilePath}\n`, agentsContent) - } - - return promptLines.join('\n') -} +export { + buildRuntimeCapabilitiesPrompt, + buildSystemEnvPrompt, + type BuildSystemEnvPromptOptions +} from '../../../lib/agentRuntime/systemEnvPromptBuilder' diff --git a/src/main/presenter/deepchatAgentPresenter/index.ts b/src/main/presenter/deepchatAgentPresenter/index.ts index 0be94e914..7ca420de5 100644 --- a/src/main/presenter/deepchatAgentPresenter/index.ts +++ b/src/main/presenter/deepchatAgentPresenter/index.ts @@ -25,11 +25,11 @@ import { nanoid } from 'nanoid' import type { SQLitePresenter } from '../sqlitePresenter' import { eventBus, SendTarget } from '@/eventbus' import { SESSION_EVENTS, STREAM_EVENTS } from '@/events' -import { presenter } from '@/presenter' import { buildRuntimeCapabilitiesPrompt, buildSystemEnvPrompt -} from '../agentPresenter/message/systemEnvPromptBuilder' +} from '@/lib/agentRuntime/systemEnvPromptBuilder' +import { presenter } from '@/presenter' import { buildContext, buildResumeContext } from './contextBuilder' import { appendSummarySection, CompactionService, type CompactionIntent } from './compactionService' import { buildPersistableMessageTracePayload } from './messageTracePayload' diff --git a/test/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.ts b/test/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.ts index c231a3123..6cce07ec4 100644 --- a/test/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.ts +++ b/test/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.ts @@ -49,7 +49,7 @@ vi.mock('@/presenter', () => ({ } })) -vi.mock('@/presenter/agentPresenter/message/systemEnvPromptBuilder', () => ({ +vi.mock('@/lib/agentRuntime/systemEnvPromptBuilder', () => ({ buildRuntimeCapabilitiesPrompt: vi.fn(() => 'RUNTIME_CAPABILITIES'), buildSystemEnvPrompt: vi.fn( async (options?: { providerId?: string; modelId?: string; now?: Date }) => { @@ -69,7 +69,7 @@ vi.mock('@/presenter/deepchatAgentPresenter/process', () => ({ import { eventBus } from '@/eventbus' import { processStream } from '@/presenter/deepchatAgentPresenter/process' import { presenter } from '@/presenter' -import { buildSystemEnvPrompt } from '@/presenter/agentPresenter/message/systemEnvPromptBuilder' +import { buildSystemEnvPrompt } from '@/lib/agentRuntime/systemEnvPromptBuilder' function createMockSqlitePresenter() { const summaryState = { @@ -184,6 +184,9 @@ function createMockConfigPresenter() { supportsVerbosityCapability: vi.fn().mockReturnValue(true), getVerbosityDefault: vi.fn().mockReturnValue('medium'), getSkillsEnabled: vi.fn().mockReturnValue(true), + getAutoCompactionEnabled: vi.fn().mockReturnValue(true), + getAutoCompactionTriggerThreshold: vi.fn().mockReturnValue(80), + getAutoCompactionRetainRecentPairs: vi.fn().mockReturnValue(2), getSetting: vi.fn().mockReturnValue(undefined) } as any } From de3a00e84dec1354b97d70384703202f586e243a Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 14 Mar 2026 16:40:56 +0800 Subject: [PATCH 05/26] refactor(agent-cleanup): extract session paths --- scripts/agent-cleanup-guard.mjs | 1 - src/main/lib/agentRuntime/sessionPaths.ts | 43 +++++++++++++++++++ .../deepchatAgentPresenter/toolOutputGuard.ts | 2 +- .../sessionPresenter/sessionPaths.ts | 38 +++------------- 4 files changed, 50 insertions(+), 34 deletions(-) create mode 100644 src/main/lib/agentRuntime/sessionPaths.ts diff --git a/scripts/agent-cleanup-guard.mjs b/scripts/agent-cleanup-guard.mjs index 26031908c..084672d82 100644 --- a/scripts/agent-cleanup-guard.mjs +++ b/scripts/agent-cleanup-guard.mjs @@ -35,7 +35,6 @@ const RENDERER_PROTECTED_DIRS = [ ] const ALLOWED_BASELINE = new Set([ - 'src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts|../sessionPresenter/sessionPaths', 'src/renderer/src/pages/ChatPage.vue|@shared/chat', 'src/renderer/src/pages/NewThreadPage.vue|@shared/chat', 'src/renderer/src/stores/ui/message.ts|@shared/chat', diff --git a/src/main/lib/agentRuntime/sessionPaths.ts b/src/main/lib/agentRuntime/sessionPaths.ts new file mode 100644 index 000000000..96563a22d --- /dev/null +++ b/src/main/lib/agentRuntime/sessionPaths.ts @@ -0,0 +1,43 @@ +import { app } from 'electron' +import path from 'path' + +export function getSessionsRoot(): string { + return path.resolve(app.getPath('home'), '.deepchat', 'sessions') +} + +export function resolveSessionDir(conversationId: string): string | null { + if (!conversationId.trim()) { + return null + } + + const sessionsRoot = getSessionsRoot() + const resolvedSessionDir = path.resolve(sessionsRoot, conversationId) + const rootWithSeparator = sessionsRoot.endsWith(path.sep) + ? sessionsRoot + : `${sessionsRoot}${path.sep}` + + if (resolvedSessionDir !== sessionsRoot && !resolvedSessionDir.startsWith(rootWithSeparator)) { + return null + } + + return resolvedSessionDir +} + +export function resolveToolOffloadPath(conversationId: string, toolCallId: string): string | null { + const sessionDir = resolveSessionDir(conversationId) + if (!sessionDir) { + return null + } + + const safeToolCallId = toolCallId.replace(/[\\/]/g, '_') + return path.join(sessionDir, `tool_${safeToolCallId}.offload`) +} + +export function resolveToolOffloadTemplatePath(conversationId: string): string | null { + const sessionDir = resolveSessionDir(conversationId) + if (!sessionDir) { + return null + } + + return path.join(sessionDir, 'tool_.offload') +} diff --git a/src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts b/src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts index bd69c69e5..bb6eba727 100644 --- a/src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts +++ b/src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts @@ -3,8 +3,8 @@ import path from 'path' import { approximateTokenSize } from 'tokenx' import type { MCPToolDefinition } from '@shared/presenter' import type { ChatMessage } from '@shared/types/core/chat-message' +import { resolveToolOffloadPath } from '@/lib/agentRuntime/sessionPaths' import { estimateMessagesTokens } from './contextBuilder' -import { resolveToolOffloadPath } from '../sessionPresenter/sessionPaths' const TOOL_OUTPUT_OFFLOAD_THRESHOLD = 5000 const TOOL_OUTPUT_PREVIEW_LENGTH = 1024 diff --git a/src/main/presenter/sessionPresenter/sessionPaths.ts b/src/main/presenter/sessionPresenter/sessionPaths.ts index 940255b99..2d6658015 100644 --- a/src/main/presenter/sessionPresenter/sessionPaths.ts +++ b/src/main/presenter/sessionPresenter/sessionPaths.ts @@ -1,32 +1,6 @@ -import { app } from 'electron' -import path from 'path' - -export function getSessionsRoot(): string { - return path.resolve(app.getPath('home'), '.deepchat', 'sessions') -} - -export function resolveSessionDir(conversationId: string): string | null { - if (!conversationId.trim()) return null - const sessionsRoot = getSessionsRoot() - const resolvedSessionDir = path.resolve(sessionsRoot, conversationId) - const rootWithSeparator = sessionsRoot.endsWith(path.sep) - ? sessionsRoot - : `${sessionsRoot}${path.sep}` - if (resolvedSessionDir !== sessionsRoot && !resolvedSessionDir.startsWith(rootWithSeparator)) { - return null - } - return resolvedSessionDir -} - -export function resolveToolOffloadPath(conversationId: string, toolCallId: string): string | null { - const sessionDir = resolveSessionDir(conversationId) - if (!sessionDir) return null - const safeToolCallId = toolCallId.replace(/[\\/]/g, '_') - return path.join(sessionDir, `tool_${safeToolCallId}.offload`) -} - -export function resolveToolOffloadTemplatePath(conversationId: string): string | null { - const sessionDir = resolveSessionDir(conversationId) - if (!sessionDir) return null - return path.join(sessionDir, 'tool_.offload') -} +export { + getSessionsRoot, + resolveSessionDir, + resolveToolOffloadPath, + resolveToolOffloadTemplatePath +} from '@/lib/agentRuntime/sessionPaths' From a868df3795c380581fa5635c2f623fb1f74fd2cf Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 14 Mar 2026 18:12:47 +0800 Subject: [PATCH 06/26] types(agent-cleanup): narrow mcp imports --- src/main/presenter/deepchatAgentPresenter/dispatch.ts | 3 ++- src/main/presenter/deepchatAgentPresenter/index.ts | 10 +++------- .../deepchatAgentPresenter/toolOutputGuard.ts | 2 +- src/main/presenter/deepchatAgentPresenter/types.ts | 3 ++- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/presenter/deepchatAgentPresenter/dispatch.ts b/src/main/presenter/deepchatAgentPresenter/dispatch.ts index 5ef5dfbb5..812c4781f 100644 --- a/src/main/presenter/deepchatAgentPresenter/dispatch.ts +++ b/src/main/presenter/deepchatAgentPresenter/dispatch.ts @@ -1,8 +1,9 @@ import { eventBus, SendTarget } from '@/eventbus' import { STREAM_EVENTS } from '@/events' import { presenter } from '@/presenter' -import type { MCPToolDefinition, SearchResult } from '@shared/presenter' +import type { SearchResult } from '@shared/presenter' import type { MCPToolCall, MCPContentItem, MCPResourceContent } from '@shared/types/core/mcp' +import type { MCPToolDefinition } from '@shared/types/core/mcp' import type { IToolPresenter } from '@shared/types/presenters/tool.presenter' import type { AssistantMessageBlock, PermissionMode } from '@shared/types/agent-interface' import { parseQuestionToolArgs, QUESTION_TOOL_NAME } from '../../lib/agentRuntime/questionTool' diff --git a/src/main/presenter/deepchatAgentPresenter/index.ts b/src/main/presenter/deepchatAgentPresenter/index.ts index 7ca420de5..0b3a3a4a2 100644 --- a/src/main/presenter/deepchatAgentPresenter/index.ts +++ b/src/main/presenter/deepchatAgentPresenter/index.ts @@ -14,12 +14,8 @@ import type { } from '@shared/types/agent-interface' import type { MCPToolCall, MCPToolResponse } from '@shared/types/core/mcp' import type { ChatMessage } from '@shared/types/core/chat-message' -import type { - IConfigPresenter, - ILlmProviderPresenter, - MCPToolDefinition, - ModelConfig -} from '@shared/presenter' +import type { IConfigPresenter, ILlmProviderPresenter, ModelConfig } from '@shared/presenter' +import type { MCPToolDefinition } from '@shared/types/core/mcp' import type { IToolPresenter } from '@shared/types/presenters/tool.presenter' import { nanoid } from 'nanoid' import type { SQLitePresenter } from '../sqlitePresenter' @@ -1018,7 +1014,7 @@ export class DeepChatAgentPresenter implements IAgentImplementation { modelConfig: ModelConfig, temperature: number, maxTokens: number, - tools: import('@shared/presenter').MCPToolDefinition[] + tools: import('@shared/types/core/mcp').MCPToolDefinition[] ) => AsyncGenerator } } diff --git a/src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts b/src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts index bb6eba727..c8c603885 100644 --- a/src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts +++ b/src/main/presenter/deepchatAgentPresenter/toolOutputGuard.ts @@ -1,8 +1,8 @@ import fs from 'fs/promises' import path from 'path' import { approximateTokenSize } from 'tokenx' -import type { MCPToolDefinition } from '@shared/presenter' import type { ChatMessage } from '@shared/types/core/chat-message' +import type { MCPToolDefinition } from '@shared/types/core/mcp' import { resolveToolOffloadPath } from '@/lib/agentRuntime/sessionPaths' import { estimateMessagesTokens } from './contextBuilder' diff --git a/src/main/presenter/deepchatAgentPresenter/types.ts b/src/main/presenter/deepchatAgentPresenter/types.ts index a3da6dac3..3bb7d2171 100644 --- a/src/main/presenter/deepchatAgentPresenter/types.ts +++ b/src/main/presenter/deepchatAgentPresenter/types.ts @@ -6,7 +6,8 @@ import type { } from '@shared/types/agent-interface' import type { LLMCoreStreamEvent } from '@shared/types/core/llm-events' import type { ChatMessage } from '@shared/types/core/chat-message' -import type { MCPToolDefinition, ModelConfig } from '@shared/presenter' +import type { MCPToolDefinition } from '@shared/types/core/mcp' +import type { ModelConfig } from '@shared/presenter' import type { IToolPresenter } from '@shared/types/presenters/tool.presenter' import type { DeepChatMessageStore } from './messageStore' import type { ToolOutputGuard } from './toolOutputGuard' From 795dc3cf76fd6bfc4e8e086257c55430b1e17e96 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sat, 14 Mar 2026 19:43:45 +0800 Subject: [PATCH 07/26] types(agent-cleanup): extract search result --- .../presenter/deepchatAgentPresenter/dispatch.ts | 2 +- .../deepchatAgentPresenter/messageStore.ts | 2 +- src/main/presenter/newAgentPresenter/index.ts | 4 ++-- .../newAgentPresenter/legacyImportService.ts | 2 +- .../src/components/message/MessageBlockContent.vue | 5 +++-- .../src/components/message/ReferencePreview.vue | 2 +- src/renderer/src/stores/reference.ts | 2 +- src/shared/types/core/search.ts | 11 +++++++++++ src/shared/types/presenters/thread.presenter.d.ts | 13 ++----------- test/renderer/components/ChatPage.test.ts | 6 ++++++ 10 files changed, 29 insertions(+), 20 deletions(-) create mode 100644 src/shared/types/core/search.ts diff --git a/src/main/presenter/deepchatAgentPresenter/dispatch.ts b/src/main/presenter/deepchatAgentPresenter/dispatch.ts index 812c4781f..1b19ff86b 100644 --- a/src/main/presenter/deepchatAgentPresenter/dispatch.ts +++ b/src/main/presenter/deepchatAgentPresenter/dispatch.ts @@ -1,9 +1,9 @@ import { eventBus, SendTarget } from '@/eventbus' import { STREAM_EVENTS } from '@/events' import { presenter } from '@/presenter' -import type { SearchResult } from '@shared/presenter' import type { MCPToolCall, MCPContentItem, MCPResourceContent } from '@shared/types/core/mcp' import type { MCPToolDefinition } from '@shared/types/core/mcp' +import type { SearchResult } from '@shared/types/core/search' import type { IToolPresenter } from '@shared/types/presenters/tool.presenter' import type { AssistantMessageBlock, PermissionMode } from '@shared/types/agent-interface' import { parseQuestionToolArgs, QUESTION_TOOL_NAME } from '../../lib/agentRuntime/questionTool' diff --git a/src/main/presenter/deepchatAgentPresenter/messageStore.ts b/src/main/presenter/deepchatAgentPresenter/messageStore.ts index 93e16197e..a9ad90b4c 100644 --- a/src/main/presenter/deepchatAgentPresenter/messageStore.ts +++ b/src/main/presenter/deepchatAgentPresenter/messageStore.ts @@ -7,7 +7,7 @@ import type { AssistantMessageBlock, MessageMetadata } from '@shared/types/agent-interface' -import type { SearchResult } from '@shared/presenter' +import type { SearchResult } from '@shared/types/core/search' import type { DeepChatMessageRow } from '../sqlitePresenter/tables/deepchatMessages' export class DeepChatMessageStore { diff --git a/src/main/presenter/newAgentPresenter/index.ts b/src/main/presenter/newAgentPresenter/index.ts index b1b1b91f8..4c814a3f5 100644 --- a/src/main/presenter/newAgentPresenter/index.ts +++ b/src/main/presenter/newAgentPresenter/index.ts @@ -18,12 +18,12 @@ import type { ToolInteractionResult } from '@shared/types/agent-interface' import type { Message } from '@shared/chat' +import type { SearchResult } from '@shared/types/core/search' import type { IConfigPresenter, ILlmProviderPresenter, ISkillPresenter, - CONVERSATION, - SearchResult + CONVERSATION } from '@shared/presenter' import type { SQLitePresenter } from '../sqlitePresenter' import type { DeepChatAgentPresenter } from '../deepchatAgentPresenter' diff --git a/src/main/presenter/newAgentPresenter/legacyImportService.ts b/src/main/presenter/newAgentPresenter/legacyImportService.ts index 7479bbdc5..064e1f531 100644 --- a/src/main/presenter/newAgentPresenter/legacyImportService.ts +++ b/src/main/presenter/newAgentPresenter/legacyImportService.ts @@ -8,7 +8,7 @@ import type { LegacyImportStatus, UserMessageContent } from '@shared/types/agent-interface' -import type { SearchResult } from '@shared/presenter' +import type { SearchResult } from '@shared/types/core/search' type LegacyRow = Record diff --git a/src/renderer/src/components/message/MessageBlockContent.vue b/src/renderer/src/components/message/MessageBlockContent.vue index 90b122afa..15da3b4d9 100644 --- a/src/renderer/src/components/message/MessageBlockContent.vue +++ b/src/renderer/src/components/message/MessageBlockContent.vue @@ -26,7 +26,7 @@ import { ref, nextTick, watch, onMounted } from 'vue' import { usePresenter } from '@/composables/usePresenter' -import { SearchResult } from '@shared/presenter' +import type { SearchResult } from '@shared/types/core/search' const newAgentPresenter = usePresenter('newAgentPresenter') const searchResults = ref([]) @@ -37,7 +37,8 @@ import ToolCallPreview from '../artifacts/ToolCallPreview.vue' import { useBlockContent } from '@/composables/useArtifacts' import { useArtifactStore } from '@/stores/artifact' import MarkdownRenderer from '@/components/markdown/MarkdownRenderer.vue' -import { AssistantMessageBlock } from '@shared/chat' +import type { AssistantMessageBlock } from '@shared/chat' + const artifactStore = useArtifactStore() const props = defineProps<{ block: AssistantMessageBlock diff --git a/src/renderer/src/components/message/ReferencePreview.vue b/src/renderer/src/components/message/ReferencePreview.vue index 639adea6d..3e20b52ae 100644 --- a/src/renderer/src/components/message/ReferencePreview.vue +++ b/src/renderer/src/components/message/ReferencePreview.vue @@ -36,7 +36,7 @@