fix(desktop): render optimistic image thumbnails from in-hand base64
The in-flight user bubble seeded image attachment refs as `@image:<localpath>`.
In remote-gateway mode that path lives on the desktop, not the gateway, so the
inline thumbnail fetch hit /api/media and 403'd ("Path outside media roots"),
flashing a fallback chip until submit uploaded the bytes.
Seed (and keep) image refs as the raw base64 preview data URL instead. It
renders inline via extractEmbeddedImages with zero network, and survives the
post-sync rewrite (the agent gets the bytes through the attached-image pipeline,
not this display ref) so the thumbnail no longer remounts/flashes. Non-image
refs are unchanged.
Adds optimisticAttachmentRef + unit coverage.
This commit is contained in:
@@ -1,6 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { coerceThinkingText } from './chat-runtime'
|
||||
import type { ComposerAttachment } from '@/store/composer'
|
||||
|
||||
import { coerceThinkingText, optimisticAttachmentRef } from './chat-runtime'
|
||||
|
||||
const DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANS'
|
||||
|
||||
function attachment(overrides: Partial<ComposerAttachment> & Pick<ComposerAttachment, 'kind'>): ComposerAttachment {
|
||||
return { id: 'a', label: 'file.png', ...overrides }
|
||||
}
|
||||
|
||||
describe('optimisticAttachmentRef', () => {
|
||||
it('renders an image from its in-hand base64 preview (no @image: path ref)', () => {
|
||||
const ref = optimisticAttachmentRef(attachment({ kind: 'image', detail: '/tmp/shot.png', previewUrl: DATA_URL }))
|
||||
|
||||
// The raw data URL flows through extractEmbeddedImages → inline thumbnail,
|
||||
// dodging the remote /api/media 403 an @image:<localpath> ref would hit.
|
||||
expect(ref).toBe(DATA_URL)
|
||||
})
|
||||
|
||||
it('falls back to an @image: path ref when no preview is available', () => {
|
||||
expect(optimisticAttachmentRef(attachment({ kind: 'image', detail: '/tmp/shot.png' }))).toBe('@image:/tmp/shot.png')
|
||||
})
|
||||
|
||||
it('ignores a non-data preview url and uses the path ref', () => {
|
||||
const ref = optimisticAttachmentRef(
|
||||
attachment({ kind: 'image', detail: '/tmp/shot.png', previewUrl: 'https://example.com/x.png' })
|
||||
)
|
||||
|
||||
expect(ref).toBe('@image:/tmp/shot.png')
|
||||
})
|
||||
|
||||
it('passes non-image attachments straight through to attachmentDisplayText', () => {
|
||||
expect(optimisticAttachmentRef(attachment({ kind: 'file', refText: '@file:src/a.ts', previewUrl: DATA_URL }))).toBe(
|
||||
'@file:src/a.ts'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('coerceThinkingText', () => {
|
||||
it('strips streaming status prefixes from thinking deltas', () => {
|
||||
|
||||
@@ -165,6 +165,29 @@ export function attachmentDisplayText(attachment: ComposerAttachment): string |
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Display ref for the optimistic (in-flight) user bubble.
|
||||
*
|
||||
* Images prefer their in-hand base64 preview (a `data:` URL) over a file path.
|
||||
* `DirectiveContent` runs `extractEmbeddedImages` first, so a raw `data:` URL
|
||||
* renders as an inline thumbnail with zero network. An `@image:<localpath>` ref
|
||||
* would instead route through `/api/media`, which in remote mode 403s ("Path
|
||||
* outside media roots") on a local path the gateway can't read yet — flashing a
|
||||
* fallback chip until submit uploads the bytes. The preview also survives the
|
||||
* post-sync rewrite (bytes go to the agent via the attached-image pipeline, not
|
||||
* this display ref), so the thumbnail stays stable instead of remounting.
|
||||
*
|
||||
* Everything else (files, folders, terminals, post-sync `@file:` refs) falls
|
||||
* through to `attachmentDisplayText`.
|
||||
*/
|
||||
export function optimisticAttachmentRef(attachment: ComposerAttachment): string | null {
|
||||
if (attachment.kind === 'image' && attachment.previewUrl?.startsWith('data:')) {
|
||||
return attachment.previewUrl
|
||||
}
|
||||
|
||||
return attachmentDisplayText(attachment)
|
||||
}
|
||||
|
||||
export function personalityNamesFromConfig(config: unknown): string[] {
|
||||
const root = config && typeof config === 'object' ? (config as Record<string, unknown>) : {}
|
||||
const agent = root.agent && typeof root.agent === 'object' ? (root.agent as Record<string, unknown>) : {}
|
||||
|
||||
Reference in New Issue
Block a user