@workerdeck/protocol 0.23.0 → 1.0.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/build/index.d.mts +204 -1707
- package/build/index.mjs +55 -460
- package/build/index.mjs.map +1 -1
- package/package.json +3 -2
package/build/index.mjs
CHANGED
|
@@ -18,47 +18,9 @@ function sessionState(info) {
|
|
|
18
18
|
if (runningSubagents(info).length > 0) return "working";
|
|
19
19
|
return "idle";
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
* The sub-agents a list row draws as live.
|
|
23
|
-
*
|
|
24
|
-
* `sessionState` deliberately does **not** grow a `subagents` bucket — a fifth
|
|
25
|
-
* state would split `working` in two for every client that filters by it,
|
|
26
|
-
* including the ones that have not shipped this yet. Instead `working` *counts*
|
|
27
|
-
* them: a synchronous `Task` keeps the turn in flight so the status already
|
|
28
|
-
* says `working`, and a **background** agent — which outlives its turn on
|
|
29
|
-
* purpose — is the carve-out the extra arm in `sessionState` exists for.
|
|
30
|
-
* That is what makes "sub-agents are an annotation on a working row" true
|
|
31
|
-
* rather than assumed: the row is in the working bucket whichever kind is
|
|
32
|
-
* running, and this list only says more about it.
|
|
33
|
-
*/
|
|
34
21
|
function runningSubagents(info) {
|
|
35
22
|
return (info.subagents ?? []).filter((sub) => sub.status === "running");
|
|
36
23
|
}
|
|
37
|
-
/**
|
|
38
|
-
* A sub-agent's identity on one line: `Explore · find the auth check`.
|
|
39
|
-
*
|
|
40
|
-
* The same two fields `taskLabel` builds its transcript row from, minus the
|
|
41
|
-
* `Task(…)` wrapper — a list row is already inside a session, so naming the tool
|
|
42
|
-
* spends the width that the description needs. Falls back to the bare agent type,
|
|
43
|
-
* then to a generic word: a row with no label at all reads as a rendering bug,
|
|
44
|
-
* and an engine is free to send neither field.
|
|
45
|
-
*/
|
|
46
|
-
/**
|
|
47
|
-
* Does this record name an **agent**, as opposed to a task the model merely
|
|
48
|
-
* described?
|
|
49
|
-
*
|
|
50
|
-
* The tracker opens a record for every spawner call and for any nested event
|
|
51
|
-
* whose parent it has not seen, so the list holds two different things wearing
|
|
52
|
-
* one shape. One carries a `subagent_type` — a delegated agent with an identity
|
|
53
|
-
* (`Explore`), whose own work is worth a surface of its own. The other carries
|
|
54
|
-
* only a description, and there is no agent there to open: a row that offered a
|
|
55
|
-
* screen and then showed a frame with nothing in it would be worse than a row
|
|
56
|
-
* that offered nothing.
|
|
57
|
-
*
|
|
58
|
-
* Here rather than in a client because it decides two things a list must not
|
|
59
|
-
* disagree about across surfaces — what is pressable, and what wears the
|
|
60
|
-
* sub-agent colour.
|
|
61
|
-
*/
|
|
62
24
|
function isAgentRecord(sub) {
|
|
63
25
|
return (sub.agentType?.trim() ?? "") !== "";
|
|
64
26
|
}
|
|
@@ -78,22 +40,9 @@ const DEFAULT_VIEW_CONFIG = {
|
|
|
78
40
|
groupBy: "state",
|
|
79
41
|
sortBy: "recent"
|
|
80
42
|
};
|
|
81
|
-
/** The adapters actually present, for the filter chips — derived rather than
|
|
82
|
-
* enumerated, so a new engine needs no change here. */
|
|
83
43
|
function adaptersOf(rows) {
|
|
84
44
|
return [...new Set(rows.map((r) => r.adapter))].sort();
|
|
85
45
|
}
|
|
86
|
-
/**
|
|
87
|
-
* The projects actually present, as `{ key, label }` for a filter control —
|
|
88
|
-
* derived like {@link adaptersOf}, and paired because the two halves differ:
|
|
89
|
-
* the *key* is what {@link ViewConfig.projects} holds (gateway-qualified root,
|
|
90
|
-
* so a rename regroups nothing) and the *label* is what a person picks by.
|
|
91
|
-
*
|
|
92
|
-
* Sorted by label, deduped by key. Two projects with the same name on two
|
|
93
|
-
* gateways therefore stay two entries wearing one word — which is honest: they
|
|
94
|
-
* really are two different directories, and the alternative is a filter that
|
|
95
|
-
* silently selects both.
|
|
96
|
-
*/
|
|
97
46
|
function projectsOf(rows) {
|
|
98
47
|
const byKey = /* @__PURE__ */ new Map();
|
|
99
48
|
for (const row of rows) byKey.set(projectKey(row), projectLabel(row));
|
|
@@ -105,77 +54,24 @@ function projectsOf(rows) {
|
|
|
105
54
|
function sessionLabel(info) {
|
|
106
55
|
return info.title ?? info.id.slice(0, 8);
|
|
107
56
|
}
|
|
108
|
-
/**
|
|
109
|
-
* The project facet's grouping key: gateway id + the project root, falling
|
|
110
|
-
* back to the session's cwd when no project is declared.
|
|
111
|
-
*
|
|
112
|
-
* The root and not the name, because a name is not a key (two repos can both
|
|
113
|
-
* be called "api", and a rename must regroup nothing); qualified by gateway,
|
|
114
|
-
* because a remote gateway's identical-looking path is another machine's
|
|
115
|
-
* directory — the same rule `ScopeRoot` states. The cwd fallback is what makes
|
|
116
|
-
* grouping by project useful before anyone has written a `.workerdeck.json`:
|
|
117
|
-
* undeclared sessions group by their folder, declared ones by their root, and
|
|
118
|
-
* a session in `packages/ui` joins its repo's group the moment the file
|
|
119
|
-
* exists. Sessions with no cwd at all (a filesystem-less engine) share one
|
|
120
|
-
* per-gateway bucket — see {@link projectLabel}.
|
|
121
|
-
*/
|
|
122
57
|
function projectKey(row) {
|
|
123
58
|
return `${row.hostId}:${normalizePath(row.info.project?.root ?? row.info.cwd)}`;
|
|
124
59
|
}
|
|
125
|
-
/**
|
|
126
|
-
* What a project group (or a row's project slot) is called: the declared name,
|
|
127
|
-
* else the cwd's basename — the exact string clients rendered before this
|
|
128
|
-
* feature existed, so an undeclared project looks like today. 'No project' is
|
|
129
|
-
* only ever the no-cwd case (a sandboxed provider session), where there is no
|
|
130
|
-
* folder to name.
|
|
131
|
-
*
|
|
132
|
-
* Takes only the `info` it reads, so a surface holding a bare `SessionInfo` —
|
|
133
|
-
* a row component, an iOS cell — can call it without inventing the rest of a
|
|
134
|
-
* `SessionRow`. That matters more than it looks: this string is what a client
|
|
135
|
-
* renders *in place of* the cwd basename it used to draw, and two spellings of
|
|
136
|
-
* it would put the list and its group headers on different names.
|
|
137
|
-
*/
|
|
138
60
|
function projectLabel(row) {
|
|
139
61
|
const name = row.info.project?.name;
|
|
140
62
|
if (name) return name;
|
|
141
63
|
const dir = normalizePath(row.info.cwd);
|
|
142
64
|
return dir.slice(dir.lastIndexOf("/") + 1) || "No project";
|
|
143
65
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Where inside its project a session actually sits — the cwd with the project
|
|
146
|
-
* root taken off the front, or `undefined` when it sits at the root, has no
|
|
147
|
-
* declared project, or has no cwd at all.
|
|
148
|
-
*
|
|
149
|
-
* The companion to {@link projectLabel}, and it exists for one situation: a list
|
|
150
|
-
* **grouped by project**. There the header has already said the project's name,
|
|
151
|
-
* so repeating it on every row spends the row's most valuable line on the one
|
|
152
|
-
* fact the reader already has. What the header cannot say is which *part* of the
|
|
153
|
-
* project a session is working in, and two sessions in the same repo are told
|
|
154
|
-
* apart by exactly that.
|
|
155
|
-
*
|
|
156
|
-
* Undefined is the honest answer for a session at the project root, and callers
|
|
157
|
-
* must render nothing rather than a `.` or a repeated name — the slot simply
|
|
158
|
-
* goes away, which is the point.
|
|
159
|
-
*/
|
|
160
66
|
function projectSubpath(row) {
|
|
161
67
|
const root = row.info.project?.root;
|
|
162
|
-
if (root === void 0 || !row.info.cwd) return
|
|
68
|
+
if (root === void 0 || !row.info.cwd) return;
|
|
163
69
|
const base = normalizePath(root);
|
|
164
70
|
const dir = normalizePath(row.info.cwd);
|
|
165
|
-
if (dir === base) return
|
|
166
|
-
if (!dir.startsWith(`${base}/`)) return
|
|
71
|
+
if (dir === base) return;
|
|
72
|
+
if (!dir.startsWith(`${base}/`)) return;
|
|
167
73
|
return dir.slice(base.length + 1) || void 0;
|
|
168
74
|
}
|
|
169
|
-
/**
|
|
170
|
-
* This session is a job run — the queue created it, and `JobInfo.sessionId`
|
|
171
|
-
* points at it.
|
|
172
|
-
*
|
|
173
|
-
* A job run is an ordinary registry session in every other respect, which is
|
|
174
|
-
* what makes this worth spelling once: a client that renders jobs on their own
|
|
175
|
-
* surface should not list them again among the sessions, and a client with no
|
|
176
|
-
* jobs surface (the extension, the phone) should, or they would be invisible.
|
|
177
|
-
* The queue stamps `meta.jobId`; nothing else may write that key.
|
|
178
|
-
*/
|
|
179
75
|
function isJobRun(info) {
|
|
180
76
|
return typeof info.meta?.jobId === "string";
|
|
181
77
|
}
|
|
@@ -183,8 +79,6 @@ function matchesSearch(row, needle) {
|
|
|
183
79
|
if (!needle) return true;
|
|
184
80
|
return sessionLabel(row.info).toLowerCase().includes(needle) || row.info.cwd.toLowerCase().includes(needle) || (row.info.project?.name.toLowerCase().includes(needle) ?? false) || row.hostName.toLowerCase().includes(needle) || row.adapter.toLowerCase().includes(needle) || row.info.id.startsWith(needle);
|
|
185
81
|
}
|
|
186
|
-
/** Trailing separators dropped and separators unified, so containment is a
|
|
187
|
-
* plain prefix test on both a posix and a Windows gateway. */
|
|
188
82
|
function normalizePath(path) {
|
|
189
83
|
return path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
190
84
|
}
|
|
@@ -193,17 +87,9 @@ function isWithin(root, path) {
|
|
|
193
87
|
const dir = normalizePath(path);
|
|
194
88
|
return dir === base || dir.startsWith(`${base}/`);
|
|
195
89
|
}
|
|
196
|
-
/**
|
|
197
|
-
* Is this session inside one of the host's folders? A gateway-tagged root only
|
|
198
|
-
* ever matches its own gateway; an untagged one only matches a loopback gateway,
|
|
199
|
-
* because a remote gateway's identical-looking path is a different machine's
|
|
200
|
-
* directory.
|
|
201
|
-
*/
|
|
202
90
|
function inScope(row, scope) {
|
|
203
91
|
return scope.roots.some((root) => (root.hostId ? root.hostId.toLowerCase() === row.hostId.toLowerCase() : row.local) && isWithin(root.path, row.info.cwd));
|
|
204
92
|
}
|
|
205
|
-
/** Whether the scope filter is actually hiding anything — it is inert with no
|
|
206
|
-
* folder open, and that is the difference between a default and a filter. */
|
|
207
93
|
function scopeActive(config, scope) {
|
|
208
94
|
return config.scoped && scope !== void 0;
|
|
209
95
|
}
|
|
@@ -218,24 +104,18 @@ function facetKey(row, facet) {
|
|
|
218
104
|
function facetLabel(row, facet) {
|
|
219
105
|
return facet === "gateway" ? row.hostName : facet === "adapter" ? row.adapter : facet === "project" ? projectLabel(row) : STATE_LABELS[row.state];
|
|
220
106
|
}
|
|
221
|
-
/** Comparable rank for a facet: states run worst-first (attention before ended),
|
|
222
|
-
* the rest alphabetically by their visible label. */
|
|
223
107
|
function facetRank(row, facet) {
|
|
224
108
|
if (facet === "state") return String(STATE_ORDER.indexOf(row.state));
|
|
225
109
|
return facetLabel(row, facet).toLowerCase();
|
|
226
110
|
}
|
|
227
|
-
|
|
111
|
+
function byRecency(a, b) {
|
|
112
|
+
return (b.info.lastActivityAt ?? b.info.createdAt) - (a.info.lastActivityAt ?? a.info.createdAt);
|
|
113
|
+
}
|
|
228
114
|
function compare(a, b, sortBy) {
|
|
229
115
|
if (sortBy === "recent") return byRecency(a, b);
|
|
230
116
|
if (sortBy === "name") return sessionLabel(a.info).localeCompare(sessionLabel(b.info), void 0, { sensitivity: "base" }) || byRecency(a, b);
|
|
231
117
|
return facetRank(a, sortBy).localeCompare(facetRank(b, sortBy)) || byRecency(a, b);
|
|
232
118
|
}
|
|
233
|
-
/**
|
|
234
|
-
* The list as rendered: filtered, grouped, and sorted within each group. Groups
|
|
235
|
-
* themselves come out in the sort's own order — grouping by state and sorting by
|
|
236
|
-
* name should still put "Needs attention" first, so groups are ordered by their
|
|
237
|
-
* facet rank, never by the row sort.
|
|
238
|
-
*/
|
|
239
119
|
function groupRows(rows, config) {
|
|
240
120
|
const sorted = [...rows].sort((a, b) => compare(a, b, config.sortBy));
|
|
241
121
|
if (config.groupBy === "none") return sorted.length ? [{
|
|
@@ -258,7 +138,7 @@ function groupRows(rows, config) {
|
|
|
258
138
|
return [...groups.values()].sort((a, b) => a.rank.localeCompare(b.rank));
|
|
259
139
|
}
|
|
260
140
|
function subsetSummary(config, scope, shown, total) {
|
|
261
|
-
if (shown >= total) return
|
|
141
|
+
if (shown >= total) return;
|
|
262
142
|
const causes = [];
|
|
263
143
|
if (scope && scopeActive(config, scope)) causes.push(scope.label);
|
|
264
144
|
const facets = (config.gateways.length ? 1 : 0) + (config.adapters.length ? 1 : 0) + (config.states.length ? 1 : 0) + (config.projects?.length ? 1 : 0);
|
|
@@ -270,19 +150,9 @@ function subsetSummary(config, scope, shown, total) {
|
|
|
270
150
|
causes
|
|
271
151
|
};
|
|
272
152
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Is anything OTHER than the workspace scope narrowing the list?
|
|
275
|
-
*
|
|
276
|
-
* The distinction an empty list turns on: "this project has no sessions" wants a
|
|
277
|
-
* different sentence, and a different way out, from "your filters match none".
|
|
278
|
-
* Scope is excluded because it is on by default — it is the state, not a choice
|
|
279
|
-
* someone made.
|
|
280
|
-
*/
|
|
281
153
|
function hasFacetFilter(config) {
|
|
282
154
|
return config.search.trim().length > 0 || config.gateways.length > 0 || config.adapters.length > 0 || config.states.length > 0 || (config.projects?.length ?? 0) > 0;
|
|
283
155
|
}
|
|
284
|
-
/** "Show me everything": every filter off, including scope. The group/sort
|
|
285
|
-
* choices are a layout preference and survive. */
|
|
286
156
|
function clearFilters(config) {
|
|
287
157
|
return {
|
|
288
158
|
...DEFAULT_VIEW_CONFIG,
|
|
@@ -293,28 +163,6 @@ function clearFilters(config) {
|
|
|
293
163
|
}
|
|
294
164
|
//#endregion
|
|
295
165
|
//#region src/usage.ts
|
|
296
|
-
/**
|
|
297
|
-
* The usage a client should render: the gateway's per-profile state where it has
|
|
298
|
-
* the window, this session's own reading where it does not.
|
|
299
|
-
*
|
|
300
|
-
* Why the profile wins outright rather than by comparing timestamps: the
|
|
301
|
-
* gateway's `ProfileUsageTracker` is fed from **every** session on the profile —
|
|
302
|
-
* including this one, from seq 0 — and keeps the newest reading per window by
|
|
303
|
-
* the event's own `ts`. So for any window it holds, it holds a reading at least
|
|
304
|
-
* as new as the one in this transcript, and a timestamp comparison could only
|
|
305
|
-
* ever go wrong: the reducer keeps a *single* `updatedAt` for the whole map, so
|
|
306
|
-
* a `five_hour` reading from this morning is dated with the afternoon's
|
|
307
|
-
* `seven_day` event and would beat a genuinely fresher profile entry.
|
|
308
|
-
*
|
|
309
|
-
* The session half is not a fallback for correctness but for *coverage*: the
|
|
310
|
-
* profile map is in-memory, so a restarted gateway serves nothing until a
|
|
311
|
-
* session reports again, and a session with no profile has no account state at
|
|
312
|
-
* all. In both cases the transcript's reading is the only one there is, and it
|
|
313
|
-
* is dated honestly (see {@link SessionUsage.updatedAt}) rather than as now.
|
|
314
|
-
*
|
|
315
|
-
* Absent stays absent throughout: a window nobody has reported is **unknown,
|
|
316
|
-
* never 0%**, and this returns an empty map rather than inventing entries.
|
|
317
|
-
*/
|
|
318
166
|
function mergeUsage(session, profile) {
|
|
319
167
|
const out = {};
|
|
320
168
|
for (const [key, info] of Object.entries(session.rateLimits ?? {})) out[key] = {
|
|
@@ -324,21 +172,6 @@ function mergeUsage(session, profile) {
|
|
|
324
172
|
for (const [key, window] of Object.entries(profile ?? {})) out[key] = window;
|
|
325
173
|
return out;
|
|
326
174
|
}
|
|
327
|
-
/**
|
|
328
|
-
* The windows in reading order: the session window, the weekly one, then the
|
|
329
|
-
* per-model weeklies alphabetically.
|
|
330
|
-
*
|
|
331
|
-
* Discovered rather than hardcoded — the engine's set of windows is an open
|
|
332
|
-
* union and has grown before — but ordered, so the first two always mean the
|
|
333
|
-
* same thing wherever they are drawn. A window with no `utilization` is
|
|
334
|
-
* **unknown, not zero**, and is dropped entirely rather than rendered as an
|
|
335
|
-
* empty bar that reads as "plenty left".
|
|
336
|
-
*
|
|
337
|
-
* Here rather than in a client because two surfaces now render the same windows
|
|
338
|
-
* from different sources — the session panel from its merged state, the
|
|
339
|
-
* dashboard's profile page straight off `ProfileInfo.usage` — and a list that
|
|
340
|
-
* ordered or filtered differently would be the same account described two ways.
|
|
341
|
-
*/
|
|
342
175
|
function orderUsageWindows(usage) {
|
|
343
176
|
const all = Object.entries(usage ?? {}).filter(([, w]) => w.info.utilization !== void 0).map(([key, w]) => ({
|
|
344
177
|
key,
|
|
@@ -350,23 +183,19 @@ function orderUsageWindows(usage) {
|
|
|
350
183
|
const perModel = all.filter((w) => w.key.startsWith("seven_day_")).sort((a, b) => a.key.localeCompare(b.key));
|
|
351
184
|
return [...named, ...perModel];
|
|
352
185
|
}
|
|
353
|
-
/** The flat `rateLimitType → reading` map every existing renderer takes, out of
|
|
354
|
-
* the dated form. Undefined in, undefined out — so a surface can keep telling
|
|
355
|
-
* "no reading" apart from "an empty one". */
|
|
356
186
|
function usageInfos(usage) {
|
|
357
|
-
if (!usage) return
|
|
187
|
+
if (!usage) return;
|
|
358
188
|
const out = {};
|
|
359
189
|
for (const [key, window] of Object.entries(usage)) out[key] = window.info;
|
|
360
190
|
return out;
|
|
361
191
|
}
|
|
362
192
|
//#endregion
|
|
363
193
|
//#region src/watermarks.ts
|
|
364
|
-
/** Entries older than this are dropped on write — a session deleted months ago
|
|
365
|
-
* should not keep a row in storage forever. */
|
|
366
194
|
const MAX_AGE_MS = 720 * 60 * 60 * 1e3;
|
|
367
|
-
/** How stale "last here" is allowed to get before a write happens anyway. */
|
|
368
195
|
const TOUCH_MS = 6e4;
|
|
369
|
-
|
|
196
|
+
function watermarkKey(hostId, sessionId) {
|
|
197
|
+
return `${hostId}:${sessionId}`;
|
|
198
|
+
}
|
|
370
199
|
var Watermarks = class {
|
|
371
200
|
#store;
|
|
372
201
|
#cache;
|
|
@@ -377,35 +206,24 @@ var Watermarks = class {
|
|
|
377
206
|
get(hostId, sessionId) {
|
|
378
207
|
return this.#cache[watermarkKey(hostId, sessionId)];
|
|
379
208
|
}
|
|
380
|
-
/** Every mark, for a caller deriving unread counts over a whole list. */
|
|
381
209
|
all() {
|
|
382
210
|
return this.#cache;
|
|
383
211
|
}
|
|
384
|
-
/**
|
|
385
|
-
* Record what is on screen now. Monotonic on purpose: a transcript that
|
|
386
|
-
* *shrank* (a compaction, a fresh attach mid-replay) must not walk the mark
|
|
387
|
-
* backwards and resurrect rows the user already read.
|
|
388
|
-
*
|
|
389
|
-
* Returns whether the mark actually moved, because an unread badge is computed
|
|
390
|
-
* from it and nothing else will say so: rows read in a panel do not touch the
|
|
391
|
-
* sessions poll, so a caller that doesn't hear about this has no other way to
|
|
392
|
-
* learn the count is now wrong.
|
|
393
|
-
*/
|
|
394
212
|
mark(hostId, sessionId, seen, now = Date.now()) {
|
|
395
213
|
const id = watermarkKey(hostId, sessionId);
|
|
396
214
|
const previous = this.#cache[id];
|
|
397
215
|
const next = {
|
|
398
216
|
itemCount: Math.max(previous?.itemCount ?? 0, seen.itemCount ?? 0),
|
|
399
217
|
activity: Math.max(previous?.activity ?? 0, seen.activity ?? 0),
|
|
218
|
+
prose: seen.prose === void 0 ? previous?.prose : Math.max(previous?.prose ?? 0, seen.prose),
|
|
400
219
|
turns: Math.max(previous?.turns ?? 0, seen.turns ?? 0),
|
|
401
220
|
seenAt: now
|
|
402
221
|
};
|
|
403
|
-
if (previous && previous.itemCount === next.itemCount && previous.activity === next.activity && previous.turns === next.turns && next.seenAt - previous.seenAt < TOUCH_MS) return false;
|
|
222
|
+
if (previous && previous.itemCount === next.itemCount && previous.activity === next.activity && previous.prose === next.prose && previous.turns === next.turns && next.seenAt - previous.seenAt < TOUCH_MS) return false;
|
|
404
223
|
this.#cache[id] = next;
|
|
405
224
|
this.#store.write(this.#prune(now));
|
|
406
225
|
return true;
|
|
407
226
|
}
|
|
408
|
-
/** Forget a session — it was deleted, and its mark is now noise. */
|
|
409
227
|
forget(hostId, sessionId) {
|
|
410
228
|
const id = watermarkKey(hostId, sessionId);
|
|
411
229
|
if (!(id in this.#cache)) return;
|
|
@@ -419,78 +237,28 @@ var Watermarks = class {
|
|
|
419
237
|
}
|
|
420
238
|
};
|
|
421
239
|
/**
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
* (five tool calls in one turn is one turn) and a stream sequence overcounts
|
|
426
|
-
* absurdly (every delta). Turns stay the fallback for a gateway too old to
|
|
427
|
-
* report it.
|
|
428
|
-
*
|
|
429
|
-
* A session never visited returns 0 — "never opened" is not "unread", and a
|
|
430
|
-
* badge that counted every session's whole history on first launch would be
|
|
431
|
-
* noise on the one day it should be quiet.
|
|
240
|
+
* The badge's number, in the best unit the pair can agree on: prose the human has not
|
|
241
|
+
* read, else rows, else turns. The ladder is what keeps an older gateway (no `proseCount`
|
|
242
|
+
* on the wire) badging exactly as it did before rather than going silent.
|
|
432
243
|
*/
|
|
433
244
|
function unseenCount(mark, info) {
|
|
434
245
|
if (!mark) return 0;
|
|
246
|
+
if (info.proseCount !== void 0) return Math.max(0, info.proseCount - (mark.prose ?? info.proseCount));
|
|
435
247
|
if (info.activityCount !== void 0) return Math.max(0, info.activityCount - mark.activity);
|
|
436
248
|
return Math.max(0, (info.turns ?? 0) - mark.turns);
|
|
437
249
|
}
|
|
438
250
|
//#endregion
|
|
439
251
|
//#region src/index.ts
|
|
440
|
-
|
|
441
|
-
* @workerdeck/protocol — the wire protocol between a workerdeck server and its clients.
|
|
442
|
-
*
|
|
443
|
-
* One session = one ordered stream of {@link SessionEvent}s (each stamped with a monotonically
|
|
444
|
-
* increasing `seq`) plus a small command set ({@link SessionCommand}). Clients attach over
|
|
445
|
-
* WebSocket, optionally replaying from a known `seq`, and drive the session with commands.
|
|
446
|
-
*
|
|
447
|
-
* This package is dependency-free and browser-safe. Anthropic API message content is modeled
|
|
448
|
-
* structurally (see {@link ApiMessage}) so clients don't need the Agent SDK to render transcripts.
|
|
449
|
-
*/
|
|
450
|
-
/** Bumped on any breaking change to events, commands, or REST shapes. */
|
|
451
|
-
const PROTOCOL_VERSION = 7;
|
|
452
|
-
/**
|
|
453
|
-
* How much of a tool result a truncating replay keeps.
|
|
454
|
-
*
|
|
455
|
-
* Chosen against the two clients' *own* budgets, and the relationship is the
|
|
456
|
-
* whole point: the terminal theme shows ~400 characters collapsed and ~2,000
|
|
457
|
-
* open, so at 8,000 the collapsed and open states are **byte-identical to an
|
|
458
|
-
* untruncated attach** and only the uncapped "show everything" press ever
|
|
459
|
-
* fetches. That collapses the entire feature to one press, and it is asserted
|
|
460
|
-
* in a test rather than trusted — lowered below the open budget, this would
|
|
461
|
-
* silently clip the open state with no marker, which is the one failure this
|
|
462
|
-
* design must not have.
|
|
463
|
-
*
|
|
464
|
-
* Measured justification: on one 1,270-row session three `tool_result` frames
|
|
465
|
-
* were 641 / 463 / 396 KB, 68% of a 3.1 MB attach. The cut is *structural* —
|
|
466
|
-
* proportional to the thing that is actually large, wherever in the log it sits
|
|
467
|
-
* — which a row window is not.
|
|
468
|
-
*/
|
|
252
|
+
const PROTOCOL_VERSION = 1;
|
|
469
253
|
const TOOL_RESULT_HEAD_CHARS = 8e3;
|
|
470
|
-
/** How many bytes a base64 payload decodes to, without decoding it. */
|
|
471
254
|
function base64Bytes(data) {
|
|
472
255
|
const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
|
|
473
256
|
return Math.max(0, Math.floor(data.length * 3 / 4) - padding);
|
|
474
257
|
}
|
|
475
|
-
/**
|
|
476
|
-
* Project one `tool_result` content part onto its {@link ImageRefPart}, or
|
|
477
|
-
* `undefined` when the part is not a base64 image and must be delivered as it
|
|
478
|
-
* stands.
|
|
479
|
-
*
|
|
480
|
-
* The rule's **one spelling**, shared by the transform that replaces parts
|
|
481
|
-
* (core), the route that serves them back (server) and the property test that
|
|
482
|
-
* proves the fold is otherwise unchanged (react) — the same reason every other
|
|
483
|
-
* member of this family lives here rather than in whichever package applies it.
|
|
484
|
-
*
|
|
485
|
-
* Deliberately narrow. The corpus holds exactly two non-text part kinds: this
|
|
486
|
-
* one, and the CLI's `tool_reference`, of which every instance across 214
|
|
487
|
-
* sessions totals 122 KB. A "drop non-text parts" rule would sweep those in for
|
|
488
|
-
* no measurable gain, and narrowness is this family's standing habit.
|
|
489
|
-
*/
|
|
490
258
|
function imagePartRef(part, index) {
|
|
491
|
-
if (part.type !== "image") return
|
|
259
|
+
if (part.type !== "image") return;
|
|
492
260
|
const source = part.source;
|
|
493
|
-
if (!source || source.type !== "base64" || typeof source.data !== "string") return
|
|
261
|
+
if (!source || source.type !== "base64" || typeof source.data !== "string") return;
|
|
494
262
|
return {
|
|
495
263
|
type: "image_ref",
|
|
496
264
|
media_type: typeof source.media_type === "string" ? source.media_type : "application/octet-stream",
|
|
@@ -498,14 +266,6 @@ function imagePartRef(part, index) {
|
|
|
498
266
|
part_index: index
|
|
499
267
|
};
|
|
500
268
|
}
|
|
501
|
-
/**
|
|
502
|
-
* The static capability record of each engine — the browser-safe default for
|
|
503
|
-
* `ProfileInfo.capabilities` / `SessionInfo.capabilities`, and the single place
|
|
504
|
-
* the values are written down. Core's adapters *reference* this record and a
|
|
505
|
-
* conformance test compares runner behaviour against it, so it cannot silently
|
|
506
|
-
* diverge from the code. When both a wire copy and this default exist, the wire
|
|
507
|
-
* copy wins.
|
|
508
|
-
*/
|
|
509
269
|
const ENGINE_CAPABILITIES = {
|
|
510
270
|
claude: {
|
|
511
271
|
interactiveApprovals: true,
|
|
@@ -612,40 +372,12 @@ const ENGINE_CAPABILITIES = {
|
|
|
612
372
|
streaming: "token"
|
|
613
373
|
}
|
|
614
374
|
};
|
|
615
|
-
/**
|
|
616
|
-
* Permission modes the model-agnostic provider engine understands.
|
|
617
|
-
* @deprecated Read `ENGINE_CAPABILITIES.provider.permissionModes` (this is an
|
|
618
|
-
* alias of it, kept for protocol-5 consumers).
|
|
619
|
-
*/
|
|
620
|
-
const PROVIDER_PERMISSION_MODES = ENGINE_CAPABILITIES.provider.permissionModes;
|
|
621
|
-
/**
|
|
622
|
-
* Whether a profile's engine can run a permission mode. The single source of
|
|
623
|
-
* truth for the restriction: create forms filter what they offer with it, the
|
|
624
|
-
* gateway rejects with it. An absent `engine` means 'claude' (every mode).
|
|
625
|
-
*/
|
|
626
375
|
function supportsPermissionMode(engine, mode) {
|
|
627
376
|
return ENGINE_CAPABILITIES[engine ?? "claude"].permissionModes.includes(mode);
|
|
628
377
|
}
|
|
629
|
-
/**
|
|
630
|
-
* How many *settled* sub-agents {@link SessionInfo.subagents} keeps behind the
|
|
631
|
-
* running ones. Small on purpose: the point of the tail is that a list row does
|
|
632
|
-
* not go blank the instant a run finishes, not that it is a history.
|
|
633
|
-
*/
|
|
634
378
|
const SUBAGENT_HISTORY = 8;
|
|
635
|
-
/**
|
|
636
|
-
* The list-sized context reading an event carries, or `undefined` for the events
|
|
637
|
-
* that carry none — the rule behind {@link SessionInfo.contextUsage}.
|
|
638
|
-
*
|
|
639
|
-
* Here rather than in each runner for the same reason {@link transcriptActivity}
|
|
640
|
-
* is: it is one rule both sides have to agree on, and three copies of "which
|
|
641
|
-
* events move the reading" is three chances to disagree. Runners fold it in
|
|
642
|
-
* their emit path; **clearing on `conversation_reset` is the caller's half** —
|
|
643
|
-
* this function answers "what does this event say the reading is", and a reset
|
|
644
|
-
* says nothing about the window, it retires the conversation the window
|
|
645
|
-
* described.
|
|
646
|
-
*/
|
|
647
379
|
function contextReading(body) {
|
|
648
|
-
if (body.type !== "context_usage") return
|
|
380
|
+
if (body.type !== "context_usage") return;
|
|
649
381
|
const { totalTokens, maxTokens, percentage } = body.usage;
|
|
650
382
|
return {
|
|
651
383
|
totalTokens,
|
|
@@ -653,21 +385,6 @@ function contextReading(body) {
|
|
|
653
385
|
percentage
|
|
654
386
|
};
|
|
655
387
|
}
|
|
656
|
-
/**
|
|
657
|
-
* How many transcript rows an event materializes — the unit behind
|
|
658
|
-
* {@link SessionInfo.activityCount}.
|
|
659
|
-
*
|
|
660
|
-
* Deliberately the *reducer's* rule (`@workerdeck/react`'s `transcript.ts`), not
|
|
661
|
-
* a server-side approximation: one row per content block of an assistant
|
|
662
|
-
* message (a text, a thought, each tool call), one for a user message, one per
|
|
663
|
-
* turn result, delivered file or error. Everything else — status changes, usage
|
|
664
|
-
* readings, stream deltas, permission bookkeeping — is state, not a row, and
|
|
665
|
-
* counts zero.
|
|
666
|
-
*
|
|
667
|
-
* It lives in `protocol` because both sides need it and neither may import the
|
|
668
|
-
* other: the runners count with it, and any client compares the totals. If the
|
|
669
|
-
* reducer's row rule changes, change this with it.
|
|
670
|
-
*/
|
|
671
388
|
function transcriptActivity(body) {
|
|
672
389
|
if ("parentToolUseId" in body && body.parentToolUseId != null) return 0;
|
|
673
390
|
switch (body.type) {
|
|
@@ -684,33 +401,40 @@ function transcriptActivity(body) {
|
|
|
684
401
|
}
|
|
685
402
|
}
|
|
686
403
|
/**
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
* `
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
* `
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* `
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
* strictly-below), which is what clears a reconnecting client that still holds
|
|
706
|
-
* pre-reset rows.
|
|
707
|
-
*
|
|
708
|
-
* Lives here beside {@link transcriptActivity} for the same reason: the
|
|
709
|
-
* reducer owns the rule and the runners filter with it, and the two sides may
|
|
710
|
-
* not import each other. If the reducer's items-mutating set changes, change
|
|
711
|
-
* this with it. Unknown/future event types are NOT content — the safe failure
|
|
712
|
-
* is replaying a stale row, never withholding state.
|
|
404
|
+
* The unread badge's unit: output **addressed to the human**, not evidence of work.
|
|
405
|
+
*
|
|
406
|
+
* `transcriptActivity` counts a tool call and a paragraph alike, which is honest as
|
|
407
|
+
* "how much has happened" and wrong as "how much is there to read" — a session that
|
|
408
|
+
* tool-loops for a minute ticks 6, 7, 8 with nothing said yet. This scores the same
|
|
409
|
+
* events through a narrower door:
|
|
410
|
+
*
|
|
411
|
+
* - assistant `text` blocks only — `thinking` is not addressed to anyone and `tool_use`
|
|
412
|
+
* is the noise being filtered out;
|
|
413
|
+
* - the **sub-agent carve-out is inherited** (`parentToolUseId != null` scores 0): prose a
|
|
414
|
+
* sub-agent wrote to its parent is not addressed to the human either;
|
|
415
|
+
* - a `turn_result` counts only when it **failed**, an interrupt or an error being a thing
|
|
416
|
+
* the human is owed; a successful turn already carried its own prose and would otherwise
|
|
417
|
+
* double-count every answer;
|
|
418
|
+
* - `session_error` and `file_delivered` count — both are output, not work;
|
|
419
|
+
* - `stream_delta` scores 0, exactly as in `transcriptActivity`. The badge is therefore
|
|
420
|
+
* correct within one poll of a message *completing*, never mid-stream, which is the
|
|
421
|
+
* deliberate price of leaving the streaming path alone.
|
|
713
422
|
*/
|
|
423
|
+
function transcriptProse(body) {
|
|
424
|
+
if ("parentToolUseId" in body && body.parentToolUseId != null) return 0;
|
|
425
|
+
switch (body.type) {
|
|
426
|
+
case "assistant_message": {
|
|
427
|
+
const content = body.message.content;
|
|
428
|
+
if (typeof content === "string") return content.trim() === "" ? 0 : 1;
|
|
429
|
+
return content.filter((block) => block.type === "text" && typeof block.text === "string" && block.text.trim() !== "").length;
|
|
430
|
+
}
|
|
431
|
+
case "user_message": return body.synthetic ? 0 : 1;
|
|
432
|
+
case "turn_result": return body.isError ? 1 : 0;
|
|
433
|
+
case "file_delivered":
|
|
434
|
+
case "session_error": return 1;
|
|
435
|
+
default: return 0;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
714
438
|
function transcriptContent(body) {
|
|
715
439
|
switch (body.type) {
|
|
716
440
|
case "user_message":
|
|
@@ -727,50 +451,6 @@ function transcriptContent(body) {
|
|
|
727
451
|
default: return false;
|
|
728
452
|
}
|
|
729
453
|
}
|
|
730
|
-
/**
|
|
731
|
-
* The dedupe key for an event that is **last-write-wins** on replay, or
|
|
732
|
-
* `undefined` for one that must always be delivered.
|
|
733
|
-
*
|
|
734
|
-
* The problem: the runner polls context usage and the plan's rate limits after
|
|
735
|
-
* every turn, so a fifty-turn session's log holds fifty context readings and
|
|
736
|
-
* fifty per rate-limit window. Replaying all of them is not merely wasteful —
|
|
737
|
-
* it is *visible*. A client applies each in turn, so opening a session shows
|
|
738
|
-
* the usage meters counting up from the session's first reading to its last
|
|
739
|
-
* over the length of the replay, announcing history as if it were news.
|
|
740
|
-
*
|
|
741
|
-
* The fix is a backwards scan over the buffered log keeping the first
|
|
742
|
-
* occurrence of each key, which is `staleReplaySeqs` in `@workerdeck/core`.
|
|
743
|
-
* The key is per *window* for rate limits, not per event type: the reducer
|
|
744
|
-
* stores them keyed by window ("so five_hour and seven_day updates don't
|
|
745
|
-
* clobber each other"), so a single key would keep only the most recently
|
|
746
|
-
* polled window and silently drop the others.
|
|
747
|
-
*
|
|
748
|
-
* **This is a claim about the reducer**, which is why it lives here rather
|
|
749
|
-
* than in core: only the server coalesces, but only `@workerdeck/react` can
|
|
750
|
-
* prove the rule correct, and neither package may import the other. The
|
|
751
|
-
* property that must hold is that coalescing is *unobservable* — folding the
|
|
752
|
-
* full log and the coalesced log through `applyEvent` yields identical state.
|
|
753
|
-
* `packages/react/test/replay-coalesce.test.ts` asserts exactly that, over
|
|
754
|
-
* every event kind. Extend the rule only with a case that test still passes.
|
|
755
|
-
*
|
|
756
|
-
* Three kinds are deliberately **excluded** despite looking eligible:
|
|
757
|
-
*
|
|
758
|
-
* - `capabilities` — `defaultModel: event.defaultModel ?? base.defaultModel`
|
|
759
|
-
* is a fallback *merge*, so a later event without one would erase an earlier
|
|
760
|
-
* event's. (It is also emitted once per session, so there is nothing to win.)
|
|
761
|
-
* - `model_changed` — `undefined` means "reset to the server default" and the
|
|
762
|
-
* reducer *keeps* the last known model, so the last event alone is not the
|
|
763
|
-
* same as the fold.
|
|
764
|
-
* - `system_init` — pure replace for the reducer, but the server's
|
|
765
|
-
* `watchAuthSource` reads the **first** one to decide an auth policy, and
|
|
766
|
-
* parking treats each as a resume point.
|
|
767
|
-
*
|
|
768
|
-
* Coalescing never drops the highest-seq event, and that is load-bearing
|
|
769
|
-
* rather than incidental: the globally-last event is by definition the last of
|
|
770
|
-
* its own key, so it always survives. `useClaudeSession`'s replay hold waits
|
|
771
|
-
* for `state.lastSeq` to reach the attach's `session.lastSeq`, and would hang
|
|
772
|
-
* on a blank panel forever if a coalescer could swallow the final event.
|
|
773
|
-
*/
|
|
774
454
|
function replayCoalesceKey(body) {
|
|
775
455
|
switch (body.type) {
|
|
776
456
|
case "context_usage": return "context_usage";
|
|
@@ -780,101 +460,16 @@ function replayCoalesceKey(body) {
|
|
|
780
460
|
default: return;
|
|
781
461
|
}
|
|
782
462
|
}
|
|
783
|
-
/**
|
|
784
|
-
* Does a **replay** have to deliver this event, or may it be dropped outright?
|
|
785
|
-
*
|
|
786
|
-
* The fifth of the family, and the closest relative of {@link snapshotRetains} —
|
|
787
|
-
* the same claim ("no client can tell") pointed at the wire instead of at a
|
|
788
|
-
* store. The difference from {@link replayCoalesceKey} is that this is not
|
|
789
|
-
* last-write-wins: there is nothing to keep. These are events the reducer reads
|
|
790
|
-
* and *discards*, so a replay that sends them is spending the reader's network
|
|
791
|
-
* on frames whose whole effect is `return base`.
|
|
792
|
-
*
|
|
793
|
-
* Today that is exactly one thing, and it is the second-largest item in a real
|
|
794
|
-
* attach: the `stream_delta`s the reducer does not model. Measured over one
|
|
795
|
-
* 1,270-row session, the delta run was 774 KB, and **~85% of it was frames the
|
|
796
|
-
* reducer throws away** — `input_json_delta` (a tool call's arguments, streamed
|
|
797
|
-
* character by character, 383 KB), `signature_delta` (encrypted-thinking
|
|
798
|
-
* signatures, 153 KB) and the `message_start`/`content_block_start`/`_stop`
|
|
799
|
-
* scaffolding (244 KB). The reducer models two delta kinds, `text_delta` and
|
|
800
|
-
* `thinking_delta`; everything else falls through its switch untouched.
|
|
801
|
-
*
|
|
802
|
-
* What is deliberately **not** dropped, though the arithmetic would allow it:
|
|
803
|
-
*
|
|
804
|
-
* - `thinking_delta` — the Claude SDK delivers thinking blocks whose `thinking`
|
|
805
|
-
* is `''`, and the reducer backfills them from the accumulated streamed text
|
|
806
|
-
* (`streamedThinking`). Dropping these erases every thought from a replayed
|
|
807
|
-
* transcript. This is the same carve-out `snapshotRetains` documents, and it
|
|
808
|
-
* is the reason that rule is provider-engine-only.
|
|
809
|
-
* - `text_delta` — superseded by the `assistant_message` that follows it, which
|
|
810
|
-
* filters the streaming id and rebuilds from the full content blocks. It could
|
|
811
|
-
* go, but only with a lookahead proving the message arrived, and at 24 KB in
|
|
812
|
-
* the measured session it is not worth a rule that has to be right about
|
|
813
|
-
* supersession. A merge is likewise not worth it: a *drop* needs no synthesized
|
|
814
|
-
* event and therefore no invented seq.
|
|
815
|
-
*
|
|
816
|
-
* A live event is never affected — this is about the buffered replay alone — and
|
|
817
|
-
* the caller must never drop the log's highest-seq event whatever this says, for
|
|
818
|
-
* the reason {@link replayCoalesceKey} gives: the replay hold waits for
|
|
819
|
-
* `state.lastSeq` to reach the attach's `session.lastSeq` and would hang on a
|
|
820
|
-
* blank panel forever.
|
|
821
|
-
*
|
|
822
|
-
* The property is the family's usual one and is a test rather than an argument:
|
|
823
|
-
* folding the full log and the retained log through `applyEvent` yields
|
|
824
|
-
* identical state (`packages/react/test/replay-retain.test.ts`).
|
|
825
|
-
*/
|
|
826
463
|
function replayRetains(body) {
|
|
827
464
|
if (body.type !== "stream_delta") return true;
|
|
828
465
|
const delta = body.event;
|
|
829
466
|
if (delta.type !== "content_block_delta") return false;
|
|
830
467
|
return delta.delta?.type === "text_delta" || delta.delta?.type === "thinking_delta";
|
|
831
468
|
}
|
|
832
|
-
/**
|
|
833
|
-
* Does a `RunnerSnapshot` keep this event in its persisted log?
|
|
834
|
-
*
|
|
835
|
-
* The fourth of the same family, and the same shape of claim as
|
|
836
|
-
* {@link replayCoalesceKey}: which events a *store* may drop without any client
|
|
837
|
-
* being able to tell. It exists because a snapshot embeds the whole event log,
|
|
838
|
-
* and a log is mostly stream deltas — a four-character token rides a ~180-byte
|
|
839
|
-
* JSON envelope, so the delta run is tens of times the size of the text it
|
|
840
|
-
* spells, sitting on disk *beside* the `assistant_message` that respells it in
|
|
841
|
-
* full. That was affordable while a snapshot was written once, at a park. It is
|
|
842
|
-
* not affordable written after every turn, which is what restart-survival needs.
|
|
843
|
-
*
|
|
844
|
-
* So: everything is retained except `stream_delta`. The reason that is safe is
|
|
845
|
-
* not that deltas are unimportant but that they are **superseded by
|
|
846
|
-
* construction**. The reducer upserts them under one constant id and the
|
|
847
|
-
* following `assistant_message` filters exactly that id out and rebuilds from
|
|
848
|
-
* the full content blocks — and a snapshot may only be taken at a rest point,
|
|
849
|
-
* where the stream loop has exited and flushed. Both exits flush, including the
|
|
850
|
-
* error path: an interrupted turn pushes its half-finished buffers into a
|
|
851
|
-
* durable `assistant_message` before it emits the failed `turn_result`. There is
|
|
852
|
-
* no rest state in which a delta is the only record of anything.
|
|
853
|
-
*
|
|
854
|
-
* **Provider engine only**, and this is the carve-out that must not be lost:
|
|
855
|
-
* against a *Claude* log the rule would be wrong. The Claude SDK delivers
|
|
856
|
-
* thinking blocks whose text is `''`, with the human-readable summary existing
|
|
857
|
-
* only in the delta stream, and the reducer carries the streamed text over to
|
|
858
|
-
* fill them (`transcript.ts`, the `streamedThinking` backfill). Dropping deltas
|
|
859
|
-
* there would silently erase every thought from a restored transcript. Today
|
|
860
|
-
* that is unreachable rather than merely avoided — only the provider engine
|
|
861
|
-
* implements `park()`/`snapshot()` at all, and `#restore` refuses a snapshot
|
|
862
|
-
* from another engine — but an engine that gains one inherits this obligation.
|
|
863
|
-
*
|
|
864
|
-
* Two properties hold it up, both of which are tests rather than arguments:
|
|
865
|
-
* folding the full log and the retained log through `applyEvent` yields
|
|
866
|
-
* identical state (`packages/react/test/snapshot-retain.test.ts`, the same
|
|
867
|
-
* property `replay-coalesce.test.ts` asserts), and the retained log's last event
|
|
868
|
-
* still carries the snapshot's own `seq`. The second matters more than it looks:
|
|
869
|
-
* `transcriptActivity(stream_delta)` is 0, so the count `#restore` recomputes
|
|
870
|
-
* from the log is bit-identical — a client's unread cursor cannot move — and the
|
|
871
|
-
* replay hold waits for `state.lastSeq` to reach the attach's `lastSeq`, which a
|
|
872
|
-
* rule that could drop the final event would hang forever.
|
|
873
|
-
*/
|
|
874
469
|
function snapshotRetains(body) {
|
|
875
470
|
return body.type !== "stream_delta";
|
|
876
471
|
}
|
|
877
472
|
//#endregion
|
|
878
|
-
export { DEFAULT_VIEW_CONFIG, ENGINE_CAPABILITIES, PROTOCOL_VERSION,
|
|
473
|
+
export { DEFAULT_VIEW_CONFIG, ENGINE_CAPABILITIES, PROTOCOL_VERSION, STATE_LABELS, STATE_ORDER, SUBAGENT_HISTORY, TOOL_RESULT_HEAD_CHARS, Watermarks, adaptersOf, clearFilters, contextReading, filterRows, groupRows, hasFacetFilter, imagePartRef, inScope, isAgentRecord, isJobRun, mergeUsage, orderUsageWindows, projectKey, projectLabel, projectSubpath, projectsOf, replayCoalesceKey, replayRetains, runningSubagents, scopeActive, sessionLabel, sessionState, snapshotRetains, subagentLabel, subsetSummary, supportsPermissionMode, transcriptActivity, transcriptContent, transcriptProse, unseenCount, usageInfos, watermarkKey };
|
|
879
474
|
|
|
880
475
|
//# sourceMappingURL=index.mjs.map
|