@agent-native/core-corpus 0.210.2 → 0.211.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 (192) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +5 -3
  3. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +41 -2
  4. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +4 -2
  5. package/corpus/templates/analytics/CHANGELOG.md +18 -0
  6. package/corpus/templates/analytics/actions/build-data-index.ts +11 -2
  7. package/corpus/templates/analytics/actions/configure-content-collection.ts +29 -4
  8. package/corpus/templates/analytics/actions/configure-first-party-events-model.ts +9 -7
  9. package/corpus/templates/analytics/actions/create-analytics-public-key.ts +4 -0
  10. package/corpus/templates/analytics/actions/estimate-content-signups-cost.ts +72 -0
  11. package/corpus/templates/analytics/actions/estimate-query-cost.ts +16 -134
  12. package/corpus/templates/analytics/actions/list-sigma-workbooks.ts +123 -42
  13. package/corpus/templates/analytics/actions/migrate-sigma-workbook.ts +20 -17
  14. package/corpus/templates/analytics/actions/revoke-analytics-public-key.ts +4 -0
  15. package/corpus/templates/analytics/app/components/dashboard/SigmaMigrationDialog.tsx +246 -680
  16. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  17. package/corpus/templates/analytics/app/i18n/en-US.ts +54 -55
  18. package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +0 -1
  19. package/corpus/templates/analytics/app/pages/DataSources.tsx +56 -0
  20. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +4 -3
  21. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +26 -8
  22. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +3 -1
  23. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/panel-cost-notice.ts +172 -0
  24. package/corpus/templates/analytics/app/pages/data-sources/SearchConsoleDetail.tsx +441 -0
  25. package/corpus/templates/analytics/changelog/2026-10-15-a-content-dashboard-lists-every-blog-article-with-its-author.md +6 -0
  26. package/corpus/templates/analytics/changelog/2026-10-15-data-sources-has-a-google-search-console-card-that-shows-the.md +6 -0
  27. package/corpus/templates/analytics/changelog/2026-10-16-the-content-dashboard-can-show-signups-per-blog-article.md +6 -0
  28. package/corpus/templates/analytics/changelog/2026-10-19-creating-or-revoking-a-public-analytics-key-now-asks-you-to-.md +6 -0
  29. package/corpus/templates/analytics/changelog/2026-10-19-search-sigma-workbooks-by-name-and-open-ones-a-teammate-alre.md +6 -0
  30. package/corpus/templates/analytics/changelog/2026-10-20-blog-signup-attribution-excludes-signups-before-the-first-vi.md +6 -0
  31. package/corpus/templates/analytics/changelog/2026-10-20-collapsed-analytics-navigation-starts-at-the-top.md +6 -0
  32. package/corpus/templates/analytics/changelog/2026-10-20-content-keeps-successful-coverage-dates-and-missing-day-gaps.md +6 -0
  33. package/corpus/templates/analytics/changelog/2026-10-20-private-github-repositories-can-be-indexed-with-your-connect.md +6 -0
  34. package/corpus/templates/analytics/changelog/2026-10-20-search-console-preserves-backfill-and-paused-collection.md +6 -0
  35. package/corpus/templates/analytics/changelog/2026-10-20-search-console-refreshes-historical-coverage-when-properties.md +6 -0
  36. package/corpus/templates/analytics/changelog/2026-10-20-search-console-setup-keeps-saved-properties-readable-and-sup.md +6 -0
  37. package/corpus/templates/analytics/seeds/dashboards/content-view.json +182 -0
  38. package/corpus/templates/analytics/server/db/schema.ts +1 -0
  39. package/corpus/templates/analytics/server/lib/bi-migration/external-dashboards-store.ts +56 -18
  40. package/corpus/templates/analytics/server/lib/bi-migration/unfinished-import.ts +64 -0
  41. package/corpus/templates/analytics/server/lib/builtin-dashboard-pass.ts +17 -2
  42. package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +2 -0
  43. package/corpus/templates/analytics/server/lib/content-metrics/config.ts +92 -3
  44. package/corpus/templates/analytics/server/lib/content-metrics/legacy-content-view.json +172 -0
  45. package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +2 -0
  46. package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +8 -3
  47. package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +16 -3
  48. package/corpus/templates/analytics/server/lib/content-metrics/steps/signups.ts +240 -0
  49. package/corpus/templates/analytics/server/lib/content-metrics/store.ts +5 -0
  50. package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +21 -0
  51. package/corpus/templates/analytics/server/lib/dashboard-panel-verification.ts +300 -5
  52. package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +2 -0
  53. package/corpus/templates/analytics/server/lib/dashboards-store.ts +28 -0
  54. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +128 -24
  55. package/corpus/templates/analytics/server/lib/google-analytics.ts +56 -16
  56. package/corpus/templates/analytics/server/lib/index-build-github.ts +5 -0
  57. package/corpus/templates/analytics/server/lib/provider-credentials.ts +47 -2
  58. package/corpus/templates/analytics/server/lib/query-cost-estimate.ts +184 -5
  59. package/corpus/templates/analytics/server/plugins/db.ts +9 -1
  60. package/corpus/templates/analytics/shared/builtin-dashboard-ids.ts +2 -0
  61. package/corpus/templates/analytics/shared/search-console-property.ts +78 -0
  62. package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +5 -3
  63. package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
  64. package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
  65. package/corpus/templates/calendar/actions/delete-event.ts +1 -3
  66. package/corpus/templates/calendar/actions/delete-events.ts +1 -2
  67. package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
  68. package/corpus/templates/calendar/actions/update-event.ts +2 -6
  69. package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  70. package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
  71. package/corpus/templates/clips/CHANGELOG.md +11 -24
  72. package/corpus/templates/clips/actions/create-recording-upload.ts +5 -1
  73. package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
  74. package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
  75. package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
  76. package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
  77. package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
  78. package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
  79. package/corpus/templates/clips/app/root.tsx +22 -10
  80. package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
  81. package/corpus/templates/clips/app/routes/import.tsx +115 -110
  82. package/corpus/templates/clips/changelog/2026-10-16-agent-uploads-can-use-free-clips-storage.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
  87. package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
  88. package/corpus/templates/clips/desktop/src/app.tsx +29 -186
  89. package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
  90. package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
  91. package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
  92. package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
  93. package/corpus/templates/clips/server/lib/recording-upload-ticket.ts +8 -0
  94. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +2 -0
  95. package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
  96. package/corpus/templates/content/CHANGELOG.md +12 -0
  97. package/corpus/templates/content/actions/_content-database-personal-view.ts +5 -15
  98. package/corpus/templates/content/actions/_database-navigation.ts +3 -3
  99. package/corpus/templates/content/actions/_database-utils.ts +10 -22
  100. package/corpus/templates/content/actions/_database-view-setup.ts +16 -76
  101. package/corpus/templates/content/actions/create-comment-ai-suggestion.ts +93 -44
  102. package/corpus/templates/content/actions/export-document.ts +2 -14
  103. package/corpus/templates/content/actions/suggest-document-edit.ts +5 -7
  104. package/corpus/templates/content/actions/update-content-database-view.ts +2 -14
  105. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +86 -30
  106. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +64 -28
  107. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -46
  108. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
  109. package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
  110. package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
  111. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
  112. package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +2 -1
  113. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +150 -166
  114. package/corpus/templates/content/app/components/editor/database/database-cell-comments.tsx +515 -0
  115. package/corpus/templates/content/app/components/editor/database/database-item-comments.tsx +5 -43
  116. package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
  117. package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
  118. package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
  119. package/corpus/templates/content/app/components/editor/property-comments.tsx +35 -4
  120. package/corpus/templates/content/app/components/editor/property-date-format.ts +45 -0
  121. package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
  122. package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
  123. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
  124. package/corpus/templates/content/app/global.css +23 -0
  125. package/corpus/templates/content/app/i18n-data.ts +1 -0
  126. package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
  127. package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
  128. package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
  129. package/corpus/templates/content/changelog/2026-10-19-edits-made-from-a-block-s-menu-or-a-table-s-row-and-column-m.md +6 -0
  130. package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
  131. package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
  132. package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +5 -1
  133. package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
  134. package/corpus/templates/content/e2e/helpers.ts +10 -0
  135. package/corpus/templates/content/shared/api.ts +18 -12
  136. package/corpus/templates/content/shared/database-query.ts +107 -13
  137. package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
  138. package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
  139. package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
  140. package/corpus/templates/design/CHANGELOG.md +2 -0
  141. package/corpus/templates/design/actions/delete-file.ts +1 -4
  142. package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
  143. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
  144. package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
  145. package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
  146. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
  147. package/corpus/templates/design/changelog/2026-10-19-reply-and-edit-on-a-comment-in-the-sidebar-or-on-a-canvas-pi.md +6 -0
  148. package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
  149. package/corpus/templates/design/e2e/global-setup.ts +185 -85
  150. package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
  151. package/corpus/templates/dispatch/CHANGELOG.md +4 -0
  152. package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
  153. package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
  154. package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
  155. package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
  156. package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
  157. package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
  158. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  159. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  160. package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  161. package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
  162. package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
  163. package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
  164. package/corpus/templates/mail/AGENTS.md +1 -1
  165. package/corpus/templates/mail/CHANGELOG.md +14 -0
  166. package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
  167. package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
  168. package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
  169. package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
  170. package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
  171. package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
  172. package/corpus/templates/mail/app/root.tsx +10 -2
  173. package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
  174. package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
  175. package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
  176. package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
  177. package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
  178. package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
  179. package/corpus/templates/mail/server/handlers/emails.ts +9 -0
  180. package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
  181. package/corpus/templates/plan/CHANGELOG.md +4 -0
  182. package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
  183. package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
  184. package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
  185. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
  186. package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
  187. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
  188. package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
  189. package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
  190. package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
  191. package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
  192. package/package.json +1 -1
@@ -1,9 +1,12 @@
1
1
  import { createContext, useContext, useEffect } from "react";
2
2
 
3
+ import { holdPageDraftWriter } from "./page-draft-journal";
4
+
3
5
  /**
4
6
  * Lets the mounted editor tell PageDraftRecovery which editor session is live.
5
7
  * A recovery draft written by that session belongs to the editor's own save
6
- * queue, so recovery must not replace the editor while it settles.
8
+ * queue, so recovery must not replace the editor while it settles. Other tabs
9
+ * learn the session is live through its writer lock.
7
10
  */
8
11
  export const LiveEditorSessionContext = createContext<
9
12
  ((editorSessionId: string | null) => void) | null
@@ -16,4 +19,5 @@ export function useRegisterLiveEditorSession(editorSessionId: string): void {
16
19
  register(editorSessionId);
17
20
  return () => register(null);
18
21
  }, [editorSessionId, register]);
22
+ useEffect(() => holdPageDraftWriter(editorSessionId), [editorSessionId]);
19
23
  }
@@ -52,6 +52,63 @@ export class PageDraftJournalError extends Error {
52
52
  const PREFIX = "content-page-draft-journal-v1:";
53
53
  // Retired: these markers kept a permanent "saved to History" banner on screen.
54
54
  const LEGACY_RETAINED_PREFIX = "content-page-draft-retained-v1:";
55
+ const WRITER_LOCK_PREFIX = "content-page-draft-writer-v1:";
56
+
57
+ function lockManager(): LockManager | null {
58
+ return typeof navigator === "undefined" ? null : (navigator.locks ?? null);
59
+ }
60
+
61
+ /**
62
+ * Holds a Web Lock named for this editor session until the returned release
63
+ * runs. Every tab shares the journal, so the lock is how another tab tells an
64
+ * entry whose editor is still open from one a closed or reloaded tab left
65
+ * behind; the browser drops it with the tab.
66
+ */
67
+ export function holdPageDraftWriter(writerId: string): () => void {
68
+ const locks = lockManager();
69
+ if (!locks) return () => {};
70
+ let released = false;
71
+ let release: (() => void) | null = null;
72
+ locks
73
+ .request(
74
+ WRITER_LOCK_PREFIX + writerId,
75
+ () =>
76
+ new Promise<void>((resolve) => {
77
+ if (released) resolve();
78
+ else release = resolve;
79
+ }),
80
+ )
81
+ .catch((error: unknown) => {
82
+ // coercion-ok: A writer whose lock was refused reads as gone, so another
83
+ // tab may replay its journal, as it did before writer locks. Reading it
84
+ // as live instead would strand that journal once this tab closed.
85
+ console.warn(
86
+ "Could not hold the page draft writer lock; another tab may replay this editor's draft",
87
+ error,
88
+ );
89
+ });
90
+ return () => {
91
+ released = true;
92
+ release?.();
93
+ };
94
+ }
95
+
96
+ /**
97
+ * The writer IDs whose editors are open in some tab, or null when this
98
+ * browser has no Web Locks and cannot tell.
99
+ */
100
+ export async function liveDraftWriterIds(): Promise<ReadonlySet<string> | null> {
101
+ const locks = lockManager();
102
+ if (!locks) return null;
103
+ const { held = [] } = await locks.query();
104
+ return new Set(
105
+ held.flatMap(({ name }) =>
106
+ name?.startsWith(WRITER_LOCK_PREFIX)
107
+ ? [name.slice(WRITER_LOCK_PREFIX.length)]
108
+ : [],
109
+ ),
110
+ );
111
+ }
55
112
 
56
113
  function storage(): Storage {
57
114
  try {
@@ -337,10 +394,13 @@ export function listPageDraftJournal(
337
394
 
338
395
  export function readPageDraftJournal(
339
396
  scope: Omit<PageDraftJournalScope, "writerId">,
397
+ skipWriterIds: ReadonlySet<string> = new Set(),
340
398
  ): PageDraftJournalEntry | null {
341
399
  return (
342
400
  listPageDraftJournal(scope).find(
343
- (entry) => entry.recoveryStatus !== "retained_in_history",
401
+ (entry) =>
402
+ entry.recoveryStatus !== "retained_in_history" &&
403
+ !skipWriterIds.has(entry.scope.writerId),
344
404
  ) ?? null
345
405
  );
346
406
  }
@@ -12,6 +12,11 @@ import {
12
12
  import type { CommentThread } from "@/hooks/use-comments";
13
13
  import { cn } from "@/lib/utils";
14
14
 
15
+ import {
16
+ formatPropertyDate,
17
+ formatPropertyDateTime,
18
+ } from "./property-date-format";
19
+
15
20
  /**
16
21
  * What a Page editor tells its Properties about their Comment threads, and how
17
22
  * a Property starts or opens one. Without a provider, Properties show no
@@ -60,9 +65,10 @@ export function bodyCommentThreads(
60
65
  export interface PropertyThreadTarget {
61
66
  name: string;
62
67
  /**
63
- * The value when the thread started. Threads that recorded none (one
64
- * still being saved, or a relation, rollup, or formula) show the value
65
- * now, and nothing once the Property is gone.
68
+ * The value when the thread started, with dates read as the table shows
69
+ * them. Threads that recorded none (one still being saved, or a relation,
70
+ * rollup, or formula) show the value now, and nothing once the Property is
71
+ * gone.
66
72
  */
67
73
  value: string | null;
68
74
  /**
@@ -73,6 +79,29 @@ export interface PropertyThreadTarget {
73
79
  state: "current" | "changed" | "removed" | "unknown";
74
80
  }
75
81
 
82
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
83
+ const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
84
+
85
+ /** ISO text as Properties show dates; anything else as it was written. */
86
+ function readableDate(text: string) {
87
+ if (ISO_DATE.test(text)) return formatPropertyDate(text);
88
+ if (ISO_DATE_TIME.test(text)) return formatPropertyDateTime(text);
89
+ return text;
90
+ }
91
+
92
+ /**
93
+ * A remembered value as its Property shows values. Comments remember dates
94
+ * as ISO text, and a range as "start - end". A value recorded before the
95
+ * Property became a date keeps its text.
96
+ */
97
+ function readablePropertyValue(value: string, type: string | undefined) {
98
+ if (type === "date") return value.split(" - ").map(readableDate).join(" - ");
99
+ if (type === "created_time" || type === "last_edited_time") {
100
+ return readableDate(value);
101
+ }
102
+ return value;
103
+ }
104
+
76
105
  export function propertyThreadTarget(
77
106
  property: NonNullable<CommentThread["property"]>,
78
107
  properties: readonly DocumentProperty[] | undefined,
@@ -84,7 +113,9 @@ export function propertyThreadTarget(
84
113
  const value = property.propertyValue ?? currentValue;
85
114
  return {
86
115
  name: property.propertyName ?? current?.definition.name ?? "",
87
- value,
116
+ value: value
117
+ ? readablePropertyValue(value, current?.definition.type)
118
+ : value,
88
119
  state: !properties
89
120
  ? "unknown"
90
121
  : !current
@@ -0,0 +1,45 @@
1
+ /** A date as Properties show it, such as "Nov 2, 2026". */
2
+ export function formatPropertyDate(value: string) {
3
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
4
+ const [year, month, day] = value.split("-").map(Number);
5
+ return new Intl.DateTimeFormat(undefined, {
6
+ month: "short",
7
+ day: "numeric",
8
+ year: "numeric",
9
+ }).format(new Date(year, month - 1, day));
10
+ }
11
+
12
+ const date = new Date(value);
13
+ if (Number.isNaN(date.getTime())) return value;
14
+ return new Intl.DateTimeFormat(undefined, {
15
+ month: "short",
16
+ day: "numeric",
17
+ year: "numeric",
18
+ }).format(date);
19
+ }
20
+
21
+ /** A date and time as Properties show it, such as "Nov 2, 2026, 3:30 PM". */
22
+ export function formatPropertyDateTime(value: string) {
23
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value)) {
24
+ const [datePart, timePart] = value.split("T");
25
+ const [year, month, day] = datePart.split("-").map(Number);
26
+ const [hour, minute] = timePart.split(":").map(Number);
27
+ return new Intl.DateTimeFormat(undefined, {
28
+ month: "short",
29
+ day: "numeric",
30
+ year: "numeric",
31
+ hour: "numeric",
32
+ minute: "2-digit",
33
+ }).format(new Date(year, month - 1, day, hour, minute));
34
+ }
35
+
36
+ const date = new Date(value);
37
+ if (Number.isNaN(date.getTime())) return value;
38
+ return new Intl.DateTimeFormat(undefined, {
39
+ month: "short",
40
+ day: "numeric",
41
+ year: "numeric",
42
+ hour: "numeric",
43
+ minute: "2-digit",
44
+ }).format(date);
45
+ }
@@ -57,6 +57,36 @@ export function suggestionsByThread(
57
57
  return grouped;
58
58
  }
59
59
 
60
+ function anchorStart(suggestion: ResourceSuggestion): number {
61
+ const from = (suggestion.operations[0]?.anchor as { from?: unknown } | null)
62
+ ?.from;
63
+ return typeof from === "number" ? from : Number.POSITIVE_INFINITY;
64
+ }
65
+
66
+ /**
67
+ * The edits reviewed together in a thread: every edit of one proposal in a
68
+ * group, in Page order, and any other suggestion alone. Groups keep the order
69
+ * of their first edit.
70
+ */
71
+ export function proposalGroups(
72
+ suggestions: readonly ResourceSuggestion[],
73
+ ): ResourceSuggestion[][] {
74
+ const groups = new Map<string, ResourceSuggestion[]>();
75
+ for (const suggestion of suggestions) {
76
+ const key = suggestion.proposalId
77
+ ? `proposal:${suggestion.proposalId}`
78
+ : suggestion.id;
79
+ groups.set(key, [...(groups.get(key) ?? []), suggestion]);
80
+ }
81
+ return [...groups.values()].map((group) =>
82
+ group.length > 1
83
+ ? [...group].sort(
84
+ (left, right) => anchorStart(left) - anchorStart(right) || 0,
85
+ )
86
+ : group,
87
+ );
88
+ }
89
+
60
90
  // Suggestion ids are URL-safe, so the encoded id in the link is the id itself.
61
91
  // The receipt's summary has its brackets stripped, so it is the whole comment.
62
92
  const RECEIPT_LINK = /^\[[^[\]]*\]\([^)\s]*[?&]suggestion=([\w.~-]+)[^)\s]*\)$/;
@@ -25,6 +25,24 @@ export interface ReconcileRecoveryDraft {
25
25
  localTitle: string;
26
26
  }
27
27
 
28
+ // After typing overtakes an automatic save, the next one waits for a pause,
29
+ // as the editor's autosave does, but never longer than the cap.
30
+ const AUTOMATIC_SAVE_IDLE_MS = 1_000;
31
+ const AUTOMATIC_SAVE_MAX_WAIT_MS = 5_000;
32
+
33
+ async function untilTypingPauses(getIdentity: () => string): Promise<void> {
34
+ const deadline = Date.now() + AUTOMATIC_SAVE_MAX_WAIT_MS;
35
+ let identity = getIdentity();
36
+ for (;;) {
37
+ const wait = Math.min(AUTOMATIC_SAVE_IDLE_MS, deadline - Date.now());
38
+ if (wait <= 0) return;
39
+ await new Promise((resolve) => setTimeout(resolve, wait));
40
+ const next = getIdentity();
41
+ if (next === identity) return;
42
+ identity = next;
43
+ }
44
+ }
45
+
28
46
  interface AutomaticSave {
29
47
  generation: number;
30
48
  newerBase: ReconcileSaveBase | null;
@@ -190,7 +208,6 @@ export function useDocumentReconcileRecovery({
190
208
  const started = run.generation;
191
209
  let saveBase: ReconcileSaveBase | undefined = base;
192
210
  let draft = initialDraft;
193
- let attempts = 0;
194
211
  let refused = false;
195
212
  try {
196
213
  while (generation.current === started && !current.current) {
@@ -201,8 +218,7 @@ export function useDocumentReconcileRecovery({
201
218
  draft = latestDraft();
202
219
  saveBase = run.newerBase;
203
220
  run.newerBase = null;
204
- attempts = 0;
205
- } else if (refused || attempts === 3) {
221
+ } else if (refused) {
206
222
  await retainLatest();
207
223
  // A newer merge can hand its page over while the draft is retained.
208
224
  if (run.newerBase) continue;
@@ -211,7 +227,6 @@ export function useDocumentReconcileRecovery({
211
227
  }
212
228
  return false;
213
229
  }
214
- attempts += 1;
215
230
  const identity = callbacks.current.getSaveIdentity();
216
231
  const persisted = await callbacks.current.save(draft, saveBase);
217
232
  if (generation.current !== started || current.current) {
@@ -220,7 +235,11 @@ export function useDocumentReconcileRecovery({
220
235
  }
221
236
  if (persisted && callbacks.current.getSaveIdentity() === identity)
222
237
  return true;
238
+ // A save that landed while typing continued is not a retry: counting
239
+ // those turns steady typing in another tab into a conflict notice.
223
240
  refused = !persisted;
241
+ if (persisted)
242
+ await untilTypingPauses(() => callbacks.current.getSaveIdentity());
224
243
  draft = latestDraft();
225
244
  saveBase = undefined;
226
245
  }
@@ -54,6 +54,7 @@ import {
54
54
  import {
55
55
  clearPageDraftJournal,
56
56
  listPageDraftJournal,
57
+ liveDraftWriterIds,
57
58
  PageDraftJournalError,
58
59
  readPageDraftJournal,
59
60
  writePageDraftJournal,
@@ -253,29 +254,54 @@ export function readCreateRecoveryDraft(
253
254
  }
254
255
  }
255
256
 
257
+ type DraftRead = () =>
258
+ | PageDraftJournalEntry
259
+ | null
260
+ | Promise<PageDraftJournalEntry | null>;
261
+
256
262
  export async function retryCreateAfterDraftRead<T>(
257
- read: () => PageDraftJournalEntry | null,
263
+ read: DraftRead,
258
264
  create: () => Promise<T>,
259
265
  ): Promise<{ created: T; draft: PageDraftJournalEntry | null }> {
260
- const beforeCreate = readCreateRecoveryDraft(read);
261
- if (beforeCreate.kind === "unreadable") throw beforeCreate.error;
262
-
266
+ // An unreadable journal stops the create before it starts.
267
+ await read();
263
268
  const created = await create();
264
- const afterCreate = readCreateRecoveryDraft(read);
265
- if (afterCreate.kind === "unreadable") throw afterCreate.error;
266
- return { created, draft: afterCreate.entry };
269
+ return { created, draft: await read() };
267
270
  }
268
271
 
269
272
  export function retryCreateAfterIntentWrite<T>(
270
273
  scope: DocumentCreateIntentScope,
271
274
  intent: DocumentCreateIntent,
272
- read: () => PageDraftJournalEntry | null,
275
+ read: DraftRead,
273
276
  create: () => Promise<T>,
274
277
  ): Promise<{ created: T; draft: PageDraftJournalEntry | null }> {
275
278
  writeDocumentCreateIntentBestEffort(scope, { ...intent, status: "pending" });
276
279
  return retryCreateAfterDraftRead(read, create);
277
280
  }
278
281
 
282
+ /**
283
+ * Reads the draft to replay into a page this tab creates for a pending intent.
284
+ * An editor open in another tab saves its own draft, and replaying that draft
285
+ * here would race its save, so its entries are skipped. Each read asks again
286
+ * which editors are open: one can open while the page is being created.
287
+ */
288
+ export function readReplayableDraft(
289
+ scope: Parameters<typeof readPageDraftJournal>[0],
290
+ ): () => Promise<PageDraftJournalEntry | null> {
291
+ return async () => {
292
+ let skipped = await liveDraftWriterIds();
293
+ for (;;) {
294
+ const entry = readPageDraftJournal(scope, skipped ?? undefined);
295
+ if (!entry || !skipped) return entry;
296
+ // An editor can open and write its first entry between the lock query
297
+ // and the read, so the entry's writer is checked after reading it.
298
+ const live = await liveDraftWriterIds();
299
+ if (!live?.has(entry.scope.writerId)) return entry;
300
+ skipped = new Set([...skipped, ...live]);
301
+ }
302
+ };
303
+ }
304
+
279
305
  export async function readDocumentBeforeCreateDiscard(
280
306
  read: () => Promise<Document>,
281
307
  ): Promise<{ kind: "committed"; document: Document } | { kind: "missing" }> {
@@ -2224,7 +2250,7 @@ export function DocumentSidebar({
2224
2250
  showRetry();
2225
2251
  return;
2226
2252
  }
2227
- const read = () => readPageDraftJournal(draftScope);
2253
+ const read = readReplayableDraft(draftScope);
2228
2254
  const { created, draft } = await retryCreateAfterIntentWrite(
2229
2255
  scope,
2230
2256
  currentIntent,
@@ -2308,7 +2334,7 @@ export function DocumentSidebar({
2308
2334
  clearPageDraftJournal(draft.scope, draft.snapshot);
2309
2335
  }
2310
2336
  }
2311
- const remainingDraft = read();
2337
+ const remainingDraft = await read();
2312
2338
  confirmedDocument = mergeCreatedDocumentWithDraft(
2313
2339
  confirmedDocument,
2314
2340
  remainingDraft,
@@ -2458,6 +2458,29 @@ audio.media-block__content {
2458
2458
  background-color: hsl(45 93% 47% / 0.15);
2459
2459
  }
2460
2460
 
2461
+ /* A table cell's Comment card. Its thread and composer cards draw their own
2462
+ frame, so the layer only places them. */
2463
+ .database-cell-comment-card {
2464
+ z-index: 290;
2465
+ width: 20rem;
2466
+ max-width: calc(100vw - 1.5rem);
2467
+ outline: none;
2468
+ --comment-popover-max-height: min(
2469
+ 32rem,
2470
+ calc(var(--radix-popover-content-available-height) - 0.5rem)
2471
+ );
2472
+ max-height: calc(var(--radix-popover-content-available-height) - 0.5rem);
2473
+ }
2474
+ /* Threads taller than the room scroll rather than run off the screen. Only
2475
+ then, since scrolling clips the cards' shadows. */
2476
+ .database-cell-comment-card[data-overflowing] {
2477
+ overflow-y: auto;
2478
+ overscroll-behavior: contain;
2479
+ }
2480
+ .database-cell-comment-card [data-comments-sidebar] {
2481
+ padding-bottom: 0;
2482
+ }
2483
+
2461
2484
  .content-editor .suggestion-settling-original {
2462
2485
  display: none;
2463
2486
  }
@@ -1557,6 +1557,7 @@ const enUS = {
1557
1557
  openRowComments_few: "{{count}} open comments on {{title}}",
1558
1558
  openRowComments_many: "{{count}} open comments on {{title}}",
1559
1559
  openRowComments_other: "{{count}} open comments on {{title}}",
1560
+ openRow: "Open row",
1560
1561
  reply: "Reply...",
1561
1562
  reopen: "Reopen",
1562
1563
  suggestions: "Suggestions",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-16
4
+ ---
5
+
6
+ Opening a page in a new tab while another tab is still saving no longer sends a second save or files a copy to History.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-16
4
+ ---
5
+
6
+ Typing steadily in one tab no longer makes another open tab show "This page changed elsewhere" after its edits saved.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-10-19
4
+ ---
5
+
6
+ Database views can filter one property more than once, number filters add ≥ and ≤, and filter chips name their operator, like "Status: is not done".
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-19
4
+ ---
5
+
6
+ Edits made from a block's menu or a table's row and column menus, such as deleting a table, adding a row or turning off the header row, are now saved instead of coming back after a reload
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-19
4
+ ---
5
+
6
+ Starting a comment puts the cursor in the comment box, ready to type.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-19
4
+ ---
5
+
6
+ Table row and column options now appear for the row or column under the pointer, Escape selects the cell before leaving the editor, wide tables keep their add-column control on screen on phones, and a tap near a table's edge on a touch screen edits the cell there instead of adding a row or column
@@ -39,7 +39,7 @@ A reviewer comments on two Blocks in a brief, replies with a Page reference, and
39
39
 
40
40
  - A Comment thread is owned by one Page and may target one or several Blocks or ranges in its Blocks fields.
41
41
  - A thread on a Collection row can instead target one Property value, such as Status or a date. It records the Property's name and the value it had when the thread started. Relations, rollups, and formulas are the exception: what they show depends on who is reading, so the thread records no value and each reader sees their own current one. Once a recorded value changes, the thread shows what it was; once the Property is deleted, the thread says so. It never moves itself onto the newer value. A Property thread has no quoted text, and replies follow the ordinary thread rules.
42
- - A Collection table shows how many open threads each row has, and marks each cell whose Property has an open thread. Opening either shows the row's Comments, at that cell's thread when there is one. Counts include only rows the viewer can read, so a row they cannot open adds nothing. One Action reads every open thread in a Collection, with each thread's row and target, so an agent need not read row by row.
42
+ - A Collection table shows how many open threads each row has, and marks each cell whose Property has an open thread. The row's count opens the row's Comments. A cell's marker opens a card on the cell with that Property's open threads, where they can be read, replied to, and resolved without leaving the table, and the card can open the row at any of them. Any cell also offers a Comment button to anyone who can comment on the row, which starts a thread on its Property in the same card; on touch screens, where nothing hovers, the button shows on a selected row and starts the thread in the row's peek, or on the row's full page when the Collection opens rows as pages. Counts include only rows the viewer can read, so a row they cannot open adds nothing. One Action reads every open thread in a Collection, with each thread's row and target, so an agent need not read row by row.
43
43
  - Rich Comment bodies use the shared Blocks-field grammar; replies and mentions retain the same Page authority.
44
44
  - Anchors follow stable Block identity where possible and preserve historical target context after deletion rather than attaching to plausible new text.
45
45
  - Resolve, reopen, edit, reply, and notification operations use shared Actions and record attributable change.
@@ -75,6 +75,10 @@ Given a Comment that mentions a teammate and Claude, when it is sent, both names
75
75
 
76
76
  Given a Collection row whose Status is Blocked, when a commenter starts a thread on Status from the row's peek, the thread reads "Status · Blocked" with no "Highlight unavailable" warning, and the Status cell in the table shows a marker that opens that thread. When Status later changes to Done, the thread reads "was Blocked"; when the Property is deleted, it reads "Status (removed)". A thread started on a Property that is not on the Page, or on a Property and quoted text at once, is rejected.
77
77
 
78
+ ### Comment on a cell from the table
79
+
80
+ Given a Collection table and a commenter who cannot edit its rows, when they hover a row's Due cell, a Comment button appears. Posting from the card it opens starts a thread that reads "Due · Nov 2, 2026" without opening the row, and the cell then shows a marker and the row's count goes up. Clicking the marker reopens the card on that thread, where it can be answered, and Open row opens the row's peek on it. When an editor resolves the thread from the same card, the marker and the count clear, and an editor who clicks the cell itself still edits its value.
81
+
78
82
  ### See open Comments from a Collection
79
83
 
80
84
  Given a Collection where one row has two open threads and a private row has one, a viewer who can read every row but the private one sees "2" beside the first row's name and no count on the private row. Clicking the count opens that row's peek with its Comments showing. Resolving the last open thread clears the count. An agent listing the Collection's Comments in one call gets the same threads, each naming its row and target, and nothing from the private row.
@@ -79,7 +79,9 @@ Saving never ends Suggesting mode; only the author's own switch does. Reloading
79
79
 
80
80
  Agents propose body suggestions through `suggest-document-edit` (Content action, `mcpTool` with a fully typed agent input schema): the agent supplies the document id, its `baseRevision` from `get-document`, an `idempotencyKey`, and exact find/replace text; the action resolves the match, builds the full before/after Markdown payload and the contextual anchor, and delegates to `create-resource-suggestion` so access, idempotent replay, threads, and notifications stay in one path. Malformed or stale calls fail with errors that name the fix (`SUGGESTION_FIND_NOT_FOUND`, `SUGGESTION_FIND_AMBIGUOUS`, `SUGGESTION_EDIT_PROTOCOL_REQUIRED`). The generic action's Markdown payload errors also state the expected object shape. Focused Content db tests cover creation, idempotent replay, missing and ambiguous finds, the external-caller protocol, and proposals on ordinary database-item Pages. Acceptance tests cover primary Blocks-field reconciliation across multiple collection memberships.
81
81
 
82
- `create-resource-suggestion` accepts `supersedes`: the author's own earlier pending suggestions on the same resource become `superseded`, with a durable decision record and resolved discussion, in the same transaction that creates the replacement. Already decided suggestions are left alone, another author's or another resource's suggestion is refused, and an exact retry replays without superseding twice (`supersede.spec.ts`). Content uses it so a revised Comment AI proposal replaces the requester's earlier pending one from that thread.
82
+ `create-resource-suggestion` accepts `supersedes`: the author's own earlier pending suggestions on the same resource become `superseded`, with a durable decision record and resolved discussion, in the same transaction that creates the replacement. Already decided suggestions are left alone, another author's or another resource's suggestion is refused, and an exact retry replays without superseding twice (`supersede.spec.ts`). Content uses it so a revised Comment AI proposal replaces the requester's earlier pending one from that thread. `create-resource-suggestion-proposal` accepts the same `supersedes` list, so a revised proposal of several edits replaces every pending edit of the one before it in one step.
83
+
84
+ Comment AI proposals use the same small-edit builder as `suggest-document-edit`. `create-comment-ai-suggestion` takes a list of exact find/replace edits; each is cut down to the words that change, so a request like "hyphenate it throughout" becomes one small suggestion per occurrence instead of one replacement that strikes through and re-inserts the unchanged text between them. Several edits are created as one proposal, and the comment thread shows its summary once above a word diff of each edit, with per-edit Accept and Reject plus Accept remaining and Reject remaining (`comment-ai-flow.test.ts`, `ThreadSuggestion.test.tsx`).
83
85
 
84
86
  ## Proof plan
85
87
 
@@ -53,6 +53,16 @@ export const RECOVERY_UI = [
53
53
  "Something went wrong",
54
54
  ];
55
55
 
56
+ /**
57
+ * Actions that need approval run over HTTP only for the signed-in app UI, which
58
+ * proves itself with this capability cookie. A spec that calls one directly
59
+ * mints it first, as the app's client does before its writes.
60
+ */
61
+ export async function presentUiCapability(page: Page): Promise<void> {
62
+ const response = await page.request.get("/_agent-native/ui-capability");
63
+ expect(response.ok(), `ui-capability (${response.status()})`).toBe(true);
64
+ }
65
+
56
66
  export async function postAction<T = Record<string, unknown>>(
57
67
  page: Page,
58
68
  name: string,
@@ -372,19 +372,25 @@ export interface ContentDatabaseSort {
372
372
  direction: ContentDatabaseSortDirection;
373
373
  }
374
374
 
375
+ export const CONTENT_DATABASE_FILTER_OPERATORS = [
376
+ "contains",
377
+ "equals",
378
+ "does_not_equal",
379
+ "greater_than",
380
+ "less_than",
381
+ "greater_than_or_equal",
382
+ "less_than_or_equal",
383
+ "before",
384
+ "after",
385
+ "between",
386
+ "is_checked",
387
+ "is_unchecked",
388
+ "is_empty",
389
+ "is_not_empty",
390
+ ] as const;
391
+
375
392
  export type ContentDatabaseFilterOperator =
376
- | "contains"
377
- | "equals"
378
- | "does_not_equal"
379
- | "greater_than"
380
- | "less_than"
381
- | "before"
382
- | "after"
383
- | "between"
384
- | "is_checked"
385
- | "is_unchecked"
386
- | "is_empty"
387
- | "is_not_empty";
393
+ (typeof CONTENT_DATABASE_FILTER_OPERATORS)[number];
388
394
 
389
395
  export interface ContentDatabaseFilter {
390
396
  key: "name" | (string & {});