diff --git a/src/web-ui/eslint.config.mjs b/src/web-ui/eslint.config.mjs index 426751c7cf..56815d1f17 100644 --- a/src/web-ui/eslint.config.mjs +++ b/src/web-ui/eslint.config.mjs @@ -19,9 +19,43 @@ export default tseslint.config( 'src/component-library/components/registry.tsx', 'src/component-library/preview/**', 'src/shared/context-system/core/types/**', - 'src/shared/context-menu-system/examples/**', ], }, + { + // Adapter-layer fence: business Tauri commands must reach the platform + // only through ApiClient (api.invoke). Direct `invoke` from + // '@tauri-apps/api/core' is reserved for the adapter implementations in + // adapters/** (and the peer-device host bridge, which intentionally runs + // outside the routed transport — see PeerHostInvokeBridge). This is the + // executable form of "front end calls go through the adapter layer"; + // reintroducing a direct invoke elsewhere fails the build. + files: ['src/**/*.{ts,tsx}'], + ignores: [ + 'src/infrastructure/api/adapters/**', + // PeerHostInvokeBridge runs on the HOST side of Peer-Device Mode: it + // executes *dynamic* command names forwarded from the peer device via a + // raw Tauri invoke. ApiClient is already routed to the peer adapter at + // this point, so routing through it would be wrong. This is the one + // intentional exception to the adapter-layer fence. + 'src/infrastructure/peer-device/PeerHostInvokeBridge.tsx', + ], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['@tauri-apps/api/core'], + importNames: ['invoke'], + message: + '业务命令必须经 api.invoke(ApiClient) 统一适配层,不可直接 import invoke。' + + '如需直连平台 invoke,放到 adapters/ 内并经 api 暴露。', + }, + ], + }, + ], + }, + }, { files: ['src/**/*.{ts,tsx}'], extends: [js.configs.recommended, ...tseslint.configs.recommended], diff --git a/src/web-ui/src/flow_chat/services/FlowChatManager.test.ts b/src/web-ui/src/flow_chat/services/FlowChatManager.test.ts index d5e9130beb..daefcf8899 100644 --- a/src/web-ui/src/flow_chat/services/FlowChatManager.test.ts +++ b/src/web-ui/src/flow_chat/services/FlowChatManager.test.ts @@ -42,12 +42,6 @@ vi.mock('../store/FlowChatStore', () => ({ }, })); -vi.mock('../../shared/services/agent-service', () => ({ - AgentService: { - getInstance: vi.fn(() => ({})), - }, -})); - vi.mock('@/infrastructure/api/service-api/ACPClientAPI', () => ({ ACPClientAPI: {}, })); diff --git a/src/web-ui/src/flow_chat/services/FlowChatManager.ts b/src/web-ui/src/flow_chat/services/FlowChatManager.ts index 690ebd6f36..3c9c8a00b6 100644 --- a/src/web-ui/src/flow_chat/services/FlowChatManager.ts +++ b/src/web-ui/src/flow_chat/services/FlowChatManager.ts @@ -10,7 +10,6 @@ import { processingStatusManager } from './ProcessingStatusManager'; import { FlowChatStore } from '../store/FlowChatStore'; import { useModernFlowChatStore } from '../store/modernFlowChatStore'; -import { AgentService } from '../../shared/services/agent-service'; import { ACPClientAPI } from '@/infrastructure/api/service-api/ACPClientAPI'; import { stateMachineManager } from '../state-machine'; import { EventBatcher } from './EventBatcher'; @@ -79,7 +78,6 @@ const EVENT_LISTENER_RETRY_MS = 2000; export class FlowChatManager { private static instance: FlowChatManager | null = null; private context: FlowChatContext; - private agentService: AgentService; private eventListenerInitialized = false; private eventListenerInitializationPromise: Promise | null = null; private eventListenerCleanup: (() => void) | null = null; @@ -117,8 +115,7 @@ export class FlowChatManager { currentWorkspacePath: null, ensureLiveSubscription: () => this.ensureEventListeners(), }; - - this.agentService = AgentService.getInstance(); + registerDriverSessionLookup( sessionId => this.context.flowChatStore.getState().sessions.get(sessionId), ); @@ -917,10 +914,6 @@ export class FlowChatManager { updateImageAnalysisItemModule(this.context, sessionId, dialogTurnId, imageId, updates); } - async getAvailableAgents(): Promise { - return this.agentService.getAvailableAgents(); - } - getCurrentSession() { return this.context.flowChatStore.getActiveSession(); } diff --git a/src/web-ui/src/infrastructure/agents/constants.ts b/src/web-ui/src/infrastructure/agents/constants.ts deleted file mode 100644 index 0adb97b202..0000000000 --- a/src/web-ui/src/infrastructure/agents/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Built-in agent ids that are displayed with "Sub-Agent" badge (e.g. Explore, FileFinder). - * Other builtin agents keep the "Built-in" badge. - */ -export const BUILTIN_SUB_AGENT_IDS = ['explore', 'file_finder'] as const; - -export function isBuiltinSubAgent(agentId: string): boolean { - const id = agentId.toLowerCase().replace(/\s+/g, '_'); - return id === 'explore' || id === 'file_finder' || id === 'filefinder'; -} diff --git a/src/web-ui/src/infrastructure/api/index.ts b/src/web-ui/src/infrastructure/api/index.ts index a058377cf3..06e641b535 100644 --- a/src/web-ui/src/infrastructure/api/index.ts +++ b/src/web-ui/src/infrastructure/api/index.ts @@ -21,7 +21,6 @@ import { aiApi } from './service-api/AIApi'; import { toolAPI } from './service-api/ToolAPI'; import { agentAPI } from './service-api/AgentAPI'; import { systemAPI } from './service-api/SystemAPI'; -import { projectAPI } from './service-api/ProjectAPI'; import { diffAPI } from './service-api/DiffAPI'; import { snapshotAPI } from './service-api/SnapshotAPI'; import { globalAPI } from './service-api/GlobalAPI'; @@ -31,7 +30,6 @@ import { permissionAPI } from './service-api/PermissionAPI'; import { pageAPI } from './service-api/PageAPI'; import { gitAPI } from './service-api/GitAPI'; import { gitAgentAPI } from './service-api/GitAgentAPI'; -import { gitRepoHistoryAPI, type GitRepoHistory } from './service-api/GitRepoHistoryAPI'; import { sessionAPI } from './service-api/SessionAPI'; import { i18nAPI } from './service-api/I18nAPI'; import { btwAPI } from './service-api/BtwAPI'; @@ -43,7 +41,7 @@ import { speechAPI } from './service-api/SpeechAPI'; import { worktreeAPI } from './service-api/WorktreeAPI'; // Export API modules -export { workspaceAPI, configAPI, aiApi, toolAPI, agentAPI, systemAPI, projectAPI, diffAPI, snapshotAPI, globalAPI, contextAPI, cronAPI, permissionAPI, pageAPI, gitAPI, gitAgentAPI, gitRepoHistoryAPI, sessionAPI, i18nAPI, btwAPI, editorAiAPI, reviewPlatformAPI, insightsApi, tokenUsageStatisticsApi, speechAPI, worktreeAPI }; +export { workspaceAPI, configAPI, aiApi, toolAPI, agentAPI, systemAPI, diffAPI, snapshotAPI, globalAPI, contextAPI, cronAPI, permissionAPI, pageAPI, gitAPI, gitAgentAPI, sessionAPI, i18nAPI, btwAPI, editorAiAPI, reviewPlatformAPI, insightsApi, tokenUsageStatisticsApi, speechAPI, worktreeAPI }; export { TokenUsageStatisticsUnavailableError } from './tokenUsageStatisticsApi'; export * from './service-api/ReviewPlatformAPI'; export type { @@ -58,38 +56,4 @@ export type { } from './tokenUsageStatisticsApi'; // Export types -export type { GitRepoHistory }; export type { CheckForUpdatesResponse } from './service-api/SystemAPI'; - -// BitFun API collection: a single access point for all API modules. -export const bitfunAPI = { - workspace: workspaceAPI, - config: configAPI, - ai: aiApi, - tool: toolAPI, - agent: agentAPI, - system: systemAPI, - project: projectAPI, - diff: diffAPI, - snapshot: snapshotAPI, - global: globalAPI, - context: contextAPI, - cron: cronAPI, - permission: permissionAPI, - pages: pageAPI, - git: gitAPI, - gitAgent: gitAgentAPI, - gitRepoHistory: gitRepoHistoryAPI, - session: sessionAPI, - i18n: i18nAPI, - btw: btwAPI, - editorAi: editorAiAPI, - reviewPlatform: reviewPlatformAPI, - insights: insightsApi, - tokenUsageStatistics: tokenUsageStatisticsApi, - speech: speechAPI, - worktree: worktreeAPI, -}; - -// Default export -export default bitfunAPI; diff --git a/src/web-ui/src/infrastructure/api/insightsApi.ts b/src/web-ui/src/infrastructure/api/insightsApi.ts index 95cf679da3..05c5c377f7 100644 --- a/src/web-ui/src/infrastructure/api/insightsApi.ts +++ b/src/web-ui/src/infrastructure/api/insightsApi.ts @@ -1,4 +1,4 @@ -import { invoke } from '@tauri-apps/api/core'; +import { api } from './service-api/ApiClient'; import { listen, type UnlistenFn } from '@tauri-apps/api/event'; import { openPath } from '@tauri-apps/plugin-opener'; @@ -176,29 +176,29 @@ export interface InsightsProgressEvent { export const insightsApi = { async generateInsights(days?: number, modelId?: string): Promise { - return invoke('generate_insights', { + return api.invoke('generate_insights', { request: { days: days ?? 30, modelId: modelId || 'auto' }, }); }, async getLatestInsights(): Promise { - return invoke('get_latest_insights'); + return api.invoke('get_latest_insights'); }, async loadReport(path: string): Promise { - return invoke('load_insights_report', { + return api.invoke('load_insights_report', { request: { path }, }); }, async hasInsightsData(days?: number): Promise { - return invoke('has_insights_data', { + return api.invoke('has_insights_data', { request: { days: days ?? 30 }, }); }, async cancelGeneration(): Promise { - return invoke('cancel_insights_generation'); + return api.invoke('cancel_insights_generation'); }, async listenProgress( diff --git a/src/web-ui/src/infrastructure/api/service-api/GitRepoHistoryAPI.ts b/src/web-ui/src/infrastructure/api/service-api/GitRepoHistoryAPI.ts deleted file mode 100644 index 3d58a89e29..0000000000 --- a/src/web-ui/src/infrastructure/api/service-api/GitRepoHistoryAPI.ts +++ /dev/null @@ -1,41 +0,0 @@ - - -import { api } from './ApiClient'; -import { createTauriCommandError } from '../errors/TauriCommandError'; - - -export interface GitRepoHistory { - url: string; - lastUsed: string; - localPath?: string; -} - - -export class GitRepoHistoryAPI { - - async saveGitRepoHistory(repos: GitRepoHistory[]): Promise { - try { - await api.invoke('save_git_repo_history', { - request: { repos } - }); - } catch (error) { - throw createTauriCommandError('save_git_repo_history', error, { repos }); - } - } - - - async loadGitRepoHistory(): Promise { - try { - return await api.invoke('load_git_repo_history', { - request: {} - }); - } catch (error) { - throw createTauriCommandError('load_git_repo_history', error); - } - } -} - - -export const gitRepoHistoryAPI = new GitRepoHistoryAPI(); - - diff --git a/src/web-ui/src/infrastructure/api/service-api/I18nAPI.ts b/src/web-ui/src/infrastructure/api/service-api/I18nAPI.ts index b1df6adcf0..270208943e 100644 --- a/src/web-ui/src/infrastructure/api/service-api/I18nAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/I18nAPI.ts @@ -1,6 +1,6 @@ -import { invoke } from '@tauri-apps/api/core'; +import { api } from './ApiClient'; import type { LocaleId, LocaleMetadata, I18nConfig } from '@/infrastructure/i18n/types'; import { getLocaleMetadata } from '@/infrastructure/i18n/presets'; import { createLogger } from '@/shared/utils/logger'; @@ -21,7 +21,7 @@ class I18nAPIClass { async getCurrentLanguage(): Promise { try { - const language = await invoke('i18n_get_current_language'); + const language = await api.invoke('i18n_get_current_language'); return language as LocaleId; } catch (error) { log.warn('Failed to get current language, using default', error); @@ -31,14 +31,14 @@ class I18nAPIClass { async setLanguage(language: LocaleId): Promise { - return invoke('i18n_set_language', { + return api.invoke('i18n_set_language', { request: { language } }); } async getSupportedLanguages(): Promise { - const response = await invoke('i18n_get_supported_languages'); + const response = await api.invoke('i18n_get_supported_languages'); return response.map(item => { const id = item.id as LocaleId; @@ -67,7 +67,7 @@ class I18nAPIClass { async getConfig(): Promise { try { - const config = await invoke('i18n_get_config'); + const config = await api.invoke('i18n_get_config'); return { currentLanguage: config.currentLanguage || 'zh-CN', fallbackLanguage: config.fallbackLanguage || 'en-US', @@ -87,7 +87,7 @@ class I18nAPIClass { async setConfig(config: Partial): Promise { - return invoke('i18n_set_config', { config }); + return api.invoke('i18n_set_config', { config }); } } diff --git a/src/web-ui/src/infrastructure/api/service-api/ProjectAPI.ts b/src/web-ui/src/infrastructure/api/service-api/ProjectAPI.ts deleted file mode 100644 index 3eaa5fa2bb..0000000000 --- a/src/web-ui/src/infrastructure/api/service-api/ProjectAPI.ts +++ /dev/null @@ -1,65 +0,0 @@ - - -import { api } from './ApiClient'; -import { createTauriCommandError } from '../errors/TauriCommandError'; - - -export class ProjectAPI { - - async analyzeProject(path: string, options?: any): Promise { - try { - return await api.invoke('analyze_project', { - request: { path, options } - }); - } catch (error) { - throw createTauriCommandError('analyze_project', error, { path, options }); - } - } - - - async getProjectStructure(path: string): Promise { - try { - return await api.invoke('get_project_structure', { - request: { path } - }); - } catch (error) { - throw createTauriCommandError('get_project_structure', error, { path }); - } - } - - - async getDependencyGraph(path: string): Promise { - try { - return await api.invoke('get_dependency_graph', { - request: { path } - }); - } catch (error) { - throw createTauriCommandError('get_dependency_graph', error, { path }); - } - } - - - async searchCode(query: string, options?: any): Promise { - try { - return await api.invoke('search_code', { - request: { query, options } - }); - } catch (error) { - throw createTauriCommandError('search_code', error, { query, options }); - } - } - - - async clearProjectCache(workspacePath: string): Promise { - try { - await api.invoke('clear_project_cache', { - request: { workspacePath } - }); - } catch (error) { - throw createTauriCommandError('clear_project_cache', error, { workspacePath }); - } - } -} - - -export const projectAPI = new ProjectAPI(); \ No newline at end of file diff --git a/src/web-ui/src/infrastructure/config/services/AgentCompanionPetService.ts b/src/web-ui/src/infrastructure/config/services/AgentCompanionPetService.ts index e91e114947..667705752e 100644 --- a/src/web-ui/src/infrastructure/config/services/AgentCompanionPetService.ts +++ b/src/web-ui/src/infrastructure/config/services/AgentCompanionPetService.ts @@ -1,4 +1,4 @@ -import { invoke } from '@tauri-apps/api/core'; +import { api } from '@/infrastructure/api/service-api/ApiClient'; import { readFile } from '@tauri-apps/plugin-fs'; import type { AgentCompanionPetSelection } from './AIExperienceConfigService'; import { isTauriRuntime } from '@/infrastructure/runtime'; @@ -172,7 +172,7 @@ export async function listAgentCompanionPets(): Promise('list_agent_companion_pets'); + const response = await api.invoke('list_agent_companion_pets'); const userPets = await Promise.all(response.pets.map(withPreviewSrc)); return [...builtinPets, ...userPets]; } catch (error) { @@ -182,14 +182,14 @@ export async function listAgentCompanionPets(): Promise { - const pet = await invoke('import_agent_companion_pet_package', { + const pet = await api.invoke('import_agent_companion_pet_package', { request: { path }, }); return withPreviewSrc(pet); } export async function deleteAgentCompanionPetPackage(packagePath: string): Promise { - await invoke('delete_agent_companion_pet_package', { + await api.invoke('delete_agent_companion_pet_package', { request: { packagePath }, }); } diff --git a/src/web-ui/src/infrastructure/services/api/index.ts b/src/web-ui/src/infrastructure/services/api/index.ts deleted file mode 100644 index ac125dc042..0000000000 --- a/src/web-ui/src/infrastructure/services/api/index.ts +++ /dev/null @@ -1,5 +0,0 @@ - - -export { default as aiService } from './aiService'; -export { default as contextService } from './contextService'; - diff --git a/src/web-ui/src/infrastructure/services/business/agentService.ts b/src/web-ui/src/infrastructure/services/business/agentService.ts deleted file mode 100644 index e7a1a35190..0000000000 --- a/src/web-ui/src/infrastructure/services/business/agentService.ts +++ /dev/null @@ -1,248 +0,0 @@ - - -import { createLogger } from '../../../shared/utils/logger'; -import { agentAPI } from '../../api'; -import { i18nService } from '@/infrastructure/i18n'; - -const logger = createLogger('AgentService'); - - -type AgentType = 'project_qa' | 'requirement_clarification' | 'core'; - -export interface AgentResponse { - content: string; - metadata: Record; -} - -export interface AgentCallOptions { - agentType: AgentType; - message: string; - workspacePath?: string; -} - - -export interface AgentExecutionRequest { - agent_type: string; - prompt: string; - model_name?: string; - workspace_path?: string; - context?: Record; - verbose?: boolean; -} - - -class SessionManager { - private sessions = new Map(); // workspacePath::agentType -> sessionId - - private buildKey(agentType: string, workspacePath: string): string { - return `${workspacePath}::${agentType}`; - } - - getSession(agentType: string, workspacePath: string): string | undefined { - return this.sessions.get(this.buildKey(agentType, workspacePath)); - } - - setSession(agentType: string, workspacePath: string, sessionId: string): void { - this.sessions.set(this.buildKey(agentType, workspacePath), sessionId); - } - - deleteSession(agentType: string, workspacePath: string): void { - this.sessions.delete(this.buildKey(agentType, workspacePath)); - } - - clear(): void { - this.sessions.clear(); - } -} - -export class AgentService { - private static sessionManager = new SessionManager(); - - - static async getOrCreateSession(agentType: string, workspacePath: string, modelName?: string): Promise { - - const existingSessionId = this.sessionManager.getSession(agentType, workspacePath); - if (existingSessionId) { - logger.debug(`Using existing session: ${existingSessionId}`); - return existingSessionId; - } - - - logger.info(`Creating new session: ${agentType}`); - - try { - const response = await agentAPI.createSession({ - sessionName: `${agentType}-session-${Date.now()}`, - agentType, - workspacePath, - config: { - modelName, - enableTools: true, - safeMode: true, - autoCompact: true, - enableContextCompression: true, - } - }); - this.sessionManager.setSession(agentType, workspacePath, response.sessionId); - logger.info(`Session created: ${response.sessionId}`); - return response.sessionId; - } catch (error) { - logger.error('Failed to create session', error); - throw error; - } - } - - - static async executeAgentTaskStream( - request: AgentExecutionRequest, - callbacks: { - onModelRoundStart?: (event: any) => void; - onTextChunk?: (event: any) => void; - onToolCall?: (event: any) => void; - onToolResult?: (event: any) => void; - onToolConfirmation?: (event: any) => void; - onProgress?: (event: any) => void; - onComplete?: (event: any) => void; - onError?: (error: any) => void; - } - ): Promise { - logger.info('Executing agent task flow', { - agentType: request.agent_type, - hasContext: !!request.context - }); - - try { - - const workspacePath = request.workspace_path; - if (!workspacePath) { - throw new Error('Workspace path is required to start an agent task'); - } - const sessionId = await this.getOrCreateSession(request.agent_type, workspacePath, request.model_name); - - - const unlistenFunctions: Array<() => void> = []; - - - if (callbacks.onTextChunk) { - const unlisten = await agentAPI.onTextChunk((event) => { - if (event.sessionId === sessionId) { - callbacks.onTextChunk?.(event); - } - }); - unlistenFunctions.push(unlisten); - } - - - if (callbacks.onModelRoundStart) { - const unlisten = await agentAPI.onModelRoundStarted((event) => { - if (event.sessionId === sessionId) { - callbacks.onModelRoundStart?.(event); - } - }); - unlistenFunctions.push(unlisten); - } - - - if (callbacks.onToolCall || callbacks.onToolResult || callbacks.onToolConfirmation) { - const unlisten = await agentAPI.onToolEvent((event) => { - if (event.sessionId === sessionId) { - const toolEvent = event.toolEvent; - - - if (toolEvent.Started || toolEvent.EarlyDetected) { - callbacks.onToolCall?.(toolEvent); - } else if (toolEvent.Completed || toolEvent.Failed) { - callbacks.onToolResult?.(toolEvent); - } else if (toolEvent.ConfirmationNeeded) { - callbacks.onToolConfirmation?.(toolEvent); - } else if (toolEvent.Progress || toolEvent.StreamChunk) { - callbacks.onProgress?.(toolEvent); - } - } - }); - unlistenFunctions.push(unlisten); - } - - - if (callbacks.onComplete) { - const unlisten = await agentAPI.onDialogTurnCompleted((event) => { - if (event.sessionId === sessionId) { - callbacks.onComplete?.(event); - - unlistenFunctions.forEach(fn => fn()); - } - }); - unlistenFunctions.push(unlisten); - } - - - await agentAPI.startDialogTurn({ - sessionId, - userInput: request.prompt, - agentType: request.agent_type, - workspacePath, - }); - - - return sessionId; - } catch (error) { - logger.error('Agent task flow failed', error); - callbacks.onError?.(error); - throw error; - } - } - - - static async cancelAgentTask(taskId: string): Promise { - try { - await agentAPI.cancelSession(taskId); - logger.info(`Task cancelled: ${taskId}`); - } catch (error) { - logger.error('Failed to cancel task', error); - throw error; - } - } - - - static async getAgentHealth(agentType: AgentType): Promise<{ healthy: boolean; name: string; description: string }> { - return { - healthy: true, - name: this.getAgentDisplayName(agentType), - description: this.getAgentDescription(agentType) - }; - } - - - private static getAgentDisplayName(agentType: AgentType): string { - const nameMap: Record = { - 'project_qa': i18nService.t('common:agents.projectQa.name'), - 'requirement_clarification': i18nService.t('common:agents.requirementClarification.name'), - 'core': i18nService.t('common:agents.core.name') - }; - return nameMap[agentType] || agentType; - } - - - private static getAgentDescription(agentType: AgentType): string { - const descMap: Record = { - 'project_qa': i18nService.t('common:agents.projectQa.description'), - 'requirement_clarification': i18nService.t('common:agents.requirementClarification.description'), - 'core': i18nService.t('common:agents.core.description') - }; - return descMap[agentType] || i18nService.t('common:agents.general.description'); - } - - - - static requiresSpecialVisualization(agentType: AgentType, metadata?: Record): boolean { - if (agentType === 'requirement_clarification') { - - return !!(metadata?.interactive_sections && Array.isArray(metadata.interactive_sections)); - } - - return false; - } - - -} -export default AgentService; diff --git a/src/web-ui/src/infrastructure/services/index.ts b/src/web-ui/src/infrastructure/services/index.ts deleted file mode 100644 index be57530f1b..0000000000 --- a/src/web-ui/src/infrastructure/services/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Core services unified exports. - * - * Service layering: API → Business → Infrastructure - */ - -// API layer: external IO and data access -export * from './api/aiService'; - -// Business layer: domain logic and orchestration -export * from './business/agentService'; -export * from './business/workspaceManager'; - -// Infrastructure layer: low-level technical services -export * from './infra/contextManager'; diff --git a/src/web-ui/src/infrastructure/services/infra/contextManager.ts b/src/web-ui/src/infrastructure/services/infra/contextManager.ts deleted file mode 100644 index 9ee643ea48..0000000000 --- a/src/web-ui/src/infrastructure/services/infra/contextManager.ts +++ /dev/null @@ -1,136 +0,0 @@ - - -import { contextAPI } from '../../api'; -import { createLogger } from '@/shared/utils/logger'; - -const log = createLogger('ContextManager'); - - -export type { ContextStats, SessionMetadata, StorageStats } from '../../api/service-api/ContextAPI'; -import type { ContextStats, SessionMetadata, StorageStats } from '../../api/service-api/ContextAPI'; - -export class ContextManager { - - async compressContext(): Promise { - try { - return await contextAPI.compressContext(); - } catch (error) { - log.error('Failed to compress context', error); - throw new Error(`Context compression failed: ${error}`); - } - } - - - async getContextStats(): Promise { - try { - return await contextAPI.getContextStats(); - } catch (error) { - log.error('Failed to get context stats', error); - throw new Error(`Failed to get context stats: ${error}`); - } - } - - - async clearContext(): Promise { - try { - return await contextAPI.clearContext(); - } catch (error) { - log.error('Failed to clear context', error); - throw new Error(`Context clear failed: ${error}`); - } - } - - - async saveSessionData(sessionData: any): Promise { - try { - return await contextAPI.saveSessionData(sessionData); - } catch (error) { - log.error('Failed to save session data', error); - throw new Error(`Session save failed: ${error}`); - } - } - - - async loadSessionData(sessionId: string): Promise { - try { - return await contextAPI.loadSessionData(sessionId); - } catch (error) { - log.error('Failed to load session data', { sessionId, error }); - throw new Error(`Session load failed: ${error}`); - } - } - - - async listSessions(includeArchived: boolean = false): Promise { - try { - return await contextAPI.listSessions(includeArchived); - } catch (error) { - log.error('Failed to list sessions', { includeArchived, error }); - throw new Error(`Failed to list sessions: ${error}`); - } - } - - - async searchSessions(query: string, tags?: string[]): Promise { - try { - return await contextAPI.searchSessions(query, tags); - } catch (error) { - log.error('Failed to search sessions', { query, tags, error }); - throw new Error(`Failed to search sessions: ${error}`); - } - } - - - async deleteSession(sessionId: string): Promise { - try { - return await contextAPI.deleteSession(sessionId); - } catch (error) { - log.error('Failed to delete session', { sessionId, error }); - throw new Error(`Failed to delete session: ${error}`); - } - } - - - async archiveSession(sessionId: string): Promise { - try { - return await contextAPI.archiveSession(sessionId); - } catch (error) { - log.error('Failed to archive session', { sessionId, error }); - throw new Error(`Failed to archive session: ${error}`); - } - } - - - async exportSession(sessionId: string, exportPath: string): Promise { - try { - return await contextAPI.exportSession(sessionId, exportPath); - } catch (error) { - log.error('Failed to export session', { sessionId, exportPath, error }); - throw new Error(`Failed to export session: ${error}`); - } - } - - - async importSession(importPath: string): Promise { - try { - return await contextAPI.importSession(importPath); - } catch (error) { - log.error('Failed to import session', { importPath, error }); - throw new Error(`Failed to import session: ${error}`); - } - } - - - async getStorageStats(): Promise { - try { - return await contextAPI.getStorageStats(); - } catch (error) { - log.error('Failed to get storage stats', error); - throw new Error(`Failed to get storage stats: ${error}`); - } - } -} - - -export const contextManager = new ContextManager(); - diff --git a/src/web-ui/src/infrastructure/services/infra/index.ts b/src/web-ui/src/infrastructure/services/infra/index.ts deleted file mode 100644 index 592d8da905..0000000000 --- a/src/web-ui/src/infrastructure/services/infra/index.ts +++ /dev/null @@ -1,4 +0,0 @@ - - -export { default as contextManager } from './contextManager'; - diff --git a/src/web-ui/src/shared/announcement-system/services/AnnouncementService.ts b/src/web-ui/src/shared/announcement-system/services/AnnouncementService.ts index 4ca4a266c3..08e61b5a55 100644 --- a/src/web-ui/src/shared/announcement-system/services/AnnouncementService.ts +++ b/src/web-ui/src/shared/announcement-system/services/AnnouncementService.ts @@ -4,7 +4,7 @@ * Wraps all Tauri `invoke` calls for the announcement system so that the * rest of the frontend never touches `invoke` directly. */ -import { invoke } from '@tauri-apps/api/core'; +import { api } from '@/infrastructure/api/service-api/ApiClient'; import type { AnnouncementCard } from '../types'; import { createLogger } from '@/shared/utils/logger'; @@ -18,7 +18,7 @@ export const announcementService = { */ async getPendingAnnouncements(): Promise { try { - return await invoke('get_pending_announcements'); + return await api.invoke('get_pending_announcements'); } catch (e) { log.error('Failed to get pending announcements', e); return []; @@ -28,7 +28,7 @@ export const announcementService = { /** Mark a card as seen (modal was opened or action button was clicked). */ async markSeen(id: string): Promise { try { - await invoke('mark_announcement_seen', { request: { id } }); + await api.invoke('mark_announcement_seen', { request: { id } }); } catch (e) { log.error('Failed to mark announcement seen', { id, error: e }); } @@ -37,7 +37,7 @@ export const announcementService = { /** Dismiss a card for the current version cycle. */ async dismiss(id: string): Promise { try { - await invoke('dismiss_announcement', { request: { id } }); + await api.invoke('dismiss_announcement', { request: { id } }); } catch (e) { log.error('Failed to dismiss announcement', { id, error: e }); } @@ -46,7 +46,7 @@ export const announcementService = { /** Permanently suppress a card. */ async neverShow(id: string): Promise { try { - await invoke('never_show_announcement', { request: { id } }); + await api.invoke('never_show_announcement', { request: { id } }); } catch (e) { log.error('Failed to suppress announcement', { id, error: e }); } @@ -58,7 +58,7 @@ export const announcementService = { */ async triggerCard(id: string): Promise { try { - return await invoke('trigger_announcement', { request: { id } }); + return await api.invoke('trigger_announcement', { request: { id } }); } catch (e) { log.error('Failed to trigger announcement', { id, error: e }); return null; @@ -68,7 +68,7 @@ export const announcementService = { /** Fetch all currently eligible tip cards (for a tips browser). */ async getTips(): Promise { try { - return await invoke('get_announcement_tips'); + return await api.invoke('get_announcement_tips'); } catch (e) { log.error('Failed to get announcement tips', e); return []; diff --git a/src/web-ui/src/shared/context-menu-system/examples/FileTreeIntegrationExample.tsx b/src/web-ui/src/shared/context-menu-system/examples/FileTreeIntegrationExample.tsx deleted file mode 100644 index bd8d2f0ad8..0000000000 --- a/src/web-ui/src/shared/context-menu-system/examples/FileTreeIntegrationExample.tsx +++ /dev/null @@ -1,117 +0,0 @@ - - -import React, { useEffect } from 'react'; -import { initContextMenuSystem } from '../init'; - - - - - - -export function initializeFileTreeContextMenu() { - initContextMenuSystem({ - registerBuiltinCommands: true, - registerBuiltinProviders: true, - debug: process.env.NODE_ENV === 'development' - }); -} - - - - - - - - - - - -import { globalEventBus } from '../../../infrastructure/event-bus'; - -export function FileTreeEventHandler() { - useEffect(() => { - - const unsubOpen = globalEventBus.on('file:open', (data: any) => { - - }); - - - const unsubNewFile = globalEventBus.on('file:new-file', (data: any) => { - - - - - }); - - - const unsubNewFolder = globalEventBus.on('file:new-folder', (data: any) => { - - }); - - - const unsubRename = globalEventBus.on('file:rename', (data: any) => { - - - - - }); - - - const unsubDelete = globalEventBus.on('file:delete', (data: any) => { - - - - - }); - - - const unsubReveal = globalEventBus.on('file:reveal', (data: any) => { - - - }); - - - const unsubTerminal = globalEventBus.on('terminal:open-at-path', (data: any) => { - - }); - - - return () => { - unsubOpen(); - unsubNewFile(); - unsubNewFolder(); - unsubRename(); - unsubDelete(); - unsubReveal(); - unsubTerminal(); - }; - }, []); - - return null; -} - - - -export function FileTreeWithContextMenuExample() { - - useEffect(() => { - initializeFileTreeContextMenu(); - }, []); - - return ( -
- - - - - -
- ); -} - - - - - - - diff --git a/src/web-ui/src/shared/context-system/core/types/FileContextImpl.tsx b/src/web-ui/src/shared/context-system/core/types/FileContextImpl.tsx index 939b96e58e..e1817b5e83 100644 --- a/src/web-ui/src/shared/context-system/core/types/FileContextImpl.tsx +++ b/src/web-ui/src/shared/context-system/core/types/FileContextImpl.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import { invoke } from '@tauri-apps/api/core'; +import { api } from '@/infrastructure/api/service-api/ApiClient'; import { FileIcon, CheckCircle } from 'lucide-react'; import type { FileContext, ValidationResult, RenderOptions } from '../../../types/context'; -import type { - ContextTransformer, - ContextValidator, - ContextCardRenderer +import type { + ContextTransformer, + ContextValidator, + ContextCardRenderer } from '../../../services/ContextRegistry'; import { i18nService } from '@/infrastructure/i18n'; @@ -42,7 +42,7 @@ export class FileContextValidator implements ContextValidator<'file'> { async validate(context: FileContext): Promise { try { - const exists = await invoke('fs_exists', { path: context.filePath }); + const exists = await api.invoke('fs_exists', { path: context.filePath }); if (!exists) { return { diff --git a/src/web-ui/src/shared/context-system/core/types/ImageContextImpl.tsx b/src/web-ui/src/shared/context-system/core/types/ImageContextImpl.tsx index d48dff6b9e..ad8a5f2b50 100644 --- a/src/web-ui/src/shared/context-system/core/types/ImageContextImpl.tsx +++ b/src/web-ui/src/shared/context-system/core/types/ImageContextImpl.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { invoke } from '@tauri-apps/api/core'; +import { api } from '@/infrastructure/api/service-api/ApiClient'; import { Image as ImageIcon, Eye } from 'lucide-react'; import { Modal, Button } from '@/component-library'; import type { ImageContext, ValidationResult, RenderOptions } from '../../../types/context'; @@ -86,7 +86,7 @@ export class ImageContextValidator implements ContextValidator<'image'> { if (context.isLocal && context.imagePath) { try { - const exists = await invoke('check_path_exists', { + const exists = await api.invoke('check_path_exists', { request: { path: context.imagePath } diff --git a/src/web-ui/src/shared/crypto/e2e-encryption.ts b/src/web-ui/src/shared/crypto/e2e-encryption.ts deleted file mode 100644 index 6e5fad1328..0000000000 --- a/src/web-ui/src/shared/crypto/e2e-encryption.ts +++ /dev/null @@ -1,184 +0,0 @@ -/** - * End-to-end encryption for Remote Connect using Web Crypto API. - * - * Key exchange: X25519 ECDH (Chrome 113+, Safari 17+). - * Symmetric encryption: AES-256-GCM. - * - * For older browsers that lack X25519 support in Web Crypto, this module - * falls back to the @noble/curves library (must be installed separately). - */ - -const ALGO_AES = 'AES-GCM'; -const KEY_LENGTH = 256; -const NONCE_LENGTH = 12; - -// X25519 is available in Web Crypto starting Chrome 113 / Safari 17. -// We detect support at runtime and fall back to @noble/curves if needed. - -let _useNobleFallback: boolean | null = null; - -async function supportsWebCryptoX25519(): Promise { - if (_useNobleFallback !== null) return !_useNobleFallback; - try { - await crypto.subtle.generateKey( - { name: 'X25519' } as any, - true, - ['deriveKey'], - ); - _useNobleFallback = false; - return true; - } catch { - _useNobleFallback = true; - return false; - } -} - -// ── Key types ────────────────────────────────────────────────────── - -export interface E2EKeyPair { - publicKey: Uint8Array; - /** Opaque handle — either a CryptoKeyPair or noble private key bytes. */ - _internal: any; -} - -// ── Key generation ───────────────────────────────────────────────── - -export async function generateKeyPair(): Promise { - if (await supportsWebCryptoX25519()) { - return generateKeyPairWebCrypto(); - } - return generateKeyPairNoble(); -} - -async function generateKeyPairWebCrypto(): Promise { - const keyPair = await crypto.subtle.generateKey( - { name: 'X25519' } as any, - true, - ['deriveKey'], - ); - const rawPub = await crypto.subtle.exportKey('raw', (keyPair as any).publicKey); - return { - publicKey: new Uint8Array(rawPub), - _internal: keyPair, - }; -} - -async function generateKeyPairNoble(): Promise { - const { x25519 } = await import('@noble/curves/ed25519'); - const privateKey = crypto.getRandomValues(new Uint8Array(32)); - const publicKey = x25519.getPublicKey(privateKey); - return { - publicKey, - _internal: privateKey, - }; -} - -// ── Shared secret derivation ─────────────────────────────────────── - -export async function deriveSharedSecret( - keyPair: E2EKeyPair, - peerPublicKey: Uint8Array, -): Promise { - if (await supportsWebCryptoX25519()) { - return deriveSharedSecretWebCrypto(keyPair, peerPublicKey); - } - return deriveSharedSecretNoble(keyPair, peerPublicKey); -} - -async function deriveSharedSecretWebCrypto( - keyPair: E2EKeyPair, - peerPublicKey: Uint8Array, -): Promise { - const peerKey = await crypto.subtle.importKey( - 'raw', - peerPublicKey, - { name: 'X25519' } as any, - true, - [], - ); - return crypto.subtle.deriveKey( - { name: 'X25519', public: peerKey } as any, - (keyPair._internal as CryptoKeyPair).privateKey, - { name: ALGO_AES, length: KEY_LENGTH }, - false, - ['encrypt', 'decrypt'], - ); -} - -async function deriveSharedSecretNoble( - keyPair: E2EKeyPair, - peerPublicKey: Uint8Array, -): Promise { - const { x25519 } = await import('@noble/curves/ed25519'); - const sharedBytes = x25519.getSharedSecret(keyPair._internal as Uint8Array, peerPublicKey); - return crypto.subtle.importKey( - 'raw', - sharedBytes, - { name: ALGO_AES, length: KEY_LENGTH }, - false, - ['encrypt', 'decrypt'], - ); -} - -// ── Encrypt / Decrypt ────────────────────────────────────────────── - -export async function encrypt( - sharedKey: CryptoKey, - plaintext: string, -): Promise<{ data: string; nonce: string }> { - const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH)); - const encoded = new TextEncoder().encode(plaintext); - const ciphertext = await crypto.subtle.encrypt( - { name: ALGO_AES, iv: nonce }, - sharedKey, - encoded, - ); - return { - data: uint8ToBase64(new Uint8Array(ciphertext)), - nonce: uint8ToBase64(nonce), - }; -} - -export async function decrypt( - sharedKey: CryptoKey, - dataBase64: string, - nonceBase64: string, -): Promise { - const ciphertext = base64ToUint8(dataBase64); - const nonce = base64ToUint8(nonceBase64); - const plainBuffer = await crypto.subtle.decrypt( - { name: ALGO_AES, iv: nonce }, - sharedKey, - ciphertext, - ); - return new TextDecoder().decode(plainBuffer); -} - -// ── Public key encoding helpers ──────────────────────────────────── - -export function publicKeyToBase64(key: Uint8Array): string { - return uint8ToBase64(key); -} - -export function base64ToPublicKey(b64: string): Uint8Array { - return base64ToUint8(b64); -} - -// ── Base64 utilities ─────────────────────────────────────────────── - -function uint8ToBase64(bytes: Uint8Array): string { - let binary = ''; - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); -} - -function base64ToUint8(b64: string): Uint8Array { - const binary = atob(b64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - return bytes; -} diff --git a/src/web-ui/src/shared/crypto/index.ts b/src/web-ui/src/shared/crypto/index.ts deleted file mode 100644 index 59632091c9..0000000000 --- a/src/web-ui/src/shared/crypto/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { - generateKeyPair, - deriveSharedSecret, - encrypt, - decrypt, - publicKeyToBase64, - base64ToPublicKey, -} from './e2e-encryption'; -export type { E2EKeyPair } from './e2e-encryption'; diff --git a/src/web-ui/src/shared/services/agent-service.ts b/src/web-ui/src/shared/services/agent-service.ts deleted file mode 100644 index 6930ae80b4..0000000000 --- a/src/web-ui/src/shared/services/agent-service.ts +++ /dev/null @@ -1,632 +0,0 @@ -/** - * Agent service (frontend). - * - * Wraps agent/tool APIs and bridges backend streaming events into a convenient - * client-side interface. - */ -import { agentAPI } from '@/infrastructure/api/service-api/AgentAPI'; -import { toolAPI } from '@/infrastructure/api/service-api/ToolAPI'; -import { listen } from '@tauri-apps/api/event'; -import { createLogger } from '@/shared/utils/logger'; - -const log = createLogger('AgentService'); -const hasTauriRuntime = (): boolean => - typeof window !== 'undefined' && - ('__TAURI_INTERNALS__' in window || '__TAURI__' in window); -import type { - AgentExecutionRequest, - AgentExecutionResponse, - AgentInfo, - ToolInfo, - ToolExecutionRequest, - ToolExecutionResponse, - ToolValidationRequest, - ToolValidationResponse, - AgentTaskUpdateEvent, - StreamChunkEvent, - StreamToolUseEvent, - StreamToolResultEvent, - StreamProgressEvent, - StreamStartEvent, - StreamCompleteEvent, - StreamErrorEvent, - ToolCallConfirmationEvent, -} from '../types/agent-api'; - -export class AgentService { - private static instance: AgentService; - private taskListeners = new Map void>(); - private streamListeners = new Map void; - onToolUse?: (event: StreamToolUseEvent) => void; - onToolResult?: (event: StreamToolResultEvent) => void; - onProgress?: (event: StreamProgressEvent) => void; - onComplete?: (event: StreamCompleteEvent) => void; - onError?: (event: StreamErrorEvent) => void; - onModelRoundStart?: (event: any) => void; - onToolConfirmation?: (event: ToolCallConfirmationEvent) => void; - }>(); - private unlistenFunctions: Array<() => void> = []; - - private constructor() { - void this.setupEventListeners().catch(error => { - log.warn('Failed to setup event listeners during startup', error); - }); - - - if (import.meta.hot) { - import.meta.hot.dispose(() => { - this.cleanup(); - }); - } - } - - static getInstance(): AgentService { - if (!AgentService.instance) { - AgentService.instance = new AgentService(); - } - return AgentService.instance; - } - - - private cleanup(): void { - this.unlistenFunctions.forEach(unlisten => { - try { - unlisten(); - } catch (e) { - log.warn('Failed to cleanup listener', e); - } - }); - this.unlistenFunctions = []; - this.taskListeners.clear(); - this.streamListeners.clear(); - } - - private async setupEventListeners() { - if (!hasTauriRuntime()) { - log.warn('Tauri runtime not available, skipping agent event listeners'); - return; - } - - const unlisten1 = await listen('agent_task_update', (event) => { - const taskEvent = event.payload; - const listener = this.taskListeners.get(taskEvent.task_id); - if (listener) { - listener(taskEvent); - } - }); - this.unlistenFunctions.push(unlisten1); - - - const unlisten2 = await listen('agentic_stream_start', () => { - // Stream started event handled - }); - this.unlistenFunctions.push(unlisten2); - - - const unlisten3 = await listen('model_round_start', (event) => { - const startEvent = event.payload; - - if (startEvent.task_id) { - const listener = this.streamListeners.get(startEvent.task_id); - if (listener && listener.onModelRoundStart) { - listener.onModelRoundStart(startEvent); - } - } - }); - this.unlistenFunctions.push(unlisten3); - - - const unlisten4 = await listen('tool_execution_event', (event) => { - const toolEvent = event.payload; - - - if (toolEvent.tool_name === 'TodoWrite' && toolEvent.type === 'tool_start') { - - Promise.all([ - import('@/flow_chat/services/FlowChatManager'), - import('@/flow_chat/state-machine') - ]).then(([{ FlowChatManager }, { stateMachineManager }]) => { - const todos = toolEvent.input?.todos || []; - const merge = toolEvent.input?.merge || false; - - - const flowChatManager = FlowChatManager.getInstance(); - const sessionId = flowChatManager.getSessionIdByTaskId(toolEvent.task_id); - - if (sessionId) { - const machine = stateMachineManager.get(sessionId); - if (machine) { - const context = machine.getContext(); - - - if (merge && context.planner) { - - const existingTodos = context.planner.todos; - const todoMap = new Map(existingTodos.map(t => [t.id, t])); - todos.forEach((todo: any) => { - todoMap.set(todo.id, todo); - }); - context.planner.todos = Array.from(todoMap.values()); - } else { - - context.planner = { - todos, - isActive: true - }; - } - } - } - }).catch(err => { - log.error('Failed to update state machine Planner', err); - }); - } - - if (toolEvent.task_id) { - const listener = this.streamListeners.get(toolEvent.task_id); - - if (listener) { - - if (toolEvent.type === 'tool_preparing' && listener.onToolUse) { - listener.onToolUse({ - task_id: toolEvent.task_id, - tool_use_id: toolEvent.tool_use_id, - tool_name: toolEvent.tool_name, - input: { _early_detection: true }, - model_round_id: toolEvent.model_round_id, - dialog_turn_id: toolEvent.dialog_turn_id, - timestamp: toolEvent.timestamp || Date.now(), - ai_intent: undefined, - requires_confirmation: false, - _is_early_detection: true - } as any); - } else if (toolEvent.type === 'tool_start' && listener.onToolUse) { - listener.onToolUse({ - task_id: toolEvent.task_id, - tool_use_id: toolEvent.tool_use_id, - tool_name: toolEvent.tool_name, - input: toolEvent.input, - model_round_id: toolEvent.model_round_id, - dialog_turn_id: toolEvent.dialog_turn_id, - timestamp: toolEvent.timestamp || Date.now(), - ai_intent: toolEvent.ai_intent, - requires_confirmation: toolEvent.requires_confirmation - } as any); - } else if (toolEvent.type === 'tool_complete' && listener.onToolResult) { - const resultEvent = { - task_id: toolEvent.task_id, - type: 'tool_result' as const, - content: toolEvent.result?.content || '', - timestamp: toolEvent.timestamp || Date.now(), - tool: toolEvent.tool_name, - tool_name: toolEvent.tool_name, - tool_use_id: toolEvent.tool_use_id, - result: { - content: toolEvent.result?.content || '', - data: toolEvent.result?.data, - type: toolEvent.success ? 'result' : 'error', - success: toolEvent.success, - error: toolEvent.error, - duration_ms: toolEvent.duration_ms - } - }; - - listener.onToolResult(resultEvent as any); - } - } - - } - }); - this.unlistenFunctions.push(unlisten4); - - - const unlisten5 = await listen('model_round_content', (event) => { - const contentEvent = event.payload; - - if (contentEvent.task_id) { - const listener = this.streamListeners.get(contentEvent.task_id); - - if (listener) { - - if (contentEvent.content_type === 'text' && contentEvent.content && listener.onChunk) { - listener.onChunk({ - task_id: contentEvent.task_id, - type: 'text' as const, - content: contentEvent.content, - model_round_id: contentEvent.model_round_id, - dialog_turn_id: contentEvent.dialog_turn_id, - timestamp: Date.now() - }); - } else if (contentEvent.content_type === 'thinking' && contentEvent.content && listener.onChunk) { - - listener.onChunk({ - task_id: contentEvent.task_id, - type: 'thinking' as const, - content: contentEvent.content, - model_round_id: contentEvent.model_round_id, - dialog_turn_id: contentEvent.dialog_turn_id, - timestamp: Date.now() - }); - } - } - - } - }); - this.unlistenFunctions.push(unlisten5); - - const unlisten6 = await listen('agentic_stream_chunk', (event) => { - const chunkEvent = event.payload; - const listener = this.streamListeners.get(chunkEvent.task_id); - if (listener?.onChunk) { - listener.onChunk(chunkEvent); - } - }); - this.unlistenFunctions.push(unlisten6); - - const unlisten7 = await listen('agentic_stream_tool_use', (event) => { - const toolUseEvent = event.payload; - const listener = this.streamListeners.get(toolUseEvent.task_id); - if (listener?.onToolUse) { - - listener.onToolUse(toolUseEvent); - } - }); - this.unlistenFunctions.push(unlisten7); - - const unlisten8 = await listen('agentic_stream_tool_result', (event) => { - const toolResultEvent = event.payload; - const listener = this.streamListeners.get(toolResultEvent.task_id); - if (listener?.onToolResult) { - listener.onToolResult(toolResultEvent); - } - }); - this.unlistenFunctions.push(unlisten8); - - const unlisten9 = await listen('agentic_stream_progress', (event) => { - const progressEvent = event.payload; - const listener = this.streamListeners.get(progressEvent.task_id); - if (listener?.onProgress) { - listener.onProgress(progressEvent); - } - }); - this.unlistenFunctions.push(unlisten9); - - const unlisten10 = await listen('agentic_stream_complete', (event) => { - const completeEvent = event.payload; - - - const listener = this.streamListeners.get(completeEvent.task_id); - if (listener?.onComplete) { - listener.onComplete(completeEvent); - } - - - - - - this.streamListeners.delete(completeEvent.task_id); - }); - this.unlistenFunctions.push(unlisten10); - - const unlisten11 = await listen('agentic_stream_error', (event) => { - const errorEvent = event.payload; - const listener = this.streamListeners.get(errorEvent.task_id); - if (listener?.onError) { - listener.onError(errorEvent); - } - - this.streamListeners.delete(errorEvent.task_id); - }); - this.unlistenFunctions.push(unlisten11); - - - const unlisten12 = await listen('backend-event-toolcallconfirmation', (event) => { - const confirmationEvent = event.payload; - - - - for (const listener of this.streamListeners.values()) { - if (listener?.onToolConfirmation) { - listener.onToolConfirmation(confirmationEvent); - break; - } - } - }); - this.unlistenFunctions.push(unlisten12); - } - - - - - async getAvailableAgents(): Promise { - - return ['general-purpose']; - } - - - async getActiveAgentConfigs(): Promise { - const agentTypes = await agentAPI.getAvailableTools(); - - return agentTypes.map(type => ({ - id: type, - name: type, - type: type, - description: `${type} agent`, - version: '1.0.0', - status: 'active' as const, - agent_type: type, - when_to_use: `Use ${type} agent for specialized tasks`, - tools: 'all', - location: 'builtin' - })); - } - - - async getAgentInfo(agentType: string): Promise { - return agentAPI.getAgentInfo(agentType); - } - - - async startAgentTaskStream( - request: AgentExecutionRequest, - onUpdate: (event: AgentTaskUpdateEvent) => void - ): Promise { - - const taskId = await this.executeAgentTaskStream(request, {}); - - - this.taskListeners.set(taskId, onUpdate); - - return taskId; - } - - - async executeAgentTaskStream( - request: AgentExecutionRequest, - callbacks: { - onChunk?: (event: StreamChunkEvent) => void; - onToolUse?: (event: StreamToolUseEvent) => void; - onToolResult?: (event: StreamToolResultEvent) => void; - onProgress?: (event: StreamProgressEvent) => void; - onComplete?: (event: StreamCompleteEvent) => void; - onError?: (event: StreamErrorEvent) => void; - onModelRoundStart?: (event: any) => void; - onToolConfirmation?: (event: ToolCallConfirmationEvent) => void; - } - ): Promise { - - - let sessionId: string; - try { - const workspacePath = request.workspace_path; - if (!workspacePath) { - throw new Error('Workspace path is required to create an agent task session'); - } - - const response = await agentAPI.createSession({ - sessionName: `task-${Date.now()}`, - agentType: request.agent_type, - workspacePath, - config: { - modelName: request.model_name, - enableTools: true, - safeMode: true, - } - }); - sessionId = response.sessionId; - } catch (error) { - log.error('Failed to create session', error); - throw error; - } - - - const existingListener = this.streamListeners.get(sessionId); - if (existingListener) { - log.warn('Session ID already has listener, will override', { sessionId }); - } - - - this.streamListeners.set(sessionId, callbacks); - - - try { - const workspacePath = request.workspace_path; - if (!workspacePath) { - throw new Error('Workspace path is required to start an agent task'); - } - - await agentAPI.startDialogTurn({ - sessionId, - userInput: request.prompt, - agentType: request.agent_type, - workspacePath, - }); - } catch (error) { - log.error('Failed to send message', error); - throw error; - } - - return sessionId; - } - - - async cancelAgentTask(taskId: string): Promise { - - await agentAPI.cancelSession(taskId); - const result = true; - - - this.taskListeners.delete(taskId); - - return result; - } - - - cleanupTaskListener(taskId: string) { - this.taskListeners.delete(taskId); - } - - - - - async getAllToolsInfo(): Promise { - return toolAPI.getAllToolsInfo(); - } - - - async getReadonlyToolsInfo(): Promise { - - const allTools = await toolAPI.getAllToolsInfo(); - return allTools.filter((tool: any) => tool.is_readonly === true); - } - - - async getToolInfo(toolName: string): Promise { - return toolAPI.getToolInfo(toolName); - } - - - async validateToolInput(request: ToolValidationRequest): Promise { - - const validationRequest = { - toolName: (request as any).tool_name || (request as any).toolName, - input: request.input || (request as any).parameters, - workspacePath: (request as any).workspace_path || (request as any).workspacePath, - }; - return toolAPI.validateToolInput(validationRequest); - } - - - async executeTool(request: ToolExecutionRequest): Promise { - - const executeRequest = { - toolName: (request as any).tool_name || (request as any).toolName, - parameters: request.input || {}, - workspacePath: (request as any).workspace_path || (request as any).workspacePath, - }; - return toolAPI.executeTool(executeRequest); - } - - - async executeTask( - description: string, - prompt: string, - agentType: string = 'general-purpose', - options: { - modelName?: string; - workspacePath?: string; - context?: Record; - safeMode?: boolean; - verbose?: boolean; - } = {} - ): Promise { - const request: AgentExecutionRequest = { - agent_type: agentType, - prompt, - description, - model_name: options.modelName, - workspace_path: options.workspacePath, - context: options.context, - safe_mode: options.safeMode, - verbose: options.verbose, - }; - - return this.executeAgentTask(request); - } - - async executeAgentTask(request: AgentExecutionRequest): Promise { - const sessionId = await this.executeAgentTaskStream(request, {}); - return { - id: sessionId, - status: 'started', - agent_type: request.agent_type, - }; - } - - - async executeTaskStream( - description: string, - prompt: string, - onUpdate: (event: AgentTaskUpdateEvent) => void, - agentType: string = 'general-purpose', - options: { - modelName?: string; - workspacePath?: string; - context?: Record; - safeMode?: boolean; - verbose?: boolean; - } = {} - ): Promise { - const request: AgentExecutionRequest = { - agent_type: agentType, - prompt, - description, - model_name: options.modelName, - workspace_path: options.workspacePath, - context: options.context, - safe_mode: options.safeMode, - verbose: options.verbose, - }; - - return this.startAgentTaskStream(request, onUpdate); - } - - - async executeTaskStreamNew( - description: string, - prompt: string, - callbacks: { - onChunk?: (text: string) => void; - onToolUse?: (toolName: string, input: any) => void; - onToolResult?: (content: string) => void; - onProgress?: () => void; - onComplete?: (result?: any) => void; - onError?: (error: string) => void; - }, - agentType: string = 'general-purpose', - options: { - modelName?: string; - workspacePath?: string; - context?: Record; - safeMode?: boolean; - verbose?: boolean; - } = {} - ): Promise { - const request: AgentExecutionRequest = { - agent_type: agentType, - prompt, - description, - model_name: options.modelName, - workspace_path: options.workspacePath, - context: options.context, - safe_mode: options.safeMode, - verbose: options.verbose, - }; - - return this.executeAgentTaskStream(request, { - onChunk: callbacks.onChunk ? (event) => callbacks.onChunk!(event.content) : undefined, - onToolUse: callbacks.onToolUse ? (event) => callbacks.onToolUse!(event.tool_name, event.input) : undefined, - onToolResult: callbacks.onToolResult ? (event) => callbacks.onToolResult!(event.content) : undefined, - onProgress: callbacks.onProgress, - onComplete: callbacks.onComplete ? (event) => callbacks.onComplete!(event.result) : undefined, - onError: callbacks.onError ? (event) => callbacks.onError!(event.error) : undefined, - }); - } - - - async isAgentAvailable(agentType: string): Promise { - const availableAgents = await this.getAvailableAgents(); - return availableAgents.includes(agentType); - } - - - async getRecommendedAgent(_taskDescription: string): Promise { - - return 'general-purpose'; - } - - -} - - -export const agentService = AgentService.getInstance(); diff --git a/src/web-ui/src/shared/services/ide-control/IdeControlEventBus.ts b/src/web-ui/src/shared/services/ide-control/IdeControlEventBus.ts index 19807ce15f..c2dcdbe149 100644 --- a/src/web-ui/src/shared/services/ide-control/IdeControlEventBus.ts +++ b/src/web-ui/src/shared/services/ide-control/IdeControlEventBus.ts @@ -4,6 +4,7 @@ * Listens to backend IDE control events and dispatches them to registered controllers. */ import { listen, UnlistenFn } from '@tauri-apps/api/event'; +import { api } from '@/infrastructure/api/service-api/ApiClient'; import { IdeControlEvent, IdeController, IdeControlOperation } from './types'; import { PanelController } from './PanelController'; import { createLogger } from '@/shared/utils/logger'; @@ -108,8 +109,7 @@ export class IdeControlEventBus { private async sendErrorResult(requestId: string, error: any): Promise { try { - const { invoke } = await import('@tauri-apps/api/core'); - await invoke('report_ide_control_result', { + await api.invoke('report_ide_control_result', { request_id: requestId, success: false, message: undefined,