@camox/api-contract 0.56.0 → 0.57.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/threads.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { DraftOutcome } from "./drafts.js";
1
2
  import { z } from "zod";
2
3
  import { AgentEvent } from "@earendil-works/pi-durable";
4
+ import { Message } from "@earendil-works/pi-ai";
3
5
  //#region src/threads.d.ts
4
6
  /**
5
7
  * The kind of the transcript entry recording an editor's message, with its sender and the
@@ -20,6 +22,41 @@ export declare const BUILD_STATUS_ENTRY = "camox.build-status";
20
22
  * editors see. The model gets Camox's instructions instead.
21
23
  */
22
24
  export declare const RECONCILE_ENTRY = "camox.reconcile";
25
+ /**
26
+ * The kind of the transcript entry opening a draft's thread a Live thread handed a request off
27
+ * to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
28
+ * messages.
29
+ */
30
+ export declare const BRIEF_ENTRY = "camox.brief";
31
+ /**
32
+ * The kind of the transcript entry telling the agent on Live that a draft it started ended,
33
+ * published or deleted, with the draft's id as data (`{ draftId }`). The model reads
34
+ * it at the end of the conversation; editors never see it, since the draft's card says so.
35
+ */
36
+ export declare const DRAFT_ENDED_ENTRY = "camox.draft-ended";
37
+ /**
38
+ * The agent's tool on Live handing a request off to a draft it starts (ADR 0025). A run whose
39
+ * call of it went through ends on the draft's card.
40
+ */
41
+ export declare const START_DRAFT_TOOL = "startDraft";
42
+ /** The draft a `startDraft` call started, as its result's details name it for the card. */
43
+ export declare const startedDraftCardSchema: z.ZodObject<{
44
+ draftId: z.ZodString;
45
+ name: z.ZodString;
46
+ }, z.core.$strip>;
47
+ export type StartedDraftCard = z.infer<typeof startedDraftCardSchema>;
48
+ /**
49
+ * Where a draft a Live thread's card names stands, as the thread looks it up whenever it's shown
50
+ * (ADR 0025): its current name, or its last once it ended, and how it ended. Never written into
51
+ * the transcript, so the model's cached prompt keeps up. A name cleared by a rename is null: the
52
+ * card keeps the name the agent gave it.
53
+ */
54
+ export type DraftCardState = {
55
+ name: string | null;
56
+ ended?: DraftOutcome;
57
+ };
58
+ /** The drafts a Live thread's cards name, by id, as the thread last looked them up. */
59
+ export type DraftCardStates = Readonly<Record<string, DraftCardState>>;
23
60
  export declare const reconcileDataSchema: z.ZodObject<{
24
61
  message: z.ZodString;
25
62
  }, z.core.$strip>;
@@ -43,17 +80,21 @@ export declare const threadSenderSchema: z.ZodObject<{
43
80
  export type ThreadSender = z.infer<typeof threadSenderSchema>;
44
81
  /** Whether `data` names a sender. */
45
82
  export declare const isSender: (data: unknown) => data is ThreadSender;
46
- /** What an editor's message entry records: who sent it, and the operation id it was sent with. */
83
+ /**
84
+ * What an editor's message entry records: who sent it, the operation id it was sent with, and for
85
+ * a message sent on Live as the agent handed the request off, the draft it went to with it.
86
+ */
47
87
  export declare const editorMessageDataSchema: z.ZodObject<{
48
88
  id: z.ZodString;
49
89
  name: z.ZodString;
50
90
  image: z.ZodNullable<z.ZodString>;
51
91
  operationId: z.ZodOptional<z.ZodString>;
92
+ handedOffTo: z.ZodOptional<z.ZodString>;
52
93
  }, z.core.$strip>;
53
94
  /**
54
- * A draft a Live thread started with its first write, becoming its thread, and the operation ids
55
- * of the messages of the run that made the write: the studio that sent one of them enters the
56
- * draft with the thread.
95
+ * A draft the agent on Live started with `startDraft`, and the operation ids of the messages of
96
+ * the run that started it: the studio that sent one of them enters the draft, if it still shows
97
+ * Live.
57
98
  */
58
99
  export declare const startedDraftSchema: z.ZodObject<{
59
100
  draftId: z.ZodString;
@@ -182,20 +223,19 @@ export type ThreadMessage = {
182
223
  id: string;
183
224
  from: "stopped";
184
225
  sender: ThreadSender;
185
- };
186
- /** Where a Live thread's messages went when it started a draft, beside its transcript. */
187
- type LiveThreadHandoff = {
188
- /**
189
- * The draft this Live thread started with its first write, whose thread it became. The studio
190
- * that sent a message of that run enters the draft with the thread; other tabs on Live move to
191
- * the editor's fresh Live thread.
192
- */
193
- startedDraft?: StartedDraft;
194
- /** The draft the editor's previous Live thread continued in, for a fresh Live thread. */
195
- continuedIn?: StartedDraft;
196
- };
226
+ } |
227
+ /**
228
+ * In place of the agent's message, for a Live turn handed off to a draft the agent started:
229
+ * the draft's card, with its current name, which opens it. Once the draft ended, it has its
230
+ * last name and how it ended, and opens nothing. The turn is finished.
231
+ */
232
+ ({
233
+ id: string;
234
+ from: "draft";
235
+ ended?: DraftOutcome;
236
+ } & StartedDraftCard);
197
237
  /** Everything the studio shows for a thread. */
198
- export type ThreadView = LiveThreadHandoff & {
238
+ export type ThreadView = {
199
239
  messages: readonly ThreadMessage[];
200
240
  /** Whether a run is going: the agent is working on a turn, and stopping would end it. */
201
241
  running: boolean;
@@ -216,13 +256,15 @@ export declare const EMPTY_THREAD_VIEW: ThreadView;
216
256
  * and whether a run is going. The studio and the tests fold Pi's events into this, and derive
217
257
  * the view from it with `threadViewOf`, so a snapshot and the events that led to it show the same.
218
258
  */
219
- export type ThreadState = LiveThreadHandoff & {
259
+ export type ThreadState = {
220
260
  steps: readonly ThreadStep[];
221
261
  running: boolean;
222
262
  /** The model's calls still running, by call id. A tool's own calls aren't the agent's. */
223
263
  toolCalls?: readonly string[];
224
264
  /** Whether the model's latest message makes tool calls: it writes them, then they run. */
225
265
  callingTools?: boolean;
266
+ /** The drafts the thread's cards name, as the thread last looked them up. */
267
+ drafts?: DraftCardStates;
226
268
  };
227
269
  export declare const EMPTY_THREAD_STATE: ThreadState;
228
270
  /** One transcript entry, as far as the studio's view is concerned. */
@@ -232,6 +274,8 @@ type ThreadStep = {
232
274
  type: "editor";
233
275
  sender: ThreadSender;
234
276
  text: string;
277
+ /** The draft the message went to, as the agent on Live handed the request off. */
278
+ handedOffTo?: string;
235
279
  } |
236
280
  /** Camox starting a turn of its own, saying why. */
237
281
  {
@@ -252,6 +296,10 @@ type ThreadStep = {
252
296
  type: "stopped";
253
297
  sender: ThreadSender;
254
298
  } |
299
+ /** A `startDraft` call that went through, which ends the turn on the draft's card. */
300
+ ({
301
+ type: "draft";
302
+ } & StartedDraftCard) |
255
303
  /** Where the build of the turn's code stands, as the turn ends. */
256
304
  {
257
305
  type: "build";
@@ -290,18 +338,21 @@ export type ThreadServerMessage =
290
338
  type: "accepted";
291
339
  operationId: string;
292
340
  } |
293
- /** This Live thread started a draft with its first write, and became that draft's thread. */
341
+ /**
342
+ * The agent on Live started a draft with `startDraft`: the studio that sent a message of the
343
+ * run enters it, if it still shows Live. Live only: a snapshot never replays it.
344
+ */
294
345
  {
295
346
  type: "started-draft";
296
347
  startedDraft: StartedDraft;
297
348
  } |
298
349
  /**
299
- * The editor's previous Live thread continued in this draft: this one is fresh. Sent before
300
- * the snapshot.
350
+ * Where the drafts a Live thread's cards name stand, after each snapshot and whenever one of
351
+ * them is renamed or ends. It replaces what the studio knew of them.
301
352
  */
302
353
  {
303
- type: "continued-in";
304
- continuedIn: StartedDraft;
354
+ type: "drafts";
355
+ drafts: DraftCardStates;
305
356
  } |
306
357
  /**
307
358
  * A code-mode script that wrote something ended, failed or not: what its successful writes
@@ -321,16 +372,18 @@ export type ThreadServerMessage =
321
372
  * thread the next time the studio connects.
322
373
  */
323
374
  export declare const THREAD_ENDED_CLOSE_CODE = 4000;
375
+ /** The text of a message's content: its text parts, joined. */
376
+ export declare const textOf: (content: Message["content"]) => string;
324
377
  /**
325
378
  * What the studio shows for a thread. A turn starts with an editor's message, or Camox's own
326
379
  * when it began the turn itself, and messages sent
327
- * while it runs join it. It ends with the agent's final message; text written beside tool calls
328
- * is never shown. A stopped turn shows who stopped it instead, and whatever its run wrote after
380
+ * while it runs join it. It ends with the agent's final message, or on Live, the card of the
381
+ * draft it handed the request off to; text written beside tool calls is never shown. A stopped turn shows who stopped it instead, and whatever its run wrote after
329
382
  * the stop is left out. A turn that ended without either shows a failed line in its place: its
330
383
  * run is over, or a later message started another run after its last model call came back
331
384
  * unfinished. While Pi retries a failed call, the run goes on, so the turn shows as working.
332
385
  */
333
- export declare function threadViewOf({ steps, running, toolCalls, callingTools, ...drafts }: ThreadState): ThreadView;
386
+ export declare function threadViewOf({ steps, running, toolCalls, callingTools, drafts }: ThreadState): ThreadView;
334
387
  /** Folds one of Pi's agent events into the thread's state. */
335
388
  export declare function reduceThreadState(state: ThreadState, event: AgentEvent): ThreadState;
336
389
  /** Folds a batch of Pi's agent events into the thread's state. */
package/dist/threads.js CHANGED
@@ -20,6 +20,28 @@ const BUILD_STATUS_ENTRY = "camox.build-status";
20
20
  * editors see. The model gets Camox's instructions instead.
21
21
  */
22
22
  const RECONCILE_ENTRY = "camox.reconcile";
23
+ /**
24
+ * The kind of the transcript entry opening a draft's thread a Live thread handed a request off
25
+ * to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
26
+ * messages.
27
+ */
28
+ const BRIEF_ENTRY = "camox.brief";
29
+ /**
30
+ * The kind of the transcript entry telling the agent on Live that a draft it started ended,
31
+ * published or deleted, with the draft's id as data (`{ draftId }`). The model reads
32
+ * it at the end of the conversation; editors never see it, since the draft's card says so.
33
+ */
34
+ const DRAFT_ENDED_ENTRY = "camox.draft-ended";
35
+ /**
36
+ * The agent's tool on Live handing a request off to a draft it starts (ADR 0025). A run whose
37
+ * call of it went through ends on the draft's card.
38
+ */
39
+ const START_DRAFT_TOOL = "startDraft";
40
+ /** The draft a `startDraft` call started, as its result's details name it for the card. */
41
+ const startedDraftCardSchema = z.object({
42
+ draftId: z.string(),
43
+ name: z.string()
44
+ });
23
45
  const reconcileDataSchema = z.object({ message: z.string() });
24
46
  const buildStatusSchema = z.enum(["building", "fixing"]);
25
47
  const buildStatusDataSchema = z.object({ status: buildStatusSchema });
@@ -31,12 +53,18 @@ const threadSenderSchema = z.object({
31
53
  });
32
54
  /** Whether `data` names a sender. */
33
55
  const isSender = (data) => threadSenderSchema.safeParse(data).success;
34
- /** What an editor's message entry records: who sent it, and the operation id it was sent with. */
35
- const editorMessageDataSchema = threadSenderSchema.extend({ operationId: z.string().optional() });
36
56
  /**
37
- * A draft a Live thread started with its first write, becoming its thread, and the operation ids
38
- * of the messages of the run that made the write: the studio that sent one of them enters the
39
- * draft with the thread.
57
+ * What an editor's message entry records: who sent it, the operation id it was sent with, and for
58
+ * a message sent on Live as the agent handed the request off, the draft it went to with it.
59
+ */
60
+ const editorMessageDataSchema = threadSenderSchema.extend({
61
+ operationId: z.string().optional(),
62
+ handedOffTo: z.string().optional()
63
+ });
64
+ /**
65
+ * A draft the agent on Live started with `startDraft`, and the operation ids of the messages of
66
+ * the run that started it: the studio that sent one of them enters the draft, if it still shows
67
+ * Live.
40
68
  */
41
69
  const startedDraftSchema = z.object({
42
70
  draftId: z.string(),
@@ -110,6 +138,7 @@ const threadClientMessageSchema = z.discriminatedUnion("type", [
110
138
  * thread the next time the studio connects.
111
139
  */
112
140
  const THREAD_ENDED_CLOSE_CODE = 4e3;
141
+ /** The text of a message's content: its text parts, joined. */
113
142
  const textOf = (content) => typeof content === "string" ? content : content.flatMap((part) => part.type === "text" ? [part.text] : []).join("").trim();
114
143
  /** What an assistant message is to the thread: a final answer, a tool round, or neither. */
115
144
  function assistantStep(id, message) {
@@ -154,13 +183,23 @@ function threadStepOf(entry) {
154
183
  status: build.status
155
184
  };
156
185
  }
186
+ if (entry.kind === "camox.draft-ended") return void 0;
157
187
  const message = entry.model?.find((candidate) => candidate.role !== "system");
158
188
  if (!message) return void 0;
159
189
  if (message.role === "user") {
160
- if (entry.kind === "camox.editor-message") return sender && {
190
+ if (entry.kind === "camox.editor-message") {
191
+ const handedOffTo = editorMessageDataSchema.safeParse(entry.data).data?.handedOffTo;
192
+ return sender && {
193
+ id,
194
+ type: "editor",
195
+ sender,
196
+ text: textOf(message.content),
197
+ ...handedOffTo && { handedOffTo }
198
+ };
199
+ }
200
+ if (entry.kind === "camox.brief") return {
161
201
  id,
162
- type: "editor",
163
- sender,
202
+ type: "camox",
164
203
  text: textOf(message.content)
165
204
  };
166
205
  if (entry.kind === "camox.reconcile") {
@@ -177,21 +216,28 @@ function threadStepOf(entry) {
177
216
  };
178
217
  }
179
218
  if (message.role === "assistant") return assistantStep(id, message);
180
- if (message.role === "toolResult") return {
181
- id,
182
- type: "tools"
183
- };
219
+ if (message.role === "toolResult") {
220
+ const card = message.toolName === "startDraft" && !message.isError ? startedDraftCardSchema.safeParse(message.details).data : void 0;
221
+ return card ? {
222
+ id,
223
+ type: "draft",
224
+ ...card
225
+ } : {
226
+ id,
227
+ type: "tools"
228
+ };
229
+ }
184
230
  }
185
231
  /**
186
232
  * What the studio shows for a thread. A turn starts with an editor's message, or Camox's own
187
233
  * when it began the turn itself, and messages sent
188
- * while it runs join it. It ends with the agent's final message; text written beside tool calls
189
- * is never shown. A stopped turn shows who stopped it instead, and whatever its run wrote after
234
+ * while it runs join it. It ends with the agent's final message, or on Live, the card of the
235
+ * draft it handed the request off to; text written beside tool calls is never shown. A stopped turn shows who stopped it instead, and whatever its run wrote after
190
236
  * the stop is left out. A turn that ended without either shows a failed line in its place: its
191
237
  * run is over, or a later message started another run after its last model call came back
192
238
  * unfinished. While Pi retries a failed call, the run goes on, so the turn shows as working.
193
239
  */
194
- function threadViewOf({ steps, running, toolCalls = [], callingTools = false, ...drafts }) {
240
+ function threadViewOf({ steps, running, toolCalls = [], callingTools = false, drafts = {} }) {
195
241
  const messages = [];
196
242
  let waiting;
197
243
  let unfinished = false;
@@ -213,7 +259,17 @@ function threadViewOf({ steps, running, toolCalls = [], callingTools = false, ..
213
259
  endTurn();
214
260
  };
215
261
  for (const step of steps) switch (step.type) {
216
- case "editor":
262
+ case "editor": {
263
+ const card = step.handedOffTo ? messages.findLastIndex((message) => message.from === "draft" && message.draftId === step.handedOffTo) : -1;
264
+ if (card !== -1) {
265
+ messages.splice(card, 0, {
266
+ id: step.id,
267
+ from: "editor",
268
+ sender: step.sender,
269
+ text: step.text
270
+ });
271
+ break;
272
+ }
217
273
  if (unfinished) fail();
218
274
  messages.push({
219
275
  id: step.id,
@@ -223,6 +279,7 @@ function threadViewOf({ steps, running, toolCalls = [], callingTools = false, ..
223
279
  });
224
280
  waiting = step.id;
225
281
  break;
282
+ }
226
283
  case "camox":
227
284
  if (unfinished) fail();
228
285
  messages.push({
@@ -257,6 +314,20 @@ function threadViewOf({ steps, running, toolCalls = [], callingTools = false, ..
257
314
  answered = before;
258
315
  }
259
316
  break;
317
+ case "draft":
318
+ if (waiting === void 0 && messages.at(-1)?.from === "stopped") break;
319
+ {
320
+ const draft = drafts[step.draftId];
321
+ messages.push({
322
+ id: step.id,
323
+ from: "draft",
324
+ draftId: step.draftId,
325
+ name: draft?.name ?? step.name,
326
+ ...draft?.ended && { ended: draft.ended }
327
+ });
328
+ }
329
+ endTurn();
330
+ break;
260
331
  case "continued":
261
332
  if (!answered) break;
262
333
  if (messages.at(-1)?.from === "agent") messages.pop();
@@ -275,7 +346,6 @@ function threadViewOf({ steps, running, toolCalls = [], callingTools = false, ..
275
346
  if (!running) fail();
276
347
  const usingTools = running && (callingTools || toolCalls.length > 0);
277
348
  return {
278
- ...drafts,
279
349
  messages,
280
350
  running,
281
351
  ...running && status && { status },
@@ -339,17 +409,13 @@ const reduceThreadEvents = (state, events) => events.reduce(reduceThreadState, s
339
409
  function reduceThreadFrame(state, frame) {
340
410
  switch (frame.type) {
341
411
  case "events": return reduceThreadEvents(state, frame.events);
342
- case "started-draft": return {
343
- ...state,
344
- startedDraft: frame.startedDraft
345
- };
346
- case "continued-in": return {
412
+ case "drafts": return {
347
413
  ...state,
348
- continuedIn: frame.continuedIn
414
+ drafts: frame.drafts
349
415
  };
350
416
  default: return state;
351
417
  }
352
418
  }
353
419
 
354
420
  //#endregion
355
- export { BUILD_STATUS_ENTRY, EDITOR_MESSAGE_ENTRY, EMPTY_THREAD_STATE, EMPTY_THREAD_VIEW, RECONCILE_ENTRY, RUN_STOPPED_ENTRY, THREAD_ENDED_CLOSE_CODE, buildStatusDataSchema, buildStatusSchema, editorMessageDataSchema, isSender, reconcileDataSchema, reduceThreadEvents, reduceThreadFrame, reduceThreadState, startedDraftSchema, threadClientMessageSchema, threadSenderSchema, threadViewOf, touchKindSchema, touchSchema, touchedBatchSchema };
421
+ export { BRIEF_ENTRY, BUILD_STATUS_ENTRY, DRAFT_ENDED_ENTRY, EDITOR_MESSAGE_ENTRY, EMPTY_THREAD_STATE, EMPTY_THREAD_VIEW, RECONCILE_ENTRY, RUN_STOPPED_ENTRY, START_DRAFT_TOOL, THREAD_ENDED_CLOSE_CODE, buildStatusDataSchema, buildStatusSchema, editorMessageDataSchema, isSender, reconcileDataSchema, reduceThreadEvents, reduceThreadFrame, reduceThreadState, startedDraftCardSchema, startedDraftSchema, textOf, threadClientMessageSchema, threadSenderSchema, threadViewOf, touchKindSchema, touchSchema, touchedBatchSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camox/api-contract",
3
- "version": "0.56.0",
3
+ "version": "0.57.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "src",
@@ -44,6 +44,10 @@
44
44
  "types": "./dist/locales.d.ts",
45
45
  "default": "./dist/locales.js"
46
46
  },
47
+ "./patrol": {
48
+ "types": "./dist/patrol.d.ts",
49
+ "default": "./dist/patrol.js"
50
+ },
47
51
  "./router": {
48
52
  "types": "./dist/router.d.ts",
49
53
  "default": "./dist/router.js"
package/src/patrol.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The Patrol (ADR 0024): every patroller of a project, the work each did as reports, as the
3
+ * studio shows it at the top of the Agent Sidebar. The API reads it for one scope, a draft or
4
+ * Live, and names each report's subject, so the studio needs no entity loaded to show it.
5
+ */
6
+
7
+ /** A patroller's stable id. The registry in the API lists them all. */
8
+ export type PatrollerId = "translator" | "seoManager";
9
+
10
+ /**
11
+ * What a structured report says was done. Each has a failed form, by the report's outcome. A
12
+ * merge with Live's localization reads as a translation.
13
+ */
14
+ export type ReportVerb =
15
+ | "translated"
16
+ | "mergedWithLive"
17
+ | "wroteSeo"
18
+ | "describedFile"
19
+ | "publishedCatchUp";
20
+
21
+ /** What a report can be about. */
22
+ export type ReportEntity =
23
+ | "page"
24
+ | "block"
25
+ | "sharedContent"
26
+ | "repeatableItem"
27
+ | "record"
28
+ | "file";
29
+
30
+ /** A report's subject, named as the Canvas names it, read when the Patrol is. */
31
+ export type ReportSubject = { entity: ReportEntity; id: string; name: string };
32
+
33
+ /**
34
+ * What a report says: structured facts the studio turns into a sentence, in the studio's
35
+ * language, or text an AI wrote, shown as is. `locale` is a locale's tag.
36
+ */
37
+ export type ReportSummary =
38
+ | {
39
+ kind: "structured";
40
+ verb: ReportVerb;
41
+ subject: ReportSubject | null;
42
+ locale: string | null;
43
+ }
44
+ | { kind: "text"; text: string };
45
+
46
+ /** The locale a summary's work was in: a structured summary's, if any. A text summary has none. */
47
+ export const reportLocale = (
48
+ summary: { kind: "structured"; locale: string | null } | { kind: "text" },
49
+ ) => (summary.kind === "structured" ? summary.locale : null);
50
+
51
+ /** Whether the work was done, or failed. A failure shows in its report only. */
52
+ export type ReportOutcome = "done" | "failed";
53
+
54
+ /** Whether a report needs a human. Nothing raises one yet. */
55
+ export type AttentionLevel = "none" | "needed";
56
+
57
+ /**
58
+ * Where the studio shows a report's subject: a page on the Canvas, with the block and repeatable
59
+ * item to select on it, a collection record, or a file.
60
+ */
61
+ export type ReportPlace =
62
+ | { kind: "page"; pageId: string; blockId: string | null; itemId: string | null }
63
+ | { kind: "record"; collectionId: string; recordId: string }
64
+ | { kind: "file"; fileId: string };
65
+
66
+ export type ReportView = {
67
+ /** Unique among the patroller's reports in the view. */
68
+ id: string;
69
+ summary: ReportSummary;
70
+ outcome: ReportOutcome;
71
+ attention: AttentionLevel;
72
+ /** When the work last settled, in milliseconds since the epoch. */
73
+ at: number;
74
+ /** Whether the report is Live's, seen from a draft or from Live. */
75
+ live: boolean;
76
+ /**
77
+ * Where its subject shows, in the report's scope and locale. Null without a subject, or for one
78
+ * with no single place, such as a synced block's shared content or a layout's block.
79
+ */
80
+ place: ReportPlace | null;
81
+ };
82
+
83
+ /** How many subjects a patroller is on in one locale, a locale's tag. */
84
+ export type LocaleCount = { locale: string; count: number };
85
+
86
+ /**
87
+ * What a patroller is on: the one subject when there's one, and how many subjects in each
88
+ * locale, the most first. A subject in several locales counts in each.
89
+ */
90
+ export type PatrollerWorking = {
91
+ subject: ReportSubject | null;
92
+ locales: LocaleCount[];
93
+ };
94
+
95
+ export type PatrollerView = {
96
+ id: PatrollerId;
97
+ /** Its role, as editors know it: "Translator", "SEO manager". */
98
+ role: string;
99
+ /** Null when it isn't working in the scope. */
100
+ working: PatrollerWorking | null;
101
+ /** Its latest activity in the scope, or null when it never worked there. */
102
+ lastActivityAt: number | null;
103
+ /** Its newest reports, newest first. */
104
+ reports: ReportView[];
105
+ /** How many more reports it has than `reports` shows, per locale, the most first. */
106
+ more: { locale: string | null; count: number }[];
107
+ };
108
+
109
+ /** Every patroller of the registry, the latest activity first; never-worked ones last. */
110
+ export type PatrolView = { patrollers: PatrollerView[] };
111
+
112
+ /** How many reports the Patrol shows of each patroller; the rest are counted. */
113
+ export const REPORTS_SHOWN = 5;
package/src/query-keys.ts CHANGED
@@ -59,6 +59,10 @@ export const queryKeys = {
59
59
  // The draft a viewer is in, scoped to it like content.
60
60
  get: ["camox", "drafts", "get"],
61
61
  },
62
+ // The Patrol of a scope: its reports and its patrollers' pending work.
63
+ patrol: {
64
+ get: ["camox", "patrol"],
65
+ },
62
66
  } satisfies Record<string, QueryKeyGroup>;
63
67
 
64
68
  /** The last segment of a query key: what the query reads, Live or a draft (ADR 0011). */