Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/mobile/src/features/review/ReviewSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ export function ReviewSheet(props: ReviewSheetProps) {
const environment = useEnvironmentPresentation(environmentId);
const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry");
const isEnvironmentReady = environment.presentation?.connection.phase === "connected";
const { draftMessage } = useThreadDraftForThread({ environmentId, threadId });
const { draftMessage, draftContext } = useThreadDraftForThread({ environmentId, threadId });
const reviewCache = useReviewCacheForThread({ environmentId, threadId });
const { selectedThreadCwd } = useSelectedThreadWorktree();
const columnMetrics = useNativeColumnLayoutMetrics();
Expand Down Expand Up @@ -475,6 +475,7 @@ export function ReviewSheet(props: ReviewSheetProps) {
cwd: selectedThreadCwd,
selectedSection,
draftMessage,
draftContext,
});
// Expo may return null before registration or in older binaries; use the fallback.
const NativeReviewDiffView = resolveNativeReviewDiffView();
Expand Down
72 changes: 72 additions & 0 deletions apps/mobile/src/features/review/reviewCommentSelection.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { describe, expect, it } from "vite-plus/test";
import { upgradeLegacyContextMessage } from "@t3tools/shared/composerContextLegacy";
import { formatComposerContextReference } from "@t3tools/shared/composerContextReferences";

import { pullRequestComposerContext } from "../../lib/composerContext";

import {
clearReviewCommentTarget,
Expand Down Expand Up @@ -50,6 +54,74 @@ function makeTarget(): ReviewCommentTarget {
}

describe("review comment serialization", () => {
it("excludes a linked pull request chip from draft review comments", () => {
const record = pullRequestComposerContext(
{
number: 42,
title: "Fix checkout",
url: "https://github.com/example/repo/pull/42",
headBranch: "fix-checkout",
baseBranch: "main",
state: "open",
isDraft: false,
},
"pr-42",
);

expect(
parseReviewInlineComments(formatComposerContextReference(record), {
version: 1,
records: [record],
}),
).toEqual([]);
});

it("reads an added review comment after the composer upgrades it to a context chip", () => {
const upgraded = upgradeLegacyContextMessage(
formatReviewCommentContext(makeTarget(), "Please keep this configurable."),
);
const comments = parseReviewInlineComments(upgraded.text, {
version: 1,
records: upgraded.records,
});

expect(comments).toEqual([
{
id: upgraded.records[0]!.contextId,
sectionId: "section-1",
sectionTitle: "Working tree",
filePath: "apps/demo/src/main.ts",
startIndex: 0,
endIndex: 1,
rangeLabel: "7",
text: "Please keep this configurable.",
diff: "@@ -7,1 +7,1 @@\n-const retryLimit = 2;\n+const retryLimit = 4;",
fenceLanguage: "diff",
},
]);
});

it("keeps legacy comments and counts a typed comment only while its chip is present", () => {
const upgraded = upgradeLegacyContextMessage(
formatReviewCommentContext(makeTarget(), "Typed comment"),
);
const context = { version: 1, records: upgraded.records } as const;
const legacy = formatReviewCommentContext(makeTarget(), "Legacy comment");
const comments = parseReviewInlineComments(
`${legacy}\n${upgraded.text}\n${upgraded.text}`,
context,
);

expect(comments.map((comment) => comment.text)).toEqual(["Legacy comment", "Typed comment"]);
expect(parseReviewInlineComments(legacy, context).map((comment) => comment.text)).toEqual([
"Legacy comment",
]);
expect(parseReviewInlineComments("", context)).toEqual([]);
expect(
parseReviewInlineComments(upgraded.text.replace("/review-comment/", "/mention/"), context),
).toEqual([]);
});

it("keeps closing-tag text inside a chip label within a real review body", () => {
const body = "Before [</review_comment>](t3-context://v1/mention/context-1) after";
const serialized = `<review_comment sectionId="s" filePath="app.ts" startIndex="0" endIndex="0">${body}</review_comment>`;
Expand Down
42 changes: 36 additions & 6 deletions apps/mobile/src/features/review/reviewCommentSelection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useSyncExternalStore } from "react";
import { replaceComposerContextReferences } from "@t3tools/shared/composerContextReferences";
import type { OrchestrationMessageContext } from "@t3tools/contracts";
import {
collectComposerContextReferences,
replaceComposerContextReferences,
} from "@t3tools/shared/composerContextReferences";

import type { ReviewRenderableLineRow } from "./reviewModel";

Expand Down Expand Up @@ -357,11 +361,10 @@ export function formatReviewCommentContext(target: ReviewCommentTarget, comment:
].join("\n");
}

export function countReviewCommentContexts(value: string): number {
return Array.from(value.matchAll(/<review_comment\b/g)).length;
}

export function parseReviewInlineComments(value: string): ReadonlyArray<ReviewInlineComment> {
export function parseReviewInlineComments(
value: string,
context?: OrchestrationMessageContext,
): ReadonlyArray<ReviewInlineComment> {
const comments: ReviewInlineComment[] = [];
// Mask chip delimiters: labels containing </review_comment> would otherwise truncate the block.
const masked = replaceComposerContextReferences(value, (reference) =>
Expand All @@ -385,6 +388,33 @@ export function parseReviewInlineComments(value: string): ReadonlyArray<ReviewIn

comments.push(comment);
}
const referencedIds = new Set(
collectComposerContextReferences(value)
.filter((reference) => reference.kind === "review-comment")
.map((reference) => reference.contextId),
);
for (const record of context?.records ?? []) {
if (
record.kind !== "review-comment" ||
"payload" in record ||
record.sectionId.startsWith("pull-request:") ||
!referencedIds.has(record.contextId)
) {
continue;
}
comments.push({
id: record.contextId,
sectionId: record.sectionId,
sectionTitle: record.sectionTitle,
filePath: record.filePath,
startIndex: record.startIndex,
endIndex: record.endIndex,
rangeLabel: record.rangeLabel,
text: record.text,
diff: record.diff,
...(record.fenceLanguage !== undefined ? { fenceLanguage: record.fenceLanguage } : {}),
});
}
return comments;
}

Expand Down
10 changes: 6 additions & 4 deletions apps/mobile/src/features/review/useReviewDiffData.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { formatDiffCount } from "@t3tools/client-runtime/diff-count";

import { countReviewCommentContexts, parseReviewInlineComments } from "./reviewCommentSelection";
import { parseReviewInlineComments } from "./reviewCommentSelection";
import { getCachedNativeReviewDiffData } from "./nativeReviewDiffAdapter";
import { getCachedReviewParsedDiff } from "./reviewState";
import {
Expand All @@ -14,6 +14,7 @@ import {

import type {
EnvironmentId,
OrchestrationMessageContext,
ReviewDiffFileStat,
ReviewDiffPreviewSource,
} from "@t3tools/contracts";
Expand Down Expand Up @@ -126,8 +127,9 @@ export function useReviewDiffData(input: {
readonly cwd: string | null;
readonly selectedSection: ReviewSectionItem | null;
readonly draftMessage: string;
readonly draftContext: OrchestrationMessageContext | undefined;
}) {
const { draftMessage, selectedSection, threadKey } = input;
const { draftMessage, draftContext, selectedSection, threadKey } = input;
const selectedSectionId = selectedSection?.id ?? null;
const source = selectedSection?.source;
const lazySource = source?.truncated && source.files ? source : null;
Expand Down Expand Up @@ -230,7 +232,7 @@ export function useReviewDiffData(input: {
() => formatHeaderDiffSummary(parsedDiff, selectedSection?.files),
[parsedDiff, selectedSection?.files],
);
const inlineReviewComments = parseReviewInlineComments(draftMessage);
const inlineReviewComments = parseReviewInlineComments(draftMessage, draftContext);
const selectedSectionInlineComments = useMemo(() => {
if (!selectedSectionId || inlineReviewComments.length === 0) {
return EMPTY_INLINE_REVIEW_COMMENTS;
Expand All @@ -241,7 +243,7 @@ export function useReviewDiffData(input: {
parsedDiff,
comments: selectedSectionInlineComments,
});
const pendingReviewCommentCount = countReviewCommentContexts(draftMessage);
const pendingReviewCommentCount = inlineReviewComments.length;

// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- records review perf marks and diagnostic logs once a parsed diff has committed
useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/state/use-thread-composer-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export function useThreadDraftForThread(input: {

return {
draftMessage: draft.text,
draftContext: draft.context,
draftAttachments: draft.attachments,
};
}
Expand Down
Loading