Skip to content

Commit 7318af4

Browse files
Merge pull request #8127 from continuedev/tomasz/con-4265
Make it easy to edit messages in the CLI and to go back to an older message
2 parents bcd6c27 + 8f116b1 commit 7318af4

File tree

10 files changed

+958
-26
lines changed

10 files changed

+958
-26
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { render } from "@testing-library/react";
5+
import type { ChatHistoryItem } from "core/index.js";
6+
import React from "react";
7+
import { describe, it, expect, vi, beforeEach } from "vitest";
8+
9+
import { EditMessageSelector } from "./EditMessageSelector.js";
10+
11+
describe("EditMessageSelector", () => {
12+
const createMockChatHistory = (count: number): ChatHistoryItem[] => {
13+
const history: ChatHistoryItem[] = [];
14+
for (let i = 0; i < count; i++) {
15+
// Alternate between user and assistant messages
16+
if (i % 2 === 0) {
17+
history.push({
18+
message: { role: "user", content: `User message ${i + 1}` },
19+
contextItems: [],
20+
});
21+
} else {
22+
history.push({
23+
message: { role: "assistant", content: `Assistant message ${i + 1}` },
24+
contextItems: [],
25+
});
26+
}
27+
}
28+
return history;
29+
};
30+
31+
let mockOnEdit: ReturnType<typeof vi.fn>;
32+
let mockOnExit: ReturnType<typeof vi.fn>;
33+
34+
beforeEach(() => {
35+
mockOnEdit = vi.fn();
36+
mockOnExit = vi.fn();
37+
vi.clearAllMocks();
38+
});
39+
40+
describe("Initialization", () => {
41+
it("should render 'no user messages' when chat history is empty", () => {
42+
const { container } = render(
43+
<EditMessageSelector
44+
chatHistory={[]}
45+
onEdit={mockOnEdit}
46+
onExit={mockOnExit}
47+
/>,
48+
);
49+
50+
expect(container.textContent).toContain("No user messages to edit");
51+
});
52+
53+
it("should render 'no user messages' when chat history has only assistant messages", () => {
54+
const chatHistory: ChatHistoryItem[] = [
55+
{
56+
message: { role: "assistant", content: "Hello" },
57+
contextItems: [],
58+
},
59+
];
60+
61+
const { container } = render(
62+
<EditMessageSelector
63+
chatHistory={chatHistory}
64+
onEdit={mockOnEdit}
65+
onExit={mockOnExit}
66+
/>,
67+
);
68+
69+
expect(container.textContent).toContain("No user messages to edit");
70+
});
71+
72+
it("should render user messages list", () => {
73+
const chatHistory = createMockChatHistory(4); // Will have 2 user messages
74+
75+
const { container } = render(
76+
<EditMessageSelector
77+
chatHistory={chatHistory}
78+
onEdit={mockOnEdit}
79+
onExit={mockOnExit}
80+
/>,
81+
);
82+
83+
expect(container.textContent).toContain("Message 1:");
84+
expect(container.textContent).toContain("Message 2:");
85+
expect(container.textContent).toContain("User message 1");
86+
expect(container.textContent).toContain("User message 3");
87+
});
88+
89+
it("should filter out non-user messages", () => {
90+
const chatHistory: ChatHistoryItem[] = [
91+
{ message: { role: "user", content: "User 1" }, contextItems: [] },
92+
{
93+
message: { role: "assistant", content: "Assistant 1" },
94+
contextItems: [],
95+
},
96+
{ message: { role: "system", content: "System 1" }, contextItems: [] },
97+
{ message: { role: "user", content: "User 2" }, contextItems: [] },
98+
];
99+
100+
const { container } = render(
101+
<EditMessageSelector
102+
chatHistory={chatHistory}
103+
onEdit={mockOnEdit}
104+
onExit={mockOnExit}
105+
/>,
106+
);
107+
108+
// Should only show 2 user messages
109+
expect(container.textContent).toContain("User 1");
110+
expect(container.textContent).toContain("User 2");
111+
expect(container.textContent).not.toContain("Assistant 1");
112+
expect(container.textContent).not.toContain("System 1");
113+
});
114+
115+
it("should show instruction text in selection mode", () => {
116+
const chatHistory = createMockChatHistory(2);
117+
118+
const { container } = render(
119+
<EditMessageSelector
120+
chatHistory={chatHistory}
121+
onEdit={mockOnEdit}
122+
onExit={mockOnExit}
123+
/>,
124+
);
125+
126+
expect(container.textContent).toContain("↑/↓ to navigate");
127+
expect(container.textContent).toContain("Enter to edit");
128+
expect(container.textContent).toContain("Esc to exit");
129+
});
130+
});
131+
132+
describe("Message preview", () => {
133+
it("should truncate long messages in preview", () => {
134+
const longMessage = "a".repeat(100);
135+
const chatHistory: ChatHistoryItem[] = [
136+
{ message: { role: "user", content: longMessage }, contextItems: [] },
137+
];
138+
139+
const { container } = render(
140+
<EditMessageSelector
141+
chatHistory={chatHistory}
142+
onEdit={mockOnEdit}
143+
onExit={mockOnExit}
144+
/>,
145+
);
146+
147+
// Should show truncation indicator
148+
expect(container.textContent).toContain("...");
149+
// Should not show full message (preview is limited to 60 chars)
150+
expect(container.textContent).not.toContain(longMessage);
151+
});
152+
153+
it("should show only first line in multiline message preview", () => {
154+
const multilineMessage = "First line\nSecond line\nThird line";
155+
const chatHistory: ChatHistoryItem[] = [
156+
{
157+
message: { role: "user", content: multilineMessage },
158+
contextItems: [],
159+
},
160+
];
161+
162+
const { container } = render(
163+
<EditMessageSelector
164+
chatHistory={chatHistory}
165+
onEdit={mockOnEdit}
166+
onExit={mockOnExit}
167+
/>,
168+
);
169+
170+
expect(container.textContent).toContain("First line");
171+
expect(container.textContent).not.toContain("Second line");
172+
});
173+
});
174+
175+
describe("Complex message content", () => {
176+
it("should handle message parts array", () => {
177+
const chatHistory: ChatHistoryItem[] = [
178+
{
179+
message: {
180+
role: "user",
181+
content: [
182+
{ type: "text", text: "Hello" },
183+
{ type: "text", text: " world" },
184+
],
185+
} as any,
186+
contextItems: [],
187+
},
188+
];
189+
190+
const { container } = render(
191+
<EditMessageSelector
192+
chatHistory={chatHistory}
193+
onEdit={mockOnEdit}
194+
onExit={mockOnExit}
195+
/>,
196+
);
197+
198+
// Should render something (component handles non-string content gracefully)
199+
expect(container.textContent).toBeDefined();
200+
});
201+
});
202+
});

0 commit comments

Comments
 (0)