prepare 0.5.25 changelog (#778)

This commit is contained in:
ekko
2026-05-16 09:40:25 +08:00
committed by GitHub
parent 07257a8964
commit cf9d0c6008
15 changed files with 208 additions and 27 deletions
@@ -41,22 +41,72 @@ const statusItems = computed(() => {
];
});
type DisplayContentFile = {
type: 'image' | 'file'
name: string
path?: string
url?: string
}
function getBlockText(block: any): string {
if (!block || typeof block !== 'object') return ''
if (block.type === 'text' || block.type === 'input_text') {
return typeof block.text === 'string' ? block.text : ''
}
return ''
}
function getImageUrlFromBlock(block: any): string | null {
if (!block || typeof block !== 'object') return null
if (block.type !== 'input_image' && block.type !== 'image_url') return null
const raw = block.image_url
if (typeof raw === 'string') return raw
if (raw && typeof raw === 'object' && typeof raw.url === 'string') return raw.url
return null
}
function imageNameFromDataUrl(url: string, index: number): string {
const match = url.match(/^data:image\/([^;,]+)/i)
const ext = match?.[1] === 'jpeg' ? 'jpg' : match?.[1] || 'png'
return `image-${index + 1}.${ext}`
}
function parseContentBlocks(content: string): Array<ContentBlock | Record<string, unknown>> | null {
const trimmed = content.trim()
if (!trimmed) return null
const parse = (value: string) => {
const parsed = JSON.parse(value)
return Array.isArray(parsed) && parsed.length > 0 && 'type' in parsed[0]
? parsed as Array<ContentBlock | Record<string, unknown>>
: null
}
try {
return parse(trimmed)
} catch {
// Hermes Agent stored some multimodal user messages via Python str(list),
// e.g. [{'type': 'text'}, {'type': 'image_url', ...}]. Convert that
// legacy repr into JSON for display only.
if (!trimmed.startsWith("[{'") && !trimmed.startsWith('[{"')) return null
try {
return parse(
trimmed
.replace(/\bNone\b/g, 'null')
.replace(/\bTrue\b/g, 'true')
.replace(/\bFalse\b/g, 'false')
.replace(/'/g, '"'),
)
} catch {
return null
}
}
}
// Parse ContentBlock[] from JSON string
const contentBlocks = computed(() => {
const content = props.message.content || '';
if (!content.trim()) return null;
try {
// Try to parse as ContentBlock[] array
const parsed = JSON.parse(content);
if (Array.isArray(parsed) && parsed.length > 0 && 'type' in parsed[0]) {
return parsed as ContentBlock[];
}
} catch {
// Not valid JSON, treat as plain text
}
return null;
return parseContentBlocks(content);
});
// Check if content is in ContentBlock[] format
@@ -70,16 +120,40 @@ const displayText = computed(() => {
// Extract text from blocks
return contentBlocks.value!
.filter(block => block.type === 'text')
.map(block => block.text)
.map(block => getBlockText(block))
.filter(Boolean)
.join('\n');
});
// Extract files from ContentBlock[]
const contentFiles = computed(() => {
const contentFiles = computed<DisplayContentFile[] | null>(() => {
if (!isContentBlockArray.value) return null;
return contentBlocks.value!.filter(block => block.type === 'image' || block.type === 'file');
return contentBlocks.value!.flatMap<DisplayContentFile>((block, index) => {
if (block.type === 'image') {
return [{
type: 'image' as const,
name: String((block as any).name || `image-${index + 1}`),
path: String((block as any).path || ''),
}].filter(file => file.path)
}
if (block.type === 'file') {
return [{
type: 'file' as const,
name: String((block as any).name || `file-${index + 1}`),
path: String((block as any).path || ''),
}].filter(file => file.path)
}
const imageUrl = getImageUrlFromBlock(block)
if (imageUrl?.startsWith('data:image/')) {
return [{
type: 'image' as const,
name: imageNameFromDataUrl(imageUrl, index),
url: imageUrl,
}]
}
return []
});
});
// Generate download URL with auth token
@@ -89,6 +163,11 @@ function getDownloadUrl(path: string, name: string): string {
return token ? `${base}&token=${encodeURIComponent(token)}` : base;
}
function getContentFileUrl(file: DisplayContentFile): string {
if (file.url) return file.url
return file.path ? getDownloadUrl(file.path, file.name) : ''
}
const toolExpanded = ref(false);
const previewUrl = ref<string | null>(null);
@@ -721,16 +800,16 @@ onBeforeUnmount(() => {
>
<template v-if="file.type === 'image'">
<img
:src="getDownloadUrl(file.path, file.name)"
:src="getContentFileUrl(file)"
:alt="file.name"
class="msg-attachment-thumb"
@click="previewUrl = getDownloadUrl(file.path, file.name)"
@click="previewUrl = getContentFileUrl(file)"
/>
</template>
<template v-else>
<div
class="msg-attachment-file"
@click="downloadFile(file.path, file.name).catch(err => toast.error(err.message || t('download.downloadFailed')))"
@click="file.path && downloadFile(file.path, file.name).catch(err => toast.error(err.message || t('download.downloadFailed')))"
style="cursor: pointer;"
:title="t('download.downloadFile')"
>