From 72d6db6db98b40838eb8b92c9cb62ebf83236383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F-Relakkes?= Date: Mon, 5 Oct 2026 20:35:10 +0800 Subject: [PATCH] fix(desktop): explain why a prompt cannot be edited instead of hiding it (#1456) The edit-and-resend action disappeared whenever an edit could not start: while a turn ran, while any background task (a dev server, a background agent) was still running, while turn checkpoints loaded, or when they failed. With nothing on screen it read as the feature being gone. Completed prompts now keep the action, disabled, with the reason on hover and as its accessible description. An open editor and its draft survive a background task starting. Sessions past the checkpoint preview budget can edit again by resolving each prompt through its transcript id; since that mode already tells the user file undo is unavailable, those edits roll back the conversation only. --- .../src/components/chat/MessageActionBar.tsx | 22 +++- .../src/components/chat/MessageList.test.tsx | 107 ++++++++++++++++-- desktop/src/components/chat/MessageList.tsx | 52 ++++++++- desktop/src/components/chat/UserMessage.tsx | 9 +- desktop/src/components/chat/turnRewind.ts | 2 +- .../chat/useUserMessageEditResend.test.tsx | 85 +++++++++++++- .../chat/useUserMessageEditResend.tsx | 41 +++++-- desktop/src/i18n/locales/en.ts | 4 + desktop/src/i18n/locales/jp.ts | 4 + desktop/src/i18n/locales/kr.ts | 4 + desktop/src/i18n/locales/zh-TW.ts | 4 + desktop/src/i18n/locales/zh.ts | 4 + 12 files changed, 306 insertions(+), 32 deletions(-) diff --git a/desktop/src/components/chat/MessageActionBar.tsx b/desktop/src/components/chat/MessageActionBar.tsx index a27c077e..3bbdc655 100644 --- a/desktop/src/components/chat/MessageActionBar.tsx +++ b/desktop/src/components/chat/MessageActionBar.tsx @@ -1,5 +1,5 @@ import { Check, Copy, GitFork, Pencil } from 'lucide-react' -import type { ReactNode } from 'react' +import { useId, type ReactNode } from 'react' import { useSettingsStore } from '../../stores/settingsStore' import { formatExactMessageTimestamp, formatMessageHoverTime } from '../../lib/formatMessageTimestamp' import { CopyButton } from '@/components/ui/CopyButton' @@ -14,6 +14,8 @@ export type MessageBranchAction = { export type MessageEditAction = { label: string disabled?: boolean + /** Shown on hover while disabled, so the action says why rather than vanishing. */ + disabledReason?: string onEdit: () => void } @@ -68,6 +70,7 @@ export function MessageActionBar({ placement = 'inline', }: Props) { const locale = useSettingsStore((state) => state.locale) + const editReasonId = useId() const hasCopy = Boolean(copyText?.trim()) const hoverTimeLabel = typeof timestamp === 'number' ? formatMessageHoverTime(timestamp, locale) @@ -116,7 +119,22 @@ export function MessageActionBar({ onPointerUp={(event) => event.currentTarget.blur()} /> ) : null} - {editAction ? ( + {editAction?.disabled && editAction.disabledReason ? ( + // A disabled button takes no pointer events, so its own title would + // never show. The reason hangs on a wrapper that still hovers. + + + ) : editAction ? (