Skip to content

feat(conversations): Add transcript export - #127364

Merged
ArthurKnaus merged 11 commits into
masterfrom
arthurknaus/tet-3103-copy-conversation-button-no-longer-works-for-large
Oct 19, 2026
Merged

ArthurKnaus merged 11 commits into
masterfrom
arthurknaus/tet-3103-copy-conversation-button-no-longer-works-for-large

Conversation

@ArthurKnaus

@ArthurKnaus ArthurKnaus commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

The transcript Copy button broke for large conversations once the detail view stopped loading every span up front, so it silently returned a partial transcript.

This replaces it with an Export transcript dropdown that copies the transcript to the clipboard or downloads it as a Markdown file. The transcript always covers the whole conversation: when the detail view already has every page cached it is reused instantly, otherwise every page is fetched on demand behind a loading state. The export fetch is independent of the display query, so exporting a large conversation does not bloat the transcript or timeline view.

Closes TET-3103

Before After
Screenshot 2026-10-14 at 11 44 48 Screenshot 2026-10-14 at 11 44 58

Replace the transcript Copy button with an Export transcript dropdown that
copies the transcript to the clipboard or downloads it as Markdown. The
transcript is built from the whole conversation: the already-cached pages are
reused instantly when the detail view has them, otherwise every page is
fetched on demand with a loading state.
@linear-code

linear-code Bot commented Oct 14, 2026

Copy link
Copy Markdown

TET-3103

@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Oct 14, 2026
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

📊 Type Coverage Diff

Metric Before After Delta
Coverage 95.75% 95.75% ±0%
Typed 142,234 142,295 🟢 +61
Untyped 6,309 6,316 🔴 +7
🔍 7 new type safety issues introduced

Non-null assertions (!) (4 new)

File Line Detail
static/app/views/explore/conversations/hooks/useExportConversation.tsx 82 cached!
static/app/views/explore/conversations/hooks/useExportConversation.tsx 83 cached!
static/app/views/explore/conversations/hooks/useExportConversation.tsx 84 cached!
static/app/views/explore/conversations/hooks/useExportConversation.tsx 90 cached!

Type assertions (as) (3 new)

File Line Detail
static/app/views/explore/conversations/hooks/useExportConversation.tsx 74 `as
    | undefined` — `queryClient.getQueryData(options.queryKey) as | ConversationInfiniteData | unde…` |

| static/app/views/explore/conversations/hooks/useExportConversation.tsx | 94 | as typeof options.queryKey — [ options.queryKey[0], options.queryKey[1], {...options.queryKey[2], export: tr… |
| static/app/views/explore/conversations/hooks/useExportConversation.tsx | 94 | as unknown — [ options.queryKey[0], options.queryKey[1], {...options.queryKey[2], export: tr… |

This is informational only and does not block the PR.

@ArthurKnaus
ArthurKnaus marked this pull request as ready for review October 14, 2026 09:46
@ArthurKnaus
ArthurKnaus requested review from a team as code owners October 14, 2026 09:46

@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.

Stale Bugbot comment from a previous run.

Comment thread static/app/views/explore/conversations/conversationDetail.tsx
Keep the export request alive when the button unmounts mid-flight (switching
off the transcript tab) so a large-conversation export no longer aborts
silently. Drop the deprecated loading toast in favor of the button's busy
state, and type the paginated response instead of casting it.
Annotate the paginated response as ApiResult<ConversationApiResponse> rather
than a hand-written tuple that was not assignable to the client's return type.
const {selection} = usePageFilters();
const [isExporting, setIsExporting] = useState(false);

const fetchConversation = useCallback(async (): Promise<ConversationExport> => {

@scttcper scttcper Oct 14, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Bot review from Codex.

Could we share the infinite-query options with useConversation and pick up where it left off? This refetches from page one unless everything is already loaded. I tested a separate, seeded InfiniteQueryObserver: it fetched just the missing pages and left the display cache alone.

Something like this, with options extracted from the display query:

const fetchConversation = useCallback(async () => {
  const observer = new InfiniteQueryObserver(queryClient, {
    ...options,
    queryKey: [
      options.queryKey[0],
      options.queryKey[1],
      {...options.queryKey[2], export: true},
    ],
    initialData: queryClient.getQueryData(options.queryKey),
  });

  let result = observer.getCurrentResult();
  try {
    while (!result.data || result.hasNextPage) {
      result = await observer.fetchNextPage({throwOnError: true});
    }
  } catch (error) {
    observer.destroy();
    throw error;
  }

  observer.destroy();
  return result.data.pages; // Build the transcript from these.
}, [queryClient, options]);

That would also reuse the existing cursor parsing.

Build the transcript from a separate InfiniteQueryObserver seeded with the
display query's cached pages, so the export resumes where the display left off
and fetches only the missing pages rather than refetching from page one. It
reuses the shared query options and their cursor parsing, runs on the app-wide
query client (so leaving the transcript tab no longer aborts it), and keeps its
extra pages out of the display cache.

@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.

Stale Bugbot comment from a previous run.

Drop the now-internal buildConversationQueryParams export (knip), replace the
try/finally blocks the React Compiler rejects with explicit cleanup on both
paths (oxlint), and give the export trigger an aria-label so the spread Button
props type-check.

@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.

Stale Bugbot comment from a previous run.

Leaving and returning to the transcript tab remounts the export button with a
reset isExporting, so a second export could start and collide with the first on
the shared query key, cancelling both. Cancel and clear any in-flight export
before starting, leave the shared entry for the next export (or gc) instead of
removing it in cleanup, and swallow the resulting CancelledError on the
superseded export.
getsantry Bot and others added 2 commits October 19, 2026 07:26
A plain scraps Button no longer accepts the DropdownMenu trigger props; use
OverlayTrigger.Button, the trigger variant the other dropdowns use.
Drop the isFullyLoaded/loadedNodes/title props: the export observer is seeded
from the display cache, so it already knows whether a fetch is needed and reads
the title from the pages. The button only needs the conversation.

@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 and found 1 potential issue.

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

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 62c6f34. Configure here.

Comment thread static/app/views/explore/conversations/hooks/useExportConversation.tsx Outdated
Reading the display cache synchronously lets a fully-cached export copy or
download within the click instead of awaiting cancelQueries first, which would
drop the user activation Safari needs. Only the fetch path (cache incomplete)
awaits and shows the spinner.
@ArthurKnaus
ArthurKnaus merged commit 2bb1bc6 into master Oct 19, 2026
91 checks passed
@ArthurKnaus
ArthurKnaus deleted the arthurknaus/tet-3103-copy-conversation-button-no-longer-works-for-large branch October 19, 2026 08:56
RudraPatel2003 pushed a commit that referenced this pull request Oct 19, 2026
The transcript Copy button broke for large conversations once the detail
view stopped loading every span up front, so it silently returned a
partial transcript.

This replaces it with an Export transcript dropdown that copies the
transcript to the clipboard or downloads it as a Markdown file. The
transcript always covers the whole conversation: when the detail view
already has every page cached it is reused instantly, otherwise every
page is fetched on demand behind a loading state. The export fetch is
independent of the display query, so exporting a large conversation does
not bloat the transcript or timeline view.

Closes TET-3103

This branch was successfully deployed

1 active deployment
Preview — 6898be0a Deployed Oct 19, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants