fix(ui): restrict edit to latest user message (#747)

fix(ui): restrict edit to latest user message (#747)

Only the latest user turn shows the pencil/edit affordance. Older user
messages remain read-only (copy + timestamp still work). Avoids the
misleading implication that historical messages can be lightly edited
when the actual action truncates the session and restarts the
conversation from that point.

Closes #744

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
This commit is contained in:
Frank Song
2026-04-20 14:11:49 +08:00
committed by GitHub
parent da1fdca22c
commit aa78175cca
3 changed files with 23 additions and 1 deletions

View File

@@ -1,5 +1,10 @@
# Hermes Web UI -- Changelog # Hermes Web UI -- Changelog
## [v0.50.97] — 2026-04-20
### Fixed
- **Only the latest user message can be edited** — older user turns no longer show the pencil/edit affordance. This avoids implying that historical turns can be lightly edited when the actual action truncates the session and restarts the conversation from that point. (Closes #744)
## [v0.50.96] — 2026-04-19 ## [v0.50.96] — 2026-04-19
### Added ### Added

View File

@@ -1409,6 +1409,13 @@ function renderMessages(){
if(msgContent(m)||m.attachments?.length||(m.role==='assistant'&&(hasTc||hasTu||_messageHasReasoningPayload(m)))) visWithIdx.push({m,rawIdx}); if(msgContent(m)||m.attachments?.length||(m.role==='assistant'&&(hasTc||hasTu||_messageHasReasoningPayload(m)))) visWithIdx.push({m,rawIdx});
rawIdx++; rawIdx++;
} }
let lastUserRawIdx=-1;
for(let i=visWithIdx.length-1;i>=0;i--){
if(visWithIdx[i].m&&visWithIdx[i].m.role==='user'){
lastUserRawIdx=visWithIdx[i].rawIdx;
break;
}
}
const insertionAnchor=_compressionAnchorIndex( const insertionAnchor=_compressionAnchorIndex(
visWithIdx, visWithIdx,
compressionState ? compressionState.anchorMessageKey : sessionCompressionAnchorKey, compressionState ? compressionState.anchorMessageKey : sessionCompressionAnchorKey,
@@ -1471,7 +1478,8 @@ function renderMessages(){
filesHtml=`<div class="msg-files">${m.attachments.map(f=>`<div class="msg-file-badge">${li('paperclip',12)} ${esc(f)}</div>`).join('')}</div>`; 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(_stripXmlToolCallsDisplay(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 isEditableUser=isUser&&rawIdx===lastUserRawIdx;
const editBtn = isEditableUser ? `<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 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>`; const copyBtn = `<button class="msg-copy-btn msg-action-btn" title="${t('copy')}" onclick="copyMsg(this)">${li('copy',13)}</button>`;
const tsVal=m._ts||m.timestamp; const tsVal=m._ts||m.timestamp;

9
tests/test_issue744.py Normal file
View File

@@ -0,0 +1,9 @@
import pathlib
def test_only_latest_user_message_gets_edit_button():
src = pathlib.Path("static/ui.js").read_text(encoding="utf-8")
assert "let lastUserRawIdx=-1;" in src
assert "const isEditableUser=isUser&&rawIdx===lastUserRawIdx;" in src
assert "const editBtn = isEditableUser ?" in src