From c54195d39f16c426ffda6c273d4fc693006b62e3 Mon Sep 17 00:00:00 2001
From: Jan Burzinski <156842394+janburzinski@users.noreply.github.com>
Date: Wed, 27 May 2026 18:27:38 +0200
Subject: [PATCH 1/3] fix(resource-monitor): stop sessions when killing
---
src/main/core/pty/controller.ts | 44 ++++++++
.../command-palette/resource-monitor-view.tsx | 106 +++++++++++++++++-
2 files changed, 146 insertions(+), 4 deletions(-)
diff --git a/src/main/core/pty/controller.ts b/src/main/core/pty/controller.ts
index 51620abc36..50d1544622 100644
--- a/src/main/core/pty/controller.ts
+++ b/src/main/core/pty/controller.ts
@@ -81,6 +81,50 @@ export const ptyController = createRPCController({
return ok();
},
+ /**
+ * Stop a session for good without deleting its conversation/terminal record.
+ *
+ * Unlike `kill`, which only terminates the OS process, this routes through
+ * the owning provider so the session is removed from its respawn tracking
+ * (`knownSessionIds`/`sessions`). A bare `kill` leaves those intact, so the
+ * provider's `onExit` handler respawns the PTY ~500ms later — which is what
+ * made killing from the resource monitor appear to do nothing. The tab and
+ * its history are preserved; the session simply stays stopped until the task
+ * is remounted or the user starts a new one.
+ */
+ stopSession: async (sessionId: string) => {
+ const parsed = parsePtySessionId(sessionId);
+ if (!parsed) return err({ type: 'invalid_session' as const });
+ const { scopeId, leafId } = parsed;
+
+ // Agents and terminals are scoped by task id, so the task lookup resolves
+ // the owning provider. Conversation PTYs carry a providerId in their
+ // registry metadata; plain terminals do not — that distinguishes the two.
+ const task = taskManager.getTask(scopeId);
+ if (task) {
+ const isConversation = ptySessionRegistry.getMetadata(sessionId)?.providerId !== undefined;
+ if (isConversation) {
+ await task.conversations.stopSession(leafId);
+ } else {
+ await task.terminals.killTerminal(leafId);
+ }
+ return ok();
+ }
+
+ // Lifecycle scripts are scoped by workspace id (no task match) and never
+ // respawn, so a raw kill is sufficient and safe.
+ const pty = ptySessionRegistry.get(sessionId);
+ if (pty) {
+ try {
+ pty.kill();
+ } catch (e) {
+ log.warn('ptyController.stopSession: error killing PTY', { sessionId, error: String(e) });
+ }
+ }
+ ptySessionRegistry.unregister(sessionId);
+ return ok();
+ },
+
/**
* Upload local files into the task's working directory on a remote SSH host
* and return their remote paths. Uses the SFTP subsystem of the already-
diff --git a/src/renderer/features/command-palette/resource-monitor-view.tsx b/src/renderer/features/command-palette/resource-monitor-view.tsx
index 6edb901a15..0dfd8d4059 100644
--- a/src/renderer/features/command-palette/resource-monitor-view.tsx
+++ b/src/renderer/features/command-palette/resource-monitor-view.tsx
@@ -1,11 +1,13 @@
-import { Activity, ArrowLeft, Check, Copy, Folder, GitBranch, Terminal } from 'lucide-react';
+import { Activity, ArrowLeft, Check, Copy, Folder, GitBranch, Terminal, X } from 'lucide-react';
import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import AgentLogo from '@renderer/lib/components/agent-logo';
+import { rpc } from '@renderer/lib/ipc';
import { agentMeta } from '@renderer/lib/providers/meta';
import { appState } from '@renderer/lib/stores/app-state';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/lib/ui/tooltip';
import { formatBytes } from '@renderer/utils/formatBytes';
+import { cn } from '@renderer/utils/utils';
import type { ResourceAppProcess, ResourceSnapshot } from '@shared/resource-monitor';
import {
appProcessLabel,
@@ -160,6 +162,7 @@ function AgentRow({ entry }: { entry: Entry }) {
const norm = appState.resourceMonitor.normalizedCpu(entry);
const meta = entry.providerId ? agentMeta[entry.providerId] : undefined;
const label = entryLabel(entry);
+ const [armed, setArmed] = useState(false);
return (