mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 02:05:22 -04:00
fix(chat): make resend message non-destructive
Keep normal resend from truncating session history while preserving replace-from-here behavior for regenerate flows.
This commit is contained in:
+23
-18
@@ -3876,9 +3876,11 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resend a user message — truncates history to that point and resubmits.
|
* Resend a user message. Normal resend appends a fresh copy at the end of
|
||||||
|
* the current thread; regenerate flows can opt into replacing from here.
|
||||||
*/
|
*/
|
||||||
export async function resendUserMessage(userMsgElement) {
|
export async function resendUserMessage(userMsgElement, opts = {}) {
|
||||||
|
const replaceFromHere = Boolean(opts && opts.replaceFromHere);
|
||||||
const box = document.getElementById('chat-history');
|
const box = document.getElementById('chat-history');
|
||||||
const allMsgs = Array.from(box.querySelectorAll('.msg'));
|
const allMsgs = Array.from(box.querySelectorAll('.msg'));
|
||||||
const msgIndex = allMsgs.indexOf(userMsgElement);
|
const msgIndex = allMsgs.indexOf(userMsgElement);
|
||||||
@@ -3924,25 +3926,28 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
|
|||||||
const sessionId = sessionModule.getCurrentSessionId();
|
const sessionId = sessionModule.getCurrentSessionId();
|
||||||
if (!sessionId) return;
|
if (!sessionId) return;
|
||||||
|
|
||||||
// Truncate backend to keep everything before this user message
|
|
||||||
const keepCount = msgIndex;
|
|
||||||
try {
|
try {
|
||||||
await fetch(`${API_BASE}/api/session/${sessionId}/truncate`, {
|
if (replaceFromHere) {
|
||||||
method: 'POST',
|
// Regenerate flows intentionally trim history to this point before
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// resubmitting. The plain "Resend message" action must not do this.
|
||||||
body: JSON.stringify({ keep_count: keepCount })
|
const keepCount = msgIndex;
|
||||||
});
|
await fetch(`${API_BASE}/api/session/${sessionId}/truncate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ keep_count: keepCount })
|
||||||
|
});
|
||||||
|
|
||||||
// Drop the AI replies after the user message but KEEP the user bubble
|
// Drop the AI replies after the user message but KEEP the user bubble
|
||||||
// itself (so its photo stays visible). Then suppress the new user
|
// itself (so its photo stays visible). Then suppress the new user
|
||||||
// bubble that send would otherwise add — same pattern as regenerate.
|
// bubble that send would otherwise add — same pattern as regenerate.
|
||||||
let sibling = userMsgElement.nextSibling;
|
let sibling = userMsgElement.nextSibling;
|
||||||
while (sibling) {
|
while (sibling) {
|
||||||
const next = sibling.nextSibling;
|
const next = sibling.nextSibling;
|
||||||
sibling.remove();
|
sibling.remove();
|
||||||
sibling = next;
|
sibling = next;
|
||||||
|
}
|
||||||
|
_hideUserBubble = true;
|
||||||
}
|
}
|
||||||
_hideUserBubble = true;
|
|
||||||
_pendingRegenAttachments = _ids;
|
_pendingRegenAttachments = _ids;
|
||||||
|
|
||||||
// Resubmit
|
// Resubmit
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ function _openVisionEditor(att, userMsgEl) {
|
|||||||
await _saveVisionText();
|
await _saveVisionText();
|
||||||
_closeVisionEditor();
|
_closeVisionEditor();
|
||||||
if (userMsgEl && window.chatModule?.resendUserMessage) {
|
if (userMsgEl && window.chatModule?.resendUserMessage) {
|
||||||
window.chatModule.resendUserMessage(userMsgEl);
|
window.chatModule.resendUserMessage(userMsgEl, { replaceFromHere: true });
|
||||||
} else if (uiModule?.showToast) {
|
} else if (uiModule?.showToast) {
|
||||||
uiModule.showToast('Saved');
|
uiModule.showToast('Saved');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""Regression guard for #4149: normal Resend must not delete chat history.
|
||||||
|
|
||||||
|
chat.js is browser-heavy, so this pins the source-level contract: the footer's
|
||||||
|
plain "Resend message" path appends a fresh send, while regenerate-only paths
|
||||||
|
must opt into truncating/replacing from the selected message.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
_REPO = Path(__file__).resolve().parent.parent
|
||||||
|
_CHAT_JS = _REPO / "static" / "js" / "chat.js"
|
||||||
|
_CHAT_RENDERER_JS = _REPO / "static" / "js" / "chatRenderer.js"
|
||||||
|
|
||||||
|
|
||||||
|
def _resend_body() -> str:
|
||||||
|
src = _CHAT_JS.read_text(encoding="utf-8")
|
||||||
|
start = src.index("export async function resendUserMessage(")
|
||||||
|
end = src.index("export async function regenerateFrom(", start)
|
||||||
|
return src[start:end]
|
||||||
|
|
||||||
|
|
||||||
|
def test_resend_message_does_not_truncate_by_default():
|
||||||
|
body = _resend_body()
|
||||||
|
|
||||||
|
assert "opts = {}" in body
|
||||||
|
assert "const replaceFromHere = Boolean(opts && opts.replaceFromHere);" in body
|
||||||
|
|
||||||
|
guard_idx = body.index("if (replaceFromHere)")
|
||||||
|
truncate_idx = body.index("/api/session/${sessionId}/truncate")
|
||||||
|
hide_idx = body.index("_hideUserBubble = true;")
|
||||||
|
|
||||||
|
assert guard_idx < truncate_idx
|
||||||
|
assert guard_idx < hide_idx
|
||||||
|
assert "/truncate" not in body[:guard_idx]
|
||||||
|
assert "_hideUserBubble = true;" not in body[:guard_idx]
|
||||||
|
|
||||||
|
|
||||||
|
def test_only_regenerate_callers_opt_into_replace_from_here():
|
||||||
|
renderer = _CHAT_RENDERER_JS.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "window.chatModule.resendUserMessage(msgElement);" in renderer
|
||||||
|
assert "window.chatModule.resendUserMessage(userMsgEl, { replaceFromHere: true });" in renderer
|
||||||
Reference in New Issue
Block a user