From 0e074c6d93bc734af79caf85a8d7c9db8b0ea7f8 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 00:32:12 -0500 Subject: [PATCH 1/2] fix(mobile): show draft review comments inline and in the count Read referenced typed draft context alongside legacy review blocks, and derive the pending count from those same comments. Co-Authored-By: GPT-6.1 Sol --- .../src/features/review/ReviewSheet.tsx | 3 +- .../review/reviewCommentSelection.test.ts | 47 +++++++++++++++++++ .../features/review/reviewCommentSelection.ts | 41 +++++++++++++--- .../src/features/review/useReviewDiffData.ts | 15 +++--- .../src/state/use-thread-composer-state.ts | 1 + 5 files changed, 92 insertions(+), 15 deletions(-) diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index 5660301371d7d..6647a885df97c 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -448,7 +448,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(); @@ -493,6 +493,7 @@ export function ReviewSheet(props: ReviewSheetProps) { selectedSection, revision: diffPreviewRevision, draftMessage, + draftContext, }); // Resolution returns null while Expo registers the native view (or forever // when the binary lacks it). Rendering a null component type crashes the diff --git a/apps/mobile/src/features/review/reviewCommentSelection.test.ts b/apps/mobile/src/features/review/reviewCommentSelection.test.ts index a5a9503b90466..abb164aaf17d1 100644 --- a/apps/mobile/src/features/review/reviewCommentSelection.test.ts +++ b/apps/mobile/src/features/review/reviewCommentSelection.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vite-plus/test"; +import { upgradeLegacyContextMessage } from "@t3tools/shared/composerContextLegacy"; import { formatReviewCommentContext, @@ -42,6 +43,52 @@ function makeTarget(): ReviewCommentTarget { } describe("review comment serialization", () => { + 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 [](t3-context://v1/mention/context-1) after"; const serialized = `${body}`; diff --git a/apps/mobile/src/features/review/reviewCommentSelection.ts b/apps/mobile/src/features/review/reviewCommentSelection.ts index b4ff47f0898a1..c5920f0a55d3c 100644 --- a/apps/mobile/src/features/review/reviewCommentSelection.ts +++ b/apps/mobile/src/features/review/reviewCommentSelection.ts @@ -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"; @@ -264,11 +268,10 @@ export function formatReviewCommentContext(target: ReviewCommentTarget, comment: ].join("\n"); } -export function countReviewCommentContexts(value: string): number { - return Array.from(value.matchAll(/ { +export function parseReviewInlineComments( + value: string, + context?: OrchestrationMessageContext, +): ReadonlyArray { const comments: ReviewInlineComment[] = []; // Match on masked delimiters, as `parseReviewCommentMessageSegments` does: a chip label may // contain ``, which would otherwise end the block early and truncate it. @@ -293,6 +296,32 @@ export function parseReviewInlineComments(value: string): ReadonlyArray reference.kind === "review-comment") + .map((reference) => reference.contextId), + ); + for (const record of context?.records ?? []) { + if ( + record.kind !== "review-comment" || + "payload" in record || + !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; } diff --git a/apps/mobile/src/features/review/useReviewDiffData.ts b/apps/mobile/src/features/review/useReviewDiffData.ts index 7ffd694077f70..7607620b227d2 100644 --- a/apps/mobile/src/features/review/useReviewDiffData.ts +++ b/apps/mobile/src/features/review/useReviewDiffData.ts @@ -1,7 +1,7 @@ import { useCallback, useContext, useEffect, useMemo, useRef, 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 { markReviewEvent, measureReviewWork } from "./reviewPerf"; import { getCachedReviewParsedDiff } from "./reviewState"; @@ -15,6 +15,7 @@ import { import type { EnvironmentId, + OrchestrationMessageContext, ReviewDiffFileStat, ReviewDiffPreviewSource, } from "@t3tools/contracts"; @@ -128,8 +129,9 @@ export function useReviewDiffData(input: { readonly selectedSection: ReviewSectionItem | null; readonly revision: string | undefined; 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; @@ -255,8 +257,8 @@ export function useReviewDiffData(input: { [parsedDiff, selectedSection?.files], ); const inlineReviewComments = useMemo( - () => parseReviewInlineComments(draftMessage), - [draftMessage], + () => parseReviewInlineComments(draftMessage, draftContext), + [draftMessage, draftContext], ); const selectedSectionInlineComments = useMemo(() => { if (!selectedSectionId || inlineReviewComments.length === 0) { @@ -274,10 +276,7 @@ export function useReviewDiffData(input: { ), [parsedDiff, selectedSectionInlineComments], ); - const pendingReviewCommentCount = useMemo( - () => countReviewCommentContexts(draftMessage), - [draftMessage], - ); + const pendingReviewCommentCount = inlineReviewComments.length; useEffect(() => { if (parsedDiff.kind !== "files") { diff --git a/apps/mobile/src/state/use-thread-composer-state.ts b/apps/mobile/src/state/use-thread-composer-state.ts index ebdf9769d46fe..c442e20617a2f 100644 --- a/apps/mobile/src/state/use-thread-composer-state.ts +++ b/apps/mobile/src/state/use-thread-composer-state.ts @@ -174,6 +174,7 @@ export function useThreadDraftForThread(input: { return { draftMessage: draft.text, + draftContext: draft.context, draftAttachments: draft.attachments, }; } From 0967dee5869b4e7552c6432930c60d1f1f9d69ae Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 03:21:02 -0500 Subject: [PATCH 2/2] fix(mobile): exclude pull request chips from draft comments Co-Authored-By: GPT-6.1 Sol --- .../review/reviewCommentSelection.test.ts | 25 +++++++++++++++++++ .../features/review/reviewCommentSelection.ts | 1 + 2 files changed, 26 insertions(+) diff --git a/apps/mobile/src/features/review/reviewCommentSelection.test.ts b/apps/mobile/src/features/review/reviewCommentSelection.test.ts index abb164aaf17d1..f6f8d337719f0 100644 --- a/apps/mobile/src/features/review/reviewCommentSelection.test.ts +++ b/apps/mobile/src/features/review/reviewCommentSelection.test.ts @@ -1,5 +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 { formatReviewCommentContext, @@ -43,6 +46,28 @@ 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."), diff --git a/apps/mobile/src/features/review/reviewCommentSelection.ts b/apps/mobile/src/features/review/reviewCommentSelection.ts index c5920f0a55d3c..3d750fabbe644 100644 --- a/apps/mobile/src/features/review/reviewCommentSelection.ts +++ b/apps/mobile/src/features/review/reviewCommentSelection.ts @@ -305,6 +305,7 @@ export function parseReviewInlineComments( if ( record.kind !== "review-comment" || "payload" in record || + record.sectionId.startsWith("pull-request:") || !referencedIds.has(record.contextId) ) { continue;