fix: XML tool-call leak + workspace empty-state + notification text — v0.50.92 (PR #712)
Strips <function_calls> XML from assistant messages before rendering, adds workspace file panel empty-state messages, and changes notification description from 'tab' to 'app'. 16 new tests. Fixes #702, #703, #704.
This commit is contained in:
@@ -147,6 +147,8 @@ const LOCALES = {
|
||||
failed_colon: 'Failed: ',
|
||||
// ui.js
|
||||
no_workspace: 'No workspace',
|
||||
workspace_empty_no_path: 'No workspace selected. Set a workspace in Settings \u2192 Workspace to browse files.',
|
||||
workspace_empty_dir: 'This workspace is empty.',
|
||||
dialog_confirm_title: 'Confirm action',
|
||||
dialog_prompt_title: 'Enter a value',
|
||||
dialog_confirm_btn: 'Confirm',
|
||||
@@ -255,7 +257,7 @@ const LOCALES = {
|
||||
settings_label_sound: 'Notification sound',
|
||||
settings_desc_sound: 'Play a sound when the assistant finishes a response.',
|
||||
settings_label_notifications: 'Browser notifications',
|
||||
settings_desc_notifications: 'Show a system notification when a response completes while the tab is in the background.',
|
||||
settings_desc_notifications: 'Show a system notification when a response completes while the app is in the background.',
|
||||
settings_desc_token_usage: 'Displays input/output token count below each assistant reply. Also toggled with /usage.',
|
||||
settings_desc_bubble_layout: 'Right-align user messages and left-align assistant replies. Off by default to keep code blocks and tool output full-width.',
|
||||
settings_desc_cli_sessions: 'Merges sessions from the Hermes CLI (state.db) into the session list. Click a CLI session to import it and continue the conversation.',
|
||||
@@ -572,6 +574,8 @@ const LOCALES = {
|
||||
failed_colon: 'Error: ',
|
||||
// ui.js
|
||||
no_workspace: 'Sin espacio de trabajo',
|
||||
workspace_empty_no_path: 'No hay espacio de trabajo seleccionado. Configure un espacio de trabajo en Ajustes \u2192 Workspace para explorar archivos.',
|
||||
workspace_empty_dir: 'Este espacio de trabajo está vacío.',
|
||||
// workspace.js
|
||||
unsaved_confirm: 'Tienes cambios sin guardar en la vista previa. ¿Descartar y navegar?',
|
||||
save: 'Guardar',
|
||||
@@ -985,6 +989,8 @@ const LOCALES = {
|
||||
failed_colon: 'Fehlgeschlagen: ',
|
||||
// ui.js
|
||||
no_workspace: 'Kein Workspace',
|
||||
workspace_empty_no_path: 'Kein Workspace ausgewählt. Wähle einen Workspace unter Einstellungen \u2192 Workspace, um Dateien zu durchsuchen.',
|
||||
workspace_empty_dir: 'Dieser Workspace ist leer.',
|
||||
dialog_confirm_title: 'Aktion bestätigen',
|
||||
dialog_prompt_title: 'Wert eingeben',
|
||||
dialog_confirm_btn: 'Bestätigen',
|
||||
@@ -1199,6 +1205,9 @@ const LOCALES = {
|
||||
theme_usage: '\u7528\u6cd5\uff1a/theme ',
|
||||
theme_set: '\u4e3b\u9898\uff1a',
|
||||
no_active_session: '\u5f53\u524d\u6ca1\u6709\u6d3b\u52a8\u4f1a\u8bdd',
|
||||
|
||||
workspace_empty_no_path: '未选择工作区。请在 设置 → 工作区 中设置工作区以浏览文件。',
|
||||
workspace_empty_dir: '此工作区为空。',
|
||||
no_personalities: '\u6ca1\u6709\u627e\u5230\u4eba\u8bbe\uff08\u53ef\u6dfb\u52a0\u5230 ~/.hermes/personalities/\uff09',
|
||||
available_personalities: '\u53ef\u7528\u4eba\u8bbe\uff1a',
|
||||
personality_switch_hint: '\n\n\u4f7f\u7528 `/personality <name>` \u5207\u6362\uff0c\u6216\u7528 `/personality none` \u6e05\u7a7a\u3002',
|
||||
@@ -1611,6 +1620,9 @@ const LOCALES = {
|
||||
theme_usage: '\u7528\u6cd5\uff1a/theme ',
|
||||
theme_set: '\u4e3b\u984c\uff1a',
|
||||
no_active_session: '\u7576\u524d\u6c92\u6709\u6d3b\u52d5\u6703\u8a71',
|
||||
|
||||
workspace_empty_no_path: '未選擇工作區。請在 設定 → 工作區 中設定工作區以瀏覽檔案。',
|
||||
workspace_empty_dir: '此工作區為空。',
|
||||
no_personalities: '\u6c92\u6709\u627e\u5230\u4eba\u8a2d\uff08\u53ef\u6dfb\u52a0\u5230 ~/.hermes/personalities/\uff09',
|
||||
available_personalities: '\u53ef\u7528\u4eba\u8a2d\uff1a',
|
||||
personality_switch_hint: '\n\n\u4f7f\u7528 `/personality <name>` \u5207\u63db\uff0c\u6216\u7528 `/personality none` \u6e05\u7a7a\u3002',
|
||||
|
||||
@@ -390,6 +390,7 @@
|
||||
</div>
|
||||
<div class="breadcrumb-bar" id="breadcrumbBar" style="display:none"></div>
|
||||
<div class="file-tree" id="fileTree"></div>
|
||||
<div id="wsEmptyState" style="display:none;flex:1;align-items:center;justify-content:center;padding:24px 16px;text-align:center;color:var(--muted);font-size:12px;line-height:1.6"></div>
|
||||
<div class="preview-area" id="previewArea">
|
||||
<div class="preview-path" id="previewPath">
|
||||
<span id="previewPathText"></span>
|
||||
|
||||
@@ -229,8 +229,17 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
let _renderPending=false;
|
||||
// Extract display text from assistantText, stripping completed thinking blocks
|
||||
// and hiding content still inside an open thinking block.
|
||||
function _stripXmlToolCalls(s){
|
||||
// Strip <function_calls>...</function_calls> blocks (DeepSeek XML tool syntax).
|
||||
// These are processed as tool calls server-side; showing them raw in the bubble
|
||||
// looks broken. Also handles orphaned opening tags mid-stream. (#702)
|
||||
if(!s||s.toLowerCase().indexOf('<function_calls>')===-1) return s;
|
||||
s=s.replace(/<function_calls>[\s\S]*?<\/function_calls>/gi,'');
|
||||
s=s.replace(/<function_calls>[\s\S]*$/i,'');
|
||||
return s.trim();
|
||||
}
|
||||
function _streamDisplay(){
|
||||
const raw=assistantText;
|
||||
const raw=_stripXmlToolCalls(assistantText);
|
||||
if(reasoningText) return raw;
|
||||
for(const {open,close} of _thinkPairs){
|
||||
// Trim leading whitespace before checking for the open tag — some models
|
||||
@@ -252,7 +261,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
return raw;
|
||||
}
|
||||
function _parseStreamState(){
|
||||
const raw=assistantText;
|
||||
const raw=_stripXmlToolCalls(assistantText);
|
||||
if(reasoningText){
|
||||
return {thinkingText:liveReasoningText, displayText:_streamDisplay(), inThinking:false};
|
||||
}
|
||||
|
||||
27
static/ui.js
27
static/ui.js
@@ -463,6 +463,17 @@ function getModelLabel(modelId){
|
||||
return modelId.split('/').pop()||'Unknown';
|
||||
}
|
||||
|
||||
function _stripXmlToolCallsDisplay(s){
|
||||
// Strip <function_calls>...</function_calls> blocks emitted by DeepSeek and
|
||||
// similar models in their raw response text. These are processed separately
|
||||
// as tool calls; leaving them in the content causes them to render visibly
|
||||
// in the settled chat bubble. (#702)
|
||||
if(!s||s.toLowerCase().indexOf('<function_calls>')===-1) return s;
|
||||
s=s.replace(/<function_calls>[\s\S]*?<\/function_calls>/gi,'');
|
||||
s=s.replace(/<function_calls>[\s\S]*$/i,'');
|
||||
return s.trim();
|
||||
}
|
||||
|
||||
function renderMd(raw){
|
||||
let s=raw||'';
|
||||
// ── MEDIA: token stash (must run first, before any other processing) ───────
|
||||
@@ -1459,7 +1470,7 @@ function renderMessages(){
|
||||
if(m.attachments&&m.attachments.length){
|
||||
filesHtml=`<div class="msg-files">${m.attachments.map(f=>`<div class="msg-file-badge">${li('paperclip',12)} ${esc(f)}</div>`).join('')}</div>`;
|
||||
}
|
||||
const bodyHtml = isUser ? esc(String(content)).replace(/\n/g,'<br>') : renderMd(String(content));
|
||||
const bodyHtml = isUser ? esc(String(content)).replace(/\n/g,'<br>') : renderMd(_stripXmlToolCallsDisplay(String(content)));
|
||||
const editBtn = isUser ? `<button class="msg-action-btn" title="${t('edit_message')}" onclick="editMessage(this)">${li('pencil',13)}</button>` : '';
|
||||
const retryBtn = isLastAssistant ? `<button class="msg-action-btn" title="${t('regenerate')}" onclick="regenerateResponse(this)">${li('rotate-ccw',13)}</button>` : '';
|
||||
const copyBtn = `<button class="msg-copy-btn msg-action-btn" title="${t('copy')}" onclick="copyMsg(this)">${li('copy',13)}</button>`;
|
||||
@@ -2135,6 +2146,20 @@ function renderFileTree(){
|
||||
const box=$('fileTree');box.innerHTML='';
|
||||
// Cache current dir entries
|
||||
S._dirCache[S.currentDir||'.']=S.entries;
|
||||
// Show empty-state when no workspace is set or the directory is empty (#703)
|
||||
const emptyEl=$('wsEmptyState');
|
||||
const hasWorkspace=!!(S.session&&S.session.workspace);
|
||||
if(!hasWorkspace){
|
||||
if(emptyEl){emptyEl.textContent=t('workspace_empty_no_path');emptyEl.style.display='flex';}
|
||||
box.style.display='none';
|
||||
return;
|
||||
}
|
||||
if(emptyEl) emptyEl.style.display='none';
|
||||
box.style.display='';
|
||||
if(!S.entries||!S.entries.length){
|
||||
if(emptyEl){emptyEl.textContent=t('workspace_empty_dir');emptyEl.style.display='flex';}
|
||||
return;
|
||||
}
|
||||
_renderTreeItems(box, S.entries, 0);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user