@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +61 -1
- package/corpus/templates/analytics/AGENTS.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +3 -0
- package/corpus/templates/analytics/actions/get-session-replay-summary.ts +9 -65
- package/corpus/templates/analytics/actions/list-event-catalog.ts +1 -1
- package/corpus/templates/analytics/actions/list-session-outputs.ts +40 -0
- package/corpus/templates/analytics/actions/list-session-recordings.ts +17 -4
- package/corpus/templates/analytics/actions/view-screen.ts +29 -3
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +38 -167
- package/corpus/templates/analytics/app/i18n/en-US.ts +27 -0
- package/corpus/templates/analytics/app/pages/sessions/EventCatalogPage.tsx +18 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +3 -1
- package/corpus/templates/analytics/app/pages/sessions/SessionOutputs.tsx +197 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsTriagePage.tsx +114 -4
- package/corpus/templates/analytics/changelog/2026-10-15-sessions-can-find-people-who-made-a-slides-deck-but-didn-t-p.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-the-activation-funnel-counts-visitors-who-decline-analytics-.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +67 -0
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +9 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +28 -22
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +36 -1
- package/corpus/templates/analytics/server/lib/output-outcomes.ts +712 -0
- package/corpus/templates/analytics/server/lib/replay-origins.ts +33 -0
- package/corpus/templates/analytics/server/lib/session-event-index.ts +20 -0
- package/corpus/templates/analytics/server/lib/session-performance.ts +6 -40
- package/corpus/templates/analytics/server/lib/session-recording-associations.ts +30 -1
- package/corpus/templates/analytics/server/lib/session-replay-triage.ts +99 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +45 -31
- package/corpus/templates/analytics/server/lib/sessions-triage-lab.ts +8 -2
- package/corpus/templates/analytics/server/plugins/db.ts +46 -0
- package/corpus/templates/analytics/shared/output-use.ts +205 -0
- package/corpus/templates/analytics/shared/session-events.ts +6 -0
- package/corpus/templates/assets/CHANGELOG.md +6 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +51 -117
- package/corpus/templates/assets/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +34 -101
- package/corpus/templates/brain/app/routes/sources.tsx +1 -1
- package/corpus/templates/brain/vitest.config.ts +7 -0
- package/corpus/templates/calendar/CHANGELOG.md +12 -0
- package/corpus/templates/calendar/actions/create-event.ts +2 -4
- package/corpus/templates/calendar/actions/update-event.ts +35 -6
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +1 -1
- package/corpus/templates/calendar/changelog/2026-10-16-booking-links-show-localized-visibility-badges.md +6 -0
- package/corpus/templates/calendar/changelog/2026-10-21-solo-events-avoid-notification-approval.md +6 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +62 -101
- package/corpus/templates/clips/CHANGELOG.md +7 -20
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +20 -77
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +63 -90
- package/corpus/templates/clips/app/components/sharing/share-test-support.tsx +180 -0
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +5 -584
- package/corpus/templates/clips/changelog/2026-10-16-share-dialogs-make-it-easier-to-see-who-has-access-and-manag.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-21-share-panels-describe-general-access-in-a-sentence.md +6 -0
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -3
- package/corpus/templates/content/CHANGELOG.md +10 -0
- package/corpus/templates/content/actions/_browser-document-save-attempt.ts +23 -1
- package/corpus/templates/content/actions/_builder-cms-blob-custody.ts +467 -0
- package/corpus/templates/content/actions/_builder-cms-execution-lease.ts +17 -0
- package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +5 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +881 -21
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +28 -2
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +109 -11
- package/corpus/templates/content/actions/_builder-docs-client.ts +1 -0
- package/corpus/templates/content/actions/_builder-private-blob.test-fixture.ts +90 -0
- package/corpus/templates/content/actions/_builder-source-execution-guard.ts +48 -0
- package/corpus/templates/content/actions/_content-save-outcomes.ts +5 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +290 -8
- package/corpus/templates/content/actions/_database-utils.ts +93 -7
- package/corpus/templates/content/actions/_database-view-setup.ts +21 -2
- package/corpus/templates/content/actions/_delete-content-space.ts +8 -1
- package/corpus/templates/content/actions/_github-source-freshness.ts +30 -4
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +23 -6
- package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +110 -26
- package/corpus/templates/content/actions/change-content-database-source-role.ts +51 -7
- package/corpus/templates/content/actions/delete-document.ts +41 -3
- package/corpus/templates/content/actions/disconnect-content-database-source.ts +68 -2
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +33 -2
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +900 -140
- package/corpus/templates/content/actions/get-document.ts +2 -0
- package/corpus/templates/content/actions/permanently-delete-document.ts +6 -0
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +155 -13
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +299 -73
- package/corpus/templates/content/actions/set-live-body-switch.ts +45 -0
- package/corpus/templates/content/actions/update-document.ts +139 -20
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +135 -26
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +119 -30
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +56 -13
- package/corpus/templates/content/app/components/editor/SuggestionText.tsx +18 -3
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +89 -22
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +23 -0
- package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +32 -8
- package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +27 -2
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +1 -0
- package/corpus/templates/content/app/components/editor/extensions/SuggestionHighlight.ts +383 -34
- package/corpus/templates/content/app/components/editor/suggestions/draft-session.ts +31 -0
- package/corpus/templates/content/app/components/editor/suggestions/presentation-rebase.ts +82 -4
- package/corpus/templates/content/app/global.css +31 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +40 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +14 -0
- package/corpus/templates/content/app/i18n-data.ts +1 -0
- package/corpus/templates/content/changelog/2026-10-16-accepting-a-suggestion-on-a-page-that-changed-since-it-was-s.md +6 -0
- package/corpus/templates/content/changelog/2026-10-17-builder-content-avoids-retrying-a-write-when-builder-accepte.md +6 -0
- package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +1 -1
- package/corpus/templates/content/changelog/2026-10-20-builder-content-sync-keeps-read-and-write-status-accurate-wh.md +6 -0
- package/corpus/templates/content/changelog/2026-10-20-suggested-edits-that-add-remove-split-or-merge-paragraphs-li.md +6 -0
- package/corpus/templates/content/changelog/2026-10-21-list-rows-can-show-each-pages-first-lines.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-a-collection-linked-to-a-big-github-folder-fills-faster-and-p.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +29 -0
- package/corpus/templates/content/parity/matrix.md +42 -42
- package/corpus/templates/content/parity/matrix.ts +3 -1
- package/corpus/templates/content/server/db/schema.ts +20 -0
- package/corpus/templates/content/server/lib/content-trash-purge.ts +15 -1
- package/corpus/templates/content/server/lib/live-body-writer.ts +138 -26
- package/corpus/templates/content/server/live-copy/binding.ts +31 -0
- package/corpus/templates/content/server/live-copy/bounded-task.d.ts +6 -0
- package/corpus/templates/content/server/live-copy/exec-db.ts +1 -0
- package/corpus/templates/content/server/live-copy/holds.ts +59 -0
- package/corpus/templates/content/server/live-copy/persist.ts +656 -0
- package/corpus/templates/content/server/live-copy/switch.ts +136 -0
- package/corpus/templates/content/server/live-copy/task-names.ts +4 -0
- package/corpus/templates/content/server/live-copy/tasks.ts +183 -0
- package/corpus/templates/content/server/live-copy/write-cause.ts +85 -0
- package/corpus/templates/content/server/live-copy/write.ts +85 -0
- package/corpus/templates/content/server/plugins/builder-private-payload-cleanup.ts +22 -0
- package/corpus/templates/content/server/plugins/collab.ts +3 -0
- package/corpus/templates/content/server/plugins/db.ts +28 -0
- package/corpus/templates/content/shared/api.ts +30 -1
- package/corpus/templates/content/shared/database-item-layout.ts +54 -22
- package/corpus/templates/content/shared/document-intent-merge.ts +16 -9
- package/corpus/templates/content/shared/og-description.ts +258 -36
- package/corpus/templates/content/shared/suggestion-diff.ts +23 -6
- package/corpus/templates/content/shared/suggestion-formatting.ts +44 -14
- package/corpus/templates/content/shared/suggestion-text.ts +20 -5
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +5 -1
- package/corpus/templates/design/.generated/bridge/shader-runtime.generated.ts +25 -4
- package/corpus/templates/design/CHANGELOG.md +5 -0
- package/corpus/templates/design/actions/get-getting-started.ts +53 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +10 -0
- package/corpus/templates/design/app/components/design/bridge/native-shader-runtime.bridge.ts +132 -79
- package/corpus/templates/design/app/components/design/bridge/native-source-provider.ts +268 -130
- package/corpus/templates/design/app/components/design/bridge/native-worker-scene-host.ts +122 -42
- package/corpus/templates/design/app/components/design/bridge/shader-runtime.bridge.ts +41 -4
- package/corpus/templates/design/app/components/design/design-canvas/text-content-change-result.ts +46 -12
- package/corpus/templates/design/app/components/home/DesignHomeLibrary.tsx +15 -8
- package/corpus/templates/design/app/components/layout/GettingStarted.tsx +50 -0
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +3 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateGrid.tsx +163 -0
- package/corpus/templates/design/app/components/templates/DesignTemplateLibrary.tsx +65 -78
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +6 -2
- package/corpus/templates/design/app/global.css +6 -0
- package/corpus/templates/design/app/i18n/en-US.ts +17 -0
- package/corpus/templates/design/app/lib/canvas-load-outcome.ts +147 -0
- package/corpus/templates/design/app/lib/canvas-outcome-tracking.ts +531 -0
- package/corpus/templates/design/app/lib/canvas-style-kind.ts +50 -0
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -1
- package/corpus/templates/design/app/pages/Index.tsx +28 -67
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +86 -29
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +38 -3
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-core.ts +9 -0
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-files-and-saving.ts +2 -0
- package/corpus/templates/design/changelog/2026-10-16-design-pages-use-localized-visibility-badges-and-a-shared-ow.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-a-getting-started-checklist-at-the-bottom-of-the-sidebar-tra.md +6 -0
- package/corpus/templates/design/changelog/2026-10-21-templates-now-open-a-read-only-preview-from-the-card-and-the.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-collapsed-shader-layers-no-longer-interrupt-other-shaders-on.md +6 -0
- package/corpus/templates/design/changelog/2026-10-22-shader-fills-keep-rendering-when-other-screen-content-cannot.md +6 -0
- package/corpus/templates/design/scripts/design-export-validation/export-baseline.json +24 -0
- package/corpus/templates/design/server/lib/getting-started.ts +267 -0
- package/corpus/templates/design/server/plugins/getting-started.ts +5 -0
- package/corpus/templates/design/shared/design-template-presets/analytics-dashboard.ts +127 -0
- package/corpus/templates/design/shared/design-template-presets/design-studio.ts +98 -0
- package/corpus/templates/design/shared/design-template-presets/messaging-app.ts +101 -0
- package/corpus/templates/design/shared/design-template-presets/project-tasks.ts +120 -0
- package/corpus/templates/design/shared/design-template-presets.ts +8 -0
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/app/i18n-data.ts +75 -0
- package/corpus/templates/dispatch/changelog/2026-10-21-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +34 -100
- package/corpus/templates/forms/app/pages/FormsListPage.tsx +1 -1
- package/corpus/templates/forms/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/plan/CHANGELOG.md +6 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +34 -109
- package/corpus/templates/plan/changelog/2026-10-16-owned-and-shared-chats-appear-in-separate-history-sections.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +7 -0
- package/corpus/templates/slides/actions/delete-deck.ts +7 -0
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +1 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +1 -1
- package/corpus/templates/slides/app/pages/Index.tsx +7 -2
- package/corpus/templates/slides/changelog/2026-10-16-deck-lists-use-the-shared-ownership-filter-and-visibility-ba.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-21-connected-deck-deletion-requires-approval.md +6 -0
- package/corpus/templates/slides/scripts/edit-fidelity/client-action.ts +61 -0
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +10 -0
- package/package.json +1 -1
- package/corpus/templates/chat/app/lib/sidebar-thread-state.ts +0 -29
- 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,
|