mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 11:40:26 +00:00
fix(webui): prevent composer draft rollback on refresh
This commit is contained in:
+5
-1
@@ -4852,7 +4852,11 @@ def handle_post(handler, parsed) -> bool:
|
||||
if files is not None:
|
||||
draft["files"] = files
|
||||
s.composer_draft = draft
|
||||
s.save()
|
||||
# Draft persistence is not conversation activity. Touching updated_at
|
||||
# here makes the active-session external-refresh poll force-reload the
|
||||
# current chat every few seconds while the user is typing, and that
|
||||
# delayed reload can restore an older draft over newer local input.
|
||||
s.save(touch_updated_at=False)
|
||||
return j(handler, {"ok": True, "draft": s.composer_draft})
|
||||
|
||||
if parsed.path == "/api/session/update":
|
||||
|
||||
+13
-4
@@ -47,7 +47,7 @@ function _saveComposerDraftNow(sid, text, files) {
|
||||
// Restore composer draft from server onto #msg textarea.
|
||||
// Only restores if there's actual text (skip empty/None drafts).
|
||||
// Guards against double-restore when rapidly switching sessions.
|
||||
function _restoreComposerDraft(draft, targetSid) {
|
||||
function _restoreComposerDraft(draft, targetSid, opts={}) {
|
||||
const ta = $('msg');
|
||||
if (!ta) return;
|
||||
// targetSid is the session that was requested — if it no longer matches
|
||||
@@ -55,10 +55,20 @@ function _restoreComposerDraft(draft, targetSid) {
|
||||
if (targetSid && _loadingSessionId !== null && _loadingSessionId !== targetSid) return;
|
||||
const text = (draft && typeof draft.text === 'string') ? draft.text : '';
|
||||
const files = (draft && Array.isArray(draft.files)) ? draft.files : [];
|
||||
const current = ta.value || '';
|
||||
const preserveActiveInput = !!(opts && opts.preserveActiveInput);
|
||||
|
||||
// Same-session force refreshes are driven by external state changes and may
|
||||
// finish seconds after the user continued typing. In that case the local
|
||||
// composer is the authoritative in-progress draft; never replace non-empty
|
||||
// local input with an older server draft. Cross-session switches still restore
|
||||
// normally so the previous session's composer contents do not leak forward.
|
||||
if (preserveActiveInput && current && current !== text) return;
|
||||
|
||||
// If there's no text and no files, clear the textarea (a previous session's
|
||||
// draft may still be sitting there from a cross-session switch).
|
||||
if (!text && !files.length) {
|
||||
if (ta.value) {
|
||||
if (current) {
|
||||
ta.value = '';
|
||||
if (typeof autoResize === 'function') autoResize();
|
||||
if (typeof updateSendBtn === 'function') updateSendBtn();
|
||||
@@ -66,7 +76,6 @@ function _restoreComposerDraft(draft, targetSid) {
|
||||
return;
|
||||
}
|
||||
// Only update if different to avoid cursor jumps on unrelated session switches.
|
||||
const current = ta.value || '';
|
||||
if (current !== text) {
|
||||
ta.value = text;
|
||||
if (typeof autoResize === 'function') autoResize();
|
||||
@@ -790,7 +799,7 @@ async function loadSession(sid){
|
||||
// against stale writes from slow responses racing to restore the previous draft).
|
||||
const _draft = S.session && S.session.composer_draft;
|
||||
if (_draft && (typeof _restoreComposerDraft === 'function')) {
|
||||
_restoreComposerDraft(_draft, sid);
|
||||
_restoreComposerDraft(_draft, sid, {preserveActiveInput:currentSid===sid&&forceReload});
|
||||
}
|
||||
|
||||
_resolveSessionModelForDisplaySoon(sid);
|
||||
|
||||
@@ -81,10 +81,24 @@ def test_draft_validation_appears_before_persist():
|
||||
src = Path(__file__).parents[1].joinpath("api", "routes.py").read_text(encoding="utf-8")
|
||||
# Anchor on the unique POST-validation comment marker.
|
||||
marker_idx = src.find("Stage-326 hardening (per Opus advisor)")
|
||||
persist_idx = src.find("s.composer_draft = draft\n s.save()")
|
||||
persist_idx = src.find("s.composer_draft = draft\n # Draft persistence is not conversation activity")
|
||||
assert marker_idx != -1 and persist_idx != -1, (
|
||||
"could not locate validation marker or persist site"
|
||||
)
|
||||
assert marker_idx < persist_idx, (
|
||||
"validation block must run before composer_draft persist"
|
||||
)
|
||||
|
||||
|
||||
def test_draft_save_does_not_touch_session_updated_at():
|
||||
"""Autosaving the composer must not look like conversation activity.
|
||||
|
||||
If POST /api/session/draft bumps updated_at, the frontend's active-session
|
||||
external refresh poll treats every keystroke autosave as a remote session
|
||||
update and force-reloads the current chat a few seconds later.
|
||||
"""
|
||||
src = Path(__file__).parents[1].joinpath("api", "routes.py").read_text(encoding="utf-8")
|
||||
persist_idx = src.find("s.composer_draft = draft")
|
||||
assert persist_idx != -1, "could not locate composer draft persist site"
|
||||
save_idx = src.find("s.save(touch_updated_at=False)", persist_idx)
|
||||
assert save_idx != -1, "composer draft save must preserve session updated_at"
|
||||
|
||||
@@ -37,3 +37,16 @@ def test_force_reload_clears_stale_blocking_prompts_immediately():
|
||||
"""
|
||||
assert "hideApprovalCard(forceReload)" in SESSIONS_JS
|
||||
assert "hideClarifyCard(forceReload, forceReload?'external-refresh':'dismissed')" in SESSIONS_JS
|
||||
|
||||
|
||||
def test_same_session_force_reload_preserves_non_empty_composer_input():
|
||||
"""A slow same-session refresh must not roll back text typed meanwhile.
|
||||
|
||||
The active-session refresh path can finish seconds after it started. If the
|
||||
user kept typing, restoring the server draft at the end of that load would
|
||||
replace newer local input with an older debounced draft.
|
||||
"""
|
||||
assert "function _restoreComposerDraft(draft, targetSid, opts={})" in SESSIONS_JS
|
||||
assert "const preserveActiveInput = !!(opts && opts.preserveActiveInput);" in SESSIONS_JS
|
||||
assert "if (preserveActiveInput && current && current !== text) return;" in SESSIONS_JS
|
||||
assert "_restoreComposerDraft(_draft, sid, {preserveActiveInput:currentSid===sid&&forceReload});" in SESSIONS_JS
|
||||
|
||||
Reference in New Issue
Block a user