@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,12 +1,111 @@
1
1
  import type {
2
2
  ContentDatabaseFilter,
3
3
  ContentDatabaseFilterMode,
4
+ ContentDatabaseFilterOperator,
4
5
  ContentDatabaseItem,
5
6
  ContentDatabaseSort,
6
7
  ContentDatabaseTableQuery,
7
8
  DocumentProperty,
8
9
  } from "./api.js";
9
- import { documentPropertyDatePart, formulaValueText } from "./properties.js";
10
+ import {
11
+ documentPropertyDatePart,
12
+ formulaValueText,
13
+ type DocumentPropertyType,
14
+ } from "./properties.js";
15
+
16
+ const OPTION_FILTER_OPERATORS = [
17
+ "contains",
18
+ "does_not_equal",
19
+ "is_empty",
20
+ "is_not_empty",
21
+ ] as const satisfies readonly ContentDatabaseFilterOperator[];
22
+
23
+ const DATE_FILTER_OPERATORS = [
24
+ "equals",
25
+ "does_not_equal",
26
+ "before",
27
+ "after",
28
+ "between",
29
+ "is_empty",
30
+ "is_not_empty",
31
+ ] as const satisfies readonly ContentDatabaseFilterOperator[];
32
+
33
+ const FILTER_OPERATORS_BY_TYPE: Partial<
34
+ Record<DocumentPropertyType, readonly ContentDatabaseFilterOperator[]>
35
+ > = {
36
+ checkbox: ["is_checked", "is_unchecked"],
37
+ select: OPTION_FILTER_OPERATORS,
38
+ status: OPTION_FILTER_OPERATORS,
39
+ multi_select: OPTION_FILTER_OPERATORS,
40
+ person: OPTION_FILTER_OPERATORS,
41
+ number: [
42
+ "equals",
43
+ "does_not_equal",
44
+ "greater_than",
45
+ "less_than",
46
+ "greater_than_or_equal",
47
+ "less_than_or_equal",
48
+ "is_empty",
49
+ "is_not_empty",
50
+ ],
51
+ date: DATE_FILTER_OPERATORS,
52
+ created_time: DATE_FILTER_OPERATORS,
53
+ last_edited_time: DATE_FILTER_OPERATORS,
54
+ };
55
+
56
+ const DEFAULT_FILTER_OPERATORS = [
57
+ "contains",
58
+ "equals",
59
+ "does_not_equal",
60
+ "is_empty",
61
+ "is_not_empty",
62
+ ] as const satisfies readonly ContentDatabaseFilterOperator[];
63
+
64
+ const VALUELESS_FILTER_OPERATORS: ReadonlySet<ContentDatabaseFilterOperator> =
65
+ new Set(["is_empty", "is_not_empty", "is_checked", "is_unchecked"]);
66
+
67
+ const NUMBER_FILTER_COMPARISONS: Partial<
68
+ Record<
69
+ ContentDatabaseFilterOperator,
70
+ (current: number, target: number) => boolean
71
+ >
72
+ > = {
73
+ greater_than: (current, target) => current > target,
74
+ less_than: (current, target) => current < target,
75
+ greater_than_or_equal: (current, target) => current >= target,
76
+ less_than_or_equal: (current, target) => current <= target,
77
+ };
78
+
79
+ // Listed in menu order; the first entry is the operator a new filter starts with.
80
+ export function contentDatabaseFilterOperatorsForType(
81
+ type: DocumentPropertyType,
82
+ ): readonly ContentDatabaseFilterOperator[] {
83
+ return FILTER_OPERATORS_BY_TYPE[type] ?? DEFAULT_FILTER_OPERATORS;
84
+ }
85
+
86
+ export function contentDatabaseFilterOperatorNeedsValue(
87
+ operator: ContentDatabaseFilterOperator,
88
+ ) {
89
+ return !VALUELESS_FILTER_OPERATORS.has(operator);
90
+ }
91
+
92
+ // A "between" value is a JSON [start, end] pair. Read it by position: a
93
+ // one-day range repeats its date, so deduplicating would lose the end.
94
+ export function contentDatabaseFilterDateRangeValues(
95
+ value: string,
96
+ ): [string, string] {
97
+ const trimmed = value.trim();
98
+ if (!trimmed) return ["", ""];
99
+ try {
100
+ const parsed = JSON.parse(trimmed) as unknown;
101
+ if (!Array.isArray(parsed)) return [trimmed, ""];
102
+ const start = typeof parsed[0] === "string" ? parsed[0].trim() : "";
103
+ const end = typeof parsed[1] === "string" ? parsed[1].trim() : "";
104
+ return [start, end];
105
+ } catch {
106
+ return [trimmed, ""];
107
+ }
108
+ }
10
109
 
11
110
  export function applyContentDatabaseTableQuery(
12
111
  items: ContentDatabaseItem[],
@@ -94,11 +193,7 @@ function combineMatches(
94
193
  }
95
194
 
96
195
  function isActiveFilter(filter: ContentDatabaseFilter) {
97
- if (
98
- !["is_empty", "is_not_empty", "is_checked", "is_unchecked"].includes(
99
- filter.operator,
100
- )
101
- ) {
196
+ if (contentDatabaseFilterOperatorNeedsValue(filter.operator)) {
102
197
  return selectedFilterValues(filter.value).length > 0;
103
198
  }
104
199
  return true;
@@ -116,13 +211,12 @@ function itemMatchesFilter(
116
211
  if (filter.operator === "is_checked") return property?.value === true;
117
212
  if (filter.operator === "is_unchecked") return property?.value !== true;
118
213
 
119
- if (filter.operator === "greater_than" || filter.operator === "less_than") {
214
+ const compareNumbers = NUMBER_FILTER_COMPARISONS[filter.operator];
215
+ if (compareNumbers) {
120
216
  const current = propertyNumberValue(property);
121
217
  const target = Number(filter.value.trim());
122
218
  if (!Number.isFinite(current) || !Number.isFinite(target)) return false;
123
- return filter.operator === "greater_than"
124
- ? current > target
125
- : current < target;
219
+ return compareNumbers(current, target);
126
220
  }
127
221
 
128
222
  if (
@@ -198,9 +292,9 @@ function selectedFilterValues(value: string) {
198
292
  }
199
293
 
200
294
  function filterDateRange(value: string): [number, number] | null {
201
- const values = selectedFilterValues(value);
202
- const start = new Date(values[0] ?? "").getTime();
203
- const end = new Date(values[1] ?? "").getTime();
295
+ const [startValue, endValue] = contentDatabaseFilterDateRangeValues(value);
296
+ const start = new Date(startValue).getTime();
297
+ const end = new Date(endValue).getTime();
204
298
  if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
205
299
  return start <= end ? [start, end] : [end, start];
206
300
  }
@@ -951,6 +951,41 @@ export function markdownSuggestionOperationsForFindReplace(input: {
951
951
  return operations;
952
952
  }
953
953
 
954
+ /**
955
+ * Splits several non-overlapping find/replace edits against one source the
956
+ * way one edit is split. Each operation's anchor context stops at its
957
+ * neighbors, so accepting one never rewrites the text another is found by.
958
+ */
959
+ export function markdownSuggestionOperationsForFindReplaceEdits(input: {
960
+ before: string;
961
+ edits: ReadonlyArray<{ find: string; replace: string; start: number }>;
962
+ }): MarkdownSuggestionOperation[] {
963
+ return isolateSiblingAnchorContexts(
964
+ input.edits
965
+ .flatMap((edit) =>
966
+ markdownSuggestionOperationsForFindReplace({
967
+ before: input.before,
968
+ ...edit,
969
+ }),
970
+ )
971
+ .sort((left, right) => left.anchor.from - right.anchor.from)
972
+ .map((operation, ordinal) => ({ ...operation, ordinal })),
973
+ );
974
+ }
975
+
976
+ /** A one-line review label for a single operation's change. */
977
+ export function markdownSuggestionOperationSummary(
978
+ operation: MarkdownSuggestionOperation,
979
+ ): string {
980
+ const removed = operation.before.changedText.slice(0, 80);
981
+ const inserted = operation.after.changedText.slice(0, 80);
982
+ return removed && inserted
983
+ ? `Replace "${removed}" with "${inserted}"`
984
+ : removed
985
+ ? `Delete "${removed}"`
986
+ : `Insert "${inserted}"`;
987
+ }
988
+
954
989
  export function markdownSuggestionOperationsForReplacements(input: {
955
990
  before: string;
956
991
  after: string;
@@ -95,9 +95,9 @@ MCP clients see no `outputSchema` by default. Set `mcpOutputSchema: true` (requi
95
95
 
96
96
  `authorize` decides whether the caller may run the action at all. It wraps
97
97
  `run`, so it holds at **all six dispatch sites** — agent tool, HTTP, frontend
98
- hook, MCP, A2A, CLI — unlike `needsApproval`, which is honoured only in the
99
- agent loop. Use `authorize` for "not everyone may do this"; use `needsApproval`
100
- to ask a human to bless one call a permitted caller is already allowed to make.
98
+ hook, MCP, A2A, CLI. Use `authorize` for "not everyone may do this"; use
99
+ `needsApproval` to ask a human to bless one call a permitted caller is already
100
+ allowed to make.
101
101
 
102
102
  ```ts
103
103
  import { coachAccess } from "../lib/access.js"; // defineAppRoles(...)
@@ -140,4 +140,6 @@ export default defineAction({
140
140
 
141
141
  When the gate is truthy and the call isn't yet approved, the loop emits an `approval_required` event and **stops the turn — `run()` never executes**. A predicate gates conditionally (e.g. only external recipients) and **fails closed**: a throw is treated as "approval required". It receives its own copy of the prepared snapshot of the validated arguments, defaults applied, and the approval binds to the snapshot's hash, so an approved call runs only with the arguments that were approved (a Date binds by its time). The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's `approvalKey`, and only then does the action run.
142
142
 
143
+ Approval is given in the app, so HTTP never grants it. The HTTP action route and the generated Workers entry run a gated call only for the proven app UI (a frontend call from the signed-in user, same origin, holding the short-lived UI capability; `isProvenUiRequest`, the proof `uiOnly` uses) or the app's MCP directory widget. Every other HTTP caller (bearer or API key, MCP OAuth token, org service token, A2A, extension iframe, mobile app) gets `409 approval_required`, and a service identity can never approve. The shared client presents the capability with every write from a signed-in session (minted once per user, reused for nine minutes, never for reads or public pages), so a gated button works on its first request. A call without proof gets `403 ui_capability_required`; the client then re-mints and retries once, which is only the fallback for an expired or rejected capability. Every surface evaluates the gate through `preparedCallNeedsApproval`, so a throwing predicate asks there too. A caller that must run unattended needs a different action, never a bypass of the gate.
144
+
143
145
  **Keep approvals rare** — the default is off and almost every action should leave it off. The canonical example is Mail's `send-email` (`needsApproval: true`). See the `security` skill and the Human Approval doc.
@@ -95,9 +95,9 @@ MCP clients see no `outputSchema` by default. Set `mcpOutputSchema: true` (requi
95
95
 
96
96
  `authorize` decides whether the caller may run the action at all. It wraps
97
97
  `run`, so it holds at **all six dispatch sites** — agent tool, HTTP, frontend
98
- hook, MCP, A2A, CLI — unlike `needsApproval`, which is honoured only in the
99
- agent loop. Use `authorize` for "not everyone may do this"; use `needsApproval`
100
- to ask a human to bless one call a permitted caller is already allowed to make.
98
+ hook, MCP, A2A, CLI. Use `authorize` for "not everyone may do this"; use
99
+ `needsApproval` to ask a human to bless one call a permitted caller is already
100
+ allowed to make.
101
101
 
102
102
  ```ts
103
103
  import { coachAccess } from "../lib/access.js"; // defineAppRoles(...)
@@ -140,4 +140,6 @@ export default defineAction({
140
140
 
141
141
  When the gate is truthy and the call isn't yet approved, the loop emits an `approval_required` event and **stops the turn — `run()` never executes**. A predicate gates conditionally (e.g. only external recipients) and **fails closed**: a throw is treated as "approval required". It receives its own copy of the prepared snapshot of the validated arguments, defaults applied, and the approval binds to the snapshot's hash, so an approved call runs only with the arguments that were approved (a Date binds by its time). The human approves via the chat UI's Approve affordance, which re-issues the turn with the call's `approvalKey`, and only then does the action run.
142
142
 
143
+ Approval is given in the app, so HTTP never grants it. The HTTP action route and the generated Workers entry run a gated call only for the proven app UI (a frontend call from the signed-in user, same origin, holding the short-lived UI capability; `isProvenUiRequest`, the proof `uiOnly` uses) or the app's MCP directory widget. Every other HTTP caller (bearer or API key, MCP OAuth token, org service token, A2A, extension iframe, mobile app) gets `409 approval_required`, and a service identity can never approve. The shared client presents the capability with every write from a signed-in session (minted once per user, reused for nine minutes, never for reads or public pages), so a gated button works on its first request. A call without proof gets `403 ui_capability_required`; the client then re-mints and retries once, which is only the fallback for an expired or rejected capability. Every surface evaluates the gate through `preparedCallNeedsApproval`, so a throwing predicate asks there too. A caller that must run unattended needs a different action, never a bypass of the gate.
144
+
143
145
  **Keep approvals rare** — the default is off and almost every action should leave it off. The canonical example is Mail's `send-email` (`needsApproval: true`). See the `security` skill and the Human Approval doc.
@@ -11,6 +11,7 @@ from the command menu (Cmd+K → "What's new") or from Settings.
11
11
 
12
12
  ### Fixed
13
13
 
14
+ - Comments return to the prior Interact screen and device, stay open when that return is blocked, and fall back to Design if that screen was removed. Layers search toggles closed.
14
15
  - Public Design viewers no longer see repeated PNG export errors
15
16
  - Design resizes more high-resolution reference images before adding them to a prompt.
16
17
  - Review menus no longer trigger canvas gestures, and rewrite previews and proposal controls follow the uniquely resolved target.
@@ -29,6 +30,7 @@ from the command menu (Cmd+K → "What's new") or from Settings.
29
30
 
30
31
  ### Fixed
31
32
 
33
+ - Reply and Edit on a comment, in the sidebar or on a canvas pin, put the cursor in the box, after the text when editing.
32
34
  - Comment mode no longer selects or hovers the design underneath, and pan and zoom work while commenting.
33
35
  - Text color adapts when layers move between screens with different backgrounds
34
36
  - Text keeps readable contrast when moved between screens with different backgrounds.
@@ -432,10 +432,7 @@ export default defineAction({
432
432
  "Legacy frontend checkpoint to validate before a delete; the live pre-delete version is always captured in the delete transaction.",
433
433
  ),
434
434
  }),
435
- // MCP evaluates approval against raw arguments before the action schema
436
- // normalizes boolean strings, so use the same truthiness boundary here.
437
- needsApproval: (args) =>
438
- Boolean((args as unknown as Record<string, unknown>).allowLockedLayers),
435
+ needsApproval: ({ allowLockedLayers }) => allowLockedLayers,
439
436
  allowPersistentApproval: false,
440
437
  run: async (
441
438
  { id, fileIds, allowLockedLayers, historyCheckpointId },
@@ -1188,6 +1188,15 @@ function LayersPanelImpl(
1188
1188
  window.requestAnimationFrame(() => searchInputRef.current?.focus());
1189
1189
  }, []);
1190
1190
 
1191
+ const toggleSearch = useCallback(() => {
1192
+ if (searchOpen || searchQuery.trim()) {
1193
+ setSearchOpen(false);
1194
+ if (searchQuery) onSearchQueryChange("");
1195
+ return;
1196
+ }
1197
+ focusSearch();
1198
+ }, [focusSearch, onSearchQueryChange, searchOpen, searchQuery]);
1199
+
1191
1200
  const beginRename = useCallback(
1192
1201
  (layerId: string): boolean => {
1193
1202
  if (!onRename) return false;
@@ -1570,7 +1579,7 @@ function LayersPanelImpl(
1570
1579
  <IconTooltipButton
1571
1580
  label={labels.searchPlaceholder}
1572
1581
  dataAction="search"
1573
- onClick={focusSearch}
1582
+ onClick={toggleSearch}
1574
1583
  >
1575
1584
  <IconSearch
1576
1585
  className="!size-[var(--design-icon-size)]"
@@ -1108,6 +1108,8 @@ export function ReviewCanvasPins({
1108
1108
  const setUnread = useSetReviewThreadUnread();
1109
1109
  const updateComment = useUpdateReviewComment();
1110
1110
  const [canvas, setCanvas] = useState<HTMLElement | null>(null);
1111
+ const onCloseRef = useRef(onClose);
1112
+ onCloseRef.current = onClose;
1111
1113
  const [layoutTick, setLayoutTick] = useState(0);
1112
1114
  const [draftPin, setDraftPin] = useState<ReviewDraftPin | null>(null);
1113
1115
  const [draftAttachments, setDraftAttachments] = useState<
@@ -1964,8 +1966,8 @@ export function ReviewCanvasPins({
1964
1966
  setReplyAttachments([]);
1965
1967
  setReplyMentions([]);
1966
1968
  setExpandedClusterKey(null);
1967
- if (active) onClose();
1968
- }, [active, cancelDraft, hidden, onClose]);
1969
+ if (active) onCloseRef.current();
1970
+ }, [active, cancelDraft, hidden]);
1969
1971
 
1970
1972
  const dropCanvasPin = useCallback(
1971
1973
  (canvasPoint: ReviewCanvasPoint) => {
@@ -3977,6 +3979,7 @@ function ReviewThreadPopover({
3977
3979
  onSendToAgent?: () => void;
3978
3980
  }) {
3979
3981
  const t = useT();
3982
+ const editChosenFromMenuRef = useRef(false);
3980
3983
  const rootAuthor = reviewAuthorLabel(thread.root, t("review.reviewer"));
3981
3984
  const avatarUrl = useAvatarUrl(thread.root.authorEmail);
3982
3985
  const [replyUploading, setReplyUploading] = useState(false);
@@ -4015,6 +4018,7 @@ function ReviewThreadPopover({
4015
4018
  mentionLabel={t("review.mention")}
4016
4019
  noMentionsLabel={t("review.noMentions")}
4017
4020
  textareaProps={{ "aria-label": t("review.editComment") }}
4021
+ autoFocus
4018
4022
  disabled={updating}
4019
4023
  onChange={onEditDraftChange}
4020
4024
  onSubmit={() => onEditSubmit()}
@@ -4058,9 +4062,24 @@ function ReviewThreadPopover({
4058
4062
  <IconDots className="size-3.5" />
4059
4063
  </Button>
4060
4064
  </DropdownMenuTrigger>
4061
- <DropdownMenuContent data-review-popover align="end" className="w-44">
4065
+ <DropdownMenuContent
4066
+ data-review-popover
4067
+ align="end"
4068
+ className="w-44"
4069
+ onCloseAutoFocus={(event) => {
4070
+ // Edit puts focus in its composer; leave it there. Other
4071
+ // actions return to the button.
4072
+ if (editChosenFromMenuRef.current) event.preventDefault();
4073
+ editChosenFromMenuRef.current = false;
4074
+ }}
4075
+ >
4062
4076
  {canEdit ? (
4063
- <DropdownMenuItem onSelect={onEdit}>
4077
+ <DropdownMenuItem
4078
+ onSelect={() => {
4079
+ editChosenFromMenuRef.current = true;
4080
+ onEdit();
4081
+ }}
4082
+ >
4064
4083
  {t("review.editComment")}
4065
4084
  </DropdownMenuItem>
4066
4085
  ) : null}
@@ -4144,6 +4163,7 @@ function ReviewThreadPopover({
4144
4163
  mentionLabel={t("review.mention")}
4145
4164
  noMentionsLabel={t("review.noMentions")}
4146
4165
  textareaProps={{ "data-review-reply-input": true }}
4166
+ autoFocus
4147
4167
  disabled={replying || resolving || replyUploading}
4148
4168
  onChange={onReplyDraftChange}
4149
4169
  onSubmit={() => onReply()}
@@ -97,7 +97,7 @@ export function runModeChange(
97
97
  /** A screen change made from inside Interact keeps the chosen device. */
98
98
  keepInteractDevice?: boolean;
99
99
  },
100
- ) {
100
+ ): boolean {
101
101
  const next = resolveModeForAnnotateLab(requested, annotateLab);
102
102
  const interactEntry =
103
103
  next === "interact"
@@ -120,7 +120,7 @@ export function runModeChange(
120
120
  : options?.targetFileId
121
121
  ? files.find((file) => file.id === options.targetFileId)
122
122
  : activeFile;
123
- if (!canEditDesign && next === "annotate") return;
123
+ if (!canEditDesign && next === "annotate") return false;
124
124
  if (interactEntry && !nextActiveFile) {
125
125
  toast.error(
126
126
  t(
@@ -129,9 +129,9 @@ export function runModeChange(
129
129
  : "designEditor.responsiveInteract.unknownScreen",
130
130
  ),
131
131
  );
132
- return;
132
+ return false;
133
133
  }
134
- if (next === "annotate" && !nextActiveFile) return;
134
+ if (next === "annotate" && !nextActiveFile) return false;
135
135
  if (
136
136
  next === "interact" &&
137
137
  hasPendingVisualEdits &&
@@ -140,7 +140,7 @@ export function runModeChange(
140
140
  ) {
141
141
  onPendingVisualEditsBlocked();
142
142
  toast.error(t("designEditor.pendingVisualStyles.interactBlocked"));
143
- return;
143
+ return false;
144
144
  }
145
145
  if (options?.discardPendingLiveEdits) {
146
146
  requestPendingVisualStyleRevert(pendingVisualStyleEdits);
@@ -155,13 +155,17 @@ export function runModeChange(
155
155
  if (routing === "enter-single-interact") {
156
156
  rememberOverviewScreenSelection(nextActiveFile!.id);
157
157
  setOverviewInteractScreenId(nextActiveFile!.id);
158
- enterSingleScreen(nextActiveFile?.id);
159
- return;
158
+ if (options?.keepInteractDevice) {
159
+ enterSingleScreen(nextActiveFile?.id, { keepInteractDevice: true });
160
+ } else {
161
+ enterSingleScreen(nextActiveFile?.id);
162
+ }
163
+ return true;
160
164
  }
161
165
  if (routing === "enter-overview") {
162
166
  if (options?.targetFileId) setActiveFileId(options.targetFileId);
163
167
  enterOverviewFromZoom(next);
164
- return;
168
+ return true;
165
169
  }
166
170
  if (next === "interact" && overviewInteractScreenId) {
167
171
  setOverviewInteractScreenId(nextActiveFile!.id);
@@ -175,7 +179,7 @@ export function runModeChange(
175
179
  } else {
176
180
  enterSingleScreen(nextActiveFile!.id);
177
181
  }
178
- return;
182
+ return true;
179
183
  }
180
184
  }
181
185
  if (options?.targetFileId) setActiveFileId(options.targetFileId);
@@ -202,4 +206,5 @@ export function runModeChange(
202
206
  setDrawMode(false);
203
207
  setPinMode(false);
204
208
  }
209
+ return true;
205
210
  }
@@ -48,6 +48,8 @@ import {
48
48
  import { computeInteractZoomToFit } from "../responsive-interact";
49
49
  import { hasSelectableCodeLayerParent } from "../selection-state";
50
50
  import {
51
+ commentModeReturnOptions,
52
+ resolveCommentModeExit,
51
53
  resolveDrawStateWithoutLab,
52
54
  resolveModeForAnnotateLab,
53
55
  shouldAutoEnableDrawOverlay,
@@ -466,6 +468,16 @@ export function useEditorModes({
466
468
 
467
469
  const lastSettledSingleZoomRef = useRef<number | null>(null);
468
470
  const suppressOverviewPopForExplicitZoomRef = useRef(false);
471
+ const commentReturnOptionsRef =
472
+ useRef<ReturnType<typeof commentModeReturnOptions>>(null);
473
+ useEffect(() => {
474
+ if (mode === "annotate") return;
475
+ commentReturnOptionsRef.current = commentModeReturnOptions(
476
+ mode,
477
+ activeFile?.id,
478
+ );
479
+ }, [activeFile?.id, mode]);
480
+
469
481
  useEffect(() => {
470
482
  if (!activeFile || viewMode !== "single" || mode !== "edit") {
471
483
  lastSettledSingleZoomRef.current = null;
@@ -781,12 +793,21 @@ export function useEditorModes({
781
793
  ]);
782
794
 
783
795
  const handleExitReviewCommentMode = useCallback(() => {
796
+ const exit = resolveCommentModeExit(
797
+ commentReturnOptionsRef.current,
798
+ new Set(overviewScreens.map((screen) => screen.id)),
799
+ new Set(files.map((file) => file.id)),
800
+ );
801
+ if (exit.mode === "interact" && !handleModeChange("interact", exit.options))
802
+ return;
803
+
804
+ commentReturnOptionsRef.current = null;
784
805
  setPinMode(false);
785
806
  setOverviewCommentPinRequest(null);
786
807
  setDrawMode(false);
787
808
  setActiveTool("move");
788
- setMode("edit");
789
- }, []);
809
+ if (exit.mode === "edit") setMode("edit");
810
+ }, [files, handleModeChange, overviewScreens]);
790
811
 
791
812
  const handleOverviewCommentPin = useCallback(
792
813
  (canvasPoint: { x: number; y: number }) => {
@@ -82,6 +82,35 @@ export function normalizeDesignMode(value: unknown): EditorMode | null {
82
82
  : null;
83
83
  }
84
84
 
85
+ export function commentModeReturnOptions(
86
+ mode: EditorMode,
87
+ activeFileId: string | null | undefined,
88
+ ): CommentModeReturnOptions | null {
89
+ return mode === "interact" && activeFileId
90
+ ? { targetFileId: activeFileId, keepInteractDevice: true }
91
+ : null;
92
+ }
93
+
94
+ export interface CommentModeReturnOptions {
95
+ targetFileId: string;
96
+ keepInteractDevice: true;
97
+ }
98
+
99
+ export function resolveCommentModeExit(
100
+ returnOptions: CommentModeReturnOptions | null,
101
+ screenIds: ReadonlySet<string>,
102
+ fileIds: ReadonlySet<string>,
103
+ ): { mode: "interact"; options: CommentModeReturnOptions } | { mode: "edit" } {
104
+ if (
105
+ returnOptions &&
106
+ screenIds.has(returnOptions.targetFileId) &&
107
+ fileIds.has(returnOptions.targetFileId)
108
+ ) {
109
+ return { mode: "interact", options: returnOptions };
110
+ }
111
+ return { mode: "edit" };
112
+ }
113
+
85
114
  /**
86
115
  * `loading` is the stretch before the Labs answer arrives. It reads as off, but
87
116
  * a request that needs the lab waits for the answer instead of being refused.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-19
4
+ ---
5
+
6
+ Reply and Edit on a comment, in the sidebar or on a canvas pin, put the cursor in the box, after the text when editing.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-20
4
+ ---
5
+
6
+ Comments return to the prior Interact screen and device, stay open when that return is blocked, and fall back to Design if that screen was removed. Layers search toggles closed.