| author | |
| committer | |
| log | 8d199b4f7b918079161d5f43e0146c099301be08 |
| tree | 35fc1bf71253d38407d478bc551265d25693ea1b |
| parent | 20254307084f1101ced00dfadeddc7804a7de45d |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Yash Singh <saiansh2525@gmail.com>7 files changed, 59 insertions(+), 12 deletions(-)
apps/mobile/src/lib/composerContext.test.ts+15| ... | ... | @@ -8,6 +8,7 @@ import { |
| 8 | 8 | } from "@t3tools/contracts"; |
| 9 | 9 | import { collectComposerInlineTokens } from "@t3tools/shared/composerInlineTokens"; |
| 10 | 10 | import { |
| 11 | ATTACHMENT_ONLY_BOOTSTRAP_PROMPT, | |
| 11 | 12 | collectComposerContextReferences, |
| 12 | 13 | formatComposerContextReference, |
| 13 | 14 | projectComposerContextForProvider, |
| ... | ... | @@ -207,6 +208,20 @@ describe("mobile composer context", () => { |
| 207 | 208 | }); |
| 208 | 209 | |
| 209 | 210 | describe("host context compatibility", () => { |
| 211 | it("sends an image-only prompt as the attachment-only prompt and keeps the image", () => { | |
| 212 | const context: OrchestrationMessageContext = { version: 1, records: [image] }; | |
| 213 | const reference = formatComposerContextReference(image); | |
| 214 | expect(serializeComposerMessageForServer(reference, context, true)).toEqual({ | |
| 215 | text: ATTACHMENT_ONLY_BOOTSTRAP_PROMPT, | |
| 216 | context, | |
| 217 | }); | |
| 218 | const withProse = `${reference} what is this?`; | |
| 219 | expect(serializeComposerMessageForServer(withProse, context, true)).toEqual({ | |
| 220 | text: withProse, | |
| 221 | context, | |
| 222 | }); | |
| 223 | }); | |
| 224 | ||
| 210 | 225 | it.each(["existing-thread", "new-task"])("serializes %s sends for an older host", (path) => { |
| 211 | 226 | const pr = pullRequestComposerContext( |
| 212 | 227 | { |
apps/mobile/src/lib/composerContext.ts+11-2| ... | ... | @@ -13,8 +13,10 @@ import { |
| 13 | 13 | } from "@t3tools/contracts"; |
| 14 | 14 | import * as Schema from "effect/Schema"; |
| 15 | 15 | import { |
| 16 | ATTACHMENT_ONLY_BOOTSTRAP_PROMPT, | |
| 16 | 17 | collectComposerContextReferences, |
| 17 | 18 | formatComposerContextReference, |
| 19 | isImageReferencesOnly, | |
| 18 | 20 | replaceComposerContextReferences, |
| 19 | 21 | sanitizeComposerContextLabel, |
| 20 | 22 | } from "@t3tools/shared/composerContextReferences"; |
| ... | ... | @@ -229,12 +231,19 @@ export function reidentifyComposerContext( |
| 229 | 231 | }; |
| 230 | 232 | } |
| 231 | 233 | |
| 232 | /** Keep queued records canonical; choose the wire format against the host at dispatch time. */ | |
| 234 | /** | |
| 235 | * Keep queued records canonical; choose the wire format against the host at dispatch time. | |
| 236 | * An image-only prompt goes out as the attachment-only prompt web sends, so the agent is still | |
| 237 | * asked to respond. The images stay in the message's attachments and context records. | |
| 238 | */ | |
| 233 | 239 | export function serializeComposerMessageForServer( |
| 234 | text: string, | |
| 240 | composerText: string, | |
| 235 | 241 | context: OrchestrationMessageContext | undefined, |
| 236 | 242 | supportsInlineMessageContext: boolean, |
| 237 | 243 | ): { text: string; context?: OrchestrationMessageContext } { |
| 244 | const text = isImageReferencesOnly(composerText) | |
| 245 | ? ATTACHMENT_ONLY_BOOTSTRAP_PROMPT | |
| 246 | : composerText; | |
| 238 | 247 | return supportsInlineMessageContext |
| 239 | 248 | ? { text, ...(context ? { context } : {}) } |
| 240 | 249 | : { text: serializeLegacyContextMessage({ text, records: context?.records ?? [] }) }; |
apps/web/src/components/ChatView.tsx+2-4| ... | ... | @@ -607,10 +607,8 @@ import { |
| 607 | 607 | supportsServerUpdateThreadContinuation, |
| 608 | 608 | } from "../versionSkew"; |
| 609 | 609 | import { useAssetUrls } from "../assets/assetUrls"; |
| 610 | import { | |
| 611 | ATTACHMENT_ONLY_BOOTSTRAP_PROMPT, | |
| 612 | recallableComposerPrompt, | |
| 613 | } from "./chat/composerPromptHistory"; | |
| 610 | import { ATTACHMENT_ONLY_BOOTSTRAP_PROMPT } from "@t3tools/shared/composerContextReferences"; | |
| 611 | import { recallableComposerPrompt } from "./chat/composerPromptHistory"; | |
| 614 | 612 | import { observeResize } from "~/lib/observeResize"; |
| 615 | 613 | |
| 616 | 614 | const EMPTY_PROVIDERS: ServerProvider[] = []; |
apps/web/src/components/chat/composerPromptHistory.test.ts+1-1| ... | ... | @@ -1,8 +1,8 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | |
| 3 | import { ATTACHMENT_ONLY_BOOTSTRAP_PROMPT } from "@t3tools/shared/composerContextReferences"; | |
| 3 | 4 | import { buildPlanImplementationPrompt } from "../../proposedPlan"; |
| 4 | 5 | import { |
| 5 | ATTACHMENT_ONLY_BOOTSTRAP_PROMPT, | |
| 6 | 6 | buildComposerPromptHistoryEntries, |
| 7 | 7 | recallableComposerPrompt, |
| 8 | 8 | stepComposerPromptHistory, |
apps/web/src/components/chat/composerPromptHistory.ts+4-5| ... | ... | @@ -1,4 +1,7 @@ |
| 1 | import { collectComposerContextReferences } from "@t3tools/shared/composerContextReferences"; | |
| 1 | import { | |
| 2 | ATTACHMENT_ONLY_BOOTSTRAP_PROMPT, | |
| 3 | collectComposerContextReferences, | |
| 4 | } from "@t3tools/shared/composerContextReferences"; | |
| 2 | 5 | import { PLAN_IMPLEMENTATION_PROMPT_PREFIX } from "../../proposedPlan"; |
| 3 | 6 | |
| 4 | 7 | /** |
| ... | ... | @@ -15,10 +18,6 @@ const REVIEW_COMMENT_BLOCK_PATTERN = /<review_comment\b[^>]*>[\s\S]*?<\/review_c |
| 15 | 18 | const TRAILING_LEGACY_CONTEXT = |
| 16 | 19 | /\n*<(terminal_context|element_context|preview_annotation)>\n([\s\S]*?)\n<\/\1>\s*$/; |
| 17 | 20 | |
| 18 | /** Text sent in place of an empty prompt when a message is attachments only. */ | |
| 19 | export const ATTACHMENT_ONLY_BOOTSTRAP_PROMPT = | |
| 20 | "[User attached one or more files without additional text. Respond using the conversation context and the attached files.]"; | |
| 21 | ||
| 22 | 21 | export interface ComposerPromptHistoryMessage { |
| 23 | 22 | readonly id: string; |
| 24 | 23 | readonly role: string; |
packages/shared/src/composerContextReferences.test.ts+12| ... | ... | @@ -6,6 +6,7 @@ import { |
| 6 | 6 | formatComposerContextHref, |
| 7 | 7 | formatComposerContextProviderMarker, |
| 8 | 8 | formatComposerContextReference, |
| 9 | isImageReferencesOnly, | |
| 9 | 10 | parseComposerContextHref, |
| 10 | 11 | projectComposerContextForProvider, |
| 11 | 12 | replaceComposerContextReferences, |
| ... | ... | @@ -86,6 +87,17 @@ describe("labels and reference links", () => { |
| 86 | 87 | expect(collectComposerContextReferences("[x](https://example.com)")).toEqual([]); |
| 87 | 88 | }); |
| 88 | 89 | |
| 90 | it("recognizes a prompt made only of image references", () => { | |
| 91 | const image = ""; | |
| 92 | const file = "[notes.md](t3-context://v1/file/ctx_3)"; | |
| 93 | expect(isImageReferencesOnly(image)).toBe(true); | |
| 94 | expect(isImageReferencesOnly(` ${image}\n `)).toBe(true); | |
| 95 | expect(isImageReferencesOnly(`${image} what is this?`)).toBe(false); | |
| 96 | expect(isImageReferencesOnly(file)).toBe(false); | |
| 97 | expect(isImageReferencesOnly(`${image} ${file}`)).toBe(false); | |
| 98 | expect(isImageReferencesOnly(" ")).toBe(false); | |
| 99 | }); | |
| 100 | ||
| 89 | 101 | it("replaces occurrences in place", () => { |
| 90 | 102 | const text = "a [x](t3-context://v1/skill/ctx_1) b [y](t3-context://v1/file/ctx_2) c"; |
| 91 | 103 | expect(replaceComposerContextReferences(text, (o) => `<${o.contextId}>`)).toBe( |
packages/shared/src/composerContextReferences.ts+14| ... | ... | @@ -92,6 +92,20 @@ export function collectComposerContextReferences( |
| 92 | 92 | return occurrences; |
| 93 | 93 | } |
| 94 | 94 | |
| 95 | /** Text sent in place of an empty prompt when a message is attachments only. */ | |
| 96 | export const ATTACHMENT_ONLY_BOOTSTRAP_PROMPT = | |
| 97 | "[User attached one or more files without additional text. Respond using the conversation context and the attached files.]"; | |
| 98 | ||
| 99 | /** Whether `text` is image references and nothing else, which is how an image-only prompt reads. */ | |
| 100 | export function isImageReferencesOnly(text: string): boolean { | |
| 101 | const references = collectComposerContextReferences(text); | |
| 102 | return ( | |
| 103 | references.length > 0 && | |
| 104 | references.every((reference) => reference.kind === "image") && | |
| 105 | replaceComposerContextReferences(text, () => "").trim() === "" | |
| 106 | ); | |
| 107 | } | |
| 108 | ||
| 95 | 109 | export function replaceComposerContextReferences( |
| 96 | 110 | text: string, |
| 97 | 111 | replace: (occurrence: ComposerContextReferenceOccurrence) => string, |