Skip to content

Commit 465e9bc

Browse files
Fix image attachment race condition and path issues [codecane]
- Add status checks in image-card.tsx and image-thumbnail.tsx to skip file reads when status is not ready - Fix path inconsistency in pending-attachments.ts (use resolvedPath in error handling) - Prevents production failures caused by reading files during processing window or with invalid paths 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent b9260b1 commit 465e9bc

File tree

4 files changed

+53
-19
lines changed

4 files changed

+53
-19
lines changed

cli/src/components/image-card.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export interface ImageCardImage {
3434
filename: string
3535
status?: 'processing' | 'ready' | 'error' // Defaults to 'ready' if not provided
3636
note?: string // Display note: 'compressed' | error message
37+
processedImage?: {
38+
base64: string
39+
mediaType: string
40+
}
3741
}
3842

3943
interface ImageCardProps {
@@ -56,20 +60,35 @@ export const ImageCard = ({
5660
// Load thumbnail if terminal supports inline images (iTerm2/Kitty)
5761
useEffect(() => {
5862
if (!canShowInlineImages) return
63+
// Skip loading while image is processing or has error to avoid race condition and unnecessary failed reads
64+
if ((image.status ?? 'ready') !== 'ready') return
5965

6066
let cancelled = false
6167

6268
const loadThumbnail = async () => {
6369
try {
64-
const imageData = fs.readFileSync(image.path)
65-
const base64Data = imageData.toString('base64')
66-
const sequence = renderInlineImage(base64Data, {
67-
width: INLINE_IMAGE_WIDTH,
68-
height: INLINE_IMAGE_HEIGHT,
69-
filename: image.filename,
70-
})
71-
if (!cancelled) {
72-
setThumbnailSequence(sequence)
70+
let base64Data: string | undefined
71+
72+
if (image.processedImage) {
73+
base64Data = image.processedImage.base64
74+
} else if (!image.path.startsWith('clipboard:')) {
75+
const imageData = fs.readFileSync(image.path)
76+
base64Data = imageData.toString('base64')
77+
}
78+
79+
if (base64Data) {
80+
const sequence = renderInlineImage(base64Data, {
81+
width: INLINE_IMAGE_WIDTH,
82+
height: INLINE_IMAGE_HEIGHT,
83+
filename: image.filename,
84+
})
85+
if (!cancelled) {
86+
setThumbnailSequence(sequence)
87+
}
88+
} else {
89+
if (!cancelled) {
90+
setThumbnailSequence(null)
91+
}
7392
}
7493
} catch {
7594
// Failed to load image, will show icon fallback
@@ -84,7 +103,7 @@ export const ImageCard = ({
84103
return () => {
85104
cancelled = true
86105
}
87-
}, [image.path, image.filename, canShowInlineImages])
106+
}, [image, image.filename, canShowInlineImages])
88107

89108
const truncatedName = truncateFilename(image.filename)
90109

@@ -106,7 +125,7 @@ export const ImageCard = ({
106125
<text>{thumbnailSequence}</text>
107126
) : (
108127
<ImageThumbnail
109-
imagePath={image.path}
128+
image={image}
110129
width={THUMBNAIL_WIDTH}
111130
height={THUMBNAIL_HEIGHT}
112131
fallback={<text style={{ fg: theme.info }}>🖼️</text>}

cli/src/components/image-thumbnail.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import React, { useEffect, useState, memo } from 'react'
8+
import { type ImageCardImage } from './image-card'
89

910
import {
1011
extractThumbnailColors,
@@ -13,7 +14,7 @@ import {
1314
} from '../utils/image-thumbnail'
1415

1516
interface ImageThumbnailProps {
16-
imagePath: string
17+
image: ImageCardImage
1718
width: number // Width in cells
1819
height: number // Height in rows (each row uses half-blocks for 2 pixel rows)
1920
fallback?: React.ReactNode
@@ -27,18 +28,32 @@ interface ImageThumbnailProps {
2728
* - ▀ (upper half block) character
2829
*/
2930
export const ImageThumbnail = memo(({
30-
imagePath,
31+
image,
3132
width,
3233
height,
3334
fallback,
3435
}: ImageThumbnailProps) => {
3536
const [thumbnailData, setThumbnailData] = useState<ThumbnailData | null>(null)
3637

3738
useEffect(() => {
39+
// Skip loading while image is processing or has error to avoid race condition and unnecessary failed reads
40+
if ((image.status ?? 'ready') !== 'ready') return
41+
3842
let cancelled = false
3943

4044
const loadThumbnail = async () => {
41-
const data = await extractThumbnailColors(imagePath, width, height)
45+
let data: ThumbnailData | null = null
46+
try {
47+
if (image.processedImage) {
48+
const imageBuffer = Buffer.from(image.processedImage.base64, 'base64')
49+
data = await extractThumbnailColors(imageBuffer, width, height)
50+
} else if (!image.path.startsWith('clipboard:')) {
51+
data = await extractThumbnailColors(image.path, width, height)
52+
}
53+
} catch {
54+
// Ignore errors, will show fallback
55+
}
56+
4257
if (!cancelled) {
4358
setThumbnailData(data)
4459
}
@@ -49,7 +64,7 @@ export const ImageThumbnail = memo(({
4964
return () => {
5065
cancelled = true
5166
}
52-
}, [imagePath, width, height])
67+
}, [image, width, height])
5368

5469
if (!thumbnailData) {
5570
return <>{fallback}</>

cli/src/utils/image-thumbnail.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ export interface ThumbnailData {
2727
* @returns Promise resolving to thumbnail data with pixel colors
2828
*/
2929
export async function extractThumbnailColors(
30-
imagePath: string,
30+
source: string | Buffer,
3131
targetWidth: number,
3232
targetHeight: number,
3333
): Promise<ThumbnailData | null> {
3434
try {
35-
const image = await Jimp.read(imagePath)
35+
const image = await Jimp.read(source)
3636

3737
// Resize to target dimensions (height * 2 because we use half-blocks)
3838
// Use bilinear interpolation for smoother downscaling (sharper than nearest-neighbor)
@@ -61,7 +61,7 @@ export async function extractThumbnailColors(
6161
} catch (error) {
6262
logger.warn(
6363
{
64-
imagePath,
64+
source: typeof source === 'string' ? source : `Buffer(len=${source.length})`,
6565
error: error instanceof Error ? error.message : String(error),
6666
},
6767
'Failed to extract thumbnail colors from image',

cli/src/utils/pending-attachments.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export async function validateAndAddImage(
170170
// Check if file exists
171171
if (!existsSync(resolvedPath)) {
172172
const error = 'file not found'
173-
addPendingImageWithError(imagePath, `❌ ${error}`)
173+
addPendingImageWithError(resolvedPath, `❌ ${error}`)
174174
return { success: false, error }
175175
}
176176

0 commit comments

Comments
 (0)