Files
HarborFork/harbor/notify.py
T
KeelrLeek dbfd1720c5 Harbor as of 2026-10-05: the coordination hub for the Open Sea's decomp team
Messages, threads, acknowledgements, claims, bands, tasks, review items and
stop flags for a team of AI agents; the chat page and board; host hooks and
MCP server; autopilot; tools/watch.py. Put under version control after the
weekend's changes so they have history and can be rolled back. Local only.
2026-10-05 10:44:23 -05:00

70 lines
3.5 KiB
Python

"""Windows notifications from the Harbor server, so the human hears about questions and urgent messages even with the
chat page closed. Off by default; switched on from the board (stored per project in the database)."""
import base64
import os
import subprocess
import sys
from xml.sax.saxutils import escape
APP_ID = r"{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe"
def toast_script(title, body):
xml = ('<toast><visual><binding template="ToastGeneric"><text>' + escape(str(title)[:80]) + "</text><text>"
+ escape(str(body)[:300]) + "</text></binding></visual></toast>")
xml = xml.replace("'", "''") # inside a PowerShell single-quoted string
return ("[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] "
"| Out-Null\n"
"[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null\n"
"$xml = New-Object Windows.Data.Xml.Dom.XmlDocument\n"
f"$xml.LoadXml('{xml}')\n"
"$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)\n"
f"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{APP_ID}').Show($toast)\n")
def toast(title, body):
"""Show a Windows notification; returns True when PowerShell accepted it."""
if sys.platform != "win32" or os.environ.get("HARBOR_NO_TOASTS"):
return False
encoded = base64.b64encode(toast_script(title, body).encode("utf-16-le")).decode("ascii")
try:
r = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded],
capture_output=True, timeout=20, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
return r.returncode == 0
except (OSError, subprocess.SubprocessError):
return False
def pending_for_human(h, limit=3):
"""Messages to the human that deserve a notification and were not notified yet (questions, urgent, direct)."""
row = h.conn.execute("SELECT value FROM meta WHERE key='toast_last'").fetchone()
last = int(row["value"]) if row else 0
rows = [h._message_dict(r) for r in h.conn.execute(
h.MESSAGE_SELECT + " JOIN deliveries d ON d.message_id=m.id AND d.agent=? WHERE d.read_at IS NULL AND m.id>? "
"ORDER BY m.id LIMIT 200", (h.human, last))]
worth = sorted((m for m in rows if m["recipients"] != ["All"] or m["importance"] == "urgent"
or m["kind"] == "question"), key=lambda m: m["id"])
top = max([m["id"] for m in rows], default=last)
return worth[-limit:], top
def enabled(h):
row = h.conn.execute("SELECT value FROM meta WHERE key='toasts'").fetchone()
return bool(row and row["value"] == "on")
def set_enabled(h, on):
top = h.conn.execute("SELECT COALESCE(MAX(id), 0) FROM messages").fetchone()[0]
def do(c):
c.execute("INSERT INTO meta(key, value) VALUES ('toasts', ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
("on" if on else "off",))
c.execute("INSERT INTO meta(key, value) VALUES ('toast_last', ?) ON CONFLICT(key) DO UPDATE SET "
"value=excluded.value", (str(top),)) # never notify the backlog
h._write(do)
def mark(h, top):
h._write(lambda c: c.execute("INSERT INTO meta(key, value) VALUES ('toast_last', ?) ON CONFLICT(key) DO UPDATE "
"SET value=excluded.value", (str(top),)))