Skip to content
Closed
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
17 changes: 17 additions & 0 deletions packages/shared/src/assistantCitations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,23 @@ export function assistantCitationsToPlainText(prompt: string): string {
});
}

// Claude 5.1+ writes some user-facing updates as thinking blocks, which the adapter shows as
// assistant messages whose percent-encoded item id ends in `:thinking:<n>`. The API rejects
// that text coming back in a prompt as reasoning extraction, so only the comment goes out.
const THINKING_DERIVED_MESSAGE_ID = /%3Athinking%3A\d+$/u;

export function withholdThinkingCitationText(prompt: string): string {
let text = "";
let cursor = 0;
for (const { citation, start, end } of collectAssistantCitations(prompt)) {
if (!THINKING_DERIVED_MESSAGE_ID.test(citation.messageId)) continue;
const comment = citation.comment === undefined ? "" : ` User comment: ${citation.comment}`;
text += `${prompt.slice(cursor, start)}[Quote of one of your earlier updates, text withheld.${comment}]`;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
cursor = end;
}
return text + prompt.slice(cursor);
}

export function expandAssistantCitationsForProvider(prompt: string): string {
const matches = collectAssistantCitations(prompt);
if (matches.length === 0) return prompt;
Expand Down
33 changes: 33 additions & 0 deletions packages/shared/src/composerContextReferences.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {
ComposerContextId,
type ComposerContextRecord,
EnvironmentId,
MessageId,
ThreadId,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { serializeAssistantCitation } from "./assistantCitations.ts";
import {
collectComposerContextReferences,
formatComposerContextHref,
Expand Down Expand Up @@ -156,6 +158,37 @@ describe("provider projection", () => {
expect(projected).toContain("- font-size: 12px → 20px");
});

it("withholds quoted text of thinking-derived messages but keeps the comment and other quotes", () => {
const quote = {
version: 1,
environmentId: EnvironmentId.make("env"),
threadId: ThreadId.make("thread"),
text: "private plan",
start: 0,
end: 12,
prefix: "before ",
suffix: " after",
} as const;
const thinking = serializeAssistantCitation({
...quote,
messageId: MessageId.make("message:provider:claudeAgent:native-item:msg_01%3Athinking%3A1"),
comment: "fix this?",
});
const reply = serializeAssistantCitation({
...quote,
text: "plain reply",
messageId: MessageId.make("msg_02"),
});
const projected = projectComposerContextForProvider({
text: `${thinking} and ${reply}`,
records: [],
});
expect(projected).toBe(
`[Quote of one of your earlier updates, text withheld. User comment: fix this?] and ${reply}`,
);
expect(projected).not.toContain("private");
});

it("uses the payload kind when a reference disagrees with its record", () => {
const projected = projectComposerContextForProvider({
text: "[log](t3-context://v1/image/ctx_t)",
Expand Down
8 changes: 5 additions & 3 deletions packages/shared/src/composerContextReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type ScopedThreadRef,
type ThreadContextRecord,
} from "@t3tools/contracts";
import { withholdThinkingCitationText } from "./assistantCitations.ts";

const CONTEXT_PROTOCOL = "t3-context:";
const COMPOSER_CONTEXT_HREF_PREFIX = `${CONTEXT_PROTOCOL}//v1/`;
Expand Down Expand Up @@ -312,13 +313,14 @@ export function projectComposerContextForProvider(input: {
text: string;
records: ReadonlyArray<ComposerContextRecord>;
}): string {
const occurrences = collectComposerContextReferences(input.text);
if (occurrences.length === 0) return input.text;
const text = withholdThinkingCitationText(input.text);
const occurrences = collectComposerContextReferences(text);
if (occurrences.length === 0) return text;
const recordsById = new Map<ComposerContextId, ComposerContextRecord | undefined>();
for (const record of input.records) {
recordsById.set(record.contextId, recordsById.has(record.contextId) ? undefined : record);
}
const body = replaceComposerContextReferences(input.text, (occurrence) =>
const body = replaceComposerContextReferences(text, (occurrence) =>
formatComposerContextProviderMarker(
recordsById.get(occurrence.contextId)?.kind ?? occurrence.kind,
occurrence.label,
Expand Down
Loading