@camox/api-contract 0.69.0 → 0.72.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/drafts.d.ts CHANGED
@@ -42,17 +42,17 @@ export declare const LOOK_PATH_PARAM = "path";
42
42
  export declare const LOOK_LOCALE_PARAM = "locale";
43
43
  /**
44
44
  * The look document's Block Definition mode: instead of a page, the block of the Block Definition
45
- * `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with its default content and
46
- * Block Settings and the overrides `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON, in
47
- * the shape the content tools take.
45
+ * `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with empty fields and the
46
+ * content and Block Settings `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON merged
47
+ * on, in the shape the content tools take.
48
48
  */
49
49
  export declare const LOOK_BLOCK_DEFINITION_PARAM = "blockDefinition";
50
50
  export declare const LOOK_CONTENT_PARAM = "content";
51
51
  export declare const LOOK_SETTINGS_PARAM = "settings";
52
52
  /**
53
53
  * The look document's Layout Definition mode: instead of a page, the curated Layout
54
- * `LOOK_LAYOUT_DEFINITION_PARAM` names, alone in the site's shell, its blocks with their default
55
- * content and Block Settings and the overrides `LOOK_BLOCKS_PARAM` gives as JSON, by Block
54
+ * `LOOK_LAYOUT_DEFINITION_PARAM` names, alone in the site's shell, its blocks with empty fields
55
+ * and the content and Block Settings `LOOK_BLOCKS_PARAM` gives as JSON merged on, by Block
56
56
  * Definition name (`{ navbar: { content, settings } }`), around a labelled placeholder where a
57
57
  * page's content goes, as tall as `LOOK_PLACEHOLDER_PARAM` says: one of `LOOK_PLACEHOLDER_HEIGHTS`,
58
58
  * the first without.
@@ -69,6 +69,54 @@ export type LookPlaceholderHeight = (typeof LOOK_PLACEHOLDER_HEIGHTS)[number];
69
69
  * `[data-camox-viewport-block="unplaced"]`.
70
70
  */
71
71
  export declare const UNPLACED_BLOCK_ID = "unplaced";
72
+ /**
73
+ * The attributes the website's artifact frames carry: the artifact field's name, and the id of the
74
+ * Repeatable Item holding it, when an item's. With the block's `data-camox-viewport-block`, they
75
+ * find one artifact's frame (`lookArtifactSelector`).
76
+ */
77
+ export declare const ARTIFACT_FIELD_ATTRIBUTE = "data-camox-artifact";
78
+ export declare const ARTIFACT_ITEM_ATTRIBUTE = "data-camox-item";
79
+ /** The CSS selector of the frame of an artifact field of a block, or of one of its items. */
80
+ export declare const lookArtifactSelector: ({ block, item, field }: {
81
+ block: string;
82
+ item?: string;
83
+ field: string;
84
+ }) => string;
85
+ /** An artifact's Timeline, as its runtime declares it: Marks by name, at times in ms. */
86
+ export type LookTimeline = {
87
+ duration: number;
88
+ loop: boolean;
89
+ marks: Record<string, number>;
90
+ /** The Mark whose frame stands for the artifact, or null. */
91
+ poster: string | null;
92
+ };
93
+ /**
94
+ * One finding of an artifact's guardrail report, about motion that escapes its Timeline or will
95
+ * look broken: a Web Animation the runtime didn't make (a CSS animation, a CSS transition that no
96
+ * visitor triggered, one the source made itself), a live timer, a Track the runtime skipped
97
+ * because it can't play it, Tracks on one element and property that overlap, or a loop cut,
98
+ * where a looping Timeline's last frame differs from its first.
99
+ */
100
+ export type LookGuardrailFinding = {
101
+ kind: "animation" | "timer" | "track" | "overlap" | "loop-cut";
102
+ /** What's wrong and what to do instead, for the Artifact Agent. */
103
+ text: string;
104
+ };
105
+ /**
106
+ * What the look document's artifact frames hold under `LOOK_ARTIFACT_CAPTURE`, for the browser
107
+ * `look` drives: the artifact's Timeline, null when it declares none; seeking it to `t` ms,
108
+ * resolved once the frame at that time painted, with the error `render` threw, if it did; and its
109
+ * guardrail report, empty when nothing escapes its Timeline. Frames of an SDK older than
110
+ * Timelines hold nothing there.
111
+ */
112
+ export type LookArtifactCapture = {
113
+ timeline(): Promise<LookTimeline | null>;
114
+ seek(t: number): Promise<{
115
+ error?: string;
116
+ }>;
117
+ guardrail(): Promise<LookGuardrailFinding[]>;
118
+ };
119
+ export declare const LOOK_ARTIFACT_CAPTURE = "camoxCapture";
72
120
  /**
73
121
  * Request header asking the API to create the draft named by `DRAFT_HEADER` if it doesn't
74
122
  * exist yet. Its value is `"1"`.
package/dist/drafts.js CHANGED
@@ -42,17 +42,17 @@ const LOOK_PATH_PARAM = "path";
42
42
  const LOOK_LOCALE_PARAM = "locale";
43
43
  /**
44
44
  * The look document's Block Definition mode: instead of a page, the block of the Block Definition
45
- * `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with its default content and
46
- * Block Settings and the overrides `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON, in
47
- * the shape the content tools take.
45
+ * `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with empty fields and the
46
+ * content and Block Settings `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON merged
47
+ * on, in the shape the content tools take.
48
48
  */
49
49
  const LOOK_BLOCK_DEFINITION_PARAM = "blockDefinition";
50
50
  const LOOK_CONTENT_PARAM = "content";
51
51
  const LOOK_SETTINGS_PARAM = "settings";
52
52
  /**
53
53
  * The look document's Layout Definition mode: instead of a page, the curated Layout
54
- * `LOOK_LAYOUT_DEFINITION_PARAM` names, alone in the site's shell, its blocks with their default
55
- * content and Block Settings and the overrides `LOOK_BLOCKS_PARAM` gives as JSON, by Block
54
+ * `LOOK_LAYOUT_DEFINITION_PARAM` names, alone in the site's shell, its blocks with empty fields
55
+ * and the content and Block Settings `LOOK_BLOCKS_PARAM` gives as JSON merged on, by Block
56
56
  * Definition name (`{ navbar: { content, settings } }`), around a labelled placeholder where a
57
57
  * page's content goes, as tall as `LOOK_PLACEHOLDER_PARAM` says: one of `LOOK_PLACEHOLDER_HEIGHTS`,
58
58
  * the first without.
@@ -69,6 +69,16 @@ const LOOK_PLACEHOLDER_HEIGHTS = ["short", "tall"];
69
69
  */
70
70
  const UNPLACED_BLOCK_ID = "unplaced";
71
71
  /**
72
+ * The attributes the website's artifact frames carry: the artifact field's name, and the id of the
73
+ * Repeatable Item holding it, when an item's. With the block's `data-camox-viewport-block`, they
74
+ * find one artifact's frame (`lookArtifactSelector`).
75
+ */
76
+ const ARTIFACT_FIELD_ATTRIBUTE = "data-camox-artifact";
77
+ const ARTIFACT_ITEM_ATTRIBUTE = "data-camox-item";
78
+ /** The CSS selector of the frame of an artifact field of a block, or of one of its items. */
79
+ const lookArtifactSelector = ({ block, item, field }) => `[data-camox-viewport-block="${block}"] iframe[${ARTIFACT_FIELD_ATTRIBUTE}="${field}"]` + (item ? `[${ARTIFACT_ITEM_ATTRIBUTE}="${item}"]` : `:not([${ARTIFACT_ITEM_ATTRIBUTE}])`);
80
+ const LOOK_ARTIFACT_CAPTURE = "camoxCapture";
81
+ /**
72
82
  * Request header asking the API to create the draft named by `DRAFT_HEADER` if it doesn't
73
83
  * exist yet. Its value is `"1"`.
74
84
  */
@@ -236,4 +246,4 @@ function isContentWrite(path) {
236
246
  }
237
247
 
238
248
  //#endregion
239
- export { CAMOX_ACTOR, CAMOX_NAME, CHECKS_RUNNING_ERROR, CONFLICTS_CHANGED_ERROR, DRAFT_CREATE_HEADER, DRAFT_ENDED_ERROR_CODE, DRAFT_HEADER, DRAFT_SEARCH_PARAM, FINDINGS_OPEN_ERROR, LIVE_READ_ONLY_ERROR, LOCALE_HEADER, LOOK_BLOCKS_PARAM, LOOK_BLOCK_DEFINITION_PARAM, LOOK_CONTENT_PARAM, LOOK_DOCUMENT_PATH, LOOK_LAYOUT_DEFINITION_PARAM, LOOK_LOCALE_PARAM, LOOK_PATH_PARAM, LOOK_PLACEHOLDER_HEIGHTS, LOOK_PLACEHOLDER_PARAM, LOOK_SETTINGS_PARAM, LOOK_TOKEN_COOKIE, LOOK_TOKEN_HEADER, NOT_PUBLISHED_CODES, PUBLISHING_DRAFT_HEADER, PUBLISHING_DRAFT_VAR, PUBLISH_ABORT_REASONS, PUBLISH_BLOCKED_ERROR, REVISION_CHANGED_ERROR, TRANSLATIONS_PENDING_ERROR, UNPLACED_BLOCK_ID, isContentWrite };
249
+ export { ARTIFACT_FIELD_ATTRIBUTE, ARTIFACT_ITEM_ATTRIBUTE, CAMOX_ACTOR, CAMOX_NAME, CHECKS_RUNNING_ERROR, CONFLICTS_CHANGED_ERROR, DRAFT_CREATE_HEADER, DRAFT_ENDED_ERROR_CODE, DRAFT_HEADER, DRAFT_SEARCH_PARAM, FINDINGS_OPEN_ERROR, LIVE_READ_ONLY_ERROR, LOCALE_HEADER, LOOK_ARTIFACT_CAPTURE, LOOK_BLOCKS_PARAM, LOOK_BLOCK_DEFINITION_PARAM, LOOK_CONTENT_PARAM, LOOK_DOCUMENT_PATH, LOOK_LAYOUT_DEFINITION_PARAM, LOOK_LOCALE_PARAM, LOOK_PATH_PARAM, LOOK_PLACEHOLDER_HEIGHTS, LOOK_PLACEHOLDER_PARAM, LOOK_SETTINGS_PARAM, LOOK_TOKEN_COOKIE, LOOK_TOKEN_HEADER, NOT_PUBLISHED_CODES, PUBLISHING_DRAFT_HEADER, PUBLISHING_DRAFT_VAR, PUBLISH_ABORT_REASONS, PUBLISH_BLOCKED_ERROR, REVISION_CHANGED_ERROR, TRANSLATIONS_PENDING_ERROR, UNPLACED_BLOCK_ID, isContentWrite, lookArtifactSelector };
package/dist/index.d.ts CHANGED
@@ -17,6 +17,29 @@ export declare function artifactTitle(source: string): string;
17
17
  */
18
18
  export declare function artifactSourceHash(source: string): Promise<string>;
19
19
  //#endregion
20
+ //#region src/empty-values.d.ts
21
+ /**
22
+ * What a field holds before anyone writes it: the empty value of its kind. Fields declare no
23
+ * default, so content missing a field, or holding a value of the wrong kind, reads as this.
24
+ * Shared by the SDK (rendering, new repeatable items, authoring forms) and the API (creating
25
+ * content, conforming it on read), so both agree on what an unset field is.
26
+ */
27
+ /** A link that goes nowhere yet. */
28
+ export declare const EMPTY_LINK: {
29
+ readonly type: "external";
30
+ readonly text: "";
31
+ readonly href: "";
32
+ readonly newTab: false;
33
+ };
34
+ /**
35
+ * The empty value of a field, from its stored JSON Schema (`fieldType` and, for an enum, its
36
+ * options): `""` for text, a link to nowhere, `false`, an enum's first option, `null` for one
37
+ * asset or record, and `[]` for lists and repeaters. A fresh copy each call.
38
+ */
39
+ export declare function emptyFieldValue(fieldSchema: unknown): unknown;
40
+ /** The empty value of every field of a content or settings schema, by name. */
41
+ export declare function emptyFieldValues(schema: unknown): Record<string, unknown>;
42
+ //#endregion
20
43
  //#region src/record-templates.d.ts
21
44
  /**
22
45
  * A collection's `toTitle` and `toDescription` are record templates: text with `{{field}}` tokens
package/dist/index.js CHANGED
@@ -43,6 +43,51 @@ async function artifactSourceHash(source) {
43
43
  return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
44
44
  }
45
45
 
46
+ //#endregion
47
+ //#region src/empty-values.ts
48
+ /**
49
+ * What a field holds before anyone writes it: the empty value of its kind. Fields declare no
50
+ * default, so content missing a field, or holding a value of the wrong kind, reads as this.
51
+ * Shared by the SDK (rendering, new repeatable items, authoring forms) and the API (creating
52
+ * content, conforming it on read), so both agree on what an unset field is.
53
+ */
54
+ /** A link that goes nowhere yet. */
55
+ const EMPTY_LINK = {
56
+ type: "external",
57
+ text: "",
58
+ href: "",
59
+ newTab: false
60
+ };
61
+ function isObject(value) {
62
+ return typeof value === "object" && value !== null && !Array.isArray(value);
63
+ }
64
+ /**
65
+ * The empty value of a field, from its stored JSON Schema (`fieldType` and, for an enum, its
66
+ * options): `""` for text, a link to nowhere, `false`, an enum's first option, `null` for one
67
+ * asset or record, and `[]` for lists and repeaters. A fresh copy each call.
68
+ */
69
+ function emptyFieldValue(fieldSchema) {
70
+ const schema = isObject(fieldSchema) ? fieldSchema : {};
71
+ switch (schema.fieldType) {
72
+ case "Link": return { ...EMPTY_LINK };
73
+ case "Boolean": return false;
74
+ case "Enum": return (Array.isArray(schema.enum) ? schema.enum : []).find((option) => typeof option === "string") ?? "";
75
+ case "Image":
76
+ case "File":
77
+ case "Reference": return null;
78
+ case "ImageList":
79
+ case "FileList":
80
+ case "ReferenceList":
81
+ case "Repeater": return [];
82
+ default: return "";
83
+ }
84
+ }
85
+ /** The empty value of every field of a content or settings schema, by name. */
86
+ function emptyFieldValues(schema) {
87
+ const properties = isObject(schema) && isObject(schema.properties) ? schema.properties : {};
88
+ return Object.fromEntries(Object.entries(properties).map(([name, field]) => [name, emptyFieldValue(field)]));
89
+ }
90
+
46
91
  //#endregion
47
92
  //#region src/record-templates.ts
48
93
  /**
@@ -144,4 +189,4 @@ function isSettingCondition(value) {
144
189
  }
145
190
 
146
191
  //#endregion
147
- export { ARTICLE_TYPES, ICON_CATALOG_VERSION, PAGE_TEXT_LINK_PREFIX, QUERY_LIMIT_CAP, SYSTEM_ORDER_KEYS, UNTITLED_ARTIFACT, artifactSourceHash, artifactSourceTitle, artifactTitle, generateUuidv7, isPageUrlMarker, isQueryBacked, isSettingCondition, isSystemOrderKey, linkedRecordId, nonStringTemplateFields, projectIdSchema, recordLink, recordText, referenceQueryProblem, scopeIconSvgIds, templateFields, uuidv7JsonSchema, uuidv7Schema, validateIconValue };
192
+ export { ARTICLE_TYPES, EMPTY_LINK, ICON_CATALOG_VERSION, PAGE_TEXT_LINK_PREFIX, QUERY_LIMIT_CAP, SYSTEM_ORDER_KEYS, UNTITLED_ARTIFACT, artifactSourceHash, artifactSourceTitle, artifactTitle, emptyFieldValue, emptyFieldValues, generateUuidv7, isPageUrlMarker, isQueryBacked, isSettingCondition, isSystemOrderKey, linkedRecordId, nonStringTemplateFields, projectIdSchema, recordLink, recordText, referenceQueryProblem, scopeIconSvgIds, templateFields, uuidv7JsonSchema, uuidv7Schema, validateIconValue };
package/dist/router.d.ts CHANGED
@@ -2279,8 +2279,8 @@ declare const schema: {
2279
2279
  }, {}, {
2280
2280
  length: number | undefined;
2281
2281
  }>;
2282
- overviewHeadline: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2283
- name: "overview_headline";
2282
+ openingRequest: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2283
+ name: "opening_request";
2284
2284
  tableName: "drafts";
2285
2285
  dataType: "string";
2286
2286
  columnType: "SQLiteText";
@@ -2298,8 +2298,8 @@ declare const schema: {
2298
2298
  }, {}, {
2299
2299
  length: number | undefined;
2300
2300
  }>;
2301
- overviewName: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2302
- name: "overview_name";
2301
+ overviewHeadline: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2302
+ name: "overview_headline";
2303
2303
  tableName: "drafts";
2304
2304
  dataType: "string";
2305
2305
  columnType: "SQLiteText";
@@ -4728,40 +4728,6 @@ declare const schema: {
4728
4728
  identity: undefined;
4729
4729
  generated: undefined;
4730
4730
  }, {}, {}>;
4731
- defaultContent: import("drizzle-orm/sqlite-core").SQLiteColumn<{
4732
- name: "default_content";
4733
- tableName: "block_definitions";
4734
- dataType: "json";
4735
- columnType: "SQLiteTextJson";
4736
- data: unknown;
4737
- driverParam: string;
4738
- notNull: false;
4739
- hasDefault: false;
4740
- isPrimaryKey: false;
4741
- isAutoincrement: false;
4742
- hasRuntimeDefault: false;
4743
- enumValues: undefined;
4744
- baseColumn: never;
4745
- identity: undefined;
4746
- generated: undefined;
4747
- }, {}, {}>;
4748
- defaultSettings: import("drizzle-orm/sqlite-core").SQLiteColumn<{
4749
- name: "default_settings";
4750
- tableName: "block_definitions";
4751
- dataType: "json";
4752
- columnType: "SQLiteTextJson";
4753
- data: unknown;
4754
- driverParam: string;
4755
- notNull: false;
4756
- hasDefault: false;
4757
- isPrimaryKey: false;
4758
- isAutoincrement: false;
4759
- hasRuntimeDefault: false;
4760
- enumValues: undefined;
4761
- baseColumn: never;
4762
- identity: undefined;
4763
- generated: undefined;
4764
- }, {}, {}>;
4765
4731
  layoutOnly: import("drizzle-orm/sqlite-core").SQLiteColumn<{
4766
4732
  name: "layout_only";
4767
4733
  tableName: "block_definitions";
@@ -9934,11 +9900,9 @@ declare const TRANSLATED_ENTITIES: readonly ["page", "block", "sharedContent", "
9934
9900
  type TranslatedEntity = (typeof TRANSLATED_ENTITIES)[number];
9935
9901
  /**
9936
9902
  * A job translating a default-locale entity into one locale of a draft (ADR 0015). Jobs are keyed
9937
- * by entity, locale, draft and source, and debounced like generation jobs. A job translating
9938
- * `defaults` translates the code-owned defaults of a block placed in that locale instead, or of
9939
- * the shared content a synced block created there, which have no default-locale source. A job
9940
- * merging with `live` merges the draft's localization with Live's, which Live changed since the
9941
- * draft copied it, so it's no longer a conflict (ADR 0016).
9903
+ * by entity, locale, draft and source, and debounced like generation jobs. A job merging with
9904
+ * `live` merges the draft's localization with Live's, which Live changed since the draft copied
9905
+ * it, so it's no longer a conflict (ADR 0016).
9942
9906
  */
9943
9907
  type TranslationJob = {
9944
9908
  kind: "translation";
@@ -9946,7 +9910,7 @@ type TranslationJob = {
9946
9910
  entityId: string;
9947
9911
  locale: string;
9948
9912
  draftId: string;
9949
- source?: "defaults" | "live";
9913
+ source?: "live";
9950
9914
  };
9951
9915
  /**
9952
9916
  * A job writing a draft's Draft Overview from its changes, keyed by draft: a newer request
@@ -10243,8 +10207,6 @@ type Field = FieldBase & ({
10243
10207
  itemContentSchema: unknown;
10244
10208
  /** The settings schema of the repeater's items, if they have settings. */
10245
10209
  itemSettingsSchema: unknown;
10246
- /** How many items a new block starts with. */
10247
- defaultItems: number;
10248
10210
  } | {
10249
10211
  kind: "Reference";
10250
10212
  collectionId: string;
@@ -10324,7 +10286,10 @@ type DraftSummary = {
10324
10286
  createdAt: number;
10325
10287
  /** When the draft last stored a change, or null while it stores nothing. */
10326
10288
  lastChangedAt: number | null;
10327
- /** How many entities the draft changes: the length of its publish preview's changes. */
10289
+ /**
10290
+ * How many changes the draft has: the entities its publish preview lists, and its code as one
10291
+ * more once the agent changed it, as publishing ships it.
10292
+ */
10328
10293
  changeCount: number;
10329
10294
  /** The tags of the locales the draft has pending translations into (ADR 0015). */
10330
10295
  translatingLocales: string[];
@@ -10376,12 +10341,10 @@ type Overview =
10376
10341
  } |
10377
10342
  /**
10378
10343
  * Written. A `stale` one describes an older version of the draft's changes: it's still shown
10379
- * while a new one is written. `name` is the short name AI suggests keeping the draft under,
10380
- * null for an overview written before overviews had one.
10344
+ * while a new one is written.
10381
10345
  */
10382
10346
  {
10383
10347
  status: "ready";
10384
- name: string | null;
10385
10348
  headline: string;
10386
10349
  bullets: string[];
10387
10350
  stale: boolean;
@@ -17026,8 +16989,6 @@ declare const router: {
17026
16989
  description: string;
17027
16990
  contentSchema: unknown;
17028
16991
  settingsSchema: unknown;
17029
- defaultContent: unknown;
17030
- defaultSettings: unknown;
17031
16992
  layoutOnly: boolean | null;
17032
16993
  synced: boolean;
17033
16994
  schemaHash: string;
@@ -17042,8 +17003,6 @@ declare const router: {
17042
17003
  description: string;
17043
17004
  contentSchema: unknown;
17044
17005
  settingsSchema: unknown;
17045
- defaultContent: unknown;
17046
- defaultSettings: unknown;
17047
17006
  layoutOnly: boolean | null;
17048
17007
  synced: boolean;
17049
17008
  schemaHash: string;
@@ -17771,8 +17730,6 @@ declare const router: {
17771
17730
  description: import("zod").ZodString;
17772
17731
  contentSchema: import("zod").ZodUnknown;
17773
17732
  settingsSchema: import("zod").ZodOptional<import("zod").ZodUnknown>;
17774
- defaultContent: import("zod").ZodOptional<import("zod").ZodUnknown>;
17775
- defaultSettings: import("zod").ZodOptional<import("zod").ZodUnknown>;
17776
17733
  layoutOnly: import("zod").ZodOptional<import("zod").ZodBoolean>;
17777
17734
  synced: import("zod").ZodOptional<import("zod").ZodBoolean>;
17778
17735
  }, import("zod/v4/core").$strip>>;
@@ -17791,20 +17748,10 @@ declare const router: {
17791
17748
  }, import("zod/v4/core").$strip>>;
17792
17749
  blocks: import("zod").ZodArray<import("zod").ZodPreprocess<import("zod").ZodObject<{
17793
17750
  type: import("zod").ZodString;
17794
- content: import("zod").ZodUnknown;
17795
- settings: import("zod").ZodOptional<import("zod").ZodUnknown>;
17796
17751
  slot: import("zod").ZodOptional<import("zod").ZodEnum<{
17797
17752
  after: "after";
17798
17753
  before: "before";
17799
17754
  }>>;
17800
- repeatableItems: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{
17801
- tempId: import("zod").ZodString;
17802
- parentTempId: import("zod").ZodNullable<import("zod").ZodString>;
17803
- fieldName: import("zod").ZodString;
17804
- content: import("zod").ZodUnknown;
17805
- settings: import("zod").ZodOptional<import("zod").ZodUnknown>;
17806
- position: import("zod").ZodString;
17807
- }, import("zod/v4/core").$strip>>>;
17808
17755
  }, import("zod/v4/core").$strip>, unknown>>;
17809
17756
  }, import("zod/v4/core").$strip>>;
17810
17757
  collections: import("zod").ZodArray<import("zod").ZodPipe<import("zod").ZodObject<{
@@ -17937,19 +17884,6 @@ declare const router: {
17937
17884
  }>>>;
17938
17885
  homepage: import("zod").ZodNullable<import("zod").ZodObject<{
17939
17886
  layoutId: import("zod").ZodString;
17940
- blocks: import("zod").ZodArray<import("zod").ZodObject<{
17941
- type: import("zod").ZodString;
17942
- content: import("zod").ZodUnknown;
17943
- settings: import("zod").ZodOptional<import("zod").ZodUnknown>;
17944
- repeatableItems: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{
17945
- tempId: import("zod").ZodString;
17946
- parentTempId: import("zod").ZodNullable<import("zod").ZodString>;
17947
- fieldName: import("zod").ZodString;
17948
- content: import("zod").ZodUnknown;
17949
- settings: import("zod").ZodOptional<import("zod").ZodUnknown>;
17950
- position: import("zod").ZodString;
17951
- }, import("zod/v4/core").$strip>>>;
17952
- }, import("zod/v4/core").$strip>>;
17953
17887
  }, import("zod/v4/core").$strip>>;
17954
17888
  }, import("zod/v4/core").$strip>;
17955
17889
  }, import("zod/v4/core").$strip>, import("@orpc/server").Schema<{
@@ -17973,13 +17907,12 @@ declare const router: {
17973
17907
  deletedDefinitionTypes: string[];
17974
17908
  count: number;
17975
17909
  } | null;
17976
- homepage: {
17910
+ homepage: ({
17977
17911
  created: false;
17978
17912
  } | {
17979
17913
  created: true;
17980
17914
  pageId: string;
17981
- blockCount: number;
17982
- } | null;
17915
+ }) | null;
17983
17916
  }, {
17984
17917
  blocks: {
17985
17918
  count: number;
@@ -18001,13 +17934,12 @@ declare const router: {
18001
17934
  deletedDefinitionTypes: string[];
18002
17935
  count: number;
18003
17936
  } | null;
18004
- homepage: {
17937
+ homepage: ({
18005
17938
  created: false;
18006
17939
  } | {
18007
17940
  created: true;
18008
17941
  pageId: string;
18009
- blockCount: number;
18010
- } | null;
17942
+ }) | null;
18011
17943
  }>, Record<never, never>, Record<never, never>>;
18012
17944
  };
18013
17945
  drafts: {
@@ -18126,7 +18058,6 @@ declare const router: {
18126
18058
  };
18127
18059
  }>, import("zod").ZodObject<{
18128
18060
  projectId: import("zod").ZodNumber;
18129
- name: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
18130
18061
  }, import("zod/v4/core").$strip>, import("@orpc/server").Schema<DraftSummary, DraftSummary>, Record<never, never>, Record<never, never>>;
18131
18062
  get: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<import("@orpc/server").MergedInitialContext<import("@orpc/server").MergedInitialContext<BaseContext & Record<never, never>, BaseContext, BaseContext>, import("@orpc/server").MergedCurrentContext<BaseContext, Record<never, never>>, import("@orpc/server").MergedCurrentContext<BaseContext, Record<never, never>>>, import("@orpc/server").MergedCurrentContext<import("@orpc/server").MergedCurrentContext<BaseContext, Record<never, never>>, {
18132
18063
  draft: DraftRequest | null;
@@ -18179,58 +18110,6 @@ declare const router: {
18179
18110
  }>, import("zod").ZodObject<{
18180
18111
  projectId: import("zod").ZodNumber;
18181
18112
  }, import("zod/v4/core").$strip>, import("@orpc/server").Schema<DraftSummary, DraftSummary>, Record<never, never>, Record<never, never>>;
18182
- rename: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<import("@orpc/server").MergedInitialContext<import("@orpc/server").MergedInitialContext<BaseContext & Record<never, never>, BaseContext, BaseContext>, import("@orpc/server").MergedCurrentContext<BaseContext, Record<never, never>>, import("@orpc/server").MergedCurrentContext<BaseContext, Record<never, never>>>, import("@orpc/server").MergedCurrentContext<import("@orpc/server").MergedCurrentContext<BaseContext, Record<never, never>>, {
18183
- draft: DraftRequest | null;
18184
- locale: string | undefined;
18185
- publishingDraft: string | undefined;
18186
- look: LookGrant | undefined;
18187
- }>, import("@orpc/server").MergedCurrentContext<import("@orpc/server").MergedCurrentContext<BaseContext, Record<never, never>>, {
18188
- draft: DraftRequest | null;
18189
- locale: string | undefined;
18190
- publishingDraft: string | undefined;
18191
- look: LookGrant | undefined;
18192
- }>>, import("@orpc/server").MergedCurrentContext<import("@orpc/server").MergedCurrentContext<import("@orpc/server").MergedCurrentContext<BaseContext, Record<never, never>>, {
18193
- draft: DraftRequest | null;
18194
- locale: string | undefined;
18195
- publishingDraft: string | undefined;
18196
- look: LookGrant | undefined;
18197
- }>, {
18198
- db: Database;
18199
- env: Bindings;
18200
- headers: Headers;
18201
- client: string;
18202
- telemetryDisabled: boolean;
18203
- waitUntil: (promise: Promise<unknown>) => void;
18204
- apiOrigin?: string;
18205
- draft: DraftRequest | null;
18206
- locale: string | undefined;
18207
- publishingDraft: string | undefined;
18208
- look: LookGrant | undefined;
18209
- user: {
18210
- id: string;
18211
- createdAt: Date;
18212
- updatedAt: Date;
18213
- email: string;
18214
- emailVerified: boolean;
18215
- name: string;
18216
- image?: string | null | undefined;
18217
- };
18218
- session: {
18219
- id: string;
18220
- createdAt: Date;
18221
- updatedAt: Date;
18222
- userId: string;
18223
- expiresAt: Date;
18224
- token: string;
18225
- ipAddress?: string | null | undefined;
18226
- userAgent?: string | null | undefined;
18227
- activeOrganizationId?: string | null | undefined;
18228
- studio?: boolean | null | undefined;
18229
- };
18230
- }>, import("zod").ZodObject<{
18231
- projectId: import("zod").ZodNumber;
18232
- name: import("zod").ZodNullable<import("zod").ZodString>;
18233
- }, import("zod/v4/core").$strip>, import("@orpc/server").Schema<DraftSummary, DraftSummary>, Record<never, never>, Record<never, never>>;
18234
18113
  previewPublish: import("@orpc/server").DecoratedProcedure<import("@orpc/server").MergedInitialContext<import("@orpc/server").MergedInitialContext<import("@orpc/server").MergedInitialContext<BaseContext & Record<never, never>, BaseContext, BaseContext>, import("@orpc/server").MergedCurrentContext<BaseContext, Record<never, never>>, import("@orpc/server").MergedCurrentContext<BaseContext, Record<never, never>>>, import("@orpc/server").MergedCurrentContext<import("@orpc/server").MergedCurrentContext<BaseContext, Record<never, never>>, {
18235
18114
  draft: DraftRequest | null;
18236
18115
  locale: string | undefined;
@@ -18314,7 +18193,6 @@ declare const router: {
18314
18193
  status: "pending";
18315
18194
  } | {
18316
18195
  status: "ready";
18317
- name: string | null;
18318
18196
  headline: string;
18319
18197
  bullets: string[];
18320
18198
  stale: boolean;
@@ -18357,7 +18235,6 @@ declare const router: {
18357
18235
  status: "pending";
18358
18236
  } | {
18359
18237
  status: "ready";
18360
- name: string | null;
18361
18238
  headline: string;
18362
18239
  bullets: string[];
18363
18240
  stale: boolean;
package/dist/threads.d.ts CHANGED
@@ -34,8 +34,8 @@ export declare const BUILD_STATUS_ENTRY = "camox.build-status";
34
34
  export declare const RECONCILE_ENTRY = "camox.reconcile";
35
35
  /**
36
36
  * The kind of the transcript entry opening a draft's thread a Live thread handed a request off
37
- * to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
38
- * messages. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
37
+ * to: the Brief, which the model reads ahead of the editor's messages, and editors see collapsed
38
+ * after them, so the request reads on as they sent it. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
39
39
  * too, with their `operationId` as data, and start the agent's run, which acts for Camox.
40
40
  */
41
41
  export declare const BRIEF_ENTRY = "camox.brief";
@@ -48,8 +48,8 @@ export declare const DRAFT_ENDED_ENTRY = "camox.draft-ended";
48
48
  /**
49
49
  * The kind of the transcript entry a Live thread starts afresh from once its agent handed a
50
50
  * request off to a draft (ADR 0036): a reset of Pi's context, with a `startedDraftCardSchema` as
51
- * data. Editors see it as the draft's card, alone, and the model reads a note naming the draft.
52
- * Everything before it is left out of both.
51
+ * data. Editors see an empty thread, and the model reads a note naming the draft. Everything
52
+ * before it is left out of both.
53
53
  */
54
54
  export declare const DRAFT_CARD_ENTRY = "camox.draft-card";
55
55
  /**
@@ -330,6 +330,15 @@ export type ThreadMessage = {
330
330
  from: "camox";
331
331
  text: string;
332
332
  } |
333
+ /**
334
+ * The Brief the agent on Live wrote as it handed the request off to the draft, shown after the
335
+ * editor's messages it opened the thread with, though the model reads it before them.
336
+ */
337
+ {
338
+ id: string;
339
+ from: "brief";
340
+ text: string;
341
+ } |
333
342
  /** The agent's final message of a turn, in Markdown. */
334
343
  {
335
344
  id: string;
@@ -470,6 +479,11 @@ type ThreadStep = {
470
479
  {
471
480
  type: "camox";
472
481
  text: string;
482
+ } |
483
+ /** The Brief opening a draft's thread a Live thread handed a request off to. */
484
+ {
485
+ type: "brief";
486
+ text: string;
473
487
  } | {
474
488
  type: "answer";
475
489
  text: string;
package/dist/threads.js CHANGED
@@ -32,8 +32,8 @@ const BUILD_STATUS_ENTRY = "camox.build-status";
32
32
  const RECONCILE_ENTRY = "camox.reconcile";
33
33
  /**
34
34
  * The kind of the transcript entry opening a draft's thread a Live thread handed a request off
35
- * to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
36
- * messages. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
35
+ * to: the Brief, which the model reads ahead of the editor's messages, and editors see collapsed
36
+ * after them, so the request reads on as they sent it. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
37
37
  * too, with their `operationId` as data, and start the agent's run, which acts for Camox.
38
38
  */
39
39
  const BRIEF_ENTRY = "camox.brief";
@@ -46,8 +46,8 @@ const DRAFT_ENDED_ENTRY = "camox.draft-ended";
46
46
  /**
47
47
  * The kind of the transcript entry a Live thread starts afresh from once its agent handed a
48
48
  * request off to a draft (ADR 0036): a reset of Pi's context, with a `startedDraftCardSchema` as
49
- * data. Editors see it as the draft's card, alone, and the model reads a note naming the draft.
50
- * Everything before it is left out of both.
49
+ * data. Editors see an empty thread, and the model reads a note naming the draft. Everything
50
+ * before it is left out of both.
51
51
  */
52
52
  const DRAFT_CARD_ENTRY = "camox.draft-card";
53
53
  /**
@@ -375,14 +375,7 @@ function threadStepOf(entry) {
375
375
  ...push
376
376
  };
377
377
  }
378
- if (entry.kind === "camox.draft-card") {
379
- const card = startedDraftCardSchema.safeParse(entry.data).data;
380
- return card && {
381
- id,
382
- type: "draft",
383
- ...card
384
- };
385
- }
378
+ if (entry.kind === "camox.draft-card") return void 0;
386
379
  const message = entry.model?.find((candidate) => candidate.role !== "system");
387
380
  if (!message) return void 0;
388
381
  if (message.role === "user") {
@@ -407,7 +400,7 @@ function threadStepOf(entry) {
407
400
  }
408
401
  if (entry.kind === "camox.brief") return {
409
402
  id,
410
- type: "camox",
403
+ type: z.object({ operationId: z.string() }).safeParse(entry.data).success ? "camox" : "brief",
411
404
  text: textOf(message.content)
412
405
  };
413
406
  if (entry.kind === "camox.reconcile") {
@@ -474,101 +467,118 @@ function threadViewOf({ steps, running, toolCalls = [], callingTools = false, ch
474
467
  });
475
468
  endTurn();
476
469
  };
477
- for (const step of steps) switch (step.type) {
478
- case "editor":
479
- if (unfinished) fail();
480
- messages.push({
481
- id: step.id,
482
- from: "editor",
483
- sender: step.sender,
484
- text: step.text
485
- });
486
- waiting = step.id;
487
- break;
488
- case "connectedAgent":
489
- if (unfinished) fail();
490
- messages.push({
491
- id: step.id,
492
- from: "connectedAgent",
493
- member: step.member,
494
- client: step.client,
495
- text: step.text
496
- });
497
- waiting = step.id;
498
- break;
499
- case "camox":
500
- if (unfinished) fail();
501
- messages.push({
502
- id: step.id,
503
- from: "camox",
504
- text: step.text
505
- });
506
- waiting = step.id;
507
- break;
508
- case "stopped":
509
- if (waiting === void 0) break;
510
- messages.push({
511
- id: step.id,
512
- from: "stopped",
513
- sender: step.sender
514
- });
515
- endTurn();
516
- break;
517
- case "answer":
518
- if (waiting === void 0 && messages.at(-1)?.from === "stopped") break;
519
- messages.push({
520
- id: step.id,
521
- from: "agent",
522
- text: step.text
523
- });
524
- {
525
- const before = {
526
- waiting,
527
- status
470
+ /** A hand-off's Brief, shown once the editors' messages it opened the thread with are. */
471
+ let brief;
472
+ const showBrief = () => {
473
+ if (brief) messages.push(brief);
474
+ brief = void 0;
475
+ };
476
+ for (const step of steps) {
477
+ if (step.type !== "editor" && step.type !== "connectedAgent") showBrief();
478
+ switch (step.type) {
479
+ case "brief":
480
+ brief = {
481
+ id: step.id,
482
+ from: "brief",
483
+ text: step.text
528
484
  };
485
+ break;
486
+ case "editor":
487
+ if (unfinished) fail();
488
+ messages.push({
489
+ id: step.id,
490
+ from: "editor",
491
+ sender: step.sender,
492
+ text: step.text
493
+ });
494
+ waiting = step.id;
495
+ break;
496
+ case "connectedAgent":
497
+ if (unfinished) fail();
498
+ messages.push({
499
+ id: step.id,
500
+ from: "connectedAgent",
501
+ member: step.member,
502
+ client: step.client,
503
+ text: step.text
504
+ });
505
+ waiting = step.id;
506
+ break;
507
+ case "camox":
508
+ if (unfinished) fail();
509
+ messages.push({
510
+ id: step.id,
511
+ from: "camox",
512
+ text: step.text
513
+ });
514
+ waiting = step.id;
515
+ break;
516
+ case "stopped":
517
+ if (waiting === void 0) break;
518
+ messages.push({
519
+ id: step.id,
520
+ from: "stopped",
521
+ sender: step.sender
522
+ });
523
+ endTurn();
524
+ break;
525
+ case "answer":
526
+ if (waiting === void 0 && messages.at(-1)?.from === "stopped") break;
527
+ messages.push({
528
+ id: step.id,
529
+ from: "agent",
530
+ text: step.text
531
+ });
532
+ {
533
+ const before = {
534
+ waiting,
535
+ status
536
+ };
537
+ endTurn();
538
+ answered = before;
539
+ }
540
+ break;
541
+ case "draft":
542
+ if (waiting === void 0 && messages.at(-1)?.from === "stopped") break;
543
+ {
544
+ const draft = drafts[step.draftId];
545
+ messages.push({
546
+ id: step.id,
547
+ from: "draft",
548
+ draftId: step.draftId,
549
+ name: draft?.name ?? step.name,
550
+ ...draft?.ended && { ended: draft.ended }
551
+ });
552
+ }
529
553
  endTurn();
530
- answered = before;
531
- }
532
- break;
533
- case "draft":
534
- if (waiting === void 0 && messages.at(-1)?.from === "stopped") break;
535
- {
536
- const draft = drafts[step.draftId];
554
+ break;
555
+ case "continued":
556
+ if (!answered) break;
557
+ if (messages.at(-1)?.from === "agent") messages.pop();
558
+ waiting = answered.waiting;
559
+ status = answered.status === "fixing" ? "fixing" : void 0;
560
+ answered = void 0;
561
+ break;
562
+ case "build":
563
+ status = step.status;
564
+ break;
565
+ case "push":
537
566
  messages.push({
538
567
  id: step.id,
539
- from: "draft",
540
- draftId: step.draftId,
541
- name: draft?.name ?? step.name,
542
- ...draft?.ended && { ended: draft.ended }
568
+ from: "push",
569
+ sender: step.pusher,
570
+ subjects: step.subjects,
571
+ outcome: step.outcome
543
572
  });
544
- }
545
- endTurn();
546
- break;
547
- case "continued":
548
- if (!answered) break;
549
- if (messages.at(-1)?.from === "agent") messages.pop();
550
- waiting = answered.waiting;
551
- status = answered.status === "fixing" ? "fixing" : void 0;
552
- answered = void 0;
553
- break;
554
- case "build":
555
- status = step.status;
556
- break;
557
- case "push":
558
- messages.push({
559
- id: step.id,
560
- from: "push",
561
- sender: step.pusher,
562
- subjects: step.subjects,
563
- outcome: step.outcome
564
- });
565
- break;
566
- case "tools":
567
- unfinished = false;
568
- status = void 0;
569
- break;
570
- case "unfinished": unfinished = true;
573
+ break;
574
+ case "tools":
575
+ unfinished = false;
576
+ status = void 0;
577
+ break;
578
+ case "unfinished": unfinished = true;
579
+ }
571
580
  }
581
+ showBrief();
572
582
  if (!running) fail();
573
583
  if (!running) return {
574
584
  messages,
@@ -607,7 +617,7 @@ const callsChangingTool = (block) => block.type === "toolCall" && changes(block.
607
617
  const callsLook = (block) => block.type === "toolCall" && block.name === "look";
608
618
  /**
609
619
  * Adds an entry to the thread's state as Pi places it. An entry heading a new context, such as
610
- * the card a Live thread starts afresh from, leaves out every step before its head, as Pi's
620
+ * the one a Live thread starts afresh from, leaves out every step before its head, as Pi's
611
621
  * snapshots do.
612
622
  */
613
623
  const append = (state, entry) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camox/api-contract",
3
- "version": "0.69.0",
3
+ "version": "0.72.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "src",
package/src/drafts.ts CHANGED
@@ -50,9 +50,9 @@ export const LOOK_LOCALE_PARAM = "locale";
50
50
 
51
51
  /**
52
52
  * The look document's Block Definition mode: instead of a page, the block of the Block Definition
53
- * `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with its default content and
54
- * Block Settings and the overrides `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON, in
55
- * the shape the content tools take.
53
+ * `LOOK_BLOCK_DEFINITION_PARAM` names, alone in the site's shell, with empty fields and the
54
+ * content and Block Settings `LOOK_CONTENT_PARAM` and `LOOK_SETTINGS_PARAM` give as JSON merged
55
+ * on, in the shape the content tools take.
56
56
  */
57
57
  export const LOOK_BLOCK_DEFINITION_PARAM = "blockDefinition";
58
58
  export const LOOK_CONTENT_PARAM = "content";
@@ -60,8 +60,8 @@ export const LOOK_SETTINGS_PARAM = "settings";
60
60
 
61
61
  /**
62
62
  * The look document's Layout Definition mode: instead of a page, the curated Layout
63
- * `LOOK_LAYOUT_DEFINITION_PARAM` names, alone in the site's shell, its blocks with their default
64
- * content and Block Settings and the overrides `LOOK_BLOCKS_PARAM` gives as JSON, by Block
63
+ * `LOOK_LAYOUT_DEFINITION_PARAM` names, alone in the site's shell, its blocks with empty fields
64
+ * and the content and Block Settings `LOOK_BLOCKS_PARAM` gives as JSON merged on, by Block
65
65
  * Definition name (`{ navbar: { content, settings } }`), around a labelled placeholder where a
66
66
  * page's content goes, as tall as `LOOK_PLACEHOLDER_PARAM` says: one of `LOOK_PLACEHOLDER_HEIGHTS`,
67
67
  * the first without.
@@ -80,6 +80,64 @@ export type LookPlaceholderHeight = (typeof LOOK_PLACEHOLDER_HEIGHTS)[number];
80
80
  */
81
81
  export const UNPLACED_BLOCK_ID = "unplaced";
82
82
 
83
+ /**
84
+ * The attributes the website's artifact frames carry: the artifact field's name, and the id of the
85
+ * Repeatable Item holding it, when an item's. With the block's `data-camox-viewport-block`, they
86
+ * find one artifact's frame (`lookArtifactSelector`).
87
+ */
88
+ export const ARTIFACT_FIELD_ATTRIBUTE = "data-camox-artifact";
89
+ export const ARTIFACT_ITEM_ATTRIBUTE = "data-camox-item";
90
+
91
+ /** The CSS selector of the frame of an artifact field of a block, or of one of its items. */
92
+ export const lookArtifactSelector = ({
93
+ block,
94
+ item,
95
+ field,
96
+ }: {
97
+ block: string;
98
+ item?: string;
99
+ field: string;
100
+ }) =>
101
+ `[data-camox-viewport-block="${block}"] iframe[${ARTIFACT_FIELD_ATTRIBUTE}="${field}"]` +
102
+ (item ? `[${ARTIFACT_ITEM_ATTRIBUTE}="${item}"]` : `:not([${ARTIFACT_ITEM_ATTRIBUTE}])`);
103
+
104
+ /** An artifact's Timeline, as its runtime declares it: Marks by name, at times in ms. */
105
+ export type LookTimeline = {
106
+ duration: number;
107
+ loop: boolean;
108
+ marks: Record<string, number>;
109
+ /** The Mark whose frame stands for the artifact, or null. */
110
+ poster: string | null;
111
+ };
112
+
113
+ /**
114
+ * One finding of an artifact's guardrail report, about motion that escapes its Timeline or will
115
+ * look broken: a Web Animation the runtime didn't make (a CSS animation, a CSS transition that no
116
+ * visitor triggered, one the source made itself), a live timer, a Track the runtime skipped
117
+ * because it can't play it, Tracks on one element and property that overlap, or a loop cut,
118
+ * where a looping Timeline's last frame differs from its first.
119
+ */
120
+ export type LookGuardrailFinding = {
121
+ kind: "animation" | "timer" | "track" | "overlap" | "loop-cut";
122
+ /** What's wrong and what to do instead, for the Artifact Agent. */
123
+ text: string;
124
+ };
125
+
126
+ /**
127
+ * What the look document's artifact frames hold under `LOOK_ARTIFACT_CAPTURE`, for the browser
128
+ * `look` drives: the artifact's Timeline, null when it declares none; seeking it to `t` ms,
129
+ * resolved once the frame at that time painted, with the error `render` threw, if it did; and its
130
+ * guardrail report, empty when nothing escapes its Timeline. Frames of an SDK older than
131
+ * Timelines hold nothing there.
132
+ */
133
+ export type LookArtifactCapture = {
134
+ timeline(): Promise<LookTimeline | null>;
135
+ seek(t: number): Promise<{ error?: string }>;
136
+ guardrail(): Promise<LookGuardrailFinding[]>;
137
+ };
138
+
139
+ export const LOOK_ARTIFACT_CAPTURE = "camoxCapture";
140
+
83
141
  /**
84
142
  * Request header asking the API to create the draft named by `DRAFT_HEADER` if it doesn't
85
143
  * exist yet. Its value is `"1"`.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * What a field holds before anyone writes it: the empty value of its kind. Fields declare no
3
+ * default, so content missing a field, or holding a value of the wrong kind, reads as this.
4
+ * Shared by the SDK (rendering, new repeatable items, authoring forms) and the API (creating
5
+ * content, conforming it on read), so both agree on what an unset field is.
6
+ */
7
+
8
+ /** A link that goes nowhere yet. */
9
+ export const EMPTY_LINK = { type: "external", text: "", href: "", newTab: false } as const;
10
+
11
+ function isObject(value: unknown): value is Record<string, unknown> {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ }
14
+
15
+ /**
16
+ * The empty value of a field, from its stored JSON Schema (`fieldType` and, for an enum, its
17
+ * options): `""` for text, a link to nowhere, `false`, an enum's first option, `null` for one
18
+ * asset or record, and `[]` for lists and repeaters. A fresh copy each call.
19
+ */
20
+ export function emptyFieldValue(fieldSchema: unknown): unknown {
21
+ const schema = isObject(fieldSchema) ? fieldSchema : {};
22
+ switch (schema.fieldType) {
23
+ case "Link":
24
+ return { ...EMPTY_LINK };
25
+ case "Boolean":
26
+ return false;
27
+ case "Enum": {
28
+ const options = Array.isArray(schema.enum) ? schema.enum : [];
29
+ const first = options.find((option) => typeof option === "string");
30
+ return first ?? "";
31
+ }
32
+ case "Image":
33
+ case "File":
34
+ case "Reference":
35
+ return null;
36
+ case "ImageList":
37
+ case "FileList":
38
+ case "ReferenceList":
39
+ case "Repeater":
40
+ return [];
41
+ default:
42
+ return "";
43
+ }
44
+ }
45
+
46
+ /** The empty value of every field of a content or settings schema, by name. */
47
+ export function emptyFieldValues(schema: unknown): Record<string, unknown> {
48
+ const properties = isObject(schema) && isObject(schema.properties) ? schema.properties : {};
49
+ return Object.fromEntries(
50
+ Object.entries(properties).map(([name, field]) => [name, emptyFieldValue(field)]),
51
+ );
52
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./artifact.ts";
2
+ export * from "./empty-values.ts";
2
3
  export * from "./icons.ts";
3
4
  export * from "./ids.ts";
4
5
  export * from "./record-templates.ts";
package/src/threads.ts CHANGED
@@ -44,8 +44,8 @@ export const RECONCILE_ENTRY = "camox.reconcile";
44
44
 
45
45
  /**
46
46
  * The kind of the transcript entry opening a draft's thread a Live thread handed a request off
47
- * to: the Brief, which the model reads and editors see as from Camox, ahead of the editor's
48
- * messages. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
47
+ * to: the Brief, which the model reads ahead of the editor's messages, and editors see collapsed
48
+ * after them, so the request reads on as they sent it. In a draft Camox opened, such as its Patrol Draft, Camox's own messages are Briefs
49
49
  * too, with their `operationId` as data, and start the agent's run, which acts for Camox.
50
50
  */
51
51
  export const BRIEF_ENTRY = "camox.brief";
@@ -60,8 +60,8 @@ export const DRAFT_ENDED_ENTRY = "camox.draft-ended";
60
60
  /**
61
61
  * The kind of the transcript entry a Live thread starts afresh from once its agent handed a
62
62
  * request off to a draft (ADR 0036): a reset of Pi's context, with a `startedDraftCardSchema` as
63
- * data. Editors see it as the draft's card, alone, and the model reads a note naming the draft.
64
- * Everything before it is left out of both.
63
+ * data. Editors see an empty thread, and the model reads a note naming the draft. Everything
64
+ * before it is left out of both.
65
65
  */
66
66
  export const DRAFT_CARD_ENTRY = "camox.draft-card";
67
67
 
@@ -302,6 +302,11 @@ export type ThreadMessage =
302
302
  }
303
303
  /** Camox's own message, starting a turn it began itself: why it did. */
304
304
  | { id: string; from: "camox"; text: string }
305
+ /**
306
+ * The Brief the agent on Live wrote as it handed the request off to the draft, shown after the
307
+ * editor's messages it opened the thread with, though the model reads it before them.
308
+ */
309
+ | { id: string; from: "brief"; text: string }
305
310
  /** The agent's final message of a turn, in Markdown. */
306
311
  | { id: string; from: "agent"; text: string }
307
312
  /**
@@ -406,6 +411,8 @@ type ThreadStep = { id: string } & (
406
411
  | { type: "connectedAgent"; member: ThreadSender; client: ConnectedAgentClient; text: string }
407
412
  /** Camox starting a turn of its own, saying why. */
408
413
  | { type: "camox"; text: string }
414
+ /** The Brief opening a draft's thread a Live thread handed a request off to. */
415
+ | { type: "brief"; text: string }
409
416
  | { type: "answer"; text: string }
410
417
  /** A round of tool calls, or their results: the run goes on after it. */
411
418
  | { type: "tools" }
@@ -560,11 +567,8 @@ function threadStepOf(entry: EntryRecord): ThreadStep | undefined {
560
567
  const push = pushDataSchema.safeParse(entry.data).data;
561
568
  return push && { id, type: "push", ...push };
562
569
  }
563
- // The model reads a note: editors see the card it starts the Live thread afresh with.
564
- if (entry.kind === DRAFT_CARD_ENTRY) {
565
- const card = startedDraftCardSchema.safeParse(entry.data).data;
566
- return card && { id, type: "draft", ...card };
567
- }
570
+ // Only the model reads it, as a note naming the draft: editors see the Live thread start empty.
571
+ if (entry.kind === DRAFT_CARD_ENTRY) return undefined;
568
572
  const message = entry.model?.find((candidate) => candidate.role !== "system");
569
573
  if (!message) return undefined;
570
574
  if (message.role === "user") {
@@ -583,7 +587,12 @@ function threadStepOf(entry: EntryRecord): ThreadStep | undefined {
583
587
  }
584
588
  );
585
589
  }
586
- if (entry.kind === BRIEF_ENTRY) return { id, type: "camox", text: textOf(message.content) };
590
+ if (entry.kind === BRIEF_ENTRY) {
591
+ // Only Camox's own messages, in a draft it opened, carry an operation id: a hand-off's
592
+ // Brief is written ahead of the editor's messages, which start the run.
593
+ const own = z.object({ operationId: z.string() }).safeParse(entry.data).success;
594
+ return { id, type: own ? "camox" : "brief", text: textOf(message.content) };
595
+ }
587
596
  if (entry.kind === RECONCILE_ENTRY) {
588
597
  const reconcile = reconcileDataSchema.safeParse(entry.data).data;
589
598
  return reconcile && { id, type: "camox", text: reconcile.message };
@@ -648,9 +657,19 @@ export function threadViewOf({
648
657
  if (waiting !== undefined) messages.push({ id: `${waiting}:failed`, from: "failed" });
649
658
  endTurn();
650
659
  };
660
+ /** A hand-off's Brief, shown once the editors' messages it opened the thread with are. */
661
+ let brief: Extract<ThreadMessage, { from: "brief" }> | undefined;
662
+ const showBrief = () => {
663
+ if (brief) messages.push(brief);
664
+ brief = undefined;
665
+ };
651
666
 
652
667
  for (const step of steps) {
668
+ if (step.type !== "editor" && step.type !== "connectedAgent") showBrief();
653
669
  switch (step.type) {
670
+ case "brief":
671
+ brief = { id: step.id, from: "brief", text: step.text };
672
+ break;
654
673
  case "editor":
655
674
  // Pi places a steering message only once a model call is settled: after a round of tool
656
675
  // calls, or with the answer ending the run. Never between a failed call and its retry, so
@@ -738,6 +757,7 @@ export function threadViewOf({
738
757
  break;
739
758
  }
740
759
  }
760
+ showBrief();
741
761
  if (!running) fail();
742
762
  if (!running) return { messages, running };
743
763
  return {
@@ -798,7 +818,7 @@ const callsLook = (block: AssistantMessage["content"][number]) =>
798
818
 
799
819
  /**
800
820
  * Adds an entry to the thread's state as Pi places it. An entry heading a new context, such as
801
- * the card a Live thread starts afresh from, leaves out every step before its head, as Pi's
821
+ * the one a Live thread starts afresh from, leaves out every step before its head, as Pi's
802
822
  * snapshots do.
803
823
  */
804
824
  const append = (state: ThreadState, entry: EntryRecord | undefined): ThreadState => {