Skip to content

Commit 1f116ad

Browse files
committed
🐛 fix(notification): use openSync/writeSync/closeSync for reliable tty writes
Replace writeFileSync with explicit openSync/writeSync/closeSync for /dev/tty to work reliably in Bun's plugin execution context. Remove escapeOSCString which could corrupt JSON payloads containing semicolons. Retain payload length truncation (4096 bytes) and 3-retry mechanism with error logging.
1 parent ff1b1f9 commit 1f116ad

2 files changed

Lines changed: 22 additions & 21 deletions

File tree

src/notify.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,22 @@
1-
import { writeFileSync } from "fs"
1+
import { openSync, writeSync, closeSync } from "fs"
22

33
const MAX_OSC_LENGTH = 4096
44

5-
function escapeOSCString(str: string): string {
6-
return str
7-
.replace(/\x1b/g, "\x1b\x5d")
8-
.replace(/\x07/g, "\x1b\x5c\x07")
9-
.replace(/;/g, "\\;")
10-
}
11-
125
function warpNotify(title: string, body: string): void {
136
if (!process.env.WARP_CLI_AGENT_PROTOCOL_VERSION) return
147

158
const maxBodyLength = MAX_OSC_LENGTH - title.length - 20
16-
const escapedBody = escapeOSCString(body)
179
const truncatedBody =
18-
escapedBody.length > maxBodyLength
19-
? escapedBody.slice(0, maxBodyLength - 3) + "..."
20-
: escapedBody
10+
body.length > maxBodyLength ? body.slice(0, maxBodyLength - 3) + "..." : body
2111

2212
const sequence = `\x1b]777;notify;${title};${truncatedBody}\x07`
2313

2414
let lastError: Error | null = null
2515
for (let attempt = 0; attempt < 3; attempt++) {
2616
try {
27-
writeFileSync("/dev/tty", sequence)
17+
const fd = openSync("/dev/tty", "w")
18+
writeSync(fd, sequence)
19+
closeSync(fd)
2820
return
2921
} catch (err) {
3022
lastError = err as Error

tests/notify.test.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@ import { describe, it, afterEach, mock } from "bun:test"
22
import { expect } from "bun:test"
33
import fs from "fs"
44

5-
const writeSpy = mock(() => {})
5+
const writeSyncSpy = mock(() => {})
6+
const openSyncSpy = mock(() => 42)
7+
const closeSyncSpy = mock(() => {})
8+
69
mock.module("fs", () => ({
710
...fs,
8-
writeFileSync: writeSpy,
11+
openSync: openSyncSpy,
12+
writeSync: writeSyncSpy,
13+
closeSync: closeSyncSpy,
914
}))
1015

1116
const { warpNotify } = await import("../src/notify")
@@ -14,7 +19,9 @@ describe("warpNotify", () => {
1419
const originalVersion = process.env.WARP_CLI_AGENT_PROTOCOL_VERSION
1520

1621
afterEach(() => {
17-
writeSpy.mockClear()
22+
openSyncSpy.mockClear()
23+
writeSyncSpy.mockClear()
24+
closeSyncSpy.mockClear()
1825
if (originalVersion === undefined) {
1926
delete process.env.WARP_CLI_AGENT_PROTOCOL_VERSION
2027
} else {
@@ -25,19 +32,21 @@ describe("warpNotify", () => {
2532
it("skips when WARP_CLI_AGENT_PROTOCOL_VERSION is not set", () => {
2633
delete process.env.WARP_CLI_AGENT_PROTOCOL_VERSION
2734
warpNotify("title", "body")
28-
expect(writeSpy).not.toHaveBeenCalled()
35+
expect(openSyncSpy).not.toHaveBeenCalled()
2936
})
3037

3138
it("writes OSC 777 sequence when Warp declares protocol support", () => {
3239
process.env.WARP_CLI_AGENT_PROTOCOL_VERSION = "1"
3340
warpNotify("warp://cli-agent", '{"event":"stop"}')
34-
expect(writeSpy).toHaveBeenCalledTimes(1)
41+
expect(openSyncSpy).toHaveBeenCalledTimes(1)
42+
expect(openSyncSpy).toHaveBeenCalledWith("/dev/tty", "w")
43+
expect(writeSyncSpy).toHaveBeenCalledTimes(1)
44+
expect(closeSyncSpy).toHaveBeenCalledTimes(1)
3545

36-
const [path, data] = writeSpy.mock.calls[0] as [string, string]
37-
expect(path).toBe("/dev/tty")
46+
const [, data] = writeSyncSpy.mock.calls[0] as [number, string]
3847
expect(data).toContain("warp://cli-agent")
3948
expect(data).toContain('{"event":"stop"}')
4049
expect(data).toMatch(/^\x1b\]777;notify;/)
4150
expect(data).toMatch(/\x07$/)
4251
})
43-
})
52+
})

0 commit comments

Comments
 (0)