@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,205 @@
1
+ /**
2
+ * "Made it, didn't use it": per app, the output a person makes, the events
3
+ * that say they made one, and the events that say they used it. Opening an
4
+ * output never counts as using it; a use shows the output to people or takes
5
+ * it out of the app.
6
+ */
7
+
8
+ /** A use counts only within this many days of the output being made. */
9
+ export const OUTPUT_USE_WINDOW_DAYS = 7;
10
+ export const OUTPUT_USE_WINDOW_MS = OUTPUT_USE_WINDOW_DAYS * 24 * 60 * 60_000;
11
+ /** A presentation shorter than this was a look, not a talk. */
12
+ export const MIN_PRESENTATION_MS = 30_000;
13
+ /** The most outputs one recording lists; the rest are counted, not listed. */
14
+ export const MAX_OUTPUTS_PER_RECORDING = 10;
15
+ /** Output ids are skipped past this length, never cut, like session ids. */
16
+ export const MAX_OUTPUT_ID_LENGTH = 256;
17
+ /** The Sessions URL param naming the app whose unused outputs to find. */
18
+ export const UNUSED_OUTPUT_PARAM = "unused";
19
+ /** Every generator sends this when it makes an output, so it marks an app that makes them. */
20
+ export const OUTPUT_MADE_EVENT = "generation_completed";
21
+
22
+ type EventProperties = Readonly<Record<string, unknown>>;
23
+
24
+ export interface OutputUseEvent {
25
+ eventName: string;
26
+ qualifies: (properties: EventProperties) => boolean;
27
+ }
28
+
29
+ export interface OutputUseRule {
30
+ outputType: string;
31
+ madeEvents: readonly string[];
32
+ usedEvents: readonly OutputUseEvent[];
33
+ /** The output's page, relative to its app's origin. */
34
+ outputPath: (outputId: string) => string;
35
+ }
36
+
37
+ /** A missing or non-numeric property never proves a use. */
38
+ function atLeast(value: unknown, minimum: number): boolean {
39
+ return (
40
+ typeof value === "number" && Number.isFinite(value) && value >= minimum
41
+ );
42
+ }
43
+
44
+ export const OUTPUT_USE_RULES = {
45
+ slides: {
46
+ outputType: "deck",
47
+ madeEvents: [OUTPUT_MADE_EVENT],
48
+ usedEvents: [
49
+ {
50
+ // Moving forward at least once means two or more slides were shown,
51
+ // even when the presentation started mid-deck.
52
+ eventName: "presentation_ended",
53
+ qualifies: (properties) =>
54
+ atLeast(properties.slides_advanced, 1) &&
55
+ atLeast(properties.duration_ms, MIN_PRESENTATION_MS),
56
+ },
57
+ {
58
+ // `slide_export_started` fires before the export runs, so only a
59
+ // finished export of a deck with slides counts.
60
+ eventName: "deck_exported",
61
+ qualifies: (properties) =>
62
+ properties.status === "completed" &&
63
+ atLeast(properties.slide_count, 1),
64
+ },
65
+ ],
66
+ outputPath: (outputId) => `/deck/${encodeURIComponent(outputId)}`,
67
+ },
68
+ } as const satisfies Record<string, OutputUseRule>;
69
+
70
+ export type OutputUseApp = keyof typeof OUTPUT_USE_RULES;
71
+
72
+ export const OUTPUT_USE_APPS = Object.keys(OUTPUT_USE_RULES) as [
73
+ OutputUseApp,
74
+ ...OutputUseApp[],
75
+ ];
76
+
77
+ export function isOutputUseApp(value: unknown): value is OutputUseApp {
78
+ return (
79
+ typeof value === "string" &&
80
+ Object.prototype.hasOwnProperty.call(OUTPUT_USE_RULES, value)
81
+ );
82
+ }
83
+
84
+ export function outputUseRule(
85
+ app: string | null | undefined,
86
+ ): OutputUseRule | null {
87
+ return isOutputUseApp(app) ? OUTPUT_USE_RULES[app] : null;
88
+ }
89
+
90
+ /** Every event name any rule reads, for the catalog and for ingest. */
91
+ export const OUTPUT_USE_EVENT_NAMES: ReadonlySet<string> = new Set(
92
+ Object.values(OUTPUT_USE_RULES).flatMap((rule: OutputUseRule) => [
93
+ ...rule.madeEvents,
94
+ ...rule.usedEvents.map((used) => used.eventName),
95
+ ]),
96
+ );
97
+
98
+ export function outputIdOf(properties: EventProperties): string | null {
99
+ const value = properties.output_id;
100
+ if (typeof value !== "string") return null;
101
+ const outputId = value.trim();
102
+ return outputId && outputId.length <= MAX_OUTPUT_ID_LENGTH ? outputId : null;
103
+ }
104
+
105
+ /** Where a recording's session ran, as far as the recording shows. */
106
+ export interface OutputRecordingPages {
107
+ firstUrl: string | null;
108
+ lastUrl: string | null;
109
+ /**
110
+ * Origins the recording's replay key lists. An empty list accepts uploads
111
+ * from anywhere, so it vouches for no origin and yields no link.
112
+ */
113
+ allowedOrigins: readonly string[];
114
+ }
115
+
116
+ function recordedPage(value: string | null): URL | null {
117
+ if (!value) return null;
118
+ try {
119
+ const url = new URL(value);
120
+ return url.origin === "null" ? null : url;
121
+ // coercion-ok: a recording without a parseable URL has no app to link to; the output id still shows.
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Where a session's output opens: the app origin the session was recorded on,
129
+ * its mount path, and the output's page. The app checks access when it opens.
130
+ * A recording's URLs are whatever its uploader sent, so only an origin the
131
+ * replay key's owner allowed becomes a link; otherwise this is null.
132
+ */
133
+ export function outputUrl(
134
+ app: string,
135
+ outputId: string,
136
+ recording: OutputRecordingPages,
137
+ ): string | null {
138
+ const rule = outputUseRule(app);
139
+ if (!rule) return null;
140
+ const pages = [recording.firstUrl, recording.lastUrl].flatMap((value) => {
141
+ const page = recordedPage(value);
142
+ return page ? [page] : [];
143
+ });
144
+ // A recording's id lives in its tab's sessionStorage, which is per origin,
145
+ // so a real recording never spans origins; pages on two came from a forged
146
+ // upload, and neither can be trusted.
147
+ const origin = pages[0]?.origin;
148
+ if (
149
+ !origin ||
150
+ pages.some((page) => page.origin !== origin) ||
151
+ !recording.allowedOrigins.includes(origin)
152
+ ) {
153
+ return null;
154
+ }
155
+ // A workspace deploy mounts each app at `/<app id>` on one origin, and a
156
+ // tab's recording follows it from app to app, so either page may show the
157
+ // mount. Reading that prefix as the mount assumes the app has no route of
158
+ // its own there (Slides has none). The recording's app label can't decide
159
+ // it: without a declared app it is the hostname's first label.
160
+ const mount = `/${app}`;
161
+ const mounted = pages.some(
162
+ (page) => page.pathname === mount || page.pathname.startsWith(`${mount}/`),
163
+ );
164
+ return `${origin}${mounted ? mount : ""}${rule.outputPath(outputId)}`;
165
+ }
166
+
167
+ /**
168
+ * What became of one output made in a session. `open` means the use window
169
+ * has not closed yet; `unmeasured` means a failed write may have dropped a
170
+ * use, so it cannot be called unused.
171
+ */
172
+ export type SessionOutputStatus = "used" | "unused" | "open" | "unmeasured";
173
+
174
+ export interface SessionOutput {
175
+ app: OutputUseApp;
176
+ outputType: string;
177
+ outputId: string;
178
+ madeAt: string;
179
+ useBy: string;
180
+ status: SessionOutputStatus;
181
+ /** The first qualifying use's event name, when it was used in time. */
182
+ usedEvent: string | null;
183
+ usedAt: string | null;
184
+ url: string | null;
185
+ }
186
+
187
+ export interface SessionRecordingOutputs {
188
+ /**
189
+ * By recording id, for each requested recording the viewer can read: its
190
+ * outputs, [] when it made none since coverage began, or null when they
191
+ * are unknown: it was shared from another tenant, whose events a share does
192
+ * not grant, its tenant has not started output tracking, or a batch lost in
193
+ * ingest overlapped it and may have held an output it made. Ids the viewer
194
+ * cannot read are absent.
195
+ */
196
+ outputs: Record<string, SessionOutput[] | null>;
197
+ /**
198
+ * How many more outputs a recording made than `outputs` lists, by
199
+ * recording id; absent when none were left out. Unused outputs are listed
200
+ * first.
201
+ */
202
+ omittedOutputs: Record<string, number>;
203
+ /** When the viewer's output tracking began, or null before it has. */
204
+ coverageStartedAt: string | null;
205
+ }
@@ -54,6 +54,8 @@ export interface EventCatalogEntry {
54
54
  description: string | null;
55
55
  automatic: boolean;
56
56
  stoppedFiring: boolean;
57
+ /** Whether "made it, didn't use it" reads this event as making or using its app's output. */
58
+ outputRole: "made" | "used" | null;
57
59
  }
58
60
 
59
61
  export interface EventCatalogApp {
@@ -61,6 +63,10 @@ export interface EventCatalogApp {
61
63
  eventCount: number;
62
64
  volume: number;
63
65
  onlyAutomaticEvents: boolean;
66
+ /** The app sent `generation_completed` in range, so it makes outputs. */
67
+ makesOutputs: boolean;
68
+ /** Events that count as using this app's output; empty for an app that makes outputs is a catalog gap. */
69
+ usedItEvents: string[];
64
70
  }
65
71
 
66
72
  export interface EventCatalogResult {
@@ -9,6 +9,12 @@ time from the command menu (Cmd+K → "What's new") or from Settings.
9
9
 
10
10
  - Apps no longer keep server functions and databases warm by default; set `AGENT_NATIVE_ENABLE_KEEP_WARM=1` to opt in.
11
11
 
12
+ ## 2026-10-16
13
+
14
+ ### Added
15
+
16
+ - Owned and shared chats appear in separate history sections, with each shared chat's owner shown as an avatar and a Copy link action on every chat.
17
+
12
18
  ## 2026-10-09
13
19
 
14
20
  ### Improved
@@ -1,11 +1,16 @@
1
1
  import {
2
2
  navigateWithAgentChatViewTransition,
3
3
  useChatThreads,
4
- type ChatThreadSummary,
5
4
  } from "@agent-native/core/client/agent-chat";
5
+ import { appPath } from "@agent-native/core/client/api-path";
6
6
  import { useActionQuery } from "@agent-native/core/client/hooks";
7
7
  import { useT } from "@agent-native/core/client/i18n";
8
8
  import { focusAgentChat } from "@agent-native/toolkit/app/chat";
9
+ import {
10
+ OwnershipChatHistoryRail,
11
+ useOwnershipChatHistory,
12
+ ownDraftRows,
13
+ } from "@agent-native/toolkit/app/chat/ownership-history";
9
14
  import { DevDatabaseLink } from "@agent-native/toolkit/app/db-admin";
10
15
  import { FeedbackButton } from "@agent-native/toolkit/app/feedback";
11
16
  import { OrgSwitcher } from "@agent-native/toolkit/app/org";
@@ -14,10 +19,6 @@ import {
14
19
  AppSidebar,
15
20
  AppSidebarNavItem,
16
21
  } from "@agent-native/toolkit/app/shared";
17
- import {
18
- ChatHistoryRail,
19
- type ChatHistoryItem,
20
- } from "@agent-native/toolkit/chat-history";
21
22
  import {
22
23
  IconClipboardList,
23
24
  IconLayoutGrid,
@@ -26,7 +27,7 @@ import {
26
27
  IconShare3,
27
28
  IconTemplate,
28
29
  } from "@tabler/icons-react";
29
- import { useEffect, useMemo, useState } from "react";
30
+ import { useEffect, useState } from "react";
30
31
  import { useLocation, useNavigate } from "react-router";
31
32
  import { toast } from "sonner";
32
33
 
@@ -53,40 +54,6 @@ const auditNavItem = {
53
54
  const COLLAPSE_KEY = "assets.sidebar.collapsed";
54
55
  const ASSETS_ACTIVE_THREAD_KEY = `agent-chat-active-thread:${ASSETS_CHAT_STORAGE_KEY}`;
55
56
 
56
- function formatThreadAge(updatedAt: number) {
57
- const diffMs = Math.max(0, Date.now() - updatedAt);
58
- const minutes = Math.floor(diffMs / 60_000);
59
- if (minutes < 1) return "now";
60
- if (minutes < 60) return `${minutes}m`;
61
- const hours = Math.floor(minutes / 60);
62
- if (hours < 24) return `${hours}h`;
63
- const days = Math.floor(hours / 24);
64
- if (days < 7) return `${days}d`;
65
- return new Date(updatedAt).toLocaleDateString([], {
66
- month: "short",
67
- day: "numeric",
68
- });
69
- }
70
-
71
- function threadTitle(thread: ChatThreadSummary) {
72
- return thread.title || thread.preview || "Untitled chat";
73
- }
74
-
75
- function threadUpdatedAt(thread: ChatThreadSummary) {
76
- return Number.isFinite(thread.updatedAt)
77
- ? thread.updatedAt
78
- : Number.isFinite(thread.createdAt)
79
- ? thread.createdAt
80
- : 0;
81
- }
82
-
83
- function compareThreads(a: ChatThreadSummary, b: ChatThreadSummary) {
84
- const aPinned = a.pinnedAt ?? 0;
85
- const bPinned = b.pinnedAt ?? 0;
86
- if (aPinned || bPinned) return bPinned - aPinned;
87
- return threadUpdatedAt(b) - threadUpdatedAt(a);
88
- }
89
-
90
57
  function persistedActiveThreadId() {
91
58
  try {
92
59
  return localStorage.getItem(ASSETS_ACTIVE_THREAD_KEY);
@@ -129,55 +96,37 @@ function AssetsChatsSection({ open }: { open: boolean }) {
129
96
  archiveThread,
130
97
  renameThread,
131
98
  createThreadShareLink,
132
- refreshThreads,
99
+ isNewThread,
133
100
  } = useChatThreads(undefined, ASSETS_CHAT_STORAGE_KEY, undefined, {
101
+ ownership: "mine",
134
102
  autoCreate: false,
135
103
  restoreActiveThread: false,
136
104
  });
137
105
 
138
- const visibleThreads = useMemo(
139
- () =>
140
- threads
141
- .filter((thread) => thread.messageCount > 0 && !thread.archivedAt)
142
- .sort(compareThreads),
143
- [threads],
144
- );
145
106
  const displayedActiveThreadId =
146
107
  threadIdFromPath(location.pathname) ??
147
108
  (location.pathname === "/home" ? null : activeThreadId);
148
- const chatItems = useMemo<ChatHistoryItem[]>(
149
- () =>
150
- visibleThreads.map((thread) => ({
151
- id: thread.id,
152
- title: threadTitle(thread),
153
- titleText: threadTitle(thread),
154
- timestamp:
155
- thread.id === displayedActiveThreadId
156
- ? undefined
157
- : formatThreadAge(threadUpdatedAt(thread)),
158
- pinned: Boolean(thread.pinnedAt),
159
- })),
160
- [displayedActiveThreadId, visibleThreads],
161
- );
162
-
163
- useEffect(() => {
164
- const refresh = () => refreshThreads();
165
- const handleRunning = (event: Event) => {
166
- const detail = (event as CustomEvent).detail as
167
- | { isRunning?: unknown }
168
- | undefined;
169
- if (typeof detail?.isRunning === "boolean") refreshThreads();
170
- };
171
-
172
- window.addEventListener("agent-chat:threads-updated", refresh);
173
- window.addEventListener("agentNative.chatRunning", handleRunning);
174
- window.addEventListener("focus", refresh);
175
- return () => {
176
- window.removeEventListener("agent-chat:threads-updated", refresh);
177
- window.removeEventListener("agentNative.chatRunning", handleRunning);
178
- window.removeEventListener("focus", refresh);
179
- };
180
- }, [refreshThreads]);
109
+ const history = useOwnershipChatHistory({
110
+ getThreadUrl: (threadId) => appPath(chatThreadPath(threadId)),
111
+ activeId: displayedActiveThreadId,
112
+ localMineRows: ownDraftRows({ threads, isNewThread }),
113
+ mutations: {
114
+ pin: pinThread,
115
+ rename: renameThread,
116
+ archive: archiveThread,
117
+ },
118
+ onMutationFailed: (kind) => {
119
+ toast.error(
120
+ t(
121
+ kind === "archive"
122
+ ? "chat.archiveFailed"
123
+ : kind === "rename"
124
+ ? "chat.renameFailed"
125
+ : "agentChat.common.saveFailed",
126
+ ),
127
+ );
128
+ },
129
+ });
181
130
 
182
131
  function openThread(threadId: string, options?: { isNew?: boolean }) {
183
132
  switchThread(threadId);
@@ -203,14 +152,12 @@ function AssetsChatsSection({ open }: { open: boolean }) {
203
152
  async function handleArchiveThread(threadId: string) {
204
153
  const wasActive =
205
154
  threadId === activeThreadId || threadId === persistedActiveThreadId();
206
- const archived = await archiveThread(threadId);
207
- if (!archived) {
208
- toast.error(t("chat.archiveFailed"));
209
- return;
210
- }
155
+ const archived = await history.listProps.onDelete!(threadId);
156
+ if (!archived) return false;
211
157
  if (wasActive) {
212
158
  await handleNewChat();
213
159
  }
160
+ return true;
214
161
  }
215
162
 
216
163
  async function handleCopyShareLink(threadId: string) {
@@ -233,12 +180,6 @@ function AssetsChatsSection({ open }: { open: boolean }) {
233
180
  }
234
181
  }
235
182
 
236
- function handleRenameThread(threadId: string, title: string) {
237
- void renameThread(threadId, title).then((renamed) => {
238
- if (!renamed) toast.error(t("chat.renameFailed"));
239
- });
240
- }
241
-
242
183
  return (
243
184
  <div
244
185
  className="an-chat-history-rail__collapse"
@@ -246,39 +187,32 @@ function AssetsChatsSection({ open }: { open: boolean }) {
246
187
  aria-hidden={!open}
247
188
  >
248
189
  <div className="ms-4">
249
- <ChatHistoryRail
250
- items={chatItems}
190
+ <OwnershipChatHistoryRail
191
+ {...history}
251
192
  activeId={displayedActiveThreadId}
252
193
  onSelect={openThread}
253
194
  onNewChat={() => void handleNewChat()}
254
195
  railLabels={{
255
196
  newChat: t("chat.newChat"),
256
- showMore: t("chat.chats"),
257
- showLess: t("chat.chats"),
258
- }}
259
- previewCount={5}
260
- expandedCount={15}
261
- onTogglePin={(threadId) => {
262
- const thread = visibleThreads.find((item) => item.id === threadId);
263
- if (thread) void pinThread(threadId, !thread.pinnedAt);
264
197
  }}
265
- onRename={handleRenameThread}
266
198
  renameMaxLength={160}
267
199
  onDelete={(threadId) => void handleArchiveThread(threadId)}
268
- renderAdditionalRowActions={(item, closeMenu) => (
269
- <button
270
- type="button"
271
- role="menuitem"
272
- className="an-chat-history-row__menu-item"
273
- onClick={() => {
274
- closeMenu();
275
- void handleCopyShareLink(item.id);
276
- }}
277
- >
278
- <IconShare3 size={13} strokeWidth={1.8} />
279
- <span>{t("chat.copyShareLink")}</span>
280
- </button>
281
- )}
200
+ renderAdditionalRowActions={(item, closeMenu) =>
201
+ item.readOnly ? null : (
202
+ <button
203
+ type="button"
204
+ role="menuitem"
205
+ className="an-chat-history-row__menu-item"
206
+ onClick={() => {
207
+ closeMenu();
208
+ void handleCopyShareLink(item.id);
209
+ }}
210
+ >
211
+ <IconShare3 size={13} strokeWidth={1.8} />
212
+ <span>{t("chat.copyShareLink")}</span>
213
+ </button>
214
+ )
215
+ }
282
216
  labels={{
283
217
  options: (item) =>
284
218
  t("chat.optionsFor", { title: item.titleText ?? "" }),
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-10-16
4
+ ---
5
+
6
+ Owned and shared chats appear in separate history sections, with each shared chat's owner shown as an avatar and a Copy link action on every chat.
@@ -1,9 +1,14 @@
1
1
  import {
2
2
  navigateWithAgentChatViewTransition,
3
3
  useChatThreads,
4
- type ChatThreadSummary,
5
4
  } from "@agent-native/core/client/agent-chat";
5
+ import { appPath } from "@agent-native/core/client/api-path";
6
6
  import { useT } from "@agent-native/core/client/i18n";
7
+ import {
8
+ OwnershipChatHistoryRail,
9
+ useOwnershipChatHistory,
10
+ ownDraftRows,
11
+ } from "@agent-native/toolkit/app/chat/ownership-history";
7
12
  import { DevDatabaseLink } from "@agent-native/toolkit/app/db-admin";
8
13
  import { FeedbackButton } from "@agent-native/toolkit/app/feedback";
9
14
  import { OrgSwitcher } from "@agent-native/toolkit/app/org";
@@ -12,12 +17,7 @@ import {
12
17
  AppSidebar,
13
18
  AppSidebarNavItem,
14
19
  } from "@agent-native/toolkit/app/shared";
15
- import {
16
- ChatHistoryRail,
17
- type ChatHistoryItem,
18
- } from "@agent-native/toolkit/chat-history";
19
20
  import { IconSearch } from "@tabler/icons-react";
20
- import { useEffect, useMemo } from "react";
21
21
  import { useLocation, useNavigate } from "react-router";
22
22
  import { toast } from "sonner";
23
23
 
@@ -43,40 +43,6 @@ interface SidebarProps {
43
43
  onCollapsedChange?: (collapsed: boolean) => void;
44
44
  }
45
45
 
46
- function formatThreadAge(updatedAt: number) {
47
- const diffMs = Math.max(0, Date.now() - updatedAt);
48
- const minutes = Math.floor(diffMs / 60_000);
49
- if (minutes < 1) return "now";
50
- if (minutes < 60) return `${minutes}m`;
51
- const hours = Math.floor(minutes / 60);
52
- if (hours < 24) return `${hours}h`;
53
- const days = Math.floor(hours / 24);
54
- if (days < 7) return `${days}d`;
55
- return new Date(updatedAt).toLocaleDateString([], {
56
- month: "short",
57
- day: "numeric",
58
- });
59
- }
60
-
61
- function threadTitle(thread: ChatThreadSummary) {
62
- return thread.title || thread.preview || "Untitled chat";
63
- }
64
-
65
- function threadUpdatedAt(thread: ChatThreadSummary) {
66
- return Number.isFinite(thread.updatedAt)
67
- ? thread.updatedAt
68
- : Number.isFinite(thread.createdAt)
69
- ? thread.createdAt
70
- : 0;
71
- }
72
-
73
- function compareThreads(a: ChatThreadSummary, b: ChatThreadSummary) {
74
- const aPinned = a.pinnedAt ?? 0;
75
- const bPinned = b.pinnedAt ?? 0;
76
- if (aPinned || bPinned) return bPinned - aPinned;
77
- return threadUpdatedAt(b) - threadUpdatedAt(a);
78
- }
79
-
80
46
  function persistedActiveThreadId() {
81
47
  try {
82
48
  return localStorage.getItem(BRAIN_ACTIVE_THREAD_KEY);
@@ -94,8 +60,9 @@ function BrainChatsSection({ open }: { open: boolean }) {
94
60
  pinThread,
95
61
  archiveThread,
96
62
  renameThread,
97
- refreshThreads,
63
+ isNewThread,
98
64
  } = useChatThreads(undefined, BRAIN_CHAT_STORAGE_KEY, undefined, {
65
+ ownership: "mine",
99
66
  autoCreate: false,
100
67
  restoreActiveThread: false,
101
68
  });
@@ -104,46 +71,27 @@ function BrainChatsSection({ open }: { open: boolean }) {
104
71
  const location = useLocation();
105
72
  const activeThreadId = brainAskThreadIdFromPath(location.pathname);
106
73
 
107
- const visibleThreads = useMemo(
108
- () =>
109
- threads
110
- .filter((thread) => thread.messageCount > 0 && !thread.archivedAt)
111
- .sort(compareThreads),
112
- [threads],
113
- );
114
- const chatItems = useMemo<ChatHistoryItem[]>(
115
- () =>
116
- visibleThreads.map((thread) => ({
117
- id: thread.id,
118
- title: threadTitle(thread),
119
- titleText: threadTitle(thread),
120
- timestamp:
121
- thread.id === activeThreadId
122
- ? undefined
123
- : formatThreadAge(threadUpdatedAt(thread)),
124
- pinned: Boolean(thread.pinnedAt),
125
- })),
126
- [activeThreadId, visibleThreads],
127
- );
128
-
129
- useEffect(() => {
130
- const refresh = () => refreshThreads();
131
- const handleRunning = (event: Event) => {
132
- const detail = (event as CustomEvent).detail as
133
- | { isRunning?: unknown }
134
- | undefined;
135
- if (typeof detail?.isRunning === "boolean") refreshThreads();
136
- };
137
-
138
- window.addEventListener("agent-chat:threads-updated", refresh);
139
- window.addEventListener("agentNative.chatRunning", handleRunning);
140
- window.addEventListener("focus", refresh);
141
- return () => {
142
- window.removeEventListener("agent-chat:threads-updated", refresh);
143
- window.removeEventListener("agentNative.chatRunning", handleRunning);
144
- window.removeEventListener("focus", refresh);
145
- };
146
- }, [refreshThreads]);
74
+ const history = useOwnershipChatHistory({
75
+ getThreadUrl: (threadId) => appPath(brainAskThreadPath(threadId)),
76
+ activeId: activeThreadId,
77
+ localMineRows: ownDraftRows({ threads, isNewThread }),
78
+ mutations: {
79
+ pin: pinThread,
80
+ rename: renameThread,
81
+ archive: archiveThread,
82
+ },
83
+ onMutationFailed: (kind) => {
84
+ toast.error(
85
+ t(
86
+ kind === "archive"
87
+ ? "chat.archiveFailed"
88
+ : kind === "rename"
89
+ ? "chat.renameFailed"
90
+ : "agentChat.common.saveFailed",
91
+ ),
92
+ );
93
+ },
94
+ });
147
95
 
148
96
  function openThread(threadId: string) {
149
97
  switchThread(threadId);
@@ -165,20 +113,12 @@ function BrainChatsSection({ open }: { open: boolean }) {
165
113
  async function handleArchiveThread(threadId: string) {
166
114
  const wasActive =
167
115
  threadId === activeThreadId || threadId === persistedActiveThreadId();
168
- const archived = await archiveThread(threadId);
169
- if (!archived) {
170
- toast.error(t("chat.archiveFailed"));
171
- return;
172
- }
116
+ const archived = await history.listProps.onDelete!(threadId);
117
+ if (!archived) return false;
173
118
  if (wasActive) {
174
119
  handleNewChat();
175
120
  }
176
- }
177
-
178
- function handleRenameThread(threadId: string, title: string) {
179
- void renameThread(threadId, title).then((renamed) => {
180
- if (!renamed) toast.error(t("chat.renameFailed"));
181
- });
121
+ return true;
182
122
  }
183
123
 
184
124
  return (
@@ -188,8 +128,8 @@ function BrainChatsSection({ open }: { open: boolean }) {
188
128
  aria-hidden={!open}
189
129
  >
190
130
  <div className="ms-4">
191
- <ChatHistoryRail
192
- items={chatItems}
131
+ <OwnershipChatHistoryRail
132
+ {...history}
193
133
  activeId={activeThreadId}
194
134
  onSelect={openThread}
195
135
  onNewChat={handleNewChat}
@@ -198,13 +138,6 @@ function BrainChatsSection({ open }: { open: boolean }) {
198
138
  showMore: t("chat.chats"),
199
139
  showLess: t("chat.chats"),
200
140
  }}
201
- previewCount={5}
202
- expandedCount={15}
203
- onTogglePin={(threadId) => {
204
- const thread = visibleThreads.find((item) => item.id === threadId);
205
- if (thread) void pinThread(threadId, !thread.pinnedAt);
206
- }}
207
- onRename={handleRenameThread}
208
141
  renameMaxLength={160}
209
142
  onDelete={(threadId) => void handleArchiveThread(threadId)}
210
143
  labels={{