fix(coding-agent): ring tmux for Warp attention events
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user