Skip to content

Commit c7ddd8d

Browse files
fix(tui): isolate /new sessions to prevent cross-client broadcast (#39217)
When multiple TUI clients connect to the same agent and one calls /new, replies were being broadcast to all connected clients because they shared the same session key. This fix differentiates /new from /reset: - /new: Creates a unique session key (tui-{uuid}) to isolate the client - /reset: Resets the shared session (existing behavior preserved) This ensures each TUI client using /new gets its own isolated session context, preventing unintended message sharing between terminals.
1 parent b4bac48 commit c7ddd8d

File tree

2 files changed

+28
-6
lines changed

2 files changed

+28
-6
lines changed

src/tui/tui-command-handlers.test.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ type SetActivityStatusMock = ReturnType<typeof vi.fn> & ((text: string) => void)
77
function createHarness(params?: {
88
sendChat?: ReturnType<typeof vi.fn>;
99
resetSession?: ReturnType<typeof vi.fn>;
10+
setSession?: ReturnType<typeof vi.fn>;
1011
loadHistory?: LoadHistoryMock;
1112
setActivityStatus?: SetActivityStatusMock;
1213
isConnected?: boolean;
1314
}) {
1415
const sendChat = params?.sendChat ?? vi.fn().mockResolvedValue({ runId: "r1" });
1516
const resetSession = params?.resetSession ?? vi.fn().mockResolvedValue({ ok: true });
17+
const setSession = params?.setSession ?? vi.fn().mockResolvedValue(undefined);
1618
const addUser = vi.fn();
1719
const addSystem = vi.fn();
1820
const requestRender = vi.fn();
@@ -36,7 +38,7 @@ function createHarness(params?: {
3638
closeOverlay: vi.fn(),
3739
refreshSessionInfo: vi.fn(),
3840
loadHistory,
39-
setSession: vi.fn(),
41+
setSession,
4042
refreshAgents: vi.fn(),
4143
abortActive: vi.fn(),
4244
setActivityStatus,
@@ -51,6 +53,7 @@ function createHarness(params?: {
5153
handleCommand,
5254
sendChat,
5355
resetSession,
56+
setSession,
5457
addUser,
5558
addSystem,
5659
requestRender,
@@ -104,16 +107,24 @@ describe("tui command handlers", () => {
104107
expect(requestRender).toHaveBeenCalled();
105108
});
106109

107-
it("passes reset reason when handling /new and /reset", async () => {
110+
it("creates unique session for /new and resets shared session for /reset", async () => {
108111
const loadHistory = vi.fn().mockResolvedValue(undefined);
109-
const { handleCommand, resetSession } = createHarness({ loadHistory });
112+
const setSessionMock = vi.fn().mockResolvedValue(undefined);
113+
const { handleCommand, resetSession } = createHarness({
114+
loadHistory,
115+
setSession: setSessionMock,
116+
});
110117

111118
await handleCommand("/new");
112119
await handleCommand("/reset");
113120

114-
expect(resetSession).toHaveBeenNthCalledWith(1, "agent:main:main", "new");
115-
expect(resetSession).toHaveBeenNthCalledWith(2, "agent:main:main", "reset");
116-
expect(loadHistory).toHaveBeenCalledTimes(2);
121+
// /new creates a unique session key (isolates TUI client) (#39217)
122+
expect(setSessionMock).toHaveBeenCalledTimes(1);
123+
expect(setSessionMock).toHaveBeenCalledWith(expect.stringMatching(/^tui-[a-f0-9]{8}$/));
124+
// /reset still resets the shared session
125+
expect(resetSession).toHaveBeenCalledTimes(1);
126+
expect(resetSession).toHaveBeenCalledWith("agent:main:main", "reset");
127+
expect(loadHistory).toHaveBeenCalledTimes(1); // Only /reset loads history
117128
});
118129

119130
it("reports send failures and marks activity status as error", async () => {

src/tui/tui-command-handlers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,17 @@ export function createCommandHandlers(context: CommandHandlerContext) {
423423
}
424424
break;
425425
case "new":
426+
try {
427+
// Generate unique session key to isolate this TUI client (#39217)
428+
// This ensures /new creates a fresh session that doesn't broadcast
429+
// to other connected TUI clients sharing the original session key.
430+
const uniqueKey = `tui-${randomUUID().slice(0, 8)}`;
431+
await setSession(uniqueKey);
432+
chatLog.addSystem(`new session: ${state.currentSessionKey}`);
433+
} catch (err) {
434+
chatLog.addSystem(`new session failed: ${String(err)}`);
435+
}
436+
break;
426437
case "reset":
427438
try {
428439
// Clear token counts immediately to avoid stale display (#1523)

0 commit comments

Comments
 (0)