@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,10 +1,10 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
2
  import createSuggestion from "@agent-native/core/review/suggestions/actions/create-resource-suggestion";
3
+ import createSuggestionProposal from "@agent-native/core/review/suggestions/actions/create-resource-suggestion-proposal";
3
4
  import listSuggestions from "@agent-native/core/review/suggestions/actions/list-resource-suggestions";
4
5
  import { and, eq, lt } from "drizzle-orm";
5
6
  import { z } from "zod";
6
7
 
7
- import { markdownSuggestionOperation } from "../app/components/editor/suggestions/markdown-operation.js";
8
8
  import { getDb, schema } from "../server/db/index.js";
9
9
  import {
10
10
  CommentAiOperationError,
@@ -20,6 +20,10 @@ import {
20
20
  import { CONTENT_DOCUMENT_SUGGESTION_ADAPTER } from "../server/lib/suggested-edits.js";
21
21
  import type { CommentAiRequest } from "../shared/comment-ai.js";
22
22
  import { resolveDocumentTextEdits } from "../shared/document-text-edits.js";
23
+ import {
24
+ markdownSuggestionOperationsForFindReplaceEdits,
25
+ markdownSuggestionOperationSummary,
26
+ } from "../shared/suggestion-diff.js";
23
27
  import { documentRevisionToken } from "./_document-edit-mutation.js";
24
28
  import { addCommentWithGuard, commentIdForIdempotency } from "./add-comment.js";
25
29
 
@@ -33,25 +37,38 @@ const payloadSchema = z.object({
33
37
  .describe(
34
38
  "One or two sentences to the commenter about this change, including anything they should check; shown on the suggestion in their comment thread",
35
39
  ),
36
- find: z
37
- .string()
40
+ edits: z
41
+ .array(
42
+ z.object({
43
+ find: z
44
+ .string()
45
+ .min(1)
46
+ .max(24000)
47
+ .describe("Exact unique text to propose replacing"),
48
+ replace: z
49
+ .string()
50
+ .max(24000)
51
+ .describe("Proposed replacement; canonical text remains unchanged"),
52
+ }),
53
+ )
38
54
  .min(1)
39
- .max(24000)
40
- .describe("Exact unique text to propose replacing"),
41
- replace: z
42
- .string()
43
- .max(24000)
44
- .describe("Proposed replacement; canonical text remains unchanged"),
55
+ .max(20)
56
+ .describe(
57
+ "One entry per separate change. Each find holds the words that change plus only enough surrounding text to occur once, so changes in different places become separate edits instead of one span of unchanged text between them.",
58
+ ),
45
59
  });
46
60
 
47
61
  /**
48
62
  * Earlier pending proposals the requester got in this thread are replaced by
49
- * a revision instead of stacking up. Only requests made before this one count,
50
- * so a retry builds the same list and its creation receipt still matches.
63
+ * a revision instead of stacking up: those from requests made before this one,
64
+ * and those an earlier attempt of this request created. Edits from this
65
+ * attempt never count, so replaying it builds the same list and its creation
66
+ * receipt still matches.
51
67
  */
52
68
  async function supersededSuggestionIds(
53
69
  request: Awaited<ReturnType<typeof requireCommentAiRequest>>,
54
70
  ctx: Parameters<typeof listSuggestions.run>[1],
71
+ attemptId: string,
55
72
  ) {
56
73
  const earlier = await getDb()
57
74
  .select({ id: schema.commentAiRequests.id })
@@ -64,8 +81,11 @@ async function supersededSuggestionIds(
64
81
  lt(schema.commentAiRequests.createdAt, request.createdAt),
65
82
  ),
66
83
  );
67
- if (!earlier.length) return [];
68
84
  const earlierIds = new Set(earlier.map((row) => row.id));
85
+ // Only an earlier request, or an earlier attempt of this one, can have left
86
+ // edits to replace. Attempts are counted rather than results, because an
87
+ // attempt can create edits and then fail before recording them.
88
+ if (!earlierIds.size && request.attemptCount <= 1) return [];
69
89
  const { suggestions } = await listSuggestions.run(
70
90
  { resourceType: "document", resourceId: request.documentId },
71
91
  ctx,
@@ -76,10 +96,12 @@ async function supersededSuggestionIds(
76
96
  suggestion.adapterKind === CONTENT_DOCUMENT_SUGGESTION_ADAPTER &&
77
97
  suggestion.authorEmail === request.requesterEmail &&
78
98
  suggestion.metadata?.sourceThreadId === request.threadId &&
79
- earlierIds.has(String(suggestion.metadata?.commentAiRequestId)),
99
+ (earlierIds.has(String(suggestion.metadata?.commentAiRequestId)) ||
100
+ (suggestion.metadata?.commentAiRequestId === request.id &&
101
+ suggestion.metadata?.commentAiAttemptId !== attemptId)),
80
102
  )
81
103
  .sort((left, right) => left.createdAt.localeCompare(right.createdAt))
82
- .slice(-20)
104
+ .slice(-100)
83
105
  .map((suggestion) => suggestion.id);
84
106
  }
85
107
 
@@ -112,7 +134,7 @@ function targetError(kind: "missing" | "ambiguous" | "overlapping") {
112
134
 
113
135
  export default defineAction({
114
136
  description:
115
- "Create one anchored suggestion against the latest Page text and link it to the original feedback. Pass the latest attemptId from get-comment-ai-context. Unrelated edits are tolerated when the exact target remains unique; missing or repeated targets become typed review conflicts. A new suggestion on the same comment replaces the requester's earlier pending one, so never ask them to reject it.",
137
+ "Propose anchored edits against the latest Page text, reviewed in the comment thread that asked for them. Pass the latest attemptId from get-comment-ai-context. Unrelated edits are tolerated when each exact target remains unique; missing, repeated, or overlapping targets become typed review conflicts. New edits on the same comment replace the requester's earlier pending ones, so never ask them to reject those.",
116
138
  schema: payloadSchema,
117
139
  run: async (args, ctx) => {
118
140
  const request = await requireCommentAiRequest("suggest");
@@ -126,45 +148,72 @@ export default defineAction({
126
148
  );
127
149
  const proposed = resolveDocumentTextEdits(
128
150
  verification.source.document.content,
129
- [{ find: payload.find, replace: payload.replace }],
151
+ payload.edits,
130
152
  );
131
153
  if (!proposed.ok) throw targetError(proposed.error.kind);
132
- const operation = markdownSuggestionOperation(
133
- verification.source.document.content,
134
- proposed.content,
135
- );
136
- if (!operation) {
154
+ const operations = markdownSuggestionOperationsForFindReplaceEdits({
155
+ before: verification.source.document.content,
156
+ edits: proposed.ranges,
157
+ });
158
+ if (!operations.length) {
137
159
  throw new CommentAiOperationError(
138
160
  "target_deleted",
139
161
  "The proposal does not change the latest Page text",
140
162
  false,
141
163
  );
142
164
  }
143
- const supersedes = await supersededSuggestionIds(request, ctx);
144
- const suggestion = await createSuggestion.run(
145
- {
146
- ...(supersedes.length ? { supersedes } : {}),
147
- resourceType: "document",
148
- resourceId: request.documentId,
149
- adapterKind: CONTENT_DOCUMENT_SUGGESTION_ADAPTER,
150
- baseRevision: verification.source.document.updatedAt,
151
- summary: payload.summary,
152
- idempotencyKey: `comment-ai:${request.id}:suggestion`,
153
- operations: [operation],
154
- metadata: {
155
- sourceCommentId: request.rootCommentId,
156
- sourceThreadId: request.threadId,
157
- sourceUrl: `/page/${encodeURIComponent(request.documentId)}?comment=${encodeURIComponent(request.threadId)}`,
158
- commentAiRequestId: request.id,
159
- commentAiOperationId: request.id,
160
- commentAiAttemptId: args.attemptId,
161
- agentThreadId: request.agentThreadId,
162
- runId: request.runId,
163
- model: request.model,
164
- },
165
- },
165
+ const supersedes = await supersededSuggestionIds(
166
+ request,
166
167
  ctx,
168
+ args.attemptId,
167
169
  );
170
+ const proposal = {
171
+ ...(supersedes.length ? { supersedes } : {}),
172
+ resourceType: "document",
173
+ resourceId: request.documentId,
174
+ adapterKind: CONTENT_DOCUMENT_SUGGESTION_ADAPTER,
175
+ baseRevision: verification.source.document.updatedAt,
176
+ summary: payload.summary,
177
+ };
178
+ const metadata = {
179
+ sourceCommentId: request.rootCommentId,
180
+ sourceThreadId: request.threadId,
181
+ sourceUrl: `/page/${encodeURIComponent(request.documentId)}?comment=${encodeURIComponent(request.threadId)}`,
182
+ commentAiRequestId: request.id,
183
+ commentAiOperationId: request.id,
184
+ commentAiAttemptId: args.attemptId,
185
+ agentThreadId: request.agentThreadId,
186
+ runId: request.runId,
187
+ model: request.model,
188
+ };
189
+ // A Content suggestion holds exactly one edit, so separate changes become
190
+ // one proposal. The receipt links its first edit. Each attempt creates
191
+ // under its own key, because a retry may propose different edits.
192
+ const suggestion =
193
+ operations.length === 1
194
+ ? await createSuggestion.run(
195
+ {
196
+ ...proposal,
197
+ idempotencyKey: `comment-ai:${args.attemptId}:suggestion`,
198
+ operations,
199
+ metadata,
200
+ },
201
+ ctx,
202
+ )
203
+ : (
204
+ await createSuggestionProposal.run(
205
+ {
206
+ ...proposal,
207
+ idempotencyKey: `comment-ai:${args.attemptId}:proposal`,
208
+ suggestions: operations.map((operation) => ({
209
+ summary: markdownSuggestionOperationSummary(operation),
210
+ operations: [{ ...operation, ordinal: 0 }],
211
+ metadata,
212
+ })),
213
+ },
214
+ ctx,
215
+ )
216
+ ).suggestions[0]!;
168
217
  await updateCommentAiRequest(request, {
169
218
  status: "running",
170
219
  result: { suggestionId: suggestion.id },
@@ -3,6 +3,7 @@ import { buildDeepLink } from "@agent-native/core/server";
3
3
  import { resolveAccess } from "@agent-native/core/sharing";
4
4
  import { z } from "zod";
5
5
 
6
+ import { CONTENT_DATABASE_FILTER_OPERATORS } from "../shared/api.js";
6
7
  import { blocksContentHash } from "../shared/blocks-field-identity.js";
7
8
  import {
8
9
  renderCollectionCsv,
@@ -31,20 +32,7 @@ const collectionSchema = z.object({
31
32
  z.object({
32
33
  key: z.string(),
33
34
  label: z.string(),
34
- operator: z.enum([
35
- "contains",
36
- "equals",
37
- "does_not_equal",
38
- "greater_than",
39
- "less_than",
40
- "before",
41
- "after",
42
- "between",
43
- "is_checked",
44
- "is_unchecked",
45
- "is_empty",
46
- "is_not_empty",
47
- ]),
35
+ operator: z.enum(CONTENT_DATABASE_FILTER_OPERATORS),
48
36
  value: z.string(),
49
37
  filterGroupId: z.string().optional(),
50
38
  parentFilterGroupId: z.string().optional(),
@@ -15,7 +15,10 @@ import { track } from "@agent-native/core/tracking";
15
15
  import { z } from "zod";
16
16
 
17
17
  import { resolveDocumentTextEdits } from "../shared/document-text-edits.js";
18
- import { markdownSuggestionOperationsForFindReplace } from "../shared/suggestion-diff.js";
18
+ import {
19
+ markdownSuggestionOperationsForFindReplace,
20
+ markdownSuggestionOperationSummary,
21
+ } from "../shared/suggestion-diff.js";
19
22
  import { contentSuggestionPath } from "../shared/suggestion-link.js";
20
23
  import { resolveDocumentAccess } from "./_document-access.js";
21
24
  import { documentRevisionToken } from "./_document-edit-mutation.js";
@@ -366,12 +369,7 @@ export default defineAction({
366
369
  proposalId: args.proposalId,
367
370
  idempotencyKey: args.idempotencyKey ?? crypto.randomUUID(),
368
371
  suggestions: operations.map((operation, index) => ({
369
- summary:
370
- operation.before.changedText && operation.after.changedText
371
- ? `Replace "${operation.before.changedText.slice(0, 80)}" with "${operation.after.changedText.slice(0, 80)}"`
372
- : operation.before.changedText
373
- ? `Delete "${operation.before.changedText.slice(0, 80)}"`
374
- : `Insert "${operation.after.changedText.slice(0, 80)}"`,
372
+ summary: markdownSuggestionOperationSummary(operation),
375
373
  operations: [{ ...operation, ordinal: 0 }],
376
374
  metadata:
377
375
  index === 0
@@ -9,6 +9,7 @@ import { z } from "zod";
9
9
 
10
10
  import { getDb, schema } from "../server/db/index.js";
11
11
  import { syncPrivateViewIconReferences } from "../server/lib/private-icon-references.js";
12
+ import { CONTENT_DATABASE_FILTER_OPERATORS } from "../shared/api.js";
12
13
  import { withSavedTableColumnPresentation } from "../shared/database-table-columns.js";
13
14
  import { lockContentDatabaseMutation } from "./_content-database-mutation-lock.js";
14
15
  import {
@@ -39,20 +40,7 @@ const filterSchema = z
39
40
  .object({
40
41
  key: z.string(),
41
42
  label: z.string(),
42
- operator: z.enum([
43
- "contains",
44
- "equals",
45
- "does_not_equal",
46
- "greater_than",
47
- "less_than",
48
- "before",
49
- "after",
50
- "between",
51
- "is_checked",
52
- "is_unchecked",
53
- "is_empty",
54
- "is_not_empty",
55
- ]),
43
+ operator: z.enum(CONTENT_DATABASE_FILTER_OPERATORS),
56
44
  value: z.string(),
57
45
  filterGroupId: z.string().optional(),
58
46
  parentFilterGroupId: z.string().optional(),
@@ -116,6 +116,7 @@ import {
116
116
  import type { DraftSuggestion } from "./suggestions/draft-session";
117
117
  import { SuggestionText } from "./SuggestionText";
118
118
  import {
119
+ proposalGroups,
119
120
  receiptSuggestion,
120
121
  suggestionSourceThreadId,
121
122
  suggestionsByThread,
@@ -741,6 +742,8 @@ interface CommentsSidebarOptions {
741
742
  surface?: CommentSurface;
742
743
  /** Closes the anchored popover; shown as its header close button. */
743
744
  onClose?: () => void;
745
+ /** Extra header actions for each comment thread's card. */
746
+ renderThreadActions?: (thread: CommentThread) => ReactNode;
744
747
  }
745
748
 
746
749
  type CommentsSidebarProps = CommentsSidebarOptions &
@@ -812,6 +815,7 @@ export function CommentsSidebar({
812
815
  presentation = "inline",
813
816
  surface: surfaceProp,
814
817
  onClose,
818
+ renderThreadActions,
815
819
  }: CommentsSidebarProps) {
816
820
  const surface: CommentSurface =
817
821
  surfaceProp ?? (presentation === "history" ? "panel" : "rail");
@@ -1148,23 +1152,52 @@ export function CommentsSidebar({
1148
1152
  const pendingFocus = displayedPendingComment?.focus;
1149
1153
  useEffect(() => {
1150
1154
  if (!pendingFocus || presentation !== "inline") return;
1151
- const timer = setTimeout(() => {
1155
+ // A new comment has no saved caret yet ({}); a draft that lost focus has
1156
+ // none at all (null) and stays put. A new comment takes focus from the
1157
+ // page, which keeps it through the toolbar click or shortcut that opened
1158
+ // the comment, so typing goes into the comment. A restored draft only
1159
+ // takes focus nobody else holds.
1160
+ const isNew =
1161
+ pendingFocus.current !== null && pendingFocus.current.start === undefined;
1162
+ let movedAway = false;
1163
+ const relinquish = (event: FocusEvent) => {
1164
+ if (!pendingCardRef.current?.contains(event.target as Node))
1165
+ movedAway = true;
1166
+ };
1167
+ document.addEventListener("focusin", relinquish);
1168
+ let attempts = 0;
1169
+ let frame = 0;
1170
+ const focusComposer = () => {
1152
1171
  const input = pendingInputRef.current;
1172
+ const card = pendingCardRef.current;
1153
1173
  const saved = pendingFocus.current;
1154
- if (!input || !saved) return;
1155
- const active = document.activeElement;
1156
- if (
1157
- active &&
1158
- active !== document.body &&
1159
- active !== document.documentElement &&
1160
- active.isConnected
1161
- )
1162
- return;
1163
- input.focus();
1164
- if (saved.start !== undefined && saved.end !== undefined)
1165
- input.setSelection(saved.start, saved.end, saved.direction);
1166
- }, 50);
1167
- return () => clearTimeout(timer);
1174
+ if (!saved || movedAway) return;
1175
+ if (input && card) {
1176
+ if (card.contains(document.activeElement)) return;
1177
+ const active = document.activeElement;
1178
+ if (
1179
+ !isNew &&
1180
+ active &&
1181
+ active !== document.body &&
1182
+ active !== document.documentElement &&
1183
+ active.isConnected
1184
+ )
1185
+ return;
1186
+ input.focus();
1187
+ if (saved.start !== undefined && saved.end !== undefined)
1188
+ input.setSelection(saved.start, saved.end, saved.direction);
1189
+ if (card.contains(document.activeElement)) return;
1190
+ }
1191
+ // The composer may still be mounting, or hidden while its popover is
1192
+ // positioned; focus can't land until it's on screen.
1193
+ if (++attempts < 30) frame = requestAnimationFrame(focusComposer);
1194
+ };
1195
+ const timer = setTimeout(focusComposer, 50);
1196
+ return () => {
1197
+ clearTimeout(timer);
1198
+ cancelAnimationFrame(frame);
1199
+ document.removeEventListener("focusin", relinquish);
1200
+ };
1168
1201
  }, [pendingFocus, presentation]);
1169
1202
 
1170
1203
  const handlePendingSubmit = async (
@@ -1622,25 +1655,47 @@ export function CommentsSidebar({
1622
1655
  const freshResolution =
1623
1656
  thread.resolved && commentAi?.freshResolutions.has(thread.threadId);
1624
1657
  const embedded = threadSuggestions.get(thread.threadId) ?? [];
1625
- const receipts = new Map<string, ResourceSuggestion>();
1658
+ // A receipt links the first edit of its proposal and stands for them all.
1659
+ const groups = proposalGroups(embedded);
1660
+ const receipts = new Map<string, ResourceSuggestion[]>();
1626
1661
  for (const comment of thread.comments) {
1627
1662
  const suggestion = receiptSuggestion(comment, embedded);
1628
- if (suggestion) receipts.set(comment.id, suggestion);
1663
+ const group =
1664
+ suggestion && groups.find((members) => members.includes(suggestion));
1665
+ if (group) receipts.set(comment.id, group);
1629
1666
  }
1630
- const receiptless = embedded.filter(
1631
- (suggestion) => ![...receipts.values()].includes(suggestion),
1667
+ const receiptless = groups.filter(
1668
+ (group) => ![...receipts.values()].includes(group),
1632
1669
  );
1633
1670
  const aiWorking = Boolean(
1634
1671
  commentAi && isCommentAiWorkingOn(commentAi, thread.threadId),
1635
1672
  );
1636
- const suggestionProps = (suggestion: ResourceSuggestion) => ({
1637
- suggestion,
1638
- canDecide: canDecideSuggestions,
1639
- busy: suggestionBusy(suggestion),
1640
- conflict: isUnplaceable(suggestion),
1641
- onDecide: (decision: SuggestionDecision) =>
1642
- onDecideSuggestion?.(suggestion, decision),
1643
- });
1673
+ const suggestionProps = (members: ResourceSuggestion[]) => {
1674
+ const proposalId = members.length > 1 ? members[0]?.proposalId : null;
1675
+ return {
1676
+ suggestions: members,
1677
+ canDecide: canDecideSuggestions,
1678
+ busy: suggestionBusy,
1679
+ conflict: isUnplaceable,
1680
+ onDecide: (
1681
+ suggestion: ResourceSuggestion,
1682
+ decision: SuggestionDecision,
1683
+ ) => onDecideSuggestion?.(suggestion, decision),
1684
+ onDecideRemaining:
1685
+ proposalId && onDecideSuggestionProposal
1686
+ ? (decision: SuggestionDecision) =>
1687
+ onDecideSuggestionProposal(
1688
+ proposalId,
1689
+ decision,
1690
+ suggestions.filter(
1691
+ (suggestion) =>
1692
+ suggestion.proposalId === proposalId &&
1693
+ suggestion.status === "pending",
1694
+ ),
1695
+ )
1696
+ : undefined,
1697
+ };
1698
+ };
1644
1699
  const showsRequestSuggestion =
1645
1700
  aiRequest?.status === "suggested" &&
1646
1701
  embedded.some(
@@ -1794,6 +1849,7 @@ export function CommentsSidebar({
1794
1849
  resolved={Boolean(thread.resolved)}
1795
1850
  surface={surface}
1796
1851
  onClose={onClose}
1852
+ threadActions={renderThreadActions?.(thread)}
1797
1853
  currentUserEmail={currentUserEmail}
1798
1854
  quote={thread.quotedText}
1799
1855
  propertyTarget={
@@ -1843,10 +1899,10 @@ export function CommentsSidebar({
1843
1899
  feedback={
1844
1900
  (aiRequest && commentAi) || receiptless.length ? (
1845
1901
  <>
1846
- {receiptless.map((suggestion) => (
1902
+ {receiptless.map((members) => (
1847
1903
  <ThreadSuggestionRow
1848
- key={suggestion.id}
1849
- {...suggestionProps(suggestion)}
1904
+ key={members[0]!.id}
1905
+ {...suggestionProps(members)}
1850
1906
  />
1851
1907
  ))}
1852
1908
  {!aiRequest || !commentAi ? null : continuation ||
@@ -264,7 +264,6 @@ import {
264
264
  } from "./local-source-write-state";
265
265
  import { NotionConflictBanner } from "./NotionConflictBanner";
266
266
  import {
267
- clearPageDraftJournal,
268
267
  clearPageDraftJournalGeneration,
269
268
  listPageDraftJournal,
270
269
  syncPageDraftJournalBeforePersistingRecoveryDraft,
@@ -449,12 +448,14 @@ export interface PageEditorSession {
449
448
 
450
449
  /**
451
450
  * Opens the Page with its Comments showing: one thread when `threadId` is
452
- * set, otherwise the list. A new `key` asks again.
451
+ * set, a new comment on the Property `propertyId` names, otherwise the list.
452
+ * A new `key` asks again.
453
453
  */
454
454
  export interface PageCommentIntent {
455
455
  key: number;
456
456
  documentId: string;
457
457
  threadId?: string | null;
458
+ propertyId?: string;
458
459
  }
459
460
 
460
461
  export interface PageEditorSurfaceProps extends DocumentEditorProps {
@@ -3346,24 +3347,11 @@ function PageEditorSessionBody({
3346
3347
  [isLinkedLocalSourceDocument, isLocalFileDocument, journalScope, t],
3347
3348
  );
3348
3349
  const clearConfirmedDraftJournal = useCallback(
3349
- (
3350
- saved: Document & {
3351
- bodyIntentOutcome?: { status: "applied" | "displaced-preserved" };
3352
- },
3353
- editGeneration: number,
3354
- ) => {
3350
+ (editGeneration: number) => {
3355
3351
  const scope = journalScope();
3356
3352
  if (!scope) return;
3357
3353
  try {
3358
- if (saved.bodyIntentOutcome) {
3359
- clearPageDraftJournalGeneration(scope, editGeneration);
3360
- } else {
3361
- clearPageDraftJournal(scope, {
3362
- editGeneration,
3363
- title: saved.title,
3364
- content: saved.content,
3365
- });
3366
- }
3354
+ clearPageDraftJournalGeneration(scope, editGeneration);
3367
3355
  } catch {
3368
3356
  if (!recoveryWriteErrorShownRef.current) {
3369
3357
  toast.error(t("editor.pageSaveBeforeNavigationFailed"));
@@ -4354,17 +4342,13 @@ function PageEditorSessionBody({
4354
4342
  : null,
4355
4343
  });
4356
4344
  }
4357
- if (
4358
- contentEditVersionRef.current === contentEditVersion &&
4359
- contentObservationEpochRef.current === contentObservationEpoch
4360
- ) {
4345
+ // Text another writer added while this save was in flight is theirs to
4346
+ // save, so it holds back neither the confirmation nor the journal clear.
4347
+ // An entry kept for it outlives this tab's edits, and the next tab to
4348
+ // open the page replays it as a save of its own.
4349
+ if (contentEditVersionRef.current === contentEditVersion) {
4361
4350
  confirmedContentEditVersionRef.current = contentEditVersion;
4362
- if (
4363
- saved.title === localTitleRef.current &&
4364
- saved.content === localContentRef.current
4365
- ) {
4366
- clearConfirmedDraftJournal(saved, editorEditGeneration);
4367
- }
4351
+ clearConfirmedDraftJournal(editorEditGeneration);
4368
4352
  }
4369
4353
 
4370
4354
  if (autoSync) {
@@ -7408,6 +7392,7 @@ function PageEditorSessionBody({
7408
7392
  )
7409
7393
  ? pagePropertiesQuery.data.properties
7410
7394
  : undefined;
7395
+ const pagePropertiesFailed = !pageProperties && pagePropertiesQuery.isError;
7411
7396
  const documentLayoutRef = useRef<HTMLDivElement>(null);
7412
7397
  const commentLaneRef = useRef<HTMLElement>(null);
7413
7398
  const anchoredCommentRef = useRef<HTMLElement>(null);
@@ -7792,6 +7777,7 @@ function PageEditorSessionBody({
7792
7777
  ],
7793
7778
  );
7794
7779
  const handledCommentDeepLinkRef = useRef<string | null>(null);
7780
+ const handledPropertyCommentLinkRef = useRef<string | null>(null);
7795
7781
 
7796
7782
  const startPropertyComment = useCallback(
7797
7783
  (property: DocumentProperty) => {
@@ -7928,6 +7914,40 @@ function PageEditorSessionBody({
7928
7914
  activateCommentThread(threadId, true);
7929
7915
  }, [activateCommentThread, documentId, location.search, threads]);
7930
7916
 
7917
+ // A Collection that opens rows as full pages links here to start a comment
7918
+ // on one Property, as the row's peek does on a touch screen.
7919
+ useEffect(() => {
7920
+ const propertyId = new URLSearchParams(location.search).get(
7921
+ "commentProperty",
7922
+ );
7923
+ if (!propertyId) {
7924
+ handledPropertyCommentLinkRef.current = null;
7925
+ return;
7926
+ }
7927
+ const linkKey = `${documentId}:${propertyId}`;
7928
+ // Wait for the Properties; a failed read opens the list instead.
7929
+ if (
7930
+ handledPropertyCommentLinkRef.current === linkKey ||
7931
+ (!pageProperties && !pagePropertiesFailed)
7932
+ ) {
7933
+ return;
7934
+ }
7935
+ handledPropertyCommentLinkRef.current = linkKey;
7936
+ const property = pageProperties?.find(
7937
+ (candidate) => candidate.definition.id === propertyId,
7938
+ );
7939
+ if (property && canComment) startPropertyComment(property);
7940
+ else handleUtilityPanelChange("comments");
7941
+ }, [
7942
+ canComment,
7943
+ documentId,
7944
+ handleUtilityPanelChange,
7945
+ location.search,
7946
+ pageProperties,
7947
+ pagePropertiesFailed,
7948
+ startPropertyComment,
7949
+ ]);
7950
+
7931
7951
  const handledCommentIntentRef = useRef<number | null>(null);
7932
7952
  useEffect(() => {
7933
7953
  if (
@@ -7935,7 +7955,7 @@ function PageEditorSessionBody({
7935
7955
  handledCommentIntentRef.current === commentIntent.key
7936
7956
  )
7937
7957
  return;
7938
- const threadId = commentIntent.threadId;
7958
+ const { threadId, propertyId } = commentIntent;
7939
7959
  if (threadId) {
7940
7960
  // Wait for the thread; a thread that is gone opens the list instead.
7941
7961
  if (!threads) return;
@@ -7944,14 +7964,30 @@ function PageEditorSessionBody({
7944
7964
  activateCommentThread(threadId);
7945
7965
  return;
7946
7966
  }
7967
+ } else if (propertyId) {
7968
+ // Wait for the Properties; one that is gone, or a failed read, opens
7969
+ // the list instead.
7970
+ if (!pageProperties && !pagePropertiesFailed) return;
7971
+ handledCommentIntentRef.current = commentIntent.key;
7972
+ const property = pageProperties?.find(
7973
+ (candidate) => candidate.definition.id === propertyId,
7974
+ );
7975
+ if (property && canComment) {
7976
+ startPropertyComment(property);
7977
+ return;
7978
+ }
7947
7979
  }
7948
7980
  handledCommentIntentRef.current = commentIntent.key;
7949
7981
  handleUtilityPanelChange("comments");
7950
7982
  }, [
7951
7983
  activateCommentThread,
7984
+ canComment,
7952
7985
  commentIntent,
7953
7986
  documentId,
7954
7987
  handleUtilityPanelChange,
7988
+ pageProperties,
7989
+ pagePropertiesFailed,
7990
+ startPropertyComment,
7955
7991
  threads,
7956
7992
  ]);
7957
7993