Skip to content

Commit d531717

Browse files
authored
Merge branch 'main' into main
2 parents de22db9 + 693bd59 commit d531717

16 files changed

Lines changed: 543 additions & 287 deletions

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

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { log, isLoggingEnabled } from "../../utils/agent/log.js";
1+
import { log } from "../../utils/agent/log.js";
22
import { Box, Text, useInput, useStdin } from "ink";
33
import React, { useState } from "react";
44
import { useInterval } from "use-interval";
@@ -40,11 +40,9 @@ export default function TerminalChatInputThinking({
4040

4141
const str = Buffer.isBuffer(data) ? data.toString("utf8") : data;
4242
if (str === "\x1b\x1b") {
43-
if (isLoggingEnabled()) {
44-
log(
45-
"raw stdin: received collapsed ESC ESC – starting confirmation timer",
46-
);
47-
}
43+
log(
44+
"raw stdin: received collapsed ESC ESC – starting confirmation timer",
45+
);
4846
setAwaitingConfirm(true);
4947
setTimeout(() => setAwaitingConfirm(false), 1500);
5048
}
@@ -65,15 +63,11 @@ export default function TerminalChatInputThinking({
6563
}
6664

6765
if (awaitingConfirm) {
68-
if (isLoggingEnabled()) {
69-
log("useInput: second ESC detected – triggering onInterrupt()");
70-
}
66+
log("useInput: second ESC detected – triggering onInterrupt()");
7167
onInterrupt();
7268
setAwaitingConfirm(false);
7369
} else {
74-
if (isLoggingEnabled()) {
75-
log("useInput: first ESC detected – waiting for confirmation");
76-
}
70+
log("useInput: first ESC detected – waiting for confirmation");
7771
setAwaitingConfirm(true);
7872
setTimeout(() => setAwaitingConfirm(false), 1500);
7973
}

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

Lines changed: 94 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import type { MultilineTextEditorHandle } from "./multiline-editor";
12
import type { ReviewDecision } from "../../utils/agent/review.js";
23
import type { HistoryEntry } from "../../utils/storage/command-history.js";
34
import type {
45
ResponseInputItem,
56
ResponseItem,
67
} from "openai/resources/responses/responses.mjs";
78

9+
import MultilineTextEditor from "./multiline-editor";
810
import { TerminalChatCommandReview } from "./terminal-chat-command-review.js";
9-
import { log, isLoggingEnabled } from "../../utils/agent/log.js";
11+
import { log } from "../../utils/agent/log.js";
1012
import { loadConfig } from "../../utils/config.js";
1113
import { createInputItem } from "../../utils/input-utils.js";
1214
import { setSessionId } from "../../utils/session.js";
@@ -16,10 +18,15 @@ import {
1618
addToHistory,
1719
} from "../../utils/storage/command-history.js";
1820
import { clearTerminal, onExit } from "../../utils/terminal.js";
19-
import TextInput from "../vendor/ink-text-input.js";
2021
import { Box, Text, useApp, useInput, useStdin } from "ink";
2122
import { fileURLToPath } from "node:url";
22-
import React, { useCallback, useState, Fragment, useEffect } from "react";
23+
import React, {
24+
useCallback,
25+
useState,
26+
Fragment,
27+
useEffect,
28+
useRef,
29+
} from "react";
2330
import { useInterval } from "use-interval";
2431

2532
const suggestions = [
@@ -83,6 +90,12 @@ export default function TerminalChatInput({
8390
const [historyIndex, setHistoryIndex] = useState<number | null>(null);
8491
const [draftInput, setDraftInput] = useState<string>("");
8592
const [skipNextSubmit, setSkipNextSubmit] = useState<boolean>(false);
93+
// Multiline text editor key to force remount after submission
94+
const [editorKey, setEditorKey] = useState(0);
95+
// Imperative handle from the multiline editor so we can query caret position
96+
const editorRef = useRef<MultilineTextEditorHandle | null>(null);
97+
// Track the caret row across keystrokes
98+
const prevCursorRow = useRef<number | null>(null);
8699

87100
// Load command history on component mount
88101
useEffect(() => {
@@ -168,6 +181,9 @@ export default function TerminalChatInput({
168181
case "/approval":
169182
openApprovalOverlay();
170183
break;
184+
case "/diff":
185+
openDiffOverlay();
186+
break;
171187
case "/bug":
172188
onSubmit(cmd);
173189
break;
@@ -181,9 +197,15 @@ export default function TerminalChatInput({
181197
}
182198
if (!confirmationPrompt && !loading) {
183199
if (_key.upArrow) {
184-
if (history.length > 0) {
200+
// Only recall history when the caret was *already* on the very first
201+
// row *before* this key-press.
202+
const cursorRow = editorRef.current?.getRow?.() ?? 0;
203+
const wasAtFirstRow = (prevCursorRow.current ?? cursorRow) === 0;
204+
205+
if (history.length > 0 && cursorRow === 0 && wasAtFirstRow) {
185206
if (historyIndex == null) {
186-
setDraftInput(input);
207+
const currentDraft = editorRef.current?.getText?.() ?? input;
208+
setDraftInput(currentDraft);
187209
}
188210

189211
let newIndex: number;
@@ -194,27 +216,37 @@ export default function TerminalChatInput({
194216
}
195217
setHistoryIndex(newIndex);
196218
setInput(history[newIndex]?.command ?? "");
219+
// Re-mount the editor so it picks up the new initialText
220+
setEditorKey((k) => k + 1);
221+
return; // we handled the key
197222
}
198-
return;
223+
// Otherwise let the event propagate so the editor moves the caret
199224
}
200225

201226
if (_key.downArrow) {
202-
if (historyIndex == null) {
203-
return;
204-
}
205-
206-
const newIndex = historyIndex + 1;
207-
if (newIndex >= history.length) {
208-
setHistoryIndex(null);
209-
setInput(draftInput);
210-
} else {
211-
setHistoryIndex(newIndex);
212-
setInput(history[newIndex]?.command ?? "");
227+
// Only move forward in history when we're already *in* history mode
228+
// AND the caret sits on the last line of the buffer
229+
if (historyIndex != null && editorRef.current?.isCursorAtLastRow()) {
230+
const newIndex = historyIndex + 1;
231+
if (newIndex >= history.length) {
232+
setHistoryIndex(null);
233+
setInput(draftInput);
234+
setEditorKey((k) => k + 1);
235+
} else {
236+
setHistoryIndex(newIndex);
237+
setInput(history[newIndex]?.command ?? "");
238+
setEditorKey((k) => k + 1);
239+
}
240+
return; // handled
213241
}
214-
return;
242+
// Otherwise let it propagate
215243
}
216244
}
217245

246+
// Update the cached cursor position *after* we've potentially handled
247+
// the key so that the next event has the correct "previous" reference.
248+
prevCursorRow.current = editorRef.current?.getRow?.() ?? null;
249+
218250
if (input.trim() === "" && isNew) {
219251
if (_key.tab) {
220252
setSelectedSuggestion(
@@ -313,15 +345,27 @@ export default function TerminalChatInput({
313345

314346
// Emit a system message to confirm the clear action. We *append*
315347
// it so Ink's <Static> treats it as new output and actually renders it.
316-
setItems((prev) => [
317-
...prev,
318-
{
319-
id: `clear-${Date.now()}`,
320-
type: "message",
321-
role: "system",
322-
content: [{ type: "input_text", text: "Context cleared" }],
323-
},
324-
]);
348+
setItems((prev) => {
349+
const filteredOldItems = prev.filter((item) => {
350+
if (
351+
item.type === "message" &&
352+
(item.role === "user" || item.role === "assistant")
353+
) {
354+
return false;
355+
}
356+
return true;
357+
});
358+
359+
return [
360+
...filteredOldItems,
361+
{
362+
id: `clear-${Date.now()}`,
363+
type: "message",
364+
role: "system",
365+
content: [{ type: "input_text", text: "Terminal cleared" }],
366+
},
367+
];
368+
});
325369

326370
return;
327371
} else if (inputValue === "/clearhistory") {
@@ -534,25 +578,27 @@ export default function TerminalChatInput({
534578
thinkingSeconds={thinkingSeconds}
535579
/>
536580
) : (
537-
<Box paddingX={1}>
538-
<TextInput
539-
focus={active}
540-
placeholder={
541-
selectedSuggestion
542-
? `"${suggestions[selectedSuggestion - 1]}"`
543-
: "send a message" +
544-
(isNew ? " or press tab to select a suggestion" : "")
545-
}
546-
showCursor
547-
value={input}
548-
onChange={(value) => {
549-
setDraftInput(value);
581+
<Box>
582+
<MultilineTextEditor
583+
ref={editorRef}
584+
onChange={(txt: string) => {
585+
setDraftInput(txt);
550586
if (historyIndex != null) {
551587
setHistoryIndex(null);
552588
}
553-
setInput(value);
589+
setInput(txt);
590+
}}
591+
key={editorKey}
592+
initialText={input}
593+
height={6}
594+
focus={active}
595+
onSubmit={(txt) => {
596+
onSubmit(txt);
597+
setEditorKey((k) => k + 1);
598+
setInput("");
599+
setHistoryIndex(null);
600+
setDraftInput("");
554601
}}
555-
onSubmit={onSubmit}
556602
/>
557603
</Box>
558604
)}
@@ -597,7 +643,7 @@ export default function TerminalChatInput({
597643
) : (
598644
<>
599645
send q or ctrl+c to exit | send "/clear" to reset | send "/help"
600-
for commands | press enter to send
646+
for commands | press enter to send | shift+enter for new line
601647
{contextLeftPercent > 25 && (
602648
<>
603649
{" — "}
@@ -692,11 +738,9 @@ function TerminalChatInputThinking({
692738
const str = Buffer.isBuffer(data) ? data.toString("utf8") : data;
693739
if (str === "\x1b\x1b") {
694740
// Treat as the first Escape press – prompt the user for confirmation.
695-
if (isLoggingEnabled()) {
696-
log(
697-
"raw stdin: received collapsed ESC ESC – starting confirmation timer",
698-
);
699-
}
741+
log(
742+
"raw stdin: received collapsed ESC ESC – starting confirmation timer",
743+
);
700744
setAwaitingConfirm(true);
701745
setTimeout(() => setAwaitingConfirm(false), 1500);
702746
}
@@ -721,15 +765,11 @@ function TerminalChatInputThinking({
721765
}
722766

723767
if (awaitingConfirm) {
724-
if (isLoggingEnabled()) {
725-
log("useInput: second ESC detected – triggering onInterrupt()");
726-
}
768+
log("useInput: second ESC detected – triggering onInterrupt()");
727769
onInterrupt();
728770
setAwaitingConfirm(false);
729771
} else {
730-
if (isLoggingEnabled()) {
731-
log("useInput: first ESC detected – waiting for confirmation");
732-
}
772+
log("useInput: first ESC detected – waiting for confirmation");
733773
setAwaitingConfirm(true);
734774
setTimeout(() => setAwaitingConfirm(false), 1500);
735775
}

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

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88

99
import MultilineTextEditor from "./multiline-editor";
1010
import { TerminalChatCommandReview } from "./terminal-chat-command-review.js";
11-
import { log, isLoggingEnabled } from "../../utils/agent/log.js";
11+
import { log } from "../../utils/agent/log.js";
1212
import { loadConfig } from "../../utils/config.js";
1313
import { createInputItem } from "../../utils/input-utils.js";
1414
import { setSessionId } from "../../utils/session.js";
@@ -505,11 +505,9 @@ function TerminalChatInputThinking({
505505
const str = Buffer.isBuffer(data) ? data.toString("utf8") : data;
506506
if (str === "\x1b\x1b") {
507507
// Treat as the first Escape press – prompt the user for confirmation.
508-
if (isLoggingEnabled()) {
509-
log(
510-
"raw stdin: received collapsed ESC ESC – starting confirmation timer",
511-
);
512-
}
508+
log(
509+
"raw stdin: received collapsed ESC ESC – starting confirmation timer",
510+
);
513511
setAwaitingConfirm(true);
514512
setTimeout(() => setAwaitingConfirm(false), 1500);
515513
}
@@ -531,15 +529,11 @@ function TerminalChatInputThinking({
531529
}
532530

533531
if (awaitingConfirm) {
534-
if (isLoggingEnabled()) {
535-
log("useInput: second ESC detected – triggering onInterrupt()");
536-
}
532+
log("useInput: second ESC detected – triggering onInterrupt()");
537533
onInterrupt();
538534
setAwaitingConfirm(false);
539535
} else {
540-
if (isLoggingEnabled()) {
541-
log("useInput: first ESC detected – waiting for confirmation");
542-
}
536+
log("useInput: first ESC detected – waiting for confirmation");
543537
setAwaitingConfirm(true);
544538
setTimeout(() => setAwaitingConfirm(false), 1500);
545539
}

codex-cli/src/components/chat/terminal-chat-response-item.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { OverlayModeType } from "./terminal-chat";
12
import type { TerminalRendererOptions } from "marked-terminal";
23
import type {
34
ResponseFunctionToolCallItem,
@@ -14,18 +15,25 @@ import chalk, { type ForegroundColorName } from "chalk";
1415
import { Box, Text } from "ink";
1516
import { parse, setOptions } from "marked";
1617
import TerminalRenderer from "marked-terminal";
17-
import React, { useMemo } from "react";
18+
import React, { useEffect, useMemo } from "react";
1819

1920
export default function TerminalChatResponseItem({
2021
item,
2122
fullStdout = false,
23+
setOverlayMode,
2224
}: {
2325
item: ResponseItem;
2426
fullStdout?: boolean;
27+
setOverlayMode?: React.Dispatch<React.SetStateAction<OverlayModeType>>;
2528
}): React.ReactElement {
2629
switch (item.type) {
2730
case "message":
28-
return <TerminalChatResponseMessage message={item} />;
31+
return (
32+
<TerminalChatResponseMessage
33+
setOverlayMode={setOverlayMode}
34+
message={item}
35+
/>
36+
);
2937
case "function_call":
3038
return <TerminalChatResponseToolCall message={item} />;
3139
case "function_call_output":
@@ -98,9 +106,23 @@ const colorsByRole: Record<string, ForegroundColorName> = {
98106

99107
function TerminalChatResponseMessage({
100108
message,
109+
setOverlayMode,
101110
}: {
102111
message: ResponseInputMessageItem | ResponseOutputMessage;
112+
setOverlayMode?: React.Dispatch<React.SetStateAction<OverlayModeType>>;
103113
}) {
114+
// auto switch to model mode if the system message contains "has been deprecated"
115+
useEffect(() => {
116+
if (message.role === "system") {
117+
const systemMessage = message.content.find(
118+
(c) => c.type === "input_text",
119+
)?.text;
120+
if (systemMessage?.includes("has been deprecated")) {
121+
setOverlayMode?.("model");
122+
}
123+
}
124+
}, [message, setOverlayMode]);
125+
104126
return (
105127
<Box flexDirection="column">
106128
<Text bold color={colorsByRole[message.role] || "gray"}>

0 commit comments

Comments
 (0)