mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
merge
This commit is contained in:
@@ -26,6 +26,7 @@ body:
|
||||
label: Which model were you using?
|
||||
description: Like `gpt-4.1`, `o4-mini`, `o3`, etc.
|
||||
- type: input
|
||||
id: platform
|
||||
attributes:
|
||||
label: What platform is your computer?
|
||||
description: |
|
||||
|
||||
+41
-30
@@ -19,40 +19,51 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
# Run codex-cli/ tasks first because they are higher signal.
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.8.1
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies (codex-cli)
|
||||
working-directory: codex-cli
|
||||
run: npm ci
|
||||
|
||||
- name: Check formatting (codex-cli)
|
||||
working-directory: codex-cli
|
||||
run: npm run format
|
||||
|
||||
- name: Run tests (codex-cli)
|
||||
working-directory: codex-cli
|
||||
run: npm run test
|
||||
|
||||
- name: Lint (codex-cli)
|
||||
working-directory: codex-cli
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
npm run lint -- \
|
||||
echo "store_path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.store_path }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
# Run all tasks using workspace filters
|
||||
|
||||
- name: Check TypeScript code formatting
|
||||
working-directory: codex-cli
|
||||
run: pnpm run format
|
||||
|
||||
- name: Check Markdown and config file formatting
|
||||
run: pnpm run format
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm run test
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
pnpm --filter @openai/codex exec -- eslint src tests --ext ts --ext tsx \
|
||||
--report-unused-disable-directives \
|
||||
--rule "no-console:error" \
|
||||
--rule "no-debugger:error" \
|
||||
--max-warnings=-1
|
||||
|
||||
- name: Type‑check (codex-cli)
|
||||
working-directory: codex-cli
|
||||
run: npm run typecheck
|
||||
- name: Type-check
|
||||
run: pnpm run typecheck
|
||||
|
||||
- name: Build (codex-cli)
|
||||
working-directory: codex-cli
|
||||
run: npm run build
|
||||
|
||||
# Run formatting checks in the root directory last.
|
||||
|
||||
- name: Install dependencies (root)
|
||||
run: npm ci
|
||||
|
||||
- name: Check formatting (root)
|
||||
run: npm run format
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
+11
-2
@@ -1,5 +1,11 @@
|
||||
# deps
|
||||
# Node.js dependencies
|
||||
node_modules
|
||||
.pnpm-store
|
||||
.pnpm-debug.log
|
||||
|
||||
# Keep pnpm-lock.yaml
|
||||
!pnpm-lock.yaml
|
||||
|
||||
# build
|
||||
dist/
|
||||
@@ -20,6 +26,10 @@ result
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# cli tools
|
||||
CLAUDE.md
|
||||
.claude/
|
||||
|
||||
# caches
|
||||
.cache/
|
||||
.turbo/
|
||||
@@ -61,9 +71,8 @@ Icon?
|
||||
# Unwanted package managers
|
||||
.yarn/
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# release
|
||||
package.json-e
|
||||
session.ts-e
|
||||
CHANGELOG.ignore.md
|
||||
CHANGELOG.ignore.md
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pnpm lint-staged
|
||||
@@ -0,0 +1,4 @@
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
node-linker=hoisted
|
||||
prefer-workspace-packages=true
|
||||
@@ -1,2 +1,3 @@
|
||||
/codex-cli/dist
|
||||
/codex-cli/node_modules
|
||||
pnpm-lock.yaml
|
||||
|
||||
+16
-2
@@ -2,7 +2,21 @@
|
||||
|
||||
You can install any of these versions: `npm install -g codex@version`
|
||||
|
||||
## 0.1.2504172351
|
||||
## `0.1.2504181820`
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- Add `/bug` report command (#312)
|
||||
- Notify when a newer version is available (#333)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Update context left display logic in TerminalChatInput component (#307)
|
||||
- Improper spawn of sh on Windows Powershell (#318)
|
||||
- `/bug` report command, thinking indicator (#381)
|
||||
- Include pnpm lock file (#377)
|
||||
|
||||
## `0.1.2504172351`
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
@@ -14,7 +28,7 @@ You can install any of these versions: `npm install -g codex@version`
|
||||
- Raw-exec-process-group.test improve reliability and error handling (#280)
|
||||
- Canonicalize the writeable paths used in seatbelt policy (#275)
|
||||
|
||||
## 0.1.2504172304
|
||||
## `0.1.2504172304`
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# Migration to pnpm
|
||||
|
||||
This project has been migrated from npm to pnpm to improve dependency management and developer experience.
|
||||
|
||||
## Why pnpm?
|
||||
|
||||
- **Faster installation**: pnpm is significantly faster than npm and yarn
|
||||
- **Disk space savings**: pnpm uses a content-addressable store to avoid duplication
|
||||
- **Phantom dependency prevention**: pnpm creates a strict node_modules structure
|
||||
- **Native workspaces support**: simplified monorepo management
|
||||
|
||||
## How to use pnpm
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Global installation of pnpm
|
||||
npm install -g pnpm@10.8.1
|
||||
|
||||
# Or with corepack (available with Node.js 22+)
|
||||
corepack enable
|
||||
corepack prepare pnpm@10.8.1 --activate
|
||||
```
|
||||
|
||||
### Common commands
|
||||
|
||||
| npm command | pnpm equivalent |
|
||||
| --------------- | ---------------- |
|
||||
| `npm install` | `pnpm install` |
|
||||
| `npm run build` | `pnpm run build` |
|
||||
| `npm test` | `pnpm test` |
|
||||
| `npm run lint` | `pnpm run lint` |
|
||||
|
||||
### Workspace-specific commands
|
||||
|
||||
| Action | Command |
|
||||
| ------------------------------------------ | ---------------------------------------- |
|
||||
| Run a command in a specific package | `pnpm --filter @openai/codex run build` |
|
||||
| Install a dependency in a specific package | `pnpm --filter @openai/codex add lodash` |
|
||||
| Run a command in all packages | `pnpm -r run test` |
|
||||
|
||||
## Monorepo structure
|
||||
|
||||
```
|
||||
codex/
|
||||
├── pnpm-workspace.yaml # Workspace configuration
|
||||
├── .npmrc # pnpm configuration
|
||||
├── package.json # Root dependencies and scripts
|
||||
├── codex-cli/ # Main package
|
||||
│ └── package.json # codex-cli specific dependencies
|
||||
└── docs/ # Documentation (future package)
|
||||
```
|
||||
|
||||
## Configuration files
|
||||
|
||||
- **pnpm-workspace.yaml**: Defines the packages included in the monorepo
|
||||
- **.npmrc**: Configures pnpm behavior
|
||||
- **Root package.json**: Contains shared scripts and dependencies
|
||||
|
||||
## CI/CD
|
||||
|
||||
CI/CD workflows have been updated to use pnpm instead of npm. Make sure your CI environments use pnpm 10.8.1 or higher.
|
||||
|
||||
## Known issues
|
||||
|
||||
If you encounter issues with pnpm, try the following solutions:
|
||||
|
||||
1. Remove the `node_modules` folder and `pnpm-lock.yaml` file, then run `pnpm install`
|
||||
2. Make sure you're using pnpm 10.8.1 or higher
|
||||
3. Verify that Node.js 22 or higher is installed
|
||||
@@ -244,6 +244,8 @@ npm install -g @openai/codex
|
||||
yarn global add @openai/codex
|
||||
# or
|
||||
bun install -g @openai/codex
|
||||
# or
|
||||
pnpm add -g @openai/codex
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -256,9 +258,12 @@ bun install -g @openai/codex
|
||||
git clone https://github.com/openai/codex.git
|
||||
cd codex/codex-cli
|
||||
|
||||
# Enable corepack
|
||||
corepack enable
|
||||
|
||||
# Install dependencies and build
|
||||
npm install
|
||||
npm run build
|
||||
pnpm install
|
||||
pnpm build
|
||||
|
||||
# Get the usage and the options
|
||||
node ./dist/cli.js --help
|
||||
@@ -267,7 +272,7 @@ node ./dist/cli.js --help
|
||||
node ./dist/cli.js
|
||||
|
||||
# Or link the command globally for convenience
|
||||
npm link
|
||||
pnpm link
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -276,13 +281,27 @@ npm link
|
||||
|
||||
## Configuration
|
||||
|
||||
Codex looks for config files in **`~/.codex/`**.
|
||||
Codex looks for config files in **`~/.codex/`** (either YAML or JSON format).
|
||||
|
||||
```yaml
|
||||
# ~/.codex/config.yaml
|
||||
model: o4-mini # Default model
|
||||
approvalMode: suggest # or auto-edit, full-auto
|
||||
fullAutoErrorMode: ask-user # or ignore-and-continue
|
||||
notify: true # Enable desktop notifications for responses
|
||||
safeCommands:
|
||||
- npm test # Automatically approve npm test
|
||||
- yarn lint # Automatically approve yarn lint
|
||||
```
|
||||
|
||||
```json
|
||||
// ~/.codex/config.json
|
||||
{
|
||||
"model": "o4-mini",
|
||||
"approvalMode": "suggest",
|
||||
"fullAutoErrorMode": "ask-user",
|
||||
"notify": true
|
||||
}
|
||||
```
|
||||
|
||||
You can also define custom instructions:
|
||||
@@ -376,7 +395,7 @@ More broadly we welcome contributions – whether you are opening your very firs
|
||||
|
||||
- Create a _topic branch_ from `main` – e.g. `feat/interactive-prompt`.
|
||||
- Keep your changes focused. Multiple unrelated fixes should be opened as separate PRs.
|
||||
- Use `npm run test:watch` during development for super‑fast feedback.
|
||||
- Use `pnpm test:watch` during development for super‑fast feedback.
|
||||
- We use **Vitest** for unit tests, **ESLint** + **Prettier** for style, and **TypeScript** for type‑checking.
|
||||
- Before pushing, run the full test/type/lint suite:
|
||||
|
||||
@@ -403,14 +422,14 @@ npm test && npm run lint && npm run typecheck
|
||||
|
||||
```bash
|
||||
# Watch mode (tests rerun on change)
|
||||
npm run test:watch
|
||||
pnpm test:watch
|
||||
|
||||
# Type‑check without emitting files
|
||||
npm run typecheck
|
||||
pnpm typecheck
|
||||
|
||||
# Automatically fix lint + prettier issues
|
||||
npm run lint:fix
|
||||
npm run format:fix
|
||||
pnpm lint:fix
|
||||
pnpm format:fix
|
||||
```
|
||||
|
||||
#### Nix Flake Development
|
||||
@@ -500,13 +519,13 @@ To publish a new version of the CLI, run the release scripts defined in `codex-c
|
||||
|
||||
1. Open the `codex-cli` directory
|
||||
2. Make sure you're on a branch like `git checkout -b bump-version`
|
||||
3. Bump the version and `CLI_VERSION` to current datetime: `npm run release:version`
|
||||
3. Bump the version and `CLI_VERSION` to current datetime: `pnpm release:version`
|
||||
4. Commit the version bump (with DCO sign-off):
|
||||
```bash
|
||||
git add codex-cli/src/utils/session.ts codex-cli/package.json
|
||||
git commit -s -m "chore(release): codex-cli v$(node -p \"require('./codex-cli/package.json').version\")"
|
||||
```
|
||||
5. Copy README, build, and publish to npm: `npm run release`
|
||||
5. Copy README, build, and publish to npm: `pnpm release`
|
||||
6. Push to branch: `git push origin HEAD`
|
||||
|
||||
---
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
if [ -z "$husky_skip_init" ]; then
|
||||
debug () {
|
||||
if [ "$HUSKY_DEBUG" = "1" ]; then
|
||||
echo "husky (debug) - $1"
|
||||
fi
|
||||
}
|
||||
|
||||
readonly hook_name="$(basename -- "$0")"
|
||||
debug "starting $hook_name..."
|
||||
|
||||
if [ "$HUSKY" = "0" ]; then
|
||||
debug "HUSKY env variable is set to 0, skipping hook"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f ~/.huskyrc ]; then
|
||||
debug "sourcing ~/.huskyrc"
|
||||
. ~/.huskyrc
|
||||
fi
|
||||
|
||||
readonly husky_skip_init=1
|
||||
export husky_skip_init
|
||||
sh -e "$0" "$@"
|
||||
exitCode="$?"
|
||||
|
||||
if [ $exitCode != 0 ]; then
|
||||
echo "husky - $hook_name hook exited with code $exitCode (error)"
|
||||
fi
|
||||
|
||||
exit $exitCode
|
||||
fi
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Run lint-staged to check files that are about to be committed
|
||||
npm run pre-commit
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# Run tests and type checking before pushing
|
||||
npm test && npm run typecheck
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"*.{ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{json,md,yml}": [
|
||||
"prettier --write"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Unified entry point for Codex CLI on all platforms
|
||||
// Dynamically loads the compiled ESM bundle in dist/cli.js
|
||||
|
||||
import path from 'path';
|
||||
import { fileURLToPath, pathToFileURL } from 'url';
|
||||
|
||||
// Determine this script's directory
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Resolve the path to the compiled CLI bundle
|
||||
const cliPath = path.resolve(__dirname, '../dist/cli.js');
|
||||
const cliUrl = pathToFileURL(cliPath).href;
|
||||
|
||||
// Load and execute the CLI
|
||||
(async () => {
|
||||
try {
|
||||
await import(cliUrl);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
// eslint-disable-next-line no-undef
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
Generated
-7372
File diff suppressed because it is too large
Load Diff
+9
-17
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@openai/codex",
|
||||
"version": "0.1.2504172351",
|
||||
"version": "0.1.2504181820",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"codex": "bin/codex"
|
||||
"codex": "bin/codex.js"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
@@ -22,10 +22,8 @@
|
||||
"build:dev": "NODE_ENV=development node build.mjs --dev && NODE_OPTIONS=--enable-source-maps node dist/cli-dev.js",
|
||||
"release:readme": "cp ../README.md ./README.md",
|
||||
"release:version": "TS=$(date +%y%m%d%H%M) && sed -E -i'' -e \"s/\\\"0\\.1\\.[0-9]{10}\\\"/\\\"0.1.${TS}\\\"/g\" package.json src/utils/session.ts",
|
||||
"release:build-and-publish": "npm run build && npm publish",
|
||||
"release": "npm run release:readme && npm run release:version && npm install && npm run release:build-and-publish",
|
||||
"prepare": "husky",
|
||||
"pre-commit": "lint-staged"
|
||||
"release:build-and-publish": "pnpm run build && npm publish",
|
||||
"release": "pnpm run release:readme && pnpm run release:version && pnpm install && pnpm run release:build-and-publish"
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
@@ -47,7 +45,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",
|
||||
@@ -62,8 +60,10 @@
|
||||
"@types/marked-terminal": "^6.1.1",
|
||||
"@types/react": "^18.0.32",
|
||||
"@types/shell-quote": "^1.7.5",
|
||||
"@types/which": "^3.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"boxen": "^8.0.1",
|
||||
"esbuild": "^0.25.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
@@ -71,21 +71,13 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"husky": "^9.1.7",
|
||||
"ink-testing-library": "^3.0.0",
|
||||
"lint-staged": "^15.5.1",
|
||||
"prettier": "^2.8.7",
|
||||
"punycode": "^2.3.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.3",
|
||||
"vitest": "^3.0.9",
|
||||
"whatwg-url": "^14.2.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"braces": "^3.0.3",
|
||||
"micromatch": "^4.0.8",
|
||||
"semver": "^7.7.1"
|
||||
},
|
||||
"overrides": {
|
||||
"punycode": "^2.3.1"
|
||||
"whatwg-url": "^14.2.0",
|
||||
"which": "^5.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -23,6 +23,16 @@ fi
|
||||
|
||||
WORK_DIR=$(realpath "$WORK_DIR")
|
||||
|
||||
# Generate a unique container name based on the normalized work directory
|
||||
CONTAINER_NAME="codex_$(echo "$WORK_DIR" | sed 's/\//_/g' | sed 's/[^a-zA-Z0-9_-]//g')"
|
||||
|
||||
# Define cleanup to remove the container on script exit, ensuring no leftover containers
|
||||
cleanup() {
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
}
|
||||
# Trap EXIT to invoke cleanup regardless of how the script terminates
|
||||
trap cleanup EXIT
|
||||
|
||||
# Ensure a command is provided.
|
||||
if [ "$#" -eq 0 ]; then
|
||||
echo "Usage: $0 [--work_dir directory] \"COMMAND\""
|
||||
@@ -35,11 +45,11 @@ if [ -z "$WORK_DIR" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove any existing container named 'codex'.
|
||||
docker rm -f codex 2>/dev/null || true
|
||||
# Kill any existing container for the working directory using cleanup(), centralizing removal logic.
|
||||
cleanup
|
||||
|
||||
# Run the container with the specified directory mounted at the same path inside the container.
|
||||
docker run --name codex -d \
|
||||
docker run --name "$CONTAINER_NAME" -d \
|
||||
-e OPENAI_API_KEY \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=NET_RAW \
|
||||
@@ -48,7 +58,7 @@ docker run --name codex -d \
|
||||
sleep infinity
|
||||
|
||||
# Initialize the firewall inside the container.
|
||||
docker exec codex bash -c "sudo /usr/local/bin/init_firewall.sh"
|
||||
docker exec "$CONTAINER_NAME" bash -c "sudo /usr/local/bin/init_firewall.sh"
|
||||
|
||||
# Execute the provided command in the container, ensuring it runs in the work directory.
|
||||
# We use a parameterized bash command to safely handle the command and directory.
|
||||
@@ -57,4 +67,4 @@ quoted_args=""
|
||||
for arg in "$@"; do
|
||||
quoted_args+=" $(printf '%q' "$arg")"
|
||||
done
|
||||
docker exec -it codex bash -c "cd \"/app$WORK_DIR\" && codex --full-auto ${quoted_args}"
|
||||
docker exec -it "$CONTAINER_NAME" bash -c "cd \"/app$WORK_DIR\" && codex --full-auto ${quoted_args}"
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
identify_files_added,
|
||||
identify_files_needed,
|
||||
} from "./utils/agent/apply-patch";
|
||||
import { loadConfig } from "./utils/config";
|
||||
import * as path from "path";
|
||||
import { parse } from "shell-quote";
|
||||
|
||||
@@ -296,6 +297,24 @@ export function isSafeCommand(
|
||||
): SafeCommandReason | null {
|
||||
const [cmd0, cmd1, cmd2, cmd3] = command;
|
||||
|
||||
const config = loadConfig();
|
||||
if (config.safeCommands && Array.isArray(config.safeCommands)) {
|
||||
for (const safe of config.safeCommands) {
|
||||
// safe: "npm test" → ["npm", "test"]
|
||||
const safeArr = typeof safe === "string" ? safe.trim().split(/\s+/) : [];
|
||||
if (
|
||||
safeArr.length > 0 &&
|
||||
safeArr.length <= command.length &&
|
||||
safeArr.every((v, i) => v === command[i])
|
||||
) {
|
||||
return {
|
||||
reason: "User-defined safe command",
|
||||
group: "User config",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (cmd0) {
|
||||
case "cd":
|
||||
return {
|
||||
|
||||
+48
-9
@@ -17,6 +17,7 @@ import { AgentLoop } from "./utils/agent/agent-loop";
|
||||
import { initLogger } from "./utils/agent/log";
|
||||
import { ReviewDecision } from "./utils/agent/review";
|
||||
import { AutoApprovalMode } from "./utils/auto-approval-mode";
|
||||
import { checkForUpdates } from "./utils/check-updates";
|
||||
import {
|
||||
loadConfig,
|
||||
PRETTY_PRINT,
|
||||
@@ -70,6 +71,9 @@ const cli = meow(
|
||||
--full-stdout Do not truncate stdout/stderr from command outputs
|
||||
--notify Enable desktop notifications for responses
|
||||
|
||||
--flex-mode Use "flex-mode" processing mode for the request (only supported
|
||||
with models o3 and o4-mini)
|
||||
|
||||
Dangerous options
|
||||
--dangerously-auto-approve-everything
|
||||
Skip all confirmation prompts and execute commands without
|
||||
@@ -139,6 +143,11 @@ const cli = meow(
|
||||
type: "string",
|
||||
description: "Path to a markdown file to include as project doc",
|
||||
},
|
||||
flexMode: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Enable the flex-mode service tier (only supported by models o3 and o4-mini)",
|
||||
},
|
||||
fullStdout: {
|
||||
type: "boolean",
|
||||
description:
|
||||
@@ -249,9 +258,29 @@ config = {
|
||||
apiKey,
|
||||
...config,
|
||||
model: model ?? config.model,
|
||||
flexMode: Boolean(cli.flags.flexMode),
|
||||
notify: Boolean(cli.flags.notify),
|
||||
};
|
||||
|
||||
// Check for updates after loading config
|
||||
// This is important because we write state file in the config dir
|
||||
await checkForUpdates().catch();
|
||||
// ---------------------------------------------------------------------------
|
||||
// --flex-mode validation (only allowed for o3 and o4-mini)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (cli.flags.flexMode) {
|
||||
const allowedFlexModels = new Set(["o3", "o4-mini"]);
|
||||
if (!allowedFlexModels.has(config.model)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`The --flex-mode option is only supported when using the 'o3' or 'o4-mini' models. ` +
|
||||
`Current model: '${config.model}'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await isModelSupportedForResponses(config.model))) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
@@ -298,9 +327,6 @@ const additionalWritableRoots: ReadonlyArray<string> = (
|
||||
|
||||
// If we are running in --quiet mode, do that and exit.
|
||||
const quietMode = Boolean(cli.flags.quiet);
|
||||
const autoApproveEverything = Boolean(
|
||||
cli.flags.dangerouslyAutoApproveEverything,
|
||||
);
|
||||
const fullStdout = Boolean(cli.flags.fullStdout);
|
||||
|
||||
if (quietMode) {
|
||||
@@ -312,12 +338,19 @@ if (quietMode) {
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Determine approval policy for quiet mode based on flags
|
||||
const quietApprovalPolicy: ApprovalPolicy =
|
||||
cli.flags.fullAuto || cli.flags.approvalMode === "full-auto"
|
||||
? AutoApprovalMode.FULL_AUTO
|
||||
: cli.flags.autoEdit || cli.flags.approvalMode === "auto-edit"
|
||||
? AutoApprovalMode.AUTO_EDIT
|
||||
: config.approvalMode || AutoApprovalMode.SUGGEST;
|
||||
|
||||
await runQuietMode({
|
||||
prompt: prompt as string,
|
||||
imagePaths: imagePaths || [],
|
||||
approvalPolicy: autoApproveEverything
|
||||
? AutoApprovalMode.FULL_AUTO
|
||||
: AutoApprovalMode.SUGGEST,
|
||||
approvalPolicy: quietApprovalPolicy,
|
||||
additionalWritableRoots,
|
||||
config,
|
||||
});
|
||||
@@ -335,14 +368,15 @@ if (quietMode) {
|
||||
// it is more dangerous than --fullAuto we deliberately give it lower
|
||||
// priority so a user specifying both flags still gets the safer behaviour.
|
||||
// 3. --autoEdit – automatically approve edits, but prompt for commands.
|
||||
// 4. Default – suggest mode (prompt for everything).
|
||||
// 4. config.approvalMode - use the approvalMode setting from ~/.codex/config.json.
|
||||
// 5. Default – suggest mode (prompt for everything).
|
||||
|
||||
const approvalPolicy: ApprovalPolicy =
|
||||
cli.flags.fullAuto || cli.flags.approvalMode === "full-auto"
|
||||
? AutoApprovalMode.FULL_AUTO
|
||||
: cli.flags.autoEdit || cli.flags.approvalMode === "auto-edit"
|
||||
? AutoApprovalMode.AUTO_EDIT
|
||||
: AutoApprovalMode.SUGGEST;
|
||||
: config.approvalMode || AutoApprovalMode.SUGGEST;
|
||||
|
||||
preloadModels();
|
||||
|
||||
@@ -440,7 +474,12 @@ async function runQuietMode({
|
||||
getCommandConfirmation: (
|
||||
_command: Array<string>,
|
||||
): Promise<CommandConfirmation> => {
|
||||
return Promise.resolve({ review: ReviewDecision.NO_CONTINUE });
|
||||
// In quiet mode, default to NO_CONTINUE, except when in full-auto mode
|
||||
const reviewDecision =
|
||||
approvalPolicy === AutoApprovalMode.FULL_AUTO
|
||||
? ReviewDecision.YES
|
||||
: ReviewDecision.NO_CONTINUE;
|
||||
return Promise.resolve({ review: reviewDecision });
|
||||
},
|
||||
onLastResponseId: () => {
|
||||
/* intentionally ignored in quiet mode */
|
||||
|
||||
@@ -259,25 +259,47 @@ const MultilineTextEditorInner = (
|
||||
console.log("[MultilineTextEditor] event", { input, key });
|
||||
}
|
||||
|
||||
// 1) CSI‑u / modifyOtherKeys (Ink strips initial ESC, so we start with '[')
|
||||
// 1a) CSI-u / modifyOtherKeys *mode 2* (Ink strips initial ESC, so we
|
||||
// start with '[') – format: "[<code>;<modifiers>u".
|
||||
if (input.startsWith("[") && input.endsWith("u")) {
|
||||
const m = input.match(/^\[([0-9]+);([0-9]+)u$/);
|
||||
if (m && m[1] === "13") {
|
||||
const mod = Number(m[2]);
|
||||
// In xterm's encoding: bit‑1 (value 2) is Shift. Everything >1 that
|
||||
// isn't exactly 1 means some modifier was held. We treat *shift
|
||||
// present* (2,4,6,8) as newline; plain (1) as submit.
|
||||
// In xterm's encoding: bit-1 (value 2) is Shift. Everything >1 that
|
||||
// isn't exactly 1 means some modifier was held. We treat *shift or
|
||||
// alt present* (2,3,4,6,8,9) as newline; Ctrl (bit-2 / value 4)
|
||||
// triggers submit. See xterm/DEC modifyOtherKeys docs.
|
||||
|
||||
// Xterm encodes modifier keys in `mod` – bit‑2 (value 4) indicates
|
||||
// that Ctrl was held. We avoid the `&` bitwise operator (disallowed
|
||||
// by our ESLint config) by using arithmetic instead.
|
||||
const hasCtrl = Math.floor(mod / 4) % 2 === 1;
|
||||
if (hasCtrl) {
|
||||
if (onSubmit) {
|
||||
onSubmit(buffer.current.getText());
|
||||
}
|
||||
} else {
|
||||
// Any variant without Ctrl just inserts newline (Shift, Alt, none)
|
||||
buffer.current.newline();
|
||||
}
|
||||
setVersion((v) => v + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 1b) CSI-~ / modifyOtherKeys *mode 1* – format: "[27;<mod>;<code>~".
|
||||
// Terminals such as iTerm2 (default), older xterm versions, or when
|
||||
// modifyOtherKeys=1 is configured, emit this legacy sequence. We
|
||||
// translate it to the same behaviour as the mode‑2 variant above so
|
||||
// that Shift+Enter (newline) / Ctrl+Enter (submit) work regardless
|
||||
// of the user’s terminal settings.
|
||||
if (input.startsWith("[27;") && input.endsWith("~")) {
|
||||
const m = input.match(/^\[27;([0-9]+);13~$/);
|
||||
if (m) {
|
||||
const mod = Number(m[1]);
|
||||
const hasCtrl = Math.floor(mod / 4) % 2 === 1;
|
||||
|
||||
if (hasCtrl) {
|
||||
if (onSubmit) {
|
||||
onSubmit(buffer.current.getText());
|
||||
}
|
||||
} else {
|
||||
buffer.current.newline();
|
||||
}
|
||||
setVersion((v) => v + 1);
|
||||
|
||||
@@ -15,11 +15,18 @@ const DEFAULT_DENY_MESSAGE =
|
||||
export function TerminalChatCommandReview({
|
||||
confirmationPrompt,
|
||||
onReviewCommand,
|
||||
// callback to switch approval mode overlay
|
||||
onSwitchApprovalMode,
|
||||
explanation: propExplanation,
|
||||
// whether this review Select is active (listening for keys)
|
||||
isActive = true,
|
||||
}: {
|
||||
confirmationPrompt: React.ReactNode;
|
||||
onReviewCommand: (decision: ReviewDecision, customMessage?: string) => void;
|
||||
onSwitchApprovalMode: () => void;
|
||||
explanation?: string;
|
||||
// when false, disable the underlying Select so it won't capture input
|
||||
isActive?: boolean;
|
||||
}): React.ReactElement {
|
||||
const [mode, setMode] = React.useState<"select" | "input" | "explanation">(
|
||||
"select",
|
||||
@@ -70,6 +77,7 @@ export function TerminalChatCommandReview({
|
||||
const opts: Array<
|
||||
| { label: string; value: ReviewDecision }
|
||||
| { label: string; value: "edit" }
|
||||
| { label: string; value: "switch" }
|
||||
> = [
|
||||
{
|
||||
label: "Yes (y)",
|
||||
@@ -93,6 +101,11 @@ export function TerminalChatCommandReview({
|
||||
label: "Edit or give feedback (e)",
|
||||
value: "edit",
|
||||
},
|
||||
// allow switching approval mode
|
||||
{
|
||||
label: "Switch approval mode (s)",
|
||||
value: "switch",
|
||||
},
|
||||
{
|
||||
label: "No, and keep going (n)",
|
||||
value: ReviewDecision.NO_CONTINUE,
|
||||
@@ -106,44 +119,50 @@ export function TerminalChatCommandReview({
|
||||
return opts;
|
||||
}, [showAlwaysApprove]);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (mode === "select") {
|
||||
if (input === "y") {
|
||||
onReviewCommand(ReviewDecision.YES);
|
||||
} else if (input === "x") {
|
||||
onReviewCommand(ReviewDecision.EXPLAIN);
|
||||
} else if (input === "e") {
|
||||
setMode("input");
|
||||
} else if (input === "n") {
|
||||
onReviewCommand(
|
||||
ReviewDecision.NO_CONTINUE,
|
||||
"Don't do that, keep going though",
|
||||
);
|
||||
} else if (input === "a" && showAlwaysApprove) {
|
||||
onReviewCommand(ReviewDecision.ALWAYS);
|
||||
} else if (key.escape) {
|
||||
onReviewCommand(ReviewDecision.NO_EXIT);
|
||||
useInput(
|
||||
(input, key) => {
|
||||
if (mode === "select") {
|
||||
if (input === "y") {
|
||||
onReviewCommand(ReviewDecision.YES);
|
||||
} else if (input === "x") {
|
||||
onReviewCommand(ReviewDecision.EXPLAIN);
|
||||
} else if (input === "e") {
|
||||
setMode("input");
|
||||
} else if (input === "n") {
|
||||
onReviewCommand(
|
||||
ReviewDecision.NO_CONTINUE,
|
||||
"Don't do that, keep going though",
|
||||
);
|
||||
} else if (input === "a" && showAlwaysApprove) {
|
||||
onReviewCommand(ReviewDecision.ALWAYS);
|
||||
} else if (input === "s") {
|
||||
// switch approval mode
|
||||
onSwitchApprovalMode();
|
||||
} else if (key.escape) {
|
||||
onReviewCommand(ReviewDecision.NO_EXIT);
|
||||
}
|
||||
} else if (mode === "explanation") {
|
||||
// When in explanation mode, any key returns to select mode
|
||||
if (key.return || key.escape || input === "x") {
|
||||
setMode("select");
|
||||
}
|
||||
} else {
|
||||
// text entry mode
|
||||
if (key.return) {
|
||||
// if user hit enter on empty msg, fall back to DEFAULT_DENY_MESSAGE
|
||||
const custom = msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg;
|
||||
onReviewCommand(ReviewDecision.NO_CONTINUE, custom);
|
||||
} else if (key.escape) {
|
||||
// treat escape as denial with default message as well
|
||||
onReviewCommand(
|
||||
ReviewDecision.NO_CONTINUE,
|
||||
msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (mode === "explanation") {
|
||||
// When in explanation mode, any key returns to select mode
|
||||
if (key.return || key.escape || input === "x") {
|
||||
setMode("select");
|
||||
}
|
||||
} else {
|
||||
// text entry mode
|
||||
if (key.return) {
|
||||
// if user hit enter on empty msg, fall back to DEFAULT_DENY_MESSAGE
|
||||
const custom = msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg;
|
||||
onReviewCommand(ReviewDecision.NO_CONTINUE, custom);
|
||||
} else if (key.escape) {
|
||||
// treat escape as denial with default message as well
|
||||
onReviewCommand(
|
||||
ReviewDecision.NO_CONTINUE,
|
||||
msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{ isActive },
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1} borderStyle="round" marginTop={1}>
|
||||
@@ -191,9 +210,13 @@ export function TerminalChatCommandReview({
|
||||
<Text>Allow command?</Text>
|
||||
<Box paddingX={2} flexDirection="column" gap={1}>
|
||||
<Select
|
||||
onChange={(value: ReviewDecision | "edit") => {
|
||||
isDisabled={!isActive}
|
||||
visibleOptionCount={approvalOptions.length}
|
||||
onChange={(value: ReviewDecision | "edit" | "switch") => {
|
||||
if (value === "edit") {
|
||||
setMode("input");
|
||||
} else if (value === "switch") {
|
||||
onSwitchApprovalMode();
|
||||
} else {
|
||||
onReviewCommand(value);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box gap={2}>
|
||||
<Spinner type="ball" />
|
||||
<Text>{frameWithSeconds}</Text>
|
||||
<Text>
|
||||
{thinkingText}
|
||||
Thinking
|
||||
{dots}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -11,18 +11,20 @@ import { log, isLoggingEnabled } from "../../utils/agent/log.js";
|
||||
import { loadConfig } from "../../utils/config.js";
|
||||
import { createInputItem } from "../../utils/input-utils.js";
|
||||
import { setSessionId } from "../../utils/session.js";
|
||||
import { SLASH_COMMANDS, type SlashCommand } from "../../utils/slash-commands";
|
||||
import {
|
||||
loadCommandHistory,
|
||||
addToHistory,
|
||||
} from "../../utils/storage/command-history.js";
|
||||
import { clearTerminal, onExit } from "../../utils/terminal.js";
|
||||
|
||||
// External UI components / Ink helpers
|
||||
import TextInput from "../vendor/ink-text-input.js";
|
||||
import { Box, Text, useApp, useInput, useStdin } from "ink";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import React, { useCallback, useState, Fragment, useEffect } from "react";
|
||||
import path from "node:path";
|
||||
import fs from "fs/promises";
|
||||
import { Box, Text, useApp, useInput, useStdin } from "ink";
|
||||
import Spinner from "../vendor/ink-spinner.js";
|
||||
import TextInput from "../vendor/ink-text-input.js";
|
||||
import { useInterval } from "use-interval";
|
||||
|
||||
// Internal imports
|
||||
@@ -49,9 +51,12 @@ export default function TerminalChatInput({
|
||||
openModelOverlay,
|
||||
openApprovalOverlay,
|
||||
openHelpOverlay,
|
||||
openDiffOverlay,
|
||||
onCompact,
|
||||
interruptAgent,
|
||||
active,
|
||||
thinkingSeconds,
|
||||
items = [],
|
||||
}: {
|
||||
isNew: boolean;
|
||||
loading: boolean;
|
||||
@@ -69,10 +74,17 @@ export default function TerminalChatInput({
|
||||
openModelOverlay: () => void;
|
||||
openApprovalOverlay: () => void;
|
||||
openHelpOverlay: () => void;
|
||||
openDiffOverlay: () => void;
|
||||
onCompact: () => void;
|
||||
interruptAgent: () => void;
|
||||
active: boolean;
|
||||
thinkingSeconds: number;
|
||||
// New: current conversation items so we can include them in bug reports
|
||||
items?: Array<ResponseItem>;
|
||||
}): React.ReactElement {
|
||||
// Slash command suggestion index
|
||||
const [selectedSlashSuggestion, setSelectedSlashSuggestion] =
|
||||
useState<number>(0);
|
||||
const app = useApp();
|
||||
//
|
||||
const [selectedSuggestion, setSelectedSuggestion] = useState<number>(0);
|
||||
@@ -100,6 +112,7 @@ export default function TerminalChatInput({
|
||||
const [history, setHistory] = useState<Array<HistoryEntry>>([]);
|
||||
const [historyIndex, setHistoryIndex] = useState<number | null>(null);
|
||||
const [draftInput, setDraftInput] = useState<string>("");
|
||||
const [skipNextSubmit, setSkipNextSubmit] = useState<boolean>(false);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Fallback raw‑data listener (test environment)
|
||||
@@ -161,17 +174,109 @@ export default function TerminalChatInput({
|
||||
|
||||
loadHistory();
|
||||
}, []);
|
||||
// Reset slash suggestion index when input prefix changes
|
||||
useEffect(() => {
|
||||
if (input.trim().startsWith("/")) {
|
||||
setSelectedSlashSuggestion(0);
|
||||
}
|
||||
}, [input]);
|
||||
|
||||
useInput(
|
||||
(_input, _key) => {
|
||||
// Debugging helper: log every key/input if DEBUG_TCI env flag is set.
|
||||
if (process.env["DEBUG_TCI"]) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("[TCI] useInput raw", JSON.stringify(_input), _key);
|
||||
}
|
||||
|
||||
// When image picker overlay is open delegate all keystrokes to it.
|
||||
// When the image picker overlay is open delegate all keystrokes to it so
|
||||
// users can navigate files without affecting the chat input.
|
||||
if (pickerCwd != null) {
|
||||
return; // ignore here; overlay has its own handlers
|
||||
return; // overlay has its own handlers
|
||||
}
|
||||
|
||||
// Slash command navigation: up/down to select, Tab to cycle, Enter to run.
|
||||
if (!confirmationPrompt && !loading && input.trim().startsWith("/")) {
|
||||
const prefix = input.trim();
|
||||
const matches = SLASH_COMMANDS.filter((cmd: SlashCommand) =>
|
||||
cmd.command.startsWith(prefix),
|
||||
);
|
||||
|
||||
if (matches.length > 0) {
|
||||
if (_key.tab) {
|
||||
// Cycle suggestions (shift+tab reverses the direction)
|
||||
const len = matches.length;
|
||||
const nextIdx = _key.shift
|
||||
? selectedSlashSuggestion <= 0
|
||||
? len - 1
|
||||
: selectedSlashSuggestion - 1
|
||||
: selectedSlashSuggestion >= len - 1
|
||||
? 0
|
||||
: selectedSlashSuggestion + 1;
|
||||
setSelectedSlashSuggestion(nextIdx);
|
||||
|
||||
const match = matches[nextIdx];
|
||||
if (match) {
|
||||
const cmd = match.command;
|
||||
setInput(cmd);
|
||||
setDraftInput(cmd);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_key.upArrow) {
|
||||
setSelectedSlashSuggestion((prev) =>
|
||||
prev <= 0 ? matches.length - 1 : prev - 1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_key.downArrow) {
|
||||
setSelectedSlashSuggestion((prev) =>
|
||||
prev < 0 || prev >= matches.length - 1 ? 0 : prev + 1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_key.return) {
|
||||
// Execute the currently selected slash command.
|
||||
const cmdObj = matches[selectedSlashSuggestion];
|
||||
if (cmdObj) {
|
||||
const cmd = cmdObj.command;
|
||||
// Clear current input and reset UI state.
|
||||
setInput("");
|
||||
setDraftInput("");
|
||||
setSelectedSlashSuggestion(0);
|
||||
|
||||
switch (cmd) {
|
||||
case "/history":
|
||||
openOverlay();
|
||||
break;
|
||||
case "/help":
|
||||
openHelpOverlay();
|
||||
break;
|
||||
case "/compact":
|
||||
onCompact();
|
||||
break;
|
||||
case "/model":
|
||||
openModelOverlay();
|
||||
break;
|
||||
case "/approval":
|
||||
openApprovalOverlay();
|
||||
break;
|
||||
case "/diff":
|
||||
openDiffOverlay();
|
||||
break;
|
||||
case "/bug":
|
||||
onSubmit(cmd);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!confirmationPrompt && !loading) {
|
||||
if (process.env["DEBUG_TCI"]) {
|
||||
@@ -271,6 +376,16 @@ export default function TerminalChatInput({
|
||||
const onSubmit = useCallback(
|
||||
async (value: string) => {
|
||||
const inputValue = value.trim();
|
||||
// If the user only entered a slash, do not send a chat message
|
||||
if (inputValue === "/") {
|
||||
setInput("");
|
||||
return;
|
||||
}
|
||||
// Skip this submit if we just autocompleted a slash command
|
||||
if (skipNextSubmit) {
|
||||
setSkipNextSubmit(false);
|
||||
return;
|
||||
}
|
||||
if (!inputValue) {
|
||||
return;
|
||||
}
|
||||
@@ -287,6 +402,12 @@ export default function TerminalChatInput({
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputValue === "/diff") {
|
||||
setInput("");
|
||||
openDiffOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputValue === "/compact") {
|
||||
setInput("");
|
||||
onCompact();
|
||||
@@ -358,6 +479,70 @@ export default function TerminalChatInput({
|
||||
},
|
||||
);
|
||||
|
||||
return;
|
||||
} else if (inputValue === "/bug") {
|
||||
// Generate a GitHub bug report URL pre‑filled with session details
|
||||
setInput("");
|
||||
|
||||
try {
|
||||
// Dynamically import dependencies to avoid unnecessary bundle size
|
||||
const [{ default: open }, os] = await Promise.all([
|
||||
import("open"),
|
||||
import("node:os"),
|
||||
]);
|
||||
|
||||
// Lazy import CLI_VERSION to avoid circular deps
|
||||
const { CLI_VERSION } = await import("../../utils/session.js");
|
||||
|
||||
const { buildBugReportUrl } = await import(
|
||||
"../../utils/bug-report.js"
|
||||
);
|
||||
|
||||
const url = buildBugReportUrl({
|
||||
items: items ?? [],
|
||||
cliVersion: CLI_VERSION,
|
||||
model: loadConfig().model ?? "unknown",
|
||||
platform: [os.platform(), os.arch(), os.release()]
|
||||
.map((s) => `\`${s}\``)
|
||||
.join(" | "),
|
||||
});
|
||||
|
||||
// Open the URL in the user's default browser
|
||||
await open(url, { wait: false });
|
||||
|
||||
// Inform the user in the chat history
|
||||
setItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `bugreport-${Date.now()}`,
|
||||
type: "message",
|
||||
role: "system",
|
||||
content: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: "📋 Opened browser to file a bug report. Please include any context that might help us fix the issue!",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
// If anything went wrong, notify the user
|
||||
setItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `bugreport-error-${Date.now()}`,
|
||||
type: "message",
|
||||
role: "system",
|
||||
content: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: `⚠️ Failed to create bug report URL: ${error}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return;
|
||||
} else if (inputValue.startsWith("/")) {
|
||||
// Handle invalid/unrecognized commands.
|
||||
@@ -497,9 +682,12 @@ export default function TerminalChatInput({
|
||||
openApprovalOverlay,
|
||||
openModelOverlay,
|
||||
openHelpOverlay,
|
||||
openDiffOverlay,
|
||||
attachedImages,
|
||||
history, // Add history to the dependency array
|
||||
history,
|
||||
onCompact,
|
||||
skipNextSubmit,
|
||||
items,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -508,7 +696,11 @@ export default function TerminalChatInput({
|
||||
<TerminalChatCommandReview
|
||||
confirmationPrompt={confirmationPrompt}
|
||||
onReviewCommand={submitConfirmation}
|
||||
// allow switching approval mode via 'v'
|
||||
onSwitchApprovalMode={openApprovalOverlay}
|
||||
explanation={explanation}
|
||||
// disable when input is inactive (e.g., overlay open)
|
||||
isActive={active}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -570,6 +762,7 @@ export default function TerminalChatInput({
|
||||
<TerminalChatInputThinking
|
||||
onInterrupt={interruptAgent}
|
||||
active={active}
|
||||
thinkingSeconds={thinkingSeconds}
|
||||
/>
|
||||
) : (
|
||||
<Box paddingX={1}>
|
||||
@@ -606,6 +799,25 @@ export default function TerminalChatInput({
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{/* Slash command autocomplete suggestions */}
|
||||
{input.trim().startsWith("/") && (
|
||||
<Box flexDirection="column" paddingX={2} marginBottom={1}>
|
||||
{SLASH_COMMANDS.filter((cmd: SlashCommand) =>
|
||||
cmd.command.startsWith(input.trim()),
|
||||
).map((cmd: SlashCommand, idx: number) => (
|
||||
<Box key={cmd.command}>
|
||||
<Text
|
||||
backgroundColor={
|
||||
idx === selectedSlashSuggestion ? "blackBright" : undefined
|
||||
}
|
||||
>
|
||||
<Text color="blueBright">{cmd.command}</Text>
|
||||
<Text> {cmd.description}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Box paddingX={2} marginBottom={1}>
|
||||
<Text dimColor>
|
||||
{isNew && !input ? (
|
||||
@@ -628,7 +840,15 @@ export default function TerminalChatInput({
|
||||
<>
|
||||
send q or ctrl+c to exit | send "/clear" to reset | send "/help"
|
||||
for commands | press enter to send
|
||||
{contextLeftPercent < 25 && (
|
||||
{contextLeftPercent > 25 && (
|
||||
<>
|
||||
{" — "}
|
||||
<Text color={contextLeftPercent > 40 ? "green" : "yellow"}>
|
||||
{Math.round(contextLeftPercent)}% context left
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{contextLeftPercent <= 25 && (
|
||||
<>
|
||||
{" — "}
|
||||
<Text color="red">
|
||||
@@ -648,12 +868,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
|
||||
@@ -701,10 +951,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
|
||||
@@ -735,8 +982,11 @@ function TerminalChatInputThinking({
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box gap={2}>
|
||||
<Spinner type="ball" />
|
||||
<Text>Thinking{dots}</Text>
|
||||
<Text>{frameWithSeconds}</Text>
|
||||
<Text>
|
||||
Thinking
|
||||
{dots}
|
||||
</Text>
|
||||
</Box>
|
||||
{awaitingConfirm && (
|
||||
<Text dimColor>
|
||||
|
||||
@@ -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,
|
||||
@@ -85,8 +52,10 @@ export default function TerminalChatInput({
|
||||
openModelOverlay,
|
||||
openApprovalOverlay,
|
||||
openHelpOverlay,
|
||||
openDiffOverlay,
|
||||
interruptAgent,
|
||||
active,
|
||||
thinkingSeconds,
|
||||
}: {
|
||||
isNew: boolean;
|
||||
loading: boolean;
|
||||
@@ -104,8 +73,10 @@ export default function TerminalChatInput({
|
||||
openModelOverlay: () => void;
|
||||
openApprovalOverlay: () => void;
|
||||
openHelpOverlay: () => void;
|
||||
openDiffOverlay: () => void;
|
||||
interruptAgent: () => void;
|
||||
active: boolean;
|
||||
thinkingSeconds: number;
|
||||
}): React.ReactElement {
|
||||
const app = useApp();
|
||||
const [selectedSuggestion, setSelectedSuggestion] = useState<number>(0);
|
||||
@@ -261,6 +232,12 @@ export default function TerminalChatInput({
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputValue === "/diff") {
|
||||
setInput("");
|
||||
openDiffOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputValue.startsWith("/model")) {
|
||||
setInput("");
|
||||
openModelOverlay();
|
||||
@@ -368,6 +345,7 @@ export default function TerminalChatInput({
|
||||
openApprovalOverlay,
|
||||
openModelOverlay,
|
||||
openHelpOverlay,
|
||||
openDiffOverlay,
|
||||
history, // Add history to the dependency array
|
||||
],
|
||||
);
|
||||
@@ -377,7 +355,11 @@ export default function TerminalChatInput({
|
||||
<TerminalChatCommandReview
|
||||
confirmationPrompt={confirmationPrompt}
|
||||
onReviewCommand={submitConfirmation}
|
||||
// allow switching approval mode via 'v'
|
||||
onSwitchApprovalMode={openApprovalOverlay}
|
||||
explanation={explanation}
|
||||
// disable when input is inactive (e.g., overlay open)
|
||||
isActive={active}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -389,6 +371,7 @@ export default function TerminalChatInput({
|
||||
<TerminalChatInputThinking
|
||||
onInterrupt={interruptAgent}
|
||||
active={active}
|
||||
thinkingSeconds={thinkingSeconds}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
@@ -454,15 +437,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 +522,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 +550,9 @@ function TerminalChatInputThinking({
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box gap={2}>
|
||||
<Spinner type="ball" />
|
||||
<Text>{frameWithSeconds}</Text>
|
||||
<Text>
|
||||
{thinkingText}
|
||||
Thinking
|
||||
{dots}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ColorName } from "chalk";
|
||||
import type { ResponseItem } from "openai/resources/responses/responses.mjs";
|
||||
|
||||
import TerminalChatInput from "./terminal-chat-input.js";
|
||||
import { TerminalChatToolCallCommand } from "./terminal-chat-tool-call-item.js";
|
||||
import { TerminalChatToolCallCommand } from "./terminal-chat-tool-call-command.js";
|
||||
import {
|
||||
calculateContextPercentRemaining,
|
||||
uniqueById,
|
||||
@@ -19,17 +19,20 @@ import { isLoggingEnabled, log } from "../../utils/agent/log.js";
|
||||
import { ReviewDecision } from "../../utils/agent/review.js";
|
||||
import { generateCompactSummary } from "../../utils/compact-summary.js";
|
||||
import { OPENAI_BASE_URL } from "../../utils/config.js";
|
||||
import { extractAppliedPatches as _extractAppliedPatches } from "../../utils/extract-applied-patches.js";
|
||||
import { getGitDiff } from "../../utils/get-diff.js";
|
||||
import { createInputItem } from "../../utils/input-utils.js";
|
||||
import { getAvailableModels } from "../../utils/model-utils.js";
|
||||
import { CLI_VERSION } from "../../utils/session.js";
|
||||
import { shortCwd } from "../../utils/short-path.js";
|
||||
import { saveRollout } from "../../utils/storage/save-rollout.js";
|
||||
import ApprovalModeOverlay from "../approval-mode-overlay.js";
|
||||
import DiffOverlay from "../diff-overlay.js";
|
||||
import HelpOverlay from "../help-overlay.js";
|
||||
import HistoryOverlay from "../history-overlay.js";
|
||||
import ModelOverlay from "../model-overlay.js";
|
||||
import { Box, Text } from "ink";
|
||||
import { exec } from "node:child_process";
|
||||
import { spawn } from "node:child_process";
|
||||
import OpenAI from "openai";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { inspect } from "util";
|
||||
@@ -59,6 +62,7 @@ const colorsByPolicy: Record<ApprovalPolicy, ColorName | undefined> = {
|
||||
async function generateCommandExplanation(
|
||||
command: Array<string>,
|
||||
model: string,
|
||||
flexMode: boolean,
|
||||
): Promise<string> {
|
||||
try {
|
||||
// Create a temporary OpenAI client
|
||||
@@ -73,6 +77,7 @@ async function generateCommandExplanation(
|
||||
// Create a prompt that asks for an explanation with a more detailed system prompt
|
||||
const response = await oai.chat.completions.create({
|
||||
model,
|
||||
...(flexMode ? { service_tier: "flex" } : {}),
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
@@ -142,7 +147,11 @@ export default function TerminalChat({
|
||||
const handleCompact = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const summary = await generateCompactSummary(items, model);
|
||||
const summary = await generateCompactSummary(
|
||||
items,
|
||||
model,
|
||||
Boolean(config.flexMode),
|
||||
);
|
||||
setItems([
|
||||
{
|
||||
id: `compact-${Date.now()}`,
|
||||
@@ -174,9 +183,16 @@ export default function TerminalChat({
|
||||
submitConfirmation,
|
||||
} = useConfirmation();
|
||||
const [overlayMode, setOverlayMode] = useState<
|
||||
"none" | "history" | "model" | "approval" | "help"
|
||||
"none" | "history" | "model" | "approval" | "help" | "diff"
|
||||
>("none");
|
||||
|
||||
// Store the diff text when opening the diff overlay so the view isn’t
|
||||
// recomputed on every re‑render while it is open.
|
||||
// diffText is passed down to the DiffOverlay component. The setter is
|
||||
// currently unused but retained for potential future updates. Prefix with
|
||||
// an underscore so eslint ignores the unused variable.
|
||||
const [diffText, _setDiffText] = useState<string>("");
|
||||
|
||||
const [initialPrompt, setInitialPrompt] = useState(_initialPrompt);
|
||||
const [initialImagePaths, setInitialImagePaths] =
|
||||
useState(_initialImagePaths);
|
||||
@@ -200,6 +216,13 @@ export default function TerminalChat({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Skip recreating the agent if awaiting a decision on a pending confirmation
|
||||
if (confirmationPrompt != null) {
|
||||
if (isLoggingEnabled()) {
|
||||
log("skip AgentLoop recreation due to pending confirmationPrompt");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isLoggingEnabled()) {
|
||||
log("creating NEW AgentLoop");
|
||||
log(
|
||||
@@ -245,7 +268,11 @@ export default function TerminalChat({
|
||||
log(`Generating explanation for command: ${commandForDisplay}`);
|
||||
|
||||
// Generate an explanation using the same model
|
||||
const explanation = await generateCommandExplanation(command, model);
|
||||
const explanation = await generateCommandExplanation(
|
||||
command,
|
||||
model,
|
||||
Boolean(config.flexMode),
|
||||
);
|
||||
log(`Generated explanation: ${explanation}`);
|
||||
|
||||
// Ask for confirmation again, but with the explanation
|
||||
@@ -283,13 +310,10 @@ export default function TerminalChat({
|
||||
agentRef.current = undefined;
|
||||
forceUpdate(); // re‑render after teardown too
|
||||
};
|
||||
}, [
|
||||
model,
|
||||
config,
|
||||
approvalPolicy,
|
||||
requestConfirmation,
|
||||
additionalWritableRoots,
|
||||
]);
|
||||
// We intentionally omit 'approvalPolicy' and 'confirmationPrompt' from the deps
|
||||
// so switching modes or showing confirmation dialogs doesn’t tear down the loop.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [model, config, requestConfirmation, additionalWritableRoots]);
|
||||
|
||||
// whenever loading starts/stops, reset or start a timer — but pause the
|
||||
// timer while a confirmation overlay is displayed so we don't trigger a
|
||||
@@ -350,9 +374,10 @@ export default function TerminalChat({
|
||||
const safePreview = preview.replace(/"/g, '\\"');
|
||||
const title = "Codex CLI";
|
||||
const cwd = PWD;
|
||||
exec(
|
||||
`osascript -e 'display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"'`,
|
||||
);
|
||||
spawn("osascript", [
|
||||
"-e",
|
||||
`display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -453,6 +478,7 @@ export default function TerminalChat({
|
||||
colorsByPolicy,
|
||||
agent,
|
||||
initialImagePaths,
|
||||
flexModeEnabled: Boolean(config.flexMode),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -482,6 +508,26 @@ export default function TerminalChat({
|
||||
openModelOverlay={() => setOverlayMode("model")}
|
||||
openApprovalOverlay={() => setOverlayMode("approval")}
|
||||
openHelpOverlay={() => setOverlayMode("help")}
|
||||
openDiffOverlay={() => {
|
||||
const { isGitRepo, diff } = getGitDiff();
|
||||
let text: string;
|
||||
if (isGitRepo) {
|
||||
text = diff;
|
||||
} else {
|
||||
text = "`/diff` — _not inside a git repository_";
|
||||
}
|
||||
setItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `diff-${Date.now()}`,
|
||||
type: "message",
|
||||
role: "system",
|
||||
content: [{ type: "input_text", text }],
|
||||
},
|
||||
]);
|
||||
// Ensure no overlay is shown.
|
||||
setOverlayMode("none");
|
||||
}}
|
||||
onCompact={handleCompact}
|
||||
active={overlayMode === "none"}
|
||||
interruptAgent={() => {
|
||||
@@ -516,6 +562,8 @@ export default function TerminalChat({
|
||||
agent.run(inputs, lastResponseId || "");
|
||||
return {};
|
||||
}}
|
||||
items={items}
|
||||
thinkingSeconds={thinkingSeconds}
|
||||
/>
|
||||
)}
|
||||
{overlayMode === "history" && (
|
||||
@@ -567,12 +615,20 @@ export default function TerminalChat({
|
||||
<ApprovalModeOverlay
|
||||
currentMode={approvalPolicy}
|
||||
onSelect={(newMode) => {
|
||||
agent?.cancel();
|
||||
setLoading(false);
|
||||
// update approval policy without cancelling an in-progress session
|
||||
if (newMode === approvalPolicy) {
|
||||
return;
|
||||
}
|
||||
// update state
|
||||
setApprovalPolicy(newMode as ApprovalPolicy);
|
||||
// update existing AgentLoop instance
|
||||
if (agentRef.current) {
|
||||
(
|
||||
agentRef.current as unknown as {
|
||||
approvalPolicy: ApprovalPolicy;
|
||||
}
|
||||
).approvalPolicy = newMode as ApprovalPolicy;
|
||||
}
|
||||
setItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
@@ -597,6 +653,13 @@ export default function TerminalChat({
|
||||
{overlayMode === "help" && (
|
||||
<HelpOverlay onExit={() => setOverlayMode("none")} />
|
||||
)}
|
||||
|
||||
{overlayMode === "diff" && (
|
||||
<DiffOverlay
|
||||
diffText={diffText}
|
||||
onExit={() => setOverlayMode("none")}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface TerminalHeaderProps {
|
||||
colorsByPolicy: Record<string, string | undefined>;
|
||||
agent?: AgentLoop;
|
||||
initialImagePaths?: Array<string>;
|
||||
flexModeEnabled?: boolean;
|
||||
}
|
||||
|
||||
const TerminalHeader: React.FC<TerminalHeaderProps> = ({
|
||||
@@ -24,6 +25,7 @@ const TerminalHeader: React.FC<TerminalHeaderProps> = ({
|
||||
colorsByPolicy,
|
||||
agent,
|
||||
initialImagePaths,
|
||||
flexModeEnabled = false,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
@@ -32,6 +34,7 @@ const TerminalHeader: React.FC<TerminalHeaderProps> = ({
|
||||
<Text>
|
||||
● Codex v{version} – {PWD} – {model} –{" "}
|
||||
<Text color={colorsByPolicy[approvalPolicy]}>{approvalPolicy}</Text>
|
||||
{flexModeEnabled ? " – flex-mode" : ""}
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
@@ -68,6 +71,12 @@ const TerminalHeader: React.FC<TerminalHeaderProps> = ({
|
||||
{approvalPolicy}
|
||||
</Text>
|
||||
</Text>
|
||||
{flexModeEnabled && (
|
||||
<Text dimColor>
|
||||
<Text color="blueBright">↳</Text> flex-mode:{" "}
|
||||
<Text bold>enabled</Text>
|
||||
</Text>
|
||||
)}
|
||||
{initialImagePaths?.map((img, idx) => (
|
||||
<Text key={img ?? idx} color="gray">
|
||||
<Text color="blueBright">↳</Text> image:{" "}
|
||||
|
||||
@@ -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<MessageHistoryProps> = ({
|
||||
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<MessageHistoryProps> = ({
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{loading && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="yellow">{`thinking for ${thinkingSeconds}s`}</Text>
|
||||
</Box>
|
||||
)}
|
||||
{/* The dedicated thinking indicator in the input area now displays the
|
||||
elapsed time, so we no longer render a separate counter here. */}
|
||||
<Static items={["header", ...messages]}>
|
||||
{(item, index) => {
|
||||
if (item === "header") {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import React, { useState } from "react";
|
||||
|
||||
/**
|
||||
* Simple scrollable view for displaying a diff.
|
||||
* The component is intentionally lightweight and mirrors the UX of
|
||||
* HistoryOverlay: Up/Down or j/k to scroll, PgUp/PgDn for paging and Esc to
|
||||
* close. The caller is responsible for computing the diff text.
|
||||
*/
|
||||
export default function DiffOverlay({
|
||||
diffText,
|
||||
onExit,
|
||||
}: {
|
||||
diffText: string;
|
||||
onExit: () => void;
|
||||
}): JSX.Element {
|
||||
const lines = diffText.length > 0 ? diffText.split("\n") : ["(no changes)"];
|
||||
|
||||
const [cursor, setCursor] = useState(0);
|
||||
|
||||
// Determine how many rows we can display – similar to HistoryOverlay.
|
||||
const rows = process.stdout.rows || 24;
|
||||
const headerRows = 2;
|
||||
const footerRows = 1;
|
||||
const maxVisible = Math.max(4, rows - headerRows - footerRows);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.escape || input === "q") {
|
||||
onExit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.downArrow || input === "j") {
|
||||
setCursor((c) => Math.min(lines.length - 1, c + 1));
|
||||
} else if (key.upArrow || input === "k") {
|
||||
setCursor((c) => Math.max(0, c - 1));
|
||||
} else if (key.pageDown) {
|
||||
setCursor((c) => Math.min(lines.length - 1, c + maxVisible));
|
||||
} else if (key.pageUp) {
|
||||
setCursor((c) => Math.max(0, c - maxVisible));
|
||||
} else if (input === "g") {
|
||||
setCursor(0);
|
||||
} else if (input === "G") {
|
||||
setCursor(lines.length - 1);
|
||||
}
|
||||
});
|
||||
|
||||
const firstVisible = Math.min(
|
||||
Math.max(0, cursor - Math.floor(maxVisible / 2)),
|
||||
Math.max(0, lines.length - maxVisible),
|
||||
);
|
||||
const visible = lines.slice(firstVisible, firstVisible + maxVisible);
|
||||
|
||||
// Very small helper to colorize diff lines in a basic way.
|
||||
function renderLine(line: string, idx: number): JSX.Element {
|
||||
let color: "green" | "red" | "cyan" | undefined = undefined;
|
||||
if (line.startsWith("+")) {
|
||||
color = "green";
|
||||
} else if (line.startsWith("-")) {
|
||||
color = "red";
|
||||
} else if (line.startsWith("@@") || line.startsWith("diff --git")) {
|
||||
color = "cyan";
|
||||
}
|
||||
return (
|
||||
<Text key={idx} color={color} wrap="truncate-end">
|
||||
{line === "" ? " " : line}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor="gray"
|
||||
width={Math.min(120, process.stdout.columns || 120)}
|
||||
>
|
||||
<Box paddingX={1}>
|
||||
<Text bold>Working tree diff ({lines.length} lines)</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" paddingX={1}>
|
||||
{visible.map((line, idx) => {
|
||||
return renderLine(line, firstVisible + idx);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
<Box paddingX={1}>
|
||||
<Text dimColor>esc Close ↑↓ Scroll PgUp/PgDn g/G First/Last</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -52,6 +52,12 @@ export default function HelpOverlay({
|
||||
<Text>
|
||||
<Text color="cyan">/clearhistory</Text> – clear command history
|
||||
</Text>
|
||||
<Text>
|
||||
<Text color="cyan">/bug</Text> – file a bug report with session log
|
||||
</Text>
|
||||
<Text>
|
||||
<Text color="cyan">/diff</Text> – view working tree git diff
|
||||
</Text>
|
||||
<Text>
|
||||
<Text color="cyan">/compact</Text> – condense context into a summary
|
||||
</Text>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Indicator, { type Props as IndicatorProps } from "./Indicator.js";
|
||||
import ItemComponent, { type Props as ItemProps } from "./Item.js";
|
||||
import Indicator, { type Props as IndicatorProps } from "./indicator.js";
|
||||
import ItemComponent, { type Props as ItemProps } from "./item.js";
|
||||
import isEqual from "fast-deep-equal";
|
||||
import { Box, useInput } from "ink";
|
||||
import React, {
|
||||
|
||||
@@ -400,6 +400,7 @@ export function SinglePassApp({
|
||||
});
|
||||
const chatResp = await openai.beta.chat.completions.parse({
|
||||
model: config.model,
|
||||
...(config.flexMode ? { service_tier: "flex" } : {}),
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
|
||||
@@ -153,6 +153,78 @@ function TextInput({
|
||||
|
||||
useInput(
|
||||
(input, key) => {
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Support Shift+Enter / Ctrl+Enter from terminals that have
|
||||
// modifyOtherKeys enabled. Such terminals encode the key‑combo in a
|
||||
// CSI sequence rather than sending a bare "\r"/"\n". Ink passes the
|
||||
// sequence through as raw text (without the initial ESC), so we need to
|
||||
// detect and translate it before the generic character handler below
|
||||
// treats it as literal input (e.g. "[27;2;13~"). We support both the
|
||||
// modern *mode 2* (CSI‑u, ending in "u") and the legacy *mode 1*
|
||||
// variant (ending in "~").
|
||||
//
|
||||
// - Shift+Enter → insert newline (same behaviour as Option+Enter)
|
||||
// - Ctrl+Enter → submit the input (same as plain Enter)
|
||||
//
|
||||
// References: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Modify-Other-Keys
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
function handleEncodedEnterSequence(raw: string): boolean {
|
||||
// CSI‑u (modifyOtherKeys=2) → "[13;<mod>u"
|
||||
let m = raw.match(/^\[([0-9]+);([0-9]+)u$/);
|
||||
if (m && m[1] === "13") {
|
||||
const mod = Number(m[2]);
|
||||
const hasCtrl = Math.floor(mod / 4) % 2 === 1;
|
||||
|
||||
if (hasCtrl) {
|
||||
if (onSubmit) {
|
||||
onSubmit(originalValue);
|
||||
}
|
||||
} else {
|
||||
const newValue =
|
||||
originalValue.slice(0, cursorOffset) +
|
||||
"\n" +
|
||||
originalValue.slice(cursorOffset);
|
||||
|
||||
setState({
|
||||
cursorOffset: cursorOffset + 1,
|
||||
cursorWidth: 0,
|
||||
});
|
||||
onChange(newValue);
|
||||
}
|
||||
return true; // handled
|
||||
}
|
||||
|
||||
// CSI‑~ (modifyOtherKeys=1) → "[27;<mod>;13~"
|
||||
m = raw.match(/^\[27;([0-9]+);13~$/);
|
||||
if (m) {
|
||||
const mod = Number(m[1]);
|
||||
const hasCtrl = Math.floor(mod / 4) % 2 === 1;
|
||||
|
||||
if (hasCtrl) {
|
||||
if (onSubmit) {
|
||||
onSubmit(originalValue);
|
||||
}
|
||||
} else {
|
||||
const newValue =
|
||||
originalValue.slice(0, cursorOffset) +
|
||||
"\n" +
|
||||
originalValue.slice(cursorOffset);
|
||||
|
||||
setState({
|
||||
cursorOffset: cursorOffset + 1,
|
||||
cursorWidth: 0,
|
||||
});
|
||||
onChange(newValue);
|
||||
}
|
||||
return true; // handled
|
||||
}
|
||||
return false; // not an encoded Enter sequence
|
||||
}
|
||||
|
||||
if (handleEncodedEnterSequence(input)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
key.upArrow ||
|
||||
key.downArrow ||
|
||||
|
||||
@@ -496,7 +496,6 @@ export class AgentLoop {
|
||||
if (this.model.startsWith("o")) {
|
||||
reasoning = { effort: "high" };
|
||||
if (this.model === "o3" || this.model === "o4-mini") {
|
||||
// @ts-expect-error waiting for API type update
|
||||
reasoning.summary = "auto";
|
||||
}
|
||||
}
|
||||
@@ -517,6 +516,7 @@ export class AgentLoop {
|
||||
stream: true,
|
||||
parallel_tool_calls: false,
|
||||
reasoning,
|
||||
...(this.config.flexMode ? { service_tier: "flex" } : {}),
|
||||
tools: [
|
||||
{
|
||||
type: "function",
|
||||
@@ -791,6 +791,41 @@ export class AgentLoop {
|
||||
this.onLoading(false);
|
||||
return;
|
||||
}
|
||||
// Suppress internal stack on JSON parse failures
|
||||
if (err instanceof SyntaxError) {
|
||||
this.onItem({
|
||||
id: `error-${Date.now()}`,
|
||||
type: "message",
|
||||
role: "system",
|
||||
content: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: "⚠️ Failed to parse streaming response (invalid JSON). Please `/clear` to reset.",
|
||||
},
|
||||
],
|
||||
});
|
||||
this.onLoading(false);
|
||||
return;
|
||||
}
|
||||
// Handle OpenAI API quota errors
|
||||
if (
|
||||
err instanceof Error &&
|
||||
(err as { code?: string }).code === "insufficient_quota"
|
||||
) {
|
||||
this.onItem({
|
||||
id: `error-${Date.now()}`,
|
||||
type: "message",
|
||||
role: "system",
|
||||
content: [
|
||||
{
|
||||
type: "input_text",
|
||||
text: "⚠️ Insufficient quota. Please check your billing details and retry.",
|
||||
},
|
||||
],
|
||||
});
|
||||
this.onLoading(false);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
this.currentStream = null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ExecInput, ExecResult } from "./sandbox/interface.js";
|
||||
import type { SpawnOptions } from "child_process";
|
||||
import type { ParseEntry } from "shell-quote";
|
||||
|
||||
import { process_patch } from "./apply-patch.js";
|
||||
import { SandboxType } from "./sandbox/interface.js";
|
||||
@@ -8,9 +9,17 @@ import { exec as rawExec } from "./sandbox/raw-exec.js";
|
||||
import { formatCommandForDisplay } from "../../format-command.js";
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import { parse } from "shell-quote";
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 10_000; // 10 seconds
|
||||
|
||||
function requiresShell(cmd: Array<string>): boolean {
|
||||
return cmd.some((arg) => {
|
||||
const tokens = parse(arg) as Array<ParseEntry>;
|
||||
return tokens.some((token) => typeof token === "object" && "op" in token);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should never return a rejected promise: errors should be
|
||||
* mapped to a non-zero exit code and the error message should be in stderr.
|
||||
@@ -33,6 +42,7 @@ export function exec(
|
||||
|
||||
const opts: SpawnOptions = {
|
||||
timeout: timeoutInMillis || DEFAULT_TIMEOUT_MS,
|
||||
...(requiresShell(cmd) ? { shell: true } : {}),
|
||||
...(workdir ? { cwd: workdir } : {}),
|
||||
};
|
||||
// Merge default writable roots with any user-specified ones.
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { SpawnOptions } from "child_process";
|
||||
|
||||
import { exec } from "./raw-exec.js";
|
||||
import { log } from "../log.js";
|
||||
import { realpathSync } from "fs";
|
||||
import { CONFIG_DIR } from "src/utils/config.js";
|
||||
|
||||
function getCommonRoots() {
|
||||
@@ -30,9 +29,7 @@ export function execWithSeatbelt(
|
||||
const { policies, params } = writableRoots
|
||||
.map((root, index) => ({
|
||||
policy: `(subpath (param "WRITABLE_ROOT_${index}"))`,
|
||||
// the kernel resolves symlinks before handing them to seatbelt for checking
|
||||
// so store the canonicalized form in the policy to be compared against
|
||||
param: `-DWRITABLE_ROOT_${index}=${realpathSync(root)}`,
|
||||
param: `-DWRITABLE_ROOT_${index}=${root}`,
|
||||
}))
|
||||
.reduce(
|
||||
(
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
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
|
||||
* template with whatever structured data we can infer from the current
|
||||
* session.
|
||||
*/
|
||||
export function buildBugReportUrl({
|
||||
items,
|
||||
cliVersion,
|
||||
model,
|
||||
platform,
|
||||
}: {
|
||||
/** Chat history so we can summarise user steps */
|
||||
items: Array<ResponseItem | ResponseOutputItem>;
|
||||
/** CLI revision string (e.g. output of `codex --revision`) */
|
||||
cliVersion: string;
|
||||
/** Active model name */
|
||||
model: string;
|
||||
/** Platform string – e.g. `darwin arm64 23.0.0` */
|
||||
platform: string;
|
||||
}): string {
|
||||
const params = new URLSearchParams({
|
||||
template: "2-bug-report.yml",
|
||||
labels: "bug",
|
||||
});
|
||||
|
||||
params.set("version", cliVersion);
|
||||
params.set("model", model);
|
||||
params.set("platform", platform);
|
||||
|
||||
const bullets: Array<string> = [];
|
||||
for (let i = 0; i < items.length; ) {
|
||||
const entry = items[i];
|
||||
if (entry?.type === "message" && entry.role === "user") {
|
||||
const contentArray = entry.content as
|
||||
| Array<{ text?: string }>
|
||||
| undefined;
|
||||
const messageText = contentArray
|
||||
?.map((c) => c.text ?? "")
|
||||
.join(" ")
|
||||
.trim();
|
||||
|
||||
let reasoning = 0;
|
||||
let toolCalls = 0;
|
||||
let j = i + 1;
|
||||
while (j < items.length) {
|
||||
const it = items[j];
|
||||
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;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
const codeBlock = `\`\`\`\n ${messageText}\n \`\`\``;
|
||||
|
||||
bullets.push(
|
||||
`- ${codeBlock}\n - \`${reasoning} reasoning\` | \`${toolCalls} tool\``,
|
||||
);
|
||||
|
||||
i = j;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (bullets.length) {
|
||||
params.set("steps", bullets.join("\n"));
|
||||
}
|
||||
|
||||
return `https://github.com/openai/codex/issues/new?${params.toString()}`;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { CONFIG_DIR } from "./config";
|
||||
import boxen from "boxen";
|
||||
import chalk from "chalk";
|
||||
import * as cp from "node:child_process";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import which from "which";
|
||||
|
||||
interface UpdateCheckState {
|
||||
lastUpdateCheck?: string;
|
||||
}
|
||||
|
||||
interface PackageInfo {
|
||||
current: string;
|
||||
wanted: string;
|
||||
latest: string;
|
||||
dependent: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
interface UpdateCheckInfo {
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
}
|
||||
|
||||
const UPDATE_CHECK_FREQUENCY = 1000 * 60 * 60 * 24; // 1 day
|
||||
|
||||
export async function getNPMCommandPath(): Promise<string | undefined> {
|
||||
try {
|
||||
return await which(process.platform === "win32" ? "npm.cmd" : "npm");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkOutdated(
|
||||
npmCommandPath: string,
|
||||
): Promise<UpdateCheckInfo | undefined> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
// TODO: support local installation
|
||||
// Right now we're using "--global", which only checks global packages.
|
||||
// But codex might be installed locally — we should check the local version first,
|
||||
// and only fall back to the global one if needed.
|
||||
const args = ["outdated", "--global", "--json", "--", "@openai/codex"];
|
||||
// corepack npm wrapper would automatically update package.json. disable that behavior.
|
||||
// COREPACK_ENABLE_AUTO_PIN disables the package.json overwrite, and
|
||||
// COREPACK_ENABLE_PROJECT_SPEC makes the npm view command succeed
|
||||
// even if packageManager specified a package manager other than npm.
|
||||
const env = {
|
||||
...process.env,
|
||||
COREPACK_ENABLE_AUTO_PIN: "0",
|
||||
COREPACK_ENABLE_PROJECT_SPEC: "0",
|
||||
};
|
||||
let options: cp.ExecFileOptions = { env };
|
||||
let commandPath = npmCommandPath;
|
||||
if (process.platform === "win32") {
|
||||
options = { ...options, shell: true };
|
||||
commandPath = `"${npmCommandPath}"`;
|
||||
}
|
||||
cp.execFile(commandPath, args, options, async (_error, stdout) => {
|
||||
try {
|
||||
const { name: packageName } = await import("../../package.json");
|
||||
const content: Record<string, PackageInfo> = JSON.parse(stdout);
|
||||
if (!content[packageName]) {
|
||||
// package not installed or not outdated
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentVersion = content[packageName].current;
|
||||
const latestVersion = content[packageName].latest;
|
||||
|
||||
resolve({ currentVersion, latestVersion });
|
||||
return;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function checkForUpdates(): Promise<void> {
|
||||
const stateFile = join(CONFIG_DIR, "update-check.json");
|
||||
let state: UpdateCheckState | undefined;
|
||||
try {
|
||||
state = JSON.parse(await readFile(stateFile, "utf8"));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (
|
||||
state?.lastUpdateCheck &&
|
||||
Date.now() - new Date(state.lastUpdateCheck).valueOf() <
|
||||
UPDATE_CHECK_FREQUENCY
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const npmCommandPath = await getNPMCommandPath();
|
||||
if (!npmCommandPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const packageInfo = await checkOutdated(npmCommandPath);
|
||||
|
||||
await writeState(stateFile, {
|
||||
...state,
|
||||
lastUpdateCheck: new Date().toUTCString(),
|
||||
});
|
||||
|
||||
if (!packageInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateMessage = `To update, run: ${chalk.cyan(
|
||||
"npm install -g @openai/codex",
|
||||
)} to update.`;
|
||||
|
||||
const box = boxen(
|
||||
`\
|
||||
Update available! ${chalk.red(packageInfo.currentVersion)} → ${chalk.green(
|
||||
packageInfo.latestVersion,
|
||||
)}.
|
||||
${updateMessage}`,
|
||||
{
|
||||
padding: 1,
|
||||
margin: 1,
|
||||
align: "center",
|
||||
borderColor: "yellow",
|
||||
borderStyle: "round",
|
||||
},
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(box);
|
||||
}
|
||||
|
||||
async function writeState(stateFilePath: string, state: UpdateCheckState) {
|
||||
await writeFile(stateFilePath, JSON.stringify(state, null, 2), {
|
||||
encoding: "utf8",
|
||||
});
|
||||
}
|
||||
@@ -9,9 +9,17 @@ import OpenAI from "openai";
|
||||
* @param model The model to use for generating the summary
|
||||
* @returns A concise structured summary string
|
||||
*/
|
||||
/**
|
||||
* Generate a condensed summary of the conversation items.
|
||||
* @param items The list of conversation items to summarize
|
||||
* @param model The model to use for generating the summary
|
||||
* @param flexMode Whether to use the flex-mode service tier
|
||||
* @returns A concise structured summary string
|
||||
*/
|
||||
export async function generateCompactSummary(
|
||||
items: Array<ResponseItem>,
|
||||
model: string,
|
||||
flexMode = false,
|
||||
): Promise<string> {
|
||||
const oai = new OpenAI({
|
||||
apiKey: process.env["OPENAI_API_KEY"],
|
||||
@@ -44,6 +52,7 @@ export async function generateCompactSummary(
|
||||
|
||||
const response = await oai.chat.completions.create({
|
||||
model,
|
||||
...(flexMode ? { service_tier: "flex" } : {}),
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
|
||||
@@ -56,6 +56,8 @@ export type StoredConfig = {
|
||||
saveHistory?: boolean;
|
||||
sensitivePatterns?: Array<string>;
|
||||
};
|
||||
/** User-defined safe commands */
|
||||
safeCommands?: Array<string>;
|
||||
};
|
||||
|
||||
// Minimal config written on first run. An *empty* model string ensures that
|
||||
@@ -75,15 +77,21 @@ export type AppConfig = {
|
||||
apiKey?: string;
|
||||
model: string;
|
||||
instructions: string;
|
||||
approvalMode?: AutoApprovalMode;
|
||||
fullAutoErrorMode?: FullAutoErrorMode;
|
||||
memory?: MemoryConfig;
|
||||
/** Whether to enable desktop notifications for responses */
|
||||
notify: boolean;
|
||||
|
||||
/** Enable the "flex-mode" processing mode for supported models (o3, o4-mini) */
|
||||
flexMode?: boolean;
|
||||
history?: {
|
||||
maxSize: number;
|
||||
saveHistory: boolean;
|
||||
sensitivePatterns: Array<string>;
|
||||
};
|
||||
/** User-defined safe commands */
|
||||
safeCommands?: Array<string>;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -268,6 +276,8 @@ export const loadConfig = (
|
||||
: DEFAULT_AGENTIC_MODEL),
|
||||
instructions: combinedInstructions,
|
||||
notify: storedConfig.notify === true,
|
||||
approvalMode: storedConfig.approvalMode,
|
||||
safeCommands: storedConfig.safeCommands ?? [],
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -345,6 +355,13 @@ export const loadConfig = (
|
||||
};
|
||||
}
|
||||
|
||||
// Load user-defined safe commands
|
||||
if (Array.isArray(storedConfig.safeCommands)) {
|
||||
config.safeCommands = storedConfig.safeCommands.map(String);
|
||||
} else {
|
||||
config.safeCommands = [];
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
@@ -376,6 +393,7 @@ export const saveConfig = (
|
||||
// Create the config object to save
|
||||
const configToSave: StoredConfig = {
|
||||
model: config.model,
|
||||
approvalMode: config.approvalMode,
|
||||
};
|
||||
|
||||
// Add history settings if they exist
|
||||
@@ -386,6 +404,10 @@ export const saveConfig = (
|
||||
sensitivePatterns: config.history.sensitivePatterns,
|
||||
};
|
||||
}
|
||||
// Save: User-defined safe commands
|
||||
if (config.safeCommands && config.safeCommands.length > 0) {
|
||||
configToSave.safeCommands = config.safeCommands;
|
||||
}
|
||||
|
||||
if (ext === ".yaml" || ext === ".yml") {
|
||||
writeFileSync(targetPath, dumpYaml(configToSave), "utf-8");
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ResponseItem } from "openai/resources/responses/responses.mjs";
|
||||
|
||||
/**
|
||||
* Extracts the patch texts of all `apply_patch` tool calls from the given
|
||||
* message history. Returns an empty string when none are found.
|
||||
*/
|
||||
export function extractAppliedPatches(items: Array<ResponseItem>): string {
|
||||
const patches: Array<string> = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (item.type !== "function_call") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { name: toolName, arguments: argsString } = item as unknown as {
|
||||
name: unknown;
|
||||
arguments: unknown;
|
||||
};
|
||||
|
||||
if (toolName !== "apply_patch" || typeof argsString !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const args = JSON.parse(argsString) as { patch?: string };
|
||||
if (typeof args.patch === "string" && args.patch.length > 0) {
|
||||
patches.push(args.patch.trim());
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed JSON – we never want to crash the overlay.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return patches.join("\n\n");
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
/**
|
||||
* Returns the current Git diff for the working directory. If the current
|
||||
* working directory is not inside a Git repository, `isGitRepo` will be
|
||||
* false and `diff` will be an empty string.
|
||||
*/
|
||||
export function getGitDiff(): {
|
||||
isGitRepo: boolean;
|
||||
diff: string;
|
||||
} {
|
||||
try {
|
||||
// First check whether we are inside a git repository. `rev‑parse` exits
|
||||
// with a non‑zero status code if not.
|
||||
execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
|
||||
|
||||
// If the above call didn’t throw, we are inside a git repo. Retrieve the
|
||||
// diff including color codes so that the overlay can render them.
|
||||
const output = execSync("git diff --color", {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 10 * 1024 * 1024, // 10 MB ought to be enough for now
|
||||
});
|
||||
|
||||
return { isGitRepo: true, diff: output };
|
||||
} catch {
|
||||
// Either git is not installed or we’re not inside a repository.
|
||||
return { isGitRepo: false, diff: "" };
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import { fileTypeFromBuffer } from "file-type";
|
||||
import fs from "fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
// Map data‑urls → original filenames so TUI can render friendly labels.
|
||||
// Populated during createInputItem.
|
||||
// Map data‑urls → original filenames so the TUI can render friendly labels.
|
||||
// This map is populated during `createInputItem` execution.
|
||||
export const imageFilenameByDataUrl = new Map<string, string>();
|
||||
|
||||
export async function createInputItem(
|
||||
@@ -20,30 +20,41 @@ export async function createInputItem(
|
||||
|
||||
for (const filePath of images) {
|
||||
/* eslint-disable no-await-in-loop */
|
||||
let binary: Buffer;
|
||||
let binary: Buffer | undefined;
|
||||
try {
|
||||
binary = await fs.readFile(filePath);
|
||||
} catch (err: unknown) {
|
||||
// Gracefully skip files that no longer exist on disk. This can happen
|
||||
// when an image was attached earlier but has since been moved or
|
||||
// deleted before the user submitted the prompt. For any other error
|
||||
// codes re‑throw so callers are still notified of unexpected issues
|
||||
// (e.g. permission errors).
|
||||
// Gracefully handle files that no longer exist on disk. This can happen
|
||||
// when an image was attached earlier but has since been moved or deleted
|
||||
// before the user submitted the prompt.
|
||||
const e = err as NodeJS.ErrnoException;
|
||||
if (e?.code === "ENOENT") {
|
||||
// Skip silently – user will simply not include the missing image.
|
||||
continue;
|
||||
// Insert a placeholder message so the user is aware a file was missing.
|
||||
inputItem.content.push({
|
||||
type: "input_text",
|
||||
text: `[missing image: ${path.basename(filePath)}]`,
|
||||
});
|
||||
continue; // skip to next image
|
||||
}
|
||||
|
||||
// For any other error (e.g. permission issues) bubble up so callers can
|
||||
// react accordingly.
|
||||
throw err as Error;
|
||||
}
|
||||
|
||||
if (!binary) {
|
||||
// Should not happen, but satisfies TypeScript.
|
||||
continue;
|
||||
}
|
||||
|
||||
const kind = await fileTypeFromBuffer(binary);
|
||||
/* eslint-enable no-await-in-loop */
|
||||
const encoded = binary.toString("base64");
|
||||
const mime = kind?.mime ?? "application/octet-stream";
|
||||
const dataUrl = `data:${mime};base64,${encoded}`;
|
||||
|
||||
// Store pretty label (relative path when possible)
|
||||
// Store a pretty label (make path relative when possible) so the TUI can
|
||||
// display something friendlier than a long data‑url.
|
||||
const label = path.isAbsolute(filePath)
|
||||
? path.relative(process.cwd(), filePath)
|
||||
: filePath;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const CLI_VERSION = "0.1.2504172351"; // Must be in sync with package.json.
|
||||
export const CLI_VERSION = "0.1.2504181820"; // Must be in sync with package.json.
|
||||
export const ORIGIN = "codex_cli_ts";
|
||||
|
||||
export type TerminalChatSession = {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Defines the available slash commands and their descriptions.
|
||||
// Used for autocompletion in the chat input.
|
||||
export interface SlashCommand {
|
||||
command: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const SLASH_COMMANDS: Array<SlashCommand> = [
|
||||
{
|
||||
command: "/clear",
|
||||
description: "Clear conversation history and free up context",
|
||||
},
|
||||
{
|
||||
command: "/clearhistory",
|
||||
description: "Clear command history",
|
||||
},
|
||||
{
|
||||
command: "/compact",
|
||||
description:
|
||||
"Clear conversation history but keep a summary in context. Optional: /compact [instructions for summarization]",
|
||||
},
|
||||
{ command: "/history", description: "Open command history" },
|
||||
{ command: "/help", description: "Show list of commands" },
|
||||
{ command: "/model", description: "Open model selection panel" },
|
||||
{ command: "/approval", description: "Open approval mode selection panel" },
|
||||
{ command: "/bug", description: "Generate a prefilled GitHub bug report" },
|
||||
{
|
||||
command: "/diff",
|
||||
description:
|
||||
"Show git diff of the working directory (or applied patches if not in git)",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Check for updates > should outputs the update message when package is outdated 1`] = `
|
||||
"
|
||||
╭─────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Update available! 1.0.0 → 2.0.0. │
|
||||
│ To update, run: npm install -g @openai/codex to update. │
|
||||
│ │
|
||||
╰─────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
@@ -1,7 +1,13 @@
|
||||
import type { SafetyAssessment } from "../src/approvals";
|
||||
|
||||
import { canAutoApprove } from "../src/approvals";
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { describe, test, expect, vi } from "vitest";
|
||||
|
||||
vi.mock("../src/utils/config", () => ({
|
||||
loadConfig: () => ({
|
||||
safeCommands: ["npm test", "sl"],
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("canAutoApprove()", () => {
|
||||
const env = {
|
||||
@@ -89,4 +95,27 @@ describe("canAutoApprove()", () => {
|
||||
|
||||
expect(check(["cargo", "build"])).toEqual({ type: "ask-user" });
|
||||
});
|
||||
|
||||
test("commands in safeCommands config should be safe", async () => {
|
||||
expect(check(["npm", "test"])).toEqual({
|
||||
type: "auto-approve",
|
||||
reason: "User-defined safe command",
|
||||
group: "User config",
|
||||
runInSandbox: false,
|
||||
});
|
||||
|
||||
expect(check(["sl"])).toEqual({
|
||||
type: "auto-approve",
|
||||
reason: "User-defined safe command",
|
||||
group: "User config",
|
||||
runInSandbox: false,
|
||||
});
|
||||
|
||||
expect(check(["npm", "test", "--watch"])).toEqual({
|
||||
type: "auto-approve",
|
||||
reason: "User-defined safe command",
|
||||
group: "User config",
|
||||
runInSandbox: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,8 @@ vi.mock("../src/utils/input-utils.js", () => ({
|
||||
}));
|
||||
|
||||
// mock external deps used inside chat input
|
||||
vi.mock("../../approvals.js", () => ({ isSafeCommand: () => null }));
|
||||
// Mock approval helper used by TerminalChatInput
|
||||
vi.mock("../src/approvals.js", () => ({ isSafeCommand: () => null }));
|
||||
vi.mock("../src/format-command.js", () => ({
|
||||
// Accept an array of command tokens and join them with spaces for display.
|
||||
formatCommandForDisplay: (c: Array<string>): string => c.join(" "),
|
||||
|
||||
@@ -11,7 +11,7 @@ vi.mock("../src/utils/input-utils.js", () => ({
|
||||
createInputItem: vi.fn(async () => ({})),
|
||||
imageFilenameByDataUrl: new Map(),
|
||||
}));
|
||||
vi.mock("../../approvals.js", () => ({ isSafeCommand: () => null }));
|
||||
vi.mock("../src/approvals.js", () => ({ isSafeCommand: () => null }));
|
||||
vi.mock("../src/format-command.js", () => ({
|
||||
formatCommandForDisplay: (c: Array<string>): string => c.join(" "),
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import {
|
||||
checkForUpdates,
|
||||
checkOutdated,
|
||||
getNPMCommandPath,
|
||||
} from "../src/utils/check-updates.js";
|
||||
import { execFile } from "node:child_process";
|
||||
import { join } from "node:path";
|
||||
import { CONFIG_DIR } from "src/utils/config.js";
|
||||
import { beforeEach } from "node:test";
|
||||
|
||||
vi.mock("which", () => ({
|
||||
default: vi.fn(() => "/usr/local/bin/npm"),
|
||||
}));
|
||||
|
||||
vi.mock("child_process", () => ({
|
||||
execFile: vi.fn((_cmd, _args, _opts, callback) => {
|
||||
const stdout = JSON.stringify({
|
||||
"@openai/codex": {
|
||||
current: "1.0.0",
|
||||
latest: "2.0.0",
|
||||
},
|
||||
});
|
||||
callback?.(null, stdout, "");
|
||||
return {} as any;
|
||||
}),
|
||||
}));
|
||||
|
||||
let memfs: Record<string, string> = {};
|
||||
|
||||
vi.mock("node:fs/promises", async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
readFile: async (path: string) => {
|
||||
if (memfs[path] === undefined) {
|
||||
throw new Error("ENOENT");
|
||||
}
|
||||
return memfs[path];
|
||||
},
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
memfs = {}; // reset in‑memory store
|
||||
});
|
||||
|
||||
describe("Check for updates", () => {
|
||||
it("should return the path to npm", async () => {
|
||||
const npmPath = await getNPMCommandPath();
|
||||
expect(npmPath).toBeDefined();
|
||||
});
|
||||
|
||||
it("should return undefined if npm is not found", async () => {
|
||||
vi.mocked(await import("which")).default.mockImplementationOnce(() => {
|
||||
throw new Error("not found");
|
||||
});
|
||||
|
||||
const npmPath = await getNPMCommandPath();
|
||||
expect(npmPath).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return the return value when package is outdated", async () => {
|
||||
const npmPath = await getNPMCommandPath();
|
||||
|
||||
const info = await checkOutdated(npmPath!);
|
||||
expect(info).toStrictEqual({
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return undefined when package is not outdated", async () => {
|
||||
const npmPath = await getNPMCommandPath();
|
||||
vi.mocked(execFile).mockImplementationOnce(
|
||||
(_cmd, _args, _opts, callback) => {
|
||||
// Simulate the case where the package is not outdated, returning an empty object
|
||||
const stdout = JSON.stringify({});
|
||||
callback?.(null, stdout, "");
|
||||
return {} as any;
|
||||
},
|
||||
);
|
||||
|
||||
const info = await checkOutdated(npmPath!);
|
||||
expect(info).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should outputs the update message when package is outdated", async () => {
|
||||
const codexStatePath = join(CONFIG_DIR, "update-check.json");
|
||||
// Use a fixed early date far in the past to ensure it's always at least 1 day before now
|
||||
memfs[codexStatePath] = JSON.stringify({
|
||||
lastUpdateCheck: new Date("2000-01-01T00:00:00Z").toUTCString(),
|
||||
});
|
||||
await checkForUpdates();
|
||||
// Spy on console.log to capture output
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await checkForUpdates();
|
||||
expect(logSpy).toHaveBeenCalled();
|
||||
// The last call should be the boxen message
|
||||
const lastCallArg = logSpy.mock.calls.at(-1)?.[0] ?? "";
|
||||
// Strip ANSI colors to make snapshot stable across environments
|
||||
const stripAnsi = (await import("strip-ansi")).default as (
|
||||
input: string,
|
||||
) => string;
|
||||
expect(stripAnsi(lastCallArg)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should not output the update message when package is not outdated", async () => {
|
||||
const codexStatePath = join(CONFIG_DIR, "update-check.json");
|
||||
memfs[codexStatePath] = JSON.stringify({
|
||||
lastUpdateCheck: new Date().toUTCString(),
|
||||
});
|
||||
await checkForUpdates();
|
||||
// Spy on console.log to capture output
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
await checkForUpdates();
|
||||
expect(logSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import type * as fsType from "fs";
|
||||
|
||||
import { loadConfig, saveConfig } from "../src/utils/config.js"; // parent import first
|
||||
import { AutoApprovalMode } from "../src/utils/auto-approval-mode.js";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { test, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
@@ -107,3 +108,43 @@ test("loads user instructions + project doc when codex.md is present", () => {
|
||||
userInstr + "\n\n--- project-doc ---\n\n" + projectDoc,
|
||||
);
|
||||
});
|
||||
|
||||
test("loads and saves approvalMode correctly", () => {
|
||||
// Setup config with approvalMode
|
||||
memfs[testConfigPath] = JSON.stringify(
|
||||
{
|
||||
model: "mymodel",
|
||||
approvalMode: AutoApprovalMode.AUTO_EDIT,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
memfs[testInstructionsPath] = "test instructions";
|
||||
|
||||
// Load config and verify approvalMode
|
||||
const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
|
||||
disableProjectDoc: true,
|
||||
});
|
||||
|
||||
// Check approvalMode was loaded correctly
|
||||
expect(loadedConfig.approvalMode).toBe(AutoApprovalMode.AUTO_EDIT);
|
||||
|
||||
// Modify approvalMode and save
|
||||
const updatedConfig = {
|
||||
...loadedConfig,
|
||||
approvalMode: AutoApprovalMode.FULL_AUTO,
|
||||
};
|
||||
|
||||
saveConfig(updatedConfig, testConfigPath, testInstructionsPath);
|
||||
|
||||
// Verify saved config contains updated approvalMode
|
||||
expect(memfs[testConfigPath]).toContain(
|
||||
`"approvalMode": "${AutoApprovalMode.FULL_AUTO}"`,
|
||||
);
|
||||
|
||||
// Load again and verify updated value
|
||||
const reloadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
|
||||
disableProjectDoc: true,
|
||||
});
|
||||
expect(reloadedConfig.approvalMode).toBe(AutoApprovalMode.FULL_AUTO);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import fs from "fs/promises";
|
||||
import { createInputItem } from "../src/utils/input-utils.js";
|
||||
|
||||
describe("createInputItem", () => {
|
||||
it("returns only text when no images provided", async () => {
|
||||
const result = await createInputItem("hello", []);
|
||||
expect(result).toEqual({
|
||||
role: "user",
|
||||
type: "message",
|
||||
content: [{ type: "input_text", text: "hello" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("includes image content for existing file", async () => {
|
||||
const fakeBuffer = Buffer.from("fake image content");
|
||||
const readSpy = vi
|
||||
.spyOn(fs, "readFile")
|
||||
.mockResolvedValue(fakeBuffer as any);
|
||||
const result = await createInputItem("hello", ["dummy-path"]);
|
||||
const expectedUrl = `data:application/octet-stream;base64,${fakeBuffer.toString(
|
||||
"base64",
|
||||
)}`;
|
||||
expect(result.role).toBe("user");
|
||||
expect(result.type).toBe("message");
|
||||
expect(result.content.length).toBe(2);
|
||||
const [textItem, imageItem] = result.content;
|
||||
expect(textItem).toEqual({ type: "input_text", text: "hello" });
|
||||
expect(imageItem).toEqual({
|
||||
type: "input_image",
|
||||
detail: "auto",
|
||||
image_url: expectedUrl,
|
||||
});
|
||||
readSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("falls back to missing image text for non-existent file", async () => {
|
||||
const filePath = "tests/__fixtures__/does-not-exist.png";
|
||||
const result = await createInputItem("hello", [filePath]);
|
||||
expect(result.content.length).toBe(2);
|
||||
const fallbackItem = result.content[1];
|
||||
expect(fallbackItem).toEqual({
|
||||
type: "input_text",
|
||||
text: "[missing image: does-not-exist.png]",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
// Regression test: Terminals with modifyOtherKeys=1 emit CSI~ sequence for
|
||||
// Shift+Enter: ESC [ 27 ; mod ; 13 ~. The editor must treat Shift+Enter as
|
||||
// newline (without submitting) and Ctrl+Enter as submit.
|
||||
|
||||
import { renderTui } from "./ui-test-helpers.js";
|
||||
import MultilineTextEditor from "../src/components/chat/multiline-editor.js";
|
||||
import * as React from "react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
async function type(
|
||||
stdin: NodeJS.WritableStream,
|
||||
text: string,
|
||||
flush: () => Promise<void>,
|
||||
) {
|
||||
stdin.write(text);
|
||||
await flush();
|
||||
}
|
||||
|
||||
describe("MultilineTextEditor – Shift+Enter with modifyOtherKeys=1", () => {
|
||||
it("inserts newline, does NOT submit", async () => {
|
||||
const onSubmit = vi.fn();
|
||||
|
||||
const { stdin, lastFrameStripped, flush, cleanup } = renderTui(
|
||||
React.createElement(MultilineTextEditor, {
|
||||
height: 5,
|
||||
width: 20,
|
||||
initialText: "",
|
||||
onSubmit,
|
||||
}),
|
||||
);
|
||||
|
||||
await flush();
|
||||
|
||||
await type(stdin, "abc", flush);
|
||||
// Shift+Enter => ESC [27;2;13~
|
||||
await type(stdin, "\u001B[27;2;13~", flush);
|
||||
await type(stdin, "def", flush);
|
||||
|
||||
const frame = lastFrameStripped();
|
||||
expect(frame).toMatch(/abc/);
|
||||
expect(frame).toMatch(/def/);
|
||||
// newline inserted -> at least 2 lines
|
||||
expect(frame.split("\n").length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { test, expect } from "vitest";
|
||||
import { SLASH_COMMANDS, type SlashCommand } from "../src/utils/slash-commands";
|
||||
|
||||
test("SLASH_COMMANDS includes expected commands", () => {
|
||||
const commands = SLASH_COMMANDS.map((c: SlashCommand) => c.command);
|
||||
expect(commands).toContain("/clear");
|
||||
expect(commands).toContain("/compact");
|
||||
expect(commands).toContain("/history");
|
||||
expect(commands).toContain("/help");
|
||||
expect(commands).toContain("/model");
|
||||
expect(commands).toContain("/approval");
|
||||
expect(commands).toContain("/clearhistory");
|
||||
expect(commands).toContain("/diff");
|
||||
});
|
||||
|
||||
test("filters slash commands by prefix", () => {
|
||||
const prefix = "/c";
|
||||
const filtered = SLASH_COMMANDS.filter((c: SlashCommand) =>
|
||||
c.command.startsWith(prefix),
|
||||
);
|
||||
const names = filtered.map((c: SlashCommand) => c.command);
|
||||
expect(names).toEqual(
|
||||
expect.arrayContaining(["/clear", "/clearhistory", "/compact"]),
|
||||
);
|
||||
expect(names).not.toEqual(
|
||||
expect.arrayContaining(["/history", "/help", "/model", "/approval"]),
|
||||
);
|
||||
|
||||
const emptyPrefixFiltered = SLASH_COMMANDS.filter((c: SlashCommand) =>
|
||||
c.command.startsWith(""),
|
||||
);
|
||||
const emptyPrefixNames = emptyPrefixFiltered.map(
|
||||
(c: SlashCommand) => c.command,
|
||||
);
|
||||
expect(emptyPrefixNames).toEqual(
|
||||
expect.arrayContaining(SLASH_COMMANDS.map((c: SlashCommand) => c.command)),
|
||||
);
|
||||
expect(emptyPrefixNames).toHaveLength(SLASH_COMMANDS.length);
|
||||
});
|
||||
@@ -17,12 +17,14 @@ describe("TerminalChatInput compact command", () => {
|
||||
setItems: () => {},
|
||||
contextLeftPercent: 10,
|
||||
openOverlay: () => {},
|
||||
openDiffOverlay: () => {},
|
||||
openModelOverlay: () => {},
|
||||
openApprovalOverlay: () => {},
|
||||
openHelpOverlay: () => {},
|
||||
onCompact: () => {},
|
||||
interruptAgent: () => {},
|
||||
active: true,
|
||||
thinkingSeconds: 0,
|
||||
};
|
||||
const { lastFrameStripped } = renderTui(<TerminalChatInput {...props} />);
|
||||
const frame = lastFrameStripped();
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
],
|
||||
"types": ["node"],
|
||||
"baseUrl": "./",
|
||||
"resolveJsonModule": false, // ESM doesn't yet support JSON modules.
|
||||
"resolveJsonModule": true, // ESM doesn't yet support JSON modules.
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"newLine": "lf",
|
||||
@@ -30,5 +30,5 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src", "tests"]
|
||||
"include": ["src", "tests", "bin"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"numTotalTestSuites":3,"numPassedTestSuites":0,"numFailedTestSuites":3,"numPendingTestSuites":0,"numTotalTests":0,"numPassedTests":0,"numFailedTests":0,"numPendingTests":0,"numTodoTests":0,"snapshot":{"added":0,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0,"didUpdate":false},"startTime":1745144446038,"success":false,"testResults":[{"assertionResults":[],"startTime":1745144446038,"endTime":1745144446038,"status":"failed","message":"Transform failed with 1 error:\n/Users/easong/code/codex-public/codex/codex-cli/src/components/chat/terminal-chat-input.tsx:283:6: ERROR: Expected \")\" but found \"if\"","name":"/Users/easong/code/codex-public/codex/codex-cli/tests/attachment-preview.test.tsx"},{"assertionResults":[],"startTime":1745144446038,"endTime":1745144446038,"status":"failed","message":"Transform failed with 1 error:\n/Users/easong/code/codex-public/codex/codex-cli/src/components/chat/terminal-chat-input.tsx:283:6: ERROR: Expected \")\" but found \"if\"","name":"/Users/easong/code/codex-public/codex/codex-cli/tests/backspace-delete-image.test.tsx"},{"assertionResults":[],"startTime":1745144446038,"endTime":1745144446038,"status":"failed","message":"Transform failed with 1 error:\n/Users/easong/code/codex-public/codex/codex-cli/src/components/chat/terminal-chat-input.tsx:283:6: ERROR: Expected \")\" but found \"if\"","name":"/Users/easong/code/codex-public/codex/codex-cli/tests/terminal-chat-input-compact.test.tsx"}]}
|
||||
Generated
-359
@@ -1,359 +0,0 @@
|
||||
{
|
||||
"name": "codex",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {
|
||||
"git-cliff": "^2.8.0",
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.3",
|
||||
"get-stream": "^8.0.1",
|
||||
"human-signals": "^5.0.0",
|
||||
"is-stream": "^3.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"npm-run-path": "^5.1.0",
|
||||
"onetime": "^6.0.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"strip-final-newline": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
|
||||
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/git-cliff": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/git-cliff/-/git-cliff-2.8.0.tgz",
|
||||
"integrity": "sha512-iKF5QTXAb9+iVvmu5HpnMPWYw7fs74xkpAaRbSf29+dZaMTTNRIUST/y+Ir2S1bDUWWJNjXlwT9ZT62JuYLQnA==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"execa": "^8.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"git-cliff": "lib/cli/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.19 || >=20.6 || >=21"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"git-cliff-darwin-arm64": "2.8.0",
|
||||
"git-cliff-darwin-x64": "2.8.0",
|
||||
"git-cliff-linux-arm64": "2.8.0",
|
||||
"git-cliff-linux-x64": "2.8.0",
|
||||
"git-cliff-windows-arm64": "2.8.0",
|
||||
"git-cliff-windows-x64": "2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/git-cliff-darwin-arm64": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/git-cliff-darwin-arm64/-/git-cliff-darwin-arm64-2.8.0.tgz",
|
||||
"integrity": "sha512-rurUV2d1Z2n+c2+wUrO0gZaFb3c1G+ej0bPfKTPfde/CblxiysMkh+4dz23NrVbc8IlS5rSYv/JFGVaVSBNJRw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/git-cliff-darwin-x64": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/git-cliff-darwin-x64/-/git-cliff-darwin-x64-2.8.0.tgz",
|
||||
"integrity": "sha512-Wtj+FGWZBWmeYUAGlkfz7QPz4+VVxxDPMhQ/7iwKVA3iryIX0slGfzYpqMurEFnTAMr0r+4IU3Q4O/ib7iUscg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/git-cliff-linux-arm64": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/git-cliff-linux-arm64/-/git-cliff-linux-arm64-2.8.0.tgz",
|
||||
"integrity": "sha512-k4RdfMdORXyefznWlQb+7wDgo7XgQF9qg8hJC34bwyJK2sODirrGau3uTx1/9Fi37g+pAOM7wM+LYppHCTZ2bQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/git-cliff-linux-x64": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/git-cliff-linux-x64/-/git-cliff-linux-x64-2.8.0.tgz",
|
||||
"integrity": "sha512-FcWX4GHgodYrQlZR03fzooanStgR03JNWvyaMQB1asplQ18nlziK2UyA+PESCIxOQmeLXauqoCApfzmdtp5myg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/git-cliff-windows-arm64": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/git-cliff-windows-arm64/-/git-cliff-windows-arm64-2.8.0.tgz",
|
||||
"integrity": "sha512-GJSrqmBVTbMtBJI3/YCDxLviZZDgYgnKqYgquBk2u2AELAnnuWFnVFQ7ZEBUqgFF2UJu9EdV2Nv6MV8d/wnP0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/git-cliff-windows-x64": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/git-cliff-windows-x64/-/git-cliff-windows-x64-2.8.0.tgz",
|
||||
"integrity": "sha512-8jl0YMXPYjUmVygUEeQ4wf1zte3Rv8LPq1sIklUKl80XE4g2Gm/8EIWbKpUPLQH6IncRwepY6VuMgpVpPXbwNw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
|
||||
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
|
||||
"integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||
"integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
|
||||
"integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path/node_modules/path-key": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
||||
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/onetime": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
|
||||
"integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-fn": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-final-newline": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
|
||||
"integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-3
@@ -1,13 +1,45 @@
|
||||
{
|
||||
"name": "codex-monorepo",
|
||||
"private": true,
|
||||
"description": "Tools for repo-wide maintenance.",
|
||||
"scripts": {
|
||||
"release": "cd codex-cli && npm run release",
|
||||
"release": "pnpm --filter @openai/codex run release",
|
||||
"format": "prettier --check *.json *.md .github/workflows/*.yml",
|
||||
"format:fix": "prettier --write *.json *.md .github/workflows/*.yml",
|
||||
"changelog": "git-cliff --config cliff.toml --output CHANGELOG.ignore.md $LAST_RELEASE_TAG..HEAD"
|
||||
"build": "pnpm --filter @openai/codex run build",
|
||||
"test": "pnpm --filter @openai/codex run test",
|
||||
"lint": "pnpm --filter @openai/codex run lint",
|
||||
"typecheck": "pnpm --filter @openai/codex run typecheck",
|
||||
"changelog": "git-cliff --config cliff.toml --output CHANGELOG.ignore.md $LAST_RELEASE_TAG..HEAD",
|
||||
"prepare": "husky",
|
||||
"husky:add": "husky add"
|
||||
},
|
||||
"devDependencies": {
|
||||
"git-cliff": "^2.8.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.5.1",
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"braces": "^3.0.3",
|
||||
"micromatch": "^4.0.8",
|
||||
"semver": "^7.7.1"
|
||||
},
|
||||
"overrides": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22",
|
||||
"pnpm": ">=9.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.json": "prettier --write",
|
||||
"*.md": "prettier --write",
|
||||
".github/workflows/*.yml": "prettier --write",
|
||||
"**/*.{js,ts,tsx}": [
|
||||
"cd codex-cli && pnpm run lint",
|
||||
"cd codex-cli && pnpm run typecheck"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.8.1"
|
||||
}
|
||||
|
||||
Generated
+4936
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
packages:
|
||||
- codex-cli
|
||||
- docs
|
||||
- packages/*
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- esbuild
|
||||
Reference in New Issue
Block a user