Skip to content

Commit 4c44512

Browse files
author
Andrew T.
committed
Harden edit regenerate turn ids and edit gating
1 parent 79116ee commit 4c44512

6 files changed

Lines changed: 97 additions & 7 deletions

File tree

src/features/messages/components/Messages.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,6 +1507,39 @@ describe("Messages", () => {
15071507
expect(screen.getByText("Plan ready")).toBeTruthy();
15081508
});
15091509

1510+
it("hides edit entry points while regenerate is in flight", () => {
1511+
const items: ConversationItem[] = [
1512+
{
1513+
id: "msg-1",
1514+
kind: "message",
1515+
role: "user",
1516+
text: "First",
1517+
},
1518+
{
1519+
id: "msg-2",
1520+
kind: "message",
1521+
role: "user",
1522+
text: "Second",
1523+
},
1524+
];
1525+
const onStartEdit = vi.fn();
1526+
1527+
render(
1528+
<Messages
1529+
items={items}
1530+
threadId="thread-1"
1531+
workspaceId="ws-1"
1532+
isThinking={false}
1533+
openTargets={[]}
1534+
selectedOpenAppId=""
1535+
isRegeneratingEdit
1536+
onStartEdit={onStartEdit}
1537+
/>,
1538+
);
1539+
1540+
expect(screen.queryByRole("button", { name: "Edit message" })).toBeNull();
1541+
});
1542+
15101543
it("calls the plan follow-up callbacks", () => {
15111544
const onPlanAccept = vi.fn();
15121545
const onPlanSubmitChanges = vi.fn();

src/features/messages/components/Messages.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export const Messages = memo(function Messages({
168168
if (item.kind === "message") {
169169
const isCopied = copiedMessageId === item.id;
170170
const isEditingThis = editingItemId === item.id;
171+
const editAction = isThinking || isRegeneratingEdit ? undefined : onStartEdit;
171172
return (
172173
<MessageRow
173174
key={item.id}
@@ -185,7 +186,7 @@ export const Messages = memo(function Messages({
185186
editText={isEditingThis ? editText : undefined}
186187
isConfirming={isEditingThis ? isConfirmingEdit : undefined}
187188
isRegenerating={isEditingThis ? isRegeneratingEdit : undefined}
188-
onStartEdit={isThinking ? undefined : onStartEdit}
189+
onStartEdit={editAction}
189190
onCancelEdit={onCancelEdit}
190191
onUpdateEditText={onUpdateEditText}
191192
onRequestRegenerate={onRequestRegenerate}

src/features/messages/hooks/useMessageEdit.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,37 @@ describe("useMessageEdit", () => {
175175
expect(result.current.isConfirming).toBe(false);
176176
expect(result.current.isRegenerating).toBe(false);
177177
});
178+
179+
it("ignores startEdit while regenerate is already in flight", async () => {
180+
let resolveRegenerate: (() => void) | null = null;
181+
const pendingRegenerate = vi.fn(
182+
() =>
183+
new Promise<void>((resolve) => {
184+
resolveRegenerate = resolve;
185+
}),
186+
);
187+
const { result } = renderHook(() =>
188+
useMessageEdit({ onRegenerate: pendingRegenerate }),
189+
);
190+
191+
act(() => {
192+
result.current.startEdit("item-1", "text");
193+
});
194+
195+
await act(async () => {
196+
void result.current.executeRegenerate();
197+
});
198+
199+
expect(result.current.isRegenerating).toBe(true);
200+
201+
act(() => {
202+
result.current.startEdit("item-2", "other text");
203+
});
204+
205+
expect(result.current.editingItemId).toBe("item-1");
206+
207+
await act(async () => {
208+
resolveRegenerate?.();
209+
});
210+
});
178211
});

src/features/messages/hooks/useMessageEdit.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,17 @@ export function useMessageEdit({ onRegenerate }: UseMessageEditOptions): UseMess
4141
const [isConfirming, setIsConfirming] = useState(false);
4242
const [isRegenerating, setIsRegenerating] = useState(false);
4343

44-
const startEdit = useCallback((itemId: string, text: string, images: string[] = []) => {
45-
setEditState({ itemId, originalText: text, editText: text, images });
46-
setIsConfirming(false);
47-
setIsRegenerating(false);
48-
}, []);
44+
const startEdit = useCallback(
45+
(itemId: string, text: string, images: string[] = []) => {
46+
if (isRegenerating) {
47+
return;
48+
}
49+
setEditState({ itemId, originalText: text, editText: text, images });
50+
setIsConfirming(false);
51+
setIsRegenerating(false);
52+
},
53+
[isRegenerating],
54+
);
4955

5056
const cancelEdit = useCallback(() => {
5157
setEditState(null);

src/utils/threadItems.conversion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { parseCollabToolCallItem } from "./threadItems.collab";
33
import { asNumber, asString } from "./threadItems.shared";
44

55
function extractTurnId(record: Record<string, unknown>) {
6-
return asString(record.id ?? record.turnId ?? record.turn_id).trim();
6+
return asString(record.turnId ?? record.turn_id ?? record.id).trim();
77
}
88

99
function extractImageInputValue(input: Record<string, unknown>) {

src/utils/threadItems.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,23 @@ describe("threadItems", () => {
940940
]);
941941
});
942942

943+
it("prefers explicit turn ids over item ids when both are present", () => {
944+
const item = buildConversationItem({
945+
id: "item-1",
946+
turnId: "turn-1",
947+
type: "userMessage",
948+
content: [{ type: "text", text: "Hello" }],
949+
});
950+
951+
expect(item).toEqual({
952+
id: "item-1",
953+
turnId: "turn-1",
954+
kind: "message",
955+
role: "user",
956+
text: "Hello",
957+
});
958+
});
959+
943960
it("parses ISO timestamps for thread updates", () => {
944961
const timestamp = getThreadTimestamp({ updated_at: "2025-01-01T00:00:00Z" });
945962
expect(timestamp).toBe(Date.parse("2025-01-01T00:00:00Z"));

0 commit comments

Comments
 (0)