Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
883e08c
fix(frontend): AutoPilot notification follow-ups β€” branding, UX, pers…
kcze Mar 16, 2026
d287a0e
fix(frontend): address review β€” validate parsed sessions, simplify no…
kcze Mar 16, 2026
f5df187
fix(frontend): add Array.isArray guard to cross-tab storage handler
kcze Mar 16, 2026
344c0df
Merge remote-tracking branch 'origin/dev' into kpczerwinski/secrt-212…
kcze Mar 16, 2026
e3a58fd
fix(frontend): fix notification sound path to match actual file
kcze Mar 16, 2026
f3f27d0
fix(frontend): fix notification sound path to match actual file
kcze Mar 16, 2026
ce00d26
chore(frontend): remove notification.wav replaced by notification.mp3
kcze Mar 16, 2026
4854b45
fix(frontend): guard Copilot store init against SSR localStorage access
kcze Mar 17, 2026
761608a
fix(frontend): harden completed sessions persistence
kcze Mar 17, 2026
87a1706
Merge remote-tracking branch 'origin/dev' into kpczerwinski/secrt-212…
kcze Mar 17, 2026
e3b4459
Merge remote-tracking branch 'origin/dev' into kpczerwinski/secrt-212…
kcze Mar 18, 2026
f56d68e
Merge remote-tracking branch 'origin/dev' into kpczerwinski/secrt-212…
kcze Mar 20, 2026
02682d5
fix(frontend): refetch sidebar sessions on cross-tab notification sync
kcze Mar 20, 2026
afef4a6
Merge remote-tracking branch 'origin/dev' into kpczerwinski/secrt-212…
kcze Mar 26, 2026
c1407f3
refactor(frontend): extract shared notification title and session par…
kcze Mar 26, 2026
fcb7405
Merge remote-tracking branch 'origin/dev' into kpczerwinski/secrt-212…
kcze Apr 1, 2026
34e157f
fix(frontend): address PR review β€” BaseFooter cn(), SSR guard, tests
kcze Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ log-ingestion.txt
/logs
*.log
*.mp3
!autogpt_platform/frontend/public/notification.mp3
Comment thread
kcze marked this conversation as resolved.
mem.sqlite3
venvAutoGPT

Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export function ChatSidebar() {
clearCompletedSession(sessionId);
const remaining = completedSessionIDs.size - 1;
document.title =
remaining > 0 ? `(${remaining}) Otto is ready - AutoGPT` : "AutoGPT";
remaining > 0 ? `(${remaining}) AutoPilot is ready - AutoGPT` : "AutoGPT";
}, [sessionId, completedSessionIDs, clearCompletedSession]);

const sessions =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export function NotificationBanner() {
<div className="flex items-center gap-3 border-b border-amber-200 bg-amber-50 px-4 py-2.5">
<BellRinging className="h-5 w-5 shrink-0 text-amber-600" weight="fill" />
<Text variant="body" className="flex-1 text-sm text-amber-800">
Enable browser notifications to know when Otto finishes working, even
when you switch tabs.
Enable browser notifications to know when AutoPilot finishes working,
Comment thread
kcze marked this conversation as resolved.
even when you switch tabs.
</Text>
<Button variant="primary" size="small" onClick={handleEnable}>
Enable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,12 @@ export function NotificationDialog() {
<BellRinging className="h-6 w-6 text-violet-600" weight="fill" />
</div>
<Text variant="body" className="text-center text-neutral-600">
Otto can notify you when a response is ready, even if you switch
tabs or close this page. Enable notifications so you never miss one.
AutoPilot can notify you when a response is ready, even if you
switch tabs or close this page. Enable notifications so you never
miss one.
</Text>
</div>
<Dialog.Footer>
<Dialog.Footer className="justify-center">
Comment thread
kcze marked this conversation as resolved.
<Button variant="secondary" onClick={handleDismiss}>
Not now
</Button>
Expand Down
44 changes: 40 additions & 4 deletions autogpt_platform/frontend/src/app/(platform)/copilot/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@ export interface DeleteTarget {
title: string | null | undefined;
}

const isClient = typeof window !== "undefined";

function loadCompletedSessions(): Set<string> {
if (!isClient) return new Set();
const raw = storage.get(Key.COPILOT_COMPLETED_SESSIONS);
if (!raw) return new Set();
try {
const parsed: unknown = JSON.parse(raw);
return Array.isArray(parsed)
? new Set<string>(parsed.filter((v) => typeof v === "string"))
: new Set();
} catch {
return new Set();
}
}

function persistCompletedSessions(ids: Set<string>) {
if (!isClient) return;
try {
if (ids.size === 0) {
storage.clean(Key.COPILOT_COMPLETED_SESSIONS);
} else {
storage.set(Key.COPILOT_COMPLETED_SESSIONS, JSON.stringify([...ids]));
}
} catch {
// Keep in-memory state authoritative if persistence is unavailable
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

interface CopilotUIState {
/** Prompt extracted from URL hash (e.g. /copilot#prompt=...) for input prefill. */
initialPrompt: string | null;
Expand Down Expand Up @@ -44,23 +73,28 @@ export const useCopilotUIStore = create<CopilotUIState>((set) => ({
isDrawerOpen: false,
setDrawerOpen: (open) => set({ isDrawerOpen: open }),

completedSessionIDs: new Set<string>(),
completedSessionIDs: loadCompletedSessions(),
addCompletedSession: (id) =>
set((state) => {
const next = new Set(state.completedSessionIDs);
next.add(id);
persistCompletedSessions(next);
return { completedSessionIDs: next };
}),
clearCompletedSession: (id) =>
set((state) => {
const next = new Set(state.completedSessionIDs);
next.delete(id);
persistCompletedSessions(next);
return { completedSessionIDs: next };
}),
clearAllCompletedSessions: () =>
set({ completedSessionIDs: new Set<string>() }),
clearAllCompletedSessions: () => {
persistCompletedSessions(new Set());
set({ completedSessionIDs: new Set<string>() });
},

isNotificationsEnabled:
isClient &&
storage.get(Key.COPILOT_NOTIFICATIONS_ENABLED) === "true" &&
typeof Notification !== "undefined" &&
Notification.permission === "granted",
Expand All @@ -69,7 +103,8 @@ export const useCopilotUIStore = create<CopilotUIState>((set) => ({
set({ isNotificationsEnabled: enabled });
},

isSoundEnabled: storage.get(Key.COPILOT_SOUND_ENABLED) !== "false",
isSoundEnabled:
!isClient || storage.get(Key.COPILOT_SOUND_ENABLED) !== "false",
toggleSound: () =>
set((state) => {
const next = !state.isSoundEnabled;
Expand All @@ -85,6 +120,7 @@ export const useCopilotUIStore = create<CopilotUIState>((set) => ({
storage.clean(Key.COPILOT_SOUND_ENABLED);
storage.clean(Key.COPILOT_NOTIFICATION_BANNER_DISMISSED);
storage.clean(Key.COPILOT_NOTIFICATION_DIALOG_DISMISSED);
storage.clean(Key.COPILOT_COMPLETED_SESSIONS);
set({
completedSessionIDs: new Set<string>(),
isNotificationsEnabled: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,62 @@
import { getGetV2ListSessionsQueryKey } from "@/app/api/__generated__/endpoints/chat/chat";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import type { WebSocketNotification } from "@/lib/autogpt-server-api/types";
import { Key } from "@/services/storage/local-storage";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef } from "react";
import { useCopilotUIStore } from "./store";

const ORIGINAL_TITLE = "AutoGPT";
const NOTIFICATION_SOUND_PATH = "/sounds/notification.mp3";
const NOTIFICATION_SOUND_PATH = "/notification.mp3";

/**
* Show a browser notification with click-to-navigate behaviour.
* Wrapped in try-catch so it degrades gracefully in service-worker or
* other restricted contexts where the Notification constructor throws.
*/
function showBrowserNotification(
title: string,
opts: { body: string; icon: string; sessionID: string },
) {
try {
const n = new Notification(title, { body: opts.body, icon: opts.icon });
n.onclick = () => {
window.focus();
const url = new URL(window.location.href);
url.searchParams.set("sessionId", opts.sessionID);
window.history.pushState({}, "", url.toString());
window.dispatchEvent(new PopStateEvent("popstate"));
n.close();
};
} catch {
// Notification constructor is unavailable (e.g. service-worker context).
// The user will still see the in-app badge and title update.
}
}

/**
* Listens for copilot completion notifications via WebSocket.
* Updates the Zustand store, plays a sound, and updates document.title.
*/
export function useCopilotNotifications(activeSessionID: string | null) {
const api = useBackendAPI();
const queryClient = useQueryClient();
const audioRef = useRef<HTMLAudioElement | null>(null);
const activeSessionRef = useRef(activeSessionID);
activeSessionRef.current = activeSessionID;
const windowFocusedRef = useRef(true);

// Pre-load audio element
// Pre-load audio element and sync document title with persisted state
useEffect(() => {
if (typeof window === "undefined") return;
const audio = new Audio(NOTIFICATION_SOUND_PATH);
audio.volume = 0.5;
audioRef.current = audio;

const count = useCopilotUIStore.getState().completedSessionIDs.size;
if (count > 0) {
document.title = `(${count}) AutoPilot is ready - ${ORIGINAL_TITLE}`;
}
}, []);

// Listen for WebSocket notifications
Expand All @@ -49,7 +83,7 @@ export function useCopilotNotifications(activeSessionID: string | null) {
// Always update UI state (checkmark + title) regardless of notification setting
state.addCompletedSession(sessionID);
const count = useCopilotUIStore.getState().completedSessionIDs.size;
document.title = `(${count}) Otto is ready - ${ORIGINAL_TITLE}`;
document.title = `(${count}) AutoPilot is ready - ${ORIGINAL_TITLE}`;
Comment thread
kcze marked this conversation as resolved.
Outdated

// Sound and browser notifications are gated by the user setting
if (!state.isNotificationsEnabled) return;
Expand All @@ -65,18 +99,11 @@ export function useCopilotNotifications(activeSessionID: string | null) {
Notification.permission === "granted" &&
isUserAway
) {
const n = new Notification("Otto is ready", {
showBrowserNotification("AutoPilot is ready", {
body: "A response is waiting for you.",
icon: "/favicon.ico",
sessionID,
});
n.onclick = () => {
window.focus();
const url = new URL(window.location.href);
url.searchParams.set("sessionId", sessionID);
window.history.pushState({}, "", url.toString());
window.dispatchEvent(new PopStateEvent("popstate"));
n.close();
};
}
}

Expand Down Expand Up @@ -115,4 +142,40 @@ export function useCopilotNotifications(activeSessionID: string | null) {
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, []);

// Sync completedSessionIDs across tabs via localStorage storage events
useEffect(() => {
function handleStorage(e: StorageEvent) {
if (e.key !== Key.COPILOT_COMPLETED_SESSIONS) return;
let next: Set<string>;
try {
if (!e.newValue) {
next = new Set<string>();
} else {
const parsed: unknown = JSON.parse(e.newValue);
next = Array.isArray(parsed)
? new Set<string>(parsed.filter((v) => typeof v === "string"))
: new Set<string>();
}
} catch {
next = new Set<string>();
Comment thread
kcze marked this conversation as resolved.
Outdated
}
// localStorage is the shared source of truth β€” adopt it directly so both
// additions (new completions) and removals (cleared sessions) propagate.
useCopilotUIStore.setState({ completedSessionIDs: next });
const count = next.size;
document.title =
count > 0
? `(${count}) AutoPilot is ready - ${ORIGINAL_TITLE}`
: ORIGINAL_TITLE;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Refetch the session list so the sidebar reflects the latest
// is_processing state (avoids stale spinner after cross-tab clear).
queryClient.invalidateQueries({
queryKey: getGetV2ListSessionsQueryKey(),
});
}
window.addEventListener("storage", handleStorage);
return () => window.removeEventListener("storage", handleStorage);
}, [queryClient]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/__legacy__/ui/popover";
import { Bell } from "@phosphor-icons/react";
import { Pulse } from "@phosphor-icons/react";
import { ActivityDropdown } from "./components/ActivityDropdown/ActivityDropdown";
import { formatNotificationCount } from "./helpers";
import { useAgentActivityDropdown } from "./useAgentActivityDropdown";
Expand All @@ -30,7 +30,7 @@ export function AgentActivityDropdown() {
data-testid="agent-activity-button"
aria-label="View Agent Activity"
>
<Bell size={22} className="text-black" />
<Pulse size={22} className="text-black" />

{activeCount > 0 && (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export enum Key {
COPILOT_NOTIFICATIONS_ENABLED = "copilot-notifications-enabled",
COPILOT_NOTIFICATION_BANNER_DISMISSED = "copilot-notification-banner-dismissed",
COPILOT_NOTIFICATION_DIALOG_DISMISSED = "copilot-notification-dialog-dismissed",
COPILOT_COMPLETED_SESSIONS = "copilot-completed-sessions",
}

function get(key: Key) {
Expand Down
Loading