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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hermes-web-ui",
"version": "0.5.24",
"version": "0.5.25",
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration",
"repository": {
"type": "git",
@@ -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')"
>
+14
View File
@@ -5,6 +5,20 @@ export interface ChangelogEntry {
}
export const changelog: ChangelogEntry[] = [
{
version: '0.5.25',
date: '2026-05-16',
changes: [
'changelog.new_0_5_25_1',
'changelog.new_0_5_25_2',
'changelog.new_0_5_25_3',
'changelog.new_0_5_25_4',
'changelog.new_0_5_25_5',
'changelog.new_0_5_25_6',
'changelog.new_0_5_25_7',
'changelog.new_0_5_25_8',
],
},
{
version: '0.5.24',
date: '2026-05-15',
+8
View File
@@ -909,6 +909,14 @@ jobTriggered: 'Job ausgelost',
new_0_5_23_4: 'Reserve the Web UI port during gateway allocation to avoid startup conflicts',
new_0_5_23_5: 'Fix self-update restart handling so successful helper exits are not reported as failures',
new_0_5_24_1: 'Align Bridge chat with API Server handling for multimodal input, system prompt, and workspace context',
new_0_5_25_1: 'Add group chat room reset and clone actions',
new_0_5_25_2: 'Make the Web UI state directory configurable for custom deployment layouts',
new_0_5_25_3: 'Add MiMo as a TTS provider in voice settings',
new_0_5_25_4: 'Fetch custom provider model lists through the backend to avoid browser CORS failures',
new_0_5_25_5: 'Fix tool approval flow for bridge sessions',
new_0_5_25_6: 'Remove the forced CLI platform hint from bridge prompts so custom media/file instructions are preserved',
new_0_5_25_7: 'Show base64 image content correctly in user message history',
new_0_5_25_8: 'Add Playwright browser tests, chat streaming contract coverage, provider model coverage, and coverage baseline',
new_0_5_5_1: '🎉 Tag der Arbeit! Heute wird nicht gearbeitet, bitte habt Verständnis',
new_0_5_5_2: 'Verlaufsseite für Hermes-Sitzungshistorie hinzugefügt',
new_0_5_5_3: 'Verlaufsseite verwaltet Sitzungen unabhängig ohne Störung des aktiven Chats',
+8
View File
@@ -1192,6 +1192,14 @@ export default {
new_0_5_23_4: 'Reserve the Web UI port during gateway allocation to avoid startup conflicts',
new_0_5_23_5: 'Fix self-update restart handling so successful helper exits are not reported as failures',
new_0_5_24_1: 'Align Bridge chat with API Server handling for multimodal input, system prompt, and workspace context',
new_0_5_25_1: 'Add group chat room reset and clone actions',
new_0_5_25_2: 'Make the Web UI state directory configurable for custom deployment layouts',
new_0_5_25_3: 'Add MiMo as a TTS provider in voice settings',
new_0_5_25_4: 'Fetch custom provider model lists through the backend to avoid browser CORS failures',
new_0_5_25_5: 'Fix tool approval flow for bridge sessions',
new_0_5_25_6: 'Remove the forced CLI platform hint from bridge prompts so custom media/file instructions are preserved',
new_0_5_25_7: 'Show base64 image content correctly in user message history',
new_0_5_25_8: 'Add Playwright browser tests, chat streaming contract coverage, provider model coverage, and coverage baseline',
new_0_5_6_1: 'Add voice playback feature with Web Speech API: manual button, auto-play toggle, rainbow border animation, and mobile optimization',
new_0_5_6_2: 'Add robust LLM JSON parser with tolerance for Python format and extract text from streaming events',
+8
View File
@@ -905,6 +905,14 @@ jobTriggered: 'Job ejecutado',
new_0_5_23_4: 'Reserve the Web UI port during gateway allocation to avoid startup conflicts',
new_0_5_23_5: 'Fix self-update restart handling so successful helper exits are not reported as failures',
new_0_5_24_1: 'Align Bridge chat with API Server handling for multimodal input, system prompt, and workspace context',
new_0_5_25_1: 'Add group chat room reset and clone actions',
new_0_5_25_2: 'Make the Web UI state directory configurable for custom deployment layouts',
new_0_5_25_3: 'Add MiMo as a TTS provider in voice settings',
new_0_5_25_4: 'Fetch custom provider model lists through the backend to avoid browser CORS failures',
new_0_5_25_5: 'Fix tool approval flow for bridge sessions',
new_0_5_25_6: 'Remove the forced CLI platform hint from bridge prompts so custom media/file instructions are preserved',
new_0_5_25_7: 'Show base64 image content correctly in user message history',
new_0_5_25_8: 'Add Playwright browser tests, chat streaming contract coverage, provider model coverage, and coverage baseline',
new_0_5_5_1: '🎉 ¡Feliz Día del Trabajo! Hoy no se trabaja, agradezcan su comprensión',
new_0_5_5_2: 'Añadida página de historial para sesiones Hermes',
new_0_5_5_3: 'La página de historial gestiona sesiones de forma independiente',
+8
View File
@@ -904,6 +904,14 @@ jobTriggered: 'Job declenche',
new_0_5_23_4: 'Reserve the Web UI port during gateway allocation to avoid startup conflicts',
new_0_5_23_5: 'Fix self-update restart handling so successful helper exits are not reported as failures',
new_0_5_24_1: 'Align Bridge chat with API Server handling for multimodal input, system prompt, and workspace context',
new_0_5_25_1: 'Add group chat room reset and clone actions',
new_0_5_25_2: 'Make the Web UI state directory configurable for custom deployment layouts',
new_0_5_25_3: 'Add MiMo as a TTS provider in voice settings',
new_0_5_25_4: 'Fetch custom provider model lists through the backend to avoid browser CORS failures',
new_0_5_25_5: 'Fix tool approval flow for bridge sessions',
new_0_5_25_6: 'Remove the forced CLI platform hint from bridge prompts so custom media/file instructions are preserved',
new_0_5_25_7: 'Show base64 image content correctly in user message history',
new_0_5_25_8: 'Add Playwright browser tests, chat streaming contract coverage, provider model coverage, and coverage baseline',
new_0_5_5_1: '🎉 Joyeuse Fête du Travail! Pas de travail aujourd\'hui, merci de votre compréhension',
new_0_5_5_2: 'Ajout d\'une page d\'historique pour les sessions Hermes',
new_0_5_5_3: 'La page d\'historique gère les sessions de manière indépendante',
+8
View File
@@ -905,6 +905,14 @@ export default {
new_0_5_23_4: 'Reserve the Web UI port during gateway allocation to avoid startup conflicts',
new_0_5_23_5: 'Fix self-update restart handling so successful helper exits are not reported as failures',
new_0_5_24_1: 'Align Bridge chat with API Server handling for multimodal input, system prompt, and workspace context',
new_0_5_25_1: 'Add group chat room reset and clone actions',
new_0_5_25_2: 'Make the Web UI state directory configurable for custom deployment layouts',
new_0_5_25_3: 'Add MiMo as a TTS provider in voice settings',
new_0_5_25_4: 'Fetch custom provider model lists through the backend to avoid browser CORS failures',
new_0_5_25_5: 'Fix tool approval flow for bridge sessions',
new_0_5_25_6: 'Remove the forced CLI platform hint from bridge prompts so custom media/file instructions are preserved',
new_0_5_25_7: 'Show base64 image content correctly in user message history',
new_0_5_25_8: 'Add Playwright browser tests, chat streaming contract coverage, provider model coverage, and coverage baseline',
new_0_5_5_1: '🎉 労働者の日!今日はお休みです、何卒ご理解ください',
new_0_5_5_2: 'Hermesセッション履歴ページを追加',
new_0_5_5_3: '履歴ページはアクティブチャットに干渉せずにセッション管理',
+8
View File
@@ -905,6 +905,14 @@ export default {
new_0_5_23_4: 'Reserve the Web UI port during gateway allocation to avoid startup conflicts',
new_0_5_23_5: 'Fix self-update restart handling so successful helper exits are not reported as failures',
new_0_5_24_1: 'Align Bridge chat with API Server handling for multimodal input, system prompt, and workspace context',
new_0_5_25_1: 'Add group chat room reset and clone actions',
new_0_5_25_2: 'Make the Web UI state directory configurable for custom deployment layouts',
new_0_5_25_3: 'Add MiMo as a TTS provider in voice settings',
new_0_5_25_4: 'Fetch custom provider model lists through the backend to avoid browser CORS failures',
new_0_5_25_5: 'Fix tool approval flow for bridge sessions',
new_0_5_25_6: 'Remove the forced CLI platform hint from bridge prompts so custom media/file instructions are preserved',
new_0_5_25_7: 'Show base64 image content correctly in user message history',
new_0_5_25_8: 'Add Playwright browser tests, chat streaming contract coverage, provider model coverage, and coverage baseline',
new_0_5_5_1: '🎉 노동절 감사합니다! 오늘은 쉬니까 양해 부탁드립니다',
new_0_5_5_2: 'Hermes 세션 기록 페이지 추가',
new_0_5_5_3: '기록 페이지는 독립적으로 세션 관리',
+8
View File
@@ -905,6 +905,14 @@ jobTriggered: 'Job acionado',
new_0_5_23_4: 'Reserve the Web UI port during gateway allocation to avoid startup conflicts',
new_0_5_23_5: 'Fix self-update restart handling so successful helper exits are not reported as failures',
new_0_5_24_1: 'Align Bridge chat with API Server handling for multimodal input, system prompt, and workspace context',
new_0_5_25_1: 'Add group chat room reset and clone actions',
new_0_5_25_2: 'Make the Web UI state directory configurable for custom deployment layouts',
new_0_5_25_3: 'Add MiMo as a TTS provider in voice settings',
new_0_5_25_4: 'Fetch custom provider model lists through the backend to avoid browser CORS failures',
new_0_5_25_5: 'Fix tool approval flow for bridge sessions',
new_0_5_25_6: 'Remove the forced CLI platform hint from bridge prompts so custom media/file instructions are preserved',
new_0_5_25_7: 'Show base64 image content correctly in user message history',
new_0_5_25_8: 'Add Playwright browser tests, chat streaming contract coverage, provider model coverage, and coverage baseline',
new_0_5_5_1: '🎉 Feliz Dia do Trabalhador! Hoje não se trabalha, obrigado pela compreensão',
new_0_5_5_2: 'Adicionada página de histórico para sessões Hermes',
new_0_5_5_3: 'Página de histórico gerencia sessões de forma independente',
@@ -1194,6 +1194,14 @@ export default {
new_0_5_23_4: 'gateway 分配連接埠時保留 Web UI 連接埠,避免啟動連接埠衝突',
new_0_5_23_5: '修復自更新重啟邏輯,避免將 restart helper 的成功退出誤報為失敗',
new_0_5_24_1: '對齊 Bridge 聊天與 API Server 的多模態輸入、系統提示詞和工作區上下文處理',
new_0_5_25_1: '新增群聊房間重設和複製操作',
new_0_5_25_2: '支援設定 Web UI 狀態目錄,方便自訂部署目錄結構',
new_0_5_25_3: '語音設定新增 MiMo TTS 提供商',
new_0_5_25_4: '自訂 Provider 模型清單改由後端代理請求,避免瀏覽器跨域失敗',
new_0_5_25_5: '修復 Bridge 工作階段的工具授權流程',
new_0_5_25_6: '移除 Bridge 強制注入的 CLI 平台提示,保留使用者自訂媒體和檔案輸出規則',
new_0_5_25_7: '使用者訊息歷史支援正確展示 base64 圖片內容',
new_0_5_25_8: '新增 Playwright 瀏覽器測試、聊天串流契約覆蓋、Provider 模型測試和覆蓋率基線',
new_0_5_6_1: '新增語音播放功能:使用 Web Speech API,支援手動播放按鈕、自動播放開關、彩虹邊框動畫和行動端最佳化',
new_0_5_6_2: '新增強健的 LLM JSON 解析器,相容 Python 格式並從串流事件中擷取文字',
new_0_5_6_3: 'Skills 功能增強:使用統計、來源過濾、封存技能、來源追溯和釘選切換',
+8
View File
@@ -1194,6 +1194,14 @@ export default {
new_0_5_23_4: 'gateway 分配端口时保留 Web UI 端口,避免启动端口冲突',
new_0_5_23_5: '修复自更新重启逻辑,避免将 restart helper 的成功退出误报为失败',
new_0_5_24_1: '对齐 Bridge 聊天与 API Server 的多模态输入、系统提示词和工作区上下文处理',
new_0_5_25_1: '新增群聊房间重置和克隆操作',
new_0_5_25_2: '支持配置 Web UI 状态目录,方便自定义部署目录结构',
new_0_5_25_3: '语音设置新增 MiMo TTS 提供商',
new_0_5_25_4: '自定义 Provider 模型列表改由后端代理请求,避免浏览器跨域失败',
new_0_5_25_5: '修复 Bridge 会话的工具授权流程',
new_0_5_25_6: '移除 Bridge 强制注入的 CLI 平台提示,保留用户自定义媒体和文件输出规则',
new_0_5_25_7: '用户消息历史支持正确展示 base64 图片内容',
new_0_5_25_8: '新增 Playwright 浏览器测试、聊天流式契约覆盖、Provider 模型测试和覆盖率基线',
new_0_5_6_1: '新增语音播放功能:使用 Web Speech API,支持手动播放按钮、自动播放开关、彩虹边框动画和移动端优化',
new_0_5_6_2: '新增健壮的 LLM JSON 解析器,兼容 Python 格式并从流式事件中提取文本',
@@ -280,12 +280,13 @@ export class AgentBridgeClient {
conversationHistory?: unknown[],
instructions?: string,
profile?: string,
options: { force_compress?: boolean } = {},
options: { force_compress?: boolean; storage_message?: AgentBridgeMessage } = {},
): Promise<AgentBridgeChatStarted> {
return this.request<AgentBridgeChatStarted>({
action: 'chat',
session_id: sessionId,
message,
...(options.storage_message !== undefined ? { storage_message: options.storage_message } : {}),
...(conversationHistory ? { conversation_history: conversationHistory } : {}),
...(instructions ? { instructions } : {}),
...(profile ? { profile } : {}),
@@ -719,10 +719,12 @@ class AgentPool:
self,
session: AgentSession,
message: Any,
storage_message: Any | None,
conversation_history: list[dict[str, Any]] | None,
profile: str | None,
) -> bool:
user_content = str(message) if not isinstance(message, dict) else str(message.get("content", message))
persist_message = storage_message if storage_message is not None else message
user_content = str(persist_message) if not isinstance(persist_message, dict) else str(persist_message.get("content", persist_message))
if not user_content.strip():
return False
@@ -839,6 +841,7 @@ class AgentPool:
self,
session_id: str,
message: Any,
storage_message: Any | None = None,
instructions: str | None = None,
conversation_history: list[dict[str, Any]] | None = None,
profile: str | None = None,
@@ -858,14 +861,14 @@ class AgentPool:
thread = threading.Thread(
target=self._run_chat,
args=(session, record, message, instructions, conversation_history, profile, force_compress),
args=(session, record, message, storage_message, instructions, conversation_history, profile, force_compress),
daemon=True,
name=f"hermes-bridge-run-{run_id[:8]}",
)
thread.start()
return record
def _run_chat(self, session: AgentSession, record: RunRecord, message: Any, instructions: str | None = None, conversation_history: list[dict[str, Any]] | None = None, profile: str | None = None, force_compress: bool = False) -> None:
def _run_chat(self, session: AgentSession, record: RunRecord, message: Any, storage_message: Any | None = None, instructions: str | None = None, conversation_history: list[dict[str, Any]] | None = None, profile: str | None = None, force_compress: bool = False) -> None:
with self._run_lock:
def stream_callback(delta: str) -> None:
with self._lock:
@@ -888,7 +891,7 @@ class AgentPool:
os.environ["HERMES_EXEC_ASK"] = "1"
except Exception:
previous_approval_callback = None
self._prepersist_user_message(session, message, conversation_history, profile)
self._prepersist_user_message(session, message, storage_message, conversation_history, profile)
db_count_after_prepersist = self._session_db_message_count(session.session_id, profile)
if force_compress:
compress = getattr(session.agent, "_compress_context", None)
@@ -1154,12 +1157,14 @@ class BridgeServer:
if action == "chat":
session_id = str(req.get("session_id") or "").strip() or uuid.uuid4().hex
message = req.get("message", req.get("input", ""))
storage_message = req.get("storage_message")
instructions = req.get("instructions") or req.get("system_message")
conversation_history = req.get("conversation_history")
profile = req.get("profile")
record = self.pool.start_chat(
session_id,
message,
storage_message,
instructions,
conversation_history,
profile,
@@ -124,6 +124,9 @@ export async function handleBridgeRun(
const bridgeInput = isContentBlockArray(input)
? await convertContentBlocksForAgent(input)
: input
const bridgeStorageInput = isContentBlockArray(input)
? inputStr
: undefined
logger.info('[chat-run-socket] starting CLI bridge run for session %s', session_id)
bridgeLogger.info({
sessionId: session_id,
@@ -133,7 +136,14 @@ export async function handleBridgeRun(
hasInstructions: Boolean(fullInstructions),
multimodalInput: isContentBlockArray(input),
}, '[chat-run-socket] starting CLI bridge run')
const started = await bridge.chat(session_id, bridgeInput as AgentBridgeMessage, bridgeHistory, fullInstructions, profile)
const started = await bridge.chat(
session_id,
bridgeInput as AgentBridgeMessage,
bridgeHistory,
fullInstructions,
profile,
bridgeStorageInput !== undefined ? { storage_message: bridgeStorageInput } : {},
)
state.runId = started.run_id
bridgeLogger.info({
sessionId: session_id,