From dfbf726eea703f241f66b3f173f7eec5a58e2d8c Mon Sep 17 00:00:00 2001 From: robomp-bot Date: Thu, 16 Jul 2026 20:59:44 +0900 Subject: [PATCH] fix(coding-agent): ring tmux for Warp attention events --- .../src/modes/warp-events.test.ts | 69 ++++++++++++++++++- .../coding-agent/src/modes/warp-events.ts | 19 ++++- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/modes/warp-events.test.ts b/packages/coding-agent/src/modes/warp-events.test.ts index e3e759ec8..23e4923f6 100644 --- a/packages/coding-agent/src/modes/warp-events.test.ts +++ b/packages/coding-agent/src/modes/warp-events.test.ts @@ -121,14 +121,79 @@ describe("Warp CLI-agent events", () => { enableWarpProtocol(); const write = vi.spyOn(process.stdout, "write").mockReturnValue(true); const tmux = vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(true); - const wrap = vi.spyOn(terminalCapabilities, "wrapTmuxPassthrough").mockImplementation(osc => `wrapped:${osc}`); + const wrap = vi.spyOn(terminalCapabilities, "wrapTmuxPassthrough"); const emitter = createWarpEventEmitter({ sessionId: "session-123" }); emitter?.emit({ event: "stop" }); expect(tmux).toHaveBeenCalledTimes(1); expect(wrap).toHaveBeenCalledWith(expect.stringContaining("warp://cli-agent")); - expect(write).toHaveBeenCalledWith(expect.stringContaining("wrapped:\x1b]777;notify;warp://cli-agent;")); + const written = write.mock.calls[0]?.[0] as string; + // Real DCS wrap ends with ST; attention events append outer BEL after it. + expect(written.startsWith("\x1bPtmux;")).toBe(true); + expect(written.endsWith("\x1b\\\x07")).toBe(true); + }); + + const attentionEvents = ["stop", "stop_failure", "permission_request", "question_asked"] as const; + const nonAttentionEvents = [ + "session_start", + "prompt_submit", + "tool_complete", + "permission_replied", + "custom_event", + ] as const; + + for (const eventName of attentionEvents) { + it(`rings tmux outer BEL for attention event ${eventName}`, () => { + enableWarpProtocol(); + const write = vi.spyOn(process.stdout, "write").mockReturnValue(true); + vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(true); + const emitter = createWarpEventEmitter({ sessionId: "session-123" }); + + emitter?.emit({ event: eventName }); + + const written = write.mock.calls[0]?.[0] as string; + // Outer BEL after DCS ST; OSC's own \x07 is interior to the passthrough. + expect(written.startsWith("\x1bPtmux;")).toBe(true); + expect(written.endsWith("\x07\x1b\\\x07")).toBe(true); + expect(written.slice(0, -1).endsWith("\x07\x1b\\")).toBe(true); + }); + } + + for (const eventName of nonAttentionEvents) { + it(`does not ring tmux outer BEL for non-attention event ${eventName}`, () => { + enableWarpProtocol(); + const write = vi.spyOn(process.stdout, "write").mockReturnValue(true); + vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(true); + const emitter = createWarpEventEmitter({ sessionId: "session-123" }); + + emitter?.emit({ event: eventName }); + + const written = write.mock.calls[0]?.[0] as string; + // DCS ST only — OSC terminator is inside the wrap, not an outer BEL. + expect(written.startsWith("\x1bPtmux;")).toBe(true); + expect(written.endsWith("\x07\x1b\\")).toBe(true); + expect(written.endsWith("\x07\x1b\\\x07")).toBe(false); + }); + } + + it("leaves direct-terminal OSC unchanged without outer BEL after OSC terminator", () => { + enableWarpProtocol(); + const write = vi.spyOn(process.stdout, "write").mockReturnValue(true); + vi.spyOn(terminalCapabilities, "isInsideTmux").mockReturnValue(false); + const wrap = vi.spyOn(terminalCapabilities, "wrapTmuxPassthrough"); + const emitter = createWarpEventEmitter({ sessionId: "session-123" }); + + for (const eventName of [...attentionEvents, ...nonAttentionEvents]) { + write.mockClear(); + emitter?.emit({ event: eventName }); + const written = write.mock.calls[0]?.[0] as string; + expect(written.startsWith(OSC_PREFIX)).toBe(true); + expect(written.endsWith("\x07")).toBe(true); + // Exactly one trailing BEL (OSC terminator), not an extra attention BEL. + expect(written.endsWith("\x07\x07")).toBe(false); + expect(wrap).not.toHaveBeenCalled(); + } }); it("creates an emitter from protocol version alone even when terminal id is base", () => { diff --git a/packages/coding-agent/src/modes/warp-events.ts b/packages/coding-agent/src/modes/warp-events.ts index 2300ece34..1a992df27 100644 --- a/packages/coding-agent/src/modes/warp-events.ts +++ b/packages/coding-agent/src/modes/warp-events.ts @@ -7,6 +7,12 @@ import { isSilentAbort, isUserInterruptAbort, SKILL_PROMPT_MESSAGE_TYPE } from " const WARP_CLI_AGENT_PROTOCOL_VERSION = 1; const WARP_CLI_AGENT_SENTINEL = "warp://cli-agent"; +const WARP_ATTENTION_EVENTS: Record = { + stop: true, + stop_failure: true, + permission_request: true, + question_asked: true, +}; /** True when Warp has negotiated the structured CLI-agent OSC protocol. */ export function isWarpCliAgentProtocolActive(): boolean { @@ -58,7 +64,18 @@ export function createWarpEventEmitter(options: WarpEventEmitterOptions): WarpEv plugin_version: VERSION, }; const osc = `\x1b]777;notify;${WARP_CLI_AGENT_SENTINEL};${JSON.stringify(body)}\x07`; - process.stdout.write(isInsideTmux() ? wrapTmuxPassthrough(osc) : osc); + if (!isInsideTmux()) { + process.stdout.write(osc); + return; + } + // DCS-wrap every OSC so Warp can parse it under allow-passthrough. + // Outer BEL after DCS is only for attention-worthy events so tmux + // monitor-bell flags the pane; the OSC's own trailing \x07 is its + // terminator and does not drive the outer bell after wrapping. + const wrapped = wrapTmuxPassthrough(osc); + const eventName = event.event; + const ring = typeof eventName === "string" && Object.hasOwn(WARP_ATTENTION_EVENTS, eventName); + process.stdout.write(ring ? `${wrapped}\x07` : wrapped); }, }; }