-
Notifications
You must be signed in to change notification settings - Fork 189
Expand file tree
/
Copy pathnotify.ts
More file actions
88 lines (75 loc) · 2.63 KB
/
notify.ts
File metadata and controls
88 lines (75 loc) · 2.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* Desktop Notification Extension
*
* Sends a native desktop notification when the agent finishes and is waiting for input.
* Uses OSC 777 escape sequence - no external dependencies.
*
* Supported terminals: Ghostty, iTerm2, WezTerm, rxvt-unicode
* Not supported: Kitty (uses OSC 99), Terminal.app, Windows Terminal, Alacritty
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui";
/**
* Send a desktop notification via OSC 777 escape sequence.
*/
const notify = (title: string, body: string): void => {
// OSC 777 format: ESC ] 777 ; notify ; title ; body BEL
process.stdout.write(`\x1b]777;notify;${title};${body}\x07`);
};
const isTextPart = (part: unknown): part is { type: "text"; text: string } =>
Boolean(part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part);
const extractLastAssistantText = (messages: Array<{ role?: string; content?: unknown }>): string | null => {
for (let i = messages.length - 1; i >= 0; i--) {
const message = messages[i];
if (message?.role !== "assistant") {
continue;
}
const content = message.content;
if (typeof content === "string") {
return content.trim() || null;
}
if (Array.isArray(content)) {
const text = content.filter(isTextPart).map((part) => part.text).join("\n").trim();
return text || null;
}
return null;
}
return null;
};
const plainMarkdownTheme: MarkdownTheme = {
heading: (text) => text,
link: (text) => text,
linkUrl: () => "",
code: (text) => text,
codeBlock: (text) => text,
codeBlockBorder: () => "",
quote: (text) => text,
quoteBorder: () => "",
hr: () => "",
listBullet: () => "",
bold: (text) => text,
italic: (text) => text,
strikethrough: (text) => text,
underline: (text) => text,
};
const simpleMarkdown = (text: string, width = 80): string => {
const markdown = new Markdown(text, 0, 0, plainMarkdownTheme);
return markdown.render(width).join("\n");
};
const formatNotification = (text: string | null): { title: string; body: string } => {
const simplified = text ? simpleMarkdown(text) : "";
const normalized = simplified.replace(/\s+/g, " ").trim();
if (!normalized) {
return { title: "Ready for input", body: "" };
}
const maxBody = 200;
const body = normalized.length > maxBody ? `${normalized.slice(0, maxBody - 1)}…` : normalized;
return { title: "π", body };
};
export default function (pi: ExtensionAPI) {
pi.on("agent_end", async (event) => {
const lastText = extractLastAssistantText(event.messages ?? []);
const { title, body } = formatNotification(lastText);
notify(title, body);
});
}