@schlessera/brain-ui-react 0.17.0 → 0.19.0
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/components/activity/activity-page.d.ts +11 -0
- package/dist/components/activity/activity-page.d.ts.map +1 -0
- package/dist/components/activity/activity-page.js +208 -0
- package/dist/components/activity/activity-page.js.map +1 -0
- package/dist/components/activity/digest-card.d.ts +10 -0
- package/dist/components/activity/digest-card.d.ts.map +1 -0
- package/dist/components/activity/digest-card.js +51 -0
- package/dist/components/activity/digest-card.js.map +1 -0
- package/dist/components/activity/push-toggle.d.ts +2 -0
- package/dist/components/activity/push-toggle.d.ts.map +1 -0
- package/dist/components/activity/push-toggle.js +82 -0
- package/dist/components/activity/push-toggle.js.map +1 -0
- package/dist/components/activity/span-bits.d.ts +28 -0
- package/dist/components/activity/span-bits.d.ts.map +1 -0
- package/dist/components/activity/span-bits.js +43 -0
- package/dist/components/activity/span-bits.js.map +1 -0
- package/dist/components/chat/chat-page.d.ts.map +1 -1
- package/dist/components/chat/chat-page.js +4 -1
- package/dist/components/chat/chat-page.js.map +1 -1
- package/dist/components/chat/session-drawer.d.ts.map +1 -1
- package/dist/components/chat/session-drawer.js +1 -15
- package/dist/components/chat/session-drawer.js.map +1 -1
- package/dist/components/chat/subagent-view.d.ts +18 -0
- package/dist/components/chat/subagent-view.d.ts.map +1 -0
- package/dist/components/chat/subagent-view.js +79 -0
- package/dist/components/chat/subagent-view.js.map +1 -0
- package/dist/components/chat/tool-call-timeline.d.ts.map +1 -1
- package/dist/components/chat/tool-call-timeline.js +40 -4
- package/dist/components/chat/tool-call-timeline.js.map +1 -1
- package/dist/components/chat/tool-views.d.ts +4 -0
- package/dist/components/chat/tool-views.d.ts.map +1 -1
- package/dist/components/chat/tool-views.js +26 -0
- package/dist/components/chat/tool-views.js.map +1 -1
- package/dist/components/layout/mobile-tab-bar.d.ts.map +1 -1
- package/dist/components/layout/mobile-tab-bar.js +10 -4
- package/dist/components/layout/mobile-tab-bar.js.map +1 -1
- package/dist/components/layout/side-rail.d.ts.map +1 -1
- package/dist/components/layout/side-rail.js +7 -4
- package/dist/components/layout/side-rail.js.map +1 -1
- package/dist/hooks/use-now.d.ts +8 -0
- package/dist/hooks/use-now.d.ts.map +1 -0
- package/dist/hooks/use-now.js +32 -0
- package/dist/hooks/use-now.js.map +1 -0
- package/dist/hooks/use-websocket.d.ts.map +1 -1
- package/dist/hooks/use-websocket.js +46 -0
- package/dist/hooks/use-websocket.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/api-client.d.ts +40 -1
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +28 -0
- package/dist/lib/api-client.js.map +1 -1
- package/dist/stores/activity-store.d.ts +93 -0
- package/dist/stores/activity-store.d.ts.map +1 -0
- package/dist/stores/activity-store.js +310 -0
- package/dist/stores/activity-store.js.map +1 -0
- package/dist/stores/ui-store.d.ts +9 -1
- package/dist/stores/ui-store.d.ts.map +1 -1
- package/dist/stores/ui-store.js +3 -0
- package/dist/stores/ui-store.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/src/components/activity/activity-page.tsx +495 -0
- package/src/components/activity/digest-card.tsx +101 -0
- package/src/components/activity/push-toggle.tsx +128 -0
- package/src/components/activity/span-bits.tsx +71 -0
- package/src/components/chat/chat-page.tsx +17 -0
- package/src/components/chat/session-drawer.tsx +1 -12
- package/src/components/chat/subagent-view.tsx +229 -0
- package/src/components/chat/tool-call-timeline.tsx +61 -3
- package/src/components/chat/tool-views.tsx +22 -0
- package/src/components/layout/mobile-tab-bar.tsx +25 -5
- package/src/components/layout/side-rail.tsx +17 -0
- package/src/hooks/use-now.ts +32 -0
- package/src/hooks/use-websocket.ts +44 -0
- package/src/index.ts +1 -0
- package/src/lib/api-client.ts +75 -0
- package/src/stores/activity-store.ts +402 -0
- package/src/stores/ui-store.ts +13 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
+
Activity,
|
|
3
4
|
Brain,
|
|
4
5
|
RefreshCw,
|
|
5
6
|
History,
|
|
@@ -11,7 +12,9 @@ import {
|
|
|
11
12
|
} from "lucide-react";
|
|
12
13
|
import { useUIStore } from "../../stores/ui-store.js";
|
|
13
14
|
import { useChatStore } from "../../stores/chat-store.js";
|
|
15
|
+
import { useActivityStore } from "../../stores/activity-store.js";
|
|
14
16
|
import { cn } from "../../lib/utils.js";
|
|
17
|
+
import { CountBadge } from "../activity/span-bits.js";
|
|
15
18
|
|
|
16
19
|
export function MobileTabBar() {
|
|
17
20
|
const activeView = useUIStore((s) => s.activeView);
|
|
@@ -21,6 +24,7 @@ export function MobileTabBar() {
|
|
|
21
24
|
const toggleFilePanel = useUIStore((s) => s.toggleFilePanel);
|
|
22
25
|
const toggleSettingsPanel = useUIStore((s) => s.toggleSettingsPanel);
|
|
23
26
|
const clearMessages = useChatStore((s) => s.clearMessages);
|
|
27
|
+
const inboxCount = useActivityStore((s) => s.inbox.length);
|
|
24
28
|
|
|
25
29
|
/** Chat-scoped panels live in the chat page — surface it before opening them. */
|
|
26
30
|
function inChat(toggle: () => void) {
|
|
@@ -66,11 +70,15 @@ export function MobileTabBar() {
|
|
|
66
70
|
clearMessages();
|
|
67
71
|
}}
|
|
68
72
|
/>
|
|
73
|
+
{/* Activity holds the tab-bar slot; Graph moved into the More menu —
|
|
74
|
+
the phone glance-check ("is real work happening?") is the headline
|
|
75
|
+
flow and must stay one tap away (planning decision). */}
|
|
69
76
|
<TabIcon
|
|
70
|
-
icon={
|
|
71
|
-
label="
|
|
72
|
-
active={activeView === "
|
|
73
|
-
|
|
77
|
+
icon={Activity}
|
|
78
|
+
label="Activity"
|
|
79
|
+
active={activeView === "activity"}
|
|
80
|
+
badge={inboxCount}
|
|
81
|
+
onClick={() => setActiveView("activity")}
|
|
74
82
|
/>
|
|
75
83
|
<TabIcon icon={FolderTree} label="Files" onClick={toggleFilePanel} />
|
|
76
84
|
<div ref={moreRef} className="relative">
|
|
@@ -85,6 +93,14 @@ export function MobileTabBar() {
|
|
|
85
93
|
role="menu"
|
|
86
94
|
className="absolute bottom-full right-0 z-50 mb-2 min-w-[10rem] overflow-hidden rounded-xl border border-border bg-surface-overlay py-1 shadow-2xl"
|
|
87
95
|
>
|
|
96
|
+
<MoreItem
|
|
97
|
+
icon={Waypoints}
|
|
98
|
+
label="Graph"
|
|
99
|
+
onClick={() => {
|
|
100
|
+
setMoreOpen(false);
|
|
101
|
+
setActiveView("graph");
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
88
104
|
<MoreItem
|
|
89
105
|
icon={RefreshCw}
|
|
90
106
|
label="Sync"
|
|
@@ -121,21 +137,25 @@ function TabIcon({
|
|
|
121
137
|
label,
|
|
122
138
|
active,
|
|
123
139
|
onClick,
|
|
140
|
+
badge,
|
|
124
141
|
}: {
|
|
125
142
|
icon: typeof Brain;
|
|
126
143
|
label: string;
|
|
127
144
|
active?: boolean;
|
|
128
145
|
onClick?: () => void;
|
|
146
|
+
/** Unread-count dot (the inbox badge). Hidden at 0. */
|
|
147
|
+
badge?: number;
|
|
129
148
|
}) {
|
|
130
149
|
return (
|
|
131
150
|
<button
|
|
132
151
|
onClick={onClick}
|
|
133
152
|
className={cn(
|
|
134
|
-
"flex flex-col items-center gap-0.5 px-2 py-1 text-[10px]",
|
|
153
|
+
"relative flex flex-col items-center gap-0.5 px-2 py-1 text-[10px]",
|
|
135
154
|
active ? "text-primary" : "text-muted-foreground"
|
|
136
155
|
)}
|
|
137
156
|
>
|
|
138
157
|
<Icon className="h-5 w-5" />
|
|
158
|
+
{badge !== undefined && <CountBadge count={badge} className="right-0 top-0" />}
|
|
139
159
|
{label}
|
|
140
160
|
</button>
|
|
141
161
|
);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Activity,
|
|
2
3
|
Brain,
|
|
3
4
|
RefreshCw,
|
|
4
5
|
Newspaper,
|
|
@@ -9,9 +10,11 @@ import {
|
|
|
9
10
|
Waypoints,
|
|
10
11
|
} from "lucide-react";
|
|
11
12
|
import { useConnectionStore } from "../../stores/connection-store.js";
|
|
13
|
+
import { useActivityStore } from "../../stores/activity-store.js";
|
|
12
14
|
import { useUIStore } from "../../stores/ui-store.js";
|
|
13
15
|
import { useChatStore, activeChat } from "../../stores/chat-store.js";
|
|
14
16
|
import { cn } from "../../lib/utils.js";
|
|
17
|
+
import { CountBadge } from "../activity/span-bits.js";
|
|
15
18
|
|
|
16
19
|
export function SideRail() {
|
|
17
20
|
const wsStatus = useConnectionStore((s) => s.wsStatus);
|
|
@@ -25,6 +28,7 @@ export function SideRail() {
|
|
|
25
28
|
const clearMessages = useChatStore((s) => s.clearMessages);
|
|
26
29
|
const hasMessages = useChatStore((s) => activeChat(s).messages.length > 0);
|
|
27
30
|
const isStreaming = useChatStore((s) => activeChat(s).isStreaming);
|
|
31
|
+
const inboxCount = useActivityStore((s) => s.inbox.length);
|
|
28
32
|
|
|
29
33
|
/** Chat-scoped panels live in the chat page — surface it before opening them. */
|
|
30
34
|
function inChat(toggle: () => void) {
|
|
@@ -79,6 +83,15 @@ export function SideRail() {
|
|
|
79
83
|
setActiveView(activeView === "graph" ? "chat" : "graph")
|
|
80
84
|
}
|
|
81
85
|
/>
|
|
86
|
+
<RailButton
|
|
87
|
+
icon={Activity}
|
|
88
|
+
label="Activity"
|
|
89
|
+
active={activeView === "activity"}
|
|
90
|
+
badge={inboxCount}
|
|
91
|
+
onClick={() =>
|
|
92
|
+
setActiveView(activeView === "activity" ? "chat" : "activity")
|
|
93
|
+
}
|
|
94
|
+
/>
|
|
82
95
|
|
|
83
96
|
{/* Spacer */}
|
|
84
97
|
<div className="flex-1" />
|
|
@@ -125,12 +138,15 @@ function RailButton({
|
|
|
125
138
|
onClick,
|
|
126
139
|
disabled,
|
|
127
140
|
active,
|
|
141
|
+
badge,
|
|
128
142
|
}: {
|
|
129
143
|
icon: typeof Brain;
|
|
130
144
|
label: string;
|
|
131
145
|
onClick: () => void;
|
|
132
146
|
disabled?: boolean;
|
|
133
147
|
active?: boolean;
|
|
148
|
+
/** Unread-count dot (the inbox badge). Hidden at 0. */
|
|
149
|
+
badge?: number;
|
|
134
150
|
}) {
|
|
135
151
|
return (
|
|
136
152
|
<button
|
|
@@ -143,6 +159,7 @@ function RailButton({
|
|
|
143
159
|
)}
|
|
144
160
|
>
|
|
145
161
|
<Icon className="h-4.5 w-4.5" />
|
|
162
|
+
{badge !== undefined && <CountBadge count={badge} className="-right-0.5 -top-0.5" />}
|
|
146
163
|
{/* Tooltip */}
|
|
147
164
|
<span className="pointer-events-none absolute left-full ml-2 whitespace-nowrap rounded-md bg-surface-overlay px-2.5 py-1 text-xs font-medium text-foreground opacity-0 shadow-lg transition-opacity group-hover:opacity-100 border border-border">
|
|
148
165
|
{label}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/** All subscribed setters, driven by ONE shared interval. */
|
|
4
|
+
const listeners = new Set<(now: number) => void>();
|
|
5
|
+
let ticker: ReturnType<typeof setInterval> | null = null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A ticking `Date.now()` for live elapsed labels. Every consumer shares a
|
|
9
|
+
* single setInterval (created on the first subscriber's cadence, torn down
|
|
10
|
+
* with the last), so a screen full of running rows costs one timer, and all
|
|
11
|
+
* their labels tick in the same frame.
|
|
12
|
+
*/
|
|
13
|
+
export function useNow(intervalMs = 1000): number {
|
|
14
|
+
const [now, setNow] = useState(() => Date.now());
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
listeners.add(setNow);
|
|
17
|
+
if (!ticker) {
|
|
18
|
+
ticker = setInterval(() => {
|
|
19
|
+
const t = Date.now();
|
|
20
|
+
for (const listener of listeners) listener(t);
|
|
21
|
+
}, intervalMs);
|
|
22
|
+
}
|
|
23
|
+
return () => {
|
|
24
|
+
listeners.delete(setNow);
|
|
25
|
+
if (listeners.size === 0 && ticker) {
|
|
26
|
+
clearInterval(ticker);
|
|
27
|
+
ticker = null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}, [intervalMs]);
|
|
31
|
+
return now;
|
|
32
|
+
}
|
|
@@ -4,6 +4,7 @@ import { useConnectionStore } from "../stores/connection-store.js";
|
|
|
4
4
|
import { useChatStore, activeChat, type ChatKey } from "../stores/chat-store.js";
|
|
5
5
|
import { useProviderStore } from "../stores/provider-store.js";
|
|
6
6
|
import { useMaskStore } from "../stores/mask-store.js";
|
|
7
|
+
import { useActivityStore, loadSessionActivityHistory } from "../stores/activity-store.js";
|
|
7
8
|
import { getWsUrl } from "../lib/backend.js";
|
|
8
9
|
import type {
|
|
9
10
|
ServerMessage,
|
|
@@ -121,9 +122,44 @@ function isOurDraftAnnouncement(
|
|
|
121
122
|
return true;
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Lazily open a session-scoped activity subscription (live subagent rows,
|
|
127
|
+
* server-stamped tool timing). Idempotent per connection; the subscribed set
|
|
128
|
+
* resets when a new socket's server_hello arrives.
|
|
129
|
+
*/
|
|
130
|
+
function ensureActivitySubscription(sessionId: string | null | undefined): void {
|
|
131
|
+
if (!sessionId) return;
|
|
132
|
+
const activity = useActivityStore.getState();
|
|
133
|
+
if (!activity.supported || activity.subscribed[sessionId]) return;
|
|
134
|
+
activity.markSubscribed(sessionId);
|
|
135
|
+
wsClient?.send({ type: "activity_subscribe", view: "session", sessionId });
|
|
136
|
+
// The subscription snapshot carries OPEN runs; finished runs (history
|
|
137
|
+
// timing, past fan-outs) come from the REST side.
|
|
138
|
+
void loadSessionActivityHistory(sessionId);
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
export function handleServerMessage(msg: ServerMessage) {
|
|
125
142
|
const state = useChatStore.getState();
|
|
126
143
|
|
|
144
|
+
// Activity stream frames feed their own store and never touch chat state.
|
|
145
|
+
if (msg.type === "server_hello") {
|
|
146
|
+
useActivityStore.getState().setSupported(msg.capabilities?.activity === true);
|
|
147
|
+
// A new hello means a new connection: server-side subscriptions are gone.
|
|
148
|
+
useActivityStore.getState().resetSubscriptions();
|
|
149
|
+
// …and view-owned subscriptions (the Activity index) must re-send too.
|
|
150
|
+
useActivityStore.getState().bumpConnectionEpoch();
|
|
151
|
+
ensureActivitySubscription(state.activeSessionId);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (msg.type === "activity_snapshot") {
|
|
155
|
+
useActivityStore.getState().applySnapshot(msg);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (msg.type === "activity_delta") {
|
|
159
|
+
useActivityStore.getState().applyDelta(msg);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
127
163
|
// Parallel-session demux (per-session buffers). Every frame is scoped by
|
|
128
164
|
// sessionId; a scoped frame lands in ITS session's buffer, so a background
|
|
129
165
|
// session's transcript keeps accumulating while another one is in view.
|
|
@@ -186,6 +222,10 @@ export function handleServerMessage(msg: ServerMessage) {
|
|
|
186
222
|
break;
|
|
187
223
|
|
|
188
224
|
case "tool_use_start":
|
|
225
|
+
// A tool call INSIDE a subagent nests under its Agent entry, rendered
|
|
226
|
+
// from the activity stream — putting it in the flat transcript would
|
|
227
|
+
// interleave fan-out work with the main turn's steps.
|
|
228
|
+
if (msg.parentToolUseId) break;
|
|
189
229
|
state.startToolCall(key, msg.toolUseId, msg.toolName);
|
|
190
230
|
break;
|
|
191
231
|
|
|
@@ -194,6 +234,9 @@ export function handleServerMessage(msg: ServerMessage) {
|
|
|
194
234
|
break;
|
|
195
235
|
|
|
196
236
|
case "tool_use_complete":
|
|
237
|
+
// Subagent tool calls render nested (activity stream), and the flat
|
|
238
|
+
// store's name-based fallback match must never see them.
|
|
239
|
+
if (msg.parentToolUseId) break;
|
|
197
240
|
state.completeToolCall(key, msg.toolUseId, msg.toolName, msg.input);
|
|
198
241
|
break;
|
|
199
242
|
|
|
@@ -237,6 +280,7 @@ export function handleServerMessage(msg: ServerMessage) {
|
|
|
237
280
|
break;
|
|
238
281
|
|
|
239
282
|
case "session_info": {
|
|
283
|
+
ensureActivitySubscription(msg.sessionId);
|
|
240
284
|
// bindDraftSession above handled draft adoption; an info frame may still
|
|
241
285
|
// re-pin the provider picker when it concerns the session in view.
|
|
242
286
|
const current = useChatStore.getState();
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { ConnectionGate } from "./components/connectivity/connection-gate.js";
|
|
|
16
16
|
export { AppShell } from "./components/layout/app-shell.js";
|
|
17
17
|
export { ChatPage } from "./components/chat/chat-page.js";
|
|
18
18
|
export { GraphPage } from "./components/graph/graph-page.js";
|
|
19
|
+
export { ActivityPage } from "./components/activity/activity-page.js";
|
|
19
20
|
|
|
20
21
|
// Markdown renderer (also useful standalone, e.g. for a dev kitchen sink).
|
|
21
22
|
export { BrainMarkdown } from "./components/chat/brain-markdown.js";
|
package/src/lib/api-client.ts
CHANGED
|
@@ -7,6 +7,23 @@ import type {
|
|
|
7
7
|
ProviderInfo,
|
|
8
8
|
PasskeySummary,
|
|
9
9
|
ModelCatalogResponse,
|
|
10
|
+
ActivityRunSummary,
|
|
11
|
+
ActivityRunDetail,
|
|
12
|
+
ActivityRollups,
|
|
13
|
+
ActivityDigest,
|
|
14
|
+
ActivityIntent,
|
|
15
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
16
|
+
|
|
17
|
+
// Activity REST types live in the SDK protocol (shared with the server);
|
|
18
|
+
// re-exported here so existing importers keep working.
|
|
19
|
+
export type {
|
|
20
|
+
ActivityRunSummary,
|
|
21
|
+
ActivityRunDetail,
|
|
22
|
+
ActivityRunRollup,
|
|
23
|
+
ActivityAggregate,
|
|
24
|
+
ActivityRollups,
|
|
25
|
+
ActivityDigest,
|
|
26
|
+
ActivityIntent,
|
|
10
27
|
} from "@schlessera/brain-ui-sdk/protocol";
|
|
11
28
|
import type {
|
|
12
29
|
AuthenticationResponseJSON,
|
|
@@ -219,4 +236,62 @@ export const api = {
|
|
|
219
236
|
fetchJson<{ ok: true }>(`/auth/passkey/${encodeURIComponent(id)}`, {
|
|
220
237
|
method: "DELETE",
|
|
221
238
|
}),
|
|
239
|
+
|
|
240
|
+
// --- Activity record (read side; live updates ride the WebSocket) ---
|
|
241
|
+
|
|
242
|
+
activityRuns: (opts?: {
|
|
243
|
+
origin?: "session" | "cron";
|
|
244
|
+
job?: string;
|
|
245
|
+
session?: string;
|
|
246
|
+
status?: string;
|
|
247
|
+
limit?: number;
|
|
248
|
+
before?: number;
|
|
249
|
+
}) =>
|
|
250
|
+
fetchJson<{ live: ActivityRunSummary[]; history: ActivityRunSummary[] }>(
|
|
251
|
+
"/activity/runs?" +
|
|
252
|
+
[
|
|
253
|
+
opts?.origin && `origin=${opts.origin}`,
|
|
254
|
+
opts?.job && `job=${encodeURIComponent(opts.job)}`,
|
|
255
|
+
opts?.session && `session=${encodeURIComponent(opts.session)}`,
|
|
256
|
+
opts?.status && `status=${encodeURIComponent(opts.status)}`,
|
|
257
|
+
opts?.limit && `limit=${opts.limit}`,
|
|
258
|
+
opts?.before && `before=${opts.before}`,
|
|
259
|
+
]
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.join("&")
|
|
262
|
+
),
|
|
263
|
+
|
|
264
|
+
activityRun: (runId: string) =>
|
|
265
|
+
fetchJson<ActivityRunDetail>(`/activity/runs/${encodeURIComponent(runId)}`),
|
|
266
|
+
|
|
267
|
+
activityRollups: (days?: number) =>
|
|
268
|
+
fetchJson<ActivityRollups>(`/activity/rollups${days ? `?days=${days}` : ""}`),
|
|
269
|
+
|
|
270
|
+
activityInbox: () => fetchJson<{ intents: ActivityIntent[] }>("/activity/inbox"),
|
|
271
|
+
|
|
272
|
+
activityInboxAck: (id: number) =>
|
|
273
|
+
fetchJson<{ ok: true }>(`/activity/inbox/${id}/ack`, { method: "POST" }),
|
|
274
|
+
|
|
275
|
+
activityInboxAckAll: () =>
|
|
276
|
+
fetchJson<{ acknowledged: number }>("/activity/inbox/ack-all", { method: "POST" }),
|
|
277
|
+
|
|
278
|
+
activityDigest: () =>
|
|
279
|
+
fetchJson<{ digest: ActivityDigest | null; dismissedAt: number }>("/activity/digest"),
|
|
280
|
+
|
|
281
|
+
activityDigestDismiss: () =>
|
|
282
|
+
fetchJson<{ ok: true }>("/activity/digest/dismiss", { method: "POST" }),
|
|
283
|
+
|
|
284
|
+
pushPublicKey: () => fetchJson<{ publicKey: string }>("/push/public-key"),
|
|
285
|
+
|
|
286
|
+
pushSubscribe: (subscription: unknown, label?: string) =>
|
|
287
|
+
fetchJson<{ ok: true }>("/push/subscribe", {
|
|
288
|
+
method: "POST",
|
|
289
|
+
body: JSON.stringify({ subscription, label }),
|
|
290
|
+
}),
|
|
291
|
+
|
|
292
|
+
pushUnsubscribe: (endpoint: string) =>
|
|
293
|
+
fetchJson<{ removed: boolean }>("/push/unsubscribe", {
|
|
294
|
+
method: "POST",
|
|
295
|
+
body: JSON.stringify({ endpoint }),
|
|
296
|
+
}),
|
|
222
297
|
};
|