Repository navigation
feat(web,mobile): image-only messages show a quiet caption instead of the agent prompt #23613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
2ea11fe
9b7697d
3111e05
0addd74
27e6b55
78c78df
23b65e9
582762e
126dded
c731ccf
0115c76
67e56e4
57e4321
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,7 @@ import { | |
| parseComposerContextHref, | ||
| collectComposerContextReferences, | ||
| replaceComposerContextReferences, | ||
| isAttachmentOnlyBootstrapText, | ||
| formatComposerContextReference, | ||
| } from "@t3tools/shared/composerContextReferences"; | ||
| import { ComposerContextSheet } from "../../components/ComposerContextSheet"; | ||
|
|
@@ -1407,6 +1408,10 @@ function renderFeedEntry( | |
| const foldExpanded = foldRowId !== null && (props.expandedWorkRows[foldRowId] ?? false); | ||
| const bubbleBodyVisible = foldRowId === null || foldExpanded; | ||
| const intentBadge = resolveUserMessageIntentBadge(message.inputIntent); | ||
| // The bootstrap prompt is for the agent, not the reader: a person's photo-only message shows a | ||
| // caption instead. Messages from other agents keep their folded rendering. | ||
| const attachmentOnly = | ||
| presentation.attribution !== "agent" && isAttachmentOnlyBootstrapText(message.text); | ||
| const referenceIds = new Set( | ||
| collectComposerContextReferences(message.text).map((reference) => reference.contextId), | ||
| ); | ||
|
|
@@ -1527,7 +1532,13 @@ function renderFeedEntry( | |
| })} | ||
| </View> | ||
| ) : null} | ||
| {message.text.trim().length > 0 ? ( | ||
| {attachmentOnly ? ( | ||
| <AttachmentOnlyCaption | ||
| label={attachments.every(isImageAttachment) ? "Image only" : "Attachment only"} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔎 Supported by static analysis
Give the attachment-only bubble a definite width. The bubble only receives a width when the message has a wide Markdown block, a review comment, or an expanded fold. A collapsed photo-only bubble has none of these, so it sizes to its content. The short caption and the 🐛 Suggested fix
|
||
| instruction={message.text.trim()} | ||
| /> | ||
| ) : null} | ||
| {message.text.trim().length > 0 && !attachmentOnly ? ( | ||
| <MarkdownImageAvailableWidthContext | ||
| value={props.userBubbleMaxWidth - USER_BUBBLE_HORIZONTAL_PADDING * 2} | ||
| > | ||
|
|
@@ -1598,7 +1609,7 @@ function renderFeedEntry( | |
| <SymbolView name="pencil" size={14} tintColor={iconSubtleColor} /> | ||
| </Pressable> | ||
| ) : null} | ||
| {presentation.text.trim().length > 0 ? ( | ||
| {presentation.text.trim().length > 0 && !attachmentOnly ? ( | ||
| <UserMessageCopyButton | ||
| text={presentation.text} | ||
| onCopy={ | ||
|
|
@@ -1764,6 +1775,31 @@ type UserMessageContentProps = { | |
| readonly renderImage: MarkdownImageRenderer; | ||
| }; | ||
|
|
||
| /** Sent with no text: a quiet caption stands in for the agent's bootstrap prompt, and a tap reveals it. */ | ||
| function AttachmentOnlyCaption(props: { readonly label: string; readonly instruction: string }) { | ||
| const [expanded, setExpanded] = useState(false); | ||
| return ( | ||
| <View className="gap-1"> | ||
| <Pressable | ||
| accessibilityRole="button" | ||
| accessibilityLabel={expanded ? "Hide agent instruction" : props.label} | ||
| accessibilityState={{ expanded }} | ||
| hitSlop={8} | ||
| onPress={() => setExpanded((value) => !value)} | ||
| > | ||
| <Text className="text-xs italic text-user-bubble-foreground-muted"> | ||
| {expanded ? "Hide" : props.label} | ||
| </Text> | ||
| </Pressable> | ||
| {expanded ? ( | ||
| <Text className="text-xs leading-relaxed text-user-bubble-foreground-muted"> | ||
| {props.instruction} | ||
| </Text> | ||
| ) : null} | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| function UserMessageContent(props: UserMessageContentProps) { | ||
| const [selected, setSelected] = useState<{ contextId: string; label: string } | null>(null); | ||
| const navigation = useNavigation(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,7 +52,10 @@ import { | |
| resolveUserMessageContext, | ||
| selectedMessageContextFragment, | ||
| } from "~/lib/composerContextRecords"; | ||
| import { collectComposerContextReferences } from "@t3tools/shared/composerContextReferences"; | ||
| import { | ||
| collectComposerContextReferences, | ||
| isAttachmentOnlyBootstrapText, | ||
| } from "@t3tools/shared/composerContextReferences"; | ||
| import { | ||
| COMPOSER_CONTEXT_CLIPBOARD_MIME, | ||
| encodeComposerContextClipboardHtml, | ||
|
|
@@ -304,6 +307,10 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message" | |
| : null, | ||
| ); | ||
| const resolvedContext = resolveUserMessageContext(row.message); | ||
| // The bootstrap prompt is for the agent, not the reader: a person's photo-only message shows a | ||
| // caption instead. Messages from other agents keep their folded rendering. | ||
| const attachmentOnly = !folded && isAttachmentOnlyBootstrapText(resolvedContext.text); | ||
| const bodyText = attachmentOnly ? "" : resolvedContext.text; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Require at least one attachment before a web message is classified as attachment-only.
🤖 Prompt for AI Agents
|
||
| const previewImages = userImages.filter((image) => image.name.startsWith("preview-annotation-")); | ||
| const revertTurnCount = row.revertTurnCount; | ||
| const chippedAttachmentIds = new Set( | ||
|
|
@@ -321,7 +328,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message" | |
| resolvedContext.text, | ||
| (reference) => reference.label, | ||
| ); | ||
| const promptLink = promptLinkFor(plainText); | ||
| const promptLink = attachmentOnly ? null : promptLinkFor(plainText); | ||
| const foldSummary = | ||
| plainText | ||
| .split(/\r?\n/) | ||
|
|
@@ -459,7 +466,17 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message" | |
| > | ||
| <MessageAuthorHeading>You</MessageAuthorHeading> | ||
| {(regularImages.length > 0 || userVideos.length > 0) && ( | ||
| <div className="mb-2 grid max-w-[210px] grid-cols-2 gap-2"> | ||
| <div | ||
| className={cn( | ||
| "mb-2 grid gap-2", | ||
| // A lone photo sent without text fills its bubble instead of leaving half of it empty. | ||
| // It sits at the bubble's right edge so opening the caption grows the bubble leftward | ||
| // without moving the photo. | ||
| attachmentOnly && regularImages.length + userVideos.length === 1 | ||
| ? "ms-auto max-w-[240px] grid-cols-1" | ||
| : "max-w-[210px] grid-cols-2", | ||
| )} | ||
| > | ||
| {regularImages.map((image) => ( | ||
| <div | ||
| key={image.id} | ||
|
|
@@ -567,9 +584,19 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message" | |
| ))} | ||
| </div> | ||
| ) : null} | ||
| {attachmentOnly ? ( | ||
| <AttachmentOnlyCaption | ||
| label={ | ||
| userVideos.length + unchippedFiles.length + unknownAttachments.length > 0 | ||
| ? "Attachment only" | ||
| : "Image only" | ||
| } | ||
| instruction={resolvedContext.text} | ||
| /> | ||
| ) : null} | ||
| <div onCopyCapture={onBodyCopyCapture}> | ||
| <CollapsibleUserMessageBody | ||
| text={resolvedContext.text} | ||
| text={bodyText} | ||
| renderContextReference={renderContextReference} | ||
| skills={ctx.skills} | ||
| markdownCwd={ctx.markdownCwd} | ||
|
|
@@ -600,7 +627,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message" | |
| {typeof revertTurnCount === "number" && ( | ||
| <RevertUserMessageButton turnCount={revertTurnCount} messageId={row.message.id} /> | ||
| )} | ||
| {resolvedContext.text && ( | ||
| {bodyText && ( | ||
| <MessageCopyButton | ||
| text={contextClipboardFragment ? resolvedContext.text : plainText} | ||
| {...(contextClipboardFragment | ||
|
|
@@ -629,6 +656,31 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message" | |
| ); | ||
| } | ||
|
|
||
| /** Sent with no text: a quiet caption stands in for the agent's bootstrap prompt, and a click reveals it. */ | ||
| function AttachmentOnlyCaption({ | ||
| label, | ||
| instruction, | ||
| }: { | ||
| readonly label: string; | ||
| readonly instruction: string; | ||
| }) { | ||
| const [expanded, setExpanded] = useState(false); | ||
| return ( | ||
| <div className="text-xs text-muted-foreground" data-user-message-attachment-only="true"> | ||
| <button | ||
| type="button" | ||
| aria-expanded={expanded} | ||
| data-scroll-anchor-ignore | ||
| onClick={() => setExpanded((value) => !value)} | ||
| className="cursor-pointer rounded-sm italic hover:underline focus-visible:outline-2 focus-visible:outline-ring" | ||
| > | ||
| {expanded ? "Hide" : label} | ||
| </button> | ||
| {expanded ? <p className="mt-1 leading-relaxed">{instruction}</p> : null} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| function UserMessageIntentMarker({ | ||
| intent, | ||
| }: { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Require an attachment before classifying the message as attachment-only.
isAttachmentOnlyBootstrapTextchecks only the message text. If a user sends the exact bootstrap prompt without an attachment, this code hides the message body and renders the caption. Becauseattachments.every(isImageAttachment)is true for an empty array, the caption incorrectly saysImage only.Require an attachment before setting
attachmentOnly.🤖 Prompt for AI Agents