Skip to content

Commit 8666263

Browse files
authored
fix: only allow going up in history when not already in history if input is empty (openai#654)
\+ cleanup below input help to be "ctrl+c to exit | "/" to see commands | enter to send" now that we have command autocompletion \+ minor other drive-by code cleanups --------- Signed-off-by: Thibault Sottiaux <[email protected]>
1 parent 2759ff3 commit 8666263

File tree

2 files changed

+45
-75
lines changed

2 files changed

+45
-75
lines changed

codex-cli/src/components/chat/terminal-chat-input.tsx

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -245,30 +245,40 @@ export default function TerminalChatInput({
245245
}
246246

247247
if (_key.upArrow) {
248-
// Only recall history when the caret was *already* on the very first
248+
let moveThroughHistory = true;
249+
250+
// Only use history when the caret was *already* on the very first
249251
// row *before* this key-press.
250252
const cursorRow = editorRef.current?.getRow?.() ?? 0;
251253
const wasAtFirstRow = (prevCursorRow.current ?? cursorRow) === 0;
254+
if (!(cursorRow === 0 && wasAtFirstRow)) {
255+
moveThroughHistory = false;
256+
}
252257

253-
if (history.length > 0 && cursorRow === 0 && wasAtFirstRow) {
254-
if (historyIndex == null) {
255-
const currentDraft = editorRef.current?.getText?.() ?? input;
256-
setDraftInput(currentDraft);
257-
}
258+
// Only use history if we are already in history mode or if the input is empty.
259+
if (historyIndex == null && input.trim() !== "") {
260+
moveThroughHistory = false;
261+
}
258262

263+
// Move through history.
264+
if (history.length && moveThroughHistory) {
259265
let newIndex: number;
260266
if (historyIndex == null) {
267+
const currentDraft = editorRef.current?.getText?.() ?? input;
268+
setDraftInput(currentDraft);
261269
newIndex = history.length - 1;
262270
} else {
263271
newIndex = Math.max(0, historyIndex - 1);
264272
}
265273
setHistoryIndex(newIndex);
274+
266275
setInput(history[newIndex]?.command ?? "");
267276
// Re-mount the editor so it picks up the new initialText
268277
setEditorKey((k) => k + 1);
269-
return; // we handled the key
278+
return; // handled
270279
}
271-
// Otherwise let the event propagate so the editor moves the caret
280+
281+
// Otherwise let it propagate.
272282
}
273283

274284
if (_key.downArrow) {
@@ -339,73 +349,60 @@ export default function TerminalChatInput({
339349
const onSubmit = useCallback(
340350
async (value: string) => {
341351
const inputValue = value.trim();
342-
// If the user only entered a slash, do not send a chat message
352+
353+
// If the user only entered a slash, do not send a chat message.
343354
if (inputValue === "/") {
344355
setInput("");
345356
return;
346357
}
347-
// Skip this submit if we just autocompleted a slash command
358+
359+
// Skip this submit if we just autocompleted a slash command.
348360
if (skipNextSubmit) {
349361
setSkipNextSubmit(false);
350362
return;
351363
}
364+
352365
if (!inputValue) {
353366
return;
354-
}
355-
356-
if (inputValue === "/history") {
367+
} else if (inputValue === "/history") {
357368
setInput("");
358369
openOverlay();
359370
return;
360-
}
361-
362-
if (inputValue === "/help") {
371+
} else if (inputValue === "/help") {
363372
setInput("");
364373
openHelpOverlay();
365374
return;
366-
}
367-
368-
if (inputValue === "/diff") {
375+
} else if (inputValue === "/diff") {
369376
setInput("");
370377
openDiffOverlay();
371378
return;
372-
}
373-
374-
if (inputValue === "/compact") {
379+
} else if (inputValue === "/compact") {
375380
setInput("");
376381
onCompact();
377382
return;
378-
}
379-
380-
if (inputValue.startsWith("/model")) {
383+
} else if (inputValue.startsWith("/model")) {
381384
setInput("");
382385
openModelOverlay();
383386
return;
384-
}
385-
386-
if (inputValue.startsWith("/approval")) {
387+
} else if (inputValue.startsWith("/approval")) {
387388
setInput("");
388389
openApprovalOverlay();
389390
return;
390-
}
391-
392-
if (inputValue === "q" || inputValue === ":q" || inputValue === "exit") {
391+
} else if (inputValue === "exit") {
393392
setInput("");
394-
// wait one 60ms frame
395393
setTimeout(() => {
396394
app.exit();
397395
onExit();
398396
process.exit(0);
399-
}, 60);
397+
}, 60); // Wait one frame.
400398
return;
401399
} else if (inputValue === "/clear" || inputValue === "clear") {
402400
setInput("");
403401
setSessionId("");
404402
setLastResponseId("");
405-
// Clear the terminal screen (including scrollback) before resetting context
406-
clearTerminal();
407403

408-
// Emit a system notice in the chat; no raw console writes so Ink keeps control.
404+
// Clear the terminal screen (including scrollback) before resetting context.
405+
clearTerminal();
409406

410407
// Emit a system message to confirm the clear action. We *append*
411408
// it so Ink's <Static> treats it as new output and actually renders it.
@@ -449,7 +446,7 @@ export default function TerminalChatInput({
449446
await clearCommandHistory();
450447
setHistory([]);
451448

452-
// Emit a system message to confirm the history clear action
449+
// Emit a system message to confirm the history clear action.
453450
setItems((prev) => [
454451
...prev,
455452
{
@@ -466,7 +463,7 @@ export default function TerminalChatInput({
466463

467464
return;
468465
} else if (inputValue === "/bug") {
469-
// Generate a GitHub bug report URL pre‑filled with session details
466+
// Generate a GitHub bug report URL pre‑filled with session details.
470467
setInput("");
471468

472469
try {
@@ -519,10 +516,10 @@ export default function TerminalChatInput({
519516

520517
return;
521518
} else if (inputValue.startsWith("/")) {
522-
// Handle invalid/unrecognized commands.
523-
// Only single-word inputs starting with '/' (e.g., /command) that are not recognized are caught here.
524-
// Any other input, including those starting with '/' but containing spaces
525-
// (e.g., "/command arg"), will fall through and be treated as a regular prompt.
519+
// Handle invalid/unrecognized commands. Only single-word inputs starting with '/'
520+
// (e.g., /command) that are not recognized are caught here. Any other input, including
521+
// those starting with '/' but containing spaces (e.g., "/command arg"), will fall through
522+
// and be treated as a regular prompt.
526523
const trimmed = inputValue.trim();
527524

528525
if (/^\/\S+$/.test(trimmed)) {
@@ -549,11 +546,13 @@ export default function TerminalChatInput({
549546
// detect image file paths for dynamic inclusion
550547
const images: Array<string> = [];
551548
let text = inputValue;
549+
552550
// markdown-style image syntax: ![alt](path)
553551
text = text.replace(/!\[[^\]]*?\]\(([^)]+)\)/g, (_m, p1: string) => {
554552
images.push(p1.startsWith("file://") ? fileURLToPath(p1) : p1);
555553
return "";
556554
});
555+
557556
// quoted file paths ending with common image extensions (e.g. '/path/to/img.png')
558557
text = text.replace(
559558
/['"]([^'"]+?\.(?:png|jpe?g|gif|bmp|webp|svg))['"]/gi,
@@ -562,6 +561,7 @@ export default function TerminalChatInput({
562561
return "";
563562
},
564563
);
564+
565565
// bare file paths ending with common image extensions
566566
text = text.replace(
567567
// eslint-disable-next-line no-useless-escape
@@ -578,10 +578,10 @@ export default function TerminalChatInput({
578578
const inputItem = await createInputItem(text, images);
579579
submitInput([inputItem]);
580580

581-
// Get config for history persistence
581+
// Get config for history persistence.
582582
const config = loadConfig();
583583

584-
// Add to history and update state
584+
// Add to history and update state.
585585
const updatedHistory = await addToHistory(value, history, {
586586
maxSize: config.history?.maxSize ?? 1000,
587587
saveHistory: config.history?.saveHistory ?? true,
@@ -723,8 +723,7 @@ export default function TerminalChatInput({
723723
/>
724724
) : (
725725
<Text dimColor>
726-
send q or ctrl+c to exit | send "/clear" to reset | send "/help" for
727-
commands | press enter to send | shift+enter for new line
726+
ctrl+c to exit | "/" to see commands | enter to send
728727
{contextLeftPercent > 25 && (
729728
<>
730729
{" — "}

codex-cli/tests/terminal-chat-input-multiline.test.tsx

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,35 +24,6 @@ vi.mock("../src/utils/input-utils.js", () => ({
2424
}));
2525

2626
describe("TerminalChatInput multiline functionality", () => {
27-
it("renders the multiline editor component", async () => {
28-
const props: ComponentProps<typeof TerminalChatInput> = {
29-
isNew: false,
30-
loading: false,
31-
submitInput: () => {},
32-
confirmationPrompt: null,
33-
explanation: undefined,
34-
submitConfirmation: () => {},
35-
setLastResponseId: () => {},
36-
setItems: () => {},
37-
contextLeftPercent: 50,
38-
openOverlay: () => {},
39-
openDiffOverlay: () => {},
40-
openModelOverlay: () => {},
41-
openApprovalOverlay: () => {},
42-
openHelpOverlay: () => {},
43-
onCompact: () => {},
44-
interruptAgent: () => {},
45-
active: true,
46-
thinkingSeconds: 0,
47-
};
48-
49-
const { lastFrameStripped } = renderTui(<TerminalChatInput {...props} />);
50-
const frame = lastFrameStripped();
51-
52-
// Check that the help text mentions shift+enter for new line
53-
expect(frame).toContain("shift+enter for new line");
54-
});
55-
5627
it("allows multiline input with shift+enter", async () => {
5728
const submitInput = vi.fn();
5829

0 commit comments

Comments
 (0)