diff --git a/src/api/copilot/auth.ts b/src/api/copilot/auth/auth.ts similarity index 100% rename from src/api/copilot/auth.ts rename to src/api/copilot/auth/auth.ts diff --git a/src/api/copilot/auth/index.ts b/src/api/copilot/auth/index.ts new file mode 100644 index 0000000..3a906b4 --- /dev/null +++ b/src/api/copilot/auth/index.ts @@ -0,0 +1,6 @@ +/** + * Copilot API Auth - Token and Authentication + */ + +export * from "./token"; +export * from "./auth"; diff --git a/src/api/copilot/token.ts b/src/api/copilot/auth/token.ts similarity index 100% rename from src/api/copilot/token.ts rename to src/api/copilot/auth/token.ts diff --git a/src/api/copilot/chat.ts b/src/api/copilot/core/chat.ts similarity index 98% rename from src/api/copilot/chat.ts rename to src/api/copilot/core/chat.ts index 78e8480..aab6275 100644 --- a/src/api/copilot/chat.ts +++ b/src/api/copilot/core/chat.ts @@ -12,7 +12,7 @@ import type { ChatCompletionResponse, StreamChunk, } from "@/types/providers"; -import { buildCopilotHeaders } from "@api/copilot/token"; +import { buildCopilotHeaders } from "@api/copilot/auth/token"; interface FormattedMessage { role: string; diff --git a/src/api/copilot/core/index.ts b/src/api/copilot/core/index.ts new file mode 100644 index 0000000..bb75937 --- /dev/null +++ b/src/api/copilot/core/index.ts @@ -0,0 +1,6 @@ +/** + * Copilot API Core - Chat and Models API + */ + +export * from "./chat"; +export * from "./models"; diff --git a/src/api/copilot/models.ts b/src/api/copilot/core/models.ts similarity index 100% rename from src/api/copilot/models.ts rename to src/api/copilot/core/models.ts diff --git a/src/api/copilot/index.ts b/src/api/copilot/index.ts index a1b55cf..ba074e3 100644 --- a/src/api/copilot/index.ts +++ b/src/api/copilot/index.ts @@ -5,18 +5,18 @@ export { fetchCopilotToken, buildCopilotHeaders, -} from "@api/copilot/token"; +} from "@api/copilot/auth/token"; export { requestDeviceCode, requestAccessToken, -} from "@api/copilot/auth"; +} from "@api/copilot/auth/auth"; -export { fetchModels } from "@api/copilot/models"; +export { fetchModels } from "@api/copilot/core/models"; export { getEndpoint, buildRequestBody, executeChatRequest, executeStreamRequest, -} from "@api/copilot/chat"; +} from "@api/copilot/core/chat"; diff --git a/src/api/ollama/chat.ts b/src/api/ollama/core/chat.ts similarity index 100% rename from src/api/ollama/chat.ts rename to src/api/ollama/core/chat.ts diff --git a/src/api/ollama/core/index.ts b/src/api/ollama/core/index.ts new file mode 100644 index 0000000..170cd49 --- /dev/null +++ b/src/api/ollama/core/index.ts @@ -0,0 +1,6 @@ +/** + * Ollama API Core - Chat and Models API + */ + +export * from "./chat"; +export * from "./models"; diff --git a/src/api/ollama/models.ts b/src/api/ollama/core/models.ts similarity index 100% rename from src/api/ollama/models.ts rename to src/api/ollama/core/models.ts diff --git a/src/api/ollama/index.ts b/src/api/ollama/index.ts index bef595f..2a6b35c 100644 --- a/src/api/ollama/index.ts +++ b/src/api/ollama/index.ts @@ -5,9 +5,9 @@ export { executeChatRequest, executeStreamRequest, -} from "@api/ollama/chat"; +} from "@api/ollama/core/chat"; export { fetchModels, checkHealth, -} from "@api/ollama/models"; +} from "@api/ollama/core/models"; diff --git a/src/commands/components/callbacks/on-tool-call.ts b/src/commands/components/callbacks/on-tool-call.ts index a5a6514..7e41ea6 100644 --- a/src/commands/components/callbacks/on-tool-call.ts +++ b/src/commands/components/callbacks/on-tool-call.ts @@ -1,5 +1,5 @@ import { appStore } from "@tui/index.ts"; -import { isQuietTool } from "@utils/tools.ts"; +import { isQuietTool } from "@utils/core/tools"; import type { ToolCallParams } from "@interfaces/ToolCallParams.ts"; export const onToolCall = (call: ToolCallParams): void => { diff --git a/src/commands/components/chat/agents/switch-agent.ts b/src/commands/components/chat/agents/switch-agent.ts index ab43a0f..85c5826 100644 --- a/src/commands/components/chat/agents/switch-agent.ts +++ b/src/commands/components/chat/agents/switch-agent.ts @@ -3,7 +3,7 @@ */ import chalk from "chalk"; -import { errorMessage, infoMessage, warningMessage } from "@utils/terminal"; +import { errorMessage, infoMessage, warningMessage } from "@utils/core/terminal"; import { agentLoader } from "@services/agent-loader"; import type { ChatState } from "@commands/components/chat/state"; diff --git a/src/commands/components/chat/commands/commandsRegistry.ts b/src/commands/components/chat/commands/commandsRegistry.ts index 286ab52..4afc5e4 100644 --- a/src/commands/components/chat/commands/commandsRegistry.ts +++ b/src/commands/components/chat/commands/commandsRegistry.ts @@ -1,4 +1,4 @@ -import { saveSession } from "@services/session"; +import { saveSession } from "@services/core/session"; import { clearConversation } from "@commands/components/chat/history/clear-conversation"; import { appStore } from "@tui/index"; import { showContextFiles } from "@commands/components/chat/context/show-context-files"; @@ -18,7 +18,7 @@ import { switchAgent } from "@commands/components/chat/agents/switch-agent"; import { handleMCP } from "@commands/components/chat/mcp/handle-mcp"; import { CommandContext } from "@interfaces/commandContext"; import type { CommandHandler } from "@/types/commandHandler"; -import { successMessage } from "@utils/terminal"; +import { successMessage } from "@utils/core/terminal"; const COMMAND_REGISTRY: Map = new Map< string, diff --git a/src/commands/components/chat/commands/handle-command.ts b/src/commands/components/chat/commands/handle-command.ts index 3d4f3fa..7cb9ef7 100644 --- a/src/commands/components/chat/commands/handle-command.ts +++ b/src/commands/components/chat/commands/handle-command.ts @@ -1,4 +1,4 @@ -import { warningMessage, infoMessage } from "@utils/terminal"; +import { warningMessage, infoMessage } from "@utils/core/terminal"; import type { ChatState } from "@commands/components/chat/state"; import COMMAND_REGISTRY from "@commands/components/chat/commands/commandsRegistry"; diff --git a/src/commands/components/chat/context/add-context-file.ts b/src/commands/components/chat/context/add-context-file.ts index 9e6c5db..33bbdc9 100644 --- a/src/commands/components/chat/context/add-context-file.ts +++ b/src/commands/components/chat/context/add-context-file.ts @@ -1,7 +1,7 @@ import { resolve } from "path"; import { existsSync } from "fs"; import fg from "fast-glob"; -import { errorMessage, warningMessage } from "@utils/terminal"; +import { errorMessage, warningMessage } from "@utils/core/terminal"; import { loadFile } from "@commands/components/chat/context/load-file"; import { IGNORE_FOLDERS } from "@constants/paths"; diff --git a/src/commands/components/chat/context/load-file.ts b/src/commands/components/chat/context/load-file.ts index 6a7da12..f5780ec 100644 --- a/src/commands/components/chat/context/load-file.ts +++ b/src/commands/components/chat/context/load-file.ts @@ -1,7 +1,7 @@ import { readFile, stat } from "fs/promises"; import { basename } from "path"; -import { warningMessage, successMessage, errorMessage } from "@utils/terminal"; -import { addContextFile } from "@services/session"; +import { warningMessage, successMessage, errorMessage } from "@utils/core/terminal"; +import { addContextFile } from "@services/core/session"; export const loadFile = async ( filePath: string, diff --git a/src/commands/components/chat/context/remove-file.ts b/src/commands/components/chat/context/remove-file.ts index e79a502..5db4899 100644 --- a/src/commands/components/chat/context/remove-file.ts +++ b/src/commands/components/chat/context/remove-file.ts @@ -1,5 +1,5 @@ import { basename } from "path"; -import { warningMessage, successMessage } from "@utils/terminal"; +import { warningMessage, successMessage } from "@utils/core/terminal"; export const removeFile = ( filename: string, diff --git a/src/commands/components/chat/context/show-context-files.ts b/src/commands/components/chat/context/show-context-files.ts index b558100..da31715 100644 --- a/src/commands/components/chat/context/show-context-files.ts +++ b/src/commands/components/chat/context/show-context-files.ts @@ -1,7 +1,7 @@ import chalk from "chalk"; import { basename } from "path"; -import { getCurrentSession } from "@services/session"; -import { infoMessage, filePath } from "@utils/terminal"; +import { getCurrentSession } from "@services/core/session"; +import { infoMessage, filePath } from "@utils/core/terminal"; export const showContextFiles = (contextFiles: Map): void => { const session = getCurrentSession(); diff --git a/src/commands/components/chat/history/clear-conversation.ts b/src/commands/components/chat/history/clear-conversation.ts index c4005e7..ed550bb 100644 --- a/src/commands/components/chat/history/clear-conversation.ts +++ b/src/commands/components/chat/history/clear-conversation.ts @@ -1,5 +1,5 @@ -import { clearMessages } from "@services/session"; -import { successMessage } from "@utils/terminal"; +import { clearMessages } from "@services/core/session"; +import { successMessage } from "@utils/core/terminal"; import type { ChatState } from "@commands/components/chat/state"; export const clearConversation = (state: ChatState): void => { diff --git a/src/commands/components/chat/history/compact-history.ts b/src/commands/components/chat/history/compact-history.ts index ebffb4f..e664e7f 100644 --- a/src/commands/components/chat/history/compact-history.ts +++ b/src/commands/components/chat/history/compact-history.ts @@ -1,4 +1,4 @@ -import { successMessage, infoMessage } from "@utils/terminal"; +import { successMessage, infoMessage } from "@utils/core/terminal"; import type { ChatState } from "@commands/components/chat/state"; export const compactHistory = (state: ChatState): void => { diff --git a/src/commands/components/chat/index.ts b/src/commands/components/chat/index.ts index a703ede..00b3573 100644 --- a/src/commands/components/chat/index.ts +++ b/src/commands/components/chat/index.ts @@ -1,13 +1,13 @@ import chalk from "chalk"; -import { infoMessage, errorMessage, warningMessage } from "@utils/terminal"; +import { infoMessage, errorMessage, warningMessage } from "@utils/core/terminal"; import { createSession, loadSession, getMostRecentSession, findSession, setWorkingDirectory, -} from "@services/session"; -import { getConfig } from "@services/config"; +} from "@services/core/session"; +import { getConfig } from "@services/core/config"; import type { Provider as ProviderName, ChatSession } from "@/types/index"; import { getProvider, getProviderStatus } from "@providers/index.ts"; import { diff --git a/src/commands/components/chat/messages/send-message.ts b/src/commands/components/chat/messages/send-message.ts index 25f979e..79458a7 100644 --- a/src/commands/components/chat/messages/send-message.ts +++ b/src/commands/components/chat/messages/send-message.ts @@ -1,9 +1,9 @@ import chalk from "chalk"; import { basename, extname } from "path"; -import { addMessage } from "@services/session"; -import { initializePermissions } from "@services/permissions"; -import { createAgent } from "@services/agent"; -import { infoMessage, errorMessage, warningMessage } from "@utils/terminal"; +import { addMessage } from "@services/core/session"; +import { initializePermissions } from "@services/core/permissions"; +import { createAgent } from "@services/core/agent"; +import { infoMessage, errorMessage, warningMessage } from "@utils/core/terminal"; import { getThinkingMessage } from "@constants/status-messages"; import { detectDebuggingRequest, diff --git a/src/commands/components/chat/models/show-providers.ts b/src/commands/components/chat/models/show-providers.ts index e1c2390..e676e20 100644 --- a/src/commands/components/chat/models/show-providers.ts +++ b/src/commands/components/chat/models/show-providers.ts @@ -1,4 +1,4 @@ -import { getConfig } from "@services/config"; +import { getConfig } from "@services/core/config"; import { displayProvidersStatus } from "@providers/index.ts"; export const showProviders = async (): Promise => { diff --git a/src/commands/components/chat/models/switch-model.ts b/src/commands/components/chat/models/switch-model.ts index 9c6f58d..fd182f1 100644 --- a/src/commands/components/chat/models/switch-model.ts +++ b/src/commands/components/chat/models/switch-model.ts @@ -3,8 +3,8 @@ import { warningMessage, successMessage, errorMessage, -} from "@utils/terminal"; -import { getConfig } from "@services/config"; +} from "@utils/core/terminal"; +import { getConfig } from "@services/core/config"; import { getProvider } from "@providers/index.ts"; import { showModels } from "./show-models.ts"; import type { ChatState } from "../state.ts"; diff --git a/src/commands/components/chat/models/switch-provider.ts b/src/commands/components/chat/models/switch-provider.ts index 7919b5f..7cb4455 100644 --- a/src/commands/components/chat/models/switch-provider.ts +++ b/src/commands/components/chat/models/switch-provider.ts @@ -4,8 +4,8 @@ import { warningMessage, infoMessage, successMessage, -} from "@utils/terminal"; -import { getConfig } from "@services/config"; +} from "@utils/core/terminal"; +import { getConfig } from "@services/core/config"; import { getProvider, getProviderStatus, diff --git a/src/commands/components/chat/print-mode.ts b/src/commands/components/chat/print-mode.ts index 09ee316..2d964ca 100644 --- a/src/commands/components/chat/print-mode.ts +++ b/src/commands/components/chat/print-mode.ts @@ -1,7 +1,7 @@ import chalk from "chalk"; import { basename, extname } from "path"; -import { initializePermissions } from "@services/permissions"; -import { createAgent } from "@services/agent"; +import { initializePermissions } from "@services/core/permissions"; +import { createAgent } from "@services/core/agent"; import type { ChatState } from "@commands/components/chat/state"; import { processFileReferences } from "@commands/components/chat/context/process-file-references"; diff --git a/src/commands/components/chat/session/list-sessions.ts b/src/commands/components/chat/session/list-sessions.ts index 988de2f..16a9390 100644 --- a/src/commands/components/chat/session/list-sessions.ts +++ b/src/commands/components/chat/session/list-sessions.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; -import { getSessionSummaries } from "@services/session"; -import { infoMessage } from "@utils/terminal"; +import { getSessionSummaries } from "@services/core/session"; +import { infoMessage } from "@utils/core/terminal"; export const listSessions = async (): Promise => { const summaries = await getSessionSummaries(); diff --git a/src/commands/components/chat/session/show-session-info.ts b/src/commands/components/chat/session/show-session-info.ts index 4228bfa..10e3c37 100644 --- a/src/commands/components/chat/session/show-session-info.ts +++ b/src/commands/components/chat/session/show-session-info.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; -import { getCurrentSession } from "@services/session"; -import { warningMessage } from "@utils/terminal"; +import { getCurrentSession } from "@services/core/session"; +import { warningMessage } from "@utils/core/terminal"; export const showSessionInfo = async (): Promise => { const session = getCurrentSession(); diff --git a/src/commands/components/chat/usage/show-usage.ts b/src/commands/components/chat/usage/show-usage.ts index a7719fd..b18e771 100644 --- a/src/commands/components/chat/usage/show-usage.ts +++ b/src/commands/components/chat/usage/show-usage.ts @@ -3,11 +3,11 @@ */ import chalk from "chalk"; -import { usageStore } from "@stores/usage-store"; -import { getUserInfo } from "@providers/copilot/credentials"; +import { usageStore } from "@stores/core/usage-store"; +import { getUserInfo } from "@providers/copilot/auth/credentials"; import { getCopilotUsage } from "@providers/copilot/usage"; import { getProvider } from "@providers/index"; -import { renderUsageBar, renderUnlimitedBar } from "@utils/progress-bar"; +import { renderUsageBar, renderUnlimitedBar } from "@utils/menu/progress-bar"; import type { ChatState } from "@commands/components/chat/state"; import type { CopilotQuotaDetail } from "@/types/copilot-usage"; diff --git a/src/commands/components/dashboard/build-config.ts b/src/commands/components/dashboard/build-config.ts index 55a2627..23900d5 100644 --- a/src/commands/components/dashboard/build-config.ts +++ b/src/commands/components/dashboard/build-config.ts @@ -3,7 +3,7 @@ */ import os from "os"; -import { getConfig } from "@services/config"; +import { getConfig } from "@services/core/config"; import { DASHBOARD_TITLE } from "@constants/dashboard"; import type { DashboardConfig } from "@/types/dashboard"; diff --git a/src/commands/components/execute/index.ts b/src/commands/components/execute/index.ts index dfdb838..866779c 100644 --- a/src/commands/components/execute/index.ts +++ b/src/commands/components/execute/index.ts @@ -13,14 +13,14 @@ import { import type { ChatServiceState } from "@services/chat-tui-service"; import type { ChatTUIOptions } from "@interfaces/ChatTUIOptions"; import type { AgentConfig } from "@/types/agent-config"; -import { getConfig } from "@services/config"; +import { getConfig } from "@services/core/config"; import { getThinkingMessage } from "@constants/status-messages"; import { enterFullscreen, registerExitHandlers, exitFullscreen, clearScreen, -} from "@utils/terminal"; +} from "@utils/core/terminal"; import { createCallbacks } from "@commands/chat-tui"; import { agentLoader } from "@services/agent-loader"; import { projectSetupService } from "@services/project-setup-service"; diff --git a/src/commands/handlers.ts b/src/commands/core/handlers.ts similarity index 91% rename from src/commands/handlers.ts rename to src/commands/core/handlers.ts index b5e0f87..8429c2c 100644 --- a/src/commands/handlers.ts +++ b/src/commands/core/handlers.ts @@ -2,7 +2,7 @@ * Command handlers - Route commands to appropriate implementations */ -import { errorMessage } from "@utils/terminal"; +import { errorMessage } from "@utils/core/terminal"; import { COMMAND_REGISTRY, isValidCommand } from "@commands/handlers/registry"; import type { CommandOptions } from "@/types/index"; diff --git a/src/commands/core/index.ts b/src/commands/core/index.ts new file mode 100644 index 0000000..231b127 --- /dev/null +++ b/src/commands/core/index.ts @@ -0,0 +1,6 @@ +/** + * Commands Core - Main command handling + */ + +export * from "./runner"; +export * from "./handlers"; diff --git a/src/commands/runner.ts b/src/commands/core/runner.ts similarity index 100% rename from src/commands/runner.ts rename to src/commands/core/runner.ts diff --git a/src/commands/handlers/classify.ts b/src/commands/handlers/classify.ts index 64c0413..425fc99 100644 --- a/src/commands/handlers/classify.ts +++ b/src/commands/handlers/classify.ts @@ -9,7 +9,7 @@ import { errorMessage, failSpinner, headerMessage, -} from "@utils/terminal"; +} from "@utils/core/terminal"; import { INTENT_KEYWORDS, CLASSIFICATION_CONFIDENCE, diff --git a/src/commands/handlers/config.ts b/src/commands/handlers/config.ts index 27076b3..f4dff3f 100644 --- a/src/commands/handlers/config.ts +++ b/src/commands/handlers/config.ts @@ -9,8 +9,8 @@ import { hightLigthedJson, headerMessage, infoMessage, -} from "@utils/terminal"; -import { getConfig } from "@services/config"; +} from "@utils/core/terminal"; +import { getConfig } from "@services/core/config"; import { VALID_CONFIG_KEYS, VALID_PROVIDERS, diff --git a/src/commands/handlers/plan.ts b/src/commands/handlers/plan.ts index b43cbfa..962b0ce 100644 --- a/src/commands/handlers/plan.ts +++ b/src/commands/handlers/plan.ts @@ -12,7 +12,7 @@ import { startSpinner, succeedSpinner, successMessage, -} from "@utils/terminal"; +} from "@utils/core/terminal"; import type { CommandOptions } from "@/types/index"; export const handlePlan = async (options: CommandOptions): Promise => { diff --git a/src/commands/handlers/run.ts b/src/commands/handlers/run.ts index 798dd4f..09cc5d2 100644 --- a/src/commands/handlers/run.ts +++ b/src/commands/handlers/run.ts @@ -2,7 +2,7 @@ * Run command handler */ -import { execute } from "@commands/runner"; +import { execute } from "@commands/core/runner"; import type { CommandOptions } from "@/types/index"; export const handleRun = async (options: CommandOptions): Promise => { diff --git a/src/commands/handlers/serve.ts b/src/commands/handlers/serve.ts index f5007b3..35286bd 100644 --- a/src/commands/handlers/serve.ts +++ b/src/commands/handlers/serve.ts @@ -2,7 +2,7 @@ * Serve command handler */ -import { boxMessage, warningMessage, infoMessage } from "@utils/terminal"; +import { boxMessage, warningMessage, infoMessage } from "@utils/core/terminal"; import type { CommandOptions } from "@/types/index"; import { SERVER_INFO } from "@constants/serve"; diff --git a/src/commands/handlers/validate.ts b/src/commands/handlers/validate.ts index c4f873d..b530e97 100644 --- a/src/commands/handlers/validate.ts +++ b/src/commands/handlers/validate.ts @@ -12,8 +12,8 @@ import { errorMessage, headerMessage, filePath, -} from "@utils/terminal"; -import { getConfig } from "@services/config"; +} from "@utils/core/terminal"; +import { getConfig } from "@services/core/config"; import type { CommandOptions } from "@/types/index"; export const handleValidate = async ( diff --git a/src/commands/mcp.ts b/src/commands/mcp.ts index 994b735..6b807c2 100644 --- a/src/commands/mcp.ts +++ b/src/commands/mcp.ts @@ -12,7 +12,7 @@ */ import chalk from "chalk"; -import { errorMessage, infoMessage, successMessage } from "@utils/terminal"; +import { errorMessage, infoMessage, successMessage } from "@utils/core/terminal"; import { initializeMCP, getMCPConfig, diff --git a/src/commands/runner/display-header.ts b/src/commands/runner/display-header.ts index 19668f4..eb8fdd9 100644 --- a/src/commands/runner/display-header.ts +++ b/src/commands/runner/display-header.ts @@ -3,7 +3,7 @@ */ import chalk from "chalk"; -import { headerMessage, filePath } from "@utils/terminal"; +import { headerMessage, filePath } from "@utils/core/terminal"; import type { RunnerOptions } from "@/types/runner"; export const displayHeader = (options: RunnerOptions): void => { diff --git a/src/commands/runner/display-plan.ts b/src/commands/runner/display-plan.ts index b5e9ae5..e768812 100644 --- a/src/commands/runner/display-plan.ts +++ b/src/commands/runner/display-plan.ts @@ -3,7 +3,7 @@ */ import chalk from "chalk"; -import { filePath } from "@utils/terminal"; +import { filePath } from "@utils/core/terminal"; import { STEP_ICONS, DEFAULT_STEP_ICON } from "@constants/runner"; import type { ExecutionPlan, PlanStep } from "@/types/index"; diff --git a/src/commands/runner/execute-plan.ts b/src/commands/runner/execute-plan.ts index a0b03bb..e4663fa 100644 --- a/src/commands/runner/execute-plan.ts +++ b/src/commands/runner/execute-plan.ts @@ -2,7 +2,7 @@ * Plan execution utilities */ -import { failSpinner, succeedSpinner, startSpinner } from "@utils/terminal"; +import { failSpinner, succeedSpinner, startSpinner } from "@utils/core/terminal"; import { RUNNER_DELAYS } from "@constants/runner"; import { getStepIcon } from "@commands/runner/display-plan"; import { delay } from "@commands/runner/utils"; diff --git a/src/commands/runner/execute.ts b/src/commands/runner/execute.ts index 037ad38..1cfb652 100644 --- a/src/commands/runner/execute.ts +++ b/src/commands/runner/execute.ts @@ -12,7 +12,7 @@ import { infoMessage, errorMessage, warningMessage, -} from "@utils/terminal"; +} from "@utils/core/terminal"; import { RUNNER_DELAYS, RUNNER_MESSAGES } from "@constants/runner"; import { displayHeader } from "@commands/runner/display-header"; import { displayPlan } from "@commands/runner/display-plan"; diff --git a/src/constants/multi-agent.ts b/src/constants/multi-agent.ts new file mode 100644 index 0000000..85a8033 --- /dev/null +++ b/src/constants/multi-agent.ts @@ -0,0 +1,112 @@ +/** + * Multi-Agent Constants + * + * Configuration defaults, messages, and descriptions for multi-agent execution. + */ + +import type { AgentExecutionMode, ConflictStrategy } from "@/types/multi-agent"; + +/** + * Default configuration values + */ +export const MULTI_AGENT_DEFAULTS = { + maxConcurrent: 3, + timeout: 300000, // 5 minutes + executionMode: "adaptive" as AgentExecutionMode, + conflictStrategy: "serialize" as ConflictStrategy, + abortOnFirstError: false, + conflictCheckInterval: 1000, // 1 second + maxRetries: 2, +} as const; + +/** + * Resource limits + */ +export const MULTI_AGENT_LIMITS = { + maxAgentsPerRequest: 10, + maxConcurrentRequests: 3, + maxQueuedAgents: 20, + maxConflictsBeforeAbort: 5, +} as const; + +/** + * Execution mode descriptions + */ +export const EXECUTION_MODE_DESCRIPTIONS: Record = { + sequential: "Execute agents one after another, safest for file modifications", + parallel: "Execute all agents concurrently, fastest but may cause conflicts", + adaptive: "Start parallel, automatically serialize when conflicts detected", +} as const; + +/** + * Conflict strategy descriptions + */ +export const CONFLICT_STRATEGY_DESCRIPTIONS: Record = { + serialize: "Wait for conflicting agent to complete before proceeding", + "abort-newer": "Abort the agent that started later when conflict detected", + "merge-results": "Attempt to merge changes from both agents", + isolated: "Each agent works in isolated context, merge at end", +} as const; + +/** + * Error messages + */ +export const MULTI_AGENT_ERRORS = { + MAX_AGENTS_EXCEEDED: (max: number) => + `Cannot spawn more than ${max} agents in a single request`, + MAX_CONCURRENT_EXCEEDED: (max: number) => + `Maximum concurrent agents (${max}) reached`, + AGENT_NOT_FOUND: (name: string) => + `Agent "${name}" not found in registry`, + AGENT_ALREADY_RUNNING: (id: string) => + `Agent "${id}" is already running`, + EXECUTION_TIMEOUT: (agentId: string, timeout: number) => + `Agent "${agentId}" timed out after ${timeout}ms`, + CONFLICT_RESOLUTION_FAILED: (filePath: string) => + `Failed to resolve conflict for file: ${filePath}`, + EXECUTION_ABORTED: "Execution aborted by user", + INVALID_EXECUTION_MODE: (mode: string) => + `Invalid execution mode: ${mode}`, + INVALID_CONFLICT_STRATEGY: (strategy: string) => + `Invalid conflict strategy: ${strategy}`, + TOO_MANY_CONFLICTS: (count: number) => + `Too many conflicts detected (${count}), aborting execution`, +} as const; + +/** + * Status messages + */ +export const MULTI_AGENT_MESSAGES = { + STARTING: "Starting multi-agent execution", + AGENT_SPAWNED: (name: string) => `Spawned agent: ${name}`, + AGENT_COMPLETED: (name: string) => `Agent completed: ${name}`, + AGENT_FAILED: (name: string, error: string) => + `Agent failed: ${name} - ${error}`, + CONFLICT_DETECTED: (file: string, agents: string[]) => + `Conflict detected on ${file} between agents: ${agents.join(", ")}`, + CONFLICT_RESOLVED: (file: string, strategy: string) => + `Conflict resolved for ${file} using ${strategy}`, + EXECUTION_COMPLETE: (success: number, failed: number) => + `Execution complete: ${success} succeeded, ${failed} failed`, + WAITING_FOR_CONFLICT: (agentId: string) => + `Agent ${agentId} waiting for conflict resolution`, +} as const; + +/** + * Agent ID generation prefix + */ +export const AGENT_ID_PREFIX = "agent_"; + +/** + * Request ID generation prefix + */ +export const REQUEST_ID_PREFIX = "req_"; + +/** + * File lock constants + */ +export const FILE_LOCK = { + acquireTimeout: 5000, // 5 seconds + retryInterval: 100, // 100ms + maxRetries: 50, +} as const; diff --git a/src/index.ts b/src/index.ts index 1cb337d..180cbef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node import { Command } from "commander"; -import { handleCommand } from "@commands/handlers"; +import { handleCommand } from "@commands/core/handlers"; import { execute } from "@commands/chat-tui"; import versionData from "@/version.json"; import { @@ -10,14 +10,14 @@ import { getProviderNames, displayProvidersStatus, } from "@providers/index"; -import { getConfig } from "@services/config"; -import { deleteSession, getSessionSummaries } from "@services/session"; +import { getConfig } from "@services/core/config"; +import { deleteSession, getSessionSummaries } from "@services/core/session"; import { initializePermissions, listPatterns, addGlobalPattern, addLocalPattern, -} from "@services/permissions"; +} from "@services/core/permissions"; import { projectConfig, initProject, @@ -32,7 +32,7 @@ import { buildLearningsContext, } from "@services/project-config"; import { createPlan, displayPlan, approvePlan } from "@services/planner"; -import { ensureXdgDirectories } from "@utils/ensure-directories"; +import { ensureXdgDirectories } from "@utils/core/ensure-directories"; import chalk from "chalk"; // Read version from version.json diff --git a/src/providers/copilot.ts b/src/providers/copilot.ts index 48e613f..421b58e 100644 --- a/src/providers/copilot.ts +++ b/src/providers/copilot.ts @@ -4,8 +4,8 @@ * Falls back to copilot.lua/copilot.vim config if available */ -import { chat, chatStream } from "@providers/copilot/chat"; -import { getModels, getDefaultModel } from "@providers/copilot/models"; +import { chat, chatStream } from "@providers/copilot/core/chat"; +import { getModels, getDefaultModel } from "@providers/copilot/core/models"; import { getCredentials, setCredentials, @@ -13,11 +13,11 @@ import { logout, isConfigured, validate, -} from "@providers/copilot/credentials"; +} from "@providers/copilot/auth/credentials"; import { initiateDeviceFlow, pollForAccessToken, -} from "@providers/copilot/auth"; +} from "@providers/copilot/auth/auth"; import { COPILOT_PROVIDER_NAME, COPILOT_DISPLAY_NAME, diff --git a/src/providers/copilot/auth.ts b/src/providers/copilot/auth/auth.ts similarity index 100% rename from src/providers/copilot/auth.ts rename to src/providers/copilot/auth/auth.ts diff --git a/src/providers/copilot/credentials.ts b/src/providers/copilot/auth/credentials.ts similarity index 92% rename from src/providers/copilot/credentials.ts rename to src/providers/copilot/auth/credentials.ts index 0eadf3b..1d70b0e 100644 --- a/src/providers/copilot/credentials.ts +++ b/src/providers/copilot/auth/credentials.ts @@ -10,7 +10,7 @@ import { setLoggedOut, clearCredentials, } from "@providers/copilot/state"; -import { getOAuthToken } from "@providers/copilot/token"; +import { getOAuthToken } from "@providers/copilot/auth/token"; import type { ProviderCredentials } from "@/types/providers"; import type { CopilotUserInfo } from "@/types/copilot"; @@ -70,7 +70,7 @@ export const validate = async (): Promise<{ error?: string; }> => { try { - const { refreshToken } = await import("@providers/copilot/token"); + const { refreshToken } = await import("@providers/copilot/auth/token"); const token = await refreshToken(); return { valid: !!token.token }; } catch (error) { diff --git a/src/providers/copilot/auth/index.ts b/src/providers/copilot/auth/index.ts new file mode 100644 index 0000000..bb7f062 --- /dev/null +++ b/src/providers/copilot/auth/index.ts @@ -0,0 +1,7 @@ +/** + * Copilot Provider Auth - Authentication and Credentials + */ + +export * from "./auth"; +export * from "./token"; +export * from "./credentials"; diff --git a/src/providers/copilot/token.ts b/src/providers/copilot/auth/token.ts similarity index 100% rename from src/providers/copilot/token.ts rename to src/providers/copilot/auth/token.ts diff --git a/src/providers/copilot/chat.ts b/src/providers/copilot/core/chat.ts similarity index 97% rename from src/providers/copilot/chat.ts rename to src/providers/copilot/core/chat.ts index 27feeab..4b2008b 100644 --- a/src/providers/copilot/chat.ts +++ b/src/providers/copilot/core/chat.ts @@ -8,8 +8,8 @@ import { COPILOT_MAX_RETRIES, COPILOT_UNLIMITED_MODEL, } from "@constants/copilot"; -import { refreshToken, buildHeaders } from "@providers/copilot/token"; -import { getDefaultModel, isModelUnlimited } from "@providers/copilot/models"; +import { refreshToken, buildHeaders } from "@providers/copilot/auth/token"; +import { getDefaultModel, isModelUnlimited } from "@providers/copilot/core/models"; import { sleep, isRateLimitError, @@ -23,7 +23,7 @@ import type { ChatCompletionResponse, StreamChunk, } from "@/types/providers"; -import { addDebugLog } from "@tui-solid/components/debug-log-panel"; +import { addDebugLog } from "@tui-solid/components/logs/debug-log-panel"; interface FormattedMessage { role: string; diff --git a/src/providers/copilot/core/index.ts b/src/providers/copilot/core/index.ts new file mode 100644 index 0000000..e0f2f92 --- /dev/null +++ b/src/providers/copilot/core/index.ts @@ -0,0 +1,6 @@ +/** + * Copilot Provider Core - Chat and Models + */ + +export * from "./chat"; +export * from "./models"; diff --git a/src/providers/copilot/models.ts b/src/providers/copilot/core/models.ts similarity index 98% rename from src/providers/copilot/models.ts rename to src/providers/copilot/core/models.ts index 4a1350d..672fe49 100644 --- a/src/providers/copilot/models.ts +++ b/src/providers/copilot/core/models.ts @@ -14,7 +14,7 @@ import { COPILOT_UNLIMITED_MODEL, } from "@constants/copilot"; import { getState, setModels } from "@providers/copilot/state"; -import { refreshToken } from "@providers/copilot/token"; +import { refreshToken } from "@providers/copilot/auth/token"; import type { ProviderModel } from "@/types/providers"; interface ModelBilling { diff --git a/src/providers/copilot/usage.ts b/src/providers/copilot/usage.ts index 2e6ac33..44901d2 100644 --- a/src/providers/copilot/usage.ts +++ b/src/providers/copilot/usage.ts @@ -4,7 +4,7 @@ import got from "got"; -import { getOAuthToken } from "@providers/copilot/token"; +import { getOAuthToken } from "@providers/copilot/auth/token"; import type { CopilotUsageResponse } from "@/types/copilot-usage"; const COPILOT_USER_URL = "https://api.github.com/copilot_internal/user"; diff --git a/src/providers/copilot/user-info.ts b/src/providers/copilot/user-info.ts new file mode 100644 index 0000000..1159c37 --- /dev/null +++ b/src/providers/copilot/user-info.ts @@ -0,0 +1,18 @@ +/** + * Copilot User Info Wrapper + * + * Provides a consistent interface for getting Copilot user information. + */ + +import { getUserInfo } from "@providers/copilot/auth/credentials"; + +/** + * Get Copilot user info with consistent interface + */ +export const getCopilotUserInfo = async (): Promise<{ + login: string; + name?: string; + email?: string; +} | null> => { + return getUserInfo(); +}; diff --git a/src/providers/chat.ts b/src/providers/core/chat.ts similarity index 97% rename from src/providers/chat.ts rename to src/providers/core/chat.ts index 740aa98..a3203f4 100644 --- a/src/providers/chat.ts +++ b/src/providers/core/chat.ts @@ -2,7 +2,7 @@ * Provider chat functions */ -import { getProvider } from "@providers/registry"; +import { getProvider } from "@providers/core/registry"; import type { ProviderName, Message, diff --git a/src/providers/credentials.ts b/src/providers/core/credentials.ts similarity index 100% rename from src/providers/credentials.ts rename to src/providers/core/credentials.ts diff --git a/src/providers/core/index.ts b/src/providers/core/index.ts new file mode 100644 index 0000000..522a27a --- /dev/null +++ b/src/providers/core/index.ts @@ -0,0 +1,8 @@ +/** + * Providers Core - Common provider functionality + */ + +export * from "./registry"; +export * from "./chat"; +export * from "./credentials"; +export * from "./status"; diff --git a/src/providers/registry.ts b/src/providers/core/registry.ts similarity index 100% rename from src/providers/registry.ts rename to src/providers/core/registry.ts diff --git a/src/providers/status.ts b/src/providers/core/status.ts similarity index 96% rename from src/providers/status.ts rename to src/providers/core/status.ts index 4498cd9..8190cae 100644 --- a/src/providers/status.ts +++ b/src/providers/core/status.ts @@ -5,7 +5,7 @@ import chalk from "chalk"; import { PROVIDER_INFO } from "@constants/providers"; -import { getProvider, getProviderNames } from "@providers/registry"; +import { getProvider, getProviderNames } from "@providers/core/registry"; import type { ProviderName, ProviderStatus } from "@/types/providers"; export const getProviderStatus = async ( diff --git a/src/providers/index.ts b/src/providers/index.ts index 0f8e1bc..3dff274 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -12,10 +12,10 @@ export { getAllProviders, getProviderNames, isValidProvider, -} from "@providers/registry"; +} from "@providers/core/registry"; // Re-export status functions -export { getProviderStatus, displayProvidersStatus } from "@providers/status"; +export { getProviderStatus, displayProvidersStatus } from "@providers/core/status"; // Re-export login functions export { @@ -26,7 +26,7 @@ export { } from "@providers/login"; // Re-export chat functions -export { chat, chatStream, getDefaultModel, getModels } from "@providers/chat"; +export { chat, chatStream, getDefaultModel, getModels } from "@providers/core/chat"; // Re-export copilot-specific functions export { diff --git a/src/providers/login.ts b/src/providers/login.ts index 5f5a29c..b4cd65f 100644 --- a/src/providers/login.ts +++ b/src/providers/login.ts @@ -6,7 +6,7 @@ export { loginProvider, logoutProvider } from "@providers/login/handlers"; export { initializeProviders, completeCopilotLogin, -} from "@providers/login/initialize"; +} from "@providers/login/core/initialize"; export { displayModels } from "@providers/login/utils"; export { loginCopilot } from "@providers/login/copilot-login"; export { loginOllama } from "@providers/login/ollama-login"; diff --git a/src/providers/login/copilot-login.ts b/src/providers/login/copilot-login.ts index f7e2931..47aa781 100644 --- a/src/providers/login/copilot-login.ts +++ b/src/providers/login/copilot-login.ts @@ -10,9 +10,9 @@ import { LOGIN_PROMPTS, AUTH_STEP_PREFIXES, } from "@constants/login"; -import { getProvider } from "@providers/registry"; -import { getProviderStatus } from "@providers/status"; -import { loadCredentials, saveCredentials } from "@providers/credentials"; +import { getProvider } from "@providers/core/registry"; +import { getProviderStatus } from "@providers/core/status"; +import { loadCredentials, saveCredentials } from "@providers/core/credentials"; import { initiateDeviceFlow, pollForAccessToken } from "@providers/copilot"; import { displayModels } from "@providers/login/utils"; import type { ProviderName, LoginHandler } from "@/types/providers"; diff --git a/src/providers/login/core/index.ts b/src/providers/login/core/index.ts new file mode 100644 index 0000000..c52edc6 --- /dev/null +++ b/src/providers/login/core/index.ts @@ -0,0 +1,5 @@ +/** + * Login Core - Initialization + */ + +export * from "./initialize"; diff --git a/src/providers/login/initialize.ts b/src/providers/login/core/initialize.ts similarity index 87% rename from src/providers/login/initialize.ts rename to src/providers/login/core/initialize.ts index 4501ef4..b67c430 100644 --- a/src/providers/login/initialize.ts +++ b/src/providers/login/core/initialize.ts @@ -2,8 +2,8 @@ * Provider initialization */ -import { getProvider, isValidProvider } from "@providers/registry"; -import { loadCredentials, saveCredentials } from "@providers/credentials"; +import { getProvider, isValidProvider } from "@providers/core/registry"; +import { loadCredentials, saveCredentials } from "@providers/core/credentials"; import { getLogoutHandler } from "@providers/login/handlers"; import type { ProviderName } from "@/types/providers"; diff --git a/src/providers/login/handlers.ts b/src/providers/login/handlers.ts index 8f2b743..20f923e 100644 --- a/src/providers/login/handlers.ts +++ b/src/providers/login/handlers.ts @@ -6,8 +6,8 @@ import chalk from "chalk"; import { PROVIDER_INFO } from "@constants/providers"; import { LOGIN_MESSAGES } from "@constants/login"; -import { getProvider } from "@providers/registry"; -import { loadCredentials, saveCredentials } from "@providers/credentials"; +import { getProvider } from "@providers/core/registry"; +import { loadCredentials, saveCredentials } from "@providers/core/credentials"; import { logoutCopilot } from "@providers/copilot"; import { loginCopilot } from "@providers/login/copilot-login"; import { loginOllama } from "@providers/login/ollama-login"; diff --git a/src/providers/login/ollama-login.ts b/src/providers/login/ollama-login.ts index 6d9682a..43ec232 100644 --- a/src/providers/login/ollama-login.ts +++ b/src/providers/login/ollama-login.ts @@ -7,8 +7,8 @@ import chalk from "chalk"; import { DEFAULT_OLLAMA_HOST } from "@constants/providers"; import { LOGIN_MESSAGES, LOGIN_PROMPTS } from "@constants/login"; -import { getProvider } from "@providers/registry"; -import { loadCredentials, saveCredentials } from "@providers/credentials"; +import { getProvider } from "@providers/core/registry"; +import { loadCredentials, saveCredentials } from "@providers/core/credentials"; import { displayModels } from "@providers/login/utils"; import type { ProviderName, LoginHandler } from "@/types/providers"; diff --git a/src/providers/ollama.ts b/src/providers/ollama.ts index bf6f7b5..accc376 100644 --- a/src/providers/ollama.ts +++ b/src/providers/ollama.ts @@ -10,8 +10,8 @@ import { import { getOllamaModels, getDefaultOllamaModel, -} from "@providers/ollama/models"; -import { ollamaChat } from "@providers/ollama/chat"; +} from "@providers/ollama/core/models"; +import { ollamaChat } from "@providers/ollama/core/chat"; import { ollamaChatStream } from "@providers/ollama/stream"; import { getOllamaCredentials, diff --git a/src/providers/ollama/chat.ts b/src/providers/ollama/core/chat.ts similarity index 98% rename from src/providers/ollama/chat.ts rename to src/providers/ollama/core/chat.ts index dc6f08d..efc158d 100644 --- a/src/providers/ollama/chat.ts +++ b/src/providers/ollama/core/chat.ts @@ -10,7 +10,7 @@ import { OLLAMA_CHAT_OPTIONS, } from "@constants/ollama"; import { getOllamaBaseUrl } from "@providers/ollama/state"; -import { getDefaultOllamaModel } from "@providers/ollama/models"; +import { getDefaultOllamaModel } from "@providers/ollama/core/models"; import type { Message, ChatCompletionOptions, diff --git a/src/providers/ollama/core/index.ts b/src/providers/ollama/core/index.ts new file mode 100644 index 0000000..522d039 --- /dev/null +++ b/src/providers/ollama/core/index.ts @@ -0,0 +1,6 @@ +/** + * Ollama Provider Core - Chat and Models + */ + +export * from "./chat"; +export * from "./models"; diff --git a/src/providers/ollama/models.ts b/src/providers/ollama/core/models.ts similarity index 100% rename from src/providers/ollama/models.ts rename to src/providers/ollama/core/models.ts diff --git a/src/providers/ollama/stream.ts b/src/providers/ollama/stream.ts index bf2490d..a4c45c5 100644 --- a/src/providers/ollama/stream.ts +++ b/src/providers/ollama/stream.ts @@ -6,14 +6,14 @@ import got from "got"; import { OLLAMA_ENDPOINTS, OLLAMA_TIMEOUTS } from "@constants/ollama"; import { getOllamaBaseUrl } from "@providers/ollama/state"; -import { buildChatRequest, mapToolCall } from "@providers/ollama/chat"; +import { buildChatRequest, mapToolCall } from "@providers/ollama/core/chat"; import type { Message, ChatCompletionOptions, StreamChunk, } from "@/types/providers"; import type { OllamaChatResponse } from "@/types/ollama"; -import { addDebugLog } from "@tui-solid/components/debug-log-panel"; +import { addDebugLog } from "@tui-solid/components/logs/debug-log-panel"; const parseStreamLine = ( line: string, diff --git a/src/services/agent-stream.ts b/src/services/agent-stream.ts index e489524..42eb2e5 100644 --- a/src/services/agent-stream.ts +++ b/src/services/agent-stream.ts @@ -20,9 +20,9 @@ import type { PartialToolCall, StreamCallbacks, } from "@/types/streaming"; -import { chatStream } from "@providers/chat"; +import { chatStream } from "@providers/core/chat"; import { getTool, getToolsForApi, refreshMCPTools } from "@tools/index"; -import { initializePermissions } from "@services/permissions"; +import { initializePermissions } from "@services/core/permissions"; import { MAX_ITERATIONS, MAX_CONSECUTIVE_ERRORS } from "@constants/agent"; import { createStreamAccumulator } from "@/types/streaming"; diff --git a/src/services/chat-tui/commands.ts b/src/services/chat-tui/commands.ts index c35b858..139b617 100644 --- a/src/services/chat-tui/commands.ts +++ b/src/services/chat-tui/commands.ts @@ -2,7 +2,7 @@ * Chat TUI command handling */ -import { saveSession as saveSessionSession } from "@services/session"; +import { saveSession as saveSessionSession } from "@services/core/session"; import { appStore } from "@tui/index"; import { CHAT_MESSAGES, type CommandName } from "@constants/chat-service"; import { handleLogin, handleLogout, showWhoami } from "@services/chat-tui/auth"; diff --git a/src/services/chat-tui/initialize.ts b/src/services/chat-tui/initialize.ts index 531b0ce..63bfb28 100644 --- a/src/services/chat-tui/initialize.ts +++ b/src/services/chat-tui/initialize.ts @@ -2,18 +2,18 @@ * Chat TUI initialization */ -import { errorMessage, infoMessage } from "@utils/terminal"; +import { errorMessage, infoMessage } from "@utils/core/terminal"; import { findSession, loadSession, createSession, getMostRecentSession, -} from "@services/session"; -import { getConfig } from "@services/config"; -import { initializePermissions } from "@services/permissions"; +} from "@services/core/session"; +import { getConfig } from "@services/core/config"; +import { initializePermissions } from "@services/core/permissions"; import { getProviderStatus } from "@providers/index"; import { appStore } from "@tui/index"; -import { themeActions } from "@stores/theme-store"; +import { themeActions } from "@stores/core/theme-store"; import { buildBaseContext, buildCompletePrompt, diff --git a/src/services/chat-tui/message-handler.ts b/src/services/chat-tui/message-handler.ts index fae92e4..2c9a7e8 100644 --- a/src/services/chat-tui/message-handler.ts +++ b/src/services/chat-tui/message-handler.ts @@ -2,7 +2,7 @@ * Chat TUI message handling */ -import { addMessage, saveSession } from "@services/session"; +import { addMessage, saveSession } from "@services/core/session"; import { createStreamingAgent } from "@services/agent-stream"; import { CHAT_MESSAGES } from "@constants/chat-service"; import { enrichMessageWithIssues } from "@services/github-issue-service"; @@ -44,7 +44,7 @@ import { checkOllamaAvailability, checkCopilotAvailability, } from "@services/cascading-provider"; -import { chat, getDefaultModel } from "@providers/chat"; +import { chat, getDefaultModel } from "@providers/core/chat"; import { AUDIT_SYSTEM_PROMPT, createAuditPrompt, parseAuditResponse } from "@prompts/audit-prompt"; import { PROVIDER_IDS } from "@constants/provider-quality"; import { appStore } from "@tui/index"; @@ -55,7 +55,7 @@ import type { ChatServiceCallbacks, ToolCallInfo, } from "@/types/chat-service"; -import { addDebugLog } from "@tui-solid/components/debug-log-panel"; +import { addDebugLog } from "@tui-solid/components/logs/debug-log-panel"; import { FILE_MODIFYING_TOOLS } from "@constants/tools"; import type { StreamCallbacksWithState } from "@interfaces/StreamCallbacksWithState"; import { diff --git a/src/services/chat-tui/models.ts b/src/services/chat-tui/models.ts index 2b48060..2b64b1e 100644 --- a/src/services/chat-tui/models.ts +++ b/src/services/chat-tui/models.ts @@ -3,7 +3,7 @@ */ import { MODEL_MESSAGES } from "@constants/chat-service"; -import { getConfig } from "@services/config"; +import { getConfig } from "@services/core/config"; import { getProvider, getDefaultModel, diff --git a/src/services/chat-tui/permissions.ts b/src/services/chat-tui/permissions.ts index df8ed44..abf3535 100644 --- a/src/services/chat-tui/permissions.ts +++ b/src/services/chat-tui/permissions.ts @@ -4,7 +4,7 @@ import { v4 as uuidv4 } from "uuid"; -import { setPermissionHandler } from "@services/permissions"; +import { setPermissionHandler } from "@services/core/permissions"; import type { PermissionPromptRequest, PermissionPromptResponse, diff --git a/src/services/chat-tui/print-mode.ts b/src/services/chat-tui/print-mode.ts index f39422b..ff80998 100644 --- a/src/services/chat-tui/print-mode.ts +++ b/src/services/chat-tui/print-mode.ts @@ -2,8 +2,8 @@ * Chat TUI print mode (non-interactive) */ -import { createAgent } from "@services/agent"; -import { initializePermissions } from "@services/permissions"; +import { createAgent } from "@services/core/agent"; +import { initializePermissions } from "@services/core/permissions"; import { processFileReferences, buildContextMessage, diff --git a/src/services/chat-tui/usage.ts b/src/services/chat-tui/usage.ts index 8c69b80..ed55ab4 100644 --- a/src/services/chat-tui/usage.ts +++ b/src/services/chat-tui/usage.ts @@ -2,8 +2,8 @@ * Usage statistics display for TUI */ -import { usageStore } from "@stores/usage-store"; -import { getUserInfo } from "@providers/copilot/credentials"; +import { usageStore } from "@stores/core/usage-store"; +import { getUserInfo } from "@providers/copilot/auth/credentials"; import { getCopilotUsage } from "@providers/copilot/usage"; import { PROGRESS_BAR } from "@constants/ui"; import type { diff --git a/src/services/agent.ts b/src/services/core/agent.ts similarity index 98% rename from src/services/agent.ts rename to src/services/core/agent.ts index e2091b0..2d1484c 100644 --- a/src/services/agent.ts +++ b/src/services/core/agent.ts @@ -22,14 +22,14 @@ import type { import { chat as providerChat } from "@providers/index"; import { getTool, getToolsForApi, refreshMCPTools } from "@tools/index"; import type { ToolContext, ToolCall, ToolResult } from "@/types/tools"; -import { initializePermissions } from "@services/permissions"; +import { initializePermissions } from "@services/core/permissions"; import { loadHooks, executePreToolUseHooks, executePostToolUseHooks, } from "@services/hooks-service"; import { MAX_ITERATIONS } from "@constants/agent"; -import { usageStore } from "@stores/usage-store"; +import { usageStore } from "@stores/core/usage-store"; /** * Agent state interface diff --git a/src/services/config.ts b/src/services/core/config.ts similarity index 100% rename from src/services/config.ts rename to src/services/core/config.ts diff --git a/src/services/executor.ts b/src/services/core/executor.ts similarity index 99% rename from src/services/executor.ts rename to src/services/core/executor.ts index 6bfd393..8912f25 100644 --- a/src/services/executor.ts +++ b/src/services/core/executor.ts @@ -7,7 +7,7 @@ import { promisify } from "util"; import chalk from "chalk"; import fs from "fs/promises"; import path from "path"; -import { promptBashPermission } from "@services/permissions"; +import { promptBashPermission } from "@services/core/permissions"; import type { ExecutionResult } from "@interfaces/ExecutionResult"; const execAsync = promisify(exec); diff --git a/src/services/core/index.ts b/src/services/core/index.ts new file mode 100644 index 0000000..9a0ba09 --- /dev/null +++ b/src/services/core/index.ts @@ -0,0 +1,91 @@ +/** + * Services Core - Core service exports + */ + +// Agent +export { + runAgentLoop, + runAgent, + createAgent, + type AgentOptions, + type AgentResult, +} from "./agent"; + +// Permissions +export { + setWorkingDir as setPermissionsWorkingDir, + setPermissionHandler, + initializePermissions, + parsePattern, + matchesBashPattern, + matchesPathPattern, + isBashAllowed, + isBashDenied, + isFileOpAllowed, + generateBashPattern, + addSessionPattern, + addGlobalPattern, + addLocalPattern, + listPatterns, + clearSessionPatterns, + promptBashPermission, + promptFilePermission, + promptPermission, + getPermissionLevel, + type ToolType, + type PermissionPattern, + type PermissionsConfig, + type PermissionHandler, +} from "./permissions"; + +// Session +export { + createSession, + loadSession, + saveSession, + addMessage, + addContextFile, + removeContextFile, + getCurrentSession, + listSessions, + deleteSession, + clearMessages, + getMostRecentSession, + getSessionSummaries, + findSession, + setWorkingDirectory, + type SessionInfo, +} from "./session"; + +// Executor +export { + setWorkingDir as setExecutorWorkingDir, + getWorkingDir, + executeCommand, + executeStreamingCommand, + readFile, + writeFile, + editFile, + deleteFile, + createDirectory, + listDirectory, + pathExists, + getStats, + type ExecutionResult, + type FileOperation, +} from "./executor"; + +// Config +export { + loadConfig, + saveConfig, + getConfigValue, + setConfigValue, + getAllConfig, + getApiKey, + getModel, + getConfigPath, + isProtectedPath, + resetConfig, + getConfig, +} from "./config"; diff --git a/src/services/permissions.ts b/src/services/core/permissions.ts similarity index 100% rename from src/services/permissions.ts rename to src/services/core/permissions.ts diff --git a/src/services/session.ts b/src/services/core/session.ts similarity index 100% rename from src/services/session.ts rename to src/services/core/session.ts diff --git a/src/services/index.ts b/src/services/index.ts index a6098cc..0ead0a0 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -2,6 +2,7 @@ * Services Module - Business logic extracted from UI components */ +// Feature services export * from "@services/file-picker-service"; export * from "@services/chat-tui-service"; export * from "@services/github-issue-service"; @@ -9,3 +10,6 @@ export * from "@services/command-suggestion-service"; export * from "@services/learning-service"; export * from "@services/rules-service"; export * as brainService from "@services/brain"; + +// Note: Core services (agent, permissions, session, executor, config) are imported +// directly from @services/core/* to avoid naming conflicts with chat-tui-service diff --git a/src/services/multi-agent/agent-manager.ts b/src/services/multi-agent/agent-manager.ts new file mode 100644 index 0000000..bee75e7 --- /dev/null +++ b/src/services/multi-agent/agent-manager.ts @@ -0,0 +1,224 @@ +/** + * Agent Manager + * + * Manages agent instance lifecycle: creation, starting, stopping, and cleanup. + */ + +import type { + AgentInstance, + AgentSpawnConfig, + AgentConversation, + AgentExecutionResult, +} from "@/types/multi-agent"; +import type { AgentDefinition } from "@/types/agent-definition"; +import { multiAgentStore } from "@stores/core/multi-agent-store"; +import { + MULTI_AGENT_ERRORS, + MULTI_AGENT_DEFAULTS, + MULTI_AGENT_LIMITS, +} from "@/constants/multi-agent"; + +/** + * Agent registry cache + */ +let agentRegistry: Map = new Map(); + +/** + * Set the agent registry (called during initialization) + */ +export const setAgentRegistry = (registry: Map): void => { + agentRegistry = registry; +}; + +/** + * Get agent definition by name + */ +export const getAgentDefinition = (name: string): AgentDefinition | undefined => { + return agentRegistry.get(name); +}; + +/** + * Create an agent instance from config + */ +export const createAgentInstance = ( + config: AgentSpawnConfig, +): AgentInstance | { error: string } => { + const definition = getAgentDefinition(config.agentName); + if (!definition) { + return { error: MULTI_AGENT_ERRORS.AGENT_NOT_FOUND(config.agentName) }; + } + + const activeCount = multiAgentStore.getActiveInstances().length; + if (activeCount >= MULTI_AGENT_LIMITS.maxConcurrentRequests) { + return { error: MULTI_AGENT_ERRORS.MAX_CONCURRENT_EXCEEDED(MULTI_AGENT_LIMITS.maxConcurrentRequests) }; + } + + const conversation: AgentConversation = { + messages: [], + toolCalls: [], + }; + + const instance: Omit = { + definition, + config, + status: "pending", + conversation, + startedAt: Date.now(), + modifiedFiles: [], + }; + + const id = multiAgentStore.addInstance(instance); + + return { + ...instance, + id, + } as AgentInstance; +}; + +/** + * Start an agent instance + */ +export const startAgent = (agentId: string): void => { + multiAgentStore.updateInstanceStatus(agentId, "running"); +}; + +/** + * Pause agent due to conflict + */ +export const pauseAgentForConflict = (agentId: string): void => { + multiAgentStore.updateInstanceStatus(agentId, "waiting_conflict"); +}; + +/** + * Resume agent after conflict resolution + */ +export const resumeAgent = (agentId: string): void => { + const instance = multiAgentStore.getState().instances.get(agentId); + if (instance?.status === "waiting_conflict") { + multiAgentStore.updateInstanceStatus(agentId, "running"); + } +}; + +/** + * Complete an agent with result + */ +export const completeAgent = ( + agentId: string, + result: AgentExecutionResult, +): void => { + const state = multiAgentStore.getState(); + const instance = state.instances.get(agentId); + if (!instance) return; + + multiAgentStore.updateInstanceStatus( + agentId, + result.success ? "completed" : "error", + result.error, + ); + + multiAgentStore.addEvent({ + type: result.success ? "agent_completed" : "agent_error", + agentId, + ...(result.success + ? { result, timestamp: Date.now() } + : { error: result.error ?? "Unknown error", timestamp: Date.now() }), + } as { type: "agent_completed"; agentId: string; result: AgentExecutionResult; timestamp: number } | { type: "agent_error"; agentId: string; error: string; timestamp: number }); +}; + +/** + * Cancel an agent + */ +export const cancelAgent = (agentId: string, reason?: string): void => { + multiAgentStore.updateInstanceStatus( + agentId, + "cancelled", + reason ?? "Cancelled by user", + ); +}; + +/** + * Get agent by ID + */ +export const getAgent = (agentId: string): AgentInstance | undefined => { + return multiAgentStore.getState().instances.get(agentId); +}; + +/** + * Validate spawn config + */ +export const validateSpawnConfig = ( + config: AgentSpawnConfig, +): { valid: boolean; errors: string[] } => { + const errors: string[] = []; + + if (!config.agentName) { + errors.push("Agent name is required"); + } + + if (!config.task) { + errors.push("Task is required"); + } + + const definition = getAgentDefinition(config.agentName); + if (!definition) { + errors.push(MULTI_AGENT_ERRORS.AGENT_NOT_FOUND(config.agentName)); + } + + if (config.timeout && config.timeout < 1000) { + errors.push("Timeout must be at least 1000ms"); + } + + if (config.timeout && config.timeout > MULTI_AGENT_DEFAULTS.timeout * 2) { + errors.push(`Timeout cannot exceed ${MULTI_AGENT_DEFAULTS.timeout * 2}ms`); + } + + return { valid: errors.length === 0, errors }; +}; + +/** + * Get all running agents + */ +export const getRunningAgents = (): AgentInstance[] => { + return multiAgentStore.getInstancesByStatus("running"); +}; + +/** + * Get all agents waiting on conflicts + */ +export const getWaitingAgents = (): AgentInstance[] => { + return multiAgentStore.getInstancesByStatus("waiting_conflict"); +}; + +/** + * Cancel all running agents + */ +export const cancelAllAgents = (reason?: string): void => { + const running = getRunningAgents(); + const waiting = getWaitingAgents(); + + [...running, ...waiting].forEach((agent) => { + cancelAgent(agent.id, reason ?? MULTI_AGENT_ERRORS.EXECUTION_ABORTED); + }); +}; + +/** + * Get agent statistics + */ +export const getAgentStats = (): { + running: number; + waiting: number; + completed: number; + failed: number; + cancelled: number; +} => { + const state = multiAgentStore.getState(); + const instances = Array.from(state.instances.values()); + + return { + running: instances.filter((i) => i.status === "running").length, + waiting: instances.filter((i) => i.status === "waiting_conflict").length, + completed: instances.filter((i) => i.status === "completed").length, + failed: instances.filter((i) => i.status === "error").length, + cancelled: instances.filter((i) => i.status === "cancelled").length, + }; +}; diff --git a/src/services/multi-agent/conflict-handler.ts b/src/services/multi-agent/conflict-handler.ts new file mode 100644 index 0000000..5feabff --- /dev/null +++ b/src/services/multi-agent/conflict-handler.ts @@ -0,0 +1,299 @@ +/** + * Conflict Handler + * + * Detects and resolves file conflicts between concurrent agents. + */ + +import type { + FileConflict, + ConflictStrategy, + ConflictResolutionResult, + AgentInstance, +} from "@/types/multi-agent"; +import { multiAgentStore } from "@stores/core/multi-agent-store"; +import { + MULTI_AGENT_ERRORS, + FILE_LOCK, +} from "@/constants/multi-agent"; +import { + pauseAgentForConflict, + resumeAgent, +} from "@/services/multi-agent/agent-manager"; + +/** + * File locks for tracking which agent owns which file + */ +const fileLocks: Map = new Map(); // filePath -> agentId + +/** + * Pending lock requests + */ +const pendingLocks: Map void; +}>> = new Map(); + +/** + * Acquire a file lock for an agent + */ +export const acquireFileLock = async ( + agentId: string, + filePath: string, +): Promise => { + const currentOwner = fileLocks.get(filePath); + + if (!currentOwner) { + fileLocks.set(filePath, agentId); + return true; + } + + if (currentOwner === agentId) { + return true; + } + + // File is locked by another agent - add to pending queue + return new Promise((resolve) => { + const pending = pendingLocks.get(filePath) ?? []; + pending.push({ agentId, resolve }); + pendingLocks.set(filePath, pending); + + // Set timeout for lock acquisition + setTimeout(() => { + const queue = pendingLocks.get(filePath) ?? []; + const idx = queue.findIndex((p) => p.agentId === agentId); + if (idx !== -1) { + queue.splice(idx, 1); + pendingLocks.set(filePath, queue); + resolve(false); + } + }, FILE_LOCK.acquireTimeout); + }); +}; + +/** + * Release a file lock + */ +export const releaseFileLock = (agentId: string, filePath: string): void => { + const currentOwner = fileLocks.get(filePath); + if (currentOwner !== agentId) return; + + fileLocks.delete(filePath); + + // Grant lock to next pending agent + const pending = pendingLocks.get(filePath) ?? []; + if (pending.length > 0) { + const next = pending.shift(); + if (next) { + pendingLocks.set(filePath, pending); + fileLocks.set(filePath, next.agentId); + next.resolve(true); + } + } +}; + +/** + * Release all locks held by an agent + */ +export const releaseAllLocks = (agentId: string): void => { + const locksToRelease: string[] = []; + + fileLocks.forEach((owner, path) => { + if (owner === agentId) { + locksToRelease.push(path); + } + }); + + locksToRelease.forEach((path) => releaseFileLock(agentId, path)); +}; + +/** + * Check if a file is locked + */ +export const isFileLocked = (filePath: string): boolean => { + return fileLocks.has(filePath); +}; + +/** + * Get the agent that holds a file lock + */ +export const getFileLockOwner = (filePath: string): string | null => { + return fileLocks.get(filePath) ?? null; +}; + +/** + * Detect conflict when agent tries to modify a file + */ +export const detectConflict = ( + agentId: string, + filePath: string, +): FileConflict | null => { + const currentOwner = fileLocks.get(filePath); + + if (!currentOwner || currentOwner === agentId) { + return null; + } + + const conflict: FileConflict = { + filePath, + conflictingAgentIds: [currentOwner, agentId], + detectedAt: Date.now(), + }; + + multiAgentStore.addConflict(conflict); + return conflict; +}; + +/** + * Resolve conflict using specified strategy + */ +export const resolveConflict = async ( + conflict: FileConflict, + strategy: ConflictStrategy, +): Promise => { + const resolutionHandlers: Record< + ConflictStrategy, + () => Promise + > = { + serialize: () => handleSerializeStrategy(conflict), + "abort-newer": () => handleAbortNewerStrategy(conflict), + "merge-results": () => handleMergeStrategy(conflict), + isolated: () => handleIsolatedStrategy(conflict), + }; + + const result = await resolutionHandlers[strategy](); + multiAgentStore.resolveConflict(conflict.filePath, result); + + return result; +}; + +/** + * Handle serialize strategy - wait for owner to finish + */ +const handleSerializeStrategy = async ( + conflict: FileConflict, +): Promise => { + const [ownerAgentId, waitingAgentId] = conflict.conflictingAgentIds; + + // Pause the waiting agent + pauseAgentForConflict(waitingAgentId); + + // Wait for owner to complete + await waitForAgentCompletion(ownerAgentId); + + // Resume waiting agent + resumeAgent(waitingAgentId); + + return { + strategy: "serialize", + winningAgentId: ownerAgentId, + resolvedAt: Date.now(), + }; +}; + +/** + * Handle abort-newer strategy - abort the agent that started later + */ +const handleAbortNewerStrategy = async ( + conflict: FileConflict, +): Promise => { + const state = multiAgentStore.getState(); + const agents = conflict.conflictingAgentIds + .map((id) => state.instances.get(id)) + .filter((a): a is AgentInstance => a !== undefined); + + // Sort by start time, newer agent is cancelled + agents.sort((a, b) => a.startedAt - b.startedAt); + const olderAgent = agents[0]; + const newerAgent = agents[1]; + + if (newerAgent) { + multiAgentStore.updateInstanceStatus( + newerAgent.id, + "cancelled", + MULTI_AGENT_ERRORS.CONFLICT_RESOLUTION_FAILED(conflict.filePath), + ); + } + + return { + strategy: "abort-newer", + winningAgentId: olderAgent?.id, + resolvedAt: Date.now(), + }; +}; + +/** + * Handle merge strategy - placeholder for merge logic + */ +const handleMergeStrategy = async ( + _conflict: FileConflict, +): Promise => { + // Merge strategy requires comparing file contents and intelligently + // combining changes. This is a placeholder - actual implementation + // would need diff/patch logic. + return { + strategy: "merge-results", + resolvedAt: Date.now(), + }; +}; + +/** + * Handle isolated strategy - each agent works in isolation + */ +const handleIsolatedStrategy = async ( + _conflict: FileConflict, +): Promise => { + // In isolated mode, conflicts are expected and handled at merge time + return { + strategy: "isolated", + resolvedAt: Date.now(), + }; +}; + +/** + * Wait for an agent to complete + */ +const waitForAgentCompletion = (agentId: string): Promise => { + return new Promise((resolve) => { + const checkInterval = setInterval(() => { + const agent = multiAgentStore.getState().instances.get(agentId); + if (!agent || ["completed", "error", "cancelled"].includes(agent.status)) { + clearInterval(checkInterval); + resolve(); + } + }, 100); + }); +}; + +/** + * Get all current conflicts for an agent + */ +export const getConflictsForAgent = (agentId: string): FileConflict[] => { + const conflicts = multiAgentStore.getUnresolvedConflicts(); + return conflicts.filter((c) => c.conflictingAgentIds.includes(agentId)); +}; + +/** + * Clear all file locks (for cleanup) + */ +export const clearAllLocks = (): void => { + fileLocks.clear(); + pendingLocks.clear(); +}; + +/** + * Get lock statistics + */ +export const getLockStats = (): { + lockedFiles: number; + pendingRequests: number; +} => { + let pendingCount = 0; + pendingLocks.forEach((pending) => { + pendingCount += pending.length; + }); + + return { + lockedFiles: fileLocks.size, + pendingRequests: pendingCount, + }; +}; diff --git a/src/services/multi-agent/executor.ts b/src/services/multi-agent/executor.ts new file mode 100644 index 0000000..66c96b3 --- /dev/null +++ b/src/services/multi-agent/executor.ts @@ -0,0 +1,403 @@ +/** + * Multi-Agent Executor + * + * Orchestrates concurrent execution of multiple agents with + * conflict detection, resource management, and result aggregation. + */ + +import type { + MultiAgentRequest, + MultiAgentResult, + AgentSpawnConfig, + AgentInstance, + AgentExecutionResult, + MultiAgentExecutorOptions, + FileConflict, +} from "@/types/multi-agent"; +import { multiAgentStore } from "@stores/core/multi-agent-store"; +import { + MULTI_AGENT_DEFAULTS, + MULTI_AGENT_ERRORS, + MULTI_AGENT_LIMITS, +} from "@/constants/multi-agent"; +import { + createAgentInstance, + startAgent, + completeAgent, + cancelAgent, + validateSpawnConfig, +} from "@/services/multi-agent/agent-manager"; +import { + createToolContext, + cleanupToolContext, +} from "@/services/multi-agent/tool-context"; +import { + resolveConflict, + clearAllLocks, +} from "@/services/multi-agent/conflict-handler"; + +/** + * Execute multiple agents according to request configuration + */ +export const executeMultiAgent = async ( + request: Omit, + options: MultiAgentExecutorOptions = {}, +): Promise => { + const startTime = Date.now(); + + // Validate request + const validationError = validateRequest(request); + if (validationError) { + throw new Error(validationError); + } + + // Add request to store + const requestId = multiAgentStore.addRequest(request); + + // Track results + const results: AgentInstance[] = []; + const conflicts: FileConflict[] = []; + + try { + // Execute based on mode + const executionHandlers: Record< + typeof request.executionMode, + () => Promise + > = { + sequential: () => executeSequential(request.agents, options, results), + parallel: () => executeParallel(request, options, results, conflicts), + adaptive: () => executeAdaptive(request, options, results, conflicts), + }; + + await executionHandlers[request.executionMode](); + + // Aggregate results + const result = aggregateResults(requestId, results, conflicts, startTime); + + // Emit completion event + options.onEvent?.({ + type: "execution_completed", + result, + timestamp: Date.now(), + }); + + return result; + } finally { + // Cleanup + multiAgentStore.removeRequest(requestId); + clearAllLocks(); + } +}; + +/** + * Validate request configuration + */ +const validateRequest = ( + request: Omit, +): string | null => { + if (request.agents.length === 0) { + return "At least one agent is required"; + } + + if (request.agents.length > MULTI_AGENT_LIMITS.maxAgentsPerRequest) { + return MULTI_AGENT_ERRORS.MAX_AGENTS_EXCEEDED(MULTI_AGENT_LIMITS.maxAgentsPerRequest); + } + + // Validate each agent config + for (const config of request.agents) { + const validation = validateSpawnConfig(config); + if (!validation.valid) { + return validation.errors[0]; + } + } + + return null; +}; + +/** + * Execute agents sequentially + */ +const executeSequential = async ( + configs: AgentSpawnConfig[], + options: MultiAgentExecutorOptions, + results: AgentInstance[], +): Promise => { + for (const config of configs) { + if (options.abortSignal?.aborted) { + break; + } + + const instance = await executeSingleAgent(config, options); + results.push(instance); + + // Check for abort on error + if (instance.status === "error" && options.abortSignal) { + break; + } + } +}; + +/** + * Execute agents in parallel + */ +const executeParallel = async ( + request: Omit, + options: MultiAgentExecutorOptions, + results: AgentInstance[], + conflicts: FileConflict[], +): Promise => { + const maxConcurrent = request.maxConcurrent ?? MULTI_AGENT_DEFAULTS.maxConcurrent; + const chunks = chunkArray(request.agents, maxConcurrent); + + for (const chunk of chunks) { + if (options.abortSignal?.aborted) { + break; + } + + // Execute chunk in parallel + const chunkPromises = chunk.map((config) => + executeSingleAgent(config, options), + ); + + const chunkResults = await Promise.all(chunkPromises); + results.push(...chunkResults); + + // Collect any conflicts + const newConflicts = multiAgentStore.getUnresolvedConflicts(); + conflicts.push(...newConflicts.filter((c) => !conflicts.includes(c))); + + // Resolve conflicts if any + for (const conflict of newConflicts) { + await resolveConflict(conflict, request.conflictStrategy); + } + } +}; + +/** + * Execute agents adaptively (start parallel, serialize on conflict) + */ +const executeAdaptive = async ( + request: Omit, + options: MultiAgentExecutorOptions, + results: AgentInstance[], + conflicts: FileConflict[], +): Promise => { + const maxConcurrent = request.maxConcurrent ?? MULTI_AGENT_DEFAULTS.maxConcurrent; + let conflictCount = 0; + let useSequential = false; + + // Start with parallel execution + const remaining = [...request.agents]; + const running: Promise[] = []; + + while (remaining.length > 0 || running.length > 0) { + if (options.abortSignal?.aborted) { + // Cancel all running agents + multiAgentStore.getActiveInstances().forEach((instance) => { + cancelAgent(instance.id, MULTI_AGENT_ERRORS.EXECUTION_ABORTED); + }); + break; + } + + // Start new agents if under limit and not in sequential mode + while ( + remaining.length > 0 && + running.length < maxConcurrent && + !useSequential + ) { + const config = remaining.shift()!; + running.push(executeSingleAgent(config, options)); + } + + // If in sequential mode, wait for current to finish before starting next + if (useSequential && remaining.length > 0 && running.length === 0) { + const config = remaining.shift()!; + running.push(executeSingleAgent(config, options)); + } + + // Wait for at least one to complete + if (running.length > 0) { + const completed = await Promise.race( + running.map((p, i) => p.then((result) => ({ result, index: i }))), + ); + + results.push(completed.result); + running.splice(completed.index, 1); + + // Check for new conflicts + const newConflicts = multiAgentStore.getUnresolvedConflicts(); + for (const conflict of newConflicts) { + if (!conflicts.includes(conflict)) { + conflicts.push(conflict); + conflictCount++; + + // Resolve conflict + await resolveConflict(conflict, request.conflictStrategy); + + // Switch to sequential mode if too many conflicts + if (conflictCount >= MULTI_AGENT_LIMITS.maxConflictsBeforeAbort) { + useSequential = true; + } + } + } + } + } +}; + +/** + * Execute a single agent + */ +const executeSingleAgent = async ( + config: AgentSpawnConfig, + options: MultiAgentExecutorOptions, +): Promise => { + // Create instance + const instanceOrError = createAgentInstance(config); + if ("error" in instanceOrError) { + throw new Error(instanceOrError.error); + } + + const instance = instanceOrError; + + // Create tool context + createToolContext( + instance.id, + process.cwd(), + config.contextFiles, + [], + ); + + // Start agent + startAgent(instance.id); + + try { + // Execute agent task + const result = await executeAgentTask(instance, config, options); + + // Complete agent + completeAgent(instance.id, result); + + return multiAgentStore.getState().instances.get(instance.id) ?? instance; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + completeAgent(instance.id, { + success: false, + error: errorMessage, + filesModified: [], + toolCallCount: 0, + duration: Date.now() - instance.startedAt, + }); + + return multiAgentStore.getState().instances.get(instance.id) ?? instance; + } finally { + // Cleanup context + cleanupToolContext(instance.id); + } +}; + +/** + * Execute the actual agent task + * This is a placeholder - actual implementation would integrate with + * the chat/provider system + */ +const executeAgentTask = async ( + instance: AgentInstance, + config: AgentSpawnConfig, + _options: MultiAgentExecutorOptions, +): Promise => { + const startTime = Date.now(); + + // This is where the actual agent execution would happen + // For now, return a placeholder result + // In real implementation, this would: + // 1. Build system prompt from agent definition + // 2. Send task to LLM provider + // 3. Handle tool calls + // 4. Track file modifications + // 5. Return result + + // Placeholder implementation + return { + success: true, + output: `Agent ${instance.definition.name} completed task: ${config.task}`, + filesModified: [], + toolCallCount: 0, + duration: Date.now() - startTime, + }; +}; + +/** + * Aggregate results from all agents + */ +const aggregateResults = ( + requestId: string, + agents: AgentInstance[], + conflicts: FileConflict[], + startTime: number, +): MultiAgentResult => { + const successful = agents.filter((a) => a.status === "completed").length; + const failed = agents.filter((a) => a.status === "error").length; + const cancelled = agents.filter((a) => a.status === "cancelled").length; + + // Aggregate output from all successful agents + const outputs = agents + .filter((a) => a.status === "completed" && a.result?.output) + .map((a) => a.result!.output); + + return { + requestId, + agents, + successful, + failed, + cancelled, + conflicts, + totalDuration: Date.now() - startTime, + aggregatedOutput: outputs.length > 0 ? outputs.join("\n\n---\n\n") : undefined, + }; +}; + +/** + * Split array into chunks + */ +const chunkArray = (array: T[], size: number): T[][] => { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += size) { + chunks.push(array.slice(i, i + size)); + } + return chunks; +}; + +/** + * Cancel a running multi-agent execution + */ +export const cancelExecution = (requestId: string): void => { + const request = multiAgentStore.getState().activeRequests.get(requestId); + if (!request) return; + + // Cancel all agents associated with this request + multiAgentStore.getActiveInstances().forEach((instance) => { + cancelAgent(instance.id, MULTI_AGENT_ERRORS.EXECUTION_ABORTED); + }); + + multiAgentStore.removeRequest(requestId); +}; + +/** + * Get execution status + */ +export const getExecutionStatus = (): { + isExecuting: boolean; + activeRequests: number; + runningAgents: number; + conflicts: number; +} => { + const state = multiAgentStore.getState(); + + return { + isExecuting: state.isExecuting, + activeRequests: state.activeRequests.size, + runningAgents: multiAgentStore.getActiveInstances().length, + conflicts: multiAgentStore.getUnresolvedConflicts().length, + }; +}; diff --git a/src/services/multi-agent/tool-context.ts b/src/services/multi-agent/tool-context.ts new file mode 100644 index 0000000..73a7a00 --- /dev/null +++ b/src/services/multi-agent/tool-context.ts @@ -0,0 +1,239 @@ +/** + * Agent Tool Context + * + * Provides isolated tool execution context for each agent, + * tracking file modifications and enforcing permissions. + */ + +import type { AgentToolContext } from "@/types/multi-agent"; +import { multiAgentStore } from "@stores/core/multi-agent-store"; +import { + acquireFileLock, + releaseFileLock, + releaseAllLocks, + detectConflict, +} from "@/services/multi-agent/conflict-handler"; + +/** + * Active tool contexts by agent ID + */ +const activeContexts: Map = new Map(); + +/** + * Create a tool context for an agent + */ +export const createToolContext = ( + agentId: string, + workingDir: string, + allowedPaths: string[] = [], + deniedPaths: string[] = [], +): AgentToolContext => { + const context: AgentToolContext = { + agentId, + workingDir, + allowedPaths, + deniedPaths, + modifiedFiles: new Set(), + lockedFiles: new Set(), + }; + + activeContexts.set(agentId, context); + return context; +}; + +/** + * Get tool context for an agent + */ +export const getToolContext = (agentId: string): AgentToolContext | null => { + return activeContexts.get(agentId) ?? null; +}; + +/** + * Check if a path is allowed for an agent + */ +export const isPathAllowed = ( + agentId: string, + filePath: string, +): boolean => { + const context = activeContexts.get(agentId); + if (!context) return false; + + // Check denied paths first (higher priority) + for (const denied of context.deniedPaths) { + if (filePath.startsWith(denied)) { + return false; + } + } + + // If no allowed paths specified, allow all (except denied) + if (context.allowedPaths.length === 0) { + return true; + } + + // Check if path matches any allowed path + for (const allowed of context.allowedPaths) { + if (filePath.startsWith(allowed)) { + return true; + } + } + + return false; +}; + +/** + * Request write access to a file + * Returns true if access granted, false if conflict or denied + */ +export const requestWriteAccess = async ( + agentId: string, + filePath: string, +): Promise<{ granted: boolean; conflict?: boolean; reason?: string }> => { + const context = activeContexts.get(agentId); + if (!context) { + return { granted: false, reason: "No active context for agent" }; + } + + // Check path permissions + if (!isPathAllowed(agentId, filePath)) { + return { granted: false, reason: "Path not allowed for this agent" }; + } + + // Detect conflicts with other agents + const conflict = detectConflict(agentId, filePath); + if (conflict) { + return { granted: false, conflict: true, reason: "File locked by another agent" }; + } + + // Acquire file lock + const acquired = await acquireFileLock(agentId, filePath); + if (!acquired) { + return { granted: false, conflict: true, reason: "Could not acquire file lock" }; + } + + // Track locked file + context.lockedFiles.add(filePath); + return { granted: true }; +}; + +/** + * Record a file modification + */ +export const recordModification = ( + agentId: string, + filePath: string, +): void => { + const context = activeContexts.get(agentId); + if (!context) return; + + context.modifiedFiles.add(filePath); + multiAgentStore.addModifiedFile(agentId, filePath); +}; + +/** + * Release write access to a file + */ +export const releaseWriteAccess = ( + agentId: string, + filePath: string, +): void => { + const context = activeContexts.get(agentId); + if (!context) return; + + context.lockedFiles.delete(filePath); + releaseFileLock(agentId, filePath); +}; + +/** + * Get all files modified by an agent + */ +export const getModifiedFiles = (agentId: string): string[] => { + const context = activeContexts.get(agentId); + if (!context) return []; + + return Array.from(context.modifiedFiles); +}; + +/** + * Get all files currently locked by an agent + */ +export const getLockedFiles = (agentId: string): string[] => { + const context = activeContexts.get(agentId); + if (!context) return []; + + return Array.from(context.lockedFiles); +}; + +/** + * Clean up tool context for an agent + */ +export const cleanupToolContext = (agentId: string): void => { + const context = activeContexts.get(agentId); + if (!context) return; + + // Release all locks + releaseAllLocks(agentId); + + // Remove context + activeContexts.delete(agentId); +}; + +/** + * Clean up all tool contexts + */ +export const cleanupAllContexts = (): void => { + activeContexts.forEach((_, agentId) => { + cleanupToolContext(agentId); + }); +}; + +/** + * Get context statistics + */ +export const getContextStats = (): { + activeContexts: number; + totalModifiedFiles: number; + totalLockedFiles: number; +} => { + let modifiedCount = 0; + let lockedCount = 0; + + activeContexts.forEach((context) => { + modifiedCount += context.modifiedFiles.size; + lockedCount += context.lockedFiles.size; + }); + + return { + activeContexts: activeContexts.size, + totalModifiedFiles: modifiedCount, + totalLockedFiles: lockedCount, + }; +}; + +/** + * Create a wrapped tool executor that uses the agent context + */ +export const createContextualToolExecutor = ( + agentId: string, + executor: (args: TArgs) => Promise, + options: { + requiresWriteAccess?: (args: TArgs) => string | null; + } = {}, +): ((args: TArgs) => Promise) => { + return async (args: TArgs): Promise => { + // Check if write access is required + if (options.requiresWriteAccess) { + const filePath = options.requiresWriteAccess(args); + if (filePath) { + const access = await requestWriteAccess(agentId, filePath); + if (!access.granted) { + throw new Error(access.reason ?? "Write access denied"); + } + } + } + + // Execute the tool + const result = await executor(args); + + return result; + }; +}; diff --git a/src/services/plan-service.ts b/src/services/plan-service.ts index c14701e..e6d3cce 100644 --- a/src/services/plan-service.ts +++ b/src/services/plan-service.ts @@ -4,7 +4,7 @@ * Provides functions for agents to create and update plans */ -import { todoStore } from "@stores/todo-store"; +import { todoStore } from "@stores/core/todo-store"; import type { TodoStatus } from "@/types/todo"; /** diff --git a/src/services/plugin-service.ts b/src/services/plugin-service.ts index 308069b..905ed0e 100644 --- a/src/services/plugin-service.ts +++ b/src/services/plugin-service.ts @@ -10,7 +10,7 @@ import type { PluginCommandDefinition, PluginLoadResult, } from "@/types/plugin"; -import type { FunctionDefinition, ToolDefinition } from "@tools/types"; +import type { FunctionDefinition, ToolDefinition } from "@tools/core/types"; import type { HookDefinition } from "@/types/hooks"; import { discoverPlugins, diff --git a/src/services/reasoning-agent.ts b/src/services/reasoning-agent.ts index 585a786..b3a7ff5 100644 --- a/src/services/reasoning-agent.ts +++ b/src/services/reasoning-agent.ts @@ -24,9 +24,9 @@ import type { import { chat as providerChat } from "@providers/index"; import { getTool, getToolsForApi, refreshMCPTools } from "@tools/index"; import type { ToolContext, ToolCall, ToolResult } from "@/types/tools"; -import { initializePermissions } from "@services/permissions"; +import { initializePermissions } from "@services/core/permissions"; import { MAX_ITERATIONS } from "@constants/agent"; -import { usageStore } from "@stores/usage-store"; +import { usageStore } from "@stores/core/usage-store"; import type { TaskConstraints, CompressibleMessage, diff --git a/src/stores/core/index.ts b/src/stores/core/index.ts new file mode 100644 index 0000000..26c64a9 --- /dev/null +++ b/src/stores/core/index.ts @@ -0,0 +1,9 @@ +/** + * Store Core - Re-exports all stores + */ + +export * from "./vim-store"; +export * from "./theme-store"; +export * from "./usage-store"; +export * from "./todo-store"; +export * from "./multi-agent-store"; diff --git a/src/stores/core/multi-agent-store.ts b/src/stores/core/multi-agent-store.ts new file mode 100644 index 0000000..0d6bda1 --- /dev/null +++ b/src/stores/core/multi-agent-store.ts @@ -0,0 +1,376 @@ +/** + * Multi-Agent Store + * + * Zustand store for managing multi-agent execution state. + */ + +import { createStore } from "zustand/vanilla"; +import type { + AgentInstance, + AgentInstanceStatus, + AgentToolCall, + FileConflict, + MultiAgentRequest, + MultiAgentEvent, + ConflictResolutionResult, +} from "@/types/multi-agent"; +import type { ChatMessage } from "@/types/common"; +import { + AGENT_ID_PREFIX, + REQUEST_ID_PREFIX, +} from "@/constants/multi-agent"; + +/** + * Multi-agent store state + */ +interface MultiAgentState { + activeRequests: Map; + instances: Map; + conflicts: FileConflict[]; + isExecuting: boolean; + lastError: string | null; + events: MultiAgentEvent[]; +} + +/** + * Initial state + */ +const initialState: MultiAgentState = { + activeRequests: new Map(), + instances: new Map(), + conflicts: [], + isExecuting: false, + lastError: null, + events: [], +}; + +/** + * Vanilla Zustand store + */ +const store = createStore(() => ({ + ...initialState, +})); + +/** + * Generate unique agent ID + */ +const generateAgentId = (): string => { + return `${AGENT_ID_PREFIX}${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +}; + +/** + * Generate unique request ID + */ +const generateRequestId = (): string => { + return `${REQUEST_ID_PREFIX}${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +}; + +/** + * Add a new request + */ +const addRequest = (request: Omit): string => { + const id = generateRequestId(); + const fullRequest: MultiAgentRequest = { ...request, id }; + + store.setState((state) => { + const newRequests = new Map(state.activeRequests); + newRequests.set(id, fullRequest); + return { + activeRequests: newRequests, + isExecuting: true, + }; + }); + + return id; +}; + +/** + * Remove a request + */ +const removeRequest = (requestId: string): void => { + store.setState((state) => { + const newRequests = new Map(state.activeRequests); + newRequests.delete(requestId); + return { + activeRequests: newRequests, + isExecuting: newRequests.size > 0, + }; + }); +}; + +/** + * Add a new agent instance + */ +const addInstance = (instance: Omit): string => { + const id = generateAgentId(); + const fullInstance: AgentInstance = { ...instance, id }; + + store.setState((state) => { + const newInstances = new Map(state.instances); + newInstances.set(id, fullInstance); + return { instances: newInstances }; + }); + + addEvent({ type: "agent_started", agentId: id, timestamp: Date.now() }); + return id; +}; + +/** + * Update agent instance status + */ +const updateInstanceStatus = ( + agentId: string, + status: AgentInstanceStatus, + error?: string, +): void => { + store.setState((state) => { + const instance = state.instances.get(agentId); + if (!instance) return state; + + const newInstances = new Map(state.instances); + newInstances.set(agentId, { + ...instance, + status, + error, + completedAt: ["completed", "error", "cancelled"].includes(status) + ? Date.now() + : undefined, + }); + + return { instances: newInstances }; + }); +}; + +/** + * Add a message to agent conversation + */ +const addAgentMessage = (agentId: string, message: ChatMessage): void => { + store.setState((state) => { + const instance = state.instances.get(agentId); + if (!instance) return state; + + const newInstances = new Map(state.instances); + newInstances.set(agentId, { + ...instance, + conversation: { + ...instance.conversation, + messages: [...instance.conversation.messages, message], + }, + }); + + return { instances: newInstances }; + }); +}; + +/** + * Add a tool call to agent conversation + */ +const addToolCall = (agentId: string, toolCall: AgentToolCall): void => { + store.setState((state) => { + const instance = state.instances.get(agentId); + if (!instance) return state; + + const newInstances = new Map(state.instances); + newInstances.set(agentId, { + ...instance, + conversation: { + ...instance.conversation, + toolCalls: [...instance.conversation.toolCalls, toolCall], + }, + }); + + return { instances: newInstances }; + }); +}; + +/** + * Update tool call result + */ +const updateToolCallResult = ( + agentId: string, + toolCallId: string, + result: AgentToolCall["result"], +): void => { + store.setState((state) => { + const instance = state.instances.get(agentId); + if (!instance) return state; + + const newInstances = new Map(state.instances); + newInstances.set(agentId, { + ...instance, + conversation: { + ...instance.conversation, + toolCalls: instance.conversation.toolCalls.map((tc) => + tc.id === toolCallId ? { ...tc, result } : tc, + ), + }, + }); + + return { instances: newInstances }; + }); +}; + +/** + * Add a file to agent's modified files list + */ +const addModifiedFile = (agentId: string, filePath: string): void => { + store.setState((state) => { + const instance = state.instances.get(agentId); + if (!instance) return state; + + if (instance.modifiedFiles.includes(filePath)) return state; + + const newInstances = new Map(state.instances); + newInstances.set(agentId, { + ...instance, + modifiedFiles: [...instance.modifiedFiles, filePath], + }); + + return { instances: newInstances }; + }); +}; + +/** + * Add a conflict + */ +const addConflict = (conflict: FileConflict): void => { + store.setState((state) => ({ + conflicts: [...state.conflicts, conflict], + })); + + addEvent({ type: "conflict_detected", conflict, timestamp: Date.now() }); +}; + +/** + * Resolve a conflict + */ +const resolveConflict = ( + filePath: string, + resolution: ConflictResolutionResult, +): void => { + store.setState((state) => { + const updatedConflicts = state.conflicts.map((c) => + c.filePath === filePath ? { ...c, resolution } : c, + ); + return { conflicts: updatedConflicts }; + }); + + const conflict = store.getState().conflicts.find((c) => c.filePath === filePath); + if (conflict) { + addEvent({ + type: "conflict_resolved", + conflict: { ...conflict, resolution }, + timestamp: Date.now(), + }); + } +}; + +/** + * Add an event + */ +const addEvent = (event: MultiAgentEvent): void => { + store.setState((state) => ({ + events: [...state.events.slice(-99), event], // Keep last 100 events + })); +}; + +/** + * Set last error + */ +const setError = (error: string | null): void => { + store.setState({ lastError: error }); +}; + +/** + * Clear all state + */ +const clear = (): void => { + store.setState({ ...initialState }); +}; + +/** + * Get active agent instances + */ +const getActiveInstances = (): AgentInstance[] => { + const { instances } = store.getState(); + return Array.from(instances.values()).filter( + (i) => i.status === "running" || i.status === "waiting_conflict", + ); +}; + +/** + * Get instances by status + */ +const getInstancesByStatus = (status: AgentInstanceStatus): AgentInstance[] => { + const { instances } = store.getState(); + return Array.from(instances.values()).filter((i) => i.status === status); +}; + +/** + * Get unresolved conflicts + */ +const getUnresolvedConflicts = (): FileConflict[] => { + const { conflicts } = store.getState(); + return conflicts.filter((c) => !c.resolution); +}; + +/** + * Check if a file is being modified by any agent + */ +const isFileBeingModified = (filePath: string): boolean => { + const activeInstances = getActiveInstances(); + return activeInstances.some((i) => i.modifiedFiles.includes(filePath)); +}; + +/** + * Get agent modifying a file + */ +const getAgentModifyingFile = (filePath: string): AgentInstance | null => { + const activeInstances = getActiveInstances(); + return activeInstances.find((i) => i.modifiedFiles.includes(filePath)) ?? null; +}; + +/** + * Multi-agent store API + */ +export const multiAgentStore = { + // Request management + addRequest, + removeRequest, + + // Instance management + addInstance, + updateInstanceStatus, + addAgentMessage, + addToolCall, + updateToolCallResult, + addModifiedFile, + + // Conflict management + addConflict, + resolveConflict, + + // Events + addEvent, + + // Error handling + setError, + + // State management + clear, + + // Queries + getActiveInstances, + getInstancesByStatus, + getUnresolvedConflicts, + isFileBeingModified, + getAgentModifyingFile, + + // State access + getState: store.getState, + subscribe: store.subscribe, +}; + +// Export store for React hooks +export { store as multiAgentStoreVanilla }; +export type { MultiAgentState }; diff --git a/src/stores/theme-store.ts b/src/stores/core/theme-store.ts similarity index 100% rename from src/stores/theme-store.ts rename to src/stores/core/theme-store.ts diff --git a/src/stores/todo-store.ts b/src/stores/core/todo-store.ts similarity index 100% rename from src/stores/todo-store.ts rename to src/stores/core/todo-store.ts diff --git a/src/stores/usage-store.ts b/src/stores/core/usage-store.ts similarity index 100% rename from src/stores/usage-store.ts rename to src/stores/core/usage-store.ts diff --git a/src/stores/vim-store.ts b/src/stores/core/vim-store.ts similarity index 100% rename from src/stores/vim-store.ts rename to src/stores/core/vim-store.ts diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 0000000..b01896f --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1,7 @@ +/** + * Stores Module - Zustand state management + * + * Re-exports all stores from core/ + */ + +export * from "./core"; diff --git a/src/tools/apply-patch/execute.ts b/src/tools/apply-patch/execute.ts index 61f4dd8..68a6ca8 100644 --- a/src/tools/apply-patch/execute.ts +++ b/src/tools/apply-patch/execute.ts @@ -21,7 +21,7 @@ import type { PatchRollback, ParsedFilePatch, } from "@/types/apply-patch"; -import type { ToolContext, ToolResult } from "@tools/types"; +import type { ToolContext, ToolResult } from "@tools/core/types"; // Rollback storage (in-memory for session) const rollbackStore: Map = new Map(); diff --git a/src/tools/apply-patch/index.ts b/src/tools/apply-patch/index.ts index d779cec..4601c85 100644 --- a/src/tools/apply-patch/index.ts +++ b/src/tools/apply-patch/index.ts @@ -4,7 +4,7 @@ * Applies unified diff patches to files with fuzzy matching support. */ -import type { ToolDefinition } from "@tools/types"; +import type { ToolDefinition } from "@tools/core/types"; import { applyPatchParams } from "@tools/apply-patch/params"; import { executeApplyPatch } from "@tools/apply-patch/execute"; diff --git a/src/tools/bash/execute.ts b/src/tools/bash/execute.ts index b2abef9..53ac3a6 100644 --- a/src/tools/bash/execute.ts +++ b/src/tools/bash/execute.ts @@ -9,7 +9,7 @@ import { BASH_MESSAGES, BASH_DESCRIPTION, } from "@constants/bash"; -import { promptPermission } from "@services/permissions"; +import { promptPermission } from "@services/core/permissions"; import { bashParams } from "@tools/bash/params"; import { truncateOutput, diff --git a/src/tools/core/index.ts b/src/tools/core/index.ts new file mode 100644 index 0000000..ebc374a --- /dev/null +++ b/src/tools/core/index.ts @@ -0,0 +1,6 @@ +/** + * Tools Core - Registry and Types + */ + +export * from "./registry"; +export * from "./types"; diff --git a/src/tools/core/registry.ts b/src/tools/core/registry.ts new file mode 100644 index 0000000..95f7685 --- /dev/null +++ b/src/tools/core/registry.ts @@ -0,0 +1,219 @@ +/** + * Tool Registry + * + * Central registry for all available tools with support for + * built-in tools, MCP tools, and plugin tools. + */ + +import type { ToolDefinition, FunctionDefinition } from "@tools/core/types"; +import { toolToFunction } from "@tools/core/types"; +import { bashTool } from "@/tools/bash"; +import { readTool } from "@/tools/read"; +import { writeTool } from "@/tools/write"; +import { editTool } from "@/tools/edit"; +import { todoWriteTool } from "@/tools/todo-write"; +import { todoReadTool } from "@/tools/todo-read"; +import { globToolDefinition } from "@/tools/glob/definition"; +import { grepToolDefinition } from "@/tools/grep/definition"; +import { webSearchTool } from "@/tools/web-search"; +import { webFetchTool } from "@/tools/web-fetch"; +import { multiEditTool } from "@/tools/multi-edit"; +import { lspTool } from "@/tools/lsp"; +import { applyPatchTool } from "@/tools/apply-patch"; +import { + isMCPTool, + executeMCPTool, + getMCPToolsForApi, +} from "@/services/mcp/tools"; +import { + isPluginTool, + getPluginTool, + getPluginToolsForApi, +} from "@/services/plugin-service"; +import { z } from "zod"; + +/** + * All built-in tools + */ +export const tools: ToolDefinition[] = [ + bashTool, + readTool, + writeTool, + editTool, + multiEditTool, + globToolDefinition, + grepToolDefinition, + todoWriteTool, + todoReadTool, + webSearchTool, + webFetchTool, + lspTool, + applyPatchTool, +]; + +/** + * Tools that are read-only (allowed in chat mode) + */ +const READ_ONLY_TOOLS = new Set([ + "read", + "glob", + "grep", + "todo_read", + "web_search", + "web_fetch", + "lsp", +]); + +/** + * Map of tools by name for fast lookup + */ +export const toolMap: Map = new Map( + tools.map((t) => [t.name, t]), +); + +/** + * Cached MCP tools + */ +let mcpToolsCache: Awaited> | null = null; + +/** + * Get tool by name (including MCP tools and plugin tools) + */ +export const getTool = (name: string): ToolDefinition | undefined => { + const builtInTool = toolMap.get(name); + if (builtInTool) { + return builtInTool; + } + + if (isPluginTool(name)) { + return getPluginTool(name); + } + + if (isMCPTool(name)) { + return { + name, + description: `MCP tool: ${name}`, + parameters: z.object({}).passthrough(), + execute: async (args) => { + const result = await executeMCPTool( + name, + args as Record, + ); + return { + success: result.success, + title: name, + output: result.output, + error: result.error, + }; + }, + }; + } + + return undefined; +}; + +/** + * Get all tools as OpenAI function definitions + */ +export const getToolFunctions = (): FunctionDefinition[] => { + return tools.map(toolToFunction); +}; + +/** + * Filter tools based on chat mode (read-only vs full access) + */ +const filterToolsForMode = ( + toolList: ToolDefinition[], + chatMode: boolean, +): ToolDefinition[] => { + if (!chatMode) return toolList; + return toolList.filter((t) => READ_ONLY_TOOLS.has(t.name)); +}; + +/** + * Get tools as format expected by Copilot/OpenAI API + * This includes both built-in tools and MCP tools + * @param chatMode - If true, only return read-only tools (no file modifications) + */ +export const getToolsForApiAsync = async ( + chatMode = false, +): Promise< + { + type: "function"; + function: FunctionDefinition; + }[] +> => { + const filteredTools = filterToolsForMode(tools, chatMode); + const builtInTools = filteredTools.map((t) => ({ + type: "function" as const, + function: toolToFunction(t), + })); + + if (chatMode) { + return builtInTools; + } + + try { + mcpToolsCache = await getMCPToolsForApi(); + const pluginTools = getPluginToolsForApi(); + return [...builtInTools, ...pluginTools, ...mcpToolsCache]; + } catch { + const pluginTools = getPluginToolsForApi(); + return [...builtInTools, ...pluginTools]; + } +}; + +/** + * Get tools synchronously (uses cached MCP tools if available) + * @param chatMode - If true, only return read-only tools (no file modifications) + */ +export const getToolsForApi = ( + chatMode = false, +): { + type: "function"; + function: FunctionDefinition; +}[] => { + const filteredTools = filterToolsForMode(tools, chatMode); + const builtInTools = filteredTools.map((t) => ({ + type: "function" as const, + function: toolToFunction(t), + })); + + if (chatMode) { + return builtInTools; + } + + const pluginTools = getPluginToolsForApi(); + + if (mcpToolsCache) { + return [...builtInTools, ...pluginTools, ...mcpToolsCache]; + } + + return [...builtInTools, ...pluginTools]; +}; + +/** + * Refresh MCP tools cache + * Returns information about the refresh result for logging + */ +export const refreshMCPTools = async (): Promise<{ + success: boolean; + toolCount: number; + error?: string; +}> => { + try { + mcpToolsCache = await getMCPToolsForApi(); + return { + success: true, + toolCount: mcpToolsCache.length, + }; + } catch (err) { + mcpToolsCache = null; + const errorMessage = err instanceof Error ? err.message : String(err); + return { + success: false, + toolCount: 0, + error: errorMessage, + }; + } +}; diff --git a/src/tools/types.ts b/src/tools/core/types.ts similarity index 100% rename from src/tools/types.ts rename to src/tools/core/types.ts diff --git a/src/tools/edit/execute.ts b/src/tools/edit/execute.ts index e0fd20f..d124b6a 100644 --- a/src/tools/edit/execute.ts +++ b/src/tools/edit/execute.ts @@ -6,7 +6,7 @@ import fs from "fs/promises"; import path from "path"; import { EDIT_MESSAGES, EDIT_TITLES, EDIT_DESCRIPTION } from "@constants/edit"; -import { isFileOpAllowed, promptFilePermission } from "@services/permissions"; +import { isFileOpAllowed, promptFilePermission } from "@services/core/permissions"; import { formatDiff } from "@utils/diff/format"; import { generateDiff } from "@utils/diff/generate"; import { editParams } from "@tools/edit/params"; diff --git a/src/tools/index.ts b/src/tools/index.ts index 73488f8..358ce39 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -2,7 +2,7 @@ * Tool registry - exports all available tools */ -export * from "@tools/types"; +export * from "@tools/core/types"; export { bashTool } from "@tools/bash"; export { readTool } from "@tools/read"; export { writeTool } from "@tools/write"; @@ -17,8 +17,8 @@ export { multiEditTool } from "@tools/multi-edit"; export { lspTool } from "@tools/lsp"; export { applyPatchTool } from "@tools/apply-patch"; -import type { ToolDefinition, FunctionDefinition } from "@tools/types"; -import { toolToFunction } from "@tools/types"; +import type { ToolDefinition, FunctionDefinition } from "@tools/core/types"; +import { toolToFunction } from "@tools/core/types"; import { bashTool } from "@tools/bash"; import { readTool } from "@tools/read"; import { writeTool } from "@tools/write"; diff --git a/src/tools/lsp.ts b/src/tools/lsp.ts index f531162..261cf82 100644 --- a/src/tools/lsp.ts +++ b/src/tools/lsp.ts @@ -17,7 +17,7 @@ import { type DocumentSymbol, type Hover, } from "@services/lsp/index"; -import type { ToolDefinition } from "@tools/types"; +import type { ToolDefinition } from "@tools/core/types"; import fs from "fs/promises"; const PositionSchema = z.object({ diff --git a/src/tools/multi-edit/execute.ts b/src/tools/multi-edit/execute.ts index ce32ad6..bc725e4 100644 --- a/src/tools/multi-edit/execute.ts +++ b/src/tools/multi-edit/execute.ts @@ -13,7 +13,7 @@ import { MULTI_EDIT_TITLES, MULTI_EDIT_DESCRIPTION, } from "@constants/multi-edit"; -import { isFileOpAllowed, promptFilePermission } from "@services/permissions"; +import { isFileOpAllowed, promptFilePermission } from "@services/core/permissions"; import { formatDiff } from "@utils/diff/format"; import { generateDiff } from "@utils/diff/generate"; import { multiEditParams } from "@tools/multi-edit/params"; diff --git a/src/tools/read/execute.ts b/src/tools/read/execute.ts index 5fc74e3..d473114 100644 --- a/src/tools/read/execute.ts +++ b/src/tools/read/execute.ts @@ -11,7 +11,7 @@ import { READ_TITLES, READ_DESCRIPTION, } from "@constants/read"; -import { isFileOpAllowed, promptFilePermission } from "@services/permissions"; +import { isFileOpAllowed, promptFilePermission } from "@services/core/permissions"; import { readParams } from "@tools/read/params"; import { processLines } from "@tools/read/format"; import type { diff --git a/src/tools/todo-read.ts b/src/tools/todo-read.ts index 084f808..7066a16 100644 --- a/src/tools/todo-read.ts +++ b/src/tools/todo-read.ts @@ -5,8 +5,8 @@ */ import { z } from "zod"; -import { todoStore } from "@stores/todo-store"; -import type { ToolDefinition } from "@tools/types"; +import { todoStore } from "@stores/core/todo-store"; +import type { ToolDefinition } from "@tools/core/types"; const parametersSchema = z.object({}); diff --git a/src/tools/todo-write.ts b/src/tools/todo-write.ts index 0ecf0a3..eb1c1dc 100644 --- a/src/tools/todo-write.ts +++ b/src/tools/todo-write.ts @@ -5,8 +5,8 @@ */ import { z } from "zod"; -import { todoStore } from "@stores/todo-store"; -import type { ToolDefinition } from "@tools/types"; +import { todoStore } from "@stores/core/todo-store"; +import type { ToolDefinition } from "@tools/core/types"; import type { TodoStatus } from "@/types/todo"; const TodoItemSchema = z.object({ diff --git a/src/tools/write/execute.ts b/src/tools/write/execute.ts index fdce4a2..29d9a7d 100644 --- a/src/tools/write/execute.ts +++ b/src/tools/write/execute.ts @@ -10,7 +10,7 @@ import { WRITE_TITLES, WRITE_DESCRIPTION, } from "@constants/write"; -import { isFileOpAllowed, promptFilePermission } from "@services/permissions"; +import { isFileOpAllowed, promptFilePermission } from "@services/core/permissions"; import { formatDiff } from "@utils/diff/format"; import { generateDiff } from "@utils/diff/generate"; import { writeParams } from "@tools/write/params"; diff --git a/src/tui-solid/components/debug-log-panel.tsx b/src/tui-solid/components/debug-log-panel.tsx deleted file mode 100644 index fb1a2bf..0000000 --- a/src/tui-solid/components/debug-log-panel.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { For, createSignal, onMount, onCleanup } from "solid-js"; -import { useKeyboard } from "@opentui/solid"; -import { TextAttributes } from "@opentui/core"; -import type { ScrollBoxRenderable } from "@opentui/core"; -import { useTheme } from "@tui-solid/context/theme"; -import { useAppStore } from "@tui-solid/context/app"; - -const SCROLL_LINES = 2; - -interface DebugEntry { - id: string; - timestamp: number; - type: "api" | "stream" | "tool" | "state" | "error" | "info" | "render"; - message: string; -} - -// Global debug log store -let debugEntries: DebugEntry[] = []; -let debugIdCounter = 0; -let listeners: Array<() => void> = []; - -const notifyListeners = (): void => { - for (const listener of listeners) { - listener(); - } -}; - -export const addDebugLog = ( - type: DebugEntry["type"], - message: string, -): void => { - const entry: DebugEntry = { - id: `debug-${++debugIdCounter}`, - timestamp: Date.now(), - type, - message, - }; - debugEntries.push(entry); - // Keep only last 500 entries - if (debugEntries.length > 500) { - debugEntries = debugEntries.slice(-500); - } - notifyListeners(); -}; - -export const clearDebugLogs = (): void => { - debugEntries = []; - debugIdCounter = 0; - notifyListeners(); -}; - -export function DebugLogPanel() { - const theme = useTheme(); - const app = useAppStore(); - let scrollboxRef: ScrollBoxRenderable | undefined; - const [entries, setEntries] = createSignal([...debugEntries]); - const [stickyEnabled, setStickyEnabled] = createSignal(true); - - const isActive = () => app.debugLogVisible(); - - onMount(() => { - const updateEntries = (): void => { - setEntries([...debugEntries]); - if (stickyEnabled() && scrollboxRef) { - scrollboxRef.scrollTo(Infinity); - } - }; - listeners.push(updateEntries); - - onCleanup(() => { - listeners = listeners.filter((l) => l !== updateEntries); - }); - }); - - const getTypeColor = (type: DebugEntry["type"]): string => { - const colorMap: Record = { - api: theme.colors.info, - stream: theme.colors.success, - tool: theme.colors.warning, - state: theme.colors.accent, - error: theme.colors.error, - info: theme.colors.textDim, - render: theme.colors.primary, - }; - return colorMap[type]; - }; - - const getTypeLabel = (type: DebugEntry["type"]): string => { - const labelMap: Record = { - api: "API", - stream: "STR", - tool: "TUL", - state: "STA", - error: "ERR", - info: "INF", - render: "RND", - }; - return labelMap[type]; - }; - - const formatTime = (timestamp: number): string => { - const date = new Date(timestamp); - return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${date.getSeconds().toString().padStart(2, "0")}`; - }; - - const scrollUp = (): void => { - if (!scrollboxRef) return; - setStickyEnabled(false); - scrollboxRef.scrollBy(-SCROLL_LINES); - }; - - const scrollDown = (): void => { - if (!scrollboxRef) return; - scrollboxRef.scrollBy(SCROLL_LINES); - - const isAtBottom = - scrollboxRef.scrollTop >= - scrollboxRef.content.height - scrollboxRef.viewport.height - 1; - if (isAtBottom) { - setStickyEnabled(true); - } - }; - - useKeyboard((evt) => { - if (!isActive()) return; - - if (evt.shift && evt.name === "pageup") { - scrollUp(); - evt.preventDefault(); - evt.stopPropagation(); - return; - } - - if (evt.shift && evt.name === "pagedown") { - scrollDown(); - evt.preventDefault(); - evt.stopPropagation(); - } - }); - - const truncateMessage = (msg: string, maxLen: number): string => { - if (msg.length <= maxLen) return msg; - return msg.substring(0, maxLen - 3) + "..."; - }; - - return ( - - - - Debug Logs ({entries().length}) - - - - - - - {(entry) => ( - - - {formatTime(entry.timestamp)}{" "} - - - [{getTypeLabel(entry.type)}]{" "} - - - {truncateMessage(entry.message, 50)} - - - )} - - - - - - Shift+PgUp/PgDn scroll - - - ); -} diff --git a/src/tui-solid/components/bouncing-loader.tsx b/src/tui-solid/components/feedback/bouncing-loader.tsx similarity index 100% rename from src/tui-solid/components/bouncing-loader.tsx rename to src/tui-solid/components/feedback/bouncing-loader.tsx diff --git a/src/tui-solid/components/feedback/index.ts b/src/tui-solid/components/feedback/index.ts new file mode 100644 index 0000000..9206f66 --- /dev/null +++ b/src/tui-solid/components/feedback/index.ts @@ -0,0 +1,6 @@ +/** + * Feedback Components + */ + +export * from "./thinking-indicator"; +export * from "./bouncing-loader"; diff --git a/src/tui-solid/components/thinking-indicator.tsx b/src/tui-solid/components/feedback/thinking-indicator.tsx similarity index 100% rename from src/tui-solid/components/thinking-indicator.tsx rename to src/tui-solid/components/feedback/thinking-indicator.tsx diff --git a/src/tui-solid/components/index.ts b/src/tui-solid/components/index.ts index 6188478..4523a1a 100644 --- a/src/tui-solid/components/index.ts +++ b/src/tui-solid/components/index.ts @@ -1,27 +1,47 @@ -export { StatusBar } from "./status-bar"; -export { Logo } from "./logo"; -export { ThinkingIndicator } from "./thinking-indicator"; -export { BouncingLoader } from "./bouncing-loader"; -export { LogPanel } from "./log-panel"; -export { LogEntryDisplay } from "./log-entry"; -export { StreamingMessage } from "./streaming-message"; -export { InputArea } from "./input-area"; -export { Header } from "./header"; -export { CommandMenu, SLASH_COMMANDS } from "./command-menu"; -export { ModelSelect } from "./model-select"; -export { AgentSelect } from "./agent-select"; -export { ThemeSelect } from "./theme-select"; -export { MCPSelect } from "./mcp-select"; -export { MCPAddForm } from "./mcp-add-form"; -export { ModeSelect } from "./mode-select"; -export { ProviderSelect } from "./provider-select"; -export { FilePicker } from "./file-picker"; -export { SelectMenu } from "./select-menu"; -export type { SelectOption } from "./select-menu"; -export { PermissionModal } from "./permission-modal"; -export { LearningModal } from "./learning-modal"; -export { HelpMenu } from "./help-menu"; -export { HelpDetail } from "./help-detail"; -export { TodoPanel } from "./todo-panel"; -export type { TodoItem, Plan } from "./todo-panel"; -export { DiffView, parseDiffOutput, isDiffContent } from "./diff-view"; +// Layout components +export { StatusBar } from "./layout/status-bar"; +export { Logo } from "./layout/logo"; +export { StreamingMessage } from "./layout/streaming-message"; +export { Header } from "./layout/header"; + +// Feedback components +export { ThinkingIndicator } from "./feedback/thinking-indicator"; +export { BouncingLoader } from "./feedback/bouncing-loader"; + +// Log components +export { LogPanel } from "./logs/log-panel"; +export { LogEntryDisplay } from "./logs/log-entry"; +export { addDebugLog, DebugLogPanel } from "./logs/debug-log-panel"; + +// Input components +export { InputArea } from "./inputs/input-area"; +export { FilePicker } from "./inputs/file-picker"; +export { MCPAddForm } from "./inputs/mcp-add-form"; + +// Menu components +export { CommandMenu, SLASH_COMMANDS } from "./menu/command-menu"; +export { SelectMenu } from "./menu/select-menu"; +export type { SelectOption } from "./menu/select-menu"; +export { HelpMenu } from "./menu/help-menu"; +export { BrainMenu } from "./menu/brain-menu"; + +// Submenu components +export { ModelSelect } from "./submenu/model-select"; +export { AgentSelect } from "./submenu/agent-select"; +export { ThemeSelect } from "./submenu/theme-select"; +export { MCPSelect } from "./submenu/mcp-select"; +export { ModeSelect } from "./submenu/mode-select"; +export { ProviderSelect } from "./submenu/provider-select"; + +// Modal components +export { PermissionModal } from "./modals/permission-modal"; +export { LearningModal } from "./modals/learning-modal"; +export { CenteredModal } from "./modals/centered-modal"; +export { ConflictResolver, ConflictIndicator } from "./modals/conflict-resolver"; + +// Panel components +export { HelpDetail } from "./panels/help-detail"; +export { TodoPanel } from "./panels/todo-panel"; +export type { TodoItem, Plan } from "./panels/todo-panel"; +export { DiffView, parseDiffOutput, isDiffContent } from "./panels/diff-view"; +export { MultiAgentPanel } from "./panels/multi-agent-panel"; diff --git a/src/tui-solid/components/file-picker.tsx b/src/tui-solid/components/inputs/file-picker.tsx similarity index 100% rename from src/tui-solid/components/file-picker.tsx rename to src/tui-solid/components/inputs/file-picker.tsx diff --git a/src/tui-solid/components/inputs/index.ts b/src/tui-solid/components/inputs/index.ts new file mode 100644 index 0000000..91e5efc --- /dev/null +++ b/src/tui-solid/components/inputs/index.ts @@ -0,0 +1,7 @@ +/** + * Input Components + */ + +export * from "./input-area"; +export * from "./file-picker"; +export * from "./mcp-add-form"; diff --git a/src/tui-solid/components/input-area.tsx b/src/tui-solid/components/inputs/input-area.tsx similarity index 100% rename from src/tui-solid/components/input-area.tsx rename to src/tui-solid/components/inputs/input-area.tsx diff --git a/src/tui-solid/components/mcp-add-form.tsx b/src/tui-solid/components/inputs/mcp-add-form.tsx similarity index 100% rename from src/tui-solid/components/mcp-add-form.tsx rename to src/tui-solid/components/inputs/mcp-add-form.tsx diff --git a/src/tui-solid/components/header.tsx b/src/tui-solid/components/layout/header.tsx similarity index 100% rename from src/tui-solid/components/header.tsx rename to src/tui-solid/components/layout/header.tsx diff --git a/src/tui-solid/components/layout/index.ts b/src/tui-solid/components/layout/index.ts new file mode 100644 index 0000000..aaa55ac --- /dev/null +++ b/src/tui-solid/components/layout/index.ts @@ -0,0 +1,8 @@ +/** + * Layout Components + */ + +export * from "./header"; +export * from "./status-bar"; +export * from "./logo"; +export * from "./streaming-message"; diff --git a/src/tui-solid/components/logo.tsx b/src/tui-solid/components/layout/logo.tsx similarity index 100% rename from src/tui-solid/components/logo.tsx rename to src/tui-solid/components/layout/logo.tsx diff --git a/src/tui-solid/components/status-bar.tsx b/src/tui-solid/components/layout/status-bar.tsx similarity index 100% rename from src/tui-solid/components/status-bar.tsx rename to src/tui-solid/components/layout/status-bar.tsx diff --git a/src/tui-solid/components/streaming-message.tsx b/src/tui-solid/components/layout/streaming-message.tsx similarity index 97% rename from src/tui-solid/components/streaming-message.tsx rename to src/tui-solid/components/layout/streaming-message.tsx index e77a0a1..44049c1 100644 --- a/src/tui-solid/components/streaming-message.tsx +++ b/src/tui-solid/components/layout/streaming-message.tsx @@ -4,7 +4,7 @@ import { useTheme } from "@tui-solid/context/theme"; import { useAppStore } from "@tui-solid/context/app"; import type { LogEntry } from "@/types/tui"; import { Spinner } from "@tui-solid/ui/spinner"; -import { addDebugLog } from "@tui-solid/components/debug-log-panel"; +import { addDebugLog } from "@tui-solid/components/logs/debug-log-panel"; interface StreamingMessageProps { entry: LogEntry; diff --git a/src/tui-solid/components/log-entry.tsx b/src/tui-solid/components/log-entry.tsx deleted file mode 100644 index 4c36dff..0000000 --- a/src/tui-solid/components/log-entry.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { Show, Switch, Match, For } from "solid-js"; -import { TextAttributes } from "@opentui/core"; -import { useTheme } from "@tui-solid/context/theme"; -import type { LogEntry, ToolStatus } from "@/types/tui"; -import { - TOOL_STATUS_ICONS, - TOOL_STATUS_COLORS, -} from "@constants/tui-components"; -import { DiffView } from "@tui-solid/components/diff-view"; -import { StreamingMessage } from "@tui-solid/components/streaming-message"; -import { parseDiffOutput, isDiffContent } from "@utils/diff/index"; - -interface LogEntryDisplayProps { - entry: LogEntry; -} - -function UserEntry(props: { entry: LogEntry }) { - const theme = useTheme(); - return ( - - - You - - - {props.entry.content} - - - ); -} - -function AssistantEntry(props: { entry: LogEntry }) { - const theme = useTheme(); - return ( - - - CodeTyper - - - {props.entry.content} - - - ); -} - -function ErrorEntry(props: { entry: LogEntry }) { - const theme = useTheme(); - return ( - - ✗ Error: {props.entry.content} - - ); -} - -function SystemEntry(props: { entry: LogEntry }) { - const theme = useTheme(); - return ( - - ⚙ {props.entry.content} - - ); -} - -function ThinkingEntry(props: { entry: LogEntry }) { - const theme = useTheme(); - return ( - - ● {props.entry.content} - - ); -} - -function ToolEntry(props: { entry: LogEntry }) { - const theme = useTheme(); - const toolStatus = (): ToolStatus => - props.entry.metadata?.toolStatus ?? "pending"; - const statusIcon = () => TOOL_STATUS_ICONS[toolStatus()]; - const statusColor = (): string => { - const colorKey = TOOL_STATUS_COLORS[toolStatus()]; - const color = theme.colors[colorKey as keyof typeof theme.colors]; - if (typeof color === "string") return color; - return theme.colors.textDim; - }; - - const hasDiff = () => - props.entry.metadata?.diffData?.isDiff || - isDiffContent(props.entry.content); - - const isMultiline = () => props.entry.content.includes("\n"); - const lines = () => props.entry.content.split("\n"); - - return ( - - } - > - - - - - - - - ); -} - -function DiffToolEntry(props: { - entry: LogEntry; - statusIcon: string; - statusColor: string; -}) { - const theme = useTheme(); - const diffData = () => parseDiffOutput(props.entry.content); - - return ( - - - {props.statusIcon} - - {props.entry.metadata?.toolName ?? "tool"} - - - : - - {props.entry.metadata?.toolDescription} - - - - - - - - ); -} - -function MultilineToolEntry(props: { - entry: LogEntry; - statusIcon: string; - statusColor: string; - lines: string[]; -}) { - const theme = useTheme(); - const hasDescription = () => Boolean(props.entry.metadata?.toolDescription); - - return ( - - - {props.statusIcon} - - {props.entry.metadata?.toolName ?? "tool"} - - : - - {props.entry.metadata?.toolDescription ?? props.lines[0]} - - - - - {(line) => {line}} - - - - ); -} - -function DefaultToolEntry(props: { - entry: LogEntry; - statusIcon: string; - statusColor: string; -}) { - const theme = useTheme(); - return ( - - {props.statusIcon} - - {props.entry.metadata?.toolName ?? "tool"} - - : - {props.entry.content} - - ); -} - -function DefaultEntry(props: { entry: LogEntry }) { - return ( - - {props.entry.content} - - ); -} - -export function LogEntryDisplay(props: LogEntryDisplayProps) { - return ( - }> - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/src/tui-solid/components/log-panel.tsx b/src/tui-solid/components/log-panel.tsx deleted file mode 100644 index 209c76a..0000000 --- a/src/tui-solid/components/log-panel.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { createMemo, createSignal, For, Show, onMount, onCleanup } from "solid-js"; -import { useKeyboard } from "@opentui/solid"; -import type { ScrollBoxRenderable } from "@opentui/core"; -import { useTheme } from "@tui-solid/context/theme"; -import { useAppStore } from "@tui-solid/context/app"; -import { LogEntryDisplay } from "@tui-solid/components/log-entry"; -import { ASCII_LOGO, ASCII_LOGO_GRADIENT, HOME_VARS } from "@constants/home"; - -const SCROLL_LINES = 3; -const MOUSE_ENABLE = "\x1b[?1000h\x1b[?1006h"; -const MOUSE_DISABLE = "\x1b[?1000l\x1b[?1006l"; -const SGR_MOUSE_PATTERN = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/; - -const parseMouseScroll = (data: string): "up" | "down" | null => { - const match = data.match(SGR_MOUSE_PATTERN); - if (!match) return null; - - const button = parseInt(match[1], 10); - if (button === 64) return "up"; - if (button === 65) return "down"; - return null; -}; - -export function LogPanel() { - const theme = useTheme(); - const app = useAppStore(); - let scrollboxRef: ScrollBoxRenderable | undefined; - const [stickyEnabled, setStickyEnabled] = createSignal(true); - - const logs = createMemo(() => { - return app.logs().filter((entry) => { - if (entry.type !== "tool") return true; - if (entry.metadata?.quiet) return false; - return true; - }); - }); - - const hasContent = createMemo(() => logs().length > 0); - - const canScroll = createMemo(() => { - const mode = app.mode(); - return ( - mode === "idle" || - mode === "thinking" || - mode === "tool_execution" || - mode === "editing" - ); - }); - - const scrollUp = (): void => { - if (!scrollboxRef) return; - setStickyEnabled(false); - scrollboxRef.scrollBy(-SCROLL_LINES); - }; - - const scrollDown = (): void => { - if (!scrollboxRef) return; - scrollboxRef.scrollBy(SCROLL_LINES); - - const isAtBottom = - scrollboxRef.scrollTop >= - scrollboxRef.content.height - scrollboxRef.viewport.height - 1; - if (isAtBottom) { - setStickyEnabled(true); - } - }; - - const scrollToBottom = (): void => { - if (!scrollboxRef) return; - scrollboxRef.scrollTo(Infinity); - setStickyEnabled(true); - }; - - useKeyboard((evt) => { - if (!canScroll()) return; - - if (evt.shift && evt.name === "up") { - scrollUp(); - evt.preventDefault(); - evt.stopPropagation(); - return; - } - - if (evt.shift && evt.name === "down") { - scrollDown(); - evt.preventDefault(); - evt.stopPropagation(); - return; - } - - if (evt.shift && evt.name === "home") { - if (scrollboxRef) { - setStickyEnabled(false); - scrollboxRef.scrollTo(0); - } - evt.preventDefault(); - evt.stopPropagation(); - return; - } - - if (evt.shift && evt.name === "end") { - scrollToBottom(); - evt.preventDefault(); - evt.stopPropagation(); - return; - } - }); - - onMount(() => { - process.stdout.write(MOUSE_ENABLE); - - const handleData = (data: Buffer): void => { - if (!canScroll()) return; - - const str = data.toString(); - const direction = parseMouseScroll(str); - - if (direction === "up") { - scrollUp(); - } else if (direction === "down") { - scrollDown(); - } - }; - - process.stdin.on("data", handleData); - - onCleanup(() => { - process.stdout.write(MOUSE_DISABLE); - process.stdin.off("data", handleData); - }); - }); - - return ( - - - - {(line, index) => ( - - {line} - - )} - - - {HOME_VARS.subTitle} - - - } - > - - - - {(entry) => } - - - - - - ); -} diff --git a/src/tui-solid/components/brain-menu.tsx b/src/tui-solid/components/menu/brain-menu.tsx similarity index 100% rename from src/tui-solid/components/brain-menu.tsx rename to src/tui-solid/components/menu/brain-menu.tsx diff --git a/src/tui-solid/components/command-menu.tsx b/src/tui-solid/components/menu/command-menu.tsx similarity index 100% rename from src/tui-solid/components/command-menu.tsx rename to src/tui-solid/components/menu/command-menu.tsx diff --git a/src/tui-solid/components/help-menu.tsx b/src/tui-solid/components/menu/help-menu.tsx similarity index 100% rename from src/tui-solid/components/help-menu.tsx rename to src/tui-solid/components/menu/help-menu.tsx diff --git a/src/tui-solid/components/menu/index.ts b/src/tui-solid/components/menu/index.ts new file mode 100644 index 0000000..39386fd --- /dev/null +++ b/src/tui-solid/components/menu/index.ts @@ -0,0 +1,8 @@ +/** + * Menu Components + */ + +export * from "./select-menu"; +export * from "./command-menu"; +export * from "./help-menu"; +export * from "./brain-menu"; diff --git a/src/tui-solid/components/select-menu.tsx b/src/tui-solid/components/menu/select-menu.tsx similarity index 100% rename from src/tui-solid/components/select-menu.tsx rename to src/tui-solid/components/menu/select-menu.tsx diff --git a/src/tui-solid/components/centered-modal.tsx b/src/tui-solid/components/modals/centered-modal.tsx similarity index 100% rename from src/tui-solid/components/centered-modal.tsx rename to src/tui-solid/components/modals/centered-modal.tsx diff --git a/src/tui-solid/components/modals/conflict-resolver.tsx b/src/tui-solid/components/modals/conflict-resolver.tsx new file mode 100644 index 0000000..f998177 --- /dev/null +++ b/src/tui-solid/components/modals/conflict-resolver.tsx @@ -0,0 +1,188 @@ +/** + * Conflict Resolver + * + * UI component for displaying and resolving file conflicts between agents. + */ + +import { For, Show, createSignal, createMemo, onMount, onCleanup } from "solid-js"; +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; +import { multiAgentStore } from "@stores/core/multi-agent-store"; +import type { FileConflict, ConflictStrategy } from "@/types/multi-agent"; +import { CONFLICT_STRATEGY_DESCRIPTIONS } from "@constants/multi-agent"; + +interface ConflictResolverProps { + visible?: boolean; + onResolve?: (filePath: string, strategy: ConflictStrategy) => void; + onDismiss?: () => void; +} + +const STRATEGY_OPTIONS: Array<{ value: ConflictStrategy; label: string }> = [ + { value: "serialize", label: "Wait" }, + { value: "abort-newer", label: "Abort Newer" }, + { value: "merge-results", label: "Merge" }, + { value: "isolated", label: "Isolate" }, +]; + +export function ConflictResolver(props: ConflictResolverProps) { + const theme = useTheme(); + const visible = () => props.visible ?? true; + + const [conflicts, setConflicts] = createSignal([]); + const [selectedConflictIndex, setSelectedConflictIndex] = createSignal(0); + const [selectedStrategyIndex] = createSignal(0); + + onMount(() => { + const unsubscribe = multiAgentStore.subscribe((state) => { + const unresolvedConflicts = state.conflicts.filter((c) => !c.resolution); + setConflicts(unresolvedConflicts); + + // Reset selection if current conflict was resolved + if (selectedConflictIndex() >= unresolvedConflicts.length) { + setSelectedConflictIndex(Math.max(0, unresolvedConflicts.length - 1)); + } + }); + + onCleanup(unsubscribe); + }); + + const currentConflict = createMemo(() => conflicts()[selectedConflictIndex()]); + + const getAgentNames = (agentIds: string[]): string[] => { + const state = multiAgentStore.getState(); + return agentIds.map((id) => { + const instance = state.instances.get(id); + return instance?.definition.name ?? id; + }); + }; + + const formatTime = (timestamp: number): string => { + const date = new Date(timestamp); + return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + }; + + const selectedStrategy = createMemo(() => STRATEGY_OPTIONS[selectedStrategyIndex()]); + + return ( + 0}> + + {/* Header */} + + + ⚠ File Conflict Detected + + + {selectedConflictIndex() + 1}/{conflicts().length} + + + + {/* Conflict Details */} + + + + File: + + {currentConflict()!.filePath} + + + + + Agents: + + {getAgentNames(currentConflict()!.conflictingAgentIds).join(" vs ")} + + + + + Detected: + + {formatTime(currentConflict()!.detectedAt)} + + + + + + {/* Resolution Options */} + + + Resolution Strategy: + + + + {(option, index) => ( + + + {index() === selectedStrategyIndex() ? "▸" : " "} + + + {option.label} + + + )} + + + + {/* Strategy Description */} + + + {CONFLICT_STRATEGY_DESCRIPTIONS[selectedStrategy().value]} + + + + {/* Actions */} + + + [↑/↓] Select [Enter] Resolve [Esc] Dismiss + + + + + ); +} + +/** + * Compact conflict indicator for status bar + */ +export function ConflictIndicator() { + const theme = useTheme(); + const [conflictCount, setConflictCount] = createSignal(0); + + onMount(() => { + const unsubscribe = multiAgentStore.subscribe((state) => { + const unresolvedCount = state.conflicts.filter((c) => !c.resolution).length; + setConflictCount(unresolvedCount); + }); + + onCleanup(unsubscribe); + }); + + return ( + 0}> + + + ⚠ {conflictCount()} conflict{conflictCount() > 1 ? "s" : ""} + + + + ); +} diff --git a/src/tui-solid/components/modals/index.ts b/src/tui-solid/components/modals/index.ts new file mode 100644 index 0000000..bb76292 --- /dev/null +++ b/src/tui-solid/components/modals/index.ts @@ -0,0 +1,8 @@ +/** + * Modal Components + */ + +export * from "./centered-modal"; +export * from "./permission-modal"; +export * from "./learning-modal"; +export * from "./conflict-resolver"; diff --git a/src/tui-solid/components/learning-modal.tsx b/src/tui-solid/components/modals/learning-modal.tsx similarity index 100% rename from src/tui-solid/components/learning-modal.tsx rename to src/tui-solid/components/modals/learning-modal.tsx diff --git a/src/tui-solid/components/permission-modal.tsx b/src/tui-solid/components/modals/permission-modal.tsx similarity index 100% rename from src/tui-solid/components/permission-modal.tsx rename to src/tui-solid/components/modals/permission-modal.tsx diff --git a/src/tui-solid/components/diff-view.tsx b/src/tui-solid/components/panels/diff-view.tsx similarity index 100% rename from src/tui-solid/components/diff-view.tsx rename to src/tui-solid/components/panels/diff-view.tsx diff --git a/src/tui-solid/components/help-detail.tsx b/src/tui-solid/components/panels/help-detail.tsx similarity index 100% rename from src/tui-solid/components/help-detail.tsx rename to src/tui-solid/components/panels/help-detail.tsx diff --git a/src/tui-solid/components/panels/index.ts b/src/tui-solid/components/panels/index.ts new file mode 100644 index 0000000..7e4328c --- /dev/null +++ b/src/tui-solid/components/panels/index.ts @@ -0,0 +1,8 @@ +/** + * Panel Components + */ + +export * from "./todo-panel"; +export * from "./diff-view"; +export * from "./multi-agent-panel"; +export * from "./help-detail"; diff --git a/src/tui-solid/components/panels/multi-agent-panel.tsx b/src/tui-solid/components/panels/multi-agent-panel.tsx new file mode 100644 index 0000000..f4f2bac --- /dev/null +++ b/src/tui-solid/components/panels/multi-agent-panel.tsx @@ -0,0 +1,202 @@ +/** + * Multi-Agent Panel + * + * Displays active agents, their status, and execution progress. + */ + +import { For, Show, createMemo, createSignal, onMount, onCleanup } from "solid-js"; +import { TextAttributes } from "@opentui/core"; +import { useTheme } from "@tui-solid/context/theme"; +import { multiAgentStore } from "@stores/core/multi-agent-store"; +import type { AgentInstance, AgentInstanceStatus } from "@/types/multi-agent"; + +interface MultiAgentPanelProps { + visible?: boolean; + onSelectAgent?: (agentId: string) => void; +} + +const STATUS_ICONS: Record = { + pending: "◯", + running: "●", + waiting_conflict: "⏸", + completed: "✓", + error: "✗", + cancelled: "⊘", +}; + +export function MultiAgentPanel(props: MultiAgentPanelProps) { + const theme = useTheme(); + const visible = () => props.visible ?? true; + + const [instances, setInstances] = createSignal([]); + const [selectedIndex] = createSignal(0); + + onMount(() => { + const unsubscribe = multiAgentStore.subscribe((state) => { + setInstances(Array.from(state.instances.values())); + }); + + onCleanup(unsubscribe); + }); + + const stats = createMemo(() => { + const all = instances(); + return { + running: all.filter((i) => i.status === "running").length, + waiting: all.filter((i) => i.status === "waiting_conflict").length, + completed: all.filter((i) => i.status === "completed").length, + failed: all.filter((i) => i.status === "error" || i.status === "cancelled").length, + total: all.length, + }; + }); + + const getStatusColor = (status: AgentInstanceStatus): string => { + const colorMap: Record = { + pending: "textDim", + running: "info", + waiting_conflict: "warning", + completed: "success", + error: "error", + cancelled: "textDim", + }; + return theme.colors[colorMap[status]] as string; + }; + + const getDuration = (instance: AgentInstance): string => { + const end = instance.completedAt ?? Date.now(); + const duration = end - instance.startedAt; + const seconds = Math.floor(duration / 1000); + + if (seconds < 60) { + return `${seconds}s`; + } + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}m ${remainingSeconds}s`; + }; + + const truncateText = (text: string, maxLen: number): string => { + if (text.length <= maxLen) return text; + return text.substring(0, maxLen - 3) + "..."; + }; + + return ( + 0}> + + {/* Header */} + + + Agents + + + {stats().running}/{stats().total} + + + + {/* Status Summary */} + + 0}> + + ● {stats().running} + + + 0}> + + ⏸ {stats().waiting} + + + 0}> + + ✓ {stats().completed} + + + 0}> + + ✗ {stats().failed} + + + + + {/* Agent List */} + + + + {(instance, index) => ( + + + + {STATUS_ICONS[instance.status]} + + + {instance.definition.name} + + + {getDuration(instance)} + + + + + + {truncateText(instance.config.task, 25)} + + + + + + + {truncateText(instance.error ?? "", 30)} + + + + + 0}> + + + {instance.modifiedFiles.length} file(s) modified + + + + + )} + + + + + {/* Footer with conflicts */} + 0}> + + + ⚠ {stats().waiting} agent(s) waiting on conflicts + + + + + + ); +} diff --git a/src/tui-solid/components/todo-panel.tsx b/src/tui-solid/components/panels/todo-panel.tsx similarity index 100% rename from src/tui-solid/components/todo-panel.tsx rename to src/tui-solid/components/panels/todo-panel.tsx diff --git a/src/tui-solid/components/agent-select.tsx b/src/tui-solid/components/submenu/agent-select.tsx similarity index 100% rename from src/tui-solid/components/agent-select.tsx rename to src/tui-solid/components/submenu/agent-select.tsx diff --git a/src/tui-solid/components/submenu/index.ts b/src/tui-solid/components/submenu/index.ts new file mode 100644 index 0000000..c293ef0 --- /dev/null +++ b/src/tui-solid/components/submenu/index.ts @@ -0,0 +1,10 @@ +/** + * Submenu Components + */ + +export * from "./agent-select"; +export * from "./model-select"; +export * from "./provider-select"; +export * from "./theme-select"; +export * from "./mode-select"; +export * from "./mcp-select"; diff --git a/src/tui-solid/components/mcp-select.tsx b/src/tui-solid/components/submenu/mcp-select.tsx similarity index 100% rename from src/tui-solid/components/mcp-select.tsx rename to src/tui-solid/components/submenu/mcp-select.tsx diff --git a/src/tui-solid/components/mode-select.tsx b/src/tui-solid/components/submenu/mode-select.tsx similarity index 100% rename from src/tui-solid/components/mode-select.tsx rename to src/tui-solid/components/submenu/mode-select.tsx diff --git a/src/tui-solid/components/model-select.tsx b/src/tui-solid/components/submenu/model-select.tsx similarity index 100% rename from src/tui-solid/components/model-select.tsx rename to src/tui-solid/components/submenu/model-select.tsx diff --git a/src/tui-solid/components/provider-select.tsx b/src/tui-solid/components/submenu/provider-select.tsx similarity index 100% rename from src/tui-solid/components/provider-select.tsx rename to src/tui-solid/components/submenu/provider-select.tsx diff --git a/src/tui-solid/components/theme-select.tsx b/src/tui-solid/components/submenu/theme-select.tsx similarity index 100% rename from src/tui-solid/components/theme-select.tsx rename to src/tui-solid/components/submenu/theme-select.tsx diff --git a/src/tui-solid/routes/home.tsx b/src/tui-solid/routes/home.tsx index 8a8af27..b51c722 100644 --- a/src/tui-solid/routes/home.tsx +++ b/src/tui-solid/routes/home.tsx @@ -1,13 +1,13 @@ import { Match, Switch } from "solid-js"; import { useTheme } from "@tui-solid/context/theme"; import { useAppStore } from "@tui-solid/context/app"; -import { Logo } from "@tui-solid/components/logo"; -import { InputArea } from "@tui-solid/components/input-area"; -import { CommandMenu } from "@tui-solid/components/command-menu"; -import { ModelSelect } from "@tui-solid/components/model-select"; -import { ThemeSelect } from "@tui-solid/components/theme-select"; -import { FilePicker } from "@tui-solid/components/file-picker"; -import { CenteredModal } from "@tui-solid/components/centered-modal"; +import { Logo } from "@tui-solid/components/layout/logo"; +import { InputArea } from "@tui-solid/components/inputs/input-area"; +import { CommandMenu } from "@tui-solid/components/menu/command-menu"; +import { ModelSelect } from "@tui-solid/components/submenu/model-select"; +import { ThemeSelect } from "@tui-solid/components/submenu/theme-select"; +import { FilePicker } from "@tui-solid/components/inputs/file-picker"; +import { CenteredModal } from "@tui-solid/components/modals/centered-modal"; import { HOME_VARS } from "@constants/home"; interface HomeProps { diff --git a/src/tui-solid/routes/session.tsx b/src/tui-solid/routes/session.tsx index eb075a5..87c6005 100644 --- a/src/tui-solid/routes/session.tsx +++ b/src/tui-solid/routes/session.tsx @@ -1,27 +1,27 @@ import { Show, Switch, Match, createSignal, createMemo, onMount } from "solid-js"; import { useTheme } from "@tui-solid/context/theme"; import { useAppStore } from "@tui-solid/context/app"; -import { Header } from "@tui-solid/components/header"; -import { LogPanel } from "@tui-solid/components/log-panel"; -import { InputArea } from "@tui-solid/components/input-area"; -import { StatusBar } from "@tui-solid/components/status-bar"; -import { CommandMenu } from "@tui-solid/components/command-menu"; -import { ModelSelect } from "@tui-solid/components/model-select"; -import { ThemeSelect } from "@tui-solid/components/theme-select"; -import { AgentSelect } from "@tui-solid/components/agent-select"; -import { MCPSelect } from "@tui-solid/components/mcp-select"; -import { MCPAddForm } from "@tui-solid/components/mcp-add-form"; -import { ModeSelect } from "@tui-solid/components/mode-select"; -import { ProviderSelect } from "@tui-solid/components/provider-select"; -import { FilePicker } from "@tui-solid/components/file-picker"; -import { PermissionModal } from "@tui-solid/components/permission-modal"; -import { LearningModal } from "@tui-solid/components/learning-modal"; -import { HelpMenu } from "@tui-solid/components/help-menu"; -import { HelpDetail } from "@tui-solid/components/help-detail"; -import { TodoPanel } from "@tui-solid/components/todo-panel"; -import { CenteredModal } from "@tui-solid/components/centered-modal"; -import { DebugLogPanel } from "@tui-solid/components/debug-log-panel"; -import { BrainMenu } from "@tui-solid/components/brain-menu"; +import { Header } from "@tui-solid/components/layout/header"; +import { LogPanel } from "@tui-solid/components/logs/log-panel"; +import { InputArea } from "@tui-solid/components/inputs/input-area"; +import { StatusBar } from "@tui-solid/components/layout/status-bar"; +import { CommandMenu } from "@tui-solid/components/menu/command-menu"; +import { ModelSelect } from "@tui-solid/components/submenu/model-select"; +import { ThemeSelect } from "@tui-solid/components/submenu/theme-select"; +import { AgentSelect } from "@tui-solid/components/submenu/agent-select"; +import { MCPSelect } from "@tui-solid/components/submenu/mcp-select"; +import { MCPAddForm } from "@tui-solid/components/inputs/mcp-add-form"; +import { ModeSelect } from "@tui-solid/components/submenu/mode-select"; +import { ProviderSelect } from "@tui-solid/components/submenu/provider-select"; +import { FilePicker } from "@tui-solid/components/inputs/file-picker"; +import { PermissionModal } from "@tui-solid/components/modals/permission-modal"; +import { LearningModal } from "@tui-solid/components/modals/learning-modal"; +import { HelpMenu } from "@tui-solid/components/menu/help-menu"; +import { HelpDetail } from "@tui-solid/components/panels/help-detail"; +import { TodoPanel } from "@tui-solid/components/panels/todo-panel"; +import { CenteredModal } from "@tui-solid/components/modals/centered-modal"; +import { DebugLogPanel } from "@tui-solid/components/logs/debug-log-panel"; +import { BrainMenu } from "@tui-solid/components/menu/brain-menu"; import { BRAIN_DISABLED } from "@constants/brain"; import { initializeMCP, getServerInstances } from "@services/mcp"; import type { PermissionScope, LearningScope, InteractionMode, MCPServerDisplay } from "@/types/tui"; diff --git a/src/types/common.ts b/src/types/common.ts new file mode 100644 index 0000000..baea091 --- /dev/null +++ b/src/types/common.ts @@ -0,0 +1,166 @@ +/** + * Common types for CodeTyper CLI + * + * Basic domain types that don't belong to a specific feature module. + */ + +/** + * Agent type for different coding tasks + */ +export type AgentType = "coder" | "tester" | "refactorer" | "documenter"; + +/** + * Intent classification for user requests + */ +export type IntentType = + | "ask" + | "code" + | "refactor" + | "fix" + | "document" + | "test" + | "explain"; + +/** + * Available LLM providers + */ +export type Provider = "copilot" | "ollama"; + +/** + * Application configuration + */ +export interface Config { + provider: Provider; + model?: string; + theme?: string; + maxIterations: number; + timeout: number; + protectedPaths: string[]; + systemPrompt?: string; + cascadeEnabled?: boolean; +} + +/** + * Intent detection request + */ +export interface IntentRequest { + prompt: string; + context?: string; + files?: string[]; +} + +/** + * Intent detection response + */ +export interface IntentResponse { + intent: IntentType; + confidence: number; + reasoning: string; + needsClarification: boolean; + clarificationQuestions?: string[]; +} + +/** + * Plan step for execution + */ +export interface PlanStep { + id: string; + type: "read" | "edit" | "create" | "delete" | "execute"; + description: string; + file?: string; + dependencies?: string[]; + tool?: string; + args?: Record; +} + +/** + * Execution plan + */ +export interface ExecutionPlan { + steps: PlanStep[]; + intent: IntentType; + summary: string; + estimatedTime?: number; +} + +/** + * Validation result + */ +export interface ValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; + protectedPaths: string[]; +} + +/** + * Chat message + */ +export interface ChatMessage { + role: "user" | "assistant" | "system"; + content: string; + timestamp: number; +} + +/** + * Chat session + */ +export interface ChatSession { + id: string; + agent: AgentType; + messages: ChatMessage[]; + contextFiles: string[]; + createdAt: number; + updatedAt: number; +} + +/** + * Command options + */ +export interface CommandOptions { + agent?: AgentType; + model?: string; + files?: string[]; + dryRun?: boolean; + maxIterations?: number; + autoApprove?: boolean; + task?: string; + prompt?: string; + context?: string; + intent?: IntentType; + output?: string; + planFile?: string; + action?: string; + key?: string; + value?: string; +} + +/** + * Tool execution result + */ +export interface ToolResult { + success: boolean; + output?: string; + error?: string; + files?: string[]; +} + +/** + * File edit operation + */ +export interface FileEdit { + file: string; + search: string; + replace: string; + description?: string; +} + +/** + * Terminal spinner interface + */ +export interface TerminalSpinner { + start(text: string): void; + succeed(text: string): void; + fail(text: string): void; + stop(): void; +} diff --git a/src/types/multi-agent.ts b/src/types/multi-agent.ts new file mode 100644 index 0000000..56884ae --- /dev/null +++ b/src/types/multi-agent.ts @@ -0,0 +1,197 @@ +/** + * Multi-Agent Execution Types + * + * Types for spawning and managing multiple agents in parallel + * with conflict detection and resolution. + */ + +import type { AgentDefinition } from "@/types/agent-definition"; +import type { ChatMessage } from "@/types/common"; + +/** + * Agent execution modes + */ +export type AgentExecutionMode = + | "sequential" // Execute agents one after another + | "parallel" // Execute all agents concurrently + | "adaptive"; // Start parallel, serialize on conflict + +/** + * Conflict resolution strategies + */ +export type ConflictStrategy = + | "serialize" // Wait for conflicting agent to complete + | "abort-newer" // Abort the newer agent + | "merge-results" // Attempt to merge both results + | "isolated"; // Each agent works in isolated context + +/** + * Agent instance status + */ +export type AgentInstanceStatus = + | "pending" // Waiting to start + | "running" // Actively executing + | "waiting_conflict" // Paused due to conflict + | "completed" // Successfully finished + | "error" // Failed with error + | "cancelled"; // Cancelled by user or system + +/** + * Tool call record for agent conversation + */ +export interface AgentToolCall { + id: string; + toolName: string; + args: Record; + result?: { + success: boolean; + output?: string; + error?: string; + }; + timestamp: number; +} + +/** + * Agent conversation state + */ +export interface AgentConversation { + messages: ChatMessage[]; + toolCalls: AgentToolCall[]; +} + +/** + * Configuration for spawning an agent + */ +export interface AgentSpawnConfig { + agentName: string; + task: string; + contextFiles?: string[]; + priority?: number; + timeout?: number; + allowedTools?: string[]; + systemPromptOverride?: string; +} + +/** + * Running agent instance + */ +export interface AgentInstance { + id: string; + definition: AgentDefinition; + config: AgentSpawnConfig; + status: AgentInstanceStatus; + conversation: AgentConversation; + startedAt: number; + completedAt?: number; + error?: string; + modifiedFiles: string[]; + result?: AgentExecutionResult; +} + +/** + * Result from a single agent execution + */ +export interface AgentExecutionResult { + success: boolean; + output?: string; + error?: string; + filesModified: string[]; + toolCallCount: number; + tokenUsage?: { + promptTokens: number; + completionTokens: number; + totalTokens: number; + }; + duration: number; +} + +/** + * File conflict between agents + */ +export interface FileConflict { + filePath: string; + conflictingAgentIds: string[]; + detectedAt: number; + resolution?: ConflictResolutionResult; +} + +/** + * Result of conflict resolution + */ +export interface ConflictResolutionResult { + strategy: ConflictStrategy; + winningAgentId?: string; + mergedContent?: string; + resolvedAt: number; +} + +/** + * Request to execute multiple agents + */ +export interface MultiAgentRequest { + id: string; + agents: AgentSpawnConfig[]; + executionMode: AgentExecutionMode; + conflictStrategy: ConflictStrategy; + maxConcurrent?: number; + timeout?: number; + abortOnFirstError?: boolean; +} + +/** + * Result of multi-agent execution + */ +export interface MultiAgentResult { + requestId: string; + agents: AgentInstance[]; + successful: number; + failed: number; + cancelled: number; + conflicts: FileConflict[]; + totalDuration: number; + aggregatedOutput?: string; +} + +/** + * Multi-agent state for store + */ +export interface MultiAgentState { + activeRequests: Map; + instances: Map; + conflicts: FileConflict[]; + isExecuting: boolean; + lastError?: string; +} + +/** + * Events emitted during multi-agent execution + */ +export type MultiAgentEvent = + | { type: "agent_started"; agentId: string; timestamp: number } + | { type: "agent_completed"; agentId: string; result: AgentExecutionResult; timestamp: number } + | { type: "agent_error"; agentId: string; error: string; timestamp: number } + | { type: "conflict_detected"; conflict: FileConflict; timestamp: number } + | { type: "conflict_resolved"; conflict: FileConflict; timestamp: number } + | { type: "execution_completed"; result: MultiAgentResult; timestamp: number }; + +/** + * Multi-agent executor options + */ +export interface MultiAgentExecutorOptions { + onEvent?: (event: MultiAgentEvent) => void; + onAgentMessage?: (agentId: string, message: ChatMessage) => void; + onToolCall?: (agentId: string, toolCall: AgentToolCall) => void; + abortSignal?: AbortSignal; +} + +/** + * Agent tool context for isolated execution + */ +export interface AgentToolContext { + agentId: string; + workingDir: string; + allowedPaths: string[]; + deniedPaths: string[]; + modifiedFiles: Set; + lockedFiles: Set; +} diff --git a/src/ui/banner.ts b/src/ui/banner.ts index 1f731a3..c40dd78 100644 --- a/src/ui/banner.ts +++ b/src/ui/banner.ts @@ -4,6 +4,6 @@ export type { BannerStyle } from "@/types/banner"; export { getBannerLines } from "@ui/banner/lines"; -export { renderBanner, renderBannerWithSubtitle } from "@ui/banner/render"; -export { printBanner, printWelcome } from "@ui/banner/print"; -export { getInlineLogo } from "@ui/banner/logo"; +export { renderBanner, renderBannerWithSubtitle } from "@ui/banner/core/render"; +export { printBanner, printWelcome } from "@ui/banner/menu/print"; +export { getInlineLogo } from "@ui/banner/core/logo"; diff --git a/src/ui/banner/core/index.ts b/src/ui/banner/core/index.ts new file mode 100644 index 0000000..e122296 --- /dev/null +++ b/src/ui/banner/core/index.ts @@ -0,0 +1,6 @@ +/** + * Banner Core - Rendering and Logo + */ + +export * from "./render"; +export * from "./logo"; diff --git a/src/ui/banner/logo.ts b/src/ui/banner/core/logo.ts similarity index 81% rename from src/ui/banner/logo.ts rename to src/ui/banner/core/logo.ts index 4d783ac..83587b5 100644 --- a/src/ui/banner/logo.ts +++ b/src/ui/banner/core/logo.ts @@ -2,7 +2,7 @@ * Banner logo utilities */ -import { Style } from "@ui/styles"; +import { Style } from "@ui/core/styles"; /** * Simple logo for inline display diff --git a/src/ui/banner/render.ts b/src/ui/banner/core/render.ts similarity index 95% rename from src/ui/banner/render.ts rename to src/ui/banner/core/render.ts index b9856f3..f10f851 100644 --- a/src/ui/banner/render.ts +++ b/src/ui/banner/core/render.ts @@ -3,7 +3,7 @@ */ import { GRADIENT_COLORS } from "@constants/banner"; -import { Style } from "@ui/styles"; +import { Style } from "@ui/core/styles"; import type { BannerStyle } from "@/types/banner"; import { getBannerLines } from "@ui/banner/lines"; diff --git a/src/ui/banner/menu/index.ts b/src/ui/banner/menu/index.ts new file mode 100644 index 0000000..4380d33 --- /dev/null +++ b/src/ui/banner/menu/index.ts @@ -0,0 +1,5 @@ +/** + * Banner Menu - Print functions + */ + +export * from "./print"; diff --git a/src/ui/banner/print.ts b/src/ui/banner/menu/print.ts similarity index 89% rename from src/ui/banner/print.ts rename to src/ui/banner/menu/print.ts index ae09bac..bd64806 100644 --- a/src/ui/banner/print.ts +++ b/src/ui/banner/menu/print.ts @@ -2,9 +2,9 @@ * Banner printing utilities */ -import { Style } from "@ui/styles"; +import { Style } from "@ui/core/styles"; import type { BannerStyle } from "@/types/banner"; -import { renderBanner } from "@ui/banner/render"; +import { renderBanner } from "@ui/banner/core/render"; /** * Print the banner to console diff --git a/src/ui/components/box.ts b/src/ui/components/core/box.ts similarity index 100% rename from src/ui/components/box.ts rename to src/ui/components/core/box.ts diff --git a/src/ui/components/core/index.ts b/src/ui/components/core/index.ts new file mode 100644 index 0000000..8c537b9 --- /dev/null +++ b/src/ui/components/core/index.ts @@ -0,0 +1,5 @@ +/** + * Components Core - Box component + */ + +export * from "./box"; diff --git a/src/ui/components/header.ts b/src/ui/components/menu/header.ts similarity index 96% rename from src/ui/components/header.ts rename to src/ui/components/menu/header.ts index 3c8fe15..1215d72 100644 --- a/src/ui/components/header.ts +++ b/src/ui/components/menu/header.ts @@ -5,7 +5,7 @@ import { Style, Theme } from "@constants/styles"; import { colors } from "@ui/styles/colors"; import { stripAnsi, getTerminalWidth, line } from "@ui/styles/text"; -import { box } from "@ui/components/box"; +import { box } from "@ui/components/core/box"; import type { HeaderStyle } from "@/types/components"; const HEADER_STYLE_HANDLERS: Record string> = { diff --git a/src/ui/components/menu/index.ts b/src/ui/components/menu/index.ts new file mode 100644 index 0000000..a865643 --- /dev/null +++ b/src/ui/components/menu/index.ts @@ -0,0 +1,8 @@ +/** + * Components Menu - Status, Message, Header, List + */ + +export * from "./status"; +export * from "./message"; +export * from "./header"; +export * from "./list"; diff --git a/src/ui/components/list.ts b/src/ui/components/menu/list.ts similarity index 100% rename from src/ui/components/list.ts rename to src/ui/components/menu/list.ts diff --git a/src/ui/components/message.ts b/src/ui/components/menu/message.ts similarity index 100% rename from src/ui/components/message.ts rename to src/ui/components/menu/message.ts diff --git a/src/ui/components/status.ts b/src/ui/components/menu/status.ts similarity index 100% rename from src/ui/components/status.ts rename to src/ui/components/menu/status.ts diff --git a/src/ui/components.ts b/src/ui/core/components.ts similarity index 56% rename from src/ui/components.ts rename to src/ui/core/components.ts index a0cc0d2..206c13d 100644 --- a/src/ui/components.ts +++ b/src/ui/core/components.ts @@ -17,8 +17,8 @@ export type { MessageRole, } from "@/types/components"; -export { box, panel, errorBox, successBox } from "@ui/components/box"; -export { header, divider } from "@ui/components/header"; -export { keyValue, list } from "@ui/components/list"; -export { status, toolCall } from "@ui/components/status"; -export { message, codeBlock } from "@ui/components/message"; +export { box, panel, errorBox, successBox } from "@ui/components/core/box"; +export { header, divider } from "@ui/components/menu/header"; +export { keyValue, list } from "@ui/components/menu/list"; +export { status, toolCall } from "@ui/components/menu/status"; +export { message, codeBlock } from "@ui/components/menu/message"; diff --git a/src/ui/core/index.ts b/src/ui/core/index.ts new file mode 100644 index 0000000..545fded --- /dev/null +++ b/src/ui/core/index.ts @@ -0,0 +1,6 @@ +/** + * UI Core - Core UI utilities + */ + +export * from "./styles"; +export * from "./components"; diff --git a/src/ui/styles.ts b/src/ui/core/styles.ts similarity index 100% rename from src/ui/styles.ts rename to src/ui/core/styles.ts diff --git a/src/ui/index.ts b/src/ui/index.ts index 1c6f4d3..8b8f637 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -2,9 +2,8 @@ * Terminal UI components for CodeTyper CLI */ +export * from "@ui/core"; export * from "@ui/banner"; -export * from "@ui/styles"; export * from "@ui/spinner"; -export * from "@ui/components"; export * from "@ui/tips"; export * from "@ui/input-editor"; diff --git a/src/ui/input-editor.ts b/src/ui/input-editor.ts index 25e6c49..c04a4db 100644 --- a/src/ui/input-editor.ts +++ b/src/ui/input-editor.ts @@ -14,4 +14,4 @@ export { createInputEditor, InputEditor, type InputEditorInstance, -} from "@ui/input-editor/editor"; +} from "@ui/input-editor/core/editor"; diff --git a/src/ui/input-editor/editor.ts b/src/ui/input-editor/core/editor.ts similarity index 100% rename from src/ui/input-editor/editor.ts rename to src/ui/input-editor/core/editor.ts diff --git a/src/ui/input-editor/core/index.ts b/src/ui/input-editor/core/index.ts new file mode 100644 index 0000000..1ede15d --- /dev/null +++ b/src/ui/input-editor/core/index.ts @@ -0,0 +1,5 @@ +/** + * Input Editor Core - Main editor functionality + */ + +export * from "./editor"; diff --git a/src/ui/spinner.ts b/src/ui/spinner.ts index 14d2a9f..8965f6f 100644 --- a/src/ui/spinner.ts +++ b/src/ui/spinner.ts @@ -11,7 +11,7 @@ export { createSpinner, Spinner, type SpinnerInstance, -} from "@ui/spinner/spinner"; +} from "@ui/spinner/core/spinner"; export { createScannerInstance, diff --git a/src/ui/spinner/core/index.ts b/src/ui/spinner/core/index.ts new file mode 100644 index 0000000..b91c339 --- /dev/null +++ b/src/ui/spinner/core/index.ts @@ -0,0 +1,5 @@ +/** + * Spinner Core - Main spinner functionality + */ + +export * from "./spinner"; diff --git a/src/ui/spinner/spinner.ts b/src/ui/spinner/core/spinner.ts similarity index 100% rename from src/ui/spinner/spinner.ts rename to src/ui/spinner/core/spinner.ts diff --git a/src/utils/ensure-directories.ts b/src/utils/core/ensure-directories.ts similarity index 100% rename from src/utils/ensure-directories.ts rename to src/utils/core/ensure-directories.ts diff --git a/src/utils/core/index.ts b/src/utils/core/index.ts new file mode 100644 index 0000000..1b95270 --- /dev/null +++ b/src/utils/core/index.ts @@ -0,0 +1,8 @@ +/** + * Utils Core - Core utility functions + */ + +export * from "./terminal"; +export * from "./string-helpers"; +export * from "./tools"; +export * from "./ensure-directories"; diff --git a/src/utils/string-helpers.ts b/src/utils/core/string-helpers.ts similarity index 100% rename from src/utils/string-helpers.ts rename to src/utils/core/string-helpers.ts diff --git a/src/utils/terminal.ts b/src/utils/core/terminal.ts similarity index 100% rename from src/utils/terminal.ts rename to src/utils/core/terminal.ts diff --git a/src/utils/tools.ts b/src/utils/core/tools.ts similarity index 100% rename from src/utils/tools.ts rename to src/utils/core/tools.ts diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..5f58dd5 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,12 @@ +/** + * Utils Module - Utility functions + */ + +export * from "./core"; +export * from "./menu"; +export * from "./diff"; +export * from "./syntax-highlight/detect"; +export * from "./syntax-highlight/highlight"; +export * from "./tui-app/input-utils"; +export * from "./tui-app/mode-utils"; +export * from "./tui-app/paste-utils"; diff --git a/src/utils/menu/index.ts b/src/utils/menu/index.ts new file mode 100644 index 0000000..d98c374 --- /dev/null +++ b/src/utils/menu/index.ts @@ -0,0 +1,5 @@ +/** + * Utils Menu - Menu-related utilities + */ + +export * from "./progress-bar"; diff --git a/src/utils/progress-bar.ts b/src/utils/menu/progress-bar.ts similarity index 100% rename from src/utils/progress-bar.ts rename to src/utils/menu/progress-bar.ts