Skip to content

feat(web): cite assistant responses with inline citations - #9146

Merged
juliusmarminge merged 4 commits into
mainfrom
assistant-response-citations
Sep 2, 2026
Merged

feat(web): cite assistant responses with inline citations#9146
juliusmarminge merged 4 commits into
mainfrom
assistant-response-citations

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Sep 2, 2026

Copy link
Copy Markdown
Member

Quoting part of an assistant response meant copying text that lost all connection to where it came from. This adds citations: select text in an assistant response, choose Cite in composer, and an inline quote chip lands at your cursor — with an optional comment bubble anchored right at the selected passage. Clicking a chip (in the draft or a sent message) navigates to the source response, smooth-scrolls it into view — loading older history and unfolding turns when needed — and pulses a highlight on the exact quoted text that holds for a moment before fading.

How it works:

  • Citations serialize as self-contained t3-citation:// Markdown links carrying the quote, source IDs, offsets, context, and optional comment — no new tables, migrations, or sidecar draft state. They survive drafts, stashes, copy/paste, reloads, and sending.
  • The composer treats them as atomic inline chips (Lexical decorator nodes) alongside mentions and skills; comments edit as one undoable change via the chip's pencil.
  • ProviderService.sendTurn expands citations into readable quote data for every adapter (quote marked as reference material, comment as user-authored), while persisted messages keep the clickable links. Title/branch generation and stash previews see plain text.
  • Source matching normalizes whitespace and uses surrounding context, so quotes still resolve after edits and never guess between ambiguous repeats.
  • The terminal's "select text to copy" menu and the new citation menu share one release-triggered selection observer with consistent positioning.
  • Mobile renders sent quotes as readable blockquotes with the comment below; it doesn't create citations. Docs in docs/user/composer.md and docs/internals/assistant-citations.md.

Demo

Selection → cite → comment bubble at the selection → chip in composer → click-to-source with the pulse-and-hold highlight:

https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/35ecd10e3b53aa45/citation-demo.mp4

Built with Claude Fable 5 (Codex CLI + Claude Code).

🤖 Generated with Claude Code


Note

Medium Risk
Changes the provider turn input pipeline (citation expansion and re-validation against send limits) and orchestration title/branch inputs, though malformed citations pass through and expansion is covered by tests.

Overview
Users can quote assistant messages end-to-end: select text in the web timeline, insert an inline citation chip in the Lexical composer (optional comment, paste/copy/undo-safe), and follow chips back to the source message with history loading, turn expansion, scroll, and a temporary highlight.

Citations are stored as t3-citation:// markdown links in message text and drafts. Server-side, ProviderService.sendTurn expands them into numbered quote placeholders plus a structured assistant_citations JSON block for adapters while keeping the canonical links in persisted messages; title/branch generation and stash previews use plain-text citation content. Mobile only renders citations as readable text in the feed.

Chat markdown renders citation links as chips; composer submission validates both encoded and expanded prompt length. The terminal drawer reuses a shared observeSelectionActions helper for selection menus (replacing bespoke mouseup timing).

Reviewed by Cursor Bugbot for commit 393a909. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add inline citations for assistant responses with selection, composer chips, and provider expansion

  • Users select assistant text in the timeline to create a citation chip in the composer; chips support comments, editing, removal, and persistence across drafts and clipboard copy
  • Citations encode as version-1 t3-citation URIs with bounded identifiers, quote text, offsets, and context; parseAssistantCitationHref and collectAssistantCitations in assistantCitations.ts validate and collect them while leaving malformed links untouched
  • expandAssistantCitationsForProvider replaces serialized citation links with numbered quote references plus a structured assistant_citations context block; ProviderService.sendTurn in ProviderService.ts runs expansion before adapter dispatch and re-validates expanded input against the provider limit
  • Citation navigation in useAssistantCitationTarget and MessagesTimeline loads missing history, expands collapsed turns, scrolls to the source, highlights it with CSS custom highlights, and warns on unavailable sources; title and branch-name generation in ProviderCommandReactor.ts now receive plain-text citation content instead of serialized markup
  • Mobile renders citations as readable text only via renderAssistantCitationsAsText in ThreadFeed.tsx; terminal selection handling in ThreadTerminalDrawer.tsx moves to the shared observeSelectionActions observer with ownership-aware menu dismissal
  • Risk: getComposerPromptLengthValidationMessage in composerSubmission.ts now rejects drafts when either serialized or expanded citation text exceeds the provider limit; CHAT_MARKDOWN_SANITIZE_SCHEMA in ChatMarkdown.tsx now permits the t3-citation protocol in hrefs

Macroscope summarized 393a909.

Select text in an assistant response and choose "Cite in composer" to
insert an inline quote chip at the cursor, with an optional comment
bubble anchored at the selected text. Chips carry the quote, its source
IDs, and the comment through drafts, stashes, copy/paste, and sending.
Clicking a chip navigates to the source response, smooth-scrolls it
into view (loading older history when needed), and pulses a highlight
on the exact quoted passage that holds briefly before fading.

Provider dispatch expands citations into readable quote data for every
adapter while persisted messages keep the clickable links; titles,
branch names, and stash previews see plain text. Mobile renders sent
quotes as blockquotes. The terminal and assistant selection menus share
one release-triggered gesture observer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Sep 2, 2026
const hashIndex = href.indexOf("#");
if (hashIndex === -1) return null;
const hash = href.slice(hashIndex + 1);
if (!hash.startsWith(CITATION_HASH_PREFIX) || hash.length > 140_000) return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium lib/assistantCitationNavigation.ts:28

Valid maximum-size citations are rejected when the encoded hash exceeds 140,000 characters, so navigation reaches the destination but the citation cannot be decoded or highlighted. Because the 8,000-unit quote/comment limits can produce a percent-encoded href over 105 KB and Base64url expands it further, raise or remove this cap (while enforcing the citation limits on the decoded value).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/assistantCitationNavigation.ts around line 28:

Valid maximum-size citations are rejected when the encoded hash exceeds 140,000 characters, so navigation reaches the destination but the citation cannot be decoded or highlighted. Because the 8,000-unit quote/comment limits can produce a percent-encoded href over 105 KB and Base64url expands it further, raise or remove this cap (while enforcing the citation limits on the decoded value).

setReady(null);
onManualNavigation();
}
if (!viewport || historyLoading) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium chat/useAssistantCitationTarget.ts:78

Citations whose source message is missing never complete when the thread is empty: MessagesTimeline does not mount a viewport, so useAssistantCitationTarget returns before the missing-source branch runs, leaving positioning true indefinitely without showing the unavailable-source warning. Perform the history/source lookup without requiring viewport, and only gate list positioning on the viewport being available.

Suggested change
if (!viewport || historyLoading) return;
if (historyLoading) return;
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/useAssistantCitationTarget.ts around line 78:

Citations whose source message is missing never complete when the thread is empty: `MessagesTimeline` does not mount a viewport, so `useAssistantCitationTarget` returns before the missing-source branch runs, leaving `positioning` true indefinitely without showing the unavailable-source warning. Perform the history/source lookup without requiring `viewport`, and only gate list positioning on the viewport being available.

Comment on lines +35 to +42
<textarea
ref={inputRef}
aria-label="Comment on selected text"
aria-description="Enter to save the citation comment; Shift+Enter for a new line."
aria-invalid={commentTooLong || undefined}
placeholder="Add an optional comment..."
rows={2}
className="field-sizing-content block max-h-40 min-h-16 w-full resize-none bg-transparent px-1 py-1.5 text-base outline-none placeholder:text-muted-foreground sm:text-sm"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This raw <textarea> reconstructs the shared Textarea primitive (field-sizing-content, min-h, text-base sm:text-sm, placeholder color, outline-none) but loses its contract: aria-invalid gets no visual treatment and disabled/focus styling is not shared, so the over-limit state is conveyed only by the helper text. The repo already has a borderless inline comment box built on the primitive — DiffCommentAnnotation uses <Textarea unstyled size="sm" className=... /> and overrides geometry through [&_[data-slot=textarea]]:….

Consider rendering the shared Textarea with unstyled and the same class overrides instead of a hand-rolled control.

Posted via Macroscope — UI Consistency

}
>
<QuoteIcon aria-hidden="true" className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME} />
<span className={cn(COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME, "max-w-[16em]")}>{label}</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label always uses the composer label class, which includes select-none, even when the chip renders read-only in a sent message (ChatMarkdown passes no onRemove). Every other chat-rendered chip keeps its label selectable — FileTagChipContent switches to CHAT_INLINE_CHIP_LABEL_CLASS_NAME via its selectable flag, and SkillInlineText uses it directly — so users can select and copy message text (and serializeRenderedMarkdownFragment can pick the chip up from a real selection). Here the quote label drops out of any selection dragged across the message.

Consider mirroring the FileTagChip pattern: import CHAT_INLINE_CHIP_LABEL_CLASS_NAME and pick it when onRemove is undefined, e.g. cn(onRemove ? COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME : CHAT_INLINE_CHIP_LABEL_CLASS_NAME, "max-w-[16em]"). The unconditional contentEditable={false} on the wrapper (line 82) is likewise only needed for the composer variant — ComposerCitationNode's decorator wrapper already sets it.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/AssistantSelectionToolbar.tsx Outdated
const { message } = entry;
const isUser = message.role === "user";
const renderedText = message.text;
const renderedText = renderAssistantCitationsAsText(message.text);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mobile renders citations for all messages unnecessarily

Low Severity

renderAssistantCitationsAsText is called unconditionally for every message (both user and assistant roles) at the top of the message rendering block. Assistant messages never contain [Assistant quote](t3-citation://...) links — citations are only inserted into user messages via the composer. For assistant messages, this runs a regex scan across potentially large text for no reason. Gating the call behind isUser avoids unnecessary work on every assistant message render.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 22c1ce8. Configure here.

Comment thread packages/shared/src/assistantCitations.ts
@macroscopeapp

macroscopeapp Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This XXL change adds a new cross-platform citation workflow with persistent composer data, virtualized source navigation, provider prompt expansion, and changes to existing terminal selection behavior. The supplied unresolved Medium findings also identify citation-size and unavailable-source handling risks that merit human review.

Not approved because:

  • 3 blocking correctness issues found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@maria-rcks

Copy link
Copy Markdown
Collaborator

Note

Written by gpt-5.6-sol on behalf of Maria

Refined the citation controls in 0562c44b5:

  • replaced the oversized selection menu with a compact Cite action
  • kept the whole composer citation clickable for source navigation
  • removed the composer tooltip and fill change on hover
  • made the composer edit and remove controls use the primary blue

Light mode

Citation controls in light mode

Dark mode

Citation controls in dark mode

Verified with 126 focused tests, web typecheck, targeted lint, formatting, and direct light/dark browser checks. Hovering a composer citation shows no tooltip, and selecting the quote or label still opens the source highlight.

Comment thread docs/user/composer.md
The quoted text and comment count toward the message limit.

Select a chip in the composer or a sent message to open the source thread, scroll to the response,
and highlight the quoted passage — including in older history. The

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium user/composer.md:71

This documentation promises that citations navigate to responses in older history, but useAssistantCitationTarget stops after requestedPages.size >= 20 and reports “Could not load the cited response” even when loadEarlier has more pages, so citations more than 20 history pages back cannot be opened as documented. Qualify or remove the “including in older history” claim.

Suggested change
and highlight the quoted passage — including in older history. The
and highlight the quoted passage when the source response is available to the citation navigator. The
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @docs/user/composer.md around line 71:

This documentation promises that citations navigate to responses in older history, but `useAssistantCitationTarget` stops after `requestedPages.size >= 20` and reports “Could not load the cited response” even when `loadEarlier` has more pages, so citations more than 20 history pages back cannot be opened as documented. Qualify or remove the “including in older history” claim.

Comment thread docs/user/composer.md
## Quote an assistant response

On web and desktop, select text in an assistant response, then choose **Cite in composer** from the
menu that appears when you release the selection. This inserts an inline quote chip at your cursor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low user/composer.md:58

Users cannot find the documented Cite in composer control because AssistantSelectionToolbar renders a Cite button, not a menu item with that visible label. Update the instruction to name the visible Cite button (its accessible label is Cite selection in composer).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @docs/user/composer.md around line 58:

Users cannot find the documented **Cite in composer** control because `AssistantSelectionToolbar` renders a **Cite** button, not a menu item with that visible label. Update the instruction to name the visible **Cite** button (its accessible label is `Cite selection in composer`).

juliusmarminge and others added 2 commits September 1, 2026 20:48
Raise the highlight from 28% to 45% of the primary color for both the
navigation pulse-and-hold and the comment-editing highlight, so the
quoted passage is visible without knowing where to look.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tations

# Conflicts:
#	docs/user/composer.md
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.5 KiB 13.5 KiB 0 B (0.0%) 15.1 KiB
Codex Thread snapshot wire 6.9 KiB 6.9 KiB +7 B (+0.1%) 7.3 KiB
Codex Live turn WebSocket wire 6.6 KiB 6.6 KiB −7 B (−0.1%) 7.8 KiB
Codex Live turn WebSocket decoded 57.0 KiB 57.0 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 10 10 0 (0.0%) 21
Claude Total thread wire 13.3 KiB 13.3 KiB +33 B (+0.2%) 15.1 KiB
Claude Thread snapshot wire 6.9 KiB 6.9 KiB +9 B (+0.1%) 7.3 KiB
Claude Live turn WebSocket wire 6.4 KiB 6.4 KiB +24 B (+0.4%) 7.8 KiB
Claude Live turn WebSocket decoded 56.4 KiB 56.4 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 10 10 0 (0.0%) 21

Baseline: 9fdafdf · PR result: 393a909 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.1 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@juliusmarminge
juliusmarminge merged commit e7deb2a into main Sep 2, 2026
26 of 27 checks passed
@juliusmarminge
juliusmarminge deleted the assistant-response-citations branch September 2, 2026 03:55
Comment on lines +252 to +255
return (
match.index +
(whitespace && normalizedOffset > offset ? match[0].length : normalizedOffset - offset)
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium lib/assistantTextSelection.ts:252

resolveAssistantCitationRange expands a citation ending at the normalized boundary of a multi-character whitespace run to the end of that raw run, so a selection such as "x " in "a x y" highlights "x " instead of the captured text. rawTextOffset treats every offset after the whitespace token's start as a request for the raw token end; at the normalized token boundary it should advance by only the normalized offset (one space), not by match[0].length.

-        match.index +
-        (whitespace && normalizedOffset > offset ? match[0].length : normalizedOffset - offset)
+        match.index + normalizedOffset - offset
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/assistantTextSelection.ts around lines 252-255:

`resolveAssistantCitationRange` expands a citation ending at the normalized boundary of a multi-character whitespace run to the end of that raw run, so a selection such as `"x "` in `"a x  y"` highlights `"x  "` instead of the captured text. `rawTextOffset` treats every offset after the whitespace token's start as a request for the raw token end; at the normalized token boundary it should advance by only the normalized offset (one space), not by `match[0].length`.

Comment on lines 759 to 763
attachmentPathLines.length === 0
? parsed.input
: [parsed.input, attachmentPathLines.join("\n")]
? inputTextWithCitations
: [inputTextWithCitations, attachmentPathLines.join("\n")]
.filter((part): part is string => typeof part === "string" && part.length > 0)
.join("\n\n");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High Layers/ProviderService.ts:759

adapter.sendTurn receives inputTextWithAttachmentPaths without validating its final length, so expanded citation text at the 120,000-character limit can become an oversized prompt when resolvable attachment path lines are appended. Validate the joined inputTextWithAttachmentPaths after constructing it.

Suggested change
attachmentPathLines.length === 0
? parsed.input
: [parsed.input, attachmentPathLines.join("\n")]
? inputTextWithCitations
: [inputTextWithCitations, attachmentPathLines.join("\n")]
.filter((part): part is string => typeof part === "string" && part.length > 0)
.join("\n\n");
const inputTextWithAttachmentPaths =
attachmentPathLines.length === 0
? inputTextWithCitations
: [inputTextWithCitations, attachmentPathLines.join("\n")]
.filter((part): part is string => typeof part === "string" && part.length > 0)
.join("\n\n");
yield* decodeInputOrValidationError({
operation: "ProviderService.sendTurn",
schema: ProviderSendTurnInput.fields.input,
payload: inputTextWithAttachmentPaths,
});
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/provider/Layers/ProviderService.ts around lines 759-763:

`adapter.sendTurn` receives `inputTextWithAttachmentPaths` without validating its final length, so expanded citation text at the 120,000-character limit can become an oversized prompt when resolvable attachment path lines are appended. Validate the joined `inputTextWithAttachmentPaths` after constructing it.

Comment on lines +144 to +167
let separator = false;

const visit = (node: Node) => {
if (node.nodeType === 3) {
const text = node as Text;
if (text.length === 0) return;
if (separator && length > 0) {
parts.push("\n");
length += 1;
}
separator = false;
chunks.push({ node: text, start: length, end: length + text.length });
parts.push(text.data);
length += text.length;
return;
}
if (node.nodeType !== 1) return;
const element = node as Element;
if (element.matches(EXCLUDED_SELECTOR)) return;
const block = element.matches(BLOCK_SELECTOR);
if (block || element.tagName === "BR") separator = true;
for (const child of element.childNodes) visit(child);
if (block) separator = true;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium lib/assistantTextSelection.ts:144

When a selection crosses consecutive <br> elements, readAssistantText returns only one \n instead of one per break, so the saved quote and offsets no longer represent the text the user selected. Because separator is a boolean, later breaks overwrite the pending separator; track the number of consecutive breaks and emit that many separators.

-  let separator = false;
+  let separator = 0;
@@
-      if (separator && length > 0) {
+      if (separator > 0 && length > 0) {
         parts.push("\n");
-        length += 1;
+        length += separator;
       }
-      separator = false;
+      separator = 0;
@@
-    if (block || element.tagName === "BR") separator = true;
+    if (element.tagName === "BR") separator += 1;
+    else if (block) separator = Math.max(separator, 1);
@@
-    if (block) separator = true;
+    if (block) separator = Math.max(separator, 1);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/assistantTextSelection.ts around lines 144-167:

When a selection crosses consecutive `<br>` elements, `readAssistantText` returns only one `\n` instead of one per break, so the saved quote and offsets no longer represent the text the user selected. Because `separator` is a boolean, later breaks overwrite the pending separator; track the number of consecutive breaks and emit that many separators.

const decodeCitation = Schema.decodeUnknownOption(AssistantCitation);

function encodePathPart(value: string): string {
return encodeURIComponent(value).replace(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/assistantCitations.ts:21

formatAssistantCitationHref emits IDs such as . and .. as literal path segments, so new URL() normalizes them and parseAssistantCitationHref returns null instead of the original citation. Sources with these valid IDs are therefore serialized but cannot be collected, expanded, or navigated; percent-encode . in encodePathPart as well.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/shared/src/assistantCitations.ts around line 21:

`formatAssistantCitationHref` emits IDs such as `.` and `..` as literal path segments, so `new URL()` normalizes them and `parseAssistantCitationHref` returns `null` instead of the original citation. Sources with these valid IDs are therefore serialized but cannot be collected, expanded, or navigated; percent-encode `.` in `encodePathPart` as well.

setupCleanups.push(() => {
window.removeEventListener("mouseup", handleMouseUp);
mount.removeEventListener("pointerdown", handlePointerDown);
selectionActions = observeSelectionActions({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium components/ThreadTerminalDrawer.tsx:778

In browser mode, clicking Copy or Add-to-chat in the selection menu never executes the action: observeSelectionActions treats the fallback menu's DOM pointerdown outside mount as an interaction, invalidates the request, and closes the menu before its click resolves. Pass the menu element through getActionElement (or otherwise exempt the fallback menu) so interactions with the menu remain part of the active selection flow.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/ThreadTerminalDrawer.tsx around line 778:

In browser mode, clicking `Copy` or `Add-to-chat` in the selection menu never executes the action: `observeSelectionActions` treats the fallback menu's DOM pointerdown outside `mount` as an interaction, invalidates the request, and closes the menu before its click resolves. Pass the menu element through `getActionElement` (or otherwise exempt the fallback menu) so interactions with the menu remain part of the active selection flow.

onCancel: () => void;
}) {
const [comment, setComment] = useState(citation.comment ?? "");
const commentTooLong = comment.length > ASSISTANT_CITATION_MAX_COMMENT_LENGTH;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium chat/AssistantCitationCommentEditor.tsx:18

Save is disabled for a comment whose trimmed value is at most ASSISTANT_CITATION_MAX_COMMENT_LENGTH, such as an 8,000-character comment with one leading or trailing space. commentTooLong checks the untrimmed value, while withAssistantCitationComment trims before persisting; validate comment.trim() so the editor uses the same value as the storage contract.

-  const commentTooLong = comment.length > ASSISTANT_CITATION_MAX_COMMENT_LENGTH;
+  const commentTooLong = comment.trim().length > ASSISTANT_CITATION_MAX_COMMENT_LENGTH;
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/AssistantCitationCommentEditor.tsx around line 18:

`Save` is disabled for a comment whose trimmed value is at most `ASSISTANT_CITATION_MAX_COMMENT_LENGTH`, such as an 8,000-character comment with one leading or trailing space. `commentTooLong` checks the untrimmed value, while `withAssistantCitationComment` trims before persisting; validate `comment.trim()` so the editor uses the same value as the storage contract.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.

Reviewed by Cursor Bugbot for commit 393a909. Configure here.

dismissed = true;
}
onDismiss("cancel");
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scroll cancels the Cite toolbar

Medium Severity

observeSelectionActions treats any descendant scroll as a cancel. After pointerup, pointerDown is already false, so a selection scroll-into-view or LegendList layout scroll clears the pending timer and sets dismissed. selectionchange will not recover, so the Cite control never appears for selections that move the timeline.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 393a909. Configure here.

const hashIndex = href.indexOf("#");
if (hashIndex === -1) return null;
const hash = href.slice(hashIndex + 1);
if (!hash.startsWith(CITATION_HASH_PREFIX) || hash.length > 140_000) return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Large citation hashes fail navigation

Low Severity

assistantCitationFromLocation drops hashes longer than 140,000 characters, but assistantCitationHash base64url-encodes the full t3-citation href. A near-max Unicode quote plus comment encodes past that cap, so chip clicks still change the thread route and then never activate highlight or history loading.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 393a909. Configure here.

juliusmarminge added a commit that referenced this pull request Sep 2, 2026
Adopts main's round-19 features into the v2 stack: payload-budgeted
orchestration replay (#8992), sidebar row subscription leases (#9052),
tool group virtualization and scroll anchoring (#9106), repeated-command
and browser-group presentation, inline assistant citations (#9146),
per-cwd provider skills discovery (#8778), Claude composer skill
dispatch (#9128), grok health probe and model negotiation (#9154), and
the failed-tool thinking fallback (#9165).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juliusmarminge added a commit that referenced this pull request Sep 2, 2026
Adopts main's round-19 features into the v2 stack: payload-budgeted
orchestration replay (#8992), sidebar row subscription leases (#9052),
tool group virtualization and scroll anchoring (#9106), repeated-command
and browser-group presentation, inline assistant citations (#9146),
per-cwd provider skills discovery (#8778), Claude composer skill
dispatch (#9128), grok health probe and model negotiation (#9154), and
the failed-tool thinking fallback (#9165).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants