Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 0 additions & 75 deletions site/src/pages/AgentsPage/AgentChatPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2944,26 +2944,6 @@ const compactCommandMessages: TypesGen.ChatMessagesResponse = {
has_more: false,
};

const compactQueuedEditChat: TypesGen.Chat = {
id: CHAT_ID,
...baseChatFields,
title: "Compact queued edit",
status: "running",
};

const compactQueuedEditMessages: TypesGen.ChatMessagesResponse = {
messages: compactCommandMessages.messages,
queued_messages: [
{
...MockChatQueuedMessage,
id: 3,
chat_id: CHAT_ID,
content: [{ type: "text", text: "Queued follow-up" }],
},
],
has_more: false,
};

/** Submitting "/compact" alone requests a manual compaction instead of
* sending a chat message. */
export const SlashCompactCommandSubmits: Story = {
Expand Down Expand Up @@ -3013,61 +2993,6 @@ export const SlashCompactCommandSubmits: Story = {
},
};

export const SlashCompactQueuedEditSaves: Story = {
parameters: {
queries: buildQueries(compactQueuedEditChat, compactQueuedEditMessages, {
diffUrl: undefined,
}),
},
beforeEach: () => {
spyOn(API.experimental, "getUserSkills").mockResolvedValue([]);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const compactSpy = spyOn(API.experimental, "compactChat");
const sendSpy = spyOn(
API.experimental,
"createChatMessage",
).mockResolvedValue({
queued: true,
queued_message: {
...MockChatQueuedMessage,
id: 4,
chat_id: CHAT_ID,
content: [{ type: "text", text: "/compact" }],
},
});
const deleteSpy = spyOn(
API.experimental,
"deleteChatQueuedMessage",
).mockResolvedValue();
spyOn(API.experimental, "getChat").mockResolvedValue(compactQueuedEditChat);
spyOn(API.experimental, "getChatMessages").mockResolvedValue({
...compactQueuedEditMessages,
queued_messages: [],
});

await userEvent.click(await canvas.findByRole("button", { name: "Edit" }));
const editor = await canvas.findByTestId("chat-message-input");
await userEvent.clear(editor);
await userEvent.type(editor, "/compact");
await userEvent.click(canvas.getByRole("button", { name: "Save" }));

await waitFor(() => {
expect(sendSpy).toHaveBeenCalledTimes(1);
expect(deleteSpy).toHaveBeenCalledTimes(1);
});
expect(sendSpy).toHaveBeenCalledWith(
CHAT_ID,
expect.objectContaining({
content: [{ type: "text", text: "/compact" }],
}),
);
expect(deleteSpy).toHaveBeenCalledWith(CHAT_ID, 3);
expect(compactSpy).not.toHaveBeenCalled();
},
};

/** A personal skill named "compact" takes precedence: "/compact" is sent
* as a normal message (skill trigger) and no compaction is requested. */
export const SlashCompactYieldsToPersonalSkill: Story = {
Expand Down
99 changes: 0 additions & 99 deletions site/src/pages/AgentsPage/AgentChatPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,6 @@ describe("useConversationEditingState", () => {

const renderEditing = (...args: [] | [string | undefined]) => {
const onSend = vi.fn().mockResolvedValue(undefined);
const onDeleteQueuedMessage = vi.fn().mockResolvedValue(undefined);
const chatInputRef = createRef<ChatMessageInputRef>();
const inputValueRef = { current: "" };
// createRef returns { current: null }, but we need it initialized
Expand All @@ -598,7 +597,6 @@ describe("useConversationEditingState", () => {
useConversationEditingState({
chatID: resolvedChatID,
onSend,
onDeleteQueuedMessage,
chatInputRef,
inputValueRef,
}),
Expand Down Expand Up @@ -688,40 +686,6 @@ describe("useConversationEditingState", () => {
unmount();
});

it("loads queue edit text into the composer and restores the prior draft on cancel without refocusing", () => {
const { result, unmount } = renderEditing();

// Simulate the user typing a draft via handleContentChange.
act(() => {
result.current.handleContentChange(
"work in progress",
"work in progress",
false,
);
});

const remountKeyBefore = result.current.remountKey;

act(() => {
result.current.handleStartQueueEdit(9, "queued message", []);
});

expect(result.current.editingQueuedMessageID).toBe(9);
expect(result.current.editorInitialValue).toBe("queued message");
expect(result.current.remountKey).toBe(remountKeyBefore + 1);

const remountKeyAfterEdit = result.current.remountKey;

act(() => {
result.current.handleCancelQueueEdit();
});

expect(result.current.editingQueuedMessageID).toBeNull();
expect(result.current.editorInitialValue).toBe("work in progress");
expect(result.current.remountKey).toBe(remountKeyAfterEdit + 1);
unmount();
});

it("does not force focus when replacing input values on mobile", () => {
setMobileViewport(true);
const { result, unmount } = renderEditing();
Expand All @@ -741,16 +705,6 @@ describe("useConversationEditingState", () => {
result.current.handleCancelHistoryEdit();
});
expect(mockInput.focus).not.toHaveBeenCalled();

act(() => {
result.current.handleStartQueueEdit(9, "queued message", []);
});
expect(mockInput.focus).not.toHaveBeenCalled();

act(() => {
result.current.handleCancelQueueEdit();
});
expect(mockInput.focus).not.toHaveBeenCalled();
unmount();
});

Expand All @@ -772,22 +726,6 @@ describe("useConversationEditingState", () => {
unmount();
});

it("falls back to the persisted draft when queue edit starts before hydration", () => {
localStorage.setItem(expectedKey, "persisted draft");
const { result, unmount } = renderEditing();

act(() => {
result.current.handleStartQueueEdit(9, "queued message", []);
});

act(() => {
result.current.handleCancelQueueEdit();
});

expect(result.current.editorInitialValue).toBe("persisted draft");
unmount();
});

it("prefers the live editor value over stale persisted draft state", () => {
localStorage.setItem(expectedKey, "stale persisted draft");
const { result, unmount } = renderEditing();
Expand Down Expand Up @@ -1181,43 +1119,6 @@ describe("useConversationEditingState", () => {
unmount();
});

it("preserves serialized editor state across queue edit then cancel", () => {
const editorState = JSON.stringify({
root: {
children: [
{
children: [{ text: "queued draft", type: "text" }],
type: "paragraph",
},
],
type: "root",
},
});
localStorage.setItem(expectedKey, editorState);

const { result, unmount } = renderEditing();

act(() => {
result.current.handleContentChange("queued draft", editorState, false);
});

act(() => {
result.current.handleStartQueueEdit(99, "queued msg", []);
});

expect(result.current.editingQueuedMessageID).toBe(99);
expect(result.current.initialEditorState).toBeUndefined();

act(() => {
result.current.handleCancelQueueEdit();
});

expect(result.current.editingQueuedMessageID).toBeNull();
expect(result.current.initialEditorState).toBe(editorState);
expect(result.current.editorInitialValue).toBe("queued draft");
unmount();
});

it("returns undefined initialEditorState after edit then cancel with plain-text draft", () => {
localStorage.setItem(expectedKey, "plain text draft");

Expand Down
85 changes: 9 additions & 76 deletions site/src/pages/AgentsPage/AgentChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -509,12 +509,10 @@ export function useConversationEditingState(deps: {
attachments?: readonly PendingAttachment[],
editedMessageID?: number,
) => Promise<void>;
onDeleteQueuedMessage: (id: number) => Promise<void>;
chatInputRef: React.RefObject<ChatMessageInputRef | null>;
inputValueRef: React.RefObject<string>;
}) {
const { chatID, onSend, onDeleteQueuedMessage, chatInputRef, inputValueRef } =
deps;
const { chatID, onSend, chatInputRef, inputValueRef } = deps;
const draftStorageKey = chatID
? `${draftInputStorageKeyPrefix}${chatID}`
: null;
Expand Down Expand Up @@ -600,53 +598,6 @@ export function useConversationEditingState(deps: {
setEditingFileBlocks([]);
};

// -- Queue editing state --
const [editingQueuedMessageID, setEditingQueuedMessageID] = useState<
number | null
>(null);
const [draftBeforeQueueEdit, setDraftBeforeQueueEdit] =
useState<ParsedDraft | null>(null);

const handleStartQueueEdit = (
id: number,
text: string,
fileBlocks: readonly ChatMessagePart[],
) => {
if (editingQueuedMessageID === null) {
const currentEditorState = draftStorageKey
? parseStoredDraft(localStorage.getItem(draftStorageKey)).editorState
: undefined;
setDraftBeforeQueueEdit({
text: inputValueRef.current,
editorState: currentEditorState,
});
}
setEditingQueuedMessageID(id);
setDraftState({
editorInitialValue: text,
initialEditorState: undefined,
});
serializedEditorStateRef.current = undefined;
setRemountKey((k) => k + 1);
inputValueRef.current = text;
setEditingFileBlocks(fileBlocks);
};

const handleCancelQueueEdit = () => {
const savedText = draftBeforeQueueEdit?.text ?? "";
const savedState = draftBeforeQueueEdit?.editorState;
setDraftState({
editorInitialValue: savedText,
initialEditorState: savedState,
});
serializedEditorStateRef.current = savedState;
setRemountKey((k) => k + 1);
inputValueRef.current = savedText;
setEditingQueuedMessageID(null);
setDraftBeforeQueueEdit(null);
setEditingFileBlocks([]);
};

// Clears the composer for an in-flight history edit and
// returns a rollback function that restores the editing draft
// if the send fails.
Expand Down Expand Up @@ -675,10 +626,7 @@ export function useConversationEditingState(deps: {
};

// Clears all input and editing state after a successful send.
const finalizeSuccessfulSend = (
editedMessageID: number | undefined,
queueEditID: number | null,
) => {
const finalizeSuccessfulSend = (editedMessageID: number | undefined) => {
chatInputRef.current?.clear();
if (!isMobileViewport()) {
chatInputRef.current?.focus();
Expand All @@ -692,23 +640,15 @@ export function useConversationEditingState(deps: {
setDraftBeforeHistoryEdit(null);
setEditingFileBlocks([]);
}
if (queueEditID !== null) {
setEditingQueuedMessageID(null);
setDraftBeforeQueueEdit(null);
setEditingFileBlocks([]);
void onDeleteQueuedMessage(queueEditID);
}
};

// Wraps the parent onSend to clear local input/editing state
// and handle queue-edit deletion.
// Wraps the parent onSend to clear local input/editing state.
const handleSendFromInput = async (
message: string,
attachments?: readonly PendingAttachment[],
) => {
const editedMessageID =
editingMessageId !== null ? editingMessageId : undefined;
const queueEditID = editingQueuedMessageID;
const sendPromise = onSend(message, attachments, editedMessageID);

// For history edits, clear input immediately and prepare
Expand All @@ -728,7 +668,7 @@ export function useConversationEditingState(deps: {
throw error;
}

finalizeSuccessfulSend(editedMessageID, queueEditID);
finalizeSuccessfulSend(editedMessageID);
};

const handleContentChange = (
Expand All @@ -739,11 +679,9 @@ export function useConversationEditingState(deps: {
inputValueRef.current = content;
serializedEditorStateRef.current = serializedEditorState;

// Don't overwrite the persisted draft while editing a
// history or queued message, the original draft (possibly
// containing file-reference chips) is saved in React state
// and should survive a cancel.
if (editingMessageId !== null || editingQueuedMessageID !== null) {
// Don't overwrite the persisted draft while editing a history message.
// The original draft is saved in React state and should survive a cancel.
if (editingMessageId !== null) {
return;
}

Expand Down Expand Up @@ -786,9 +724,6 @@ export function useConversationEditingState(deps: {
editingFileBlocks,
handleEditUserMessage,
handleCancelHistoryEdit,
editingQueuedMessageID,
handleStartQueueEdit,
handleCancelQueueEdit,
handleSendFromInput,
handleContentChange,
handleLoadingDraftChange,
Expand Down Expand Up @@ -1501,7 +1436,6 @@ const AgentChatPage: FC = () => {
const editing = useConversationEditingState({
chatID: agentId,
onSend: handleSend,
onDeleteQueuedMessage: handleDeleteQueuedMessage,
chatInputRef,
inputValueRef,
});
Expand Down Expand Up @@ -1690,12 +1624,11 @@ const AgentChatPage: FC = () => {

// "/compact" on its own (no attachments or file references)
// requests a manual context compaction instead of sending a
// message. Only new sends are intercepted; history and queued
// edits keep their original meaning, and a personal or workspace
// message. Only new sends are intercepted; history edits keep their
// original meaning, and a personal or workspace
// skill named "compact" takes precedence so the command cannot shadow it.
const isExactCompactSubmission =
editedMessageID === undefined &&
editing.editingQueuedMessageID === null &&
content.length === 1 &&
content[0].type === "text" &&
content[0].text?.trim() ===
Expand Down
Loading
Loading