@@ -11,6 +11,160 @@ function cleanupCodeBlocks(content: string): string {
1111 return content . replace ( / ( [ a - z A - Z 0 - 9 _ + - ] + ) \s * ` ` ` \1\b / g, '```$1' ) ;
1212}
1313
14+ /**
15+ * Extract pasted content from Claude's pasted document side panel
16+ * @param pastedBlock - The pasted document element to click
17+ * @returns The extracted content with title and index, or null if extraction failed
18+ */
19+ async function extractClaudePastedContent ( pastedBlock : Element ) : Promise < string | null > {
20+ const MAX_RETRIES = 3 ;
21+ const BASE_WAIT_TIME = 1500 ;
22+
23+ for ( let attempt = 0 ; attempt < MAX_RETRIES ; attempt ++ ) {
24+ try {
25+ const pastedTitle = 'Pasted Content' ;
26+ // click the pasted block to open the side panel
27+ ( pastedBlock as HTMLElement ) . click ( ) ;
28+
29+ // wait for the side panel to open and load content
30+ const waitTime = BASE_WAIT_TIME + attempt * 500 ;
31+ await new Promise ( ( resolve ) => setTimeout ( resolve , waitTime ) ) ;
32+
33+ // find the content in the side panel
34+ const contentElement = document . querySelector ( '.whitespace-pre-wrap.break-all.text-xs' ) ;
35+ if ( ! contentElement ) {
36+ console . error ( 'Could not find pasted content element' ) ;
37+ // try to close any open panel
38+ const closeButtons = document . querySelectorAll (
39+ 'button[aria-label="Close"], button svg path[d*="15.8536"]'
40+ ) ;
41+ if ( closeButtons . length > 0 ) {
42+ const closeBtn = closeButtons [ 0 ] . closest ( 'button' ) as HTMLButtonElement ;
43+ if ( closeBtn ) closeBtn . click ( ) ;
44+ }
45+ return null ;
46+ }
47+
48+ // extract the content text
49+ const pastedContent = contentElement . textContent || '' ;
50+
51+ if ( pastedContent ) {
52+ // close the pasted panel
53+ const closeButton = document . querySelector (
54+ 'button[data-testid="close-file-preview"]'
55+ ) as HTMLButtonElement ;
56+ if ( closeButton ) {
57+ closeButton . click ( ) ;
58+ await new Promise ( ( resolve ) => setTimeout ( resolve , 200 ) ) ;
59+ }
60+
61+ // return formatted with title and index
62+ return `${ pastedTitle } \n\n\`\`\`\n${ pastedContent } \n\`\`\`` ;
63+ } else {
64+ console . warn (
65+ `Pasted content extraction attempt ${ attempt + 1 } : No content detected, retrying...`
66+ ) ;
67+ // close panel and retry
68+ const closeButton = document . querySelector (
69+ 'button[data-testid="close-file-preview"]'
70+ ) as HTMLButtonElement ;
71+ if ( closeButton ) {
72+ closeButton . click ( ) ;
73+ await new Promise ( ( resolve ) => setTimeout ( resolve , 200 ) ) ;
74+ }
75+ continue ;
76+ }
77+ } catch ( error ) {
78+ console . error ( `Pasted content extraction attempt ${ attempt + 1 } failed:` , error ) ;
79+ if ( attempt === MAX_RETRIES - 1 ) {
80+ console . error ( 'All pasted content extraction attempts failed' ) ;
81+ return null ;
82+ }
83+ // wait before retry
84+ await new Promise ( ( resolve ) => setTimeout ( resolve , 500 ) ) ;
85+ }
86+ }
87+
88+ console . error ( 'All pasted content extraction attempts failed' ) ;
89+ return null ;
90+ }
91+
92+ /**
93+ * Extract all pasted content from a Claude user message container
94+ * @param messageContainer - The parent container of the user message
95+ * @returns Array of extracted pasted content blocks
96+ */
97+ export async function extractAllClaudePastedContent ( messageContainer : Element ) : Promise < string [ ] > {
98+ const pastedContents : string [ ] = [ ] ;
99+ let extractionErrors = 0 ;
100+
101+ try {
102+ // find all pasted document blocks in this message
103+ // look for elements with "pasted" badge
104+ const pastedBlocks : Element [ ] = [ ] ;
105+
106+ // First, look for regular file thumbnails with pasted badges
107+ const fileThumbnails = messageContainer . querySelectorAll ( 'div[data-testid="file-thumbnail"]' ) ;
108+ fileThumbnails . forEach ( ( thumbnail ) => {
109+ const flexCol = thumbnail . querySelector ( '.flex-col' ) ;
110+ if ( flexCol ) {
111+ const badgeElement = flexCol . querySelector ( '.text-text-300' ) ;
112+ if ( badgeElement && badgeElement . textContent ?. toLowerCase ( ) . includes ( 'pasted' ) ) {
113+ pastedBlocks . push ( flexCol ) ;
114+ }
115+ }
116+ } ) ;
117+
118+ // Also look for pasted documents that might be direct children (for pasted-only messages)
119+ const directPastedElements = messageContainer . querySelectorAll ( '.flex-col .text-text-300' ) ;
120+ directPastedElements . forEach ( ( badgeElement ) => {
121+ if ( badgeElement . textContent ?. toLowerCase ( ) . includes ( 'pasted' ) ) {
122+ const flexCol = badgeElement . closest ( '.flex-col' ) ;
123+ if ( flexCol && ! pastedBlocks . includes ( flexCol ) ) {
124+ pastedBlocks . push ( flexCol ) ;
125+ }
126+ }
127+ } ) ;
128+
129+ console . log ( `Found ${ pastedBlocks . length } pasted content blocks in message container` ) ;
130+
131+ for ( let i = 0 ; i < pastedBlocks . length ; i ++ ) {
132+ const pastedBlock = pastedBlocks [ i ] ;
133+ console . log ( `Extracting pasted content ${ i + 1 } /${ pastedBlocks . length } ` ) ;
134+
135+ try {
136+ const pastedContent = await extractClaudePastedContent ( pastedBlock ) ;
137+ if ( pastedContent ) {
138+ pastedContents . push ( pastedContent ) ;
139+ console . log ( `Successfully extracted pasted content ${ i + 1 } ` ) ;
140+ } else {
141+ extractionErrors ++ ;
142+ console . warn ( `Failed to extract pasted content ${ i + 1 } : No content returned` ) ;
143+ }
144+ } catch ( error ) {
145+ extractionErrors ++ ;
146+ console . error ( `Error extracting pasted content ${ i + 1 } :` , error ) ;
147+ }
148+
149+ // small delay between pasted contents to prevent overwhelming the UI
150+ await new Promise ( ( resolve ) => setTimeout ( resolve , 300 ) ) ;
151+ }
152+
153+ if ( extractionErrors > 0 ) {
154+ console . warn (
155+ `Pasted content extraction completed with ${ extractionErrors } errors out of ${ pastedBlocks . length } total pasted contents`
156+ ) ;
157+ } else {
158+ console . log ( `Successfully extracted all ${ pastedContents . length } pasted contents` ) ;
159+ }
160+
161+ return pastedContents ;
162+ } catch ( error ) {
163+ console . error ( 'Critical error in extractAllClaudePastedContent:' , error ) ;
164+ return pastedContents ; // return whatever we managed to extract
165+ }
166+ }
167+
14168export async function getClaudePastedContent ( div : Element ) : Promise < string | null > {
15169 // find all file and image elements
16170 const fileElements = div . querySelectorAll (
@@ -69,9 +223,21 @@ export const getClaudeChatContent = async () => {
69223 // claude.ai
70224 // user messages have data-testid="user-message"
71225 // assistant messages have class="font-claude-response"
226+ // also look for pasted-only messages (no data-testid but contain pasted documents)
72227 const userMessages = document . querySelectorAll ( '[data-testid="user-message"]' ) ;
73228 const assistantMessages = document . querySelectorAll ( 'div.font-claude-response' ) ;
74229
230+ // find pasted-only messages (messages that contain pasted documents but no data-testid)
231+ const pastedOnlyMessages : Element [ ] = [ ] ;
232+ const messageGroups = document . querySelectorAll ( 'div[data-test-render-count]' ) ;
233+
234+ messageGroups . forEach ( ( group ) => {
235+ const badgeElement = group . querySelector ( '.text-text-300' ) ;
236+ if ( badgeElement && badgeElement . textContent ?. toLowerCase ( ) . includes ( 'pasted' ) ) {
237+ pastedOnlyMessages . push ( group ) ;
238+ }
239+ } ) ;
240+
75241 let failedClaudeMessages = 0 ;
76242 const claudeMessages : Array < Message > = [ ] ;
77243
@@ -91,6 +257,13 @@ export const getClaudeChatContent = async () => {
91257 allMessages . push ( { element : el , role : 'assistant' , position } ) ;
92258 } ) ;
93259
260+ // add pasted-only messages as user messages
261+ pastedOnlyMessages . forEach ( ( el ) => {
262+ const rect = el . getBoundingClientRect ( ) ;
263+ const position = rect . top + window . scrollY ;
264+ allMessages . push ( { element : el , role : 'user' , position } ) ;
265+ } ) ;
266+
94267 // sort by position (top to bottom)
95268 allMessages . sort ( ( a , b ) => a . position - b . position ) ;
96269
@@ -132,24 +305,53 @@ export const getClaudeChatContent = async () => {
132305 // for user messages, handle regular text, code blocks, and pasted content
133306 const messageParts : string [ ] = [ ] ;
134307
135- // look for pasted/attached content (images, files)
136- // find the parent message container and look for thumbnails
137- const messageContainer = div . closest ( '[data-test-render-count]' ) ;
138- const thumbnailContainer = messageContainer ?. querySelector ( 'div.group\\/thumbnail' )
139- ?. parentElement ?. parentElement as HTMLElement ;
308+ // check if this is a pasted-only message (no data-testid attribute)
309+ const isPastedOnlyMessage = ! div . hasAttribute ( 'data-testid' ) ;
140310
141- if ( thumbnailContainer ) {
142- const pastedContent = await getClaudePastedContent ( thumbnailContainer ) ;
143- if ( pastedContent ) {
144- messageParts . push ( pastedContent ) ;
311+ if ( isPastedOnlyMessage ) {
312+ // this is a pasted-only message, extract pasted content directly from the message element
313+ const pastedContents = await extractAllClaudePastedContent ( div ) ;
314+ if ( pastedContents . length > 0 ) {
315+ // format pasted content with indices
316+ pastedContents . forEach ( ( pastedContent , index ) => {
317+ messageParts . push ( `Pasted Content #${ index + 1 } :\n\n${ pastedContent } ` ) ;
318+ } ) ;
145319 await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
146320 }
147- }
321+ } else {
322+ // this is a regular user message, handle both pasted content and text
323+ // look for pasted/attached content (images, files)
324+ // find the parent message container and look for thumbnails
325+ const messageContainer = div . closest ( '[data-test-render-count]' ) ;
326+
327+ if ( messageContainer ) {
328+ // extract pasted content from pasted document blocks
329+ const pastedContents = await extractAllClaudePastedContent ( messageContainer ) ;
330+ if ( pastedContents . length > 0 ) {
331+ // format pasted content with indices
332+ pastedContents . forEach ( ( pastedContent , index ) => {
333+ messageParts . push ( `Pasted Content #${ index + 1 } :\n\n${ pastedContent } ` ) ;
334+ } ) ;
335+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
336+ }
148337
149- // extract text content from user message
150- const textContent = await extractFormattedText ( div ) ;
151- if ( textContent ) {
152- messageParts . push ( textContent ) ;
338+ // also handle regular file thumbnails (non-pasted)
339+ const thumbnailContainer = messageContainer ?. querySelector ( 'div.group\\/thumbnail' )
340+ ?. parentElement ?. parentElement as HTMLElement ;
341+ if ( thumbnailContainer ) {
342+ const regularPastedContent = await getClaudePastedContent ( thumbnailContainer ) ;
343+ if ( regularPastedContent ) {
344+ messageParts . push ( regularPastedContent ) ;
345+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
346+ }
347+ }
348+ }
349+
350+ // extract text content from user message
351+ const textContent = await extractFormattedText ( div ) ;
352+ if ( textContent ) {
353+ messageParts . push ( textContent ) ;
354+ }
153355 }
154356
155357 content = messageParts . join ( '\n' ) ;
0 commit comments