@agent-native/core-corpus 0.216.0 → 0.218.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.
Files changed (196) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +61 -1
  3. package/corpus/templates/analytics/AGENTS.md +1 -1
  4. package/corpus/templates/analytics/CHANGELOG.md +3 -0
  5. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +9 -65
  6. package/corpus/templates/analytics/actions/list-event-catalog.ts +1 -1
  7. package/corpus/templates/analytics/actions/list-session-outputs.ts +40 -0
  8. package/corpus/templates/analytics/actions/list-session-recordings.ts +17 -4
  9. package/corpus/templates/analytics/actions/view-screen.ts +29 -3
  10. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
  11. package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
  12. package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
  13. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
  14. package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
  15. package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
  16. package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
  17. package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  18. package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
  19. package/corpus/templates/analytics/server/db/schema.ts +67 -0
  20. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
  21. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
  22. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
  23. package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
  24. package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
  25. package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
  26. package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
  27. package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
  28. package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
  29. package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
  30. package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
  31. package/corpus/templates/analytics/server/plugins/db.ts +46 -0
  32. package/corpus/templates/analytics/shared/output-use.ts +205 -0
  33. package/corpus/templates/analytics/shared/session-events.ts +6 -0
  34. package/corpus/templates/assets/CHANGELOG.md +6 -0
  35. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
  36. package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  37. package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
  38. package/corpus/templates/brain/app/routes/sources.tsx +1 -1
  39. package/corpus/templates/brain/vitest.config.ts +7 -0
  40. package/corpus/templates/calendar/CHANGELOG.md +12 -0
  41. package/corpus/templates/calendar/actions/create-event.ts +2 -4
  42. package/corpus/templates/calendar/actions/update-event.ts +35 -6
  43. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
  44. package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
  45. package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
  46. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
  47. package/corpus/templates/clips/CHANGELOG.md +7 -20
  48. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
  49. package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
  50. package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
  51. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
  52. package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
  53. package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
  54. package/corpus/templates/community/qr/package.json +1 -1
  55. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
  56. package/corpus/templates/content/CHANGELOG.md +10 -0
  57. package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
  58. package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
  59. package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
  60. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
  61. package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
  62. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
  63. package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
  64. package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
  65. package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
  66. package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
  67. package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
  68. package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
  69. package/corpus/templates/content/actions/_database-utils.ts +93 -7
  70. package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
  71. package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
  72. package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
  73. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
  74. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
  75. package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
  76. package/corpus/templates/content/actions/delete-document.ts +41 -3
  77. package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
  78. package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
  79. package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
  80. package/corpus/templates/content/actions/get-document.ts +2 -0
  81. package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
  82. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
  83. package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
  84. package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
  85. package/corpus/templates/content/actions/update-document.ts +139 -20
  86. package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
  87. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +119 -30
  88. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +56 -13
  89. package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
  90. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
  91. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
  92. package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
  93. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
  94. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
  95. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
  96. package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
  97. package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
  98. package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
  99. package/corpus/templates/content/app/global.css +31 -0
  100. package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
  101. package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
  102. package/corpus/templates/content/app/i18n-data.ts +1 -0
  103. package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
  104. package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
  105. package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
  106. package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
  107. package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
  108. package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
  109. package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
  110. package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
  111. package/corpus/templates/content/parity/matrix.md +42 -42
  112. package/corpus/templates/content/parity/matrix.ts +3 -1
  113. package/corpus/templates/content/server/db/schema.ts +20 -0
  114. package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
  115. package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
  116. package/corpus/templates/content/server/live-copy/binding.ts +31 -0
  117. package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
  118. package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
  119. package/corpus/templates/content/server/live-copy/holds.ts +59 -0
  120. package/corpus/templates/content/server/live-copy/persist.ts +656 -0
  121. package/corpus/templates/content/server/live-copy/switch.ts +136 -0
  122. package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
  123. package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
  124. package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
  125. package/corpus/templates/content/server/live-copy/write.ts +85 -0
  126. package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
  127. package/corpus/templates/content/server/plugins/collab.ts +3 -0
  128. package/corpus/templates/content/server/plugins/db.ts +28 -0
  129. package/corpus/templates/content/shared/api.ts +30 -1
  130. package/corpus/templates/content/shared/database-item-layout.ts +54 -22
  131. package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
  132. package/corpus/templates/content/shared/og-description.ts +258 -36
  133. package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
  134. package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
  135. package/corpus/templates/content/shared/suggestion-text.ts +20 -5
  136. package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
  137. package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
  138. package/corpus/templates/design/CHANGELOG.md +5 -0
  139. package/corpus/templates/design/actions/get-getting-started.ts +53 -0
  140. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
  141. package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
  142. package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
  143. package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
  144. package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
  145. package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
  146. package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
  147. package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
  148. package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
  149. package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
  150. package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
  151. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
  152. package/corpus/templates/design/app/global.css +6 -0
  153. package/corpus/templates/design/app/i18n/en-US.ts +17 -0
  154. package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
  155. package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
  156. package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
  157. package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
  158. package/corpus/templates/design/app/pages/Index.tsx +28 -67
  159. package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
  160. package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
  161. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
  162. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
  163. package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
  164. package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
  165. package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
  166. package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
  167. package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
  168. package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
  169. package/corpus/templates/design/server/lib/getting-started.ts +267 -0
  170. package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
  171. package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
  172. package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
  173. package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
  174. package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
  175. package/corpus/templates/design/shared/design-template-presets.ts +8 -0
  176. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  177. package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
  178. package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  179. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
  180. package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
  181. package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
  182. package/corpus/templates/plan/CHANGELOG.md +6 -0
  183. package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
  184. package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
  185. package/corpus/templates/slides/CHANGELOG.md +7 -0
  186. package/corpus/templates/slides/actions/delete-deck.ts +7 -0
  187. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
  188. package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
  189. package/corpus/templates/slides/app/pages/Index.tsx +7 -2
  190. package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
  191. package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
  192. package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
  193. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
  194. package/package.json +1 -1
  195. package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
  196. package/corpus/templates/slides/app/components/deck/DeckFilterMenu.tsx +0 -83
@@ -0,0 +1,531 @@
1
+ import type { CanvasStyleKind } from "./canvas-style-kind";
2
+ import { trackDesignRelay } from "./design-relay-tracking";
3
+ import { isRejectedRestoreClaimError } from "./design-save-outbox";
4
+
5
+ /**
6
+ * Outcome counts for canvas edits. Each canvas command counts exactly one
7
+ * outcome; counts with the same operation, outcome, reason, label and design
8
+ * are summed and sent as one `canvas_edit_outcome_counts` event with a `count`,
9
+ * every 15 s, at once for the first failure of a kind, or after the page shows
10
+ * again. `design_id` is a join key, not a dimension to chart by: a page holds
11
+ * one design, so it adds no keys in practice.
12
+ */
13
+ export const CANVAS_OUTCOME_COUNTS_EVENT = "canvas_edit_outcome_counts";
14
+
15
+ export const CANVAS_OUTCOME_FLUSH_MS = 15_000;
16
+ const MAX_PENDING_KEYS = 50;
17
+
18
+ const CANVAS_OPERATIONS = [
19
+ "canvas_load",
20
+ "save",
21
+ "save_replay",
22
+ "style_commit",
23
+ "text_edit",
24
+ "text_retry",
25
+ ] as const;
26
+ export type CanvasOperation = (typeof CANVAS_OPERATIONS)[number];
27
+
28
+ const CANVAS_OUTCOMES = [
29
+ "succeeded",
30
+ "failed",
31
+ "skipped",
32
+ "deferred",
33
+ "unsettled",
34
+ ] as const;
35
+ export type CanvasOutcome = (typeof CANVAS_OUTCOMES)[number];
36
+ type ReportedOutcome = Exclude<CanvasOutcome, "unsettled">;
37
+
38
+ export const CANVAS_OUTCOME_REASONS = [
39
+ "none",
40
+ "exception",
41
+ "unknown",
42
+ "overflow",
43
+ // canvas_load
44
+ "hidden_during_load",
45
+ "stalled",
46
+ // save / save_replay
47
+ "aborted",
48
+ "conflict",
49
+ "dropped",
50
+ "error",
51
+ "invalid_html",
52
+ "offline",
53
+ "offline_queued",
54
+ "rebased",
55
+ "stale_mirror",
56
+ "superseded",
57
+ // style_commit
58
+ "client_rendered_shell",
59
+ "edit_blocked",
60
+ "empty_styles",
61
+ "integrity_failed",
62
+ "linked_edit_queued",
63
+ "linked_scope_unsupported",
64
+ "linked_source_unsupported",
65
+ "no_edit_rights",
66
+ "no_rendered_box",
67
+ "node_unresolved",
68
+ "not_persisted",
69
+ "patch_failed",
70
+ "runtime_pending",
71
+ "save_unfinished",
72
+ "selector_ambiguous",
73
+ "selector_missing",
74
+ "shader_write_failed",
75
+ "snapshot_not_loaded",
76
+ // text_edit / text_retry
77
+ "fallback_queued",
78
+ "not_written",
79
+ "refused",
80
+ ] as const;
81
+ export type CanvasOutcomeReason = (typeof CANVAS_OUTCOME_REASONS)[number];
82
+
83
+ export type CanvasLoadReadyBucket = "lt_1s" | "1_to_4s" | "gt_4s";
84
+
85
+ export interface CanvasOperationHandle {
86
+ succeed(): void;
87
+ fail(reason: CanvasOutcomeReason): void;
88
+ skip(reason: CanvasOutcomeReason): void;
89
+ defer(reason: CanvasOutcomeReason): void;
90
+ /**
91
+ * Keeps the operation open until the returned function runs. That function
92
+ * always runs its work, even after the operation was counted: tracking
93
+ * observes an edit and never decides whether it happens. An operation still
94
+ * open when the page closes is not counted, since a save in flight then is
95
+ * finished by its keepalive request.
96
+ */
97
+ continueLater(): (run: (op: CanvasOperationHandle) => void) => void;
98
+ /** Labels a `style_commit` by kind of change; ignored for other operations. */
99
+ setStyleKind(kind: CanvasStyleKind): void;
100
+ }
101
+
102
+ const KNOWN_REASONS = new Set<string>(CANVAS_OUTCOME_REASONS);
103
+
104
+ // Higher wins when one operation reports several outcomes.
105
+ const OUTCOME_RANK: Record<ReportedOutcome, number> = {
106
+ skipped: 1,
107
+ succeeded: 2,
108
+ deferred: 3,
109
+ failed: 4,
110
+ };
111
+
112
+ function normalizeReason(reason: unknown): CanvasOutcomeReason {
113
+ return typeof reason === "string" && KNOWN_REASONS.has(reason)
114
+ ? (reason as CanvasOutcomeReason)
115
+ : "unknown";
116
+ }
117
+
118
+ const UNTRACKED_HANDLE: CanvasOperationHandle = {
119
+ succeed: () => {},
120
+ fail: () => {},
121
+ skip: () => {},
122
+ defer: () => {},
123
+ setStyleKind: () => {},
124
+ continueLater: () => (run) => run(UNTRACKED_HANDLE),
125
+ };
126
+
127
+ class Operation {
128
+ private outcome: ReportedOutcome | null = null;
129
+ private reason: CanvasOutcomeReason = "none";
130
+ private styleKind: CanvasStyleKind | undefined;
131
+ private openParts = 1;
132
+ private recorded = false;
133
+
134
+ readonly handle: CanvasOperationHandle = {
135
+ succeed: () => this.report("succeeded", "none"),
136
+ fail: (reason) => this.report("failed", reason),
137
+ skip: (reason) => this.report("skipped", reason),
138
+ defer: (reason) => this.report("deferred", reason),
139
+ setStyleKind: (kind) => {
140
+ this.styleKind ??= kind;
141
+ },
142
+ continueLater: () => {
143
+ if (this.recorded) return (run) => run(UNTRACKED_HANDLE);
144
+ this.openParts += 1;
145
+ let resumed = false;
146
+ return (run) => {
147
+ if (resumed || this.recorded) return run(UNTRACKED_HANDLE);
148
+ resumed = true;
149
+ this.runPart(run);
150
+ };
151
+ },
152
+ };
153
+
154
+ constructor(
155
+ private readonly operation: CanvasOperation,
156
+ // Fixed at the start: a save can finish after the user changed design.
157
+ private readonly designId: string | undefined,
158
+ ) {}
159
+
160
+ runPart<T>(run: (op: CanvasOperationHandle) => T): T {
161
+ const previous = activeOperation;
162
+ activeOperation = this;
163
+ let result: T;
164
+ try {
165
+ result = run(this.handle);
166
+ } catch (error) {
167
+ this.report("failed", "exception");
168
+ this.releasePart();
169
+ throw error;
170
+ } finally {
171
+ activeOperation = previous;
172
+ }
173
+ if (!isPromiseLike(result)) {
174
+ this.releasePart();
175
+ return result;
176
+ }
177
+ return result.then(
178
+ (value) => {
179
+ this.releasePart();
180
+ return value;
181
+ },
182
+ (error: unknown) => {
183
+ this.report("failed", "exception");
184
+ this.releasePart();
185
+ throw error;
186
+ },
187
+ ) as T;
188
+ }
189
+
190
+ private report(outcome: ReportedOutcome, reason: CanvasOutcomeReason): void {
191
+ if (this.recorded) return;
192
+ if (this.outcome && OUTCOME_RANK[this.outcome] >= OUTCOME_RANK[outcome]) {
193
+ return;
194
+ }
195
+ this.outcome = outcome;
196
+ this.reason = outcome === "succeeded" ? "none" : normalizeReason(reason);
197
+ }
198
+
199
+ private releasePart(): void {
200
+ this.openParts -= 1;
201
+ if (this.openParts > 0 || this.recorded) return;
202
+ this.recorded = true;
203
+ recordCanvasOutcome(
204
+ this.operation,
205
+ this.outcome ?? "unsettled",
206
+ this.outcome ? this.reason : "none",
207
+ { designId: this.designId, styleKind: this.styleKind },
208
+ );
209
+ }
210
+ }
211
+
212
+ function isPromiseLike<T>(value: T): value is T & PromiseLike<unknown> {
213
+ return (
214
+ typeof value === "object" &&
215
+ value !== null &&
216
+ typeof (value as { then?: unknown }).then === "function"
217
+ );
218
+ }
219
+
220
+ let activeOperation: Operation | null = null;
221
+
222
+ /**
223
+ * Runs one canvas command and counts exactly one outcome for it. A command
224
+ * that runs inside another one reports into the outer operation, so a nudge
225
+ * that commits styles counts once. Nesting is tracked only synchronously:
226
+ * work after an `await` or a timer is its own operation unless it is resumed
227
+ * through `continueLater()`.
228
+ */
229
+ export function runCanvasOperation<T>(
230
+ operation: CanvasOperation,
231
+ run: (op: CanvasOperationHandle) => T,
232
+ ): T {
233
+ if (activeOperation) return run(activeOperation.handle);
234
+ return new Operation(operation, currentDesignId).runPart(run);
235
+ }
236
+
237
+ /**
238
+ * Like `runCanvasOperation`, but never joins an operation already running.
239
+ * For work that outlives the edit that started it, such as a queued save,
240
+ * which also passes the design it was queued for.
241
+ */
242
+ export function runDetachedCanvasOperation<T>(
243
+ operation: CanvasOperation,
244
+ run: (op: CanvasOperationHandle) => T,
245
+ options: { designId?: string } = {},
246
+ ): T {
247
+ const previous = activeOperation;
248
+ activeOperation = null;
249
+ try {
250
+ const designId = "designId" in options ? options.designId : currentDesignId;
251
+ return new Operation(operation, designId).runPart(run);
252
+ } finally {
253
+ activeOperation = previous;
254
+ }
255
+ }
256
+
257
+ interface PendingCount {
258
+ operation: CanvasOperation | "overflow";
259
+ outcome: CanvasOutcome;
260
+ reason: CanvasOutcomeReason;
261
+ readyBucket?: CanvasLoadReadyBucket;
262
+ styleKind?: CanvasStyleKind;
263
+ designId: string | undefined;
264
+ count: number;
265
+ }
266
+
267
+ const pendingCounts = new Map<string, PendingCount>();
268
+ const failuresSentThisPage = new Set<string>();
269
+ let windowStartedAt: number | null = null;
270
+ let flushTimer: ReturnType<typeof setTimeout> | null = null;
271
+ let listenersInstalled = false;
272
+ let currentDesignId: string | undefined;
273
+ let viewerSignedIn: boolean | undefined;
274
+
275
+ /** Counts outcomes directly, for boundaries that already know them. */
276
+ export function recordCanvasOutcome(
277
+ operation: CanvasOperation,
278
+ outcome: CanvasOutcome,
279
+ reason: CanvasOutcomeReason,
280
+ options: {
281
+ count?: number;
282
+ readyBucket?: CanvasLoadReadyBucket;
283
+ styleKind?: CanvasStyleKind;
284
+ designId?: string;
285
+ } = {},
286
+ ): void {
287
+ // Tracking runs inside edit and save paths, so it must never throw into them.
288
+ try {
289
+ const count = options.count ?? 1;
290
+ if (!Number.isInteger(count) || count <= 0) return;
291
+ const normalizedReason = normalizeReason(reason);
292
+ const { key, entry } = addCount(
293
+ {
294
+ operation,
295
+ outcome,
296
+ reason: normalizedReason,
297
+ readyBucket:
298
+ operation === "canvas_load" ? options.readyBucket : undefined,
299
+ styleKind: operation === "style_commit" ? options.styleKind : undefined,
300
+ designId: "designId" in options ? options.designId : currentDesignId,
301
+ },
302
+ count,
303
+ );
304
+ installListeners();
305
+
306
+ const failureKey = `${operation}|${normalizedReason}`;
307
+ if (
308
+ (outcome === "failed" || outcome === "unsettled") &&
309
+ entry.operation !== "overflow" &&
310
+ !failuresSentThisPage.has(failureKey)
311
+ ) {
312
+ failuresSentThisPage.add(failureKey);
313
+ sendKeys([key]);
314
+ }
315
+ armFlushTimer();
316
+ } catch {
317
+ // coercion-ok: losing a count is preferable to failing the edit it describes.
318
+ }
319
+ }
320
+
321
+ function addCount(
322
+ fields: Omit<PendingCount, "count">,
323
+ count: number,
324
+ ): { key: string; entry: PendingCount } {
325
+ let key = [
326
+ fields.operation,
327
+ fields.outcome,
328
+ fields.reason,
329
+ fields.readyBucket ?? "",
330
+ fields.styleKind ?? "",
331
+ fields.designId ?? "",
332
+ ].join("|");
333
+ let entry = pendingCounts.get(key);
334
+ if (!entry && pendingCounts.size >= MAX_PENDING_KEYS) {
335
+ // The overflow key keeps the outcome so the success rate stays honest.
336
+ key = `overflow|${fields.outcome}|${fields.designId ?? ""}`;
337
+ entry = pendingCounts.get(key);
338
+ fields = {
339
+ operation: "overflow",
340
+ outcome: fields.outcome,
341
+ reason: "overflow",
342
+ designId: fields.designId,
343
+ };
344
+ }
345
+ if (!entry) {
346
+ entry = {
347
+ operation: fields.operation,
348
+ outcome: fields.outcome,
349
+ reason: fields.reason,
350
+ ...(fields.readyBucket ? { readyBucket: fields.readyBucket } : {}),
351
+ ...(fields.styleKind ? { styleKind: fields.styleKind } : {}),
352
+ designId: fields.designId,
353
+ count: 0,
354
+ };
355
+ pendingCounts.set(key, entry);
356
+ }
357
+ entry.count += count;
358
+ windowStartedAt ??= Date.now();
359
+ return { key, entry };
360
+ }
361
+
362
+ function newBatchId(): string {
363
+ return (
364
+ globalThis.crypto?.randomUUID?.() ??
365
+ `${Date.now()}-${Math.random().toString(36).slice(2)}`
366
+ );
367
+ }
368
+
369
+ // Nothing is sent while the page is hidden: the editor's own keepalive save
370
+ // runs at close, keepalive requests share one 64 KiB budget, and a count sent
371
+ // then could get that save rejected. Counts wait in memory until the page
372
+ // shows again. A tab hides before it closes, so closing it loses what is
373
+ // pending: at most one window, never a failure already sent at once.
374
+ function sendKeys(keys: string[]): void {
375
+ if (pageIsHidden()) return;
376
+ // The relay needs a session, so counts wait until the viewer is known; a
377
+ // signed-out viewer's go through the browser beacon instead.
378
+ if (viewerSignedIn === undefined) return;
379
+ const windowMs = windowStartedAt === null ? 0 : Date.now() - windowStartedAt;
380
+ const batchId = newBatchId();
381
+ for (const entry of keys.flatMap((key) => pendingCounts.get(key) ?? [])) {
382
+ trackDesignRelay(
383
+ CANVAS_OUTCOME_COUNTS_EVENT,
384
+ {
385
+ operation: entry.operation,
386
+ outcome: entry.outcome,
387
+ reason: entry.reason,
388
+ ...(entry.readyBucket ? { ready_bucket: entry.readyBucket } : {}),
389
+ ...(entry.styleKind ? { style_kind: entry.styleKind } : {}),
390
+ count: entry.count,
391
+ ...(entry.designId ? { design_id: entry.designId } : {}),
392
+ batch_id: batchId,
393
+ window_ms: windowMs,
394
+ },
395
+ { signedIn: viewerSignedIn },
396
+ );
397
+ }
398
+ clearPending(keys);
399
+ }
400
+
401
+ function clearPending(keys: string[]): void {
402
+ for (const key of keys) pendingCounts.delete(key);
403
+ if (pendingCounts.size > 0) return;
404
+ windowStartedAt = null;
405
+ if (flushTimer) clearTimeout(flushTimer);
406
+ flushTimer = null;
407
+ }
408
+
409
+ function armFlushTimer(): void {
410
+ if (pendingCounts.size === 0 || flushTimer) return;
411
+ flushTimer = setTimeout(() => {
412
+ flushTimer = null;
413
+ flushCanvasOutcomeCounts();
414
+ }, CANVAS_OUTCOME_FLUSH_MS);
415
+ }
416
+
417
+ /** Sends every pending count now. Safe to call when nothing is pending. */
418
+ export function flushCanvasOutcomeCounts(): void {
419
+ try {
420
+ if (pendingCounts.size > 0) sendKeys([...pendingCounts.keys()]);
421
+ } catch {
422
+ // coercion-ok: losing a count is preferable to failing the page.
423
+ }
424
+ }
425
+
426
+ function pageIsHidden(): boolean {
427
+ return (
428
+ typeof document !== "undefined" && document.visibilityState === "hidden"
429
+ );
430
+ }
431
+
432
+ function handleVisibilityChange(): void {
433
+ if (!pageIsHidden()) flushCanvasOutcomeCounts();
434
+ }
435
+
436
+ function installListeners(): void {
437
+ if (listenersInstalled) return;
438
+ try {
439
+ document.addEventListener("visibilitychange", handleVisibilityChange);
440
+ listenersInstalled = true;
441
+ } catch {
442
+ // coercion-ok: without listeners counts still go out on the timer.
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Who later counts belong to. `signedIn` is undefined while the session loads,
448
+ * and counts wait for it. Counts already pending keep the design they were
449
+ * recorded under.
450
+ */
451
+ export function setCanvasOutcomeContext(context: {
452
+ designId: string | undefined;
453
+ signedIn: boolean | undefined;
454
+ }): void {
455
+ try {
456
+ currentDesignId = context.designId;
457
+ viewerSignedIn = context.signedIn;
458
+ installListeners();
459
+ armFlushTimer();
460
+ } catch {
461
+ // coercion-ok: tracking context must never break the editor.
462
+ }
463
+ }
464
+
465
+ /** The design new counts belong to, for work that reports later. */
466
+ export function currentCanvasOutcomeDesignId(): string | undefined {
467
+ return currentDesignId;
468
+ }
469
+
470
+ /**
471
+ * The editor closed its design: send what is pending and stop attributing new
472
+ * counts to it. The viewer stays known, so late saves still go out.
473
+ */
474
+ export function clearCanvasOutcomeDesign(): void {
475
+ currentDesignId = undefined;
476
+ flushCanvasOutcomeCounts();
477
+ }
478
+
479
+ /**
480
+ * Counts the file saves one outbox drain finished. The outbox also replays
481
+ * frame geometry and tweaks, which are not canvas saves. Entries that failed
482
+ * stay queued and return on the next drain, which runs on every return to the
483
+ * tab, so counting them would repeat one edit. These are replay requests,
484
+ * not edits: a drain does not wait for a live save of the same entry, so an
485
+ * edit can be sent by both and count under both `save` and `save_replay`.
486
+ */
487
+ export function recordSaveReplayOutcomes(
488
+ result: {
489
+ saved: ReadonlyArray<{ actionName?: string }>;
490
+ rebased: ReadonlyArray<{ entry: { actionName?: string }; error: unknown }>;
491
+ dropped: ReadonlyArray<{ entry: { actionName?: string } }>;
492
+ },
493
+ designId: string | undefined,
494
+ ): void {
495
+ const fileSaves = (entries: ReadonlyArray<{ actionName?: string }>) =>
496
+ entries.filter((entry) => entry.actionName === "update-file").length;
497
+ const replays = [
498
+ ["succeeded", "none", fileSaves(result.saved)],
499
+ [
500
+ "failed",
501
+ "rebased",
502
+ // A rejected restore claim is reconciled and replayed again, so only a
503
+ // save the server's newer content replaced is a final rebase.
504
+ fileSaves(
505
+ result.rebased
506
+ .filter(({ error }) => !isRejectedRestoreClaimError(error))
507
+ .map(({ entry }) => entry),
508
+ ),
509
+ ],
510
+ ["failed", "dropped", fileSaves(result.dropped.map(({ entry }) => entry))],
511
+ ] as const;
512
+ for (const [outcome, reason, count] of replays) {
513
+ recordCanvasOutcome("save_replay", outcome, reason, { count, designId });
514
+ }
515
+ }
516
+
517
+ /** Resets module state; the viewer starts unknown, as on a fresh page. */
518
+ export function __resetCanvasOutcomeTrackingForTests(): void {
519
+ if (listenersInstalled) {
520
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
521
+ listenersInstalled = false;
522
+ }
523
+ if (flushTimer) clearTimeout(flushTimer);
524
+ flushTimer = null;
525
+ pendingCounts.clear();
526
+ failuresSentThisPage.clear();
527
+ activeOperation = null;
528
+ windowStartedAt = null;
529
+ currentDesignId = undefined;
530
+ viewerSignedIn = undefined;
531
+ }
@@ -0,0 +1,50 @@
1
+ export type CanvasStyleKind =
2
+ | "position"
3
+ | "size"
4
+ | "fill"
5
+ | "typography"
6
+ | "spacing"
7
+ | "effects"
8
+ | "mixed"
9
+ | "other";
10
+
11
+ type SingleStyleKind = Exclude<CanvasStyleKind, "mixed">;
12
+
13
+ const STYLE_KIND_RULES: Array<[RegExp, SingleStyleKind]> = [
14
+ [
15
+ /^(left|top|right|bottom|inset.*|position|transform|translate|z-index)$/,
16
+ "position",
17
+ ],
18
+ [/^((min-|max-)?(width|height)|aspect-ratio|scale)$/, "size"],
19
+ [/^(background.*|color|fill|stroke.*|border(-[a-z]+)*-color)$/, "fill"],
20
+ [
21
+ /^(font.*|line-height|letter-spacing|text-.*|white-space|word-.*)$/,
22
+ "typography",
23
+ ],
24
+ [/^(padding.*|margin.*|gap|row-gap|column-gap)$/, "spacing"],
25
+ [
26
+ /^(opacity|box-shadow|filter|backdrop-filter|border(-[a-z]+)*-radius|mix-blend-mode|rotate)$/,
27
+ "effects",
28
+ ],
29
+ ];
30
+
31
+ /**
32
+ * The kind of change a style write is, from its property names alone; values
33
+ * and selectors never leave. A corner resize also moves the box, so position
34
+ * plus size is still a resize.
35
+ */
36
+ export function canvasStyleKind(properties: Iterable<string>): CanvasStyleKind {
37
+ const kinds = new Set<SingleStyleKind>();
38
+ for (const property of properties) {
39
+ const name = property.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
40
+ kinds.add(
41
+ STYLE_KIND_RULES.find(([rule]) => rule.test(name))?.[1] ?? "other",
42
+ );
43
+ }
44
+ if (kinds.size === 0) return "other";
45
+ if (kinds.size === 1) return [...kinds][0];
46
+ if (kinds.size === 2 && kinds.has("size") && kinds.has("position")) {
47
+ return "size";
48
+ }
49
+ return "mixed";
50
+ }
@@ -14,7 +14,7 @@ import {
14
14
  useSetPageTitle,
15
15
  } from "@agent-native/toolkit/app-shell";
16
16
  import { ShareButton } from "@agent-native/toolkit/app/sharing";
17
- import { VisibilityBadge } from "@agent-native/toolkit/sharing";
17
+ import { VisibilityBadge } from "@agent-native/toolkit/app/sharing/VisibilityBadge";
18
18
  import {
19
19
  IconCheckbox,
20
20
  IconChecks,