diff --git a/codex-cli/bin/codex b/codex-cli/bin/codex deleted file mode 100755 index 9bf96bf2..00000000 --- a/codex-cli/bin/codex +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env sh -# resolve script path in case of symlink -SOURCE="$0" -while [ -h "$SOURCE" ]; do - DIR=$(dirname "$SOURCE") - SOURCE=$(readlink "$SOURCE") - case "$SOURCE" in - /*) ;; # absolute path - *) SOURCE="$DIR/$SOURCE" ;; # relative path - esac -done -DIR=$(cd "$(dirname "$SOURCE")" && pwd) -if command -v node >/dev/null 2>&1; then - exec node "$DIR/../dist/cli.js" "$@" -elif command -v bun >/dev/null 2>&1; then - exec bun "$DIR/../dist/cli.js" "$@" -else - echo "Error: node or bun is required to run codex" >&2 - exit 1 -fi \ No newline at end of file diff --git a/codex-cli/bin/codex.js b/codex-cli/bin/codex.js old mode 100644 new mode 100755 index 347dc854..1df18d1f --- a/codex-cli/bin/codex.js +++ b/codex-cli/bin/codex.js @@ -1,4 +1,5 @@ #!/usr/bin/env node + // Unified entry point for Codex CLI on all platforms // Dynamically loads the compiled ESM bundle in dist/cli.js @@ -18,7 +19,9 @@ const cliUrl = pathToFileURL(cliPath).href; try { await import(cliUrl); } catch (err) { + // eslint-disable-next-line no-console console.error(err); + // eslint-disable-next-line no-undef process.exit(1); } })(); diff --git a/codex-cli/package.json b/codex-cli/package.json index cc1714f4..df3edd1c 100644 --- a/codex-cli/package.json +++ b/codex-cli/package.json @@ -47,7 +47,7 @@ "marked-terminal": "^7.3.0", "meow": "^13.2.0", "open": "^10.1.0", - "openai": "^4.89.0", + "openai": "^4.95.1", "react": "^18.2.0", "shell-quote": "^1.8.2", "strip-ansi": "^7.1.0", diff --git a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx index 987e04f3..213dd8c9 100644 --- a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx @@ -1,82 +1,28 @@ import { log, isLoggingEnabled } from "../../utils/agent/log.js"; -import Spinner from "../vendor/ink-spinner.js"; import { Box, Text, useInput, useStdin } from "ink"; import React, { useState } from "react"; import { useInterval } from "use-interval"; -const thinkingTexts = ["Thinking"]; /* [ - "Consulting the rubber duck", - "Maximizing paperclips", - "Reticulating splines", - "Immanentizing the Eschaton", - "Thinking", - "Thinking about thinking", - "Spinning in circles", - "Counting dust specks", - "Updating priors", - "Feeding the utility monster", - "Taking off", - "Wireheading", - "Counting to infinity", - "Staring into the Basilisk", - "Negotiationing acausal trades", - "Searching the library of babel", - "Multiplying matrices", - "Solving the halting problem", - "Counting grains of sand", - "Simulating a simulation", - "Asking the oracle", - "Detangling qubits", - "Reading tea leaves", - "Pondering universal love and transcendent joy", - "Feeling the AGI", - "Shaving the yak", - "Escaping local minima", - "Pruning the search tree", - "Descending the gradient", - "Bikeshedding", - "Securing funding", - "Rewriting in Rust", - "Engaging infinite improbability drive", - "Clapping with one hand", - "Synthesizing", - "Rebasing thesis onto antithesis", - "Transcending the loop", - "Frogeposting", - "Summoning", - "Peeking beyond the veil", - "Seeking", - "Entering deep thought", - "Meditating", - "Decomposing", - "Creating", - "Beseeching the machine spirit", - "Calibrating moral compass", - "Collapsing the wave function", - "Doodling", - "Translating whale song", - "Whispering to silicon", - "Looking for semicolons", - "Asking ChatGPT", - "Bargaining with entropy", - "Channeling", - "Cooking", - "Parroting stochastically", -]; */ +// Retaining a single static placeholder text for potential future use. The +// more elaborate randomised thinking prompts were removed to streamline the +// UI – the elapsed‑time counter now provides sufficient feedback. export default function TerminalChatInputThinking({ onInterrupt, active, + thinkingSeconds, }: { onInterrupt: () => void; active: boolean; + thinkingSeconds: number; }): React.ReactElement { - const [dots, setDots] = useState(""); const [awaitingConfirm, setAwaitingConfirm] = useState(false); + const [dots, setDots] = useState(""); - const [thinkingText, setThinkingText] = useState( - () => thinkingTexts[Math.floor(Math.random() * thinkingTexts.length)], - ); + // Animate the ellipsis + useInterval(() => { + setDots((prev) => (prev.length < 3 ? prev + "." : "")); + }, 500); const { stdin, setRawMode } = useStdin(); @@ -110,25 +56,7 @@ export default function TerminalChatInputThinking({ }; }, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]); - useInterval(() => { - setDots((prev) => (prev.length < 3 ? prev + "." : "")); - }, 500); - - useInterval( - () => { - setThinkingText((prev) => { - let next = prev; - if (thinkingTexts.length > 1) { - while (next === prev) { - next = - thinkingTexts[Math.floor(Math.random() * thinkingTexts.length)]; - } - } - return next; - }); - }, - active ? 30000 : null, - ); + // No timers required beyond tracking the elapsed seconds supplied via props. useInput( (_input, key) => { @@ -153,12 +81,41 @@ export default function TerminalChatInputThinking({ { isActive: active }, ); + // Custom ball animation including the elapsed seconds + const ballFrames = [ + "( ● )", + "( ● )", + "( ● )", + "( ● )", + "( ●)", + "( ● )", + "( ● )", + "( ● )", + "( ● )", + "(● )", + ]; + + const [frame, setFrame] = useState(0); + + useInterval(() => { + setFrame((idx) => (idx + 1) % ballFrames.length); + }, 80); + + // Preserve the spinner (ball) animation while keeping the elapsed seconds + // text static. We achieve this by rendering the bouncing ball inside the + // parentheses and appending the seconds counter *after* the spinner rather + // than injecting it directly next to the ball (which caused the counter to + // move horizontally together with the ball). + + const frameTemplate = ballFrames[frame] ?? ballFrames[0]; + const frameWithSeconds = `${frameTemplate} ${thinkingSeconds}s`; + return ( - + {frameWithSeconds} - {thinkingText} + Thinking {dots} diff --git a/codex-cli/src/components/chat/terminal-chat-input.tsx b/codex-cli/src/components/chat/terminal-chat-input.tsx index fda19b6b..d032e22d 100644 --- a/codex-cli/src/components/chat/terminal-chat-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input.tsx @@ -15,7 +15,6 @@ import { addToHistory, } from "../../utils/storage/command-history.js"; import { clearTerminal, onExit } from "../../utils/terminal.js"; -import Spinner from "../vendor/ink-spinner.js"; import TextInput from "../vendor/ink-text-input.js"; import { Box, Text, useApp, useInput, useStdin } from "ink"; import { fileURLToPath } from "node:url"; @@ -45,6 +44,7 @@ export default function TerminalChatInput({ onCompact, interruptAgent, active, + thinkingSeconds, items = [], }: { isNew: boolean; @@ -66,6 +66,7 @@ export default function TerminalChatInput({ onCompact: () => void; interruptAgent: () => void; active: boolean; + thinkingSeconds: number; // New: current conversation items so we can include them in bug reports items?: Array; }): React.ReactElement { @@ -265,7 +266,9 @@ export default function TerminalChatInput({ items: items ?? [], cliVersion: CLI_VERSION, model: loadConfig().model ?? "unknown", - platform: `${os.platform()} ${os.arch()} ${os.release()}`, + platform: [os.platform(), os.arch(), os.release()] + .map((s) => `\`${s}\``) + .join(" | "), }); // Open the URL in the user's default browser @@ -416,6 +419,7 @@ export default function TerminalChatInput({ ) : ( @@ -491,12 +495,42 @@ export default function TerminalChatInput({ function TerminalChatInputThinking({ onInterrupt, active, + thinkingSeconds, }: { onInterrupt: () => void; active: boolean; + thinkingSeconds: number; }) { - const [dots, setDots] = useState(""); const [awaitingConfirm, setAwaitingConfirm] = useState(false); + const [dots, setDots] = useState(""); + + // Animate ellipsis + useInterval(() => { + setDots((prev) => (prev.length < 3 ? prev + "." : "")); + }, 500); + + // Spinner frames with embedded seconds + const ballFrames = [ + "( ● )", + "( ● )", + "( ● )", + "( ● )", + "( ●)", + "( ● )", + "( ● )", + "( ● )", + "( ● )", + "(● )", + ]; + const [frame, setFrame] = useState(0); + + useInterval(() => { + setFrame((idx) => (idx + 1) % ballFrames.length); + }, 80); + + // Keep the elapsed‑seconds text fixed while the ball animation moves. + const frameTemplate = ballFrames[frame] ?? ballFrames[0]; + const frameWithSeconds = `${frameTemplate} ${thinkingSeconds}s`; // --------------------------------------------------------------------- // Raw stdin listener to catch the case where the terminal delivers two @@ -544,10 +578,7 @@ function TerminalChatInputThinking({ }; }, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]); - // Cycle the "Thinking…" animation dots. - useInterval(() => { - setDots((prev) => (prev.length < 3 ? prev + "." : "")); - }, 500); + // No local timer: the parent component supplies the elapsed time via props. // Listen for the escape key to allow the user to interrupt the current // operation. We require two presses within a short window (1.5s) to avoid @@ -578,8 +609,11 @@ function TerminalChatInputThinking({ return ( - - Thinking{dots} + {frameWithSeconds} + + Thinking + {dots} + {awaitingConfirm && ( diff --git a/codex-cli/src/components/chat/terminal-chat-new-input.tsx b/codex-cli/src/components/chat/terminal-chat-new-input.tsx index 9edb4e63..948329d9 100644 --- a/codex-cli/src/components/chat/terminal-chat-new-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-new-input.tsx @@ -17,7 +17,6 @@ import { addToHistory, } from "../../utils/storage/command-history.js"; import { clearTerminal, onExit } from "../../utils/terminal.js"; -import Spinner from "../vendor/ink-spinner.js"; import { Box, Text, useApp, useInput, useStdin } from "ink"; import { fileURLToPath } from "node:url"; import React, { useCallback, useState, Fragment, useEffect } from "react"; @@ -37,39 +36,7 @@ const typeHelpText = `ctrl+c to exit | "/clear" to reset context | "/help" for c const DEBUG_HIST = process.env["DEBUG_TCI"] === "1" || process.env["DEBUG_TCI"] === "true"; -const thinkingTexts = ["Thinking"]; /* [ - "Consulting the rubber duck", - "Maximizing paperclips", - "Reticulating splines", - "Immanentizing the Eschaton", - "Thinking", - "Thinking about thinking", - "Spinning in circles", - "Counting dust specks", - "Updating priors", - "Feeding the utility monster", - "Taking off", - "Wireheading", - "Counting to infinity", - "Staring into the Basilisk", - "Running acausal tariff negotiations", - "Searching the library of babel", - "Multiplying matrices", - "Solving the halting problem", - "Counting grains of sand", - "Simulating a simulation", - "Asking the oracle", - "Detangling qubits", - "Reading tea leaves", - "Pondering universal love and transcendent joy", - "Feeling the AGI", - "Shaving the yak", - "Escaping local minima", - "Pruning the search tree", - "Descending the gradient", - "Painting the bikeshed", - "Securing funding", -]; */ +// Placeholder for potential dynamic prompts – currently not used. export default function TerminalChatInput({ isNew: _isNew, @@ -87,6 +54,7 @@ export default function TerminalChatInput({ openHelpOverlay, interruptAgent, active, + thinkingSeconds, }: { isNew: boolean; loading: boolean; @@ -106,6 +74,7 @@ export default function TerminalChatInput({ openHelpOverlay: () => void; interruptAgent: () => void; active: boolean; + thinkingSeconds: number; }): React.ReactElement { const app = useApp(); const [selectedSuggestion, setSelectedSuggestion] = useState(0); @@ -389,6 +358,7 @@ export default function TerminalChatInput({ ) : ( @@ -454,15 +424,43 @@ export default function TerminalChatInput({ function TerminalChatInputThinking({ onInterrupt, active, + thinkingSeconds, }: { onInterrupt: () => void; active: boolean; + thinkingSeconds: number; }) { - const [dots, setDots] = useState(""); const [awaitingConfirm, setAwaitingConfirm] = useState(false); + const [dots, setDots] = useState(""); - const [thinkingText] = useState( - () => thinkingTexts[Math.floor(Math.random() * thinkingTexts.length)], + // Animate ellipsis + useInterval(() => { + setDots((prev) => (prev.length < 3 ? prev + "." : "")); + }, 500); + + // Spinner frames with seconds embedded + const ballFrames = [ + "( ● )", + "( ● )", + "( ● )", + "( ● )", + "( ●)", + "( ● )", + "( ● )", + "( ● )", + "( ● )", + "(● )", + ]; + const [frame, setFrame] = useState(0); + + useInterval(() => { + setFrame((idx) => (idx + 1) % ballFrames.length); + }, 80); + + const frameTemplate = ballFrames[frame] ?? ballFrames[0]; + const frameWithSeconds = (frameTemplate as string).replace( + "●", + `●${thinkingSeconds}s`, ); // --------------------------------------------------------------------- @@ -511,9 +509,7 @@ function TerminalChatInputThinking({ }; }, [stdin, awaitingConfirm, onInterrupt, active, setRawMode]); - useInterval(() => { - setDots((prev) => (prev.length < 3 ? prev + "." : "")); - }, 500); + // Elapsed time provided via props – no local interval needed. useInput( (_input, key) => { @@ -541,9 +537,9 @@ function TerminalChatInputThinking({ return ( - + {frameWithSeconds} - {thinkingText} + Thinking {dots} diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index 1cfeffe1..d2cb64c4 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -517,6 +517,7 @@ export default function TerminalChat({ return {}; }} items={items} + thinkingSeconds={thinkingSeconds} /> )} {overlayMode === "history" && ( diff --git a/codex-cli/src/components/chat/terminal-message-history.tsx b/codex-cli/src/components/chat/terminal-message-history.tsx index 07c3ac37..e20eaabe 100644 --- a/codex-cli/src/components/chat/terminal-message-history.tsx +++ b/codex-cli/src/components/chat/terminal-message-history.tsx @@ -4,7 +4,7 @@ import type { ResponseItem } from "openai/resources/responses/responses.mjs"; import TerminalChatResponseItem from "./terminal-chat-response-item.js"; import TerminalHeader from "./terminal-header.js"; -import { Box, Static, Text } from "ink"; +import { Box, Static } from "ink"; import React, { useMemo } from "react"; // A batch entry can either be a standalone response item or a grouped set of @@ -26,8 +26,9 @@ type MessageHistoryProps = { const MessageHistory: React.FC = ({ batch, headerProps, - loading, - thinkingSeconds, + // `loading` and `thinkingSeconds` handled by input component now. + loading: _loading, + thinkingSeconds: _thinkingSeconds, fullStdout, }) => { // Flatten batch entries to response items. @@ -35,11 +36,8 @@ const MessageHistory: React.FC = ({ return ( - {loading && ( - - {`thinking for ${thinkingSeconds}s`} - - )} + {/* The dedicated thinking indicator in the input area now displays the + elapsed time, so we no longer render a separate counter here. */} {(item, index) => { if (item === "header") { diff --git a/codex-cli/src/utils/bug-report.ts b/codex-cli/src/utils/bug-report.ts index 0fbd0329..768c695d 100644 --- a/codex-cli/src/utils/bug-report.ts +++ b/codex-cli/src/utils/bug-report.ts @@ -1,4 +1,7 @@ -import type { ResponseItem } from "openai/resources/responses/responses.mjs"; +import type { + ResponseItem, + ResponseOutputItem, +} from "openai/resources/responses/responses.mjs"; /** * Build a GitHub issues‐new URL that pre‑fills the Codex 2‑bug‑report.yml @@ -12,7 +15,7 @@ export function buildBugReportUrl({ platform, }: { /** Chat history so we can summarise user steps */ - items: Array; + items: Array; /** CLI revision string (e.g. output of `codex --revision`) */ cliVersion: string; /** Active model name */ @@ -25,16 +28,10 @@ export function buildBugReportUrl({ labels: "bug", }); - // Template ids ------------------------------------------------------------- params.set("version", cliVersion); params.set("model", model); + params.set("platform", platform); - // The platform input has no explicit `id`, so GitHub falls back to a slug of - // the label text. For “What platform is your computer?” that slug is: - // what-platform-is-your-computer - params.set("what-platform-is-your-computer", platform); - - // Build the steps bullet list --------------------------------------------- const bullets: Array = []; for (let i = 0; i < items.length; ) { const entry = items[i]; @@ -50,12 +47,14 @@ export function buildBugReportUrl({ let reasoning = 0; let toolCalls = 0; let j = i + 1; - while ( - j < items.length && - !(entry?.type === "message" && entry.role === "user") - ) { + while (j < items.length) { const it = items[j]; - if (it?.type === "message" && it?.role === "assistant") { + if (it?.type === "message" && it?.role === "user") { + break; + } else if ( + it?.type === "reasoning" || + (it?.type === "message" && it?.role === "assistant") + ) { reasoning += 1; } else if (it?.type === "function_call") { toolCalls += 1; @@ -63,8 +62,10 @@ export function buildBugReportUrl({ j++; } + const codeBlock = `\`\`\`\n ${messageText}\n \`\`\``; + bullets.push( - `- "${messageText}"\n - \`${reasoning} reasoning steps\` | \`${toolCalls} tool calls\``, + `- ${codeBlock}\n - \`${reasoning} reasoning\` | \`${toolCalls} tool\``, ); i = j; diff --git a/codex-cli/tests/terminal-chat-input-compact.test.tsx b/codex-cli/tests/terminal-chat-input-compact.test.tsx index 194a61ca..d93a07ab 100644 --- a/codex-cli/tests/terminal-chat-input-compact.test.tsx +++ b/codex-cli/tests/terminal-chat-input-compact.test.tsx @@ -23,6 +23,7 @@ describe("TerminalChatInput compact command", () => { onCompact: () => {}, interruptAgent: () => {}, active: true, + thinkingSeconds: 0, }; const { lastFrameStripped } = renderTui(); const frame = lastFrameStripped(); diff --git a/codex-cli/tsconfig.json b/codex-cli/tsconfig.json index e441160f..43a2287e 100644 --- a/codex-cli/tsconfig.json +++ b/codex-cli/tsconfig.json @@ -30,5 +30,5 @@ "forceConsistentCasingInFileNames": true, "skipLibCheck": true }, - "include": ["src", "tests"] + "include": ["src", "tests", "bin"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b8e2dae..d4bee57d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,7 +65,7 @@ importers: specifier: ^10.1.0 version: 10.1.1 openai: - specifier: ^4.89.0 + specifier: ^4.95.1 version: 4.95.1(ws@8.18.1)(zod@3.24.3) react: specifier: ^18.2.0 @@ -2739,7 +2739,7 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: - '@types/node': 18.19.86 + '@types/node': 22.14.1 form-data: 4.0.2 '@types/node@18.19.86':