feat(composer): attach threads as context - #12296
Conversation
Thread transfer impact
This comment will update automatically after the next completed run. |
There was a problem hiding this comment.
🟠 High
t3code/apps/web/src/components/chat/ChatComposer.tsx
Lines 6338 to 6339 in 4c7ddb4
getSendContext returns a stale threadContexts array after the user adds or removes a thread context, so the next dispatched message can omit the context currently attached to the composer. Because composerThreadContexts is missing from the useImperativeHandle dependency list, the imperative handle is not rebuilt when that state changes; include it in the dependencies.
composerTerminalContextsRef,
+ composerThreadContexts,
composerPreviewAnnotations,🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/ChatComposer.tsx around lines 6338-6339:
`getSendContext` returns a stale `threadContexts` array after the user adds or removes a thread context, so the next dispatched message can omit the context currently attached to the composer. Because `composerThreadContexts` is missing from the `useImperativeHandle` dependency list, the imperative handle is not rebuilt when that state changes; include it in the dependencies.
| imageCount: composerImages.length + composerFiles.length, | ||
| terminalContexts: composerTerminalContexts, | ||
| elementContextCount: composerPreviewAnnotations.length + composerReviewComments.length, | ||
| elementContextCount: |
There was a problem hiding this comment.
🟠 High components/ChatView.tsx:8082
A draft containing only an attached thread remains unsendable: ChatComposer sets hasSendableContent to false, so it disables the Send button and prevents submission. The handler passes composerThreadContexts to deriveComposerSendState here, but the composer's own calculation counts only preview annotations and review comments; include thread contexts in that calculation as well.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/ChatView.tsx around line 8082:
A draft containing only an attached thread remains unsendable: `ChatComposer` sets `hasSendableContent` to `false`, so it disables the Send button and prevents submission. The handler passes `composerThreadContexts` to `deriveComposerSendState` here, but the composer's own calculation counts only preview annotations and review comments; include thread contexts in that calculation as well.
| const shell = useThreadShell({ environmentId, threadId }); | ||
| const title = shell?.title?.trim() || props.record.title; | ||
| return ( | ||
| <Tooltip> | ||
| <TooltipTrigger | ||
| render={ | ||
| <Link | ||
| to="/$environmentId/$threadId" | ||
| params={{ environmentId, threadId }} | ||
| aria-label={`Thread, ${title}`} | ||
| data-markdown-copy={props.copyMarkdown} | ||
| className={cn( | ||
| props.className, | ||
| CONTEXT_INLINE_CHIP_TONE_CLASS_NAMES.thread, | ||
| CONTEXT_INLINE_CHIP_INTERACTIVE_CLASS_NAME, | ||
| "no-underline", | ||
| )} | ||
| > | ||
| <MessagesSquareIcon className={COMPOSER_INLINE_CHIP_ICON_CLASS_NAME} /> | ||
| <span className={props.labelClassName}>{title}</span> | ||
| </Link> | ||
| } | ||
| /> | ||
| <TooltipPopup side="top">{shell ? "Open thread" : "Thread no longer available"}</TooltipPopup> |
There was a problem hiding this comment.
🟡 Medium components/ThreadContextChip.tsx:25
A deleted thread still renders as an active Link with the tooltip Open thread, so clicking the chip navigates to a thread whose shell is deleted and may redirect or show an empty chat surface. Because shell remains truthy after deletion, derive availability from !shell.deletedAt and block navigation for unavailable threads.
- const shell = useThreadShell({ environmentId, threadId });
+ const shell = useThreadShell({ environmentId, threadId });
+ const isAvailable = Boolean(shell && !shell.deletedAt);
const title = shell?.title?.trim() || props.record.title;
@@
params={{ environmentId, threadId }}
aria-label={`Thread, ${title}`}
+ aria-disabled={!isAvailable}
+ onClick={(event) => {
+ if (!isAvailable) event.preventDefault();
+ }}
data-markdown-copy={props.copyMarkdown}
@@
- <TooltipPopup side="top">{shell ? "Open thread" : "Thread no longer available"}</TooltipPopup>
+ <TooltipPopup side="top">{isAvailable ? "Open thread" : "Thread no longer available"}</TooltipPopup>🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/ThreadContextChip.tsx around lines 25-48:
A deleted thread still renders as an active `Link` with the tooltip `Open thread`, so clicking the chip navigates to a thread whose shell is deleted and may redirect or show an empty chat surface. Because `shell` remains truthy after deletion, derive availability from `!shell.deletedAt` and block navigation for unavailable threads.
There was a problem hiding this comment.
🟡 Medium
t3code/apps/web/src/components/chat/ChatComposer.tsx
Lines 2963 to 2964 in 4c7ddb4
buildContextClipboardFragment drops selected thread chips from the clipboard payload, so pasting a thread reference silently loses its attached-thread context (and returns null when it is the only selection). Include matching composerThreadContexts records in records so the thread importer can restore them.
),
+ ...composerThreadContexts.filter((context) => wanted.has(context.contextId)),
...[...composerImages, ...composerFiles]🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/ChatComposer.tsx around lines 2963-2964:
`buildContextClipboardFragment` drops selected thread chips from the clipboard payload, so pasting a thread reference silently loses its attached-thread context (and returns `null` when it is the only selection). Include matching `composerThreadContexts` records in `records` so the thread importer can restore them.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This is a substantial cross-platform feature that adds persistent thread context, new composer workflows, provider instructions, and a broader MCP read-access path across project boundaries. Its production behavior and access-control implications, along with unresolved findings around sending, stale state, deleted threads, and clipboard restoration, warrant human review. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |
| return { draftsByThreadKey: nextDraftsByThreadKey }; | ||
| }); | ||
| }, | ||
| addThreadContexts: (threadRef, records, options) => { |
There was a problem hiding this comment.
🟡 Medium src/composerDraftStore.ts:4030
moveComposerPromptAndImages preserves source.threadContexts but rebuilds the source prompt with only terminal references, so buildMessageContext still sends the thread context without a visible chip or reference. When an unsaved queued edit is recovered, the user therefore cannot see or remove the attached thread. Include the source thread-context references when rebuilding nextSource.prompt.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/composerDraftStore.ts around line 4030:
`moveComposerPromptAndImages` preserves `source.threadContexts` but rebuilds the source prompt with only terminal references, so `buildMessageContext` still sends the thread context without a visible chip or reference. When an unsaved queued edit is recovered, the user therefore cannot see or remove the attached thread. Include the source thread-context references when rebuilding `nextSource.prompt`.
| ); | ||
| if (!shell) return; | ||
| const record = threadComposerContext(item.thread, shell.title); | ||
| const existing = getComposerDraftSnapshot(ownerKey).context?.records ?? []; |
There was a problem hiding this comment.
🟡 Medium threads/use-composer-command-menu.ts:514
Selecting a thread while editing a queued message inserts the reference into the queued draft but saves its context record under the thread-level ownerKey, so the queued message cannot retain or send that context and the record may be pruned as unreferenced. Use the draft key for the visible queued editor when reading and updating the composer draft context.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/threads/use-composer-command-menu.ts around line 514:
Selecting a thread while editing a queued message inserts the reference into the queued draft but saves its context record under the thread-level `ownerKey`, so the queued message cannot retain or send that context and the record may be pruned as unreferenced. Use the draft key for the visible queued editor when reading and updating the composer draft context.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Stacked on #2829 (
t3code/codex-turn-mapping). Supersedes #12251, which built a parallelt3-thread://link format instead of using context records.Users want to point a new conversation at work that happened in another thread. This adds a
threadcomposer context kind on the existing inline context system: aThreadContextRecordinpackages/contracts, one provider-projection case inpackages/shared, and one chip registration per surface. Everything else (drafts, persistence, paste, copy, undo, legacy hosts, unknown-kind fallback) is inherited.Two ways in. Type
@plus part of a title to pick a thread on web and mobile; thread matches lead the list and only appear for a typed query. On web and desktop, drag a thread out of the sidebar and drop it on the composer; a multi-selection drops together. The drag reuses the sidebar's pointer sensor, cancels the sort when the release lands outside the list, and moves the ghost through a tiny external store so pointer moves do not re-render the sidebar.The prompt carries only the reference. The provider envelope gets the thread's identity plus a one-line instruction to read it with
t3_thread_read, which now also accepts a thread the user attached even when it lives in another project. OnlycreatedBy: "user"records grant that read, so an agent cannot widen its own reach, and writes stay project-scoped. Chips read the live title, so a renamed thread never shows a stale label, and open the thread on click (webLink, mobile navigation).Also fixes a pre-existing bug found while verifying: the
launchThreadWebSocket handler droppedinitialMessage.contextfor every context kind, so a new thread's first message lost all its context records. Follow-ups were unaffected.Verified with focused tests (contracts round-trip, shared projection, client-runtime matcher, server MCP read/deny/no-write, web draft store and pointer sensor), scoped typecheck and lint for web, mobile, server, shared, client-runtime, and a live browser run: picker, drag, chip in draft and sent message, persistence across reload, and a real Codex turn calling
t3_thread_readon the attached thread.@pickerModel: Claude Fable 5. Harness: Claude Code.
🤖 Generated with Claude Code