Skip to content

Commit 547aecd

Browse files
committed
fix(remote-connect): refuse rollback mid-turn and refresh right after it lands
Two problems in the mobile rollback flow, both about the window between the host mutation landing and the phone finding out. `begin_session_maintenance` takes its permit by clearing the pending queue, cancelling background subagents and cancelling the active turn, so a rollback requested while the desktop is working silently destroys that work. The desktop refuses instead: `assertSessionIdleForHistoryMutation` requires an idle session and an empty queue. Match it — disable both menu entries and the confirm button while a turn is active, guard the handler, and say why. The transcript also stayed stale on the failure path. `nudge()` ran only after the whole operation succeeded, so a rollback that landed followed by a failed send left the retired turns on screen until the next idle poll, which is ten seconds out. Nudge as soon as the rollback returns, before the send can fail. Also: - `.chat-msg__menu-sheet` had no height bound, so the edit sheet's autofocused textarea could raise the keyboard and push the confirm button out of an unscrollable sheet. Add `max-height: 85dvh` and `overflow-y: auto`. - Edit mode reported `rollbackDone` ("Rolled back") after it had also sent the new turn. Give it its own string.
1 parent 6d8af96 commit 547aecd

3 files changed

Lines changed: 46 additions & 6 deletions

File tree

‎src/mobile-web/src/i18n/messages.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ export const messages: Record<MobileLanguage, MessageTree> = {
176176
editPlaceholder: 'Edit this message...',
177177
rollbackDone: 'Rolled back',
178178
rollbackDoneRestored: 'Rolled back, {count} file(s) restored',
179+
editDone: 'Rolled back and sent',
180+
rollbackBlockedWhileBusy: 'Stop the running turn before changing history.',
179181
messageCopied: 'Copied',
180182
messageDeleted: 'Deleted',
181183
copyFailed: 'Copy failed',
@@ -378,6 +380,8 @@ export const messages: Record<MobileLanguage, MessageTree> = {
378380
editPlaceholder: '编辑这条消息...',
379381
rollbackDone: '已回滚',
380382
rollbackDoneRestored: '已回滚,还原了 {count} 个文件',
383+
editDone: '已回滚并重新发送',
384+
rollbackBlockedWhileBusy: '请先停止正在执行的轮次,再修改历史。',
381385
messageCopied: '已复制',
382386
messageDeleted: '已删除',
383387
copyFailed: '复制失败',
@@ -580,6 +584,8 @@ export const messages: Record<MobileLanguage, MessageTree> = {
580584
editPlaceholder: '編輯這條訊息...',
581585
rollbackDone: '已回溯',
582586
rollbackDoneRestored: '已回溯,還原了 {count} 個檔案',
587+
editDone: '已回溯並重新送出',
588+
rollbackBlockedWhileBusy: '請先停止正在執行的輪次,再修改歷史。',
583589
messageCopied: '已複製',
584590
messageDeleted: '已刪除',
585591
copyFailed: '複製失敗',

‎src/mobile-web/src/pages/ChatPage.tsx‎

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2663,6 +2663,10 @@ const ChatPage: React.FC<ChatPageProps> = ({ sessionMgr, sessionId, sessionName,
26632663
// message.
26642664
const handleConfirmRollback = useCallback(async () => {
26652665
if (!rollbackTarget || rollbackBusy) return;
2666+
// The host cancels the running turn and clears the queue to take the
2667+
// maintenance permit, so it never rolls back mid-task. Match the desktop,
2668+
// which refuses the mutation instead of killing work the user can't see.
2669+
if (isStreaming) return;
26662670
const { message, mode } = rollbackTarget;
26672671
const turnId = message.turn_id;
26682672
if (!turnId) return;
@@ -2675,6 +2679,10 @@ const ChatPage: React.FC<ChatPageProps> = ({ sessionMgr, sessionId, sessionName,
26752679
try {
26762680
const result = await sessionMgr.rollbackSessionToTurn(sessionId, turnId, message.turn_index);
26772681
if (!isChatTargetCurrent(targetEpoch)) return;
2682+
// History changed on the host. Pull the authoritative snapshot now, before
2683+
// the follow-up send can fail, or the transcript keeps showing turns that
2684+
// no longer exist until the next idle poll ten seconds later.
2685+
pollerRef.current?.nudge();
26782686

26792687
if (mode === 'edit') {
26802688
const imageContexts = message.images?.length
@@ -2704,9 +2712,11 @@ const ChatPage: React.FC<ChatPageProps> = ({ sessionMgr, sessionId, sessionName,
27042712
}
27052713

27062714
showMsgToast(
2707-
result.restored_files.length > 0
2708-
? t('chat.rollbackDoneRestored', { count: result.restored_files.length })
2709-
: t('chat.rollbackDone'),
2715+
mode === 'edit'
2716+
? t('chat.editDone')
2717+
: result.restored_files.length > 0
2718+
? t('chat.rollbackDoneRestored', { count: result.restored_files.length })
2719+
: t('chat.rollbackDone'),
27102720
);
27112721
pollerRef.current?.nudge();
27122722
} catch (e: any) {
@@ -2720,6 +2730,7 @@ const ChatPage: React.FC<ChatPageProps> = ({ sessionMgr, sessionId, sessionName,
27202730
agentMode,
27212731
captureChatTargetEpoch,
27222732
isChatTargetCurrent,
2733+
isStreaming,
27232734
rollbackBusy,
27242735
rollbackDraft,
27252736
rollbackTarget,
@@ -3433,20 +3444,31 @@ const ChatPage: React.FC<ChatPageProps> = ({ sessionMgr, sessionId, sessionName,
34333444
)}
34343445
{menuMessage.role === 'user' && !!menuMessage.turn_id && (
34353446
<>
3436-
<button className="chat-msg__menu-btn" onClick={() => openRollbackSheet('edit')}>
3447+
<button
3448+
className="chat-msg__menu-btn"
3449+
onClick={() => openRollbackSheet('edit')}
3450+
disabled={isStreaming}
3451+
>
34373452
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
34383453
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
34393454
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4z" />
34403455
</svg>
34413456
<span>{t('chat.editAndResend')}</span>
34423457
</button>
3443-
<button className="chat-msg__menu-btn" onClick={() => openRollbackSheet('rollback')}>
3458+
<button
3459+
className="chat-msg__menu-btn"
3460+
onClick={() => openRollbackSheet('rollback')}
3461+
disabled={isStreaming}
3462+
>
34443463
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
34453464
<polyline points="1 4 1 10 7 10" />
34463465
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
34473466
</svg>
34483467
<span>{t('chat.rollbackToHere')}</span>
34493468
</button>
3469+
{isStreaming && (
3470+
<p className="chat-msg__menu-note">{t('chat.rollbackBlockedWhileBusy')}</p>
3471+
)}
34503472
</>
34513473
)}
34523474
<button
@@ -3493,12 +3515,15 @@ const ChatPage: React.FC<ChatPageProps> = ({ sessionMgr, sessionId, sessionName,
34933515
) : (
34943516
<p className="chat-msg__rollback-quote">{sanitizeMessageText(rollbackTarget.message.content)}</p>
34953517
)}
3518+
{isStreaming && (
3519+
<p className="chat-msg__menu-note">{t('chat.rollbackBlockedWhileBusy')}</p>
3520+
)}
34963521
</div>
34973522
<div className="chat-msg__menu-actions">
34983523
<button
34993524
className="chat-msg__menu-btn chat-msg__menu-btn--danger chat-msg__menu-btn--confirm"
35003525
onClick={handleConfirmRollback}
3501-
disabled={rollbackBusy || (rollbackTarget.mode === 'edit' && !rollbackDraft.trim())}
3526+
disabled={rollbackBusy || isStreaming || (rollbackTarget.mode === 'edit' && !rollbackDraft.trim())}
35023527
>
35033528
<span>
35043529
{rollbackBusy

‎src/mobile-web/src/styles/components/chat.scss‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,8 @@
364364
.chat-msg__menu-sheet {
365365
width: 100%;
366366
max-width: 440px;
367+
max-height: 85dvh;
368+
overflow-y: auto;
367369
background: var(--color-bg-elevated);
368370
border-radius: 20px 20px 0 0;
369371
padding: var(--size-gap-2) var(--size-gap-4) var(--size-gap-6);
@@ -430,6 +432,13 @@
430432
}
431433
}
432434

435+
.chat-msg__menu-note {
436+
margin: 0;
437+
padding: 0 var(--size-gap-2);
438+
font-size: var(--font-size-sm);
439+
color: var(--color-text-secondary);
440+
}
441+
433442
// ── Rollback / edit confirmation ──────────────────────────────────
434443
.chat-msg__rollback {
435444
display: flex;

0 commit comments

Comments
 (0)