Skip to content
Merged
8 changes: 8 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
type TerminalPreset,
} from "@superset/local-db";
import { TRPCError } from "@trpc/server";
import { app } from "electron";
import { quitWithoutConfirmation } from "main/index";
import { localDb } from "main/lib/local-db";
import {
DEFAULT_CONFIRM_ON_QUIT,
Expand Down Expand Up @@ -265,5 +267,11 @@ export const createSettingsRouter = () => {

return { success: true };
}),

restartApp: publicProcedure.mutation(() => {
app.relaunch();
quitWithoutConfirmation();
return { success: true };
}),
});
};
34 changes: 15 additions & 19 deletions apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { localDb } from "main/lib/local-db";
import { tryListExistingDaemonSessions } from "main/lib/terminal";
import { getTerminalHostClient } from "main/lib/terminal-host/client";
import { getWorkspaceRuntimeRegistry } from "main/lib/workspace-runtime";
import { z } from "zod";
import { publicProcedure, router } from "../..";
Expand Down Expand Up @@ -285,23 +287,19 @@ export const createTerminalRouter = () => {
}),

listDaemonSessions: publicProcedure.query(async () => {
// Use capability-based check instead of instanceof
if (!terminal.management) {
return { daemonModeEnabled: false, sessions: [] };
}

const response = await terminal.management.listSessions();
return { daemonModeEnabled: true, sessions: response.sessions };
const { daemonRunning, sessions } = await tryListExistingDaemonSessions();
return { daemonModeEnabled: daemonRunning, sessions };
}),

killAllDaemonSessions: publicProcedure.mutation(async () => {
// Use capability-based check instead of instanceof
if (!terminal.management) {
const client = getTerminalHostClient();
const connected = await client.tryConnectAndAuthenticate();
if (!connected) {
return { daemonModeEnabled: false, killedCount: 0, remainingCount: 0 };
}

// Get sessions before kill for accurate count
const before = await terminal.management.listSessions();
const before = await client.listSessions();
const beforeIds = before.sessions.map((s) => s.sessionId);
for (const id of beforeIds) {
userKilledSessions.add(id);
Expand All @@ -314,7 +312,7 @@ export const createTerminalRouter = () => {
);

// Request kill of all sessions
await terminal.management.killAllSessions();
await client.killAll({});

// Wait and verify loop - poll until sessions are actually dead
// This ensures we don't return success before daemon has finished cleanup
Expand All @@ -325,7 +323,7 @@ export const createTerminalRouter = () => {

for (let i = 0; i < MAX_RETRIES && remainingCount > 0; i++) {
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
const after = await terminal.management.listSessions();
const after = await client.listSessions();
afterIds = after.sessions
.filter((s) => s.isAlive)
.map((s) => s.sessionId);
Expand Down Expand Up @@ -355,31 +353,29 @@ export const createTerminalRouter = () => {
killDaemonSessionsForWorkspace: publicProcedure
.input(z.object({ workspaceId: z.string() }))
.mutation(async ({ input }) => {
// Use capability-based check instead of instanceof
if (!terminal.management) {
const client = getTerminalHostClient();
const connected = await client.tryConnectAndAuthenticate();
if (!connected) {
return { daemonModeEnabled: false, killedCount: 0 };
}

const { sessions } = await terminal.management.listSessions();
const { sessions } = await client.listSessions();
const toKill = sessions.filter(
(session) => session.workspaceId === input.workspaceId,
);

for (const session of toKill) {
userKilledSessions.add(session.sessionId);
await terminal.kill({ paneId: session.sessionId });
await client.kill({ sessionId: session.sessionId });
}

return { daemonModeEnabled: true, killedCount: toKill.length };
}),

clearTerminalHistory: publicProcedure.mutation(async () => {
// Note: Disk-based terminal history was removed. This is now a no-op
// for non-daemon mode. In daemon mode, it resets the history persistence.
if (terminal.management) {
await terminal.management.resetHistoryPersistence();
}

return { success: true };
}),

Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,6 @@ if (!gotTheLock) {
await reconcileDaemonSessions();

// Shutdown orphaned daemon if persistence is disabled
// (cleans up daemon left from previous session with persistence enabled)
await shutdownOrphanedDaemon();

try {
Expand Down
61 changes: 36 additions & 25 deletions apps/desktop/src/main/lib/terminal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
disposeTerminalHostClient,
getTerminalHostClient,
} from "main/lib/terminal-host/client";
import type { ListSessionsResponse } from "main/lib/terminal-host/types";
import { DEFAULT_TERMINAL_PERSISTENCE } from "shared/constants";
import {
DaemonTerminalManager,
Expand All @@ -25,46 +26,38 @@ export type {
// Terminal Manager Selection
// =============================================================================

// Cache the daemon mode setting to avoid repeated DB reads
// This is set once at app startup and doesn't change until restart
let cachedDaemonMode: boolean | null = null;
const DEBUG_TERMINAL = process.env.SUPERSET_TERMINAL_DEBUG === "1";

/**
* Check if daemon mode is enabled.
* Reads from user settings (terminalPersistence) or falls back to env var.
* The value is cached since it requires app restart to take effect.
*/
export function isDaemonModeEnabled(): boolean {
// Return cached value if available
if (cachedDaemonMode !== null) {
return cachedDaemonMode;
}

// First check environment variable override (for development/testing)
if (process.env.SUPERSET_TERMINAL_DAEMON === "1") {
console.log(
"[TerminalManager] Daemon mode: ENABLED (via SUPERSET_TERMINAL_DAEMON env var)",
);
cachedDaemonMode = true;
if (DEBUG_TERMINAL) {
console.log(
"[TerminalManager] Daemon mode: ENABLED (via SUPERSET_TERMINAL_DAEMON env var)",
);
}
return true;
}

// Read from user settings
try {
const row = localDb.select().from(settings).get();
const enabled = row?.terminalPersistence ?? DEFAULT_TERMINAL_PERSISTENCE;
console.log(
`[TerminalManager] Daemon mode: ${enabled ? "ENABLED" : "DISABLED"} (via settings.terminalPersistence)`,
);
cachedDaemonMode = enabled;
if (DEBUG_TERMINAL) {
console.log(
`[TerminalManager] Daemon mode: ${enabled ? "ENABLED" : "DISABLED"} (via settings.terminalPersistence)`,
);
}
return enabled;
} catch (error) {
console.warn(
"[TerminalManager] Failed to read settings, defaulting to disabled:",
error,
);
cachedDaemonMode = DEFAULT_TERMINAL_PERSISTENCE;
return DEFAULT_TERMINAL_PERSISTENCE;
}
}
Expand Down Expand Up @@ -117,20 +110,16 @@ export async function reconcileDaemonSessions(): Promise<void> {

/**
* Shutdown any orphaned daemon process.
* Should be called on app startup when daemon mode is disabled to clean up
* Called on app startup when daemon mode is disabled to clean up
* any daemon left running from a previous session with persistence enabled.
*
* Uses shutdownIfRunning() to avoid spawning a new daemon just to shut it down.
*/
export async function shutdownOrphanedDaemon(): Promise<void> {
if (isDaemonModeEnabled()) {
// Daemon mode is enabled, don't shutdown
return;
}

try {
const client = getTerminalHostClient();
// Use shutdownIfRunning to avoid spawning a daemon if none exists
const { wasRunning } = await client.shutdownIfRunning({
killSessions: true,
});
Expand All @@ -140,13 +129,35 @@ export async function shutdownOrphanedDaemon(): Promise<void> {
console.log("[TerminalManager] No orphaned daemon to shutdown");
}
} catch (error) {
// Unexpected error during shutdown attempt
console.warn(
"[TerminalManager] Error during orphan daemon cleanup:",
error,
);
} finally {
// Always dispose the client to clean up any partial state
disposeTerminalHostClient();
}
}

export async function tryListExistingDaemonSessions(): Promise<{
daemonRunning: boolean;
sessions: ListSessionsResponse["sessions"];
}> {
try {
const client = getTerminalHostClient();
const connected = await client.tryConnectAndAuthenticate();
if (!connected) {
return { daemonRunning: false, sessions: [] };
}

const result = await client.listSessions();
return { daemonRunning: true, sessions: result.sessions };
} catch (error) {
if (DEBUG_TERMINAL) {
console.log(
"[TerminalManager] Failed to list existing daemon sessions:",
error,
);
}
return { daemonRunning: false, sessions: [] };
Comment thread
Kitenite marked this conversation as resolved.
}
}
42 changes: 14 additions & 28 deletions apps/desktop/src/main/lib/tray/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ import {
Tray,
} from "electron";
import { localDb } from "main/lib/local-db";
import {
getActiveTerminalManager,
isDaemonModeEnabled,
} from "main/lib/terminal";
import { DaemonTerminalManager } from "main/lib/terminal/daemon-manager";
import { tryListExistingDaemonSessions } from "main/lib/terminal";
import { getTerminalHostClient } from "main/lib/terminal-host/client";
import type { ListSessionsResponse } from "main/lib/terminal-host/types";

Expand Down Expand Up @@ -118,9 +114,10 @@ function openSessionInSuperset(workspaceId: string): void {

async function killAllSessions(): Promise<void> {
try {
const manager = getActiveTerminalManager();
if (manager instanceof DaemonTerminalManager) {
await manager.forceKillAll();
const client = getTerminalHostClient();
const connected = await client.tryConnectAndAuthenticate();
if (connected) {
await client.killAll({});
console.log("[Tray] Killed all daemon sessions");
Comment on lines 115 to 121

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Log when terminal host is unavailable to avoid silent no-ops.
Right now, if tryConnectAndAuthenticate() returns false, the action silently does nothing. Consider logging (and apply the same pattern in killSession) so failures are diagnosable.

🔧 Suggested tweak
-		const connected = await client.tryConnectAndAuthenticate();
-		if (connected) {
-			await client.killAll({});
-			console.log("[Tray] Killed all daemon sessions");
-		}
+		const connected = await client.tryConnectAndAuthenticate();
+		if (!connected) {
+			console.warn("[Tray/killAllSessions] Terminal host unavailable; no sessions killed");
+		} else {
+			await client.killAll({});
+			console.log("[Tray] Killed all daemon sessions");
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function killAllSessions(): Promise<void> {
try {
const manager = getActiveTerminalManager();
if (manager instanceof DaemonTerminalManager) {
await manager.forceKillAll();
const client = getTerminalHostClient();
const connected = await client.tryConnectAndAuthenticate();
if (connected) {
await client.killAll({});
console.log("[Tray] Killed all daemon sessions");
async function killAllSessions(): Promise<void> {
try {
const client = getTerminalHostClient();
const connected = await client.tryConnectAndAuthenticate();
if (!connected) {
console.warn("[Tray/killAllSessions] Terminal host unavailable; no sessions killed");
} else {
await client.killAll({});
console.log("[Tray] Killed all daemon sessions");
}
} catch (error) {
console.error("[Tray] Failed to kill all sessions:", error);
}
}
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/tray/index.ts` around lines 115 - 121, In
killAllSessions (and similarly in killSession) add a diagnostic log when
tryConnectAndAuthenticate() returns false so the function doesn't silently
no-op: after calling const client = getTerminalHostClient() and awaiting
client.tryConnectAndAuthenticate(), if connected is false call
processLogger.warn or console.warn with a clear message (e.g., "[Tray] Terminal
host unavailable, cannot kill all sessions" and include any identifying context)
and then return; keep the existing success log path unchanged and reuse the same
pattern in killSession to mirror behavior.

}
} catch (error) {
Expand All @@ -132,9 +129,10 @@ async function killAllSessions(): Promise<void> {

async function killSession(paneId: string): Promise<void> {
try {
const manager = getActiveTerminalManager();
if (manager instanceof DaemonTerminalManager) {
await manager.kill({ paneId, deleteHistory: false });
const client = getTerminalHostClient();
const connected = await client.tryConnectAndAuthenticate();
if (connected) {
await client.kill({ sessionId: paneId });
console.log(`[Tray] Killed session: ${paneId}`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
} catch (error) {
Expand Down Expand Up @@ -167,7 +165,7 @@ function formatSessionLabel(

function buildSessionsSubmenu(
sessions: ListSessionsResponse["sessions"],
daemonEnabled: boolean,
daemonRunning: boolean,
): MenuItemConstructorOptions[] {
const aliveSessions = sessions.filter((s) => s.isAlive);
const menuItems: MenuItemConstructorOptions[] = [];
Expand Down Expand Up @@ -222,7 +220,7 @@ function buildSessionsSubmenu(

menuItems.push({
label: "Restart Daemon",
enabled: daemonEnabled,
enabled: daemonRunning,
click: restartDaemon,
});

Expand All @@ -245,22 +243,10 @@ async function restartDaemon(): Promise<void> {
async function updateTrayMenu(): Promise<void> {
if (!tray) return;

const daemonEnabled = isDaemonModeEnabled();
let sessionCount = 0;
let sessions: ListSessionsResponse["sessions"] = [];

if (daemonEnabled) {
try {
const manager = getActiveTerminalManager();
if (manager instanceof DaemonTerminalManager) {
const result = await manager.listDaemonSessions();
sessions = result.sessions;
sessionCount = sessions.filter((s) => s.isAlive).length;
}
} catch {}
}
const { daemonRunning, sessions } = await tryListExistingDaemonSessions();
const sessionCount = sessions.filter((s) => s.isAlive).length;

const sessionsSubmenu = buildSessionsSubmenu(sessions, daemonEnabled);
const sessionsSubmenu = buildSessionsSubmenu(sessions, daemonRunning);
const sessionsLabel =
sessionCount > 0
? `Background Sessions (${sessionCount})`
Expand Down
Loading
Loading