Skip to content

Commit 4e0d785

Browse files
authored
Merge branch 'main' into main
2 parents 3abb181 + 463a230 commit 4e0d785

21 files changed

+874
-87
lines changed

.github/workflows/rust-release.yml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,9 @@ jobs:
142142
with:
143143
tag_name: ${{ env.RELEASE_TAG }}
144144
files: dist/**
145-
# TODO(ragona): I'm going to leave these as draft for now.
146-
# It gives us 1) clarity that these are not yet a stable version, and
147-
# 2) allows a human step to review the release before publishing the draft.
148-
prerelease: false
149-
draft: true
145+
# For now, tag releases as "prerelease" because we are not claiming
146+
# the Rust CLI is stable yet.
147+
prerelease: true
150148

151149
- uses: facebook/dotslash-publish-release@v2
152150
env:

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@
22

33
You can install any of these versions: `npm install -g codex@version`
44

5+
## `0.1.2504301751`
6+
7+
### 🚀 Features
8+
9+
- User config api key (#569)
10+
- `@mention` files in codex (#701)
11+
- Add `--reasoning` CLI flag (#314)
12+
- Lower default retry wait time and increase number of tries (#720)
13+
- Add common package registries domains to allowed-domains list (#414)
14+
15+
### 🪲 Bug Fixes
16+
17+
- Insufficient quota message (#758)
18+
- Input keyboard shortcut opt+delete (#685)
19+
- `/diff` should include untracked files (#686)
20+
- Only allow running without sandbox if explicitly marked in safe container (#699)
21+
- Tighten up check for /usr/bin/sandbox-exec (#710)
22+
- Check if sandbox-exec is available (#696)
23+
- Duplicate messages in quiet mode (#680)
24+
525
## `0.1.2504251709`
626

727
### 🚀 Features

codex-cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openai/codex",
3-
"version": "0.1.2504251709",
3+
"version": "0.1.2504301751",
44
"license": "Apache-2.0",
55
"bin": {
66
"codex": "bin/codex.js"

codex-cli/src/components/chat/multiline-editor.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ export interface MultilineTextEditorProps {
137137

138138
// Called when the internal text buffer updates.
139139
readonly onChange?: (text: string) => void;
140+
141+
// Optional initial cursor position (character offset)
142+
readonly initialCursorOffset?: number;
140143
}
141144

142145
// Expose a minimal imperative API so parent components (e.g. TerminalChatInput)
@@ -169,14 +172,15 @@ const MultilineTextEditorInner = (
169172
onSubmit,
170173
focus = true,
171174
onChange,
175+
initialCursorOffset,
172176
}: MultilineTextEditorProps,
173177
ref: React.Ref<MultilineTextEditorHandle | null>,
174178
): React.ReactElement => {
175179
// ---------------------------------------------------------------------------
176180
// Editor State
177181
// ---------------------------------------------------------------------------
178182

179-
const buffer = useRef(new TextBuffer(initialText));
183+
const buffer = useRef(new TextBuffer(initialText, initialCursorOffset));
180184
const [version, setVersion] = useState(0);
181185

182186
// Keep track of the current terminal size so that the editor grows/shrinks

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

Lines changed: 146 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { MultilineTextEditorHandle } from "./multiline-editor";
22
import type { ReviewDecision } from "../../utils/agent/review.js";
3+
import type { FileSystemSuggestion } from "../../utils/file-system-suggestions.js";
34
import type { HistoryEntry } from "../../utils/storage/command-history.js";
45
import type {
56
ResponseInputItem,
@@ -11,6 +12,7 @@ import { TerminalChatCommandReview } from "./terminal-chat-command-review.js";
1112
import TextCompletions from "./terminal-chat-completions.js";
1213
import { loadConfig } from "../../utils/config.js";
1314
import { getFileSystemSuggestions } from "../../utils/file-system-suggestions.js";
15+
import { expandFileTags } from "../../utils/file-tag-utils";
1416
import { createInputItem } from "../../utils/input-utils.js";
1517
import { log } from "../../utils/logger/log.js";
1618
import { setSessionId } from "../../utils/session.js";
@@ -92,16 +94,120 @@ export default function TerminalChatInput({
9294
const [historyIndex, setHistoryIndex] = useState<number | null>(null);
9395
const [draftInput, setDraftInput] = useState<string>("");
9496
const [skipNextSubmit, setSkipNextSubmit] = useState<boolean>(false);
95-
const [fsSuggestions, setFsSuggestions] = useState<Array<string>>([]);
97+
const [fsSuggestions, setFsSuggestions] = useState<
98+
Array<FileSystemSuggestion>
99+
>([]);
96100
const [selectedCompletion, setSelectedCompletion] = useState<number>(-1);
97101
// Multiline text editor key to force remount after submission
98-
const [editorKey, setEditorKey] = useState(0);
102+
const [editorState, setEditorState] = useState<{
103+
key: number;
104+
initialCursorOffset?: number;
105+
}>({ key: 0 });
99106
// Imperative handle from the multiline editor so we can query caret position
100107
const editorRef = useRef<MultilineTextEditorHandle | null>(null);
101108
// Track the caret row across keystrokes
102109
const prevCursorRow = useRef<number | null>(null);
103110
const prevCursorWasAtLastRow = useRef<boolean>(false);
104111

112+
// --- Helper for updating input, remounting editor, and moving cursor to end ---
113+
const applyFsSuggestion = useCallback((newInputText: string) => {
114+
setInput(newInputText);
115+
setEditorState((s) => ({
116+
key: s.key + 1,
117+
initialCursorOffset: newInputText.length,
118+
}));
119+
}, []);
120+
121+
// --- Helper for updating file system suggestions ---
122+
function updateFsSuggestions(
123+
txt: string,
124+
alwaysUpdateSelection: boolean = false,
125+
) {
126+
// Clear file system completions if a space is typed
127+
if (txt.endsWith(" ")) {
128+
setFsSuggestions([]);
129+
setSelectedCompletion(-1);
130+
} else {
131+
// Determine the current token (last whitespace-separated word)
132+
const words = txt.trim().split(/\s+/);
133+
const lastWord = words[words.length - 1] ?? "";
134+
135+
const shouldUpdateSelection =
136+
lastWord.startsWith("@") || alwaysUpdateSelection;
137+
138+
// Strip optional leading '@' for the path prefix
139+
let pathPrefix: string;
140+
if (lastWord.startsWith("@")) {
141+
pathPrefix = lastWord.slice(1);
142+
// If only '@' is typed, list everything in the current directory
143+
pathPrefix = pathPrefix.length === 0 ? "./" : pathPrefix;
144+
} else {
145+
pathPrefix = lastWord;
146+
}
147+
148+
if (shouldUpdateSelection) {
149+
const completions = getFileSystemSuggestions(pathPrefix);
150+
setFsSuggestions(completions);
151+
if (completions.length > 0) {
152+
setSelectedCompletion((prev) =>
153+
prev < 0 || prev >= completions.length ? 0 : prev,
154+
);
155+
} else {
156+
setSelectedCompletion(-1);
157+
}
158+
} else if (fsSuggestions.length > 0) {
159+
// Token cleared → clear menu
160+
setFsSuggestions([]);
161+
setSelectedCompletion(-1);
162+
}
163+
}
164+
}
165+
166+
/**
167+
* Result of replacing text with a file system suggestion
168+
*/
169+
interface ReplacementResult {
170+
/** The new text with the suggestion applied */
171+
text: string;
172+
/** The selected suggestion if a replacement was made */
173+
suggestion: FileSystemSuggestion | null;
174+
/** Whether a replacement was actually made */
175+
wasReplaced: boolean;
176+
}
177+
178+
// --- Helper for replacing input with file system suggestion ---
179+
function getFileSystemSuggestion(
180+
txt: string,
181+
requireAtPrefix: boolean = false,
182+
): ReplacementResult {
183+
if (fsSuggestions.length === 0 || selectedCompletion < 0) {
184+
return { text: txt, suggestion: null, wasReplaced: false };
185+
}
186+
187+
const words = txt.trim().split(/\s+/);
188+
const lastWord = words[words.length - 1] ?? "";
189+
190+
// Check if @ prefix is required and the last word doesn't have it
191+
if (requireAtPrefix && !lastWord.startsWith("@")) {
192+
return { text: txt, suggestion: null, wasReplaced: false };
193+
}
194+
195+
const selected = fsSuggestions[selectedCompletion];
196+
if (!selected) {
197+
return { text: txt, suggestion: null, wasReplaced: false };
198+
}
199+
200+
const replacement = lastWord.startsWith("@")
201+
? `@${selected.path}`
202+
: selected.path;
203+
words[words.length - 1] = replacement;
204+
return {
205+
text: words.join(" "),
206+
suggestion: selected,
207+
wasReplaced: true,
208+
};
209+
}
210+
105211
// Load command history on component mount
106212
useEffect(() => {
107213
async function loadHistory() {
@@ -223,21 +329,12 @@ export default function TerminalChatInput({
223329
}
224330

225331
if (_key.tab && selectedCompletion >= 0) {
226-
const words = input.trim().split(/\s+/);
227-
const selected = fsSuggestions[selectedCompletion];
228-
229-
if (words.length > 0 && selected) {
230-
words[words.length - 1] = selected;
231-
const newText = words.join(" ");
232-
setInput(newText);
233-
// Force remount of the editor with the new text
234-
setEditorKey((k) => k + 1);
235-
236-
// We need to move the cursor to the end after editor remounts
237-
setTimeout(() => {
238-
editorRef.current?.moveCursorToEnd?.();
239-
}, 0);
332+
const { text: newText, wasReplaced } =
333+
getFileSystemSuggestion(input);
240334

335+
// Only proceed if the text was actually changed
336+
if (wasReplaced) {
337+
applyFsSuggestion(newText);
241338
setFsSuggestions([]);
242339
setSelectedCompletion(-1);
243340
}
@@ -277,7 +374,7 @@ export default function TerminalChatInput({
277374

278375
setInput(history[newIndex]?.command ?? "");
279376
// Re-mount the editor so it picks up the new initialText
280-
setEditorKey((k) => k + 1);
377+
setEditorState((s) => ({ key: s.key + 1 }));
281378
return; // handled
282379
}
283380

@@ -296,28 +393,23 @@ export default function TerminalChatInput({
296393
if (newIndex >= history.length) {
297394
setHistoryIndex(null);
298395
setInput(draftInput);
299-
setEditorKey((k) => k + 1);
396+
setEditorState((s) => ({ key: s.key + 1 }));
300397
} else {
301398
setHistoryIndex(newIndex);
302399
setInput(history[newIndex]?.command ?? "");
303-
setEditorKey((k) => k + 1);
400+
setEditorState((s) => ({ key: s.key + 1 }));
304401
}
305402
return; // handled
306403
}
307404
// Otherwise let it propagate
308405
}
309406

310-
if (_key.tab) {
311-
const words = input.split(/\s+/);
312-
const mostRecentWord = words[words.length - 1];
313-
if (mostRecentWord === undefined || mostRecentWord === "") {
314-
return;
315-
}
316-
const completions = getFileSystemSuggestions(mostRecentWord);
317-
setFsSuggestions(completions);
318-
if (completions.length > 0) {
319-
setSelectedCompletion(0);
320-
}
407+
// Defer filesystem suggestion logic to onSubmit if enter key is pressed
408+
if (!_key.return) {
409+
// Pressing tab should trigger the file system suggestions
410+
const shouldUpdateSelection = _key.tab;
411+
const targetInput = _key.delete ? input.slice(0, -1) : input + _input;
412+
updateFsSuggestions(targetInput, shouldUpdateSelection);
321413
}
322414
}
323415

@@ -599,7 +691,10 @@ export default function TerminalChatInput({
599691
);
600692
text = text.trim();
601693

602-
const inputItem = await createInputItem(text, images);
694+
// Expand @file tokens into XML blocks for the model
695+
const expandedText = await expandFileTags(text);
696+
697+
const inputItem = await createInputItem(expandedText, images);
603698
submitInput([inputItem]);
604699

605700
// Get config for history persistence.
@@ -673,28 +768,30 @@ export default function TerminalChatInput({
673768
setHistoryIndex(null);
674769
}
675770
setInput(txt);
676-
677-
// Clear tab completions if a space is typed
678-
if (txt.endsWith(" ")) {
679-
setFsSuggestions([]);
680-
setSelectedCompletion(-1);
681-
} else if (fsSuggestions.length > 0) {
682-
// Update file suggestions as user types
683-
const words = txt.trim().split(/\s+/);
684-
const mostRecentWord =
685-
words.length > 0 ? words[words.length - 1] : "";
686-
if (mostRecentWord !== undefined) {
687-
setFsSuggestions(getFileSystemSuggestions(mostRecentWord));
688-
}
689-
}
690771
}}
691-
key={editorKey}
772+
key={editorState.key}
773+
initialCursorOffset={editorState.initialCursorOffset}
692774
initialText={input}
693775
height={6}
694776
focus={active}
695777
onSubmit={(txt) => {
696-
onSubmit(txt);
697-
setEditorKey((k) => k + 1);
778+
// If final token is an @path, replace with filesystem suggestion if available
779+
const {
780+
text: replacedText,
781+
suggestion,
782+
wasReplaced,
783+
} = getFileSystemSuggestion(txt, true);
784+
785+
// If we replaced @path token with a directory, don't submit
786+
if (wasReplaced && suggestion?.isDirectory) {
787+
applyFsSuggestion(replacedText);
788+
// Update suggestions for the new directory
789+
updateFsSuggestions(replacedText, true);
790+
return;
791+
}
792+
793+
onSubmit(replacedText);
794+
setEditorState((s) => ({ key: s.key + 1 }));
698795
setInput("");
699796
setHistoryIndex(null);
700797
setDraftInput("");
@@ -741,7 +838,7 @@ export default function TerminalChatInput({
741838
</Text>
742839
) : fsSuggestions.length > 0 ? (
743840
<TextCompletions
744-
completions={fsSuggestions}
841+
completions={fsSuggestions.map((suggestion) => suggestion.path)}
745842
selectedCompletion={selectedCompletion}
746843
displayLimit={5}
747844
/>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
} from "openai/resources/responses/responses";
1111

1212
import { useTerminalSize } from "../../hooks/use-terminal-size";
13+
import { collapseXmlBlocks } from "../../utils/file-tag-utils";
1314
import { parseToolCall, parseToolCallOutput } from "../../utils/parsers";
1415
import chalk, { type ForegroundColorName } from "chalk";
1516
import { Box, Text } from "ink";
@@ -137,7 +138,7 @@ function TerminalChatResponseMessage({
137138
: c.type === "refusal"
138139
? c.refusal
139140
: c.type === "input_text"
140-
? c.text
141+
? collapseXmlBlocks(c.text)
141142
: c.type === "input_image"
142143
? "<Image>"
143144
: c.type === "input_file"

0 commit comments

Comments
 (0)