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:
3 files changed
+57
-3
No files matched your search
@@ -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
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user