fix(coding-agent): ring tmux for Warp attention events

This commit is contained in:
robomp-bot
2026-07-16 20:59:44 +09:00
parent 06f1f092a0
commit dfbf726eea
2 changed files with 85 additions and 3 deletions
@@ -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", () => {
+18 -1
View File
@@ -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<string, true> = {
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);
},
};
}