Skip to content

Commit 754f3aa

Browse files
committed
feat(cli): show image errors in pending banner with auto-remove
- Show "file not found" and "unsupported format" errors in the pending images banner instead of message history - Add isError flag to PendingImage type for proper error detection - Auto-remove error messages after 3 seconds - Style error-only banner with red border - Use pluralize helper for image count
1 parent 34ce6ad commit 754f3aa

File tree

4 files changed

+59
-15
lines changed

4 files changed

+59
-15
lines changed

cli/src/commands/image.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getProjectRoot } from '../project-files'
2-
import { getSystemMessage } from '../utils/message-history'
32
import { validateAndAddImage } from '../utils/add-pending-image'
3+
import { getSystemMessage } from '../utils/message-history'
44

55
import type { PostUserMessageFn } from '../types/contracts/send-message'
66

@@ -30,14 +30,8 @@ export async function handleImageCommand(args: string): Promise<{
3030
const projectRoot = getProjectRoot()
3131

3232
// Validate and add the image (handles path resolution, format check, and processing)
33-
const result = await validateAndAddImage(imagePath, projectRoot)
34-
if (!result.success) {
35-
const postUserMessage: PostUserMessageFn = (prev) => [
36-
...prev,
37-
getSystemMessage(`❌ ${result.error}`),
38-
]
39-
return { postUserMessage }
40-
}
33+
// Errors are shown in the pending images banner with auto-remove
34+
await validateAndAddImage(imagePath, projectRoot)
4135

4236
// Use the optional message as the prompt, or empty to just attach the image
4337
const transformedPrompt = message || ''

cli/src/components/pending-images-banner.tsx

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { pluralize } from '@codebuff/common/util/string'
2+
13
import { ImageCard } from './image-card'
24
import { useTerminalLayout } from '../hooks/use-terminal-layout'
35
import { useTheme } from '../hooks/use-theme'
@@ -10,10 +12,41 @@ export const PendingImagesBanner = () => {
1012
const pendingImages = useChatStore((state) => state.pendingImages)
1113
const removePendingImage = useChatStore((state) => state.removePendingImage)
1214

15+
// Separate error messages from actual images
16+
const errorImages = pendingImages.filter((img) => img.isError)
17+
const validImages = pendingImages.filter((img) => !img.isError)
18+
1319
if (pendingImages.length === 0) {
1420
return null
1521
}
1622

23+
// If we only have errors (no valid images), show just the error messages
24+
if (validImages.length === 0 && errorImages.length > 0) {
25+
return (
26+
<box
27+
style={{
28+
flexDirection: 'column',
29+
marginLeft: width.is('sm') ? 0 : 1,
30+
marginRight: width.is('sm') ? 0 : 1,
31+
borderStyle: 'single',
32+
borderColor: theme.error,
33+
paddingLeft: 1,
34+
paddingRight: 1,
35+
paddingTop: 0,
36+
paddingBottom: 0,
37+
}}
38+
border={['bottom', 'left', 'right']}
39+
customBorderChars={BORDER_CHARS}
40+
>
41+
{errorImages.map((image, index) => (
42+
<text key={`${image.path}-${index}`} style={{ fg: theme.error }}>
43+
{image.note} ({image.filename})
44+
</text>
45+
))}
46+
</box>
47+
)
48+
}
49+
1750
return (
1851
<box
1952
style={{
@@ -30,21 +63,27 @@ export const PendingImagesBanner = () => {
3063
border={['bottom', 'left', 'right']}
3164
customBorderChars={BORDER_CHARS}
3265
>
66+
{/* Error messages shown above the header */}
67+
{errorImages.map((image, index) => (
68+
<text key={`error-${image.path}-${index}`} style={{ fg: theme.error }}>
69+
{image.note} ({image.filename})
70+
</text>
71+
))}
72+
3373
{/* Header */}
3474
<text style={{ fg: theme.info }}>
35-
📎 {pendingImages.length} image{pendingImages.length > 1 ? 's' : ''}{' '}
36-
attached
75+
📎 {pluralize(validImages.length, 'image')} attached
3776
</text>
3877

39-
{/* Image cards in a horizontal row */}
78+
{/* Image cards in a horizontal row - only valid images */}
4079
<box
4180
style={{
4281
flexDirection: 'row',
4382
gap: 1,
4483
flexWrap: 'wrap',
4584
}}
4685
>
47-
{pendingImages.map((image, index) => (
86+
{validImages.map((image, index) => (
4887
<ImageCard
4988
key={`${image.path}-${index}`}
5089
image={image}

cli/src/state/chat-store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export type PendingImage = {
4848
filename: string
4949
size?: number
5050
note?: string // Status: "processing…" | "compressed" | error message
51+
isError?: boolean // True if this is an error entry (e.g., file not found)
5152
processedImage?: {
5253
base64: string
5354
mediaType: string

cli/src/utils/add-pending-image.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,12 @@ export async function addPendingImageFromBase64(
7878
useChatStore.getState().addPendingImage(pendingImage)
7979
}
8080

81+
const AUTO_REMOVE_ERROR_DELAY_MS = 3000
82+
8183
/**
8284
* Add a pending image with an error note (e.g., unsupported format, not found).
8385
* Used when we want to show the image in the banner with an error state.
86+
* Error images are automatically removed after a short delay.
8487
*/
8588
export function addPendingImageWithError(
8689
imagePath: string,
@@ -91,7 +94,13 @@ export function addPendingImageWithError(
9194
path: imagePath,
9295
filename,
9396
note,
97+
isError: true,
9498
})
99+
100+
// Auto-remove error images after a delay
101+
setTimeout(() => {
102+
useChatStore.getState().removePendingImage(imagePath)
103+
}, AUTO_REMOVE_ERROR_DELAY_MS)
95104
}
96105

97106
/**
@@ -107,13 +116,14 @@ export async function validateAndAddImage(
107116

108117
// Check if file exists
109118
if (!existsSync(resolvedPath)) {
110-
return { success: false, error: `Image file not found: ${imagePath}` }
119+
addPendingImageWithError(imagePath, '❌ file not found')
120+
return { success: true }
111121
}
112122

113123
// Check if it's a supported format
114124
if (!isImageFile(resolvedPath)) {
115125
const ext = path.extname(imagePath).toLowerCase()
116-
addPendingImageWithError(resolvedPath, `unsupported format ${ext}`)
126+
addPendingImageWithError(resolvedPath, `unsupported format ${ext}`)
117127
return { success: true }
118128
}
119129

0 commit comments

Comments
 (0)