Merge pull request #5040 from nesquena/release/stage-5036-notif

Release: completion notification previews settled final answer (#5036, fixes #5035)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-06-26 22:07:57 -07:00
commit d38aa51a8c
3 files changed
+57 -3

No files matched your search

+2
View File
@@ -5,6 +5,8 @@
### Fixed
- **The "Response complete" notification now previews the final answer, not the start of the stream.** The browser/PWA completion notification body was built from the live streaming accumulator sliced to 100 chars, so it showed the *beginning* of the response (often a preamble or empty) rather than what the user actually got. It now derives the preview from the settled final answer (anchor projection → message content → raw text), strips inline thinking, normalizes whitespace, and truncates at 100 chars — and works for completions that finish while the session isn't the active pane. Thanks @claw-io. (#5036, fixes #5035)
- **Onboarding now has the same searchable model picker as Settings.** A fresh install (e.g. an OpenRouter-only first run) previously showed only a small default model list in the onboarding step — no search box and no way to enter a custom model ID — so picking a model not in the short list meant finishing setup and fixing it later. Onboarding now reuses the same searchable picker (with custom-model-ID entry) that Settings uses, instead of a separate onboarding-only dropdown that could drift. The custom-provider free-text branch is unchanged, and the plain-`<select>` fallback (when the shared picker is unavailable) correctly preserves a saved/default model instead of resetting to the first option. Thanks @rodboev. (#5031, fixes #4706)
- **French localization is complete again — 40 missing UI strings now have French translations.** The `fr` locale was missing ~40 keys that exist in English (goal status messages, profile management, session metadata, upload/checkpoint messages, "Open in VS Code", and more), so those strings fell back to English for French users. All 40 are now translated, idiomatic-French wording fixes from the contributor are applied (including correcting a machine-mistranslation of "Steer" → "Bœuf"/beef), and the `theme_usage` non-breaking space before the colon is preserved per French typography. Thanks @Pichatu. (#4876)
+40 -2
View File
@@ -4652,6 +4652,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
if(isActiveSession){
S.activeStreamId=null;
}
let lastAsst=null;
if(isActiveSession){
// Capture previous session totals BEFORE overwriting S.session with the new
// cumulative values from the done event. prevIn/prevOut are the totals as of
@@ -4681,7 +4682,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
window._compressionUi={...window._compressionUi, sessionId:d.session.session_id};
}
// Find the last assistant message once for both reasoning persistence and timestamp
const lastAsst=[...S.messages].reverse().find(m=>m.role==='assistant');
lastAsst=[...S.messages].reverse().find(m=>m.role==='assistant');
// Persist reasoning trace for Worklog Thinking Cards; normal transcript
// rendering keeps provider reasoning out of the final answer.
if(reasoningText&&lastAsst&&!lastAsst.reasoning) lastAsst.reasoning=reasoningText;
@@ -4805,6 +4806,9 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
// TTS auto-read: speak the last assistant response if enabled (#499)
if(typeof autoReadLastAssistant==='function') setTimeout(()=>autoReadLastAssistant(), 300);
}
if(!lastAsst&&d.session&&Array.isArray(d.session.messages)){
lastAsst=[...d.session.messages].reverse().find(m=>m&&m.role==='assistant')||null;
}
if(isActiveSession&&_pendingGoalContinuation&&typeof queueSessionMessage==='function'){
const _goalNext=_pendingGoalContinuation;
_pendingGoalContinuation=null;
@@ -4827,7 +4831,11 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
// If the user watched the whole stream, _wasEverHidden stays false and
// the notification is suppressed (matches Slack/Discord/Gmail/Claude).
const _wasEverBackgrounded=_shouldForceCompletionNotification(activeSid, streamId);
sendBrowserNotification('Response complete',assistantText?assistantText.slice(0,100):'Task finished',{forceHidden:_wasEverBackgrounded,sid:activeSid});
const _completionPreview=_completionNotificationPreviewText(lastAsst,{
sessionId:completedSid,
liveDisplayText:typeof _streamDisplay==='function'?_streamDisplay():assistantText,
});
sendBrowserNotification('Response complete',_completionPreview||'Task finished',{forceHidden:_wasEverBackgrounded,sid:activeSid});
};
if(_shouldUseStreamFade()&&assistantBody){
_cancelAnimationFramePendingStreamRender();
@@ -6724,6 +6732,36 @@ function stopClarifyPolling() {
// ── Notifications and Sound ──────────────────────────────────────────────────
function _completionNotificationPreviewText(lastAssistantMessage, options){
const opts=(options&&typeof options==='object')?options:{};
const sessionId=String(opts.sessionId||'').trim();
let text='';
if(lastAssistantMessage&&typeof lastAssistantMessage==='object'){
if(typeof _assistantTurnAnchorSettledFinalAnswer==='function'){
const anchorFinal=_assistantTurnAnchorSettledFinalAnswer(
lastAssistantMessage,
lastAssistantMessage.content,
{session_id:sessionId||undefined}
);
if(anchorFinal!==null&&anchorFinal!==undefined) text=String(anchorFinal||'').trim();
}
if(!text&&typeof msgContent==='function') text=String(msgContent(lastAssistantMessage)||'').trim();
if(!text){
let raw=lastAssistantMessage.content||'';
if(Array.isArray(raw)) raw=raw.filter(p=>p&&p.type==='text').map(p=>p.text||'').join('').trim();
text=String(raw||'').trim();
}
if(text&&typeof _extractInlineThinkingFromContent==='function'){
const split=_extractInlineThinkingFromContent(text, lastAssistantMessage.reasoning, {streaming:false});
if(split&&typeof split.content==='string') text=split.content.trim();
}
}
if(!text&&typeof opts.liveDisplayText==='string') text=opts.liveDisplayText.trim();
if(!text) return '';
const normalized=text.replace(/\s+/g,' ').trim();
return normalized.length>100?`${normalized.slice(0,100)}…`:normalized;
}
function playNotificationSound(){
if(!window._soundEnabled) return;
try{
+15 -1
View File
@@ -37,11 +37,25 @@ def test_notification_payload_uses_completion_session_when_provided():
assert "_sessionUrlForSid(sid)" in MESSAGES_JS
assert "data:{url}" in MESSAGES_JS
assert "tag:sid?`hermes-${sid}`" in MESSAGES_JS
assert "sendBrowserNotification('Response complete',assistantText?assistantText.slice(0,100):'Task finished',{forceHidden:_wasEverBackgrounded,sid:activeSid})" in MESSAGES_JS
assert "function _completionNotificationPreviewText" in MESSAGES_JS
assert "_completionNotificationPreviewText(lastAsst," in MESSAGES_JS
assert "sendBrowserNotification('Response complete',_completionPreview||'Task finished',{forceHidden:_wasEverBackgrounded,sid:activeSid})" in MESSAGES_JS
assert "assistantText?assistantText.slice(0,100)" not in MESSAGES_JS
assert "sendBrowserNotification('Approval required',d.description||'Tool approval needed',{sid:activeSid})" in MESSAGES_JS
assert "sendBrowserNotification('Clarification needed',d.question||'Tool clarification needed',{sid:activeSid})" in MESSAGES_JS
def test_completion_notification_preview_uses_settled_message_not_live_prefix():
"""Background completion preview must not slice the live-stream accumulator."""
assert "function _completionNotificationPreviewText" in MESSAGES_JS
assert "String(msgContent(lastAssistantMessage)||'').trim()" in MESSAGES_JS
assert "_assistantTurnAnchorSettledFinalAnswer" in MESSAGES_JS
done_block = _source_between("source.addEventListener('done'", "source.addEventListener('stream_end'")
assert "let lastAsst=null;" in done_block
assert "d.session.messages" in done_block
assert "liveDisplayText:typeof _streamDisplay==='function'?_streamDisplay():assistantText" in done_block
def test_completion_notification_fires_when_tab_was_hidden_during_stream():
"""#4416: a throttled background-tab SSE delivers `done` late (after the user
returns, document.hidden=false), which silently dropped the completion