@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +41 -2
- package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +4 -2
- package/corpus/templates/analytics/CHANGELOG.md +18 -0
- package/corpus/templates/analytics/actions/build-data-index.ts +11 -2
- package/corpus/templates/analytics/actions/configure-content-collection.ts +29 -4
- package/corpus/templates/analytics/actions/configure-first-party-events-model.ts +9 -7
- package/corpus/templates/analytics/actions/create-analytics-public-key.ts +4 -0
- package/corpus/templates/analytics/actions/estimate-content-signups-cost.ts +72 -0
- package/corpus/templates/analytics/actions/estimate-query-cost.ts +16 -134
- package/corpus/templates/analytics/actions/list-sigma-workbooks.ts +123 -42
- package/corpus/templates/analytics/actions/migrate-sigma-workbook.ts +20 -17
- package/corpus/templates/analytics/actions/revoke-analytics-public-key.ts +4 -0
- package/corpus/templates/analytics/app/components/dashboard/SigmaMigrationDialog.tsx +246 -680
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/analytics/app/i18n/en-US.ts +54 -55
- package/corpus/templates/analytics/app/pages/DashboardOverview.tsx +0 -1
- package/corpus/templates/analytics/app/pages/DataSources.tsx +56 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +4 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +26 -8
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/interpolate.ts +3 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/panel-cost-notice.ts +172 -0
- package/corpus/templates/analytics/app/pages/data-sources/SearchConsoleDetail.tsx +441 -0
- package/corpus/templates/analytics/changelog/2026-10-15-a-content-dashboard-lists-every-blog-article-with-its-author.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-15-data-sources-has-a-google-search-console-card-that-shows-the.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-16-the-content-dashboard-can-show-signups-per-blog-article.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-19-creating-or-revoking-a-public-analytics-key-now-asks-you-to-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-19-search-sigma-workbooks-by-name-and-open-ones-a-teammate-alre.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-blog-signup-attribution-excludes-signups-before-the-first-vi.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-collapsed-analytics-navigation-starts-at-the-top.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-content-keeps-successful-coverage-dates-and-missing-day-gaps.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-private-github-repositories-can-be-indexed-with-your-connect.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-preserves-backfill-and-paused-collection.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-refreshes-historical-coverage-when-properties.md +6 -0
- package/corpus/templates/analytics/changelog/2026-10-20-search-console-setup-keeps-saved-properties-readable-and-sup.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/content-view.json +182 -0
- package/corpus/templates/analytics/server/db/schema.ts +1 -0
- package/corpus/templates/analytics/server/lib/bi-migration/external-dashboards-store.ts +56 -18
- package/corpus/templates/analytics/server/lib/bi-migration/unfinished-import.ts +64 -0
- package/corpus/templates/analytics/server/lib/builtin-dashboard-pass.ts +17 -2
- package/corpus/templates/analytics/server/lib/content-metrics/collect.ts +2 -0
- package/corpus/templates/analytics/server/lib/content-metrics/config.ts +92 -3
- package/corpus/templates/analytics/server/lib/content-metrics/legacy-content-view.json +172 -0
- package/corpus/templates/analytics/server/lib/content-metrics/migration.ts +2 -0
- package/corpus/templates/analytics/server/lib/content-metrics/steps/daily-metrics.ts +8 -3
- package/corpus/templates/analytics/server/lib/content-metrics/steps/search-console.ts +16 -3
- package/corpus/templates/analytics/server/lib/content-metrics/steps/signups.ts +240 -0
- package/corpus/templates/analytics/server/lib/content-metrics/store.ts +5 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +21 -0
- package/corpus/templates/analytics/server/lib/dashboard-panel-verification.ts +300 -5
- package/corpus/templates/analytics/server/lib/dashboard-seeds.ts +2 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +28 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +128 -24
- package/corpus/templates/analytics/server/lib/google-analytics.ts +56 -16
- package/corpus/templates/analytics/server/lib/index-build-github.ts +5 -0
- package/corpus/templates/analytics/server/lib/provider-credentials.ts +47 -2
- package/corpus/templates/analytics/server/lib/query-cost-estimate.ts +184 -5
- package/corpus/templates/analytics/server/plugins/db.ts +9 -1
- package/corpus/templates/analytics/shared/builtin-dashboard-ids.ts +2 -0
- package/corpus/templates/analytics/shared/search-console-property.ts +78 -0
- package/corpus/templates/assets/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/brain/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/calendar/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/calendar/actions/delete-event.ts +1 -3
- package/corpus/templates/calendar/actions/delete-events.ts +1 -2
- package/corpus/templates/calendar/actions/event-action-helpers.ts +0 -4
- package/corpus/templates/calendar/actions/update-event.ts +2 -6
- package/corpus/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/clips/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/clips/CHANGELOG.md +11 -24
- package/corpus/templates/clips/actions/create-recording-upload.ts +5 -1
- package/corpus/templates/clips/app/components/player/chapter-list.tsx +7 -6
- package/corpus/templates/clips/app/components/player/comment-composer.tsx +5 -1
- package/corpus/templates/clips/app/components/player/player-controls.tsx +1 -0
- package/corpus/templates/clips/app/components/player/scrubber.tsx +4 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -3
- package/corpus/templates/clips/app/lib/extension-auth.ts +70 -0
- package/corpus/templates/clips/app/root.tsx +22 -10
- package/corpus/templates/clips/app/routes/_app.notifications.tsx +3 -0
- package/corpus/templates/clips/app/routes/import.tsx +115 -110
- package/corpus/templates/clips/changelog/2026-10-16-agent-uploads-can-use-free-clips-storage.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-agents-see-a-clip-s-frames-as-soon-as-it-finishes-processing.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-chapter-list-times-now-match-the-player-s-clock-and-each-cha.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-editing-a-comment-puts-the-cursor-after-its-text-and-reply-o.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-19-youtube-import-followups.md +6 -0
- package/corpus/templates/clips/changelog/2026-10-20-locally-saved-desktop-recordings-now-appear-in-the-recovery-.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +29 -186
- package/corpus/templates/clips/desktop/src/components/RecordingRecovery.tsx +171 -16
- package/corpus/templates/clips/desktop/src/lib/record-first.ts +9 -32
- package/corpus/templates/clips/desktop/src/lib/recording-recovery.ts +30 -1
- package/corpus/templates/clips/server/lib/agent-frame-request.ts +5 -2
- package/corpus/templates/clips/server/lib/recording-upload-ticket.ts +8 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +2 -0
- package/corpus/templates/content/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/content/CHANGELOG.md +12 -0
- package/corpus/templates/content/actions/_content-database-personal-view.ts +5 -15
- package/corpus/templates/content/actions/_database-navigation.ts +3 -3
- package/corpus/templates/content/actions/_database-utils.ts +10 -22
- package/corpus/templates/content/actions/_database-view-setup.ts +16 -76
- package/corpus/templates/content/actions/create-comment-ai-suggestion.ts +93 -44
- package/corpus/templates/content/actions/export-document.ts +2 -14
- package/corpus/templates/content/actions/suggest-document-edit.ts +5 -7
- package/corpus/templates/content/actions/update-content-database-view.ts +2 -14
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +86 -30
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +64 -28
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +6 -46
- package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +72 -7
- package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +292 -103
- package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +147 -40
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +108 -11
- package/corpus/templates/content/app/components/editor/database/ContentTable.tsx +2 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +150 -166
- package/corpus/templates/content/app/components/editor/database/database-cell-comments.tsx +515 -0
- package/corpus/templates/content/app/components/editor/database/database-item-comments.tsx +5 -43
- package/corpus/templates/content/app/components/editor/database/filter-sort.ts +8 -783
- package/corpus/templates/content/app/components/editor/live-editor-session.ts +5 -1
- package/corpus/templates/content/app/components/editor/page-draft-journal.ts +61 -1
- package/corpus/templates/content/app/components/editor/property-comments.tsx +35 -4
- package/corpus/templates/content/app/components/editor/property-date-format.ts +45 -0
- package/corpus/templates/content/app/components/editor/thread-suggestions.ts +30 -0
- package/corpus/templates/content/app/components/editor/useDocumentReconcileRecovery.ts +23 -4
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +36 -10
- package/corpus/templates/content/app/global.css +23 -0
- package/corpus/templates/content/app/i18n-data.ts +1 -0
- package/corpus/templates/content/changelog/2026-10-16-opening-a-page-in-a-new-tab-while-another-tab-is-still-savin.md +6 -0
- package/corpus/templates/content/changelog/2026-10-16-typing-steadily-in-one-tab-no-longer-makes-another-open-tab-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-database-views-can-filter-the-same-property-more-than-once-n.md +6 -0
- 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
- package/corpus/templates/content/changelog/2026-10-19-starting-a-comment-puts-the-cursor-in-the-comment-box-ready-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-table-row-and-column-options-now-appear-for-the-row-or-colum.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +5 -1
- package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +3 -1
- package/corpus/templates/content/e2e/helpers.ts +10 -0
- package/corpus/templates/content/shared/api.ts +18 -12
- package/corpus/templates/content/shared/database-query.ts +107 -13
- package/corpus/templates/content/shared/suggestion-diff.ts +35 -0
- package/corpus/templates/crm/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/design/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/design/CHANGELOG.md +2 -0
- package/corpus/templates/design/actions/delete-file.ts +1 -4
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +10 -1
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +24 -4
- package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +14 -9
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-modes.ts +23 -2
- package/corpus/templates/design/app/pages/design-editor/tool-state.ts +29 -0
- 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
- package/corpus/templates/design/changelog/2026-10-20-comments-return-to-the-mode-they-came-from-and-layers-search.md +6 -0
- package/corpus/templates/design/e2e/global-setup.ts +185 -85
- package/corpus/templates/dispatch/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/dispatch/CHANGELOG.md +4 -0
- package/corpus/templates/dispatch/actions/apply-workspace-connection-setup.ts +4 -0
- package/corpus/templates/dispatch/actions/delete-workspace-connection.ts +4 -0
- package/corpus/templates/dispatch/actions/set-workspace-connection-grant.ts +4 -0
- package/corpus/templates/dispatch/actions/upsert-workspace-connection.ts +4 -0
- package/corpus/templates/dispatch/changelog/2026-10-19-vault-secrets-grants-link-tokens-app-access-and-workspace-co.md +6 -0
- package/corpus/templates/factory/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
- package/corpus/templates/forms/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/mail/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/mail/.agents/skills/contacts-and-crm/SKILL.md +22 -17
- package/corpus/templates/mail/AGENTS.md +1 -1
- package/corpus/templates/mail/CHANGELOG.md +14 -0
- package/corpus/templates/mail/actions/provider-api-request.ts +3 -3
- package/corpus/templates/mail/app/components/email/IntegrationsSidebar.tsx +211 -54
- package/corpus/templates/mail/app/hooks/use-inbox-threads.ts +26 -2
- package/corpus/templates/mail/app/i18n/en-US.ts +5 -1
- package/corpus/templates/mail/app/lib/gong-connection.ts +27 -0
- package/corpus/templates/mail/app/lib/thread-cache.ts +110 -9
- package/corpus/templates/mail/app/root.tsx +10 -2
- package/corpus/templates/mail/changelog/2026-10-20-connected-gong-settings-now-open-the-shared-integration-mana.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-gong-connection-details-now-match-the-call-data-available-in.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-improve-gong-setup-with-a-click-to-connect-flow-and-permissi.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-mail-lets-you-retry-when-it-cannot-check-your-gong-access.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-mail-thread-cache-stays-isolated-when-mailbox-scope-changes.md +6 -0
- package/corpus/templates/mail/changelog/2026-10-20-the-inbox-and-recently-opened-threads-now-open-instantly-fro.md +6 -0
- package/corpus/templates/mail/server/handlers/emails.ts +9 -0
- package/corpus/templates/plan/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/plan/CHANGELOG.md +4 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +173 -33
- package/corpus/templates/plan/changelog/2026-10-19-opening-a-comment-thread-puts-the-cursor-in-the-reply-box-an.md +6 -0
- package/corpus/templates/slides/.agents/skills/actions/references/action-fields.md +5 -3
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +6 -42
- package/corpus/templates/slides/app/components/editor/FloatingToolbar.tsx +3 -7
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +5 -14
- package/corpus/templates/slides/app/i18n/en-US.ts +0 -2
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +27 -33
- package/corpus/templates/slides/scripts/edit-fidelity/run.ts +1 -3
- package/corpus/templates/tasks/.agents/skills/actions/references/action-fields.md +5 -3
- 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) =>
|
|
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
|
|
64
|
-
* still being saved, or a relation,
|
|
65
|
-
* now, and nothing once the Property is
|
|
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
|
-
|
|
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:
|
|
263
|
+
read: DraftRead,
|
|
258
264
|
create: () => Promise<T>,
|
|
259
265
|
): Promise<{ created: T; draft: PageDraftJournalEntry | null }> {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
266
|
+
// An unreadable journal stops the create before it starts.
|
|
267
|
+
await read();
|
|
263
268
|
const created = await create();
|
|
264
|
-
|
|
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:
|
|
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 = (
|
|
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-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.
|
|
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
|
-
|
|
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 & {});
|