@schlessera/brain-ui-react 0.18.0 → 0.20.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 +243 -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 +55 -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 +121 -0
- package/dist/components/activity/push-toggle.js.map +1 -0
- package/dist/components/activity/span-bits.d.ts +76 -0
- package/dist/components/activity/span-bits.d.ts.map +1 -0
- package/dist/components/activity/span-bits.js +127 -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 +83 -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/components/settings/models-tab.d.ts +15 -0
- package/dist/components/settings/models-tab.d.ts.map +1 -1
- package/dist/components/settings/models-tab.js +100 -21
- package/dist/components/settings/models-tab.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 +67 -1
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +41 -0
- package/dist/lib/api-client.js.map +1 -1
- package/dist/stores/activity-store.d.ts +108 -0
- package/dist/stores/activity-store.d.ts.map +1 -0
- package/dist/stores/activity-store.js +359 -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 +623 -0
- package/src/components/activity/digest-card.tsx +105 -0
- package/src/components/activity/push-toggle.tsx +157 -0
- package/src/components/activity/span-bits.tsx +194 -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 +239 -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/components/settings/models-tab.tsx +135 -21
- 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 +111 -0
- package/src/stores/activity-store.ts +451 -0
- package/src/stores/ui-store.ts +13 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { Eye, EyeOff, Loader2, RefreshCw } from "lucide-react";
|
|
3
3
|
import type {
|
|
4
|
+
BillingMode,
|
|
4
5
|
ModelCatalogEntry,
|
|
5
6
|
ModelCatalogResponse,
|
|
6
7
|
} from "@schlessera/brain-ui-sdk/protocol";
|
|
@@ -21,6 +22,9 @@ export function ModelsTab({ active }: { active: boolean }) {
|
|
|
21
22
|
const [refreshing, setRefreshing] = useState(false);
|
|
22
23
|
const [error, setError] = useState<string | null>(null);
|
|
23
24
|
const loadProviders = useProviderStore((s) => s.loadProviders);
|
|
25
|
+
const commitGate = useRef(createRequestGate());
|
|
26
|
+
/** Serializes full-record PUTs — see commitCatalog. */
|
|
27
|
+
const commitQueue = useRef<Promise<void>>(Promise.resolve());
|
|
24
28
|
|
|
25
29
|
useEffect(() => {
|
|
26
30
|
if (!active) return;
|
|
@@ -45,34 +49,86 @@ export function ModelsTab({ active }: { active: boolean }) {
|
|
|
45
49
|
};
|
|
46
50
|
}, [active]);
|
|
47
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Optimistic-update skeleton shared by the hidden toggle and the billing
|
|
54
|
+
* select: the change is the user's own click, so reflect it immediately,
|
|
55
|
+
* commit, then reload the composer picker's own roster copy (fetched once
|
|
56
|
+
* on mount, it would otherwise lag until a page reload). A failed write
|
|
57
|
+
* rolls back and surfaces the error.
|
|
58
|
+
*
|
|
59
|
+
* Ordered through `createRequestGate`: two rows edited within one
|
|
60
|
+
* round-trip interleave, and without the guard the FIRST response (or its
|
|
61
|
+
* failure rollback) lands last and silently overwrites the newer edit. A
|
|
62
|
+
* superseded response/rollback is dropped — the newer request's payload
|
|
63
|
+
* was built on top of this one's optimistic state, so it already carries
|
|
64
|
+
* this change (and its own catch surfaces any error that still matters).
|
|
65
|
+
*/
|
|
66
|
+
async function commitCatalog(
|
|
67
|
+
optimistic: ModelCatalogResponse,
|
|
68
|
+
commit: () => Promise<ModelCatalogResponse>
|
|
69
|
+
) {
|
|
70
|
+
const isCurrent = commitGate.current.begin();
|
|
71
|
+
const previous = catalog;
|
|
72
|
+
setCatalog(optimistic);
|
|
73
|
+
setError(null);
|
|
74
|
+
// The gate drops superseded RESPONSES; this queue serializes the WRITES.
|
|
75
|
+
// Both matter: the server stores full records, so two concurrent PUTs
|
|
76
|
+
// could land older-last and silently clobber the newer record server-side
|
|
77
|
+
// even while the client looked right. Each commit waits for the previous
|
|
78
|
+
// one to settle; payloads are built on optimistic state, so the newest
|
|
79
|
+
// write already carries every earlier edit.
|
|
80
|
+
const run = commitQueue.current.then(async () => {
|
|
81
|
+
try {
|
|
82
|
+
const confirmed = await commit();
|
|
83
|
+
if (isCurrent()) setCatalog(confirmed);
|
|
84
|
+
void loadProviders();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (isCurrent()) {
|
|
87
|
+
setCatalog(previous);
|
|
88
|
+
setError(err instanceof Error ? err.message : "Could not save");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
commitQueue.current = run;
|
|
93
|
+
await run;
|
|
94
|
+
}
|
|
95
|
+
|
|
48
96
|
async function toggleHidden(entry: ModelCatalogEntry) {
|
|
49
97
|
if (!catalog) return;
|
|
50
|
-
const previous = catalog;
|
|
51
98
|
const hidden = catalog.models
|
|
52
99
|
.filter((model) =>
|
|
53
100
|
model.id === entry.id ? !entry.hidden : model.hidden
|
|
54
101
|
)
|
|
55
102
|
.map((model) => model.id);
|
|
56
103
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
await commitCatalog(
|
|
105
|
+
{
|
|
106
|
+
...catalog,
|
|
107
|
+
models: catalog.models.map((model) =>
|
|
108
|
+
model.id === entry.id ? { ...model, hidden: !model.hidden } : model
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
() => api.setHiddenModels(hidden)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function changeBilling(entry: ModelCatalogEntry, next: BillingMode | "auto") {
|
|
116
|
+
if (!catalog) return;
|
|
117
|
+
|
|
118
|
+
// What "auto" resolves to is only known server-side, so switching back to
|
|
119
|
+
// auto keeps the current resolved mode until the confirmed catalog
|
|
120
|
+
// corrects it a beat later.
|
|
121
|
+
await commitCatalog(
|
|
122
|
+
{
|
|
123
|
+
...catalog,
|
|
124
|
+
models: catalog.models.map((model) => {
|
|
125
|
+
if (model.id !== entry.id) return model;
|
|
126
|
+
const { billingOverride: _cleared, ...base } = model;
|
|
127
|
+
return next === "auto" ? base : { ...base, billingOverride: next, billingMode: next };
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
() => api.setBillingOverrides(nextBillingOverrides(catalog.models, entry.id, next))
|
|
131
|
+
);
|
|
76
132
|
}
|
|
77
133
|
|
|
78
134
|
async function onRefresh() {
|
|
@@ -115,6 +171,7 @@ export function ModelsTab({ active }: { active: boolean }) {
|
|
|
115
171
|
key={entry.id}
|
|
116
172
|
entry={entry}
|
|
117
173
|
onToggle={() => toggleHidden(entry)}
|
|
174
|
+
onBilling={(next) => changeBilling(entry, next)}
|
|
118
175
|
/>
|
|
119
176
|
))}
|
|
120
177
|
{catalog?.models.length === 0 && (
|
|
@@ -162,12 +219,51 @@ export function ModelsTab({ active }: { active: boolean }) {
|
|
|
162
219
|
);
|
|
163
220
|
}
|
|
164
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Request-ordering guard for optimistic commits: `begin()` claims a token
|
|
224
|
+
* and returns a predicate that holds only while no later request has begun.
|
|
225
|
+
* An older in-flight request must never write over a newer edit's state.
|
|
226
|
+
*/
|
|
227
|
+
export function createRequestGate(): { begin: () => () => boolean } {
|
|
228
|
+
let seq = 0;
|
|
229
|
+
return {
|
|
230
|
+
begin() {
|
|
231
|
+
const token = ++seq;
|
|
232
|
+
return () => seq === token;
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The billing-override record PUT after changing one profile: every other
|
|
239
|
+
* profile keeps its stored override, the changed one is set — or, for "auto",
|
|
240
|
+
* REMOVED, never stored as a redundant explicit value.
|
|
241
|
+
*/
|
|
242
|
+
export function nextBillingOverrides(
|
|
243
|
+
models: ModelCatalogEntry[],
|
|
244
|
+
id: string,
|
|
245
|
+
next: BillingMode | "auto"
|
|
246
|
+
): Record<string, BillingMode> {
|
|
247
|
+
const billing: Record<string, BillingMode> = {};
|
|
248
|
+
for (const model of models) {
|
|
249
|
+
const value = model.id === id ? (next === "auto" ? undefined : next) : model.billingOverride;
|
|
250
|
+
if (value) billing[model.id] = value;
|
|
251
|
+
}
|
|
252
|
+
return billing;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function billingLabel(mode: BillingMode): string {
|
|
256
|
+
return mode === "api" ? "API" : "Subscription";
|
|
257
|
+
}
|
|
258
|
+
|
|
165
259
|
function ModelRow({
|
|
166
260
|
entry,
|
|
167
261
|
onToggle,
|
|
262
|
+
onBilling,
|
|
168
263
|
}: {
|
|
169
264
|
entry: ModelCatalogEntry;
|
|
170
265
|
onToggle: () => void;
|
|
266
|
+
onBilling: (next: BillingMode | "auto") => void;
|
|
171
267
|
}) {
|
|
172
268
|
const Icon = entry.hidden ? EyeOff : Eye;
|
|
173
269
|
return (
|
|
@@ -185,6 +281,24 @@ function ModelRow({
|
|
|
185
281
|
{entry.source === "declared" ? " · configured" : ""}
|
|
186
282
|
</p>
|
|
187
283
|
</div>
|
|
284
|
+
{/* Tri-state billing: the collapsed control always reads as the
|
|
285
|
+
RESOLVED mode — the Auto option carries what auto resolves to, so
|
|
286
|
+
"Auto (subscription)" and a forced "Subscription" are both legible
|
|
287
|
+
at a glance. */}
|
|
288
|
+
<select
|
|
289
|
+
value={entry.billingOverride ?? "auto"}
|
|
290
|
+
onChange={(e) => onBilling(e.target.value as BillingMode | "auto")}
|
|
291
|
+
aria-label={`Billing for ${entry.label}`}
|
|
292
|
+
className="h-8 shrink-0 rounded-lg border border-border-subtle bg-surface px-1.5 text-[11px] text-muted-foreground transition-colors hover:border-primary hover:text-foreground"
|
|
293
|
+
>
|
|
294
|
+
<option value="auto">
|
|
295
|
+
{!entry.billingOverride && entry.billingMode
|
|
296
|
+
? `Auto (${billingLabel(entry.billingMode).toLowerCase()})`
|
|
297
|
+
: "Auto"}
|
|
298
|
+
</option>
|
|
299
|
+
<option value="subscription">Subscription</option>
|
|
300
|
+
<option value="api">API</option>
|
|
301
|
+
</select>
|
|
188
302
|
<button
|
|
189
303
|
onClick={onToggle}
|
|
190
304
|
title={entry.hidden ? "Show in picker" : "Hide from picker"}
|
|
@@ -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
|
@@ -6,7 +6,25 @@ import type {
|
|
|
6
6
|
PronunciationOverride,
|
|
7
7
|
ProviderInfo,
|
|
8
8
|
PasskeySummary,
|
|
9
|
+
BillingMode,
|
|
9
10
|
ModelCatalogResponse,
|
|
11
|
+
ActivityRunSummary,
|
|
12
|
+
ActivityRunDetail,
|
|
13
|
+
ActivityRollups,
|
|
14
|
+
ActivityDigest,
|
|
15
|
+
ActivityIntent,
|
|
16
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
17
|
+
|
|
18
|
+
// Activity REST types live in the SDK protocol (shared with the server);
|
|
19
|
+
// re-exported here so existing importers keep working.
|
|
20
|
+
export type {
|
|
21
|
+
ActivityRunSummary,
|
|
22
|
+
ActivityRunDetail,
|
|
23
|
+
ActivityRunRollup,
|
|
24
|
+
ActivityAggregate,
|
|
25
|
+
ActivityRollups,
|
|
26
|
+
ActivityDigest,
|
|
27
|
+
ActivityIntent,
|
|
10
28
|
} from "@schlessera/brain-ui-sdk/protocol";
|
|
11
29
|
import type {
|
|
12
30
|
AuthenticationResponseJSON,
|
|
@@ -35,6 +53,23 @@ export interface BrainSearchResponse {
|
|
|
35
53
|
warnings: string[];
|
|
36
54
|
}
|
|
37
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Pricing-table freshness (GET /api/models/pricing) — mirrors the server
|
|
58
|
+
* pricing service's state. The route is additive: an older server 404s, and
|
|
59
|
+
* callers treat the rejection as "no freshness signal, show nothing".
|
|
60
|
+
*/
|
|
61
|
+
export interface PricingState {
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
/** When a refresh last succeeded (either source); null when none ever has. */
|
|
64
|
+
fetchedAt: number | null;
|
|
65
|
+
/** The current table is older than the TTL (or was never fetched). */
|
|
66
|
+
stale: boolean;
|
|
67
|
+
/** What the table is served from: remote data (cache included) or the bundled snapshot. */
|
|
68
|
+
source: "remote" | "snapshot";
|
|
69
|
+
/** Last refresh failure, if the current table is served despite one. */
|
|
70
|
+
error?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
38
73
|
/** Backend id + capability flags the client renders behavior from. */
|
|
39
74
|
export interface BackendInfo {
|
|
40
75
|
id: string;
|
|
@@ -164,6 +199,16 @@ export const api = {
|
|
|
164
199
|
body: JSON.stringify({ hidden }),
|
|
165
200
|
}),
|
|
166
201
|
|
|
202
|
+
/** Replace the billing-override record (full record, not a delta); returns the new catalog. */
|
|
203
|
+
setBillingOverrides: (billing: Record<string, BillingMode>) =>
|
|
204
|
+
fetchJson<ModelCatalogResponse>("/models/billing", {
|
|
205
|
+
method: "PUT",
|
|
206
|
+
body: JSON.stringify({ billing }),
|
|
207
|
+
}),
|
|
208
|
+
|
|
209
|
+
/** Pricing-table freshness for the Activity staleness indicator (see `PricingState`). */
|
|
210
|
+
pricingState: () => fetchJson<PricingState>("/models/pricing"),
|
|
211
|
+
|
|
167
212
|
/** Force a discovery refresh, bypassing the TTL. */
|
|
168
213
|
refreshModels: () =>
|
|
169
214
|
fetchJson<ModelCatalogResponse>("/models/refresh", { method: "POST" }),
|
|
@@ -219,4 +264,70 @@ export const api = {
|
|
|
219
264
|
fetchJson<{ ok: true }>(`/auth/passkey/${encodeURIComponent(id)}`, {
|
|
220
265
|
method: "DELETE",
|
|
221
266
|
}),
|
|
267
|
+
|
|
268
|
+
// --- Activity record (read side; live updates ride the WebSocket) ---
|
|
269
|
+
|
|
270
|
+
activityRuns: (opts?: {
|
|
271
|
+
origin?: "session" | "cron";
|
|
272
|
+
job?: string;
|
|
273
|
+
session?: string;
|
|
274
|
+
status?: string;
|
|
275
|
+
limit?: number;
|
|
276
|
+
before?: number;
|
|
277
|
+
}) =>
|
|
278
|
+
fetchJson<{ live: ActivityRunSummary[]; history: ActivityRunSummary[] }>(
|
|
279
|
+
"/activity/runs?" +
|
|
280
|
+
[
|
|
281
|
+
opts?.origin && `origin=${opts.origin}`,
|
|
282
|
+
opts?.job && `job=${encodeURIComponent(opts.job)}`,
|
|
283
|
+
opts?.session && `session=${encodeURIComponent(opts.session)}`,
|
|
284
|
+
opts?.status && `status=${encodeURIComponent(opts.status)}`,
|
|
285
|
+
opts?.limit && `limit=${opts.limit}`,
|
|
286
|
+
opts?.before && `before=${opts.before}`,
|
|
287
|
+
]
|
|
288
|
+
.filter(Boolean)
|
|
289
|
+
.join("&")
|
|
290
|
+
),
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* One run's detail. Payload bodies (tool_input/tool_output events) are
|
|
294
|
+
* excluded by default — the session-history fetch only needs span timings —
|
|
295
|
+
* and opted into by the drill-in views via `includePayloads`.
|
|
296
|
+
*/
|
|
297
|
+
activityRun: (runId: string, opts?: { includePayloads?: boolean }) =>
|
|
298
|
+
fetchJson<ActivityRunDetail>(
|
|
299
|
+
`/activity/runs/${encodeURIComponent(runId)}` +
|
|
300
|
+
(opts?.includePayloads ? "?include=payloads" : "")
|
|
301
|
+
),
|
|
302
|
+
|
|
303
|
+
activityRollups: (days?: number) =>
|
|
304
|
+
fetchJson<ActivityRollups>(`/activity/rollups${days ? `?days=${days}` : ""}`),
|
|
305
|
+
|
|
306
|
+
activityInbox: () => fetchJson<{ intents: ActivityIntent[] }>("/activity/inbox"),
|
|
307
|
+
|
|
308
|
+
activityInboxAck: (id: number) =>
|
|
309
|
+
fetchJson<{ ok: true }>(`/activity/inbox/${id}/ack`, { method: "POST" }),
|
|
310
|
+
|
|
311
|
+
activityInboxAckAll: () =>
|
|
312
|
+
fetchJson<{ acknowledged: number }>("/activity/inbox/ack-all", { method: "POST" }),
|
|
313
|
+
|
|
314
|
+
activityDigest: () =>
|
|
315
|
+
fetchJson<{ digest: ActivityDigest | null; dismissedAt: number }>("/activity/digest"),
|
|
316
|
+
|
|
317
|
+
activityDigestDismiss: () =>
|
|
318
|
+
fetchJson<{ ok: true }>("/activity/digest/dismiss", { method: "POST" }),
|
|
319
|
+
|
|
320
|
+
pushPublicKey: () => fetchJson<{ publicKey: string }>("/push/public-key"),
|
|
321
|
+
|
|
322
|
+
pushSubscribe: (subscription: unknown, label?: string) =>
|
|
323
|
+
fetchJson<{ ok: true }>("/push/subscribe", {
|
|
324
|
+
method: "POST",
|
|
325
|
+
body: JSON.stringify({ subscription, label }),
|
|
326
|
+
}),
|
|
327
|
+
|
|
328
|
+
pushUnsubscribe: (endpoint: string) =>
|
|
329
|
+
fetchJson<{ removed: boolean }>("/push/unsubscribe", {
|
|
330
|
+
method: "POST",
|
|
331
|
+
body: JSON.stringify({ endpoint }),
|
|
332
|
+
}),
|
|
222
333
|
};
|