@agent-native/core 0.84.54 → 0.84.56
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/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/recap.ts +146 -5
- package/corpus/core/src/client/AssistantChat.tsx +58 -32
- package/corpus/core/src/client/agent-chat-adapter.ts +14 -4
- package/corpus/core/src/client/chat/message-components.tsx +5 -1
- package/corpus/core/src/client/sse-event-processor.ts +11 -4
- package/corpus/core/src/client/use-agent-engine-configured.ts +54 -12
- package/corpus/core/src/server/agent-chat-plugin.ts +9 -5
- package/corpus/templates/plan/actions/create-visual-recap.ts +39 -0
- package/corpus/templates/plan/actions/list-visual-plans.ts +2 -0
- package/corpus/templates/plan/actions/view-screen.ts +2 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +279 -114
- package/corpus/templates/plan/changelog/2026-07-02-plan-comment-shortcuts-now-open-comment-mode-from-the-keyboa.md +6 -0
- package/corpus/templates/plan/server/db/schema.ts +3 -0
- package/corpus/templates/plan/server/lib/comment-notifications.ts +21 -2
- package/corpus/templates/plan/server/plans.ts +6 -0
- package/corpus/templates/plan/server/plugins/db.ts +11 -0
- package/corpus/templates/plan/shared/comment-context.ts +1 -0
- package/corpus/templates/plan/shared/types.ts +4 -0
- package/dist/cli/recap.d.ts +14 -0
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +88 -5
- package/dist/cli/recap.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +33 -20
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +12 -2
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +5 -1
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +9 -4
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-agent-engine-configured.d.ts +10 -1
- package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
- package/dist/client/use-agent-engine-configured.js +35 -10
- package/dist/client/use-agent-engine-configured.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +4 -4
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +9 -2
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/package.json +1 -1
|
@@ -44,12 +44,24 @@ const sourceTypeSchema = z
|
|
|
44
44
|
|
|
45
45
|
const sourcePrStateSchema = z.enum(["open", "closed", "merged", "unknown"]);
|
|
46
46
|
|
|
47
|
+
const sourceAuthorEmailSchema = z
|
|
48
|
+
.string()
|
|
49
|
+
.trim()
|
|
50
|
+
.email()
|
|
51
|
+
.transform((email) => email.toLowerCase())
|
|
52
|
+
.optional();
|
|
53
|
+
|
|
54
|
+
const sourceAuthorTextSchema = z.string().trim().min(1).optional();
|
|
55
|
+
|
|
47
56
|
type RecapSourceMetadata = {
|
|
48
57
|
sourceType?: string;
|
|
49
58
|
sourceRepo?: string;
|
|
50
59
|
sourcePrNumber?: number;
|
|
51
60
|
sourcePrState?: "open" | "closed" | "merged" | "unknown";
|
|
52
61
|
sourcePrMergedAt?: string;
|
|
62
|
+
sourceAuthorEmail?: string;
|
|
63
|
+
sourceAuthorName?: string;
|
|
64
|
+
sourceAuthorLogin?: string;
|
|
53
65
|
};
|
|
54
66
|
|
|
55
67
|
type RecapVisibility = "private" | "org" | "public";
|
|
@@ -82,17 +94,26 @@ function normalizeRecapSourceMetadata(args: {
|
|
|
82
94
|
sourcePrNumber?: number;
|
|
83
95
|
sourcePrState?: "open" | "closed" | "merged" | "unknown";
|
|
84
96
|
sourcePrMergedAt?: string;
|
|
97
|
+
sourceAuthorEmail?: string;
|
|
98
|
+
sourceAuthorName?: string;
|
|
99
|
+
sourceAuthorLogin?: string;
|
|
85
100
|
}): RecapSourceMetadata {
|
|
86
101
|
const inferred = inferGithubPullRequestSource(args.sourceUrl);
|
|
87
102
|
const sourcePrMergedAt = args.sourcePrMergedAt?.trim();
|
|
88
103
|
const sourcePrState =
|
|
89
104
|
args.sourcePrState ?? (sourcePrMergedAt ? "merged" : undefined);
|
|
105
|
+
const sourceAuthorEmail = args.sourceAuthorEmail?.trim().toLowerCase();
|
|
106
|
+
const sourceAuthorName = args.sourceAuthorName?.trim();
|
|
107
|
+
const sourceAuthorLogin = args.sourceAuthorLogin?.trim();
|
|
90
108
|
return {
|
|
91
109
|
sourceType: args.sourceType ?? inferred.sourceType,
|
|
92
110
|
sourceRepo: args.sourceRepo ?? inferred.sourceRepo,
|
|
93
111
|
sourcePrNumber: args.sourcePrNumber ?? inferred.sourcePrNumber,
|
|
94
112
|
sourcePrState,
|
|
95
113
|
sourcePrMergedAt: sourcePrMergedAt || undefined,
|
|
114
|
+
sourceAuthorEmail: sourceAuthorEmail || undefined,
|
|
115
|
+
sourceAuthorName: sourceAuthorName || undefined,
|
|
116
|
+
sourceAuthorLogin: sourceAuthorLogin || undefined,
|
|
96
117
|
};
|
|
97
118
|
}
|
|
98
119
|
|
|
@@ -229,6 +250,15 @@ export default defineAction({
|
|
|
229
250
|
.min(1)
|
|
230
251
|
.optional()
|
|
231
252
|
.describe("ISO timestamp for when the source pull request was merged."),
|
|
253
|
+
sourceAuthorEmail: sourceAuthorEmailSchema.describe(
|
|
254
|
+
"Email address for the human author of the source PR, used as the default human comment target for recap feedback.",
|
|
255
|
+
),
|
|
256
|
+
sourceAuthorName: sourceAuthorTextSchema.describe(
|
|
257
|
+
"Display name for the human author of the source PR.",
|
|
258
|
+
),
|
|
259
|
+
sourceAuthorLogin: sourceAuthorTextSchema.describe(
|
|
260
|
+
"GitHub login for the human author of the source PR.",
|
|
261
|
+
),
|
|
232
262
|
idempotencyKey: z
|
|
233
263
|
.string()
|
|
234
264
|
.trim()
|
|
@@ -328,6 +358,15 @@ export default defineAction({
|
|
|
328
358
|
...(sourceMetadata.sourcePrMergedAt !== undefined
|
|
329
359
|
? { sourcePrMergedAt: sourceMetadata.sourcePrMergedAt }
|
|
330
360
|
: {}),
|
|
361
|
+
...(sourceMetadata.sourceAuthorEmail !== undefined
|
|
362
|
+
? { sourceAuthorEmail: sourceMetadata.sourceAuthorEmail }
|
|
363
|
+
: {}),
|
|
364
|
+
...(sourceMetadata.sourceAuthorName !== undefined
|
|
365
|
+
? { sourceAuthorName: sourceMetadata.sourceAuthorName }
|
|
366
|
+
: {}),
|
|
367
|
+
...(sourceMetadata.sourceAuthorLogin !== undefined
|
|
368
|
+
? { sourceAuthorLogin: sourceMetadata.sourceAuthorLogin }
|
|
369
|
+
: {}),
|
|
331
370
|
...(idempotencyKey ? { recapIdempotencyKey: idempotencyKey } : {}),
|
|
332
371
|
};
|
|
333
372
|
if (Object.keys(planPatch).length > 0) {
|
|
@@ -77,6 +77,8 @@ export default defineAction({
|
|
|
77
77
|
hostedPlanId: schema.plans.hostedPlanId,
|
|
78
78
|
hostedPlanUrl: schema.plans.hostedPlanUrl,
|
|
79
79
|
sourceUrl: schema.plans.sourceUrl,
|
|
80
|
+
sourceAuthorName: schema.plans.sourceAuthorName,
|
|
81
|
+
sourceAuthorLogin: schema.plans.sourceAuthorLogin,
|
|
80
82
|
createdAt: schema.plans.createdAt,
|
|
81
83
|
updatedAt: schema.plans.updatedAt,
|
|
82
84
|
approvedAt: schema.plans.approvedAt,
|
|
@@ -152,6 +152,8 @@ export default defineAction({
|
|
|
152
152
|
hostedPlanId: schema.plans.hostedPlanId,
|
|
153
153
|
hostedPlanUrl: schema.plans.hostedPlanUrl,
|
|
154
154
|
sourceUrl: schema.plans.sourceUrl,
|
|
155
|
+
sourceAuthorName: schema.plans.sourceAuthorName,
|
|
156
|
+
sourceAuthorLogin: schema.plans.sourceAuthorLogin,
|
|
155
157
|
createdAt: schema.plans.createdAt,
|
|
156
158
|
updatedAt: schema.plans.updatedAt,
|
|
157
159
|
approvedAt: schema.plans.approvedAt,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type DomainMatchOrg,
|
|
27
27
|
} from "@agent-native/core/client/org";
|
|
28
28
|
import {
|
|
29
|
+
SOURCE_AUTHOR_COMMENT_MENTION_EMAIL,
|
|
29
30
|
extractCommentMentions,
|
|
30
31
|
formatPlanCommentAnchorForAgent,
|
|
31
32
|
formatPlanCommentMentionToken,
|
|
@@ -374,7 +375,7 @@ function readDesktopPlanAutoSync(planId: string | undefined): boolean {
|
|
|
374
375
|
|
|
375
376
|
type PlanAnnotationAnchor = PlanCommentAnchor & { x: number; y: number };
|
|
376
377
|
|
|
377
|
-
type CommentDraft = {
|
|
378
|
+
export type CommentDraft = {
|
|
378
379
|
message: string;
|
|
379
380
|
mentions: PlanCommentMention[];
|
|
380
381
|
resolutionTarget: PlanCommentResolutionTarget;
|
|
@@ -1245,6 +1246,110 @@ function displayNameForMention(email: string) {
|
|
|
1245
1246
|
return emailToName(email).replace(/\s+/g, " ").trim() || email;
|
|
1246
1247
|
}
|
|
1247
1248
|
|
|
1249
|
+
function elementForShortcutTarget(target: EventTarget | null) {
|
|
1250
|
+
if (target instanceof Element) return target;
|
|
1251
|
+
if (target instanceof Node) return target.parentElement;
|
|
1252
|
+
return null;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
export function isPlanCommentShortcutEditableTarget(
|
|
1256
|
+
target: EventTarget | null,
|
|
1257
|
+
) {
|
|
1258
|
+
const element = elementForShortcutTarget(target);
|
|
1259
|
+
if (!element) return false;
|
|
1260
|
+
return Boolean(
|
|
1261
|
+
element.closest(
|
|
1262
|
+
"input, textarea, select, [contenteditable]:not([contenteditable='false']), [role='textbox']",
|
|
1263
|
+
),
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
export function shouldHandlePlanCommentShortcut(
|
|
1268
|
+
event: Pick<
|
|
1269
|
+
KeyboardEvent,
|
|
1270
|
+
| "altKey"
|
|
1271
|
+
| "ctrlKey"
|
|
1272
|
+
| "defaultPrevented"
|
|
1273
|
+
| "key"
|
|
1274
|
+
| "metaKey"
|
|
1275
|
+
| "shiftKey"
|
|
1276
|
+
| "target"
|
|
1277
|
+
>,
|
|
1278
|
+
) {
|
|
1279
|
+
if (event.defaultPrevented) return false;
|
|
1280
|
+
const activeElement =
|
|
1281
|
+
typeof document === "undefined" ? null : document.activeElement;
|
|
1282
|
+
if (
|
|
1283
|
+
isPlanCommentShortcutEditableTarget(activeElement) ||
|
|
1284
|
+
isPlanCommentShortcutEditableTarget(event.target)
|
|
1285
|
+
) {
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
const key = event.key.toLowerCase();
|
|
1289
|
+
if (
|
|
1290
|
+
key === "c" &&
|
|
1291
|
+
!event.metaKey &&
|
|
1292
|
+
!event.ctrlKey &&
|
|
1293
|
+
!event.altKey &&
|
|
1294
|
+
!event.shiftKey
|
|
1295
|
+
) {
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
return (
|
|
1299
|
+
key === "m" &&
|
|
1300
|
+
event.metaKey &&
|
|
1301
|
+
event.shiftKey &&
|
|
1302
|
+
!event.ctrlKey &&
|
|
1303
|
+
!event.altKey
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
export function defaultInlineCommentDraftForPlanContext(input: {
|
|
1308
|
+
planKind?: PlanKind | null;
|
|
1309
|
+
ownerEmail?: string | null;
|
|
1310
|
+
sourceAuthorName?: string | null;
|
|
1311
|
+
sourceAuthorLogin?: string | null;
|
|
1312
|
+
accessRole?: NonNullable<PlanBundle["access"]>["role"] | null;
|
|
1313
|
+
currentEmail?: string | null;
|
|
1314
|
+
}): CommentDraft {
|
|
1315
|
+
const currentEmail = normalizeCommentEmail(input.currentEmail);
|
|
1316
|
+
if (input.planKind === "recap") {
|
|
1317
|
+
const targetLabel =
|
|
1318
|
+
input.sourceAuthorName?.trim() || input.sourceAuthorLogin?.trim();
|
|
1319
|
+
if (!targetLabel || input.accessRole === "owner") {
|
|
1320
|
+
return { message: "", mentions: [], resolutionTarget: "agent" };
|
|
1321
|
+
}
|
|
1322
|
+
const mention: PlanCommentMention = {
|
|
1323
|
+
email: SOURCE_AUTHOR_COMMENT_MENTION_EMAIL,
|
|
1324
|
+
label: targetLabel,
|
|
1325
|
+
role: "source-author",
|
|
1326
|
+
};
|
|
1327
|
+
return {
|
|
1328
|
+
message: `${formatPlanCommentMentionToken(mention)} `,
|
|
1329
|
+
mentions: [mention],
|
|
1330
|
+
resolutionTarget: "human",
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const targetEmail = normalizeCommentEmail(input.ownerEmail);
|
|
1335
|
+
if (
|
|
1336
|
+
!targetEmail ||
|
|
1337
|
+
input.accessRole === "owner" ||
|
|
1338
|
+
targetEmail === currentEmail
|
|
1339
|
+
) {
|
|
1340
|
+
return { message: "", mentions: [], resolutionTarget: "agent" };
|
|
1341
|
+
}
|
|
1342
|
+
const mention = {
|
|
1343
|
+
email: targetEmail,
|
|
1344
|
+
label: displayNameForMention(targetEmail),
|
|
1345
|
+
};
|
|
1346
|
+
return {
|
|
1347
|
+
message: `${formatPlanCommentMentionToken(mention)} `,
|
|
1348
|
+
mentions: [mention],
|
|
1349
|
+
resolutionTarget: "human",
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1248
1353
|
function safeDecodeURIComponent(value: string): string {
|
|
1249
1354
|
try {
|
|
1250
1355
|
return decodeURIComponent(value);
|
|
@@ -3282,25 +3387,22 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
|
|
|
3282
3387
|
bundle && (localPlanMode || session || canEditPlanContent),
|
|
3283
3388
|
);
|
|
3284
3389
|
const defaultInlineCommentDraft = useMemo<CommentDraft>(() => {
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
resolutionTarget: "human",
|
|
3302
|
-
};
|
|
3303
|
-
}, [bundle?.access?.ownerEmail, collabUser?.email, effectivePlanAccessRole]);
|
|
3390
|
+
return defaultInlineCommentDraftForPlanContext({
|
|
3391
|
+
planKind: bundle?.plan.kind,
|
|
3392
|
+
ownerEmail: bundle?.access?.ownerEmail,
|
|
3393
|
+
sourceAuthorName: bundle?.plan.sourceAuthorName,
|
|
3394
|
+
sourceAuthorLogin: bundle?.plan.sourceAuthorLogin,
|
|
3395
|
+
accessRole: effectivePlanAccessRole,
|
|
3396
|
+
currentEmail: collabUser?.email,
|
|
3397
|
+
});
|
|
3398
|
+
}, [
|
|
3399
|
+
bundle?.access?.ownerEmail,
|
|
3400
|
+
bundle?.plan.kind,
|
|
3401
|
+
bundle?.plan.sourceAuthorName,
|
|
3402
|
+
bundle?.plan.sourceAuthorLogin,
|
|
3403
|
+
collabUser?.email,
|
|
3404
|
+
effectivePlanAccessRole,
|
|
3405
|
+
]);
|
|
3304
3406
|
const commentThreads = useMemo(
|
|
3305
3407
|
() => buildCommentThreads(bundle?.comments ?? []),
|
|
3306
3408
|
[bundle?.comments],
|
|
@@ -4547,13 +4649,13 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
|
|
|
4547
4649
|
});
|
|
4548
4650
|
};
|
|
4549
4651
|
|
|
4550
|
-
const startCommenting = () => {
|
|
4652
|
+
const startCommenting = useCallback(() => {
|
|
4551
4653
|
setCanvasMarkupMode("none");
|
|
4552
4654
|
setActiveAnnotation(null);
|
|
4553
4655
|
setAnnotationsOpen(false);
|
|
4554
4656
|
setCommentVisibility("open");
|
|
4555
4657
|
setAnnotateMode(true);
|
|
4556
|
-
};
|
|
4658
|
+
}, []);
|
|
4557
4659
|
|
|
4558
4660
|
const selectReviewMode = (mode: CanvasMarkupMode) => {
|
|
4559
4661
|
preservePlanReaderScroll(() => {
|
|
@@ -4609,96 +4711,135 @@ export function PlansPage({ localPlanSlug }: { localPlanSlug?: string } = {}) {
|
|
|
4609
4711
|
scheduleNativeMarkerUpdate();
|
|
4610
4712
|
};
|
|
4611
4713
|
|
|
4612
|
-
const readNativeSelectionComment =
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
planTitle: bundle?.plan.title,
|
|
4656
|
-
}),
|
|
4657
|
-
snippet,
|
|
4658
|
-
textQuote: snippet,
|
|
4659
|
-
anchorKind: "text",
|
|
4660
|
-
tagName: "selection",
|
|
4661
|
-
blockType,
|
|
4662
|
-
...quoteContext,
|
|
4663
|
-
} satisfies PlanAnnotationAnchor;
|
|
4664
|
-
const toolbarWidth = 132;
|
|
4665
|
-
const toolbarLeft = clamp(
|
|
4666
|
-
pointX - toolbarWidth / 2,
|
|
4667
|
-
12,
|
|
4668
|
-
Math.max(12, readerRect.width - toolbarWidth - 12),
|
|
4669
|
-
);
|
|
4670
|
-
const toolbarTop = clamp(
|
|
4671
|
-
selectionRect.top - readerRect.top - 48,
|
|
4672
|
-
12,
|
|
4673
|
-
Math.max(12, readerRect.height - 48),
|
|
4674
|
-
);
|
|
4675
|
-
return {
|
|
4676
|
-
anchor,
|
|
4677
|
-
toolbarLeft,
|
|
4678
|
-
toolbarTop,
|
|
4679
|
-
position:
|
|
4680
|
-
getPositionFromAnchor(anchor) ??
|
|
4681
|
-
resolveInlineCommentPosition({
|
|
4714
|
+
const readNativeSelectionComment =
|
|
4715
|
+
useCallback((): NativeSelectionComment | null => {
|
|
4716
|
+
const reader = nativeReaderRef.current;
|
|
4717
|
+
const selection = window.getSelection();
|
|
4718
|
+
if (!reader || !selection || selection.rangeCount === 0) return null;
|
|
4719
|
+
if (selection.isCollapsed) return null;
|
|
4720
|
+
const textQuote = selection.toString().replace(/\s+/g, " ").trim();
|
|
4721
|
+
if (!textQuote) return null;
|
|
4722
|
+
const range = selection.getRangeAt(0);
|
|
4723
|
+
if (!reader.contains(range.commonAncestorContainer)) return null;
|
|
4724
|
+
|
|
4725
|
+
const rects = Array.from(range.getClientRects()).filter(
|
|
4726
|
+
(rect) => rect.width > 0 && rect.height > 0,
|
|
4727
|
+
);
|
|
4728
|
+
const selectionRect = rects[0] ?? range.getBoundingClientRect();
|
|
4729
|
+
if (selectionRect.width <= 0 || selectionRect.height <= 0) return null;
|
|
4730
|
+
|
|
4731
|
+
const readerRect = reader.getBoundingClientRect();
|
|
4732
|
+
const pointX =
|
|
4733
|
+
selectionRect.left + selectionRect.width / 2 - readerRect.left;
|
|
4734
|
+
const pointY =
|
|
4735
|
+
selectionRect.top + selectionRect.height / 2 - readerRect.top;
|
|
4736
|
+
const startElement =
|
|
4737
|
+
range.startContainer instanceof Element
|
|
4738
|
+
? range.startContainer
|
|
4739
|
+
: range.startContainer.parentElement;
|
|
4740
|
+
const blockElement =
|
|
4741
|
+
startElement?.closest<HTMLElement>("[data-block-id]");
|
|
4742
|
+
const blockType = blockElement?.dataset.blockId
|
|
4743
|
+
? findPlanBlockById(
|
|
4744
|
+
bundle?.plan.content?.blocks ?? [],
|
|
4745
|
+
blockElement.dataset.blockId,
|
|
4746
|
+
)?.type
|
|
4747
|
+
: undefined;
|
|
4748
|
+
const quoteContext = textQuoteContextForBlock({
|
|
4749
|
+
block: blockElement,
|
|
4750
|
+
quote: textQuote,
|
|
4751
|
+
});
|
|
4752
|
+
const snippet = textQuote.slice(0, 220);
|
|
4753
|
+
const anchor = {
|
|
4754
|
+
...buildNativeAnchorFromElement({
|
|
4755
|
+
reader,
|
|
4756
|
+
target: startElement instanceof HTMLElement ? startElement : reader,
|
|
4682
4757
|
pointX,
|
|
4683
4758
|
pointY,
|
|
4684
|
-
|
|
4685
|
-
viewportHeight: readerRect.height,
|
|
4759
|
+
planTitle: bundle?.plan.title,
|
|
4686
4760
|
}),
|
|
4687
|
-
|
|
4688
|
-
|
|
4761
|
+
snippet,
|
|
4762
|
+
textQuote: snippet,
|
|
4763
|
+
anchorKind: "text",
|
|
4764
|
+
tagName: "selection",
|
|
4765
|
+
blockType,
|
|
4766
|
+
...quoteContext,
|
|
4767
|
+
} satisfies PlanAnnotationAnchor;
|
|
4768
|
+
const toolbarWidth = 132;
|
|
4769
|
+
const toolbarLeft = clamp(
|
|
4770
|
+
pointX - toolbarWidth / 2,
|
|
4771
|
+
12,
|
|
4772
|
+
Math.max(12, readerRect.width - toolbarWidth - 12),
|
|
4773
|
+
);
|
|
4774
|
+
const toolbarTop = clamp(
|
|
4775
|
+
selectionRect.top - readerRect.top - 48,
|
|
4776
|
+
12,
|
|
4777
|
+
Math.max(12, readerRect.height - 48),
|
|
4778
|
+
);
|
|
4779
|
+
return {
|
|
4780
|
+
anchor,
|
|
4781
|
+
toolbarLeft,
|
|
4782
|
+
toolbarTop,
|
|
4783
|
+
position:
|
|
4784
|
+
getPositionFromAnchor(anchor) ??
|
|
4785
|
+
resolveInlineCommentPosition({
|
|
4786
|
+
pointX,
|
|
4787
|
+
pointY,
|
|
4788
|
+
viewportWidth: readerRect.width,
|
|
4789
|
+
viewportHeight: readerRect.height,
|
|
4790
|
+
}),
|
|
4791
|
+
};
|
|
4792
|
+
}, [
|
|
4793
|
+
bundle?.plan.content?.blocks,
|
|
4794
|
+
bundle?.plan.title,
|
|
4795
|
+
getPositionFromAnchor,
|
|
4796
|
+
]);
|
|
4797
|
+
|
|
4798
|
+
const openNativeSelectionComment = useCallback(
|
|
4799
|
+
(selectionComment: NativeSelectionComment) => {
|
|
4800
|
+
documentStateRef.current = readNativeDocumentState();
|
|
4801
|
+
setCanvasMarkupMode("none");
|
|
4802
|
+
setActiveAnnotation(null);
|
|
4803
|
+
setAnnotationsOpen(false);
|
|
4804
|
+
setCommentVisibility("open");
|
|
4805
|
+
setAnnotateMode(true);
|
|
4806
|
+
setPendingAnnotation(selectionComment.anchor);
|
|
4807
|
+
setInlineCommentPosition(selectionComment.position);
|
|
4808
|
+
setNativeSelectionComment(null);
|
|
4809
|
+
window.getSelection()?.removeAllRanges();
|
|
4810
|
+
},
|
|
4811
|
+
[readNativeDocumentState],
|
|
4812
|
+
);
|
|
4689
4813
|
|
|
4690
4814
|
const beginNativeSelectionComment = () => {
|
|
4691
4815
|
if (!nativeSelectionComment) return;
|
|
4692
|
-
|
|
4693
|
-
// Implicitly enter annotate mode when a selection comment is started
|
|
4694
|
-
// outside of review mode so the inline comment popover renders correctly.
|
|
4695
|
-
if (!annotateMode) setAnnotateMode(true);
|
|
4696
|
-
setPendingAnnotation(nativeSelectionComment.anchor);
|
|
4697
|
-
setInlineCommentPosition(nativeSelectionComment.position);
|
|
4698
|
-
setNativeSelectionComment(null);
|
|
4699
|
-
window.getSelection()?.removeAllRanges();
|
|
4816
|
+
openNativeSelectionComment(nativeSelectionComment);
|
|
4700
4817
|
};
|
|
4701
4818
|
|
|
4819
|
+
useEffect(() => {
|
|
4820
|
+
if (!bundle) return;
|
|
4821
|
+
const handleCommentShortcut = (event: KeyboardEvent) => {
|
|
4822
|
+
if (!shouldHandlePlanCommentShortcut(event)) return;
|
|
4823
|
+
event.preventDefault();
|
|
4824
|
+
preservePlanReaderScroll(() => {
|
|
4825
|
+
const selectionComment = readNativeSelectionComment();
|
|
4826
|
+
if (selectionComment) {
|
|
4827
|
+
openNativeSelectionComment(selectionComment);
|
|
4828
|
+
return;
|
|
4829
|
+
}
|
|
4830
|
+
startCommenting();
|
|
4831
|
+
});
|
|
4832
|
+
};
|
|
4833
|
+
window.addEventListener("keydown", handleCommentShortcut);
|
|
4834
|
+
return () => window.removeEventListener("keydown", handleCommentShortcut);
|
|
4835
|
+
}, [
|
|
4836
|
+
bundle,
|
|
4837
|
+
openNativeSelectionComment,
|
|
4838
|
+
preservePlanReaderScroll,
|
|
4839
|
+
readNativeSelectionComment,
|
|
4840
|
+
startCommenting,
|
|
4841
|
+
]);
|
|
4842
|
+
|
|
4702
4843
|
const handleNativeReaderPointerDown = (
|
|
4703
4844
|
event: PointerEvent<HTMLDivElement>,
|
|
4704
4845
|
) => {
|
|
@@ -9303,6 +9444,22 @@ function commentBodyText(message: string) {
|
|
|
9303
9444
|
.trim();
|
|
9304
9445
|
}
|
|
9305
9446
|
|
|
9447
|
+
export function canSubmitInlineCommentDraft(input: {
|
|
9448
|
+
draft: CommentDraft;
|
|
9449
|
+
isSubmitting?: boolean;
|
|
9450
|
+
lockToAgent?: boolean;
|
|
9451
|
+
}) {
|
|
9452
|
+
const needsHumanMention =
|
|
9453
|
+
!input.lockToAgent &&
|
|
9454
|
+
input.draft.resolutionTarget === "human" &&
|
|
9455
|
+
input.draft.mentions.length === 0;
|
|
9456
|
+
return (
|
|
9457
|
+
commentBodyText(input.draft.message).length > 0 &&
|
|
9458
|
+
!needsHumanMention &&
|
|
9459
|
+
!input.isSubmitting
|
|
9460
|
+
);
|
|
9461
|
+
}
|
|
9462
|
+
|
|
9306
9463
|
export function mentionQueryAtCaret(root: HTMLElement) {
|
|
9307
9464
|
const selection = window.getSelection();
|
|
9308
9465
|
if (!selection || selection.rangeCount === 0) return null;
|
|
@@ -9600,7 +9757,11 @@ function InlineCommentPopover({
|
|
|
9600
9757
|
mountedRef.current = false;
|
|
9601
9758
|
};
|
|
9602
9759
|
}, []);
|
|
9603
|
-
const canSubmit =
|
|
9760
|
+
const canSubmit = canSubmitInlineCommentDraft({
|
|
9761
|
+
draft,
|
|
9762
|
+
isSubmitting,
|
|
9763
|
+
lockToAgent,
|
|
9764
|
+
});
|
|
9604
9765
|
const submit = async () => {
|
|
9605
9766
|
if (!canSubmit) return;
|
|
9606
9767
|
setSubmitError(false);
|
|
@@ -9666,15 +9827,19 @@ function InlineCommentPopover({
|
|
|
9666
9827
|
autoFocus
|
|
9667
9828
|
onSubmitShortcut={submit}
|
|
9668
9829
|
onChange={(value) =>
|
|
9669
|
-
setDraft((current) =>
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9830
|
+
setDraft((current) => {
|
|
9831
|
+
const addedMention =
|
|
9832
|
+
value.mentions.length > current.mentions.length;
|
|
9833
|
+
return {
|
|
9834
|
+
...current,
|
|
9835
|
+
...value,
|
|
9836
|
+
resolutionTarget: resolverTouched
|
|
9837
|
+
? current.resolutionTarget
|
|
9838
|
+
: addedMention
|
|
9839
|
+
? "human"
|
|
9840
|
+
: current.resolutionTarget,
|
|
9841
|
+
};
|
|
9842
|
+
})
|
|
9678
9843
|
}
|
|
9679
9844
|
/>
|
|
9680
9845
|
<Button
|
|
@@ -59,6 +59,9 @@ export const plans = table("plans", {
|
|
|
59
59
|
sourcePrNumber: integer("source_pr_number"),
|
|
60
60
|
sourcePrState: text("source_pr_state"),
|
|
61
61
|
sourcePrMergedAt: text("source_pr_merged_at"),
|
|
62
|
+
sourceAuthorEmail: text("source_author_email"),
|
|
63
|
+
sourceAuthorName: text("source_author_name"),
|
|
64
|
+
sourceAuthorLogin: text("source_author_login"),
|
|
62
65
|
// Stable key used by PR Visual Recap publish retries to replace the recap
|
|
63
66
|
// created by an earlier attempt instead of creating duplicate recap rows.
|
|
64
67
|
recapIdempotencyKey: text("recap_idempotency_key"),
|
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
} from "@agent-native/core/server";
|
|
8
8
|
import { eq } from "drizzle-orm";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
SOURCE_AUTHOR_COMMENT_MENTION_EMAIL,
|
|
12
|
+
extractCommentMentions,
|
|
13
|
+
} from "../../shared/comment-context.js";
|
|
11
14
|
import type { PlanBundle, PlanComment } from "../../shared/types.js";
|
|
12
15
|
import { getDb, schema } from "../db/index.js";
|
|
13
16
|
|
|
@@ -29,6 +32,7 @@ function normalizeEmail(email: string | null | undefined): string | null {
|
|
|
29
32
|
|
|
30
33
|
function isSyntheticQaEmail(email: string): boolean {
|
|
31
34
|
const trimmed = email.trim().toLowerCase();
|
|
35
|
+
if (trimmed === SOURCE_AUTHOR_COMMENT_MENTION_EMAIL) return true;
|
|
32
36
|
const at = trimmed.lastIndexOf("@");
|
|
33
37
|
if (at <= 0) return false;
|
|
34
38
|
const local = trimmed.slice(0, at);
|
|
@@ -150,9 +154,11 @@ export function planCommentNotificationRecipients(input: {
|
|
|
150
154
|
comment: PlanComment;
|
|
151
155
|
comments: PlanComment[];
|
|
152
156
|
planOwnerEmail?: string | null;
|
|
157
|
+
sourceAuthorEmail?: string | null;
|
|
153
158
|
}): NotificationRecipient[] {
|
|
154
159
|
if (input.comment.createdBy !== "human") return [];
|
|
155
160
|
const actorEmail = normalizeEmail(input.comment.authorEmail);
|
|
161
|
+
const sourceAuthorEmail = normalizeEmail(input.sourceAuthorEmail);
|
|
156
162
|
const recipients = new Map<string, NotificationRecipient>();
|
|
157
163
|
const addRecipient = (
|
|
158
164
|
email: string | null | undefined,
|
|
@@ -166,12 +172,22 @@ export function planCommentNotificationRecipients(input: {
|
|
|
166
172
|
recipients.set(normalized, { email: normalized, reason });
|
|
167
173
|
};
|
|
168
174
|
|
|
169
|
-
addRecipient(input.planOwnerEmail, "plan-owner");
|
|
170
175
|
const mentionedPeople =
|
|
171
176
|
input.comment.mentions && input.comment.mentions.length > 0
|
|
172
177
|
? input.comment.mentions
|
|
173
178
|
: extractCommentMentions(input.comment.message);
|
|
179
|
+
const mentionsSourceAuthor = mentionedPeople.some(
|
|
180
|
+
(mention) =>
|
|
181
|
+
normalizeEmail(mention.email) === SOURCE_AUTHOR_COMMENT_MENTION_EMAIL,
|
|
182
|
+
);
|
|
183
|
+
if (!(mentionsSourceAuthor && sourceAuthorEmail)) {
|
|
184
|
+
addRecipient(input.planOwnerEmail, "plan-owner");
|
|
185
|
+
}
|
|
174
186
|
for (const mention of mentionedPeople) {
|
|
187
|
+
if (normalizeEmail(mention.email) === SOURCE_AUTHOR_COMMENT_MENTION_EMAIL) {
|
|
188
|
+
addRecipient(sourceAuthorEmail, "mention");
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
175
191
|
addRecipient(mention.email, "mention");
|
|
176
192
|
}
|
|
177
193
|
|
|
@@ -238,10 +254,12 @@ export async function notifyPlanCommentRecipients({
|
|
|
238
254
|
id: schema.plans.id,
|
|
239
255
|
title: schema.plans.title,
|
|
240
256
|
ownerEmail: schema.plans.ownerEmail,
|
|
257
|
+
sourceAuthorEmail: schema.plans.sourceAuthorEmail,
|
|
241
258
|
})
|
|
242
259
|
.from(schema.plans)
|
|
243
260
|
.where(eq(schema.plans.id, bundle.plan.id));
|
|
244
261
|
const planOwnerEmail = planRow?.ownerEmail ?? null;
|
|
262
|
+
const sourceAuthorEmail = planRow?.sourceAuthorEmail ?? null;
|
|
245
263
|
const planTitle = planRow?.title ?? bundle.plan.title;
|
|
246
264
|
const insertedById = new Map(
|
|
247
265
|
bundle.comments
|
|
@@ -261,6 +279,7 @@ export async function notifyPlanCommentRecipients({
|
|
|
261
279
|
comment,
|
|
262
280
|
comments: commentsForRecipients,
|
|
263
281
|
planOwnerEmail,
|
|
282
|
+
sourceAuthorEmail,
|
|
264
283
|
});
|
|
265
284
|
for (const recipient of recipients) {
|
|
266
285
|
try {
|