diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index c7139eb37fb..c3e61069656 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -21,7 +21,7 @@ import { PROTOCOL_SCHEME, } from "shared/constants"; import { getWorkspaceName } from "shared/env.shared"; -import { setupAgentHooks } from "./lib/agent-setup"; +import { ensureAgentHooks } from "./lib/agent-setup"; import { initAppState } from "./lib/app-state"; import { requestAppleEventsAccess } from "./lib/apple-events-permission"; import { setupAutoUpdater } from "./lib/auto-updater"; @@ -296,11 +296,9 @@ if (!gotTheLock) { // Must happen before renderer restore runs await reconcileDaemonSessions(); - try { - setupAgentHooks(); - } catch (error) { + void ensureAgentHooks().catch((error) => { console.error("[main] Failed to set up agent hooks:", error); - } + }); await makeAppSetup(() => MainWindow()); setupAutoUpdater(); diff --git a/apps/desktop/src/main/lib/agent-setup/ensure-agent-hooks.ts b/apps/desktop/src/main/lib/agent-setup/ensure-agent-hooks.ts index 9ae403a5e5b..ac4d6b587c0 100644 --- a/apps/desktop/src/main/lib/agent-setup/ensure-agent-hooks.ts +++ b/apps/desktop/src/main/lib/agent-setup/ensure-agent-hooks.ts @@ -5,6 +5,7 @@ import { buildWrapperScript, COPILOT_HOOK_MARKER, CURSOR_HOOK_MARKER, + cleanupGlobalOpenCodePlugin, GEMINI_HOOK_MARKER, getClaudeSettingsContent, getClaudeSettingsPath, @@ -18,7 +19,6 @@ import { getGeminiHookScriptPath, getGeminiSettingsJsonContent, getGeminiSettingsJsonPath, - getOpenCodeGlobalPluginPath, getOpenCodePluginContent, getOpenCodePluginPath, getWrapperPath, @@ -31,11 +31,24 @@ import { NOTIFY_SCRIPT_MARKER, } from "./notify-hook"; import { + BASH_DIR, BIN_DIR, HOOKS_DIR, OPENCODE_CONFIG_DIR, OPENCODE_PLUGIN_DIR, + ZSH_DIR, } from "./paths"; +import { + getBashRcfileContent, + getBashRcfilePath, + getZshLoginContent, + getZshLoginPath, + getZshProfileContent, + getZshProfilePath, + getZshRcContent, + getZshRcPath, + SHELL_WRAPPER_MARKER, +} from "./shell-wrappers"; let inFlight: Promise | null = null; @@ -134,140 +147,158 @@ export function ensureAgentHooks(): Promise { inFlight = (async () => { await new Promise((resolve) => setImmediate(resolve)); - await fs.mkdir(BIN_DIR, { recursive: true }); - await fs.mkdir(HOOKS_DIR, { recursive: true }); - await fs.mkdir(OPENCODE_CONFIG_DIR, { recursive: true }); - await fs.mkdir(OPENCODE_PLUGIN_DIR, { recursive: true }); - const globalOpenCodePluginPath = getOpenCodeGlobalPluginPath(); - try { - await fs.mkdir(path.dirname(globalOpenCodePluginPath), { - recursive: true, - }); - } catch (error) { - console.warn( - "[agent-setup] Failed to create global OpenCode plugin directory:", - error, - ); - } + // Phase 1: create all directories in parallel + await Promise.all([ + fs.mkdir(BIN_DIR, { recursive: true }), + fs.mkdir(HOOKS_DIR, { recursive: true }), + fs.mkdir(ZSH_DIR, { recursive: true }), + fs.mkdir(BASH_DIR, { recursive: true }), + fs.mkdir(OPENCODE_CONFIG_DIR, { recursive: true }), + fs.mkdir(OPENCODE_PLUGIN_DIR, { recursive: true }), + ]); + cleanupGlobalOpenCodePlugin(); + // Phase 2: notify script + claude settings (other files depend on these) const notifyPath = getNotifyScriptPath(); - await ensureScriptFile({ - filePath: notifyPath, - content: getNotifyScriptContent(), - mode: 0o755, - marker: NOTIFY_SCRIPT_MARKER, - logLabel: "notify hook", - }); - - await ensureClaudeSettings(); + await Promise.all([ + ensureScriptFile({ + filePath: notifyPath, + content: getNotifyScriptContent(), + mode: 0o755, + marker: NOTIFY_SCRIPT_MARKER, + logLabel: "notify hook", + }), + ensureClaudeSettings(), + ]); - const wrappers: Array<{ binaryName: string; content: string }> = [ - { - binaryName: "claude", + // Phase 3: everything else in parallel + await Promise.all([ + // Agent wrappers + ensureScriptFile({ + filePath: getWrapperPath("claude"), content: buildWrapperScript( "claude", `exec "$REAL_BIN" --settings "${getClaudeSettingsPath()}" "$@"`, ), - }, - { - binaryName: "codex", + mode: 0o755, + marker: WRAPPER_MARKER, + logLabel: "claude wrapper", + }), + ensureScriptFile({ + filePath: getWrapperPath("codex"), content: buildWrapperScript( "codex", `exec "$REAL_BIN" -c 'notify=["bash","${notifyPath}"]' "$@"`, ), - }, - { - binaryName: "opencode", + mode: 0o755, + marker: WRAPPER_MARKER, + logLabel: "codex wrapper", + }), + ensureScriptFile({ + filePath: getWrapperPath("opencode"), content: buildWrapperScript( "opencode", `export OPENCODE_CONFIG_DIR="${OPENCODE_CONFIG_DIR}"\nexec "$REAL_BIN" "$@"`, ), - }, - { - binaryName: "cursor-agent", + mode: 0o755, + marker: WRAPPER_MARKER, + logLabel: "opencode wrapper", + }), + ensureScriptFile({ + filePath: getWrapperPath("cursor-agent"), content: buildWrapperScript("cursor-agent", `exec "$REAL_BIN" "$@"`), - }, - { - binaryName: "gemini", + mode: 0o755, + marker: WRAPPER_MARKER, + logLabel: "cursor-agent wrapper", + }), + ensureScriptFile({ + filePath: getWrapperPath("gemini"), content: buildWrapperScript("gemini", `exec "$REAL_BIN" "$@"`), - }, - { - binaryName: "copilot", + mode: 0o755, + marker: WRAPPER_MARKER, + logLabel: "gemini wrapper", + }), + ensureScriptFile({ + filePath: getWrapperPath("copilot"), content: buildWrapperScript("copilot", buildCopilotWrapperExecLine()), - }, - ]; - - for (const { binaryName, content } of wrappers) { - await ensureScriptFile({ - filePath: getWrapperPath(binaryName), - content, mode: 0o755, marker: WRAPPER_MARKER, - logLabel: `${binaryName} wrapper`, - }); - } - - await ensureScriptFile({ - filePath: getOpenCodePluginPath(), - content: getOpenCodePluginContent(notifyPath), - mode: 0o644, - marker: OPENCODE_PLUGIN_MARKER, - logLabel: "OpenCode plugin", - }); + logLabel: "copilot wrapper", + }), - try { - await ensureScriptFile({ - filePath: globalOpenCodePluginPath, + // Plugins + ensureScriptFile({ + filePath: getOpenCodePluginPath(), content: getOpenCodePluginContent(notifyPath), mode: 0o644, marker: OPENCODE_PLUGIN_MARKER, - logLabel: "OpenCode global plugin", - }); - } catch (error) { - console.warn( - "[agent-setup] Failed to write global OpenCode plugin:", - error, - ); - } - - await ensureScriptFile({ - filePath: getCursorHookScriptPath(), - content: getCursorHookScriptContent(), - mode: 0o755, - marker: CURSOR_HOOK_MARKER, - logLabel: "Cursor hook script", - }); + logLabel: "OpenCode plugin", + }), - try { - await ensureCursorHooksJson(); - } catch (error) { - console.warn("[agent-setup] Failed to write Cursor hooks.json:", error); - } - - await ensureScriptFile({ - filePath: getGeminiHookScriptPath(), - content: getGeminiHookScriptContent(), - mode: 0o755, - marker: GEMINI_HOOK_MARKER, - logLabel: "Gemini hook script", - }); + // Hook scripts + ensureScriptFile({ + filePath: getCursorHookScriptPath(), + content: getCursorHookScriptContent(), + mode: 0o755, + marker: CURSOR_HOOK_MARKER, + logLabel: "Cursor hook script", + }), + ensureScriptFile({ + filePath: getGeminiHookScriptPath(), + content: getGeminiHookScriptContent(), + mode: 0o755, + marker: GEMINI_HOOK_MARKER, + logLabel: "Gemini hook script", + }), + ensureScriptFile({ + filePath: getCopilotHookScriptPath(), + content: getCopilotHookScriptContent(), + mode: 0o755, + marker: COPILOT_HOOK_MARKER, + logLabel: "Copilot hook script", + }), - try { - await ensureGeminiSettings(); - } catch (error) { - console.warn( - "[agent-setup] Failed to write Gemini settings.json:", - error, - ); - } + // External tool settings (may fail if dirs don't exist) + ensureCursorHooksJson().catch((error) => + console.warn("[agent-setup] Failed to write Cursor hooks.json:", error), + ), + ensureGeminiSettings().catch((error) => + console.warn( + "[agent-setup] Failed to write Gemini settings.json:", + error, + ), + ), - await ensureScriptFile({ - filePath: getCopilotHookScriptPath(), - content: getCopilotHookScriptContent(), - mode: 0o755, - marker: COPILOT_HOOK_MARKER, - logLabel: "Copilot hook script", - }); + // Shell wrappers + ensureScriptFile({ + filePath: getZshProfilePath(), + content: getZshProfileContent(), + mode: 0o644, + marker: SHELL_WRAPPER_MARKER, + logLabel: "zsh .zprofile", + }), + ensureScriptFile({ + filePath: getZshRcPath(), + content: getZshRcContent(), + mode: 0o644, + marker: SHELL_WRAPPER_MARKER, + logLabel: "zsh .zshrc", + }), + ensureScriptFile({ + filePath: getZshLoginPath(), + content: getZshLoginContent(), + mode: 0o644, + marker: SHELL_WRAPPER_MARKER, + logLabel: "zsh .zlogin", + }), + ensureScriptFile({ + filePath: getBashRcfilePath(), + content: getBashRcfileContent(), + mode: 0o644, + marker: SHELL_WRAPPER_MARKER, + logLabel: "bash rcfile", + }), + ]); })().finally(() => { inFlight = null; }); diff --git a/apps/desktop/src/main/lib/agent-setup/index.ts b/apps/desktop/src/main/lib/agent-setup/index.ts index bbc9e350f89..e473db43c3f 100644 --- a/apps/desktop/src/main/lib/agent-setup/index.ts +++ b/apps/desktop/src/main/lib/agent-setup/index.ts @@ -1,68 +1,6 @@ -import fs from "node:fs"; -import { - cleanupGlobalOpenCodePlugin, - createClaudeWrapper, - createCodexWrapper, - createCopilotHookScript, - createCopilotWrapper, - createCursorAgentWrapper, - createCursorHookScript, - createCursorHooksJson, - createGeminiHookScript, - createGeminiSettingsJson, - createGeminiWrapper, - createOpenCodePlugin, - createOpenCodeWrapper, -} from "./agent-wrappers"; -import { createNotifyScript } from "./notify-hook"; -import { - BASH_DIR, - BIN_DIR, - HOOKS_DIR, - OPENCODE_PLUGIN_DIR, - ZSH_DIR, -} from "./paths"; -import { - createBashWrapper, - createZshWrapper, +export { ensureAgentHooks } from "./ensure-agent-hooks"; +export { getCommandShellArgs, getShellArgs, getShellEnv, } from "./shell-wrappers"; - -export function setupAgentHooks(): void { - console.log("[agent-setup] Initializing agent hooks..."); - - fs.mkdirSync(BIN_DIR, { recursive: true }); - fs.mkdirSync(HOOKS_DIR, { recursive: true }); - fs.mkdirSync(ZSH_DIR, { recursive: true }); - fs.mkdirSync(BASH_DIR, { recursive: true }); - fs.mkdirSync(OPENCODE_PLUGIN_DIR, { recursive: true }); - - cleanupGlobalOpenCodePlugin(); - - createNotifyScript(); - createClaudeWrapper(); - createCodexWrapper(); - createOpenCodePlugin(); - createOpenCodeWrapper(); - createCursorHookScript(); - createCursorAgentWrapper(); - createCursorHooksJson(); - createGeminiHookScript(); - createGeminiWrapper(); - createGeminiSettingsJson(); - createCopilotHookScript(); - createCopilotWrapper(); - - createZshWrapper(); - createBashWrapper(); - - console.log("[agent-setup] Agent hooks initialized"); -} - -export function getSupersetBinDir(): string { - return BIN_DIR; -} - -export { getCommandShellArgs, getShellArgs, getShellEnv }; diff --git a/apps/desktop/src/main/lib/agent-setup/shell-wrappers.test.ts b/apps/desktop/src/main/lib/agent-setup/shell-wrappers.test.ts index a14554f62b7..c1ff6d4546b 100644 --- a/apps/desktop/src/main/lib/agent-setup/shell-wrappers.test.ts +++ b/apps/desktop/src/main/lib/agent-setup/shell-wrappers.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; -import { mkdirSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; +import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { homedir, tmpdir } from "node:os"; import path from "node:path"; const TEST_ROOT = path.join( @@ -23,8 +23,29 @@ mock.module("./paths", () => ({ OPENCODE_PLUGIN_DIR: TEST_OPENCODE_PLUGIN_DIR, })); -const { createBashWrapper, createZshWrapper, getCommandShellArgs } = - await import("./shell-wrappers"); +const { + getBashRcfileContent, + getBashRcfilePath, + getCommandShellArgs, + getShellArgs, + getShellEnv, + getZshLoginContent, + getZshLoginPath, + getZshProfileContent, + getZshProfilePath, + getZshRcContent, + getZshRcPath, +} = await import("./shell-wrappers"); + +function writeZshWrappers(): void { + writeFileSync(getZshProfilePath(), getZshProfileContent(), { mode: 0o644 }); + writeFileSync(getZshRcPath(), getZshRcContent(), { mode: 0o644 }); + writeFileSync(getZshLoginPath(), getZshLoginContent(), { mode: 0o644 }); +} + +function writeBashWrapper(): void { + writeFileSync(getBashRcfilePath(), getBashRcfileContent(), { mode: 0o644 }); +} describe("shell-wrappers", () => { beforeEach(() => { @@ -38,7 +59,7 @@ describe("shell-wrappers", () => { }); it("creates zsh wrappers with interactive .zlogin sourcing and command shims", () => { - createZshWrapper(); + writeZshWrappers(); const zshrc = readFileSync(path.join(TEST_ZSH_DIR, ".zshrc"), "utf-8"); const zlogin = readFileSync(path.join(TEST_ZSH_DIR, ".zlogin"), "utf-8"); @@ -48,7 +69,8 @@ describe("shell-wrappers", () => { expect(zshrc).toContain(`codex() { "${TEST_BIN_DIR}/codex" "$@"; }`); expect(zshrc).toContain(`opencode() { "${TEST_BIN_DIR}/opencode" "$@"; }`); expect(zshrc).toContain(`copilot() { "${TEST_BIN_DIR}/copilot" "$@"; }`); - expect(zshrc).toContain("rehash 2>/dev/null || true"); + // rehash only runs in .zlogin (after all init files), not in .zshrc + expect(zshrc).not.toContain("rehash"); expect(zlogin).toContain("if [[ -o interactive ]]; then"); expect(zlogin).toContain('source "$_superset_home/.zlogin"'); @@ -59,7 +81,7 @@ describe("shell-wrappers", () => { }); it("creates bash wrapper with command shims and idempotent PATH prepend", () => { - createBashWrapper(); + writeBashWrapper(); const rcfile = readFileSync(path.join(TEST_BASH_DIR, "rcfile"), "utf-8"); expect(rcfile).toContain("_superset_prepend_bin()"); @@ -71,7 +93,7 @@ describe("shell-wrappers", () => { }); it("uses login zsh command args when wrappers exist", () => { - createZshWrapper(); + writeZshWrappers(); const args = getCommandShellArgs("/bin/zsh", "echo ok"); expect(args).toEqual([ @@ -84,4 +106,25 @@ describe("shell-wrappers", () => { const args = getCommandShellArgs("/bin/zsh", "echo ok"); expect(args).toEqual(["-lc", "echo ok"]); }); + + it("only injects zsh wrapper env when wrapper files exist", () => { + expect(getShellEnv("/bin/zsh")).toEqual({}); + + writeZshWrappers(); + + expect(getShellEnv("/bin/zsh")).toEqual({ + SUPERSET_ORIG_ZDOTDIR: process.env.ZDOTDIR || homedir(), + ZDOTDIR: TEST_ZSH_DIR, + }); + }); + + it("falls back to login bash when wrapper is missing", () => { + expect(getShellArgs("/bin/bash")).toEqual(["-l"]); + + writeBashWrapper(); + expect(getShellArgs("/bin/bash")).toEqual([ + "--rcfile", + path.join(TEST_BASH_DIR, "rcfile"), + ]); + }); }); diff --git a/apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts b/apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts index 8d55f150e28..89a7bbd1c1d 100644 --- a/apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts +++ b/apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts @@ -1,11 +1,17 @@ -import fs from "node:fs"; +import { existsSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { BASH_DIR, BIN_DIR, ZSH_DIR } from "./paths"; +const ZSH_PROFILE = path.join(ZSH_DIR, ".zprofile"); const ZSH_RC = path.join(ZSH_DIR, ".zshrc"); +const ZSH_LOGIN = path.join(ZSH_DIR, ".zlogin"); const BASH_RCFILE = path.join(BASH_DIR, "rcfile"); +const SHELL_WRAPPER_SIGNATURE = "# Superset shell-wrapper"; +const SHELL_WRAPPER_VERSION = "v2"; +export const SHELL_WRAPPER_MARKER = `${SHELL_WRAPPER_SIGNATURE} ${SHELL_WRAPPER_VERSION}`; + /** Agent binaries that get wrapper shims to guarantee resolution. */ const SHIMMED_BINARIES = ["claude", "codex", "opencode", "gemini", "copilot"]; @@ -31,36 +37,56 @@ function buildPathPrependFunction(): string { _superset_prepend_bin`; } -export function createZshWrapper(): void { - // .zprofile must NOT reset ZDOTDIR — our .zshrc needs to run after it - const zprofilePath = path.join(ZSH_DIR, ".zprofile"); - const zprofileScript = `# Superset zsh profile wrapper +// --- Content getters (pure, no I/O) --- + +export function getZshProfilePath(): string { + return path.join(ZSH_DIR, ".zprofile"); +} + +export function getZshRcPath(): string { + return path.join(ZSH_DIR, ".zshrc"); +} + +export function getZshLoginPath(): string { + return path.join(ZSH_DIR, ".zlogin"); +} + +export function getBashRcfilePath(): string { + return path.join(BASH_DIR, "rcfile"); +} + +export function getZshProfileContent(): string { + return `${SHELL_WRAPPER_MARKER} +# Superset zsh profile wrapper _superset_home="\${SUPERSET_ORIG_ZDOTDIR:-$HOME}" [[ -f "$_superset_home/.zprofile" ]] && source "$_superset_home/.zprofile" `; - fs.writeFileSync(zprofilePath, zprofileScript, { mode: 0o644 }); +} - // Reset ZDOTDIR before sourcing so Oh My Zsh works correctly - const zshrcPath = path.join(ZSH_DIR, ".zshrc"); - const zshrcScript = `# Superset zsh rc wrapper +export function getZshRcContent(): string { + // .zshrc applies PATH + shims after sourcing the user's .zshrc. + // No rehash here — .zlogin re-applies everything after .zlogin runs, + // so a single rehash there covers both phases. + return `${SHELL_WRAPPER_MARKER} +# Superset zsh rc wrapper _superset_home="\${SUPERSET_ORIG_ZDOTDIR:-$HOME}" export ZDOTDIR="$_superset_home" [[ -f "$_superset_home/.zshrc" ]] && source "$_superset_home/.zshrc" ${buildPathPrependFunction()} ${buildShimFunctions()} -rehash 2>/dev/null || true # Restore ZDOTDIR so our .zlogin runs after user's .zlogin export ZDOTDIR="${ZSH_DIR}" `; - fs.writeFileSync(zshrcPath, zshrcScript, { mode: 0o644 }); +} +export function getZshLoginContent(): string { // .zlogin runs AFTER .zshrc in login shells. By restoring ZDOTDIR above, // zsh sources our .zlogin instead of the user's directly. We source the // user's .zlogin only for interactive shells, then re-apply command shims // and prepend BIN_DIR so tools like mise, nvm, or PATH exports in .zlogin // can't shadow our wrappers. - const zloginPath = path.join(ZSH_DIR, ".zlogin"); - const zloginScript = `# Superset zsh login wrapper + return `${SHELL_WRAPPER_MARKER} +# Superset zsh login wrapper _superset_home="\${SUPERSET_ORIG_ZDOTDIR:-$HOME}" if [[ -o interactive ]]; then [[ -f "$_superset_home/.zlogin" ]] && source "$_superset_home/.zlogin" @@ -70,14 +96,11 @@ ${buildShimFunctions()} rehash 2>/dev/null || true export ZDOTDIR="$_superset_home" `; - fs.writeFileSync(zloginPath, zloginScript, { mode: 0o644 }); - - console.log("[agent-setup] Created zsh wrapper"); } -export function createBashWrapper(): void { - const rcfilePath = path.join(BASH_DIR, "rcfile"); - const script = `# Superset bash rcfile wrapper +export function getBashRcfileContent(): string { + return `${SHELL_WRAPPER_MARKER} +# Superset bash rcfile wrapper # Source system profile [[ -f /etc/profile ]] && source /etc/profile @@ -101,12 +124,20 @@ hash -r 2>/dev/null || true # Minimal prompt (path/env shown in toolbar) - emerald to match app theme export PS1=$'\\[\\e[1;38;2;52;211;153m\\]❯\\[\\e[0m\\] ' `; - fs.writeFileSync(rcfilePath, script, { mode: 0o644 }); - console.log("[agent-setup] Created bash wrapper"); +} + +// --- Runtime helpers --- + +function hasZshWrappers(): boolean { + return existsSync(ZSH_PROFILE) && existsSync(ZSH_RC) && existsSync(ZSH_LOGIN); +} + +function hasBashWrapper(): boolean { + return existsSync(BASH_RCFILE); } export function getShellEnv(shell: string): Record { - if (shell.includes("zsh")) { + if (shell.includes("zsh") && hasZshWrappers()) { return { SUPERSET_ORIG_ZDOTDIR: process.env.ZDOTDIR || os.homedir(), ZDOTDIR: ZSH_DIR, @@ -120,6 +151,9 @@ export function getShellArgs(shell: string): string[] { return ["-l"]; } if (shell.includes("bash")) { + if (!hasBashWrapper()) { + return ["-l"]; + } return ["--rcfile", BASH_RCFILE]; } return []; @@ -128,17 +162,17 @@ export function getShellArgs(shell: string): string[] { /** * Shell args for non-interactive command execution (`-c`) that sources * user profiles via wrappers. Falls back to login shell if wrappers - * don't exist yet (e.g. before setupAgentHooks runs). + * don't exist yet (e.g. before ensureAgentHooks runs). * * Unlike getShellArgs (interactive), we must source profiles inline because: * - zsh skips .zshrc for non-interactive shells * - bash ignores --rcfile when -c is present */ export function getCommandShellArgs(shell: string, command: string): string[] { - if (shell.includes("zsh") && fs.existsSync(ZSH_RC)) { + if (shell.includes("zsh") && existsSync(ZSH_RC)) { return ["-lc", `source "${ZSH_RC}" && ${command}`]; } - if (shell.includes("bash") && fs.existsSync(BASH_RCFILE)) { + if (shell.includes("bash") && existsSync(BASH_RCFILE)) { return ["-c", `source "${BASH_RCFILE}" && ${command}`]; } return ["-lc", command]; diff --git a/apps/desktop/src/main/lib/terminal/daemon/daemon-manager.ts b/apps/desktop/src/main/lib/terminal/daemon/daemon-manager.ts index e41ee988fc7..a1dbb7ae985 100644 --- a/apps/desktop/src/main/lib/terminal/daemon/daemon-manager.ts +++ b/apps/desktop/src/main/lib/terminal/daemon/daemon-manager.ts @@ -1,5 +1,6 @@ import { EventEmitter } from "node:events"; import { workspaces } from "@superset/local-db"; +import { ensureAgentHooks } from "main/lib/agent-setup"; import { track } from "main/lib/analytics"; import { appState } from "main/lib/app-state"; import { localDb } from "main/lib/local-db"; @@ -25,6 +26,8 @@ import { HistoryManager } from "./history-manager"; import { PrioritySemaphore } from "./priority-semaphore"; import type { ColdRestoreInfo, SessionInfo } from "./types"; +const AGENT_HOOKS_TIMEOUT_MS = 2000; + export class DaemonTerminalManager extends EventEmitter { private client!: TerminalHostClient; private sessions = new Map(); @@ -371,6 +374,10 @@ export class DaemonTerminalManager extends EventEmitter { await this.historyManager.cleanupHistory(paneId, workspaceId); } + if (!daemonHasSession) { + await this.waitForAgentHooks(); + } + const shell = getDefaultShell(); const env = buildTerminalEnv({ shell, @@ -718,6 +725,21 @@ export class DaemonTerminalManager extends EventEmitter { } } + private async waitForAgentHooks(): Promise { + const timeout = new Promise((resolve) => { + const timer = setTimeout(resolve, AGENT_HOOKS_TIMEOUT_MS); + timer.unref?.(); + }); + try { + await Promise.race([ensureAgentHooks(), timeout]); + } catch (error) { + console.warn( + "[DaemonTerminalManager] Failed to ensure agent hooks:", + error, + ); + } + } + async resetHistoryPersistence(): Promise { await this.historyManager.resetAll(this.sessions); } diff --git a/apps/desktop/src/main/terminal-host/session.ts b/apps/desktop/src/main/terminal-host/session.ts index f0874b6aa50..474b5ef364d 100644 --- a/apps/desktop/src/main/terminal-host/session.ts +++ b/apps/desktop/src/main/terminal-host/session.ts @@ -11,6 +11,7 @@ import { type ChildProcess, spawn } from "node:child_process"; import type { Socket } from "node:net"; import * as path from "node:path"; +import { getShellArgs as getAgentShellArgs } from "../lib/agent-setup"; import { buildSafeEnv } from "../lib/terminal/env"; import { HeadlessEmulator } from "../lib/terminal-host/headless-emulator"; import type { @@ -939,9 +940,14 @@ export class Session { * Get shell arguments for login shell */ private getShellArgs(shell: string): string[] { + const args = getAgentShellArgs(shell); + if (args.length > 0) { + return args; + } + const shellName = shell.split("/").pop() || ""; - if (["zsh", "bash", "sh", "ksh", "fish"].includes(shellName)) { + if (["sh", "ksh", "fish"].includes(shellName)) { return ["-l"]; }