Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "@superset/local-db";
import { TRPCError } from "@trpc/server";
import { localDb } from "main/lib/local-db";
import { getTerminalHostClient } from "main/lib/terminal-host/client";
import {
DEFAULT_CONFIRM_ON_QUIT,
DEFAULT_TERMINAL_LINK_BEHAVIOR,
Expand Down Expand Up @@ -253,7 +254,7 @@ export const createSettingsRouter = () => {

setTerminalPersistence: publicProcedure
.input(z.object({ enabled: z.boolean() }))
.mutation(({ input }) => {
.mutation(async ({ input }) => {
localDb
.insert(settings)
.values({ id: 1, terminalPersistence: input.enabled })
Expand All @@ -263,6 +264,20 @@ export const createSettingsRouter = () => {
})
.run();

// Spawn daemon immediately when enabled so restart/management works
if (input.enabled) {
try {
const client = getTerminalHostClient();
await client.ensureConnected();
} catch (error) {
console.error(
"[Settings] Failed to spawn daemon after enabling persistence:",
error,
);
// Don't fail the mutation - daemon will spawn on next terminal use
}
}

return { success: true };
}),
});
Expand Down
26 changes: 26 additions & 0 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,7 @@ import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { localDb } from "main/lib/local-db";
import { restartDaemon as restartDaemonClient } 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 @@ -383,6 +384,31 @@ export const createTerminalRouter = () => {
return { success: true };
}),

/**
* Restart the terminal daemon.
* Kills all sessions and shuts down the daemon. The daemon will
* auto-spawn on the next terminal operation.
*/
restartDaemon: publicProcedure.mutation(async () => {
if (!terminal.management) {
return { daemonModeEnabled: false, success: false };
}

try {
await restartDaemonClient();
console.log(
"[Terminal Router] Daemon restarted (will spawn on next use)",
);
return { daemonModeEnabled: true, success: true };
} catch (error) {
console.error("[Terminal Router] Failed to restart daemon:", error);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to restart daemon",
});
}
}),

getSession: publicProcedure
.input(z.string())
.query(async ({ input: paneId }) => {
Expand Down
21 changes: 20 additions & 1 deletion apps/desktop/src/main/lib/terminal-host/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1372,7 +1372,15 @@ export class TerminalHostClient extends EventEmitter {
throw error;
}

await this.sendRequest<EmptyResponse>("shutdown", request);
try {
await this.sendRequest<EmptyResponse>("shutdown", request);
} catch (error) {
// "Connection lost" is expected - daemon shuts down before responding
const message = error instanceof Error ? error.message : String(error);
if (!message.includes("Connection lost")) {
throw error;
}
}
return { wasRunning: true };
} finally {
this.disconnect();
Expand Down Expand Up @@ -1424,3 +1432,14 @@ export function disposeTerminalHostClient(): void {
clientInstance = null;
}
}

/**
* Restart the terminal daemon by shutting it down (killing all sessions).
* The daemon will auto-spawn on the next terminal operation.
* Returns true if daemon was running, false if not.
*/
export async function restartDaemon(): Promise<boolean> {
const client = getTerminalHostClient();
const { wasRunning } = await client.shutdownIfRunning({ killSessions: true });
return wasRunning;
}
21 changes: 1 addition & 20 deletions apps/desktop/src/main/lib/terminal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,46 +25,27 @@ 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;
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;
return enabled;
return row?.terminalPersistence ?? DEFAULT_TERMINAL_PERSISTENCE;
} catch (error) {
console.warn(
"[TerminalManager] Failed to read settings, defaulting to disabled:",
error,
);
cachedDaemonMode = DEFAULT_TERMINAL_PERSISTENCE;
return DEFAULT_TERMINAL_PERSISTENCE;
}
}
Expand Down
6 changes: 2 additions & 4 deletions apps/desktop/src/main/lib/tray/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
isDaemonModeEnabled,
} from "main/lib/terminal";
import { DaemonTerminalManager } from "main/lib/terminal/daemon-manager";
import { getTerminalHostClient } from "main/lib/terminal-host/client";
import { restartDaemon as restartDaemonClient } from "main/lib/terminal-host/client";
import type { ListSessionsResponse } from "main/lib/terminal-host/types";

const POLL_INTERVAL_MS = 5000;
Expand Down Expand Up @@ -231,9 +231,7 @@ function buildSessionsSubmenu(

async function restartDaemon(): Promise<void> {
try {
const client = getTerminalHostClient();
await client.shutdownIfRunning({ killSessions: true });
// Daemon auto-spawns on next terminal operation
await restartDaemonClient();
console.log("[Tray] Daemon restarted (will spawn on next use)");
} catch (error) {
console.error("[Tray] Failed to restart daemon:", error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ function TerminalSettingsPage() {

const [confirmKillAllOpen, setConfirmKillAllOpen] = useState(false);
const [confirmClearHistoryOpen, setConfirmClearHistoryOpen] = useState(false);
const [confirmRestartDaemonOpen, setConfirmRestartDaemonOpen] =
useState(false);
const [showSessionList, setShowSessionList] = useState(false);
const [pendingKillSession, setPendingKillSession] = useState<{
sessionId: string;
Expand Down Expand Up @@ -75,6 +77,8 @@ function TerminalSettingsPage() {
onSettled: () => {
// Refetch to ensure sync with server
utils.settings.getTerminalPersistence.invalidate();
// Daemon may have spawned/stopped, refresh session list
utils.terminal.listDaemonSessions.invalidate();
},
});

Expand Down Expand Up @@ -159,6 +163,42 @@ function TerminalSettingsPage() {
},
});

const restartDaemon = electronTrpc.terminal.restartDaemon.useMutation({
onMutate: async () => {
await utils.terminal.listDaemonSessions.cancel();
const previous = utils.terminal.listDaemonSessions.getData();
utils.terminal.listDaemonSessions.setData(undefined, {
daemonModeEnabled: true,
sessions: [],
});
return { previous };
},
onSuccess: (result) => {
if (result.daemonModeEnabled) {
toast.success("Daemon restarted", {
description: "The daemon will start on next terminal use.",
});
} else {
toast.error("Terminal persistence is not active", {
description: "Enable terminal persistence and restart the app.",
});
}
},
onError: (error, _vars, context) => {
if (context?.previous) {
utils.terminal.listDaemonSessions.setData(undefined, context.previous);
}
toast.error("Failed to restart daemon", {
description: error.message,
});
},
onSettled: () => {
setTimeout(() => {
utils.terminal.listDaemonSessions.invalidate();
}, 500);
},
});

const formatTimestamp = (value?: string) => {
if (!value) return "—";
return value.replace("T", " ").replace(/\.\d+Z$/, "Z");
Expand Down Expand Up @@ -335,6 +375,31 @@ function TerminalSettingsPage() {
</div>
)}
</div>

<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-sm font-medium">Restart daemon</Label>
<p className="text-xs text-muted-foreground">
Restart the background terminal daemon process. This will kill all
running sessions.
</p>
<p className="text-xs text-muted-foreground/70 mt-1">
Use this if terminals are behaving unexpectedly or to free up
resources.
</p>
</div>
<Button
variant="secondary"
size="sm"
disabled={
!(terminalPersistence ?? DEFAULT_TERMINAL_PERSISTENCE) ||
restartDaemon.isPending
}
onClick={() => setConfirmRestartDaemonOpen(true)}
>
Restart
</Button>
</div>
</div>

<AlertDialog
Expand Down Expand Up @@ -430,6 +495,54 @@ function TerminalSettingsPage() {
</AlertDialogContent>
</AlertDialog>

<AlertDialog
open={confirmRestartDaemonOpen}
onOpenChange={setConfirmRestartDaemonOpen}
>
<AlertDialogContent className="max-w-[520px] gap-0 p-0">
<AlertDialogHeader className="px-4 pt-4 pb-2">
<AlertDialogTitle className="font-medium">
Restart terminal daemon?
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="text-muted-foreground space-y-1.5">
<span className="block">
This will kill all running terminal sessions and restart the
background daemon process.
</span>
<span className="block">
Use this if terminals are behaving unexpectedly or to free up
resources.
</span>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="px-4 pb-4 pt-2 flex-row justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setConfirmRestartDaemonOpen(false)}
>
Cancel
</Button>
<Button
variant="secondary"
size="sm"
disabled={restartDaemon.isPending}
onClick={() => {
setConfirmRestartDaemonOpen(false);
for (const session of sessions) {
markTerminalKilledByUser(session.sessionId);
}
restartDaemon.mutate();
}}
>
Restart daemon
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

<AlertDialog
open={!!pendingKillSession}
onOpenChange={(open) => {
Expand Down
Loading