@agent-native/core 0.84.55 → 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 +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/recap.ts +146 -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/collab/routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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 {
|
|
@@ -766,6 +766,8 @@ export async function loadPlanBundle(planId: string): Promise<PlanBundle> {
|
|
|
766
766
|
hostedPlanId: plan.hostedPlanId,
|
|
767
767
|
hostedPlanUrl: plan.hostedPlanUrl,
|
|
768
768
|
sourceUrl: plan.sourceUrl,
|
|
769
|
+
sourceAuthorName: plan.sourceAuthorName,
|
|
770
|
+
sourceAuthorLogin: plan.sourceAuthorLogin,
|
|
769
771
|
html: plan.html,
|
|
770
772
|
markdown: plan.markdown,
|
|
771
773
|
content: parsePlanContent(plan.content),
|
|
@@ -819,6 +821,8 @@ export async function summarizePlans(
|
|
|
819
821
|
| "hostedPlanId"
|
|
820
822
|
| "hostedPlanUrl"
|
|
821
823
|
| "sourceUrl"
|
|
824
|
+
| "sourceAuthorName"
|
|
825
|
+
| "sourceAuthorLogin"
|
|
822
826
|
| "createdAt"
|
|
823
827
|
| "updatedAt"
|
|
824
828
|
| "approvedAt"
|
|
@@ -877,6 +881,8 @@ export async function summarizePlans(
|
|
|
877
881
|
hostedPlanId: plan.hostedPlanId,
|
|
878
882
|
hostedPlanUrl: plan.hostedPlanUrl,
|
|
879
883
|
sourceUrl: plan.sourceUrl,
|
|
884
|
+
sourceAuthorName: plan.sourceAuthorName,
|
|
885
|
+
sourceAuthorLogin: plan.sourceAuthorLogin,
|
|
880
886
|
createdAt: plan.createdAt,
|
|
881
887
|
updatedAt: plan.updatedAt,
|
|
882
888
|
approvedAt: plan.approvedAt,
|
|
@@ -314,6 +314,17 @@ CREATE INDEX IF NOT EXISTS plans_recap_pr_merged_idx ON plans(kind, source_type,
|
|
|
314
314
|
CREATE INDEX IF NOT EXISTS plans_source_pr_idx ON plans(source_repo, source_pr_number)`,
|
|
315
315
|
},
|
|
316
316
|
},
|
|
317
|
+
{
|
|
318
|
+
version: 33,
|
|
319
|
+
sql: {
|
|
320
|
+
postgres: `ALTER TABLE plans ADD COLUMN IF NOT EXISTS source_author_email TEXT;
|
|
321
|
+
ALTER TABLE plans ADD COLUMN IF NOT EXISTS source_author_name TEXT;
|
|
322
|
+
ALTER TABLE plans ADD COLUMN IF NOT EXISTS source_author_login TEXT`,
|
|
323
|
+
sqlite: `ALTER TABLE plans ADD COLUMN source_author_email TEXT;
|
|
324
|
+
ALTER TABLE plans ADD COLUMN source_author_name TEXT;
|
|
325
|
+
ALTER TABLE plans ADD COLUMN source_author_login TEXT`,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
317
328
|
],
|
|
318
329
|
{ table: "plans_migrations" },
|
|
319
330
|
);
|
|
@@ -98,6 +98,8 @@ export interface PlanSummary {
|
|
|
98
98
|
hostedPlanId?: string | null;
|
|
99
99
|
hostedPlanUrl?: string | null;
|
|
100
100
|
sourceUrl?: string | null;
|
|
101
|
+
sourceAuthorName?: string | null;
|
|
102
|
+
sourceAuthorLogin?: string | null;
|
|
101
103
|
createdAt: string;
|
|
102
104
|
updatedAt: string;
|
|
103
105
|
approvedAt?: string | null;
|
|
@@ -122,6 +124,8 @@ export interface Plan {
|
|
|
122
124
|
hostedPlanId?: string | null;
|
|
123
125
|
hostedPlanUrl?: string | null;
|
|
124
126
|
sourceUrl?: string | null;
|
|
127
|
+
sourceAuthorName?: string | null;
|
|
128
|
+
sourceAuthorLogin?: string | null;
|
|
125
129
|
html?: string | null;
|
|
126
130
|
markdown?: string | null;
|
|
127
131
|
content?: PlanContent | null;
|
package/dist/cli/recap.d.ts
CHANGED
|
@@ -241,6 +241,16 @@ type GitHubComment = {
|
|
|
241
241
|
type?: string | null;
|
|
242
242
|
} | null;
|
|
243
243
|
};
|
|
244
|
+
export declare function resolveGitHubPullRequestAuthor(input: {
|
|
245
|
+
token: string;
|
|
246
|
+
repo: string;
|
|
247
|
+
pr: string;
|
|
248
|
+
fetchFn?: typeof fetch;
|
|
249
|
+
}): Promise<{
|
|
250
|
+
email?: string;
|
|
251
|
+
name?: string;
|
|
252
|
+
login?: string;
|
|
253
|
+
}>;
|
|
244
254
|
export declare function isPullRequestHeadCurrent(input: {
|
|
245
255
|
token: string;
|
|
246
256
|
owner: string;
|
|
@@ -293,6 +303,7 @@ export declare function fetchRecapBlockReference(input: {
|
|
|
293
303
|
export declare function publishRecapSource(input: {
|
|
294
304
|
appUrl: string;
|
|
295
305
|
token: string;
|
|
306
|
+
githubToken?: string;
|
|
296
307
|
sourcePath?: string;
|
|
297
308
|
out?: string;
|
|
298
309
|
prevPlanId?: string;
|
|
@@ -304,6 +315,9 @@ export declare function publishRecapSource(input: {
|
|
|
304
315
|
sourcePrNumber?: string;
|
|
305
316
|
sourcePrState?: string;
|
|
306
317
|
sourcePrMergedAt?: string;
|
|
318
|
+
sourceAuthorEmail?: string;
|
|
319
|
+
sourceAuthorName?: string;
|
|
320
|
+
sourceAuthorLogin?: string;
|
|
307
321
|
fetchFn?: typeof fetch;
|
|
308
322
|
cwd?: string;
|
|
309
323
|
}): Promise<{
|