@agent-native/core-corpus 0.222.2-nightly-20261029152548 → 0.222.2
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/clips/CHANGELOG.md +7 -10
- package/corpus/templates/clips/server/lib/content-clip-peer.ts +158 -28
- package/corpus/templates/clips/shared/content-clip-contract.ts +23 -1
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +8 -0
- package/corpus/templates/content/CHANGELOG.md +4 -0
- package/corpus/templates/content/actions/_clip-embed-access.ts +31 -1
- package/corpus/templates/content/actions/add-clip-comment.ts +95 -0
- package/corpus/templates/content/actions/list-clip-comments.ts +68 -30
- package/corpus/templates/content/actions/resolve-clip-comment.ts +56 -0
- package/corpus/templates/content/app/clip-embed-messages.ts +22 -0
- package/corpus/templates/content/app/components/ContentErrorBoundary.tsx +50 -0
- package/corpus/templates/content/app/components/editor/CommentStack.tsx +3 -2
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +188 -62
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +87 -10
- package/corpus/templates/content/app/components/editor/StaticDocumentBody.tsx +36 -0
- package/corpus/templates/content/app/components/editor/TableHoverControls.tsx +7 -2
- package/corpus/templates/content/app/components/editor/clip-embed/ClipEmbedBlock.tsx +113 -9
- package/corpus/templates/content/app/components/editor/clip-embed/ClipPlayer.tsx +33 -1
- package/corpus/templates/content/app/components/editor/clip-embed/clip-comments.ts +397 -0
- package/corpus/templates/content/app/components/editor/clip-embed/player-controls.tsx +17 -3
- package/corpus/templates/content/app/components/editor/clip-embed/scrubber-position.ts +12 -0
- package/corpus/templates/content/app/components/editor/clip-embed/scrubber.tsx +8 -13
- package/corpus/templates/content/app/components/editor/clip-embed/use-clip-comment-writes.ts +348 -0
- package/corpus/templates/content/app/components/editor/clip-embed/use-clip-playback.ts +22 -17
- package/corpus/templates/content/app/components/editor/comment-block-anchors.ts +13 -4
- package/corpus/templates/content/app/components/editor/comment-stacks.ts +6 -1
- package/corpus/templates/content/app/components/editor/discussion-panel.tsx +8 -5
- package/corpus/templates/content/app/components/editor/extensions/TablePresentation.ts +20 -2
- package/corpus/templates/content/app/components/editor/table-sizing.ts +749 -0
- package/corpus/templates/content/app/components/editor/table-view.ts +61 -84
- package/corpus/templates/content/app/global.css +119 -6
- package/corpus/templates/content/app/hooks/use-comments.ts +26 -7
- package/corpus/templates/content/app/lib/private-link-redirect.ts +68 -6
- package/corpus/templates/content/app/root.tsx +14 -50
- package/corpus/templates/content/app/routes/p.$id.tsx +35 -7
- package/corpus/templates/content/changelog/2026-10-22-shareable-local-file-copies-open-inline-with-document-editin.md +6 -0
- package/corpus/templates/content/changelog/2026-10-27-a-public-page-s-link-opens-a-read-only-copy-for-people-who-ar.md +6 -0
- package/corpus/templates/content/changelog/2026-10-27-edits-to-a-public-page-reach-its-readers-within-a-minute-and.md +6 -0
- package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +7 -0
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +11 -5
- package/corpus/templates/content/server/lib/chatgpt-directory-tools.ts +23 -20
- package/corpus/templates/content/server/lib/clips-peer.ts +67 -13
- package/corpus/templates/content/shared/clips-peer-contract.ts +40 -3
- package/corpus/templates/design/CHANGELOG.md +1 -0
- package/corpus/templates/design/README.md +14 -0
- package/corpus/templates/design/actions/enroll-first-output-experiment.ts +99 -0
- package/corpus/templates/design/actions/visual-edit-browser-request.ts +23 -1
- package/corpus/templates/design/app/hooks/use-agent-generating.ts +13 -0
- package/corpus/templates/design/app/lib/agent-run-status.ts +24 -0
- package/corpus/templates/design/app/lib/design-funnel-tracking.ts +5 -0
- package/corpus/templates/design/app/pages/design-editor/commands/start-retry-generation.ts +59 -0
- package/corpus/templates/design/app/pages/design-editor/domains/use-editor-generation-and-access.ts +6 -0
- package/corpus/templates/design/app/pages/design-editor/view/first-output-next-step.tsx +95 -15
- package/corpus/templates/design/changelog/2026-10-22-generation-sessions-and-coding-handoffs-open-the-design-inli.md +6 -0
- package/corpus/templates/design/server/db/schema.ts +11 -0
- package/corpus/templates/design/server/lib/builder-identity-enabled.ts +23 -0
- package/corpus/templates/design/server/lib/builder-identity.ts +2 -21
- package/corpus/templates/design/server/lib/chatgpt-directory-tools.ts +16 -0
- package/corpus/templates/design/server/plugins/builder-host-embed-headers.ts +2 -7
- package/corpus/templates/design/server/plugins/db.ts +11 -0
- package/corpus/templates/design/shared/builder-host-origins.ts +28 -0
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +37 -8
- package/corpus/templates/slides/CHANGELOG.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-29-deck-change-notifications-are-now-only-sent-to-people-who-ca.md +6 -0
- package/corpus/templates/slides/server/handlers/decks.ts +70 -24
- package/package.json +1 -1
- package/corpus/templates/analytics/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/assets/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/design/.generated/bridge/native-builtin-metadata.generated.ts +0 -326
- package/corpus/templates/design/.generated/bridge/native-worker-source.generated.ts +0 -6
- package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
- package/corpus/templates/slides-2/.deploy-tmp/agent-native-provided-plugins.mjs +0 -10
package/corpus/README.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable user-facing changes to Clips are documented here. Open it any time
|
|
4
4
|
from the command menu (Cmd+K → "What's new") or from Settings.
|
|
5
5
|
|
|
6
|
+
## 2026-10-29
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Screenshots are available when enabled in Labs.
|
|
11
|
+
|
|
6
12
|
## 2026-10-28
|
|
7
13
|
|
|
8
14
|
### Improved
|
|
@@ -13,6 +19,7 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
13
19
|
|
|
14
20
|
### Fixed
|
|
15
21
|
|
|
22
|
+
- Fixed the Share → Advanced popover so the Save buttons fit and the password toggle matches other switches
|
|
16
23
|
- Clips desktop preserves your server choice across updates and reconnects after temporary sign-in outages.
|
|
17
24
|
- Recording chapters remain reachable when the list extends below the player.
|
|
18
25
|
- The Chrome extension recorder has more room and a clearer feedback link.
|
|
@@ -1600,14 +1607,4 @@ from the command menu (Cmd+K → "What's new") or from Settings.
|
|
|
1600
1607
|
|
|
1601
1608
|
- Mobile recording pages keep videos in a 16:9 player with activity below.
|
|
1602
1609
|
|
|
1603
|
-
## 2026-07-02
|
|
1604
|
-
|
|
1605
|
-
### Added
|
|
1606
|
-
|
|
1607
|
-
- Private recordings can be shared with agents through temporary links without making them public.
|
|
1608
|
-
|
|
1609
|
-
### Fixed
|
|
1610
|
-
|
|
1611
|
-
- Recordings now wait for uploaded media to play from storage before finishing, and playback falls back when a compressed storage copy is temporarily broken.
|
|
1612
|
-
|
|
1613
1610
|
For the full list of updates, see the [changelog folder](./changelog/).
|
|
@@ -13,7 +13,18 @@ import {
|
|
|
13
13
|
} from "@agent-native/core/server";
|
|
14
14
|
import { ForbiddenError, resolveAccess } from "@agent-native/core/sharing";
|
|
15
15
|
import { isEmailDerivedName } from "@agent-native/core/user-profile";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
and,
|
|
18
|
+
asc,
|
|
19
|
+
count,
|
|
20
|
+
eq,
|
|
21
|
+
inArray,
|
|
22
|
+
isNotNull,
|
|
23
|
+
isNull,
|
|
24
|
+
sql,
|
|
25
|
+
type SQL,
|
|
26
|
+
} from "drizzle-orm";
|
|
27
|
+
import { alias } from "drizzle-orm/pg-core";
|
|
17
28
|
import { getQuery, getRequestHeader, getRouterParam, type H3Event } from "h3";
|
|
18
29
|
import { z } from "zod";
|
|
19
30
|
|
|
@@ -29,8 +40,13 @@ import {
|
|
|
29
40
|
CONTENT_CLIP_CLAIMS,
|
|
30
41
|
CONTENT_CLIP_COMMENT_AUTHOR_CHARS,
|
|
31
42
|
CONTENT_CLIP_COMMENT_PREVIEW_CHARS,
|
|
43
|
+
CONTENT_CLIP_COMMENT_TEXT_CHARS,
|
|
44
|
+
CONTENT_CLIP_MAX_COMMENT_CHARS,
|
|
32
45
|
CONTENT_CLIP_MAX_COMMENT_THREADS,
|
|
46
|
+
CONTENT_CLIP_MAX_LISTED_REPLIES,
|
|
33
47
|
CONTENT_CLIP_MAX_TOKEN_SECONDS,
|
|
48
|
+
CONTENT_CLIP_MAX_VIDEO_TIMESTAMP_MS,
|
|
49
|
+
CONTENT_CLIP_OPEN_THREADS_VIEW,
|
|
34
50
|
CONTENT_CLIP_PUBLIC_READER,
|
|
35
51
|
CONTENT_CLIP_RECORDING_TITLE_CHARS,
|
|
36
52
|
CONTENT_CLIP_SCOPES,
|
|
@@ -92,10 +108,8 @@ const MAX_DOCUMENT_ID_CHARS = 256;
|
|
|
92
108
|
const MAX_ISSUER_CHARS = 2048;
|
|
93
109
|
const MAX_VIEWER_NAME_CHARS = 200;
|
|
94
110
|
const ISSUED_AT_SKEW_SECONDS = 60;
|
|
95
|
-
const MAX_COMMENT_CHARS = 10_000;
|
|
96
111
|
const MAX_COMMENT_ID_CHARS = 128;
|
|
97
112
|
const MAX_EMOJI_CHARS = 32;
|
|
98
|
-
const MAX_VIDEO_TIMESTAMP_MS = 7 * 24 * 60 * 60 * 1000;
|
|
99
113
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
100
114
|
const viewerEmailSchema = z.string().email().max(320);
|
|
101
115
|
|
|
@@ -556,50 +570,152 @@ const commentListQuerySchema = z.object({
|
|
|
556
570
|
.transform(Number)
|
|
557
571
|
.pipe(z.number().int().min(1).max(CONTENT_CLIP_MAX_COMMENT_THREADS))
|
|
558
572
|
.default(CONTENT_CLIP_MAX_COMMENT_THREADS),
|
|
573
|
+
view: z.literal(CONTENT_CLIP_OPEN_THREADS_VIEW).optional(),
|
|
559
574
|
});
|
|
560
575
|
|
|
561
576
|
/**
|
|
562
577
|
* The first `limit` thread starts by video time, as `list-comments` orders
|
|
563
|
-
* them, reading one more to tell a full page from the whole list.
|
|
564
|
-
*
|
|
565
|
-
*
|
|
578
|
+
* them, reading one more to tell a full page from the whole list. The
|
|
579
|
+
* open-threads view lists only open threads, each with its replies up to the
|
|
580
|
+
* contract's bound. Text and names are cut to the contract's lengths. A
|
|
581
|
+
* signed-out reader's list carries no commenter's email, nor a name made
|
|
582
|
+
* from one.
|
|
566
583
|
*/
|
|
567
|
-
async function listCommentThreads(
|
|
584
|
+
async function listCommentThreads(
|
|
585
|
+
peer: ContentClipPeer,
|
|
586
|
+
limit: number,
|
|
587
|
+
openThreads: boolean,
|
|
588
|
+
) {
|
|
568
589
|
const { recordingId } = peer;
|
|
569
590
|
const signedOut = peer.viewerEmail === null;
|
|
570
591
|
await assertCommentAccess(recordingId, "read");
|
|
571
592
|
const comments = schema.recordingComments;
|
|
593
|
+
const text = sql<string>`left(${comments.content}, ${
|
|
594
|
+
openThreads
|
|
595
|
+
? CONTENT_CLIP_COMMENT_TEXT_CHARS
|
|
596
|
+
: CONTENT_CLIP_COMMENT_PREVIEW_CHARS
|
|
597
|
+
})`;
|
|
572
598
|
const rows = await getDb()
|
|
573
599
|
.select({
|
|
574
600
|
id: comments.id,
|
|
575
601
|
authorEmail: comments.authorEmail,
|
|
576
602
|
authorName: comments.authorName,
|
|
577
|
-
content:
|
|
603
|
+
content: text,
|
|
578
604
|
videoTimestampMs: comments.videoTimestampMs,
|
|
605
|
+
createdAt: comments.createdAt,
|
|
579
606
|
})
|
|
580
607
|
.from(comments)
|
|
581
608
|
.where(
|
|
582
|
-
and(
|
|
609
|
+
and(
|
|
610
|
+
eq(comments.recordingId, recordingId),
|
|
611
|
+
isNull(comments.parentId),
|
|
612
|
+
openThreads ? eq(comments.resolved, false) : undefined,
|
|
613
|
+
),
|
|
614
|
+
)
|
|
615
|
+
.orderBy(
|
|
616
|
+
asc(comments.videoTimestampMs),
|
|
617
|
+
asc(comments.createdAt),
|
|
618
|
+
asc(comments.id),
|
|
583
619
|
)
|
|
584
|
-
.orderBy(asc(comments.videoTimestampMs), asc(comments.createdAt))
|
|
585
620
|
.limit(limit + 1);
|
|
621
|
+
const author = <
|
|
622
|
+
Row extends { authorEmail: string; authorName: string | null },
|
|
623
|
+
>(
|
|
624
|
+
row: Row,
|
|
625
|
+
) => {
|
|
626
|
+
const authorName =
|
|
627
|
+
signedOut && isEmailDerivedName(row.authorName, row.authorEmail)
|
|
628
|
+
? null
|
|
629
|
+
: row.authorName;
|
|
630
|
+
return {
|
|
631
|
+
...row,
|
|
632
|
+
authorEmail: signedOut
|
|
633
|
+
? null
|
|
634
|
+
: row.authorEmail.slice(0, CONTENT_CLIP_COMMENT_AUTHOR_CHARS),
|
|
635
|
+
authorName:
|
|
636
|
+
authorName?.slice(0, CONTENT_CLIP_COMMENT_AUTHOR_CHARS) ?? null,
|
|
637
|
+
};
|
|
638
|
+
};
|
|
586
639
|
const threads = await hydrateCommentAuthorNames(rows.slice(0, limit));
|
|
640
|
+
const truncated = rows.length > limit;
|
|
641
|
+
if (!openThreads) {
|
|
642
|
+
return {
|
|
643
|
+
comments: threads.map(({ createdAt: _createdAt, ...thread }) =>
|
|
644
|
+
author(thread),
|
|
645
|
+
),
|
|
646
|
+
truncated,
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const threadIds = threads.map((thread) => thread.id);
|
|
651
|
+
const replies = threadIds.length
|
|
652
|
+
? await listThreadReplies(recordingId, threadIds, text)
|
|
653
|
+
: { rows: [], counts: new Map<string, number>() };
|
|
654
|
+
const repliesOf = new Map<string, unknown[]>();
|
|
655
|
+
for (const { threadId, ...reply } of await hydrateCommentAuthorNames(
|
|
656
|
+
replies.rows,
|
|
657
|
+
)) {
|
|
658
|
+
repliesOf.set(threadId, [
|
|
659
|
+
...(repliesOf.get(threadId) ?? []),
|
|
660
|
+
author(reply),
|
|
661
|
+
]);
|
|
662
|
+
}
|
|
587
663
|
return {
|
|
588
|
-
comments: threads.map((thread) => {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
664
|
+
comments: threads.map((thread) => ({
|
|
665
|
+
...author(thread),
|
|
666
|
+
replyCount: replies.counts.get(thread.id) ?? 0,
|
|
667
|
+
replies: repliesOf.get(thread.id) ?? [],
|
|
668
|
+
})),
|
|
669
|
+
truncated,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* The listed threads' replies, oldest first, from the earliest thread on and
|
|
675
|
+
* no more than the contract's bound, with how many each thread has.
|
|
676
|
+
*/
|
|
677
|
+
async function listThreadReplies(
|
|
678
|
+
recordingId: string,
|
|
679
|
+
threadIds: string[],
|
|
680
|
+
text: SQL<string>,
|
|
681
|
+
) {
|
|
682
|
+
const comments = schema.recordingComments;
|
|
683
|
+
const root = alias(comments, "thread_start");
|
|
684
|
+
const inThreads = and(
|
|
685
|
+
eq(comments.recordingId, recordingId),
|
|
686
|
+
inArray(comments.threadId, threadIds),
|
|
687
|
+
isNotNull(comments.parentId),
|
|
688
|
+
);
|
|
689
|
+
const [rows, counts] = await Promise.all([
|
|
690
|
+
getDb()
|
|
691
|
+
.select({
|
|
692
|
+
id: comments.id,
|
|
693
|
+
threadId: comments.threadId,
|
|
694
|
+
authorEmail: comments.authorEmail,
|
|
695
|
+
authorName: comments.authorName,
|
|
696
|
+
content: text,
|
|
697
|
+
createdAt: comments.createdAt,
|
|
698
|
+
})
|
|
699
|
+
.from(comments)
|
|
700
|
+
.innerJoin(root, eq(root.id, comments.threadId))
|
|
701
|
+
.where(inThreads)
|
|
702
|
+
.orderBy(
|
|
703
|
+
asc(root.videoTimestampMs),
|
|
704
|
+
asc(root.createdAt),
|
|
705
|
+
asc(root.id),
|
|
706
|
+
asc(comments.createdAt),
|
|
707
|
+
asc(comments.id),
|
|
708
|
+
)
|
|
709
|
+
.limit(CONTENT_CLIP_MAX_LISTED_REPLIES),
|
|
710
|
+
getDb()
|
|
711
|
+
.select({ threadId: comments.threadId, replies: count() })
|
|
712
|
+
.from(comments)
|
|
713
|
+
.where(inThreads)
|
|
714
|
+
.groupBy(comments.threadId),
|
|
715
|
+
]);
|
|
716
|
+
return {
|
|
717
|
+
rows,
|
|
718
|
+
counts: new Map(counts.map((row) => [row.threadId, Number(row.replies)])),
|
|
603
719
|
};
|
|
604
720
|
}
|
|
605
721
|
|
|
@@ -629,21 +745,35 @@ export async function handleContentClipComments(
|
|
|
629
745
|
);
|
|
630
746
|
}
|
|
631
747
|
try {
|
|
632
|
-
return json(
|
|
748
|
+
return json(
|
|
749
|
+
await listCommentThreads(
|
|
750
|
+
peer,
|
|
751
|
+
query.data.limit,
|
|
752
|
+
query.data.view === CONTENT_CLIP_OPEN_THREADS_VIEW,
|
|
753
|
+
),
|
|
754
|
+
);
|
|
633
755
|
} catch (error) {
|
|
634
756
|
return forbiddenResponse(error);
|
|
635
757
|
}
|
|
636
758
|
});
|
|
637
759
|
}
|
|
638
760
|
|
|
639
|
-
const commentText = z
|
|
761
|
+
const commentText = z
|
|
762
|
+
.string()
|
|
763
|
+
.trim()
|
|
764
|
+
.min(1)
|
|
765
|
+
.max(CONTENT_CLIP_MAX_COMMENT_CHARS);
|
|
640
766
|
const commentId = z.string().trim().min(1).max(MAX_COMMENT_ID_CHARS);
|
|
641
767
|
const commentWriteSchema = z.discriminatedUnion("operation", [
|
|
642
768
|
z
|
|
643
769
|
.object({
|
|
644
770
|
operation: z.literal("add"),
|
|
645
771
|
content: commentText,
|
|
646
|
-
videoTimestampMs: z
|
|
772
|
+
videoTimestampMs: z
|
|
773
|
+
.number()
|
|
774
|
+
.int()
|
|
775
|
+
.min(0)
|
|
776
|
+
.max(CONTENT_CLIP_MAX_VIDEO_TIMESTAMP_MS),
|
|
647
777
|
})
|
|
648
778
|
.strict(),
|
|
649
779
|
z
|
|
@@ -77,10 +77,32 @@ export const CONTENT_CLIP_PLAYBACK_GRANT_SECONDS = 5 * 60;
|
|
|
77
77
|
/**
|
|
78
78
|
* The most comment threads one comment-list call returns. Content asks with
|
|
79
79
|
* `?limit=<n>`, n from 1 to this, and gets the first n thread starts by video
|
|
80
|
-
* time, with `truncated: true` when there are more. Replies
|
|
80
|
+
* time, with `truncated: true` when there are more. Replies come back only in
|
|
81
|
+
* the open-threads view.
|
|
81
82
|
*/
|
|
82
83
|
export const CONTENT_CLIP_MAX_COMMENT_THREADS = 500;
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The comment list for a document's comment margin, asked for with
|
|
87
|
+
* `?view=open-threads`: open threads only, each with `createdAt`, its text
|
|
88
|
+
* cut to `CONTENT_CLIP_COMMENT_TEXT_CHARS` rather than the preview length,
|
|
89
|
+
* `replyCount`, and `replies` oldest first, each `{ id, authorEmail,
|
|
90
|
+
* authorName, content, createdAt }` cut the same way. At most
|
|
91
|
+
* `CONTENT_CLIP_MAX_LISTED_REPLIES` replies come back across the list, from
|
|
92
|
+
* the earliest thread on, so a thread whose `replies` is shorter than its
|
|
93
|
+
* `replyCount` had some held back. A Clips from before this view ignores it
|
|
94
|
+
* and answers with no `replies`.
|
|
95
|
+
*/
|
|
96
|
+
export const CONTENT_CLIP_OPEN_THREADS_VIEW = "open-threads";
|
|
97
|
+
export const CONTENT_CLIP_COMMENT_TEXT_CHARS = 2_000;
|
|
98
|
+
export const CONTENT_CLIP_MAX_LISTED_REPLIES = 500;
|
|
99
|
+
|
|
100
|
+
/** The longest comment or reply Content may add or edit a Clip comment to. */
|
|
101
|
+
export const CONTENT_CLIP_MAX_COMMENT_CHARS = 10_000;
|
|
102
|
+
|
|
103
|
+
/** The latest moment of a video a comment from Content may sit at. */
|
|
104
|
+
export const CONTENT_CLIP_MAX_VIDEO_TIMESTAMP_MS = 7 * 24 * 60 * 60 * 1000;
|
|
105
|
+
|
|
84
106
|
/**
|
|
85
107
|
* A listed thread start's text is cut to this many characters, and its
|
|
86
108
|
* author's name and email to `CONTENT_CLIP_COMMENT_AUTHOR_CHARS`, so Content
|
|
@@ -301,6 +301,8 @@ On a collection page, each Blocks field tab has its own comments. Pass `fieldId`
|
|
|
301
301
|
|
|
302
302
|
A thread can sit on a whole video instead of on text. Pass `blockId` with the video's `id` attribute from `get-document`'s content (`<video src="…" id="video-…">`), plus `fieldId` for a video in another tab. The thread quotes no text; people see it beside the video, and `list-comments` returns `block_id` and `block_type` on it. A video saved without an `id` has none to pass: give it a new unique `video-` id with `edit-document`, then comment with that id. `add-comment` refuses the id such a video goes by until then (`comment_block_id_unsaved`), because that id passes to another video when videos with its source are added or reordered. Each video's `id` is its own: when you copy a `<video>` line, give the copy a new unique `video-` id or none. An edit that gives a second video an id the page already uses is refused (`DOCUMENT_VIDEO_ID_SHARED`); a new page isn't checked, since no thread is on it yet. Videos that already share an id show none of its threads, and `add-comment` refuses that id (`comment_block_id_shared`); give the video you mean a new id with `edit-document` first. Never give it a `block_id` from `list-comments`; one no video carries may be a deleted video's, and reusing it moves that thread onto this video. A thread made before videos had ids quotes the word "Video"; on a page with exactly one video it shows on that video, and with more it stays on its quote.
|
|
303
303
|
|
|
304
|
+
A Clip block (`<ClipEmbed recordingId="…" />`, behind the `content.clip-embed` flag) keeps its comments in Clips, so `list-comments` never returns them and every page holding that Clip shows the same ones. People see each open thread as its own card beside the Clip, at its moment of the video. `list-clip-comments` returns a Clip's open threads in the order of their moments, with replies; `add-clip-comment` starts a thread at `videoTimestampMs` (milliseconds from the start of the video) or replies with a `threadId` from that list; `resolve-clip-comment` resolves a thread, which Clips lets only the thread's author and the Clip's editors do (`clip_forbidden`). Writing needs comment access to the page. Clip comments take no `mentions` and no Ask AI, and Content sends no email for them. When the Clip's comments are off, every write fails with `clip_comments_disabled`, and so does reading for anyone but the Clip's owner.
|
|
305
|
+
|
|
304
306
|
```bash
|
|
305
307
|
# List threads (returns anchor fields + parsed mentions)
|
|
306
308
|
pnpm action list-comments --documentId abc123
|
|
@@ -325,6 +327,12 @@ pnpm action add-comment --documentId abc123 --threadId t123 --parentId c123 --co
|
|
|
325
327
|
# Resolve / reopen the whole thread
|
|
326
328
|
pnpm action update-comment --id c123 --resolved true
|
|
327
329
|
pnpm action update-comment --id c123 --resolved false
|
|
330
|
+
|
|
331
|
+
# A Clip's comments: list, start one at 0:42, reply, resolve
|
|
332
|
+
pnpm action list-clip-comments --documentId abc123 --recordingId rec456
|
|
333
|
+
pnpm action add-clip-comment --documentId abc123 --recordingId rec456 --videoTimestampMs 42000 --content "Cut this pause"
|
|
334
|
+
pnpm action add-clip-comment --documentId abc123 --recordingId rec456 --threadId cc789 --content "Done"
|
|
335
|
+
pnpm action resolve-clip-comment --documentId abc123 --recordingId rec456 --threadId cc789 --resolved true
|
|
328
336
|
```
|
|
329
337
|
|
|
330
338
|
`--authorName` sets the comment's display name; it defaults to a name derived from the author's email.
|
|
@@ -18,10 +18,13 @@ time from the command menu (Cmd+K → "What's new").
|
|
|
18
18
|
|
|
19
19
|
### Improved
|
|
20
20
|
|
|
21
|
+
- A public page's link opens a read-only copy for people who aren't signed in, instead of asking them to sign in
|
|
21
22
|
- An AI suggestion in a comment thread uses the same group as suggested changes in the margin: its summary, edit count, Accept all and Reject all come first, and opening it shows each edit with the usual check and X.
|
|
22
23
|
|
|
23
24
|
### Fixed
|
|
24
25
|
|
|
26
|
+
- Edits to a public page reach its readers within a minute, and making it private or moving it to Trash takes it down just as fast
|
|
27
|
+
- Two collections connected to the same Builder model can no longer push over each other's changes to the same entry at the same moment.
|
|
25
28
|
- Restoring page history preserves text without exposing unauthorized private callout images
|
|
26
29
|
- An agent's Accept on words someone is still typing in returns the suggestion to pending instead of failing with a server error
|
|
27
30
|
- Opening a page you can't access goes straight to the no-access screen without a brief error first, including when you come back to it
|
|
@@ -97,6 +100,7 @@ time from the command menu (Cmd+K → "What's new").
|
|
|
97
100
|
|
|
98
101
|
### Improved
|
|
99
102
|
|
|
103
|
+
- Shareable local-file copies open inline with document editing and sharing controls in compatible agent hosts.
|
|
100
104
|
- Approval links from a connected agent, such as Claude Code, now open the review inside the app, in its own theme.
|
|
101
105
|
- Tables get Header row, Header column and Fit to width in their block menu, colours for rows and columns, and menus that close the moment you press Escape.
|
|
102
106
|
- The block menu turns a block into another kind, colours it and comments on it, filters as you type, and shows each action’s shortcut. It says why a locked block can’t be duplicated or deleted, and a click beside the page lets go of a selected block.
|
|
@@ -70,6 +70,10 @@ const FAILURES: Record<
|
|
|
70
70
|
statusCode: 403,
|
|
71
71
|
message: "Comments are turned off for this Clip",
|
|
72
72
|
},
|
|
73
|
+
[CLIP_PLAYBACK_ERRORS.commentRequired]: {
|
|
74
|
+
statusCode: 403,
|
|
75
|
+
message: "Only people who can comment on the page can comment on its Clips",
|
|
76
|
+
},
|
|
73
77
|
[CLIP_PLAYBACK_ERRORS.forbidden]: {
|
|
74
78
|
statusCode: 403,
|
|
75
79
|
message: "Clips refused this reader",
|
|
@@ -121,14 +125,39 @@ async function documentState(documentId: string) {
|
|
|
121
125
|
export async function authorizeClipReader(
|
|
122
126
|
documentId: string,
|
|
123
127
|
): Promise<ClipReader> {
|
|
128
|
+
return (await authorizeReader(documentId)).reader;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The reader Content may vouch for to Clips to comment on a Clip in this
|
|
133
|
+
* page: a signed-in reader who can comment on the page itself. A signed-out
|
|
134
|
+
* reader of a public page never can.
|
|
135
|
+
*/
|
|
136
|
+
export async function authorizeClipCommenter(
|
|
137
|
+
documentId: string,
|
|
138
|
+
): Promise<ClipUploader> {
|
|
139
|
+
const { reader, role } = await authorizeReader(documentId);
|
|
140
|
+
if (
|
|
141
|
+
reader.kind !== "signed-in" ||
|
|
142
|
+
!role ||
|
|
143
|
+
!roleSatisfies(role, "commenter")
|
|
144
|
+
) {
|
|
145
|
+
failClip(CLIP_PLAYBACK_ERRORS.commentRequired);
|
|
146
|
+
}
|
|
147
|
+
return reader;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function authorizeReader(documentId: string) {
|
|
124
151
|
const email = getRequestUserEmail();
|
|
125
152
|
const readerEmail = email && !isPublicViewerOwner(email) ? email : null;
|
|
126
153
|
|
|
154
|
+
let role: Parameters<typeof roleSatisfies>[0] | null = null;
|
|
127
155
|
if (readerEmail) {
|
|
128
156
|
const access = await resolveDocumentAccess(documentId, {
|
|
129
157
|
skipResourceBody: true,
|
|
130
158
|
});
|
|
131
159
|
if (!access) failClip(CLIP_PLAYBACK_ERRORS.documentNotFound);
|
|
160
|
+
role = access.role;
|
|
132
161
|
}
|
|
133
162
|
const state = await documentState(documentId);
|
|
134
163
|
if (!state || state.trashedAt) {
|
|
@@ -147,13 +176,14 @@ export async function authorizeClipReader(
|
|
|
147
176
|
});
|
|
148
177
|
if (!enabled) failClip(CLIP_PLAYBACK_ERRORS.disabled);
|
|
149
178
|
|
|
150
|
-
|
|
179
|
+
const reader: ClipReader = readerEmail
|
|
151
180
|
? {
|
|
152
181
|
kind: "signed-in",
|
|
153
182
|
email: readerEmail,
|
|
154
183
|
name: getRequestUserName() ?? null,
|
|
155
184
|
}
|
|
156
185
|
: { kind: "public" };
|
|
186
|
+
return { reader, role };
|
|
157
187
|
}
|
|
158
188
|
|
|
159
189
|
/**
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { defineAction, fail } from "@agent-native/core/action";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type ClipCommentWrite,
|
|
6
|
+
writeClipComment,
|
|
7
|
+
} from "../server/lib/clips-peer.js";
|
|
8
|
+
import {
|
|
9
|
+
CLIP_PLAYBACK_ERRORS,
|
|
10
|
+
CONTENT_CLIP_MAX_COMMENT_CHARS,
|
|
11
|
+
CONTENT_CLIP_MAX_VIDEO_TIMESTAMP_MS,
|
|
12
|
+
} from "../shared/clips-peer-contract.js";
|
|
13
|
+
import {
|
|
14
|
+
authorizeClipCommenter,
|
|
15
|
+
clipEmbedArgsSchema,
|
|
16
|
+
failClip,
|
|
17
|
+
} from "./_clip-embed-access.js";
|
|
18
|
+
|
|
19
|
+
const answerSchema = z.object({ id: z.string(), threadId: z.string() });
|
|
20
|
+
|
|
21
|
+
// Coercion would read null or a blank as 0:00 and start a real thread there.
|
|
22
|
+
const timestampArg = z.preprocess(
|
|
23
|
+
(value) =>
|
|
24
|
+
typeof value === "string" && value.trim() !== "" ? Number(value) : value,
|
|
25
|
+
z.number().int().min(0).max(CONTENT_CLIP_MAX_VIDEO_TIMESTAMP_MS),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const ONE_TARGET =
|
|
29
|
+
"Pass videoTimestampMs to start a thread or threadId to reply, not both";
|
|
30
|
+
|
|
31
|
+
export default defineAction({
|
|
32
|
+
description:
|
|
33
|
+
"Comment on a Clip placed in a page: start a thread at a moment of its video (videoTimestampMs), or reply to one of its threads (threadId from list-clip-comments). The comment is saved in Clips as the caller, who must be able to comment on the page. Fails with clip_comments_disabled when the Clip's comments are off.",
|
|
34
|
+
mcpAnnotations: {
|
|
35
|
+
readOnlyHint: false,
|
|
36
|
+
destructiveHint: false,
|
|
37
|
+
idempotentHint: false,
|
|
38
|
+
openWorldHint: false,
|
|
39
|
+
},
|
|
40
|
+
schema: clipEmbedArgsSchema
|
|
41
|
+
.extend({
|
|
42
|
+
content: z
|
|
43
|
+
.string()
|
|
44
|
+
.trim()
|
|
45
|
+
.min(1)
|
|
46
|
+
.max(CONTENT_CLIP_MAX_COMMENT_CHARS)
|
|
47
|
+
.describe("Comment text"),
|
|
48
|
+
videoTimestampMs: timestampArg
|
|
49
|
+
.optional()
|
|
50
|
+
.describe(
|
|
51
|
+
"Moment of the video to start a thread at, in milliseconds from its start. Omit when replying.",
|
|
52
|
+
),
|
|
53
|
+
threadId: z
|
|
54
|
+
.string()
|
|
55
|
+
.trim()
|
|
56
|
+
.min(1)
|
|
57
|
+
.max(128)
|
|
58
|
+
.optional()
|
|
59
|
+
.describe(
|
|
60
|
+
"Id of the thread to reply to, from list-clip-comments. Omit to start a thread.",
|
|
61
|
+
),
|
|
62
|
+
})
|
|
63
|
+
.refine(
|
|
64
|
+
(args) =>
|
|
65
|
+
(args.videoTimestampMs === undefined) !== (args.threadId === undefined),
|
|
66
|
+
ONE_TARGET,
|
|
67
|
+
),
|
|
68
|
+
outputSchema: answerSchema,
|
|
69
|
+
run: async ({
|
|
70
|
+
documentId,
|
|
71
|
+
recordingId,
|
|
72
|
+
content,
|
|
73
|
+
videoTimestampMs,
|
|
74
|
+
threadId,
|
|
75
|
+
}) => {
|
|
76
|
+
const write: ClipCommentWrite | null =
|
|
77
|
+
threadId !== undefined
|
|
78
|
+
? { operation: "reply", commentId: threadId, content }
|
|
79
|
+
: videoTimestampMs !== undefined
|
|
80
|
+
? { operation: "add", content, videoTimestampMs }
|
|
81
|
+
: null;
|
|
82
|
+
if (!write) fail(ONE_TARGET, { errorCode: "clip_comment_target" });
|
|
83
|
+
const reader = await authorizeClipCommenter(documentId);
|
|
84
|
+
const result = await writeClipComment({
|
|
85
|
+
reader,
|
|
86
|
+
documentId,
|
|
87
|
+
recordingId,
|
|
88
|
+
write,
|
|
89
|
+
});
|
|
90
|
+
if (!result.ok) failClip(result.code);
|
|
91
|
+
const parsed = answerSchema.safeParse(result.body);
|
|
92
|
+
if (!parsed.success) failClip(CLIP_PLAYBACK_ERRORS.unreachable);
|
|
93
|
+
return { id: parsed.data.id, threadId: parsed.data.threadId };
|
|
94
|
+
},
|
|
95
|
+
});
|