"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
    if (value !== null && value !== void 0) {
        if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
        var dispose, inner;
        if (async) {
            if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
            dispose = value[Symbol.asyncDispose];
        }
        if (dispose === void 0) {
            if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
            dispose = value[Symbol.dispose];
            if (async) inner = dispose;
        }
        if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
        if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
        env.stack.push({ value: value, dispose: dispose, async: async });
    }
    else if (async) {
        env.stack.push({ async: true });
    }
    return value;
};
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
    return function (env) {
        function fail(e) {
            env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
            env.hasError = true;
        }
        var r, s = 0;
        function next() {
            while (r = env.stack.pop()) {
                try {
                    if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
                    if (r.dispose) {
                        var result = r.dispose.call(r.value);
                        if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
                    }
                    else s |= 1;
                }
                catch (e) {
                    fail(e);
                }
            }
            if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
            if (env.hasError) throw env.error;
        }
        return next();
    };
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
    var e = new Error(message);
    return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.IpcMain = void 0;
const assert_1 = __importDefault(require("../utils/assert"));
const child_process_1 = require("child_process");
const fsPromises = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const git_1 = require("../git");
const aiService_1 = require("../services/aiService");
const historyService_1 = require("../services/historyService");
const partialService_1 = require("../services/partialService");
const agentSession_1 = require("../services/agentSession");
const log_1 = require("../services/log");
const tokenizer_1 = require("../utils/main/tokenizer");
const tokenStatsCalculator_1 = require("../utils/tokens/tokenStatsCalculator");
const ipc_constants_1 = require("../constants/ipc-constants");
const result_1 = require("../types/result");
const workspaceValidation_1 = require("../utils/validation/workspaceValidation");
const bash_1 = require("../services/tools/bash");
const secrets_1 = require("../types/secrets");
const tempDir_1 = require("../services/tempDir");
const initStateManager_1 = require("../services/initStateManager");
const runtimeFactory_1 = require("../runtime/runtimeFactory");
const pathUtils_1 = require("../utils/pathUtils");
/**
 * IpcMain - Manages all IPC handlers and service coordination
 *
 * This class encapsulates:
 * - All ipcMain handler registration
 * - Service lifecycle management (AIService, HistoryService, PartialService, InitStateManager)
 * - Event forwarding from services to renderer
 *
 * Design:
 * - Constructor accepts only Config for dependency injection
 * - Services are created internally from Config
 * - register() accepts ipcMain and BrowserWindow for handler setup
 */
class IpcMain {
    config;
    historyService;
    partialService;
    aiService;
    initStateManager;
    sessions = new Map();
    sessionSubscriptions = new Map();
    mainWindow = null;
    registered = false;
    constructor(config) {
        this.config = config;
        this.historyService = new historyService_1.HistoryService(config);
        this.partialService = new partialService_1.PartialService(config, this.historyService);
        this.initStateManager = new initStateManager_1.InitStateManager(config);
        this.aiService = new aiService_1.AIService(config, this.historyService, this.partialService, this.initStateManager);
    }
    getOrCreateSession(workspaceId) {
        (0, assert_1.default)(typeof workspaceId === "string", "workspaceId must be a string");
        const trimmed = workspaceId.trim();
        (0, assert_1.default)(trimmed.length > 0, "workspaceId must not be empty");
        let session = this.sessions.get(trimmed);
        if (session) {
            return session;
        }
        session = new agentSession_1.AgentSession({
            workspaceId: trimmed,
            config: this.config,
            historyService: this.historyService,
            partialService: this.partialService,
            aiService: this.aiService,
            initStateManager: this.initStateManager,
        });
        const chatUnsubscribe = session.onChatEvent((event) => {
            if (!this.mainWindow) {
                return;
            }
            const channel = (0, ipc_constants_1.getChatChannel)(event.workspaceId);
            this.mainWindow.webContents.send(channel, event.message);
        });
        const metadataUnsubscribe = session.onMetadataEvent((event) => {
            if (!this.mainWindow) {
                return;
            }
            this.mainWindow.webContents.send(ipc_constants_1.IPC_CHANNELS.WORKSPACE_METADATA, {
                workspaceId: event.workspaceId,
                metadata: event.metadata,
            });
        });
        this.sessions.set(trimmed, session);
        this.sessionSubscriptions.set(trimmed, {
            chat: chatUnsubscribe,
            metadata: metadataUnsubscribe,
        });
        return session;
    }
    disposeSession(workspaceId) {
        const session = this.sessions.get(workspaceId);
        if (!session) {
            return;
        }
        const subscriptions = this.sessionSubscriptions.get(workspaceId);
        if (subscriptions) {
            subscriptions.chat();
            subscriptions.metadata();
            this.sessionSubscriptions.delete(workspaceId);
        }
        session.dispose();
        this.sessions.delete(workspaceId);
    }
    /**
     * Register all IPC handlers and setup event forwarding
     * @param ipcMain - Electron's ipcMain module
     * @param mainWindow - The main BrowserWindow for sending events
     */
    register(ipcMain, mainWindow) {
        // Always update the window reference (windows can be recreated on macOS)
        this.mainWindow = mainWindow;
        // Skip registration if handlers are already registered
        // This prevents "handler already registered" errors when windows are recreated
        if (this.registered) {
            return;
        }
        this.registerWindowHandlers(ipcMain);
        this.registerTokenizerHandlers(ipcMain);
        this.registerWorkspaceHandlers(ipcMain);
        this.registerProviderHandlers(ipcMain);
        this.registerProjectHandlers(ipcMain);
        this.registerSubscriptionHandlers(ipcMain);
        this.registered = true;
    }
    registerWindowHandlers(ipcMain) {
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WINDOW_SET_TITLE, (_event, title) => {
            if (!this.mainWindow)
                return;
            this.mainWindow.setTitle(title);
        });
    }
    registerTokenizerHandlers(ipcMain) {
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.TOKENIZER_COUNT_TOKENS, async (_event, model, input) => {
            (0, assert_1.default)(typeof model === "string" && model.length > 0, "Tokenizer countTokens requires model name");
            (0, assert_1.default)(typeof input === "string", "Tokenizer countTokens requires text");
            return (0, tokenizer_1.countTokens)(model, input);
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.TOKENIZER_COUNT_TOKENS_BATCH, async (_event, model, texts) => {
            (0, assert_1.default)(typeof model === "string" && model.length > 0, "Tokenizer countTokensBatch requires model name");
            (0, assert_1.default)(Array.isArray(texts), "Tokenizer countTokensBatch requires an array of strings");
            return (0, tokenizer_1.countTokensBatch)(model, texts);
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.TOKENIZER_CALCULATE_STATS, async (_event, messages, model) => {
            (0, assert_1.default)(Array.isArray(messages), "Tokenizer IPC requires an array of messages");
            (0, assert_1.default)(typeof model === "string" && model.length > 0, "Tokenizer IPC requires model name");
            try {
                return await (0, tokenStatsCalculator_1.calculateTokenStats)(messages, model);
            }
            catch (error) {
                log_1.log.error("[IpcMain] Token stats calculation failed", error);
                throw error;
            }
        });
    }
    registerWorkspaceHandlers(ipcMain) {
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_CREATE, async (_event, projectPath, branchName, trunkBranch, runtimeConfig) => {
            // Validate workspace name
            const validation = (0, workspaceValidation_1.validateWorkspaceName)(branchName);
            if (!validation.valid) {
                return { success: false, error: validation.error };
            }
            if (typeof trunkBranch !== "string" || trunkBranch.trim().length === 0) {
                return { success: false, error: "Trunk branch is required" };
            }
            const normalizedTrunkBranch = trunkBranch.trim();
            // Generate stable workspace ID (stored in config, not used for directory name)
            const workspaceId = this.config.generateStableId();
            // Create runtime for workspace creation (defaults to local with srcDir as base)
            const finalRuntimeConfig = runtimeConfig ?? {
                type: "local",
                srcBaseDir: this.config.srcDir,
            };
            // Create temporary runtime to resolve srcBaseDir path
            // This allows tilde paths to work for both local and SSH runtimes
            let runtime;
            let resolvedSrcBaseDir;
            try {
                runtime = (0, runtimeFactory_1.createRuntime)(finalRuntimeConfig);
                // Resolve srcBaseDir to absolute path (expanding tildes, etc.)
                resolvedSrcBaseDir = await runtime.resolvePath(finalRuntimeConfig.srcBaseDir);
                // If path was resolved to something different, recreate runtime with resolved path
                if (resolvedSrcBaseDir !== finalRuntimeConfig.srcBaseDir) {
                    const resolvedRuntimeConfig = {
                        ...finalRuntimeConfig,
                        srcBaseDir: resolvedSrcBaseDir,
                    };
                    runtime = (0, runtimeFactory_1.createRuntime)(resolvedRuntimeConfig);
                    // Update finalRuntimeConfig to store resolved path in config
                    finalRuntimeConfig.srcBaseDir = resolvedSrcBaseDir;
                }
            }
            catch (error) {
                const errorMsg = error instanceof Error ? error.message : String(error);
                return { success: false, error: errorMsg };
            }
            // Create session BEFORE starting init so events can be forwarded
            const session = this.getOrCreateSession(workspaceId);
            // Start init tracking (creates in-memory state + emits init-start event)
            // This MUST complete before workspace creation returns so replayInit() finds state
            this.initStateManager.startInit(workspaceId, projectPath);
            // Create InitLogger that bridges to InitStateManager
            const initLogger = {
                logStep: (message) => {
                    this.initStateManager.appendOutput(workspaceId, message, false);
                },
                logStdout: (line) => {
                    this.initStateManager.appendOutput(workspaceId, line, false);
                },
                logStderr: (line) => {
                    this.initStateManager.appendOutput(workspaceId, line, true);
                },
                logComplete: (exitCode) => {
                    void this.initStateManager.endInit(workspaceId, exitCode);
                },
            };
            // Phase 1: Create workspace structure (FAST - returns immediately)
            const createResult = await runtime.createWorkspace({
                projectPath,
                branchName,
                trunkBranch: normalizedTrunkBranch,
                directoryName: branchName, // Use branch name as directory name
                initLogger,
            });
            if (!createResult.success || !createResult.workspacePath) {
                return { success: false, error: createResult.error ?? "Failed to create workspace" };
            }
            const projectName = projectPath.split("/").pop() ?? projectPath.split("\\").pop() ?? "unknown";
            // Initialize workspace metadata with stable ID and name
            const metadata = {
                id: workspaceId,
                name: branchName, // Name is separate from ID
                projectName,
                projectPath, // Full project path for computing worktree path
                createdAt: new Date().toISOString(),
            };
            // Note: metadata.json no longer written - config is the only source of truth
            // Update config to include the new workspace (with full metadata)
            this.config.editConfig((config) => {
                let projectConfig = config.projects.get(projectPath);
                if (!projectConfig) {
                    // Create project config if it doesn't exist
                    projectConfig = {
                        workspaces: [],
                    };
                    config.projects.set(projectPath, projectConfig);
                }
                // Add workspace to project config with full metadata
                projectConfig.workspaces.push({
                    path: createResult.workspacePath,
                    id: workspaceId,
                    name: branchName,
                    createdAt: metadata.createdAt,
                    runtimeConfig: finalRuntimeConfig, // Save runtime config for exec operations
                });
                return config;
            });
            // No longer creating symlinks - directory name IS the workspace name
            // Get complete metadata from config (includes paths)
            const allMetadata = this.config.getAllWorkspaceMetadata();
            const completeMetadata = allMetadata.find((m) => m.id === workspaceId);
            if (!completeMetadata) {
                return { success: false, error: "Failed to retrieve workspace metadata" };
            }
            // Emit metadata event for new workspace (session already created above)
            session.emitMetadata(completeMetadata);
            // Phase 2: Initialize workspace asynchronously (SLOW - runs in background)
            // This streams progress via initLogger and doesn't block the IPC return
            void runtime
                .initWorkspace({
                projectPath,
                branchName,
                trunkBranch: normalizedTrunkBranch,
                workspacePath: createResult.workspacePath,
                initLogger,
            })
                .catch((error) => {
                const errorMsg = error instanceof Error ? error.message : String(error);
                log_1.log.error(`initWorkspace failed for ${workspaceId}:`, error);
                initLogger.logStderr(`Initialization failed: ${errorMsg}`);
                initLogger.logComplete(-1);
            });
            // Return immediately - init streams separately via initLogger events
            return {
                success: true,
                metadata: completeMetadata,
            };
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_REMOVE, async (_event, workspaceId, options) => {
            return this.removeWorkspaceInternal(workspaceId, { force: options?.force ?? false });
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_RENAME, async (_event, workspaceId, newName) => {
            try {
                // Block rename during active streaming to prevent race conditions
                // (bash processes would have stale cwd, system message would be wrong)
                if (this.aiService.isStreaming(workspaceId)) {
                    return (0, result_1.Err)("Cannot rename workspace while AI stream is active. Please wait for the stream to complete.");
                }
                // Validate workspace name
                const validation = (0, workspaceValidation_1.validateWorkspaceName)(newName);
                if (!validation.valid) {
                    return (0, result_1.Err)(validation.error ?? "Invalid workspace name");
                }
                // Get current metadata
                const metadataResult = this.aiService.getWorkspaceMetadata(workspaceId);
                if (!metadataResult.success) {
                    return (0, result_1.Err)(`Failed to get workspace metadata: ${metadataResult.error}`);
                }
                const oldMetadata = metadataResult.data;
                const oldName = oldMetadata.name;
                // If renaming to itself, just return success (no-op)
                if (newName === oldName) {
                    return (0, result_1.Ok)({ newWorkspaceId: workspaceId });
                }
                // Check if new name collides with existing workspace name or ID
                const allWorkspaces = this.config.getAllWorkspaceMetadata();
                const collision = allWorkspaces.find((ws) => (ws.name === newName || ws.id === newName) && ws.id !== workspaceId);
                if (collision) {
                    return (0, result_1.Err)(`Workspace with name "${newName}" already exists`);
                }
                // Find project path from config
                const workspace = this.config.findWorkspace(workspaceId);
                if (!workspace) {
                    return (0, result_1.Err)("Failed to find workspace in config");
                }
                const { projectPath } = workspace;
                // Create runtime instance for this workspace
                // For local runtimes, workdir should be srcDir, not the individual workspace path
                const runtime = (0, runtimeFactory_1.createRuntime)(oldMetadata.runtimeConfig ?? { type: "local", srcBaseDir: this.config.srcDir });
                // Delegate rename to runtime (handles both local and SSH)
                // Runtime computes workspace paths internally from workdir + projectPath + workspace names
                const renameResult = await runtime.renameWorkspace(projectPath, oldName, newName);
                if (!renameResult.success) {
                    return (0, result_1.Err)(renameResult.error);
                }
                const { oldPath, newPath } = renameResult;
                // Update config with new name and path
                this.config.editConfig((config) => {
                    const projectConfig = config.projects.get(projectPath);
                    if (projectConfig) {
                        const workspaceEntry = projectConfig.workspaces.find((w) => w.path === oldPath);
                        if (workspaceEntry) {
                            workspaceEntry.name = newName;
                            workspaceEntry.path = newPath; // Update path to reflect new directory name
                            // Note: We don't need to update runtimeConfig.srcBaseDir on rename
                            // because srcBaseDir is the base directory, not the individual workspace path
                            // The workspace path is computed dynamically via runtime.getWorkspacePath()
                        }
                    }
                    return config;
                });
                // Get updated metadata from config (includes updated name and paths)
                const allMetadata = this.config.getAllWorkspaceMetadata();
                const updatedMetadata = allMetadata.find((m) => m.id === workspaceId);
                if (!updatedMetadata) {
                    return (0, result_1.Err)("Failed to retrieve updated workspace metadata");
                }
                // Emit metadata event with updated metadata (same workspace ID)
                const session = this.sessions.get(workspaceId);
                if (session) {
                    session.emitMetadata(updatedMetadata);
                }
                else if (this.mainWindow) {
                    this.mainWindow.webContents.send(ipc_constants_1.IPC_CHANNELS.WORKSPACE_METADATA, {
                        workspaceId,
                        metadata: updatedMetadata,
                    });
                }
                return (0, result_1.Ok)({ newWorkspaceId: workspaceId });
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                return (0, result_1.Err)(`Failed to rename workspace: ${message}`);
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_FORK, async (_event, sourceWorkspaceId, newName) => {
            try {
                // Validate new workspace name
                const validation = (0, workspaceValidation_1.validateWorkspaceName)(newName);
                if (!validation.valid) {
                    return { success: false, error: validation.error };
                }
                // If streaming, commit the partial response to history first
                // This preserves the streamed content in both workspaces
                if (this.aiService.isStreaming(sourceWorkspaceId)) {
                    await this.partialService.commitToHistory(sourceWorkspaceId);
                }
                // Get source workspace metadata
                const sourceMetadataResult = this.aiService.getWorkspaceMetadata(sourceWorkspaceId);
                if (!sourceMetadataResult.success) {
                    return {
                        success: false,
                        error: `Failed to get source workspace metadata: ${sourceMetadataResult.error}`,
                    };
                }
                const sourceMetadata = sourceMetadataResult.data;
                const foundProjectPath = sourceMetadata.projectPath;
                const projectName = sourceMetadata.projectName;
                // Create runtime for source workspace
                const sourceRuntimeConfig = sourceMetadata.runtimeConfig ?? {
                    type: "local",
                    srcBaseDir: this.config.srcDir,
                };
                const runtime = (0, runtimeFactory_1.createRuntime)(sourceRuntimeConfig);
                // Generate stable workspace ID for the new workspace
                const newWorkspaceId = this.config.generateStableId();
                // Create session BEFORE forking so init events can be forwarded
                const session = this.getOrCreateSession(newWorkspaceId);
                // Start init tracking
                this.initStateManager.startInit(newWorkspaceId, foundProjectPath);
                // Create InitLogger
                const initLogger = {
                    logStep: (message) => {
                        this.initStateManager.appendOutput(newWorkspaceId, message, false);
                    },
                    logStdout: (line) => {
                        this.initStateManager.appendOutput(newWorkspaceId, line, false);
                    },
                    logStderr: (line) => {
                        this.initStateManager.appendOutput(newWorkspaceId, line, true);
                    },
                    logComplete: (exitCode) => {
                        void this.initStateManager.endInit(newWorkspaceId, exitCode);
                    },
                };
                // Delegate fork operation to runtime
                const forkResult = await runtime.forkWorkspace({
                    projectPath: foundProjectPath,
                    sourceWorkspaceName: sourceMetadata.name,
                    newWorkspaceName: newName,
                    initLogger,
                });
                if (!forkResult.success) {
                    return { success: false, error: forkResult.error };
                }
                // Copy session files (chat.jsonl, partial.json) - local backend operation
                const sourceSessionDir = this.config.getSessionDir(sourceWorkspaceId);
                const newSessionDir = this.config.getSessionDir(newWorkspaceId);
                try {
                    await fsPromises.mkdir(newSessionDir, { recursive: true });
                    // Copy chat.jsonl if it exists
                    const sourceChatPath = path.join(sourceSessionDir, "chat.jsonl");
                    const newChatPath = path.join(newSessionDir, "chat.jsonl");
                    try {
                        await fsPromises.copyFile(sourceChatPath, newChatPath);
                    }
                    catch (error) {
                        if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
                            throw error;
                        }
                    }
                    // Copy partial.json if it exists
                    const sourcePartialPath = path.join(sourceSessionDir, "partial.json");
                    const newPartialPath = path.join(newSessionDir, "partial.json");
                    try {
                        await fsPromises.copyFile(sourcePartialPath, newPartialPath);
                    }
                    catch (error) {
                        if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
                            throw error;
                        }
                    }
                }
                catch (copyError) {
                    // If copy fails, clean up everything we created
                    await runtime.deleteWorkspace(foundProjectPath, newName, true);
                    try {
                        await fsPromises.rm(newSessionDir, { recursive: true, force: true });
                    }
                    catch (cleanupError) {
                        log_1.log.error(`Failed to clean up session dir ${newSessionDir}:`, cleanupError);
                    }
                    const message = copyError instanceof Error ? copyError.message : String(copyError);
                    return { success: false, error: `Failed to copy chat history: ${message}` };
                }
                // Initialize workspace metadata
                const metadata = {
                    id: newWorkspaceId,
                    name: newName,
                    projectName,
                    projectPath: foundProjectPath,
                    createdAt: new Date().toISOString(),
                };
                // Write metadata to config.json
                this.config.addWorkspace(foundProjectPath, metadata);
                // Emit metadata event
                session.emitMetadata(metadata);
                return {
                    success: true,
                    metadata,
                    projectPath: foundProjectPath,
                };
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                return { success: false, error: `Failed to fork workspace: ${message}` };
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_LIST, () => {
            try {
                // getAllWorkspaceMetadata now returns complete metadata with paths
                return this.config.getAllWorkspaceMetadata();
            }
            catch (error) {
                console.error("Failed to list workspaces:", error);
                return [];
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_GET_INFO, (_event, workspaceId) => {
            // Get complete metadata from config (includes paths)
            const allMetadata = this.config.getAllWorkspaceMetadata();
            return allMetadata.find((m) => m.id === workspaceId) ?? null;
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_SEND_MESSAGE, async (_event, workspaceId, message, options) => {
            log_1.log.debug("sendMessage handler: Received", {
                workspaceId,
                messagePreview: message.substring(0, 50),
                mode: options?.mode,
                options,
            });
            try {
                const session = this.getOrCreateSession(workspaceId);
                const result = await session.sendMessage(message, options);
                if (!result.success) {
                    log_1.log.error("sendMessage handler: session returned error", {
                        workspaceId,
                        error: result.error,
                    });
                }
                return result;
            }
            catch (error) {
                const errorMessage = error instanceof Error ? error.message : JSON.stringify(error, null, 2);
                log_1.log.error("Unexpected error in sendMessage handler:", error);
                const sendError = {
                    type: "unknown",
                    raw: `Failed to send message: ${errorMessage}`,
                };
                return { success: false, error: sendError };
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_RESUME_STREAM, async (_event, workspaceId, options) => {
            log_1.log.debug("resumeStream handler: Received", {
                workspaceId,
                options,
            });
            try {
                const session = this.getOrCreateSession(workspaceId);
                const result = await session.resumeStream(options);
                if (!result.success) {
                    log_1.log.error("resumeStream handler: session returned error", {
                        workspaceId,
                        error: result.error,
                    });
                }
                return result;
            }
            catch (error) {
                // Convert to SendMessageError for typed error handling
                const errorMessage = error instanceof Error ? error.message : String(error);
                log_1.log.error("Unexpected error in resumeStream handler:", error);
                const sendError = {
                    type: "unknown",
                    raw: `Failed to resume stream: ${errorMessage}`,
                };
                return { success: false, error: sendError };
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_INTERRUPT_STREAM, async (_event, workspaceId, options) => {
            log_1.log.debug("interruptStream handler: Received", { workspaceId, options });
            try {
                const session = this.getOrCreateSession(workspaceId);
                const stopResult = await session.interruptStream();
                if (!stopResult.success) {
                    log_1.log.error("Failed to stop stream:", stopResult.error);
                    return { success: false, error: stopResult.error };
                }
                // If abandonPartial is true, delete the partial instead of committing it
                if (options?.abandonPartial) {
                    log_1.log.debug("Abandoning partial for workspace:", workspaceId);
                    await this.partialService.deletePartial(workspaceId);
                }
                return { success: true, data: undefined };
            }
            catch (error) {
                const errorMessage = error instanceof Error ? error.message : String(error);
                log_1.log.error("Unexpected error in interruptStream handler:", error);
                return { success: false, error: `Failed to interrupt stream: ${errorMessage}` };
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_TRUNCATE_HISTORY, async (_event, workspaceId, percentage) => {
            // Block truncate if there's an active stream
            // User must press Esc first to stop stream and commit partial to history
            if (this.aiService.isStreaming(workspaceId)) {
                return {
                    success: false,
                    error: "Cannot truncate history while stream is active. Press Esc to stop the stream first.",
                };
            }
            // Truncate chat.jsonl (only operates on committed history)
            // Note: partial.json is NOT touched here - it has its own lifecycle
            // Interrupted messages are committed to history by stream-abort handler
            const truncateResult = await this.historyService.truncateHistory(workspaceId, percentage ?? 1.0);
            if (!truncateResult.success) {
                return { success: false, error: truncateResult.error };
            }
            // Send DeleteMessage event to frontend with deleted historySequence numbers
            const deletedSequences = truncateResult.data;
            if (deletedSequences.length > 0 && this.mainWindow) {
                const deleteMessage = {
                    type: "delete",
                    historySequences: deletedSequences,
                };
                this.mainWindow.webContents.send((0, ipc_constants_1.getChatChannel)(workspaceId), deleteMessage);
            }
            return { success: true, data: undefined };
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_REPLACE_HISTORY, async (_event, workspaceId, summaryMessage) => {
            // Block replace if there's an active stream, UNLESS this is a compacted message
            // (which is called from stream-end handler before stream cleanup completes)
            const isCompaction = summaryMessage.metadata?.compacted === true;
            if (!isCompaction && this.aiService.isStreaming(workspaceId)) {
                return (0, result_1.Err)("Cannot replace history while stream is active. Press Esc to stop the stream first.");
            }
            try {
                // Get all existing messages to collect their historySequence numbers
                const historyResult = await this.historyService.getHistory(workspaceId);
                const deletedSequences = historyResult.success
                    ? historyResult.data
                        .map((msg) => msg.metadata?.historySequence ?? -1)
                        .filter((s) => s >= 0)
                    : [];
                // Clear entire history
                const clearResult = await this.historyService.clearHistory(workspaceId);
                if (!clearResult.success) {
                    return (0, result_1.Err)(`Failed to clear history: ${clearResult.error}`);
                }
                // Append the summary message to history (gets historySequence assigned by backend)
                // Frontend provides the message with all metadata (compacted, timestamp, etc.)
                const appendResult = await this.historyService.appendToHistory(workspaceId, summaryMessage);
                if (!appendResult.success) {
                    return (0, result_1.Err)(`Failed to append summary: ${appendResult.error}`);
                }
                // Send delete event to frontend for all old messages
                if (deletedSequences.length > 0 && this.mainWindow) {
                    const deleteMessage = {
                        type: "delete",
                        historySequences: deletedSequences,
                    };
                    this.mainWindow.webContents.send((0, ipc_constants_1.getChatChannel)(workspaceId), deleteMessage);
                }
                // Send the new summary message to frontend
                if (this.mainWindow) {
                    this.mainWindow.webContents.send((0, ipc_constants_1.getChatChannel)(workspaceId), summaryMessage);
                }
                return (0, result_1.Ok)(undefined);
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                return (0, result_1.Err)(`Failed to replace history: ${message}`);
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_EXECUTE_BASH, async (_event, workspaceId, script, options) => {
            try {
                const env_1 = { stack: [], error: void 0, hasError: false };
                try {
                    // Get workspace metadata
                    const metadataResult = this.aiService.getWorkspaceMetadata(workspaceId);
                    if (!metadataResult.success) {
                        return (0, result_1.Err)(`Failed to get workspace metadata: ${metadataResult.error}`);
                    }
                    const metadata = metadataResult.data;
                    // Get actual workspace path from config (handles both legacy and new format)
                    // Legacy workspaces: path stored in config doesn't match computed path
                    // New workspaces: path can be computed, but config is still source of truth
                    const workspace = this.config.findWorkspace(workspaceId);
                    if (!workspace) {
                        return (0, result_1.Err)(`Workspace ${workspaceId} not found in config`);
                    }
                    // Load project secrets
                    const projectSecrets = this.config.getProjectSecrets(metadata.projectPath);
                    // Create scoped temp directory for this IPC call
                    const tempDir = __addDisposableResource(env_1, new tempDir_1.DisposableTempDir("cmux-ipc-bash"), false);
                    // Create runtime and compute workspace path
                    // Runtime owns the path computation logic
                    const runtimeConfig = metadata.runtimeConfig ?? {
                        type: "local",
                        srcBaseDir: this.config.srcDir,
                    };
                    const runtime = (0, runtimeFactory_1.createRuntime)(runtimeConfig);
                    const workspacePath = runtime.getWorkspacePath(metadata.projectPath, metadata.name);
                    // Create bash tool with workspace's cwd and secrets
                    // All IPC bash calls are from UI (background operations) - use truncate to avoid temp file spam
                    // No init wait needed - IPC calls are user-initiated, not AI tool use
                    const bashTool = (0, bash_1.createBashTool)({
                        cwd: workspacePath, // Bash executes in the workspace directory
                        runtime,
                        secrets: (0, secrets_1.secretsToRecord)(projectSecrets),
                        niceness: options?.niceness,
                        runtimeTempDir: tempDir.path,
                        overflow_policy: "truncate",
                    });
                    // Execute the script with provided options
                    const result = (await bashTool.execute({
                        script,
                        timeout_secs: options?.timeout_secs ?? 120,
                    }, {
                        toolCallId: `bash-${Date.now()}`,
                        messages: [],
                    }));
                    return (0, result_1.Ok)(result);
                }
                catch (e_1) {
                    env_1.error = e_1;
                    env_1.hasError = true;
                }
                finally {
                    __disposeResources(env_1);
                }
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                return (0, result_1.Err)(`Failed to execute bash command: ${message}`);
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.WORKSPACE_OPEN_TERMINAL, async (_event, workspacePath) => {
            try {
                if (process.platform === "darwin") {
                    // macOS - try Ghostty first, fallback to Terminal.app
                    const terminal = await this.findAvailableCommand(["ghostty", "terminal"]);
                    if (terminal === "ghostty") {
                        // Match main: pass workspacePath to 'open -a Ghostty' to avoid regressions
                        const cmd = "open";
                        const args = ["-a", "Ghostty", workspacePath];
                        log_1.log.info(`Opening terminal: ${cmd} ${args.join(" ")}`);
                        const child = (0, child_process_1.spawn)(cmd, args, {
                            detached: true,
                            stdio: "ignore",
                        });
                        child.unref();
                    }
                    else {
                        // Terminal.app opens in the directory when passed as argument
                        const cmd = "open";
                        const args = ["-a", "Terminal", workspacePath];
                        log_1.log.info(`Opening terminal: ${cmd} ${args.join(" ")}`);
                        const child = (0, child_process_1.spawn)(cmd, args, {
                            detached: true,
                            stdio: "ignore",
                        });
                        child.unref();
                    }
                }
                else if (process.platform === "win32") {
                    // Windows
                    const cmd = "cmd";
                    const args = ["/c", "start", "cmd", "/K", "cd", "/D", workspacePath];
                    log_1.log.info(`Opening terminal: ${cmd} ${args.join(" ")}`);
                    const child = (0, child_process_1.spawn)(cmd, args, {
                        detached: true,
                        shell: true,
                        stdio: "ignore",
                    });
                    child.unref();
                }
                else {
                    // Linux - try terminal emulators in order of preference
                    // x-terminal-emulator is checked first as it respects user's system-wide preference
                    const terminals = [
                        { cmd: "x-terminal-emulator", args: [], cwd: workspacePath },
                        { cmd: "ghostty", args: ["--working-directory=" + workspacePath] },
                        { cmd: "alacritty", args: ["--working-directory", workspacePath] },
                        { cmd: "kitty", args: ["--directory", workspacePath] },
                        { cmd: "wezterm", args: ["start", "--cwd", workspacePath] },
                        { cmd: "gnome-terminal", args: ["--working-directory", workspacePath] },
                        { cmd: "konsole", args: ["--workdir", workspacePath] },
                        { cmd: "xfce4-terminal", args: ["--working-directory", workspacePath] },
                        { cmd: "xterm", args: [], cwd: workspacePath },
                    ];
                    const availableTerminal = await this.findAvailableTerminal(terminals);
                    if (availableTerminal) {
                        const cwdInfo = availableTerminal.cwd ? ` (cwd: ${availableTerminal.cwd})` : "";
                        log_1.log.info(`Opening terminal: ${availableTerminal.cmd} ${availableTerminal.args.join(" ")}${cwdInfo}`);
                        const child = (0, child_process_1.spawn)(availableTerminal.cmd, availableTerminal.args, {
                            cwd: availableTerminal.cwd ?? workspacePath,
                            detached: true,
                            stdio: "ignore",
                        });
                        child.unref();
                    }
                    else {
                        log_1.log.error("No terminal emulator found. Tried: " + terminals.map((t) => t.cmd).join(", "));
                    }
                }
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                log_1.log.error(`Failed to open terminal: ${message}`);
            }
        });
        // Debug IPC - only for testing
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.DEBUG_TRIGGER_STREAM_ERROR, (_event, workspaceId, errorMessage) => {
            try {
                // eslint-disable-next-line @typescript-eslint/dot-notation -- accessing private member for testing
                const triggered = this.aiService["streamManager"].debugTriggerStreamError(workspaceId, errorMessage);
                return { success: triggered };
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                log_1.log.error(`Failed to trigger stream error: ${message}`);
                return { success: false, error: message };
            }
        });
    }
    /**
     * Internal workspace removal logic shared by both force and non-force deletion
     */
    async removeWorkspaceInternal(workspaceId, options) {
        try {
            // Get workspace metadata
            const metadataResult = this.aiService.getWorkspaceMetadata(workspaceId);
            if (!metadataResult.success) {
                // If metadata doesn't exist, workspace is already gone - consider it success
                log_1.log.info(`Workspace ${workspaceId} metadata not found, considering removal successful`);
                return { success: true };
            }
            const metadata = metadataResult.data;
            // Get workspace from config to get projectPath
            const workspace = this.config.findWorkspace(workspaceId);
            if (!workspace) {
                log_1.log.info(`Workspace ${workspaceId} metadata exists but not found in config`);
                return { success: true }; // Consider it already removed
            }
            const { projectPath, workspacePath } = workspace;
            // Create runtime instance for this workspace
            // For local runtimes, workdir should be srcDir, not the individual workspace path
            const runtime = (0, runtimeFactory_1.createRuntime)(metadata.runtimeConfig ?? { type: "local", srcBaseDir: this.config.srcDir });
            // Delegate deletion to runtime - it handles all path computation, existence checks, and pruning
            const deleteResult = await runtime.deleteWorkspace(projectPath, metadata.name, options.force);
            if (!deleteResult.success) {
                // Real error (e.g., dirty workspace without force) - return it
                return { success: false, error: deleteResult.error };
            }
            // Remove the workspace from AI service
            const aiResult = await this.aiService.deleteWorkspace(workspaceId);
            if (!aiResult.success) {
                return { success: false, error: aiResult.error };
            }
            // Update config to remove the workspace from all projects
            const projectsConfig = this.config.loadConfigOrDefault();
            let configUpdated = false;
            for (const [_projectPath, projectConfig] of projectsConfig.projects.entries()) {
                const initialCount = projectConfig.workspaces.length;
                projectConfig.workspaces = projectConfig.workspaces.filter((w) => w.path !== workspacePath);
                if (projectConfig.workspaces.length < initialCount) {
                    configUpdated = true;
                }
            }
            if (configUpdated) {
                this.config.saveConfig(projectsConfig);
            }
            // Emit metadata event for workspace removal (with null metadata to indicate deletion)
            const existingSession = this.sessions.get(workspaceId);
            if (existingSession) {
                existingSession.emitMetadata(null);
            }
            else if (this.mainWindow) {
                this.mainWindow.webContents.send(ipc_constants_1.IPC_CHANNELS.WORKSPACE_METADATA, {
                    workspaceId,
                    metadata: null,
                });
            }
            this.disposeSession(workspaceId);
            return { success: true };
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            return { success: false, error: `Failed to remove workspace: ${message}` };
        }
    }
    registerProviderHandlers(ipcMain) {
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.PROVIDERS_SET_CONFIG, (_event, provider, keyPath, value) => {
            try {
                // Load current providers config or create empty
                const providersConfig = this.config.loadProvidersConfig() ?? {};
                // Ensure provider exists
                if (!providersConfig[provider]) {
                    providersConfig[provider] = {};
                }
                // Set nested property value
                let current = providersConfig[provider];
                for (let i = 0; i < keyPath.length - 1; i++) {
                    const key = keyPath[i];
                    if (!(key in current) || typeof current[key] !== "object" || current[key] === null) {
                        current[key] = {};
                    }
                    current = current[key];
                }
                if (keyPath.length > 0) {
                    current[keyPath[keyPath.length - 1]] = value;
                }
                // Save updated config
                this.config.saveProvidersConfig(providersConfig);
                return { success: true, data: undefined };
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                return { success: false, error: `Failed to set provider config: ${message}` };
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.PROVIDERS_LIST, () => {
            try {
                // Return all supported providers, not just configured ones
                // This matches the providers defined in the registry
                return ["anthropic", "openai"];
            }
            catch (error) {
                log_1.log.error("Failed to list providers:", error);
                return [];
            }
        });
    }
    registerProjectHandlers(ipcMain) {
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.PROJECT_CREATE, async (_event, projectPath) => {
            try {
                // Validate and expand path (handles tilde, checks existence and directory status)
                const validation = await (0, pathUtils_1.validateProjectPath)(projectPath);
                if (!validation.valid) {
                    return (0, result_1.Err)(validation.error ?? "Invalid project path");
                }
                // Use the expanded/normalized path
                const normalizedPath = validation.expandedPath;
                const config = this.config.loadConfigOrDefault();
                // Check if project already exists (using normalized path)
                if (config.projects.has(normalizedPath)) {
                    return (0, result_1.Err)("Project already exists");
                }
                // Create new project config
                const projectConfig = {
                    workspaces: [],
                };
                // Add to config with normalized path
                config.projects.set(normalizedPath, projectConfig);
                this.config.saveConfig(config);
                // Return both the config and the normalized path so frontend can use it
                return (0, result_1.Ok)({ projectConfig, normalizedPath });
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                return (0, result_1.Err)(`Failed to create project: ${message}`);
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.PROJECT_REMOVE, (_event, projectPath) => {
            try {
                const config = this.config.loadConfigOrDefault();
                const projectConfig = config.projects.get(projectPath);
                if (!projectConfig) {
                    return (0, result_1.Err)("Project not found");
                }
                // Check if project has any workspaces
                if (projectConfig.workspaces.length > 0) {
                    return (0, result_1.Err)(`Cannot remove project with active workspaces. Please remove all ${projectConfig.workspaces.length} workspace(s) first.`);
                }
                // Remove project from config
                config.projects.delete(projectPath);
                this.config.saveConfig(config);
                // Also remove project secrets if any
                try {
                    this.config.updateProjectSecrets(projectPath, []);
                }
                catch (error) {
                    log_1.log.error(`Failed to clean up secrets for project ${projectPath}:`, error);
                    // Continue - don't fail the whole operation if secrets cleanup fails
                }
                return (0, result_1.Ok)(undefined);
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                return (0, result_1.Err)(`Failed to remove project: ${message}`);
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.PROJECT_LIST, () => {
            try {
                const config = this.config.loadConfigOrDefault();
                // Return array of [projectPath, projectConfig] tuples
                return Array.from(config.projects.entries());
            }
            catch (error) {
                log_1.log.error("Failed to list projects:", error);
                return [];
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.PROJECT_LIST_BRANCHES, async (_event, projectPath) => {
            if (typeof projectPath !== "string" || projectPath.trim().length === 0) {
                throw new Error("Project path is required to list branches");
            }
            try {
                // Validate and expand path (handles tilde)
                const validation = await (0, pathUtils_1.validateProjectPath)(projectPath);
                if (!validation.valid) {
                    throw new Error(validation.error ?? "Invalid project path");
                }
                const normalizedPath = validation.expandedPath;
                const branches = await (0, git_1.listLocalBranches)(normalizedPath);
                const recommendedTrunk = await (0, git_1.detectDefaultTrunkBranch)(normalizedPath, branches);
                return { branches, recommendedTrunk };
            }
            catch (error) {
                log_1.log.error("Failed to list branches:", error);
                throw error instanceof Error ? error : new Error(String(error));
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.PROJECT_SECRETS_GET, (_event, projectPath) => {
            try {
                return this.config.getProjectSecrets(projectPath);
            }
            catch (error) {
                log_1.log.error("Failed to get project secrets:", error);
                return [];
            }
        });
        ipcMain.handle(ipc_constants_1.IPC_CHANNELS.PROJECT_SECRETS_UPDATE, (_event, projectPath, secrets) => {
            try {
                this.config.updateProjectSecrets(projectPath, secrets);
                return (0, result_1.Ok)(undefined);
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                return (0, result_1.Err)(`Failed to update project secrets: ${message}`);
            }
        });
    }
    registerSubscriptionHandlers(ipcMain) {
        // Handle subscription events for chat history
        ipcMain.on(`workspace:chat:subscribe`, (_event, workspaceId) => {
            void (async () => {
                const session = this.getOrCreateSession(workspaceId);
                const chatChannel = (0, ipc_constants_1.getChatChannel)(workspaceId);
                await session.replayHistory((event) => {
                    if (!this.mainWindow) {
                        return;
                    }
                    this.mainWindow.webContents.send(chatChannel, event.message);
                });
            })();
        });
        // Handle subscription events for metadata
        ipcMain.on(ipc_constants_1.IPC_CHANNELS.WORKSPACE_METADATA_SUBSCRIBE, () => {
            try {
                const workspaceMetadata = this.config.getAllWorkspaceMetadata();
                // Emit current metadata for each workspace
                for (const metadata of workspaceMetadata) {
                    this.mainWindow?.webContents.send(ipc_constants_1.IPC_CHANNELS.WORKSPACE_METADATA, {
                        workspaceId: metadata.id,
                        metadata,
                    });
                }
            }
            catch (error) {
                console.error("Failed to emit current metadata:", error);
            }
        });
    }
    /**
     * Check if a command is available in the system PATH or known locations
     */
    async isCommandAvailable(command) {
        // Special handling for ghostty on macOS - check common installation paths
        if (command === "ghostty" && process.platform === "darwin") {
            const ghosttyPaths = [
                "/opt/homebrew/bin/ghostty",
                "/Applications/Ghostty.app/Contents/MacOS/ghostty",
                "/usr/local/bin/ghostty",
            ];
            for (const ghosttyPath of ghosttyPaths) {
                try {
                    const stats = await fsPromises.stat(ghosttyPath);
                    // Check if it's a file and any executable bit is set (owner, group, or other)
                    if (stats.isFile() && (stats.mode & 0o111) !== 0) {
                        return true;
                    }
                }
                catch {
                    // Try next path
                }
            }
            // If none of the known paths work, fall through to which check
        }
        try {
            const result = (0, child_process_1.spawnSync)("which", [command], { encoding: "utf8" });
            return result.status === 0;
        }
        catch {
            return false;
        }
    }
    /**
     * Find the first available command from a list of commands
     */
    async findAvailableCommand(commands) {
        for (const cmd of commands) {
            if (await this.isCommandAvailable(cmd)) {
                return cmd;
            }
        }
        return null;
    }
    /**
     * Find the first available terminal from a list of terminal configurations
     */
    async findAvailableTerminal(terminals) {
        for (const terminal of terminals) {
            if (await this.isCommandAvailable(terminal.cmd)) {
                return terminal;
            }
        }
        return null;
    }
}
exports.IpcMain = IpcMain;
//# sourceMappingURL=ipcMain.js.map