From 2c161d2a8fc84d6faab29fbe8b7d8f625943f047 Mon Sep 17 00:00:00 2001 From: roboomp Date: Sat, 11 Jul 2026 17:29:38 +0000 Subject: [PATCH] fix(coding-agent): preserved btw codex websocket routing - Preserved the session websocket preference for ephemeral /btw side-channel turns so Codex websocket-only models do not fall back to SSE. - Prioritized active /btw and /omfg panels in Esc handling before loop, maintenance, and main-turn interrupts. - Added regression coverage for side-channel websocket options and /btw Escape priority. Fixes #5213 --- packages/coding-agent/CHANGELOG.md | 1 + .../src/modes/controllers/input-controller.ts | 15 ++++++---- .../coding-agent/src/session/agent-session.ts | 2 +- .../agent-session-message-pipeline.test.ts | 5 ++-- .../test/input-controller-escape.test.ts | 29 +++++++++++++++++++ 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 049ccb186..08a89fe7b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -29,6 +29,7 @@ ### Fixed +- Fixed `/btw` side-channel turns on Codex models such as `gpt-5.6-luna` by preserving the session websocket preference instead of forcing SSE, and made Esc dismiss the active `/btw` panel before interrupting loop/maintenance work. ([#5213](https://github.com/can1357/oh-my-pi/issues/5213)) - Fixed visible per-keystroke lag while searching in the `/resume` session picker. Literal matches now rank synchronously from a cached per-session haystack, fuzzy scoring runs in bounded background chunks that converge to the same ranking (large listings previously rebuilt a fuzzy index per token per session on every keystroke), and the prompt-history SQLite lookup — an FTS query plus a LIKE scan over every stored prompt — is debounced off the keystroke path. - Fixed compiled Linux binary extension loading when bundled web-search header generation cannot read `header-generator` data files from the build-time path. ([#5178](https://github.com/can1357/oh-my-pi/issues/5178)) diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index f5aa2766a..3ccd61b87 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -266,6 +266,8 @@ export class InputController { }); } this.ctx.editor.onEscape = () => { + // Side-channel panels are the topmost view. Esc dismisses them before + // touching loop mode, maintenance, or the underlying main turn. // Active context maintenance owns Esc: auto/manual compaction, // handoff generation, and auto-retry backoff all advertise // "(esc to cancel)". Dispatch on live session state instead of @@ -281,6 +283,13 @@ export class InputController { // (see EventController). Main-session maintenance still owns Esc and // stays cancellable from the main view (focused submit gates /compact // and handoff, so manual maintenance is main-only anyway). + if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) { + return; + } + if (this.ctx.hasActiveOmfg() && this.ctx.handleOmfgEscape()) { + return; + } + if (!this.ctx.focusedAgentId) { const viewSession = this.ctx.viewSession; let aborted = false; @@ -308,12 +317,6 @@ export class InputController { } return; } - if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) { - return; - } - if (this.ctx.hasActiveOmfg() && this.ctx.handleOmfgEscape()) { - return; - } if (this.ctx.focusedAgentId) { // Esc never interrupts the focused agent's turn: clear typed text, // else return the view to the main session. Interrupt via empty diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 43e469f52..5e11ce750 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -14706,7 +14706,7 @@ export class AgentSession { // stable, but give provider routing a unique request lineage. sessionId: `${cacheSessionId}:side:${Snowflake.next()}`, promptCacheKey: cacheSessionId, - preferWebsockets: false, + preferWebsockets: this.#preferWebsockets, reasoning: toReasoningEffort(this.thinkingLevel), disableReasoning: shouldDisableReasoning(this.thinkingLevel), hideThinkingSummary: this.agent.hideThinkingSummary, diff --git a/packages/coding-agent/test/agent-session-message-pipeline.test.ts b/packages/coding-agent/test/agent-session-message-pipeline.test.ts index 5c47c74b4..cb9528eb2 100644 --- a/packages/coding-agent/test/agent-session-message-pipeline.test.ts +++ b/packages/coding-agent/test/agent-session-message-pipeline.test.ts @@ -233,7 +233,7 @@ describe("AgentSession message pipeline", () => { expect(requestOnPayload).toHaveBeenCalledWith({ original: true, session: true }, undefined); expect(result).toEqual({ original: true, session: true }); }); - it("keeps ephemeral side-channel cache key separate from provider routing", async () => { + it("keeps ephemeral side-channel cache key separate from provider routing while preserving websocket preference", async () => { const api = "test-ephemeral-side-channel"; let capturedOptions: SimpleStreamOptions | undefined; registerCustomApi(api, (_model, _context, options) => { @@ -271,6 +271,7 @@ describe("AgentSession message pipeline", () => { sessionManager: SessionManager.inMemory(), settings: Settings.isolated({ "compaction.enabled": false }), modelRegistry: createModelRegistryStub() as never, + preferWebsockets: true, }); sessions.push(session); const cacheSessionId = session.sessionId; @@ -281,7 +282,7 @@ describe("AgentSession message pipeline", () => { expect(capturedOptions?.promptCacheKey).toBe(cacheSessionId); expect(capturedOptions?.sessionId).toStartWith(`${cacheSessionId}:side:`); expect(capturedOptions?.sessionId).not.toBe(cacheSessionId); - expect(capturedOptions?.preferWebsockets).toBe(false); + expect(capturedOptions?.preferWebsockets).toBe(true); }); it("runs ephemeral side-channel requests through the configured side stream function", async () => { diff --git a/packages/coding-agent/test/input-controller-escape.test.ts b/packages/coding-agent/test/input-controller-escape.test.ts index d2828106d..c5fe89ce8 100644 --- a/packages/coding-agent/test/input-controller-escape.test.ts +++ b/packages/coding-agent/test/input-controller-escape.test.ts @@ -448,6 +448,35 @@ describe("InputController escape behavior", () => { expect(spies.abort).not.toHaveBeenCalled(); }); + it("dismisses an active /btw panel before aborting loop mode", () => { + const { ctx, editor, spies } = createContext(); + ctx.loopModeEnabled = true; + mutableSessionState(ctx).isStreaming = true; + spies.hasActiveBtw.mockReturnValue(true); + const controller = new InputController(ctx); + + controller.setupKeyHandlers(); + editor.onEscape?.(); + + expect(spies.handleBtwEscape).toHaveBeenCalledTimes(1); + expect(spies.abort).not.toHaveBeenCalled(); + expect(ctx.loopModeEnabled).toBe(true); + }); + + it("dismisses an active /btw panel before aborting maintenance", () => { + const { ctx, editor, spies } = createContext(); + abortViewSession(ctx).isGeneratingHandoff = true; + spies.hasActiveBtw.mockReturnValue(true); + const controller = new InputController(ctx); + + controller.setupKeyHandlers(); + editor.onEscape?.(); + + expect(spies.handleBtwEscape).toHaveBeenCalledTimes(1); + expect(spies.abortHandoff).not.toHaveBeenCalled(); + expect(spies.abort).not.toHaveBeenCalled(); + }); + it("aborts an active streaming turn on the first Esc without asking for confirmation", () => { const { ctx, editor, spies } = createContext(); mutableSessionState(ctx).isStreaming = true;