Add browser notifications for agent messages with EndOfTurn=true
When a message with EndOfTurn=true is received, show a system notification using the browser Notifications API. Include the sketch title and beginning of the message content in the notification. Added a toggle for users to enable/disable notifications.
Co-Authored-By: sketch <hello@sketch.dev>
diff --git a/webui/src/web-components/sketch-app-shell.ts b/webui/src/web-components/sketch-app-shell.ts
index 517ec5f..3ee27f6 100644
--- a/webui/src/web-components/sketch-app-shell.ts
+++ b/webui/src/web-components/sketch-app-shell.ts
@@ -244,10 +244,12 @@
background-color: #c82333 !important;
}
- .poll-updates {
+ .poll-updates,
+ .notifications-toggle {
display: flex;
align-items: center;
font-size: 12px;
+ margin-right: 10px;
}
`;
@@ -259,6 +261,10 @@
@state()
lastCommitCopied: boolean = false;
+ // Track notification preferences
+ @state()
+ notificationsEnabled: boolean = true;
+
@property()
connectionErrorMessage: string = "";
@@ -311,6 +317,18 @@
this._handleShowCommitDiff = this._handleShowCommitDiff.bind(this);
this._handlePopState = this._handlePopState.bind(this);
this._handleStopClick = this._handleStopClick.bind(this);
+ this._handleNotificationsToggle =
+ this._handleNotificationsToggle.bind(this);
+
+ // Load notification preference from localStorage
+ try {
+ const savedPref = localStorage.getItem("sketch-notifications-enabled");
+ if (savedPref !== null) {
+ this.notificationsEnabled = savedPref === "true";
+ }
+ } catch (error) {
+ console.error("Error loading notification preference:", error);
+ }
}
// See https://lit.dev/docs/components/lifecycle/
@@ -548,6 +566,83 @@
document.title = docTitle;
}
+ // Check and request notification permission if needed
+ private async checkNotificationPermission(): Promise<boolean> {
+ // Check if the Notification API is supported
+ if (!("Notification" in window)) {
+ console.log("This browser does not support notifications");
+ return false;
+ }
+
+ // Check if permission is already granted
+ if (Notification.permission === "granted") {
+ return true;
+ }
+
+ // If permission is not denied, request it
+ if (Notification.permission !== "denied") {
+ const permission = await Notification.requestPermission();
+ return permission === "granted";
+ }
+
+ return false;
+ }
+
+ // Handle notifications toggle change
+ private _handleNotificationsToggle(event: Event): void {
+ const toggleCheckbox = event.target as HTMLInputElement;
+ this.notificationsEnabled = toggleCheckbox.checked;
+
+ // If enabling notifications, check permissions
+ if (this.notificationsEnabled) {
+ this.checkNotificationPermission();
+ }
+
+ // Save preference to localStorage
+ try {
+ localStorage.setItem(
+ "sketch-notifications-enabled",
+ String(this.notificationsEnabled),
+ );
+ } catch (error) {
+ console.error("Error saving notification preference:", error);
+ }
+ }
+
+ // Show notification for message with EndOfTurn=true
+ private async showEndOfTurnNotification(
+ message: AgentMessage,
+ ): Promise<void> {
+ // Don't show notifications if they're disabled
+ if (!this.notificationsEnabled) return;
+
+ // Check if we have permission to show notifications
+ const hasPermission = await this.checkNotificationPermission();
+ if (!hasPermission) return;
+
+ // Only show notifications for agent messages with end_of_turn=true
+ if (message.type !== "agent" || !message.end_of_turn) return;
+
+ // Create a title that includes the sketch title
+ const notificationTitle = `Sketch: ${this.title || "untitled"}`;
+
+ // Extract the beginning of the message content (first 100 chars)
+ const messagePreview = message.content
+ ? message.content.substring(0, 100) +
+ (message.content.length > 100 ? "..." : "")
+ : "Agent has completed its turn";
+
+ // Create and show the notification
+ try {
+ new Notification(notificationTitle, {
+ body: messagePreview,
+ icon: "/static/favicon.ico", // Use sketch favicon if available
+ });
+ } catch (error) {
+ console.error("Error showing notification:", error);
+ }
+ }
+
private handleDataChanged(eventData: {
state: State;
newMessages: AgentMessage[];
@@ -588,6 +683,16 @@
// Process new messages to find commit messages
this.updateLastCommitInfo(newMessages);
+
+ // Check for agent messages with end_of_turn=true and show notifications
+ if (newMessages && newMessages.length > 0 && !isFirstFetch) {
+ for (const message of newMessages) {
+ if (message.type === "agent" && message.end_of_turn) {
+ this.showEndOfTurnNotification(message);
+ break; // Only show one notification per batch of messages
+ }
+ }
+ }
}
private handleConnectionStatusChanged(
@@ -774,6 +879,16 @@
<label for="pollToggle">Poll</label>
</div>
+ <div class="notifications-toggle">
+ <input
+ type="checkbox"
+ id="notificationsToggle"
+ ?checked=${this.notificationsEnabled}
+ @change=${this._handleNotificationsToggle}
+ />
+ <label for="notificationsToggle">Notifications</label>
+ </div>
+
<sketch-network-status
message=${this.messageStatus}
connection=${this.connectionStatus}