From e2e6c644358e1cf2a180237759e6fad1a4bf272e Mon Sep 17 00:00:00 2001 From: Harshith Mullapudi Date: Sun, 18 Oct 2026 03:50:46 +0000 Subject: [PATCH] fix(desktop): Chat toggle attaches to the chat a terminal id already runs (SUPER-2635) A chat-surface automation run starts a host chat session bound to a terminal id that has no PTY. When that id ended up in a terminal pane, the pane offered the CLI/Chat toggle from the chat's binding, and "Chat" replaced it with a chat pane whose sessionId was null. AcpChatPane then created a second session resuming the same agent conversation, so the user's message went to a new agent while the run's session kept running. The switch now reads the terminal's binding: a terminal that already runs a chat gets a chat pane on that chat session, and no terminal is killed, since there is none. The toggle is offered for a chat binding even before the agent has reported its own session id. --- .../hooks/useAgentSurface/useAgentSurface.ts | 12 +++- .../hooks/useAgentSurfaceSwitch/index.ts | 5 +- .../useAgentSurfaceSwitch.test.ts | 33 +++++++++++ .../useAgentSurfaceSwitch.ts | 57 +++++++++++++++---- 4 files changed, 92 insertions(+), 15 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurfaceSwitch/useAgentSurfaceSwitch.test.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurface/useAgentSurface.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurface/useAgentSurface.ts index a508a2b039e..c8f74ac3a36 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurface/useAgentSurface.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurface/useAgentSurface.ts @@ -31,8 +31,16 @@ export function useAgentSurface( const harness = acpHarnessForPreset(binding?.agentId); const agent = - harness && binding?.agentId && binding.agentSessionId && !binding.endedAt - ? { id: binding.agentId, sessionId: binding.agentSessionId } + harness && + binding?.agentId && + (binding.agentSessionId || binding.chatSessionId) && + !binding.endedAt + ? { + id: binding.agentId, + ...(binding.agentSessionId + ? { sessionId: binding.agentSessionId } + : {}), + } : undefined; return { surface: "cli", diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurfaceSwitch/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurfaceSwitch/index.ts index d85f7d8e9e4..19ce3b6f29b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurfaceSwitch/index.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurfaceSwitch/index.ts @@ -3,4 +3,7 @@ export type { AgentSurface, AgentSurfaceSwitch, } from "./useAgentSurfaceSwitch"; -export { useAgentSurfaceSwitch } from "./useAgentSurfaceSwitch"; +export { + chatPaneDataForTerminal, + useAgentSurfaceSwitch, +} from "./useAgentSurfaceSwitch"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurfaceSwitch/useAgentSurfaceSwitch.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurfaceSwitch/useAgentSurfaceSwitch.test.ts new file mode 100644 index 00000000000..dd7a665a6a1 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurfaceSwitch/useAgentSurfaceSwitch.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "bun:test"; +import { chatPaneDataForTerminal } from "./useAgentSurfaceSwitch"; + +describe("chatPaneDataForTerminal", () => { + const agent = { id: "claude", sessionId: "agent-session" }; + + it("attaches to the chat already running on the terminal", () => { + expect( + chatPaneDataForTerminal({ + terminalId: "terminal-1", + agent, + binding: { chatSessionId: "chat-1" }, + }), + ).toEqual({ terminalId: "terminal-1", sessionId: "chat-1", agent }); + }); + + it("starts a chat for a terminal agent", () => { + expect( + chatPaneDataForTerminal({ + terminalId: "terminal-1", + agent, + binding: {}, + }), + ).toEqual({ terminalId: "terminal-1", sessionId: null, agent }); + expect( + chatPaneDataForTerminal({ + terminalId: "terminal-1", + agent, + binding: undefined, + }), + ).toEqual({ terminalId: "terminal-1", sessionId: null, agent }); + }); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurfaceSwitch/useAgentSurfaceSwitch.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurfaceSwitch/useAgentSurfaceSwitch.ts index 0fa10198b91..66ec393b8d0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurfaceSwitch/useAgentSurfaceSwitch.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/AgentTerminalPane/hooks/useAgentSurfaceSwitch/useAgentSurfaceSwitch.ts @@ -4,6 +4,10 @@ import type { CreatePaneInput, RendererContext } from "@superset/panes"; import { toast } from "@superset/ui/sonner"; import { workspaceTrpc } from "@superset/workspace-client"; import { useCallback, useMemo } from "react"; +import { + type TerminalAgentBinding, + useTerminalAgentBindings, +} from "renderer/hooks/host-service/useTerminalAgentBindings"; import { useTerminalAppearance } from "renderer/hooks/useTerminalAppearance"; import { terminalQueryColors } from "renderer/lib/terminal/terminal-query-colors"; import { terminalRuntimeRegistry } from "renderer/lib/terminal/terminal-runtime-registry"; @@ -34,9 +38,31 @@ export type AgentSurfaceSwitch = { stopChat(sessionId: string): Promise; }; +/** + * The chat pane that replaces a terminal pane. A terminal id that already runs + * a chat (an automation run, a host-started chat) keeps that session: a pane + * with no session id starts a second agent on the same conversation. + */ +export function chatPaneDataForTerminal({ + terminalId, + agent, + binding, +}: { + terminalId: string; + agent: AgentIdentity; + binding: Pick | undefined; +}): ChatPaneData { + return { + terminalId, + sessionId: binding?.chatSessionId ?? null, + agent, + }; +} + export function useAgentSurfaceSwitch(workspaceId: string): AgentSurfaceSwitch { const { t } = useLingui(); const wiring = useChatWiring(); + const bindings = useTerminalAgentBindings(workspaceId); const killTerminal = workspaceTrpc.terminal.killSession.useMutation(); const runAgent = workspaceTrpc.agents.run.useMutation(); const appearance = useTerminalAppearance(); @@ -75,24 +101,23 @@ export function useAgentSurfaceSwitch(workspaceId: string): AgentSurfaceSwitch { if (surface === "acp") { if (ctx.pane.kind !== "terminal" || !agent) return; const { terminalId } = ctx.pane.data as TerminalPaneData; + const binding = bindings.get(terminalId); terminalRuntimeRegistry.dispose(terminalId); - try { - await killTerminal.mutateAsync({ terminalId, workspaceId }); - } catch (error) { - console.warn("[acp-chat] could not stop the terminal", error); - toast.error(t({ message: "Couldn't stop the agent's terminal" })); - return; + if (!binding?.chatSessionId) { + try { + await killTerminal.mutateAsync({ terminalId, workspaceId }); + } catch (error) { + console.warn("[acp-chat] could not stop the terminal", error); + toast.error(t({ message: "Couldn't stop the agent's terminal" })); + return; + } } replaceWith({ kind: "chat-v3", ...(ctx.pane.titleOverride ? { titleOverride: ctx.pane.titleOverride } : {}), - data: { - terminalId, - sessionId: null, - agent, - } satisfies ChatPaneData, + data: chatPaneDataForTerminal({ terminalId, agent, binding }), }); return; } @@ -134,7 +159,15 @@ export function useAgentSurfaceSwitch(workspaceId: string): AgentSurfaceSwitch { }); } }, - [killTerminal, runAgent, stopChat, workspaceId, t, appearance.theme], + [ + bindings, + killTerminal, + runAgent, + stopChat, + workspaceId, + t, + appearance.theme, + ], ); return useMemo(