@brainpilot/web 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-DWOsU22G.css +1 -0
- package/dist/assets/index-j3rGyO6m.js +445 -0
- package/dist/index.html +2 -2
- package/package.json +6 -3
- package/src/__tests__/agentsReducer.test.ts +67 -0
- package/src/__tests__/api.test.ts +118 -0
- package/src/__tests__/chatScrollBehavior.test.ts +48 -0
- package/src/__tests__/chatScrollMemory.test.ts +49 -0
- package/src/__tests__/demoConversation.test.ts +96 -0
- package/src/__tests__/demoReset.test.ts +24 -0
- package/src/__tests__/internalToolStrip.test.ts +108 -0
- package/src/__tests__/runningToast.test.ts +29 -0
- package/src/__tests__/tokenUsage.test.ts +48 -0
- package/src/__tests__/toolDisplay.test.ts +55 -0
- package/src/__tests__/traceReducer.test.ts +62 -0
- package/src/components/chat/MessageStream.tsx +104 -56
- package/src/components/chat/PromptComposer.tsx +120 -29
- package/src/components/chat/chatScrollMemory.ts +49 -0
- package/src/components/demo/DemoView.tsx +98 -29
- package/src/components/demo/TraceNodeModal.tsx +6 -2
- package/src/components/demo/demoBundle.ts +7 -2
- package/src/components/demo/demoReset.ts +16 -0
- package/src/components/session/AgentNetwork.tsx +68 -75
- package/src/components/session/AgentTraceViews.tsx +35 -70
- package/src/components/session/AnalyticsTab.tsx +58 -224
- package/src/components/session/TraceGraphView.tsx +36 -30
- package/src/components/session/TraceNodeDetail.tsx +61 -24
- package/src/components/session/agentNetworkShared.ts +10 -0
- package/src/components/session/traceLayout.ts +32 -0
- package/src/components/settings/SettingsDialog.tsx +19 -1
- package/src/components/shell/DesktopShell.tsx +72 -17
- package/src/components/sidebar/SessionList.tsx +127 -0
- package/src/components/sidebar/Sidebar.tsx +94 -98
- package/src/contexts/SSEContext.tsx +90 -1
- package/src/contexts/SessionContext.tsx +397 -43
- package/src/contexts/agentsReducer.ts +49 -0
- package/src/contexts/messageGroups.ts +56 -0
- package/src/contexts/messageReducer.ts +4 -0
- package/src/contexts/runningToast.ts +33 -0
- package/src/contexts/traceReducer.ts +62 -0
- package/src/contexts/turnTimer.test.ts +97 -0
- package/src/contexts/turnTimer.ts +108 -0
- package/src/contexts/useTurnTimer.ts +104 -0
- package/src/contracts/backend.ts +53 -2
- package/src/i18n/messages/analytics.ts +16 -6
- package/src/i18n/messages/chat.ts +26 -4
- package/src/i18n/messages/contexts.ts +2 -0
- package/src/i18n/messages/network.ts +13 -9
- package/src/i18n/messages/profile.ts +4 -0
- package/src/i18n/messages/settings.ts +4 -0
- package/src/i18n/messages/shell.ts +2 -0
- package/src/i18n/messages/trace.ts +69 -17
- package/src/mocks/backend.ts +7 -0
- package/src/styles/global.css +289 -70
- package/src/utils/api.ts +105 -8
- package/src/utils/toolDisplay.ts +74 -0
- package/dist/assets/index-C-8G4D4j.js +0 -448
- package/dist/assets/index-C501m5OS.css +0 -1
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
Check,
|
|
4
|
-
MessageCircle,
|
|
5
|
-
MessageSquarePlus,
|
|
2
|
+
MessagesSquare,
|
|
6
3
|
MonitorPlay,
|
|
7
4
|
PanelLeft,
|
|
8
5
|
PenLine,
|
|
9
|
-
Plug,
|
|
10
|
-
Search,
|
|
11
6
|
Settings,
|
|
12
|
-
Trash2,
|
|
13
|
-
X,
|
|
14
7
|
} from "lucide-react";
|
|
15
|
-
import {
|
|
8
|
+
import { useEffect, useRef, useState } from "react";
|
|
16
9
|
import { useSessions } from "../../contexts/SessionContext";
|
|
17
10
|
import { useT } from "../../i18n/useT";
|
|
18
11
|
import { IconButton } from "../primitives/IconButton";
|
|
12
|
+
import { SessionList } from "./SessionList";
|
|
19
13
|
|
|
20
14
|
type SidebarProps = {
|
|
21
15
|
isCollapsed: boolean;
|
|
@@ -39,20 +33,46 @@ export function Sidebar({ isCollapsed, activePage, onOpenDemo, onGoWorkspace, on
|
|
|
39
33
|
deleteSession,
|
|
40
34
|
} = useSessions();
|
|
41
35
|
const t = useT();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
// #131 — when collapsed to the icon rail, the session list moves into a
|
|
37
|
+
// floating popover opened from a single icon, so it no longer competes for
|
|
38
|
+
// horizontal space yet stays one click away.
|
|
39
|
+
const [isSessionsPopoverOpen, setIsSessionsPopoverOpen] = useState(false);
|
|
40
|
+
const sessionsPopoverRef = useRef<HTMLDivElement | null>(null);
|
|
45
41
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
setEditingId(null);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
await updateSessionTitle(editingId, editingTitle.trim());
|
|
53
|
-
setEditingId(null);
|
|
42
|
+
const newConversation = () => {
|
|
43
|
+
onGoWorkspace();
|
|
44
|
+
startDraftSession();
|
|
54
45
|
};
|
|
55
46
|
|
|
47
|
+
const selectAndGo = (sessionId: string) => {
|
|
48
|
+
onGoWorkspace();
|
|
49
|
+
selectSession(sessionId);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Collapsing the rail (manually or at narrow widths) closes a stale popover.
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!isCollapsed) setIsSessionsPopoverOpen(false);
|
|
55
|
+
}, [isCollapsed]);
|
|
56
|
+
|
|
57
|
+
// Dismiss the popover on outside click / Escape, like a standard menu.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!isSessionsPopoverOpen) return;
|
|
60
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
61
|
+
if (!sessionsPopoverRef.current?.contains(event.target as Node)) {
|
|
62
|
+
setIsSessionsPopoverOpen(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
66
|
+
if (event.key === "Escape") setIsSessionsPopoverOpen(false);
|
|
67
|
+
};
|
|
68
|
+
window.addEventListener("pointerdown", onPointerDown);
|
|
69
|
+
window.addEventListener("keydown", onKeyDown);
|
|
70
|
+
return () => {
|
|
71
|
+
window.removeEventListener("pointerdown", onPointerDown);
|
|
72
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
73
|
+
};
|
|
74
|
+
}, [isSessionsPopoverOpen]);
|
|
75
|
+
|
|
56
76
|
return (
|
|
57
77
|
<aside className="sidebar" aria-label={t("sidebar.aria.nav")}>
|
|
58
78
|
<div
|
|
@@ -76,21 +96,54 @@ export function Sidebar({ isCollapsed, activePage, onOpenDemo, onGoWorkspace, on
|
|
|
76
96
|
</div>
|
|
77
97
|
|
|
78
98
|
<nav className="sidebar__nav" aria-label={t("sidebar.aria.primary")}>
|
|
79
|
-
<button className="nav-item nav-item--strong" onClick={
|
|
99
|
+
<button className="nav-item nav-item--strong" onClick={newConversation} type="button" title={t("sidebar.newChat")}>
|
|
80
100
|
<PenLine size={16} />
|
|
81
101
|
<span>{t("sidebar.newChat")}</span>
|
|
82
102
|
</button>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
<
|
|
90
|
-
|
|
103
|
+
{/*
|
|
104
|
+
#131 — collapsed icon rail: a single Sessions icon opens the session
|
|
105
|
+
list in a popover (the inline list below is hidden when collapsed).
|
|
106
|
+
Rendered only in the rail so the expanded sidebar keeps its full list.
|
|
107
|
+
*/}
|
|
108
|
+
{isCollapsed ? (
|
|
109
|
+
<div className="sidebar__sessions-popover-anchor" ref={sessionsPopoverRef}>
|
|
110
|
+
<button
|
|
111
|
+
aria-expanded={isSessionsPopoverOpen}
|
|
112
|
+
aria-haspopup="menu"
|
|
113
|
+
className={`nav-item ${isSessionsPopoverOpen ? "is-active" : ""}`}
|
|
114
|
+
onClick={() => setIsSessionsPopoverOpen((open) => !open)}
|
|
115
|
+
title={t("sidebar.conversations")}
|
|
116
|
+
type="button"
|
|
117
|
+
>
|
|
118
|
+
<MessagesSquare size={16} />
|
|
119
|
+
<span>{t("sidebar.conversations")}</span>
|
|
120
|
+
</button>
|
|
121
|
+
{isSessionsPopoverOpen ? (
|
|
122
|
+
<div className="sidebar__sessions-popover" role="menu" aria-label={t("sidebar.conversations")}>
|
|
123
|
+
<div className="sidebar__sessions-popover-head">
|
|
124
|
+
<h2>{t("sidebar.conversations")}</h2>
|
|
125
|
+
<button className="nav-item nav-item--strong" onClick={() => { newConversation(); setIsSessionsPopoverOpen(false); }} type="button">
|
|
126
|
+
<PenLine size={14} />
|
|
127
|
+
<span>{t("sidebar.newChat")}</span>
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
<SessionList
|
|
131
|
+
sessions={sessions}
|
|
132
|
+
currentId={currentSession?.id}
|
|
133
|
+
isLoading={isLoading}
|
|
134
|
+
onSelect={(id) => { selectAndGo(id); setIsSessionsPopoverOpen(false); }}
|
|
135
|
+
onRename={updateSessionTitle}
|
|
136
|
+
onDelete={deleteSession}
|
|
137
|
+
onOpenSearch={() => { onOpenSearch(); setIsSessionsPopoverOpen(false); }}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
) : null}
|
|
141
|
+
</div>
|
|
142
|
+
) : null}
|
|
91
143
|
<button
|
|
92
144
|
className={`nav-item ${activePage === "demo" ? "is-active" : ""}`}
|
|
93
145
|
onClick={onOpenDemo}
|
|
146
|
+
title={t("sidebar.demo")}
|
|
94
147
|
type="button"
|
|
95
148
|
>
|
|
96
149
|
<MonitorPlay size={16} />
|
|
@@ -102,82 +155,25 @@ export function Sidebar({ isCollapsed, activePage, onOpenDemo, onGoWorkspace, on
|
|
|
102
155
|
<div className="section-heading">
|
|
103
156
|
<h2 id="conversations-heading">{t("sidebar.conversations")}</h2>
|
|
104
157
|
<div className="section-heading__actions">
|
|
105
|
-
<IconButton label={t("sidebar.aria.newConversation")} onClick={
|
|
106
|
-
<
|
|
158
|
+
<IconButton label={t("sidebar.aria.newConversation")} onClick={newConversation}>
|
|
159
|
+
<PenLine size={13} />
|
|
107
160
|
</IconButton>
|
|
108
161
|
</div>
|
|
109
162
|
</div>
|
|
110
163
|
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
const isConfirming = confirmDeleteId === session.id;
|
|
121
|
-
return (
|
|
122
|
-
<div className={`conversation-item ${currentSession?.id === session.id ? "is-active" : ""}`} key={session.id}>
|
|
123
|
-
{isEditing ? (
|
|
124
|
-
<form className="conversation-edit" onSubmit={submitRename}>
|
|
125
|
-
<input
|
|
126
|
-
autoFocus
|
|
127
|
-
onChange={(event) => setEditingTitle(event.target.value)}
|
|
128
|
-
value={editingTitle}
|
|
129
|
-
/>
|
|
130
|
-
<IconButton label={t("sidebar.aria.saveTitle")} type="submit">
|
|
131
|
-
<Check size={14} />
|
|
132
|
-
</IconButton>
|
|
133
|
-
<IconButton label={t("sidebar.aria.cancelRename")} onClick={() => setEditingId(null)}>
|
|
134
|
-
<X size={14} />
|
|
135
|
-
</IconButton>
|
|
136
|
-
</form>
|
|
137
|
-
) : (
|
|
138
|
-
<>
|
|
139
|
-
<button className="conversation-row" onClick={() => { onGoWorkspace(); selectSession(session.id); }} type="button">
|
|
140
|
-
<MessageCircle size={16} />
|
|
141
|
-
<span>{session.title}</span>
|
|
142
|
-
<small>{new Date(session.updatedAt).toLocaleDateString()}</small>
|
|
143
|
-
</button>
|
|
144
|
-
<div className="conversation-actions">
|
|
145
|
-
{isConfirming ? (
|
|
146
|
-
<>
|
|
147
|
-
<IconButton label={t("sidebar.aria.confirmDelete")} onClick={() => void deleteSession(session.id)}>
|
|
148
|
-
<Check size={14} />
|
|
149
|
-
</IconButton>
|
|
150
|
-
<IconButton label={t("sidebar.aria.cancelDelete")} onClick={() => setConfirmDeleteId(null)}>
|
|
151
|
-
<X size={14} />
|
|
152
|
-
</IconButton>
|
|
153
|
-
</>
|
|
154
|
-
) : (
|
|
155
|
-
<>
|
|
156
|
-
<IconButton
|
|
157
|
-
label={t("sidebar.aria.rename")}
|
|
158
|
-
onClick={() => {
|
|
159
|
-
setEditingId(session.id);
|
|
160
|
-
setEditingTitle(session.title);
|
|
161
|
-
}}
|
|
162
|
-
>
|
|
163
|
-
<PenLine size={14} />
|
|
164
|
-
</IconButton>
|
|
165
|
-
<IconButton label={t("sidebar.aria.delete")} onClick={() => setConfirmDeleteId(session.id)}>
|
|
166
|
-
<Trash2 size={14} />
|
|
167
|
-
</IconButton>
|
|
168
|
-
</>
|
|
169
|
-
)}
|
|
170
|
-
</div>
|
|
171
|
-
</>
|
|
172
|
-
)}
|
|
173
|
-
</div>
|
|
174
|
-
);
|
|
175
|
-
})}
|
|
176
|
-
</div>
|
|
164
|
+
<SessionList
|
|
165
|
+
sessions={sessions}
|
|
166
|
+
currentId={currentSession?.id}
|
|
167
|
+
isLoading={isLoading}
|
|
168
|
+
onSelect={selectAndGo}
|
|
169
|
+
onRename={updateSessionTitle}
|
|
170
|
+
onDelete={deleteSession}
|
|
171
|
+
onOpenSearch={onOpenSearch}
|
|
172
|
+
/>
|
|
177
173
|
</section>
|
|
178
174
|
|
|
179
175
|
<div className="sidebar__footer">
|
|
180
|
-
<button className="nav-item" onClick={onOpenSettings} type="button">
|
|
176
|
+
<button className="nav-item" onClick={onOpenSettings} type="button" title={t("sidebar.settings")}>
|
|
181
177
|
<Settings size={16} />
|
|
182
178
|
<span>{t("sidebar.settings")}</span>
|
|
183
179
|
</button>
|
|
@@ -25,11 +25,18 @@ const SSEContext = createContext<SSEContextValue | null>(null);
|
|
|
25
25
|
|
|
26
26
|
const RECONNECT_BASE_MS = 3000;
|
|
27
27
|
const RECONNECT_MAX_MS = 30000;
|
|
28
|
+
// #106: if an EventSource never fires `onopen` within this window we treat the
|
|
29
|
+
// connection as dead and force a rebuild. A frozen tab / bfcache restore can
|
|
30
|
+
// leave a stale source stuck in CONNECTING whose onopen/onerror never fire
|
|
31
|
+
// again — without this watchdog the UI sits on "正在连接实时通道" forever.
|
|
32
|
+
const OPEN_WATCHDOG_MS = 8000;
|
|
28
33
|
|
|
29
34
|
interface SessionConn {
|
|
30
35
|
source: EventSource;
|
|
31
36
|
reconnectAttempt: number;
|
|
32
37
|
reconnectTimer: number | null;
|
|
38
|
+
/** #106: fires if onopen doesn't arrive in time — forces a reconnect. */
|
|
39
|
+
openWatchdog: number | null;
|
|
33
40
|
/** Whether disconnectSession was called — disable auto-reconnect. */
|
|
34
41
|
manuallyClosed: boolean;
|
|
35
42
|
}
|
|
@@ -61,20 +68,62 @@ export function SSEProvider({ children }: { children: ReactNode }) {
|
|
|
61
68
|
return;
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
// A stale entry may exist (e.g. watchdog-forced rebuild) — clear its timers
|
|
72
|
+
// and close its source before replacing it.
|
|
73
|
+
if (conn) {
|
|
74
|
+
if (conn.reconnectTimer !== null) window.clearTimeout(conn.reconnectTimer);
|
|
75
|
+
if (conn.openWatchdog !== null) window.clearTimeout(conn.openWatchdog);
|
|
76
|
+
try {
|
|
77
|
+
conn.source.close();
|
|
78
|
+
} catch {
|
|
79
|
+
/* already closed */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
64
83
|
console.log(`[SSE] openConnection: ${sessionId}`);
|
|
65
84
|
setStatus(sessionId, "connecting");
|
|
66
85
|
const source = new EventSource(getSSEUrl(sessionId));
|
|
67
86
|
|
|
68
87
|
const entry: SessionConn = {
|
|
69
88
|
source,
|
|
70
|
-
reconnectAttempt: 0,
|
|
89
|
+
reconnectAttempt: conn?.reconnectAttempt ?? 0,
|
|
71
90
|
reconnectTimer: null,
|
|
91
|
+
openWatchdog: null,
|
|
72
92
|
manuallyClosed: false,
|
|
73
93
|
};
|
|
74
94
|
connsRef.current.set(sessionId, entry);
|
|
75
95
|
|
|
96
|
+
// #106: if onopen never lands, the connection is wedged. Tear it down and
|
|
97
|
+
// reconnect through the normal backoff path so the composer doesn't stay
|
|
98
|
+
// disabled on a dead "connecting" state.
|
|
99
|
+
entry.openWatchdog = window.setTimeout(() => {
|
|
100
|
+
entry.openWatchdog = null;
|
|
101
|
+
if (entry.manuallyClosed) return;
|
|
102
|
+
if (entry.source.readyState === EventSource.OPEN) return;
|
|
103
|
+
console.warn(`[SSE] open watchdog fired for ${sessionId} — forcing reconnect`);
|
|
104
|
+
try {
|
|
105
|
+
entry.source.close();
|
|
106
|
+
} catch {
|
|
107
|
+
/* already closed */
|
|
108
|
+
}
|
|
109
|
+
setStatus(sessionId, "error");
|
|
110
|
+
entry.reconnectAttempt += 1;
|
|
111
|
+
const delay = Math.min(
|
|
112
|
+
RECONNECT_BASE_MS * Math.pow(2, entry.reconnectAttempt - 1),
|
|
113
|
+
RECONNECT_MAX_MS,
|
|
114
|
+
);
|
|
115
|
+
entry.reconnectTimer = window.setTimeout(() => {
|
|
116
|
+
entry.reconnectTimer = null;
|
|
117
|
+
openConnection(sessionId);
|
|
118
|
+
}, delay);
|
|
119
|
+
}, OPEN_WATCHDOG_MS);
|
|
120
|
+
|
|
76
121
|
source.onopen = () => {
|
|
77
122
|
entry.reconnectAttempt = 0;
|
|
123
|
+
if (entry.openWatchdog !== null) {
|
|
124
|
+
window.clearTimeout(entry.openWatchdog);
|
|
125
|
+
entry.openWatchdog = null;
|
|
126
|
+
}
|
|
78
127
|
console.log(`[SSE] onopen: ${sessionId}`);
|
|
79
128
|
setStatus(sessionId, "open");
|
|
80
129
|
};
|
|
@@ -106,6 +155,10 @@ export function SSEProvider({ children }: { children: ReactNode }) {
|
|
|
106
155
|
|
|
107
156
|
source.onerror = () => {
|
|
108
157
|
console.error(`[SSE] onerror: ${sessionId}, reconnectAttempt=${entry.reconnectAttempt + 1}`);
|
|
158
|
+
if (entry.openWatchdog !== null) {
|
|
159
|
+
window.clearTimeout(entry.openWatchdog);
|
|
160
|
+
entry.openWatchdog = null;
|
|
161
|
+
}
|
|
109
162
|
setStatus(sessionId, "error");
|
|
110
163
|
source.close();
|
|
111
164
|
if (entry.manuallyClosed) return;
|
|
@@ -139,6 +192,10 @@ export function SSEProvider({ children }: { children: ReactNode }) {
|
|
|
139
192
|
window.clearTimeout(entry.reconnectTimer);
|
|
140
193
|
entry.reconnectTimer = null;
|
|
141
194
|
}
|
|
195
|
+
if (entry.openWatchdog !== null) {
|
|
196
|
+
window.clearTimeout(entry.openWatchdog);
|
|
197
|
+
entry.openWatchdog = null;
|
|
198
|
+
}
|
|
142
199
|
entry.source.close();
|
|
143
200
|
connsRef.current.delete(sessionId);
|
|
144
201
|
setStatus(sessionId, "idle");
|
|
@@ -152,12 +209,44 @@ export function SSEProvider({ children }: { children: ReactNode }) {
|
|
|
152
209
|
if (entry.reconnectTimer !== null) {
|
|
153
210
|
window.clearTimeout(entry.reconnectTimer);
|
|
154
211
|
}
|
|
212
|
+
if (entry.openWatchdog !== null) {
|
|
213
|
+
window.clearTimeout(entry.openWatchdog);
|
|
214
|
+
}
|
|
155
215
|
entry.source.close();
|
|
156
216
|
}
|
|
157
217
|
connsRef.current.clear();
|
|
158
218
|
};
|
|
159
219
|
}, [isAuthReady, currentSandbox?.status]);
|
|
160
220
|
|
|
221
|
+
// #106: bfcache / frozen-tab restore can leave an EventSource that looks
|
|
222
|
+
// alive (readyState !== CLOSED) but whose onopen/onerror never fire again, so
|
|
223
|
+
// the composer stays stuck on "connecting". On page restore or tab
|
|
224
|
+
// re-focus, force any non-open connection to rebuild. The browser-native
|
|
225
|
+
// `pageshow` (persisted) covers bfcache; `visibilitychange` covers the more
|
|
226
|
+
// common "switched away and back" case.
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
const revive = () => {
|
|
229
|
+
for (const [sessionId, entry] of connsRef.current) {
|
|
230
|
+
if (entry.manuallyClosed) continue;
|
|
231
|
+
if (entry.source.readyState === EventSource.OPEN) continue;
|
|
232
|
+
console.log(`[SSE] revive stale connection on restore: ${sessionId}`);
|
|
233
|
+
openConnection(sessionId);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const onPageShow = (event: PageTransitionEvent) => {
|
|
237
|
+
if (event.persisted) revive();
|
|
238
|
+
};
|
|
239
|
+
const onVisibility = () => {
|
|
240
|
+
if (document.visibilityState === "visible") revive();
|
|
241
|
+
};
|
|
242
|
+
window.addEventListener("pageshow", onPageShow);
|
|
243
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
244
|
+
return () => {
|
|
245
|
+
window.removeEventListener("pageshow", onPageShow);
|
|
246
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
247
|
+
};
|
|
248
|
+
}, [openConnection]);
|
|
249
|
+
|
|
161
250
|
const value = useMemo<SSEContextValue>(
|
|
162
251
|
() => ({ connectSession, disconnectSession, queueRef, tick, connections }),
|
|
163
252
|
[connectSession, disconnectSession, tick, connections],
|